Add HeXi bot codebase: custom plugins, web frontends, tests

- hexi core: message handling, rate limiting, cooldown, plugin manager
- Custom plugins: BF stats, daily check-in, quotes, persona cards, etc.
- Community plugins vendored under hexi/plugins with local fixes
- Web admin frontends (learning-chat, persona-admin), unified hexi/web
- Tests for rate_limit/cooldown/memes/persona; poetry.lock

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
sansenhoshiandClaude committed 2026-09-01 13:13:40 +08:00
1 parent 1783c60afa
commit b61d09f09f
3201 files changed
+160436 -171

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 867 B

@@ -0,0 +1,45 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).echarts={})}(this,(function(t){"use strict";
/*! *****************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[n]=e[n])},e(t,n)};function n(t,n){if("function"!=typeof n&&null!==n)throw new TypeError("Class extends value "+String(n)+" is not a constructor or null");function i(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(i.prototype=n.prototype,new i)}var i=function(){this.firefox=!1,this.ie=!1,this.edge=!1,this.newEdge=!1,this.weChat=!1},r=new function(){this.browser=new i,this.node=!1,this.wxa=!1,this.worker=!1,this.svgSupported=!1,this.touchEventsSupported=!1,this.pointerEventsSupported=!1,this.domSupported=!1,this.transformSupported=!1,this.transform3dSupported=!1,this.hasGlobalWindow="undefined"!=typeof window};"object"==typeof wx&&"function"==typeof wx.getSystemInfoSync?(r.wxa=!0,r.touchEventsSupported=!0):"undefined"==typeof document&&"undefined"!=typeof self?r.worker=!0:"undefined"==typeof navigator||0===navigator.userAgent.indexOf("Node.js")?(r.node=!0,r.svgSupported=!0):function(t,e){var n=e.browser,i=t.match(/Firefox\/([\d.]+)/),r=t.match(/MSIE\s([\d.]+)/)||t.match(/Trident\/.+?rv:(([\d.]+))/),o=t.match(/Edge?\/([\d.]+)/),a=/micromessenger/i.test(t);i&&(n.firefox=!0,n.version=i[1]);r&&(n.ie=!0,n.version=r[1]);o&&(n.edge=!0,n.version=o[1],n.newEdge=+o[1].split(".")[0]>18);a&&(n.weChat=!0);e.svgSupported="undefined"!=typeof SVGRect,e.touchEventsSupported="ontouchstart"in window&&!n.ie&&!n.edge,e.pointerEventsSupported="onpointerdown"in window&&(n.edge||n.ie&&+n.version>=11),e.domSupported="undefined"!=typeof document;var s=document.documentElement.style;e.transform3dSupported=(n.ie&&"transition"in s||n.edge||"WebKitCSSMatrix"in window&&"m11"in new WebKitCSSMatrix||"MozPerspective"in s)&&!("OTransition"in s),e.transformSupported=e.transform3dSupported||n.ie&&+n.version>=9}(navigator.userAgent,r);var o="sans-serif",a="12px "+o;var s,l,u=function(t){var e={};if("undefined"==typeof JSON)return e;for(var n=0;n<t.length;n++){var i=String.fromCharCode(n+32),r=(t.charCodeAt(n)-20)/100;e[i]=r}return e}("007LLmW'55;N0500LLLLLLLLLL00NNNLzWW\\\\WQb\\0FWLg\\bWb\\WQ\\WrWWQ000CL5LLFLL0LL**F*gLLLL5F0LF\\FFF5.5N"),h={createCanvas:function(){return"undefined"!=typeof document&&document.createElement("canvas")},measureText:function(t,e){if(!s){var n=h.createCanvas();s=n&&n.getContext("2d")}if(s)return l!==e&&(l=s.font=e||a),s.measureText(t);t=t||"";var i=/((?:\d+)?\.?\d*)px/.exec(e=e||a),r=i&&+i[1]||12,o=0;if(e.indexOf("mono")>=0)o=r*t.length;else for(var c=0;c<t.length;c++){var p=u[t[c]];o+=null==p?r:p*r}return{width:o}},loadImage:function(t,e,n){var i=new Image;return i.onload=e,i.onerror=n,i.src=t,i}};function c(t){for(var e in h)t[e]&&(h[e]=t[e])}var p=V(["Function","RegExp","Date","Error","CanvasGradient","CanvasPattern","Image","Canvas"],(function(t,e){return t["[object "+e+"]"]=!0,t}),{}),d=V(["Int8","Uint8","Uint8Clamped","Int16","Uint16","Int32","Uint32","Float32","Float64"],(function(t,e){return t["[object "+e+"Array]"]=!0,t}),{}),f=Object.prototype.toString,g=Array.prototype,y=g.forEach,v=g.filter,m=g.slice,x=g.map,_=function(){}.constructor,b=_?_.prototype:null,w="__proto__",S=2311;function M(){return S++}function I(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];"undefined"!=typeof console&&console.error.apply(console,t)}function T(t){if(null==t||"object"!=typeof t)return t;var e=t,n=f.call(t);if("[object Array]"===n){if(!pt(t)){e=[];for(var i=0,r=t.length;i<r;i++)e[i]=T(t[i])}}else if(d[n]){if(!pt(t)){var o=t.constructor;if(o.from)e=o.from(t);else{e=new o(t.length);for(i=0,r=t.length;i<r;i++)e[i]=t[i]}}}else if(!p[n]&&!pt(t)&&!J(t))for(var a in e={},t)t.hasOwnProperty(a)&&a!==w&&(e[a]=T(t[a]));return e}function C(t,e,n){if(!q(e)||!q(t))return n?T(e):t;for(var i in e)if(e.hasOwnProperty(i)&&i!==w){var r=t[i],o=e[i];!q(o)||!q(r)||Y(o)||Y(r)||J(o)||J(r)||K(o)||K(r)||pt(o)||pt(r)?!n&&i in t||(t[i]=T(e[i])):C(r,o,n)}return t}function D(t,e){for(var n=t[0],i=1,r=t.length;i<r;i++)n=C(n,t[i],e);return n}function A(t,e){if(Object.assign)Object.assign(t,e);else for(var n in e)e.hasOwnProperty(n)&&n!==w&&(t[n]=e[n]);return t}function k(t,e,n){for(var i=G(e),r=0;r<i.length;r++){var o=i[r];(n?null!=e[o]:null==t[o])&&(t[o]=e[o])}return t}var L=h.createCanvas;function P(t,e){if(t){if(t.indexOf)return t.indexOf(e);for(var n=0,i=t.length;n<i;n++)if(t[n]===e)return n}return-1}function O(t,e){var n=t.prototype;function i(){}for(var r in i.prototype=e.prototype,t.prototype=new i,n)n.hasOwnProperty(r)&&(t.prototype[r]=n[r]);t.prototype.constructor=t,t.superClass=e}function R(t,e,n){if(t="prototype"in t?t.prototype:t,e="prototype"in e?e.prototype:e,Object.getOwnPropertyNames)for(var i=Object.getOwnPropertyNames(e),r=0;r<i.length;r++){var o=i[r];"constructor"!==o&&(n?null!=e[o]:null==t[o])&&(t[o]=e[o])}else k(t,e,n)}function N(t){retuLine truncated
/*!
* ZRender, a high performance 2d drawing library.
*
* Copyright (c) 2013, Baidu Inc.
* All rights reserved.
*
* LICENSE
* https://github.com/ecomfe/zrender/blob/master/LICENSE.txt
*/
var Fr={},Gr={};var Wr,Hr=function(){function t(t,e,n){var i=this;this._sleepAfterStill=10,this._stillFrameAccum=0,this._needsRefresh=!0,this._needsRefreshHover=!0,this._darkMode=!1,n=n||{},this.dom=e,this.id=t;var o=new rn,a=n.renderer||"canvas";Fr[a]||(a=G(Fr)[0]),n.useDirtyRect=null!=n.useDirtyRect&&n.useDirtyRect;var s=new Fr[a](e,o,n,t),l=n.ssr||s.ssrOnly;this.storage=o,this.painter=s;var u,h=r.node||r.worker||l?null:new or(s.getViewportRoot(),s.root),c=n.useCoarsePointer;(null==c||"auto"===c?r.touchEventsSupported:!!c)&&(u=rt(n.pointerSize,44)),this.handler=new Ye(o,s,h,s.root,u),this.animation=new Wi({stage:{update:l?null:function(){return i._flush(!0)}}}),l||this.animation.start()}return t.prototype.add=function(t){!this._disposed&&t&&(this.storage.addRoot(t),t.addSelfToZr(this),this.refresh())},t.prototype.remove=function(t){!this._disposed&&t&&(this.storage.delRoot(t),t.removeSelfFromZr(this),this.refresh())},t.prototype.configLayer=function(t,e){this._disposed||(this.painter.configLayer&&this.painter.configLayer(t,e),this.refresh())},t.prototype.setBackgroundColor=function(t){this._disposed||(this.painter.setBackgroundColor&&this.painter.setBackgroundColor(t),this.refresh(),this._backgroundColor=t,this._darkMode=function(t){if(!t)return!1;if("string"==typeof t)return oi(t,1)<.4;if(t.colorStops){for(var e=t.colorStops,n=0,i=e.length,r=0;r<i;r++)n+=oi(e[r].color,1);return(n/=i)<.4}return!1}(t))},t.prototype.getBackgroundColor=function(){return this._backgroundColor},t.prototype.setDarkMode=function(t){this._darkMode=t},t.prototype.isDarkMode=function(){return this._darkMode},t.prototype.refreshImmediately=function(t){this._disposed||(t||this.animation.update(!0),this._needsRefresh=!1,this.painter.refresh(),this._needsRefresh=!1)},t.prototype.refresh=function(){this._disposed||(this._needsRefresh=!0,this.animation.start())},t.prototype.flush=function(){this._disposed||this._flush(!1)},t.prototype._flush=function(t){var e,n=Bi();this._needsRefresh&&(e=!0,this.refreshImmediately(t)),this._needsRefreshHover&&(e=!0,this.refreshHoverImmediately());var i=Bi();e?(this._stillFrameAccum=0,this.trigger("rendered",{elapsedTime:i-n})):this._sleepAfterStill>0&&(this._stillFrameAccum++,this._stillFrameAccum>this._sleepAfterStill&&this.animation.stop())},t.prototype.setSleepAfterStill=function(t){this._sleepAfterStill=t},t.prototype.wakeUp=function(){this._disposed||(this.animation.start(),this._stillFrameAccum=0)},t.prototype.refreshHover=function(){this._needsRefreshHover=!0},t.prototype.refreshHoverImmediately=function(){this._disposed||(this._needsRefreshHover=!1,this.painter.refreshHover&&"canvas"===this.painter.getType()&&this.painter.refreshHover())},t.prototype.resize=function(t){this._disposed||(t=t||{},this.painter.resize(t.width,t.height),this.handler.resize())},t.prototype.clearAnimation=function(){this._disposed||this.animation.clear()},t.prototype.getWidth=function(){if(!this._disposed)return this.painter.getWidth()},t.prototype.getHeight=function(){if(!this._disposed)return this.painter.getHeight()},t.prototype.setCursorStyle=function(t){this._disposed||this.handler.setCursorStyle(t)},t.prototype.findHover=function(t,e){if(!this._disposed)return this.handler.findHover(t,e)},t.prototype.on=function(t,e,n){return this._disposed||this.handler.on(t,e,n),this},t.prototype.off=function(t,e){this._disposed||this.handler.off(t,e)},t.prototype.trigger=function(t,e){this._disposed||this.handler.trigger(t,e)},t.prototype.clear=function(){if(!this._disposed){for(var t=this.storage.getRoots(),e=0;e<t.length;e++)t[e]instanceof Br&&t[e].removeSelfFromZr(this);this.storage.delAllRoots(),this.painter.clear()}},t.prototype.dispose=function(){var t;this._disposed||(this.animation.stop(),this.clear(),this.storage.dispose(),this.painter.dispose(),this.handler.dispose(),this.animation=this.storage=this.painter=this.handler=null,this._disposed=!0,t=this.id,delete Gr[t])},t}();function Yr(t,e){var n=new Hr(M(),t,e);return Gr[n.id]=n,n}function Xr(t,e){Fr[t]=e}function Ur(t){if("function"==typeof Wr)return Wr(t)}function Zr(t){Wr=t}var jr=Object.freeze({__proto__:null,init:Yr,dispose:function(t){t.dispose()},disposeAll:function(){for(var t in Gr)Gr.hasOwnProperty(t)&&Gr[t].dispose();Gr={}},getInstance:function(t){return Gr[t]},registerPainter:Xr,getElementSSRData:Ur,registerSSRDataGetter:Zr,version:"5.6.0"}),qr=1e-4;function Kr(t,e,n,i){var r=e[0],o=e[1],a=n[0],s=n[1],l=o-r,u=s-a;if(0===l)return 0===u?a:(a+s)/2;if(i)if(l>0){if(t<=r)return a;if(t>=o)return s}else{if(t>=r)return a;if(t<=o)return s}else{if(t===r)return a;if(t===o)return s}return(t-r)/l*u+a}function $r(t,e){switch(t){case"center":case"middle":t="50%";break;case"left":case"top":t="0%";break;case"right":case"bottom":t="100%"}return U(t)?(n=t,n.replace(/^\s+|\s+$/g,"")).match(/%$/)?parseFloat(t)/100*e:parseFloat(t):null==t?NaN:+t;var n}function Jr(t,e,n){return null==e&&(e=10),e=Math.min(Math.max(0,e),20),t=(+t).toFixed(e),n?t:+t}function Qr(t){return t.sort((funLine truncated
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

@@ -0,0 +1,343 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<style>
@font-face { font-family: 'MiSansR'; src: url('__FONT_REGULAR__'); }
@font-face { font-family: 'MiSansB'; src: url('__FONT_BOLD__'); }
:root {
--surface: #f6f6f7;
--card: #ffffff;
--hairline: #e3e3e6;
--text-primary: #1a1a1a;
--text-secondary: #52514e;
--text-muted: #898781;
--axis: #c3c2b7;
--bar: #2a78d6;
--bar-fill-top: #5598e7;
--accent: #2a78d6;
--game-text: #242424;
--fallback-bg: #f0f0f2;
}
/* 主题覆盖(light/dark 由 Python 注入) */
__THEME_CSS__
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 1400px;
background: var(--surface);
font-family: 'MiSansR', 'Microsoft YaHei', sans-serif;
padding: 28px 36px 36px;
color: var(--text-primary);
}
/* ---- 头部:头像 + 昵称 ---- */
.header {
display: flex;
align-items: center;
gap: 18px;
}
.avatar {
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
border: 2px solid var(--hairline);
flex-shrink: 0;
}
.avatar-fallback {
display: flex;
align-items: center;
justify-content: center;
background: var(--fallback-bg);
font-family: 'MiSansB', 'MiSansR', 'Microsoft YaHei', sans-serif;
font-size: 34px;
color: var(--text-muted);
}
.nickname {
font-family: 'MiSansB', 'MiSansR', 'Microsoft YaHei', sans-serif;
font-size: 32px;
display: flex;
align-items: baseline;
min-width: 0;
}
.nick-tag {
font-family: 'MiSansR', 'Microsoft YaHei', sans-serif;
font-size: 16px;
color: var(--text-muted);
margin-left: 8px;
vertical-align: baseline;
min-width: 0;
max-width: 260px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.stats {
font-size: 20px;
color: var(--text-secondary);
margin-top: 6px;
}
/* ---- 双列排行表格 ---- */
.tables {
display: flex;
gap: 20px;
margin-top: 22px;
}
.col {
flex: 1;
min-width: 0;
}
table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
col.rank { width: 72px; }
col.hours { width: 150px; }
th {
font-size: 18px;
font-weight: normal;
color: var(--text-muted);
text-align: left;
padding: 6px 8px;
}
td {
font-size: 25px;
padding: 12px 8px;
border-bottom: 1px solid var(--hairline);
vertical-align: middle;
}
tr:last-child td { border-bottom: none; }
.rank { width: 72px; }
.rank-badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
border-radius: 8px;
font-size: 20px;
font-family: 'MiSansB', 'MiSansR', 'Microsoft YaHei', sans-serif;
color: #ffffff;
}
.rank-plain {
display: inline-block;
width: 34px;
text-align: center;
font-size: 25px;
color: var(--text-muted);
}
.game {
color: var(--game-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.hours {
width: 150px;
text-align: right;
color: var(--text-secondary);
white-space: nowrap;
}
/* ---- 图表卡片 ---- */
.card {
background: var(--card);
border-radius: 16px;
padding: 16px 20px 12px;
margin-top: 22px;
}
.card-title {
font-family: 'MiSansB', 'MiSansR', 'Microsoft YaHei', sans-serif;
font-size: 24px;
}
.card-sub {
font-size: 18px;
color: var(--text-muted);
margin-top: 2px;
}
.chart {
width: 100%;
height: 330px;
}
/* ---- 底部 ---- */
.footer {
font-size: 18px;
color: var(--text-muted);
margin-top: 20px;
text-align: right;
}
</style>
</head>
<body>
<div class="header">
__AVATAR__
<div>
<div class="nickname">__STEAM_NAME____PLUGIN_TAG__</div>
<div class="stats">共 __GAME_COUNT__ 款游戏 · 总时长 __TOTAL_HOURS__</div>
</div>
</div>
<div class="tables">
<div class="col">
<table>
<colgroup>
<col class="rank">
<col>
<col class="hours">
</colgroup>
<thead>
<tr>
<th class="rank">排名</th>
<th>游戏</th>
<th class="hours">时长</th>
</tr>
</thead>
<tbody>
__ROWS_LEFT__
</tbody>
</table>
</div>
<div class="col">
__RIGHT_TABLE__
</div>
</div>
<div class="card">
<div class="card-title">各时长段游戏数量</div>
<div class="card-sub">全库按时长分段,0 = 未游玩 · 对数轴</div>
<div class="chart" id="bar"></div>
</div>
<div class="card">
<div class="card-title">时长占比</div>
<div class="card-sub">Top 8 游戏 + 其余聚合</div>
<div class="chart" id="pie"></div>
</div>
<div class="footer">__FOOTER__</div>
<script src="__ECHARTS__"></script>
<script>
const DATA = __DATA__;
// 主题色从 CSS 变量读取(亮/暗由 Python 注入 :root 覆盖)
function cssVar(name) {
return getComputedStyle(document.documentElement)
.getPropertyValue(name).trim() || '#000';
}
// 各时长段游戏数量柱状图(对数轴:0 时长大桶不会压扁其他桶)
const bar = echarts.init(document.getElementById('bar'));
bar.setOption({
animation: false,
backgroundColor: 'transparent',
tooltip: {
trigger: 'axis',
backgroundColor: cssVar('--card'),
borderColor: cssVar('--hairline'),
textStyle: { color: cssVar('--text-primary'), fontSize: 14 },
},
grid: { left: 8, right: 16, top: 30, bottom: 30, containLabel: true },
xAxis: {
type: 'category',
data: DATA.bar.names,
axisLine: { lineStyle: { color: cssVar('--axis') } },
axisTick: { show: false },
axisLabel: { color: cssVar('--text-muted'), fontSize: 15 },
},
yAxis: {
type: 'log',
show: false,
splitLine: { lineStyle: { color: cssVar('--hairline') } },
},
series: [{
type: 'bar',
barWidth: '45%',
barMaxWidth: 110,
data: DATA.bar.counts,
itemStyle: { color: cssVar('--bar'), borderRadius: [5, 5, 0, 0] },
label: {
show: true,
position: 'top',
color: cssVar('--text-primary'),
fontSize: 16,
},
}],
});
// 时长占比环形饼图(禁用动画:截图可能截到入场动画早期帧导致整图空白)
const pie = echarts.init(document.getElementById('pie'));
pie.setOption({
animation: false,
backgroundColor: 'transparent',
color: DATA.pie.colors,
tooltip: {
trigger: 'item',
backgroundColor: cssVar('--card'),
borderColor: cssVar('--hairline'),
textStyle: { color: cssVar('--text-primary'), fontSize: 14 },
},
legend: {
orient: 'vertical',
right: 10,
top: 'center',
itemWidth: 12,
itemHeight: 12,
textStyle: { color: cssVar('--text-secondary'), fontSize: 14 },
formatter: function (name) {
try {
const item = (DATA.pie.items || []).find(i => i.name === name);
if (!item || !DATA.pie.total) return name;
return name + ' ' + (item.value / DATA.pie.total * 100).toFixed(1) + '%';
} catch (e) {
return name;
}
},
},
series: [{
type: 'pie',
radius: ['48%', '68%'],
center: ['37%', '50%'],
itemStyle: { borderColor: cssVar('--card'), borderWidth: 2 },
label: {
color: cssVar('--text-primary'),
fontSize: 14,
formatter: function (p) {
return p.percent >= 5 ? p.percent.toFixed(1) + '%' : '';
},
},
labelLine: { show: false },
data: DATA.pie.items,
}],
});
window.__rendered = true;
</script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 B