@koishi-ce/plugin-analytics 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1 -1
- package/dist/style.css +1 -1
- package/lib/assets/de-DE-C35VOgt9.yml +6 -0
- package/lib/assets/en-US-C8mb89z7.yml +6 -0
- package/lib/assets/fr-FR-WljZXOoA.yml +6 -0
- package/lib/assets/ja-JP-DP_l11TV.yml +6 -0
- package/lib/assets/ru-RU-DWF7MRwv.yml +6 -0
- package/lib/assets/zh-CN-Cof4p_74.yml +6 -0
- package/lib/assets/zh-TW-_jfudK1s.yml +6 -0
- package/lib/index.d.ts +2 -2
- package/lib/index.mjs +15 -0
- package/package.json +3 -3
- package/src/index.test.ts +68 -23
- package/src/index.ts +126 -44
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{icons as e,store as t}from"@koishi-ce/client";import{Fragment as n,computed as r,createBlock as i,createCommentVNode as a,createElementBlock as o,createElementVNode as s,createSlots as ee,createTextVNode as c,createVNode as l,defineAsyncComponent as u,defineComponent as d,h as f,openBlock as p,provide as m,ref as h,renderSlot as g,resolveComponent as _,toDisplayString as v,unref as y,withCtx as b}from"vue";var x=u(()=>import(`./echarts-BMfAiWrm.js`)),S=h(`send`);function C({title:e,fields:n=[],showTab:r,options:i}){return d({render:()=>{if(!n.every(e=>t[e]))return null;let a=i(t,S.value);if(a)return f(_(`k-card`),{class:`frameless analytic-chart`},{header:()=>[f(`span`,{class:`left`},[e]),...r?[f(`span`,{class:`right`},[f(`span`,{class:`tab-item`+(S.value===`send`?` active`:``),onClick:()=>S.value=`send`},[`发送`]),f(`span`,{class:`tab-item`+(S.value===`receive`?` active`:``),onClick:()=>S.value=`receive`},[`接收`])])]:[]],default:()=>f(x,{option:a,autoresize:!0})})}})}var w={item(e){return{trigger:`item`,formatter:e}},axis(e){return{trigger:`axis`,axisPointer:{type:`cross`},formatter:e}}},T=e=>{e.slot({type:`analytic-chart`,component:C({title:`各平台消息占比`,fields:[`analytics`],showTab:!0,options({analytics:e},t){if(!e)return;let n=Object.entries(e.messageByBot).map(([e,n])=>({name:e,children:Object.entries(n).map(([e,n])=>({name:n.name||e,value:n[t]}))}));if(n.reduce((e,{children:t})=>e+t.reduce((e,{value:t})=>e+t,0),0))return{tooltip:w.item(({data:e})=>`${e.children?`平台`:`昵称`}:${e.name}<br>日均消息数量:${+e.value.toFixed(1)}`),series:[{type:`sunburst`,data:n,radius:[`0`,`65%`],nodeClick:!1,emphasis:{focus:`ancestor`},levels:[{},{label:{rotate:`tangential`}},{label:{position:`outside`,padding:3,silent:!1}}]}]}}})})},E=e=>{e.slot({type:`analytic-chart`,component:C({title:`指令调用频率`,fields:[`analytics`],options({analytics:e}){if(!e)return;let t=Object.entries(e.commandRate).sort((e,t)=>t[1]-e[1]).map(([e,t])=>({name:e,value:t}));if(t.length)return{tooltip:w.item(({data:e})=>{let t=[e.name];return t.push(`日均调用:${+e.value.toFixed(1)}`),t.join(`<br>`)}),series:[{type:`pie`,data:t,radius:[`35%`,`65%`],minShowLabelAngle:3}]}}})})},D=`日一二三四五六`,O=e=>{e.slot({type:`analytic-chart`,component:C({title:`历史消息数量`,fields:[`analytics`],showTab:!0,options({analytics:e},t){if(!e||!e.messageByDate.length)return;let n=e.messageByDate.slice(1);return{tooltip:w.axis(([e])=>{if(!e)return``;let{name:t,value:n}=e;return`${t} 星期${D[new Date(t).getDay()]}<br>消息数量:${n}`}),xAxis:{type:`category`,data:n.map((e,t)=>new Date(Date.now()-(t+1)*864e5).toLocaleDateString(`zh-CN`)).reverse()},yAxis:{type:`value`},series:{type:`line`,smooth:!0,data:n.map(e=>e[t]).reverse()}}}})})},k=e=>`${(e-.5).toFixed()}:00-${(e+.5).toFixed()}:00`,A=e=>{e.slot({type:`analytic-chart`,component:C({title:`每小时消息数量`,fields:[`analytics`],showTab:!0,options({analytics:e},t){if(e&&!e.messageByHour.every(e=>!e[t]))return{tooltip:w.axis(([n])=>{if(!n)return``;let[r=0]=n.data,{dataIndex:i}=n,a=e.messageByHour[i];return[`${k(r)}`,`日均消息数量:${+(a?.[t]||0).toFixed(1)}`].join(`<br>`)}),xAxis:{type:`value`,min:0,max:24,minInterval:1,maxInterval:4,axisLabel:{formatter:e=>`${e}:00`},axisPointer:{label:{formatter:({value:e})=>k(e)}}},yAxis:{type:`value`},series:[{data:e.messageByHour.map((e,n)=>[n+.5,e[t]||0]),type:`bar`,stack:`1`}]}}})})},j=e=>{e.plugin(O),e.plugin(A),e.plugin(T),e.plugin(E)},M={class:`content`},N={class:`title`},P={class:`value`},F={class:`left`},I={class:`right`},L=d({__name:`numeric`,props:{title:{},icon:{}},setup(e){return(t,n)=>{let r=_(`k-icon`),a=_(`k-card`);return p(),i(a,{class:`k-analytic-number`},ee({default:b(()=>[l(r,{name:e.icon},null,8,[`name`]),s(`div`,M,[s(`p`,N,v(e.title),1),s(`p`,P,[g(t.$slots,`default`,{},()=>[n[0]||=c(`-`,-1)])])])]),_:2},[t.$slots[`footer-left`]?{name:`footer`,fn:b(()=>[s(`span`,F,[g(t.$slots,`footer-left`)]),s(`span`,I,[g(t.$slots,`footer-right`,{},()=>[n[1]||=c(`-`,-1)])])]),key:`0`}:void 0]),1024)}}}),R={key:0,class:`card-grid numeric-grid`},z=d({__name:`index`,setup(e){m(`component:analytic-number`,L);let n=r(()=>{let e=t.analytics.dauHistory.slice(1),n=t.analytics.dauHistory.length-1;return n?e.reduce((e,t)=>e+t,0)/Math.min(e.length,n):0});return(e,r)=>{let i=_(`k-slot-item`),s=_(`k-slot`);return y(t).analytics?(p(),o(`div`,R,[l(s,{name:`analytic-number`},{default:b(()=>[l(i,null,{default:b(()=>[l(L,{icon:`analytic:user`,title:`用户数量`},{default:b(()=>[c(v(y(t).analytics.userCount),1)]),"footer-left":b(()=>[...r[0]||=[c(`昨日新增用户`,-1)]]),"footer-right":b(()=>[c(v(y(t).analytics.userIncrement),1)]),_:1}),l(L,{icon:`analytic:guild`,title:`群组数量`},{default:b(()=>[c(v(y(t).analytics.guildCount),1)]),"footer-left":b(()=>[...r[1]||=[c(`昨日新增群组`,-1)]]),"footer-right":b(()=>[c(v(y(t).analytics.guildIncrement),1)]),_:1}),l(L,{icon:`analytic:heart`,title:`今日 DAU`},{default:b(()=>[c(v(y(t).analytics.dauHistory[0]),1)]),"footer-left":b(()=>[...r[2]||=[c(`近期 DAU`,-1)]]),"footer-right":b(()=>[c(v(+n.value.toFixed(1)),1)]),_:1})]),_:1})]),_:1})])):a(`v-if`,!0)}}}),B=(e,t)=>{let n=e.__vccOpts||e;for(let[e,r]of t)n[e]=r;return n},V=B(z,[[`__scopeId`,`data-v-41af15ac`]]),H={class:`card-grid chart-grid`},U=d({__name:`home`,setup(e){return(e,t)=>{let r=_(`k-slot`);return p(),o(n,null,[l(V),s(`div`,H,[l(r,{name:`analytic-chart`})])],64)}}}),W={},G={class:`k-icon`,xmlns:`http://www.w3.org/2000/svg`,viewBox:`0 0 640 512`};function K(e,t){return p(),o(`svg`,G,[...t[0]||=[s(`path`,{fill:`currentColor`,d:`M274.7 304H173.3c-95.73 0-173.3 77.6-173.3 173.3C0 496.5 15.52 512 34.66 512H413.3C432.5 512 448 496.5 448 477.3C448 381.6 370.4 304 274.7 304zM413.3 480H34.66C33.2 480 32 478.8 32 477.3C32 399.4 95.4 336 173.3 336H274.7C352.6 336 416 399.4 416 477.3C416 478.8 414.8 480 413.3 480zM224 256c70.7 0 128-57.31 128-128S294.7 0 224 0C153.3 0 96 57.31 96 128S153.3 256 224 256zM224 32c52.94 0 96 43.06 96 96c0 52.93-43.06 96-96 96S128 180.9 128 128C128 75.06 171.1 32 224 32zM375.1 241C392.9 250.8 412.3 256 432 256C493.8 256 544 205.8 544 144S493.8 32 432 32c-12.83 0-25.39 2.156-37.34 6.391c-8.328 2.953-12.69 12.09-9.734 20.42c2.953 8.344 12.12 12.66 20.42 9.734C413.9 65.53 422.8 64 432 64C476.1 64 512 99.89 512 144S476.1 224 432 224c-14.08 0-27.91-3.703-39.98-10.69c-7.656-4.453-17.44-1.828-21.86 5.828C365.7 226.8 368.3 236.6 375.1 241zM490.7 320H448c-8.844 0-16 7.156-16 16S439.2 352 448 352h42.67C555.4 352 608 404.6 608 469.3C608 475.2 603.2 480 597.3 480H496c-8.844 0-16 7.156-16 16s7.156 16 16 16h101.3C620.9 512 640 492.9 640 469.3C640 386.1 573 320 490.7 320z`},null,-1)]])}var q=B(W,[[`render`,K]]),J={},Y={class:`k-icon`,xmlns:`http://www.w3.org/2000/svg`,viewBox:`0 0 512 512`};function X(e,t){return p(),o(`svg`,Y,[...t[0]||=[s(`path`,{fill:`currentColor`,d:`M255.1 96L267.1 84.02C300.6 51.37 347 36.51 392.6 44.1C461.5 55.58 512 115.2 512 185.1V190.9C512 232.4 494.8 272.1 464.4 300.4L283.7 469.1C276.2 476.1 266.3 480 256 480C245.7 480 235.8 476.1 228.3 469.1L47.59 300.4C17.23 272.1 0 232.4 0 190.9V185.1C0 115.2 50.52 55.58 119.4 44.1C164.1 36.51 211.4 51.37 244 84.02L255.1 96zM255.1 141.3L221.4 106.6C196.1 81.31 160 69.77 124.7 75.66C71.21 84.58 31.1 130.9 31.1 185.1V190.9C31.1 223.6 45.55 254.7 69.42 277L250.1 445.7C251.7 447.2 253.8 448 255.1 448C258.2 448 260.3 447.2 261.9 445.7L442.6 277C466.4 254.7 480 223.6 480 190.9V185.1C480 130.9 440.8 84.58 387.3 75.66C351.1 69.77 315.9 81.31 290.6 106.6L255.1 141.3z`},null,-1)]])}var Z=B(J,[[`render`,X]]),Q={},$={class:`k-icon`,xmlns:`http://www.w3.org/2000/svg`,viewBox:`0 0 512 512`};function te(e,t){return p(),o(`svg`,$,[...t[0]||=[s(`path`,{fill:`currentColor`,d:`M256 0C397.4 0 512 114.6 512 256C512 397.4 397.4 512 256 512C172.2 512 97.87 471.8 51.19 409.6C45.88 402.5 47.31 392.5 54.37 387.2C61.44 381.9 71.47 383.3 76.78 390.4C117.7 444.8 182.7 480 256 480C379.7 480 480 379.7 480 256C480 132.3 379.7 32 256 32C166.7 32 89.51 84.3 53.55 160H144C152.8 160 160 167.2 160 176C160 184.8 152.8 192 144 192H16C7.164 192 0 184.8 0 176V48C0 39.16 7.164 32 16 32C24.84 32 32 39.16 32 48V131.1C75.66 53.29 159.6 0 256 0zM256 128C264.8 128 272 135.2 272 144V249.4L347.3 324.7C353.6 330.9 353.6 341.1 347.3 347.3C341.1 353.6 330.9 353.6 324.7 347.3L244.7 267.3C241.7 264.3 239.1 260.2 239.1 256V144C239.1 135.2 247.2 128 255.1 128H256z`},null,-1)]])}var ne=B(Q,[[`render`,te]]),re={},ie={class:`k-icon`,xmlns:`http://www.w3.org/2000/svg`,viewBox:`0 0 448 512`};function ae(e,t){return p(),o(`svg`,ie,[...t[0]||=[s(`path`,{fill:`currentColor`,d:`M224 256c70.7 0 128-57.31 128-128s-57.3-128-128-128C153.3 0 96 57.31 96 128S153.3 256 224 256zM224 32c52.94 0 96 43.06 96 96c0 52.93-43.06 96-96 96S128 180.9 128 128C128 75.06 171.1 32 224 32zM274.7 304H173.3C77.61 304 0 381.6 0 477.3c0 19.14 15.52 34.67 34.66 34.67h378.7C432.5 512 448 496.5 448 477.3C448 381.6 370.4 304 274.7 304zM413.3 480H34.66C33.2 480 32 478.8 32 477.3C32 399.4 95.4 336 173.3 336h101.3C352.6 336 416 399.4 416 477.3C416 478.8 414.8 480 413.3 480z`},null,-1)]])}var oe=B(re,[[`render`,ae]]);e.register(`analytic:guild`,q),e.register(`analytic:heart`,Z),e.register(`analytic:history`,ne),e.register(`analytic:user`,oe);var se=e=>{e.plugin(j),e.slot({type:`home`,component:U,order:0})};export{se as default};
|
|
1
|
+
import{icons as e,root as t,store as n,useI18n as r}from"@koishi-ce/client";import{Fragment as i,computed as a,createBlock as o,createCommentVNode as s,createElementBlock as c,createElementVNode as l,createSlots as u,createTextVNode as d,createVNode as f,defineAsyncComponent as p,defineComponent as m,h,openBlock as g,provide as ee,ref as _,renderSlot as v,resolveComponent as y,toDisplayString as b,unref as x,withCtx as S}from"vue";var C=p(()=>import(`./echarts-BMfAiWrm.js`)),w=_(`send`);function T({title:e,fields:r=[],showTab:i,options:a}){return m({render:()=>{if(!r.every(e=>n[e]))return null;let o=a(n,w.value);if(!o)return;let s=e=>t.$i18n.t(e);return h(y(`k-card`),{class:`frameless analytic-chart`},{header:()=>[h(`span`,{class:`left`},[typeof e==`function`?e():e]),...i?[h(`span`,{class:`right`},[h(`span`,{class:`tab-item`+(w.value===`send`?` active`:``),onClick:()=>w.value=`send`},[s(`analytics.stats.send`)]),h(`span`,{class:`tab-item`+(w.value===`receive`?` active`:``),onClick:()=>w.value=`receive`},[s(`analytics.stats.receive`)])])]:[]],default:()=>h(C,{option:o,autoresize:!0})})}})}var E={item(e){return{trigger:`item`,formatter:e}},axis(e){return{trigger:`axis`,axisPointer:{type:`cross`},formatter:e}}},D=e=>{e.slot({type:`analytic-chart`,component:T({title:()=>e.$i18n.t(`analytics.bot.title`),fields:[`analytics`],showTab:!0,options({analytics:t},n){if(!t)return;let r=Object.entries(t.messageByBot).map(([e,t])=>({name:e,children:Object.entries(t).map(([e,t])=>({name:t.name||e,value:t[n]}))}));if(r.reduce((e,{children:t})=>e+t.reduce((e,{value:t})=>e+t,0),0))return{tooltip:E.item(({data:t})=>e.$i18n.t(t.children?`analytics.bot.tipPlatform`:`analytics.bot.tipAccount`,[t.name,+t.value.toFixed(1)])),series:[{type:`sunburst`,data:r,radius:[`0`,`65%`],nodeClick:!1,emphasis:{focus:`ancestor`},levels:[{},{label:{rotate:`tangential`}},{label:{position:`outside`,padding:3,silent:!1}}]}]}}})})},O=e=>{e.slot({type:`analytic-chart`,component:T({title:()=>e.$i18n.t(`analytics.command.title`),fields:[`analytics`],options({analytics:t}){if(!t)return;let n=Object.entries(t.commandRate).sort((e,t)=>t[1]-e[1]).map(([e,t])=>({name:e,value:t}));if(n.length)return{tooltip:E.item(({data:t})=>{let n=[t.name];return n.push(e.$i18n.t(`analytics.command.tip`,[+t.value.toFixed(1)])),n.join(`<br>`)}),series:[{type:`pie`,data:n,radius:[`35%`,`65%`],minShowLabelAngle:3}]}}})})},k=e=>{e.slot({type:`analytic-chart`,component:T({title:()=>e.$i18n.t(`analytics.history.title`),fields:[`analytics`],showTab:!0,options({analytics:t},n){if(!t||!t.messageByDate.length)return;let r=t.messageByDate.slice(1),i=e.$i18n.i18n.global.locale.value;return{tooltip:E.axis(([t])=>{if(!t)return``;let{name:n,value:r}=t,i=new Date(n).getDay(),a=e.$i18n.t(`analytics.history.weekdays`).split(` `)[i];return e.$i18n.t(`analytics.history.tip`,[n,a,r])}),xAxis:{type:`category`,data:r.map((e,t)=>new Date(Date.now()-(t+1)*864e5).toLocaleDateString(i)).reverse()},yAxis:{type:`value`},series:{type:`line`,smooth:!0,data:r.map(e=>e[n]).reverse()}}}})})},A=e=>`${(e-.5).toFixed()}:00-${(e+.5).toFixed()}:00`,j=e=>{e.slot({type:`analytic-chart`,component:T({title:()=>e.$i18n.t(`analytics.hour.title`),fields:[`analytics`],showTab:!0,options({analytics:t},n){if(t&&!t.messageByHour.every(e=>!e[n]))return{tooltip:E.axis(([r])=>{if(!r)return``;let[i=0]=r.data,{dataIndex:a}=r,o=t.messageByHour[a];return[`${A(i)}`,e.$i18n.t(`analytics.hour.tip`,[+(o?.[n]||0).toFixed(1)])].join(`<br>`)}),xAxis:{type:`value`,min:0,max:24,minInterval:1,maxInterval:4,axisLabel:{formatter:e=>`${e}:00`},axisPointer:{label:{formatter:({value:e})=>A(e)}}},yAxis:{type:`value`},series:[{data:t.messageByHour.map((e,t)=>[t+.5,e[n]||0]),type:`bar`,stack:`1`}]}}})})},M=e=>{e.plugin(k),e.plugin(j),e.plugin(D),e.plugin(O)},N={class:`content`},P={class:`title`},F={class:`value`},I={class:`left`},L={class:`right`},R=m({__name:`numeric`,props:{title:{},icon:{}},setup(e){return(t,n)=>{let r=y(`k-icon`),i=y(`k-card`);return g(),o(i,{class:`k-analytic-number`},u({default:S(()=>[f(r,{name:e.icon},null,8,[`name`]),l(`div`,N,[l(`p`,P,b(e.title),1),l(`p`,F,[v(t.$slots,`default`,{},()=>[n[0]||=d(`-`,-1)])])])]),_:2},[t.$slots[`footer-left`]?{name:`footer`,fn:S(()=>[l(`span`,I,[v(t.$slots,`footer-left`)]),l(`span`,L,[v(t.$slots,`footer-right`,{},()=>[n[1]||=d(`-`,-1)])])]),key:`0`}:void 0]),1024)}}}),z={key:0,class:`card-grid numeric-grid`},B=m({__name:`index`,setup(e){let{t}=r();ee(`component:analytic-number`,R);let i=a(()=>{let e=n.analytics.dauHistory.slice(1),t=n.analytics.dauHistory.length-1;return t?e.reduce((e,t)=>e+t,0)/Math.min(e.length,t):0});return(e,r)=>{let a=y(`k-slot-item`),o=y(`k-slot`);return x(n).analytics?(g(),c(`div`,z,[f(o,{name:`analytic-number`},{default:S(()=>[f(a,null,{default:S(()=>[f(R,{icon:`analytic:user`,title:x(t)(`analytics.numbers.users`)},{default:S(()=>[d(b(x(n).analytics.userCount),1)]),"footer-left":S(()=>[d(b(x(t)(`analytics.numbers.newUsers`)),1)]),"footer-right":S(()=>[d(b(x(n).analytics.userIncrement),1)]),_:1},8,[`title`]),f(R,{icon:`analytic:guild`,title:x(t)(`analytics.numbers.guilds`)},{default:S(()=>[d(b(x(n).analytics.guildCount),1)]),"footer-left":S(()=>[d(b(x(t)(`analytics.numbers.newGuilds`)),1)]),"footer-right":S(()=>[d(b(x(n).analytics.guildIncrement),1)]),_:1},8,[`title`]),f(R,{icon:`analytic:heart`,title:x(t)(`analytics.numbers.dauToday`)},{default:S(()=>[d(b(x(n).analytics.dauHistory[0]),1)]),"footer-left":S(()=>[d(b(x(t)(`analytics.numbers.dauRecent`)),1)]),"footer-right":S(()=>[d(b(+i.value.toFixed(1)),1)]),_:1},8,[`title`])]),_:1})]),_:1})])):s(`v-if`,!0)}}}),V=(e,t)=>{let n=e.__vccOpts||e;for(let[e,r]of t)n[e]=r;return n},H=V(B,[[`__scopeId`,`data-v-bc1c77bd`]]),U={class:`card-grid chart-grid`},W=m({__name:`home`,setup(e){return(e,t)=>{let n=y(`k-slot`);return g(),c(i,null,[f(H),l(`div`,U,[f(n,{name:`analytic-chart`})])],64)}}}),G={analytics:{hour:{title:`Nachrichten pro Stunde`,tip:`Tagesdurchschnitt Nachrichten: {0}`},bot:{title:`Nachrichtenanteil pro Plattform`,tipPlatform:`Plattform: {0}<br>Tagesdurchschnitt: {1}`,tipAccount:`Spitzname: {0}<br>Tagesdurchschnitt: {1}`},command:{title:`Befehlsaufruf-Häufigkeit`,tip:`Tagesdurchschnitt Aufrufe: {0}`},history:{title:`Nachrichtenverlauf`,weekdays:`So Mo Di Mi Do Fr Sa`,tip:`{0} {1}<br>Nachrichten: {2}`},stats:{send:`Gesendet`,receive:`Empfangen`},numbers:{users:`Benutzer`,newUsers:`Neue Benutzer gestern`,guilds:`Gruppen`,newGuilds:`Neue Gruppen gestern`,dauToday:`DAU heute`,dauRecent:`DAU zuletzt`}}},te={analytics:{hour:{title:`Messages per Hour`,tip:`Daily average messages: {0}`},bot:{title:`Message Share by Platform`,tipPlatform:`Platform: {0}<br>Daily average messages: {1}`,tipAccount:`Nickname: {0}<br>Daily average messages: {1}`},command:{title:`Command Invocation Frequency`,tip:`Daily average invocations: {0}`},history:{title:`Message History`,weekdays:`Sun Mon Tue Wed Thu Fri Sat`,tip:`{0} {1}<br>Messages: {2}`},stats:{send:`Sent`,receive:`Received`},numbers:{users:`Users`,newUsers:`New users yesterday`,guilds:`Groups`,newGuilds:`New groups yesterday`,dauToday:`DAU today`,dauRecent:`Recent DAU`}}},K={analytics:{hour:{title:`Messages par heure`,tip:`Moyenne quotidienne de messages : {0}`},bot:{title:`Répartition des messages par plateforme`,tipPlatform:`Plateforme : {0}<br>Moyenne quotidienne : {1}`,tipAccount:`Pseudo : {0}<br>Moyenne quotidienne : {1}`},command:{title:`Fréquence d'appel des commandes`,tip:`Appels quotidiens moyens : {0}`},history:{title:`Historique des messages`,weekdays:`dim. lun. mar. mer. jeu. ven. sam.`,tip:`{0} {1}<br>Messages : {2}`},stats:{send:`Envoyés`,receive:`Reçus`},numbers:{users:`Utilisateurs`,newUsers:`Nouveaux utilisateurs hier`,guilds:`Groupes`,newGuilds:`Nouveaux groupes hier`,dauToday:`DAU du jour`,dauRecent:`DAU récent`}}},q={analytics:{hour:{title:`毎時間のメッセージ数`,tip:`日平均メッセージ数:{0}`},bot:{title:`プラットフォーム別メッセージ割合`,tipPlatform:`プラットフォーム:{0}<br>日平均メッセージ数:{1}`,tipAccount:`ニックネーム:{0}<br>日平均メッセージ数:{1}`},command:{title:`コマンド実行頻度`,tip:`日平均実行回数:{0}`},history:{title:`履歴メッセージ数`,weekdays:`日 月 火 水 木 金 土`,tip:`{0} {1}曜日<br>メッセージ数:{2}`},stats:{send:`送信`,receive:`受信`},numbers:{users:`ユーザー数`,newUsers:`昨日の新規ユーザー`,guilds:`グループ数`,newGuilds:`昨日の新規グループ`,dauToday:`本日の DAU`,dauRecent:`最近の DAU`}}},J={analytics:{hour:{title:`Сообщений по часам`,tip:`В среднем сообщений в день: {0}`},bot:{title:`Доля сообщений по платформам`,tipPlatform:`Платформа: {0}<br>В среднем сообщений в день: {1}`,tipAccount:`Ник: {0}<br>В среднем сообщений в день: {1}`},command:{title:`Частота вызова команд`,tip:`В среднем вызовов в день: {0}`},history:{title:`История сообщений`,weekdays:`Вс Пн Вт Ср Чт Пт Сб`,tip:`{0} {1}<br>Сообщений: {2}`},stats:{send:`Отправлено`,receive:`Получено`},numbers:{users:`Пользователи`,newUsers:`Новые пользователи за вчера`,guilds:`Группы`,newGuilds:`Новые группы за вчера`,dauToday:`DAU сегодня`,dauRecent:`DAU за последнее время`}}},Y={analytics:{hour:{title:`每小时消息数量`,tip:`日均消息数量:{0}`},bot:{title:`各平台消息占比`,tipPlatform:`平台:{0}<br>日均消息数量:{1}`,tipAccount:`昵称:{0}<br>日均消息数量:{1}`},command:{title:`指令调用频率`,tip:`日均调用:{0}`},history:{title:`历史消息数量`,weekdays:`日 一 二 三 四 五 六`,tip:`{0} 星期{1}<br>消息数量:{2}`},stats:{send:`发送`,receive:`接收`},numbers:{users:`用户数量`,newUsers:`昨日新增用户`,guilds:`群组数量`,newGuilds:`昨日新增群组`,dauToday:`今日 DAU`,dauRecent:`近期 DAU`}}},X={analytics:{hour:{title:`每小時訊息數量`,tip:`日均訊息數量:{0}`},bot:{title:`各平台訊息佔比`,tipPlatform:`平台:{0}<br>日均訊息數量:{1}`,tipAccount:`暱稱:{0}<br>日均訊息數量:{1}`},command:{title:`指令呼叫頻率`,tip:`日均呼叫:{0}`},history:{title:`歷史訊息數量`,weekdays:`日 一 二 三 四 五 六`,tip:`{0} 星期{1}<br>訊息數量:{2}`},stats:{send:`傳送`,receive:`接收`},numbers:{users:`使用者數量`,newUsers:`昨日新增使用者`,guilds:`群組數量`,newGuilds:`昨日新增群組`,dauToday:`今日 DAU`,dauRecent:`近期 DAU`}}},Z={},Q={class:`k-icon`,xmlns:`http://www.w3.org/2000/svg`,viewBox:`0 0 640 512`};function ne(e,t){return g(),c(`svg`,Q,[...t[0]||=[l(`path`,{fill:`currentColor`,d:`M274.7 304H173.3c-95.73 0-173.3 77.6-173.3 173.3C0 496.5 15.52 512 34.66 512H413.3C432.5 512 448 496.5 448 477.3C448 381.6 370.4 304 274.7 304zM413.3 480H34.66C33.2 480 32 478.8 32 477.3C32 399.4 95.4 336 173.3 336H274.7C352.6 336 416 399.4 416 477.3C416 478.8 414.8 480 413.3 480zM224 256c70.7 0 128-57.31 128-128S294.7 0 224 0C153.3 0 96 57.31 96 128S153.3 256 224 256zM224 32c52.94 0 96 43.06 96 96c0 52.93-43.06 96-96 96S128 180.9 128 128C128 75.06 171.1 32 224 32zM375.1 241C392.9 250.8 412.3 256 432 256C493.8 256 544 205.8 544 144S493.8 32 432 32c-12.83 0-25.39 2.156-37.34 6.391c-8.328 2.953-12.69 12.09-9.734 20.42c2.953 8.344 12.12 12.66 20.42 9.734C413.9 65.53 422.8 64 432 64C476.1 64 512 99.89 512 144S476.1 224 432 224c-14.08 0-27.91-3.703-39.98-10.69c-7.656-4.453-17.44-1.828-21.86 5.828C365.7 226.8 368.3 236.6 375.1 241zM490.7 320H448c-8.844 0-16 7.156-16 16S439.2 352 448 352h42.67C555.4 352 608 404.6 608 469.3C608 475.2 603.2 480 597.3 480H496c-8.844 0-16 7.156-16 16s7.156 16 16 16h101.3C620.9 512 640 492.9 640 469.3C640 386.1 573 320 490.7 320z`},null,-1)]])}var re=V(Z,[[`render`,ne]]),ie={},ae={class:`k-icon`,xmlns:`http://www.w3.org/2000/svg`,viewBox:`0 0 512 512`};function oe(e,t){return g(),c(`svg`,ae,[...t[0]||=[l(`path`,{fill:`currentColor`,d:`M255.1 96L267.1 84.02C300.6 51.37 347 36.51 392.6 44.1C461.5 55.58 512 115.2 512 185.1V190.9C512 232.4 494.8 272.1 464.4 300.4L283.7 469.1C276.2 476.1 266.3 480 256 480C245.7 480 235.8 476.1 228.3 469.1L47.59 300.4C17.23 272.1 0 232.4 0 190.9V185.1C0 115.2 50.52 55.58 119.4 44.1C164.1 36.51 211.4 51.37 244 84.02L255.1 96zM255.1 141.3L221.4 106.6C196.1 81.31 160 69.77 124.7 75.66C71.21 84.58 31.1 130.9 31.1 185.1V190.9C31.1 223.6 45.55 254.7 69.42 277L250.1 445.7C251.7 447.2 253.8 448 255.1 448C258.2 448 260.3 447.2 261.9 445.7L442.6 277C466.4 254.7 480 223.6 480 190.9V185.1C480 130.9 440.8 84.58 387.3 75.66C351.1 69.77 315.9 81.31 290.6 106.6L255.1 141.3z`},null,-1)]])}var $=V(ie,[[`render`,oe]]),se={},ce={class:`k-icon`,xmlns:`http://www.w3.org/2000/svg`,viewBox:`0 0 512 512`};function le(e,t){return g(),c(`svg`,ce,[...t[0]||=[l(`path`,{fill:`currentColor`,d:`M256 0C397.4 0 512 114.6 512 256C512 397.4 397.4 512 256 512C172.2 512 97.87 471.8 51.19 409.6C45.88 402.5 47.31 392.5 54.37 387.2C61.44 381.9 71.47 383.3 76.78 390.4C117.7 444.8 182.7 480 256 480C379.7 480 480 379.7 480 256C480 132.3 379.7 32 256 32C166.7 32 89.51 84.3 53.55 160H144C152.8 160 160 167.2 160 176C160 184.8 152.8 192 144 192H16C7.164 192 0 184.8 0 176V48C0 39.16 7.164 32 16 32C24.84 32 32 39.16 32 48V131.1C75.66 53.29 159.6 0 256 0zM256 128C264.8 128 272 135.2 272 144V249.4L347.3 324.7C353.6 330.9 353.6 341.1 347.3 347.3C341.1 353.6 330.9 353.6 324.7 347.3L244.7 267.3C241.7 264.3 239.1 260.2 239.1 256V144C239.1 135.2 247.2 128 255.1 128H256z`},null,-1)]])}var ue=V(se,[[`render`,le]]),de={},fe={class:`k-icon`,xmlns:`http://www.w3.org/2000/svg`,viewBox:`0 0 448 512`};function pe(e,t){return g(),c(`svg`,fe,[...t[0]||=[l(`path`,{fill:`currentColor`,d:`M224 256c70.7 0 128-57.31 128-128s-57.3-128-128-128C153.3 0 96 57.31 96 128S153.3 256 224 256zM224 32c52.94 0 96 43.06 96 96c0 52.93-43.06 96-96 96S128 180.9 128 128C128 75.06 171.1 32 224 32zM274.7 304H173.3C77.61 304 0 381.6 0 477.3c0 19.14 15.52 34.67 34.66 34.67h378.7C432.5 512 448 496.5 448 477.3C448 381.6 370.4 304 274.7 304zM413.3 480H34.66C33.2 480 32 478.8 32 477.3C32 399.4 95.4 336 173.3 336h101.3C352.6 336 416 399.4 416 477.3C416 478.8 414.8 480 413.3 480z`},null,-1)]])}var me=V(de,[[`render`,pe]]);e.register(`analytic:guild`,re),e.register(`analytic:heart`,$),e.register(`analytic:history`,ue),e.register(`analytic:user`,me);var he=e=>{e.$i18n.extend(`de-DE`,G),e.$i18n.extend(`en-US`,te),e.$i18n.extend(`fr-FR`,K),e.$i18n.extend(`ja-JP`,q),e.$i18n.extend(`ru-RU`,J),e.$i18n.extend(`zh-CN`,Y),e.$i18n.extend(`zh-TW`,X),e.plugin(M),e.slot({type:`home`,component:W,order:0})};export{he as default};
|
package/dist/style.css
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
.analytic-chart header{justify-content:space-between;display:flex}.analytic-chart header .right{gap:1rem;font-size:1rem;font-weight:400;display:flex}.analytic-chart header .tab-item{cursor:pointer;padding:0 4px}.analytic-chart header .tab-item.active{color:var(--primary);border-bottom:2px solid var(--primary)}.k-analytic-number .k-card-body{justify-content:space-between;align-items:center;margin:1rem 0;display:flex}.k-analytic-number .k-card-body .k-icon{width:3rem;height:2.5rem;color:var(--k-text-normal);transition:color .3s}@media screen and (width<=768px){.k-analytic-number .k-card-body .k-icon{height:2.25rem}}.k-analytic-number .k-card-body .content{float:right;text-align:right;flex-direction:column;justify-content:space-around;height:3rem;display:flex}.k-analytic-number .k-card-body p{margin:0;font-size:.9375rem}.k-analytic-number .k-card-body .value{font-size:1rem}@media (width<=768px){.k-analytic-number .k-card-body{margin:.5rem 0;padding:0 1rem}}.k-analytic-number footer{border-top:1px solid var(--k-card-border);justify-content:space-between;margin-bottom:.75rem;padding-top:.75rem;font-size:.9em;display:flex}@media (width<=768px){.k-analytic-number footer{margin:.5rem 0;padding:.5rem 1rem 0}}.numeric-grid[data-v-
|
|
1
|
+
.analytic-chart header{justify-content:space-between;display:flex}.analytic-chart header .right{gap:1rem;font-size:1rem;font-weight:400;display:flex}.analytic-chart header .tab-item{cursor:pointer;padding:0 4px}.analytic-chart header .tab-item.active{color:var(--primary);border-bottom:2px solid var(--primary)}.k-analytic-number .k-card-body{justify-content:space-between;align-items:center;margin:1rem 0;display:flex}.k-analytic-number .k-card-body .k-icon{width:3rem;height:2.5rem;color:var(--k-text-normal);transition:color .3s}@media screen and (width<=768px){.k-analytic-number .k-card-body .k-icon{height:2.25rem}}.k-analytic-number .k-card-body .content{float:right;text-align:right;flex-direction:column;justify-content:space-around;height:3rem;display:flex}.k-analytic-number .k-card-body p{margin:0;font-size:.9375rem}.k-analytic-number .k-card-body .value{font-size:1rem}@media (width<=768px){.k-analytic-number .k-card-body{margin:.5rem 0;padding:0 1rem}}.k-analytic-number footer{border-top:1px solid var(--k-card-border);justify-content:space-between;margin-bottom:.75rem;padding-top:.75rem;font-size:.9em;display:flex}@media (width<=768px){.k-analytic-number footer{margin:.5rem 0;padding:.5rem 1rem 0}}.numeric-grid[data-v-bc1c77bd]{grid-template-columns:repeat(4,1fr)}@media screen and (width<=1280px){.numeric-grid[data-v-bc1c77bd]{grid-template-columns:repeat(2,1fr)}}.card-grid{margin:var(--card-margin);grid-gap:var(--card-margin);display:grid}.chart-grid .echarts{max-width:100%;margin:0 auto}@media (width>=1280px){.chart-grid{grid-template-columns:repeat(2,1fr)}}@media (width>=1280px) and (width>=1600px){.chart-grid .echarts{width:600px;max-width:100%;height:400px;margin:0 auto}}@media (width>=1280px) and (width<=1600px){.chart-grid .echarts{width:480px;height:360px}}@media (width<=1280px){.chart-grid{grid-template-columns:1fr}}@media (width<=1280px) and (width>=768px){.chart-grid .echarts{width:800px;height:400px}}@media (width<=1280px) and (width<=768px) and (width>=480px){.chart-grid .echarts{width:600px;height:400px}}@media (width<=1280px) and (width<=480px){.chart-grid .echarts{width:100%;height:300px}}.grid{display:grid}.flex{display:flex}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter,backdrop-filter;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease{transition-timing-function:cubic-bezier(.4,0,.2,1)}
|
|
2
2
|
/*$vite$:1*/
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
# Copyright (c) 2019-present Shigma and Koishijs contributors.
|
|
3
|
+
# Copyright (c) 2026-present Koishi-CE contributors.
|
|
4
|
+
|
|
5
|
+
statsInternal: Intervall für das Senden der Statistiken.
|
|
6
|
+
recentDayCount: Anzahl der letzten Tage, die in die Statistiken einfließen.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
# Copyright (c) 2019-present Shigma and Koishijs contributors.
|
|
3
|
+
# Copyright (c) 2026-present Koishi-CE contributors.
|
|
4
|
+
|
|
5
|
+
statsInternal: Interval for pushing statistics.
|
|
6
|
+
recentDayCount: Number of recent days to include in statistics.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
# Copyright (c) 2019-present Shigma and Koishijs contributors.
|
|
3
|
+
# Copyright (c) 2026-present Koishi-CE contributors.
|
|
4
|
+
|
|
5
|
+
statsInternal: Intervalle d'envoi des statistiques.
|
|
6
|
+
recentDayCount: Nombre de jours récents à inclure dans les statistiques.
|
package/lib/index.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ declare module "@koishi-ce/console" {
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
/** 收发消息计数对:send 为发出条数,receive 为收到条数。 */
|
|
18
|
-
interface MessageStats {
|
|
18
|
+
export interface MessageStats {
|
|
19
19
|
send: number;
|
|
20
20
|
receive: number;
|
|
21
21
|
}
|
|
@@ -149,4 +149,4 @@ declare namespace Analytics {
|
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
//#endregion
|
|
152
|
-
export {
|
|
152
|
+
export { Analytics as default };
|
package/lib/index.mjs
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { DataService } from "@koishi-ce/console";
|
|
3
3
|
import { $, Logger, Schema, Time, deepEqual, pick } from "@koishi-ce/koishi";
|
|
4
|
+
import deDE from "./assets/de-DE-C35VOgt9.yml";
|
|
5
|
+
import enUS from "./assets/en-US-C8mb89z7.yml";
|
|
6
|
+
import frFR from "./assets/fr-FR-WljZXOoA.yml";
|
|
7
|
+
import jaJP from "./assets/ja-JP-DP_l11TV.yml";
|
|
8
|
+
import ruRU from "./assets/ru-RU-DWF7MRwv.yml";
|
|
9
|
+
import zhCN from "./assets/zh-CN-Cof4p_74.yml";
|
|
10
|
+
import zhTW from "./assets/zh-TW-_jfudK1s.yml";
|
|
4
11
|
//#region src/index.ts
|
|
5
12
|
/**
|
|
6
13
|
* analytics 插件(node 侧):消息与指令的统计分析服务。
|
|
@@ -18,6 +25,14 @@ var Analytics = class extends DataService {
|
|
|
18
25
|
static Config = Schema.object({
|
|
19
26
|
statsInternal: Schema.natural().role("ms").description("统计数据推送的时间间隔。").default(Time.minute * 10),
|
|
20
27
|
recentDayCount: Schema.natural().description("统计最近几天的数据。").default(7)
|
|
28
|
+
}).i18n({
|
|
29
|
+
"de-DE": deDE,
|
|
30
|
+
"en-US": enUS,
|
|
31
|
+
"fr-FR": frFR,
|
|
32
|
+
"ja-JP": jaJP,
|
|
33
|
+
"ru-RU": ruRU,
|
|
34
|
+
"zh-CN": zhCN,
|
|
35
|
+
"zh-TW": zhTW
|
|
21
36
|
});
|
|
22
37
|
config;
|
|
23
38
|
/** 上次落库时间,用于判断距下次上传是否超过 statsInternal。 */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@koishi-ce/plugin-analytics",
|
|
3
3
|
"description": "Show statistics for Koishi application",
|
|
4
|
-
"version": "1.0
|
|
4
|
+
"version": "1.1.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.mjs",
|
|
7
7
|
"typings": "lib/index.d.ts",
|
|
@@ -54,9 +54,9 @@
|
|
|
54
54
|
"@koishi-ce/koishi": "^1.0.0"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
|
-
"@koishi-ce/client": "^1.0
|
|
57
|
+
"@koishi-ce/client": "^1.1.0",
|
|
58
58
|
"echarts": "^6.1.0",
|
|
59
|
-
"@koishi-ce/koishi": "^1.0.
|
|
59
|
+
"@koishi-ce/koishi": "^1.0.8",
|
|
60
60
|
"vite": "^8.2.2",
|
|
61
61
|
"vue-echarts": "^8.1.0"
|
|
62
62
|
},
|
package/src/index.test.ts
CHANGED
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
// Copyright (c) 2019-present Shigma and Koishijs contributors.
|
|
3
3
|
// Copyright (c) 2026-present Koishi-CE contributors.
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
afterAll,
|
|
7
|
+
beforeAll,
|
|
8
|
+
describe,
|
|
9
|
+
expect,
|
|
10
|
+
it,
|
|
11
|
+
} from "bun:test";
|
|
6
12
|
import { Console, type Entry } from "@koishi-ce/console";
|
|
7
13
|
import { App, type Plugin, Time } from "@koishi-ce/koishi";
|
|
8
14
|
import mock from "@koishi-ce/plugin-mock";
|
|
@@ -14,7 +20,10 @@ type Analytics = InstanceType<typeof analytics>;
|
|
|
14
20
|
|
|
15
21
|
/** 控制台服务桩:仅实现入口登记所需的最小面。 */
|
|
16
22
|
class FakeConsole extends Console {
|
|
17
|
-
protected resolveEntry(
|
|
23
|
+
protected resolveEntry(
|
|
24
|
+
_files: Entry.Files,
|
|
25
|
+
_key: string,
|
|
26
|
+
): string[] {
|
|
18
27
|
return [];
|
|
19
28
|
}
|
|
20
29
|
}
|
|
@@ -24,15 +33,21 @@ const app = new App();
|
|
|
24
33
|
// 同 admin:CJS 实现配 ESM 声明,nodenext 互操作视图多包一层 default,类型层穿透取真实类
|
|
25
34
|
app.plugin(memory as unknown as typeof memory.default);
|
|
26
35
|
// Console 基类的 static inject 是 cordis 3 旧形态,与 Plugin.Constructor 期待类型不兼容,仅做类型层转型
|
|
27
|
-
app.plugin(
|
|
36
|
+
app.plugin(
|
|
37
|
+
FakeConsole as unknown as Plugin.Constructor<App>,
|
|
38
|
+
);
|
|
28
39
|
// statsInternal 归零:每条消息事件后立即落库,便于断言
|
|
29
|
-
app.plugin(analytics, {
|
|
40
|
+
app.plugin(analytics, {
|
|
41
|
+
statsInternal: 0,
|
|
42
|
+
recentDayCount: 7,
|
|
43
|
+
});
|
|
30
44
|
app.plugin(mock);
|
|
31
45
|
|
|
32
46
|
// 注册一个空指令,用于驱动 command/execute 计数;带回复以驱动 send 事件
|
|
33
47
|
app.command("analytics-probe").action(() => "probe-done");
|
|
34
48
|
|
|
35
|
-
const service = () =>
|
|
49
|
+
const service = () =>
|
|
50
|
+
app.get("console.services.analytics") as Analytics;
|
|
36
51
|
|
|
37
52
|
const client = app.mock.client("123", "321");
|
|
38
53
|
|
|
@@ -82,19 +97,27 @@ describe("analytics 统计服务", () => {
|
|
|
82
97
|
await service().upload(true);
|
|
83
98
|
|
|
84
99
|
const today = Time.getDateNumber();
|
|
85
|
-
const rows = await app.database.get(
|
|
86
|
-
|
|
87
|
-
|
|
100
|
+
const rows = await app.database.get(
|
|
101
|
+
"analytics.message",
|
|
102
|
+
{
|
|
103
|
+
date: today,
|
|
104
|
+
},
|
|
105
|
+
);
|
|
88
106
|
// mock 会话:用户消息计入 receive(mock 编码器不派发 send 会话,发出侧见下一用例)
|
|
89
107
|
expect(rows.length).toBeGreaterThan(0);
|
|
90
|
-
const receiveRow = rows.find(
|
|
108
|
+
const receiveRow = rows.find(
|
|
109
|
+
(row) => row.type === "receive",
|
|
110
|
+
);
|
|
91
111
|
expect(receiveRow?.count).toBe(1);
|
|
92
112
|
expect(receiveRow?.platform).toBe("mock");
|
|
93
113
|
expect(receiveRow?.selfId).toBe("514");
|
|
94
114
|
|
|
95
|
-
const commands = await app.database.get(
|
|
96
|
-
|
|
97
|
-
|
|
115
|
+
const commands = await app.database.get(
|
|
116
|
+
"analytics.command",
|
|
117
|
+
{
|
|
118
|
+
date: today,
|
|
119
|
+
},
|
|
120
|
+
);
|
|
98
121
|
expect(commands).toHaveLength(1);
|
|
99
122
|
expect(commands[0]).toMatchObject({
|
|
100
123
|
name: "analytics-probe",
|
|
@@ -105,13 +128,19 @@ describe("analytics 统计服务", () => {
|
|
|
105
128
|
|
|
106
129
|
it("send 事件计入发出消息数", async () => {
|
|
107
130
|
// mock 编码器不派发 send 会话,此处直接派发最小会话驱动计数
|
|
108
|
-
app.emit("send", {
|
|
131
|
+
app.emit("send", {
|
|
132
|
+
selfId: "514",
|
|
133
|
+
platform: "mock",
|
|
134
|
+
} as never);
|
|
109
135
|
await service().upload(true);
|
|
110
136
|
const today = Time.getDateNumber();
|
|
111
|
-
const rows = await app.database.get(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
137
|
+
const rows = await app.database.get(
|
|
138
|
+
"analytics.message",
|
|
139
|
+
{
|
|
140
|
+
date: today,
|
|
141
|
+
type: "send",
|
|
142
|
+
},
|
|
143
|
+
);
|
|
115
144
|
expect(rows[0]?.count).toBe(1);
|
|
116
145
|
});
|
|
117
146
|
|
|
@@ -121,14 +150,18 @@ describe("analytics 统计服务", () => {
|
|
|
121
150
|
const before = +service().lastUpdate;
|
|
122
151
|
await service().upload();
|
|
123
152
|
// 触发后 lastUpdate 刷新为更新的时间
|
|
124
|
-
expect(+service().lastUpdate).toBeGreaterThanOrEqual(
|
|
153
|
+
expect(+service().lastUpdate).toBeGreaterThanOrEqual(
|
|
154
|
+
before,
|
|
155
|
+
);
|
|
125
156
|
});
|
|
126
157
|
|
|
127
158
|
it("非强制上传在跨小时边界触发", async () => {
|
|
128
159
|
// 将记录的小时号置为不可能值:dateHour !== updateHour 分支命中
|
|
129
160
|
service().updateHour = -1;
|
|
130
161
|
await service().upload();
|
|
131
|
-
expect(service().updateHour).toBe(
|
|
162
|
+
expect(service().updateHour).toBe(
|
|
163
|
+
new Date().getHours(),
|
|
164
|
+
);
|
|
132
165
|
});
|
|
133
166
|
|
|
134
167
|
it("download 聚合数值指标与各图表数据", async () => {
|
|
@@ -155,13 +188,25 @@ describe("analytics 统计服务", () => {
|
|
|
155
188
|
expect(bot?.send).toBe(3);
|
|
156
189
|
|
|
157
190
|
// 按日历史:昨天收 2 发 3,今天恒为 0 占位
|
|
158
|
-
expect(payload.messageByDate[1]).toEqual({
|
|
159
|
-
|
|
191
|
+
expect(payload.messageByDate[1]).toEqual({
|
|
192
|
+
receive: 2,
|
|
193
|
+
send: 3,
|
|
194
|
+
});
|
|
195
|
+
expect(payload.messageByDate[0]).toEqual({
|
|
196
|
+
receive: 0,
|
|
197
|
+
send: 0,
|
|
198
|
+
});
|
|
160
199
|
|
|
161
200
|
// 按小时分布:24 个时段,10 点的日均收发
|
|
162
201
|
expect(payload.messageByHour).toHaveLength(24);
|
|
163
|
-
expect(payload.messageByHour[10]).toEqual({
|
|
164
|
-
|
|
202
|
+
expect(payload.messageByHour[10]).toEqual({
|
|
203
|
+
receive: 2,
|
|
204
|
+
send: 3,
|
|
205
|
+
});
|
|
206
|
+
expect(payload.messageByHour[23]).toEqual({
|
|
207
|
+
receive: 0,
|
|
208
|
+
send: 0,
|
|
209
|
+
});
|
|
165
210
|
});
|
|
166
211
|
|
|
167
212
|
it("get 按自然日缓存聚合结果", async () => {
|
package/src/index.ts
CHANGED
|
@@ -28,6 +28,13 @@ import {
|
|
|
28
28
|
Time,
|
|
29
29
|
type Universal,
|
|
30
30
|
} from "@koishi-ce/koishi";
|
|
31
|
+
import deDE from "../locales/de-DE.yml";
|
|
32
|
+
import enUS from "../locales/en-US.yml";
|
|
33
|
+
import frFR from "../locales/fr-FR.yml";
|
|
34
|
+
import jaJP from "../locales/ja-JP.yml";
|
|
35
|
+
import ruRU from "../locales/ru-RU.yml";
|
|
36
|
+
import zhCN from "../locales/zh-CN.yml";
|
|
37
|
+
import zhTW from "../locales/zh-TW.yml";
|
|
31
38
|
|
|
32
39
|
declare module "@koishi-ce/koishi" {
|
|
33
40
|
interface Tables {
|
|
@@ -65,6 +72,14 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
65
72
|
recentDayCount: Schema.natural()
|
|
66
73
|
.description("统计最近几天的数据。")
|
|
67
74
|
.default(7),
|
|
75
|
+
}).i18n({
|
|
76
|
+
"de-DE": deDE,
|
|
77
|
+
"en-US": enUS,
|
|
78
|
+
"fr-FR": frFR,
|
|
79
|
+
"ja-JP": jaJP,
|
|
80
|
+
"ru-RU": ruRU,
|
|
81
|
+
"zh-CN": zhCN,
|
|
82
|
+
"zh-TW": zhTW,
|
|
68
83
|
});
|
|
69
84
|
|
|
70
85
|
// 基类 Service 声明了 config: unknown;此处收敛为必填并在构造时归一化
|
|
@@ -89,7 +104,8 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
89
104
|
super(ctx, "analytics");
|
|
90
105
|
|
|
91
106
|
this.config = {
|
|
92
|
-
statsInternal:
|
|
107
|
+
statsInternal:
|
|
108
|
+
config.statsInternal ?? Time.minute * 10,
|
|
93
109
|
recentDayCount: config.recentDayCount ?? 7,
|
|
94
110
|
};
|
|
95
111
|
|
|
@@ -105,7 +121,13 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
105
121
|
count: "integer",
|
|
106
122
|
},
|
|
107
123
|
{
|
|
108
|
-
primary: [
|
|
124
|
+
primary: [
|
|
125
|
+
"date",
|
|
126
|
+
"hour",
|
|
127
|
+
"type",
|
|
128
|
+
"selfId",
|
|
129
|
+
"platform",
|
|
130
|
+
],
|
|
109
131
|
},
|
|
110
132
|
);
|
|
111
133
|
|
|
@@ -157,20 +179,23 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
157
179
|
this.upload();
|
|
158
180
|
});
|
|
159
181
|
|
|
160
|
-
ctx
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
182
|
+
ctx
|
|
183
|
+
.any()
|
|
184
|
+
.before("command/execute", ({ command, session }) => {
|
|
185
|
+
if (!command || !session) return;
|
|
186
|
+
// 观察字段在类型上不可索引,按宽泛视图读取(与 createIndex 同理)
|
|
187
|
+
// biome-ignore lint/suspicious/noExplicitAny: Session<never,...> 的 user
|
|
188
|
+
const user = (session as Session<any, any, any>)
|
|
189
|
+
.user;
|
|
190
|
+
this.addAudit(this.commands, {
|
|
191
|
+
...this.createIndex(session),
|
|
192
|
+
name: command.name,
|
|
193
|
+
// 库表列声明为 integer,而 user.id 为字符串,这里按数值归一
|
|
194
|
+
userId: +(user?.["id"] || 0),
|
|
195
|
+
channelId: session.channelId ?? "",
|
|
196
|
+
});
|
|
197
|
+
this.upload();
|
|
171
198
|
});
|
|
172
|
-
this.upload();
|
|
173
|
-
});
|
|
174
199
|
|
|
175
200
|
ctx.console.addEntry({
|
|
176
201
|
dev: resolve(__dirname, "../client/index.ts"),
|
|
@@ -179,8 +204,10 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
179
204
|
}
|
|
180
205
|
|
|
181
206
|
// 不协变,各事件回调处的具体泛型互不相同,内部工具方法统一放宽
|
|
182
|
-
|
|
183
|
-
|
|
207
|
+
private createIndex(
|
|
208
|
+
// biome-ignore lint/suspicious/noExplicitAny: Session 泛型在 user 观察字段上
|
|
209
|
+
session: Session<any, any, any>,
|
|
210
|
+
): Analytics.Index {
|
|
184
211
|
return {
|
|
185
212
|
selfId: session.selfId,
|
|
186
213
|
platform: session.platform,
|
|
@@ -198,7 +225,10 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
198
225
|
index: Omit<T, "count">,
|
|
199
226
|
) {
|
|
200
227
|
const audit = buffer.find((data) =>
|
|
201
|
-
deepEqual(
|
|
228
|
+
deepEqual(
|
|
229
|
+
pick(data, Object.keys(index) as (keyof T)[]),
|
|
230
|
+
index,
|
|
231
|
+
),
|
|
202
232
|
);
|
|
203
233
|
if (audit) {
|
|
204
234
|
audit.count += 1;
|
|
@@ -236,14 +266,21 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
236
266
|
const dateHour = date.getHours();
|
|
237
267
|
if (
|
|
238
268
|
forced ||
|
|
239
|
-
+date - +this.lastUpdate >
|
|
269
|
+
+date - +this.lastUpdate >
|
|
270
|
+
this.config.statsInternal ||
|
|
240
271
|
dateHour !== this.updateHour
|
|
241
272
|
) {
|
|
242
273
|
this.lastUpdate = date;
|
|
243
274
|
this.updateHour = dateHour;
|
|
244
275
|
await Promise.all([
|
|
245
|
-
this.uploadAudit(
|
|
246
|
-
|
|
276
|
+
this.uploadAudit(
|
|
277
|
+
"analytics.message",
|
|
278
|
+
this.messages,
|
|
279
|
+
),
|
|
280
|
+
this.uploadAudit(
|
|
281
|
+
"analytics.command",
|
|
282
|
+
this.commands,
|
|
283
|
+
),
|
|
247
284
|
]);
|
|
248
285
|
logger.debug("analytics updated");
|
|
249
286
|
}
|
|
@@ -252,7 +289,8 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
252
289
|
/** 最近 N 天(不含今天)的日期号查询区间,供各聚合查询复用。 */
|
|
253
290
|
private queryRecent(): Query.FieldExpr<number> {
|
|
254
291
|
return {
|
|
255
|
-
$gte:
|
|
292
|
+
$gte:
|
|
293
|
+
Time.getDateNumber() - this.config.recentDayCount,
|
|
256
294
|
$lt: Time.getDateNumber(),
|
|
257
295
|
};
|
|
258
296
|
}
|
|
@@ -263,7 +301,9 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
263
301
|
*
|
|
264
302
|
* @param lengthTask 参与平均的天数(见 download 中的计算)
|
|
265
303
|
*/
|
|
266
|
-
private async getCommandRate(
|
|
304
|
+
private async getCommandRate(
|
|
305
|
+
lengthTask: Promise<number>,
|
|
306
|
+
) {
|
|
267
307
|
const data = await this.ctx.database
|
|
268
308
|
.select("analytics.command", {
|
|
269
309
|
date: this.queryRecent(),
|
|
@@ -288,14 +328,20 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
288
328
|
private async getDauHistory() {
|
|
289
329
|
const data = await this.ctx.database
|
|
290
330
|
.select("analytics.command", {
|
|
291
|
-
date: {
|
|
331
|
+
date: {
|
|
332
|
+
$gte:
|
|
333
|
+
Time.getDateNumber() -
|
|
334
|
+
this.config.recentDayCount,
|
|
335
|
+
},
|
|
292
336
|
userId: { $gt: 0 },
|
|
293
337
|
})
|
|
294
338
|
.groupBy(["date"], {
|
|
295
339
|
count: (row) => $.count(row.userId),
|
|
296
340
|
})
|
|
297
341
|
.execute();
|
|
298
|
-
const result: number[] = new Array(
|
|
342
|
+
const result: number[] = new Array(
|
|
343
|
+
this.config.recentDayCount + 1,
|
|
344
|
+
).fill(0);
|
|
299
345
|
const today = Time.getDateNumber();
|
|
300
346
|
data.forEach((stat) => {
|
|
301
347
|
result[today - stat.date] = stat.count;
|
|
@@ -310,7 +356,9 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
310
356
|
*
|
|
311
357
|
* @param lengthTask 参与平均的天数(见 download 中的计算)
|
|
312
358
|
*/
|
|
313
|
-
private async getMessageByBot(
|
|
359
|
+
private async getMessageByBot(
|
|
360
|
+
lengthTask: Promise<number>,
|
|
361
|
+
) {
|
|
314
362
|
const data = await this.ctx.database
|
|
315
363
|
.select("analytics.message", {
|
|
316
364
|
date: this.queryRecent(),
|
|
@@ -321,16 +369,22 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
321
369
|
.execute();
|
|
322
370
|
const length = await lengthTask;
|
|
323
371
|
// 机器人资料(bot.user)运行时可能缺席,按 Partial 记录
|
|
324
|
-
const result = {} as Dict<
|
|
372
|
+
const result = {} as Dict<
|
|
373
|
+
Dict<MessageStats & Partial<Universal.User>>
|
|
374
|
+
>;
|
|
325
375
|
data.forEach((stat) => {
|
|
326
|
-
const bot =
|
|
327
|
-
|
|
376
|
+
const bot =
|
|
377
|
+
this.ctx.bots[`${stat.platform}:${stat.selfId}`];
|
|
378
|
+
const entry = ((result[stat.platform] ||= {})[
|
|
379
|
+
stat.selfId
|
|
380
|
+
] ||= {
|
|
328
381
|
...(bot?.user ?? {}),
|
|
329
382
|
send: 0,
|
|
330
383
|
receive: 0,
|
|
331
384
|
});
|
|
332
385
|
// type 列的取值集合由写入端约定为 send / receive 两种
|
|
333
|
-
entry[stat.type as "send" | "receive"] =
|
|
386
|
+
entry[stat.type as "send" | "receive"] =
|
|
387
|
+
stat.count / length;
|
|
334
388
|
});
|
|
335
389
|
return result;
|
|
336
390
|
}
|
|
@@ -353,7 +407,10 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
353
407
|
const today = Time.getDateNumber();
|
|
354
408
|
const result: MessageStats[] = [];
|
|
355
409
|
data.forEach((stat) => {
|
|
356
|
-
const entry = (result[today - stat.date] ||= {
|
|
410
|
+
const entry = (result[today - stat.date] ||= {
|
|
411
|
+
send: 0,
|
|
412
|
+
receive: 0,
|
|
413
|
+
});
|
|
357
414
|
entry[stat.type as "send" | "receive"] = stat.count;
|
|
358
415
|
});
|
|
359
416
|
for (let i = 0; i < result.length; i++) {
|
|
@@ -368,7 +425,9 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
368
425
|
*
|
|
369
426
|
* @param lengthTask 参与平均的天数(见 download 中的计算)
|
|
370
427
|
*/
|
|
371
|
-
private async getMessageByHour(
|
|
428
|
+
private async getMessageByHour(
|
|
429
|
+
lengthTask: Promise<number>,
|
|
430
|
+
) {
|
|
372
431
|
const data = await this.ctx.database
|
|
373
432
|
.select("analytics.message", {
|
|
374
433
|
date: this.queryRecent(),
|
|
@@ -384,7 +443,8 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
384
443
|
data.forEach((stat) => {
|
|
385
444
|
const entry = result[stat.hour];
|
|
386
445
|
if (!entry) return;
|
|
387
|
-
entry[stat.type as "send" | "receive"] =
|
|
446
|
+
entry[stat.type as "send" | "receive"] =
|
|
447
|
+
stat.count / length;
|
|
388
448
|
});
|
|
389
449
|
return result;
|
|
390
450
|
}
|
|
@@ -400,7 +460,10 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
400
460
|
async download(): Promise<Analytics.Payload> {
|
|
401
461
|
const messageByDateTask = this.getMessageByDate();
|
|
402
462
|
const lengthTask = messageByDateTask.then((data) => {
|
|
403
|
-
return Math.min(
|
|
463
|
+
return Math.min(
|
|
464
|
+
Math.max(data.length - 1, 1),
|
|
465
|
+
this.config.recentDayCount,
|
|
466
|
+
);
|
|
404
467
|
});
|
|
405
468
|
const [
|
|
406
469
|
userCount,
|
|
@@ -414,14 +477,22 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
414
477
|
messageByHour,
|
|
415
478
|
] = await Promise.all([
|
|
416
479
|
// 用户总数
|
|
417
|
-
this.ctx.database.eval("user", (row) =>
|
|
480
|
+
this.ctx.database.eval("user", (row) =>
|
|
481
|
+
$.count(row.id),
|
|
482
|
+
),
|
|
418
483
|
// 昨日新增用户数(createdAt 落在昨天一整天)
|
|
419
|
-
this.ctx.database.eval(
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
484
|
+
this.ctx.database.eval(
|
|
485
|
+
"user",
|
|
486
|
+
(row) => $.count(row.id),
|
|
487
|
+
{
|
|
488
|
+
createdAt: {
|
|
489
|
+
$gte: Time.fromDateNumber(
|
|
490
|
+
Time.getDateNumber() - 1,
|
|
491
|
+
),
|
|
492
|
+
$lt: Time.fromDateNumber(Time.getDateNumber()),
|
|
493
|
+
},
|
|
423
494
|
},
|
|
424
|
-
|
|
495
|
+
),
|
|
425
496
|
// 群组总数:channel 表中 id === guildId 的行即群本身(而非普通子频道)
|
|
426
497
|
this.ctx.database.eval(
|
|
427
498
|
"channel",
|
|
@@ -435,8 +506,14 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
435
506
|
(row) =>
|
|
436
507
|
$.and(
|
|
437
508
|
$.eq(row.id, row.guildId),
|
|
438
|
-
$.gte(
|
|
439
|
-
|
|
509
|
+
$.gte(
|
|
510
|
+
row.createdAt,
|
|
511
|
+
Time.fromDateNumber(Time.getDateNumber() - 1),
|
|
512
|
+
),
|
|
513
|
+
$.lt(
|
|
514
|
+
row.createdAt,
|
|
515
|
+
Time.fromDateNumber(Time.getDateNumber()),
|
|
516
|
+
),
|
|
440
517
|
),
|
|
441
518
|
),
|
|
442
519
|
this.getCommandRate(lengthTask),
|
|
@@ -465,7 +542,10 @@ class Analytics extends DataService<Analytics.Payload> {
|
|
|
465
542
|
*/
|
|
466
543
|
override async get() {
|
|
467
544
|
const date = new Date();
|
|
468
|
-
const dateNumber = Time.getDateNumber(
|
|
545
|
+
const dateNumber = Time.getDateNumber(
|
|
546
|
+
date,
|
|
547
|
+
date.getTimezoneOffset(),
|
|
548
|
+
);
|
|
469
549
|
if (dateNumber !== this.cachedDate) {
|
|
470
550
|
this.cachedData = this.download();
|
|
471
551
|
this.cachedDate = dateNumber;
|
|
@@ -511,7 +591,9 @@ namespace Analytics {
|
|
|
511
591
|
guildIncrement: number;
|
|
512
592
|
dauHistory: number[];
|
|
513
593
|
commandRate: Dict<number>;
|
|
514
|
-
messageByBot: Dict<
|
|
594
|
+
messageByBot: Dict<
|
|
595
|
+
Dict<MessageStats & Partial<Universal.User>>
|
|
596
|
+
>;
|
|
515
597
|
messageByDate: MessageStats[];
|
|
516
598
|
messageByHour: MessageStats[];
|
|
517
599
|
}
|