refactor(electron): replace member_stats and time_stats with shared tool definitions

Create shared-tool-adapter to convert @openchatlab/tools ToolDefinition → AgentTool.
Delete Electron-specific get-member-stats.ts and get-time-stats.ts,
replaced by shared definitions via adaptSharedTool().
Make ToolExecutionContext.db optional to support Electron (no DatabaseAdapter).
This commit is contained in:
digua committed 2026-05-15 19:11:59 +08:00
1 parent 619b59270b
commit f35676cb23
7 files changed
+109 -113

No files matched your search

@@ -1,39 +0,0 @@
import { Type } from '@openchatlab/node-runtime'
import type { AgentTool } from '@openchatlab/node-runtime'
import type { ToolContext } from '../types'
import * as workerManager from '../../../worker/workerManager'
import { isChineseLocale } from '../utils/format'
const schema = Type.Object({
top_n: Type.Optional(Type.Number({ description: 'ai.tools.get_member_stats.params.top_n' })),
})
/** 获取群成员的活跃度统计数据。适用于回答"谁最活跃"、"发言最多的是谁"等问题。 */
export function createTool(context: ToolContext): AgentTool<typeof schema> {
return {
name: 'get_member_stats',
label: 'get_member_stats',
description: 'ai.tools.get_member_stats.desc',
parameters: schema,
execute: async (_toolCallId, params) => {
const { sessionId, timeFilter, locale } = context
const topN = params.top_n || 10
const result = await workerManager.getMemberActivity(sessionId, timeFilter)
const topMembers = result.slice(0, topN)
const msgSuffix = isChineseLocale(locale) ? '条' : ''
const data = {
totalMembers: result.length,
topMembers: topMembers.map(
(m, index) => `${index + 1}. ${m.name} ${m.messageCount}${msgSuffix}(${m.percentage}%)`
),
}
return {
content: [{ type: 'text', text: JSON.stringify(data) }],
details: data,
}
},
}
}
@@ -1,65 +0,0 @@
import { Type } from '@openchatlab/node-runtime'
import type { AgentTool } from '@openchatlab/node-runtime'
import type { ToolContext } from '../types'
import * as workerManager from '../../../worker/workerManager'
import { isChineseLocale, i18nTexts, t } from '../utils/format'
const schema = Type.Object({
type: Type.Union([Type.Literal('hourly'), Type.Literal('weekday'), Type.Literal('daily')], {
description: 'ai.tools.get_time_stats.params.type',
}),
})
/** 获取群聊的时间分布统计。适用于回答"什么时候最活跃"、"大家一般几点聊天"等问题。 */
export function createTool(context: ToolContext): AgentTool<typeof schema> {
return {
name: 'get_time_stats',
label: 'get_time_stats',
description: 'ai.tools.get_time_stats.desc',
parameters: schema,
execute: async (_toolCallId, params) => {
const { sessionId, timeFilter, locale } = context
const msgSuffix = isChineseLocale(locale) ? '条' : ''
let data: Record<string, unknown>
switch (params.type) {
case 'hourly': {
const result = await workerManager.getHourlyActivity(sessionId, timeFilter)
const peak = result.reduce((max, curr) => (curr.messageCount > max.messageCount ? curr : max))
data = {
peakHour: `${peak.hour}:00 (${peak.messageCount}${msgSuffix})`,
distribution: result.map((h) => `${h.hour}:00 ${h.messageCount}${msgSuffix}`),
}
break
}
case 'weekday': {
const weekdayNames = t('weekdays', locale) as string[]
const result = await workerManager.getWeekdayActivity(sessionId, timeFilter)
const peak = result.reduce((max, curr) => (curr.messageCount > max.messageCount ? curr : max))
data = {
peakDay: `${weekdayNames[peak.weekday]} (${peak.messageCount}${msgSuffix})`,
distribution: result.map((w) => `${weekdayNames[w.weekday]} ${w.messageCount}${msgSuffix}`),
}
break
}
case 'daily': {
const result = await workerManager.getDailyActivity(sessionId, timeFilter)
const recent = result.slice(-30)
const total = recent.reduce((sum, d) => sum + d.messageCount, 0)
const avg = Math.round(total / recent.length)
const summaryFn = i18nTexts.dailySummary[isChineseLocale(locale) ? 'zh' : 'en']
data = {
summary: summaryFn(recent.length, total, avg),
trend: recent.map((d) => `${d.date} ${d.messageCount}${msgSuffix}`),
}
break
}
}
return {
content: [{ type: 'text', text: JSON.stringify(data) }],
details: data,
}
},
}
}
+21 -4
View File
@@ -3,10 +3,13 @@
*
* TOOL_REGISTRY 是全局唯一的工具清单,驱动后端加载和前端目录展示。
* 新增工具只需在此追加一条 ToolRegistryEntry。
*
* 部分工具已迁移到 @openchatlab/tools 共享定义,通过 shared-tool-adapter 适配。
*/
import type { ToolRegistryEntry } from '../types'
// Electron-only 工具(保留原生实现)
import { createTool as createGetChatOverview } from './get-chat-overview'
import { createTool as createSearchMessages } from './search-messages'
import { createTool as createDeepSearchMessages } from './deep-search-messages'
@@ -15,8 +18,6 @@ import { createTool as createGetMessageContext } from './get-message-context'
import { createTool as createSearchSessions } from './search-sessions'
import { createTool as createGetSessionMessages } from './get-session-messages'
import { createTool as createGetMembers } from './get-group-members'
import { createTool as createGetMemberStats } from './get-member-stats'
import { createTool as createGetTimeStats } from './get-time-stats'
import { createTool as createGetMemberNameHistory } from './get-member-name-history'
import { createTool as createGetConversationBetween } from './get-conversation-between'
import { createTool as createGetSessionSummaries } from './get-session-summaries'
@@ -25,8 +26,22 @@ import { createTool as createKeywordFrequency } from './keyword-frequency'
import { sqlToolEntries } from './sql-analysis'
// 共享工具定义(通过 adapter 适配为 Electron AgentTool)
import { memberStatsTool, timeStatsTool } from '@openchatlab/tools'
import { adaptSharedTool } from '../shared-tool-adapter'
export { sqlToolEntries } from './sql-analysis'
const sharedMemberStats = adaptSharedTool(memberStatsTool, {
electronName: 'get_member_stats',
category: 'analysis',
})
const sharedTimeStats = adaptSharedTool(timeStatsTool, {
electronName: 'get_time_stats',
category: 'analysis',
})
export const TOOL_REGISTRY: ToolRegistryEntry[] = [
// ==================== Core 工具(始终加载) ====================
{ name: 'get_chat_overview', factory: createGetChatOverview, category: 'core' },
@@ -49,8 +64,10 @@ export const TOOL_REGISTRY: ToolRegistryEntry[] = [
{ name: 'get_members', factory: createGetMembers, category: 'core' },
// ==================== Analysis 工具(按需加载) ====================
{ name: 'get_member_stats', factory: createGetMemberStats, category: 'analysis' },
{ name: 'get_time_stats', factory: createGetTimeStats, category: 'analysis' },
// 共享工具(via @openchatlab/tools)
sharedMemberStats,
sharedTimeStats,
// Electron-only 工具
{ name: 'get_member_name_history', factory: createGetMemberNameHistory, category: 'analysis' },
{
name: 'get_conversation_between',
@@ -0,0 +1,77 @@
/**
* 共享工具适配器
*
* 将 @openchatlab/tools 的 ToolDefinition 转换为 Electron 的 ToolRegistryEntry。
* Electron 端使用 WorkerDataProvider 替代 Server 端的 CoreDataProvider。
*/
import type { ToolDefinition, ToolExecutionContext } from '@openchatlab/tools'
import type { AgentTool, AgentToolResult } from '@openchatlab/node-runtime'
import type { ToolContext, ToolRegistryEntry, ToolCategory } from './types'
import { WorkerDataProvider } from './worker-data-provider'
interface AdaptOptions {
/** Electron 端工具名(覆盖共享定义的名称) */
electronName: string
category: ToolCategory
truncationStrategy?: 'keep_first' | 'keep_last'
}
function buildExecutionContext(ctx: ToolContext): ToolExecutionContext {
return {
dataProvider: new WorkerDataProvider(ctx.sessionId),
sessionId: ctx.sessionId,
locale: ctx.locale,
timeFilter: ctx.timeFilter,
}
}
/**
* 将单个 ToolDefinition 适配为 Electron ToolRegistryEntry
*/
export function adaptSharedTool(tool: ToolDefinition, options: AdaptOptions): ToolRegistryEntry {
return {
name: options.electronName,
category: options.category,
truncationStrategy: options.truncationStrategy,
factory(context: ToolContext): AgentTool<any> {
const schema = {
type: 'object' as const,
properties: { ...tool.inputSchema.properties },
required: tool.inputSchema.required ?? [],
}
return {
name: options.electronName,
label: options.electronName,
description: `ai.tools.${options.electronName}.desc`,
parameters: schema as any,
async execute(_toolCallId: string, params: Record<string, unknown>): Promise<AgentToolResult<unknown>> {
const execCtx = buildExecutionContext(context)
try {
const result = await tool.handler(params, execCtx)
if (result.rawMessages && result.rawMessages.length > 0) {
const baseData = (typeof result.data === 'object' && result.data !== null ? result.data : {}) as Record<
string,
unknown
>
return {
content: [{ type: 'text', text: result.content }],
details: { ...baseData, rawMessages: result.rawMessages },
}
}
return {
content: [{ type: 'text', text: result.content }],
details: result.data ?? null,
}
} catch (error) {
const msg = error instanceof Error ? error.message : String(error)
return { content: [{ type: 'text', text: `Error: ${msg}` }], details: null }
}
},
}
},
}
}
@@ -19,14 +19,15 @@ const inputSchema: JsonSchema = {
}
function handler(params: Record<string, unknown>, context: ToolExecutionContext): ToolResult {
const meta = getSessionMeta(context.db)
const overview = getSessionOverview(context.db)
const db = context.db!
const meta = getSessionMeta(db)
const overview = getSessionOverview(db)
const includeMembers = params.include_members as boolean
const data: Record<string, unknown> = { ...meta, ...overview }
if (includeMembers) {
data.members = getMembers(context.db)
data.members = getMembers(db)
}
return {
+5
View File
@@ -7,6 +7,11 @@
export { TOOL_REGISTRY, getToolByName } from './registry'
export { CoreDataProvider } from './providers/core-data-provider'
export { memberStatsTool } from './definitions/member-stats'
export { timeStatsTool } from './definitions/time-stats'
export { searchTool } from './definitions/search'
export { recentMessagesTool } from './definitions/recent-messages'
export { sqlQueryTool, schemaTool } from './definitions/sql-query'
export type {
ToolDefinition,
ToolExecutionContext,
+2 -2
View File
@@ -91,8 +91,8 @@ export interface ToolExecutionContext {
timeFilter?: TimeFilter
/** 抽象查询接口,迁移后的工具使用此字段获取数据 */
dataProvider?: ToolDataProvider
/** @deprecated 逐步迁移到 dataProvider 后移除 */
db: DatabaseAdapter
/** @deprecated 逐步迁移到 dataProvider 后移除;Electron 端不提供此字段 */
db?: DatabaseAdapter
}
// ==================== Raw Message ====================