@numa-tech/numa 1.11.13 → 1.12.1
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/README.md +197 -25
- package/dist/ai-registry/client.d.ts +65 -0
- package/dist/ai-registry/client.js +260 -0
- package/dist/ai-registry/client.js.map +1 -0
- package/dist/ai-registry/commands.d.ts +2 -0
- package/dist/ai-registry/commands.js +438 -0
- package/dist/ai-registry/commands.js.map +1 -0
- package/dist/ai-registry/errors.d.ts +8 -0
- package/dist/ai-registry/errors.js +40 -0
- package/dist/ai-registry/errors.js.map +1 -0
- package/dist/ai-registry/mcp-installer.d.ts +37 -0
- package/dist/ai-registry/mcp-installer.js +273 -0
- package/dist/ai-registry/mcp-installer.js.map +1 -0
- package/dist/ai-registry/schemas.d.ts +86 -0
- package/dist/ai-registry/schemas.js +39 -0
- package/dist/ai-registry/schemas.js.map +1 -0
- package/dist/ai-registry/skill-installer.d.ts +21 -0
- package/dist/ai-registry/skill-installer.js +262 -0
- package/dist/ai-registry/skill-installer.js.map +1 -0
- package/dist/app-config.d.ts +4 -2
- package/dist/app-config.js +15 -3
- package/dist/app-config.js.map +1 -1
- package/dist/cli.js +40 -12
- package/dist/cli.js.map +1 -1
- package/dist/codex-module.d.ts +8 -0
- package/dist/codex-module.js +152 -0
- package/dist/codex-module.js.map +1 -0
- package/dist/command-catalog.js +170 -64
- package/dist/command-catalog.js.map +1 -1
- package/dist/config-page.js +17 -0
- package/dist/config-page.js.map +1 -1
- package/dist/config.d.ts +4 -1
- package/dist/config.js +26 -7
- package/dist/config.js.map +1 -1
- package/dist/media/commands.js +12 -11
- package/dist/media/commands.js.map +1 -1
- package/dist/oauth.d.ts +2 -30
- package/dist/oauth.js +1 -294
- package/dist/oauth.js.map +1 -1
- package/dist/pipeline/client.d.ts +126 -0
- package/dist/pipeline/client.js +105 -0
- package/dist/pipeline/client.js.map +1 -0
- package/dist/pipeline/commands.d.ts +17 -0
- package/dist/pipeline/commands.js +283 -0
- package/dist/pipeline/commands.js.map +1 -0
- package/dist/pipeline/errors.d.ts +8 -0
- package/dist/pipeline/errors.js +37 -0
- package/dist/pipeline/errors.js.map +1 -0
- package/dist/pipeline/schemas.d.ts +243 -0
- package/dist/pipeline/schemas.js +60 -0
- package/dist/pipeline/schemas.js.map +1 -0
- package/dist/platform-profile.d.ts +75 -0
- package/dist/platform-profile.js +169 -0
- package/dist/platform-profile.js.map +1 -0
- package/dist/types.d.ts +1 -40
- package/dist/web-console-page.d.ts +2 -2
- package/dist/web-console-page.js +21 -7
- package/dist/web-console-page.js.map +1 -1
- package/dist/web-console.d.ts +17 -3
- package/dist/web-console.js +70 -5
- package/dist/web-console.js.map +1 -1
- package/examples/claude-desktop-npx.json +1 -1
- package/examples/codex-npx.json +1 -1
- package/package.json +30 -14
- package/skills/numa-ai-registry/SKILL.md +47 -0
- package/skills/numa-ai-registry/evals/evals.json +35 -0
- package/skills/numa-ai-registry/references/command-contract.md +53 -0
- package/assets/codex-shortcut-icon.icns +0 -0
- package/assets/codex-shortcut-icon.ico +0 -0
- package/dist/chatgpt-console.d.ts +0 -53
- package/dist/chatgpt-console.js +0 -105
- package/dist/chatgpt-console.js.map +0 -1
- package/dist/codex-auth-agent.d.ts +0 -60
- package/dist/codex-auth-agent.js +0 -331
- package/dist/codex-auth-agent.js.map +0 -1
- package/dist/codex-commands.d.ts +0 -13
- package/dist/codex-commands.js +0 -571
- package/dist/codex-commands.js.map +0 -1
- package/dist/codex-legacy-cleanup.d.ts +0 -5
- package/dist/codex-legacy-cleanup.js +0 -77
- package/dist/codex-legacy-cleanup.js.map +0 -1
- package/dist/codex-process.d.ts +0 -13
- package/dist/codex-process.js +0 -193
- package/dist/codex-process.js.map +0 -1
- package/dist/codex-protocol.d.ts +0 -352
- package/dist/codex-protocol.js +0 -116
- package/dist/codex-protocol.js.map +0 -1
- package/dist/codex-quota-cache.d.ts +0 -20
- package/dist/codex-quota-cache.js +0 -82
- package/dist/codex-quota-cache.js.map +0 -1
- package/dist/codex-selection.d.ts +0 -18
- package/dist/codex-selection.js +0 -47
- package/dist/codex-selection.js.map +0 -1
- package/dist/codex-service-client.d.ts +0 -11
- package/dist/codex-service-client.js +0 -91
- package/dist/codex-service-client.js.map +0 -1
- package/dist/codex-shortcuts.d.ts +0 -43
- package/dist/codex-shortcuts.js +0 -429
- package/dist/codex-shortcuts.js.map +0 -1
- package/dist/codex-tui.d.ts +0 -15
- package/dist/codex-tui.js +0 -235
- package/dist/codex-tui.js.map +0 -1
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export declare const CONSOLE_HTML = "<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n <title>Numa CLI \u63A7\u5236\u53F0</title>\n <link rel=\"stylesheet\" href=\"/assets/console.css\">\n</head>\n<body>\n <div id=\"app\" class=\"boot\"><div class=\"boot-mark\">N</div><p>\u6B63\u5728\u8FDE\u63A5\u672C\u5730\u63A7\u5236\u53F0\u2026</p></div>\n <script type=\"module\" src=\"/assets/console.js\"></script>\n</body>\n</html>";
|
|
2
|
-
export declare const CONSOLE_CSS = "\n:root{color-scheme:light;--ink:#0b1728;--navy:#07182b;--navy-2:#102943;--cyan:#0eb7bf;--cyan-soft:#dff7f6;--mist:#eef4f8;--paper:#fbfdff;--line:#d7e2ea;--muted:#64778c;--amber:#e99a32;--red:#d94b58;--green:#198a68;--shadow:0 18px 50px rgba(12,38,62,.1)}\n*{box-sizing:border-box}html,body,#app{min-height:100%}body{min-height:100vh;margin:0;background:var(--mist);color:var(--ink);font-family:\"Avenir Next\",\"Segoe UI\",system-ui,sans-serif}button,input,select,textarea{font:inherit}.mono,code,pre{font-family:\"SFMono-Regular\",Consolas,\"Liberation Mono\",monospace}.boot{min-height:100vh;display:grid;place-content:center;justify-items:center;gap:14px;color:var(--muted)}.boot-mark,.brand-mark{display:grid;place-items:center;background:var(--cyan);color:#00262a;font-weight:900}.boot-mark{width:50px;height:50px;border-radius:15px;box-shadow:0 0 0 8px rgba(14,183,191,.12)}\n.shell{min-height:100vh;display:grid;grid-template-columns:260px 1fr}.rail{position:sticky;top:0;height:100vh;padding:24px 18px;background:var(--navy);color:#d7e6f3;display:flex;flex-direction:column}.brand{display:flex;align-items:center;gap:12px;padding:8px 8px 26px}.brand-mark{width:38px;height:38px;border-radius:12px}.brand strong{display:block;letter-spacing:.04em}.brand small{display:block;margin-top:4px;color:#708aa3;font-size:10px;letter-spacing:.18em;text-transform:uppercase}.nav{display:grid;gap:7px}.nav button{border:0;background:transparent;color:#8da4b8;border-radius:11px;text-align:left;padding:11px 13px;cursor:pointer;display:flex;justify-content:space-between}.nav button:hover,.nav button.active{background:var(--navy-2);color:white}.nav button.active{box-shadow:inset 3px 0 var(--cyan)}.rail-foot{margin-top:auto;border:1px solid #203b55;border-radius:14px;padding:14px}.rail-foot .pulse{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--cyan);box-shadow:0 0 12px var(--cyan);margin-right:7px}.rail-foot p{margin:7px 0 0;color:#7791a8;font-size:11px;line-height:1.5}\n.main{min-width:0;min-height:100vh;display:flex;flex-direction:column}.topbar{height:76px;padding:0 34px;background:rgba(251,253,255,.94);border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:3;backdrop-filter:blur(12px)}.topbar h1{font-size:17px;margin:0}.eyebrow{font:700 10px/1.2 \"SFMono-Regular\",monospace;text-transform:uppercase;letter-spacing:.16em;color:var(--cyan)}.user-chip{display:flex;align-items:center;gap:10px}.avatar{width:34px;height:34px;border-radius:50%;display:grid;place-items:center;background:var(--cyan-soft);color:#087d84;font-weight:800}.user-chip small{color:var(--muted)}.content{width:100%;max-width:1420px;flex:1;margin:0 auto;padding:30px 34px 56px}.hero{background:var(--navy);color:white;border-radius:22px;padding:28px 30px;display:grid;grid-template-columns:1fr auto;gap:24px;position:relative;overflow:hidden;box-shadow:var(--shadow)}.hero:after{content:\"\";position:absolute;right:-45px;top:-90px;width:320px;height:240px;border:34px solid rgba(14,183,191,.09);transform:rotate(-18deg);border-radius:48px}.hero h2{font-size:32px;line-height:1.08;margin:10px 0}.hero p{max-width:720px;color:#91aac0;margin:0;line-height:1.6}.session-track{align-self:center;display:flex;gap:0;position:relative;z-index:1}.track-node{min-width:112px;border:1px solid #2a4661;padding:13px 15px;background:#0b1e33}.track-node:first-child{border-radius:13px 0 0 13px}.track-node:last-child{border-radius:0 13px 13px 0}.track-node span{display:block;color:#7690a7;font-size:9px;text-transform:uppercase;letter-spacing:.12em}.track-node strong{display:block;margin-top:6px;font-size:13px}.track-node.live strong{color:#58e0df}\n.grid{display:grid;grid-template-columns:repeat(12,1fr);gap:18px;margin-top:20px}.card{grid-column:span 4;background:var(--paper);border:1px solid var(--line);border-radius:17px;padding:20px;box-shadow:0 10px 30px rgba(25,54,79,.05)}.card.wide{grid-column:span 8}.card.full{grid-column:1/-1}.card h3{margin:0;font-size:15px}.card p{color:var(--muted);font-size:12px;line-height:1.6}.metric{font-size:28px;font-weight:750;margin-top:16px}.metric small{font-size:11px;color:var(--muted);font-weight:500}.tag{display:inline-flex;border-radius:999px;padding:5px 8px;font:700 9px/1 \"SFMono-Regular\",monospace;text-transform:uppercase;letter-spacing:.05em;background:var(--cyan-soft);color:#087d84}.tag.public{background:#edf1f5;color:#5d7082}.tag.role{background:#fff0d9;color:#9c5c09}.actions{display:flex;gap:8px;flex-wrap:wrap}.button{border:1px solid var(--line);border-radius:10px;background:white;color:var(--ink);padding:9px 12px;cursor:pointer;font-weight:700;font-size:12px}.button:hover{border-color:#9ab6c9;background:#f7fbfd}.button.primary{background:#0b949b;border-color:#0b949b;color:white}.button.danger{color:var(--red);border-color:#f0bec4}.button:disabled{opacity:.45;cursor:not-allowed}.field{display:grid;gap:6px}.field label{font-size:11px;color:var(--muted);font-weight:700}.field input,.field select,.field textarea{width:100%;border:1px solid var(--line);border-radius:10px;background:white;padding:10px 11px;color:var(--ink);outline:none}.field input:focus,.field textarea:focus{border-color:var(--cyan);box-shadow:0 0 0 3px rgba(14,183,191,.12)}.field textarea{min-height:110px;resize:vertical}.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}.form-grid .span-2{grid-column:1/-1}.command-toolbar{display:flex;gap:10px;margin-bottom:15px}.grow{flex:1}.mt-10{margin-top:10px}.mt-12{margin-top:12px}.mt-14{margin-top:14px}.command-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:11px}.command{border:1px solid var(--line);border-radius:13px;padding:14px;background:white}.command-head{display:flex;gap:8px;align-items:center;justify-content:space-between}.command code{font-size:11px;color:#0b6f76}.command p{margin:9px 0}.command small{display:block;color:#8a9bab;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.output{background:#071522;color:#b8d1e2;border-radius:13px;padding:15px;min-height:170px;max-height:420px;overflow:auto;white-space:pre-wrap;font-size:11px;line-height:1.65}.notice{margin-top:14px;border-radius:11px;padding:11px 13px;font-size:12px}.notice.error{background:#fff0f2;color:#a52c3b;border:1px solid #f1c4ca}.notice.success{background:#e8f7f1;color:#176c54;border:1px solid #bce4d5}.login-gate{min-height:calc(100vh - 76px);display:grid;place-content:center;padding:30px}.login-card{width:min(520px,90vw);background:white;border:1px solid var(--line);border-radius:22px;padding:32px;box-shadow:var(--shadow)}.login-card h2{font-size:28px;margin:14px 0 9px}.login-card p{color:var(--muted);line-height:1.65}.hidden{display:none!important}\n.sync-board{display:grid;grid-template-columns:1fr 56px 1fr;align-items:stretch;margin-top:20px}.sync-stage{background:var(--paper);border:1px solid var(--line);border-radius:17px;padding:18px 20px;box-shadow:0 10px 30px rgba(25,54,79,.05)}.sync-stage strong{display:block;margin-top:8px;font-size:18px}.sync-stage p{margin:7px 0 0;color:var(--muted);font-size:11px}.sync-link{display:grid;place-items:center;color:var(--cyan);font-size:22px}.sync-link:before{content:\"\";height:2px;width:100%;background:linear-gradient(90deg,var(--cyan),#bad8dc)}.table-card{margin-top:18px;background:var(--paper);border:1px solid var(--line);border-radius:17px;box-shadow:0 10px 30px rgba(25,54,79,.05);overflow:hidden}.table-head{padding:18px 20px;display:flex;align-items:center;justify-content:space-between;gap:16px;border-bottom:1px solid var(--line)}.table-head h3{margin:0}.table-head p{margin:5px 0 0;color:var(--muted);font-size:11px}.account-table-wrap{overflow:auto}.account-table{width:100%;border-collapse:collapse;min-width:920px}.account-table th,.account-table td{text-align:left;padding:14px 16px;border-bottom:1px solid #e7eef3;vertical-align:middle}.account-table th{background:#f5f9fb;color:#6c8094;font-size:10px;text-transform:uppercase;letter-spacing:.08em}.account-table tr:last-child td{border-bottom:0}.account-name strong{display:block;font-size:13px}.account-name small{display:block;margin-top:4px;color:var(--muted);font-size:10px}.quota-stack{min-width:210px;display:grid;gap:8px}.quota-line{display:grid;grid-template-columns:42px 1fr 38px;align-items:center;gap:7px;font-size:10px;color:var(--muted)}progress{width:100%;height:7px;border:0;border-radius:99px;overflow:hidden;background:#e7eef3}progress::-webkit-progress-bar{background:#e7eef3;border-radius:99px}progress::-webkit-progress-value{background:var(--cyan);border-radius:99px}progress::-moz-progress-bar{background:var(--cyan);border-radius:99px}.status-stack{display:flex;gap:6px;flex-wrap:wrap}.tag.success{background:#e8f7f1;color:var(--green)}.tag.failed{background:#fff0f2;color:var(--red)}.tag.waiting{background:#fff0d9;color:#9c5c09}.tag.local{background:#e8f1fa;color:#32658f}.empty-state{padding:48px 20px;text-align:center;color:var(--muted)}.empty-state.account-request{margin:24px;border:2px solid #f0b5bd;border-radius:16px;background:#fff2f4;color:#8d2334;box-shadow:0 12px 30px rgba(167,44,64,.1)}.empty-state.account-request strong{display:block;margin-bottom:9px;font-size:16px}.empty-state.account-request span{display:block;font-size:13px;line-height:1.7}.table-meta{color:var(--muted);font-size:10px;line-height:1.55}.spin{display:inline-block;animation:turn 1s linear infinite}@keyframes turn{to{transform:rotate(360deg)}}\n@media(max-width:980px){.shell{grid-template-columns:78px 1fr}.rail{padding:18px 10px}.brand div:last-child,.nav button span:first-child,.rail-foot p,.rail-foot strong{display:none}.brand{justify-content:center}.nav button{justify-content:center}.card,.card.wide{grid-column:span 6}.hero{grid-template-columns:1fr}.session-track{display:none}}\n@media(max-width:680px){.shell{display:block}.rail{height:auto;position:static;flex-direction:row;align-items:center;padding:10px 14px;overflow-x:auto}.brand{padding:0;margin-right:auto;flex:0 0 auto}.nav{display:flex}.nav button{padding:9px;flex:0 0 auto}.rail-foot{display:none}.topbar{padding:0 18px}.content{padding:20px 14px}.hero{padding:23px}.hero h2{font-size:25px}.card,.card.wide{grid-column:1/-1}.form-grid,.command-list{grid-template-columns:1fr}.form-grid .span-2{grid-column:auto}.user-chip div:not(.avatar){display:none}.sync-board{grid-template-columns:1fr}.sync-link{height:32px;transform:rotate(90deg)}.table-head{align-items:flex-start;flex-direction:column}}\n@media(prefers-reduced-motion:no-preference){.card,.hero{animation:arrive .35s ease both}@keyframes arrive{from{opacity:0;transform:translateY(7px)}to{opacity:1;transform:none}}}\n";
|
|
3
|
-
export declare const CONSOLE_JS = "\nconst app=document.getElementById('app');let state=null;let csrf='';let active='overview';let output='\u9009\u62E9\u4E00\u6761\u547D\u4EE4\u6216\u8F93\u5165\u53C2\u6570\u540E\u8FD0\u884C\u3002';let chatgptLocal=null;let chatgptRemote=null;let chatgptLoading=false;const noAvailableAccountGuidance='\u5F53\u524D\u7528\u6237\u6CA1\u6709\u7ED1\u5B9A\u53EF\u7528\u7684 ChatGPT \u8D26\u53F7\u3002\u8BF7\u524D\u5F80 OA \u7CFB\u7EDF\u63D0\u4EA4 IT11 \u5DE5\u5355\uFF0C\u7533\u8BF7\u72EC\u4EAB\u6216\u5171\u4EAB\u7684 ChatGPT \u4E2A\u4EBA\u8D26\u53F7\u3002';\nconst esc=(v)=>String(v??'').replace(/[&<>\"']/g,(c)=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));\nasync function api(path,init={}){const headers={'Accept':'application/json',...(init.body?{'Content-Type':'application/json'}:{}),...(csrf?{'x-numa-console-csrf':csrf}:{})};const response=await fetch(path,{...init,headers:{...headers,...(init.headers||{})},credentials:'same-origin'});const body=await response.json().catch(()=>({error:{message:'\u54CD\u5E94\u4E0D\u662F JSON'}}));if(!response.ok)throw new Error(body.error?.message||body.stderr||body.stdout||('HTTP '+response.status));return body}\nasync function bootstrap(attempt=0){try{state=await api('/api/bootstrap');csrf=state.csrfToken;const callbackState=new URLSearchParams(location.search).get('login');const waitingForSuccess=callbackState==='complete'&&!state.loggedIn&&attempt<40;const waitingForFailure=callbackState==='failed'&&!state.loginError&&attempt<8;if(waitingForSuccess||waitingForFailure){app.innerHTML='<div class=\"boot\"><div class=\"boot-mark\">N</div><p>'+(callbackState==='complete'?'\u767B\u5F55\u6210\u529F\uFF0C\u6B63\u5728\u5EFA\u7ACB\u672C\u673A\u4F1A\u8BDD\u2026':'\u6B63\u5728\u8BFB\u53D6\u767B\u5F55\u5931\u8D25\u4FE1\u606F\u2026')+'</p></div>';setTimeout(()=>bootstrap(attempt+1),250);return}if(callbackState==='complete'&&!state.loggedIn&&!state.loginError)state.loginError='\u767B\u5F55\u51ED\u636E\u4EA4\u6362\u5C1A\u672A\u5B8C\u6210\uFF0C\u8BF7\u91CD\u8BD5\u767B\u5F55\u3002';if(callbackState==='failed'&&!state.loginError)state.loginError='Keycloak \u767B\u5F55\u672A\u5B8C\u6210\uFF0C\u8BF7\u91CD\u8BD5\u3002';if(callbackState)history.replaceState({},'',location.pathname);render()}catch(error){app.innerHTML='<div class=\"login-gate\"><div class=\"login-card\"><span class=\"tag role\">\u8FDE\u63A5\u5931\u8D25</span><h2>\u672C\u5730\u63A7\u5236\u53F0\u4E0D\u53EF\u7528</h2><p>'+esc(error.message)+'</p></div></div>'}}\nfunction navButton(id,label,mark){return '<button data-nav=\"'+id+'\" class=\"'+(active===id?'active':'')+'\"><span>'+label+'</span><span class=\"mono\">'+mark+'</span></button>'}\nfunction shell(content){const user=state.user||{};const label=user.username||user.email||'\u672A\u767B\u5F55';return '<div class=\"shell\"><aside class=\"rail\"><div class=\"brand\"><div class=\"brand-mark\">N</div><div><strong>Numa CLI</strong><small>Local control plane</small></div></div><nav class=\"nav\">'+navButton('overview','\u8FD0\u884C\u6982\u89C8','01')+navButton('chatgpt','ChatGPT \u8D26\u53F7','02')+navButton('config','\u57FA\u7840\u914D\u7F6E','03')+navButton('commands','\u547D\u4EE4\u80FD\u529B','04')+navButton('runner','\u6267\u884C\u5668','05')+'</nav><div class=\"rail-foot\"><strong><span class=\"pulse\"></span>\u4EC5\u76D1\u542C\u672C\u673A</strong><p>Token \u4E0D\u4F1A\u53D1\u9001\u7ED9\u7F51\u9875\u811A\u672C\uFF0C\u4E5F\u4E0D\u4F1A\u51FA\u73B0\u5728\u547D\u4EE4\u8F93\u51FA\u4E2D\u3002</p></div></aside><main class=\"main\"><header class=\"topbar\"><div><div class=\"eyebrow\">Numa / Web console</div><h1>'+sectionTitle()+'</h1></div><div class=\"user-chip\"><div class=\"avatar\">'+esc(label.slice(0,1).toUpperCase())+'</div><div><strong>'+esc(label)+'</strong><br><small>'+esc(state.loggedIn?'Keycloak \u4F1A\u8BDD\u6709\u6548':'\u9700\u8981\u767B\u5F55')+'</small></div></div></header>'+content+'</main></div>'}\nfunction sectionTitle(){return {overview:'\u8FD0\u884C\u6982\u89C8',chatgpt:'ChatGPT \u8D26\u53F7',config:'\u57FA\u7840\u914D\u7F6E',commands:'\u547D\u4EE4\u80FD\u529B',runner:'\u5B89\u5168\u6267\u884C\u5668'}[active]||'\u63A7\u5236\u53F0'}\nfunction loginGate(){const error=state?.loginError?'<div class=\"notice error\">'+esc(state.loginError)+'</div>':'';return '<div class=\"login-gate\"><div class=\"login-card\"><span class=\"tag role\">Keycloak PKCE</span><h2>\u767B\u5F55\u540E\u8FDB\u5165\u672C\u5730\u63A7\u5236\u53F0</h2><p>\u6D4F\u89C8\u5668\u53EA\u8D1F\u8D23\u5B8C\u6210\u8BA4\u8BC1\u548C\u64CD\u4F5C\u672C\u673A API\u3002\u8BBF\u95EE\u4EE4\u724C\u4FDD\u5B58\u5728 CLI \u7684\u79C1\u6709\u7F13\u5B58\u4E2D\uFF0C\u4E0D\u5199\u5165 localStorage\uFF0C\u4E5F\u4E0D\u8FD4\u56DE\u7ED9\u9875\u9762\u3002</p><div class=\"actions\"><button class=\"button primary\" data-action=\"login\">\u4F7F\u7528 Keycloak \u767B\u5F55</button></div><div id=\"notice\">'+error+'</div></div></div>'}\nfunction overview(){const u=state.user||{};const roles=[...(u.realmRoles||[]),...Object.values(u.clientRoles||{}).flat()];return '<div class=\"content\"><section class=\"hero\"><div><div class=\"eyebrow\">Authenticated workstation</div><h2>\u672C\u673A\u4F1A\u8BDD\u5DF2\u63A5\u5165\u7814\u53D1\u63A7\u5236\u9762</h2><p>\u4ECE\u540C\u4E00\u5904\u68C0\u67E5\u8EAB\u4EFD\u3001\u8C03\u6574 CLI \u914D\u7F6E\uFF0C\u5E76\u4EE5\u7ED3\u6784\u5316\u53C2\u6570\u8FD0\u884C\u53D7\u652F\u6301\u547D\u4EE4\u3002\u6240\u6709\u7F51\u7EDC\u8BF7\u6C42\u7EE7\u7EED\u7531 CLI \u6CE8\u5165 Bearer Token\u3002</p></div><div class=\"session-track\"><div class=\"track-node live\"><span>\u8EAB\u4EFD</span><strong>'+esc(u.username||'\u5DF2\u767B\u5F55')+'</strong></div><div class=\"track-node\"><span>API</span><strong>'+esc(state.config.apiBaseUrl?'\u5DF2\u914D\u7F6E':'\u672A\u914D\u7F6E')+'</strong></div><div class=\"track-node\"><span>\u89D2\u8272</span><strong>'+roles.length+'</strong></div></div></section><section class=\"grid\"><article class=\"card\"><div class=\"eyebrow\">Session</div><div class=\"metric\">'+esc(state.expiresAt?new Date(state.expiresAt*1000).toLocaleTimeString('zh-CN',{hour:'2-digit',minute:'2-digit'}):'\u2014')+' <small>\u8BBF\u95EE\u4EE4\u724C\u5230\u671F</small></div><p>\u9700\u8981\u65F6\u7531 CLI \u4F7F\u7528 refresh token \u81EA\u52A8\u7EED\u671F\u3002</p></article><article class=\"card\"><div class=\"eyebrow\">Commands</div><div class=\"metric\">'+state.commands.length+' <small>\u6761\u80FD\u529B\u5951\u7EA6</small></div><p>\u7F51\u9875\u6267\u884C\u5668\u53EA\u63A5\u53D7\u76EE\u5F55\u4E2D\u58F0\u660E\u7684\u547D\u4EE4\u3002</p></article><article class=\"card\"><div class=\"eyebrow\">Endpoint</div><div class=\"metric\">'+(state.config.apiBaseUrl?'READY':'SETUP')+'</div><p>'+esc(state.config.apiBaseUrl||'\u8BF7\u5728\u57FA\u7840\u914D\u7F6E\u4E2D\u586B\u5199\u5E73\u53F0 API \u5730\u5740')+'</p></article><article class=\"card wide\"><h3>\u5F53\u524D\u8EAB\u4EFD</h3><p>'+esc(u.displayName||u.username||'-')+' \u00B7 '+esc(u.email||'-')+'</p><div class=\"actions\">'+roles.slice(0,8).map((r)=>'<span class=\"tag\">'+esc(r)+'</span>').join('')+(roles.length>8?'<span class=\"tag public\">+'+(roles.length-8)+'</span>':'')+'</div></article><article class=\"card\"><h3>\u4F1A\u8BDD\u64CD\u4F5C</h3><p>\u5F3A\u5236\u5237\u65B0\u8EAB\u4EFD\u8D44\u6599\uFF0C\u6216\u6E05\u9664\u672C\u673A Keycloak \u7F13\u5B58\u3002</p><div class=\"actions\"><button class=\"button\" data-action=\"reload\">\u5237\u65B0\u72B6\u6001</button><button class=\"button danger\" data-action=\"logout\">\u9000\u51FA\u767B\u5F55</button></div></article></section></div>'}\nfunction instant(value){if(!value)return null;const number=typeof value==='number'?(value<1000000000000?value*1000:value):Date.parse(value);return Number.isFinite(number)?number:null}\nfunction timeText(value){const at=instant(value);return at?new Date(at).toLocaleString('zh-CN',{month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}):'\u2014'}\nfunction windowName(window,fallback){const minutes=window?.windowMinutes;if(!minutes)return fallback;if(minutes%1440===0)return (minutes/1440)+'d';if(minutes%60===0)return (minutes/60)+'h';return minutes+'m'}\nfunction quotaWindowHasData(window){if(!window)return false;const minutes=window.windowMinutes;const reset=window.resetAt;const hasMinutes=typeof minutes==='number'&&Number.isFinite(minutes)&&minutes>0;const hasReset=(typeof reset==='number'&&Number.isFinite(reset)&&reset>0)||(typeof reset==='string'&&Number.isFinite(Date.parse(reset)));const hasUsage=typeof window.usedPercent==='number'&&Number.isFinite(window.usedPercent)&&window.usedPercent>0;return hasMinutes||hasReset||hasUsage}\nfunction quotaLine(window,fallback){if(!quotaWindowHasData(window)||typeof window.usedPercent!=='number')return '';const remain=Math.max(0,Math.min(100,100-window.usedPercent));return '<div class=\"quota-line\" title=\"\u989D\u5EA6\u91CD\u7F6E\u65F6\u95F4 '+esc(timeText(window.resetAt))+'\"><span>'+esc(windowName(window,fallback))+'</span><progress max=\"100\" value=\"'+remain.toFixed(1)+'\"></progress><strong>'+remain.toFixed(0)+'%</strong></div>'}\nfunction quotaCell(quota){if(!quota)return '<span class=\"table-meta\">\u6682\u65E0\u989D\u5EA6\u91C7\u6837</span>';const lines=quotaLine(quota.primary,'\u4E3B\u7A97\u53E3')+quotaLine(quota.secondary,'\u6B21\u7A97\u53E3');const detail=[quota.planType,quota.model,quota.activeLimit!=null?'limit '+quota.activeLimit:null].filter(Boolean).join(' \u00B7 ');return '<div class=\"quota-stack\">'+(lines||'<span class=\"table-meta\">\u989D\u5EA6\u7A97\u53E3\u6682\u65E0\u6570\u636E</span>')+'<div class=\"table-meta\">'+esc(detail||'\u5DF2\u91C7\u6837')+'<br>\u91C7\u6837 '+esc(timeText(quota.updatedAt))+'</div></div>'}\nfunction newerQuota(localQuota,remoteQuota){if(!localQuota)return remoteQuota;if(!remoteQuota)return localQuota;return (instant(remoteQuota.updatedAt)||0)>=(instant(localQuota.updatedAt)||0)?remoteQuota:localQuota}\nfunction mergedChatgptAccounts(){const local=chatgptLocal?.accounts||[];const remote=chatgptRemote?.accounts||[];const used=new Set();const rows=local.map((item)=>{const index=remote.findIndex((candidate,i)=>!used.has(i)&&item.accountId&&candidate.accountId===item.accountId);const match=index>=0?remote[index]:null;if(index>=0)used.add(index);return {key:item.id,local:item,remote:match,label:item.label,email:item.email,accountId:item.accountId||match?.accountId,enabled:item.enabled,selected:item.selected,quota:newerQuota(item.quota,match?.quota)}});remote.forEach((item,index)=>{if(!used.has(index))rows.push({key:'remote_'+item.id,local:null,remote:item,label:item.label,email:null,accountId:item.accountId,enabled:true,selected:item.isDefault,quota:item.quota})});return rows}\nfunction syncTag(row){if(chatgptRemote?.status==='LOADING')return '<span class=\"tag waiting\">\u6B63\u5728\u540C\u6B65</span>';if(chatgptRemote?.status==='FAILED')return '<span class=\"tag failed\">\u8FDC\u7A0B\u540C\u6B65\u5931\u8D25</span>';if(chatgptRemote?.status==='NOT_CONFIGURED')return '<span class=\"tag waiting\">\u672A\u914D\u7F6E\u5E73\u53F0</span>';if(row.local&&row.remote)return '<span class=\"tag success\">\u5DF2\u540C\u6B65</span>';if(row.local)return '<span class=\"tag local\">\u4EC5\u672C\u5730</span>';return '<span class=\"tag success\">\u5E73\u53F0\u53EF\u7528</span>'}\nfunction remoteStage(){const remote=chatgptRemote;if(!remote||remote.status==='LOADING')return {tag:'<span class=\"tag waiting\"><span class=\"spin\">\u21BB</span> \u8FDE\u63A5\u4E2D</span>',title:'\u6B63\u5728\u8FDE\u63A5\u5E73\u53F0',detail:'\u672C\u5730\u8D26\u53F7\u5DF2\u7ECF\u53EF\u89C1\uFF0C\u6B63\u5728\u8BFB\u53D6\u8FDC\u7A0B\u53EF\u7528\u8D26\u53F7\u3002'};if(remote.status==='SUCCESS')return {tag:'<span class=\"tag success\">\u540C\u6B65\u6210\u529F</span>',title:remote.accounts.length+' \u4E2A\u5E73\u53F0\u8D26\u53F7',detail:'\u5B8C\u6210\u4E8E '+timeText(remote.syncedAt)};if(remote.status==='FAILED')return {tag:'<span class=\"tag failed\">\u8FDC\u7A0B\u540C\u6B65\u5931\u8D25</span>',title:'\u4FDD\u7559\u672C\u5730\u7ED3\u679C',detail:remote.error?.message||'\u65E0\u6CD5\u8FDE\u63A5\u8FDC\u7A0B\u8D26\u53F7\u670D\u52A1'};return {tag:'<span class=\"tag waiting\">\u672A\u914D\u7F6E</span>',title:'\u672A\u914D\u7F6E\u5E73\u53F0\u5730\u5740',detail:'\u53EF\u5728\u57FA\u7840\u914D\u7F6E\u4E2D\u586B\u5199\u5E73\u53F0 API \u5730\u5740\u3002'}}\nfunction accountRows(rows){return rows.map((row)=>'<tr><td><div class=\"account-name\"><strong>'+esc(row.label)+'</strong><small>'+esc(row.email||row.accountId||row.key)+'</small></div></td><td><div class=\"status-stack\">'+(row.enabled?'<span class=\"tag success\">\u53EF\u7528</span>':'<span class=\"tag failed\">\u5DF2\u505C\u7528</span>')+(row.selected?'<span class=\"tag role\">\u5BA2\u6237\u7AEF\u5DF2\u9009</span>':'')+'</div></td><td>'+quotaCell(row.quota)+'</td><td><div class=\"table-meta\">Token '+esc(row.local?'\u672C\u5730\u53EF\u7528':'\u6309\u9700\u4ECE\u5E73\u53F0\u83B7\u53D6')+'<br>\u5230\u671F '+esc(timeText(row.local?.accessExpiresAt||row.remote?.accessExpiresAt))+'</div></td><td><div class=\"status-stack\">'+syncTag(row)+'</div></td></tr>').join('')}\nfunction chatgptPage(){const rows=mergedChatgptAccounts();const remote=remoteStage();const localReady=chatgptLocal?.status==='READY';const localCount=chatgptLocal?.available??0;const failure=chatgptRemote?.status==='FAILED'?'<div class=\"notice error\"><strong>\u8FDC\u7A0B\u540C\u6B65\u5931\u8D25\u3002</strong> '+esc(chatgptRemote.error?.message||'\u5E73\u53F0\u6682\u65F6\u4E0D\u53EF\u7528')+'\uFF1B\u672C\u5730\u8D26\u53F7\u6570\u636E\u4ECD\u53EF\u6B63\u5E38\u67E5\u770B\u3002</div>':'';return '<div class=\"content\"><section class=\"hero\"><div><div class=\"eyebrow\">Local first / Remote second</div><h2>ChatGPT \u8D26\u53F7\u8FD0\u884C\u89C6\u56FE</h2><p>\u5148\u8BFB\u53D6 codex-multi-auth \u672C\u5730\u8D26\u53F7\u4E0E\u989D\u5EA6\u7F13\u5B58\uFF0C\u518D\u8FDE\u63A5\u5E73\u53F0\u8865\u5145\u5F53\u524D\u7528\u6237\u53EF\u7528\u8D26\u53F7\u3002\u9875\u9762\u6C38\u4E0D\u63A5\u6536 access token \u6216 refresh token\u3002</p></div><div class=\"session-track\"><div class=\"track-node live\"><span>\u672C\u5730\u53EF\u7528</span><strong>'+localCount+'</strong></div><div class=\"track-node\"><span>\u6C47\u603B\u8D26\u53F7</span><strong>'+rows.length+'</strong></div><div class=\"track-node\"><span>\u8FDC\u7A0B</span><strong>'+esc(chatgptRemote?.status||'WAITING')+'</strong></div></div></section><section class=\"sync-board\"><article class=\"sync-stage\"><div class=\"eyebrow\">01 / Local pool</div><strong>'+(localReady?localCount+' \u4E2A\u672C\u5730\u53EF\u7528\u8D26\u53F7':'\u6B63\u5728\u8BFB\u53D6\u672C\u5730\u8D26\u53F7')+'</strong><p>'+(localReady?'\u8BFB\u53D6\u5B8C\u6210\u4E8E '+timeText(chatgptLocal.readAt):'\u4F18\u5148\u8BFB\u53D6 codex-multi-auth \u5B89\u5168\u6458\u8981\u3002')+'</p></article><div class=\"sync-link\" aria-label=\"\u7136\u540E\u8FDE\u63A5\u8FDC\u7A0B\"></div><article class=\"sync-stage\"><div class=\"eyebrow\">02 / Platform service</div>'+remote.tag+'<strong>'+esc(remote.title)+'</strong><p>'+esc(remote.detail)+'</p></article></section>'+failure+'<section class=\"table-card\"><header class=\"table-head\"><div><h3>\u53EF\u7528\u8D26\u53F7\u4E0E\u5269\u4F59\u989D\u5EA6</h3><p>\u989D\u5EA6\u4F18\u5148\u5C55\u793A\u672C\u5730\u6216\u670D\u52A1\u5668\u7AEF\u66F4\u65B0\u8F83\u65B0\u7684\u5B89\u5168\u5FEB\u7167\u3002</p></div><button class=\"button\" data-action=\"chatgpt-refresh\" '+(chatgptLoading?'disabled':'')+'>'+(chatgptLoading?'<span class=\"spin\">\u21BB</span> \u540C\u6B65\u4E2D':'\u21BB \u91CD\u65B0\u540C\u6B65')+'</button></header>'+(rows.length?'<div class=\"account-table-wrap\"><table class=\"account-table\"><thead><tr><th>\u8D26\u53F7</th><th>\u53EF\u7528\u72B6\u6001</th><th>\u5269\u4F59 limit</th><th>\u51ED\u636E\u65F6\u6548</th><th>\u540C\u6B65\u72B6\u6001</th></tr></thead><tbody>'+accountRows(rows)+'</tbody></table></div>':'<div class=\"empty-state'+(chatgptLoading?'':' account-request')+'\">'+(chatgptLoading?'\u6B63\u5728\u8BFB\u53D6\u672C\u5730\u8D26\u53F7\u2026':'<strong>\u9700\u8981\u7533\u8BF7 ChatGPT \u8D26\u53F7</strong><span>'+esc(noAvailableAccountGuidance)+'</span>')+'</div>')+'</section></div>'}\nfunction configPage(){const c=state.config;return '<div class=\"content\"><section class=\"grid\"><article class=\"card full\"><div class=\"eyebrow\">Configuration file</div><h3>\u57FA\u7840\u914D\u7F6E</h3><p>\u4E0E <code>numa config --set</code> \u4F7F\u7528\u540C\u4E00\u4EFD\u914D\u7F6E\u548C\u6821\u9A8C\u903B\u8F91\u3002</p><form id=\"config-form\" class=\"form-grid\"><div class=\"field\"><label>\u754C\u9762\u8BED\u8A00</label><select name=\"locale\"><option '+(c.locale==='zh-CN'?'selected':'')+'>zh-CN</option><option '+(c.locale==='en-US'?'selected':'')+'>en-US</option></select></div><div class=\"field\"><label>\u9ED8\u8BA4\u8F93\u51FA</label><select name=\"output\"><option '+(c.output==='text'?'selected':'')+'>text</option><option '+(c.output==='json'?'selected':'')+'>json</option></select></div><div class=\"field span-2\"><label>\u5E73\u53F0 API \u5730\u5740</label><input name=\"apiBaseUrl\" value=\"'+esc(c.apiBaseUrl)+'\" placeholder=\"https://platform.example/api\"></div><div class=\"field span-2\"><label>npm registry</label><input name=\"registryUrl\" value=\"'+esc(c.registryUrl)+'\"></div><label class=\"field span-2\"><span>\u66F4\u65B0\u68C0\u67E5</span><select name=\"checkUpdates\"><option value=\"true\" '+(c.checkUpdates?'selected':'')+'>\u542F\u7528</option><option value=\"false\" '+(!c.checkUpdates?'selected':'')+'>\u5173\u95ED</option></select></label><div class=\"actions span-2\"><button class=\"button primary\" type=\"submit\">\u4FDD\u5B58\u914D\u7F6E</button></div></form><div id=\"notice\"></div></article></section></div>'}\nfunction commandsPage(){return '<div class=\"content\"><section class=\"card full\"><div class=\"command-toolbar\"><div class=\"field grow\"><label>\u8FC7\u6EE4\u547D\u4EE4</label><input id=\"command-search\" placeholder=\"\u4F8B\u5982 codex\u3001media\u3001auth\"></div></div><div id=\"command-list\" class=\"command-list\">'+commandCards('')+'</div></section></div>'}\nfunction commandCards(query){const q=query.toLowerCase();return state.commands.filter((c)=>(c.command+' '+c.summary).toLowerCase().includes(q)).map((c)=>'<article class=\"command\"><div class=\"command-head\"><code>'+esc(c.command)+'</code><span class=\"tag '+esc(c.access)+'\">'+esc(c.access)+'</span></div><p>'+esc(c.summary)+'</p><small>'+esc(c.examples[0]||'')+'</small><div class=\"actions mt-10\"><button class=\"button\" data-load=\"'+esc(JSON.stringify(commandPrefix(c.command)))+'\">\u8F7D\u5165\u6267\u884C\u5668</button></div></article>').join('')||'<p>\u6CA1\u6709\u5339\u914D\u547D\u4EE4\u3002</p>'}\nfunction commandPrefix(command){return command.split(/s+/).filter((part)=>!part.startsWith('[')&&!part.startsWith('<')&&!part.startsWith('--'))}\nfunction runnerPage(){return '<div class=\"content\"><section class=\"grid\"><article class=\"card\"><div class=\"eyebrow\">Structured argv</div><h3>\u5B89\u5168\u6267\u884C\u5668</h3><p>\u4F7F\u7528 JSON \u6570\u7EC4\u4F20\u5165\u53C2\u6570\uFF0C\u4E0D\u7ECF\u8FC7 shell\u3002\u957F\u9A7B\u6216\u4EA4\u4E92\u547D\u4EE4\u9700\u8981\u5728\u7EC8\u7AEF\u8FD0\u884C\u3002</p><form id=\"runner-form\"><div class=\"field\"><label>\u547D\u4EE4\u53C2\u6570</label><textarea id=\"argv\" class=\"mono\">[\"doctor\"]</textarea></div><label class=\"field mt-12\"><span>\u5371\u9669\u64CD\u4F5C\u786E\u8BA4</span><select id=\"confirm\"><option value=\"false\">\u672A\u786E\u8BA4</option><option value=\"true\">\u786E\u8BA4\u6267\u884C\u5220\u9664/\u91CD\u7F6E</option></select></label><div class=\"actions mt-14\"><button class=\"button primary\" type=\"submit\">\u8FD0\u884C\u547D\u4EE4</button></div></form></article><article class=\"card wide\"><div class=\"eyebrow\">Result stream</div><h3>\u7ED3\u6784\u5316\u8F93\u51FA</h3><pre id=\"output\" class=\"output\">'+esc(output)+'</pre></article></section></div>'}\nasync function loadChatgpt(){if(chatgptLoading)return;chatgptLoading=true;chatgptLocal=null;chatgptRemote={status:'LOADING',accounts:[]};render();try{chatgptLocal=await api('/api/chatgpt/local')}catch(error){chatgptLocal={status:'FAILED',accounts:[],available:0,total:0,error:{message:error.message}}}render();try{chatgptRemote=await api('/api/chatgpt/remote')}catch(error){chatgptRemote={status:'FAILED',accounts:[],syncedAt:Date.now(),error:{message:error.message}}}finally{chatgptLoading=false;render()}}\nfunction render(){if(!state.loggedIn){app.innerHTML=loginGate();bind();return}const page=active==='overview'?overview():active==='chatgpt'?chatgptPage():active==='config'?configPage():active==='commands'?commandsPage():runnerPage();app.innerHTML=shell(page);bind()}\nfunction notice(message,type='success'){const target=document.getElementById('notice');if(target)target.innerHTML='<div class=\"notice '+type+'\">'+esc(message)+'</div>'}\nfunction bindCommandLoaders(){document.querySelectorAll('[data-load]').forEach((b)=>b.addEventListener('click',()=>{const argv=JSON.parse(b.dataset.load);active='runner';render();document.getElementById('argv').value=JSON.stringify(argv)}))}\nfunction bind(){document.querySelectorAll('[data-nav]').forEach((b)=>b.addEventListener('click',()=>{active=b.dataset.nav;render();if(active==='chatgpt'&&!chatgptLocal&&!chatgptLoading)void loadChatgpt()}));document.querySelector('[data-action=\"chatgpt-refresh\"]')?.addEventListener('click',()=>void loadChatgpt());document.querySelector('[data-action=\"login\"]')?.addEventListener('click',async()=>{try{notice('\u6B63\u5728\u542F\u52A8\u767B\u5F55\u2026');const result=await api('/api/auth/login',{method:'POST',body:'{}'});location.href=result.authorizationUrl}catch(e){notice(e.message,'error')}});document.querySelector('[data-action=\"reload\"]')?.addEventListener('click',()=>bootstrap());document.querySelector('[data-action=\"logout\"]')?.addEventListener('click',async()=>{if(!confirm('\u6E05\u9664\u672C\u673A Keycloak \u767B\u5F55\u7F13\u5B58\uFF1F'))return;await api('/api/auth/logout',{method:'POST',body:'{}'});location.reload()});document.getElementById('config-form')?.addEventListener('submit',async(e)=>{e.preventDefault();const data=Object.fromEntries(new FormData(e.currentTarget));data.checkUpdates=data.checkUpdates==='true';try{const result=await api('/api/config',{method:'PUT',body:JSON.stringify(data)});state.config=result.config;notice('\u914D\u7F6E\u5DF2\u4FDD\u5B58')}catch(err){notice(err.message,'error')}});document.getElementById('command-search')?.addEventListener('input',(e)=>{document.getElementById('command-list').innerHTML=commandCards(e.target.value);bindCommandLoaders()});bindCommandLoaders();document.getElementById('runner-form')?.addEventListener('submit',async(e)=>{e.preventDefault();try{const argv=JSON.parse(document.getElementById('argv').value);const confirmed=document.getElementById('confirm')?.value==='true';output='\u8FD0\u884C\u4E2D\u2026';render();const result=await api('/api/commands/run',{method:'POST',body:JSON.stringify({argv,confirm:confirmed})});output=result.stdout||result.stderr||JSON.stringify(result,null,2);render()}catch(err){output='\u9519\u8BEF: '+err.message;render()}})}\nbootstrap();\n";
|
|
2
|
+
export declare const CONSOLE_CSS = "\n:root{color-scheme:light;--ink:#0b1728;--navy:#07182b;--navy-2:#102943;--cyan:#0eb7bf;--cyan-soft:#dff7f6;--mist:#eef4f8;--paper:#fbfdff;--line:#d7e2ea;--muted:#64778c;--amber:#e99a32;--red:#d94b58;--green:#198a68;--shadow:0 18px 50px rgba(12,38,62,.1)}\n*{box-sizing:border-box}html,body,#app{min-height:100%}body{min-height:100vh;margin:0;background:var(--mist);color:var(--ink);font-family:\"Avenir Next\",\"Segoe UI\",system-ui,sans-serif}button,input,select,textarea{font:inherit}.mono,code,pre{font-family:\"SFMono-Regular\",Consolas,\"Liberation Mono\",monospace}.boot{min-height:100vh;display:grid;place-content:center;justify-items:center;gap:14px;color:var(--muted)}.boot-mark,.brand-mark{display:grid;place-items:center;background:var(--cyan);color:#00262a;font-weight:900}.boot-mark{width:50px;height:50px;border-radius:15px;box-shadow:0 0 0 8px rgba(14,183,191,.12)}\n.shell{min-height:100vh;display:grid;grid-template-columns:260px 1fr}.rail{position:sticky;top:0;height:100vh;padding:24px 18px;background:var(--navy);color:#d7e6f3;display:flex;flex-direction:column}.brand{display:flex;align-items:center;gap:12px;padding:8px 8px 26px}.brand-mark{width:38px;height:38px;border-radius:12px}.brand strong{display:block;letter-spacing:.04em}.brand small{display:block;margin-top:4px;color:#708aa3;font-size:10px;letter-spacing:.18em;text-transform:uppercase}.nav{display:grid;gap:7px}.nav button{border:0;background:transparent;color:#8da4b8;border-radius:11px;text-align:left;padding:11px 13px;cursor:pointer;display:flex;justify-content:space-between}.nav button:hover,.nav button.active{background:var(--navy-2);color:white}.nav button.active{box-shadow:inset 3px 0 var(--cyan)}.rail-foot{margin-top:auto;border:1px solid #203b55;border-radius:14px;padding:14px}.rail-foot .pulse{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--cyan);box-shadow:0 0 12px var(--cyan);margin-right:7px}.rail-foot p{margin:7px 0 0;color:#7791a8;font-size:11px;line-height:1.5}\n.main{min-width:0;min-height:100vh;display:flex;flex-direction:column}.topbar{height:76px;padding:0 34px;background:rgba(251,253,255,.94);border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:3;backdrop-filter:blur(12px)}.topbar h1{font-size:17px;margin:0}.eyebrow{font:700 10px/1.2 \"SFMono-Regular\",monospace;text-transform:uppercase;letter-spacing:.16em;color:var(--cyan)}.user-chip{display:flex;align-items:center;gap:10px}.avatar{width:34px;height:34px;border-radius:50%;display:grid;place-items:center;background:var(--cyan-soft);color:#087d84;font-weight:800}.user-chip small{color:var(--muted)}.content{width:100%;max-width:1420px;flex:1;margin:0 auto;padding:30px 34px 56px}.hero{background:var(--navy);color:white;border-radius:22px;padding:28px 30px;display:grid;grid-template-columns:1fr auto;gap:24px;position:relative;overflow:hidden;box-shadow:var(--shadow)}.hero:after{content:\"\";position:absolute;right:-45px;top:-90px;width:320px;height:240px;border:34px solid rgba(14,183,191,.09);transform:rotate(-18deg);border-radius:48px}.hero h2{font-size:32px;line-height:1.08;margin:10px 0}.hero p{max-width:720px;color:#91aac0;margin:0;line-height:1.6}.session-track{align-self:center;display:flex;gap:0;position:relative;z-index:1}.track-node{min-width:112px;border:1px solid #2a4661;padding:13px 15px;background:#0b1e33}.track-node:first-child{border-radius:13px 0 0 13px}.track-node:last-child{border-radius:0 13px 13px 0}.track-node span{display:block;color:#7690a7;font-size:9px;text-transform:uppercase;letter-spacing:.12em}.track-node strong{display:block;margin-top:6px;font-size:13px}.track-node.live strong{color:#58e0df}\n.grid{display:grid;grid-template-columns:repeat(12,1fr);gap:18px;margin-top:20px}.card{grid-column:span 4;background:var(--paper);border:1px solid var(--line);border-radius:17px;padding:20px;box-shadow:0 10px 30px rgba(25,54,79,.05)}.card.wide{grid-column:span 8}.card.full{grid-column:1/-1}.card h3{margin:0;font-size:15px}.card p{color:var(--muted);font-size:12px;line-height:1.6}.metric{font-size:28px;font-weight:750;margin-top:16px}.metric small{font-size:11px;color:var(--muted);font-weight:500}.tag{display:inline-flex;border-radius:999px;padding:5px 8px;font:700 9px/1 \"SFMono-Regular\",monospace;text-transform:uppercase;letter-spacing:.05em;background:var(--cyan-soft);color:#087d84}.tag.public{background:#edf1f5;color:#5d7082}.tag.role{background:#fff0d9;color:#9c5c09}.actions{display:flex;gap:8px;flex-wrap:wrap}.button{border:1px solid var(--line);border-radius:10px;background:white;color:var(--ink);padding:9px 12px;cursor:pointer;font-weight:700;font-size:12px}.button:hover{border-color:#9ab6c9;background:#f7fbfd}.button.primary{background:#0b949b;border-color:#0b949b;color:white}.button.danger{color:var(--red);border-color:#f0bec4}.button:disabled{opacity:.45;cursor:not-allowed}.field{display:grid;gap:6px}.field label{font-size:11px;color:var(--muted);font-weight:700}.field input,.field select,.field textarea{width:100%;border:1px solid var(--line);border-radius:10px;background:white;padding:10px 11px;color:var(--ink);outline:none}.field input:focus,.field textarea:focus{border-color:var(--cyan);box-shadow:0 0 0 3px rgba(14,183,191,.12)}.field textarea{min-height:110px;resize:vertical}.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}.form-grid .span-2{grid-column:1/-1}.command-toolbar{display:flex;gap:10px;margin-bottom:15px}.grow{flex:1}.mt-10{margin-top:10px}.mt-12{margin-top:12px}.mt-14{margin-top:14px}.command-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:11px}.command{border:1px solid var(--line);border-radius:13px;padding:14px;background:white}.command-head{display:flex;gap:8px;align-items:center;justify-content:space-between}.command code{font-size:11px;color:#0b6f76}.command p{margin:9px 0}.command small{display:block;color:#8a9bab;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.output{background:#071522;color:#b8d1e2;border-radius:13px;padding:15px;min-height:170px;max-height:420px;overflow:auto;white-space:pre-wrap;font-size:11px;line-height:1.65}.notice{margin-top:14px;border-radius:11px;padding:11px 13px;font-size:12px}.notice.error{background:#fff0f2;color:#a52c3b;border:1px solid #f1c4ca}.notice.success{background:#e8f7f1;color:#176c54;border:1px solid #bce4d5}.login-gate{min-height:calc(100vh - 76px);display:grid;place-content:center;padding:30px}.login-card{width:min(520px,90vw);background:white;border:1px solid var(--line);border-radius:22px;padding:32px;box-shadow:var(--shadow)}.login-card h2{font-size:28px;margin:14px 0 9px}.login-card p{color:var(--muted);line-height:1.65}.hidden{display:none!important}\n.sync-board{display:grid;grid-template-columns:1fr 56px 1fr;align-items:stretch;margin-top:20px}.sync-stage{background:var(--paper);border:1px solid var(--line);border-radius:17px;padding:18px 20px;box-shadow:0 10px 30px rgba(25,54,79,.05)}.sync-stage strong{display:block;margin-top:8px;font-size:18px}.sync-stage p{margin:7px 0 0;color:var(--muted);font-size:11px}.sync-link{display:grid;place-items:center;color:var(--cyan);font-size:22px}.sync-link:before{content:\"\";height:2px;width:100%;background:linear-gradient(90deg,var(--cyan),#bad8dc)}.table-card{margin-top:18px;background:var(--paper);border:1px solid var(--line);border-radius:17px;box-shadow:0 10px 30px rgba(25,54,79,.05);overflow:hidden}.table-head{padding:18px 20px;display:flex;align-items:center;justify-content:space-between;gap:16px;border-bottom:1px solid var(--line)}.table-head h3{margin:0}.table-head p{margin:5px 0 0;color:var(--muted);font-size:11px}.account-table-wrap{overflow:auto}.account-table{width:100%;border-collapse:collapse;min-width:920px}.account-table th,.account-table td{text-align:left;padding:14px 16px;border-bottom:1px solid #e7eef3;vertical-align:middle}.account-table th{background:#f5f9fb;color:#6c8094;font-size:10px;text-transform:uppercase;letter-spacing:.08em}.account-table tr:last-child td{border-bottom:0}.account-name strong{display:block;font-size:13px}.account-name small{display:block;margin-top:4px;color:var(--muted);font-size:10px}.quota-stack{min-width:210px;display:grid;gap:8px}.quota-line{display:grid;grid-template-columns:42px 1fr 38px;align-items:center;gap:7px;font-size:10px;color:var(--muted)}progress{width:100%;height:7px;border:0;border-radius:99px;overflow:hidden;background:#e7eef3}progress::-webkit-progress-bar{background:#e7eef3;border-radius:99px}progress::-webkit-progress-value{background:var(--cyan);border-radius:99px}progress::-moz-progress-bar{background:var(--cyan);border-radius:99px}.status-stack{display:flex;gap:6px;flex-wrap:wrap}.tag.success{background:#e8f7f1;color:var(--green)}.tag.failed{background:#fff0f2;color:var(--red)}.tag.waiting{background:#fff0d9;color:#9c5c09}.tag.local{background:#e8f1fa;color:#32658f}.empty-state{padding:48px 20px;text-align:center;color:var(--muted)}.empty-state.account-request{margin:24px;border:2px solid #f0b5bd;border-radius:16px;background:#fff2f4;color:#8d2334;box-shadow:0 12px 30px rgba(167,44,64,.1)}.empty-state.account-request strong{display:block;margin-bottom:9px;font-size:16px}.empty-state.account-request span{display:block;font-size:13px;line-height:1.7}.table-meta{color:var(--muted);font-size:10px;line-height:1.55}.spin{display:inline-block;animation:turn 1s linear infinite}@keyframes turn{to{transform:rotate(360deg)}}\n.registry-hero{background:linear-gradient(118deg,#06172a 0%,#0b2d42 67%,#0a4c55 100%)}.lifecycle{display:grid;grid-template-columns:repeat(3,84px);align-items:center;position:relative;z-index:1}.life-node{position:relative;text-align:center;color:#7293a7;font:700 9px/1.2 \"SFMono-Regular\",monospace;text-transform:uppercase;letter-spacing:.08em}.life-node:before{content:\"\";display:block;width:11px;height:11px;margin:0 auto 9px;border:2px solid #58e0df;border-radius:50%;background:#0a2638;box-shadow:0 0 0 5px rgba(88,224,223,.08)}.life-node:not(:last-child):after{content:\"\";position:absolute;height:1px;width:calc(100% - 24px);top:6px;left:calc(50% + 12px);background:linear-gradient(90deg,#58e0df,#31546a)}.life-node.live{color:#d5ffff}.life-node.live:before{background:#58e0df;box-shadow:0 0 15px #58e0df}.registry-toolbar{display:grid;grid-template-columns:minmax(190px,1fr) auto minmax(190px,1fr) auto;gap:10px;align-items:end;margin-top:20px}.segment{display:flex;padding:3px;background:#dfe9ef;border-radius:11px}.segment button{border:0;background:transparent;padding:8px 13px;border-radius:8px;color:var(--muted);font-weight:750;font-size:11px;cursor:pointer}.segment button.active{background:white;color:#087d84;box-shadow:0 3px 12px rgba(7,24,43,.1)}.registry-board{display:grid;grid-template-columns:minmax(260px,32%) 1fr;min-height:560px;margin-top:14px;background:var(--paper);border:1px solid var(--line);border-radius:18px;overflow:hidden;box-shadow:var(--shadow)}.registry-list{border-right:1px solid var(--line);background:#f4f8fa}.registry-list-head{padding:15px 17px;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;align-items:center}.registry-items{padding:9px;display:grid;gap:6px;max-height:680px;overflow:auto}.registry-item{width:100%;border:1px solid transparent;background:transparent;border-radius:12px;padding:12px;text-align:left;cursor:pointer;display:grid;grid-template-columns:12px 1fr;gap:10px;color:var(--ink)}.registry-item:hover,.registry-item.active{background:white;border-color:var(--line);box-shadow:0 6px 20px rgba(18,49,71,.07)}.registry-dot{width:8px;height:8px;margin-top:4px;border-radius:50%;background:var(--amber);box-shadow:0 0 0 4px rgba(233,154,50,.1)}.registry-dot.online{background:var(--green);box-shadow:0 0 0 4px rgba(25,138,104,.1)}.registry-item strong,.registry-item small{display:block}.registry-item strong{font-size:12px}.registry-item small{margin-top:5px;color:var(--muted);font-size:10px}.registry-detail{min-width:0;padding:22px;display:flex;flex-direction:column}.registry-detail-head{display:flex;justify-content:space-between;align-items:start;gap:16px}.registry-detail h3{margin:5px 0 3px;font-size:20px}.registry-detail pre{flex:1;margin:16px 0;min-height:270px}.registry-actions{border-top:1px solid var(--line);padding-top:15px;display:grid;gap:12px}.install-row{display:grid;grid-template-columns:1fr 1fr auto;gap:9px}.registry-error{margin-top:14px}.button:focus-visible,.nav button:focus-visible,.registry-item:focus-visible,.segment button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:3px solid rgba(14,183,191,.3);outline-offset:2px}\n@media(max-width:980px){.shell{grid-template-columns:78px 1fr}.rail{padding:18px 10px}.brand div:last-child,.nav button span:first-child,.rail-foot p,.rail-foot strong{display:none}.brand{justify-content:center}.nav button{justify-content:center}.card,.card.wide{grid-column:span 6}.hero{grid-template-columns:1fr}.session-track,.lifecycle{display:none}.registry-toolbar{grid-template-columns:1fr 1fr}.registry-board{grid-template-columns:1fr}.registry-list{border-right:0;border-bottom:1px solid var(--line)}.registry-items{max-height:260px}}\n@media(max-width:680px){.shell{display:block}.rail{height:auto;position:static;flex-direction:row;align-items:center;padding:10px 14px;overflow-x:auto}.brand{padding:0;margin-right:auto;flex:0 0 auto}.nav{display:flex}.nav button{padding:9px;flex:0 0 auto}.rail-foot{display:none}.topbar{padding:0 18px}.content{padding:20px 14px}.hero{padding:23px}.hero h2{font-size:25px}.card,.card.wide{grid-column:1/-1}.form-grid,.command-list,.registry-toolbar,.install-row{grid-template-columns:1fr}.form-grid .span-2{grid-column:auto}.user-chip div:not(.avatar){display:none}.sync-board{grid-template-columns:1fr}.sync-link{height:32px;transform:rotate(90deg)}.table-head{align-items:flex-start;flex-direction:column}.registry-detail{padding:17px}}\n@media(prefers-reduced-motion:no-preference){.card,.hero{animation:arrive .35s ease both}@keyframes arrive{from{opacity:0;transform:translateY(7px)}to{opacity:1;transform:none}}}\n";
|
|
3
|
+
export declare const CONSOLE_JS = "\nconst app=document.getElementById('app');let state=null;let csrf='';let active='overview';let output='\u9009\u62E9\u4E00\u6761\u547D\u4EE4\u6216\u8F93\u5165\u53C2\u6570\u540E\u8FD0\u884C\u3002';let chatgptLocal=null;let chatgptRemote=null;let chatgptLoading=false;let registryNamespaces=[];let registryItems=[];let registryDetail=null;let registryMode='skill';let registryNamespaceId=null;let registryName='';let registrySearch='';let registryLoading=false;let registryError='';const noAvailableAccountGuidance='\u5F53\u524D\u7528\u6237\u6CA1\u6709\u7ED1\u5B9A\u53EF\u7528\u7684 ChatGPT \u8D26\u53F7\u3002\u8BF7\u524D\u5F80 OA \u7CFB\u7EDF\u63D0\u4EA4 IT11 \u5DE5\u5355\uFF0C\u7533\u8BF7\u72EC\u4EAB\u6216\u5171\u4EAB\u7684 ChatGPT \u4E2A\u4EBA\u8D26\u53F7\u3002';\nconst esc=(v)=>String(v??'').replace(/[&<>\"']/g,(c)=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));\nasync function api(path,init={}){const headers={'Accept':'application/json',...(init.body?{'Content-Type':'application/json'}:{}),...(csrf?{'x-numa-console-csrf':csrf}:{})};const response=await fetch(path,{...init,headers:{...headers,...(init.headers||{})},credentials:'same-origin'});const body=await response.json().catch(()=>({error:{message:'\u54CD\u5E94\u4E0D\u662F JSON'}}));if(!response.ok)throw new Error(body.error?.message||body.stderr||body.stdout||('HTTP '+response.status));return body}\nasync function bootstrap(attempt=0){try{state=await api('/api/bootstrap');csrf=state.csrfToken;const callbackState=new URLSearchParams(location.search).get('login');const waitingForSuccess=callbackState==='complete'&&!state.loggedIn&&attempt<40;const waitingForFailure=callbackState==='failed'&&!state.loginError&&attempt<8;if(waitingForSuccess||waitingForFailure){app.innerHTML='<div class=\"boot\"><div class=\"boot-mark\">N</div><p>'+(callbackState==='complete'?'\u767B\u5F55\u6210\u529F\uFF0C\u6B63\u5728\u5EFA\u7ACB\u672C\u673A\u4F1A\u8BDD\u2026':'\u6B63\u5728\u8BFB\u53D6\u767B\u5F55\u5931\u8D25\u4FE1\u606F\u2026')+'</p></div>';setTimeout(()=>bootstrap(attempt+1),250);return}if(callbackState==='complete'&&!state.loggedIn&&!state.loginError)state.loginError='\u767B\u5F55\u51ED\u636E\u4EA4\u6362\u5C1A\u672A\u5B8C\u6210\uFF0C\u8BF7\u91CD\u8BD5\u767B\u5F55\u3002';if(callbackState==='failed'&&!state.loginError)state.loginError='Keycloak \u767B\u5F55\u672A\u5B8C\u6210\uFF0C\u8BF7\u91CD\u8BD5\u3002';if(callbackState)history.replaceState({},'',location.pathname);render()}catch(error){app.innerHTML='<div class=\"login-gate\"><div class=\"login-card\"><span class=\"tag role\">\u8FDE\u63A5\u5931\u8D25</span><h2>\u672C\u5730\u63A7\u5236\u53F0\u4E0D\u53EF\u7528</h2><p>'+esc(error.message)+'</p></div></div>'}}\nfunction navButton(id,label,mark){return '<button data-nav=\"'+id+'\" class=\"'+(active===id?'active':'')+'\"><span>'+label+'</span><span class=\"mono\">'+mark+'</span></button>'}\nfunction shell(content){const user=state.user||{};const label=user.username||user.email||'\u672A\u767B\u5F55';return '<div class=\"shell\"><aside class=\"rail\"><div class=\"brand\"><div class=\"brand-mark\">N</div><div><strong>Numa CLI</strong><small>Local control plane</small></div></div><nav class=\"nav\">'+navButton('overview','\u8FD0\u884C\u6982\u89C8','01')+navButton('registry','AI Registry','02')+navButton('chatgpt','ChatGPT \u8D26\u53F7','03')+navButton('config','\u57FA\u7840\u914D\u7F6E','04')+navButton('commands','\u547D\u4EE4\u80FD\u529B','05')+navButton('runner','\u6267\u884C\u5668','06')+'</nav><div class=\"rail-foot\"><strong><span class=\"pulse\"></span>\u4EC5\u76D1\u542C\u672C\u673A</strong><p>Token \u4E0D\u4F1A\u53D1\u9001\u7ED9\u7F51\u9875\u811A\u672C\uFF0C\u4E5F\u4E0D\u4F1A\u51FA\u73B0\u5728\u547D\u4EE4\u8F93\u51FA\u4E2D\u3002</p></div></aside><main class=\"main\"><header class=\"topbar\"><div><div class=\"eyebrow\">Numa / Web console</div><h1>'+sectionTitle()+'</h1></div><div class=\"user-chip\"><div class=\"avatar\">'+esc(label.slice(0,1).toUpperCase())+'</div><div><strong>'+esc(label)+'</strong><br><small>'+esc(state.loggedIn?'Keycloak \u4F1A\u8BDD\u6709\u6548':'\u9700\u8981\u767B\u5F55')+'</small></div></div></header>'+content+'</main></div>'}\nfunction sectionTitle(){return {overview:'\u8FD0\u884C\u6982\u89C8',registry:'AI Registry',chatgpt:'ChatGPT \u8D26\u53F7',config:'\u57FA\u7840\u914D\u7F6E',commands:'\u547D\u4EE4\u80FD\u529B',runner:'\u5B89\u5168\u6267\u884C\u5668'}[active]||'\u63A7\u5236\u53F0'}\nfunction loginGate(){const error=state?.loginError?'<div class=\"notice error\">'+esc(state.loginError)+'</div>':'';return '<div class=\"login-gate\"><div class=\"login-card\"><span class=\"tag role\">Keycloak PKCE</span><h2>\u767B\u5F55\u540E\u8FDB\u5165\u672C\u5730\u63A7\u5236\u53F0</h2><p>\u6D4F\u89C8\u5668\u53EA\u8D1F\u8D23\u5B8C\u6210\u8BA4\u8BC1\u548C\u64CD\u4F5C\u672C\u673A API\u3002\u8BBF\u95EE\u4EE4\u724C\u4FDD\u5B58\u5728 CLI \u7684\u79C1\u6709\u7F13\u5B58\u4E2D\uFF0C\u4E0D\u5199\u5165 localStorage\uFF0C\u4E5F\u4E0D\u8FD4\u56DE\u7ED9\u9875\u9762\u3002</p><div class=\"actions\"><button class=\"button primary\" data-action=\"login\">\u4F7F\u7528 Keycloak \u767B\u5F55</button></div><div id=\"notice\">'+error+'</div></div></div>'}\nfunction overview(){const u=state.user||{};const roles=[...(u.realmRoles||[]),...Object.values(u.clientRoles||{}).flat()];return '<div class=\"content\"><section class=\"hero\"><div><div class=\"eyebrow\">Authenticated workstation</div><h2>\u672C\u673A\u4F1A\u8BDD\u5DF2\u63A5\u5165\u7814\u53D1\u63A7\u5236\u9762</h2><p>\u4ECE\u540C\u4E00\u5904\u68C0\u67E5\u8EAB\u4EFD\u3001\u8C03\u6574 CLI \u914D\u7F6E\uFF0C\u5E76\u4EE5\u7ED3\u6784\u5316\u53C2\u6570\u8FD0\u884C\u53D7\u652F\u6301\u547D\u4EE4\u3002\u6240\u6709\u7F51\u7EDC\u8BF7\u6C42\u7EE7\u7EED\u7531 CLI \u6CE8\u5165 Bearer Token\u3002</p></div><div class=\"session-track\"><div class=\"track-node live\"><span>\u8EAB\u4EFD</span><strong>'+esc(u.username||'\u5DF2\u767B\u5F55')+'</strong></div><div class=\"track-node\"><span>API</span><strong>'+esc(state.config.apiBaseUrl?'\u5DF2\u914D\u7F6E':'\u672A\u914D\u7F6E')+'</strong></div><div class=\"track-node\"><span>\u89D2\u8272</span><strong>'+roles.length+'</strong></div></div></section><section class=\"grid\"><article class=\"card\"><div class=\"eyebrow\">Session</div><div class=\"metric\">'+esc(state.expiresAt?new Date(state.expiresAt*1000).toLocaleTimeString('zh-CN',{hour:'2-digit',minute:'2-digit'}):'\u2014')+' <small>\u8BBF\u95EE\u4EE4\u724C\u5230\u671F</small></div><p>\u9700\u8981\u65F6\u7531 CLI \u4F7F\u7528 refresh token \u81EA\u52A8\u7EED\u671F\u3002</p></article><article class=\"card\"><div class=\"eyebrow\">Commands</div><div class=\"metric\">'+state.commands.length+' <small>\u6761\u80FD\u529B\u5951\u7EA6</small></div><p>\u7F51\u9875\u6267\u884C\u5668\u53EA\u63A5\u53D7\u76EE\u5F55\u4E2D\u58F0\u660E\u7684\u547D\u4EE4\u3002</p></article><article class=\"card\"><div class=\"eyebrow\">Endpoint</div><div class=\"metric\">'+(state.config.apiBaseUrl?'READY':'SETUP')+'</div><p>'+esc(state.config.apiBaseUrl||'\u8BF7\u5728\u57FA\u7840\u914D\u7F6E\u4E2D\u586B\u5199\u5E73\u53F0 API \u5730\u5740')+'</p></article><article class=\"card wide\"><h3>\u5F53\u524D\u8EAB\u4EFD</h3><p>'+esc(u.displayName||u.username||'-')+' \u00B7 '+esc(u.email||'-')+'</p><div class=\"actions\">'+roles.slice(0,8).map((r)=>'<span class=\"tag\">'+esc(r)+'</span>').join('')+(roles.length>8?'<span class=\"tag public\">+'+(roles.length-8)+'</span>':'')+'</div></article><article class=\"card\"><h3>\u4F1A\u8BDD\u64CD\u4F5C</h3><p>\u5F3A\u5236\u5237\u65B0\u8EAB\u4EFD\u8D44\u6599\uFF0C\u6216\u6E05\u9664\u672C\u673A Keycloak \u7F13\u5B58\u3002</p><div class=\"actions\"><button class=\"button\" data-action=\"reload\">\u5237\u65B0\u72B6\u6001</button><button class=\"button danger\" data-action=\"logout\">\u9000\u51FA\u767B\u5F55</button></div></article></section></div>'}\nfunction instant(value){if(!value)return null;const number=typeof value==='number'?(value<1000000000000?value*1000:value):Date.parse(value);return Number.isFinite(number)?number:null}\nfunction timeText(value){const at=instant(value);return at?new Date(at).toLocaleString('zh-CN',{month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}):'\u2014'}\nfunction windowName(window,fallback){const minutes=window?.windowMinutes;if(!minutes)return fallback;if(minutes%1440===0)return (minutes/1440)+'d';if(minutes%60===0)return (minutes/60)+'h';return minutes+'m'}\nfunction quotaWindowHasData(window){if(!window)return false;const minutes=window.windowMinutes;const reset=window.resetAt;const hasMinutes=typeof minutes==='number'&&Number.isFinite(minutes)&&minutes>0;const hasReset=(typeof reset==='number'&&Number.isFinite(reset)&&reset>0)||(typeof reset==='string'&&Number.isFinite(Date.parse(reset)));const hasUsage=typeof window.usedPercent==='number'&&Number.isFinite(window.usedPercent)&&window.usedPercent>0;return hasMinutes||hasReset||hasUsage}\nfunction quotaLine(window,fallback){if(!quotaWindowHasData(window)||typeof window.usedPercent!=='number')return '';const remain=Math.max(0,Math.min(100,100-window.usedPercent));return '<div class=\"quota-line\" title=\"\u989D\u5EA6\u91CD\u7F6E\u65F6\u95F4 '+esc(timeText(window.resetAt))+'\"><span>'+esc(windowName(window,fallback))+'</span><progress max=\"100\" value=\"'+remain.toFixed(1)+'\"></progress><strong>'+remain.toFixed(0)+'%</strong></div>'}\nfunction quotaCell(quota){if(!quota)return '<span class=\"table-meta\">\u6682\u65E0\u989D\u5EA6\u91C7\u6837</span>';const lines=quotaLine(quota.primary,'\u4E3B\u7A97\u53E3')+quotaLine(quota.secondary,'\u6B21\u7A97\u53E3');const detail=[quota.planType,quota.model,quota.activeLimit!=null?'limit '+quota.activeLimit:null].filter(Boolean).join(' \u00B7 ');return '<div class=\"quota-stack\">'+(lines||'<span class=\"table-meta\">\u989D\u5EA6\u7A97\u53E3\u6682\u65E0\u6570\u636E</span>')+'<div class=\"table-meta\">'+esc(detail||'\u5DF2\u91C7\u6837')+'<br>\u91C7\u6837 '+esc(timeText(quota.updatedAt))+'</div></div>'}\nfunction newerQuota(localQuota,remoteQuota){if(!localQuota)return remoteQuota;if(!remoteQuota)return localQuota;return (instant(remoteQuota.updatedAt)||0)>=(instant(localQuota.updatedAt)||0)?remoteQuota:localQuota}\nfunction mergedChatgptAccounts(){const local=chatgptLocal?.accounts||[];const remote=chatgptRemote?.accounts||[];const used=new Set();const rows=local.map((item)=>{const index=remote.findIndex((candidate,i)=>!used.has(i)&&item.accountId&&candidate.accountId===item.accountId);const match=index>=0?remote[index]:null;if(index>=0)used.add(index);return {key:item.id,local:item,remote:match,label:item.label,email:item.email,accountId:item.accountId||match?.accountId,enabled:item.enabled,selected:item.selected,quota:newerQuota(item.quota,match?.quota)}});remote.forEach((item,index)=>{if(!used.has(index))rows.push({key:'remote_'+item.id,local:null,remote:item,label:item.label,email:null,accountId:item.accountId,enabled:true,selected:item.isDefault,quota:item.quota})});return rows}\nfunction syncTag(row){if(chatgptRemote?.status==='LOADING')return '<span class=\"tag waiting\">\u6B63\u5728\u540C\u6B65</span>';if(chatgptRemote?.status==='FAILED')return '<span class=\"tag failed\">\u8FDC\u7A0B\u540C\u6B65\u5931\u8D25</span>';if(chatgptRemote?.status==='NOT_CONFIGURED')return '<span class=\"tag waiting\">\u672A\u914D\u7F6E\u5E73\u53F0</span>';if(row.local&&row.remote)return '<span class=\"tag success\">\u5DF2\u540C\u6B65</span>';if(row.local)return '<span class=\"tag local\">\u4EC5\u672C\u5730</span>';return '<span class=\"tag success\">\u5E73\u53F0\u53EF\u7528</span>'}\nfunction remoteStage(){const remote=chatgptRemote;if(!remote||remote.status==='LOADING')return {tag:'<span class=\"tag waiting\"><span class=\"spin\">\u21BB</span> \u8FDE\u63A5\u4E2D</span>',title:'\u6B63\u5728\u8FDE\u63A5\u5E73\u53F0',detail:'\u672C\u5730\u8D26\u53F7\u5DF2\u7ECF\u53EF\u89C1\uFF0C\u6B63\u5728\u8BFB\u53D6\u8FDC\u7A0B\u53EF\u7528\u8D26\u53F7\u3002'};if(remote.status==='SUCCESS')return {tag:'<span class=\"tag success\">\u540C\u6B65\u6210\u529F</span>',title:remote.accounts.length+' \u4E2A\u5E73\u53F0\u8D26\u53F7',detail:'\u5B8C\u6210\u4E8E '+timeText(remote.syncedAt)};if(remote.status==='FAILED')return {tag:'<span class=\"tag failed\">\u8FDC\u7A0B\u540C\u6B65\u5931\u8D25</span>',title:'\u4FDD\u7559\u672C\u5730\u7ED3\u679C',detail:remote.error?.message||'\u65E0\u6CD5\u8FDE\u63A5\u8FDC\u7A0B\u8D26\u53F7\u670D\u52A1'};return {tag:'<span class=\"tag waiting\">\u672A\u914D\u7F6E</span>',title:'\u672A\u914D\u7F6E\u5E73\u53F0\u5730\u5740',detail:'\u53EF\u5728\u57FA\u7840\u914D\u7F6E\u4E2D\u586B\u5199\u5E73\u53F0 API \u5730\u5740\u3002'}}\nfunction accountRows(rows){return rows.map((row)=>'<tr><td><div class=\"account-name\"><strong>'+esc(row.label)+'</strong><small>'+esc(row.email||row.accountId||row.key)+'</small></div></td><td><div class=\"status-stack\">'+(row.enabled?'<span class=\"tag success\">\u53EF\u7528</span>':'<span class=\"tag failed\">\u5DF2\u505C\u7528</span>')+(row.selected?'<span class=\"tag role\">\u5BA2\u6237\u7AEF\u5DF2\u9009</span>':'')+'</div></td><td>'+quotaCell(row.quota)+'</td><td><div class=\"table-meta\">Token '+esc(row.local?'\u672C\u5730\u53EF\u7528':'\u6309\u9700\u4ECE\u5E73\u53F0\u83B7\u53D6')+'<br>\u5230\u671F '+esc(timeText(row.local?.accessExpiresAt||row.remote?.accessExpiresAt))+'</div></td><td><div class=\"status-stack\">'+syncTag(row)+'</div></td></tr>').join('')}\nfunction chatgptPage(){const rows=mergedChatgptAccounts();const remote=remoteStage();const localReady=chatgptLocal?.status==='READY';const localCount=chatgptLocal?.available??0;const failure=chatgptRemote?.status==='FAILED'?'<div class=\"notice error\"><strong>\u8FDC\u7A0B\u540C\u6B65\u5931\u8D25\u3002</strong> '+esc(chatgptRemote.error?.message||'\u5E73\u53F0\u6682\u65F6\u4E0D\u53EF\u7528')+'\uFF1B\u672C\u5730\u8D26\u53F7\u6570\u636E\u4ECD\u53EF\u6B63\u5E38\u67E5\u770B\u3002</div>':'';return '<div class=\"content\"><section class=\"hero\"><div><div class=\"eyebrow\">Local first / Remote second</div><h2>ChatGPT \u8D26\u53F7\u8FD0\u884C\u89C6\u56FE</h2><p>\u5148\u8BFB\u53D6 codex-multi-auth \u672C\u5730\u8D26\u53F7\u4E0E\u989D\u5EA6\u7F13\u5B58\uFF0C\u518D\u8FDE\u63A5\u5E73\u53F0\u8865\u5145\u5F53\u524D\u7528\u6237\u53EF\u7528\u8D26\u53F7\u3002\u9875\u9762\u6C38\u4E0D\u63A5\u6536 access token \u6216 refresh token\u3002</p></div><div class=\"session-track\"><div class=\"track-node live\"><span>\u672C\u5730\u53EF\u7528</span><strong>'+localCount+'</strong></div><div class=\"track-node\"><span>\u6C47\u603B\u8D26\u53F7</span><strong>'+rows.length+'</strong></div><div class=\"track-node\"><span>\u8FDC\u7A0B</span><strong>'+esc(chatgptRemote?.status||'WAITING')+'</strong></div></div></section><section class=\"sync-board\"><article class=\"sync-stage\"><div class=\"eyebrow\">01 / Local pool</div><strong>'+(localReady?localCount+' \u4E2A\u672C\u5730\u53EF\u7528\u8D26\u53F7':'\u6B63\u5728\u8BFB\u53D6\u672C\u5730\u8D26\u53F7')+'</strong><p>'+(localReady?'\u8BFB\u53D6\u5B8C\u6210\u4E8E '+timeText(chatgptLocal.readAt):'\u4F18\u5148\u8BFB\u53D6 codex-multi-auth \u5B89\u5168\u6458\u8981\u3002')+'</p></article><div class=\"sync-link\" aria-label=\"\u7136\u540E\u8FDE\u63A5\u8FDC\u7A0B\"></div><article class=\"sync-stage\"><div class=\"eyebrow\">02 / Platform service</div>'+remote.tag+'<strong>'+esc(remote.title)+'</strong><p>'+esc(remote.detail)+'</p></article></section>'+failure+'<section class=\"table-card\"><header class=\"table-head\"><div><h3>\u53EF\u7528\u8D26\u53F7\u4E0E\u5269\u4F59\u989D\u5EA6</h3><p>\u989D\u5EA6\u4F18\u5148\u5C55\u793A\u672C\u5730\u6216\u670D\u52A1\u5668\u7AEF\u66F4\u65B0\u8F83\u65B0\u7684\u5B89\u5168\u5FEB\u7167\u3002</p></div><button class=\"button\" data-action=\"chatgpt-refresh\" '+(chatgptLoading?'disabled':'')+'>'+(chatgptLoading?'<span class=\"spin\">\u21BB</span> \u540C\u6B65\u4E2D':'\u21BB \u91CD\u65B0\u540C\u6B65')+'</button></header>'+(rows.length?'<div class=\"account-table-wrap\"><table class=\"account-table\"><thead><tr><th>\u8D26\u53F7</th><th>\u53EF\u7528\u72B6\u6001</th><th>\u5269\u4F59 limit</th><th>\u51ED\u636E\u65F6\u6548</th><th>\u540C\u6B65\u72B6\u6001</th></tr></thead><tbody>'+accountRows(rows)+'</tbody></table></div>':'<div class=\"empty-state'+(chatgptLoading?'':' account-request')+'\">'+(chatgptLoading?'\u6B63\u5728\u8BFB\u53D6\u672C\u5730\u8D26\u53F7\u2026':'<strong>\u9700\u8981\u7533\u8BF7 ChatGPT \u8D26\u53F7</strong><span>'+esc(noAvailableAccountGuidance)+'</span>')+'</div>')+'</section></div>'}\nfunction registryPageItems(payload){if(Array.isArray(payload))return payload.filter((item)=>item&&typeof item==='object');for(const key of ['pageItems','items','records','list','data']){const value=payload?.[key];if(Array.isArray(value))return value.filter((item)=>item&&typeof item==='object');if(value&&typeof value==='object'){const nested=registryPageItems(value);if(nested.length)return nested}}return []}\nfunction registryItemName(item,index){return String(item?.name??item?.skillName??item?.mcpName??((registryMode==='skill'?'skill-':'mcp-')+(index+1)))}\nfunction registryItemStatus(item){return String(item?.status??item?.releaseStatus??item?.state??item?.version??'registered')}\nfunction registryCanEdit(){const u=state.user||{};const roles=[...(u.realmRoles||[]),...Object.values(u.clientRoles||{}).flat()].map(String);return roles.includes('mci-devops-platform-ai-registry-admin')||roles.some((role)=>role.endsWith('-ops-admin'))}\nfunction registryNamespaceOptions(){return registryNamespaces.map((namespace)=>'<option value=\"'+namespace.id+'\" '+(Number(namespace.id)===Number(registryNamespaceId)?'selected':'')+'>'+esc(namespace.displayName)+' \u00B7 '+esc(namespace.namespaceKey)+' \u00B7 '+esc(namespace.instanceCode)+(namespace.isDefault?' \u00B7 default':'')+'</option>').join('')}\nfunction registryItemButtons(){return registryItems.map((item,index)=>{const name=registryItemName(item,index);const status=registryItemStatus(item);const online=/online|published|release/i.test(status);return '<button class=\"registry-item '+(name===registryName?'active':'')+'\" data-registry-name=\"'+esc(name)+'\"><span class=\"registry-dot '+(online?'online':'')+'\"></span><span><strong>'+esc(name)+'</strong><small>'+esc(status)+(item.scope?' \u00B7 '+esc(item.scope):'')+'</small></span></button>'}).join('')||'<div class=\"empty-state\">\u6CA1\u6709\u5339\u914D\u7684 '+(registryMode==='skill'?'Skill':'MCP Server')+'</div>'}\nfunction registryTargets(){const targets=registryMode==='skill'?['chatgpt','codex','claude','cursor','workbuddy','openclaw','agents']:['chatgpt','codex','claude','cursor','workbuddy','openclaw'];return targets.map((target)=>'<option value=\"'+target+'\">'+target+'</option>').join('')}\nfunction registryDetailPanel(){if(registryLoading)return '<div class=\"empty-state\"><span class=\"spin\">\u21BB</span> \u6B63\u5728\u8BFB\u53D6\u79C1\u6709 Registry\u2026</div>';if(!registryName)return '<div class=\"empty-state\">\u4ECE\u5DE6\u4FA7\u9009\u62E9\u4E00\u9879\u67E5\u770B\u8BE6\u60C5\u4E0E\u64CD\u4F5C\u3002</div>';const canEdit=registryCanEdit();const mutating=registryMode==='skill'?'<button class=\"button\" data-registry-action=\"submit\" '+(canEdit?'':'disabled')+'>\u63D0\u4EA4\u5BA1\u6838</button><button class=\"button\" data-registry-action=\"publish\" '+(canEdit?'':'disabled')+'>\u53D1\u5E03\u7248\u672C</button><button class=\"button\" data-registry-action=\"online\" '+(canEdit?'':'disabled')+'>\u4E0A\u7EBF</button><button class=\"button\" data-registry-action=\"offline\" '+(canEdit?'':'disabled')+'>\u4E0B\u7EBF</button>':'';return '<div class=\"registry-detail-head\"><div><div class=\"eyebrow\">'+esc(registryMode==='skill'?'Skill package':'MCP definition')+'</div><h3>'+esc(registryName)+'</h3><span class=\"tag '+(canEdit?'role':'public')+'\">'+(canEdit?'registry admin':'reader')+'</span></div><button class=\"button\" data-action=\"registry-refresh\">\u21BB \u5237\u65B0</button></div><pre class=\"output\">'+esc(JSON.stringify(registryDetail??{},null,2))+'</pre><div class=\"registry-actions\"><div class=\"install-row\"><label class=\"field\"><span>\u5B89\u88C5\u76EE\u6807</span><select id=\"registry-target\">'+registryTargets()+'</select></label><label class=\"field\"><span>\u5B89\u88C5\u8303\u56F4</span><select id=\"registry-scope\"><option value=\"project\">\u5F53\u524D\u9879\u76EE</option><option value=\"user\">\u5F53\u524D\u7528\u6237</option></select></label><button class=\"button primary\" data-registry-action=\"install\">\u5B89\u88C5</button></div><div class=\"actions\">'+mutating+'</div><small class=\"table-meta\">\u53D1\u5E03\u7C7B\u6309\u94AE\u53EA\u662F\u6743\u9650\u63D0\u793A\uFF1B\u6700\u7EC8\u6388\u6743\u4ECD\u7531\u5E73\u53F0 Keycloak Policy Enforcer \u6267\u884C\u3002</small><div id=\"registry-notice\"></div></div>'}\nfunction registryPage(){const error=registryError?'<div class=\"notice error registry-error\">'+esc(registryError)+'</div>':'';return '<div class=\"content\"><section class=\"hero registry-hero\"><div><div class=\"eyebrow\">Nacos private AI registry</div><h2>\u53D1\u73B0\u3001\u6CBB\u7406\u5E76\u5B89\u88C5\u56E2\u961F\u80FD\u529B</h2><p>Nacos \u5B9E\u4F8B\u3001OIDC \u914D\u7F6E\u4E0E namespace \u8DEF\u7531\u7531\u5E73\u53F0\u6570\u636E\u5E93\u7BA1\u7406\uFF1B\u6D4F\u89C8\u5668\u4E0E CLI \u53EA\u643A\u5E26\u5F53\u524D\u7528\u6237\u7684 Keycloak \u8EAB\u4EFD\u3002</p></div><div class=\"lifecycle\" aria-label=\"Skill \u751F\u547D\u5468\u671F\"><div class=\"life-node live\">draft</div><div class=\"life-node\">review</div><div class=\"life-node\">online</div></div></section>'+error+'<section class=\"registry-toolbar\"><label class=\"field\"><span>Nacos namespace</span><select id=\"registry-namespace\">'+registryNamespaceOptions()+'</select></label><div class=\"segment\" role=\"group\" aria-label=\"\u8D44\u6E90\u7C7B\u578B\"><button data-registry-mode=\"skill\" class=\"'+(registryMode==='skill'?'active':'')+'\">Skills</button><button data-registry-mode=\"mcp\" class=\"'+(registryMode==='mcp'?'active':'')+'\">MCP</button></div><label class=\"field\"><span>\u8FC7\u6EE4</span><input id=\"registry-search\" value=\"'+esc(registrySearch)+'\" placeholder=\"\u540D\u79F0\u6216\u63CF\u8FF0\"></label><button class=\"button\" data-load=\"'+esc(JSON.stringify(['registry','namespaces','list']))+'\">\u5B9E\u4F8B/namespace \u547D\u4EE4</button></section><section class=\"registry-board\"><aside class=\"registry-list\"><div class=\"registry-list-head\"><strong>'+(registryMode==='skill'?'Skill track':'MCP track')+'</strong><span class=\"tag public\">'+registryItems.length+'</span></div><div class=\"registry-items\">'+registryItemButtons()+'</div></aside><article class=\"registry-detail\">'+registryDetailPanel()+'</article></section></div>'}\nasync function loadRegistry(resetNamespaces=false){if(registryLoading)return;registryLoading=true;registryError='';render();try{if(resetNamespaces||!registryNamespaces.length){registryNamespaces=await api('/api/registry/namespaces');if(!Array.isArray(registryNamespaces))throw new Error('namespace \u54CD\u5E94\u683C\u5F0F\u65E0\u6548');if(!registryNamespaceId||!registryNamespaces.some((namespace)=>Number(namespace.id)===Number(registryNamespaceId)))registryNamespaceId=registryNamespaces.find((namespace)=>namespace.status==='ACTIVE'&&namespace.instanceStatus==='ACTIVE'&&namespace.isDefault)?.id??registryNamespaces.find((namespace)=>namespace.status==='ACTIVE'&&namespace.instanceStatus==='ACTIVE')?.id??registryNamespaces[0]?.id??null}if(!registryNamespaceId){registryItems=[];registryDetail=null;return}const endpoint=registryMode==='skill'?'/api/registry/skills':'/api/registry/mcp';const payload=await api(endpoint+'?namespaceId='+encodeURIComponent(registryNamespaceId)+(registrySearch?'&search='+encodeURIComponent(registrySearch):''));registryItems=registryPageItems(payload);if(registryName&&!registryItems.some((item,index)=>registryItemName(item,index)===registryName)){registryName='';registryDetail=null}}catch(error){registryError=error.message;registryItems=[];registryDetail=null}finally{registryLoading=false;render()}}\nasync function selectRegistryItem(name){registryName=name;registryLoading=true;registryError='';render();try{const endpoint=registryMode==='skill'?'/api/registry/skill':'/api/registry/mcp-detail';registryDetail=await api(endpoint+'?namespaceId='+encodeURIComponent(registryNamespaceId)+'&name='+encodeURIComponent(name))}catch(error){registryError=error.message;registryDetail=null}finally{registryLoading=false;render()}}\nasync function runRegistryCommand(argv,successMessage){const target=document.getElementById('registry-notice');if(target)target.innerHTML='<div class=\"notice success\">\u6B63\u5728\u6267\u884C\u2026</div>';try{const result=await api('/api/commands/run',{method:'POST',body:JSON.stringify({argv,confirm:false})});if(result.exitCode!==0)throw new Error(result.stderr||result.stdout||'\u547D\u4EE4\u6267\u884C\u5931\u8D25');if(registryMode==='skill'&®istryName&&argv[2]!=='install')await selectRegistryItem(registryName);const current=document.getElementById('registry-notice');if(current)current.innerHTML='<div class=\"notice success\">'+esc(successMessage)+'</div>'}catch(error){const current=document.getElementById('registry-notice');if(current)current.innerHTML='<div class=\"notice error\">'+esc(error.message)+'</div>'}}\nasync function registryAction(action){if(!registryNamespaceId||!registryName)return;const selected=registryNamespaces.find((namespace)=>Number(namespace.id)===Number(registryNamespaceId));if(!selected)throw new Error('\u672A\u9009\u62E9 namespace');const targetArgs=['--namespace',String(selected.namespaceKey),'--instance',String(selected.instanceCode)];if(action==='install'){const agent=document.getElementById('registry-target').value;let scope=document.getElementById('registry-scope').value;if(registryMode==='mcp'&&(agent==='codex'||agent==='openclaw'))scope='user';const argv=['registry',registryMode,'install',registryName,...targetArgs,'--agent',agent,'--scope',scope];if(registryMode==='skill')argv.push('--label','latest');await runRegistryCommand(argv,'\u5DF2\u5B8C\u6210\u672C\u673A\u5B89\u88C5');return}if(!confirm('\u786E\u8BA4\u5BF9 '+registryName+' \u6267\u884C '+action+'\uFF1F'))return;const argv=['registry','skill',action,registryName,...targetArgs];if(action==='publish'){const version=prompt('\u8F93\u5165\u8981\u53D1\u5E03\u7684 SemVer');if(!version)return;argv.push('--version',version)}await runRegistryCommand(argv,action+' \u5B8C\u6210')}\nfunction configPage(){const c=state.config;return '<div class=\"content\"><section class=\"grid\"><article class=\"card full\"><div class=\"eyebrow\">Configuration file</div><h3>\u57FA\u7840\u914D\u7F6E</h3><p>\u4E0E <code>numa config --set</code> \u4F7F\u7528\u540C\u4E00\u4EFD\u914D\u7F6E\u548C\u6821\u9A8C\u903B\u8F91\u3002</p><form id=\"config-form\" class=\"form-grid\"><div class=\"field\"><label>\u754C\u9762\u8BED\u8A00</label><select name=\"locale\"><option '+(c.locale==='zh-CN'?'selected':'')+'>zh-CN</option><option '+(c.locale==='en-US'?'selected':'')+'>en-US</option></select></div><div class=\"field\"><label>\u9ED8\u8BA4\u8F93\u51FA</label><select name=\"output\"><option '+(c.output==='text'?'selected':'')+'>text</option><option '+(c.output==='json'?'selected':'')+'>json</option></select></div><div class=\"field span-2\"><label>\u5E73\u53F0 API \u5730\u5740</label><input name=\"apiBaseUrl\" value=\"'+esc(c.apiBaseUrl)+'\" placeholder=\"https://platform.example/api\"></div><div class=\"field span-2\"><label>npm registry</label><input name=\"registryUrl\" value=\"'+esc(c.registryUrl)+'\"></div><label class=\"field span-2\"><span>\u66F4\u65B0\u68C0\u67E5</span><select name=\"checkUpdates\"><option value=\"true\" '+(c.checkUpdates?'selected':'')+'>\u542F\u7528</option><option value=\"false\" '+(!c.checkUpdates?'selected':'')+'>\u5173\u95ED</option></select></label><div class=\"actions span-2\"><button class=\"button primary\" type=\"submit\">\u4FDD\u5B58\u914D\u7F6E</button></div></form><div id=\"notice\"></div></article></section></div>'}\nfunction commandsPage(){return '<div class=\"content\"><section class=\"card full\"><div class=\"command-toolbar\"><div class=\"field grow\"><label>\u8FC7\u6EE4\u547D\u4EE4</label><input id=\"command-search\" placeholder=\"\u4F8B\u5982 codex\u3001media\u3001auth\"></div></div><div id=\"command-list\" class=\"command-list\">'+commandCards('')+'</div></section></div>'}\nfunction commandCards(query){const q=query.toLowerCase();return state.commands.filter((c)=>(c.command+' '+c.summary).toLowerCase().includes(q)).map((c)=>'<article class=\"command\"><div class=\"command-head\"><code>'+esc(c.command)+'</code><span class=\"tag '+esc(c.access)+'\">'+esc(c.access)+'</span></div><p>'+esc(c.summary)+'</p><small>'+esc(c.examples[0]||'')+'</small><div class=\"actions mt-10\"><button class=\"button\" data-load=\"'+esc(JSON.stringify(commandPrefix(c.command)))+'\">\u8F7D\u5165\u6267\u884C\u5668</button></div></article>').join('')||'<p>\u6CA1\u6709\u5339\u914D\u547D\u4EE4\u3002</p>'}\nfunction commandPrefix(command){return command.split(/s+/).filter((part)=>!part.startsWith('[')&&!part.startsWith('<')&&!part.startsWith('--'))}\nfunction runnerPage(){return '<div class=\"content\"><section class=\"grid\"><article class=\"card\"><div class=\"eyebrow\">Structured argv</div><h3>\u5B89\u5168\u6267\u884C\u5668</h3><p>\u4F7F\u7528 JSON \u6570\u7EC4\u4F20\u5165\u53C2\u6570\uFF0C\u4E0D\u7ECF\u8FC7 shell\u3002\u957F\u9A7B\u6216\u4EA4\u4E92\u547D\u4EE4\u9700\u8981\u5728\u7EC8\u7AEF\u8FD0\u884C\u3002</p><form id=\"runner-form\"><div class=\"field\"><label>\u547D\u4EE4\u53C2\u6570</label><textarea id=\"argv\" class=\"mono\">[\"doctor\"]</textarea></div><label class=\"field mt-12\"><span>\u5371\u9669\u64CD\u4F5C\u786E\u8BA4</span><select id=\"confirm\"><option value=\"false\">\u672A\u786E\u8BA4</option><option value=\"true\">\u786E\u8BA4\u6267\u884C\u5220\u9664/\u91CD\u7F6E</option></select></label><div class=\"actions mt-14\"><button class=\"button primary\" type=\"submit\">\u8FD0\u884C\u547D\u4EE4</button></div></form></article><article class=\"card wide\"><div class=\"eyebrow\">Result stream</div><h3>\u7ED3\u6784\u5316\u8F93\u51FA</h3><pre id=\"output\" class=\"output\">'+esc(output)+'</pre></article></section></div>'}\nasync function loadChatgpt(){if(chatgptLoading)return;chatgptLoading=true;chatgptLocal=null;chatgptRemote={status:'LOADING',accounts:[]};render();try{chatgptLocal=await api('/api/chatgpt/local')}catch(error){chatgptLocal={status:'FAILED',accounts:[],available:0,total:0,error:{message:error.message}}}render();try{chatgptRemote=await api('/api/chatgpt/remote')}catch(error){chatgptRemote={status:'FAILED',accounts:[],syncedAt:Date.now(),error:{message:error.message}}}finally{chatgptLoading=false;render()}}\nfunction render(){if(!state.loggedIn){app.innerHTML=loginGate();bind();return}const page=active==='overview'?overview():active==='registry'?registryPage():active==='chatgpt'?chatgptPage():active==='config'?configPage():active==='commands'?commandsPage():runnerPage();app.innerHTML=shell(page);bind()}\nfunction notice(message,type='success'){const target=document.getElementById('notice');if(target)target.innerHTML='<div class=\"notice '+type+'\">'+esc(message)+'</div>'}\nfunction bindCommandLoaders(){document.querySelectorAll('[data-load]').forEach((b)=>b.addEventListener('click',()=>{const argv=JSON.parse(b.dataset.load);active='runner';render();document.getElementById('argv').value=JSON.stringify(argv)}))}\nfunction bind(){document.querySelectorAll('[data-nav]').forEach((b)=>b.addEventListener('click',()=>{active=b.dataset.nav;render();if(active==='chatgpt'&&!chatgptLocal&&!chatgptLoading)void loadChatgpt();if(active==='registry'&&!registryNamespaces.length&&!registryLoading)void loadRegistry(true)}));document.querySelector('[data-action=\"chatgpt-refresh\"]')?.addEventListener('click',()=>void loadChatgpt());document.querySelector('[data-action=\"registry-refresh\"]')?.addEventListener('click',()=>registryName?void selectRegistryItem(registryName):void loadRegistry());document.querySelectorAll('[data-registry-mode]').forEach((button)=>button.addEventListener('click',()=>{registryMode=button.dataset.registryMode;registryName='';registryDetail=null;void loadRegistry()}));document.querySelectorAll('[data-registry-name]').forEach((button)=>button.addEventListener('click',()=>void selectRegistryItem(button.dataset.registryName)));document.querySelectorAll('[data-registry-action]').forEach((button)=>button.addEventListener('click',()=>void registryAction(button.dataset.registryAction)));document.getElementById('registry-namespace')?.addEventListener('change',(event)=>{registryNamespaceId=Number(event.target.value);registryName='';registryDetail=null;void loadRegistry()});document.getElementById('registry-search')?.addEventListener('change',(event)=>{registrySearch=event.target.value.trim();registryName='';registryDetail=null;void loadRegistry()});document.getElementById('registry-search')?.addEventListener('keydown',(event)=>{if(event.key==='Enter'){event.preventDefault();registrySearch=event.target.value.trim();registryName='';registryDetail=null;void loadRegistry()}});document.querySelector('[data-action=\"login\"]')?.addEventListener('click',async()=>{try{notice('\u6B63\u5728\u542F\u52A8\u767B\u5F55\u2026');const result=await api('/api/auth/login',{method:'POST',body:'{}'});location.href=result.authorizationUrl}catch(e){notice(e.message,'error')}});document.querySelector('[data-action=\"reload\"]')?.addEventListener('click',()=>bootstrap());document.querySelector('[data-action=\"logout\"]')?.addEventListener('click',async()=>{if(!confirm('\u6E05\u9664\u672C\u673A Keycloak \u767B\u5F55\u7F13\u5B58\uFF1F'))return;await api('/api/auth/logout',{method:'POST',body:'{}'});location.reload()});document.getElementById('config-form')?.addEventListener('submit',async(e)=>{e.preventDefault();const data=Object.fromEntries(new FormData(e.currentTarget));data.checkUpdates=data.checkUpdates==='true';try{const result=await api('/api/config',{method:'PUT',body:JSON.stringify(data)});state.config=result.config;notice('\u914D\u7F6E\u5DF2\u4FDD\u5B58')}catch(err){notice(err.message,'error')}});document.getElementById('command-search')?.addEventListener('input',(e)=>{document.getElementById('command-list').innerHTML=commandCards(e.target.value);bindCommandLoaders()});bindCommandLoaders();document.getElementById('runner-form')?.addEventListener('submit',async(e)=>{e.preventDefault();try{const argv=JSON.parse(document.getElementById('argv').value);const confirmed=document.getElementById('confirm')?.value==='true';output='\u8FD0\u884C\u4E2D\u2026';render();const result=await api('/api/commands/run',{method:'POST',body:JSON.stringify({argv,confirm:confirmed})});output=result.stdout||result.stderr||JSON.stringify(result,null,2);render()}catch(err){output='\u9519\u8BEF: '+err.message;render()}})}\nbootstrap();\n";
|
package/dist/web-console-page.js
CHANGED
|
@@ -18,18 +18,19 @@ export const CONSOLE_CSS = `
|
|
|
18
18
|
.main{min-width:0;min-height:100vh;display:flex;flex-direction:column}.topbar{height:76px;padding:0 34px;background:rgba(251,253,255,.94);border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:3;backdrop-filter:blur(12px)}.topbar h1{font-size:17px;margin:0}.eyebrow{font:700 10px/1.2 "SFMono-Regular",monospace;text-transform:uppercase;letter-spacing:.16em;color:var(--cyan)}.user-chip{display:flex;align-items:center;gap:10px}.avatar{width:34px;height:34px;border-radius:50%;display:grid;place-items:center;background:var(--cyan-soft);color:#087d84;font-weight:800}.user-chip small{color:var(--muted)}.content{width:100%;max-width:1420px;flex:1;margin:0 auto;padding:30px 34px 56px}.hero{background:var(--navy);color:white;border-radius:22px;padding:28px 30px;display:grid;grid-template-columns:1fr auto;gap:24px;position:relative;overflow:hidden;box-shadow:var(--shadow)}.hero:after{content:"";position:absolute;right:-45px;top:-90px;width:320px;height:240px;border:34px solid rgba(14,183,191,.09);transform:rotate(-18deg);border-radius:48px}.hero h2{font-size:32px;line-height:1.08;margin:10px 0}.hero p{max-width:720px;color:#91aac0;margin:0;line-height:1.6}.session-track{align-self:center;display:flex;gap:0;position:relative;z-index:1}.track-node{min-width:112px;border:1px solid #2a4661;padding:13px 15px;background:#0b1e33}.track-node:first-child{border-radius:13px 0 0 13px}.track-node:last-child{border-radius:0 13px 13px 0}.track-node span{display:block;color:#7690a7;font-size:9px;text-transform:uppercase;letter-spacing:.12em}.track-node strong{display:block;margin-top:6px;font-size:13px}.track-node.live strong{color:#58e0df}
|
|
19
19
|
.grid{display:grid;grid-template-columns:repeat(12,1fr);gap:18px;margin-top:20px}.card{grid-column:span 4;background:var(--paper);border:1px solid var(--line);border-radius:17px;padding:20px;box-shadow:0 10px 30px rgba(25,54,79,.05)}.card.wide{grid-column:span 8}.card.full{grid-column:1/-1}.card h3{margin:0;font-size:15px}.card p{color:var(--muted);font-size:12px;line-height:1.6}.metric{font-size:28px;font-weight:750;margin-top:16px}.metric small{font-size:11px;color:var(--muted);font-weight:500}.tag{display:inline-flex;border-radius:999px;padding:5px 8px;font:700 9px/1 "SFMono-Regular",monospace;text-transform:uppercase;letter-spacing:.05em;background:var(--cyan-soft);color:#087d84}.tag.public{background:#edf1f5;color:#5d7082}.tag.role{background:#fff0d9;color:#9c5c09}.actions{display:flex;gap:8px;flex-wrap:wrap}.button{border:1px solid var(--line);border-radius:10px;background:white;color:var(--ink);padding:9px 12px;cursor:pointer;font-weight:700;font-size:12px}.button:hover{border-color:#9ab6c9;background:#f7fbfd}.button.primary{background:#0b949b;border-color:#0b949b;color:white}.button.danger{color:var(--red);border-color:#f0bec4}.button:disabled{opacity:.45;cursor:not-allowed}.field{display:grid;gap:6px}.field label{font-size:11px;color:var(--muted);font-weight:700}.field input,.field select,.field textarea{width:100%;border:1px solid var(--line);border-radius:10px;background:white;padding:10px 11px;color:var(--ink);outline:none}.field input:focus,.field textarea:focus{border-color:var(--cyan);box-shadow:0 0 0 3px rgba(14,183,191,.12)}.field textarea{min-height:110px;resize:vertical}.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}.form-grid .span-2{grid-column:1/-1}.command-toolbar{display:flex;gap:10px;margin-bottom:15px}.grow{flex:1}.mt-10{margin-top:10px}.mt-12{margin-top:12px}.mt-14{margin-top:14px}.command-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:11px}.command{border:1px solid var(--line);border-radius:13px;padding:14px;background:white}.command-head{display:flex;gap:8px;align-items:center;justify-content:space-between}.command code{font-size:11px;color:#0b6f76}.command p{margin:9px 0}.command small{display:block;color:#8a9bab;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.output{background:#071522;color:#b8d1e2;border-radius:13px;padding:15px;min-height:170px;max-height:420px;overflow:auto;white-space:pre-wrap;font-size:11px;line-height:1.65}.notice{margin-top:14px;border-radius:11px;padding:11px 13px;font-size:12px}.notice.error{background:#fff0f2;color:#a52c3b;border:1px solid #f1c4ca}.notice.success{background:#e8f7f1;color:#176c54;border:1px solid #bce4d5}.login-gate{min-height:calc(100vh - 76px);display:grid;place-content:center;padding:30px}.login-card{width:min(520px,90vw);background:white;border:1px solid var(--line);border-radius:22px;padding:32px;box-shadow:var(--shadow)}.login-card h2{font-size:28px;margin:14px 0 9px}.login-card p{color:var(--muted);line-height:1.65}.hidden{display:none!important}
|
|
20
20
|
.sync-board{display:grid;grid-template-columns:1fr 56px 1fr;align-items:stretch;margin-top:20px}.sync-stage{background:var(--paper);border:1px solid var(--line);border-radius:17px;padding:18px 20px;box-shadow:0 10px 30px rgba(25,54,79,.05)}.sync-stage strong{display:block;margin-top:8px;font-size:18px}.sync-stage p{margin:7px 0 0;color:var(--muted);font-size:11px}.sync-link{display:grid;place-items:center;color:var(--cyan);font-size:22px}.sync-link:before{content:"";height:2px;width:100%;background:linear-gradient(90deg,var(--cyan),#bad8dc)}.table-card{margin-top:18px;background:var(--paper);border:1px solid var(--line);border-radius:17px;box-shadow:0 10px 30px rgba(25,54,79,.05);overflow:hidden}.table-head{padding:18px 20px;display:flex;align-items:center;justify-content:space-between;gap:16px;border-bottom:1px solid var(--line)}.table-head h3{margin:0}.table-head p{margin:5px 0 0;color:var(--muted);font-size:11px}.account-table-wrap{overflow:auto}.account-table{width:100%;border-collapse:collapse;min-width:920px}.account-table th,.account-table td{text-align:left;padding:14px 16px;border-bottom:1px solid #e7eef3;vertical-align:middle}.account-table th{background:#f5f9fb;color:#6c8094;font-size:10px;text-transform:uppercase;letter-spacing:.08em}.account-table tr:last-child td{border-bottom:0}.account-name strong{display:block;font-size:13px}.account-name small{display:block;margin-top:4px;color:var(--muted);font-size:10px}.quota-stack{min-width:210px;display:grid;gap:8px}.quota-line{display:grid;grid-template-columns:42px 1fr 38px;align-items:center;gap:7px;font-size:10px;color:var(--muted)}progress{width:100%;height:7px;border:0;border-radius:99px;overflow:hidden;background:#e7eef3}progress::-webkit-progress-bar{background:#e7eef3;border-radius:99px}progress::-webkit-progress-value{background:var(--cyan);border-radius:99px}progress::-moz-progress-bar{background:var(--cyan);border-radius:99px}.status-stack{display:flex;gap:6px;flex-wrap:wrap}.tag.success{background:#e8f7f1;color:var(--green)}.tag.failed{background:#fff0f2;color:var(--red)}.tag.waiting{background:#fff0d9;color:#9c5c09}.tag.local{background:#e8f1fa;color:#32658f}.empty-state{padding:48px 20px;text-align:center;color:var(--muted)}.empty-state.account-request{margin:24px;border:2px solid #f0b5bd;border-radius:16px;background:#fff2f4;color:#8d2334;box-shadow:0 12px 30px rgba(167,44,64,.1)}.empty-state.account-request strong{display:block;margin-bottom:9px;font-size:16px}.empty-state.account-request span{display:block;font-size:13px;line-height:1.7}.table-meta{color:var(--muted);font-size:10px;line-height:1.55}.spin{display:inline-block;animation:turn 1s linear infinite}@keyframes turn{to{transform:rotate(360deg)}}
|
|
21
|
-
|
|
22
|
-
@media(max-width:
|
|
21
|
+
.registry-hero{background:linear-gradient(118deg,#06172a 0%,#0b2d42 67%,#0a4c55 100%)}.lifecycle{display:grid;grid-template-columns:repeat(3,84px);align-items:center;position:relative;z-index:1}.life-node{position:relative;text-align:center;color:#7293a7;font:700 9px/1.2 "SFMono-Regular",monospace;text-transform:uppercase;letter-spacing:.08em}.life-node:before{content:"";display:block;width:11px;height:11px;margin:0 auto 9px;border:2px solid #58e0df;border-radius:50%;background:#0a2638;box-shadow:0 0 0 5px rgba(88,224,223,.08)}.life-node:not(:last-child):after{content:"";position:absolute;height:1px;width:calc(100% - 24px);top:6px;left:calc(50% + 12px);background:linear-gradient(90deg,#58e0df,#31546a)}.life-node.live{color:#d5ffff}.life-node.live:before{background:#58e0df;box-shadow:0 0 15px #58e0df}.registry-toolbar{display:grid;grid-template-columns:minmax(190px,1fr) auto minmax(190px,1fr) auto;gap:10px;align-items:end;margin-top:20px}.segment{display:flex;padding:3px;background:#dfe9ef;border-radius:11px}.segment button{border:0;background:transparent;padding:8px 13px;border-radius:8px;color:var(--muted);font-weight:750;font-size:11px;cursor:pointer}.segment button.active{background:white;color:#087d84;box-shadow:0 3px 12px rgba(7,24,43,.1)}.registry-board{display:grid;grid-template-columns:minmax(260px,32%) 1fr;min-height:560px;margin-top:14px;background:var(--paper);border:1px solid var(--line);border-radius:18px;overflow:hidden;box-shadow:var(--shadow)}.registry-list{border-right:1px solid var(--line);background:#f4f8fa}.registry-list-head{padding:15px 17px;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;align-items:center}.registry-items{padding:9px;display:grid;gap:6px;max-height:680px;overflow:auto}.registry-item{width:100%;border:1px solid transparent;background:transparent;border-radius:12px;padding:12px;text-align:left;cursor:pointer;display:grid;grid-template-columns:12px 1fr;gap:10px;color:var(--ink)}.registry-item:hover,.registry-item.active{background:white;border-color:var(--line);box-shadow:0 6px 20px rgba(18,49,71,.07)}.registry-dot{width:8px;height:8px;margin-top:4px;border-radius:50%;background:var(--amber);box-shadow:0 0 0 4px rgba(233,154,50,.1)}.registry-dot.online{background:var(--green);box-shadow:0 0 0 4px rgba(25,138,104,.1)}.registry-item strong,.registry-item small{display:block}.registry-item strong{font-size:12px}.registry-item small{margin-top:5px;color:var(--muted);font-size:10px}.registry-detail{min-width:0;padding:22px;display:flex;flex-direction:column}.registry-detail-head{display:flex;justify-content:space-between;align-items:start;gap:16px}.registry-detail h3{margin:5px 0 3px;font-size:20px}.registry-detail pre{flex:1;margin:16px 0;min-height:270px}.registry-actions{border-top:1px solid var(--line);padding-top:15px;display:grid;gap:12px}.install-row{display:grid;grid-template-columns:1fr 1fr auto;gap:9px}.registry-error{margin-top:14px}.button:focus-visible,.nav button:focus-visible,.registry-item:focus-visible,.segment button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:3px solid rgba(14,183,191,.3);outline-offset:2px}
|
|
22
|
+
@media(max-width:980px){.shell{grid-template-columns:78px 1fr}.rail{padding:18px 10px}.brand div:last-child,.nav button span:first-child,.rail-foot p,.rail-foot strong{display:none}.brand{justify-content:center}.nav button{justify-content:center}.card,.card.wide{grid-column:span 6}.hero{grid-template-columns:1fr}.session-track,.lifecycle{display:none}.registry-toolbar{grid-template-columns:1fr 1fr}.registry-board{grid-template-columns:1fr}.registry-list{border-right:0;border-bottom:1px solid var(--line)}.registry-items{max-height:260px}}
|
|
23
|
+
@media(max-width:680px){.shell{display:block}.rail{height:auto;position:static;flex-direction:row;align-items:center;padding:10px 14px;overflow-x:auto}.brand{padding:0;margin-right:auto;flex:0 0 auto}.nav{display:flex}.nav button{padding:9px;flex:0 0 auto}.rail-foot{display:none}.topbar{padding:0 18px}.content{padding:20px 14px}.hero{padding:23px}.hero h2{font-size:25px}.card,.card.wide{grid-column:1/-1}.form-grid,.command-list,.registry-toolbar,.install-row{grid-template-columns:1fr}.form-grid .span-2{grid-column:auto}.user-chip div:not(.avatar){display:none}.sync-board{grid-template-columns:1fr}.sync-link{height:32px;transform:rotate(90deg)}.table-head{align-items:flex-start;flex-direction:column}.registry-detail{padding:17px}}
|
|
23
24
|
@media(prefers-reduced-motion:no-preference){.card,.hero{animation:arrive .35s ease both}@keyframes arrive{from{opacity:0;transform:translateY(7px)}to{opacity:1;transform:none}}}
|
|
24
25
|
`;
|
|
25
26
|
export const CONSOLE_JS = `
|
|
26
|
-
const app=document.getElementById('app');let state=null;let csrf='';let active='overview';let output='选择一条命令或输入参数后运行。';let chatgptLocal=null;let chatgptRemote=null;let chatgptLoading=false;const noAvailableAccountGuidance='当前用户没有绑定可用的 ChatGPT 账号。请前往 OA 系统提交 IT11 工单,申请独享或共享的 ChatGPT 个人账号。';
|
|
27
|
+
const app=document.getElementById('app');let state=null;let csrf='';let active='overview';let output='选择一条命令或输入参数后运行。';let chatgptLocal=null;let chatgptRemote=null;let chatgptLoading=false;let registryNamespaces=[];let registryItems=[];let registryDetail=null;let registryMode='skill';let registryNamespaceId=null;let registryName='';let registrySearch='';let registryLoading=false;let registryError='';const noAvailableAccountGuidance='当前用户没有绑定可用的 ChatGPT 账号。请前往 OA 系统提交 IT11 工单,申请独享或共享的 ChatGPT 个人账号。';
|
|
27
28
|
const esc=(v)=>String(v??'').replace(/[&<>"']/g,(c)=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
28
29
|
async function api(path,init={}){const headers={'Accept':'application/json',...(init.body?{'Content-Type':'application/json'}:{}),...(csrf?{'x-numa-console-csrf':csrf}:{})};const response=await fetch(path,{...init,headers:{...headers,...(init.headers||{})},credentials:'same-origin'});const body=await response.json().catch(()=>({error:{message:'响应不是 JSON'}}));if(!response.ok)throw new Error(body.error?.message||body.stderr||body.stdout||('HTTP '+response.status));return body}
|
|
29
30
|
async function bootstrap(attempt=0){try{state=await api('/api/bootstrap');csrf=state.csrfToken;const callbackState=new URLSearchParams(location.search).get('login');const waitingForSuccess=callbackState==='complete'&&!state.loggedIn&&attempt<40;const waitingForFailure=callbackState==='failed'&&!state.loginError&&attempt<8;if(waitingForSuccess||waitingForFailure){app.innerHTML='<div class="boot"><div class="boot-mark">N</div><p>'+(callbackState==='complete'?'登录成功,正在建立本机会话…':'正在读取登录失败信息…')+'</p></div>';setTimeout(()=>bootstrap(attempt+1),250);return}if(callbackState==='complete'&&!state.loggedIn&&!state.loginError)state.loginError='登录凭据交换尚未完成,请重试登录。';if(callbackState==='failed'&&!state.loginError)state.loginError='Keycloak 登录未完成,请重试。';if(callbackState)history.replaceState({},'',location.pathname);render()}catch(error){app.innerHTML='<div class="login-gate"><div class="login-card"><span class="tag role">连接失败</span><h2>本地控制台不可用</h2><p>'+esc(error.message)+'</p></div></div>'}}
|
|
30
31
|
function navButton(id,label,mark){return '<button data-nav="'+id+'" class="'+(active===id?'active':'')+'"><span>'+label+'</span><span class="mono">'+mark+'</span></button>'}
|
|
31
|
-
function shell(content){const user=state.user||{};const label=user.username||user.email||'未登录';return '<div class="shell"><aside class="rail"><div class="brand"><div class="brand-mark">N</div><div><strong>Numa CLI</strong><small>Local control plane</small></div></div><nav class="nav">'+navButton('overview','运行概览','01')+navButton('chatgpt','ChatGPT 账号','
|
|
32
|
-
function sectionTitle(){return {overview:'运行概览',chatgpt:'ChatGPT 账号',config:'基础配置',commands:'命令能力',runner:'安全执行器'}[active]||'控制台'}
|
|
32
|
+
function shell(content){const user=state.user||{};const label=user.username||user.email||'未登录';return '<div class="shell"><aside class="rail"><div class="brand"><div class="brand-mark">N</div><div><strong>Numa CLI</strong><small>Local control plane</small></div></div><nav class="nav">'+navButton('overview','运行概览','01')+navButton('registry','AI Registry','02')+navButton('chatgpt','ChatGPT 账号','03')+navButton('config','基础配置','04')+navButton('commands','命令能力','05')+navButton('runner','执行器','06')+'</nav><div class="rail-foot"><strong><span class="pulse"></span>仅监听本机</strong><p>Token 不会发送给网页脚本,也不会出现在命令输出中。</p></div></aside><main class="main"><header class="topbar"><div><div class="eyebrow">Numa / Web console</div><h1>'+sectionTitle()+'</h1></div><div class="user-chip"><div class="avatar">'+esc(label.slice(0,1).toUpperCase())+'</div><div><strong>'+esc(label)+'</strong><br><small>'+esc(state.loggedIn?'Keycloak 会话有效':'需要登录')+'</small></div></div></header>'+content+'</main></div>'}
|
|
33
|
+
function sectionTitle(){return {overview:'运行概览',registry:'AI Registry',chatgpt:'ChatGPT 账号',config:'基础配置',commands:'命令能力',runner:'安全执行器'}[active]||'控制台'}
|
|
33
34
|
function loginGate(){const error=state?.loginError?'<div class="notice error">'+esc(state.loginError)+'</div>':'';return '<div class="login-gate"><div class="login-card"><span class="tag role">Keycloak PKCE</span><h2>登录后进入本地控制台</h2><p>浏览器只负责完成认证和操作本机 API。访问令牌保存在 CLI 的私有缓存中,不写入 localStorage,也不返回给页面。</p><div class="actions"><button class="button primary" data-action="login">使用 Keycloak 登录</button></div><div id="notice">'+error+'</div></div></div>'}
|
|
34
35
|
function overview(){const u=state.user||{};const roles=[...(u.realmRoles||[]),...Object.values(u.clientRoles||{}).flat()];return '<div class="content"><section class="hero"><div><div class="eyebrow">Authenticated workstation</div><h2>本机会话已接入研发控制面</h2><p>从同一处检查身份、调整 CLI 配置,并以结构化参数运行受支持命令。所有网络请求继续由 CLI 注入 Bearer Token。</p></div><div class="session-track"><div class="track-node live"><span>身份</span><strong>'+esc(u.username||'已登录')+'</strong></div><div class="track-node"><span>API</span><strong>'+esc(state.config.apiBaseUrl?'已配置':'未配置')+'</strong></div><div class="track-node"><span>角色</span><strong>'+roles.length+'</strong></div></div></section><section class="grid"><article class="card"><div class="eyebrow">Session</div><div class="metric">'+esc(state.expiresAt?new Date(state.expiresAt*1000).toLocaleTimeString('zh-CN',{hour:'2-digit',minute:'2-digit'}):'—')+' <small>访问令牌到期</small></div><p>需要时由 CLI 使用 refresh token 自动续期。</p></article><article class="card"><div class="eyebrow">Commands</div><div class="metric">'+state.commands.length+' <small>条能力契约</small></div><p>网页执行器只接受目录中声明的命令。</p></article><article class="card"><div class="eyebrow">Endpoint</div><div class="metric">'+(state.config.apiBaseUrl?'READY':'SETUP')+'</div><p>'+esc(state.config.apiBaseUrl||'请在基础配置中填写平台 API 地址')+'</p></article><article class="card wide"><h3>当前身份</h3><p>'+esc(u.displayName||u.username||'-')+' · '+esc(u.email||'-')+'</p><div class="actions">'+roles.slice(0,8).map((r)=>'<span class="tag">'+esc(r)+'</span>').join('')+(roles.length>8?'<span class="tag public">+'+(roles.length-8)+'</span>':'')+'</div></article><article class="card"><h3>会话操作</h3><p>强制刷新身份资料,或清除本机 Keycloak 缓存。</p><div class="actions"><button class="button" data-action="reload">刷新状态</button><button class="button danger" data-action="logout">退出登录</button></div></article></section></div>'}
|
|
35
36
|
function instant(value){if(!value)return null;const number=typeof value==='number'?(value<1000000000000?value*1000:value):Date.parse(value);return Number.isFinite(number)?number:null}
|
|
@@ -44,16 +45,29 @@ function syncTag(row){if(chatgptRemote?.status==='LOADING')return '<span class="
|
|
|
44
45
|
function remoteStage(){const remote=chatgptRemote;if(!remote||remote.status==='LOADING')return {tag:'<span class="tag waiting"><span class="spin">↻</span> 连接中</span>',title:'正在连接平台',detail:'本地账号已经可见,正在读取远程可用账号。'};if(remote.status==='SUCCESS')return {tag:'<span class="tag success">同步成功</span>',title:remote.accounts.length+' 个平台账号',detail:'完成于 '+timeText(remote.syncedAt)};if(remote.status==='FAILED')return {tag:'<span class="tag failed">远程同步失败</span>',title:'保留本地结果',detail:remote.error?.message||'无法连接远程账号服务'};return {tag:'<span class="tag waiting">未配置</span>',title:'未配置平台地址',detail:'可在基础配置中填写平台 API 地址。'}}
|
|
45
46
|
function accountRows(rows){return rows.map((row)=>'<tr><td><div class="account-name"><strong>'+esc(row.label)+'</strong><small>'+esc(row.email||row.accountId||row.key)+'</small></div></td><td><div class="status-stack">'+(row.enabled?'<span class="tag success">可用</span>':'<span class="tag failed">已停用</span>')+(row.selected?'<span class="tag role">客户端已选</span>':'')+'</div></td><td>'+quotaCell(row.quota)+'</td><td><div class="table-meta">Token '+esc(row.local?'本地可用':'按需从平台获取')+'<br>到期 '+esc(timeText(row.local?.accessExpiresAt||row.remote?.accessExpiresAt))+'</div></td><td><div class="status-stack">'+syncTag(row)+'</div></td></tr>').join('')}
|
|
46
47
|
function chatgptPage(){const rows=mergedChatgptAccounts();const remote=remoteStage();const localReady=chatgptLocal?.status==='READY';const localCount=chatgptLocal?.available??0;const failure=chatgptRemote?.status==='FAILED'?'<div class="notice error"><strong>远程同步失败。</strong> '+esc(chatgptRemote.error?.message||'平台暂时不可用')+';本地账号数据仍可正常查看。</div>':'';return '<div class="content"><section class="hero"><div><div class="eyebrow">Local first / Remote second</div><h2>ChatGPT 账号运行视图</h2><p>先读取 codex-multi-auth 本地账号与额度缓存,再连接平台补充当前用户可用账号。页面永不接收 access token 或 refresh token。</p></div><div class="session-track"><div class="track-node live"><span>本地可用</span><strong>'+localCount+'</strong></div><div class="track-node"><span>汇总账号</span><strong>'+rows.length+'</strong></div><div class="track-node"><span>远程</span><strong>'+esc(chatgptRemote?.status||'WAITING')+'</strong></div></div></section><section class="sync-board"><article class="sync-stage"><div class="eyebrow">01 / Local pool</div><strong>'+(localReady?localCount+' 个本地可用账号':'正在读取本地账号')+'</strong><p>'+(localReady?'读取完成于 '+timeText(chatgptLocal.readAt):'优先读取 codex-multi-auth 安全摘要。')+'</p></article><div class="sync-link" aria-label="然后连接远程"></div><article class="sync-stage"><div class="eyebrow">02 / Platform service</div>'+remote.tag+'<strong>'+esc(remote.title)+'</strong><p>'+esc(remote.detail)+'</p></article></section>'+failure+'<section class="table-card"><header class="table-head"><div><h3>可用账号与剩余额度</h3><p>额度优先展示本地或服务器端更新较新的安全快照。</p></div><button class="button" data-action="chatgpt-refresh" '+(chatgptLoading?'disabled':'')+'>'+(chatgptLoading?'<span class="spin">↻</span> 同步中':'↻ 重新同步')+'</button></header>'+(rows.length?'<div class="account-table-wrap"><table class="account-table"><thead><tr><th>账号</th><th>可用状态</th><th>剩余 limit</th><th>凭据时效</th><th>同步状态</th></tr></thead><tbody>'+accountRows(rows)+'</tbody></table></div>':'<div class="empty-state'+(chatgptLoading?'':' account-request')+'">'+(chatgptLoading?'正在读取本地账号…':'<strong>需要申请 ChatGPT 账号</strong><span>'+esc(noAvailableAccountGuidance)+'</span>')+'</div>')+'</section></div>'}
|
|
48
|
+
function registryPageItems(payload){if(Array.isArray(payload))return payload.filter((item)=>item&&typeof item==='object');for(const key of ['pageItems','items','records','list','data']){const value=payload?.[key];if(Array.isArray(value))return value.filter((item)=>item&&typeof item==='object');if(value&&typeof value==='object'){const nested=registryPageItems(value);if(nested.length)return nested}}return []}
|
|
49
|
+
function registryItemName(item,index){return String(item?.name??item?.skillName??item?.mcpName??((registryMode==='skill'?'skill-':'mcp-')+(index+1)))}
|
|
50
|
+
function registryItemStatus(item){return String(item?.status??item?.releaseStatus??item?.state??item?.version??'registered')}
|
|
51
|
+
function registryCanEdit(){const u=state.user||{};const roles=[...(u.realmRoles||[]),...Object.values(u.clientRoles||{}).flat()].map(String);return roles.includes('mci-devops-platform-ai-registry-admin')||roles.some((role)=>role.endsWith('-ops-admin'))}
|
|
52
|
+
function registryNamespaceOptions(){return registryNamespaces.map((namespace)=>'<option value="'+namespace.id+'" '+(Number(namespace.id)===Number(registryNamespaceId)?'selected':'')+'>'+esc(namespace.displayName)+' · '+esc(namespace.namespaceKey)+' · '+esc(namespace.instanceCode)+(namespace.isDefault?' · default':'')+'</option>').join('')}
|
|
53
|
+
function registryItemButtons(){return registryItems.map((item,index)=>{const name=registryItemName(item,index);const status=registryItemStatus(item);const online=/online|published|release/i.test(status);return '<button class="registry-item '+(name===registryName?'active':'')+'" data-registry-name="'+esc(name)+'"><span class="registry-dot '+(online?'online':'')+'"></span><span><strong>'+esc(name)+'</strong><small>'+esc(status)+(item.scope?' · '+esc(item.scope):'')+'</small></span></button>'}).join('')||'<div class="empty-state">没有匹配的 '+(registryMode==='skill'?'Skill':'MCP Server')+'</div>'}
|
|
54
|
+
function registryTargets(){const targets=registryMode==='skill'?['chatgpt','codex','claude','cursor','workbuddy','openclaw','agents']:['chatgpt','codex','claude','cursor','workbuddy','openclaw'];return targets.map((target)=>'<option value="'+target+'">'+target+'</option>').join('')}
|
|
55
|
+
function registryDetailPanel(){if(registryLoading)return '<div class="empty-state"><span class="spin">↻</span> 正在读取私有 Registry…</div>';if(!registryName)return '<div class="empty-state">从左侧选择一项查看详情与操作。</div>';const canEdit=registryCanEdit();const mutating=registryMode==='skill'?'<button class="button" data-registry-action="submit" '+(canEdit?'':'disabled')+'>提交审核</button><button class="button" data-registry-action="publish" '+(canEdit?'':'disabled')+'>发布版本</button><button class="button" data-registry-action="online" '+(canEdit?'':'disabled')+'>上线</button><button class="button" data-registry-action="offline" '+(canEdit?'':'disabled')+'>下线</button>':'';return '<div class="registry-detail-head"><div><div class="eyebrow">'+esc(registryMode==='skill'?'Skill package':'MCP definition')+'</div><h3>'+esc(registryName)+'</h3><span class="tag '+(canEdit?'role':'public')+'">'+(canEdit?'registry admin':'reader')+'</span></div><button class="button" data-action="registry-refresh">↻ 刷新</button></div><pre class="output">'+esc(JSON.stringify(registryDetail??{},null,2))+'</pre><div class="registry-actions"><div class="install-row"><label class="field"><span>安装目标</span><select id="registry-target">'+registryTargets()+'</select></label><label class="field"><span>安装范围</span><select id="registry-scope"><option value="project">当前项目</option><option value="user">当前用户</option></select></label><button class="button primary" data-registry-action="install">安装</button></div><div class="actions">'+mutating+'</div><small class="table-meta">发布类按钮只是权限提示;最终授权仍由平台 Keycloak Policy Enforcer 执行。</small><div id="registry-notice"></div></div>'}
|
|
56
|
+
function registryPage(){const error=registryError?'<div class="notice error registry-error">'+esc(registryError)+'</div>':'';return '<div class="content"><section class="hero registry-hero"><div><div class="eyebrow">Nacos private AI registry</div><h2>发现、治理并安装团队能力</h2><p>Nacos 实例、OIDC 配置与 namespace 路由由平台数据库管理;浏览器与 CLI 只携带当前用户的 Keycloak 身份。</p></div><div class="lifecycle" aria-label="Skill 生命周期"><div class="life-node live">draft</div><div class="life-node">review</div><div class="life-node">online</div></div></section>'+error+'<section class="registry-toolbar"><label class="field"><span>Nacos namespace</span><select id="registry-namespace">'+registryNamespaceOptions()+'</select></label><div class="segment" role="group" aria-label="资源类型"><button data-registry-mode="skill" class="'+(registryMode==='skill'?'active':'')+'">Skills</button><button data-registry-mode="mcp" class="'+(registryMode==='mcp'?'active':'')+'">MCP</button></div><label class="field"><span>过滤</span><input id="registry-search" value="'+esc(registrySearch)+'" placeholder="名称或描述"></label><button class="button" data-load="'+esc(JSON.stringify(['registry','namespaces','list']))+'">实例/namespace 命令</button></section><section class="registry-board"><aside class="registry-list"><div class="registry-list-head"><strong>'+(registryMode==='skill'?'Skill track':'MCP track')+'</strong><span class="tag public">'+registryItems.length+'</span></div><div class="registry-items">'+registryItemButtons()+'</div></aside><article class="registry-detail">'+registryDetailPanel()+'</article></section></div>'}
|
|
57
|
+
async function loadRegistry(resetNamespaces=false){if(registryLoading)return;registryLoading=true;registryError='';render();try{if(resetNamespaces||!registryNamespaces.length){registryNamespaces=await api('/api/registry/namespaces');if(!Array.isArray(registryNamespaces))throw new Error('namespace 响应格式无效');if(!registryNamespaceId||!registryNamespaces.some((namespace)=>Number(namespace.id)===Number(registryNamespaceId)))registryNamespaceId=registryNamespaces.find((namespace)=>namespace.status==='ACTIVE'&&namespace.instanceStatus==='ACTIVE'&&namespace.isDefault)?.id??registryNamespaces.find((namespace)=>namespace.status==='ACTIVE'&&namespace.instanceStatus==='ACTIVE')?.id??registryNamespaces[0]?.id??null}if(!registryNamespaceId){registryItems=[];registryDetail=null;return}const endpoint=registryMode==='skill'?'/api/registry/skills':'/api/registry/mcp';const payload=await api(endpoint+'?namespaceId='+encodeURIComponent(registryNamespaceId)+(registrySearch?'&search='+encodeURIComponent(registrySearch):''));registryItems=registryPageItems(payload);if(registryName&&!registryItems.some((item,index)=>registryItemName(item,index)===registryName)){registryName='';registryDetail=null}}catch(error){registryError=error.message;registryItems=[];registryDetail=null}finally{registryLoading=false;render()}}
|
|
58
|
+
async function selectRegistryItem(name){registryName=name;registryLoading=true;registryError='';render();try{const endpoint=registryMode==='skill'?'/api/registry/skill':'/api/registry/mcp-detail';registryDetail=await api(endpoint+'?namespaceId='+encodeURIComponent(registryNamespaceId)+'&name='+encodeURIComponent(name))}catch(error){registryError=error.message;registryDetail=null}finally{registryLoading=false;render()}}
|
|
59
|
+
async function runRegistryCommand(argv,successMessage){const target=document.getElementById('registry-notice');if(target)target.innerHTML='<div class="notice success">正在执行…</div>';try{const result=await api('/api/commands/run',{method:'POST',body:JSON.stringify({argv,confirm:false})});if(result.exitCode!==0)throw new Error(result.stderr||result.stdout||'命令执行失败');if(registryMode==='skill'&®istryName&&argv[2]!=='install')await selectRegistryItem(registryName);const current=document.getElementById('registry-notice');if(current)current.innerHTML='<div class="notice success">'+esc(successMessage)+'</div>'}catch(error){const current=document.getElementById('registry-notice');if(current)current.innerHTML='<div class="notice error">'+esc(error.message)+'</div>'}}
|
|
60
|
+
async function registryAction(action){if(!registryNamespaceId||!registryName)return;const selected=registryNamespaces.find((namespace)=>Number(namespace.id)===Number(registryNamespaceId));if(!selected)throw new Error('未选择 namespace');const targetArgs=['--namespace',String(selected.namespaceKey),'--instance',String(selected.instanceCode)];if(action==='install'){const agent=document.getElementById('registry-target').value;let scope=document.getElementById('registry-scope').value;if(registryMode==='mcp'&&(agent==='codex'||agent==='openclaw'))scope='user';const argv=['registry',registryMode,'install',registryName,...targetArgs,'--agent',agent,'--scope',scope];if(registryMode==='skill')argv.push('--label','latest');await runRegistryCommand(argv,'已完成本机安装');return}if(!confirm('确认对 '+registryName+' 执行 '+action+'?'))return;const argv=['registry','skill',action,registryName,...targetArgs];if(action==='publish'){const version=prompt('输入要发布的 SemVer');if(!version)return;argv.push('--version',version)}await runRegistryCommand(argv,action+' 完成')}
|
|
47
61
|
function configPage(){const c=state.config;return '<div class="content"><section class="grid"><article class="card full"><div class="eyebrow">Configuration file</div><h3>基础配置</h3><p>与 <code>numa config --set</code> 使用同一份配置和校验逻辑。</p><form id="config-form" class="form-grid"><div class="field"><label>界面语言</label><select name="locale"><option '+(c.locale==='zh-CN'?'selected':'')+'>zh-CN</option><option '+(c.locale==='en-US'?'selected':'')+'>en-US</option></select></div><div class="field"><label>默认输出</label><select name="output"><option '+(c.output==='text'?'selected':'')+'>text</option><option '+(c.output==='json'?'selected':'')+'>json</option></select></div><div class="field span-2"><label>平台 API 地址</label><input name="apiBaseUrl" value="'+esc(c.apiBaseUrl)+'" placeholder="https://platform.example/api"></div><div class="field span-2"><label>npm registry</label><input name="registryUrl" value="'+esc(c.registryUrl)+'"></div><label class="field span-2"><span>更新检查</span><select name="checkUpdates"><option value="true" '+(c.checkUpdates?'selected':'')+'>启用</option><option value="false" '+(!c.checkUpdates?'selected':'')+'>关闭</option></select></label><div class="actions span-2"><button class="button primary" type="submit">保存配置</button></div></form><div id="notice"></div></article></section></div>'}
|
|
48
62
|
function commandsPage(){return '<div class="content"><section class="card full"><div class="command-toolbar"><div class="field grow"><label>过滤命令</label><input id="command-search" placeholder="例如 codex、media、auth"></div></div><div id="command-list" class="command-list">'+commandCards('')+'</div></section></div>'}
|
|
49
63
|
function commandCards(query){const q=query.toLowerCase();return state.commands.filter((c)=>(c.command+' '+c.summary).toLowerCase().includes(q)).map((c)=>'<article class="command"><div class="command-head"><code>'+esc(c.command)+'</code><span class="tag '+esc(c.access)+'">'+esc(c.access)+'</span></div><p>'+esc(c.summary)+'</p><small>'+esc(c.examples[0]||'')+'</small><div class="actions mt-10"><button class="button" data-load="'+esc(JSON.stringify(commandPrefix(c.command)))+'">载入执行器</button></div></article>').join('')||'<p>没有匹配命令。</p>'}
|
|
50
64
|
function commandPrefix(command){return command.split(/\s+/).filter((part)=>!part.startsWith('[')&&!part.startsWith('<')&&!part.startsWith('--'))}
|
|
51
65
|
function runnerPage(){return '<div class="content"><section class="grid"><article class="card"><div class="eyebrow">Structured argv</div><h3>安全执行器</h3><p>使用 JSON 数组传入参数,不经过 shell。长驻或交互命令需要在终端运行。</p><form id="runner-form"><div class="field"><label>命令参数</label><textarea id="argv" class="mono">["doctor"]</textarea></div><label class="field mt-12"><span>危险操作确认</span><select id="confirm"><option value="false">未确认</option><option value="true">确认执行删除/重置</option></select></label><div class="actions mt-14"><button class="button primary" type="submit">运行命令</button></div></form></article><article class="card wide"><div class="eyebrow">Result stream</div><h3>结构化输出</h3><pre id="output" class="output">'+esc(output)+'</pre></article></section></div>'}
|
|
52
66
|
async function loadChatgpt(){if(chatgptLoading)return;chatgptLoading=true;chatgptLocal=null;chatgptRemote={status:'LOADING',accounts:[]};render();try{chatgptLocal=await api('/api/chatgpt/local')}catch(error){chatgptLocal={status:'FAILED',accounts:[],available:0,total:0,error:{message:error.message}}}render();try{chatgptRemote=await api('/api/chatgpt/remote')}catch(error){chatgptRemote={status:'FAILED',accounts:[],syncedAt:Date.now(),error:{message:error.message}}}finally{chatgptLoading=false;render()}}
|
|
53
|
-
function render(){if(!state.loggedIn){app.innerHTML=loginGate();bind();return}const page=active==='overview'?overview():active==='chatgpt'?chatgptPage():active==='config'?configPage():active==='commands'?commandsPage():runnerPage();app.innerHTML=shell(page);bind()}
|
|
67
|
+
function render(){if(!state.loggedIn){app.innerHTML=loginGate();bind();return}const page=active==='overview'?overview():active==='registry'?registryPage():active==='chatgpt'?chatgptPage():active==='config'?configPage():active==='commands'?commandsPage():runnerPage();app.innerHTML=shell(page);bind()}
|
|
54
68
|
function notice(message,type='success'){const target=document.getElementById('notice');if(target)target.innerHTML='<div class="notice '+type+'">'+esc(message)+'</div>'}
|
|
55
69
|
function bindCommandLoaders(){document.querySelectorAll('[data-load]').forEach((b)=>b.addEventListener('click',()=>{const argv=JSON.parse(b.dataset.load);active='runner';render();document.getElementById('argv').value=JSON.stringify(argv)}))}
|
|
56
|
-
function bind(){document.querySelectorAll('[data-nav]').forEach((b)=>b.addEventListener('click',()=>{active=b.dataset.nav;render();if(active==='chatgpt'&&!chatgptLocal&&!chatgptLoading)void loadChatgpt()}));document.querySelector('[data-action="chatgpt-refresh"]')?.addEventListener('click',()=>void loadChatgpt());document.querySelector('[data-action="login"]')?.addEventListener('click',async()=>{try{notice('正在启动登录…');const result=await api('/api/auth/login',{method:'POST',body:'{}'});location.href=result.authorizationUrl}catch(e){notice(e.message,'error')}});document.querySelector('[data-action="reload"]')?.addEventListener('click',()=>bootstrap());document.querySelector('[data-action="logout"]')?.addEventListener('click',async()=>{if(!confirm('清除本机 Keycloak 登录缓存?'))return;await api('/api/auth/logout',{method:'POST',body:'{}'});location.reload()});document.getElementById('config-form')?.addEventListener('submit',async(e)=>{e.preventDefault();const data=Object.fromEntries(new FormData(e.currentTarget));data.checkUpdates=data.checkUpdates==='true';try{const result=await api('/api/config',{method:'PUT',body:JSON.stringify(data)});state.config=result.config;notice('配置已保存')}catch(err){notice(err.message,'error')}});document.getElementById('command-search')?.addEventListener('input',(e)=>{document.getElementById('command-list').innerHTML=commandCards(e.target.value);bindCommandLoaders()});bindCommandLoaders();document.getElementById('runner-form')?.addEventListener('submit',async(e)=>{e.preventDefault();try{const argv=JSON.parse(document.getElementById('argv').value);const confirmed=document.getElementById('confirm')?.value==='true';output='运行中…';render();const result=await api('/api/commands/run',{method:'POST',body:JSON.stringify({argv,confirm:confirmed})});output=result.stdout||result.stderr||JSON.stringify(result,null,2);render()}catch(err){output='错误: '+err.message;render()}})}
|
|
70
|
+
function bind(){document.querySelectorAll('[data-nav]').forEach((b)=>b.addEventListener('click',()=>{active=b.dataset.nav;render();if(active==='chatgpt'&&!chatgptLocal&&!chatgptLoading)void loadChatgpt();if(active==='registry'&&!registryNamespaces.length&&!registryLoading)void loadRegistry(true)}));document.querySelector('[data-action="chatgpt-refresh"]')?.addEventListener('click',()=>void loadChatgpt());document.querySelector('[data-action="registry-refresh"]')?.addEventListener('click',()=>registryName?void selectRegistryItem(registryName):void loadRegistry());document.querySelectorAll('[data-registry-mode]').forEach((button)=>button.addEventListener('click',()=>{registryMode=button.dataset.registryMode;registryName='';registryDetail=null;void loadRegistry()}));document.querySelectorAll('[data-registry-name]').forEach((button)=>button.addEventListener('click',()=>void selectRegistryItem(button.dataset.registryName)));document.querySelectorAll('[data-registry-action]').forEach((button)=>button.addEventListener('click',()=>void registryAction(button.dataset.registryAction)));document.getElementById('registry-namespace')?.addEventListener('change',(event)=>{registryNamespaceId=Number(event.target.value);registryName='';registryDetail=null;void loadRegistry()});document.getElementById('registry-search')?.addEventListener('change',(event)=>{registrySearch=event.target.value.trim();registryName='';registryDetail=null;void loadRegistry()});document.getElementById('registry-search')?.addEventListener('keydown',(event)=>{if(event.key==='Enter'){event.preventDefault();registrySearch=event.target.value.trim();registryName='';registryDetail=null;void loadRegistry()}});document.querySelector('[data-action="login"]')?.addEventListener('click',async()=>{try{notice('正在启动登录…');const result=await api('/api/auth/login',{method:'POST',body:'{}'});location.href=result.authorizationUrl}catch(e){notice(e.message,'error')}});document.querySelector('[data-action="reload"]')?.addEventListener('click',()=>bootstrap());document.querySelector('[data-action="logout"]')?.addEventListener('click',async()=>{if(!confirm('清除本机 Keycloak 登录缓存?'))return;await api('/api/auth/logout',{method:'POST',body:'{}'});location.reload()});document.getElementById('config-form')?.addEventListener('submit',async(e)=>{e.preventDefault();const data=Object.fromEntries(new FormData(e.currentTarget));data.checkUpdates=data.checkUpdates==='true';try{const result=await api('/api/config',{method:'PUT',body:JSON.stringify(data)});state.config=result.config;notice('配置已保存')}catch(err){notice(err.message,'error')}});document.getElementById('command-search')?.addEventListener('input',(e)=>{document.getElementById('command-list').innerHTML=commandCards(e.target.value);bindCommandLoaders()});bindCommandLoaders();document.getElementById('runner-form')?.addEventListener('submit',async(e)=>{e.preventDefault();try{const argv=JSON.parse(document.getElementById('argv').value);const confirmed=document.getElementById('confirm')?.value==='true';output='运行中…';render();const result=await api('/api/commands/run',{method:'POST',body:JSON.stringify({argv,confirm:confirmed})});output=result.stdout||result.stderr||JSON.stringify(result,null,2);render()}catch(err){output='错误: '+err.message;render()}})}
|
|
57
71
|
bootstrap();
|
|
58
72
|
`;
|
|
59
73
|
//# sourceMappingURL=web-console-page.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web-console-page.js","sourceRoot":"","sources":["../src/web-console-page.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,YAAY,GAAG;;;;;;;;;;;;QAYpB,CAAC;AAET,MAAM,CAAC,MAAM,WAAW,GAAG
|
|
1
|
+
{"version":3,"file":"web-console-page.js","sourceRoot":"","sources":["../src/web-console-page.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,YAAY,GAAG;;;;;;;;;;;;QAYpB,CAAC;AAET,MAAM,CAAC,MAAM,WAAW,GAAG;;;;;;;;;;;CAW1B,CAAC;AAEF,MAAM,CAAC,MAAM,UAAU,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8CzB,CAAC"}
|
package/dist/web-console.d.ts
CHANGED
|
@@ -1,16 +1,30 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { CodexRemoteStatus, LocalCodexPoolAccount } from "@tokensrc/codex";
|
|
2
|
+
import { AiRegistryClient } from "./ai-registry/client.js";
|
|
2
3
|
export interface ConsoleCommandResult {
|
|
3
4
|
exitCode: number;
|
|
4
5
|
stdout: string;
|
|
5
6
|
stderr: string;
|
|
6
7
|
}
|
|
8
|
+
export interface LocalCodexConsoleState {
|
|
9
|
+
status: "READY";
|
|
10
|
+
readAt: number;
|
|
11
|
+
total: number;
|
|
12
|
+
available: number;
|
|
13
|
+
accounts: LocalCodexPoolAccount[];
|
|
14
|
+
}
|
|
15
|
+
export type RemoteCodexConsoleState = CodexRemoteStatus;
|
|
7
16
|
export interface WebConsoleOptions {
|
|
8
17
|
host?: string;
|
|
9
18
|
port?: number;
|
|
10
19
|
openBrowser?: boolean;
|
|
11
20
|
commandRunner?: (argv: string[]) => Promise<ConsoleCommandResult>;
|
|
12
|
-
|
|
13
|
-
|
|
21
|
+
localCodexLoader?: () => Promise<LocalCodexConsoleState>;
|
|
22
|
+
remoteCodexLoader?: () => Promise<RemoteCodexConsoleState>;
|
|
23
|
+
/** @deprecated Use localCodexLoader. */
|
|
24
|
+
localChatgptLoader?: () => Promise<LocalCodexConsoleState>;
|
|
25
|
+
/** @deprecated Use remoteCodexLoader. */
|
|
26
|
+
remoteChatgptLoader?: () => Promise<RemoteCodexConsoleState>;
|
|
27
|
+
registryClientFactory?: () => Pick<AiRegistryClient, "listNamespaces" | "listSkills" | "listMcpServers" | "getSkill" | "getMcpServer">;
|
|
14
28
|
}
|
|
15
29
|
export interface WebConsoleHandle {
|
|
16
30
|
url: string;
|
package/dist/web-console.js
CHANGED
|
@@ -5,7 +5,9 @@ import process from "node:process";
|
|
|
5
5
|
import open from "open";
|
|
6
6
|
import { CONFIG_KEYS, loadAppConfig, saveAppConfig, updateConfigValue } from "./app-config.js";
|
|
7
7
|
import { COMMAND_CATALOG } from "./command-catalog.js";
|
|
8
|
-
import {
|
|
8
|
+
import { AiRegistryClient } from "./ai-registry/client.js";
|
|
9
|
+
import { sanitizeRegistryOutput } from "./ai-registry/errors.js";
|
|
10
|
+
import { loadNumaCodexLocalStatus, loadNumaCodexRemoteStatus } from "./codex-module.js";
|
|
9
11
|
import { loadConfig } from "./config.js";
|
|
10
12
|
import { currentIdentity } from "./identity.js";
|
|
11
13
|
import { AuthError, completeBrowserLogin, deleteTokenCache, nowSeconds, readTokenCache, startBrowserLogin } from "./oauth.js";
|
|
@@ -15,6 +17,16 @@ const MAX_BODY_BYTES = 1024 * 1024;
|
|
|
15
17
|
const MAX_COMMAND_OUTPUT_BYTES = 1024 * 1024;
|
|
16
18
|
const COMMAND_TIMEOUT_MS = 120_000;
|
|
17
19
|
const COOKIE_NAME = "numa_console_session";
|
|
20
|
+
async function loadLocalCodexConsoleState() {
|
|
21
|
+
const snapshot = await loadNumaCodexLocalStatus();
|
|
22
|
+
return {
|
|
23
|
+
status: snapshot.status,
|
|
24
|
+
readAt: snapshot.readAt,
|
|
25
|
+
total: snapshot.total,
|
|
26
|
+
available: snapshot.available,
|
|
27
|
+
accounts: snapshot.accounts
|
|
28
|
+
};
|
|
29
|
+
}
|
|
18
30
|
function commandPrefix(descriptor) {
|
|
19
31
|
const prefix = [];
|
|
20
32
|
for (const part of descriptor.command.split(/\s+/u)) {
|
|
@@ -44,7 +56,7 @@ export function validateConsoleCommand(input, confirmed = false) {
|
|
|
44
56
|
if (!descriptor)
|
|
45
57
|
throw new Error("该命令不在 numa 命令目录中。");
|
|
46
58
|
const commandKey = descriptor.prefix.join(" ");
|
|
47
|
-
if (["login", "console", "serve", "codex run", "codex app", "codex auth-agent"].includes(commandKey)) {
|
|
59
|
+
if (["login", "console", "serve", "codex run", "codex app", "codex auth-agent", "registry tui"].includes(commandKey)) {
|
|
48
60
|
throw new Error(`${commandKey} 是浏览器、交互或长驻命令,请在终端直接运行。`);
|
|
49
61
|
}
|
|
50
62
|
if (commandKey === "config" && argv.length === 1) {
|
|
@@ -52,9 +64,15 @@ export function validateConsoleCommand(input, confirmed = false) {
|
|
|
52
64
|
}
|
|
53
65
|
const destructive = commandKey === "auth logout" ||
|
|
54
66
|
commandKey === "codex reset" ||
|
|
67
|
+
commandKey === "registry instances delete" ||
|
|
68
|
+
commandKey === "registry namespaces delete" ||
|
|
69
|
+
commandKey === "registry skill delete" ||
|
|
70
|
+
commandKey === "registry mcp delete" ||
|
|
71
|
+
commandKey === "pipeline stop" ||
|
|
72
|
+
commandKey === "pipeline retry" ||
|
|
55
73
|
(commandKey === "config" && argv.includes("--reset"));
|
|
56
74
|
if (destructive && !confirmed) {
|
|
57
|
-
throw new Error("
|
|
75
|
+
throw new Error("该命令会删除或重置本地/远端状态,请先启用危险操作确认。");
|
|
58
76
|
}
|
|
59
77
|
return argv.includes("--json") ? argv : [...argv, "--json"];
|
|
60
78
|
}
|
|
@@ -168,6 +186,19 @@ async function jsonBody(request) {
|
|
|
168
186
|
function safeConfig(config) {
|
|
169
187
|
return { ...config };
|
|
170
188
|
}
|
|
189
|
+
function positiveId(value, label) {
|
|
190
|
+
const parsed = Number(value);
|
|
191
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1) {
|
|
192
|
+
throw new Error(`${label} 必须是正整数。`);
|
|
193
|
+
}
|
|
194
|
+
return parsed;
|
|
195
|
+
}
|
|
196
|
+
function requiredQuery(value, label) {
|
|
197
|
+
const normalized = value?.trim();
|
|
198
|
+
if (!normalized || normalized.length > 160)
|
|
199
|
+
throw new Error(`${label} 参数无效。`);
|
|
200
|
+
return normalized;
|
|
201
|
+
}
|
|
171
202
|
async function waitForLogin(session) {
|
|
172
203
|
try {
|
|
173
204
|
while (!session.code && !session.error && session.expiresAt > nowSeconds()) {
|
|
@@ -188,8 +219,9 @@ export async function startWebConsole(options = {}) {
|
|
|
188
219
|
const sessionSecret = randomBytes(32).toString("base64url");
|
|
189
220
|
const csrfToken = randomBytes(32).toString("base64url");
|
|
190
221
|
const commandRunner = options.commandRunner ?? runConsoleCommand;
|
|
191
|
-
const localChatgptLoader = options.localChatgptLoader ??
|
|
192
|
-
const remoteChatgptLoader = options.remoteChatgptLoader ??
|
|
222
|
+
const localChatgptLoader = options.localCodexLoader ?? options.localChatgptLoader ?? loadLocalCodexConsoleState;
|
|
223
|
+
const remoteChatgptLoader = options.remoteCodexLoader ?? options.remoteChatgptLoader ?? loadNumaCodexRemoteStatus;
|
|
224
|
+
const registryClientFactory = options.registryClientFactory ?? (() => new AiRegistryClient(loadConfig("aiRegistry")));
|
|
193
225
|
let baseUrl = "";
|
|
194
226
|
let loginError;
|
|
195
227
|
let closedResolve;
|
|
@@ -257,6 +289,39 @@ export async function startWebConsole(options = {}) {
|
|
|
257
289
|
sendJson(response, 200, await remoteChatgptLoader());
|
|
258
290
|
return;
|
|
259
291
|
}
|
|
292
|
+
if (request.method === "GET" && url.pathname === "/api/registry/namespaces") {
|
|
293
|
+
sendJson(response, 200, sanitizeRegistryOutput(await registryClientFactory().listNamespaces()));
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (request.method === "GET" && url.pathname === "/api/registry/skills") {
|
|
297
|
+
const namespaceId = positiveId(url.searchParams.get("namespaceId"), "namespaceId");
|
|
298
|
+
sendJson(response, 200, sanitizeRegistryOutput(await registryClientFactory().listSkills(namespaceId, {
|
|
299
|
+
search: url.searchParams.get("search")?.trim() || undefined,
|
|
300
|
+
pageSize: 100
|
|
301
|
+
})));
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (request.method === "GET" && url.pathname === "/api/registry/mcp") {
|
|
305
|
+
const namespaceId = positiveId(url.searchParams.get("namespaceId"), "namespaceId");
|
|
306
|
+
sendJson(response, 200, sanitizeRegistryOutput(await registryClientFactory().listMcpServers(namespaceId, {
|
|
307
|
+
search: url.searchParams.get("search")?.trim() || undefined,
|
|
308
|
+
pageSize: 100
|
|
309
|
+
})));
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (request.method === "GET" && url.pathname === "/api/registry/skill") {
|
|
313
|
+
const namespaceId = positiveId(url.searchParams.get("namespaceId"), "namespaceId");
|
|
314
|
+
const name = requiredQuery(url.searchParams.get("name"), "name");
|
|
315
|
+
sendJson(response, 200, sanitizeRegistryOutput(await registryClientFactory().getSkill(namespaceId, name)));
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (request.method === "GET" && url.pathname === "/api/registry/mcp-detail") {
|
|
319
|
+
const namespaceId = positiveId(url.searchParams.get("namespaceId"), "namespaceId");
|
|
320
|
+
const name = requiredQuery(url.searchParams.get("name"), "name");
|
|
321
|
+
const version = url.searchParams.get("version")?.trim() || undefined;
|
|
322
|
+
sendJson(response, 200, sanitizeRegistryOutput(await registryClientFactory().getMcpServer(namespaceId, name, version)));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
260
325
|
if (request.method !== "GET" && request.headers["x-numa-console-csrf"] !== csrfToken) {
|
|
261
326
|
sendError(response, new Error("CSRF validation failed."), 403);
|
|
262
327
|
return;
|