ezllm 0.0.28 → 0.0.29

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.
Files changed (50) hide show
  1. package/README.md +8 -0
  2. package/bin/ezllm.js +3 -0
  3. package/dist/cli.js +39 -24512
  4. package/dist/daemon.js +25 -0
  5. package/dist/server.js +202 -41916
  6. package/dist/web-ui/assets/DashboardView-5ujMkgws.js +1 -0
  7. package/dist/web-ui/assets/DashboardView-CZy4j9_m.css +1 -0
  8. package/dist/web-ui/assets/{LayoutView-C8YPw0x2.js → LayoutView-C-x9LlJB.js} +1 -1
  9. package/dist/web-ui/assets/{LoginView-C4hkT-3T.js → LoginView-Dgr8SbIP.js} +1 -1
  10. package/dist/web-ui/assets/LogsView-BZCLikyn.css +1 -0
  11. package/dist/web-ui/assets/LogsView-DME2_Vhc.js +9 -0
  12. package/dist/web-ui/assets/ProvidersView-B__welRn.css +1 -0
  13. package/dist/web-ui/assets/ProvidersView-CcTikZnF.js +14 -0
  14. package/dist/web-ui/assets/ServiceStatusView-C4N2XQ_w.js +2 -0
  15. package/dist/web-ui/assets/ServiceStatusView-CwXVx_18.css +1 -0
  16. package/dist/web-ui/assets/SettingsView-0a5M4naA.css +1 -0
  17. package/dist/web-ui/assets/SettingsView-Cck08cQ7.js +1 -0
  18. package/dist/web-ui/assets/{SetupView-BZFBBRSd.js → SetupView-DbiSodBy.js} +1 -1
  19. package/dist/web-ui/assets/clipboard-JYWVdyjq.js +1 -0
  20. package/dist/web-ui/assets/{config-Dbms19Et.js → config-DcxW5IHq.js} +1 -1
  21. package/dist/web-ui/assets/format-OBe8peUU.js +1 -0
  22. package/dist/web-ui/assets/{index-C6wv4UB9.js → index-Bxg3HL8C.js} +1 -1
  23. package/dist/web-ui/assets/{index-BNieAChJ.css → index-DmoPyGio.css} +1 -1
  24. package/dist/web-ui/assets/{message-CEBSyhlN.js → message-BfNKbnQ4.js} +1 -1
  25. package/dist/web-ui/assets/providers-DBzc7lYt.js +1 -0
  26. package/dist/web-ui/assets/{router-BqkHX02I.js → router-BbFt1lFf.js} +2 -2
  27. package/dist/web-ui/assets/useCopyToast-SPDhUFJc.js +1 -0
  28. package/dist/web-ui/assets/{useManagerApi-fVrRDZkc.js → useManagerApi-B65ileRW.js} +1 -1
  29. package/dist/web-ui/assets/useToast-D0wtmNdH.js +1 -0
  30. package/dist/web-ui/assets/{version-quqH5oMc.js → version-DegxwpkG.js} +1 -1
  31. package/dist/web-ui/index.html +4 -4
  32. package/package.json +8 -3
  33. package/bin/ellm-proxy.js +0 -3
  34. package/dist/web-ui/assets/DashboardView-ChJpgKis.js +0 -1
  35. package/dist/web-ui/assets/DashboardView-DOtZtkUN.css +0 -1
  36. package/dist/web-ui/assets/LogsView-D7xJOKSv.css +0 -1
  37. package/dist/web-ui/assets/LogsView-DdS8qKEs.js +0 -9
  38. package/dist/web-ui/assets/ProvidersView-BD33caY8.js +0 -14
  39. package/dist/web-ui/assets/ProvidersView-DNqX_AWw.css +0 -1
  40. package/dist/web-ui/assets/ServiceStatusView-BtF40E0V.js +0 -2
  41. package/dist/web-ui/assets/ServiceStatusView-DXbn--55.css +0 -1
  42. package/dist/web-ui/assets/SettingsView-BY2QIyXL.css +0 -1
  43. package/dist/web-ui/assets/SettingsView-SdL8ednl.js +0 -1
  44. package/dist/web-ui/assets/clipboard-CorU9X8e.js +0 -1
  45. package/dist/web-ui/assets/format-DrbP4_cY.js +0 -1
  46. package/dist/web-ui/assets/providers-DkujdEZ8.js +0 -1
  47. package/dist/web-ui/assets/useLoadError-Yu7YH4ZH.js +0 -1
  48. package/dist/web-ui/assets/useToast-DJwG-33L.js +0 -1
  49. package/docs/DESIGN.md +0 -98
  50. /package/{hook.example.js → hook.example.cjs} +0 -0
