(params: Record<string, unknown>, context: ToolExecutionContext)
| 22 | } |
| 23 | |
| 24 | async function handler(params: Record<string, unknown>, context: ToolExecutionContext): Promise<ToolResult> { |
| 25 | const { locale } = context |
| 26 | const isZh = isChineseLocale(locale) |
| 27 | const days = (params.days as number) || 30 |
| 28 | const topN = (params.top_n as number) || 10 |
| 29 | |
| 30 | const sql = ` |
| 31 | SELECT msg.sender_id, COALESCE(m.group_nickname, m.account_name) AS name, msg.ts |
| 32 | FROM message msg |
| 33 | JOIN member m ON msg.sender_id = m.id |
| 34 | WHERE msg.type = 0 |
| 35 | AND msg.ts > unixepoch('now', '-' || @days || ' days') |
| 36 | ORDER BY msg.ts ASC |
| 37 | ` |
| 38 | const rows = await context.dataProvider!.executeParameterizedSql<MsgRow>(sql, { days }) |
| 39 | if (!rows || rows.length < 2) { |
| 40 | const text = isZh |
| 41 | ? '该时间范围内消息不足,无法分析响应时间' |
| 42 | : 'Not enough messages in this time range to analyze response time' |
| 43 | return { content: text, data: null } |
| 44 | } |
| 45 | |
| 46 | const responseTimes = new Map<number, { name: string; times: number[] }>() |
| 47 | |
| 48 | for (let i = 1; i < rows.length; i++) { |
| 49 | const prev = rows[i - 1] |
| 50 | const curr = rows[i] |
| 51 | if (curr.sender_id === prev.sender_id) continue |
| 52 | |
| 53 | const gap = curr.ts - prev.ts |
| 54 | if (gap < 5 || gap > 1800) continue |
| 55 | |
| 56 | if (!responseTimes.has(curr.sender_id)) { |
| 57 | responseTimes.set(curr.sender_id, { name: curr.name, times: [] }) |
| 58 | } |
| 59 | responseTimes.get(curr.sender_id)!.times.push(gap) |
| 60 | } |
| 61 | |
| 62 | const stats = [...responseTimes.entries()] |
| 63 | .map(([id, { name, times }]) => { |
| 64 | times.sort((a, b) => a - b) |
| 65 | const avg = Math.round(times.reduce((s, t) => s + t, 0) / times.length) |
| 66 | const median = times[Math.floor(times.length / 2)] |
| 67 | return { id, name, avgSeconds: avg, medianSeconds: median, responseCount: times.length } |
| 68 | }) |
| 69 | .filter((s) => s.responseCount >= 3) |
| 70 | .sort((a, b) => a.medianSeconds - b.medianSeconds) |
| 71 | .slice(0, topN) |
| 72 | |
| 73 | if (stats.length === 0) { |
| 74 | const text = isZh ? '没有足够的响应数据进行分析' : 'Not enough response data for analysis' |
| 75 | return { content: text, data: null } |
| 76 | } |
| 77 | |
| 78 | const formatTime = (s: number) => { |
| 79 | if (s < 60) return isZh ? `${s}秒` : `${s}s` |
| 80 | const m = Math.floor(s / 60) |
| 81 | const sec = s % 60 |
nothing calls this directly
no test coverage detected