ai-zero-token 1.0.8 → 1.0.10
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 +39 -0
- package/README.zh-CN.md +39 -0
- package/admin-ui/dist/assets/index-BBXWfa-w.js +11 -0
- package/admin-ui/dist/assets/index-n7rmcV5d.css +1 -0
- package/admin-ui/dist/assets/wechat-contact-Dlaib1YP.png +0 -0
- package/admin-ui/dist/index.html +13 -0
- package/build/icon.icns +0 -0
- package/build/icon.ico +0 -0
- package/build/icon.png +0 -0
- package/dist/core/providers/http-client.js +1 -1
- package/dist/core/providers/openai-codex/chat.js +23 -0
- package/dist/core/providers/openai-codex/oauth.js +24 -1
- package/dist/core/services/auth-service.js +264 -21
- package/dist/core/services/chat-service.js +2 -2
- package/dist/core/services/config-service.js +15 -3
- package/dist/core/services/image-service.js +2 -2
- package/dist/core/services/version-service.js +18 -13
- package/dist/core/store/settings-store.js +6 -0
- package/dist/desktop/main.js +127 -0
- package/dist/server/admin-page.js +1094 -100
- package/dist/server/app.js +160 -6
- package/docs/DESKTOP_RELEASE.md +64 -0
- package/docs/PRODUCT_UPDATE_DESKTOP_TOOLBOX.md +429 -0
- package/package.json +70 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;--bg:#f8fafc;--panel:#fff;--panel-soft:#f8fafc;--line:#e2e8f0;--line-strong:#cbd5e1;--text:#0f172a;--text-soft:#334155;--text-muted:#64748b;--brand:#635bff;--brand-strong:#4f46e5;--brand-soft:#635bff1a;--blue:#3b82f6;--blue-soft:#3b82f61f;--green:#22c55e;--green-soft:#22c55e1f;--orange:#f59e0b;--orange-soft:#f59e0b1f;--red:#ef4444;--red-soft:#ef44441f;--plan-color:#94a3b8;--plan-soft:#94a3b81f;--plan-border:var(--line);--shadow:0 2px 10px #0f172a0f;--shadow-sm:var(--shadow);--radius:16px;--radius-sm:12px;--radius-xs:10px}*{box-sizing:border-box}html{background:var(--bg)}body{min-width:320px;min-height:100vh;color:var(--text);background:radial-gradient(circle at top left, #635bff14, transparent 28%), radial-gradient(circle at right top, #3b82f60f, transparent 32%), var(--bg);margin:0;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,PingFang SC,sans-serif}button,input,select,textarea{font:inherit}button,a{-webkit-tap-highlight-color:transparent}button{cursor:pointer}a{color:inherit;text-decoration:none}.app-shell{grid-template-columns:260px minmax(0,1fr);align-items:start;gap:20px;width:calc(100vw - 24px);max-width:none;margin:12px auto 24px;display:grid}.sidebar,.card,.trend-card,.summary-card,.account-card,.log-table-wrap,.tester-tabs,.service-card,.update-panel{border:1px solid var(--line);box-shadow:var(--shadow-sm);background:#fffffff0}.sidebar{border-radius:24px;gap:24px;min-width:0;max-height:calc(100vh - 40px);padding:22px 18px;display:grid;position:sticky;top:20px;overflow:auto}.brand{align-items:flex-start;gap:12px;display:flex}.brand-mark{width:36px;height:36px;color:var(--brand);background:linear-gradient(#635bff24,#635bff0a);border:1px solid #635bff33;border-radius:12px;flex:none;place-items:center;display:grid}.brand strong{font-size:18px;line-height:1.25;display:block}.brand span{color:var(--text-muted);margin-top:5px;font-size:12px;line-height:1.5;display:block}.nav{gap:6px;display:grid}.nav-item{width:100%;min-height:44px;color:var(--text-soft);text-align:left;background:0 0;border:1px solid #0000;border-radius:12px;align-items:center;gap:10px;padding:0 12px;display:flex}.nav-item.is-active,.nav-item:hover{color:var(--brand);background:#635bff14;border-color:#635bff1a;font-weight:650}.main{gap:20px;min-width:0;display:grid}.topbar{grid-template-columns:minmax(0,1fr) auto;align-items:start;gap:16px;display:grid}.page-title h1{margin:0;font-size:30px;line-height:1.15}.page-title p{color:var(--text-muted);margin:8px 0 0;font-size:14px;line-height:1.6}.top-actions,.section-actions,.button-row,.example-row{flex-wrap:wrap;justify-content:flex-end;gap:10px;display:flex}.btn-primary,.btn-secondary,.btn-danger{border:1px solid var(--line);white-space:nowrap;min-height:40px;color:var(--text-soft);background:#fff;border-radius:12px;justify-content:center;align-items:center;gap:8px;padding:0 14px;font-size:13px;font-weight:700;display:inline-flex;box-shadow:0 1px #0f172a05}.btn-primary{color:#fff;border-color:var(--brand);background:var(--brand)}.btn-primary:hover{background:var(--brand-strong)}.btn-secondary:hover{border-color:var(--line-strong);background:#f8fafc}.btn-danger{color:#ef4444;background:var(--red-soft);border-color:#ef44442e}.btn-danger:hover{background:#ef444429}.btn-primary:disabled,.btn-secondary:disabled,.btn-danger:disabled{cursor:not-allowed;opacity:.58}.icon-only{width:42px;padding:0}.summary-grid{grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:14px;display:grid}.summary-card{border-radius:16px;align-content:start;gap:12px;min-height:130px;padding:18px;display:grid}.summary-card>div:last-child{gap:8px;min-width:0;display:grid}.summary-card span{color:var(--text-muted);font-size:13px;font-weight:650}.summary-card strong{overflow-wrap:break-word;min-width:0;font-size:23px;line-height:1.2;display:block}.summary-card.compact-value strong{overflow-wrap:anywhere;font-size:20px;line-height:1.25}.usage-summary{gap:7px;min-width:0;display:grid}.usage-summary-row{grid-template-columns:46px minmax(0,1fr);align-items:center;gap:8px;min-width:0;display:grid}.usage-summary-row span{min-width:0;height:22px;color:var(--brand-700);background:#4053ff17;border-radius:999px;justify-content:center;align-items:center;font-size:11px;font-weight:850;display:inline-flex}.summary-card.compact-value .usage-summary-row strong{white-space:nowrap;text-overflow:ellipsis;overflow-wrap:normal;min-width:0;font-size:14px;line-height:1.25;overflow:hidden}.summary-card p{color:var(--text-muted);margin:0;font-size:13px;line-height:1.55}.summary-icon{width:26px;height:26px;color:var(--brand);background:var(--brand-soft);border-radius:999px;place-items:center;display:grid}.summary-icon.blue{color:var(--blue);background:var(--blue-soft)}.summary-icon.green{color:var(--green);background:var(--green-soft)}.summary-icon.orange{color:var(--orange);background:var(--orange-soft)}.main-grid{grid-template-columns:minmax(0,1.8fr) minmax(360px,.95fr);align-items:start;gap:22px;display:grid}.left-column,.right-column{gap:18px;min-width:0;display:grid}.card,.trend-card,.log-table-wrap,.update-panel{border-radius:20px;padding:20px}.section-head{justify-content:space-between;align-items:flex-start;gap:16px;margin-bottom:18px;display:flex}.section-head.compact{margin-bottom:14px}.section-head h2,.section-head h3{margin:0;font-size:20px;line-height:1.25}.section-head p{color:var(--text-muted);margin:7px 0 0;font-size:13px;line-height:1.6}.trend-card{gap:14px;display:grid}.chart-wrap{border:1px solid var(--line);background:linear-gradient(#f8fafc99,#fff);border-radius:16px;width:100%;padding:14px;overflow:hidden}.chart-legend{color:var(--text-muted);flex-wrap:wrap;gap:14px;font-size:12px;display:flex}.legend-item{align-items:center;gap:8px;display:inline-flex}.legend-swatch{border-radius:999px;width:10px;height:10px}.legend-swatch.purple{background:#635bff}.legend-swatch.blue{background:#3b82f6}.trend-svg{width:100%;height:210px;margin-top:10px;display:block}.trend-labels{color:var(--text-muted);grid-template-columns:repeat(6,minmax(0,1fr));gap:6px;margin-top:10px;font-size:11px;line-height:1.4;display:grid}.filter-row{flex-wrap:wrap;align-items:center;gap:10px;margin-bottom:18px;display:flex}.search-box{border:1px solid var(--line);min-width:0;min-height:40px;color:var(--text-muted);background:#fff;border-radius:12px;flex:220px;align-items:center;gap:10px;padding:0 12px;display:flex}.search-box input{width:100%;min-width:0;color:var(--text);background:0 0;border:0;outline:none}.input,.control,.textarea{border:1px solid var(--line);width:100%;color:var(--text);background:#fff;border-radius:12px;outline:none}.input,.control{min-height:40px;padding:0 12px}.filter-row .control{flex:0 0 156px;min-width:156px}.textarea{resize:vertical;min-height:180px;padding:14px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;line-height:1.55}.field{color:var(--text-soft);gap:8px;font-size:13px;font-weight:700;display:grid}.hint,.status-inline{color:var(--text-muted);margin:9px 0;font-size:13px;line-height:1.6}.account-selected-count{color:var(--text-muted);white-space:nowrap;font-size:12px;font-weight:700}.account-grid{justify-content:stretch;align-items:start;gap:16px;width:100%;display:grid}.account-grid.profile-count-1{grid-template-columns:minmax(340px,520px)}.account-grid.profile-count-2{grid-template-columns:repeat(2,minmax(340px,1fr))}.account-grid.profile-count-3{grid-template-columns:repeat(3,minmax(320px,1fr))}.account-grid.profile-count-many{grid-template-columns:repeat(auto-fit,minmax(340px,1fr))}.account-card{--plan-color:#94a3b8;--plan-soft:#94a3b81f;--plan-border:var(--line);--usage-color:#16a34a;--usage-soft:#16a34a1f;border-color:var(--plan-border);border-radius:16px;grid-template-rows:none;align-content:start;align-self:start;gap:12px;min-width:0;padding:14px;display:grid;position:relative;overflow:hidden}.account-card:before{content:"";background:var(--plan-color);height:3px;position:absolute;inset:0 0 auto}.account-card.plan-free{--plan-color:#94a3b8;--plan-soft:#94a3b81f;--plan-border:var(--line)}.account-card.plan-plus{--plan-color:#635bff;--plan-soft:#635bff1c;--plan-border:#635bff33}.account-card.plan-pro{--plan-color:#4f46e5;--plan-soft:#4f46e51c;--plan-border:#4f46e538}.account-card.plan-team{--plan-color:#0f766e;--plan-soft:#0f766e1c;--plan-border:#0f766e38}.account-card.plan-premium{--plan-color:#d97706;--plan-soft:#d977061f;--plan-border:#d9770657;box-shadow:0 10px 26px #b453091a, var(--shadow)}.account-card.plan-enterprise{--plan-color:#a16207;--plan-soft:#a1620724;--plan-border:#47556947;box-shadow:0 12px 28px #0f172a1a, var(--shadow)}.account-card.is-auth-invalid{--plan-color:#ef4444;--plan-soft:#ef444417;--plan-border:#ef444452;border-color:var(--plan-border);box-shadow:0 12px 28px #b91c1c14, var(--shadow);background:linear-gradient(#fef2f2e6,#fffffffa 46%),#fff}.account-card.is-auth-invalid:before{background:#ef4444;height:3px}.account-card.is-auth-invalid .usage-corner{color:#991b1b;background:linear-gradient(135deg,#fef2f2,#fee2e2);border-color:#ef444447;box-shadow:0 8px 18px #b91c1c1a,inset 0 1px #ffffffd1}.account-card.is-auth-invalid .avatar{color:#dc2626;background:#fff;border-color:#ef444475;box-shadow:0 0 0 3px #ef44441a}.account-card.is-auth-invalid .progress-bar{opacity:.72}.account-card.is-auth-invalid .usage-status-row,.account-card.is-auth-invalid .progress-track{background:#fef2f2ad}.account-card.is-auth-invalid .account-actions .btn-secondary:disabled{opacity:.86;color:#991b1b;background:#fff;border-color:#ef44442e}.account-head{justify-content:space-between;align-items:flex-start;gap:12px;padding-top:8px;display:flex}.account-title{flex:1;gap:6px;min-width:0;display:grid}.account-name{align-items:center;gap:8px;min-width:0;display:flex}.account-name strong{-webkit-line-clamp:2;text-overflow:ellipsis;white-space:normal;overflow-wrap:anywhere;-webkit-box-orient:vertical;min-width:0;font-size:13px;line-height:1.35;display:-webkit-box;overflow:hidden}.account-icon-btn{border:1px solid var(--line);width:24px;height:24px;color:var(--text-muted);background:#fff;border-radius:999px;flex:none;justify-content:center;align-items:center;padding:0;display:flex}.account-icon-btn:hover{color:var(--plan-color);border-color:var(--plan-color);background:var(--plan-soft)}.avatar{background:var(--panel-soft);border:1px solid var(--plan-color);width:24px;height:24px;box-shadow:0 0 0 3px var(--plan-soft);color:var(--plan-color);border-radius:999px;flex:none;place-items:center;font-size:11px;font-weight:700;display:grid}.badge-row{flex-wrap:wrap;gap:6px;display:flex}.account-select{border:1px solid var(--line);min-height:28px;color:var(--text-muted);cursor:pointer;-webkit-user-select:none;user-select:none;white-space:nowrap;background:#fff;border-radius:8px;align-items:center;gap:6px;margin-top:28px;padding:0 8px;font-size:12px;font-weight:600;display:inline-flex}.account-select input{width:14px;height:14px;margin:0}.badge{white-space:nowrap;border-radius:999px;justify-content:center;align-items:center;min-height:22px;padding:0 8px;font-size:11px;font-weight:600;display:inline-flex}.badge.brand{color:var(--plan-color);background:var(--plan-soft)}.usage-corner{color:#047857;letter-spacing:0;pointer-events:none;z-index:1;background:linear-gradient(135deg,#ecfdf5,#d1fae5);border:1px solid #10b98147;border-radius:999px;align-items:center;gap:5px;min-height:24px;padding:0 10px 0 8px;font-size:10px;font-weight:800;line-height:22px;display:inline-flex;position:absolute;top:10px;right:12px;box-shadow:0 8px 18px #10b9811f,inset 0 1px #ffffffd1}.usage-corner:before{content:"";background:currentColor;border-radius:999px;flex:none;width:6px;height:6px;box-shadow:0 0 0 3px #10b9811f}.usage-corner span{line-height:1}.usage-corner.codex-only{color:#1d4ed8;background:linear-gradient(135deg,#eff6ff,#dbeafe);border-color:#2563eb3d;box-shadow:0 8px 18px #2563eb1f,inset 0 1px #ffffffd1}.usage-corner.codex-only:before{box-shadow:0 0 0 3px #2563eb1f}.usage-corner.dual{color:#4f46e5;background:linear-gradient(135deg,#f5f3ff,#ede9fe);border-color:#635bff40;box-shadow:0 8px 18px #635bff21,inset 0 1px #ffffffd1}.usage-corner.dual:before{box-shadow:0 0 0 3px #635bff1f}.badge.green{color:#15803d;background:var(--green-soft)}.badge.orange{color:#b45309;background:var(--orange-soft)}.badge.red{color:#dc2626;background:var(--red-soft)}.badge.muted{color:var(--text-muted);background:#f1f5f9}.account-metrics{gap:10px;display:grid}.quota-row{gap:6px;display:grid}.quota-line{color:var(--text-soft);justify-content:space-between;align-items:center;gap:10px;font-size:11px;line-height:1.45;display:flex}.quota-line span{min-width:0}.quota-line strong{color:var(--text);flex-shrink:0;font-size:12px}.progress-track{background:#eef2f7;border-radius:999px;width:100%;height:5px;overflow:hidden}.progress-bar{border-radius:inherit;background:var(--brand);height:100%}.progress-bar.blue{background:var(--blue)}.progress-bar.orange{background:var(--orange)}.progress-bar.red{background:#f43f5e}.usage-status-row{background:var(--panel-soft);color:var(--text-muted);border-radius:10px;flex-wrap:nowrap;justify-content:space-between;align-items:center;gap:8px;padding:8px 10px;font-size:11px;line-height:1.4;display:flex}.usage-status{white-space:nowrap;align-items:center;gap:5px;min-width:0;font-weight:700;display:inline-flex}.usage-status svg{width:12px;height:12px;color:var(--text-muted);flex:none}.usage-dot{background:#cbd5e1;border-radius:999px;flex:none;width:6px;height:6px}.usage-dot.active{background:#22c55e;box-shadow:0 0 0 3px #22c55e1f}.usage-state-text{color:var(--text-muted);font-weight:700}.usage-status.is-active .usage-state-text{color:#15803d}.compact-meta-row{min-width:0;color:var(--text-muted);gap:8px;font-size:11px;line-height:1.45;display:grid}.compact-reset-list{flex-wrap:nowrap;align-items:center;gap:10px;min-width:0;display:flex}.compact-meta-item{flex:1 1 0;align-items:baseline;gap:5px;min-width:0;display:flex}.compact-meta-item label{color:var(--text-muted);white-space:nowrap;font-size:10px;line-height:1.4}.compact-meta-item strong{color:var(--text-soft);text-align:left;overflow-wrap:anywhere;font-size:11px;line-height:1.4}.compact-meta-actions{justify-content:center;align-items:center;gap:10px;margin-top:2px;display:flex}.compact-meta-actions:before,.compact-meta-actions:after{content:"";background:var(--line);flex:auto;min-width:18px;height:1px}.details-toggle{min-height:24px;color:var(--brand);white-space:nowrap;cursor:pointer;background:0 0;border:0;justify-content:center;align-items:center;gap:5px;padding:0 6px;font-size:11px;font-weight:700;display:inline-flex}.details-toggle:hover{color:#4338ca}.details-toggle svg{width:12px;height:12px;transition:transform .16s}.details-toggle.is-expanded svg{transform:rotate(180deg)}.meta-grid{border-top:1px solid var(--line);grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 12px;padding-top:10px;display:grid}.meta-item{gap:3px;min-width:0;display:grid}.meta-grid .service-row label{color:var(--text-muted);font-size:10px;line-height:1.4}.meta-grid .service-row strong,.meta-grid .service-row span,.meta-grid .service-row code{color:var(--text-soft);word-break:break-word;overflow-wrap:anywhere;font-size:11px;line-height:1.45}.account-actions{flex-wrap:wrap;gap:8px;margin-top:0;display:flex}.account-actions .btn-secondary,.account-actions .btn-danger{border-radius:10px;flex:120px;min-height:36px;padding:0 12px;font-size:12px}.account-actions .btn-secondary.is-current{opacity:1;color:#047857;cursor:default;background:linear-gradient(135deg,#f0fdf4,#dcfce7);border-color:#10b9815c;position:relative;box-shadow:inset 0 1px #fffc,0 6px 14px #10b98114}.account-actions .btn-secondary.is-current:before{content:"";background:#22c55e;border-radius:999px;flex:none;width:7px;height:7px;box-shadow:0 0 0 3px #22c55e1f}.account-actions .btn-secondary.is-current.codex{color:#1d4ed8;background:linear-gradient(135deg,#eff6ff,#dbeafe);border-color:#2563eb52;box-shadow:inset 0 1px #fffc,0 6px 14px #2563eb14}.account-actions .btn-secondary.is-current.codex:before{background:#3b82f6;box-shadow:0 0 0 3px #3b82f61f}.tester-card{gap:14px;display:grid}.tester-tabs,.tester-result-tabs{border-radius:14px;gap:4px;padding:6px;display:flex;overflow-x:auto}.tester-result-tabs{box-shadow:none;background:0 0;border:0;padding:0}.tab-btn{min-height:38px;color:var(--text-muted);white-space:nowrap;background:0 0;border:0;border-radius:10px;padding:0 14px;font-weight:800}.tab-btn.is-active{color:var(--brand);background:#fff;box-shadow:0 6px 16px #0f172a0f}.tester-textarea{min-height:184px}.pre{color:#e2e8f0;white-space:pre-wrap;overflow-wrap:anywhere;background:#0f172a;border-radius:12px;width:100%;min-height:170px;max-height:360px;margin:0;padding:16px;font-size:12px;line-height:1.6;overflow:auto}.preview-panel{min-height:170px}.preview-empty,.empty-state{border:1px dashed var(--line-strong);text-align:center;min-height:140px;color:var(--text-muted);background:#f8fafc;border-radius:14px;place-items:center;padding:20px;display:grid}.preview-grid{grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:12px;display:grid}.preview-card{gap:8px;margin:0;display:grid}.preview-card button{border:1px solid var(--line);background:#fff;border-radius:12px;padding:0;overflow:hidden}.preview-card img{aspect-ratio:1;object-fit:cover;width:100%;display:block}.preview-card figcaption{color:var(--text-muted);overflow-wrap:anywhere;font-size:12px}.preview-actions{flex-wrap:wrap;gap:8px;display:flex}.preview-actions a{border:1px solid var(--line);min-height:34px;color:var(--text-soft);border-radius:10px;justify-content:center;align-items:center;padding:0 12px;font-size:12px;font-weight:600;display:inline-flex}.service-card{border-radius:16px;gap:14px;padding:16px;display:grid}.service-head{justify-content:space-between;align-items:center;gap:12px;display:flex}.service-list{gap:10px;display:grid}.compact-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.service-row{min-width:0}.service-row label{color:var(--text-muted);margin-bottom:4px;font-size:12px;display:block}.service-row strong,.service-row code{color:var(--text);overflow-wrap:anywhere;font-size:13px;line-height:1.5}.meta-grid .service-row label{margin-bottom:0;font-size:10px;line-height:1.4}.meta-grid .service-row strong,.meta-grid .service-row code{color:var(--text-soft);font-size:11px;line-height:1.45}.status-dot{background:var(--green);border-radius:999px;width:8px;height:8px;box-shadow:0 0 0 4px #22c55e24}.status-dot.offline{background:var(--orange);box-shadow:0 0 0 4px #f59e0b24}.update-panel{background:#fffbebf0;border-color:#f59e0b40;justify-content:space-between;align-items:center;gap:16px;display:flex}.update-panel div{gap:5px;display:grid}.update-panel span{color:var(--text-muted);font-size:13px}.update-panel code{color:#92400e;overflow-wrap:anywhere;background:#fff;border-radius:10px;padding:9px 11px}.table-scroller{overflow-x:auto}table{border-collapse:collapse;width:100%;min-width:720px}th,td{border-bottom:1px solid var(--line);text-align:left;vertical-align:top;color:var(--text-soft);padding:12px 10px;font-size:13px}th{color:var(--text-muted);font-size:12px;font-weight:800}td code{color:var(--text)}.table-footer{color:var(--text-muted);margin-top:10px;font-size:12px}.modal-backdrop,.drawer-backdrop,.loading-cover{z-index:20;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background:#0f172a6b;place-items:center;padding:20px;display:grid;position:fixed;inset:0}.loading-cover{z-index:30;color:var(--text);background:#f8fafcc2;gap:12px}.modal-card{border:1px solid var(--line);width:min(760px,100%);max-height:min(760px,100vh - 40px);box-shadow:var(--shadow);background:#fff;border-radius:18px;overflow:auto}.modal-card.wide{width:min(980px,100%)}.modal-head{z-index:1;border-bottom:1px solid var(--line);background:#fffffff5;justify-content:space-between;align-items:center;gap:12px;padding:18px 20px;display:flex;position:sticky;top:0}.modal-head h3{margin:0}.modal-body{padding:20px}.modal-grid{gap:16px;display:grid}.modal-section{border:1px solid var(--line);background:#f8fafc;border-radius:14px;gap:12px;padding:16px;display:grid}.modal-section h4{margin:0}.modal-section p,.contact-notes span{color:var(--text-muted);margin:0;font-size:13px;line-height:1.6}.import-textarea{min-height:220px}.settings-drawer{width:min(520px,100%);height:100%;box-shadow:var(--shadow);background:#fff;border-radius:18px 0 0 18px;grid-template-rows:auto minmax(0,1fr) auto;justify-self:end;display:grid}.settings-drawer-head,.settings-drawer-footer{border-bottom:1px solid var(--line);justify-content:space-between;align-items:flex-start;gap:12px;padding:18px 20px;display:flex}.settings-drawer-footer{border-top:1px solid var(--line);border-bottom:0;align-items:center}.settings-drawer-head h3{margin:0}.settings-drawer-head p,.settings-drawer-footer p{color:var(--text-muted);margin:6px 0 0;font-size:13px;line-height:1.6}.settings-drawer-body{align-content:start;padding:20px;display:grid;overflow:auto}.settings-section{border-bottom:1px solid var(--line);gap:13px;margin-bottom:20px;padding:0 0 20px;display:grid}.settings-section:last-child{border-bottom:0;margin-bottom:0}.settings-section h4{margin:0}.switch-line{color:var(--text-soft);align-items:flex-start;gap:10px;font-size:13px;font-weight:700;line-height:1.5;display:flex}.contact-notes{gap:14px;display:grid}.contact-note,.contact-qr{gap:6px;display:grid}.contact-note{border:1px solid var(--line);background:var(--panel-soft);border-radius:16px;padding:14px 16px}.contact-note strong{font-size:14px;line-height:1.4}.contact-note span,.contact-note a,.contact-note code{color:var(--text-soft);word-break:break-word;font-size:13px;line-height:1.6}.contact-qr{border:1px solid var(--line);background:#fff;border-radius:20px;gap:10px;padding:14px}.contact-qr img{border:1px solid var(--line);background:var(--panel-soft);border-radius:16px;width:100%;max-width:420px;height:auto;display:block}.contact-qr span{color:var(--text-muted);font-size:12px;line-height:1.6}.image-preview-stage{background:#0f172a;border-radius:14px;place-items:center;min-height:320px;display:grid;overflow:hidden}.image-preview-stage img{max-width:100%;max-height:min(70vh,680px);display:block}.preview-modal-meta{color:var(--text-soft);word-break:break-word;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:12px;font-size:13px;line-height:1.7;display:flex}.spin{animation:.9s linear infinite spin}@keyframes spin{to{transform:rotate(360deg)}}@media (width<=1380px){.main-grid{grid-template-columns:1fr}.right-column{grid-template-columns:minmax(0,1fr)}.tester-card{order:-1}}@media (width<=1120px){.app-shell{grid-template-columns:1fr;padding:10px}.sidebar{max-height:none;position:relative;top:0}.nav{grid-template-columns:repeat(5,minmax(max-content,1fr));padding-bottom:2px;overflow-x:auto}.nav-item{justify-content:center}.sidebar-status{display:none}.topbar{grid-template-columns:1fr}.top-actions{justify-content:flex-start}}@media (width<=900px){.app-shell{gap:12px;padding:0}.sidebar,.card,.trend-card,.log-table-wrap,.update-panel{border-left:0;border-right:0;border-radius:0}.sidebar{padding:16px 12px}.brand span,.sidebar-status{display:none}.nav{grid-template-columns:none;grid-auto-columns:max-content;grid-auto-flow:column}.nav-item{width:auto}.main{gap:12px}.topbar{padding:0 12px}.page-title h1{font-size:25px}.top-actions{flex-wrap:nowrap;padding-bottom:4px;overflow-x:auto}.summary-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;padding:0 12px}.summary-card{min-height:122px;padding:14px}.summary-card strong{font-size:20px}.main-grid{gap:12px}.section-head,.update-panel,.settings-drawer-footer{display:grid}.section-actions,.button-row,.example-row{justify-content:flex-start}.filter-row{display:flex}.filter-row .control{flex:100%;min-width:0}.account-grid,.account-grid.profile-count-1,.account-grid.profile-count-2,.account-grid.profile-count-3,.account-grid.profile-count-many,.meta-grid{grid-template-columns:1fr}.account-card{border-radius:14px;padding:16px}.account-head{flex-direction:column;align-items:stretch}.account-select{width:max-content;margin-top:0}.compact-grid{grid-template-columns:1fr}.account-actions{display:flex}.tester-textarea{min-height:150px}.pre{max-height:320px}.modal-backdrop,.drawer-backdrop{padding:0}.modal-card,.settings-drawer{border-radius:0;width:100%;height:100%;max-height:none}.modal-body,.settings-drawer-body,.settings-drawer-head,.settings-drawer-footer{padding:16px}}@media (width<=460px){.summary-grid{grid-template-columns:1fr}.btn-primary,.btn-secondary,.btn-danger{min-height:40px;padding:0 12px}.card,.trend-card,.log-table-wrap{padding:16px 12px}}
|
|
Binary file
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>AI Zero Token</title>
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-BBXWfa-w.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-n7rmcV5d.css">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div id="root"></div>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
package/build/icon.icns
ADDED
|
Binary file
|
package/build/icon.ico
ADDED
|
Binary file
|
package/build/icon.png
ADDED
|
Binary file
|
|
@@ -168,7 +168,7 @@ async function loadNetworkProxySettings() {
|
|
|
168
168
|
}
|
|
169
169
|
async function requestText(init) {
|
|
170
170
|
const requestId = nextRequestId();
|
|
171
|
-
const proxy = await loadNetworkProxySettings();
|
|
171
|
+
const proxy = init.ignoreProxy ? void 0 : init.proxyOverride ?? await loadNetworkProxySettings();
|
|
172
172
|
const useCurlOnly = process.env.OAUTH_DEMO_USE_CURL === "1";
|
|
173
173
|
const useConfiguredProxy = !!proxy?.enabled && !!proxy.url.trim();
|
|
174
174
|
const timeoutMs = init.timeoutMs;
|
|
@@ -36,6 +36,24 @@ function parseOptionalText(value) {
|
|
|
36
36
|
const trimmed = value.trim();
|
|
37
37
|
return trimmed ? trimmed : void 0;
|
|
38
38
|
}
|
|
39
|
+
function parseUpstreamErrorBody(body) {
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(body);
|
|
42
|
+
const error = parsed.error;
|
|
43
|
+
if (!error || typeof error !== "object") {
|
|
44
|
+
return void 0;
|
|
45
|
+
}
|
|
46
|
+
const record = error;
|
|
47
|
+
return {
|
|
48
|
+
message: typeof record.message === "string" ? record.message : void 0,
|
|
49
|
+
type: typeof record.type === "string" ? record.type : void 0,
|
|
50
|
+
code: typeof record.code === "string" ? record.code : void 0,
|
|
51
|
+
param: typeof record.param === "string" || record.param === null ? record.param : void 0
|
|
52
|
+
};
|
|
53
|
+
} catch {
|
|
54
|
+
return void 0;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
39
57
|
function extractCodexQuotaSnapshot(headers, requestId) {
|
|
40
58
|
const activeLimit = parseOptionalText(headers["x-codex-active-limit"]);
|
|
41
59
|
const planType = parseOptionalText(headers["x-codex-plan-type"]);
|
|
@@ -280,8 +298,13 @@ async function askOpenAICodex(params) {
|
|
|
280
298
|
});
|
|
281
299
|
const quota = extractCodexQuotaSnapshot(response.headers, response.requestId);
|
|
282
300
|
if (response.status < 200 || response.status >= 300) {
|
|
301
|
+
const upstreamError = parseUpstreamErrorBody(response.body);
|
|
283
302
|
const error = new Error(`\u8C03\u7528 Responses API \u5931\u8D25: HTTP ${response.status} via ${response.transport} ${response.body}`);
|
|
284
303
|
error.quota = quota;
|
|
304
|
+
error.upstreamStatus = response.status;
|
|
305
|
+
error.upstreamErrorCode = upstreamError?.code;
|
|
306
|
+
error.upstreamErrorType = upstreamError?.type;
|
|
307
|
+
error.upstreamErrorMessage = upstreamError?.message;
|
|
285
308
|
throw error;
|
|
286
309
|
}
|
|
287
310
|
return {
|
|
@@ -54,6 +54,23 @@ function extractEmailFromPayload(payload) {
|
|
|
54
54
|
}
|
|
55
55
|
return void 0;
|
|
56
56
|
}
|
|
57
|
+
function parseUpstreamErrorBody(body) {
|
|
58
|
+
try {
|
|
59
|
+
const parsed = JSON.parse(body);
|
|
60
|
+
const error = parsed.error;
|
|
61
|
+
if (!error || typeof error !== "object") {
|
|
62
|
+
return void 0;
|
|
63
|
+
}
|
|
64
|
+
const record = error;
|
|
65
|
+
return {
|
|
66
|
+
message: typeof record.message === "string" ? record.message : void 0,
|
|
67
|
+
type: typeof record.type === "string" ? record.type : void 0,
|
|
68
|
+
code: typeof record.code === "string" ? record.code : void 0
|
|
69
|
+
};
|
|
70
|
+
} catch {
|
|
71
|
+
return void 0;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
57
74
|
function parseAuthorizationInput(value) {
|
|
58
75
|
const trimmed = value.trim();
|
|
59
76
|
if (!trimmed) {
|
|
@@ -143,7 +160,13 @@ async function refreshOpenAICodexToken(profile) {
|
|
|
143
160
|
}).toString()
|
|
144
161
|
});
|
|
145
162
|
if (response.status < 200 || response.status >= 300) {
|
|
146
|
-
|
|
163
|
+
const upstreamError = parseUpstreamErrorBody(response.body);
|
|
164
|
+
const error = new Error(`\u5237\u65B0 token \u5931\u8D25: HTTP ${response.status} via ${response.transport} ${response.body}`);
|
|
165
|
+
error.upstreamStatus = response.status;
|
|
166
|
+
error.upstreamErrorCode = upstreamError?.code;
|
|
167
|
+
error.upstreamErrorType = upstreamError?.type;
|
|
168
|
+
error.upstreamErrorMessage = upstreamError?.message;
|
|
169
|
+
throw error;
|
|
147
170
|
}
|
|
148
171
|
const json = JSON.parse(response.body);
|
|
149
172
|
if (!json.access_token || !json.refresh_token || typeof json.expires_in !== "number") {
|
|
@@ -50,9 +50,168 @@ class AuthService {
|
|
|
50
50
|
expiresAt: profile.expires,
|
|
51
51
|
accessTokenPreview: this.maskSecret(profile.access),
|
|
52
52
|
refreshTokenPreview: this.maskSecret(profile.refresh),
|
|
53
|
-
isActive: profile.profileId === activeProfileId
|
|
53
|
+
isActive: profile.profileId === activeProfileId,
|
|
54
|
+
authStatus: profile.authStatus
|
|
54
55
|
};
|
|
55
56
|
}
|
|
57
|
+
createOkAuthStatus() {
|
|
58
|
+
return {
|
|
59
|
+
state: "ok",
|
|
60
|
+
checkedAt: Date.now()
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
createAuthStatusFromError(error) {
|
|
64
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
65
|
+
const details = normalized;
|
|
66
|
+
const httpStatus = typeof details.upstreamStatus === "number" ? details.upstreamStatus : void 0;
|
|
67
|
+
const code = typeof details.upstreamErrorCode === "string" ? details.upstreamErrorCode : void 0;
|
|
68
|
+
const upstreamMessage = typeof details.upstreamErrorMessage === "string" ? details.upstreamErrorMessage : void 0;
|
|
69
|
+
const message = upstreamMessage || normalized.message;
|
|
70
|
+
const fingerprint = `${code || ""} ${message}`.toLowerCase();
|
|
71
|
+
if (code === "token_invalidated" || fingerprint.includes("token_invalidated") || fingerprint.includes("authentication token has been invalidated")) {
|
|
72
|
+
return {
|
|
73
|
+
state: "token_invalidated",
|
|
74
|
+
checkedAt: Date.now(),
|
|
75
|
+
message,
|
|
76
|
+
code: code || "token_invalidated",
|
|
77
|
+
httpStatus
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (httpStatus === 401 || httpStatus === 403 || /http\s+40[13]/i.test(message) || fingerprint.includes("\u5237\u65B0 token \u5931\u8D25")) {
|
|
81
|
+
return {
|
|
82
|
+
state: "auth_error",
|
|
83
|
+
checkedAt: Date.now(),
|
|
84
|
+
message,
|
|
85
|
+
code,
|
|
86
|
+
httpStatus
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return void 0;
|
|
90
|
+
}
|
|
91
|
+
getQuotaPercents(profile) {
|
|
92
|
+
const quota = profile.quota;
|
|
93
|
+
if (!quota) {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
return [quota.primaryUsedPercent, quota.secondaryUsedPercent].filter(
|
|
97
|
+
(value) => typeof value === "number" && Number.isFinite(value)
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
isQuotaExhausted(profile) {
|
|
101
|
+
const percents = this.getQuotaPercents(profile);
|
|
102
|
+
return percents.length > 0 && percents.some((value) => value >= 100);
|
|
103
|
+
}
|
|
104
|
+
hasKnownAvailableQuota(profile) {
|
|
105
|
+
if (profile.authStatus?.state === "token_invalidated" || profile.authStatus?.state === "auth_error") {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
const percents = this.getQuotaPercents(profile);
|
|
109
|
+
return percents.length > 0 && percents.every((value) => value < 100);
|
|
110
|
+
}
|
|
111
|
+
hasInvalidAuthStatus(profile) {
|
|
112
|
+
return profile.authStatus?.state === "token_invalidated" || profile.authStatus?.state === "auth_error";
|
|
113
|
+
}
|
|
114
|
+
getQuotaUsageScore(profile) {
|
|
115
|
+
const percents = this.getQuotaPercents(profile);
|
|
116
|
+
if (percents.length === 0) {
|
|
117
|
+
return 100;
|
|
118
|
+
}
|
|
119
|
+
return Math.max(...percents);
|
|
120
|
+
}
|
|
121
|
+
async applyProfileRuntimeUpdate(profileId, provider, updater, options) {
|
|
122
|
+
const updated = await updateProfile(profileId, (profile) => {
|
|
123
|
+
if (profile.provider !== provider) {
|
|
124
|
+
return profile;
|
|
125
|
+
}
|
|
126
|
+
return updater(profile);
|
|
127
|
+
});
|
|
128
|
+
if (!options?.checkAutoSwitch || options.skipAutoSwitch || !updated || updated.provider !== provider || !this.isQuotaExhausted(updated)) {
|
|
129
|
+
return updated;
|
|
130
|
+
}
|
|
131
|
+
const activeProfile = await this.getActiveProfile(provider);
|
|
132
|
+
if (activeProfile?.profileId === updated.profileId) {
|
|
133
|
+
await this.maybeAutoSwitchProfile(updated, provider);
|
|
134
|
+
}
|
|
135
|
+
return updated;
|
|
136
|
+
}
|
|
137
|
+
async refreshStoredProfile(profile, provider) {
|
|
138
|
+
try {
|
|
139
|
+
const refreshed = await refreshOpenAICodexToken(profile);
|
|
140
|
+
const merged = await this.applyProfileRuntimeUpdate(
|
|
141
|
+
profile.profileId,
|
|
142
|
+
provider,
|
|
143
|
+
(current) => ({
|
|
144
|
+
...refreshed,
|
|
145
|
+
email: refreshed.email ?? current.email,
|
|
146
|
+
quota: current.quota,
|
|
147
|
+
authStatus: this.createOkAuthStatus()
|
|
148
|
+
})
|
|
149
|
+
);
|
|
150
|
+
return this.toManagedProfile(merged ?? {
|
|
151
|
+
...refreshed,
|
|
152
|
+
quota: profile.quota,
|
|
153
|
+
authStatus: this.createOkAuthStatus()
|
|
154
|
+
});
|
|
155
|
+
} catch (error) {
|
|
156
|
+
await this.recordProfileRequestFailure(profile.profileId, error, void 0, provider, {
|
|
157
|
+
skipAutoSwitch: true
|
|
158
|
+
});
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async maybeAutoSwitchProfile(profile, provider) {
|
|
163
|
+
const settings = await this.configService.getSettings();
|
|
164
|
+
if (!settings.autoSwitch.enabled || !this.isQuotaExhausted(profile)) {
|
|
165
|
+
return profile;
|
|
166
|
+
}
|
|
167
|
+
const [profiles, codexStatus] = await Promise.all([
|
|
168
|
+
listProfiles(),
|
|
169
|
+
getCodexAuthStatus()
|
|
170
|
+
]);
|
|
171
|
+
const currentIndex = profiles.findIndex((item) => item.profileId === profile.profileId);
|
|
172
|
+
const codexAccountId = codexStatus.accountId;
|
|
173
|
+
const candidates = profiles.map((item, index) => ({
|
|
174
|
+
profile: item,
|
|
175
|
+
index,
|
|
176
|
+
distance: currentIndex >= 0 ? (index - currentIndex + profiles.length) % profiles.length : index + 1
|
|
177
|
+
})).filter((item) => item.profile.provider === provider && item.profile.profileId !== profile.profileId).filter((item) => this.hasKnownAvailableQuota(item.profile)).sort((left, right) => {
|
|
178
|
+
const leftCodexConflict = codexAccountId && left.profile.accountId === codexAccountId ? 1 : 0;
|
|
179
|
+
const rightCodexConflict = codexAccountId && right.profile.accountId === codexAccountId ? 1 : 0;
|
|
180
|
+
const codexDiff = leftCodexConflict - rightCodexConflict;
|
|
181
|
+
if (codexDiff !== 0) {
|
|
182
|
+
return codexDiff;
|
|
183
|
+
}
|
|
184
|
+
const distanceDiff = left.distance - right.distance;
|
|
185
|
+
if (distanceDiff !== 0) {
|
|
186
|
+
return distanceDiff;
|
|
187
|
+
}
|
|
188
|
+
const usageDiff = this.getQuotaUsageScore(left.profile) - this.getQuotaUsageScore(right.profile);
|
|
189
|
+
if (usageDiff !== 0) {
|
|
190
|
+
return usageDiff;
|
|
191
|
+
}
|
|
192
|
+
const leftCapturedAt = left.profile.quota?.capturedAt ?? 0;
|
|
193
|
+
const rightCapturedAt = right.profile.quota?.capturedAt ?? 0;
|
|
194
|
+
if (leftCapturedAt !== rightCapturedAt) {
|
|
195
|
+
return leftCapturedAt - rightCapturedAt;
|
|
196
|
+
}
|
|
197
|
+
return right.profile.expires - left.profile.expires;
|
|
198
|
+
}).map((item) => item.profile);
|
|
199
|
+
const nextProfile = candidates[0];
|
|
200
|
+
if (!nextProfile) {
|
|
201
|
+
return profile;
|
|
202
|
+
}
|
|
203
|
+
const activated = await setActiveProfile(nextProfile.profileId);
|
|
204
|
+
if (!activated) {
|
|
205
|
+
return profile;
|
|
206
|
+
}
|
|
207
|
+
console.info("[auth] auto switched active profile after quota exhaustion", {
|
|
208
|
+
provider,
|
|
209
|
+
fromProfileId: profile.profileId,
|
|
210
|
+
toProfileId: activated.profileId,
|
|
211
|
+
avoidedCodexAccount: Boolean(codexAccountId && activated.accountId !== codexAccountId)
|
|
212
|
+
});
|
|
213
|
+
return this.toManagedProfile(activated);
|
|
214
|
+
}
|
|
56
215
|
async login(provider) {
|
|
57
216
|
if (provider !== "openai-codex") {
|
|
58
217
|
throw new Error(`\u6682\u4E0D\u652F\u6301 provider: ${provider}`);
|
|
@@ -151,17 +310,16 @@ class AuthService {
|
|
|
151
310
|
}
|
|
152
311
|
await removeProfile(profileId);
|
|
153
312
|
}
|
|
154
|
-
async requireUsableProfile(provider = "openai-codex") {
|
|
155
|
-
const
|
|
313
|
+
async requireUsableProfile(provider = "openai-codex", options) {
|
|
314
|
+
const activeProfile = await this.getActiveProfile(provider);
|
|
315
|
+
const profile = activeProfile ? options?.skipAutoSwitch ? activeProfile : await this.maybeAutoSwitchProfile(activeProfile, provider) : null;
|
|
156
316
|
if (!profile) {
|
|
157
317
|
throw new Error(`\u8FD8\u6CA1\u6709\u767B\u5F55 ${provider}\u3002\u5148\u8FD0\u884C azt login`);
|
|
158
318
|
}
|
|
159
319
|
if (Date.now() < profile.expires) {
|
|
160
320
|
return profile;
|
|
161
321
|
}
|
|
162
|
-
|
|
163
|
-
await saveProfile(refreshed);
|
|
164
|
-
return this.toManagedProfile(refreshed);
|
|
322
|
+
return this.refreshStoredProfile(profile, provider);
|
|
165
323
|
}
|
|
166
324
|
async requireFreshProfileWithIdToken(profileId, provider = "openai-codex") {
|
|
167
325
|
const profiles = await listProfiles();
|
|
@@ -172,8 +330,7 @@ class AuthService {
|
|
|
172
330
|
if (profile.idToken && Date.now() < profile.expires) {
|
|
173
331
|
return this.toManagedProfile(profile);
|
|
174
332
|
}
|
|
175
|
-
const refreshed = await
|
|
176
|
-
await saveProfile(refreshed);
|
|
333
|
+
const refreshed = await this.refreshStoredProfile(profile, provider);
|
|
177
334
|
if (!refreshed.idToken) {
|
|
178
335
|
throw new Error("\u5237\u65B0 token \u6210\u529F\uFF0C\u4F46\u4E0A\u6E38\u6CA1\u6709\u8FD4\u56DE id_token\u3002");
|
|
179
336
|
}
|
|
@@ -185,7 +342,9 @@ class AuthService {
|
|
|
185
342
|
async syncActiveProfileQuota(provider = "openai-codex", options) {
|
|
186
343
|
let profile;
|
|
187
344
|
try {
|
|
188
|
-
profile = await this.requireUsableProfile(provider
|
|
345
|
+
profile = await this.requireUsableProfile(provider, {
|
|
346
|
+
skipAutoSwitch: options?.skipAutoSwitch
|
|
347
|
+
});
|
|
189
348
|
} catch (error) {
|
|
190
349
|
if (options?.suppressErrors) {
|
|
191
350
|
return;
|
|
@@ -193,9 +352,43 @@ class AuthService {
|
|
|
193
352
|
throw error;
|
|
194
353
|
}
|
|
195
354
|
const model = await this.configService.getDefaultModel(provider);
|
|
355
|
+
await this.syncQuotaForProfile(profile, model, provider, options);
|
|
356
|
+
}
|
|
357
|
+
async syncProfileQuota(profileId, provider = "openai-codex", options) {
|
|
358
|
+
const profiles = await listProfiles();
|
|
359
|
+
const profile = profiles.find((item) => item.provider === provider && item.profileId === profileId);
|
|
360
|
+
if (!profile) {
|
|
361
|
+
throw new Error(`\u6CA1\u6709\u627E\u5230\u8D26\u53F7: ${profileId}`);
|
|
362
|
+
}
|
|
363
|
+
const model = await this.configService.getDefaultModel(provider);
|
|
364
|
+
await this.syncQuotaForProfile(profile, model, provider, options);
|
|
365
|
+
}
|
|
366
|
+
async syncAllProfileQuotas(provider = "openai-codex", options) {
|
|
367
|
+
const [profiles, activeProfile, model] = await Promise.all([
|
|
368
|
+
listProfiles(),
|
|
369
|
+
this.getActiveProfile(provider),
|
|
370
|
+
this.configService.getDefaultModel(provider)
|
|
371
|
+
]);
|
|
372
|
+
const providerProfiles = profiles.filter((profile) => profile.provider === provider);
|
|
373
|
+
const skipped = providerProfiles.filter((profile) => this.hasInvalidAuthStatus(profile)).length;
|
|
374
|
+
const targets = providerProfiles.filter((profile) => !this.hasInvalidAuthStatus(profile)).sort((left, right) => Number(left.profileId === activeProfile?.profileId) - Number(right.profileId === activeProfile?.profileId));
|
|
375
|
+
const results = [];
|
|
376
|
+
for (const profile of targets) {
|
|
377
|
+
results.push(await this.syncQuotaForProfile(profile, model, provider, options));
|
|
378
|
+
}
|
|
379
|
+
const failed = results.filter((item) => !item.ok).length;
|
|
380
|
+
return {
|
|
381
|
+
total: providerProfiles.length,
|
|
382
|
+
synced: results.length - failed,
|
|
383
|
+
failed,
|
|
384
|
+
skipped
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
async syncQuotaForProfile(profile, model, provider, options) {
|
|
196
388
|
try {
|
|
389
|
+
const usableProfile = Date.now() < profile.expires ? this.toManagedProfile(profile) : await this.refreshStoredProfile(profile, provider);
|
|
197
390
|
const result = await askOpenAICodex({
|
|
198
|
-
profile,
|
|
391
|
+
profile: usableProfile,
|
|
199
392
|
model,
|
|
200
393
|
system: "Reply with OK only.",
|
|
201
394
|
prompt: "ping",
|
|
@@ -203,33 +396,83 @@ class AuthService {
|
|
|
203
396
|
text: { verbosity: "low" }
|
|
204
397
|
}
|
|
205
398
|
});
|
|
206
|
-
await this.
|
|
399
|
+
await this.recordProfileRequestSuccess(usableProfile.profileId, result.quota, provider, {
|
|
400
|
+
skipAutoSwitch: options?.skipAutoSwitch
|
|
401
|
+
});
|
|
402
|
+
return {
|
|
403
|
+
ok: true,
|
|
404
|
+
profileId: usableProfile.profileId
|
|
405
|
+
};
|
|
207
406
|
} catch (error) {
|
|
208
407
|
const quota = error.quota;
|
|
209
|
-
await this.
|
|
408
|
+
await this.recordProfileRequestFailure(profile.profileId, error, quota, provider, {
|
|
409
|
+
skipAutoSwitch: options?.skipAutoSwitch
|
|
410
|
+
});
|
|
210
411
|
if (!options?.suppressErrors) {
|
|
211
412
|
throw error;
|
|
212
413
|
}
|
|
213
|
-
console.warn("[auth] sync
|
|
414
|
+
console.warn("[auth] sync profile quota failed", {
|
|
214
415
|
provider,
|
|
215
416
|
profileId: profile.profileId,
|
|
216
417
|
error: error instanceof Error ? error.message : String(error)
|
|
217
418
|
});
|
|
419
|
+
return {
|
|
420
|
+
ok: false,
|
|
421
|
+
profileId: profile.profileId,
|
|
422
|
+
error: error instanceof Error ? error.message : String(error)
|
|
423
|
+
};
|
|
218
424
|
}
|
|
219
425
|
}
|
|
220
|
-
async
|
|
221
|
-
|
|
426
|
+
async recordProfileRequestSuccess(profileId, quota, provider = "openai-codex", options) {
|
|
427
|
+
await this.applyProfileRuntimeUpdate(
|
|
428
|
+
profileId,
|
|
429
|
+
provider,
|
|
430
|
+
(profile) => ({
|
|
431
|
+
...profile,
|
|
432
|
+
...quota ? { quota } : {},
|
|
433
|
+
authStatus: this.createOkAuthStatus()
|
|
434
|
+
}),
|
|
435
|
+
{
|
|
436
|
+
skipAutoSwitch: options?.skipAutoSwitch,
|
|
437
|
+
checkAutoSwitch: Boolean(quota)
|
|
438
|
+
}
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
async recordProfileRequestFailure(profileId, error, quota, provider = "openai-codex", options) {
|
|
442
|
+
const authStatus = this.createAuthStatusFromError(error);
|
|
443
|
+
if (!quota && !authStatus) {
|
|
222
444
|
return;
|
|
223
445
|
}
|
|
224
|
-
await
|
|
225
|
-
|
|
226
|
-
|
|
446
|
+
await this.applyProfileRuntimeUpdate(
|
|
447
|
+
profileId,
|
|
448
|
+
provider,
|
|
449
|
+
(profile) => ({
|
|
450
|
+
...profile,
|
|
451
|
+
...quota ? { quota } : {},
|
|
452
|
+
...authStatus ? { authStatus } : {}
|
|
453
|
+
}),
|
|
454
|
+
{
|
|
455
|
+
skipAutoSwitch: options?.skipAutoSwitch,
|
|
456
|
+
checkAutoSwitch: Boolean(quota)
|
|
227
457
|
}
|
|
228
|
-
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
async updateProfileQuota(profileId, quota, provider = "openai-codex", options) {
|
|
461
|
+
if (!quota) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
await this.applyProfileRuntimeUpdate(
|
|
465
|
+
profileId,
|
|
466
|
+
provider,
|
|
467
|
+
(profile) => ({
|
|
229
468
|
...profile,
|
|
230
469
|
quota
|
|
231
|
-
}
|
|
232
|
-
|
|
470
|
+
}),
|
|
471
|
+
{
|
|
472
|
+
skipAutoSwitch: options?.skipAutoSwitch,
|
|
473
|
+
checkAutoSwitch: true
|
|
474
|
+
}
|
|
475
|
+
);
|
|
233
476
|
}
|
|
234
477
|
async getStatus() {
|
|
235
478
|
const [profile, profiles] = await Promise.all([
|
|
@@ -18,7 +18,7 @@ class ChatService {
|
|
|
18
18
|
system: request.system,
|
|
19
19
|
bodyOverride: request.experimental?.codexBody
|
|
20
20
|
});
|
|
21
|
-
await this.deps.authService.
|
|
21
|
+
await this.deps.authService.recordProfileRequestSuccess(profile.profileId, result.quota, provider);
|
|
22
22
|
return {
|
|
23
23
|
provider,
|
|
24
24
|
model,
|
|
@@ -28,7 +28,7 @@ class ChatService {
|
|
|
28
28
|
};
|
|
29
29
|
} catch (error) {
|
|
30
30
|
const quota = error.quota;
|
|
31
|
-
await this.deps.authService.
|
|
31
|
+
await this.deps.authService.recordProfileRequestFailure(profile.profileId, error, quota, provider);
|
|
32
32
|
throw error;
|
|
33
33
|
}
|
|
34
34
|
}
|
|
@@ -42,8 +42,10 @@ class ConfigService {
|
|
|
42
42
|
return next;
|
|
43
43
|
}
|
|
44
44
|
async setNetworkProxy(params) {
|
|
45
|
-
const
|
|
46
|
-
const
|
|
45
|
+
const settings = await this.getSettings();
|
|
46
|
+
const requestedUrl = params.url?.trim() ?? "";
|
|
47
|
+
const url = requestedUrl || (!params.enabled ? settings.networkProxy.url : "");
|
|
48
|
+
const noProxy = params.noProxy?.trim() || settings.networkProxy.noProxy || "localhost,127.0.0.1,::1";
|
|
47
49
|
if (params.enabled) {
|
|
48
50
|
if (!url) {
|
|
49
51
|
throw new Error("\u542F\u7528\u4EE3\u7406\u65F6\u5FC5\u987B\u586B\u5199\u4EE3\u7406\u5730\u5740\u3002");
|
|
@@ -59,7 +61,6 @@ class ConfigService {
|
|
|
59
61
|
throw new Error("\u4EE3\u7406\u5730\u5740\u4EC5\u652F\u6301 http\u3001https\u3001socks4\u3001socks4a\u3001socks5 \u6216 socks5h\u3002");
|
|
60
62
|
}
|
|
61
63
|
}
|
|
62
|
-
const settings = await this.getSettings();
|
|
63
64
|
const next = {
|
|
64
65
|
...settings,
|
|
65
66
|
networkProxy: {
|
|
@@ -71,6 +72,17 @@ class ConfigService {
|
|
|
71
72
|
await saveSettings(next);
|
|
72
73
|
return next;
|
|
73
74
|
}
|
|
75
|
+
async setAutoSwitch(params) {
|
|
76
|
+
const settings = await this.getSettings();
|
|
77
|
+
const next = {
|
|
78
|
+
...settings,
|
|
79
|
+
autoSwitch: {
|
|
80
|
+
enabled: params.enabled
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
await saveSettings(next);
|
|
84
|
+
return next;
|
|
85
|
+
}
|
|
74
86
|
async getServerConfig() {
|
|
75
87
|
const settings = await this.getSettings();
|
|
76
88
|
return settings.server;
|
|
@@ -334,10 +334,10 @@ class ImageService {
|
|
|
334
334
|
include: ["reasoning.encrypted_content"]
|
|
335
335
|
}
|
|
336
336
|
});
|
|
337
|
-
await this.deps.authService.
|
|
337
|
+
await this.deps.authService.recordProfileRequestSuccess(profile.profileId, result.quota, "openai-codex");
|
|
338
338
|
} catch (error) {
|
|
339
339
|
const quota = error.quota;
|
|
340
|
-
await this.deps.authService.
|
|
340
|
+
await this.deps.authService.recordProfileRequestFailure(profile.profileId, error, quota, "openai-codex");
|
|
341
341
|
throw error;
|
|
342
342
|
}
|
|
343
343
|
const raw = isRecord(result.raw) ? result.raw : {};
|
|
@@ -56,19 +56,7 @@ class VersionService {
|
|
|
56
56
|
const manifest = await readPackageManifest();
|
|
57
57
|
const registryUrl = `https://registry.npmjs.org/${encodeURIComponent(manifest.name)}/latest`;
|
|
58
58
|
try {
|
|
59
|
-
const
|
|
60
|
-
method: "GET",
|
|
61
|
-
url: registryUrl,
|
|
62
|
-
timeoutMs: 5e3
|
|
63
|
-
});
|
|
64
|
-
if (response.status < 200 || response.status >= 300) {
|
|
65
|
-
throw new Error(`npm registry returned ${response.status}`);
|
|
66
|
-
}
|
|
67
|
-
const parsed = JSON.parse(response.body);
|
|
68
|
-
const latestVersion = typeof parsed.version === "string" && parsed.version ? parsed.version : void 0;
|
|
69
|
-
if (!latestVersion) {
|
|
70
|
-
throw new Error("npm registry did not return a version");
|
|
71
|
-
}
|
|
59
|
+
const latestVersion = await this.fetchNpmLatestVersion(registryUrl);
|
|
72
60
|
const needsUpdate = compareSemver(manifest.version, latestVersion) < 0;
|
|
73
61
|
return {
|
|
74
62
|
packageName: manifest.name,
|
|
@@ -91,6 +79,23 @@ class VersionService {
|
|
|
91
79
|
};
|
|
92
80
|
}
|
|
93
81
|
}
|
|
82
|
+
async fetchNpmLatestVersion(registryUrl) {
|
|
83
|
+
const response = await requestText({
|
|
84
|
+
method: "GET",
|
|
85
|
+
url: registryUrl,
|
|
86
|
+
timeoutMs: 5e3,
|
|
87
|
+
ignoreProxy: true
|
|
88
|
+
});
|
|
89
|
+
if (response.status < 200 || response.status >= 300) {
|
|
90
|
+
throw new Error(`npm registry returned ${response.status}`);
|
|
91
|
+
}
|
|
92
|
+
const parsed = JSON.parse(response.body);
|
|
93
|
+
const latestVersion = typeof parsed.version === "string" && parsed.version ? parsed.version : void 0;
|
|
94
|
+
if (!latestVersion) {
|
|
95
|
+
throw new Error("npm registry did not return a version");
|
|
96
|
+
}
|
|
97
|
+
return latestVersion;
|
|
98
|
+
}
|
|
94
99
|
}
|
|
95
100
|
export {
|
|
96
101
|
VersionService
|
|
@@ -15,6 +15,9 @@ function createDefaultSettings() {
|
|
|
15
15
|
url: "",
|
|
16
16
|
noProxy: "localhost,127.0.0.1,::1"
|
|
17
17
|
},
|
|
18
|
+
autoSwitch: {
|
|
19
|
+
enabled: false
|
|
20
|
+
},
|
|
18
21
|
server: {
|
|
19
22
|
host: "0.0.0.0",
|
|
20
23
|
port: 8787
|
|
@@ -36,6 +39,9 @@ async function loadSettings() {
|
|
|
36
39
|
url: parsed.networkProxy?.url ?? defaults.networkProxy.url,
|
|
37
40
|
noProxy: parsed.networkProxy?.noProxy ?? defaults.networkProxy.noProxy
|
|
38
41
|
},
|
|
42
|
+
autoSwitch: {
|
|
43
|
+
enabled: parsed.autoSwitch?.enabled ?? defaults.autoSwitch.enabled
|
|
44
|
+
},
|
|
39
45
|
server: {
|
|
40
46
|
host: parsed.server?.host ?? defaults.server.host,
|
|
41
47
|
port: parsed.server?.port ?? defaults.server.port
|