@@ -1 +0,0 @@
1
- .quota-block[data-v-654b578d]{background:#fafbfc;border:1px solid #f0f2f5;border-radius:6px;margin-bottom:4px;padding:8px 10px}.quota-header[data-v-654b578d]{justify-content:space-between;align-items:center;gap:8px;margin-bottom:6px;display:flex}.quota-block-title[data-v-654b578d]{color:#303133;flex-shrink:0;font-size:13px;font-weight:600}.quota-header-stats[data-v-654b578d]{flex-wrap:wrap;justify-content:flex-end;align-items:center;gap:4px;min-width:0;display:flex}.quota-hint[data-v-654b578d]{color:#c0c4cc;font-size:12px}.quota-error[data-v-654b578d]{color:#f56c6c;word-break:break-all;margin-bottom:4px;font-size:12px}.quota-row[data-v-654b578d]{align-items:flex-start;gap:8px;padding:3px 0;display:flex}.quota-label[data-v-654b578d]{color:#909399;flex-shrink:0;width:72px;font-size:12px;line-height:18px}.quota-main[data-v-654b578d]{flex:1;min-width:0}.quota-main[data-v-654b578d] .el-progress{width:100%}.quota-main[data-v-654b578d] .el-progress-bar__outer{background-color:#f2f2f2}.quota-main[data-v-654b578d] .el-progress-bar__inner{align-items:center;display:flex}.quota-main[data-v-654b578d] .el-progress-bar__innerText{color:#303133;white-space:nowrap;margin:0 5px;font-size:12px;line-height:1}.quota-cap-null[data-v-654b578d]{color:#606266;font-size:12px}.provider-card[data-v-44bd817a]{cursor:grab;min-width:0;transition:box-shadow .2s,border-color .2s}.provider-card[data-v-44bd817a] .el-card__header{padding-bottom:10px}.provider-card[data-v-44bd817a] .el-card__body{padding-top:12px}.provider-card[data-v-44bd817a]:hover{border-color:#b1d2ff;box-shadow:0 4px 16px #3b82f61a}.provider-card.dragging[data-v-44bd817a]{cursor:grabbing}.provider-card.is-disabled[data-v-44bd817a]{background:#fafafa}.provider-card.is-disabled .provider-name[data-v-44bd817a]{color:#a8abb2}.provider-card.is-disabled[data-v-44bd817a] .el-card__header{background:#fafafa}.card-header-right[data-v-44bd817a]{flex-shrink:0;align-items:center;gap:10px;display:flex}.header-switch[data-v-44bd817a]{cursor:auto;align-items:center;display:flex}.header-switch[data-v-44bd817a] .el-switch{cursor:pointer}.card-header[data-v-44bd817a]{justify-content:space-between;align-items:center;gap:12px;display:flex}.card-title[data-v-44bd817a]{align-items:center;gap:8px;min-width:0;display:flex}.provider-name[data-v-44bd817a]{color:#303133;text-overflow:ellipsis;white-space:nowrap;font-size:15px;font-weight:600;overflow:hidden}.card-actions[data-v-44bd817a]{cursor:auto;flex-shrink:0;gap:0;display:flex}.model-tags[data-v-44bd817a]{flex-wrap:wrap;gap:6px;margin-top:10px;display:flex}.model-tag[data-v-44bd817a]{color:#606266;cursor:pointer;text-overflow:ellipsis;white-space:nowrap;background:#f5f7fa;border:1px solid #0000;border-radius:999px;align-items:center;max-width:100%;padding:3px 10px;font-size:12px;line-height:18px;transition:color .15s,background .15s;display:inline-flex;overflow:hidden}.model-tag[data-v-44bd817a]:hover{color:#3b82f6;background:#eff6ff}.model-tag.ok[data-v-44bd817a]{color:#16a34a;background:#f0fdf4}.model-tag.error[data-v-44bd817a]{color:#dc2626;background:#fef2f2}.badge-mark[data-v-44bd817a]{margin-left:3px}.card-footer[data-v-44bd817a]{border-top:1px solid #f0f2f5;justify-content:space-between;align-items:center;margin-top:14px;padding-top:12px;display:flex}.model-count[data-v-44bd817a]{color:#16a34a;cursor:pointer;border-radius:4px;font-size:13px;font-weight:500}.model-count[data-v-44bd817a]:hover{text-decoration:underline}.drag-hint-icon[data-v-44bd817a]{color:#c0c4cc;cursor:grab;flex-shrink:0}.drag-hint-icon[data-v-44bd817a]:hover{color:#909399}.detail-body[data-v-08dfa117]{max-height:70vh;overflow-y:auto}.detail-head[data-v-08dfa117]{align-items:center;gap:12px;margin-bottom:12px;display:flex}.detail-name[data-v-08dfa117]{font-size:14px;font-weight:600}.detail-pre[data-v-08dfa117]{color:#303133;white-space:pre-wrap;word-break:break-all;background:#f5f7fa;border:1px solid #e4e7ed;border-radius:6px;max-height:300px;margin:0;padding:12px;font-size:12px;line-height:1.6;overflow:auto}.protocol-help .help-intro[data-v-2248b61a]{color:#606266;margin-bottom:14px;font-size:13px}.protocol-help .help-item[data-v-2248b61a]{margin-bottom:12px}.protocol-help .help-title[data-v-2248b61a]{color:#303133;align-items:center;gap:8px;margin-bottom:4px;font-size:14px;font-weight:600;display:flex}.protocol-help .help-item p[data-v-2248b61a]{color:#606266;margin:0;font-size:13px;line-height:1.7}.protocol-help .help-table-note[data-v-2248b61a]{margin-bottom:8px}.protocol-help .help-table[data-v-2248b61a]{width:100%}.model-select-wrap[data-v-ff3f761b]{flex-shrink:0;width:400px}.test-result-link[data-v-ff3f761b]{font-size:12px}.col-help[data-v-ff3f761b]{margin-right:4px}.col-help-icon[data-v-ff3f761b]{color:#c0c4cc;cursor:help;vertical-align:-2px}.col-help-icon[data-v-ff3f761b]:hover{color:#909399}.dialog-body[data-v-cc9f2dd1]{max-height:80vh;padding-bottom:100px;overflow:hidden auto}.test-ok-text[data-v-cc9f2dd1]{color:#16a34a;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle;max-width:140px;font-size:12px;display:inline-block;overflow:hidden}.test-fail-text[data-v-cc9f2dd1]{color:#dc2626;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle;max-width:140px;font-size:12px;display:inline-block;overflow:hidden}.model-select-wrap[data-v-cc9f2dd1]{flex-shrink:0;width:400px}.protocol-hint[data-v-cc9f2dd1]{line-height:1.8}.label-action-btn[data-v-cc9f2dd1]{height:auto;margin-left:8px;font-size:12px}.label-inline-toggle[data-v-cc9f2dd1]{align-items:center;gap:6px;margin-left:8px;display:inline-flex}.test-result-link[data-v-cc9f2dd1]{font-size:12px}.col-help[data-v-cc9f2dd1]{margin-right:4px}.col-help-icon[data-v-cc9f2dd1]{color:#c0c4cc;cursor:help;vertical-align:-2px}.col-help-icon[data-v-cc9f2dd1]:hover{color:#909399}.models-dialog-body[data-v-7b39dd25]{flex-direction:column;height:calc(94vh - 100px);display:flex}.models-dialog-body .models-table[data-v-7b39dd25]{flex:1;min-height:0}.error-tag[data-v-7b39dd25]{text-overflow:ellipsis;white-space:nowrap;max-width:100px;overflow:hidden}.quota-help[data-v-7bec5b3a]{background:#fff;height:70vh;overflow:hidden auto}.quota-help .help-intro[data-v-7bec5b3a]{color:#606266;margin-bottom:14px;font-size:13px;line-height:1.7}.quota-help .help-item[data-v-7bec5b3a]{margin-bottom:12px}.quota-help .help-title[data-v-7bec5b3a]{color:#303133;margin-bottom:4px;font-size:14px;font-weight:600}.quota-help .help-item p[data-v-7bec5b3a]{color:#606266;margin:0;font-size:13px;line-height:1.7}.quota-help .help-tip[data-v-7bec5b3a]{color:#909399!important;margin-top:4px!important}.quota-help .help-shot[data-v-7bec5b3a]{border:1px solid #dcdfe6;border-radius:6px;width:100%;margin:8px 0;display:block}.quota-help code[data-v-7bec5b3a]{background:#f5f7fa;border-radius:3px;padding:1px 4px;font-size:12px}.quota-help .help-table[data-v-7bec5b3a]{width:100%}.footer[data-v-7bec5b3a]{place-content:center;display:flex}.dialog-body[data-v-39c58b29]{min-height:70vh;overflow-y:auto}.muted-sm[data-v-39c58b29]{color:#909399;font-size:12px;line-height:1.6}.status-row[data-v-39c58b29]{margin-top:4px}.test-ok-text-full[data-v-39c58b29]{color:#16a34a;font-size:12px}.test-fail-text-full[data-v-39c58b29]{color:#dc2626;word-break:break-all;font-size:12px}.label-action-btn[data-v-39c58b29]{height:auto;margin-left:8px;font-size:12px}.col-help-icon[data-v-39c58b29]{color:#c0c4cc;cursor:help;vertical-align:-2px;margin-left:4px}.col-help-icon[data-v-39c58b29]:hover{color:#909399}.ghost[data-v-a49f316c]{opacity:.4}.provider-grid[data-v-a49f316c]{grid-template-columns:repeat(2,1fr);gap:16px;display:grid}@media (width<=992px){.provider-grid[data-v-a49f316c]{grid-template-columns:1fr}}
@@ -1,2 +0,0 @@
1
- import{$ as e,A as t,Et as n,F as r,Ft as i,Gt as a,H as o,I as s,L as c,M as l,N as u,Q as d,T as f,a as p,at as m,cn as ee,d as h,gt as g,ht as _,j as v,k as y,ot as b,pn as x,rt as S,tt as C,un as w,yt as T}from"./router-BqkHX02I.js";import{t as E}from"./useToast-DJwG-33L.js";import{t as D}from"./useLoadError-Yu7YH4ZH.js";import{t as te}from"./clipboard-CorU9X8e.js";import{c as ne,i as O}from"./format-DrbP4_cY.js";o(),T(),a();var re={class:`service-status-page`},ie={class:`flex-between`},ae={class:`flex-gap-8`},oe={class:`flex-gap-8`},se={class:`gantt-wrap`},ce={key:0,class:`gantt`},le={class:`gantt-row gantt-head`},ue={class:`col-cells`},de=[`title`],fe={class:`cell-head-date`},pe={class:`col-name`},k=[`title`,`onClick`],A=[`title`],j={class:`col-cells`},M=[`title`],N={key:1,class:`gantt-empty`},P=36e5,F=24,I=`#909399`,L=h(c({__name:`ServiceStatusView`,setup(a){let{toast:o}=E(),{loadError:c,run:h}=D(`延迟数据加载失败,请重试`),T=n([]),L=n(!0),R=n(!1),z=n(null),B=n(!1),V=n(!1),H,U,W=n(K()),G=n(new Map);function K(){let e=new Date,t=new Date(e.getFullYear(),e.getMonth(),e.getDate(),e.getHours()).getTime();return Array.from({length:F},(e,n)=>t-(23-n)*P)}let q=e=>{let t=new Date(e);if(Number.isNaN(t.getTime()))return`-`;let n=e=>String(e).padStart(2,`0`);return`${t.getFullYear()}-${n(t.getMonth()+1)}-${n(t.getDate())} ${n(t.getHours())}:00`},me=y(()=>W.value.map(e=>{let t=new Date(e),n=e=>String(e).padStart(2,`0`),r=n(t.getHours());return{start:e,dateLabel:`${n(t.getMonth()+1)}-${n(t.getDate())}`,hourLabel:r,fullLabel:`${q(e)} - ${r}:59`}})),he=[103,194,58],J=[230,162,60],ge=[245,108,108],_e=y(()=>{let e=[];for(let t of G.value.values())for(let n of t.values())n.avgLatencyMs!==null&&e.push(n.avgLatencyMs);if(e.length===0)return{min:0,max:0};let t=e[0],n=e[0];for(let r of e)r<t&&(t=r),r>n&&(n=r);return{min:t,max:n}}),Y=e=>{let{min:t,max:n}=_e.value,r=n===t?0:(e-t)/(n-t),[i,a]=r<=.5?[he,J]:[J,ge],o=r<=.5?r*2:(r-.5)*2,s=i.map((e,t)=>Math.round(e+(a[t]-e)*o));return`rgba(${String(s[0])}, ${String(s[1])}, ${String(s[2])}, 0.9)`},X=(e,t)=>{let n=`${q(t)} - ${String(new Date(t+P).getHours()).padStart(2,`0`)}:00`;return e===void 0||e.total===0?`${n}\n无探测数据`:`${n}\n平均延迟 ${O(e.avgLatencyMs)}\n成功 ${String(e.success)} / 失败 ${String(e.failed)}`},Z=y(()=>ye.value.map(e=>{let t=G.value.get(e.model),n=W.value.map(e=>{let n=t?.get(e);return n!==void 0&&n.failed>0?{start:e,className:`cell--down`,style:{backgroundColor:I},title:X(n,e)}:n===void 0||n.avgLatencyMs===null?{start:e,className:`cell--empty`,style:{},title:X(n,e)}:{start:e,className:``,style:{backgroundColor:Y(n.avgLatencyMs)},title:X(n,e)}});return{model:e.model,avg24h:e.avg24h,statusTitle:[e.avg24h===null?`最近 24 小时无成功探测记录`:`最近 24 小时成功探测平均延迟 ${O(e.avg24h)}`,e.lastLatencyMs==null?`最近一次探测:无成功延迟记录`:`最近一次探测延迟 ${O(e.lastLatencyMs)}`].join(`
2
- `),cells:n}})),ve=(e,t)=>e===null&&t===null?0:e===null?1:t===null?-1:e-t,ye=y(()=>[...T.value].sort((e,t)=>{let n=e.lastOk===!1;if(n!==(t.lastOk===!1))return n?1:-1;let r=ve(e.avg24h,t.avg24h);return r===0?e.model.localeCompare(t.model):r})),Q=async e=>{if(e.length===0){G.value=new Map;return}let t=await Promise.all(e.map(async e=>{try{let t=await p.get(`/api/latency/trend?model=${encodeURIComponent(e)}&granularity=hour`);return[e,new Map((t.buckets??[]).map(e=>[e.bucketStart,e]))]}catch{return[e,new Map]}}));G.value=new Map(t)},$=()=>h(async()=>{try{let e=await p.get(`/api/latency/history`);T.value=e.models??[],R.value=e.running,z.value=e.lastRunAt,W.value=K(),await Q(T.value.map(e=>e.model))}finally{L.value=!1}}),be=()=>$(),xe=()=>{V.value?H=window.setInterval(()=>void $(),5e3):H!==void 0&&(window.clearInterval(H),H=void 0)},Se=async()=>{B.value=!0;try{await p.post(`/api/latency/run`)}catch(e){o(e.message,`error`),B.value=!1;return}U=window.setInterval(async()=>{try{let e=await p.get(`/api/latency/history`);T.value=e.models??[],R.value=e.running,z.value=e.lastRunAt,e.running||(window.clearInterval(U),U=void 0,B.value=!1,W.value=K(),await Q(T.value.map(e=>e.model)))}catch{}},2e3)},Ce=async e=>{await te(e)?o(`已复制到剪贴板`,`success`):o(`复制失败`,`error`)};return d(()=>{$()}),e(()=>{H!==void 0&&window.clearInterval(H),U!==void 0&&window.clearInterval(U)}),(e,n)=>{let a=m(`el-button`),o=m(`el-alert`),d=m(`el-tag`),p=m(`el-checkbox`),h=m(`el-card`),y=b(`loading`);return C(),u(`div`,re,[i(c)?(C(),v(o,{key:0,title:i(c),type:`error`,"show-icon":``,closable:!1,class:`mb-16`},{default:_(()=>[s(a,{size:`small`,onClick:be},{default:_(()=>[...n[1]||=[r(`重试`,-1)]]),_:1})]),_:1},8,[`title`])):l(``,!0),s(h,{shadow:`never`,class:`main-card`},{header:_(()=>[t(`div`,ie,[t(`div`,ae,[n[3]||=t(`span`,{class:`fw-600`},`服务状态`,-1),R.value?(C(),v(d,{key:0,type:`warning`,size:`small`},{default:_(()=>[...n[2]||=[r(`测试进行中`,-1)]]),_:1})):z.value?(C(),v(d,{key:1,type:`info`,size:`small`},{default:_(()=>[r(` 上次测试 `+x(i(ne)(z.value)),1)]),_:1})):l(``,!0)]),t(`div`,oe,[s(p,{modelValue:V.value,"onUpdate:modelValue":n[0]||=e=>V.value=e,onChange:xe},{default:_(()=>[...n[4]||=[r(` 自动刷新(5s) `,-1)]]),_:1},8,[`modelValue`]),s(a,{loading:B.value,onClick:Se},{default:_(()=>[...n[5]||=[r(` 延迟测试 `,-1)]]),_:1},8,[`loading`])])])]),default:_(()=>[n[9]||=t(`div`,{class:`legend`},[t(`span`,{class:`legend-text`},`延迟`),t(`span`,{class:`legend-text`},`低`),t(`span`,{class:`legend-gradient`}),t(`span`,{class:`legend-text`},`高`),t(`span`,{class:`legend-swatch legend-swatch--down`}),t(`span`,{class:`legend-text`},`该小时有失败探测`),t(`span`,{class:`legend-swatch legend-swatch--empty`}),t(`span`,{class:`legend-text`},`无探测数据`)],-1),g((C(),u(`div`,se,[n[8]||=t(`div`,{class:`muted-sm hint`},` 右侧 24 格为最近 24 小时(每格 1 小时,本地整点对齐,最右格为当前小时)。 格子底色为当小时成功探测的平均延迟,全表统一色标(本期最快→最慢 绿→红); 当小时存在失败探测即视为不可用,显示灰色。 行按 24h 平均延迟升序,最近一次探测失败的模型沉底。 `,-1),Z.value.length>0?(C(),u(`div`,ce,[t(`div`,le,[n[6]||=t(`div`,{class:`col-name`},`模型名称`,-1),n[7]||=t(`div`,{class:`col-status`},`状态`,-1),t(`div`,ue,[(C(!0),u(f,null,S(me.value,e=>(C(),u(`div`,{key:e.start,class:`cell cell--head`,title:e.fullLabel},[t(`span`,fe,x(e.dateLabel),1),t(`span`,null,x(e.hourLabel),1)],8,de))),128))])]),(C(!0),u(f,null,S(Z.value,e=>(C(),u(`div`,{key:e.model,class:`gantt-row`},[t(`div`,pe,[t(`span`,{class:`copy-text model-name`,title:e.model,onClick:t=>Ce(e.model)},x(e.model),9,k)]),t(`div`,{class:`col-status`,title:e.statusTitle},x(i(O)(e.avg24h)),9,A),t(`div`,j,[(C(!0),u(f,null,S(e.cells,e=>(C(),u(`div`,{key:e.start,class:ee([`cell`,e.className]),style:w(e.style),title:e.title},null,14,M))),128))])]))),128))])):(C(),u(`div`,N,`暂无延迟数据,点击「延迟测试」开始`))])),[[y,L.value]])]),_:1})])}}}),[[`__scopeId`,`data-v-0be6fcd8`]]);export{L as default};
@@ -1 +0,0 @@
1
- .service-status-page[data-v-0be6fcd8]{flex-direction:column;height:calc(100vh - 124px);display:flex;overflow:hidden}.main-card[data-v-0be6fcd8]{flex-direction:column;flex:1;min-height:0;display:flex;overflow:hidden}.main-card[data-v-0be6fcd8] .el-card__body{flex-direction:column;height:100%;display:flex;overflow:hidden}.legend[data-v-0be6fcd8]{color:#606266;flex-shrink:0;align-items:center;gap:6px;margin-bottom:8px;font-size:12px;display:flex}.legend-gradient[data-v-0be6fcd8]{background:linear-gradient(90deg,#67c23a,#e6a23c,#f56c6c);border-radius:2px;width:96px;height:10px}.legend-swatch[data-v-0be6fcd8]{border-radius:2px;width:14px;height:10px;margin-left:10px}.legend-swatch--down[data-v-0be6fcd8]{background-color:#909399}.legend-swatch--empty[data-v-0be6fcd8]{background-color:#f2f3f5;border:1px solid #ebeef5}.hint[data-v-0be6fcd8]{flex-shrink:0;margin-bottom:12px}.gantt-wrap[data-v-0be6fcd8]{flex-direction:column;flex:1;min-height:0;display:flex;overflow:hidden}.gantt[data-v-0be6fcd8]{flex:1;min-height:0;overflow:hidden auto}.gantt-row[data-v-0be6fcd8]{border-bottom:1px solid #f2f6fc;align-items:center;height:32px;display:flex}.gantt-row[data-v-0be6fcd8]:hover{background-color:#fafcff}.gantt-head[data-v-0be6fcd8]{z-index:1;color:#909399;background-color:#fff;border-bottom:1px solid #ebeef5;height:auto;min-height:34px;padding:2px 0;font-size:12px;font-weight:600;position:sticky;top:0}.col-name[data-v-0be6fcd8]{flex:0 0 240px;width:240px;padding-right:12px;font-size:13px}.col-status[data-v-0be6fcd8]{color:#606266;flex:0 0 100px;width:100px;padding-right:12px;font-size:13px}.col-cells[data-v-0be6fcd8]{flex:1;min-width:0;display:flex}.cell[data-v-0be6fcd8]{flex:1 1 0;min-width:0;height:20px;box-shadow:inset -1px 0 #fff}.cell[data-v-0be6fcd8]:last-child{box-shadow:none}.cell--head[data-v-0be6fcd8]{white-space:nowrap;flex-direction:column;align-items:center;height:auto;font-size:10px;font-weight:400;line-height:1.15;display:flex;overflow:hidden}.cell-head-date[data-v-0be6fcd8]{color:#c0c4cc;font-size:9px}.cell--empty[data-v-0be6fcd8]{background-color:#f2f3f5}.gantt-empty[data-v-0be6fcd8]{color:#909399;flex:1;justify-content:center;align-items:center;font-size:13px;display:flex}.copy-text[data-v-0be6fcd8]{color:#409eff;cursor:pointer}.copy-text[data-v-0be6fcd8]:hover{text-decoration:underline}.model-name[data-v-0be6fcd8]{white-space:nowrap;text-overflow:ellipsis;display:block;overflow:hidden}
@@ -1 +0,0 @@
1
- .switch-row[data-v-5084d579]{justify-content:space-between;align-items:center;gap:24px;padding:4px 0;display:flex}.switch-title[data-v-5084d579]{align-items:center;gap:8px;margin-bottom:4px;font-weight:600;display:flex}.switch-desc[data-v-5084d579]{color:var(--el-text-color-secondary);font-size:13px;line-height:1.6}.switch-desc code[data-v-5084d579]{background:var(--el-fill-color-light);border-radius:4px;padding:1px 6px;font-size:12px}.switch-divider[data-v-5084d579]{margin:16px 0}.mb-16[data-v-d51cfdf5]{margin-bottom:16px}.mt-8[data-v-d51cfdf5]{margin-top:8px}.switch-row[data-v-d51cfdf5]{justify-content:space-between;align-items:center;gap:24px;padding:4px 0;display:flex}.switch-title[data-v-d51cfdf5]{align-items:center;gap:8px;margin-bottom:4px;font-weight:600;display:flex}.switch-desc[data-v-d51cfdf5]{color:var(--el-text-color-secondary);font-size:13px;line-height:1.6}.switch-desc code[data-v-d51cfdf5]{background:var(--el-fill-color-light);border-radius:4px;padding:1px 6px;font-size:12px}.version-row[data-v-d51cfdf5]{align-items:center;gap:8px;margin-bottom:8px;display:flex}.version-label[data-v-d51cfdf5]{color:var(--el-text-color-secondary);font-size:13px}.version-actions[data-v-d51cfdf5]{gap:8px;margin-top:12px;display:flex}.settings-tabs[data-v-e1ea3ddd] .el-tabs__content{overflow:visible}.save-bar[data-v-e1ea3ddd]{justify-content:center;padding:8px 0;display:flex;position:sticky;bottom:0}.login-row[data-v-e1ea3ddd]{flex-wrap:wrap;gap:8px;display:flex}.monitor-row[data-v-e1ea3ddd]{justify-content:space-between;align-items:center;gap:24px;display:flex}.monitor-title[data-v-e1ea3ddd]{margin-bottom:4px;font-weight:600}
@@ -1 +0,0 @@
1
- import{$ as e,A as t,Et as n,F as r,Ft as i,Gt as a,H as o,I as s,L as c,M as l,N as u,Q as d,T as f,a as p,at as m,d as h,gt as g,ht as _,j as v,k as y,n as b,ot as x,pn as S,rt as C,tt as w,yt as T}from"./router-BqkHX02I.js";import{t as E}from"./index-C6wv4UB9.js";import{n as D,t as O}from"./useManagerApi-fVrRDZkc.js";import{t as k}from"./config-Dbms19Et.js";import{t as A}from"./useToast-DJwG-33L.js";import{t as j}from"./useLoadError-Yu7YH4ZH.js";import{t as M}from"./clipboard-CorU9X8e.js";import{n as N,t as P}from"./version-quqH5oMc.js";o(),a(),T();var F={class:`flex-between`},I={class:`muted-sm mb-8`},L={class:`switch-row`},R={class:`switch-info`},z={class:`switch-title`},B={class:`switch-row`},V={class:`switch-info`},H={class:`switch-title`},U={class:`switch-row`},W={class:`switch-info`},G={class:`switch-title`},K=h(c({__name:`HarnessSwitches`,setup(e){let{toast:i}=A(),a=n(!0),o=n(!1),c=n(null),p=n(null),h=n(null),y=n(null),b=n(`success`),C=async e=>{e||(c.value?o.value=!0:a.value=!0);try{c.value=await O.harness(),p.value=null}catch(t){e||(p.value=t.message)}finally{e||(a.value=!1,o.value=!1)}};d(()=>C(!1));let T={claudeCode:{label:`Claude Code`,file:`~/.claude/settings.json`,toggle:O.setClaudeCode},codex:{label:`Codex CLI`,file:`~/.codex/config.toml`,toggle:O.setCodex},dsh:{label:`DeepSeek Harness`,file:`~/.dsh/settings.yaml`,toggle:O.setDsh}},D=async(e,t)=>{if(!c.value||h.value)return;let n=T[e],r=t?`确定开启 ${n.label} 代理?将改写 ${n.file}(开启前自动备份,关闭时从备份还原)。`:`确定关闭 ${n.label} 代理?将从备份还原 ${n.file}(无备份时移除代理相关配置)。`;try{await E.confirm(r,t?`开启确认`:`关闭确认`,{confirmButtonText:t?`开启`:`关闭`,cancelButtonText:`取消`,type:`warning`})}catch{return}h.value=e;try{let e=await n.toggle(t);y.value=e.note??null,b.value=t?`success`:`info`,i(t?`已开启代理`:`已关闭代理`,`success`)}catch(e){i(e.message||`操作失败`,`error`)}finally{h.value=null,await C(!0)}};return(e,n)=>{let i=m(`el-button`),d=m(`el-alert`),T=m(`el-tag`),E=m(`el-switch`),O=m(`el-divider`),k=m(`el-empty`),A=m(`el-card`),j=x(`loading`);return g((w(),v(A,{shadow:`never`},{header:_(()=>[t(`div`,F,[n[7]||=t(`span`,{class:`fw-600`},`代理开关`,-1),s(i,{loading:o.value,onClick:n[0]||=e=>C(!1)},{default:_(()=>[...n[6]||=[r(`刷新`,-1)]]),_:1},8,[`loading`])])]),default:_(()=>[c.value?(w(),u(f,{key:0},[t(`div`,I,` 将本机 CLI 工具的流量一键指向本机网关(`+S(c.value.gatewayBase)+`)。 开启前自动备份原配置为 *.ellm-bak(保留最早的原始备份),关闭时从备份还原。 `,1),y.value?(w(),v(d,{key:0,title:y.value,type:b.value,"show-icon":``,class:`mb-8`,onClose:n[1]||=e=>y.value=null},null,8,[`title`,`type`])):l(``,!0),t(`div`,L,[t(`div`,R,[t(`div`,z,[n[8]||=r(` Claude Code `,-1),s(T,{type:c.value.claudeCode?`success`:`info`,size:`small`,"disable-transitions":``},{default:_(()=>[r(S(c.value.claudeCode?`已开启`:`未开启`),1)]),_:1},8,[`type`])]),n[9]||=t(`div`,{class:`switch-desc`},[r(` 写入 `),t(`code`,null,`~/.claude/settings.json`),r(` 的 env.ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN, 鉴权取网关配置的 masterKey(未设置将开启失败);其余字段原样保留,重启 Claude Code 会话后生效。 `)],-1)]),s(E,{"model-value":c.value.claudeCode,loading:h.value===`claudeCode`,onChange:n[2]||=e=>D(`claudeCode`,!!e)},null,8,[`model-value`,`loading`])]),s(O,{class:`switch-divider`}),t(`div`,B,[t(`div`,V,[t(`div`,H,[n[10]||=r(` Codex CLI `,-1),s(T,{type:c.value.codex?`success`:`info`,size:`small`,"disable-transitions":``},{default:_(()=>[r(S(c.value.codex?`已开启`:`未开启`),1)]),_:1},8,[`type`])]),n[11]||=t(`div`,{class:`switch-desc`},[r(` 写入 `),t(`code`,null,`~/.codex/config.toml`),r(` 的 model_provider 与 [model_providers.ellm-proxy] 段 (保留用户注释与排版);开启后需按提示设置环境变量 ELLM_PROXY_API_KEY 供 Codex 鉴权。 `)],-1)]),s(E,{"model-value":c.value.codex,loading:h.value===`codex`,onChange:n[3]||=e=>D(`codex`,!!e)},null,8,[`model-value`,`loading`])]),s(O,{class:`switch-divider`}),t(`div`,U,[t(`div`,W,[t(`div`,G,[n[12]||=r(` DeepSeek Harness `,-1),s(T,{type:c.value.dsh?`success`:`info`,size:`small`,"disable-transitions":``},{default:_(()=>[r(S(c.value.dsh?`已开启`:`未开启`),1)]),_:1},8,[`type`])]),n[13]||=t(`div`,{class:`switch-desc`},[r(` 写入 `),t(`code`,null,`~/.dsh/settings.yaml`),r(` 的 llm-deepseek.baseURL 与 `),t(`code`,null,`~/.dsh/.credentials.yaml`),r(` 的 DEEPSEEK_API_KEY(取网关 masterKey,未设置则写占位值);模型请在 dsh 内选择与网关部署名一致的模型。 dsh 为 developer preview,配置格式可能随版本变化。 `)],-1)]),s(E,{"model-value":c.value.dsh,loading:h.value===`dsh`,onChange:n[4]||=e=>D(`dsh`,!!e)},null,8,[`model-value`,`loading`])])],64)):(w(),v(k,{key:1,description:p.value||`暂无开关状态`},{default:_(()=>[s(i,{type:`primary`,onClick:n[5]||=e=>C(!1)},{default:_(()=>[...n[14]||=[r(`重试`,-1)]]),_:1})]),_:1},8,[`description`]))]),_:1})),[[j,a.value]])}}}),[[`__scopeId`,`data-v-5084d579`]]);o(),a(),T();var q={class:`service-settings`},J={class:`flex-between`},Y={key:0,class:`switch-row`},X={class:`switch-info`},Z={class:`switch-title`},Q={key:0,class:`switch-row`},ee={class:`switch-info`},te={class:`switch-title`},ne={class:`switch-desc`},re={key:0,class:`version-block`},ie={class:`version-row`},ae={class:`muted-sm`},oe={key:0,class:`muted-sm`},se={key:1,class:`muted-sm`},ce={class:`muted-sm`},le={class:`version-actions`},$=h(c({__name:`ServiceUpdateSettings`,setup(i){let{toast:a}=A(),o=n(!0),c=n(!1),p=n(null),h=n(null),b=n(null),C=n(null),T=n(null),k=n(!1),j=n(!1),M=n(null),F=null;e(()=>F?.abort());let I=async e=>{if(!e){if(c.value)return;c.value=!0}try{let[e,t]=await Promise.allSettled([O.autostart(),O.update()]);p.value=e.status===`fulfilled`?e.value:null,h.value=e.status===`rejected`?e.reason?.message:null,b.value=t.status===`fulfilled`?t.value:null,C.value=t.status===`rejected`?t.reason?.message:null}finally{o.value=!1,c.value=!1}};d(()=>I(!1));let L=y(()=>b.value?.lastCheck?.currentVersion??null),R=y(()=>P(L.value)),z=y(()=>b.value?.lastCheck?.latestVersion??null),B=y(()=>b.value?.lastCheck?.hasUpdate===!0),V=y(()=>{let e=b.value?.lastCheck?.checkedAt;return e?new Date(e).toLocaleString():null}),H=y(()=>{let e=b.value?.lastApply;if(!e)return null;let t=e.appliedAt?new Date(e.appliedAt).toLocaleString():``;return`${e.fromVersion} → ${e.toVersion}${t?`(${t})`:``}`}),U=e=>e?new Date(e).toLocaleString():``,W=async e=>{if(!p.value||T.value)return;let t=e?`确定开启开机自启?将注册登录触发的自启项(登录时自动恢复服务)。`:`确定关闭开机自启?将移除自启项,且此后首次启动不再自动注册。`;try{await E.confirm(t,e?`开启确认`:`关闭确认`,{confirmButtonText:e?`开启`:`关闭`,cancelButtonText:`取消`,type:`warning`})}catch{return}T.value=`autostart`;try{let t=await O.setAutostart(e);t.note?a(t.note,`success`):a(e?`开机自启已开启`:`开机自启已关闭`,`success`)}catch(e){a(e.message||`操作失败`,`error`)}finally{T.value=null,await I(!0)}},G=async e=>{if(!b.value||T.value)return;let t=e?`确定开启自动更新?将每小时检查 npm 源,发现新版本自动安装并重启服务。`:`确定关闭自动更新?不再自动检查(手动检查更新不受影响)。`;try{await E.confirm(t,e?`开启确认`:`关闭确认`,{confirmButtonText:e?`开启`:`关闭`,cancelButtonText:`取消`,type:`warning`})}catch{return}T.value=`updateEnabled`;try{let t=await O.setUpdateEnabled(e);t.note&&a(t.note,`success`)}catch(e){a(e.message||`操作失败`,`error`)}finally{T.value=null,await I(!0)}},K=async()=>{k.value=!0;try{let e=await O.checkUpdate();M.value=e.hasUpdate?`发现新版本 ${e.latestVersion}(当前 ${e.currentVersion}):点击「立即更新」即可安装(服务自动重启,期间管理台短暂不可访问)`:`已是最新版本 ${e.currentVersion}(检查于 ${U(e.checkedAt)})`,await I(!0)}catch(e){a(e.message||`检查更新失败`,`error`)}finally{k.value=!1}},$=async()=>{try{await E.confirm(`确定更新到 ${z.value}?服务会自动重启并在启动时安装新版本:重启期间管理台短暂不可访问(预计 1~3 分钟),更新完成后自动恢复。`,`更新确认`,{confirmButtonText:`立即更新`,cancelButtonText:`取消`,type:`warning`})}catch{return}j.value=!0;let e=new AbortController;F=e;try{let t=await O.update(),n=await O.applyUpdate();if(!n.updated){M.value=`已是最新版本 ${n.toVersion}`;return}M.value=`正在重启服务并安装 ${n.toVersion}(预计 1~3 分钟):重启期间管理台短暂不可访问,完成后这里会自动显示结果。`;let r=await D({targetVersion:n.toVersion,sinceAppliedAt:t.lastApply?.appliedAt??null,signal:e.signal});if(e.signal.aborted)return;r&&!r.error?(a(`已更新到 ${r.toVersion}`,`success`),M.value=`已更新 ${r.fromVersion} → ${r.toVersion},服务已在新版本上运行。`):r?.error?(a(`更新失败:${r.error}`,`error`),M.value=N(r.error)):M.value=`更新仍在进行(安装耗时较长):稍后刷新页面查看最新状态。`}catch(e){a(e.message||`更新失败`,`error`)}finally{j.value=!1,F===e&&(F=null),e.signal.aborted||await I(!0)}};return(e,n)=>{let i=m(`el-button`),a=m(`el-tag`),d=m(`el-switch`),y=m(`el-empty`),E=m(`el-card`),D=m(`el-link`),O=m(`el-alert`),A=x(`loading`);return g((w(),u(`div`,q,[s(E,{shadow:`never`,class:`mb-16`},{header:_(()=>[t(`div`,J,[n[7]||=t(`span`,{class:`fw-600`},`开机自启`,-1),s(i,{loading:c.value,onClick:n[0]||=e=>I(!1)},{default:_(()=>[...n[6]||=[r(`刷新`,-1)]]),_:1},8,[`loading`])])]),default:_(()=>[p.value?(w(),u(`div`,Y,[t(`div`,X,[t(`div`,Z,[n[8]||=r(` 登录时自动启动服务 `,-1),s(a,{type:p.value.enabled?`success`:`info`,size:`small`,"disable-transitions":``},{default:_(()=>[r(S(p.value.enabled?`已开启`:`未开启`),1)]),_:1},8,[`type`])]),n[9]||=t(`div`,{class:`switch-desc`},[r(` 登录操作系统时自动启动服务(无需管理员权限)。 关闭后首次启动 `),t(`code`,null,`ezllm`),r(` 也不会再自动注册,重新开启即可解除。 `)],-1)]),s(d,{"model-value":p.value.enabled,loading:T.value===`autostart`,onChange:n[1]||=e=>W(!!e)},null,8,[`model-value`,`loading`])])):(w(),v(y,{key:1,description:h.value||`暂无自启状态`},{default:_(()=>[s(i,{type:`primary`,onClick:n[2]||=e=>I(!1)},{default:_(()=>[...n[10]||=[r(`重试`,-1)]]),_:1})]),_:1},8,[`description`]))]),_:1}),s(E,{shadow:`never`,class:`mb-16`},{header:_(()=>[...n[11]||=[t(`div`,{class:`flex-between`},[t(`span`,{class:`fw-600`},`自动更新`)],-1)]]),default:_(()=>[b.value?(w(),u(`div`,Q,[t(`div`,ee,[t(`div`,te,[n[12]||=r(` 自动检查并安装新版本 `,-1),s(a,{type:b.value.enabled?`success`:`info`,size:`small`,"disable-transitions":``},{default:_(()=>[r(S(b.value.enabled?`已开启`:`未开启`),1)]),_:1},8,[`type`])]),t(`div`,ne,[b.value.supported?(w(),u(f,{key:1},[r(` 每小时检查 npm 源,发现新版本自动重启服务并在启动时安装(更新期间服务短暂中断); 关闭后仍可在下方手动检查与更新。 `)],64)):(w(),u(f,{key:0},[r(` 当前为源码运行模式,更新功能不可用(仅 npm 全局安装的包支持自动更新)。 `)],64))])]),b.value.supported?(w(),v(d,{key:0,"model-value":b.value.enabled,loading:T.value===`updateEnabled`,onChange:n[3]||=e=>G(!!e)},null,8,[`model-value`,`loading`])):l(``,!0)])):(w(),v(y,{key:1,description:C.value||`暂无更新状态`},{default:_(()=>[s(i,{type:`primary`,onClick:n[4]||=e=>I(!1)},{default:_(()=>[...n[13]||=[r(`重试`,-1)]]),_:1})]),_:1},8,[`description`]))]),_:1}),s(E,{shadow:`never`},{header:_(()=>[...n[14]||=[t(`div`,{class:`flex-between`},[t(`span`,{class:`fw-600`},`版本信息`)],-1)]]),default:_(()=>[b.value?(w(),u(`div`,re,[t(`div`,ie,[n[18]||=t(`span`,{class:`version-label`},`当前版本`,-1),s(a,{size:`small`},{default:_(()=>[r(S(L.value||`未知`),1)]),_:1}),R.value?(w(),v(a,{key:0,type:`warning`,size:`small`,"disable-transitions":``},{default:_(()=>[...n[15]||=[r(`本地构建`,-1)]]),_:1})):l(``,!0),z.value?(w(),u(f,{key:1},[n[17]||=t(`span`,{class:`version-label`},`最新版本`,-1),s(a,{type:B.value?`warning`:`success`,size:`small`,"disable-transitions":``},{default:_(()=>[r(S(z.value),1)]),_:1},8,[`type`]),B.value?(w(),v(a,{key:0,type:`warning`,size:`small`,"disable-transitions":``},{default:_(()=>[...n[16]||=[r(`发现新版本`,-1)]]),_:1})):l(``,!0)],64)):l(``,!0)]),t(`div`,ae,[r(` 上次检查:`+S(V.value||`尚未检查`)+` `,1),b.value.updating?(w(),u(f,{key:0},[r(` · 更新执行中...`)],64)):l(``,!0)]),b.value.supported?l(``,!0):(w(),u(`div`,oe,` 源码运行模式:手动检查与更新不可用。 `)),H.value?(w(),u(`div`,se,` 上次更新:`+S(H.value),1)):l(``,!0),t(`div`,ce,[n[20]||=r(` npm 包主页:`,-1),s(D,{type:`primary`,href:`https://www.npmjs.com/package/ezllm`,target:`_blank`},{default:_(()=>[...n[19]||=[r(`npmjs.com/package/ezllm`,-1)]]),_:1})]),t(`div`,le,[s(i,{loading:k.value,disabled:!b.value.supported||b.value.updating,onClick:K},{default:_(()=>[r(S(k.value?`检查中...`:`检查更新`),1)]),_:1},8,[`loading`,`disabled`]),B.value&&b.value.supported?(w(),v(i,{key:0,type:`primary`,loading:j.value,disabled:b.value.updating,onClick:$},{default:_(()=>[r(S(j.value?`更新中...`:`立即更新到 ${z.value}`),1)]),_:1},8,[`loading`,`disabled`])):l(``,!0)]),M.value?(w(),v(O,{key:2,title:M.value,type:`info`,"show-icon":``,class:`mt-8`,onClose:n[5]||=e=>M.value=null},null,8,[`title`])):l(``,!0)])):(w(),v(y,{key:1,description:`更新状态不可用`}))]),_:1})])),[[A,o.value]])}}}),[[`__scopeId`,`data-v-d51cfdf5`]]);o(),T(),a();var ue={class:`flex-col-gap`},de={class:`flex-between`},fe={class:`muted-sm`},pe={class:`login-row`},me={class:`muted-sm`},he={class:`flex-between`},ge={class:`monitor-row`},_e={class:`save-bar`},ve=h(c({__name:`SettingsView`,setup(e){let{toast:a}=A(),o=k(),c=n(`basic`),h=n(!0),T=n(!1),E=n(``),D=async()=>{E.value&&(await M(E.value)?a(`已复制到剪贴板`,`success`):a(`复制失败`,`error`))},O=n(29770),N=n(null),P=n(!0),F=n(null),I=n(null),L=n(``),R=n(``),z=y(()=>E.value.trim()!==``||L.value!==``&&R.value!==``),B=n(!1),V=n(60),H=[{label:`10分钟`,value:10},{label:`30分钟`,value:30},{label:`1小时`,value:60},{label:`2小时`,value:120},{label:`5小时`,value:300},{label:`8小时`,value:480},{label:`12小时`,value:720},{label:`24小时`,value:1440}],{loadError:U,run:W}=j(`设置加载失败,请重试`),G=()=>W(async()=>{try{await o.load(),E.value=o.config.masterKey||``,O.value=o.config.port??29770;let e=o.config[`web-ui`]?.[`login-account`];L.value=e?.username||``,R.value=e?.password||``,P.value=o.config.allowLan!==!1;let t=o.config.latencyTest;B.value=t?.enabled===!0,V.value=t?.intervalMinutes??60;try{let e=await p.get(`/api/status`);N.value=e.listenPort,F.value=e.listenHost??null,I.value=e.listenSource??null}catch{N.value=null,F.value=null,I.value=null}}finally{h.value=!1}}),q=async()=>{if(L.value.trim()!==``||R.value!==``){if(L.value.trim()===``||R.value===``){a(`用户名和密码需同时填写`,`error`);return}if(R.value.length<6){a(`密码至少 6 位`,`error`);return}}T.value=!0;let e=B.value&&o.config.latencyTest?.enabled!==!0;try{let t=await o.update(e=>{e.masterKey=E.value.trim(),e.port=O.value,e.allowLan=P.value,e.latencyTest={enabled:B.value,intervalMinutes:V.value},e[`web-ui`]??={},e[`web-ui`][`login-account`]??={},e[`web-ui`][`login-account`].username=L.value.trim(),e[`web-ui`][`login-account`].password=R.value});await b(!0),a(`设置已保存并生效`,`success`),e&&a(`已开启自动测试:服务将定期直连所有上游发送探测请求,有少量 token 消耗`,`warning`),t.restartHint&&a(t.restartHint,`warning`)}catch(e){a(e.message,`error`)}finally{T.value=!1}};return d(G),(e,n)=>{let a=m(`el-button`),o=m(`el-alert`),d=m(`el-tag`),p=m(`el-input`),y=m(`el-form-item`),b=m(`el-input-number`),k=m(`el-form`),A=m(`el-card`),j=m(`el-switch`),M=m(`el-divider`),W=m(`el-option`),J=m(`el-select`),Y=m(`el-tab-pane`),X=m(`el-tabs`),Z=x(`loading`);return w(),v(X,{modelValue:c.value,"onUpdate:modelValue":n[7]||=e=>c.value=e,class:`settings-tabs`},{default:_(()=>[s(Y,{label:`基本设置`,name:`basic`},{default:_(()=>[g((w(),u(`div`,ue,[i(U)?(w(),v(o,{key:0,title:i(U),type:`error`,"show-icon":``,closable:!1},{default:_(()=>[s(a,{size:`small`,onClick:G},{default:_(()=>[...n[8]||=[r(`重试`,-1)]]),_:1})]),_:1},8,[`title`])):l(``,!0),s(A,{shadow:`never`},{header:_(()=>[t(`div`,de,[n[10]||=t(`span`,{class:`fw-600`},`网关访问`,-1),s(d,{type:`success`},{default:_(()=>[...n[9]||=[r(`运行中`,-1)]]),_:1})])]),default:_(()=>[s(k,{"label-position":`top`},{default:_(()=>[s(y,{label:`API 密钥 (masterKey)`},{default:_(()=>[s(p,{modelValue:E.value,"onUpdate:modelValue":n[0]||=e=>E.value=e,placeholder:`sk-1234`,clearable:``},{append:_(()=>[s(a,{onClick:D},{default:_(()=>[...n[11]||=[r(`复制`,-1)]]),_:1})]),_:1},8,[`modelValue`]),n[12]||=t(`div`,{class:`muted-sm`},` 客户端请求 /v1/* 时的认证密钥(Bearer <masterKey>;留空 = 本地放行) `,-1)]),_:1}),s(y,{label:`监听端口 (port)`},{default:_(()=>[s(b,{modelValue:O.value,"onUpdate:modelValue":n[1]||=e=>O.value=e,min:1,max:65535,step:1,style:{width:`220px`}},null,8,[`modelValue`]),t(`div`,fe,[n[14]||=r(` 网关与管理台 API 的监听端口,保存后重启生效 `,-1),N.value===null?l(``,!0):(w(),u(f,{key:0},[n[13]||=r(` ;服务上报的端口 `,-1),t(`b`,null,S(N.value),1),I.value===`env`?(w(),u(f,{key:0},[r(`(监听配置由环境变量覆盖,此配置值不生效)`)],64)):l(``,!0)],64))])]),_:1})]),_:1})]),_:1}),s(A,{shadow:`never`},{header:_(()=>[...n[15]||=[t(`span`,{class:`fw-600`},`安全与登录`,-1)]]),default:_(()=>[s(k,{"label-position":`top`},{default:_(()=>[s(y,{label:`管理台登录账号`},{default:_(()=>[t(`div`,pe,[s(p,{modelValue:L.value,"onUpdate:modelValue":n[2]||=e=>L.value=e,placeholder:`用户名`,style:{width:`200px`}},null,8,[`modelValue`]),s(p,{modelValue:R.value,"onUpdate:modelValue":n[3]||=e=>R.value=e,type:`password`,placeholder:`密码(至少 6 位)`,"show-password":``,style:{width:`200px`}},null,8,[`modelValue`])]),n[16]||=t(`div`,{class:`muted-sm`},` 管理台登录凭证;留空 = 免登录直接访问(仅本机使用时推荐)。 修改后立即生效,需用新账号重新登录 `,-1)]),_:1}),s(y,{label:`允许局域网访问 (allowLan)`},{default:_(()=>[s(j,{modelValue:P.value,"onUpdate:modelValue":n[4]||=e=>P.value=e},null,8,[`modelValue`]),t(`div`,me,[n[19]||=r(` 开启 = 绑定全部网卡,局域网内设备可访问;关闭 = 仅本机 127.0.0.1。保存后需重启生效 `,-1),I.value===`env`?(w(),u(f,{key:0},[n[17]||=r(` ;当前实际监听 `,-1),t(`b`,null,S(F.value),1),n[18]||=r(`(由 HOST 环境变量覆盖,此开关不生效) `,-1)],64)):z.value?l(``,!0):(w(),u(f,{key:1},[r(` ;未设置任何凭据(密钥与登录账号皆无)时始终仅绑本机 `)],64))])]),_:1})]),_:1})]),_:1}),s(A,{shadow:`never`},{header:_(()=>[t(`div`,he,[n[20]||=t(`span`,{class:`fw-600`},`模型服务监控`,-1),s(d,{type:B.value?`success`:`info`,size:`small`,"disable-transitions":``},{default:_(()=>[r(S(B.value?`已开启`:`未开启`),1)]),_:1},8,[`type`])])]),default:_(()=>[t(`div`,ge,[n[21]||=t(`div`,{class:`monitor-info`},[t(`div`,{class:`monitor-title`},`自动测试模型服务状态`),t(`div`,{class:`muted-sm`},` 按设定间隔自动直连所有上游模型发送极小探测请求(max_tokens: 1,有少量 token 消耗); 测试结果展示在「服务状态」页面 `)],-1),s(j,{modelValue:B.value,"onUpdate:modelValue":n[5]||=e=>B.value=e},null,8,[`modelValue`])]),B.value?(w(),u(f,{key:0},[s(M,{style:{margin:`12px 0`}}),s(k,{"label-position":`top`},{default:_(()=>[s(y,{label:`测试间隔`,style:{"margin-bottom":`0`}},{default:_(()=>[s(J,{modelValue:V.value,"onUpdate:modelValue":n[6]||=e=>V.value=e,style:{width:`220px`}},{default:_(()=>[(w(),u(f,null,C(H,e=>s(W,{key:e.value,label:e.label,value:e.value},null,8,[`label`,`value`])),64))]),_:1},8,[`modelValue`]),n[22]||=t(`div`,{class:`muted-sm`},` 自动测试的执行间隔,保存后即时生效(无需重启) `,-1)]),_:1})]),_:1})],64)):l(``,!0)]),_:1}),t(`div`,_e,[s(a,{type:`primary`,size:`large`,loading:T.value,disabled:T.value||i(U)!==null,onClick:q},{default:_(()=>[r(S(T.value?`保存中...`:`保存设置`),1)]),_:1},8,[`loading`,`disabled`])])])),[[Z,h.value]])]),_:1}),s(Y,{label:`代理开关`,name:`harness`,lazy:``},{default:_(()=>[s(K)]),_:1}),s(Y,{label:`服务与更新`,name:`service`,lazy:``},{default:_(()=>[s($)]),_:1})]),_:1},8,[`modelValue`])}}}),[[`__scopeId`,`data-v-e1ea3ddd`]]);export{ve as default};
@@ -1 +0,0 @@
1
- async function e(e){try{if(navigator.clipboard&&window.isSecureContext)return await navigator.clipboard.writeText(e),!0}catch{}try{let t=document.createElement(`textarea`);t.value=e,t.style.position=`fixed`,t.style.opacity=`0`,document.body.appendChild(t),t.select();let n=document.execCommand(`copy`);return document.body.removeChild(t),n}catch{return!1}}export{e as t};
@@ -1 +0,0 @@
1
- var e=e=>{if(!e)return`-`;let t=new Date(e);if(Number.isNaN(t.getTime()))return`-`;let n=e=>String(e).padStart(2,`0`);return`${t.getFullYear()}-${n(t.getMonth()+1)}-${n(t.getDate())} ${n(t.getHours())}:${n(t.getMinutes())}:${n(t.getSeconds())}`},t=e=>e?typeof e==`string`?e:JSON.stringify(e,null,2):`-`,n=e=>{if(!e)return`-`;try{return JSON.stringify(JSON.parse(e),null,2)}catch{return e}},r=e=>(e??0).toLocaleString(),i=e=>e?e>=1024**3?(e/1024**3).toFixed(1)+`G`:e>=1024**2?(e/1024**2).toFixed(1)+`M`:e>=1024?(e/1024).toFixed(1)+`K`:`${e}B`:`-`,a=({protocol:e,hostname:t,port:n})=>`${e===`https:`?`https:`:`http:`}//${t}${n?`:${n}`:``}`,o=e=>e==null||e<=0?`-`:e>=1e3?(e/1e3).toFixed(2)+`s`:Math.round(e)+`ms`,s=e=>e==null?`-`:e<1e3?`${String(e)}ms`:`${(e/1e3).toFixed(1)}s`,c=e=>{let t=e.input_tokens||0,n=e.cache_read_tokens||0;return t<=0?`-`:(n/t*100).toFixed(1)+`%`};export{i as a,e as c,s as i,n as l,c as n,r as o,o as r,t as s,a as t};
@@ -1 +0,0 @@
1
- import{Et as e,H as t,k as n,yt as r}from"./router-BqkHX02I.js";import{n as i}from"./index-C6wv4UB9.js";import{t as a}from"./config-Dbms19Et.js";r(),t();var o=i(`providers`,()=>{let t=a(),r=n(()=>t.providers),i=n(()=>t.loading),o=n(()=>r.value.length),s=n(()=>r.value.reduce((e,t)=>e+(t.models?.length??0),0)),c=e([]);function l(){try{let e=localStorage.getItem(`provider_order`);e&&(c.value=JSON.parse(e))}catch{}}function u(){try{localStorage.setItem(`provider_order`,JSON.stringify(c.value))}catch{}}let d=n(()=>{let e=c.value.map(e=>r.value.find(t=>t.name===e)).filter(e=>!!e);for(let t of r.value)e.find(e=>e.name===t.name)||e.push(t);let t=e.map(e=>e.name);(t.length!==c.value.length||t.some((e,t)=>c.value[t]!==e))&&(c.value=t,u());let n=e=>e.enabled!==!1;return[...e.filter(n),...e.filter(e=>!n(e))]});function f(e){c.value=e.map(e=>e.name),u()}function p(e){c.value=[e,...c.value.filter(t=>t!==e)],u()}async function m(){l(),await t.load()}async function h(e){let n=t.config.providers;t.config.providers=e;try{return await t.save()}catch(e){throw t.config.providers=n,e}}async function g(e){return await h(r.value.filter(t=>t.name!==e.name))}async function _(e){return await h(r.value.map(t=>({...t,models:(t.models??[]).filter(t=>t.modelName!==e)})))}return{suppliers:r,loading:i,orderedProviders:d,providerCount:o,totalModelCount:s,loadProviders:m,saveProviders:h,deleteProvider:g,deleteModel:_,reorderProviders:f,moveProviderToTop:p}});export{o as t};
@@ -1 +0,0 @@
1
- import{Et as e,yt as t}from"./router-BqkHX02I.js";t();function n(t=`加载失败,请稍后重试`){let n=e(null);async function r(e){n.value=null;try{return await e(),!0}catch(e){return n.value=e?.message||t,!1}}return{loadError:n,run:r}}export{n as t};
@@ -1 +0,0 @@
1
- import{t as e}from"./message-CEBSyhlN.js";function t(){function t(t,n=`success`){e({message:t,type:n,duration:n===`warning`?8e3:3e3})}return{toast:t}}export{t};
package/docs/DESIGN.md DELETED
@@ -1,98 +0,0 @@
1
- # ezllm 发布包设计文档
2
-
3
- > 本文档写发布包的**当前**结构与行为约束(包布局、路径 env 化、启停形态、更新链路、维护须知)。
4
- > 使用说明见 [README.md](../README.md)。
5
-
6
- ## 1. 目标与形态
7
-
8
- 把 monorepo(`app/cli` 发布包 / `app/web` 前端 / `packages/server` 服务 / `packages/gateway` 网关 / `packages/protocol` 协议层)里的 server 单进程应用,构建为可发布的 npm 包 **`ezllm`**:
9
-
10
- ```bash
11
- npm i -g ezllm
12
- ezllm start # 后台启动(隐藏窗口,首次自动创建配置)
13
- ezllm stop # 停止后台服务
14
- ```
15
-
16
- 发布形态是**完整单进程应用**(网关 `/v1/*` + 管理台 `/api/*` + 管理域 API + web-ui 静态托管),不是 SDK。包名与内部子包 `gateway` 的相似性已知并接受。
17
-
18
- ## 2. 包结构与发布流程
19
-
20
- ```
21
- app/cli/
22
- ├── bin/ellm-proxy.js # CLI shim(import dist/cli.js;npm bin 指向)
23
- ├── dist/server.js # 服务本体(vite 8 从 packages/server/server.ts 构建的自包含单文件 ESM,banner 只注入 shebang)
24
- ├── dist/cli.js # CLI(同配置的另一入口,从 packages/server/src/cli/bin.ts 构建,与 server 共享 proc/paths 原语)
25
- ├── dist/web-ui/ # web-ui vite 构建产物(服务进程托管于 http://127.0.0.1:<port>/)
26
- ├── vite.config.ts # 发布构建配置(两个入口分两次单入口构建;ssr.noExternal 全量内联、关闭 code splitting、banner 只注 shebang)
27
- ├── scripts/build-bundles.ts # 纯编译(对应根 build:server:只重编译两个 bundle,不聚合 web-ui、不做门禁)
28
- ├── scripts/build-package.ts # 聚合 + 门禁(prepack:调 vite 构建 server+cli → 拷 web-ui → 复制 example → shebang 校验)
29
- ├── scripts/lib/release-utils.ts # 升级/发布脚本的纯函数层(版本号改写、前置校验、阶段参数、失败处置)
30
- ├── scripts/lib/exec.ts # 命令执行底座(只有 Windows 的 npm/npx/pnpm 走 shell,git 直连以防参数被拆)
31
- └── scripts/dev-upgrade.ts | release.ts # 本地一键升级 / 发布(质检与发布两个阶段共用 release.ts)
32
- ```
33
-
34
- 发布 manifest 要点:`bin=bin/ellm-proxy.js`;`files` 白名单(bin/、dist/、示例配置与示例钩子、README、LICENSE、docs);**`dependencies` 为空**(全部逻辑 bundle 自包含),devDependencies 只放构建与测试工具;发布时用 `npm publish`(不改写 manifest,`prepack` 自动构建)。
35
-
36
- 构建链:`pnpm build:all` = web-ui(vite)→ 发布包聚合(vite 双 bundle + web-ui + 示例配置 + 示例钩子 + shebang 门禁)。服务端打包器与前端统一用 vite 8(rolldown),配置见 `vite.config.ts`(要点见 §6 第 5~7 条);产物校验由 `build-package.ts` 承担,缺 web-ui/dist、示例配置或示例钩子直接失败。
37
-
38
- **服务端产物只构建一份**(2026-09-20 定案):`packages/server` 不再有自己的构建与 dist,唯一产物由本包承载(`dist/server.js` 与 `dist/cli.js` 是同一份 `vite.config.ts` 的两个入口)。因此:
39
- - `pnpm build:server`(根)= 本包 `build:bundles` = `tsx scripts/build-bundles.ts`(内部按 `ELLM_BUILD_ENTRY` 连续跑两次 vite 单入口构建),**只编译不聚合**,语义为「重编译产物」且不要求 web-ui 产物在(保留改动前的等价语义);
40
- - `pnpm build:all` / `npm pack`(prepack)= `build-package.ts`,编译 + 聚合 + 门禁;
41
- - 仓库模式启动改用 `pnpm start`(注入 `ELLM_APP_DIR=packages/server`)——产物在 `app/cli/dist` 时 `APP_DIR` 的缺省公式(`hereDir/..`)会推导到 `app/cli/` 而找不到配置,必须显式注入。
42
-
43
- 发布链:本地分两步 —— `pnpm release:checks`(前置校验:工作区干净 + 有 remote + 不落后远端 → 质量闸 typecheck/test/format:check;只读不动 git)与 `pnpm release`(构建校验 → patch+1 → 提交 → 打 `v<版本>` tag → 推送)→ GitHub Actions 在 tag 上校验 tag 与 `app/cli/package.json` 版本一致后 `npm publish`(凭 `NPM_TOKEN`),真正的发版动作在 CI 侧。
44
-
45
- ## 3. 路径全 env 化(核心设计)
46
-
47
- 所有路径「env 优先、缺省公式兜底」,相对路径按当前工作目录展开,绝对路径原样:
48
-
49
- | 配置 | 环境变量 | 缺省公式 | 包模式来源 |
50
- | --- | --- | --- | --- |
51
- | 应用目录(配置/数据基准) | `ELLM_APP_DIR` | `packages/server/`(hereDir 推导) | CLI 注入(默认 `~/.ezllm`) |
52
- | 配置文件 | `ELLM_CONFIG` | `<APP_DIR>/ezllm.config.json` | CLI `--config` 或用户 env |
53
- | 数据目录 | `ELLM_DATA_DIR` | `<APP_DIR>/data` | CLI `--data-dir` 或用户 env |
54
- | web-ui 静态资源 | `ELLM_WEBUI_DIR`(可选覆盖) | 按模块位置推导:`dist/web-ui` 与 bundle 同级(包模式);`app/web/dist`(仓库模式) | 服务进程启动时自解析 |
55
-
56
- 服务端不加载任何 `.env` / env 文件;环境变量一律由用户会话导出或 CLI 注入。约束与边界:
57
-
58
- - **server 源码零模式分支**:`paths.ts` 只有「env ?? 公式」两种取值,不存在模式判断;仓库开发模式不设任何 env 即工作。
59
- - **包模式必须经 CLI 启动**:绕过它裸跑 `dist/server.js` 时 env 未注入,会因找不到配置退出(报错信息指引)。官方入口始终是 `ezllm start/stop`。
60
- - **CLI 在转调前把路径参数统一转为绝对路径**:CLI 的 cwd 与 node 进程的工作目录不同,透传相对字符串会在另一基准下解析错位。
61
- - **CLI 负责首次配置自愈**(server 端只读不建):`ensureConfig()` 查找链与 server 端一致,创建后提示编辑或经管理台修改。
62
-
63
- ## 4. 启停与进程管理
64
-
65
- - **start**:配置自愈 → 已运行检测(run.json + pid 存活)→ 端口预算(`--port` > `ELLM_PORT` > 配置 `port`;被占则顺延到空闲端口并提示,顺延仅本次运行生效)→ `spawnDetached` → 轮询 run.json + HTTP 探活(端口真实可服务才算就绪)→ 首启自启自愈。
66
- - **stop**:按 run.json 的 pid 杀进程树并清记录。`restart` = stop + start(保证最新参数生效)。
67
- - **进程管理原语**(`packages/server/src/proc/`):`run.json` 运行记录(pid/端口/startedAt/serverJs);`spawnDetached`(日志重定向到 `<appDir>/logs/`);`selfRespawn`(孵化后继进程后自身退出,`ELLM_RESPAWNED=1` 的后继对 EADDRINUSE 重试绑定而非顺延端口);`killTree`(Windows `taskkill /T /F`,POSIX 杀进程组);`waitHttpOnline`;`installCrashRecovery`(uncaughtException → respawn,1 分钟 10 次封顶)。
68
- - **开机自启**(`ezllm autostart on|off`,跨平台、登录触发、免管理员):Windows 写启动文件夹(`.vbs` 隐藏窗口入口 + appDir 内 `.cmd`,`chcp 65001` 统一日志编码)、macOS 写 LaunchAgent、Linux 写 systemd user unit;自启项内容含解释器路径与 `ELLM_*` env 快照,保证登录启动与手动启动同参。
69
- - **解释器解析**(`proc/interpreter.ts`):服务依赖 `node:sqlite`(≥ 22.13),解释器一律用**托管运行时** `<appDir>/node`(由 `@doyzheng/ezn` 的 `Node.ensure` 按主版本落位,缺失则下载;当前锁主版本 24),不随 CLI 自身的 node 漂移。
70
- - **管理域不设生命周期入口**:启停统一走 CLI(管理台由服务本体托管,页面无法自举拉起已停止的服务);管理域只留状态查询与设置类接口,每条路由带登录鉴权。
71
-
72
- ## 5. 更新链路
73
-
74
- 自动更新采用「启动时生效 + self-respawn 双跳」:
75
-
76
- 1. 检查(管理台请求惰性触发 + 定时检查每小时轮询,两通道共享互斥与墙钟节流;失败 10 分钟后可重试)发现新版本 → 写 `pendingUpdate` 标记 → respawn;
77
- 2. 新进程启动引导读到标记 → `npm i -g ezllm@<版本>`(此时服务未起,无文件句柄冲突)→ 清标记 → 再 respawn 一次换新代码;
78
- 3. 安装失败:清标记、写 `lastApply` 错误,以旧版本继续启动。
79
-
80
- 开关是配置文件顶层 `autoUpdate`(运行状态 `lastCheck`/`lastApply`/`pendingUpdate` 存 `<appDir>/updater.json`);源码运行(包根不在 `node_modules` 下)时更新域整体关闭,防 dev 会话误更新全局包;更新源可用 `ELLM_UPDATE_REGISTRY` 覆盖(默认 npm 官方)。停机窗口 = npm 安装时长。
81
-
82
- ## 6. 跨包不变式(维护须知)
83
-
84
- 1. **`packages/gateway/src/index.ts` 必须保持零顶层副作用**:server 经 vite/rolldown 全量内联(`ssr.noExternal: true`)gateway 源码,任何 import 副作用都会执行。独立网关装配在 `src/standalone-app.ts`。
85
- 2. **`dist/web-ui/` 与 `dist/server.js` 同级**:`api/webui-static.ts` 的缺省推导依赖此布局(`ELLM_WEBUI_DIR` 可覆盖)。
86
- 3. **`ezllm.config.json` 是配置唯一数据源**(原样读写,勿加转换层)。
87
- 4. **管理域 `gatewayBase` 每请求现算**:`ctx.port` 在 listen 后才回填,sub-app 创建时固化会得到 `:0`。
88
- 5. **不要在 `vite.config.ts` 注入 `const require = createRequire(...)` banner**(`vite.config.ts` 文件头已写明):rolldown 会把该赋值当作未使用的顶层变量删掉、只留裸调用,随后与源码内联进来的 `import { createRequire }` 撞名(`SyntaxError: Identifier 'createRequire' has already been declared`)。ESM bundle 里的 CJS 依赖(pidusage 等)由 rolldown 自带的 `__require` shim 承接,无需 banner 兜底。banner 只注入 shebang,供 unix 下 npm 建 shim。
89
- 6. **`node:` 前缀必须保住**:产物里 `node:sqlite` 不能变成裸名 `sqlite`——它是唯一只有 `node:` 形式、没有裸名的模块,改写后产物 `import "sqlite"` 直接 `ERR_MODULE_NOT_FOUND`,服务启动即崩(`node --check` 查不出,只有实跑能暴露)。vite/rolldown 默认不改写该前缀(与 tsup 的 `removeNodeProtocol` 缺省相反),故这是**现状约束**而非配置项:若将来加 normalize/改写插件,须按本条回归。
90
- 7. **`output.codeSplitting: false` + 分两次单入口构建(app/cli 双入口必须)**:rolldown 在多入口下不支持 `codeSplitting: false`(报 `INVALID_OPTION "multiple inputs are not supported"`),因此 `vite.config.ts` 导出**函数**、按 `ELLM_BUILD_ENTRY` 连续跑两次单入口构建。不关 splitting 的后果是共享代码被抽成 `assets/*.js`,破坏「每个产物自包含单文件」的发布包契约(`bin/ellm-proxy.js` 直接 import `dist/cli.js`)。
91
-
92
- ## 7. 验证基线(发布前回归清单)
93
-
94
- - `pnpm -r test:coverage` / `pnpm -r typecheck` / `pnpm format:check` / `pnpm check:knip` 全绿(覆盖率为各包 vitest.config.ts 的门槛)。
95
- - CLI 冒烟:`-h`、`config`(首次创建)、`start`(参数透传 + env 注入 + 鉴权 200/401)、`stop`(树杀 + 端口释放)、`restart`(运行中/未运行两态)。
96
- - 仓库产物模式(零 env)与包模式(CLI 注入)双模式启动均正常。
97
- - 包目录零读写(`ls` 包目录无新增文件);`npm pack` 核对 tarball 无 `src/`/`test/`/真实配置。
98
- - 实机升级:`pnpm upgrade:local` 走完 build + 安装 + 重启(`ELLM_NO_INSTALL=1` 可只验构建链路)。
File without changes