mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-04-12 15:08:36 +00:00
瞎改了一通,现在完全不能用了
This commit is contained in:
@@ -62,6 +62,11 @@ function normalizeText(value: unknown, fallback = ''): string {
|
||||
return text || fallback
|
||||
}
|
||||
|
||||
function parseOptionalInt(value: unknown): number | undefined {
|
||||
const n = Number(value)
|
||||
return Number.isFinite(n) ? Math.floor(n) : undefined
|
||||
}
|
||||
|
||||
function buildApiUrl(baseUrl: string, path: string): string {
|
||||
const base = baseUrl.replace(/\/+$/, '')
|
||||
const suffix = path.startsWith('/') ? path : `/${path}`
|
||||
@@ -382,9 +387,9 @@ class AiAgentService {
|
||||
const rawContent = normalizeText(res?.choices?.[0]?.message?.content)
|
||||
const sql = extractSqlText(rawContent)
|
||||
const usage: TokenUsage = {
|
||||
promptTokens: Number(res?.usage?.prompt_tokens || 0),
|
||||
completionTokens: Number(res?.usage?.completion_tokens || 0),
|
||||
totalTokens: Number(res?.usage?.total_tokens || 0)
|
||||
promptTokens: parseOptionalInt(res?.usage?.prompt_tokens),
|
||||
completionTokens: parseOptionalInt(res?.usage?.completion_tokens),
|
||||
totalTokens: parseOptionalInt(res?.usage?.total_tokens)
|
||||
}
|
||||
if (!sql) {
|
||||
runtime.onChunk({
|
||||
@@ -447,4 +452,3 @@ class AiAgentService {
|
||||
}
|
||||
|
||||
export const aiAgentService = new AiAgentService()
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,7 @@
|
||||
- 先范围后细节:优先定位会话/时间范围,再拉取具体时间轴或消息。
|
||||
- 可解释性:最终结论尽量附带来源范围与统计口径。
|
||||
- 语音消息不能臆测:必须先拿语音 ID,再点名转写,再总结。
|
||||
- 联系人排行题(“谁聊得最多/最常联系”)命中 ai_query_top_contacts 后,必须直接给出“前N名+消息数”。
|
||||
- 联系人排行题(“谁聊得最多/最常联系”)命中 get_member_stats 后,必须直接给出“前N名+消息数”。
|
||||
- 除非用户明确要求,联系人排行默认不包含群聊和公众号。
|
||||
- 用户提到“最近/近期/lately/recent”但未给时间窗时,默认按近30天口径统计并写明口径。
|
||||
- 用户提到联系人简称(如“lr”)时,先把它当联系人缩写处理,优先命中个人会话,不要默认落到群聊。
|
||||
@@ -18,8 +18,8 @@
|
||||
|
||||
Agent执行要求:
|
||||
- 用户输入直接进入推理,本地不做关键词分流,你自主决定工具计划。
|
||||
- 当用户说“今天凌晨/昨晚/某段时间的聊天”,优先调用 ai_query_time_window_activity。
|
||||
- 拿到活跃会话后,调用 ai_query_session_glimpse 对多个会话逐个抽样阅读,不要只读一个会话就停止。
|
||||
- 当用户说“今天凌晨/昨晚/某段时间的聊天”,优先调用 get_time_stats。
|
||||
- 拿到活跃会话后,调用 get_recent_messages 对多个会话逐个抽样阅读,不要只读一个会话就停止。
|
||||
- 如果初步探索后用户目标仍模糊,主动提出 1 个关键澄清问题继续多轮对话。
|
||||
- 仅当你确认任务完成时,输出结束标记 `[[WF_DONE]]`,并紧跟 `<final_answer>...</final_answer>`。
|
||||
- 若还未完成,不要输出结束标记,继续调用工具。
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
工具:ai_fetch_message_briefs
|
||||
工具:get_message_context
|
||||
|
||||
何时用:
|
||||
- 需要核对少量关键消息原文,避免全量展开。
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
工具:ai_query_session_candidates
|
||||
工具:search_sessions
|
||||
|
||||
何时用:
|
||||
- 用户未明确具体会话,但给了关键词/关系词(如“老婆”“买车”)。
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
工具:ai_query_session_glimpse
|
||||
工具:get_recent_messages
|
||||
|
||||
何时用:
|
||||
- 已确定候选会话,需要“先看一点”理解上下文。
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
工具:ai_query_source_refs
|
||||
工具:get_session_summaries
|
||||
|
||||
何时用:
|
||||
- 输出总结或分析后,用于来源说明与可解释卡片。
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
工具:ai_query_time_window_activity
|
||||
工具:get_time_stats
|
||||
|
||||
何时用:
|
||||
- 用户提到“今天凌晨/昨晚/某个时间段”的聊天分析。
|
||||
|
||||
Agent策略:
|
||||
- 第一步必须先扫时间窗活跃会话,不要直接下结论。
|
||||
- 拿到活跃会话后,再调用 ai_query_session_glimpse 逐个会话抽样阅读。
|
||||
- 拿到活跃会话后,再调用 get_recent_messages 逐个会话抽样阅读。
|
||||
- 若用户目标仍不清晰,先追问 1 个关键澄清问题再继续。
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
工具:ai_query_timeline
|
||||
工具:search_messages
|
||||
|
||||
何时用:
|
||||
- 回忆事件经过、梳理时间线、提取关键节点。
|
||||
@@ -6,4 +6,4 @@
|
||||
调用建议:
|
||||
- 默认 detailLevel=minimal。
|
||||
- 先小批次 limit(40~120),不够再分页 offset。
|
||||
- 需要引用原文证据时,可搭配 ai_fetch_message_briefs。
|
||||
- 需要引用原文证据时,可搭配 get_message_context。
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
工具:ai_query_top_contacts
|
||||
工具:get_member_stats
|
||||
|
||||
何时用:
|
||||
- 用户问“谁联系最密切”“谁聊得最多”“最常联系的是谁”。
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
工具:ai_query_topic_stats
|
||||
工具:get_chat_overview
|
||||
|
||||
何时用:
|
||||
- 用户问“多少、占比、趋势、对比”。
|
||||
|
||||
@@ -40,16 +40,16 @@ presetQuestions:
|
||||
- 帮我总结一下最近一周的重要聊天
|
||||
- 帮我找一下关于“旅游”的讨论
|
||||
allowedBuiltinTools:
|
||||
- ai_query_time_window_activity
|
||||
- ai_query_session_candidates
|
||||
- ai_query_session_glimpse
|
||||
- ai_query_timeline
|
||||
- ai_fetch_message_briefs
|
||||
- get_time_stats
|
||||
- search_sessions
|
||||
- get_recent_messages
|
||||
- search_messages
|
||||
- get_message_context
|
||||
- ai_list_voice_messages
|
||||
- ai_transcribe_voice_messages
|
||||
- ai_query_topic_stats
|
||||
- ai_query_source_refs
|
||||
- ai_query_top_contacts
|
||||
- get_chat_overview
|
||||
- get_session_summaries
|
||||
- get_member_stats
|
||||
---
|
||||
|
||||
你是 WeFlow 的全局聊天分析助手。请使用工具获取证据,给出简洁、准确、可执行的结论。
|
||||
@@ -70,16 +70,16 @@ presetQuestions:
|
||||
- Who are the most active contacts?
|
||||
- Summarize my key chat topics this week
|
||||
allowedBuiltinTools:
|
||||
- ai_query_time_window_activity
|
||||
- ai_query_session_candidates
|
||||
- ai_query_session_glimpse
|
||||
- ai_query_timeline
|
||||
- ai_fetch_message_briefs
|
||||
- get_time_stats
|
||||
- search_sessions
|
||||
- get_recent_messages
|
||||
- search_messages
|
||||
- get_message_context
|
||||
- ai_list_voice_messages
|
||||
- ai_transcribe_voice_messages
|
||||
- ai_query_topic_stats
|
||||
- ai_query_source_refs
|
||||
- ai_query_top_contacts
|
||||
- get_chat_overview
|
||||
- get_session_summaries
|
||||
- get_member_stats
|
||||
---
|
||||
|
||||
You are WeFlow's global chat analysis assistant.
|
||||
@@ -95,16 +95,16 @@ presetQuestions:
|
||||
- 一番アクティブな相手は誰?
|
||||
- 今週の重要な会話を要約して
|
||||
allowedBuiltinTools:
|
||||
- ai_query_time_window_activity
|
||||
- ai_query_session_candidates
|
||||
- ai_query_session_glimpse
|
||||
- ai_query_timeline
|
||||
- ai_fetch_message_briefs
|
||||
- get_time_stats
|
||||
- search_sessions
|
||||
- get_recent_messages
|
||||
- search_messages
|
||||
- get_message_context
|
||||
- ai_list_voice_messages
|
||||
- ai_transcribe_voice_messages
|
||||
- ai_query_topic_stats
|
||||
- ai_query_source_refs
|
||||
- ai_query_top_contacts
|
||||
- get_chat_overview
|
||||
- get_session_summaries
|
||||
- get_member_stats
|
||||
---
|
||||
|
||||
あなたは WeFlow のグローバルチャット分析アシスタントです。
|
||||
@@ -231,16 +231,16 @@ function toMarkdown(config: AssistantConfigFull): string {
|
||||
|
||||
function defaultBuiltinToolCatalog(): Array<{ name: string; category: AssistantToolCategory }> {
|
||||
return [
|
||||
{ name: 'ai_query_time_window_activity', category: 'core' },
|
||||
{ name: 'ai_query_session_candidates', category: 'core' },
|
||||
{ name: 'ai_query_session_glimpse', category: 'core' },
|
||||
{ name: 'ai_query_timeline', category: 'core' },
|
||||
{ name: 'ai_fetch_message_briefs', category: 'core' },
|
||||
{ name: 'get_time_stats', category: 'core' },
|
||||
{ name: 'search_sessions', category: 'core' },
|
||||
{ name: 'get_recent_messages', category: 'core' },
|
||||
{ name: 'search_messages', category: 'core' },
|
||||
{ name: 'get_message_context', category: 'core' },
|
||||
{ name: 'ai_list_voice_messages', category: 'core' },
|
||||
{ name: 'ai_transcribe_voice_messages', category: 'core' },
|
||||
{ name: 'ai_query_topic_stats', category: 'analysis' },
|
||||
{ name: 'ai_query_source_refs', category: 'analysis' },
|
||||
{ name: 'ai_query_top_contacts', category: 'analysis' },
|
||||
{ name: 'get_chat_overview', category: 'analysis' },
|
||||
{ name: 'get_session_summaries', category: 'analysis' },
|
||||
{ name: 'get_member_stats', category: 'analysis' },
|
||||
{ name: 'activate_skill', category: 'analysis' }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -32,18 +32,18 @@ tags:
|
||||
- evidence
|
||||
chatScope: all
|
||||
tools:
|
||||
- ai_query_time_window_activity
|
||||
- ai_query_session_candidates
|
||||
- ai_query_session_glimpse
|
||||
- ai_query_timeline
|
||||
- ai_fetch_message_briefs
|
||||
- ai_query_source_refs
|
||||
- get_time_stats
|
||||
- search_sessions
|
||||
- get_recent_messages
|
||||
- search_messages
|
||||
- get_message_context
|
||||
- get_session_summaries
|
||||
---
|
||||
你是“深度时间线追踪”技能。
|
||||
执行步骤:
|
||||
1. 先按时间窗扫描活跃会话,必要时补关键词筛选候选会话。
|
||||
2. 对候选会话先抽样,再拉取时间轴。
|
||||
3. 对关键节点用 ai_fetch_message_briefs 校对原文。
|
||||
3. 对关键节点用 get_message_context 校对原文。
|
||||
4. 最后输出“结论 + 关键节点 + 来源范围”。`
|
||||
|
||||
const SKILL_CONTACT_FOCUS_MD = `---
|
||||
@@ -55,17 +55,17 @@ tags:
|
||||
- relation
|
||||
chatScope: private
|
||||
tools:
|
||||
- ai_query_top_contacts
|
||||
- ai_query_topic_stats
|
||||
- ai_query_session_glimpse
|
||||
- ai_query_timeline
|
||||
- ai_query_source_refs
|
||||
- get_member_stats
|
||||
- get_chat_overview
|
||||
- get_recent_messages
|
||||
- search_messages
|
||||
- get_session_summaries
|
||||
---
|
||||
你是“联系人关系聚焦”技能。
|
||||
执行步骤:
|
||||
1. 优先调用 ai_query_top_contacts 得到候选联系人排名。
|
||||
1. 优先调用 get_member_stats 得到候选联系人排名。
|
||||
2. 针对 Top 联系人读取抽样消息并补充时间轴。
|
||||
3. 如果用户问题涉及“变化趋势”,补 ai_query_topic_stats。
|
||||
3. 如果用户问题涉及“变化趋势”,补 get_chat_overview。
|
||||
4. 输出时必须给出对比口径(时间窗、样本范围、消息数量)。`
|
||||
|
||||
const SKILL_VOICE_AUDIT_MD = `---
|
||||
@@ -79,7 +79,7 @@ chatScope: all
|
||||
tools:
|
||||
- ai_list_voice_messages
|
||||
- ai_transcribe_voice_messages
|
||||
- ai_query_source_refs
|
||||
- get_session_summaries
|
||||
---
|
||||
你是“语音证据审计”技能。
|
||||
硬规则:
|
||||
|
||||
@@ -174,6 +174,36 @@ interface GetContactsOptions {
|
||||
lite?: boolean
|
||||
}
|
||||
|
||||
interface AiTimeFilter {
|
||||
startTs?: number
|
||||
endTs?: number
|
||||
}
|
||||
|
||||
interface AiMessageResult {
|
||||
id: number
|
||||
localId: number
|
||||
sessionId: string
|
||||
senderName: string
|
||||
senderPlatformId: string
|
||||
senderUsername: string
|
||||
content: string
|
||||
timestamp: number
|
||||
type: number
|
||||
isSend: number | null
|
||||
replyToMessageId: string | null
|
||||
replyToContent: string | null
|
||||
replyToSenderName: string | null
|
||||
}
|
||||
|
||||
interface AiSessionSearchResult {
|
||||
id: string
|
||||
startTs: number
|
||||
endTs: number
|
||||
messageCount: number
|
||||
isComplete: boolean
|
||||
previewMessages: AiMessageResult[]
|
||||
}
|
||||
|
||||
interface ExportSessionStats {
|
||||
totalMessages: number
|
||||
voiceMessages: number
|
||||
@@ -8474,6 +8504,451 @@ class ChatService {
|
||||
}
|
||||
}
|
||||
|
||||
private normalizeAiFilter(filter?: AiTimeFilter): { begin: number; end: number } {
|
||||
const begin = this.normalizeTimestampSeconds(Number(filter?.startTs || 0))
|
||||
const end = this.normalizeTimestampSeconds(Number(filter?.endTs || 0))
|
||||
return { begin, end }
|
||||
}
|
||||
|
||||
private hashSenderId(senderUsername: string): number {
|
||||
const text = String(senderUsername || '').trim().toLowerCase()
|
||||
if (!text) return 0
|
||||
let hash = 5381
|
||||
for (let i = 0; i < text.length; i += 1) {
|
||||
hash = ((hash << 5) + hash + text.charCodeAt(i)) | 0
|
||||
}
|
||||
return Math.abs(hash)
|
||||
}
|
||||
|
||||
private messageMatchesKeywords(message: Message, keywords?: string[]): boolean {
|
||||
if (!Array.isArray(keywords) || keywords.length === 0) return true
|
||||
const text = String(message.parsedContent || message.rawContent || '').toLowerCase()
|
||||
if (!text) return false
|
||||
return keywords.every((keyword) => {
|
||||
const token = String(keyword || '').trim().toLowerCase()
|
||||
if (!token) return true
|
||||
return text.includes(token)
|
||||
})
|
||||
}
|
||||
|
||||
private toAiMessage(sessionId: string, message: Message): AiMessageResult {
|
||||
const senderUsername = String(message.senderUsername || '').trim()
|
||||
const senderName = senderUsername || (message.isSend === 1 ? '我' : '未知成员')
|
||||
const content = String(message.parsedContent || message.rawContent || '').trim()
|
||||
return {
|
||||
id: message.localId,
|
||||
localId: message.localId,
|
||||
sessionId,
|
||||
senderName,
|
||||
senderPlatformId: senderUsername,
|
||||
senderUsername,
|
||||
content,
|
||||
timestamp: Number(message.createTime || 0),
|
||||
type: Number(message.localType || 0),
|
||||
isSend: message.isSend,
|
||||
replyToMessageId: message.messageKey || null,
|
||||
replyToContent: message.quotedContent || null,
|
||||
replyToSenderName: message.quotedSender || null
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchMessagesByCursorWithKey(
|
||||
sessionId: string,
|
||||
key: { sortSeq?: number; createTime?: number; localId?: number },
|
||||
limit: number,
|
||||
ascending: boolean,
|
||||
beginTimestamp = 0,
|
||||
endTimestamp = 0
|
||||
): Promise<{ success: boolean; messages?: Message[]; hasMore?: boolean; error?: string }> {
|
||||
const batchSize = Math.max(limit + 8, Math.min(240, limit * 2))
|
||||
const cursorResult = await wcdbService.openMessageCursorWithKey(
|
||||
sessionId,
|
||||
batchSize,
|
||||
ascending,
|
||||
beginTimestamp,
|
||||
endTimestamp,
|
||||
key
|
||||
)
|
||||
if (!cursorResult.success || !cursorResult.cursor) {
|
||||
return { success: false, error: cursorResult.error || '创建游标失败' }
|
||||
}
|
||||
|
||||
try {
|
||||
const collected = await this.collectVisibleMessagesFromCursor(sessionId, cursorResult.cursor, limit)
|
||||
if (!collected.success) {
|
||||
return { success: false, error: collected.error || '读取消息失败' }
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
messages: collected.messages || [],
|
||||
hasMore: collected.hasMore === true
|
||||
}
|
||||
} finally {
|
||||
await wcdbService.closeMessageCursor(cursorResult.cursor).catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
async getRecentMessagesForAI(
|
||||
sessionId: string,
|
||||
filter?: AiTimeFilter,
|
||||
limit = 100
|
||||
): Promise<{ messages: AiMessageResult[]; total: number }> {
|
||||
const normalizedLimit = Math.max(1, Math.min(500, Number(limit || 100)))
|
||||
const { begin, end } = this.normalizeAiFilter(filter)
|
||||
const result = await this.getLatestMessages(sessionId, normalizedLimit)
|
||||
if (!result.success || !Array.isArray(result.messages)) {
|
||||
return { messages: [], total: 0 }
|
||||
}
|
||||
const bounded = result.messages.filter((message) => {
|
||||
if (begin > 0 && Number(message.createTime || 0) < begin) return false
|
||||
if (end > 0 && Number(message.createTime || 0) > end) return false
|
||||
return String(message.parsedContent || message.rawContent || '').trim().length > 0
|
||||
})
|
||||
return {
|
||||
messages: bounded.slice(-normalizedLimit).map((message) => this.toAiMessage(sessionId, message)),
|
||||
total: bounded.length
|
||||
}
|
||||
}
|
||||
|
||||
async getMessagesBeforeForAI(
|
||||
sessionId: string,
|
||||
beforeId: number,
|
||||
limit = 50,
|
||||
filter?: AiTimeFilter,
|
||||
senderId?: number,
|
||||
keywords?: string[]
|
||||
): Promise<{ messages: AiMessageResult[]; hasMore: boolean }> {
|
||||
const base = await this.getMessageById(sessionId, Number(beforeId))
|
||||
if (!base.success || !base.message) {
|
||||
return { messages: [], hasMore: false }
|
||||
}
|
||||
const normalizedLimit = Math.max(1, Math.min(300, Number(limit || 50)))
|
||||
const { begin, end } = this.normalizeAiFilter(filter)
|
||||
const cursor = await this.fetchMessagesByCursorWithKey(
|
||||
sessionId,
|
||||
{
|
||||
sortSeq: base.message.sortSeq,
|
||||
createTime: base.message.createTime,
|
||||
localId: base.message.localId
|
||||
},
|
||||
Math.max(normalizedLimit * 2, normalizedLimit + 12),
|
||||
false,
|
||||
begin,
|
||||
end
|
||||
)
|
||||
if (!cursor.success) {
|
||||
return { messages: [], hasMore: false }
|
||||
}
|
||||
const filtered = (cursor.messages || []).filter((message) => {
|
||||
if (senderId && senderId > 0) {
|
||||
const hashed = this.hashSenderId(String(message.senderUsername || ''))
|
||||
if (hashed !== senderId) return false
|
||||
}
|
||||
return this.messageMatchesKeywords(message, keywords)
|
||||
})
|
||||
const sliced = filtered.slice(-normalizedLimit)
|
||||
return {
|
||||
messages: sliced.map((message) => this.toAiMessage(sessionId, message)),
|
||||
hasMore: cursor.hasMore === true || filtered.length > normalizedLimit
|
||||
}
|
||||
}
|
||||
|
||||
async getMessagesAfterForAI(
|
||||
sessionId: string,
|
||||
afterId: number,
|
||||
limit = 50,
|
||||
filter?: AiTimeFilter,
|
||||
senderId?: number,
|
||||
keywords?: string[]
|
||||
): Promise<{ messages: AiMessageResult[]; hasMore: boolean }> {
|
||||
const base = await this.getMessageById(sessionId, Number(afterId))
|
||||
if (!base.success || !base.message) {
|
||||
return { messages: [], hasMore: false }
|
||||
}
|
||||
const normalizedLimit = Math.max(1, Math.min(300, Number(limit || 50)))
|
||||
const { begin, end } = this.normalizeAiFilter(filter)
|
||||
const cursor = await this.fetchMessagesByCursorWithKey(
|
||||
sessionId,
|
||||
{
|
||||
sortSeq: base.message.sortSeq,
|
||||
createTime: base.message.createTime,
|
||||
localId: base.message.localId
|
||||
},
|
||||
Math.max(normalizedLimit * 2, normalizedLimit + 12),
|
||||
true,
|
||||
begin,
|
||||
end
|
||||
)
|
||||
if (!cursor.success) {
|
||||
return { messages: [], hasMore: false }
|
||||
}
|
||||
const filtered = (cursor.messages || []).filter((message) => {
|
||||
if (senderId && senderId > 0) {
|
||||
const hashed = this.hashSenderId(String(message.senderUsername || ''))
|
||||
if (hashed !== senderId) return false
|
||||
}
|
||||
return this.messageMatchesKeywords(message, keywords)
|
||||
})
|
||||
const sliced = filtered.slice(0, normalizedLimit)
|
||||
return {
|
||||
messages: sliced.map((message) => this.toAiMessage(sessionId, message)),
|
||||
hasMore: cursor.hasMore === true || filtered.length > normalizedLimit
|
||||
}
|
||||
}
|
||||
|
||||
async getMessageContextForAI(
|
||||
sessionId: string,
|
||||
messageIds: number | number[],
|
||||
contextSize = 20
|
||||
): Promise<AiMessageResult[]> {
|
||||
const ids = Array.isArray(messageIds) ? messageIds : [messageIds]
|
||||
const uniqueIds = Array.from(new Set(ids.map((id) => Number(id)).filter((id) => Number.isFinite(id) && id > 0)))
|
||||
if (uniqueIds.length === 0) return []
|
||||
const size = Math.max(0, Math.min(120, Number(contextSize || 20)))
|
||||
const merged = new Map<number, AiMessageResult>()
|
||||
|
||||
for (const id of uniqueIds) {
|
||||
const target = await this.getMessageById(sessionId, id)
|
||||
if (target.success && target.message) {
|
||||
merged.set(id, this.toAiMessage(sessionId, target.message))
|
||||
}
|
||||
if (size <= 0) continue
|
||||
const [before, after] = await Promise.all([
|
||||
this.getMessagesBeforeForAI(sessionId, id, size),
|
||||
this.getMessagesAfterForAI(sessionId, id, size)
|
||||
])
|
||||
for (const item of before.messages) merged.set(item.id, item)
|
||||
for (const item of after.messages) merged.set(item.id, item)
|
||||
}
|
||||
|
||||
return Array.from(merged.values()).sort((a, b) => {
|
||||
if (a.timestamp !== b.timestamp) return a.timestamp - b.timestamp
|
||||
return a.id - b.id
|
||||
})
|
||||
}
|
||||
|
||||
async getSearchMessageContextForAI(
|
||||
sessionId: string,
|
||||
messageIds: number[],
|
||||
contextBefore = 2,
|
||||
contextAfter = 2
|
||||
): Promise<AiMessageResult[]> {
|
||||
const uniqueIds = Array.from(new Set((messageIds || []).map((id) => Number(id)).filter((id) => Number.isFinite(id) && id > 0)))
|
||||
if (uniqueIds.length === 0) return []
|
||||
const beforeLimit = Math.max(0, Math.min(30, Number(contextBefore || 2)))
|
||||
const afterLimit = Math.max(0, Math.min(30, Number(contextAfter || 2)))
|
||||
const merged = new Map<number, AiMessageResult>()
|
||||
|
||||
for (const id of uniqueIds) {
|
||||
const target = await this.getMessageById(sessionId, id)
|
||||
if (target.success && target.message) {
|
||||
merged.set(id, this.toAiMessage(sessionId, target.message))
|
||||
}
|
||||
const [before, after] = await Promise.all([
|
||||
beforeLimit > 0 ? this.getMessagesBeforeForAI(sessionId, id, beforeLimit) : Promise.resolve({ messages: [], hasMore: false }),
|
||||
afterLimit > 0 ? this.getMessagesAfterForAI(sessionId, id, afterLimit) : Promise.resolve({ messages: [], hasMore: false })
|
||||
])
|
||||
for (const item of before.messages) merged.set(item.id, item)
|
||||
for (const item of after.messages) merged.set(item.id, item)
|
||||
}
|
||||
|
||||
return Array.from(merged.values()).sort((a, b) => {
|
||||
if (a.timestamp !== b.timestamp) return a.timestamp - b.timestamp
|
||||
return a.id - b.id
|
||||
})
|
||||
}
|
||||
|
||||
async getConversationBetweenForAI(
|
||||
sessionId: string,
|
||||
memberId1: number,
|
||||
memberId2: number,
|
||||
filter?: AiTimeFilter,
|
||||
limit = 100
|
||||
): Promise<{ messages: AiMessageResult[]; total: number; member1Name: string; member2Name: string }> {
|
||||
const normalizedLimit = Math.max(1, Math.min(500, Number(limit || 100)))
|
||||
const { begin, end } = this.normalizeAiFilter(filter)
|
||||
const sample = await this.getMessages(sessionId, 0, Math.max(600, normalizedLimit * 8), begin, end, false)
|
||||
if (!sample.success || !Array.isArray(sample.messages) || sample.messages.length === 0) {
|
||||
return { messages: [], total: 0, member1Name: '', member2Name: '' }
|
||||
}
|
||||
|
||||
const idSet = new Set<number>([Number(memberId1), Number(memberId2)].filter((id) => Number.isFinite(id) && id > 0))
|
||||
const filtered = sample.messages.filter((message) => {
|
||||
const senderId = this.hashSenderId(String(message.senderUsername || ''))
|
||||
return idSet.has(senderId) && String(message.parsedContent || message.rawContent || '').trim().length > 0
|
||||
})
|
||||
const picked = filtered.slice(-normalizedLimit)
|
||||
const names = Array.from(new Set(picked.map((message) => String(message.senderUsername || '').trim()).filter(Boolean)))
|
||||
return {
|
||||
messages: picked.map((message) => this.toAiMessage(sessionId, message)),
|
||||
total: filtered.length,
|
||||
member1Name: names[0] || '',
|
||||
member2Name: names[1] || names[0] || ''
|
||||
}
|
||||
}
|
||||
|
||||
async searchSessionsForAI(
|
||||
_sessionId: string,
|
||||
keywords?: string[],
|
||||
timeFilter?: AiTimeFilter,
|
||||
limit = 20,
|
||||
previewCount = 5
|
||||
): Promise<AiSessionSearchResult[]> {
|
||||
const normalizedLimit = Math.max(1, Math.min(60, Number(limit || 20)))
|
||||
const normalizedPreview = Math.max(1, Math.min(20, Number(previewCount || 5)))
|
||||
const { begin, end } = this.normalizeAiFilter(timeFilter)
|
||||
const tokenList = Array.from(new Set((keywords || []).map((keyword) => String(keyword || '').trim()).filter(Boolean)))
|
||||
|
||||
const sessionsResult = await this.getSessions()
|
||||
if (!sessionsResult.success || !Array.isArray(sessionsResult.sessions)) return []
|
||||
const sessionMap = new Map<string, ChatSession>()
|
||||
for (const session of sessionsResult.sessions) {
|
||||
const sid = String(session.username || '').trim()
|
||||
if (!sid) continue
|
||||
sessionMap.set(sid, session)
|
||||
}
|
||||
|
||||
const rows: Array<{ sessionId: string; hitCount: number }> = []
|
||||
if (tokenList.length > 0) {
|
||||
const native = await wcdbService.aiQuerySessionCandidates({
|
||||
keyword: tokenList.join(' '),
|
||||
limit: normalizedLimit * 4,
|
||||
beginTimestamp: begin,
|
||||
endTimestamp: end
|
||||
})
|
||||
if (native.success && Array.isArray(native.rows)) {
|
||||
for (const row of native.rows as Record<string, any>[]) {
|
||||
const sid = String(row.session_id || row._session_id || row.sessionId || '').trim()
|
||||
if (!sid) continue
|
||||
rows.push({
|
||||
sessionId: sid,
|
||||
hitCount: this.toSafeInt(row.hit_count ?? row.count ?? row.message_count, 0)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const candidateIds = rows.length > 0
|
||||
? Array.from(new Set(rows.map((item) => item.sessionId)))
|
||||
: sessionsResult.sessions
|
||||
.filter((session) => {
|
||||
if (begin > 0 && Number(session.lastTimestamp || session.sortTimestamp || 0) < begin) return false
|
||||
if (end > 0 && Number(session.lastTimestamp || session.sortTimestamp || 0) > end) return false
|
||||
return true
|
||||
})
|
||||
.slice(0, normalizedLimit * 2)
|
||||
.map((session) => String(session.username || '').trim())
|
||||
.filter(Boolean)
|
||||
|
||||
const output: AiSessionSearchResult[] = []
|
||||
for (const sid of candidateIds.slice(0, normalizedLimit)) {
|
||||
const latest = await this.getLatestMessages(sid, normalizedPreview)
|
||||
const messages = Array.isArray(latest.messages) ? latest.messages : []
|
||||
const mapped = messages.map((message) => this.toAiMessage(sid, message)).slice(-normalizedPreview)
|
||||
const hitRow = rows.find((item) => item.sessionId === sid)
|
||||
const session = sessionMap.get(sid)
|
||||
const tsList = mapped.map((item) => item.timestamp).filter((value) => Number.isFinite(value) && value > 0)
|
||||
const startTs = tsList.length > 0 ? Math.min(...tsList) : 0
|
||||
const endTs = tsList.length > 0 ? Math.max(...tsList) : Number(session?.lastTimestamp || session?.sortTimestamp || 0)
|
||||
output.push({
|
||||
id: sid,
|
||||
startTs,
|
||||
endTs,
|
||||
messageCount: hitRow?.hitCount || mapped.length,
|
||||
isComplete: mapped.length <= normalizedPreview,
|
||||
previewMessages: mapped
|
||||
})
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
async getSessionMessagesForAI(
|
||||
_sessionId: string,
|
||||
chatSessionId: string | number,
|
||||
limit = 500
|
||||
): Promise<{
|
||||
sessionId: string
|
||||
startTs: number
|
||||
endTs: number
|
||||
messageCount: number
|
||||
returnedCount: number
|
||||
participants: string[]
|
||||
messages: AiMessageResult[]
|
||||
} | null> {
|
||||
const sid = String(chatSessionId || '').trim()
|
||||
if (!sid) return null
|
||||
const normalizedLimit = Math.max(1, Math.min(1000, Number(limit || 500)))
|
||||
const latest = await this.getLatestMessages(sid, normalizedLimit)
|
||||
if (!latest.success || !Array.isArray(latest.messages)) return null
|
||||
const mapped = latest.messages.map((message) => this.toAiMessage(sid, message))
|
||||
const tsList = mapped.map((item) => item.timestamp).filter((value) => Number.isFinite(value) && value > 0)
|
||||
const count = await this.getMessageCount(sid)
|
||||
return {
|
||||
sessionId: sid,
|
||||
startTs: tsList.length > 0 ? Math.min(...tsList) : 0,
|
||||
endTs: tsList.length > 0 ? Math.max(...tsList) : 0,
|
||||
messageCount: count.success ? Number(count.count || mapped.length) : mapped.length,
|
||||
returnedCount: mapped.length,
|
||||
participants: Array.from(new Set(mapped.map((item) => item.senderName).filter(Boolean))),
|
||||
messages: mapped
|
||||
}
|
||||
}
|
||||
|
||||
async getSessionSummariesForAI(
|
||||
_sessionId: string,
|
||||
options?: {
|
||||
sessionIds?: string[]
|
||||
limit?: number
|
||||
previewCount?: number
|
||||
}
|
||||
): Promise<Array<{
|
||||
sessionId: string
|
||||
sessionName: string
|
||||
messageCount: number
|
||||
latestTs: number
|
||||
previewMessages: AiMessageResult[]
|
||||
}>> {
|
||||
const normalizedLimit = Math.max(1, Math.min(60, Number(options?.limit || 20)))
|
||||
const previewCount = Math.max(1, Math.min(20, Number(options?.previewCount || 3)))
|
||||
const sessionsResult = await this.getSessions()
|
||||
if (!sessionsResult.success || !Array.isArray(sessionsResult.sessions)) return []
|
||||
const explicitIds = Array.isArray(options?.sessionIds)
|
||||
? options?.sessionIds.map((value) => String(value || '').trim()).filter(Boolean)
|
||||
: []
|
||||
const candidates = explicitIds.length > 0
|
||||
? sessionsResult.sessions.filter((session) => explicitIds.includes(String(session.username || '').trim()))
|
||||
: sessionsResult.sessions.slice(0, normalizedLimit)
|
||||
|
||||
const summaries: Array<{
|
||||
sessionId: string
|
||||
sessionName: string
|
||||
messageCount: number
|
||||
latestTs: number
|
||||
previewMessages: AiMessageResult[]
|
||||
}> = []
|
||||
|
||||
for (const session of candidates.slice(0, normalizedLimit)) {
|
||||
const sid = String(session.username || '').trim()
|
||||
if (!sid) continue
|
||||
const [countResult, latestResult] = await Promise.all([
|
||||
this.getMessageCount(sid),
|
||||
this.getLatestMessages(sid, previewCount)
|
||||
])
|
||||
const previewMessages = Array.isArray(latestResult.messages)
|
||||
? latestResult.messages.map((message) => this.toAiMessage(sid, message)).slice(-previewCount)
|
||||
: []
|
||||
summaries.push({
|
||||
sessionId: sid,
|
||||
sessionName: String(session.displayName || sid),
|
||||
messageCount: countResult.success ? Number(countResult.count || previewMessages.length) : previewMessages.length,
|
||||
latestTs: Number(session.lastTimestamp || session.sortTimestamp || 0),
|
||||
previewMessages
|
||||
})
|
||||
}
|
||||
return summaries
|
||||
}
|
||||
|
||||
async getMessageById(sessionId: string, localId: number): Promise<{ success: boolean; message?: Message; error?: string }> {
|
||||
try {
|
||||
const nativeResult = await wcdbService.getMessageById(sessionId, localId)
|
||||
|
||||
@@ -62,6 +62,8 @@ export class WcdbCore {
|
||||
private wcdbGetMessageDates: any = null
|
||||
private wcdbOpenMessageCursor: any = null
|
||||
private wcdbOpenMessageCursorLite: any = null
|
||||
private wcdbOpenMessageCursorWithKey: any = null
|
||||
private wcdbOpenMessageCursorLiteWithKey: any = null
|
||||
private wcdbFetchMessageBatch: any = null
|
||||
private wcdbCloseMessageCursor: any = null
|
||||
private wcdbGetLogs: any = null
|
||||
@@ -89,6 +91,15 @@ export class WcdbCore {
|
||||
private wcdbAiQueryTimeline: any = null
|
||||
private wcdbAiQueryTopicStats: any = null
|
||||
private wcdbAiQuerySourceRefs: any = null
|
||||
private wcdbAiGetRecentMessages: any = null
|
||||
private wcdbAiGetMessagesBefore: any = null
|
||||
private wcdbAiGetMessagesAfter: any = null
|
||||
private wcdbAiGetMessageContext: any = null
|
||||
private wcdbAiGetSearchMessageContext: any = null
|
||||
private wcdbAiGetConversationBetween: any = null
|
||||
private wcdbAiSearchSessions: any = null
|
||||
private wcdbAiGetSessionMessages: any = null
|
||||
private wcdbAiGetSessionSummaries: any = null
|
||||
private wcdbGetSnsTimeline: any = null
|
||||
private wcdbGetSnsAnnualStats: any = null
|
||||
private wcdbGetSnsUsernames: any = null
|
||||
@@ -947,6 +958,15 @@ export class WcdbCore {
|
||||
// wcdb_status wcdb_open_message_cursor(wcdb_handle handle, const char* session_id, int32_t batch_size, int32_t ascending, int32_t begin_timestamp, int32_t end_timestamp, wcdb_cursor* out_cursor)
|
||||
this.wcdbOpenMessageCursor = this.lib.func('int32 wcdb_open_message_cursor(int64 handle, const char* sessionId, int32 batchSize, int32 ascending, int32 beginTimestamp, int32 endTimestamp, _Out_ int64* outCursor)')
|
||||
|
||||
// wcdb_status wcdb_open_message_cursor_with_key(...)
|
||||
try {
|
||||
this.wcdbOpenMessageCursorWithKey = this.lib.func(
|
||||
'int32 wcdb_open_message_cursor_with_key(int64 handle, const char* sessionId, int32 batchSize, int32 ascending, int32 beginTimestamp, int32 endTimestamp, int32 keyValid, int64 keySortSeq, int64 keyCreateTime, int64 keyLocalId, _Out_ int64* outCursor)'
|
||||
)
|
||||
} catch {
|
||||
this.wcdbOpenMessageCursorWithKey = null
|
||||
}
|
||||
|
||||
// wcdb_status wcdb_open_message_cursor_lite(wcdb_handle handle, const char* session_id, int32_t batch_size, int32_t ascending, int32_t begin_timestamp, int32_t end_timestamp, wcdb_cursor* out_cursor)
|
||||
try {
|
||||
this.wcdbOpenMessageCursorLite = this.lib.func('int32 wcdb_open_message_cursor_lite(int64 handle, const char* sessionId, int32 batchSize, int32 ascending, int32 beginTimestamp, int32 endTimestamp, _Out_ int64* outCursor)')
|
||||
@@ -954,6 +974,15 @@ export class WcdbCore {
|
||||
this.wcdbOpenMessageCursorLite = null
|
||||
}
|
||||
|
||||
// wcdb_status wcdb_open_message_cursor_lite_with_key(...)
|
||||
try {
|
||||
this.wcdbOpenMessageCursorLiteWithKey = this.lib.func(
|
||||
'int32 wcdb_open_message_cursor_lite_with_key(int64 handle, const char* sessionId, int32 batchSize, int32 ascending, int32 beginTimestamp, int32 endTimestamp, int32 keyValid, int64 keySortSeq, int64 keyCreateTime, int64 keyLocalId, _Out_ int64* outCursor)'
|
||||
)
|
||||
} catch {
|
||||
this.wcdbOpenMessageCursorLiteWithKey = null
|
||||
}
|
||||
|
||||
// wcdb_status wcdb_fetch_message_batch(wcdb_handle handle, wcdb_cursor cursor, char** out_json, int32_t* out_has_more)
|
||||
this.wcdbFetchMessageBatch = this.lib.func('int32 wcdb_fetch_message_batch(int64 handle, int64 cursor, _Out_ void** outJson, _Out_ int32* outHasMore)')
|
||||
|
||||
@@ -1084,6 +1113,51 @@ export class WcdbCore {
|
||||
} catch {
|
||||
this.wcdbAiQuerySourceRefs = null
|
||||
}
|
||||
try {
|
||||
this.wcdbAiGetRecentMessages = this.lib.func('int32 wcdb_ai_get_recent_messages(int64 handle, const char* optionsJson, _Out_ void** outJson)')
|
||||
} catch {
|
||||
this.wcdbAiGetRecentMessages = null
|
||||
}
|
||||
try {
|
||||
this.wcdbAiGetMessagesBefore = this.lib.func('int32 wcdb_ai_get_messages_before(int64 handle, const char* optionsJson, _Out_ void** outJson)')
|
||||
} catch {
|
||||
this.wcdbAiGetMessagesBefore = null
|
||||
}
|
||||
try {
|
||||
this.wcdbAiGetMessagesAfter = this.lib.func('int32 wcdb_ai_get_messages_after(int64 handle, const char* optionsJson, _Out_ void** outJson)')
|
||||
} catch {
|
||||
this.wcdbAiGetMessagesAfter = null
|
||||
}
|
||||
try {
|
||||
this.wcdbAiGetMessageContext = this.lib.func('int32 wcdb_ai_get_message_context(int64 handle, const char* optionsJson, _Out_ void** outJson)')
|
||||
} catch {
|
||||
this.wcdbAiGetMessageContext = null
|
||||
}
|
||||
try {
|
||||
this.wcdbAiGetSearchMessageContext = this.lib.func('int32 wcdb_ai_get_search_message_context(int64 handle, const char* optionsJson, _Out_ void** outJson)')
|
||||
} catch {
|
||||
this.wcdbAiGetSearchMessageContext = null
|
||||
}
|
||||
try {
|
||||
this.wcdbAiGetConversationBetween = this.lib.func('int32 wcdb_ai_get_conversation_between(int64 handle, const char* optionsJson, _Out_ void** outJson)')
|
||||
} catch {
|
||||
this.wcdbAiGetConversationBetween = null
|
||||
}
|
||||
try {
|
||||
this.wcdbAiSearchSessions = this.lib.func('int32 wcdb_ai_search_sessions(int64 handle, const char* optionsJson, _Out_ void** outJson)')
|
||||
} catch {
|
||||
this.wcdbAiSearchSessions = null
|
||||
}
|
||||
try {
|
||||
this.wcdbAiGetSessionMessages = this.lib.func('int32 wcdb_ai_get_session_messages(int64 handle, const char* optionsJson, _Out_ void** outJson)')
|
||||
} catch {
|
||||
this.wcdbAiGetSessionMessages = null
|
||||
}
|
||||
try {
|
||||
this.wcdbAiGetSessionSummaries = this.lib.func('int32 wcdb_ai_get_session_summaries(int64 handle, const char* optionsJson, _Out_ void** outJson)')
|
||||
} catch {
|
||||
this.wcdbAiGetSessionSummaries = null
|
||||
}
|
||||
|
||||
// wcdb_status wcdb_get_sns_timeline(wcdb_handle handle, int32_t limit, int32_t offset, const char* username, const char* keyword, int32_t start_time, int32_t end_time, char** out_json)
|
||||
try {
|
||||
@@ -3280,6 +3354,80 @@ export class WcdbCore {
|
||||
}
|
||||
}
|
||||
|
||||
async openMessageCursorWithKey(
|
||||
sessionId: string,
|
||||
batchSize: number,
|
||||
ascending: boolean,
|
||||
beginTimestamp: number,
|
||||
endTimestamp: number,
|
||||
key?: { sortSeq?: number; createTime?: number; localId?: number }
|
||||
): Promise<{ success: boolean; cursor?: number; error?: string }> {
|
||||
if (!this.ensureReady()) {
|
||||
return { success: false, error: 'WCDB 未连接' }
|
||||
}
|
||||
const keySortSeq = Number.isFinite(Number(key?.sortSeq)) ? Math.floor(Number(key?.sortSeq)) : 0
|
||||
const keyCreateTime = Number.isFinite(Number(key?.createTime)) ? Math.floor(Number(key?.createTime)) : 0
|
||||
const keyLocalId = Number.isFinite(Number(key?.localId)) ? Math.floor(Number(key?.localId)) : 0
|
||||
const keyValid = keySortSeq > 0 || keyCreateTime > 0 || keyLocalId > 0
|
||||
|
||||
if (!keyValid || !this.wcdbOpenMessageCursorWithKey) {
|
||||
return this.openMessageCursor(sessionId, batchSize, ascending, beginTimestamp, endTimestamp)
|
||||
}
|
||||
|
||||
try {
|
||||
const outCursor = [0]
|
||||
let result = this.wcdbOpenMessageCursorWithKey(
|
||||
this.handle,
|
||||
sessionId,
|
||||
batchSize,
|
||||
ascending ? 1 : 0,
|
||||
beginTimestamp,
|
||||
endTimestamp,
|
||||
1,
|
||||
keySortSeq,
|
||||
keyCreateTime,
|
||||
keyLocalId,
|
||||
outCursor
|
||||
)
|
||||
if (result === -3 && outCursor[0] <= 0 && this.shouldRetryCursorAfterNoDb()) {
|
||||
this.writeLog('openMessageCursorWithKey: result=-3 (no message db), attempting forceReopen...', true)
|
||||
const reopened = await this.forceReopen()
|
||||
if (reopened && this.handle !== null) {
|
||||
outCursor[0] = 0
|
||||
result = this.wcdbOpenMessageCursorWithKey(
|
||||
this.handle,
|
||||
sessionId,
|
||||
batchSize,
|
||||
ascending ? 1 : 0,
|
||||
beginTimestamp,
|
||||
endTimestamp,
|
||||
1,
|
||||
keySortSeq,
|
||||
keyCreateTime,
|
||||
keyLocalId,
|
||||
outCursor
|
||||
)
|
||||
this.writeLog(`openMessageCursorWithKey retry after forceReopen: result=${result} cursor=${outCursor[0]}`, true)
|
||||
}
|
||||
}
|
||||
if (result !== 0 || outCursor[0] <= 0) {
|
||||
if (result !== -3) {
|
||||
await this.printLogs(true)
|
||||
this.writeLog(
|
||||
`openMessageCursorWithKey failed: sessionId=${sessionId} batchSize=${batchSize} ascending=${ascending ? 1 : 0} begin=${beginTimestamp} end=${endTimestamp} result=${result} cursor=${outCursor[0]}`,
|
||||
true
|
||||
)
|
||||
}
|
||||
return { success: false, error: `创建游标失败: ${result}` }
|
||||
}
|
||||
return { success: true, cursor: outCursor[0] }
|
||||
} catch (e) {
|
||||
await this.printLogs(true)
|
||||
this.writeLog(`openMessageCursorWithKey exception: ${String(e)}`, true)
|
||||
return { success: false, error: '创建游标异常,请查看日志' }
|
||||
}
|
||||
}
|
||||
|
||||
async openMessageCursorLite(sessionId: string, batchSize: number, ascending: boolean, beginTimestamp: number, endTimestamp: number): Promise<{ success: boolean; cursor?: number; error?: string }> {
|
||||
if (!this.ensureReady()) {
|
||||
return { success: false, error: 'WCDB 未连接' }
|
||||
@@ -3342,6 +3490,83 @@ export class WcdbCore {
|
||||
}
|
||||
}
|
||||
|
||||
async openMessageCursorLiteWithKey(
|
||||
sessionId: string,
|
||||
batchSize: number,
|
||||
ascending: boolean,
|
||||
beginTimestamp: number,
|
||||
endTimestamp: number,
|
||||
key?: { sortSeq?: number; createTime?: number; localId?: number }
|
||||
): Promise<{ success: boolean; cursor?: number; error?: string }> {
|
||||
if (!this.ensureReady()) {
|
||||
return { success: false, error: 'WCDB 未连接' }
|
||||
}
|
||||
const keySortSeq = Number.isFinite(Number(key?.sortSeq)) ? Math.floor(Number(key?.sortSeq)) : 0
|
||||
const keyCreateTime = Number.isFinite(Number(key?.createTime)) ? Math.floor(Number(key?.createTime)) : 0
|
||||
const keyLocalId = Number.isFinite(Number(key?.localId)) ? Math.floor(Number(key?.localId)) : 0
|
||||
const keyValid = keySortSeq > 0 || keyCreateTime > 0 || keyLocalId > 0
|
||||
|
||||
if (!keyValid) {
|
||||
return this.openMessageCursorLite(sessionId, batchSize, ascending, beginTimestamp, endTimestamp)
|
||||
}
|
||||
if (!this.wcdbOpenMessageCursorLiteWithKey) {
|
||||
return this.openMessageCursorWithKey(sessionId, batchSize, ascending, beginTimestamp, endTimestamp, key)
|
||||
}
|
||||
|
||||
try {
|
||||
const outCursor = [0]
|
||||
let result = this.wcdbOpenMessageCursorLiteWithKey(
|
||||
this.handle,
|
||||
sessionId,
|
||||
batchSize,
|
||||
ascending ? 1 : 0,
|
||||
beginTimestamp,
|
||||
endTimestamp,
|
||||
1,
|
||||
keySortSeq,
|
||||
keyCreateTime,
|
||||
keyLocalId,
|
||||
outCursor
|
||||
)
|
||||
if (result === -3 && outCursor[0] <= 0 && this.shouldRetryCursorAfterNoDb()) {
|
||||
this.writeLog('openMessageCursorLiteWithKey: result=-3 (no message db), attempting forceReopen...', true)
|
||||
const reopened = await this.forceReopen()
|
||||
if (reopened && this.handle !== null) {
|
||||
outCursor[0] = 0
|
||||
result = this.wcdbOpenMessageCursorLiteWithKey(
|
||||
this.handle,
|
||||
sessionId,
|
||||
batchSize,
|
||||
ascending ? 1 : 0,
|
||||
beginTimestamp,
|
||||
endTimestamp,
|
||||
1,
|
||||
keySortSeq,
|
||||
keyCreateTime,
|
||||
keyLocalId,
|
||||
outCursor
|
||||
)
|
||||
this.writeLog(`openMessageCursorLiteWithKey retry after forceReopen: result=${result} cursor=${outCursor[0]}`, true)
|
||||
}
|
||||
}
|
||||
if (result !== 0 || outCursor[0] <= 0) {
|
||||
if (result !== -3) {
|
||||
await this.printLogs(true)
|
||||
this.writeLog(
|
||||
`openMessageCursorLiteWithKey failed: sessionId=${sessionId} batchSize=${batchSize} ascending=${ascending ? 1 : 0} begin=${beginTimestamp} end=${endTimestamp} result=${result} cursor=${outCursor[0]}`,
|
||||
true
|
||||
)
|
||||
}
|
||||
return { success: false, error: `创建游标失败: ${result}` }
|
||||
}
|
||||
return { success: true, cursor: outCursor[0] }
|
||||
} catch (e) {
|
||||
await this.printLogs(true)
|
||||
this.writeLog(`openMessageCursorLiteWithKey exception: ${String(e)}`, true)
|
||||
return { success: false, error: '创建游标异常,请查看日志' }
|
||||
}
|
||||
}
|
||||
|
||||
async fetchMessageBatch(cursor: number): Promise<{ success: boolean; rows?: any[]; hasMore?: boolean; error?: string }> {
|
||||
if (!this.ensureReady()) {
|
||||
return { success: false, error: 'WCDB 未连接' }
|
||||
@@ -4305,6 +4530,243 @@ export class WcdbCore {
|
||||
}
|
||||
}
|
||||
|
||||
async aiGetRecentMessages(options: {
|
||||
sessionId: string
|
||||
limit?: number
|
||||
beginTimestamp?: number
|
||||
endTimestamp?: number
|
||||
}): Promise<{ success: boolean; rows?: any[]; error?: string }> {
|
||||
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||
if (!this.wcdbAiGetRecentMessages) return { success: false, error: '当前数据服务版本不支持 AI 最近消息查询' }
|
||||
try {
|
||||
const outPtr = [null as any]
|
||||
const result = this.wcdbAiGetRecentMessages(this.handle, JSON.stringify({
|
||||
session_id: options.sessionId || '',
|
||||
limit: options.limit || 120,
|
||||
begin_timestamp: options.beginTimestamp || 0,
|
||||
end_timestamp: options.endTimestamp || 0
|
||||
}), outPtr)
|
||||
if (result !== 0 || !outPtr[0]) return { success: false, error: `AI 最近消息查询失败: ${result}` }
|
||||
const jsonStr = this.decodeJsonPtr(outPtr[0])
|
||||
if (!jsonStr) return { success: false, error: '解析 AI 最近消息查询失败' }
|
||||
return { success: true, rows: this.parseMessageJson(jsonStr) }
|
||||
} catch (e) {
|
||||
return { success: false, error: String(e) }
|
||||
}
|
||||
}
|
||||
|
||||
async aiGetMessagesBefore(options: {
|
||||
sessionId: string
|
||||
beforeId?: number
|
||||
beforeLocalId?: number
|
||||
beforeCreateTime?: number
|
||||
beforeSortSeq?: number
|
||||
limit?: number
|
||||
beginTimestamp?: number
|
||||
endTimestamp?: number
|
||||
}): Promise<{ success: boolean; rows?: any[]; error?: string }> {
|
||||
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||
if (!this.wcdbAiGetMessagesBefore) return { success: false, error: '当前数据服务版本不支持 AI 前向消息查询' }
|
||||
try {
|
||||
const outPtr = [null as any]
|
||||
const result = this.wcdbAiGetMessagesBefore(this.handle, JSON.stringify({
|
||||
session_id: options.sessionId || '',
|
||||
before_id: options.beforeId || 0,
|
||||
before_local_id: options.beforeLocalId || options.beforeId || 0,
|
||||
before_create_time: options.beforeCreateTime || 0,
|
||||
before_sort_seq: options.beforeSortSeq || 0,
|
||||
limit: options.limit || 120,
|
||||
begin_timestamp: options.beginTimestamp || 0,
|
||||
end_timestamp: options.endTimestamp || 0
|
||||
}), outPtr)
|
||||
if (result !== 0 || !outPtr[0]) return { success: false, error: `AI 前向消息查询失败: ${result}` }
|
||||
const jsonStr = this.decodeJsonPtr(outPtr[0])
|
||||
if (!jsonStr) return { success: false, error: '解析 AI 前向消息查询失败' }
|
||||
return { success: true, rows: this.parseMessageJson(jsonStr) }
|
||||
} catch (e) {
|
||||
return { success: false, error: String(e) }
|
||||
}
|
||||
}
|
||||
|
||||
async aiGetMessagesAfter(options: {
|
||||
sessionId: string
|
||||
afterId?: number
|
||||
afterLocalId?: number
|
||||
afterCreateTime?: number
|
||||
afterSortSeq?: number
|
||||
limit?: number
|
||||
beginTimestamp?: number
|
||||
endTimestamp?: number
|
||||
}): Promise<{ success: boolean; rows?: any[]; error?: string }> {
|
||||
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||
if (!this.wcdbAiGetMessagesAfter) return { success: false, error: '当前数据服务版本不支持 AI 后向消息查询' }
|
||||
try {
|
||||
const outPtr = [null as any]
|
||||
const result = this.wcdbAiGetMessagesAfter(this.handle, JSON.stringify({
|
||||
session_id: options.sessionId || '',
|
||||
after_id: options.afterId || 0,
|
||||
after_local_id: options.afterLocalId || options.afterId || 0,
|
||||
after_create_time: options.afterCreateTime || 0,
|
||||
after_sort_seq: options.afterSortSeq || 0,
|
||||
limit: options.limit || 120,
|
||||
begin_timestamp: options.beginTimestamp || 0,
|
||||
end_timestamp: options.endTimestamp || 0
|
||||
}), outPtr)
|
||||
if (result !== 0 || !outPtr[0]) return { success: false, error: `AI 后向消息查询失败: ${result}` }
|
||||
const jsonStr = this.decodeJsonPtr(outPtr[0])
|
||||
if (!jsonStr) return { success: false, error: '解析 AI 后向消息查询失败' }
|
||||
return { success: true, rows: this.parseMessageJson(jsonStr) }
|
||||
} catch (e) {
|
||||
return { success: false, error: String(e) }
|
||||
}
|
||||
}
|
||||
|
||||
async aiGetMessageContext(options: {
|
||||
sessionId: string
|
||||
messageIds: number[]
|
||||
}): Promise<{ success: boolean; rows?: any[]; error?: string }> {
|
||||
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||
if (!this.wcdbAiGetMessageContext) return { success: false, error: '当前数据服务版本不支持 AI 消息上下文查询' }
|
||||
try {
|
||||
const outPtr = [null as any]
|
||||
const result = this.wcdbAiGetMessageContext(this.handle, JSON.stringify({
|
||||
session_id: options.sessionId || '',
|
||||
message_ids: Array.isArray(options.messageIds) ? options.messageIds : []
|
||||
}), outPtr)
|
||||
if (result !== 0 || !outPtr[0]) return { success: false, error: `AI 消息上下文查询失败: ${result}` }
|
||||
const jsonStr = this.decodeJsonPtr(outPtr[0])
|
||||
if (!jsonStr) return { success: false, error: '解析 AI 消息上下文查询失败' }
|
||||
return { success: true, rows: this.parseMessageJson(jsonStr) }
|
||||
} catch (e) {
|
||||
return { success: false, error: String(e) }
|
||||
}
|
||||
}
|
||||
|
||||
async aiGetSearchMessageContext(options: {
|
||||
sessionId: string
|
||||
messageIds: number[]
|
||||
}): Promise<{ success: boolean; rows?: any[]; error?: string }> {
|
||||
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||
if (!this.wcdbAiGetSearchMessageContext) return { success: false, error: '当前数据服务版本不支持 AI 搜索上下文查询' }
|
||||
try {
|
||||
const outPtr = [null as any]
|
||||
const result = this.wcdbAiGetSearchMessageContext(this.handle, JSON.stringify({
|
||||
session_id: options.sessionId || '',
|
||||
message_ids: Array.isArray(options.messageIds) ? options.messageIds : []
|
||||
}), outPtr)
|
||||
if (result !== 0 || !outPtr[0]) return { success: false, error: `AI 搜索上下文查询失败: ${result}` }
|
||||
const jsonStr = this.decodeJsonPtr(outPtr[0])
|
||||
if (!jsonStr) return { success: false, error: '解析 AI 搜索上下文查询失败' }
|
||||
return { success: true, rows: this.parseMessageJson(jsonStr) }
|
||||
} catch (e) {
|
||||
return { success: false, error: String(e) }
|
||||
}
|
||||
}
|
||||
|
||||
async aiGetConversationBetween(options: {
|
||||
sessionId: string
|
||||
memberId1?: number
|
||||
memberId2?: number
|
||||
limit?: number
|
||||
beginTimestamp?: number
|
||||
endTimestamp?: number
|
||||
}): Promise<{ success: boolean; rows?: any[]; error?: string }> {
|
||||
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||
if (!this.wcdbAiGetConversationBetween) return { success: false, error: '当前数据服务版本不支持 AI 双人对话查询' }
|
||||
try {
|
||||
const outPtr = [null as any]
|
||||
const result = this.wcdbAiGetConversationBetween(this.handle, JSON.stringify({
|
||||
session_id: options.sessionId || '',
|
||||
member_id1: options.memberId1 || 0,
|
||||
member_id2: options.memberId2 || 0,
|
||||
limit: options.limit || 120,
|
||||
begin_timestamp: options.beginTimestamp || 0,
|
||||
end_timestamp: options.endTimestamp || 0
|
||||
}), outPtr)
|
||||
if (result !== 0 || !outPtr[0]) return { success: false, error: `AI 双人对话查询失败: ${result}` }
|
||||
const jsonStr = this.decodeJsonPtr(outPtr[0])
|
||||
if (!jsonStr) return { success: false, error: '解析 AI 双人对话查询失败' }
|
||||
return { success: true, rows: this.parseMessageJson(jsonStr) }
|
||||
} catch (e) {
|
||||
return { success: false, error: String(e) }
|
||||
}
|
||||
}
|
||||
|
||||
async aiSearchSessions(options: {
|
||||
keyword?: string
|
||||
limit?: number
|
||||
beginTimestamp?: number
|
||||
endTimestamp?: number
|
||||
}): Promise<{ success: boolean; rows?: any[]; error?: string }> {
|
||||
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||
if (!this.wcdbAiSearchSessions) return { success: false, error: '当前数据服务版本不支持 AI 会话搜索' }
|
||||
try {
|
||||
const outPtr = [null as any]
|
||||
const result = this.wcdbAiSearchSessions(this.handle, JSON.stringify({
|
||||
keyword: options.keyword || '',
|
||||
limit: options.limit || 20,
|
||||
begin_timestamp: options.beginTimestamp || 0,
|
||||
end_timestamp: options.endTimestamp || 0
|
||||
}), outPtr)
|
||||
if (result !== 0 || !outPtr[0]) return { success: false, error: `AI 会话搜索失败: ${result}` }
|
||||
const jsonStr = this.decodeJsonPtr(outPtr[0])
|
||||
if (!jsonStr) return { success: false, error: '解析 AI 会话搜索失败' }
|
||||
const rows = JSON.parse(jsonStr)
|
||||
return { success: true, rows: Array.isArray(rows) ? rows : [] }
|
||||
} catch (e) {
|
||||
return { success: false, error: String(e) }
|
||||
}
|
||||
}
|
||||
|
||||
async aiGetSessionMessages(options: {
|
||||
sessionId: string
|
||||
limit?: number
|
||||
beginTimestamp?: number
|
||||
endTimestamp?: number
|
||||
}): Promise<{ success: boolean; rows?: any[]; error?: string }> {
|
||||
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||
if (!this.wcdbAiGetSessionMessages) return { success: false, error: '当前数据服务版本不支持 AI 会话消息查询' }
|
||||
try {
|
||||
const outPtr = [null as any]
|
||||
const result = this.wcdbAiGetSessionMessages(this.handle, JSON.stringify({
|
||||
session_id: options.sessionId || '',
|
||||
limit: options.limit || 500,
|
||||
begin_timestamp: options.beginTimestamp || 0,
|
||||
end_timestamp: options.endTimestamp || 0
|
||||
}), outPtr)
|
||||
if (result !== 0 || !outPtr[0]) return { success: false, error: `AI 会话消息查询失败: ${result}` }
|
||||
const jsonStr = this.decodeJsonPtr(outPtr[0])
|
||||
if (!jsonStr) return { success: false, error: '解析 AI 会话消息查询失败' }
|
||||
return { success: true, rows: this.parseMessageJson(jsonStr) }
|
||||
} catch (e) {
|
||||
return { success: false, error: String(e) }
|
||||
}
|
||||
}
|
||||
|
||||
async aiGetSessionSummaries(options: {
|
||||
sessionIds?: string[]
|
||||
beginTimestamp?: number
|
||||
endTimestamp?: number
|
||||
}): Promise<{ success: boolean; data?: any; error?: string }> {
|
||||
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||
if (!this.wcdbAiGetSessionSummaries) return { success: false, error: '当前数据服务版本不支持 AI 会话摘要查询' }
|
||||
try {
|
||||
const outPtr = [null as any]
|
||||
const result = this.wcdbAiGetSessionSummaries(this.handle, JSON.stringify({
|
||||
session_ids_json: JSON.stringify(options.sessionIds || []),
|
||||
begin_timestamp: options.beginTimestamp || 0,
|
||||
end_timestamp: options.endTimestamp || 0
|
||||
}), outPtr)
|
||||
if (result !== 0 || !outPtr[0]) return { success: false, error: `AI 会话摘要查询失败: ${result}` }
|
||||
const jsonStr = this.decodeJsonPtr(outPtr[0])
|
||||
if (!jsonStr) return { success: false, error: '解析 AI 会话摘要查询失败' }
|
||||
const data = JSON.parse(jsonStr)
|
||||
return { success: true, data }
|
||||
} catch (e) {
|
||||
return { success: false, error: String(e) }
|
||||
}
|
||||
}
|
||||
|
||||
async getSnsTimeline(limit: number, offset: number, usernames?: string[], keyword?: string, startTime?: number, endTime?: number): Promise<{ success: boolean; timeline?: any[]; error?: string }> {
|
||||
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||
if (!this.wcdbGetSnsTimeline) return { success: false, error: '当前数据服务版本不支持获取朋友圈' }
|
||||
|
||||
@@ -468,6 +468,24 @@ export class WcdbService {
|
||||
return this.callWorker('openMessageCursor', { sessionId, batchSize, ascending, beginTimestamp, endTimestamp })
|
||||
}
|
||||
|
||||
async openMessageCursorWithKey(
|
||||
sessionId: string,
|
||||
batchSize: number,
|
||||
ascending: boolean,
|
||||
beginTimestamp: number,
|
||||
endTimestamp: number,
|
||||
key?: { sortSeq?: number; createTime?: number; localId?: number }
|
||||
): Promise<{ success: boolean; cursor?: number; error?: string }> {
|
||||
return this.callWorker('openMessageCursorWithKey', {
|
||||
sessionId,
|
||||
batchSize,
|
||||
ascending,
|
||||
beginTimestamp,
|
||||
endTimestamp,
|
||||
key
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开轻量级消息游标
|
||||
*/
|
||||
@@ -475,6 +493,24 @@ export class WcdbService {
|
||||
return this.callWorker('openMessageCursorLite', { sessionId, batchSize, ascending, beginTimestamp, endTimestamp })
|
||||
}
|
||||
|
||||
async openMessageCursorLiteWithKey(
|
||||
sessionId: string,
|
||||
batchSize: number,
|
||||
ascending: boolean,
|
||||
beginTimestamp: number,
|
||||
endTimestamp: number,
|
||||
key?: { sortSeq?: number; createTime?: number; localId?: number }
|
||||
): Promise<{ success: boolean; cursor?: number; error?: string }> {
|
||||
return this.callWorker('openMessageCursorLiteWithKey', {
|
||||
sessionId,
|
||||
batchSize,
|
||||
ascending,
|
||||
beginTimestamp,
|
||||
endTimestamp,
|
||||
key
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下一批消息
|
||||
*/
|
||||
@@ -616,6 +652,92 @@ export class WcdbService {
|
||||
return this.callWorker('aiQuerySourceRefs', { options })
|
||||
}
|
||||
|
||||
async aiGetRecentMessages(options: {
|
||||
sessionId: string
|
||||
limit?: number
|
||||
beginTimestamp?: number
|
||||
endTimestamp?: number
|
||||
}): Promise<{ success: boolean; rows?: any[]; error?: string }> {
|
||||
return this.callWorker('aiGetRecentMessages', { options })
|
||||
}
|
||||
|
||||
async aiGetMessagesBefore(options: {
|
||||
sessionId: string
|
||||
beforeId?: number
|
||||
beforeLocalId?: number
|
||||
beforeCreateTime?: number
|
||||
beforeSortSeq?: number
|
||||
limit?: number
|
||||
beginTimestamp?: number
|
||||
endTimestamp?: number
|
||||
}): Promise<{ success: boolean; rows?: any[]; error?: string }> {
|
||||
return this.callWorker('aiGetMessagesBefore', { options })
|
||||
}
|
||||
|
||||
async aiGetMessagesAfter(options: {
|
||||
sessionId: string
|
||||
afterId?: number
|
||||
afterLocalId?: number
|
||||
afterCreateTime?: number
|
||||
afterSortSeq?: number
|
||||
limit?: number
|
||||
beginTimestamp?: number
|
||||
endTimestamp?: number
|
||||
}): Promise<{ success: boolean; rows?: any[]; error?: string }> {
|
||||
return this.callWorker('aiGetMessagesAfter', { options })
|
||||
}
|
||||
|
||||
async aiGetMessageContext(options: {
|
||||
sessionId: string
|
||||
messageIds: number[]
|
||||
}): Promise<{ success: boolean; rows?: any[]; error?: string }> {
|
||||
return this.callWorker('aiGetMessageContext', { options })
|
||||
}
|
||||
|
||||
async aiGetSearchMessageContext(options: {
|
||||
sessionId: string
|
||||
messageIds: number[]
|
||||
}): Promise<{ success: boolean; rows?: any[]; error?: string }> {
|
||||
return this.callWorker('aiGetSearchMessageContext', { options })
|
||||
}
|
||||
|
||||
async aiGetConversationBetween(options: {
|
||||
sessionId: string
|
||||
memberId1?: number
|
||||
memberId2?: number
|
||||
limit?: number
|
||||
beginTimestamp?: number
|
||||
endTimestamp?: number
|
||||
}): Promise<{ success: boolean; rows?: any[]; error?: string }> {
|
||||
return this.callWorker('aiGetConversationBetween', { options })
|
||||
}
|
||||
|
||||
async aiSearchSessions(options: {
|
||||
keyword?: string
|
||||
limit?: number
|
||||
beginTimestamp?: number
|
||||
endTimestamp?: number
|
||||
}): Promise<{ success: boolean; rows?: any[]; error?: string }> {
|
||||
return this.callWorker('aiSearchSessions', { options })
|
||||
}
|
||||
|
||||
async aiGetSessionMessages(options: {
|
||||
sessionId: string
|
||||
limit?: number
|
||||
beginTimestamp?: number
|
||||
endTimestamp?: number
|
||||
}): Promise<{ success: boolean; rows?: any[]; error?: string }> {
|
||||
return this.callWorker('aiGetSessionMessages', { options })
|
||||
}
|
||||
|
||||
async aiGetSessionSummaries(options: {
|
||||
sessionIds?: string[]
|
||||
beginTimestamp?: number
|
||||
endTimestamp?: number
|
||||
}): Promise<{ success: boolean; data?: any; error?: string }> {
|
||||
return this.callWorker('aiGetSessionSummaries', { options })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取语音数据
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user