@koishi-ce/plugin-status 1.0.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.css +2 -0
- package/dist/index.js +4 -0
- package/lib/assets/zh-CN-BHjPKRmW.yml +15 -0
- package/lib/index.d.ts +115 -0
- package/lib/index.mjs +260 -0
- package/locales/de-DE.yml +15 -0
- package/locales/en-US.yml +15 -0
- package/locales/fr-FR.yml +15 -0
- package/locales/ja-JP.yml +15 -0
- package/locales/ru-RU.yml +15 -0
- package/locales/zh-CN.yml +15 -0
- package/locales/zh-TW.yml +15 -0
- package/package.json +72 -0
- package/src/envinfo.ts +87 -0
- package/src/index.ts +68 -0
- package/src/profile.ts +257 -0
- package/src/which-pm-runs.d.ts +12 -0
package/dist/index.css
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
.status-light[data-v-ea7f6fce]{transition:var(--color-transition);border-radius:.625rem;width:.625rem;height:.625rem}.status-light.online[data-v-ea7f6fce]{background-color:var(--k-color-success)}.status-light.connect[data-v-ea7f6fce],.status-light.reconnect[data-v-ea7f6fce]{background-color:var(--k-color-warning)}.status-light.error[data-v-ea7f6fce]{background-color:var(--k-color-danger)!important}.status-light.offline[data-v-ea7f6fce]{background-color:var(--k-color-disabled)}.status-light+.status-light[data-v-ea7f6fce]{margin-left:4px}.bot-view[data-v-1cbf178f]{width:15rem;padding:.75rem 1rem;font-size:14px;transition:all .3s;display:flex}.bot-view+.bot-view[data-v-1cbf178f]{border-top:1px solid var(--k-color-divider)}.bot-view.active>div.avatar[data-v-1cbf178f]{border-color:var(--active)}.bot-view>div.avatar[data-v-1cbf178f]{box-sizing:content-box;border:1px solid var(--k-color-divider);background-repeat:no-repeat;background-size:100%;border-radius:100%;flex-shrink:0;width:4rem;height:4rem;transition:all .1s;position:relative}.bot-view>div.avatar .status-light[data-v-1cbf178f]{border:1px solid var(--k-color-divider);width:.875rem;height:.875rem;position:absolute;bottom:-1px;right:-1px}.bot-view>div.info[data-v-1cbf178f]{flex-direction:column;flex-grow:1;justify-content:space-around;margin-left:1.25rem;display:flex;overflow:hidden}.bot-view>div.info .k-icon[data-v-1cbf178f]{text-align:center;vertical-align:-2px;width:20px;margin-right:6px}.bot-view.has-link[data-v-1cbf178f]{cursor:pointer}.bot-view.has-link[data-v-1cbf178f]:hover{background-color:var(--bg1)}.k-status .k-icon[data-v-e9800c9a]{margin-right:4px}.k-status *+.k-icon[data-v-e9800c9a]{margin-left:6px}.k-status .count[data-v-e9800c9a]{letter-spacing:1px;margin:0 4px}.bots-container .bot-view[data-v-19020f78]{background-color:var(--bg0);border-radius:.5rem}p[data-v-184a36f6]{margin:8px;padding:0 6px;font-size:12px;line-height:1}.info[data-v-184a36f6]{padding-left:20px}.load-bar[data-v-c8d81760]{-webkit-user-select:none;user-select:none;align-items:center;margin:.5rem 0;font-size:.9em;display:flex}.load-bar .title[data-v-c8d81760]{min-width:3em}.load-bar .body[data-v-c8d81760]{background-color:var(--k-c-divisor);width:10rem;height:.8rem;transition:var(--color-transition);color:var(--fg1);border-radius:1rem;font-size:10px;display:inline;position:relative;overflow:hidden}.load-bar .bar[data-v-c8d81760]{float:left;white-space:pre;justify-content:center;align-items:center;height:100%;display:flex;position:relative}.load-bar .used[data-v-c8d81760]{background-color:var(--primary);color:#fff;transition:color .3s,background-color .3s}.load-bar .used[data-v-c8d81760]:hover{background-color:var(--primary-tint,var(--primary))}.load-bar .app[data-v-c8d81760]{background-color:var(--k-color-warning);transition:color .3s,background-color .3s}.load-bar .app[data-v-c8d81760]:hover{background-color:var(--k-color-warning-tint,var(--k-color-warning))}.load[data-v-8c7391c4]{padding:.25rem 1rem}.absolute{position:absolute}.relative{position:relative}.inline{display:inline}.hidden{display:none}.h2{height:.5rem}.flex{display:flex}.flex-shrink{flex-shrink:1}.flex-grow{flex-grow:1}.flex-wrap{flex-wrap:wrap}.gap-4{gap:1rem}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.border{border-width:1px}.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
|
+
/*$vite$:1*/
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{Schema as e,Universal as t,capitalize as n,icons as r,message as i,router as a,store as o,useConfig as ee,withProxy as s}from"@koishi-ce/client";import{Fragment as c,computed as l,createBlock as u,createCommentVNode as d,createElementBlock as f,createElementVNode as p,createTextVNode as m,createVNode as h,defineComponent as g,inject as _,normalizeClass as v,normalizeStyle as y,openBlock as b,renderList as x,resolveComponent as S,toDisplayString as C,unref as w,withCtx as T}from"vue";var te=g({__name:`analytics`,setup(e){let t=_(`component:analytic-number`),n=l(()=>Object.values(o.status.bots).reduce((e,t)=>e+t.messageReceived,0)/60),r=l(()=>Object.values(o.analytics.messageByDate).slice(-7).reduce((e,t)=>e+t.receive,0)/7/24/60/60);return(e,i)=>(b(),u(w(t),{icon:`analytic:pulse`,title:`当前 QPS`},{default:T(()=>[m(C(+n.value.toFixed(2)),1)]),"footer-left":T(()=>[...i[0]||=[m(`近期 QPS`,-1)]]),"footer-right":T(()=>[m(C(+r.value.toFixed(2)),1)]),_:1}))}}),E=(e,t)=>{let n=e.__vccOpts||e;for(let[e,r]of t)n[e]=r;return n},D={},O={class:`status-light`};function ne(e,t){return b(),f(`span`,O)}var k=E(D,[[`render`,ne],[`__scopeId`,`data-v-ea7f6fce`]]),A={OFFLINE:0,ONLINE:1,CONNECT:2,DISCONNECT:3,RECONNECT:4};function j(e){switch(e){case A.OFFLINE:return`offline`;case A.ONLINE:return`online`;case A.CONNECT:return`connect`;case A.DISCONNECT:return`disconnect`;case A.RECONNECT:return`reconnect`}}var M={class:`bot-view`},N={class:`info`},P=[`title`],F=[`title`],I={class:`truncate cur-frequency`},L={style:{"margin-right":`8px`}},R=E(g({__name:`preview`,props:{data:{}},setup(e){let n={[t.Status.ONLINE]:`运行中`,[t.Status.OFFLINE]:`离线`,[t.Status.CONNECT]:`正在连接`,[t.Status.RECONNECT]:`正在重连`,[t.Status.DISCONNECT]:`正在断开`};return(t,r)=>{let i=S(`el-tooltip`),a=S(`k-icon`);return b(),f(`section`,M,[p(`div`,{class:`avatar`,style:y({backgroundImage:`url(${w(s)(e.data.user.avatar)})`}),onClick:r[0]||=e=>t.$emit(`avatar-click`)},[h(i,{content:n[e.data.status],placement:`right`},{default:T(()=>[h(k,{class:v(w(j)(e.data.status))},null,8,[`class`])]),_:1},8,[`content`])],4),p(`div`,N,[p(`div`,{class:`truncate`,title:e.data.user.name},[h(a,{name:`robot`}),m(C(e.data.user.name),1)],8,P),p(`div`,{class:`truncate`,title:e.data.platform},[h(a,{name:`platform`}),m(C(e.data.platform),1)],8,F),p(`div`,I,[p(`span`,L,[h(a,{name:`arrow-up`}),p(`span`,null,C(e.data.messageSent)+`/min`,1)]),p(`span`,null,[h(a,{name:`arrow-down`}),p(`span`,null,C(e.data.messageReceived)+`/min`,1)])])])])}}}),[[`__scopeId`,`data-v-1cbf178f`]]),z={key:0,class:`el-popper__empty`},B={class:`count`},V=E(g({__name:`index`,setup(e){let t=ee(),n=l(()=>{let e={};for(let t of Object.values(o.status.bots)){let n=j(t.status);e[n]=(e[n]||0)+1}return Object.fromEntries(Object.entries(e).sort((e,t)=>e[0].localeCompare(t[0])))}),r=l(()=>Object.values(o.status.bots).reduce((e,t)=>e+t.messageSent,0)),i=l(()=>Object.values(o.status.bots).reduce((e,t)=>e+t.messageReceived,0));return(e,ee)=>{let s=S(`k-icon`),l=S(`k-status`);return w(o).status?(b(),u(l,{key:0},{tooltip:T(()=>[Object.values(w(o).status.bots).length?d(`v-if`,!0):(b(),f(`span`,z)),(b(!0),f(c,null,x(w(o).status.bots,(e,t)=>(b(),u(R,{key:t,data:e,class:v({"has-link":e.paths?.length}),onClick:t=>w(a).push(`/plugins/`+e.paths[0].replace(/\./,`/`))},null,8,[`data`,`class`,`onClick`]))),128))]),default:T(()=>[(b(!0),f(c,null,x(n.value,(e,n)=>(b(),f(c,{key:n},[e>(w(t).mergeThreshold??10)?(b(),f(c,{key:0},[h(k,{class:v(n)},null,8,[`class`]),p(`span`,B,`×`+C(e),1)],64)):(b(!0),f(c,{key:1},x(Array(e),(e,t)=>(b(),u(k,{key:t,class:v(n)},null,8,[`class`]))),128))],64))),128)),h(s,{name:`arrow-up`}),p(`span`,null,C(r.value)+`/min`,1),h(s,{name:`arrow-down`}),p(`span`,null,C(i.value)+`/min`,1)]),_:1})):d(`v-if`,!0)}}}),[[`__scopeId`,`data-v-e9800c9a`]]),H=e=>{e.slot({type:`status-right`,component:V})},U={class:`bots-container flex flex-wrap gap-4`},W=E(g({__name:`config`,setup(e){let t=_(`manager.settings.current`),n=l(()=>Object.values(o.status?.bots||{}).filter(e=>e.paths?.includes(t.value.path)));return(e,t)=>n.value?.length?(b(),f(c,{key:0},[t[0]||=p(`h2`,{class:`k-schema-header`},` 机器人 `,-1),p(`div`,U,[(b(!0),f(c,null,x(n.value,(e,t)=>(b(),u(R,{key:t,data:e},null,8,[`data`]))),128))])],64)):d(`v-if`,!0)}}),[[`__scopeId`,`data-v-19020f78`]]);async function G(e){try{return navigator.clipboard.writeText(e)}catch{let t=document.createElement(`textarea`),n=document.activeElement;t.value=e,t.setAttribute(`readonly`,``),t.style.contain=`strict`,t.style.position=`absolute`,t.style.left=`-9999px`,t.style.fontSize=`12pt`;let r=document.getSelection(),i=r?r.rangeCount>0&&r.getRangeAt(0):null;document.body.appendChild(t),t.select(),t.selectionStart=0,t.selectionEnd=e.length,document.execCommand(`copy`),document.body.removeChild(t),i&&r&&(r.removeAllRanges(),r.addRange(i)),n&&n.focus()}}var K={class:`title`},q=E(g({__name:`envinfo`,setup(e){async function t(){await G(Object.entries(o.envinfo).map(([e,t])=>`${n(e)}:\n`+Object.entries(t).map(([e,t])=>` ${e}: ${t}`).join(`
|
|
2
|
+
`)).join(`
|
|
3
|
+
|
|
4
|
+
`)),i.success(`已复制环境信息!`)}return(e,r)=>{let i=S(`k-status`);return w(o).envinfo?(b(),u(i,{key:0,class:`version`,onClick:t},{tooltip:T(()=>[(b(!0),f(c,null,x(w(o).envinfo,(e,t)=>(b(),f(`div`,{class:`section`,key:t},[p(`p`,K,C(w(n)(t))+`:`,1),(b(!0),f(c,null,x(e,(e,t)=>(b(),f(`p`,{class:`info`,key:t},C(t)+`: `+C(e),1))),128))]))),128))]),default:T(()=>[m(` Koishi v`+C(w(o).envinfo.koishi.Core)+` `,1)]),_:1})):d(`v-if`,!0)}}}),[[`__scopeId`,`data-v-184a36f6`]]),J={class:`load-bar`},Y={class:`title`},X={class:`body`},Z=E(g({__name:`load-bar`,props:{rate:{},title:{}},setup(e){let t=e;function n(e,t=1){return`${+(e*100).toFixed(t)}%`}let r=[`used`,`app`,`free`],i=l(()=>[t.rate[1]-t.rate[0],t.rate[0],1-t.rate[1]]),a=l(()=>i.value.findIndex(e=>e>=.5)),o=l(()=>`${n(t.rate[0],1)} / ${n(t.rate[1],1)}`);return(t,ee)=>(b(),f(`div`,J,[p(`span`,Y,C(e.title),1),p(`span`,X,[(b(),f(c,null,x(r,(e,t)=>p(`span`,{key:e,class:v([e,`bar`]),style:y({width:n(i.value[t])})},[t===a.value?(b(),f(c,{key:0},[m(C(o.value),1)],64)):a.value===-1&&i.value[t]>=.2?(b(),f(c,{key:1},[m(C(n(i.value[t])),1)],64)):d(`v-if`,!0)],6)),64))])]))}}),[[`__scopeId`,`data-v-c8d81760`]]);function Q(e,t=3){return`${(e*100).toFixed(t)}%`}var re={class:`load`},ie=E(g({__name:`index`,setup(e){return(e,t)=>{let n=S(`k-status`);return w(o).status?(b(),u(n,{key:0},{tooltip:T(()=>[p(`div`,re,[h(Z,{title:`CPU`,rate:w(o).status.cpu},null,8,[`rate`]),h(Z,{title:`内存`,rate:w(o).status.memory},null,8,[`rate`])])]),default:T(()=>[m(` CPU: `+C(w(Q)(w(o).status.cpu[1],0))+` 内存: `+C(w(Q)(w(o).status.memory[1],0)),1)]),_:1})):d(`v-if`,!0)}}}),[[`__scopeId`,`data-v-8c7391c4`]]),ae=e=>{e.slot({type:`status-right`,component:ie})},oe={},se={class:`k-icon`,xmlns:`http://www.w3.org/2000/svg`,viewBox:`0 0 384 512`};function ce(e,t){return b(),f(`svg`,se,[...t[0]||=[p(`path`,{fill:`currentColor`,d:`M377.4 296.6l-168 176C204.8 477.3 198.6 480 192 480s-12.84-2.688-17.38-7.438l-168-176C-2.5 286.1-2.156 271.8 7.438 262.6c9.5-9.156 24.75-8.812 33.94 .8125L168 396.1V56.02c0-13.25 10.75-24.01 23.1-24.01S216 42.77 216 56.02v340.1l126.6-132.7c9.156-9.625 24.41-9.969 33.94-.8125C386.2 271.8 386.5 286.1 377.4 296.6z`},null,-1)]])}var le=E(oe,[[`render`,ce]]),ue={},de={class:`k-icon`,xmlns:`http://www.w3.org/2000/svg`,viewBox:`0 0 384 512`};function fe(e,t){return b(),f(`svg`,de,[...t[0]||=[p(`path`,{fill:`currentColor`,d:`M6.625 215.5l168-176C179.2 34.7 185.4 32.02 192 32.02s12.84 2.688 17.38 7.438l168 176c9.125 9.594 8.781 24.78-.8125 33.94c-9.5 9.156-24.75 8.812-33.94-.8125L216 115.9V456c0 13.25-10.75 23.1-23.1 23.1S168 469.3 168 456V115.9l-126.6 132.7C32.22 258.2 16.97 258.5 7.438 249.4C-2.156 240.2-2.5 225 6.625 215.5z`},null,-1)]])}var pe=E(ue,[[`render`,fe]]),me={},he={class:`k-icon`,xmlns:`http://www.w3.org/2000/svg`,viewBox:`0 0 512 512`};function ge(e,t){return b(),f(`svg`,he,[...t[0]||=[p(`path`,{fill:`currentColor`,d:`M12.41 148.02l232.94 105.67c6.8 3.09 14.49 3.09 21.29 0l232.94-105.67c16.55-7.51 16.55-32.52 0-40.03L266.65 2.31a25.607 25.607 0 0 0-21.29 0L12.41 107.98c-16.55 7.51-16.55 32.53 0 40.04zm487.18 88.28l-58.09-26.33-161.64 73.27c-7.56 3.43-15.59 5.17-23.86 5.17s-16.29-1.74-23.86-5.17L70.51 209.97l-58.1 26.33c-16.55 7.5-16.55 32.5 0 40l232.94 105.59c6.8 3.08 14.49 3.08 21.29 0L499.59 276.3c16.55-7.5 16.55-32.5 0-40zm0 127.8l-57.87-26.23-161.86 73.37c-7.56 3.43-15.59 5.17-23.86 5.17s-16.29-1.74-23.86-5.17L70.29 337.87 12.41 364.1c-16.55 7.5-16.55 32.5 0 40l232.94 105.59c6.8 3.08 14.49 3.08 21.29 0L499.59 404.1c16.55-7.5 16.55-32.5 0-40z`},null,-1)]])}var _e=E(me,[[`render`,ge]]),ve={},ye={class:`k-icon`,xmlns:`http://www.w3.org/2000/svg`,viewBox:`0 0 640 512`};function be(e,t){return b(),f(`svg`,ye,[...t[0]||=[p(`path`,{fill:`currentColor`,d:`M640 240C640 248.8 632.8 256 624 256h-133.1l-60 150c-2.625 6.375-8.375 10.25-16 9.1C408 415.5 402.1 410.6 400.5 403.9l-78.75-315l-82 410.3C238.3 506.5 231.9 511.8 224.4 512H224c-7.25 0-13.75-5-15.5-12.12L147.5 256H16C7.201 256 0 248.8 0 240s7.201-15.1 16-15.1H160c7.375 0 13.75 4.993 15.5 12.12l46.75 187l82-410.3C305.8 5.5 312 .25 319.5 0c8.125 .125 14.25 4.875 16 12.12l84 336l45.63-114.1C467.5 228 473.5 224 480 224h144C632.8 224 640 231.2 640 240z`},null,-1)]])}var $=E(ve,[[`render`,be]]),xe={},Se={class:`k-icon`,xmlns:`http://www.w3.org/2000/svg`,viewBox:`0 0 640 512`};function Ce(e,t){return b(),f(`svg`,Se,[...t[0]||=[p(`path`,{fill:`currentColor`,d:`M32,224H64V416H32A31.96166,31.96166,0,0,1,0,384V256A31.96166,31.96166,0,0,1,32,224Zm512-48V448a64.06328,64.06328,0,0,1-64,64H160a64.06328,64.06328,0,0,1-64-64V176a79.974,79.974,0,0,1,80-80H288V32a32,32,0,0,1,64,0V96H464A79.974,79.974,0,0,1,544,176ZM264,256a40,40,0,1,0-40,40A39.997,39.997,0,0,0,264,256Zm-8,128H192v32h64Zm96,0H288v32h64ZM456,256a40,40,0,1,0-40,40A39.997,39.997,0,0,0,456,256Zm-8,128H384v32h64ZM640,256V384a31.96166,31.96166,0,0,1-32,32H576V224h32A31.96166,31.96166,0,0,1,640,256Z`},null,-1)]])}var we=E(xe,[[`render`,Ce]]);r.register(`arrow-up`,pe),r.register(`arrow-down`,le),r.register(`platform`,_e),r.register(`analytic:pulse`,$),r.register(`robot`,we);var Te=t=>{t.plugin(H),t.plugin(ae),t.slot({type:`status-left`,component:q}),t.slot({type:`analytic-number`,component:te}),t.slot({type:`plugin-details`,component:W,order:-500}),t.settings({id:`status`,schema:e.object({mergeThreshold:e.number().default(10).description(`当机器人的数量超过这个值时将合并显示状态指示灯。`)}).description(`机器人设置`)})};export{Te as default};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
commands:
|
|
2
|
+
status:
|
|
3
|
+
description: 查看运行状态
|
|
4
|
+
messages:
|
|
5
|
+
status:
|
|
6
|
+
1: 运行中
|
|
7
|
+
0: 离线
|
|
8
|
+
2: 正在连接
|
|
9
|
+
4: 正在重连
|
|
10
|
+
3: 正在断开
|
|
11
|
+
bot: <p>[{platform}] {user.name}:<i18n path={"commands.status.messages.status." + status}/></p>
|
|
12
|
+
epilog: |-
|
|
13
|
+
==========
|
|
14
|
+
CPU 使用率:{ (cpu[0] * 100).toFixed() }% / { (cpu[1] * 100).toFixed() }%
|
|
15
|
+
内存使用率:{ (memory[0] * 100).toFixed() }% / { (memory[1] * 100).toFixed() }%
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { Bot, Context, Dict, Schema, Universal } from "@koishi-ce/koishi";
|
|
2
|
+
import { DataService } from "@koishi-ce/console";
|
|
3
|
+
//#region src/envinfo.d.ts
|
|
4
|
+
declare class EnvInfoProvider extends DataService<Dict<Dict<string>>> {
|
|
5
|
+
/** 采集任务的缓存:get() 首次调用时创建,之后始终复用同一 Promise。 */
|
|
6
|
+
private task;
|
|
7
|
+
config: EnvInfoProvider.Config;
|
|
8
|
+
constructor(ctx: Context, config: EnvInfoProvider.Config);
|
|
9
|
+
/** 执行一次实际采集:系统信息、运行时版本、Koishi 生态版本。 */
|
|
10
|
+
_get(): Promise<Dict<Dict<string>>>;
|
|
11
|
+
/** 首次调用时发起采集并缓存 Promise,后续调用直接返回同一结果。 */
|
|
12
|
+
get(): Promise<Dict<Dict<string>>>;
|
|
13
|
+
static Config: Schema<EnvInfoProvider.Config>;
|
|
14
|
+
}
|
|
15
|
+
declare namespace EnvInfoProvider {
|
|
16
|
+
type Config = Record<never, never>;
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/profile.d.ts
|
|
20
|
+
declare module "@koishi-ce/koishi" {
|
|
21
|
+
interface Bot {
|
|
22
|
+
_messageSent: TickCounter;
|
|
23
|
+
_messageReceived: TickCounter;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 滑动窗口计数器:以 60 个槽位统计最近一分钟内的事件次数。
|
|
28
|
+
*
|
|
29
|
+
* 每秒执行一次 tick,队头插入 0 并丢弃队尾,窗口随时间向前滑动;
|
|
30
|
+
* add() 累加到当前秒所在的队头槽位,get() 求整个窗口的和。
|
|
31
|
+
* 每个 bot 挂载两个实例(_messageSent / _messageReceived)分别统计收发消息。
|
|
32
|
+
*/
|
|
33
|
+
declare class TickCounter {
|
|
34
|
+
/** 停止内部定时器(随上下文销毁由调用方触发)。 */
|
|
35
|
+
stop: () => void;
|
|
36
|
+
/** 60 个槽位的环形数据,data[0] 恒为当前秒的计数。 */
|
|
37
|
+
private data;
|
|
38
|
+
private tick;
|
|
39
|
+
constructor(ctx: Context);
|
|
40
|
+
/** 在当前秒的槽位上累加计数。 */
|
|
41
|
+
add(value?: number): void;
|
|
42
|
+
/** 返回最近一分钟内的总计数。 */
|
|
43
|
+
get(): any;
|
|
44
|
+
/** 为指定 bot 初始化收/发两个计数器。 */
|
|
45
|
+
static initialize(bot: Bot, ctx: Context): void;
|
|
46
|
+
}
|
|
47
|
+
/** 负载率二元组:[app, total],app 为本进程占比,total 为整机占比,取值 0~1。 */
|
|
48
|
+
type LoadRate = [app: number, total: number];
|
|
49
|
+
/**
|
|
50
|
+
* 性能数据服务:以 DataService 形式向前端广播 Payload,
|
|
51
|
+
* 前端经 store.status 读取(键名即服务名 "status")。
|
|
52
|
+
*/
|
|
53
|
+
declare class ProfileProvider extends DataService<ProfileProvider.Payload> {
|
|
54
|
+
cached: ProfileProvider.Payload | undefined;
|
|
55
|
+
config: ProfileProvider.Config;
|
|
56
|
+
constructor(ctx: Context, config: ProfileProvider.Config);
|
|
57
|
+
/**
|
|
58
|
+
* 采集一帧性能数据:CPU 占用率、内存占用率与各机器人收发速率。
|
|
59
|
+
*
|
|
60
|
+
* @param forced 为 true 时跳过缓存强制重算(refresh 链路会传入)
|
|
61
|
+
*/
|
|
62
|
+
get(forced?: boolean): Promise<ProfileProvider.Payload>;
|
|
63
|
+
static Config: Schema<ProfileProvider.Config>;
|
|
64
|
+
}
|
|
65
|
+
declare namespace ProfileProvider {
|
|
66
|
+
interface Config {
|
|
67
|
+
/** 采集与推送性能数据的时间间隔(毫秒),默认 5 秒。 */
|
|
68
|
+
tickInterval?: number;
|
|
69
|
+
}
|
|
70
|
+
/** 推送给前端的单个机器人数据:登录信息 + 错误消息 + 配置路径 + 收发速率。 */
|
|
71
|
+
interface BotData extends Universal.Login {
|
|
72
|
+
error?: string;
|
|
73
|
+
paths?: string[];
|
|
74
|
+
messageSent: number;
|
|
75
|
+
messageReceived: number;
|
|
76
|
+
}
|
|
77
|
+
/** 每次采集推送给前端的完整负载:内存 / CPU 负载率与机器人数据字典。 */
|
|
78
|
+
interface Payload {
|
|
79
|
+
memory: LoadRate;
|
|
80
|
+
cpu: LoadRate;
|
|
81
|
+
bots: Dict<BotData>;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
//#endregion
|
|
85
|
+
//#region src/index.d.ts
|
|
86
|
+
/** 频道活跃度记录:键为小时数(0~23),值为该小时内触发频道的次数。 */
|
|
87
|
+
type Activity = Record<number, number>;
|
|
88
|
+
declare module "@koishi-ce/koishi" {
|
|
89
|
+
interface Channel {
|
|
90
|
+
name: string;
|
|
91
|
+
activity: Activity;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
declare module "@koishi-ce/console" {
|
|
95
|
+
namespace Console {
|
|
96
|
+
interface Services {
|
|
97
|
+
envinfo: EnvInfoProvider;
|
|
98
|
+
status: ProfileProvider;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
declare const name = "status";
|
|
103
|
+
declare const inject: string[];
|
|
104
|
+
interface Config extends ProfileProvider.Config, EnvInfoProvider.Config {}
|
|
105
|
+
declare const Config: Schema<Config>;
|
|
106
|
+
/**
|
|
107
|
+
* 插件入口:注册控制台前端资源(dev 指向 client 源码,prod 指向构建产物),
|
|
108
|
+
* 再将环境信息与性能采集两个服务作为子插件挂载,共享同一份配置。
|
|
109
|
+
*
|
|
110
|
+
* @param ctx 应用上下文
|
|
111
|
+
* @param config 插件配置(两个子服务配置的交集合并)
|
|
112
|
+
*/
|
|
113
|
+
declare function apply(ctx: Context, config: Config): void;
|
|
114
|
+
//#endregion
|
|
115
|
+
export { Activity, Config, EnvInfoProvider, LoadRate, ProfileProvider, apply, inject, name };
|
package/lib/index.mjs
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { Schema, Time, version } from "@koishi-ce/koishi";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { DataService } from "@koishi-ce/console";
|
|
6
|
+
import { helpers } from "envinfo";
|
|
7
|
+
import { cpus, freemem, totalmem } from "node:os";
|
|
8
|
+
import zhCN from "./assets/zh-CN-BHjPKRmW.yml";
|
|
9
|
+
//#region \0rolldown/runtime.js
|
|
10
|
+
var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/envinfo.ts
|
|
13
|
+
/**
|
|
14
|
+
* 运行环境信息服务(服务名 "envinfo")。
|
|
15
|
+
*
|
|
16
|
+
* 一次性采集系统与运行时版本信息,整理为 { system, binaries, koishi }
|
|
17
|
+
* 三组分节字符串字典,供控制台前端在状态栏展示与一键复制。
|
|
18
|
+
* 环境信息在进程生命周期内不会变化,因此采集结果以 Promise 形式永久缓存。
|
|
19
|
+
*/
|
|
20
|
+
const whichPMRuns = createRequire(import.meta.url)("which-pm-runs");
|
|
21
|
+
var EnvInfoProvider = class extends DataService {
|
|
22
|
+
/** 采集任务的缓存:get() 首次调用时创建,之后始终复用同一 Promise。 */
|
|
23
|
+
task;
|
|
24
|
+
config;
|
|
25
|
+
constructor(ctx, config) {
|
|
26
|
+
super(ctx, "envinfo");
|
|
27
|
+
this.config = config;
|
|
28
|
+
}
|
|
29
|
+
/** 执行一次实际采集:系统信息、运行时版本、Koishi 生态版本。 */
|
|
30
|
+
async _get() {
|
|
31
|
+
const [[, Os = ""], [, Cpu = ""]] = await Promise.all([helpers.getOSInfo(), helpers.getCPUInfo()]);
|
|
32
|
+
const agent = whichPMRuns();
|
|
33
|
+
const system = {
|
|
34
|
+
OS: Os,
|
|
35
|
+
CPU: Cpu
|
|
36
|
+
};
|
|
37
|
+
const binaries = { Node: process.versions.node };
|
|
38
|
+
if (agent) {
|
|
39
|
+
if (agent.name === "yarn") agent.name = "Yarn";
|
|
40
|
+
binaries[agent.name] = agent.version;
|
|
41
|
+
}
|
|
42
|
+
const metapath = __require.resolve("@koishi-ce/console/package.json");
|
|
43
|
+
const meta = await readFile(metapath, "utf8").then(JSON.parse);
|
|
44
|
+
const koishi = {
|
|
45
|
+
Core: version,
|
|
46
|
+
Console: meta.version
|
|
47
|
+
};
|
|
48
|
+
if (process.env["KOISHI_AGENT"]) {
|
|
49
|
+
const [name, agentVersion] = process.env["KOISHI_AGENT"].split("/");
|
|
50
|
+
if (name && agentVersion) koishi[name] = agentVersion;
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
system,
|
|
54
|
+
binaries,
|
|
55
|
+
koishi
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** 首次调用时发起采集并缓存 Promise,后续调用直接返回同一结果。 */
|
|
59
|
+
async get() {
|
|
60
|
+
if (!this.task) this.task = this._get();
|
|
61
|
+
return this.task;
|
|
62
|
+
}
|
|
63
|
+
static Config = Schema.object({});
|
|
64
|
+
};
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/profile.ts
|
|
67
|
+
/**
|
|
68
|
+
* 性能数据采集服务(服务名 "status")。
|
|
69
|
+
*
|
|
70
|
+
* 周期性采集三类指标并推送给控制台前端:
|
|
71
|
+
* - CPU:进程占用率与整机占用率(基于两次采样差值计算);
|
|
72
|
+
* - 内存:进程 RSS 占比与整机已用占比;
|
|
73
|
+
* - 机器人:每个 bot 最近一分钟的消息收发量(TickCounter 滑动窗口)。
|
|
74
|
+
*
|
|
75
|
+
* 另提供 `status` 指令,在聊天环境内以文本形式输出各机器人状态摘要。
|
|
76
|
+
*/
|
|
77
|
+
/**
|
|
78
|
+
* 滑动窗口计数器:以 60 个槽位统计最近一分钟内的事件次数。
|
|
79
|
+
*
|
|
80
|
+
* 每秒执行一次 tick,队头插入 0 并丢弃队尾,窗口随时间向前滑动;
|
|
81
|
+
* add() 累加到当前秒所在的队头槽位,get() 求整个窗口的和。
|
|
82
|
+
* 每个 bot 挂载两个实例(_messageSent / _messageReceived)分别统计收发消息。
|
|
83
|
+
*/
|
|
84
|
+
var TickCounter = class TickCounter {
|
|
85
|
+
/** 停止内部定时器(随上下文销毁由调用方触发)。 */
|
|
86
|
+
stop;
|
|
87
|
+
/** 60 个槽位的环形数据,data[0] 恒为当前秒的计数。 */
|
|
88
|
+
data = new Array(60).fill(0);
|
|
89
|
+
tick = () => {
|
|
90
|
+
this.data.unshift(0);
|
|
91
|
+
this.data.splice(-1, 1);
|
|
92
|
+
};
|
|
93
|
+
constructor(ctx) {
|
|
94
|
+
this.stop = ctx.setInterval(() => this.tick(), Time.second);
|
|
95
|
+
}
|
|
96
|
+
/** 在当前秒的槽位上累加计数。 */
|
|
97
|
+
add(value = 1) {
|
|
98
|
+
this.data[0] += value;
|
|
99
|
+
}
|
|
100
|
+
/** 返回最近一分钟内的总计数。 */
|
|
101
|
+
get() {
|
|
102
|
+
return this.data.reduce((prev, curr) => prev + curr, 0);
|
|
103
|
+
}
|
|
104
|
+
/** 为指定 bot 初始化收/发两个计数器。 */
|
|
105
|
+
static initialize(bot, ctx) {
|
|
106
|
+
bot._messageSent = new TickCounter(ctx);
|
|
107
|
+
bot._messageReceived = new TickCounter(ctx);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
let usage = getCpuUsage();
|
|
111
|
+
let appRate;
|
|
112
|
+
let usedRate;
|
|
113
|
+
/** 采集内存负载率:[进程 RSS / 总内存, 1 - 空闲内存 / 总内存]。 */
|
|
114
|
+
async function memoryRate() {
|
|
115
|
+
const total = totalmem();
|
|
116
|
+
return [process.memoryUsage().rss / total, 1 - freemem() / total];
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* 读取一次 CPU 累计时间快照。
|
|
120
|
+
*
|
|
121
|
+
* - app:进程用户态 CPU 时间(cpuUsage().user,毫秒);
|
|
122
|
+
* - used:所有核心的 累计时间 - 空闲时间(毫秒);
|
|
123
|
+
* - total:所有核心的累计时间总和(毫秒)。
|
|
124
|
+
* 单次快照只是累计值,需经 updateCpuUsage() 做差才能得到占用率。
|
|
125
|
+
*/
|
|
126
|
+
function getCpuUsage() {
|
|
127
|
+
let totalIdle = 0, totalTick = 0;
|
|
128
|
+
const cpuInfo = cpus();
|
|
129
|
+
const usage = process.cpuUsage().user;
|
|
130
|
+
for (const cpu of cpuInfo) {
|
|
131
|
+
for (const time of Object.values(cpu.times)) totalTick += time;
|
|
132
|
+
totalIdle += cpu.times.idle;
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
app: usage / 1e3,
|
|
136
|
+
used: totalTick - totalIdle,
|
|
137
|
+
total: totalTick
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* 用新快照与上次快照的差值计算 CPU 占用率并缓存到模块级变量:
|
|
142
|
+
* appRate = 本进程增量时间 / 全机总增量时间,usedRate 同理(整机口径)。
|
|
143
|
+
*/
|
|
144
|
+
function updateCpuUsage() {
|
|
145
|
+
const newUsage = getCpuUsage();
|
|
146
|
+
const totalDifference = newUsage.total - usage.total;
|
|
147
|
+
appRate = (newUsage.app - usage.app) / totalDifference;
|
|
148
|
+
usedRate = (newUsage.used - usage.used) / totalDifference;
|
|
149
|
+
usage = newUsage;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* 性能数据服务:以 DataService 形式向前端广播 Payload,
|
|
153
|
+
* 前端经 store.status 读取(键名即服务名 "status")。
|
|
154
|
+
*/
|
|
155
|
+
var ProfileProvider = class extends DataService {
|
|
156
|
+
config;
|
|
157
|
+
constructor(ctx, config) {
|
|
158
|
+
super(ctx, "status");
|
|
159
|
+
this.config = config;
|
|
160
|
+
ctx.i18n.define("zh-CN", zhCN);
|
|
161
|
+
const { tickInterval } = config;
|
|
162
|
+
ctx.on("ready", () => {
|
|
163
|
+
ctx.setInterval(() => {
|
|
164
|
+
updateCpuUsage();
|
|
165
|
+
this.refresh();
|
|
166
|
+
}, tickInterval ?? Time.second * 5);
|
|
167
|
+
});
|
|
168
|
+
ctx.any().before("send", (session) => {
|
|
169
|
+
session.bot._messageSent?.add(1);
|
|
170
|
+
});
|
|
171
|
+
ctx.any().on("message", (session) => {
|
|
172
|
+
session.bot._messageReceived?.add(1);
|
|
173
|
+
});
|
|
174
|
+
ctx.bots.forEach((bot) => {
|
|
175
|
+
TickCounter.initialize(bot, ctx);
|
|
176
|
+
});
|
|
177
|
+
const update = ctx.debounce(() => this.refresh(), 0);
|
|
178
|
+
ctx.on("login-added", ({ bot }) => {
|
|
179
|
+
TickCounter.initialize(bot, ctx);
|
|
180
|
+
update();
|
|
181
|
+
});
|
|
182
|
+
ctx.on("login-removed", ({ bot }) => {
|
|
183
|
+
bot._messageSent.stop();
|
|
184
|
+
bot._messageReceived.stop();
|
|
185
|
+
update();
|
|
186
|
+
});
|
|
187
|
+
ctx.on("login-updated", () => {
|
|
188
|
+
update();
|
|
189
|
+
});
|
|
190
|
+
ctx.command("status").action(async ({ session }) => {
|
|
191
|
+
if (!session) return;
|
|
192
|
+
const data = await this.get();
|
|
193
|
+
const output = Object.values(data.bots).map((bot) => {
|
|
194
|
+
return session.text(".bot", bot);
|
|
195
|
+
});
|
|
196
|
+
output.push(session.text(".epilog", data));
|
|
197
|
+
return output.join("\n");
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* 采集一帧性能数据:CPU 占用率、内存占用率与各机器人收发速率。
|
|
202
|
+
*
|
|
203
|
+
* @param forced 为 true 时跳过缓存强制重算(refresh 链路会传入)
|
|
204
|
+
*/
|
|
205
|
+
async get(forced = false) {
|
|
206
|
+
if (this.cached && !forced) return this.cached;
|
|
207
|
+
const memory = await memoryRate();
|
|
208
|
+
const cpu = [appRate, usedRate];
|
|
209
|
+
const bots = {};
|
|
210
|
+
for (const bot of this.ctx.bots) {
|
|
211
|
+
if (bot.hidden) continue;
|
|
212
|
+
const paths = this.ctx.get("loader")?.paths(bot.ctx.scope);
|
|
213
|
+
bots[bot.sid] = {
|
|
214
|
+
...bot.toJSON(),
|
|
215
|
+
error: bot.error?.message,
|
|
216
|
+
messageSent: bot._messageSent.get(),
|
|
217
|
+
messageReceived: bot._messageReceived.get(),
|
|
218
|
+
...paths ? { paths } : {}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
memory,
|
|
223
|
+
cpu,
|
|
224
|
+
bots
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
static Config = Schema.object({ tickInterval: Schema.natural().role("ms").description("性能数据推送的时间间隔。").default(Time.second * 5) });
|
|
228
|
+
};
|
|
229
|
+
//#endregion
|
|
230
|
+
//#region src/index.ts
|
|
231
|
+
/**
|
|
232
|
+
* status 插件(node 侧)入口。
|
|
233
|
+
*
|
|
234
|
+
* 插件本体拆分为两个 DataService 子服务:
|
|
235
|
+
* - EnvInfoProvider(envinfo):一次性采集运行环境信息(OS / CPU / Node / 包管理器等);
|
|
236
|
+
* - ProfileProvider(profile):周期性采集性能数据(CPU / 内存 / 各机器人收发消息速率)。
|
|
237
|
+
*
|
|
238
|
+
* 本文件负责注册控制台前端资源入口、以子插件形式挂载上述两个服务,
|
|
239
|
+
* 并合并两者的 Config Schema 作为插件整体配置。
|
|
240
|
+
*/
|
|
241
|
+
const name = "status";
|
|
242
|
+
const inject = ["console"];
|
|
243
|
+
const Config = Schema.intersect([EnvInfoProvider.Config, ProfileProvider.Config]);
|
|
244
|
+
/**
|
|
245
|
+
* 插件入口:注册控制台前端资源(dev 指向 client 源码,prod 指向构建产物),
|
|
246
|
+
* 再将环境信息与性能采集两个服务作为子插件挂载,共享同一份配置。
|
|
247
|
+
*
|
|
248
|
+
* @param ctx 应用上下文
|
|
249
|
+
* @param config 插件配置(两个子服务配置的交集合并)
|
|
250
|
+
*/
|
|
251
|
+
function apply(ctx, config) {
|
|
252
|
+
ctx.console.addEntry({
|
|
253
|
+
dev: resolve(__dirname, "../client/index.ts"),
|
|
254
|
+
prod: resolve(__dirname, "../dist")
|
|
255
|
+
});
|
|
256
|
+
ctx.plugin(EnvInfoProvider, config);
|
|
257
|
+
ctx.plugin(ProfileProvider, config);
|
|
258
|
+
}
|
|
259
|
+
//#endregion
|
|
260
|
+
export { Config, EnvInfoProvider, ProfileProvider, apply, inject, name };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
commands:
|
|
2
|
+
status:
|
|
3
|
+
description: 查看运行状态
|
|
4
|
+
messages:
|
|
5
|
+
status:
|
|
6
|
+
1: 运行中
|
|
7
|
+
0: 离线
|
|
8
|
+
2: 正在连接
|
|
9
|
+
4: 正在重连
|
|
10
|
+
3: 正在断开
|
|
11
|
+
bot: <p>{user.name}:<i18n path={"commands.status.messages.status." + status}/></p>
|
|
12
|
+
epilog: |-
|
|
13
|
+
==========
|
|
14
|
+
CPU 使用率:{ (cpu[0] * 100).toFixed() }% / { (cpu[1] * 100).toFixed() }%
|
|
15
|
+
内存使用率:{ (memory[0] * 100).toFixed() }% / { (memory[1] * 100).toFixed() }%
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
commands:
|
|
2
|
+
status:
|
|
3
|
+
description: Display the status
|
|
4
|
+
messages:
|
|
5
|
+
status:
|
|
6
|
+
1: 运行中
|
|
7
|
+
0: 离线
|
|
8
|
+
2: 正在连接
|
|
9
|
+
4: 正在重连
|
|
10
|
+
3: 正在断开
|
|
11
|
+
bot: <p>{user.name}:<i18n path={"commands.status.messages.status." + status}/></p>
|
|
12
|
+
epilog: |-
|
|
13
|
+
==========
|
|
14
|
+
CPU 使用率:{ (cpu[0] * 100).toFixed() }% / { (cpu[1] * 100).toFixed() }%
|
|
15
|
+
内存使用率:{ (memory[0] * 100).toFixed() }% / { (memory[1] * 100).toFixed() }%
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
commands:
|
|
2
|
+
status:
|
|
3
|
+
description: 查看运行状态
|
|
4
|
+
messages:
|
|
5
|
+
status:
|
|
6
|
+
1: 运行中
|
|
7
|
+
0: 离线
|
|
8
|
+
2: 正在连接
|
|
9
|
+
4: 正在重连
|
|
10
|
+
3: 正在断开
|
|
11
|
+
bot: <p>{user.name}:<i18n path={"commands.status.messages.status." + status}/></p>
|
|
12
|
+
epilog: |-
|
|
13
|
+
==========
|
|
14
|
+
CPU 使用率:{ (cpu[0] * 100).toFixed() }% / { (cpu[1] * 100).toFixed() }%
|
|
15
|
+
内存使用率:{ (memory[0] * 100).toFixed() }% / { (memory[1] * 100).toFixed() }%
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
commands:
|
|
2
|
+
status:
|
|
3
|
+
description: 実行状態を確認する
|
|
4
|
+
messages:
|
|
5
|
+
status:
|
|
6
|
+
1: 运行中
|
|
7
|
+
0: 离线
|
|
8
|
+
2: 正在连接
|
|
9
|
+
4: 正在重连
|
|
10
|
+
3: 正在断开
|
|
11
|
+
bot: <p>{user.name}:<i18n path={"commands.status.messages.status." + status}/></p>
|
|
12
|
+
epilog: |-
|
|
13
|
+
==========
|
|
14
|
+
CPU 使用率:{ (cpu[0] * 100).toFixed() }% / { (cpu[1] * 100).toFixed() }%
|
|
15
|
+
内存使用率:{ (memory[0] * 100).toFixed() }% / { (memory[1] * 100).toFixed() }%
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
commands:
|
|
2
|
+
status:
|
|
3
|
+
description: 查看运行状态
|
|
4
|
+
messages:
|
|
5
|
+
status:
|
|
6
|
+
1: 运行中
|
|
7
|
+
0: 离线
|
|
8
|
+
2: 正在连接
|
|
9
|
+
4: 正在重连
|
|
10
|
+
3: 正在断开
|
|
11
|
+
bot: <p>{user.name}:<i18n path={"commands.status.messages.status." + status}/></p>
|
|
12
|
+
epilog: |-
|
|
13
|
+
==========
|
|
14
|
+
CPU 使用率:{ (cpu[0] * 100).toFixed() }% / { (cpu[1] * 100).toFixed() }%
|
|
15
|
+
内存使用率:{ (memory[0] * 100).toFixed() }% / { (memory[1] * 100).toFixed() }%
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
commands:
|
|
2
|
+
status:
|
|
3
|
+
description: 查看运行状态
|
|
4
|
+
messages:
|
|
5
|
+
status:
|
|
6
|
+
1: 运行中
|
|
7
|
+
0: 离线
|
|
8
|
+
2: 正在连接
|
|
9
|
+
4: 正在重连
|
|
10
|
+
3: 正在断开
|
|
11
|
+
bot: <p>[{platform}] {user.name}:<i18n path={"commands.status.messages.status." + status}/></p>
|
|
12
|
+
epilog: |-
|
|
13
|
+
==========
|
|
14
|
+
CPU 使用率:{ (cpu[0] * 100).toFixed() }% / { (cpu[1] * 100).toFixed() }%
|
|
15
|
+
内存使用率:{ (memory[0] * 100).toFixed() }% / { (memory[1] * 100).toFixed() }%
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
commands:
|
|
2
|
+
status:
|
|
3
|
+
description: 查看運行狀態
|
|
4
|
+
messages:
|
|
5
|
+
status:
|
|
6
|
+
1: 运行中
|
|
7
|
+
0: 离线
|
|
8
|
+
2: 正在连接
|
|
9
|
+
4: 正在重连
|
|
10
|
+
3: 正在断开
|
|
11
|
+
bot: <p>{user.name}:<i18n path={"commands.status.messages.status." + status}/></p>
|
|
12
|
+
epilog: |-
|
|
13
|
+
==========
|
|
14
|
+
CPU 使用率:{ (cpu[0] * 100).toFixed() }% / { (cpu[1] * 100).toFixed() }%
|
|
15
|
+
內存使用率:{ (memory[0] * 100).toFixed() }% / { (memory[1] * 100).toFixed() }%
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@koishi-ce/plugin-status",
|
|
3
|
+
"description": "Status view for Koishi",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.mjs",
|
|
7
|
+
"typings": "lib/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"lib",
|
|
10
|
+
"dist",
|
|
11
|
+
"src",
|
|
12
|
+
"locales"
|
|
13
|
+
],
|
|
14
|
+
"contributors": [
|
|
15
|
+
"Shigma <shigma10826@gmail.com>",
|
|
16
|
+
"Oppenheymu <oppenheymu@gmail.com>"
|
|
17
|
+
],
|
|
18
|
+
"license": "AGPL-3.0",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/Koishi-CE/koishi.git",
|
|
22
|
+
"directory": "plugins/webui/status"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/Koishi-CE/koishi/issues"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://koishi.chat/plugins/console/status.html",
|
|
28
|
+
"keywords": [
|
|
29
|
+
"bot",
|
|
30
|
+
"chatbot",
|
|
31
|
+
"koishi",
|
|
32
|
+
"plugin",
|
|
33
|
+
"status",
|
|
34
|
+
"environment",
|
|
35
|
+
"information"
|
|
36
|
+
],
|
|
37
|
+
"koishi": {
|
|
38
|
+
"description": {
|
|
39
|
+
"en": "Display running status and environment information in the console",
|
|
40
|
+
"zh": "在控制台中显示运行状态和环境信息"
|
|
41
|
+
},
|
|
42
|
+
"service": {
|
|
43
|
+
"required": [
|
|
44
|
+
"console"
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"@koishijs/plugin-console": "^5.30.11",
|
|
50
|
+
"koishi": "^4.18.11"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@koishi-ce/client": "^1.0.0",
|
|
54
|
+
"@koishi-ce/koishi": "^1.0.0",
|
|
55
|
+
"@koishi-ce/plugin-analytics": "^1.0.0",
|
|
56
|
+
"@koishi-ce/plugin-config": "^1.0.0",
|
|
57
|
+
"@types/envinfo": "^7.8.3"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"@koishi-ce/console": "^1.0.0",
|
|
61
|
+
"envinfo": "^7.21.0",
|
|
62
|
+
"which-pm-runs": "^1.1.0"
|
|
63
|
+
},
|
|
64
|
+
"exports": {
|
|
65
|
+
".": {
|
|
66
|
+
"source": "./src/index.ts",
|
|
67
|
+
"types": "./lib/index.d.ts",
|
|
68
|
+
"import": "./lib/index.mjs",
|
|
69
|
+
"default": "./lib/index.mjs"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
package/src/envinfo.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 运行环境信息服务(服务名 "envinfo")。
|
|
3
|
+
*
|
|
4
|
+
* 一次性采集系统与运行时版本信息,整理为 { system, binaries, koishi }
|
|
5
|
+
* 三组分节字符串字典,供控制台前端在状态栏展示与一键复制。
|
|
6
|
+
* 环境信息在进程生命周期内不会变化,因此采集结果以 Promise 形式永久缓存。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFile } from "node:fs/promises";
|
|
10
|
+
import { createRequire } from "node:module";
|
|
11
|
+
import { DataService } from "@koishi-ce/console";
|
|
12
|
+
import { type Context, type Dict, Schema, version } from "@koishi-ce/koishi";
|
|
13
|
+
import { helpers } from "envinfo";
|
|
14
|
+
|
|
15
|
+
// 经 createRequire 加载 CJS 包并就地断言签名:不走 ESM 导入互操作,
|
|
16
|
+
// 规避多包合并类型检查(大一统 tsconfig)下 export = 交织失效问题
|
|
17
|
+
// (此前具名导入在 Bun 运行时下本就拿不到具名导出,属顺带修复)
|
|
18
|
+
const whichPMRuns = createRequire(import.meta.url)("which-pm-runs") as () =>
|
|
19
|
+
| undefined
|
|
20
|
+
| { name: string; version: string };
|
|
21
|
+
|
|
22
|
+
class EnvInfoProvider extends DataService<Dict<Dict<string>>> {
|
|
23
|
+
/** 采集任务的缓存:get() 首次调用时创建,之后始终复用同一 Promise。 */
|
|
24
|
+
private task!: Promise<Dict<Dict<string>>>;
|
|
25
|
+
|
|
26
|
+
// 基类链(cordis Service)上已有 config 成员,故需 override
|
|
27
|
+
override config: EnvInfoProvider.Config;
|
|
28
|
+
|
|
29
|
+
constructor(ctx: Context, config: EnvInfoProvider.Config) {
|
|
30
|
+
super(ctx, "envinfo");
|
|
31
|
+
this.config = config;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 执行一次实际采集:系统信息、运行时版本、Koishi 生态版本。 */
|
|
35
|
+
async _get(): Promise<Dict<Dict<string>>> {
|
|
36
|
+
// @types/envinfo 把各 helper 声明为 Promise<string>,运行时实际返回
|
|
37
|
+
// [标题, 值] 二元组;解构默认值仅用于收窄类型,正常路径不会触发
|
|
38
|
+
const [[, Os = ""], [, Cpu = ""]] = await Promise.all([
|
|
39
|
+
helpers.getOSInfo(),
|
|
40
|
+
helpers.getCPUInfo(),
|
|
41
|
+
]);
|
|
42
|
+
const agent = whichPMRuns();
|
|
43
|
+
const system: Dict<string> = { OS: Os, CPU: Cpu };
|
|
44
|
+
const binaries: Dict<string> = {
|
|
45
|
+
Node: process.versions.node,
|
|
46
|
+
};
|
|
47
|
+
if (agent) {
|
|
48
|
+
// yarn 的名称归一为首字母大写,与 Node 等键的展示风格一致
|
|
49
|
+
if (agent.name === "yarn") {
|
|
50
|
+
agent.name = "Yarn";
|
|
51
|
+
}
|
|
52
|
+
binaries[agent.name] = agent.version;
|
|
53
|
+
}
|
|
54
|
+
// 不直接 require package.json 是为了避免其被模块缓存固定住,
|
|
55
|
+
// 热更新后读取到的仍是旧版本号
|
|
56
|
+
const metapath = require.resolve("@koishi-ce/console/package.json");
|
|
57
|
+
const meta = await readFile(metapath, "utf8").then(JSON.parse);
|
|
58
|
+
const koishi: Dict<string> = {
|
|
59
|
+
Core: version,
|
|
60
|
+
Console: meta.version,
|
|
61
|
+
};
|
|
62
|
+
// 宿主代理(如 koishi-plugin-browser)可通过该环境变量上报自身名称与版本
|
|
63
|
+
if (process.env["KOISHI_AGENT"]) {
|
|
64
|
+
const [name, agentVersion] = process.env["KOISHI_AGENT"].split("/");
|
|
65
|
+
if (name && agentVersion) {
|
|
66
|
+
koishi[name] = agentVersion;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return { system, binaries, koishi };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 首次调用时发起采集并缓存 Promise,后续调用直接返回同一结果。 */
|
|
73
|
+
override async get() {
|
|
74
|
+
if (!this.task) this.task = this._get();
|
|
75
|
+
return this.task;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// erasableSyntaxOnly 禁止含运行时值的 namespace,
|
|
79
|
+
// 原 namespace 内的 Config 常量移到此处的静态字段,对外形状不变
|
|
80
|
+
static Config: Schema<EnvInfoProvider.Config> = Schema.object({});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
namespace EnvInfoProvider {
|
|
84
|
+
export type Config = Record<never, never>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export default EnvInfoProvider;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* status 插件(node 侧)入口。
|
|
3
|
+
*
|
|
4
|
+
* 插件本体拆分为两个 DataService 子服务:
|
|
5
|
+
* - EnvInfoProvider(envinfo):一次性采集运行环境信息(OS / CPU / Node / 包管理器等);
|
|
6
|
+
* - ProfileProvider(profile):周期性采集性能数据(CPU / 内存 / 各机器人收发消息速率)。
|
|
7
|
+
*
|
|
8
|
+
* 本文件负责注册控制台前端资源入口、以子插件形式挂载上述两个服务,
|
|
9
|
+
* 并合并两者的 Config Schema 作为插件整体配置。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { resolve } from "node:path";
|
|
13
|
+
import type {} from "@koishi-ce/console";
|
|
14
|
+
import { type Context, Schema } from "@koishi-ce/koishi";
|
|
15
|
+
import EnvInfoProvider from "./envinfo.ts";
|
|
16
|
+
import ProfileProvider from "./profile.ts";
|
|
17
|
+
|
|
18
|
+
/** 频道活跃度记录:键为小时数(0~23),值为该小时内触发频道的次数。 */
|
|
19
|
+
export type Activity = Record<number, number>;
|
|
20
|
+
|
|
21
|
+
declare module "@koishi-ce/koishi" {
|
|
22
|
+
interface Channel {
|
|
23
|
+
name: string;
|
|
24
|
+
activity: Activity;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
declare module "@koishi-ce/console" {
|
|
29
|
+
namespace Console {
|
|
30
|
+
interface Services {
|
|
31
|
+
envinfo: EnvInfoProvider;
|
|
32
|
+
status: ProfileProvider;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export * from "./envinfo.ts";
|
|
38
|
+
export * from "./profile.ts";
|
|
39
|
+
export { EnvInfoProvider, ProfileProvider };
|
|
40
|
+
|
|
41
|
+
export const name = "status";
|
|
42
|
+
export const inject = ["console"];
|
|
43
|
+
|
|
44
|
+
export interface Config
|
|
45
|
+
extends ProfileProvider.Config,
|
|
46
|
+
EnvInfoProvider.Config {}
|
|
47
|
+
|
|
48
|
+
export const Config: Schema<Config> = Schema.intersect([
|
|
49
|
+
EnvInfoProvider.Config,
|
|
50
|
+
ProfileProvider.Config,
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 插件入口:注册控制台前端资源(dev 指向 client 源码,prod 指向构建产物),
|
|
55
|
+
* 再将环境信息与性能采集两个服务作为子插件挂载,共享同一份配置。
|
|
56
|
+
*
|
|
57
|
+
* @param ctx 应用上下文
|
|
58
|
+
* @param config 插件配置(两个子服务配置的交集合并)
|
|
59
|
+
*/
|
|
60
|
+
export function apply(ctx: Context, config: Config) {
|
|
61
|
+
ctx.console.addEntry({
|
|
62
|
+
dev: resolve(__dirname, "../client/index.ts"),
|
|
63
|
+
prod: resolve(__dirname, "../dist"),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
ctx.plugin(EnvInfoProvider, config);
|
|
67
|
+
ctx.plugin(ProfileProvider, config);
|
|
68
|
+
}
|
package/src/profile.ts
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 性能数据采集服务(服务名 "status")。
|
|
3
|
+
*
|
|
4
|
+
* 周期性采集三类指标并推送给控制台前端:
|
|
5
|
+
* - CPU:进程占用率与整机占用率(基于两次采样差值计算);
|
|
6
|
+
* - 内存:进程 RSS 占比与整机已用占比;
|
|
7
|
+
* - 机器人:每个 bot 最近一分钟的消息收发量(TickCounter 滑动窗口)。
|
|
8
|
+
*
|
|
9
|
+
* 另提供 `status` 指令,在聊天环境内以文本形式输出各机器人状态摘要。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { cpus, freemem, totalmem } from "node:os";
|
|
13
|
+
import { DataService } from "@koishi-ce/console";
|
|
14
|
+
import {
|
|
15
|
+
type Bot,
|
|
16
|
+
type Context,
|
|
17
|
+
type Dict,
|
|
18
|
+
Schema,
|
|
19
|
+
Time,
|
|
20
|
+
type Universal,
|
|
21
|
+
} from "@koishi-ce/koishi";
|
|
22
|
+
import zhCN from "../locales/zh-CN.yml";
|
|
23
|
+
|
|
24
|
+
declare module "@koishi-ce/koishi" {
|
|
25
|
+
interface Bot {
|
|
26
|
+
_messageSent: TickCounter;
|
|
27
|
+
_messageReceived: TickCounter;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 滑动窗口计数器:以 60 个槽位统计最近一分钟内的事件次数。
|
|
33
|
+
*
|
|
34
|
+
* 每秒执行一次 tick,队头插入 0 并丢弃队尾,窗口随时间向前滑动;
|
|
35
|
+
* add() 累加到当前秒所在的队头槽位,get() 求整个窗口的和。
|
|
36
|
+
* 每个 bot 挂载两个实例(_messageSent / _messageReceived)分别统计收发消息。
|
|
37
|
+
*/
|
|
38
|
+
class TickCounter {
|
|
39
|
+
/** 停止内部定时器(随上下文销毁由调用方触发)。 */
|
|
40
|
+
public stop: () => void;
|
|
41
|
+
|
|
42
|
+
/** 60 个槽位的环形数据,data[0] 恒为当前秒的计数。 */
|
|
43
|
+
private data = new Array(60).fill(0);
|
|
44
|
+
|
|
45
|
+
private tick = () => {
|
|
46
|
+
this.data.unshift(0);
|
|
47
|
+
this.data.splice(-1, 1);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
constructor(ctx: Context) {
|
|
51
|
+
this.stop = ctx.setInterval(() => this.tick(), Time.second);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 在当前秒的槽位上累加计数。 */
|
|
55
|
+
public add(value = 1) {
|
|
56
|
+
this.data[0] += value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 返回最近一分钟内的总计数。 */
|
|
60
|
+
public get() {
|
|
61
|
+
return this.data.reduce((prev, curr) => prev + curr, 0);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 为指定 bot 初始化收/发两个计数器。 */
|
|
65
|
+
static initialize(bot: Bot, ctx: Context) {
|
|
66
|
+
bot._messageSent = new TickCounter(ctx);
|
|
67
|
+
bot._messageReceived = new TickCounter(ctx);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 负载率二元组:[app, total],app 为本进程占比,total 为整机占比,取值 0~1。 */
|
|
72
|
+
export type LoadRate = [app: number, total: number];
|
|
73
|
+
|
|
74
|
+
// 模块级 CPU 采样状态:usage 保存上一次采样值,appRate / usedRate 为最近一次差值计算结果
|
|
75
|
+
let usage = getCpuUsage();
|
|
76
|
+
let appRate: number;
|
|
77
|
+
let usedRate: number;
|
|
78
|
+
|
|
79
|
+
/** 采集内存负载率:[进程 RSS / 总内存, 1 - 空闲内存 / 总内存]。 */
|
|
80
|
+
async function memoryRate(): Promise<LoadRate> {
|
|
81
|
+
const total = totalmem();
|
|
82
|
+
return [process.memoryUsage().rss / total, 1 - freemem() / total];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 读取一次 CPU 累计时间快照。
|
|
87
|
+
*
|
|
88
|
+
* - app:进程用户态 CPU 时间(cpuUsage().user,毫秒);
|
|
89
|
+
* - used:所有核心的 累计时间 - 空闲时间(毫秒);
|
|
90
|
+
* - total:所有核心的累计时间总和(毫秒)。
|
|
91
|
+
* 单次快照只是累计值,需经 updateCpuUsage() 做差才能得到占用率。
|
|
92
|
+
*/
|
|
93
|
+
function getCpuUsage() {
|
|
94
|
+
let totalIdle = 0,
|
|
95
|
+
totalTick = 0;
|
|
96
|
+
const cpuInfo = cpus();
|
|
97
|
+
const usage = process.cpuUsage().user;
|
|
98
|
+
|
|
99
|
+
for (const cpu of cpuInfo) {
|
|
100
|
+
for (const time of Object.values(cpu.times)) {
|
|
101
|
+
totalTick += time;
|
|
102
|
+
}
|
|
103
|
+
totalIdle += cpu.times.idle;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
// process.cpuUsage() 返回微秒,除以 1000 归一到毫秒
|
|
108
|
+
app: usage / 1000,
|
|
109
|
+
used: totalTick - totalIdle,
|
|
110
|
+
total: totalTick,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 用新快照与上次快照的差值计算 CPU 占用率并缓存到模块级变量:
|
|
116
|
+
* appRate = 本进程增量时间 / 全机总增量时间,usedRate 同理(整机口径)。
|
|
117
|
+
*/
|
|
118
|
+
function updateCpuUsage() {
|
|
119
|
+
const newUsage = getCpuUsage();
|
|
120
|
+
const totalDifference = newUsage.total - usage.total;
|
|
121
|
+
appRate = (newUsage.app - usage.app) / totalDifference;
|
|
122
|
+
usedRate = (newUsage.used - usage.used) / totalDifference;
|
|
123
|
+
usage = newUsage;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 性能数据服务:以 DataService 形式向前端广播 Payload,
|
|
128
|
+
* 前端经 store.status 读取(键名即服务名 "status")。
|
|
129
|
+
*/
|
|
130
|
+
class ProfileProvider extends DataService<ProfileProvider.Payload> {
|
|
131
|
+
// 仅在 get() 内作读取判断,本类从不写入(恒为 undefined),
|
|
132
|
+
// 用 declare + 显式 undefined 如实反映运行时形状,不产生字段初始化
|
|
133
|
+
declare cached: ProfileProvider.Payload | undefined;
|
|
134
|
+
|
|
135
|
+
// 基类链(cordis Service)上已有 config 成员,故需 override
|
|
136
|
+
override config: ProfileProvider.Config;
|
|
137
|
+
|
|
138
|
+
constructor(ctx: Context, config: ProfileProvider.Config) {
|
|
139
|
+
super(ctx, "status");
|
|
140
|
+
|
|
141
|
+
this.config = config;
|
|
142
|
+
|
|
143
|
+
ctx.i18n.define("zh-CN", zhCN);
|
|
144
|
+
|
|
145
|
+
// 就绪后按配置间隔刷新 CPU 占用率并推送性能数据
|
|
146
|
+
const { tickInterval } = config;
|
|
147
|
+
ctx.on("ready", () => {
|
|
148
|
+
ctx.setInterval(
|
|
149
|
+
() => {
|
|
150
|
+
updateCpuUsage();
|
|
151
|
+
this.refresh();
|
|
152
|
+
},
|
|
153
|
+
tickInterval ?? Time.second * 5,
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// 消息收发事件分别计入对应 bot 的滑动窗口计数器
|
|
158
|
+
ctx.any().before("send", (session) => {
|
|
159
|
+
session.bot._messageSent?.add(1);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
ctx.any().on("message", (session) => {
|
|
163
|
+
session.bot._messageReceived?.add(1);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
ctx.bots.forEach((bot) => {
|
|
167
|
+
TickCounter.initialize(bot, ctx);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// bot 列表变化时防抖刷新(同帧内多次事件只触发一次推送)
|
|
171
|
+
const update = ctx.debounce(() => this.refresh(), 0);
|
|
172
|
+
|
|
173
|
+
ctx.on("login-added", ({ bot }) => {
|
|
174
|
+
TickCounter.initialize(bot, ctx);
|
|
175
|
+
update();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
ctx.on("login-removed", ({ bot }) => {
|
|
179
|
+
bot._messageSent.stop();
|
|
180
|
+
bot._messageReceived.stop();
|
|
181
|
+
update();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
ctx.on("login-updated", () => {
|
|
185
|
+
update();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// 聊天侧指令:逐个输出机器人状态,末尾附加整体性能摘要
|
|
189
|
+
ctx.command("status").action(async ({ session }) => {
|
|
190
|
+
if (!session) return;
|
|
191
|
+
const data = await this.get();
|
|
192
|
+
const output = Object.values(data.bots).map((bot) => {
|
|
193
|
+
return session.text(".bot", bot);
|
|
194
|
+
});
|
|
195
|
+
output.push(session.text(".epilog", data));
|
|
196
|
+
return output.join("\n");
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 采集一帧性能数据:CPU 占用率、内存占用率与各机器人收发速率。
|
|
202
|
+
*
|
|
203
|
+
* @param forced 为 true 时跳过缓存强制重算(refresh 链路会传入)
|
|
204
|
+
*/
|
|
205
|
+
override async get(forced = false) {
|
|
206
|
+
if (this.cached && !forced) return this.cached;
|
|
207
|
+
const memory = await memoryRate();
|
|
208
|
+
const cpu: LoadRate = [appRate, usedRate];
|
|
209
|
+
const bots: Dict<ProfileProvider.BotData> = {};
|
|
210
|
+
for (const bot of this.ctx.bots) {
|
|
211
|
+
if (bot.hidden) continue;
|
|
212
|
+
const paths = this.ctx.get("loader")?.paths(bot.ctx.scope);
|
|
213
|
+
bots[bot.sid] = {
|
|
214
|
+
...bot.toJSON(),
|
|
215
|
+
error: bot.error?.message,
|
|
216
|
+
messageSent: bot._messageSent.get(),
|
|
217
|
+
messageReceived: bot._messageReceived.get(),
|
|
218
|
+
// exactOptionalPropertyTypes:paths 可选属性不在 undefined 时展开
|
|
219
|
+
...(paths ? { paths } : {}),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return { memory, cpu, bots };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// erasableSyntaxOnly 禁止含运行时值的 namespace,
|
|
226
|
+
// 原 namespace 内的 Config 常量移到此处的静态字段,对外形状不变
|
|
227
|
+
static Config: Schema<ProfileProvider.Config> = Schema.object({
|
|
228
|
+
tickInterval: Schema.natural()
|
|
229
|
+
.role("ms")
|
|
230
|
+
.description("性能数据推送的时间间隔。")
|
|
231
|
+
.default(Time.second * 5),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
namespace ProfileProvider {
|
|
236
|
+
export interface Config {
|
|
237
|
+
/** 采集与推送性能数据的时间间隔(毫秒),默认 5 秒。 */
|
|
238
|
+
tickInterval?: number;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** 推送给前端的单个机器人数据:登录信息 + 错误消息 + 配置路径 + 收发速率。 */
|
|
242
|
+
export interface BotData extends Universal.Login {
|
|
243
|
+
error?: string;
|
|
244
|
+
paths?: string[];
|
|
245
|
+
messageSent: number;
|
|
246
|
+
messageReceived: number;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** 每次采集推送给前端的完整负载:内存 / CPU 负载率与机器人数据字典。 */
|
|
250
|
+
export interface Payload {
|
|
251
|
+
memory: LoadRate;
|
|
252
|
+
cpu: LoadRate;
|
|
253
|
+
bots: Dict<BotData>;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export default ProfileProvider;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// which-pm-runs 没有官方类型声明,npm 上也不存在 @types/which-pm-runs。
|
|
2
|
+
// 以下按其 1.1.0 源码(index.js)手写形状:
|
|
3
|
+
// 未检测到 npm_config_user_agent 环境变量时返回 undefined,
|
|
4
|
+
// 否则返回包管理器的名称(npminstall 会归一化为 cnpm)与版本。
|
|
5
|
+
declare module "which-pm-runs" {
|
|
6
|
+
export interface PackageManager {
|
|
7
|
+
name: string;
|
|
8
|
+
version: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function whichPMRuns(): PackageManager | undefined;
|
|
12
|
+
}
|