| <!DOCTYPE html> |
| <html lang="zh-CN"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title data-i18n="config.pageTitle">Grok2API - ้
็ฝฎ็ฎก็</title> |
| <link rel="icon" href="/favicon.ico?v={{APP_VERSION}}"> |
| <link href="https://cdn.jsdelivr.net/npm/geist@1.0.0/dist/fonts/geist-sans/style.css" rel="stylesheet"> |
| <link href="/static/css/app.css?v={{APP_VERSION}}" rel="stylesheet"> |
| <style> |
| body { background: #FAF9F5 } |
| |
| |
| .page-hd { display:flex; align-items:flex-start; justify-content:space-between; margin-bottom:20px } |
| .page-title{ font-size:22px; font-weight:700; line-height:1.2 } |
| .page-sub { font-size:13px; color:var(--fg-muted); margin-top:5px } |
| .page-actions { display:flex; gap:8px; align-items:center; flex-shrink:0 } |
| |
| .page-action-btn { |
| height:32px; padding:0 14px; border-radius:999px; |
| display:inline-flex; align-items:center; justify-content:center; gap:6px; |
| font-size:13px; font-weight:600; |
| border:1px solid #e6e6e6; background:#fafafa; color:#444; |
| } |
| .page-action-btn:hover { background:#f3f3f3; border-color:#dcdcdc } |
| .page-action-btn-primary { background:#111; border-color:#111; color:#fff } |
| .page-action-btn-primary:hover { background:#222; border-color:#222 } |
| .page-action-btn:disabled { opacity:.4; pointer-events:none } |
| |
| |
| .cfg-nav { |
| display:flex; |
| align-items:center; |
| gap:6px; |
| flex-wrap:wrap; |
| margin-bottom:16px; |
| padding:0; |
| } |
| .cfg-tab { |
| height:30px; |
| padding:0 12px; |
| border-radius:999px; |
| font-size:12px; |
| font-weight:500; |
| color:#8f8f8f; |
| background:#f5f5f5; |
| transition:color .15s, background .15s; |
| cursor:pointer; |
| } |
| .cfg-tab:hover { color:#555; background:#f1f1f1 } |
| .cfg-tab.active { |
| background:#111; |
| color:#fff; |
| font-weight:600; |
| } |
| .cfg-tab .dot { |
| display:none; |
| width:6px; |
| height:6px; |
| border-radius:50%; |
| background:currentColor; |
| margin-left:5px; |
| vertical-align:middle; |
| opacity:.45; |
| } |
| .cfg-tab.dirty .dot { display:inline-block } |
| |
| |
| .cfg-panel { display:none } |
| .cfg-panel.active { display:block } |
| |
| |
| .cfg-group { |
| background:#fff; |
| border:none; |
| border-radius:12px; |
| overflow:hidden; |
| margin-bottom:12px; |
| } |
| .cfg-group-title { |
| font-size:11px; |
| font-weight:700; |
| color:#8d8d8d; |
| text-transform:uppercase; |
| letter-spacing:.08em; |
| padding:12px 16px 6px; |
| background:#fff; |
| border-bottom:none; |
| } |
| .cfg-row { |
| display:grid; |
| grid-template-columns:minmax(0, 2fr) minmax(300px, 1fr); |
| gap:18px; |
| align-items:center; |
| padding:11px 16px; |
| border-bottom:1px solid #f5f5f5; |
| transition:background .15s; |
| } |
| .cfg-row:last-child { border-bottom:none } |
| .cfg-row:hover { background:#fcfcfc } |
| .cfg-row.modified { background:#fffaf0 } |
| .cfg-row.modified:hover { background:#fff6e7 } |
| .cfg-label-col { min-width:0; display:grid; gap:3px; align-content:start } |
| .cfg-label { |
| display:inline-flex; |
| align-items:center; |
| gap:6px; |
| font-size:12px; |
| font-weight:600; |
| color:#222; |
| line-height:1.35; |
| } |
| .cfg-desc { max-width:520px; font-size:11px; color:#9a9a9a; line-height:1.45 } |
| .cfg-help { |
| position:relative; |
| width:16px; |
| height:16px; |
| display:inline-flex; |
| align-items:center; |
| justify-content:center; |
| padding:0; |
| border-radius:50%; |
| border:1px solid #e6e6e6; |
| background:#fafafa; |
| color:#8d8d8d; |
| font-size:11px; |
| line-height:1; |
| font-weight:700; |
| cursor:help; |
| flex-shrink:0; |
| } |
| .cfg-help:hover, |
| .cfg-help:focus-visible { |
| color:#222; |
| border-color:#d6d6d6; |
| background:#fff; |
| outline:none; |
| } |
| .cfg-help-tip { |
| position:fixed; |
| box-sizing:border-box; |
| max-width:min(320px, calc(100vw - 24px)); |
| padding:9px 10px; |
| border-radius:8px; |
| background:#171717; |
| color:#fff; |
| font-size:12px; |
| font-weight:500; |
| line-height:1.5; |
| text-align:left; |
| white-space:pre-line; |
| overflow-wrap:anywhere; |
| box-shadow:0 8px 24px rgba(0,0,0,.16); |
| opacity:0; |
| pointer-events:none; |
| transform:translateY(3px); |
| transition:opacity .15s, transform .15s; |
| z-index:1000; |
| } |
| .cfg-help-tip.visible { |
| opacity:1; |
| transform:translateY(0); |
| } |
| .cfg-help-tip::before { |
| content:''; |
| position:absolute; |
| left:var(--arrow-left, 50%); |
| width:8px; |
| height:8px; |
| background:#171717; |
| transform:translateX(-50%) rotate(45deg); |
| } |
| .cfg-help-tip[data-placement="top"]::before { bottom:-4px } |
| .cfg-help-tip[data-placement="bottom"]::before { top:-4px } |
| .cfg-input-col { min-width:0; display:flex; align-items:center; justify-content:flex-start; gap:8px } |
| .cfg-input-col.is-bool { justify-content:flex-end } |
| .cfg-inline-action { |
| width:16px; |
| height:16px; |
| display:inline-flex; |
| align-items:center; |
| justify-content:center; |
| padding:0; |
| color:#9a9a9a; |
| border:0; |
| background:transparent; |
| cursor:pointer; |
| flex-shrink:0; |
| transition:color .15s, opacity .15s, transform .15s; |
| } |
| .cfg-inline-action:hover { color:#222; transform:translateY(-.5px) } |
| .cfg-inline-action:disabled { |
| opacity:.32; |
| cursor:not-allowed; |
| pointer-events:none; |
| } |
| .cfg-inline-action svg { width:14px; height:14px; display:block } |
| |
| |
| .cfg-text { |
| width:100%; |
| max-width:none; |
| height:32px; |
| padding:0 10px; |
| font-size:13px; |
| border-radius:8px; |
| border:1px solid #ececec; |
| background:#fbfbfb; |
| transition:border-color .15s, background .15s; |
| font-family: inherit; text-align:right; |
| } |
| .cfg-text:focus { border-color:#d7d7d7; background:#fff; outline:none; box-shadow:none } |
| .cfg-text.wide { max-width:none } |
| .cfg-textarea { |
| width:100%; |
| max-width:none; |
| min-height:72px; |
| padding:8px 10px; |
| font-size:12px; |
| border-radius:10px; |
| border:1px solid #ececec; |
| background:#fbfbfb; |
| resize:vertical; |
| transition:border-color .15s, background .15s; |
| font-family: 'Geist Mono', 'Courier New', monospace; line-height:1.5; text-align:right; |
| } |
| .cfg-textarea:focus { border-color:#d7d7d7; background:#fff; outline:none } |
| .cfg-number { |
| width:100%; |
| max-width:none; |
| height:32px; |
| padding:0 10px; |
| font-size:13px; |
| border-radius:8px; |
| border:1px solid #ececec; |
| background:#fbfbfb; |
| text-align:right; |
| } |
| .cfg-number:focus { border-color:#d7d7d7; background:#fff; outline:none } |
| .cfg-select { |
| width:100%; |
| height:32px; |
| padding:0 28px 0 10px; |
| font-size:13px; |
| border-radius:8px; |
| border:1px solid #ececec; |
| background:#fbfbfb; |
| appearance:none; background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24'%3E%3Cpath d='M6 9l6 6 6-6' stroke='%23666' stroke-width='2' fill='none'/%3E%3C/svg%3E"); |
| background-repeat:no-repeat; background-position:right 8px center; |
| cursor:pointer; text-align:right; |
| } |
| .cfg-select:focus { border-color:#d7d7d7; background:#fff; outline:none } |
| |
| |
| .toggle { |
| position:relative; display:inline-block; width:36px; height:20px; flex-shrink:0; |
| } |
| .toggle input { opacity:0; width:0; height:0 } |
| .toggle-track { |
| position:absolute; cursor:pointer; inset:0; |
| background:#ddd; border-radius:20px; transition:background .2s; |
| } |
| .toggle-track::before { |
| content:''; position:absolute; width:14px; height:14px; |
| left:3px; bottom:3px; background:#fff; border-radius:50%; |
| transition:transform .2s; box-shadow:0 1px 2px rgba(0,0,0,.15); |
| } |
| .toggle input:checked + .toggle-track { background:#111 } |
| .toggle input:checked + .toggle-track::before { transform:translateX(16px) } |
| |
| |
| .cfg-modified-dot { |
| display:inline-block; width:6px; height:6px; border-radius:50%; |
| background:#f59e0b; margin-left:6px; vertical-align:middle; |
| visibility:hidden; |
| } |
| .cfg-row.modified .cfg-modified-dot { visibility:visible } |
| |
| |
| .cfg-rand-btn { |
| width:30px; height:30px; border-radius:999px; |
| display:inline-flex; align-items:center; justify-content:center; |
| color:#6a6a6a; border:1px solid #ececec; background:#fafafa; |
| cursor:pointer; flex-shrink:0; transition:background .15s, border-color .15s; |
| } |
| .cfg-rand-btn:hover { background:#f3f3f3; border-color:#dddddd; color:#222 } |
| |
| |
| .save-bar { |
| position:fixed; |
| left:50%; |
| bottom:18px; |
| z-index:50; |
| width:min(calc(100% - 56px), 1280px); |
| transform:translate(-50%, calc(100% + 20px)); |
| background:rgba(255,255,255,.96); |
| border:1px solid #efefef; |
| border-radius:16px; |
| padding:12px 16px; |
| display:flex; |
| align-items:center; |
| justify-content:space-between; |
| gap:12px; |
| transition:transform .25s ease; |
| } |
| .save-bar.visible { transform:translate(-50%, 0) } |
| .save-bar-msg { font-size:13px; color:var(--fg-muted) } |
| .save-bar-actions { display:flex; gap:8px } |
| .save-bar-btn { |
| height:32px; |
| padding:0 14px; |
| border-radius:999px; |
| font-size:13px; |
| font-weight:600; |
| border:1px solid transparent; |
| cursor:pointer; |
| } |
| .save-bar-btn-cancel { border-color:#e6e6e6; background:#fafafa; color:#444 } |
| .save-bar-btn-cancel:hover { background:#f3f3f3; border-color:#dcdcdc } |
| .save-bar-btn-save { background:#111; color:#fff } |
| .save-bar-btn-save:hover { background:#222 } |
| |
| @media (max-width:640px) { |
| .cfg-row { grid-template-columns:1fr; gap:8px; align-items:flex-start } |
| .cfg-input-col { justify-content:flex-start } |
| .cfg-nav { gap:8px } |
| .save-bar { |
| width:calc(100% - 24px); |
| bottom:12px; |
| padding:12px; |
| flex-direction:column; |
| align-items:stretch; |
| } |
| .save-bar-actions { justify-content:flex-end } |
| } |
| </style> |
| </head> |
| <body> |
|
|
| |
| <div id="admin-header" data-active="/admin/config"></div> |
|
|
| |
| <main class="admin-main" style="padding-bottom:80px"> |
|
|
| <div class="page-hd"> |
| <div> |
| <div class="page-title" data-i18n="config.pageHeading">้
็ฝฎ็ฎก็</div> |
| <div class="page-sub" data-i18n="config.pageSubtitle">็ปไธ็ฎก็่ฟ่กๆถๅๆฐไธๆๅก่กไธบใไฟๅญๅๅณๆถ็ๆ๏ผ้จๅ้
็ฝฎ้้ๅฏๆๅกใ</div> |
| </div> |
| <div class="page-actions"> |
| <button id="btn-reset" onclick="doReset()" class="page-action-btn"> |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12a9 9 0 1 0 3-6.708"/><path d="M3 4v5h5"/></svg> |
| <span data-i18n="config.resetShort">้็ฝฎ</span> |
| </button> |
| <button id="btn-save" onclick="doSave()" class="page-action-btn page-action-btn-primary" disabled> |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m5 12 4.2 4.2L19 6.5"/></svg> |
| <span data-i18n="config.saveShort">ไฟๅญ</span> |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div class="cfg-nav" id="cfg-nav"></div> |
|
|
| |
| <div id="cfg-panels"></div> |
|
|
| </main> |
|
|
| |
| <div class="save-bar" id="save-bar"> |
| <div class="save-bar-msg" id="save-bar-msg" data-i18n="config.unsavedDefault">ๅญๅจๆชไฟๅญ็ๆดๆน</div> |
| <div class="save-bar-actions"> |
| <button class="save-bar-btn save-bar-btn-cancel" onclick="doReset()" data-i18n="config.discardShort">ๆค้</button> |
| <button class="save-bar-btn save-bar-btn-save" onclick="doSave()" data-i18n="config.saveBarShort">ไฟๅญ</button> |
| </div> |
| </div> |
|
|
| <script src="/static/js/i18n.js?v={{APP_VERSION}}"></script> |
| <script src="/static/js/auth.js?v={{APP_VERSION}}"></script> |
| <script src="/static/js/admin-header.js?v={{APP_VERSION}}"></script> |
| <script src="/static/js/toast.js?v={{APP_VERSION}}"></script> |
| <script src="/static/js/footer.js?v={{APP_VERSION}}"></script> |
| <script> |
| |
| |
| |
| |
| const SCHEMA_DEF = [ |
| { |
| id: 'system', label: '็ณป็ป้
็ฝฎ', labelKey: 'config.schema.tabs.system', |
| groups: [ |
| { |
| title: 'ๅบ็ก่ฎฟ้ฎๆงๅถ', titleKey: 'config.schema.groups.baseAccess', |
| section: 'app', |
| fields: [ |
| { key: 'app_url', label: 'APP ่ฎฟ้ฎๅฐๅ', labelKey: 'config.schema.fields.appUrl.label', type: 'text', desc: 'ๅบ็จๅฏนๅคๆไพๆๅก็ๆ น URL๏ผๆ ผๅผๅฆ https://your-domain.comใๆฌๅฐไปฃ็ๆจกๅผๅฟ
้กป้
็ฝฎใ', descKey: 'config.schema.fields.appUrl.desc', wide: true }, |
| { key: 'app_key', label: 'APP ่ฎฟ้ฎๅฏ็ ', labelKey: 'config.schema.fields.appKey.label', type: 'password', desc: '็ฎก็ๅๅฐ้ดๆๅฏ็ ใไฟฎๆนๅๅทฒๆ Session ๅฐ็ซๅณๅคฑๆ๏ผ้้ๆฐ็ปๅฝใ', descKey: 'config.schema.fields.appKey.desc', rand: true }, |
| { key: 'api_key', label: 'API ่ฐ็จๅฏ้ฅ', labelKey: 'config.schema.fields.apiKey.label', type: 'text', desc: 'OpenAI ๅ
ผๅฎน API ็้ดๆๅฏ้ฅใๅคไธชๅผ่ฏทไฝฟ็จ่ฑๆ้ๅทๅ้๏ผ็็ฉบๅ็ฆ็จ้ดๆใ', descKey: 'config.schema.fields.apiKey.desc', rand: true }, |
| { key: 'webui_enabled', label: 'ๅฏ็จ WebUI', labelKey: 'config.schema.fields.webuiEnabled.label', type: 'bool', desc: 'ๅผๆพ WebUI ๅ่ฝๅ
ฅๅฃใๅ
ณ้ญๅ็ธๅ
ณ่ทฏ็ฑๅฐ่ฟๅ 404ใ', descKey: 'config.schema.fields.webuiEnabled.desc' }, |
| { key: 'webui_key', label: 'WebUI ่ฎฟ้ฎๅฏ็ ', labelKey: 'config.schema.fields.webuiKey.label', type: 'password', desc: 'WebUI ็่ฎฟ้ฎ้ดๆๅฏ็ ใ้
็ฝฎๅ่ฎฟ้ฎๆถๅฟ
้กปๆไพใ', descKey: 'config.schema.fields.webuiKey.desc', rand: true, showIf: 'app.webui_enabled' }, |
| ] |
| }, |
| { |
| title: 'ๆฅๅฟ้
็ฝฎ', titleKey: 'config.schema.groups.logging', |
| section: 'logging', |
| fields: [ |
| { |
| key: 'file_level', |
| label: 'ๆไปถๆฅๅฟ็ญ็บง', |
| labelKey: 'config.schema.fields.fileLogLevel.label', |
| type: 'select', |
| options: [ |
| { value: 'DEBUG', label: 'DEBUG' }, |
| { value: 'INFO', label: 'INFO' }, |
| { value: 'WARNING', label: 'WARNING' }, |
| { value: 'ERROR', label: 'ERROR' }, |
| ], |
| desc: 'ๆฅๅฟๆไปถๅๅ
ฅ็ๆไฝ็ญ็บง๏ผๅฏ็ฌ็ซไบๆงๅถๅฐ้
็ฝฎใ้ป่ฎค INFOใ', |
| descKey: 'config.schema.fields.fileLogLevel.desc', |
| }, |
| { |
| key: 'max_files', |
| label: 'ๆฅๅฟไฟ็ๅคฉๆฐ', |
| labelKey: 'config.schema.fields.logMaxFiles.label', |
| type: 'number', |
| desc: 'ๆๅคฉ่ฝฎ่ฝฌๆถๆๅคไฟ็็ๆฅๅฟๆไปถๆฐ้ใ้ป่ฎค 7ใ', |
| descKey: 'config.schema.fields.logMaxFiles.desc', |
| }, |
| ] |
| }, |
| ] |
| }, |
| { |
| id: 'features', label: 'ๅบ็จๅ่ฝ', labelKey: 'config.schema.tabs.features', |
| groups: [ |
| { |
| title: 'ๅบๆฌๅ่ฝ่ฎพ็ฝฎ', titleKey: 'config.schema.groups.features', |
| section: 'features', |
| fields: [ |
| { key: 'temporary', label: 'ไธดๆถไผ่ฏ', labelKey: 'config.schema.fields.temporary.label', type: 'bool', desc: 'ๅฏ็จๅ่ฏทๆฑไธไผๅๅ
ฅ Grok ไผ่ฏๅๅฒ๏ผ็ญๅไบๆ ็ๅฏน่ฏใ', descKey: 'config.schema.fields.temporary.desc' }, |
| { key: 'memory', label: 'ๅผๅฏไผ่ฏ่ฎฐๅฟ', labelKey: 'config.schema.fields.memory.label', type: 'bool', desc: 'ๅฏ็จ Grok Memory๏ผๅ
่ฎธๆจกๅ่ทจไผ่ฏ่ฎฐๅฟ็จๆทๅๅฅฝไธไธไธๆใ', descKey: 'config.schema.fields.memory.desc' }, |
| { key: 'stream', label: '้ป่ฎคๆตๅผ่พๅบ', labelKey: 'config.schema.fields.stream.label', type: 'bool', desc: '่ฏทๆฑๆชๆพๅผๆๅฎ stream ๅๆฐๆถๆ้็จ็้ป่ฎคๅผใ', descKey: 'config.schema.fields.stream.desc' }, |
| { key: 'thinking', label: '้ป่ฎคๆ่่พๅบ', labelKey: 'config.schema.fields.thinking.label', type: 'bool', desc: '่ฏทๆฑๆชๆพๅผๆๅฎ thinking ๅๆฐๆถๆ้็จ็้ป่ฎคๅผใๅฏ็จๅๅฐๅจ reasoning_content ๅญๆฎต่ฟๅๆ่่ฟ็จใ', descKey: 'config.schema.fields.thinking.desc' }, |
| { key: 'auto_chat_mode_fallback', label: 'AUTO ่ๅคฉๆจกๅผๅ้', labelKey: 'config.schema.fields.autoChatModeFallback.label', type: 'bool', desc: 'AUTO ่ๅคฉๆจกๅๅจ auto ้ขๅบฆไธๅฏ็จๆถ๏ผ่ชๅจๅฐ่ฏ fast ๅ expert ่ๅคฉ้ขๅบฆ็ชๅฃใ', descKey: 'config.schema.fields.autoChatModeFallback.desc' }, |
| { key: 'thinking_summary', label: 'ๆ่็ฒพ็ฎ่พๅบ', labelKey: 'config.schema.fields.thinkingSummary.label', type: 'bool', desc: 'ๅฏ็จๅๅฐๆ่่ฟ็จๆ็ผไธบ็ปๆๅๆ่ฆใๅ
ณ้ญๆถ่พๅบๅฎๆด็ๅๅงๆจ็่ฟ็จ๏ผๆฏๆๅค Agent ๆจกๅ็ๅไฝ่ฏฆๆ
ไธๅทฅๅ
ท่ฐ็จๅฑ็คบใ', descKey: 'config.schema.fields.thinkingSummary.desc' }, |
| { key: 'dynamic_statsig', label: 'ๅจๆ Statsig', labelKey: 'config.schema.fields.dynamicStatsig.label', type: 'bool', desc: 'ไธบๆฏๆฌก่ฏทๆฑๅจๆ็ๆ Statsig ่ฎพๅคๆ็บน๏ผไปฅ้ไฝ้ฃๆงๆฆๆชๆฆ็ใ', descKey: 'config.schema.fields.dynamicStatsig.desc' }, |
| { key: 'enable_nsfw', label: 'ๅ
่ฎธ NSFW ็ๆ', labelKey: 'config.schema.fields.enableNsfw.label', type: 'bool', desc: 'ๅ
่ฎธๅพๅ็ๆๆฅๅฃ็ป่ฟ NSFW ๅ
ๅฎน่ฟๆปคใ', descKey: 'config.schema.fields.enableNsfw.desc' }, |
| { key: 'show_search_sources', label: 'ๆญฃๆ่ฟฝๅ ไฟกๆบ', labelKey: 'config.schema.fields.showSearchSources.label', type: 'bool', desc: 'ๆ็ดขไฟกๆบๅง็ปไปฅ search_sources ๅญๆฎต่พๅบใๆญค้้กนๆงๅถๆฏๅฆๅๆถๅจๆญฃๆๆซๅฐพ่ฟฝๅ ## Sources ๆฎต่ฝ๏ผๅ
ผๅฎนๆๆฌ่งฃๆๅฎขๆท็ซฏ๏ผใ', descKey: 'config.schema.fields.showSearchSources.desc' }, |
| { key: 'custom_instruction', label: 'ๅ
จๅฑ้ๅ ๆไปค', labelKey: 'config.schema.fields.customInstruction.label', type: 'textarea', desc: 'ไธบๆฏๆฌก่ฏทๆฑๆณจๅ
ฅ็ปไธ็ system ๆถๆฏ๏ผ็จไบ็บฆๆๆจกๅ่กไธบๆๅบๅฎ่ง่ฒ่ฎพๅฎใ', descKey: 'config.schema.fields.customInstruction.desc' }, |
| ] |
| }, |
| { |
| title: 'ๅชไฝ่ฟๅๆ ผๅผ', titleKey: 'config.schema.groups.mediaFormat', |
| section: 'features', |
| fields: [ |
| { |
| key: 'image_format', |
| label: 'ๅพ็่ฟๅๆ ผๅผ', |
| labelKey: 'config.schema.fields.imageFormat.label', |
| type: 'select', |
| options: [ |
| { value: 'grok_url', label: 'URL๏ผGrok ๅ็๏ผ', labelKey: 'config.schema.options.imageFormat.grokUrl' }, |
| { value: 'local_url', label: 'URL๏ผๆฌๅฐไปฃ็๏ผ', labelKey: 'config.schema.options.imageFormat.localUrl', disabledWhen: 'no_app_url', disabledTip: '่ฏทๅ
ๅกซๅ APP ่ฎฟ้ฎๅฐๅ', disabledTipKey: 'config.disabledTip.appUrlRequired' }, |
| { value: 'grok_md', label: 'Markdown๏ผGrok ๅ็๏ผ', labelKey: 'config.schema.options.imageFormat.grokMarkdown' }, |
| { value: 'local_md', label: 'Markdown๏ผๆฌๅฐไปฃ็๏ผ', labelKey: 'config.schema.options.imageFormat.localMarkdown', disabledWhen: 'no_app_url', disabledTip: '่ฏทๅ
ๅกซๅ APP ่ฎฟ้ฎๅฐๅ', disabledTipKey: 'config.disabledTip.appUrlRequired' }, |
| { value: 'base64', label: 'Base64๏ผๅ
ๅต๏ผ', labelKey: 'config.schema.options.imageFormat.base64' }, |
| ], |
| desc: 'grok_url / grok_md ้ป่ฎค็ดๆฅ่ฟๅ Grok CDN ๅฐๅ๏ผlocal_* ๆจกๅผไผๅ
ไธ่ฝฝๅฐๆๅก็ซฏ๏ผๅ้่ฟๆฌๅฐ URL ไปฃ็ๅๅ๏ผbase64 ไผไปฅๅ
ๅต Data URI ่ฟๅใๅผๅฏ Imagine Public ๅพ็ไปฃ็ๅ๏ผWebSocket ่ฟๅ็ imagine-public ๅพ็ไนไผๆฌๅฐไปฃ็ใ', |
| descKey: 'config.schema.fields.imageFormat.desc', |
| }, |
| { |
| key: 'imagine_public_image_proxy', |
| label: 'Imagine Public ๅพ็ไปฃ็', |
| labelKey: 'config.schema.fields.imaginePublicImageProxy.label', |
| type: 'bool', |
| desc: 'ๅผๅฏๅๅฐ WebSocket ่ฟๅ็ imagine-public ๅพ็ไธ่ฝฝๅฐๆๅก็ซฏ๏ผๅ้่ฟๆฌๅฐ URL ไปฃ็ๅๅ๏ผๅ
ณ้ญๆถไฟๆๅ
ฌๅผๅพ็็ด่ฟใ', |
| descKey: 'config.schema.fields.imaginePublicImageProxy.desc', |
| }, |
| { |
| key: 'video_format', |
| label: '่ง้ข่ฟๅๆ ผๅผ', |
| labelKey: 'config.schema.fields.videoFormat.label', |
| type: 'select', |
| options: [ |
| { value: 'grok_url', label: 'URL๏ผGrok ๅ็๏ผ', labelKey: 'config.schema.options.videoFormat.grokUrl' }, |
| { value: 'local_url', label: 'URL๏ผๆฌๅฐไปฃ็๏ผ', labelKey: 'config.schema.options.videoFormat.localUrl', disabledWhen: 'no_app_url', disabledTip: '่ฏทๅ
ๅกซๅ APP ่ฎฟ้ฎๅฐๅ', disabledTipKey: 'config.disabledTip.appUrlRequired' }, |
| { value: 'grok_html', label: 'HTML๏ผGrok ๅ็๏ผ', labelKey: 'config.schema.options.videoFormat.grokHtml' }, |
| { value: 'local_html', label: 'HTML๏ผๆฌๅฐไปฃ็๏ผ', labelKey: 'config.schema.options.videoFormat.localHtml', disabledWhen: 'no_app_url', disabledTip: '่ฏทๅ
ๅกซๅ APP ่ฎฟ้ฎๅฐๅ', disabledTipKey: 'config.disabledTip.appUrlRequired' }, |
| ], |
| desc: 'local_* ๆจกๅผไผๅ
ๅฐ่ง้ขไธ่ฝฝ่ณๆๅก็ซฏ๏ผๅ้่ฟๆฌๅฐ URL ไปฃ็ๅๅ๏ผ่ฏท็กฎไฟๅทฒ้
็ฝฎ APP ่ฎฟ้ฎๅฐๅ๏ผๅนถ่ฏไผฐๅบ็ซๅธฆๅฎฝๆถ่ใ', |
| descKey: 'config.schema.fields.videoFormat.desc', |
| }, |
| ] |
| }, |
| ] |
| }, |
| { |
| id: 'schedule', label: 'ไปปๅก่ฐๅบฆ', labelKey: 'config.schema.tabs.schedule', |
| groups: [ |
| { |
| title: '้
้ขๅทๆฐ่ฐๅบฆ', titleKey: 'config.schema.groups.refreshSchedule', |
| section: 'account.refresh', |
| fields: [ |
| { key: 'enabled', label: 'ๅฏ็จ้
้ขๅทๆฐ', labelKey: 'config.schema.fields.refreshEnabled.label', type: 'bool', desc: 'ๅผๅฏๅ่ชๅจ่ฟๅ
ฅ้
้ขๅทๆฐๆจกๅผ๏ผๅ
ณ้ญๅ่ชๅจ่ฟๅ
ฅ่ชๅจ้่ฏๆจกๅผใ', descKey: 'config.schema.fields.refreshEnabled.desc', help: 'ๅผๅฏ๏ผ้
้ขๅทๆฐๆจกๅผ๏ผscheduler ๅจๆๅๆญฅ็ๅฎ้
้ข๏ผ้ๅทๆ่ฏๅใ\nๅ
ณ้ญ๏ผ่ชๅจ้่ฏๆจกๅผ๏ผไธไธปๅจๆขๆต upstream๏ผ้ๅท้ๆบ๏ผๅบ้่ชๅจๆขๅท้่ฏๆๅค 5 ๆฌกใ\nๅปบ่ฎฎ๏ผไธ็บงไปฅไธ่ดฆๅทๅ
ณ้ญ๏ผ้ฟๅ
ไธปๅจๆขๆต่งฆๅ upstream 429ใ', helpKey: 'config.schema.fields.refreshEnabled.help' }, |
| { key: 'basic_interval_sec', label: 'Basic ๅจๆ๏ผ็ง๏ผ', labelKey: 'config.schema.fields.basicInterval.label', type: 'number', desc: 'basic ๅทๆฑ ๅ
ฑ็จๅจๆ๏ผquota ๆจกๅผไธ็จไบๅๅฐๅทๆฐ๏ผrandom ๆจกๅผไธ็จไบ 429 ๅทๅดใ้ป่ฎค 86400sใ', descKey: 'config.schema.fields.basicInterval.desc' }, |
| { key: 'super_interval_sec', label: 'Super ๅจๆ๏ผ็ง๏ผ', labelKey: 'config.schema.fields.superInterval.label', type: 'number', desc: 'super ๅทๆฑ ๅ
ฑ็จๅจๆ๏ผquota ๆจกๅผไธ็จไบๅๅฐๅทๆฐ๏ผrandom ๆจกๅผไธ็จไบ 429 ๅทๅดใ้ป่ฎค 7200sใ', descKey: 'config.schema.fields.superInterval.desc' }, |
| { key: 'heavy_interval_sec', label: 'Heavy ๅจๆ๏ผ็ง๏ผ', labelKey: 'config.schema.fields.heavyInterval.label', type: 'number', desc: 'heavy ๅทๆฑ ๅ
ฑ็จๅจๆ๏ผquota ๆจกๅผไธ็จไบๅๅฐๅทๆฐ๏ผrandom ๆจกๅผไธ็จไบ 429 ๅทๅดใ้ป่ฎค 7200sใ', descKey: 'config.schema.fields.heavyInterval.desc' }, |
| { key: 'on_demand_min_interval_sec', label: 'ๆ้ๅทๆฐ้ด้๏ผ็ง๏ผ', labelKey: 'config.schema.fields.onDemandMinInterval.label', type: 'number', desc: '่ฏทๆฑ้พ่ทฏ่งฆๅๅทๆฐ๏ผไพๅฆๆถๅฐ 429๏ผๆถ็่ๆต้ด้ใN ็งๅ
้ๅค่งฆๅๅชๆง่กไธๆฌก๏ผ้ฟๅ
ๆน้ๆ็้
้ขๆฅๅฃใ', descKey: 'config.schema.fields.onDemandMinInterval.desc' }, |
| { key: 'usage_concurrency', label: 'ๅทๆฐๅนถๅๆฐ', labelKey: 'config.schema.fields.usageConcurrency.label', type: 'number', desc: 'ๅนถๅ่ฐ็จ usage ๆฅๅฃๅทๆฐ้ขๅบฆๆถๅ
่ฎธ็ๆๅคงๅนถ่กๆฐใ', descKey: 'config.schema.fields.usageConcurrency.desc' }, |
| ] |
| }, |
| { |
| title: '้ๅทๅนถๅ้ๅถ', titleKey: 'config.schema.groups.selection', |
| section: 'account.selection', |
| fields: [ |
| { key: 'max_inflight', label: 'ๅๅทๅนถๅไธ้', labelKey: 'config.schema.fields.maxInflight.label', type: 'number', desc: 'ๅไธช่ดฆๅทๅๆถๅจๆง่ก็่ฏทๆฑไธ้๏ผ่ถ
่ฟๅ้ๅท่ทฏๅพ่ทณ่ฟ่ฏฅๅท๏ผไธค็งๆจกๅผๅ
ฑ็จ๏ผใ', descKey: 'config.schema.fields.maxInflight.desc' }, |
| ] |
| }, |
| { |
| title: 'ๆน้ๆไฝๅนถๅ', titleKey: 'config.schema.groups.batchConcurrency', |
| section: 'batch', |
| fields: [ |
| { key: 'nsfw_concurrency', label: 'ๅผๅฏ NSFW ๅนถๅๆฐ', labelKey: 'config.schema.fields.nsfwConcurrency.label', type: 'number', desc: 'ๆน้ๅผๅฏ NSFW ๆถ็ token ็บงๅนถๅๆฐ๏ผๆฏไธช token ้็ปๅไธๆญฅ็ฝ็ป่ฏทๆฑใ', descKey: 'config.schema.fields.nsfwConcurrency.desc' }, |
| { key: 'refresh_concurrency', label: 'ๅทๆฐ Usage ๅนถๅๆฐ', labelKey: 'config.schema.fields.refreshConcurrency.label', type: 'number', desc: 'ๆน้ๅทๆฐ่ดฆๆท้ขๅบฆๆถ็ token ็บงๅนถๅๆฐใ', descKey: 'config.schema.fields.refreshConcurrency.desc' }, |
| { key: 'asset_upload_concurrency', label: 'ไธไผ Asset ๅนถๅๆฐ', labelKey: 'config.schema.fields.assetUploadConcurrency.label', type: 'number', desc: 'API ๆฅๆถๆไปถ้ไปถๅ่งฆๅ็ Asset ไธไผ ไปปๅก๏ผๅ
จๅฑๆๅคงๅนถๅๆฐ๏ผ่ทจๆๆ่ฏทๆฑๅ
ฑไบซใ', descKey: 'config.schema.fields.assetUploadConcurrency.desc' }, |
| { key: 'asset_list_concurrency', label: 'ๆฅ่ฏข Asset ๅนถๅๆฐ', labelKey: 'config.schema.fields.assetListConcurrency.label', type: 'number', desc: 'list_assets ๆฅๅฃ็ๅ
จๅฑๆๅคงๅนถๅๆฐ๏ผ่ทจๆๆ่ฏทๆฑๅ
ฑไบซใ', descKey: 'config.schema.fields.assetListConcurrency.desc' }, |
| { key: 'asset_delete_concurrency', label: 'ๅ ้ค Asset ๅนถๅๆฐ', labelKey: 'config.schema.fields.assetDeleteConcurrency.label', type: 'number', desc: 'delete_asset ๆฅๅฃ็ๅ
จๅฑๆๅคงๅนถๅๆฐ๏ผๅๆถไฝไธบ็ฎก็ๅๅฐๆน้ๆธ
็็ token ็บง้ป่ฎคๅนถๅๅผใ', descKey: 'config.schema.fields.assetDeleteConcurrency.desc' }, |
| ] |
| }, |
| ] |
| }, |
| { |
| id: 'proxy', label: '็ฝ็ปไปฃ็', labelKey: 'config.schema.tabs.proxy', |
| groups: [ |
| { |
| title: 'ๅบ็ซไปฃ็่ฎพ็ฝฎ', titleKey: 'config.schema.groups.egressProxy', |
| section: 'proxy.egress', |
| fields: [ |
| { |
| key: 'mode', |
| label: 'ไปฃ็ๆจกๅผ', |
| labelKey: 'config.schema.fields.proxyMode.label', |
| type: 'select', |
| options: [ |
| { value: 'direct', label: '็ด่ฟ', labelKey: 'config.schema.options.proxyMode.direct' }, |
| { value: 'single_proxy', label: 'ไปฃ็๏ผๅบๅฎ๏ผ', labelKey: 'config.schema.options.proxyMode.singleProxy' }, |
| { value: 'proxy_pool', label: 'ไปฃ็๏ผ่ฝฎ่ฝฌ๏ผ', labelKey: 'config.schema.options.proxyMode.proxyPool' }, |
| ], |
| desc: 'ๆงๅถๅบ็ซๆต้ไปฃ็็ญ็ฅใdirect ไธบ็ด่ฟ๏ผsingle_proxy ไฝฟ็จๅบๅฎไปฃ็๏ผproxy_pool ๅจไปฃ็ๆฑ ไธญ่ฝฎ่ฝฌใ', |
| descKey: 'config.schema.fields.proxyMode.desc', |
| }, |
| { |
| key: 'proxy_url', |
| label: 'ๅบ็กไปฃ็ URL', |
| labelKey: 'config.schema.fields.proxyUrl.label', |
| type: 'text', |
| wide: true, |
| desc: 'API ๆต้็ๅบ็ซไปฃ็ๅฐๅ๏ผๆฏๆ httpใhttpsใsocks5 ๅ่ฎฎใ', |
| descKey: 'config.schema.fields.proxyUrl.desc', |
| showIf: { path: 'proxy.egress.mode', values: ['single_proxy'] }, |
| }, |
| { |
| key: 'resource_proxy_url', |
| label: '่ตๆบไปฃ็ URL', |
| labelKey: 'config.schema.fields.resourceProxyUrl.label', |
| type: 'text', |
| wide: true, |
| desc: 'ๅพ็ใ่ง้ข็ญๅชไฝ่ตๆบไธ่ฝฝ็ไธ็จไปฃ็ๅฐๅ๏ผ็็ฉบๆถๅ่ฝ่ณๅบ็กไปฃ็ใ', |
| descKey: 'config.schema.fields.resourceProxyUrl.desc', |
| showIf: { path: 'proxy.egress.mode', values: ['single_proxy'] }, |
| }, |
| { |
| key: 'proxy_pool', |
| label: 'ๅบ็กไปฃ็ๆฑ ', |
| labelKey: 'config.schema.fields.proxyPool.label', |
| type: 'textarea', |
| desc: 'API ๆต้ไปฃ็ๆฑ ๏ผๆฏ่กไธไธชไปฃ็ URL๏ผ่ฏทๆฑๆถ่ฝฎ่ฝฌไฝฟ็จใ', |
| descKey: 'config.schema.fields.proxyPool.desc', |
| showIf: { path: 'proxy.egress.mode', values: ['proxy_pool'] }, |
| }, |
| { |
| key: 'resource_proxy_pool', |
| label: '่ตๆบไปฃ็ๆฑ ', |
| labelKey: 'config.schema.fields.resourceProxyPool.label', |
| type: 'textarea', |
| desc: 'ๅชไฝ่ตๆบไธ่ฝฝไธ็จไปฃ็ๆฑ ๏ผๆฏ่กไธไธชไปฃ็ URL๏ผ็็ฉบๆถๅ่ฝ่ณๅบ็กไปฃ็ๆฑ ใ', |
| descKey: 'config.schema.fields.resourceProxyPool.desc', |
| showIf: { path: 'proxy.egress.mode', values: ['proxy_pool'] }, |
| }, |
| { |
| key: 'skip_ssl_verify', |
| label: '่ทณ่ฟ SSL ๆ ก้ช', |
| labelKey: 'config.schema.fields.skipSslVerify.label', |
| type: 'bool', |
| desc: '่ทณ่ฟไปฃ็ๆๅกๅจ TLS ่ฏไนฆๆ ก้ช๏ผไป
ๅจไฝฟ็จ่ช็ญพๅ่ฏไนฆ็ไปฃ็ๆถๅฏ็จใ', |
| descKey: 'config.schema.fields.skipSslVerify.desc', |
| showIf: { path: 'proxy.egress.mode', values: ['single_proxy', 'proxy_pool'] }, |
| }, |
| ] |
| }, |
| { |
| title: 'Cloudflare Clearance', titleKey: 'config.schema.groups.clearance', |
| section: 'proxy.clearance', |
| fields: [ |
| { |
| key: 'mode', |
| label: 'Clearance ๆ กๅ', |
| labelKey: 'config.schema.fields.clearanceMode.label', |
| type: 'select', |
| options: [ |
| { value: 'none', label: 'ๅ
ณ้ญ', labelKey: 'config.schema.options.clearanceMode.none' }, |
| { value: 'manual', label: 'ๆๅจ', labelKey: 'config.schema.options.clearanceMode.manual' }, |
| { value: 'flaresolverr', label: 'FlareSolverr', labelKey: 'config.schema.options.clearanceMode.flaresolverr' }, |
| ], |
| desc: 'ๆงๅถ Cloudflare Clearance ็่ทๅๆนๅผใ', |
| descKey: 'config.schema.fields.clearanceMode.desc', |
| }, |
| { |
| key: 'browser', |
| label: 'ๆต่งๅจๆ็บน', |
| labelKey: 'config.schema.fields.browserFingerprint.label', |
| type: 'text', |
| desc: 'curl_cffi ๆต่งๅจๆ็บนๆ ่ฏ๏ผ้ไธ Cloudflare ๆๆ็ JA3 ๆ็บนๅน้
๏ผไพๅฆ chrome136ใ', |
| descKey: 'config.schema.fields.browserFingerprint.desc', |
| showIf: { path: 'proxy.clearance.mode', values: ['manual', 'flaresolverr'] }, |
| }, |
| { |
| key: 'user_agent', |
| label: 'User-Agent', |
| labelKey: 'config.schema.fields.userAgent.label', |
| type: 'text', |
| wide: true, |
| desc: '่ฏทๆฑๅคดไธญ็ User-Agent๏ผ้ไธ Cloudflare ๆ ก้ช Cookie ๆถๆไฝฟ็จ็ๆต่งๅจไฟๆไธ่ดใ', |
| descKey: 'config.schema.fields.userAgent.desc', |
| showIf: { path: 'proxy.clearance.mode', values: ['manual'] }, |
| }, |
| { |
| key: 'cf_cookies', |
| label: 'Cloudflare Cookies', |
| labelKey: 'config.schema.fields.cfCookies.label', |
| type: 'text', |
| wide: true, |
| desc: 'Cloudflare ไธๅ็ๅฎๆด Cookie ๅญ็ฌฆไธฒ๏ผๅบๅ
ๅซ cf_clearance ็ญๅญๆฎตใ', |
| descKey: 'config.schema.fields.cfCookies.desc', |
| showIf: { path: 'proxy.clearance.mode', values: ['manual'] }, |
| }, |
| { |
| key: 'flaresolverr_url', |
| label: 'FlareSolverr URL', |
| labelKey: 'config.schema.fields.flaresolverrUrl.label', |
| type: 'text', |
| wide: true, |
| desc: 'FlareSolverr ๅฎไพ็ HTTP ๆๅกๅฐๅใ', |
| descKey: 'config.schema.fields.flaresolverrUrl.desc', |
| showIf: { path: 'proxy.clearance.mode', values: ['flaresolverr'] }, |
| }, |
| { |
| key: 'timeout_sec', |
| label: 'ๆๆ่ถ
ๆถ๏ผ็ง๏ผ', |
| labelKey: 'config.schema.fields.challengeTimeout.label', |
| type: 'number', |
| desc: 'FlareSolverr ๅฎๆ Cloudflare ๆๆๅ
่ฎธ็ๆ้ฟ็ญๅพ
ๆถ้ฟใ', |
| descKey: 'config.schema.fields.challengeTimeout.desc', |
| showIf: { path: 'proxy.clearance.mode', values: ['flaresolverr'] }, |
| }, |
| { |
| key: 'refresh_interval', |
| label: 'ๅทๆฐ้ด้๏ผ็ง๏ผ', |
| labelKey: 'config.schema.fields.refreshInterval.label', |
| type: 'number', |
| desc: 'Clearance Cookie ้ข็ญๅทๆฐไปปๅก็่ฐๅบฆ้ด้ใ', |
| descKey: 'config.schema.fields.refreshInterval.desc', |
| showIf: { path: 'proxy.clearance.mode', values: ['flaresolverr'] }, |
| }, |
| ] |
| }, |
| { |
| title: '้่ฏ็ญ็ฅ', titleKey: 'config.schema.groups.retry', |
| section: 'retry', |
| fields: [ |
| { key: 'max_retries', label: 'ๅบ็จๅฑ้่ฏไธ้', labelKey: 'config.schema.fields.maxRetries.label', type: 'number', desc: '่ฏทๆฑๅคฑ่ดฅๅ็ๅบ็จๅฑๆๅคง้่ฏๆฌกๆฐ๏ผ0 ่กจ็คบไธ้่ฏใ้็จไบ chat.completions ไธ responses ็ซฏ็นใ', descKey: 'config.schema.fields.maxRetries.desc' }, |
| { key: 'on_codes', label: 'ๅบ็จๅฑ้่ฏ่งฆๅ็ถๆ็ ', labelKey: 'config.schema.fields.retryCodes.label', type: 'text', desc: 'ๆถๅฐๆๅฎ HTTP ็ถๆ็ ๆถ่งฆๅๅบ็จๅฑ้่ฏ๏ผๅคไธชๅผ่ฏทไฝฟ็จ่ฑๆ้ๅทๅ้๏ผไพๅฆ 429,503ใ', descKey: 'config.schema.fields.retryCodes.desc' }, |
| { key: 'reset_session_status_codes', label: 'ไผ ่พๅฑไผ่ฏ้ๅปบ็ถๆ็ ', labelKey: 'config.schema.fields.resetSessionCodes.label', type: 'text', desc: 'ๆถๅฐๆๅฎ HTTP ็ถๆ็ ๆถ่ชๅจ้ๅปบไผ ่พๅฑไปฃ็ไผ่ฏ๏ผTransport Session๏ผ๏ผๅคไธชๅผ่ฏทไฝฟ็จ่ฑๆ้ๅทๅ้ใ', descKey: 'config.schema.fields.resetSessionCodes.desc' }, |
| ] |
| }, |
| { |
| title: '่ฏทๆฑ่ถ
ๆถ่ฎพ็ฝฎ', titleKey: 'config.schema.groups.timeouts', |
| section: 'chat', |
| fields: [ |
| { key: 'timeout', section: 'chat', label: 'ๅฏน่ฏ่ฏทๆฑ่ถ
ๆถ๏ผ็ง๏ผ', labelKey: 'config.schema.fields.chatTimeout.label', type: 'number', desc: 'ๅฏน่ฏไธๅพๅ็ผ่พ่ฏทๆฑ็ HTTP ่ฟๆฅ่ถ
ๆถๆถ้ดใ', descKey: 'config.schema.fields.chatTimeout.desc' }, |
| { key: 'timeout', section: 'image', label: 'ๅพๅ่ฏทๆฑ่ถ
ๆถ๏ผ็ง๏ผ', labelKey: 'config.schema.fields.imageTimeout.label', type: 'number', desc: 'ๅพๅ็ๆ WebSocket ่ฏทๆฑ็ๆป่ถ
ๆถๆถ้ดใ', descKey: 'config.schema.fields.imageTimeout.desc' }, |
| { key: 'stream_timeout', section: 'image', label: 'ๅพๅๆตๅผ็ฉบ้ฒ่ถ
ๆถ๏ผ็ง๏ผ', type: 'number', desc: 'ๅพๅ็ๆ WebSocket ๅจๆตๅผๆฅๆถ้ถๆฎตๅ
่ฎธ็ๆ้ฟ่ฟ็ปญ็ฉบ้ฒ็ญๅพ
ๆถ้ฟใ' }, |
| { key: 'timeout', section: 'video', label: '่ง้ข่ฏทๆฑ่ถ
ๆถ๏ผ็ง๏ผ', labelKey: 'config.schema.fields.videoTimeout.label', type: 'number', desc: '่ง้ข็ๆ่ฏทๆฑ็็ปไธ่ถ
ๆถๆถ้ดใ', descKey: 'config.schema.fields.videoTimeout.desc' }, |
| { key: 'timeout', section: 'voice', label: '่ฏญ้ณ่ฏทๆฑ่ถ
ๆถ๏ผ็ง๏ผ', labelKey: 'config.schema.fields.voiceTimeout.label', type: 'number', desc: '่ฏญ้ณ่ฏทๆฑ็็ปไธ่ถ
ๆถๆถ้ดใ', descKey: 'config.schema.fields.voiceTimeout.desc' }, |
| { key: 'timeout', section: 'nsfw', label: 'NSFW ๅค็่ถ
ๆถ๏ผ็ง๏ผ', labelKey: 'config.schema.fields.nsfwTimeout.label', type: 'number', desc: 'set_birth_date ไธ enable_nsfw ๆฅๅฃ่ฐ็จ็่ถ
ๆถๆถ้ดใ', descKey: 'config.schema.fields.nsfwTimeout.desc' }, |
| { key: 'upload_timeout', section: 'asset', label: '่ตไบงไธไผ ่ถ
ๆถ๏ผ็ง๏ผ', labelKey: 'config.schema.fields.assetUploadTimeout.label', type: 'number', desc: 'Asset ไธไผ ๆฅๅฃ็่ถ
ๆถๆถ้ดใ', descKey: 'config.schema.fields.assetUploadTimeout.desc' }, |
| { key: 'download_timeout', section: 'asset', label: '่ตไบงไธ่ฝฝ่ถ
ๆถ๏ผ็ง๏ผ', labelKey: 'config.schema.fields.assetDownloadTimeout.label', type: 'number', desc: 'Asset ไธ่ฝฝๆฅๅฃ็่ถ
ๆถๆถ้ดใ', descKey: 'config.schema.fields.assetDownloadTimeout.desc' }, |
| { key: 'list_timeout', section: 'asset', label: '่ตไบงๆฅ่ฏข่ถ
ๆถ๏ผ็ง๏ผ', labelKey: 'config.schema.fields.assetListTimeout.label', type: 'number', desc: 'Asset ๆฅ่ฏขๆฅๅฃ็่ถ
ๆถๆถ้ดใ', descKey: 'config.schema.fields.assetListTimeout.desc' }, |
| { key: 'delete_timeout', section: 'asset', label: '่ตไบงๅ ้ค่ถ
ๆถ๏ผ็ง๏ผ', labelKey: 'config.schema.fields.assetDeleteTimeout.label', type: 'number', desc: 'Asset ๅ ้คๆฅๅฃ็่ถ
ๆถๆถ้ดใ', descKey: 'config.schema.fields.assetDeleteTimeout.desc' }, |
| ] |
| }, |
| ] |
| }, |
| { |
| id: 'cache', label: '็ผๅญๅญๅจ', labelKey: 'config.schema.tabs.cache', |
| groups: [ |
| { |
| title: 'ๆฌๅฐๅชไฝ็ผๅญ', titleKey: 'config.schema.groups.localMediaCache', |
| section: 'cache.local', |
| fields: [ |
| { |
| key: 'image_max_mb', |
| label: 'ๅพ็็ผๅญไธ้๏ผMB๏ผ', |
| labelKey: 'config.schema.fields.imageCacheMaxMb.label', |
| type: 'number', |
| desc: '0 ่กจ็คบไธ้ๅถใไป
ๅฏนๆฌๅฐไปฃ็่ฝ็็ๆ๏ผ่ถ
้ๅๆๆๆงๆไปถไผๅ
ๆธ
็๏ผๅนถไธๆฌกๆงๅ่ฝๅฐไธ้็ 60%ใ', |
| descKey: 'config.schema.fields.imageCacheMaxMb.desc', |
| }, |
| { |
| key: 'video_max_mb', |
| label: '่ง้ข็ผๅญไธ้๏ผMB๏ผ', |
| labelKey: 'config.schema.fields.videoCacheMaxMb.label', |
| type: 'number', |
| desc: '0 ่กจ็คบไธ้ๅถใไป
ๅฏนๆฌๅฐไปฃ็่ฝ็็ๆ๏ผ่ถ
้ๅๆๆๆงๆไปถไผๅ
ๆธ
็๏ผๅนถไธๆฌกๆงๅ่ฝๅฐไธ้็ 60%ใ', |
| descKey: 'config.schema.fields.videoCacheMaxMb.desc', |
| }, |
| ] |
| }, |
| ] |
| }, |
| ]; |
| |
| |
| let SCHEMA = []; |
| let _cfg = {}; |
| let _dirty = {}; |
| let _activeTab = SCHEMA_DEF[0].id; |
| let _configLoaded = false; |
| let _rowRefs = new Map(); |
| let _tabButtons = new Map(); |
| let _tabDirtyCounts = new Map(); |
| let _tabFieldIndex = new Map(); |
| let _conditionalRows = []; |
| let _selectRefs = []; |
| let _showIfDeps = new Map(); |
| let _disabledSelectDeps = new Map(); |
| |
| function tr(key, params, fallback) { |
| if (!key) return fallback ?? ''; |
| const value = t(key, params); |
| return value === key ? (fallback ?? key) : value; |
| } |
| |
| function waitI18n() { |
| return new Promise((resolve) => I18n.onReady(resolve)); |
| } |
| |
| function _addDependency(depMap, path, target) { |
| if (!path || !target) return; |
| if (!depMap.has(path)) depMap.set(path, []); |
| depMap.get(path).push(target); |
| } |
| |
| function _showIfPaths(showIf) { |
| if (!showIf) return []; |
| return [typeof showIf === 'string' ? showIf : showIf.path].filter(Boolean); |
| } |
| |
| function _disabledWhenPaths(condition) { |
| if (condition === 'no_app_url') return ['app.app_url']; |
| return []; |
| } |
| |
| function _collectDependentTargets(paths, depMap) { |
| if (!Array.isArray(paths) || !paths.length) return []; |
| const seen = new Set(); |
| const items = []; |
| paths.forEach((path) => { |
| (depMap.get(path) || []).forEach((target) => { |
| if (seen.has(target)) return; |
| seen.add(target); |
| items.push(target); |
| }); |
| }); |
| return items; |
| } |
| |
| function _syncTabDirtyState(tabId) { |
| const btn = _tabButtons.get(tabId); |
| if (!btn) return; |
| btn.classList.toggle('dirty', (_tabDirtyCounts.get(tabId) || 0) > 0); |
| } |
| |
| function _updateDirtyCount(fp, delta) { |
| const tabId = _tabFieldIndex.get(fp); |
| if (!tabId || !delta) return; |
| const next = Math.max(0, (_tabDirtyCounts.get(tabId) || 0) + delta); |
| _tabDirtyCounts.set(tabId, next); |
| _syncTabDirtyState(tabId); |
| } |
| |
| function _resetRenderCaches() { |
| _rowRefs = new Map(); |
| _tabButtons = new Map(); |
| _tabDirtyCounts = new Map(); |
| _tabFieldIndex = new Map(); |
| _conditionalRows = []; |
| _selectRefs = []; |
| _showIfDeps = new Map(); |
| _disabledSelectDeps = new Map(); |
| } |
| |
| function _localizeOption(opt) { |
| if (typeof opt !== 'object') return opt; |
| return { |
| ...opt, |
| label: tr(opt.labelKey, null, opt.label ?? opt.value), |
| ...(opt.disabledTipKey ? { disabledTip: tr(opt.disabledTipKey, null, opt.disabledTip ?? '') } : {}), |
| }; |
| } |
| |
| function _localizeField(field) { |
| return { |
| ...field, |
| label: field.labelKey ? tr(field.labelKey, null, field.label ?? field.key) : (field.label ?? field.key), |
| desc: field.descKey ? tr(field.descKey, null, field.desc ?? '') : (field.desc ?? ''), |
| help: field.helpKey ? tr(field.helpKey, null, field.help ?? '') : (field.help ?? ''), |
| options: field.options ? field.options.map(_localizeOption) : field.options, |
| }; |
| } |
| |
| function buildSchema() { |
| return SCHEMA_DEF.map((tab) => ({ |
| ...tab, |
| label: tr(tab.labelKey, null, tab.label ?? tab.id), |
| groups: tab.groups.map((group) => ({ |
| ...group, |
| title: group.titleKey ? tr(group.titleKey, null, group.title ?? '') : (group.title ?? ''), |
| fields: group.fields.map(_localizeField), |
| })), |
| })); |
| } |
| |
| function applyConfigI18n() { |
| document.title = tr('config.pageTitle', null, 'Grok2API - ้
็ฝฎ็ฎก็'); |
| SCHEMA = buildSchema(); |
| if (!_configLoaded) return; |
| renderAll(); |
| _refreshDirtyState({ autoNormalizeSelects: false }); |
| } |
| |
| |
| async function _api(method, path, body) { |
| const key = await adminKey.get(); |
| const r = await fetch(ADMIN_API + path, { |
| method, |
| headers: { ...(body != null && { 'Content-Type': 'application/json' }), Authorization: `Bearer ${key}` }, |
| ...(body != null && { body: JSON.stringify(body) }), |
| }); |
| if (!r.ok) { const d = await r.json().catch(() => ({})); throw new Error(d.detail || r.status); } |
| return r.json(); |
| } |
| |
| |
| |
| |
| function _getByPath(obj, dottedPath) { |
| return dottedPath.split('.').reduce((node, k) => |
| (node != null && typeof node === 'object') ? node[k] : undefined, obj); |
| } |
| |
| |
| function _setByPath(obj, dottedPath, value) { |
| const keys = dottedPath.split('.'); |
| let cur = obj; |
| for (let i = 0; i < keys.length - 1; i++) { |
| if (cur[keys[i]] == null || typeof cur[keys[i]] !== 'object') cur[keys[i]] = {}; |
| cur = cur[keys[i]]; |
| } |
| cur[keys[keys.length - 1]] = value; |
| } |
| |
| |
| |
| function _evalDisabledWhen(condition) { |
| if (condition === 'no_app_url') { |
| const v = 'app.app_url' in _dirty ? _dirty['app.app_url'] : _getByPath(_cfg, 'app.app_url'); |
| return !v || String(v).trim() === ''; |
| } |
| return false; |
| } |
| |
| |
| |
| |
| function _evalShowIf(showIf) { |
| if (!showIf) return true; |
| const path = typeof showIf === 'string' ? showIf : showIf.path; |
| const cur = path in _dirty ? _dirty[path] : _getByPath(_cfg, path); |
| if (typeof showIf === 'string') return !!cur; |
| return showIf.values.includes(cur); |
| } |
| |
| |
| function _fullPath(section, key) { return `${section}.${key}`; } |
| |
| |
| function _getValue(section, key) { return _getByPath(_cfg, _fullPath(section, key)); } |
| |
| function _getCurrentValue(section, key) { |
| const fp = _fullPath(section, key); |
| return fp in _dirty ? _dirty[fp] : _getValue(section, key); |
| } |
| |
| |
| function _el(tag, attrs, ...children) { |
| const el = document.createElement(tag); |
| Object.entries(attrs || {}).forEach(([k, v]) => { |
| if (k === 'class') el.className = v; |
| else if (k.startsWith('on')) el.addEventListener(k.slice(2), v); |
| else el.setAttribute(k, v); |
| }); |
| children.flat().forEach(c => { |
| if (c == null) return; |
| el.appendChild(typeof c === 'string' ? document.createTextNode(c) : c); |
| }); |
| return el; |
| } |
| |
| let _helpTipEl = null; |
| let _activeHelpButton = null; |
| |
| function _hideHelpTip() { |
| if (_activeHelpButton) _activeHelpButton.removeAttribute('aria-describedby'); |
| if (_helpTipEl) _helpTipEl.remove(); |
| _helpTipEl = null; |
| _activeHelpButton = null; |
| } |
| |
| function _positionHelpTip(button, tip) { |
| const margin = 12; |
| const rect = button.getBoundingClientRect(); |
| const viewportWidth = window.innerWidth || document.documentElement.clientWidth; |
| const viewportHeight = window.innerHeight || document.documentElement.clientHeight; |
| const maxWidth = Math.max(120, viewportWidth - margin * 2); |
| |
| tip.style.maxWidth = `${Math.min(320, maxWidth)}px`; |
| tip.style.left = '0px'; |
| tip.style.top = '0px'; |
| |
| const tipRect = tip.getBoundingClientRect(); |
| let left = rect.left + rect.width / 2 - tipRect.width / 2; |
| left = Math.max(margin, Math.min(left, viewportWidth - tipRect.width - margin)); |
| |
| let placement = 'top'; |
| let top = rect.top - tipRect.height - 8; |
| if (top < margin) { |
| placement = 'bottom'; |
| top = rect.bottom + 8; |
| } |
| if (top + tipRect.height > viewportHeight - margin) { |
| top = Math.max(margin, viewportHeight - tipRect.height - margin); |
| } |
| |
| const arrowLeft = Math.max(12, Math.min(rect.left + rect.width / 2 - left, tipRect.width - 12)); |
| tip.dataset.placement = placement; |
| tip.style.setProperty('--arrow-left', `${arrowLeft}px`); |
| tip.style.left = `${left}px`; |
| tip.style.top = `${top}px`; |
| } |
| |
| function _showHelpTip(button) { |
| const text = button.getAttribute('data-tip'); |
| if (!text) return; |
| _hideHelpTip(); |
| |
| const tip = _el('div', { |
| class: 'cfg-help-tip', |
| id: 'cfg-help-tip', |
| role: 'tooltip', |
| }, text); |
| document.body.appendChild(tip); |
| button.setAttribute('aria-describedby', 'cfg-help-tip'); |
| |
| _helpTipEl = tip; |
| _activeHelpButton = button; |
| _positionHelpTip(button, tip); |
| requestAnimationFrame(() => tip.classList.add('visible')); |
| } |
| |
| window.addEventListener('resize', _hideHelpTip); |
| window.addEventListener('scroll', _hideHelpTip, true); |
| document.addEventListener('keydown', (event) => { |
| if (event.key === 'Escape') _hideHelpTip(); |
| }); |
| document.addEventListener('click', (event) => { |
| if (!event.target.closest?.('.cfg-help')) _hideHelpTip(); |
| }); |
| |
| function renderInput(field, section, value) { |
| const fp = _fullPath(section, field.key); |
| |
| const onChange = (v) => { |
| const wasDirty = fp in _dirty; |
| const orig = _getValue(section, field.key); |
| const same = String(orig) === String(v) || orig === v; |
| if (same) delete _dirty[fp]; else _dirty[fp] = v; |
| const isDirty = fp in _dirty; |
| if (wasDirty !== isDirty) _updateDirtyCount(fp, isDirty ? 1 : -1); |
| _refreshRow(section, field.key); |
| _refreshDirtyState({ changedPaths: [fp] }); |
| }; |
| |
| if (field.type === 'bool') { |
| const chk = document.createElement('input'); |
| chk.type = 'checkbox'; |
| chk.checked = Boolean(value); |
| chk.id = `field-${section}-${field.key}`; |
| chk.addEventListener('change', () => onChange(chk.checked)); |
| const track = _el('span', { class: 'toggle-track' }); |
| const label = _el('label', { class: 'toggle', for: chk.id }, chk, track); |
| return label; |
| } |
| |
| if (field.type === 'select') { |
| const sel = _el('select', { class: 'cfg-select', id: `field-${section}-${field.key}` }); |
| const deps = new Set(); |
| (field.options || []).forEach(opt => { |
| const o = document.createElement('option'); |
| o.value = (typeof opt === 'object') ? opt.value : opt; |
| o.textContent = (typeof opt === 'object') ? opt.label : opt; |
| if (o.value === value) o.selected = true; |
| if (typeof opt === 'object' && opt.disabledWhen) { |
| o.dataset.disabledWhen = opt.disabledWhen; |
| if (opt.disabledTip) o.dataset.disabledTip = opt.disabledTip; |
| const isDisabled = _evalDisabledWhen(opt.disabledWhen); |
| o.disabled = isDisabled; |
| if (isDisabled && opt.disabledTip) o.title = opt.disabledTip; |
| _disabledWhenPaths(opt.disabledWhen).forEach((path) => deps.add(path)); |
| } |
| sel.appendChild(o); |
| }); |
| if (deps.size) { |
| _selectRefs.push(sel); |
| deps.forEach((path) => _addDependency(_disabledSelectDeps, path, sel)); |
| } |
| sel.addEventListener('change', () => onChange(sel.value)); |
| return sel; |
| } |
| |
| if (field.type === 'textarea') { |
| const ta = _el('textarea', { class: 'cfg-textarea', id: `field-${section}-${field.key}`, |
| rows: '3', placeholder: '' }); |
| ta.value = value ?? ''; |
| ta.addEventListener('input', () => onChange(ta.value)); |
| return ta; |
| } |
| |
| if (field.type === 'number') { |
| const inp = _el('input', { class: 'cfg-number', type: 'number', |
| id: `field-${section}-${field.key}`, value: value ?? '' }); |
| inp.addEventListener('input', () => { |
| const n = inp.value === '' ? '' : Number(inp.value); |
| onChange(n); |
| }); |
| return inp; |
| } |
| |
| |
| const inp = _el('input', { |
| class: `cfg-text${field.wide ? ' wide' : ''}`, |
| type: field.type === 'password' ? 'password' : 'text', |
| id: `field-${section}-${field.key}`, |
| value: value ?? '', |
| autocomplete: 'off', |
| }); |
| inp.addEventListener('input', () => onChange(inp.value)); |
| |
| if (field.rand) { |
| const btn = _el('button', { class: 'cfg-rand-btn', type: 'button', title: tr('config.randomize', null, '้ๆบ็ๆ'), onclick: () => { |
| const uuid = crypto.randomUUID().replace(/-/g, '').slice(0, 24); |
| inp.value = uuid; |
| inp.type = 'text'; |
| onChange(uuid); |
| }}); |
| btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="20" rx="3"/><circle cx="8" cy="8" r="1.5" fill="currentColor" stroke="none"/><circle cx="16" cy="8" r="1.5" fill="currentColor" stroke="none"/><circle cx="8" cy="16" r="1.5" fill="currentColor" stroke="none"/><circle cx="16" cy="16" r="1.5" fill="currentColor" stroke="none"/><circle cx="12" cy="12" r="1.5" fill="currentColor" stroke="none"/></svg>'; |
| const wrap = _el('div', { style: 'display:flex;align-items:center;gap:6px;flex:1;min-width:0' }, inp, btn); |
| return wrap; |
| } |
| |
| return inp; |
| } |
| |
| function renderRow(field, section, tabId) { |
| const fp = _fullPath(section, field.key); |
| const value = _getValue(section, field.key); |
| const isDirty = fp in _dirty; |
| |
| const dot = _el('span', { class: 'cfg-modified-dot' }); |
| const input = renderInput(field, section, isDirty ? _dirty[fp] : value); |
| const labelChildren = [field.label]; |
| |
| if (section === 'app' && field.key === 'webui_enabled') { |
| const btn = _el('button', { |
| class: 'cfg-inline-action', |
| type: 'button', |
| title: 'ๆๅผ WebUI', |
| 'aria-label': 'ๆๅผ WebUI', |
| 'data-webui-link': 'true', |
| onclick: () => window.open('/webui/login', '_blank', 'noopener,noreferrer'), |
| }); |
| btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 5h5v5"/><path d="M10 14 19 5"/><path d="M19 14v5h-14v-14h5"/></svg>'; |
| btn.disabled = !Boolean(_getCurrentValue(section, field.key)); |
| labelChildren.push(btn); |
| } |
| |
| if (field.help) { |
| labelChildren.push(_el('button', { |
| class: 'cfg-help', |
| type: 'button', |
| 'aria-label': field.help, |
| 'data-tip': field.help, |
| onmouseenter: (event) => _showHelpTip(event.currentTarget), |
| onmouseleave: _hideHelpTip, |
| onfocus: (event) => _showHelpTip(event.currentTarget), |
| onblur: _hideHelpTip, |
| onclick: (event) => { |
| event.preventDefault(); |
| event.stopPropagation(); |
| _showHelpTip(event.currentTarget); |
| }, |
| }, '?')); |
| } |
| |
| labelChildren.push(dot); |
| |
| const labelCol = _el('div', { class: 'cfg-label-col' }, |
| _el('div', { class: 'cfg-label' }, labelChildren), |
| field.desc ? _el('div', { class: 'cfg-desc' }, field.desc) : null, |
| ); |
| const inputCol = _el('div', { class: `cfg-input-col${field.type === 'bool' ? ' is-bool' : ''}` }, input); |
| |
| const row = _el('div', { |
| class: `cfg-row${isDirty ? ' modified' : ''}`, |
| 'data-section': section, |
| 'data-key': field.key, |
| }, labelCol, inputCol); |
| |
| _rowRefs.set(fp, row); |
| _tabFieldIndex.set(fp, tabId); |
| if (isDirty) _tabDirtyCounts.set(tabId, (_tabDirtyCounts.get(tabId) || 0) + 1); |
| |
| |
| if (field.showIf) { |
| row._showIfSpec = field.showIf; |
| row.style.display = _evalShowIf(field.showIf) ? '' : 'none'; |
| _conditionalRows.push(row); |
| _showIfPaths(field.showIf).forEach((path) => _addDependency(_showIfDeps, path, row)); |
| } |
| |
| return row; |
| } |
| |
| function renderGroup(group, tabId) { |
| const section = group.section; |
| const rows = group.fields.map(f => renderRow(f, f.section || section, tabId)); |
| const titleEl = _el('div', { class: 'cfg-group-title' }, group.title || ''); |
| const wrap = _el('div', { class: 'cfg-group' }, titleEl, ...rows); |
| return wrap; |
| } |
| |
| function renderPanel(tab) { |
| const panel = _el('div', { class: `cfg-panel${tab.id === _activeTab ? ' active' : ''}`, |
| id: `panel-${tab.id}` }); |
| tab.groups.forEach(g => panel.appendChild(renderGroup(g, tab.id))); |
| return panel; |
| } |
| |
| function renderAll() { |
| const nav = document.getElementById('cfg-nav'); |
| const panels = document.getElementById('cfg-panels'); |
| _resetRenderCaches(); |
| nav.innerHTML = ''; panels.innerHTML = ''; |
| |
| SCHEMA.forEach(tab => { |
| |
| const btn = _el('button', { |
| class: `cfg-tab${tab.id === _activeTab ? ' active' : ''}`, |
| onclick: () => switchTab(tab.id), |
| 'data-tab': tab.id, |
| }, tab.label, _el('span', { class: 'dot' })); |
| _tabButtons.set(tab.id, btn); |
| nav.appendChild(btn); |
| |
| |
| panels.appendChild(renderPanel(tab)); |
| }); |
| _tabButtons.forEach((_, tabId) => _syncTabDirtyState(tabId)); |
| } |
| |
| function switchTab(id) { |
| _activeTab = id; |
| document.querySelectorAll('.cfg-tab').forEach(b => |
| b.classList.toggle('active', b.dataset.tab === id)); |
| document.querySelectorAll('.cfg-panel').forEach(p => |
| p.classList.toggle('active', p.id === `panel-${id}`)); |
| } |
| |
| |
| function _refreshRow(section, key) { |
| const row = _rowRefs.get(_fullPath(section, key)); |
| if (row) row.classList.toggle('modified', _fullPath(section, key) in _dirty); |
| } |
| |
| function _refreshConditionalRows(changedPaths = null) { |
| const rows = changedPaths ? _collectDependentTargets(changedPaths, _showIfDeps) : _conditionalRows; |
| rows.forEach((row) => { |
| row.style.display = _evalShowIf(row._showIfSpec) ? '' : 'none'; |
| }); |
| } |
| |
| function _refreshSelectStates(changedPaths = null, { autoNormalizeSelects = true } = {}) { |
| const selects = changedPaths ? _collectDependentTargets(changedPaths, _disabledSelectDeps) : _selectRefs; |
| selects.forEach((sel) => { |
| let anyChanged = false; |
| [...sel.options].forEach(o => { |
| if (!o.dataset.disabledWhen) return; |
| const isDisabled = _evalDisabledWhen(o.dataset.disabledWhen); |
| o.disabled = isDisabled; |
| o.title = isDisabled ? (o.dataset.disabledTip || '') : ''; |
| if (autoNormalizeSelects && isDisabled && o.selected) anyChanged = true; |
| }); |
| if (anyChanged) { |
| const firstEnabled = [...sel.options].find(o => !o.disabled); |
| if (firstEnabled) { |
| firstEnabled.selected = true; |
| sel.dispatchEvent(new Event('change')); |
| } |
| } |
| }); |
| } |
| |
| function _refreshDirtyState({ autoNormalizeSelects = true, changedPaths = null } = {}) { |
| const n = Object.keys(_dirty).length; |
| document.getElementById('btn-save').disabled = n === 0; |
| document.getElementById('btn-reset').disabled = n === 0; |
| const bar = document.getElementById('save-bar'); |
| bar.classList.toggle('visible', n > 0); |
| document.getElementById('save-bar-msg').textContent = |
| n === 0 |
| ? tr('config.unsavedDefault', null, 'ๅญๅจๆชไฟๅญ็ๆดๆน') |
| : n === 1 |
| ? tr('config.unsavedSingle', { n }, 'ๅญๅจ 1 ้กนๆชไฟๅญ็ๆดๆน') |
| : tr('config.unsavedMultiple', { n }, `ๅญๅจ ${n} ้กนๆชไฟๅญ็ๆดๆน`); |
| |
| const webuiLinkBtn = document.querySelector('[data-webui-link="true"]'); |
| if (webuiLinkBtn) { |
| const enabled = Boolean(_getCurrentValue('app', 'webui_enabled')); |
| webuiLinkBtn.disabled = !enabled; |
| webuiLinkBtn.title = enabled ? 'ๆๅผ WebUI' : 'ๅฏ็จ WebUI ๅๅฏๆๅผ'; |
| webuiLinkBtn.setAttribute('aria-label', webuiLinkBtn.title); |
| } |
| |
| _refreshConditionalRows(changedPaths); |
| _refreshSelectStates(changedPaths, { autoNormalizeSelects }); |
| if (!changedPaths) _tabButtons.forEach((_, tabId) => _syncTabDirtyState(tabId)); |
| } |
| |
| |
| async function load() { |
| try { |
| _cfg = await _api('GET', '/config'); |
| _dirty = {}; |
| _configLoaded = true; |
| if (!SCHEMA.length) SCHEMA = buildSchema(); |
| renderAll(); |
| _refreshDirtyState({ autoNormalizeSelects: false }); |
| } catch (e) { |
| showToast(tr('config.loadFailed', { message: e.message }, `้
็ฝฎๅ ่ฝฝๅคฑ่ดฅ: ${e.message}`), 'error'); |
| } |
| } |
| |
| |
| async function doSave() { |
| if (!Object.keys(_dirty).length) return; |
| |
| |
| |
| const patch = {}; |
| Object.entries(_dirty).forEach(([fp, v]) => _setByPath(patch, fp, v)); |
| |
| try { |
| showToast(tr('config.saving', null, 'ๆญฃๅจไฟๅญๆดๆนโฆ'), 'info'); |
| await _api('POST', '/config', patch); |
| |
| Object.entries(_dirty).forEach(([fp, v]) => _setByPath(_cfg, fp, v)); |
| _dirty = {}; |
| |
| renderAll(); |
| _refreshDirtyState({ autoNormalizeSelects: false }); |
| showToast(tr('config.saveSuccess', null, 'ๆดๆนๅทฒไฟๅญ'), 'success'); |
| } catch (e) { |
| showToast(tr('config.saveFailed', { message: e.message }, `ไฟๅญๆดๆนๅคฑ่ดฅ: ${e.message}`), 'error'); |
| } |
| } |
| |
| |
| function doReset() { |
| if (!Object.keys(_dirty).length) return; |
| _dirty = {}; |
| renderAll(); |
| _refreshDirtyState({ autoNormalizeSelects: false }); |
| showToast(tr('config.resetSuccess', null, 'ๆชไฟๅญ็ๆดๆนๅทฒๆค้'), 'info'); |
| } |
| |
| |
| (async () => { |
| try { |
| SCHEMA = buildSchema(); |
| |
| waitI18n().then(() => { |
| applyConfigI18n(); |
| }).catch((e) => { |
| console.error('i18n init failed', e); |
| }); |
| |
| await renderAdminHeader?.(); |
| await renderSiteFooter?.(); |
| const key = await adminKey.get(); |
| if (!key || !await verifyKey(ADMIN_API + '/verify', key).catch(() => false)) |
| return location.href = '/admin/login'; |
| await load(); |
| } catch (e) { |
| console.error('config page boot failed', e); |
| showToast(tr('config.loadFailed', { message: e.message }, `้
็ฝฎ้กตๅๅงๅๅคฑ่ดฅ: ${e.message}`), 'error'); |
| } |
| })(); |
| </script> |
| </body> |
| </html> |
|
|