PageSourceSearch

https://manshakotragmailcom-stepaudio-3-music.hf.space/studio/assets/js/apikey.js

js manshakotragmailcom-stepaudio-3-music.hf.space collected 2026-10-03 17:05:44 UTC 6,138 bytes, 153 lines download raw bytes

1/* =============================================================================
2   apikey.js · API Key 配置面板
3   -----------------------------------------------------------------------------
4   访客自备 Key(原因见 config.js 顶部注释)。这个模块管两件事:
5     1. openApiKeyDialog()  —— 左侧「API 配置」点开的那个框:填 Key、校验、保存
6     2. ensureKey()         —— 创作前的守卫:没配 Key 就弹提示并给一个直达按钮
7
8   Key 只写进 localStorage,不发往除 api.stepfun.com 之外的任何地方。
9   ============================================================================= */
10
11import { API, getKey, setKey, clearKey, maskKey, needsUserKey } from './config.js';
12import { verifyKey } from './api.js';
13import { t } from './i18n.js';
14import { modal, icon, esc, toast } from './ui.js';
15
16/* 配置发生变化时通知界面(创作页的按钮文案/额度行要跟着变) */
17const listeners = new Set();
18export const onKeyChange = fn => { listeners.add(fn); return () => listeners.delete(fn); };
19const emit = () => listeners.forEach(fn => { try { fn(); } catch { /* 单个订阅者出错不该拖垮其它 */ } });
20
21/* ── 配置面板 ───────────────────────────────────────────────────────────── */
22export function openApiKeyDialog() {
23  const existing = getKey();
24
25  const { host, close } = modal(`
26    <h3 id="mdlT">${esc(t('key.title'))}</h3>
27
28    <div class="key-promo">
29      <span class="key-promo-txt">${esc(t('key.promo'))}</span>
30      <button type="button" class="btn btn-quiet btn-sm" data-a="go">
31        ${esc(t('key.goPlatform'))}${icon('i-arrow', 'icon-sm', 2)}
32      </button>
33    </div>
34
35    <label class="field key-field">
36      <span class="field-label">${esc(t('key.label'))}</span>
37      <span class="key-input-wrap">
38        <input class="input" id="keyInput" type="password" spellcheck="false"
39          autocomplete="off" autocapitalize="off" placeholder="${esc(t('key.placeholder'))}">
40        <button type="button" class="key-eye" data-a="peek"
41          aria-label="${esc(t('key.show'))}">${icon('i-eye', 'icon-sm', 1.9)}</button>
42      </span>
43    </label>
44
45    ${existing ? `<p class="key-cur">${esc(t('key.current'))} <code>${esc(maskKey(existing))}</code></p>` : ''}
46    <p class="key-note">${esc(t('key.privacy'))}</p>
47    <p class="key-status" id="keyStatus" role="status" aria-live="polite" hidden></p>
48
49    <div class="dlg-foot">
50      ${existing ? `<button type="button" class="btn btn-ghost" data-a="clear">${esc(t('key.clear'))}</button>` : ''}
51      <button type="button" class="btn btn-ghost" data-a="cancel">${esc(t('confirm.cancel'))}</button>
52      <button type="button" class="btn btn-primary" data-a="save">${esc(t('key.save'))}</button>
53    </div>`);
54
55  const $ = s => host.querySelector(s);
56  const input = $('#keyInput');
57  const status = $('#keyStatus');
58  const saveBtn = $('[data-a="save"]');
59
60  const show = (msg, kind) => {
61    status.hidden = false;
62    status.textContent = msg;
63    status.className = 'key-status' + (kind ? ' ' + kind : '');
64  };
65
66  // 已有 Key 时预填,方便直接改;用 password 型避免肩窥
67  if (existing) input.value = existing;
68
69  $('[data-a="go"]').onclick = () =>
70    window.open(API.PLATFORM_URL, '_blank', 'noopener,noreferrer');
71
72  $('[data-a="peek"]').onclick = e => {
73    const b = e.currentTarget;
74    const showing = input.type === 'text';
75    input.type = showing ? 'password' : 'text';
76    b.setAttribute('aria-label', t(showing ? 'key.show' : 'key.hide'));
77    b.classList.toggle('on', !showing);
78  };
79
80  $('[data-a="cancel"]').onclick = close;
81
82  host.querySelector('[data-a="clear"]')?.addEventListener('click', () => {
83    clearKey();
84    emit();
85    toast(t('key.cleared'));
86    close();
87  });
88
89  let busy = false;
90  const submit = async () => {
91    if (busy) return;
92    const k = input.value.trim();
93    if (!k) { show(t('key.err.key_empty'), 'err'); input.focus(); return; }
94
95    busy = true;
96    saveBtn.disabled = true;
97    show(t('key.verifying'), 'busy');
98
99    const r = await verifyKey(k);
100    busy = false;
101    saveBtn.disabled = false;
102
103    if (!r.ok) {
104      // 校验没过就不落盘 —— 存一个坏 Key 只会让后面每次生成都失败
105      show(t('key.err.' + r.code) + (r.message ? `(${r.message})` : ''), 'err');
106      input.focus();
107      input.select();
108      return;
109    }
110    setKey(k);
111    emit();
112    show(t('key.ok'), 'ok');
113    toast(t(r.code === 'key_ok_throttled' ? 'key.savedThrottled' : 'key.saved'));
114    setTimeout(close, 600);          // 让用户看到那一行"可用"再关
115  };
116
117  saveBtn.onclick = submit;
118  input.addEventListener('keydown', e => {
119    if (e.key === 'Enter') { e.preventDefault(); submit(); }
120  });
121
122  input.focus();
123  input.select();
124}
125
126/* ── 创作前的守卫 ───────────────────────────────────────────────────────── */
127
128/**
129 * 有 Key 返回 true;没有则弹提示(带直达配置的按钮)并返回 false。
130 * proxy 模式下额度由平台统一供给,访客不需要自备 Key,直接放行。
131 */
132export function ensureKey() {
133  if (!needsUserKey() || getKey()) return true;
134
135  const { host, close } = modal(`
136    <h3 id="mdlT">${esc(t('key.needTitle'))}</h3>
137    <p>${esc(t('key.needBody'))}</p>
138    <div class="dlg-foot">
139      <button type="button" class="btn btn-ghost" data-a="later">${esc(t('key.later'))}</button>
140      <button type="button" class="btn btn-primary" data-a="setup">
141        ${esc(t('key.goSetup'))}${icon('i-arrow', 'icon-sm', 2)}
142      </button>
143    </div>`);
144
145  host.querySelector('[data-a="later"]').onclick = close;
146  host.querySelector('[data-a="setup"]').onclick = () => {
147    close();
148    // 等上一层遮罩淡出再开,避免两层 scrim 叠在一起闪一下
149    setTimeout(openApiKeyDialog, 210);
150  };
151  host.querySelector('[data-a="setup"]').focus();
152  return false;
153}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.