mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-26 02:04:37 +08:00
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:
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,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -91,8 +91,8 @@ export interface ToolExecutionContext {
|
||||
timeFilter?: TimeFilter
|
||||
/** 抽象查询接口,迁移后的工具使用此字段获取数据 */
|
||||
dataProvider?: ToolDataProvider
|
||||
/** @deprecated 逐步迁移到 dataProvider 后移除 */
|
||||
db: DatabaseAdapter
|
||||
/** @deprecated 逐步迁移到 dataProvider 后移除;Electron 端不提供此字段 */
|
||||
db?: DatabaseAdapter
|
||||
}
|
||||
|
||||
// ==================== Raw Message ====================
|
||||
|
||||
Reference in new issue
Block a user