From 0b9dd22f24e7e41ac15598e16215bdf19d7ba4c3 Mon Sep 17 00:00:00 2001 From: shishi Date: Sun, 9 Aug 2026 12:32:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=89=8D=E7=AB=AF=E5=AE=8C=E6=95=B4?= =?UTF-8?q?=E6=BA=90=E7=A0=81=E7=BA=B3=E5=85=A5=20Go=20=E4=BB=93=E5=BA=93?= =?UTF-8?q?=EF=BC=88web/=EF=BC=8C=E6=A0=87=E9=A2=98=E5=B7=B2=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=20=E7=B3=BB=E7=BB=9F=E8=B0=83=E4=BC=98=201.0=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/.gitignore | 12 + web/README.md | 51 +++ web/public/vite.svg | 1 + web/src/App.vue | 669 ++++++++++++++++++++++++++++++++++++++++ web/src/api.js | 39 +++ web/src/assets/icon.png | Bin 0 -> 26540 bytes web/src/icons.js | 54 ++++ web/src/main.js | 6 + web/src/params.js | 190 ++++++++++++ web/src/style.css | 3 + web/vite.config.js | 15 + 11 files changed, 1040 insertions(+) create mode 100644 web/.gitignore create mode 100644 web/README.md create mode 100644 web/public/vite.svg create mode 100644 web/src/App.vue create mode 100644 web/src/api.js create mode 100644 web/src/assets/icon.png create mode 100644 web/src/icons.js create mode 100644 web/src/main.js create mode 100644 web/src/params.js create mode 100644 web/src/style.css create mode 100644 web/vite.config.js diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..0cbfcfe --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,12 @@ +__pycache__ +*.pyc +*.log +__pycache__/ +.venv +.idea +test +dist/* +dist/ +.DS_Store +/node_modules/ +/www/ diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..da63f57 --- /dev/null +++ b/web/README.md @@ -0,0 +1,51 @@ +# Vue 3 + Vite 项目模板 + +这是一个基于 Vue 3 和 Vite 的项目模板,旨在帮助开发者快速搭建和开发现代前端应用。 + +## 技术栈 + +- **Vue 3**: 使用 Vue 3 的组合式 API 和 ` + + diff --git a/web/src/api.js b/web/src/api.js new file mode 100644 index 0000000..cad884b --- /dev/null +++ b/web/src/api.js @@ -0,0 +1,39 @@ +// ===================== API 封装 ===================== +// GM 应用内通过 $gm.request 调用后端 RPC(socket 传输) +const ORG = 'user_TGPJp0KLpNsa5TAo' +const APP = 'ddhv' + +export function call(method, params = {}) { + return new Promise((resolve, reject) => { + const doCall = () => { + $gm.request.post(`/api/call/${ORG}/${APP}/${method}`, { + version: '1.0.0', + transport: 'socket', + params, + }).then(res => { + // 后端统一返回 {code, data, msg} + const body = res && res.data !== undefined ? res.data : res + if (body && body.code === 200) { + resolve(body.data) + } else { + reject(new Error((body && body.msg) || '请求失败')) + } + }).catch(err => { + reject(err) + }) + } + if (window.$gm && $gm.request) { + doCall() + } else { + // 开发环境无 SDK 时降级(便于浏览器直接调试) + reject(new Error('GM SDK 未初始化(请在面板内打开应用)')) + } + }) +} + +// 消息提示(兼容 SDK 缺失环境) +export const toast = { + success(msg) { if (window.$gm && $gm.message) $gm.message.success(msg); else alert('[OK] ' + msg) }, + error(msg) { if (window.$gm && $gm.message) $gm.message.error(msg); else alert('[失败] ' + msg) }, + warning(msg) { if (window.$gm && $gm.message) $gm.message.warning(msg); else alert('[警告] ' + msg) }, +} diff --git a/web/src/assets/icon.png b/web/src/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1bc03bc7c13df13958a91077b4a319ea71d00528 GIT binary patch literal 26540 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�b$<^QCm2mLpfbO`>#ZX*G zRi$-k`W55eTMdtVsiM$(s&ssjvqW4pGU~~#n-$eOrCn-k%m`ZRn32$@7Q&JK|+D63FJ~5DyTlxk(%RayiO0hxB(M0OTqrBSwC~qEHE_ zIB2nCv~kMd~`lSutr8wm%4e*RiqSvShZ0 zMhZ2O?4$SRk*1LE%v;gt!DrAjtJGrgn>M-6k%G)Q58ZN28j3>4;3_Owd|SO7>6ns`RS75U024N-~XNFM4+3=;=noQl7|}GEMl4bEgvzz!C6>{^%)RvI=3M z_$7sYE|30WBMMQWQl~9`(U2nSmZmw*hN}-*_c!5c0{xJsQbQVx^`=6Y(|(K#2b1 zXt~XtvE3WsNKraD817B{_R&cExQi=8<4)1umO18rWtwpH@ljQ)voJAKVrL+je)C&2 zXL(6U$%2+qun@DMJ*cjU(-)a5WTblE=332yzD7XJj#IMH4|m&o?~sZ&^svtjSj#%+ z&QA>rV@sREmp`%%%YDla`#I0N->{x=Zpp*RN^%1vG}9$e%$5%G;k3e{E6BbnZft)U zAqR6e{uZWJZR7Y}C_yPLW_D6XBdIZ0G9JzIt#ITDn&r@s6C!m*aGM>jl*_z zT>_$w&CLfrFidE-`HB^FMO%GnyD4mmX#XFg`z3IUXhmVh$l7HzkMGECp7q_UMDBLUO%zam+ zx)O!V4s1g`(0^*dl@QpD@l3Xk@PD(UUWhYQy$v6Ncr!mNmne^}OqvUw4OPJyF_+ME zhzuPM9kUn)j+tYq13qcpaaP`k>XsWC?$26n(9b7| zHnP2p|-d}49ded*W7WVEok8LrH%9^gPHP6e# zrGLe)d1Q)9F7s`iG{u9Zqe66lm4>SLqaOys$o=zObFpMoJL@vuc_s{f5b6k99cn|G zS$<1iim5&O-%k3ymVK#oKtN?Y!^4|`Ac6~0fG)Z6$O zLyPoe{C`5KR-qh?AMCDD42t`=HS@NjMB`Tm(`2P@%=ka_JN)6xs{ZgyIyzLyAII^y!56FvbyK1ZF0KoEA~!Kb z!QzF|*VSF}0|2~X2(3P*iE$R5rAT>|H8frs( zwleJ~s{#p(t?pdGQaz_pSTsVnA^HmFsjzU>RnN_7*{>k6muGmh@fbO{$*)+ufqt_O zAmD~`-}g09{XYsiv-edP#nj%%onu}TZ?W6yp^|dBF)AE;OXW92sZX=_(Uc}C8yGSz zERI?hK5>bML}lWgtuwMTV*g>%$koghwJqdt!M#2j0WOBdhV>z47C6T&{b>Tfk^E|^ zd+}=kP{f69dj(T3565lMzBcDtPjqvgACP`c%$iMulqj2L;9E`igSFMV>P+|5r&jFE zf$ADmAUCAGcY{r<7*oQtQ$f~*AwOfc4vYkcT+#WNRS`Os9q}izLlIw`@1?qBgbiBn)O^~fmIg;yD*H23#VpO zB`UNn)2wTB6m=QbY5}?*HFGGaR*Cl2?ALsaOyb9E|1yg+G}VXv(hoC&U<60|il&;@ zu7;BFD*vt6opzqR}P32rEW?L_3+D1CVi3Pli9$i1Izu- z5tjecInQ$4BG${`ZlhVFtQSDcD)g|=xPfDFf!!f#LtsU zCEQ@0{cL@y`o0E7x5&iNkqkRx*L~u7cwK}LT28VQGBdrC#(Iaa%1LbMwXP45klbK) zQO>jk$#+TxV(IB@^;=YJ{-h5Qy0&@0@-nOG59WqOhp1;via-8i#$24aj8?kFu0nU} z!(T-9``wmVZiUbw6W=4{_7jCfi{;Lr&Gk^m;~ls2I~)OPkhx|cpK={O>;%XdU{C>M zbj_5XFd~2A7ZT6*Y#4AJzhez%1+y-wgkZO)D%34>x~ay_*PpWsi5tyN$zyN`E5g?)->^)5A*4Cph_Gw) zl-LswiGXB5Bq5N$Z5wQ-o5X{t-;~dnMt8v+_n-NCtq%5tB~TA6cbiMzN&mPo_q9S| zE_vuj7Sx54g*3u9pOf}C$M(xH-{o__H1ydYR|EN#o2$jH+Ki5Tx`P^KY%>?P_ zs>?i=UR-1y(hqpnWVWhAma~432fvQn)|BZUEZkcb$=>dvtDvP>26^ei*}Q##in9Li ziNc#*vfaY8A+wXHo`_-Qvs5MsqaTZuw+LY3XsoTZk{!ND(Uc9U=qaV7gR#E7b0aVW z^_Kou>jQrNnV7PzmN;G>GF6VT#Lv0_#XsyQH`=VdMa9B2;_2(Qa`_HxXrdind3lhC zU@a;@JkRcszIk78+ozi+kPj6^UpD{eC=2OEMRM8_Z=z)Jufr)5Q+kwA!f0P<-+H4$ zA4Rt|ItNHg=TxWQR2jcz&1KCcV|+9y0l;}Dk!*sj3N+Ow>&Sd;wqroTOcOn)q(^P- z&@%$~$)NHc`9?3#9VjU%1>2pAttz#h0&E8;8Gq=o%|%Eyp%SKQ&M}YHw#@S;NA@(M znblG&Bi7$Opj3PxG{?Bn-o}F}-gJ%2q}yB_d5^|d9(8fU15+p#0u8c}f0n?T{>T!9 zYVz5i1I`|gNW#x6R58!vGPC49wv7rMiRwiJ^SOccjFKD99@SkXt$MOMp8$IHn99+; z;<=mmhXUcK5Pdg8iLt&&HDA~iQA5G&NXmU9XH6H>>7%#j)zp?voOu^@bG<98!dK%* zPIzc93Q3mWKp7!a^0s6H65fU^1^Hk3na3HLMEKX;<~7*Z~Oz$$UfVUScO?YGIkLU{90rUz`5SQq|1d zPSCziWvbrC)z6iUe7dA&u3+nfd7OVUSZ6q8#V5D5*N}*<7(+0MLVA$HVdwSnUeQW= zS1nE3IDU>}%a~m#5zHJVX_3f&10FKP?5p(@UHc!IPCc=Q-04-0HhrDS$pFjV zM5l`?_~*KM@?aN+f-x^!#830M(ib0r`@#L~MMcd;53=yWgc>&6xyLp;D~!m90xtpF zvpqVH-tE~8@4}H#O$%YSKV{i)?9F3-d=m4x33-0fkV!UWbq}H8Md^tfX4}BsL8jJ% z#aBHePE@A#_?<_1#5i-jg2fsmt@VNs3d;2Rp?}LX8Db8c%gSf-a}SamS8V z;aGp@jPsmaFUa1?os*7bzNwe;1`R09s)V?|HRKg0sm+;;^@?MMEX;@fq zfml5}3g{l_QThyDO^~v*&;VN+ymKCpXYDLJ#UnVt1;g@AaB>S6gJc?gu=zq8h($xR z2=QCG(ACf=TO~QRLMnVf0KFI={Ul~`#RK|!Fopml&#doQpc(sl1o3f2Q z7&EJ*wzLxYOAlx}Y8_N=%c!IYpD?b|g++(u*L-%oAHDb#i)_JJJt^PwSn*gvTd}Y| z^^Lv#G2q9fuD$i~4@O24NujyMhsP)^YEzlsPtwxJWNkDng0#_IdC&1>_J&74<5^=RW0xcVM9`RAhH;RXE2%JKvDU^=^orn|9Msq!mM zb?=j;lmp`>JJ)X{6xfVd<+>)gW%;lR6P-+B9Va?=l$~Pxr%CVYSJ};dkzQL#@XHD% zAxGlzH=EJf&{|Kjhj*Vd(u|L&Dbop_4}eCim>Lgh2qFn43CetTF}d?^q`YshjLAV=RvBs2XnSNSGEos?8l`y+?73gYZ`(8rjwD zAus#I&B%D(UZ&34HR!-fh70ZIY|(Eq=6@8s;Tyo?m)#wET*c2sJeF~w^a=@`zg39) zy}B&yH7e-yx0g+aNQBZuD^YWGvvs*$Tt1dd%__yTlzQUQx!h33bu*wJK z10Q1X_0#noAPd@^$fLK$bH^Eltd}&*MB!p^5jgO*+gH*AOz_El?KHWsPRt>%kT<@f z5cX=@{R=pQDkc`A$VX0_zy%I`S`2=a2jb7yXh+|60TA37n?^? zi?8k8zm<$K2Hl!Z0`uAhoL1~xlM!eV82Bj|UcA@m=R<{pl4WVdnU8yqd)wn?h7B`G z{>$45y4M-ojCD8C{xW|Qha0sTWg)en>(`z(`~MnewOyI~P^vsjA(>Os^xa7=o%aiG zdJEz3rPbLcnEe?cgL=eFe=^-mSp2)nKyudI>h1QfWzCAS{vrJInH>Q+2-4o;2}L+q z3t0F0oah4Xt~f98AKJ??$}uvVc*Qb&@7t$wkN8Idj|5(Y;WxgrzSA9TKKQt=xENad z;K)J6OeJ$X(mY}8w<`?xx$k5*Svc1;w#0C(S?oRZ6Rwt?r*t}V zAi}ygaoUtW$QxwaFrH^>`?z!KS_GcoloVaREGPNg4VV9+DUZyI1`D&u|7i&8%yGkl zFwC_~*8cug(}OPD1_-Nnmn3SjGsa7}udB6ELe%FAf%)SRYHSg^f`3mAe`S7tz!>q( zLABSwUmGJL`Fwlga~op&y&nt*_gTP3_n582FdkxeU9)ldUiCcR{rA+7UH#5=0~u)z ze;XccX9)$5fhFq(4pvUi$m<3+zf8>IipHCKki$+?Fpx>8bh>;kHrTIm+^mqhe===z zEW~rhI<2qm@iy1^?6Lk`WtgI(LYShSY#Z-w0f7RtBzS1dvAp%c1#i?H0Io82YRW=B`UfYq zG@klT>f!XujM2Y@AJYG&nYO0Fo(cJo@T-5Q`P?#7g7~KcgtGZWHa_)VW5={s~bMQgn zM8=2=o7KdYT@ePq|59rQhnUM0&?{4jLj@>xQ$E&NGU&w2aYHf_-t}|K64m||VYgtbIn4ZDi)UD#xxO~n3Uf{cn zdpmv^Ztu+j!TOZeeX!(~f1MwMQSak%u5 zw)K~%b#5190i5`~Y&;YL) zr0}%{uO!H}6tIDzX_U=k=_vq`X3Kkms$ZwJ07a*=bPS9Okm|Bvkn8N#lYV&;4g!XO zMEnJqNFraFG0pHqrPpJ_lES(Fam`~Iz1JWft0QNyXCY(7+7I@ZBL>rmg24F1+C!}t zCHr^(;gGL;b~PAdH*8mX6$*jAubA2)JyjKNT4hIjQR-RcPcBA3kWK&wCZwkhFrS}X zn&gOR1GWtIifO`HzgPW-F%=b0{KK z;oYp}6R_h?DKrJ5{>@Y=j&~wi9v7T{;gqT0z5UqE;O4=4anf-QCgniV_d3dxf%I9m zcU{h1q0Ligh*Y*+-HP3{;#TD{YE1K2QAi0=CKQg-n^Fv)3ZzHwqx zJNp)6JMpRmvAvet-I4dqK=n(aah&3+-^J4h05*rsI#yhkUyMmz4m4|84+LU33xW3q zNxPcBj_oq{snOn+2JF9NwHNOnuppzU9E@b+$tDKx#TQe zR7uW--~(b$Z>FAEa1DN)Yd1Bo#IRDlC~GA35=jyC+;Cq#peeH;O+RIgmnYlO#yPZg z2J5nx2diqN@LTPv?MsguDyuUTopZAl-&E>61wJLs(w|D7NS{amNx*XF;Q2c7p`5aY zaYTUfAkV^jVuNp&y0O#zy5Fxfc~VF?T_79q-LzhOiF9A@_g$DfIwMt*r(XOA;@f3A zkyk&@biE=kt7Y+`JV<9=XdY_r#!h{n`cytKg_{#8v0o)T0gh-+dWm*z-ftX!La`tm zD&@Ly1^CXdzPcIDT#y*%0USWwFNnjh`!<+!*bMq&8laJv(;bMiGum~-n~Yc0EX2ZV zez@s{4^$_0*Lc;5djD>xldS>gqK#KbYV;~X zb34bPuuZ;6#C@^uoS5tO5O~ZS?bVufzL;s^WyK`Z;_`-?qSVv3)|;eY!TJ=%2OG>V z^e?jvqw(I`Eo3t11=-k-+dWuIgDEB8i(P6y`Tp?bSY0pD8oE$?|M$;NwnZY~OJxM2 z2wg2jIF3Z1WM==XdJeqnJUSM%_HBujrMdjzFGGz2KCV=$8Xky;P-NTm@f#*EUJP ztn=)Tz>06lbxo6X*3M|OQCyEd_Un-PB6M6(mEdkOnQd}_z0&zjD_xv0IXXFPc1R4&4@8Cydj5gY1~R#r?fn Uyi{#7sfz%u$GVTo)oh~v7hrP1i2wiq literal 0 HcmV?d00001 diff --git a/web/src/icons.js b/web/src/icons.js new file mode 100644 index 0000000..b6b2532 --- /dev/null +++ b/web/src/icons.js @@ -0,0 +1,54 @@ +// ===================== SVG 图标库 ===================== +// Feather 风格线性图标(stroke=currentColor,随文字颜色变化) +// 用法: + +import { h } from 'vue' + +export const paths = { + // 通用 + zap: '', + memory: '', + rocket: '', + settings: '', + shield: '', + fileText: '', + cpu: '', + hardDrive: '', + box: '', + lock: '', + search: '', + refresh: '', + check: '', + x: '', + camera: '', + rotate: '', + globe: '', + gauge: '', + activity: '', + power: '', + external: '', +} + +export const Icon = { + name: 'Icon', + props: { + name: { type: String, required: true }, + size: { type: [Number, String], default: 16 }, + }, + render() { + return h('svg', { + class: 'icon', + width: this.size, + height: this.size, + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + 'stroke-width': 2, + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + innerHTML: paths[this.name] || '', + }) + }, +} + +export default Icon diff --git a/web/src/main.js b/web/src/main.js new file mode 100644 index 0000000..cb54c4b --- /dev/null +++ b/web/src/main.js @@ -0,0 +1,6 @@ +import 'gm-app-sdk'; +import { createApp } from 'vue' +import './style.css' +import App from './App.vue' + +createApp(App).mount('#app') diff --git a/web/src/params.js b/web/src/params.js new file mode 100644 index 0000000..7fdf013 --- /dev/null +++ b/web/src/params.js @@ -0,0 +1,190 @@ +// ===================== 内核参数知识库 ===================== +// 每个参数:中文名、说明、风险等级(risk: safe/warn/danger)、推荐值、单位/格式提示 +// 用于可视化展示与编辑引导 + +export const PARAM_GROUPS = [ + { + key: 'network', + name: '网络', + icon: 'globe', + params: { + 'net.ipv4.tcp_congestion_control': { + name: 'TCP 拥塞控制算法', + desc: '控制 TCP 在高延迟/丢包下的发送策略。BBR 由 Google 开发,对高带宽长距离链路提升显著', + risk: 'safe', recommend: 'bbr', example: 'cubic / bbr', + }, + 'net.core.default_qdisc': { + name: '默认队列规则', + desc: '与 BBR 配套使用的排队规则,fq(fair queue) 可平滑突发流量、降低延迟', + risk: 'safe', recommend: 'fq', example: 'pfifo_fast / fq', + }, + 'net.ipv4.tcp_fastopen': { + name: 'TCP Fast Open', + desc: '允许在 TCP 握手期间携带数据,减少一次 RTT。3 = 客户端+服务端都启用', + risk: 'warn', recommend: '3', example: '0=关 / 1=客户端 / 2=服务端 / 3=全开', + }, + 'net.ipv4.tcp_slow_start_after_idle': { + name: '空闲后慢启动', + desc: '连接空闲后是否重置拥塞窗口。设为 0 可让长连接保持高吞吐,对 Web 服务友好', + risk: 'safe', recommend: '0', example: '0 / 1', + }, + 'net.ipv4.tcp_mtu_probing': { + name: 'MTU 探测', + desc: '探测路径 MTU,避免分片。1 = 在 DF 标志下探测', + risk: 'safe', recommend: '1', example: '0 / 1', + }, + 'net.core.somaxconn': { + name: '最大连接队列', + desc: 'socket 监听队列上限,高并发服务建议调大(配合应用 backlog 配置)', + risk: 'safe', recommend: '1024~4096', example: '128~65535', + }, + 'net.ipv4.tcp_max_syn_backlog': { + name: 'SYN 半连接队列', + desc: '未完成三次握手的连接队列长度,抗 SYN 洪水、高并发连接有用', + risk: 'safe', recommend: '4096~8192', example: '128~65535', + }, + 'net.ipv4.tcp_syncookies': { + name: 'SYN Cookies', + desc: 'SYN 洪水防护,正常情况下保持 1 开启即可', + risk: 'safe', recommend: '1', example: '0 / 1', + }, + 'net.ipv4.tcp_tw_reuse': { + name: 'TIME_WAIT 复用', + desc: '允许复用 TIME_WAIT 状态的连接。[NAT 环境下可能引发连接异常,需谨慎]', + risk: 'danger', recommend: '1(谨慎)', example: '0 / 1', + }, + 'net.ipv4.ip_local_port_range': { + name: '本地端口范围', + desc: '发起连接可用的本地端口区间,范围越大并发出站连接越多', + risk: 'warn', recommend: '1024 65535', example: '1024 65535', + }, + 'net.core.rmem_max': { + name: '最大接收缓冲区', + desc: '所有协议的最大接收缓冲。过大浪费内存,需与内存匹配', + risk: 'danger', recommend: '16777216(16MB)', example: '整数(字节)', + }, + 'net.core.wmem_max': { + name: '最大发送缓冲区', + desc: '所有协议的最大发送缓冲。过大浪费内存', + risk: 'danger', recommend: '16777216(16MB)', example: '整数(字节)', + }, + 'net.ipv4.tcp_rmem': { + name: 'TCP 接收缓冲范围', + desc: 'TCP 接收缓冲的动态范围(最小值 默认值 最大值),不当设置影响吞吐', + risk: 'danger', recommend: '4096 87380 16777216', example: '三个整数', + }, + 'net.ipv4.tcp_wmem': { + name: 'TCP 发送缓冲范围', + desc: 'TCP 发送缓冲的动态范围,不当设置影响吞吐', + risk: 'danger', recommend: '4096 16384 16777216', example: '三个整数', + }, + }, + }, + { + key: 'memory', + name: '内存', + icon: 'memory', + params: { + 'vm.swappiness': { + name: 'Swappiness 交换倾向', + desc: '内核使用 swap 的积极程度(0-100)。服务器建议 10,数据库 0-1,桌面 60。调优核心参数', + risk: 'danger', recommend: '10(服务器) / 0-1(数据库)', example: '0~100', + }, + 'vm.vfs_cache_pressure': { + name: '缓存回收压力', + desc: '内核回收目录/文件缓存(dentry/inode)的倾向。值越低缓存保留越久,但内存不足时可能 OOM', + risk: 'danger', recommend: '50~100', example: '0~1000', + }, + 'vm.dirty_ratio': { + name: '脏页比例(全局)', + desc: '进程写入时脏页占内存比例上限,超过后强制写回。调高提升写吞吐但崩溃丢数据风险增大', + risk: 'warn', recommend: '20~30', example: '0~100(百分比)', + }, + 'vm.dirty_background_ratio': { + name: '脏页后台写回阈值', + desc: '脏页达到该比例时后台开始写回。建议为 dirty_ratio 的一半左右', + risk: 'warn', recommend: '10~15', example: '0~100(百分比)', + }, + 'vm.overcommit_memory': { + name: '内存过载策略', + desc: '0=启发式 1=总是允许 2=禁止过载。数据库/大内存应用建议评估后再改', + risk: 'danger', recommend: '0(默认)', example: '0 / 1 / 2', + }, + }, + }, + { + key: 'fs', + name: '文件系统', + icon: 'hardDrive', + params: { + 'fs.file-max': { + name: '系统最大文件句柄', + desc: '全系统可打开的文件描述符总数上限。高并发服务需要调大', + risk: 'safe', recommend: '65535~1048576', example: '整数', + }, + 'fs.inotify.max_user_watches': { + name: 'inotify 监听上限', + desc: '单个用户可创建的 inotify 监听数量。文件监听类应用(如 Node 热重载)不够会报错', + risk: 'safe', recommend: '524288', example: '整数', + }, + 'fs.aio-max-nr': { + name: '异步 IO 上限', + desc: '系统级异步 IO 请求数上限,数据库/高 IO 应用需要', + risk: 'safe', recommend: '1048576', example: '整数', + }, + }, + }, + { + key: 'kernel', + name: '内核', + icon: 'settings', + params: { + 'kernel.pid_max': { + name: 'PID 上限', + desc: '系统最大进程数。进程多的服务器(容器/常驻任务)建议调大', + risk: 'safe', recommend: '4194304(64位)', example: '整数', + }, + 'kernel.threads-max': { + name: '线程上限', + desc: '系统最大线程数', + risk: 'safe', recommend: '跟随 pid_max', example: '整数', + }, + 'kernel.core_uses_pid': { + name: 'core 文件带 PID', + desc: 'core dump 文件名附加 PID,方便多进程崩溃定位', + risk: 'safe', recommend: '1', example: '0 / 1', + }, + 'kernel.sysrq': { + name: 'SysRq 魔术键', + desc: '紧急系统操作快捷键开关。服务器建议 1,云主机可能被限制', + risk: 'warn', recommend: '1', example: '0 / 1', + }, + }, + }, +] + +// 拍平所有参数(供搜索) +export const ALL_PARAMS = (() => { + const map = {} + for (const g of PARAM_GROUPS) { + for (const [key, info] of Object.entries(g.params)) { + map[key] = { ...info, group: g.name, groupKey: g.key } + } + } + return map +})() + +export function searchParams(keyword) { + const kw = (keyword || '').trim().toLowerCase() + if (!kw) return Object.entries(ALL_PARAMS) + return Object.entries(ALL_PARAMS).filter(([key, info]) => + key.toLowerCase().includes(kw) || info.name.includes(kw) || info.desc.includes(kw) + ) +} + +// 风险徽章 +export const RISK_META = { + safe: { label: '安全', color: '#34d399' }, + warn: { label: '谨慎', color: '#f59e0b' }, + danger: { label: '危险', color: '#ef4444' }, +} diff --git a/web/src/style.css b/web/src/style.css new file mode 100644 index 0000000..30e4e77 --- /dev/null +++ b/web/src/style.css @@ -0,0 +1,3 @@ +:root { + +} diff --git a/web/vite.config.js b/web/vite.config.js new file mode 100644 index 0000000..7221347 --- /dev/null +++ b/web/vite.config.js @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [vue()], + base: './', + server: { + host: '0.0.0.0', + allowedHosts: true, + }, + build: { + outDir: 'www' + } +})