diff --git a/electron/main/ai/tools/definitions/get-member-stats.ts b/electron/main/ai/tools/definitions/get-member-stats.ts deleted file mode 100644 index 7ecb4ff8..00000000 --- a/electron/main/ai/tools/definitions/get-member-stats.ts +++ /dev/null @@ -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 { - 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, - } - }, - } -} diff --git a/electron/main/ai/tools/definitions/get-time-stats.ts b/electron/main/ai/tools/definitions/get-time-stats.ts deleted file mode 100644 index 6c173bdc..00000000 --- a/electron/main/ai/tools/definitions/get-time-stats.ts +++ /dev/null @@ -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 { - 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 - 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, - } - }, - } -} diff --git a/electron/main/ai/tools/definitions/index.ts b/electron/main/ai/tools/definitions/index.ts index c7fe256c..a7e28052 100644 --- a/electron/main/ai/tools/definitions/index.ts +++ b/electron/main/ai/tools/definitions/index.ts @@ -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', diff --git a/electron/main/ai/tools/shared-tool-adapter.ts b/electron/main/ai/tools/shared-tool-adapter.ts new file mode 100644 index 00000000..fcabbac9 --- /dev/null +++ b/electron/main/ai/tools/shared-tool-adapter.ts @@ -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 { + 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): Promise> { + 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 } + } + }, + } + }, + } +} diff --git a/packages/tools/src/definitions/session-info.ts b/packages/tools/src/definitions/session-info.ts index 7ffd740e..18b0d869 100644 --- a/packages/tools/src/definitions/session-info.ts +++ b/packages/tools/src/definitions/session-info.ts @@ -19,14 +19,15 @@ const inputSchema: JsonSchema = { } function handler(params: Record, 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 = { ...meta, ...overview } if (includeMembers) { - data.members = getMembers(context.db) + data.members = getMembers(db) } return { diff --git a/packages/tools/src/index.ts b/packages/tools/src/index.ts index cbdb47b6..0e99b21a 100644 --- a/packages/tools/src/index.ts +++ b/packages/tools/src/index.ts @@ -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, diff --git a/packages/tools/src/types.ts b/packages/tools/src/types.ts index 6a19c1b9..f026d643 100644 --- a/packages/tools/src/types.ts +++ b/packages/tools/src/types.ts @@ -91,8 +91,8 @@ export interface ToolExecutionContext { timeFilter?: TimeFilter /** 抽象查询接口,迁移后的工具使用此字段获取数据 */ dataProvider?: ToolDataProvider - /** @deprecated 逐步迁移到 dataProvider 后移除 */ - db: DatabaseAdapter + /** @deprecated 逐步迁移到 dataProvider 后移除;Electron 端不提供此字段 */ + db?: DatabaseAdapter } // ==================== Raw Message ====================