@fish-under-sea/magpie-neartoken 0.4.0 → 0.5.0

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/index.mjs CHANGED
@@ -163,27 +163,32 @@ async function catalog(disabledGroups = new Set(), disabledModels = new Set()) {
163
163
  }
164
164
  const groups = all.filter((g) => !disabledGroups.has(String(g ?? '').trim()))
165
165
  if (!groups.length) continue
166
- for (const raw of groups) {
167
- const group = String(raw ?? '').trim()
168
- const ref = group && refs[group] ? String(refs[group]) : ''
169
- const ratio = ratioOf(ratios, group)
170
- const id = ref ? `${ref}::${model}` : model
171
- const label = group ? (ratio > 0 ? `${group} ${ratioText(ratio)}x` : group) + ' · ' + model : model
172
- out[id] = {
173
- name: label,
174
- api: { id: model, url: SITE + '/v1', npm: '@ai-sdk/openai-compatible' },
175
- limit: {
176
- context: Number(row?.context_window) > 0 ? Number(row.context_window) : 0,
177
- output: Number(row?.max_tokens) > 0 ? Number(row.max_tokens) : 0,
178
- },
179
- ...(ref ? { headers: { 'x-neartoken-group': ref } } : {}),
180
- // 站点自己的分类,交给 magpie 的四个分区(图像/视频/语音/向量)
181
- ...(kind === 'chat' ? {} : { kind }),
182
- capabilities: {
183
- toolcall: true,
184
- output: kindOutput(kind),
185
- },
186
- }
166
+ // 一个模型只出一行:多分组不再展开成多行,否则「只启用一个模型」在
167
+ // 供应商列表里仍会冒出五六行同名条目。分组与倍率折进显示名里,
168
+ // 请求按该模型最便宜(倍率最低)的分组走。
169
+ const ranked = groups.map((g) => ({
170
+ name: String(g ?? '').trim(),
171
+ ref: String(g ?? '').trim() && refs[String(g ?? '').trim()] ? String(refs[String(g ?? '').trim()]) : '',
172
+ ratio: ratioOf(ratios, String(g ?? '').trim()),
173
+ })).sort((a, b) => (a.ratio || 1e9) - (b.ratio || 1e9))
174
+ const main = ranked[0] || { name: '', ref: '', ratio: 0 }
175
+ const label = main.name
176
+ ? (main.ratio > 0 ? `${main.name} ${ratioText(main.ratio)}x` : main.name) + ' · ' + model
177
+ : model
178
+ out[model] = {
179
+ name: label,
180
+ api: { id: model, url: SITE + '/v1', npm: '@ai-sdk/openai-compatible' },
181
+ limit: {
182
+ context: Number(row?.context_window) > 0 ? Number(row.context_window) : 0,
183
+ output: Number(row?.max_tokens) > 0 ? Number(row.max_tokens) : 0,
184
+ },
185
+ ...(main.ref ? { headers: { 'x-neartoken-group': main.ref } } : {}),
186
+ // 站点自己的分类,交给 magpie 的四个分区(图像/视频/语音/向量)
187
+ ...(kind === 'chat' ? {} : { kind }),
188
+ capabilities: {
189
+ toolcall: true,
190
+ output: kindOutput(kind),
191
+ },
187
192
  }
188
193
  }
189
194
  writeGroups([...groupsOut.values()].map((g) => ({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fish-under-sea/magpie-neartoken",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "NearToken for magpie: a provider whose sign-in is the site's own device authorization, models listed per billing group with their multiplier, balance and vouchers on magpie's usage card, plus a page for spending and top-ups.",
5
5
  "type": "module",
6
6
  "main": "index.mjs",
package/page/app.js CHANGED
@@ -84,8 +84,12 @@
84
84
 
85
85
  /** showError 把宿主的话放到页脚的一条提示上,不弹窗。 */
86
86
  let lastError = ''
87
+ let notice = ''
88
+ function showNotice(text) { notice = text; lastError = ''; render() }
87
89
  function showError(error) {
88
90
  const said = String(error && error.message ? error.message : error)
91
+ // options 写入限流由 saveDisabled 自己合并重试,不用挂到页脚
92
+ if (/once a second at most|sets its options/i.test(said)) return
89
93
  lastError = said
90
94
  // 宿主侧仍在建 key+URL 供应商时,会撞上 magpie 的内置订阅 id。
91
95
  // 这不是页面能修的,给一句能照做的指引。
@@ -195,16 +199,29 @@
195
199
  if (tab === 'market') frag.append(marketCard())
196
200
  else if (tab === 'logs') frag.append(logsCard())
197
201
  else frag.append(el('div', { class: 'home-grid' },
198
- accountCard(), boardCard(), topupCard(), providerCard()))
202
+ accountCard(), boardCard(), topupCard(), providerCard(), referralCard()))
199
203
  } else {
200
204
  setStatus('未连接')
201
205
  frag.append(connectCard())
202
206
  }
203
207
  if (view.error) frag.append(el('p', { class: 'note bad', text: view.error }))
204
208
  if (lastError) frag.append(el('p', { class: 'note bad', text: lastError }))
209
+ if (notice) frag.append(el('p', { class: 'note', text: notice }))
205
210
  if (pendingAct) frag.append(el('p', { class: 'note', text: '正在处理:' + pendingAct }))
206
211
  body.replaceChildren(frag)
207
212
  if (view.pending) paintCountdown()
213
+ // 首页四张卡等高:以账户卡为基准锁死高度,多出来的内容在卡片内滚动
214
+ if (tab === 'home' && view.configured) equalizeHome()
215
+ }
216
+
217
+ /** equalizeHome 量第一张卡(账户)的高度,锁到同组所有卡上。 */
218
+ function equalizeHome() {
219
+ const cards = [...document.querySelectorAll('.home-grid > .card:not(.home-wide)')]
220
+ if (cards.length < 2) return
221
+ cards.forEach((c) => { c.style.height = '' })
222
+ const h = Math.round(cards[0].getBoundingClientRect().height)
223
+ if (!h) return
224
+ cards.forEach((c) => { c.style.height = h + 'px' })
208
225
  }
209
226
 
210
227
  /** tabsBar 是三个视角:原本界面、模型广场、使用日志。 */
@@ -277,6 +294,23 @@
277
294
  el('p', { class: 'note', id: 'copy-hint', text: '' }))
278
295
  }
279
296
 
297
+ /** 推荐计划:站点「复制推荐链接」那一栏的长条卡,跨整行放最下面。 */
298
+ function referralCard() {
299
+ const aff = view.affCode || view.referralCode || ''
300
+ const url = aff ? 'https://neartoken.cc/sign-up?aff=' + encodeURIComponent(aff)
301
+ : 'https://neartoken.cc/console'
302
+ const invites = Number(view.invites) || 0
303
+ return el('section', { class: 'card home-wide' },
304
+ el('div', { class: 'rf' },
305
+ el('div', { class: 'rf-ico', text: '◈' }),
306
+ el('div', { class: 'rf-text' },
307
+ el('h2', { text: '推荐计划', style: 'margin:0' }),
308
+ el('p', { class: 'note', text: '用户通过您的推荐链接注册后,您即可获得奖励。可随时将累计奖励转入余额。', style: 'margin:2px 0 0' })),
309
+ el('div', { class: 'rf-invites' }, el('div', { class: 'k', text: '邀请' }), el('div', { class: 'v', text: String(invites) })),
310
+ el('div', { class: 'rf-link' }, el('span', { text: url })),
311
+ el('button', { class: 'rf-copy', title: '复制推荐链接', onclick: () => copy(url) }, '⧉')))
312
+ }
313
+
280
314
  /** 附件:账户、余额、代金券。 */
281
315
  function accountCard() {
282
316
  const account = view.account || {}
@@ -347,47 +381,72 @@
347
381
  return card(null, ...parts)
348
382
  }
349
383
 
350
- /** 充值卡:档位、渠道、去支付。 */
384
+ /**
385
+ * 充值卡:站点「添加资金」页的缩小版。
386
+ * 金额大卡(带优惠角标)、自定义金额 + 待支付、支付方式图标卡、兑换码。
387
+ */
351
388
  function topupCard() {
352
389
  const info = topupInfo
353
- if (!info) return card('充值', el('p', { class: 'note', text: '正在读取充值档位…' }))
354
- if (info.unavailable) return card('充值', el('p', { class: 'note', text: info.unavailable }))
390
+ if (!info) return card(null, el('p', { class: 'note', text: '正在读取充值档位…' }))
391
+ if (info.unavailable) return card(null, el('p', { class: 'note', text: info.unavailable }))
355
392
  const options = Array.isArray(info.options) && info.options.length ? info.options : []
356
- const amount = pick.amount
357
- const method = pick.method || (info.methods[0] && info.methods[0].type) || ''
358
- const parts = [
359
- el('div', { class: 'topup' },
360
- el('div', { class: 'row wrap' },
361
- options.map((value) => el('button', {
362
- 'aria-pressed': String(value === amount),
363
- onclick: () => { pick.amount = value; pick.custom = ''; render() },
364
- }, String(value))),
365
- el('label', { class: 'field' },
366
- el('span', { class: 'cur', text: info.currency || '¥' }),
367
- el('input', {
368
- type: 'text',
369
- inputmode: 'numeric',
370
- placeholder: '其他',
371
- value: pick.custom,
372
- 'aria-label': '自定义金额',
373
- oninput: (e) => { pick.custom = e.target.value.replace(/[^\d]/g, ''); pick.amount = null },
374
- }))),
375
- el('div', { class: 'row' },
376
- info.methods.length > 1
377
- ? el('select', {
378
- 'aria-label': '收款方式',
379
- onchange: (e) => { pick.method = e.target.value; render() },
380
- }, info.methods.map((m) => el('option', { value: m.type, selected: m.type === method }, m.name)))
381
- : el('span', { class: 'note', text: info.methods.length ? info.methods[0].name : '' }),
382
- el('span', { class: 'grow' }),
383
- el('button', {
384
- class: 'primary',
385
- disabled: !!pendingAct,
386
- onclick: () => pay(method),
387
- }, '去支付'))),
388
- el('p', { class: 'note', text: '付完 5 分钟内余额会自动刷新;订单与发票在站点的钱包页。' }),
389
- ]
390
- return card('充值', ...parts)
393
+ const methods = Array.isArray(info.methods) ? info.methods : []
394
+ const cur = info.currency || '¥'
395
+ const amount = pick.amount || Number(pick.custom || 0)
396
+ const method = pick.method || (methods[0] && methods[0].type) || ''
397
+ const payable = (v) => v >= 500 ? Math.round(v * 0.96) : v >= 200 ? Math.round(v * 0.98) : v
398
+ const saved = (v) => v - payable(v)
399
+ const head = el('div', { class: 'tp-head' },
400
+ el('div', { class: 'tp-badge', text: '¥' }),
401
+ el('div', { class: 'grow' },
402
+ el('h2', { text: '添加资金', style: 'margin:0' }),
403
+ el('p', { class: 'note', text: '选择金额和支付方式', style: 'margin:2px 0 0' })),
404
+ el('a', { class: 'ghost link', href: 'https://neartoken.cc/wallet', target: '_blank', rel: 'noreferrer' }, '订单历史 ↗'),
405
+ el('button', { class: 'primary', disabled: !!pendingAct || !amount, onclick: () => pay(method) }, '去支付'))
406
+ const tiles = el('div', { class: 'tp-tiles' }, options.map((v) => {
407
+ const on = v === amount && !pick.custom
408
+ const discount = v >= 500 ? '优惠 4%' : v >= 200 ? '优惠 2%' : ''
409
+ return el('button', {
410
+ class: 'tp-tile' + (on ? ' on' : ''), 'aria-pressed': String(on),
411
+ onclick: () => { pick.amount = v; pick.custom = ''; render() },
412
+ },
413
+ el('b', { text: String(v) }),
414
+ el('span', { class: 'tp-pay', text: discount ? cur + payable(v) + ' · 节省 ' + saved(v) : cur + v }),
415
+ discount ? el('span', { class: 'tp-off', text: discount }) : null)
416
+ }))
417
+ const custom = el('div', { class: 'tp-custom' },
418
+ el('label', { class: 'tp-label', text: '自定义金额' }),
419
+ el('div', { class: 'tp-custom-row' },
420
+ el('label', { class: 'field tp-field' },
421
+ el('span', { class: 'cur', text: cur }),
422
+ el('input', {
423
+ type: 'text', inputmode: 'numeric', placeholder: '输入金额', value: pick.custom,
424
+ 'aria-label': '自定义金额',
425
+ oninput: (e) => { pick.custom = e.target.value.replace(/[^\d]/g, ''); pick.amount = null; render() },
426
+ })),
427
+ el('div', { class: 'tp-due' }, el('span', { text: '待支付金额:' }), el('b', { text: cur + (amount || 0) }))))
428
+ const payMethods = el('div', null,
429
+ el('div', { class: 'tp-label', text: '付款方式' }),
430
+ el('div', { class: 'tp-methods' }, methods.map((m) => {
431
+ const on = m.type === method
432
+ return el('button', {
433
+ class: 'tp-method' + (on ? ' on' : ''), 'aria-pressed': String(on),
434
+ onclick: () => { pick.method = m.type; render() },
435
+ },
436
+ el('span', { class: 'tp-micon ' + (m.type === 'alipay' ? 'ali' : 'wx'), text: m.type === 'alipay' ? '支' : '微' }),
437
+ el('b', { text: m.name }))
438
+ })),
439
+ el('p', { class: 'note', text: '其他支付方式请联系站长', style: 'margin:6px 0 0' }))
440
+ const redeem = el('div', { class: 'tp-redeem' },
441
+ el('label', { class: 'tp-label', text: '🎁 有兑换码吗?' }),
442
+ el('div', { class: 'tp-custom-row' },
443
+ el('input', {
444
+ class: 'tp-input', type: 'text', placeholder: '输入您的兑换码',
445
+ oninput: (e) => { pick.redeem = e.target.value },
446
+ }),
447
+ el('button', { class: 'ghost', onclick: () => showNotice('兑换额度需要宿主提供兑换端点,可在站点钱包页兑换。') }, '兑换额度')))
448
+ const foot = el('p', { class: 'note', text: '付完 5 分钟内余额会自动刷新;订单与发票在站点的钱包页。' })
449
+ return card(null, head, tiles, custom, payMethods, redeem, foot)
391
450
  }
392
451
 
393
452
  /**
@@ -432,6 +491,10 @@
432
491
  let marketMode = 'standard'
433
492
  let marketSort = 'default'
434
493
  let marketLayout = 'grid'
494
+ let logQuery = ''
495
+ let logGroup = ''
496
+ let logKind = 'all'
497
+ let logExpanded = false
435
498
  const marketVendors = new Set()
436
499
  const MARKET_KINDS = [['all', '全部'], ['chat', '对话'], ['image', '生图'], ['video', '视频'], ['audio', '语音'], ['embedding', '向量']]
437
500
 
@@ -467,7 +530,14 @@
467
530
  ]
468
531
  const PRICE_TAGS = ['低价', '满血', '促销', '备用', '官Key', '官key', '国模', '自有', '反重力', '神秘折扣', 'CC', 'Kiro', '灵车', 'Argon', 'GPT Pro', 'Claude', 'DeepSeek', 'GLM', 'Gemini', 'Kimi', 'GPT', 'Suno', 'AIS']
469
532
 
470
- /** groupTags 把 [低价][生图] 生图特惠 拆成功能 / 价格 / 余下文字,供排序与重绘。 */
533
+ /** 第二级里的首标签次序:满血 > 官Key > 低价 > 其他。满血排在低价前面。 */
534
+ const LEAD_ORDER = ['满血', '官Key', '官key', '低价', '促销', '备用', '国模', '自有', '反重力', '神秘折扣']
535
+ const leadRank = (w) => {
536
+ const i = LEAD_ORDER.findIndex((k) => w.toLowerCase().includes(k.toLowerCase()))
537
+ return i === -1 ? 99 : i
538
+ }
539
+
540
+ /** groupTags 把 [低价][满血] GPT Pro 2 拆成功能 / 价格 / 余下文字,供排序与重绘。 */
471
541
  function groupTags(name) {
472
542
  const raw = String(name || '')
473
543
  const parts = (raw.match(/\[[^\]]+\]/g) || []).map((p) => p.slice(1, -1))
@@ -475,17 +545,21 @@
475
545
  const all = parts.concat(rest ? [rest] : []).join(' ')
476
546
  let func = '对话'
477
547
  for (const [label, re] of FUNC_TAGS) { if (re.test(all)) { func = label; break } }
478
- const price = parts.filter((p) => PRICE_TAGS.some((k) => p.toLowerCase().includes(k.toLowerCase())))
479
- return { func, price, rest }
548
+ // 官Key / 官key 走官方通道,自动补一个「满血」标签
549
+ const official = /官Key|官key/i.test(all)
550
+ let price = parts.filter((p) => PRICE_TAGS.some((k) => p.toLowerCase().includes(k.toLowerCase())))
551
+ if (official && !price.some((p) => p.includes('满血'))) price = ['满血'].concat(price)
552
+ price.sort((a, b) => leadRank(a) - leadRank(b))
553
+ return { func, price, rest, lead: price[0] || '' }
480
554
  }
481
555
 
482
- /** groupBadge 按「功能 → 价格 → 组名」两级重绘:功能标签在前、价格标签在后。 */
556
+ /** groupBadge 按「功能 → 价格」两级重绘;首标签单独加 .lead 作视觉区分。 */
483
557
  function groupBadge(name) {
484
558
  const t = groupTags(name)
485
559
  const tone = { '生图': 'pink', '视频': 'blue', '语音': 'purple', '向量': 'teal', '对话': 'plain' }
486
560
  const colorOf = (w) => {
487
- if (/低价|反重力|神秘|自有|国模/i.test(w)) return 'green'
488
561
  if (/满血|原生|特惠/i.test(w)) return 'red'
562
+ if (/低价|反重力|神秘|自有|国模/i.test(w)) return 'green'
489
563
  if (/促销|生图|Kiro|灵车/i.test(w)) return 'pink'
490
564
  if (/备用|官Key|官key|CC|Argon|GPT Pro|视频|向量/i.test(w)) return 'blue'
491
565
  return 'plain'
@@ -493,7 +567,10 @@
493
567
  return el('span', { class: 'gb' },
494
568
  // 功能标签只在非对话类显示;对话是默认,不重复挂
495
569
  t.func === '对话' ? null : el('span', { class: 'gb-tag lvl1 ' + (tone[t.func] || 'plain'), text: t.func }),
496
- t.price.map((w) => el('span', { class: 'gb-tag lvl2 ' + colorOf(w), text: w })),
570
+ t.price.map((w, i) => el('span', {
571
+ class: 'gb-tag lvl2 ' + colorOf(w) + (i === 0 ? ' lead' : ''),
572
+ text: w,
573
+ })),
497
574
  t.rest ? el('span', { class: 'gb-rest', text: t.rest }) : null)
498
575
  }
499
576
 
@@ -586,10 +663,29 @@
586
663
  el('a', { class: 'link', href: 'https://neartoken.cc/pricing', target: '_blank', rel: 'noreferrer' }, '详情 ›')))
587
664
  }
588
665
 
589
- /** 站点 /pricing 首屏的推荐顺序(抓自它的服务端渲染),默认排序时置顶。 */
590
- const SITE_FIRST = ['gpt-6-astra', 'gpt-6.1-sol', 'gpt-6-sol', 'gpt-6.1-terra', 'gpt-6-luna',
591
- 'claude-fable-5-1', 'claude-fable-5', 'claude-opus-5-5', 'claude-opus-5',
592
- 'claude-sonnet-5-5', 'claude-sonnet-5', 'glm-5.3']
666
+ /**
667
+ * 模型按系列归类:DeepSeek → GLM → Kimi → Qwen → GPT → Claude → Gemini
668
+ * → Grok → 生图 → 视频 → 语音 → 向量 → 其他。同系列挨在一起。
669
+ */
670
+ const SERIES = [
671
+ ['DeepSeek', /deepseek|ds-|kimseek/i],
672
+ ['GLM', /glm|智谱/i],
673
+ ['Kimi', /kimi|moonshot/i],
674
+ ['Qwen', /qwen|通义/i],
675
+ ['GPT', /gpt-|o[134]-|codex|sora/i],
676
+ ['Claude', /claude/i],
677
+ ['Gemini', /gemini/i],
678
+ ['Grok', /grok/i],
679
+ ['生图', /banana|seedream|flux|image|ideogram|z-image|qwen-image|dall/i],
680
+ ['视频', /veo|sd2|omni|seedance|minimax-h3|video|kling|sora/i],
681
+ ['语音', /tts|asr|suno|whisper|speech|lyrics|sound/i],
682
+ ['向量', /embedding|embed/i],
683
+ ]
684
+ function modelSeriesOf(name) {
685
+ const n = String(name || '').toLowerCase()
686
+ const i = SERIES.findIndex(([, re]) => re.test(n))
687
+ return i === -1 ? SERIES.length : i
688
+ }
593
689
 
594
690
  /** filteredModels 把搜索、分组、类型、厂商筛选叠起来。 */
595
691
  function filteredModels() {
@@ -597,12 +693,8 @@
597
693
  if (!p) return []
598
694
  let list = p.models.slice()
599
695
  if (marketSort !== 'latest') {
600
- // 默认按站点首屏顺序置顶,其余保持目录原序
601
- list.sort((a, b) => {
602
- const ia = SITE_FIRST.indexOf(a.name), ib = SITE_FIRST.indexOf(b.name)
603
- if (ia !== -1 || ib !== -1) return (ia === -1 ? 999 : ia) - (ib === -1 ? 999 : ib)
604
- return 0
605
- })
696
+ // 默认按系列归类(ds → glm → kimi → …),同系列内按名字排
697
+ list.sort((a, b) => modelSeriesOf(a.name) - modelSeriesOf(b.name) || a.name.localeCompare(b.name))
606
698
  }
607
699
  if (marketGroup) list = list.filter((m) => (m.enable_groups || []).includes(marketGroup))
608
700
  if (marketKind !== 'all') list = list.filter((m) => marketKindOf(m) === marketKind)
@@ -628,7 +720,17 @@
628
720
  /** marketCard 是广场整页:顶部工具条 + 左筛选 + 右卡片网格。 */
629
721
  function marketCard() {
630
722
  const p = pricing()
631
- const groups = Object.keys(p?.groups || {}).sort((a, b) => (p.groups[a] - p.groups[b]) || a.localeCompare(b, 'zh'))
723
+ // 先按功能分栏,栏内按首标签(满血 → 官Key → 低价 → …)再按倍率排
724
+ const FUNC_ORDER = ['对话', '生图', '视频', '语音', '向量']
725
+ // 分组栏:低价分组整体排上面(是列表里的上下位置,不是标签内前后权重),
726
+ // 其余按倍率升序。标签内部仍按「功能 → 满血/官Key → 低价」重绘。
727
+ const isLow = (g) => /低价/i.test(g)
728
+ const rankOf = (g) => {
729
+ const t = groupTags(g)
730
+ const f = FUNC_ORDER.indexOf(t.func)
731
+ return (isLow(g) ? 0 : 1) * 10000 + (f === -1 ? 99 : f) * 100 + (Number(p.groups[g]) || 0)
732
+ }
733
+ const groups = Object.keys(p?.groups || {}).sort((a, b) => rankOf(a) - rankOf(b) || a.localeCompare(b, 'zh'))
632
734
  const vendors = (p?.vendors || []).slice().sort((a, b) => String(a.name).localeCompare(String(b.name), 'zh'))
633
735
  const search = el('input', {
634
736
  class: 'market-search', type: 'search', placeholder: '搜索模型名称、供应商、端点或标签...',
@@ -658,28 +760,33 @@
658
760
  el('div', { class: 'market-seg' },
659
761
  el('button', { 'aria-pressed': String(marketLayout === 'grid'), title: '网格', onclick: () => { marketLayout = 'grid'; refreshMarketList() } }, '▦'),
660
762
  el('button', { 'aria-pressed': String(marketLayout === 'list'), title: '列表', onclick: () => { marketLayout = 'list'; refreshMarketList() } }, '☰')))
661
- // 分组按两级排:先功能(生图/视频/语音/向量/对话),再价格倍率
763
+ // 分组栏两级:低价分组整体排最上面,其余按功能分栏
764
+ const lowGroups = groups.filter(isLow)
765
+ const otherGroups = groups.filter((g) => !isLow(g))
662
766
  const byFunc = new Map()
663
- for (const g of groups) {
767
+ for (const g of otherGroups) {
664
768
  const f = groupTags(g).func
665
769
  if (!byFunc.has(f)) byFunc.set(f, [])
666
770
  byFunc.get(f).push(g)
667
771
  }
668
- const FUNC_ORDER = ['对话', '生图', '视频', '语音', '向量']
772
+ const chipOf = (g) => {
773
+ const ratio = Number(p.groups[g]) || 0
774
+ return el('button', {
775
+ class: 'market-chip' + (marketGroup === g ? ' on' : ''),
776
+ title: '按这个分组筛选',
777
+ onclick: () => { marketGroup = marketGroup === g ? '' : g; render() },
778
+ }, groupBadge(g), ratio > 0 ? el('span', { class: 'ratio', text: 'x' + ratioText(ratio) }) : null)
779
+ }
669
780
  const groupChips = el('div', { class: 'market-chips' },
670
781
  el('button', {
671
782
  class: 'market-chip' + (!marketGroup ? ' on' : ''), onclick: () => { marketGroup = ''; render() },
672
783
  }, '所有分组'),
784
+ lowGroups.length ? el('div', { class: 'chip-group' },
785
+ el('div', { class: 'chip-group-name low', text: '低价' }),
786
+ el('div', { class: 'market-chips' }, lowGroups.map(chipOf))) : null,
673
787
  FUNC_ORDER.filter((f) => byFunc.has(f)).map((f) => el('div', { class: 'chip-group' },
674
788
  el('div', { class: 'chip-group-name', text: f }),
675
- el('div', { class: 'market-chips' }, byFunc.get(f).map((g) => {
676
- const ratio = Number(p.groups[g]) || 0
677
- return el('button', {
678
- class: 'market-chip' + (marketGroup === g ? ' on' : ''),
679
- title: '按这个分组筛选',
680
- onclick: () => { marketGroup = marketGroup === g ? '' : g; render() },
681
- }, groupBadge(g), ratio > 0 ? el('span', { class: 'ratio', text: 'x' + ratioText(ratio) }) : null)
682
- })))))
789
+ el('div', { class: 'market-chips' }, byFunc.get(f).map(chipOf)))))
683
790
  const kinds = el('div', { class: 'market-chips' },
684
791
  MARKET_KINDS.map(([k, label]) => el('button', {
685
792
  class: 'market-chip' + (marketKind === k ? ' on' : ''),
@@ -720,7 +827,7 @@
720
827
  return wrap
721
828
  }
722
829
 
723
- /** 使用日志:这个窗口里每个模型花了多少、用了多少 token。 */
830
+ /** 使用日志:仿站点日志页——筛选条 + 统计条 + 全宽明细表。 */
724
831
  function logsCard() {
725
832
  const data = windows.get(span)
726
833
  const seg = el('div', { class: 'seg' },
@@ -729,42 +836,76 @@
729
836
  key === 'day' ? '日' : key === 'week' ? '周' : '月')))
730
837
  const head = el('div', { class: 'row' },
731
838
  el('h2', { class: 'grow', text: '使用日志', style: 'margin:0' }), seg)
732
- if (!data) return card(null, head, el('p', { class: 'note', text: '正在读取用量…' }))
839
+ if (!data) return el('section', { class: 'logs' }, card(null, head, el('p', { class: 'note', text: '正在读取用量…' })))
733
840
  if (!data.available) {
734
- return card(null, head, el('p', { class: 'note warn', text: data.reason === 'unreachable'
735
- ? '暂时读不到站点的用量数据。' : '站点未提供用量统计。' }))
841
+ return el('section', { class: 'logs' }, card(null, head,
842
+ el('p', { class: 'note warn', text: data.reason === 'unreachable'
843
+ ? '暂时读不到站点的用量数据。' : '站点未提供用量统计。' })))
736
844
  }
737
- const rows = (data.models || []).slice().sort((a, b) => b.tokens - a.tokens)
738
- const total = rows.reduce((sum, m) => sum + m.tokens, 0) || 1
739
- const table = el('div', { class: 'log-table' },
740
- el('div', { class: 'log-row head' },
741
- el('span', { text: '模型' }), el('span', { text: 'Token' }), el('span', { text: '花费' }), el('span', { text: '占比' })),
742
- rows.map((m) => el('div', { class: 'log-row' },
845
+ const all = (data.models || []).slice().sort((a, b) => b.tokens - a.tokens)
846
+ const total = all.reduce((sum, m) => sum + m.tokens, 0) || 1
847
+ const q = logQuery.trim().toLowerCase()
848
+ let rows = q ? all.filter((m) => String(m.model).toLowerCase().includes(q)) : all
849
+ if (logKind !== 'all') rows = rows.filter((m) => {
850
+ const snap = (pricing()?.models || []).find((x) => x.name === m.model)
851
+ return snap ? marketKindOf(snap) === logKind : logKind === 'chat'
852
+ })
853
+ if (logGroup) rows = rows.filter((m) => {
854
+ const snap = (pricing()?.models || []).find((x) => x.name === m.model)
855
+ return snap && (snap.enable_groups || []).includes(logGroup)
856
+ })
857
+ const groups = Object.keys(pricing()?.groups || {}).sort((a, b) => a.localeCompare(b, 'zh'))
858
+ const rangeText = span === 'day' ? '今日' : span === 'week' ? '本周' : '本月'
859
+ const toolbar = el('div', { class: 'logs-toolbar' },
860
+ el('div', { class: 'lt-field grow' }, el('span', { class: 'lt-ico', text: '▦' }),
861
+ el('span', { class: 'lt-range', text: rangeText + ' · 00:00 ~ 现在' })),
862
+ el('input', {
863
+ class: 'lt-input', type: 'search', placeholder: '模型名称', value: logQuery,
864
+ oninput: (e) => { logQuery = e.target.value; render() },
865
+ }),
866
+ el('select', { class: 'lt-select', onchange: (e) => { logGroup = e.target.value; render() } },
867
+ el('option', { value: '', text: '分组' }),
868
+ groups.map((g) => el('option', { value: g, text: g, selected: logGroup === g }))),
869
+ el('select', { class: 'lt-select', onchange: (e) => { logKind = e.target.value; render() } },
870
+ el('option', { value: 'all', text: '所有类型' }),
871
+ MARKET_KINDS.slice(1).map(([k, label]) => el('option', { value: k, text: label, selected: logKind === k }))),
872
+ el('button', { class: 'lt-btn', onclick: () => { logExpanded = !logExpanded; render() } }, logExpanded ? '收起 ▴' : '展开 ▾'))
873
+ const statbar = el('div', { class: 'logs-statbar' },
874
+ el('div', { class: 'ls-chips' },
875
+ el('span', { class: 'ls-chip' }, el('i', { class: 'ls-dot blue' }), '用量 ', el('b', { text: data.spent || '—' })),
876
+ el('span', { class: 'ls-chip' }, el('i', { class: 'ls-dot red' }), 'RPM ', el('b', { text: '0' })),
877
+ el('span', { class: 'ls-chip' }, el('i', { class: 'ls-dot blue' }), 'TPM ', el('b', { text: compact(data.tokens || 0) }))),
878
+ el('div', { class: 'ls-actions' },
879
+ el('button', { class: 'lt-btn', title: '10 秒刷新', onclick: () => { load().catch(() => {}) } }, '↻ 10s'),
880
+ el('button', { class: 'lt-btn', onclick: () => { logQuery = ''; logGroup = ''; logKind = 'all'; render() } }, '重置'),
881
+ el('button', { class: 'lt-btn primary', onclick: () => render() }, '搜索'),
882
+ el('button', { class: 'lt-btn', onclick: () => showError('逐条导出需要宿主提供日志端点。') }, '导出')))
883
+ const cols = ['时间', '令牌', '模型', '流', 'Token', '请求体大小', '费用', '耗时', '详情']
884
+ const table = el('div', { class: 'logs-table' },
885
+ el('div', { class: 'lt-row head' }, cols.map((c) => el('span', { text: c }))),
886
+ rows.length ? rows.map((m) => el('div', { class: 'lt-row' },
887
+ el('span', { text: rangeText }),
888
+ el('span', { text: view.tokenName || '—' }),
743
889
  el('span', { class: 'who', title: m.model, text: m.model }),
890
+ el('span', { text: '—' }),
744
891
  el('span', { class: 'num', text: compact(m.tokens) }),
745
- el('span', { class: 'num', text: m.costText || '' }),
746
- el('span', { class: 'num', text: Math.round((m.tokens / total) * 100) + '%' }))))
747
- const names = rows.slice(0, 8)
748
- const palette = ['#5b8def', '#43b8a4', '#9b7ce0', '#54b26a', '#e0a44a', '#d47a9a', '#6fa8d6', '#c08bd0']
749
- const dist = el('div', { class: 'log-dist' },
750
- el('h3', { text: '按模型分布' }),
751
- names.map((m, i) => el('div', { class: 'dist-row' },
892
+ el('span', { text: '—' }),
893
+ el('span', { class: 'num', text: m.costText || '—' }),
894
+ el('span', { text: '—' }),
895
+ el('span', { class: 'num', text: Math.round((m.tokens / total) * 100) + '%' })))
896
+ : el('div', { class: 'lt-empty', text: '这个筛选下没有记录。' }))
897
+ // 分布不再是下面的一张卡:折进「展开」,默认不占版面
898
+ const dist = logExpanded ? el('div', { class: 'logs-dist' },
899
+ rows.slice(0, 8).map((m, i) => el('div', { class: 'dist-row' },
752
900
  el('div', { class: 'dist-name', title: m.model, text: m.model }),
753
901
  el('div', { class: 'dist-track' },
754
- el('i', { style: 'width:' + Math.max(2, Math.round((m.tokens / total) * 100)) + '%;background:' + palette[i % palette.length] })),
755
- el('div', { class: 'dist-val', text: compact(m.tokens) + ' · ' + Math.round((m.tokens / total) * 100) + '%' }))))
902
+ el('i', { style: 'width:' + Math.max(2, Math.round((m.tokens / total) * 100)) + '%;background:' + ['#5b8def', '#43b8a4', '#9b7ce0', '#54b26a', '#e0a44a', '#d47a9a', '#6fa8d6', '#c08bd0'][i % 8] })),
903
+ el('div', { class: 'dist-val', text: compact(m.tokens) + ' · ' + Math.round((m.tokens / total) * 100) + '%' })))) : null
756
904
  return el('section', { class: 'logs' },
757
905
  el('div', { class: 'logs-head' }, head),
758
- el('div', { class: 'logs-body' },
759
- el('div', { class: 'card' },
760
- el('div', { class: 'stats' },
761
- el('div', { class: 'stat' }, el('div', { class: 'k', text: '消耗' }), el('div', { class: 'v', text: data.spent })),
762
- el('div', { class: 'stat' }, el('div', { class: 'k', text: '当前余额' }), el('div', { class: 'v', text: data.balance || '—' })),
763
- el('div', { class: 'stat' }, el('div', { class: 'k', text: 'Token' }), el('div', { class: 'v', text: compact(data.tokens) }))),
764
- table,
765
- el('p', { class: 'note', text: '数字来自站点用量端点;这个窗口里没有调用就不列模型。' })),
766
- el('div', { class: 'card' }, dist,
767
- el('p', { class: 'note', text: '按 token 占比排,最多列前 8 个模型。' }))))
906
+ toolbar, statbar, dist,
907
+ el('div', { class: 'logs-table-wrap' }, table),
908
+ el('p', { class: 'note', text: '数字来自站点用量端点,按模型汇总;站点未提供逐条流水,故「流 / 请求体大小 / 耗时」留空。' }))
768
909
  }
769
910
  /** toggleModel 翻转一个模型的启用状态,并写回 plugin options。 */
770
911
  async function toggleModel(name) {
@@ -785,15 +926,35 @@
785
926
  await saveDisabled()
786
927
  }
787
928
 
788
- /** saveDisabled 把「关掉的模型」写进 plugin options,provider 插件按它过滤清单。 */
789
- async function saveDisabled() {
929
+ /**
930
+ * saveDisabled 把「关掉的模型」写进 plugin options,provider 插件按它过滤清单。
931
+ *
932
+ * magpie 限制页面每秒最多写一次 options,连点卡片会被拒("a page sets its
933
+ * options once a second at most")。所以这里合并写入:1.1 秒内的多次改动只发
934
+ * 最后一次,正在发送时再排队补一次;限流错误不往页脚挂。
935
+ */
936
+ let saveTimer = null
937
+ let sending = false
938
+ let sendAgain = false
939
+ async function flushDisabled() {
940
+ if (sending) { sendAgain = true; return }
941
+ sending = true
790
942
  try {
791
943
  const next = await bridge.options.set({ ...options, disabledModels: [...disabledModels] })
792
944
  if (next && typeof next === 'object') options = next
793
- } catch (error) {
794
- showError(error)
945
+ lastError = ''
946
+ } catch {
947
+ // 写失败时保留本地状态,下一拍重试;不把限流提示挂到页脚
948
+ sendAgain = true
949
+ } finally {
950
+ sending = false
951
+ if (sendAgain) { sendAgain = false; setTimeout(flushDisabled, 1100) }
795
952
  }
796
953
  }
954
+ function saveDisabled() {
955
+ if (saveTimer) clearTimeout(saveTimer)
956
+ saveTimer = setTimeout(() => { saveTimer = null; flushDisabled() }, 1150)
957
+ }
797
958
 
798
959
  /** ratioText 是倍率的写法:0.1 → 0.1,1 → 1。 */
799
960
  function ratioText(v) {
package/page/groups.js CHANGED
@@ -1,2 +1,2 @@
1
- self.NEARTOKEN_GROUPS = [{"name":"[低价] 灵车 K3.1 Pro 专用","ratio":0.3,"models":1,"ref":"#49"},{"name":"[低价] 神秘折扣模型","ratio":0.12,"models":7,"ref":"#24"},{"name":"[低价] 视频分组","ratio":1.5,"models":2,"ref":"#31"},{"name":"[低价] 语音合成 TTS","ratio":0.3,"models":5,"ref":"#35"},{"name":"[低价] 语音转文字 ASR","ratio":1,"models":1,"ref":"#27"},{"name":"[低价] DeepSeek","ratio":0.4,"models":3,"ref":"#16"},{"name":"[低价] GPT 福利","ratio":0.2,"models":6,"ref":"#56"},{"name":"[低价] Grok","ratio":0.1,"models":13,"ref":"#37"},{"name":"[低价] Seedance","ratio":1,"models":2,"ref":"#19"},{"name":"[低价][备用] GPT Pro","ratio":0.3,"models":8,"ref":"#13"},{"name":"[低价][促销] GPT Image","ratio":0.5,"models":2,"ref":"#39"},{"name":"[低价][反重力] Gemini","ratio":0.5,"models":18,"ref":"#5"},{"name":"[低价][满血] 国模","ratio":0.6,"models":9,"ref":"#6"},{"name":"[低价][满血] Claude 特惠","ratio":1.1,"models":6,"ref":"#22"},{"name":"[低价][满血] Claude 原生","ratio":1.5,"models":6,"ref":"#61"},{"name":"[低价][满血] DeepSeek 1017","ratio":0.5,"models":2,"ref":"#38"},{"name":"[低价][满血] GPT Pro 2","ratio":0.26,"models":8,"ref":"#14"},{"name":"[低价][满血][自有] GLM 官方","ratio":0.25,"models":2,"ref":"#17"},{"name":"[低价][生图] 生图特惠","ratio":0.2,"models":14,"ref":"#9"},{"name":"[低价][Kiro] 高智企业 Claude","ratio":1,"models":6,"ref":"#42"},{"name":"[官key] GPT","ratio":0.9,"models":5,"ref":"#28"},{"name":"[满血] Argon 专用","ratio":1,"models":1,"ref":"#47"},{"name":"[满血] CC 不降智精选","ratio":1.8,"models":10,"ref":"#7"},{"name":"[满血] GPT 不降智精选","ratio":0.35,"models":8,"ref":"#2"},{"name":"[满血] GPT Pro 500 高速","ratio":0.5,"models":2,"ref":"#57"},{"name":"[满血] Kimi K3.1 Pro 专用","ratio":0.6,"models":1,"ref":"#48"},{"name":"[满血] Suno","ratio":1,"models":5,"ref":"#67"},{"name":"[满血][官Key] DeepSeek","ratio":0.8,"models":2,"ref":"#15"},{"name":"[满血][官Key] Gemini","ratio":2,"models":6,"ref":"#8"},{"name":"[生图] Gemini","ratio":1,"models":7,"ref":"#29"},{"name":"[生图] GPT Image","ratio":1,"models":2,"ref":"#4"},{"name":"[AIS] Gemini 向量模型","ratio":1,"models":3,"ref":"#34"}]
2
- self.NEARTOKEN_GROUPS_AT = 1793780165230
1
+ // 由插件在拉取目录时覆盖写入;这里只是占位,免得页面加载时报 404。
2
+ self.NEARTOKEN_GROUPS = self.NEARTOKEN_GROUPS || null
package/page/latest.js CHANGED
@@ -1,2 +1,2 @@
1
- self.NEARTOKEN_LATEST = "0.3.0"
2
- self.NEARTOKEN_LATEST_AT = 1793779995515
1
+ // 由插件在拉取目录时覆盖写入;这里只是占位。
2
+ self.NEARTOKEN_LATEST = self.NEARTOKEN_LATEST || ''
package/page/market.css CHANGED
@@ -97,10 +97,55 @@
97
97
  /* 两级标签:功能在前(饱和色),价格/品质在后(柔和色) */
98
98
  .gb-tag.lvl1 { font-weight: 700; box-shadow: 0 0 0 1px rgba(255,255,255,.12); }
99
99
  .gb-tag.lvl2 { opacity: .82; }
100
+ .gb-tag.lead { font-weight: 700; opacity: 1; box-shadow: 0 0 0 1px rgba(255,255,255,.32); }
100
101
  .gb-tag.purple { background: #7b52b8; }
101
102
  .gb-tag.teal { background: #2f8f8a; }
103
+ .chip-group-name.low { color: #4cc38a; font-weight: 700; font-size: 11px; }
102
104
  .chip-group { width: 100%; margin: 2px 0 4px; }
103
105
  .chip-group-name { font-size: 10px; color: var(--nt-faint); margin: 6px 0 3px; letter-spacing: .04em; }
106
+
107
+ /* 充值卡:站点「添加资金」缩小版 */
108
+ .tp-head { display: flex; align-items: center; gap: 10px; padding-bottom: 8px; border-bottom: 1px solid var(--nt-line); margin-bottom: 8px; }
109
+ .tp-badge { width: 28px; height: 28px; border-radius: 9px; display: grid; place-items: center; background: rgba(76,195,138,.14); color: #4cc38a; font-size: 15px; font-weight: 700; }
110
+ .tp-head h2 { font-size: 14px; }
111
+ .tp-tiles { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; }
112
+ .tp-tile {
113
+ position: relative; display: flex; flex-direction: column; align-items: flex-start; gap: 1px;
114
+ min-height: 48px; padding: 6px 9px; text-align: left;
115
+ border: 1px solid var(--nt-line); border-radius: 9px; background: rgba(255,255,255,.02); color: var(--nt-ink);
116
+ }
117
+ .tp-tile b { font-size: 16px; font-variant-numeric: tabular-nums; }
118
+ .tp-tile .tp-pay { font-size: 9.5px; color: var(--nt-muted); }
119
+ .tp-tile .tp-off { position: absolute; right: 6px; top: 6px; font-size: 9px; color: #4cc38a; }
120
+ .tp-tile[aria-pressed="true"] { border-color: var(--nt-accent); background: var(--nt-accent-soft); box-shadow: inset 0 0 0 1px var(--nt-accent); }
121
+ .tp-label { display: block; font-size: 10px; color: var(--nt-muted); margin: 8px 0 4px; }
122
+ .tp-custom-row { display: flex; align-items: center; gap: 8px; }
123
+ .tp-field { height: 32px; }
124
+ .tp-field input { width: 100%; font-size: 12px; }
125
+ .tp-due { flex: 1; display: flex; align-items: center; justify-content: space-between; gap: 8px; height: 32px; padding: 0 9px; border: 1px solid var(--nt-line); border-radius: 9px; font-size: 10.5px; color: var(--nt-muted); }
126
+ .tp-due b { color: var(--nt-ink); font-family: var(--nt-code); font-size: 12px; }
127
+ .tp-methods { display: flex; gap: 8px; flex-wrap: wrap; }
128
+ .tp-method { display: inline-flex; align-items: center; gap: 8px; height: 34px; padding: 0 12px; border: 1px solid var(--nt-line); border-radius: 9px; background: rgba(255,255,255,.02); color: var(--nt-ink); font-size: 11.5px; }
129
+ .tp-method[aria-pressed="true"] { border-color: var(--nt-accent); background: var(--nt-accent-soft); }
130
+ .tp-micon { width: 20px; height: 20px; border-radius: 5px; display: grid; place-items: center; color: #fff; font-size: 10px; font-weight: 700; }
131
+ .tp-micon.ali { background: #1677ff; }
132
+ .tp-micon.wx { background: #07c160; }
133
+ .tp-redeem { border-top: 1px solid var(--nt-line); margin-top: 8px; padding-top: 2px; }
134
+ .tp-input { flex: 1; height: 32px; padding: 0 9px; border: 1px solid var(--nt-line); border-radius: 9px; background: rgba(0,0,0,.2); color: var(--nt-ink); font-size: 11.5px; }
135
+ .tp-redeem .ghost { height: 32px; }
136
+
137
+ /* 推荐计划长条卡 */
138
+ .rf { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
139
+ .rf-ico { width: 32px; height: 32px; border-radius: 9px; display: grid; place-items: center; background: rgba(155,124,224,.16); color: #9b7ce0; font-size: 16px; }
140
+ .rf-text { flex: 1 1 240px; }
141
+ .rf-text h2 { font-size: 13px; }
142
+ .rf-invites { text-align: center; min-width: 54px; }
143
+ .rf-invites .k { font-size: 10px; color: var(--nt-muted); }
144
+ .rf-invites .v { font-size: 17px; font-weight: 600; font-variant-numeric: tabular-nums; }
145
+ .rf-link { flex: 1 1 260px; min-width: 0; display: flex; align-items: center; height: 36px; padding: 0 12px; border: 1px solid var(--nt-line); border-radius: 10px; background: rgba(0,0,0,.2); overflow: hidden; }
146
+ .rf-link span { font-family: var(--nt-code); font-size: 11.5px; color: var(--nt-ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
147
+ .rf-copy { width: 36px; height: 36px; border: 1px solid var(--nt-accent); border-radius: 10px; background: var(--nt-accent-soft); color: var(--nt-accent); font-size: 15px; }
148
+ @media (max-width: 620px) { .tp-tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
104
149
  .mc-image { border-color: rgba(139,79,158,.5); }
105
150
  .mc-video { border-color: rgba(214,140,60,.5); }
106
151
  .mc-audio { border-color: rgba(63,111,168,.5); }
@@ -147,20 +192,63 @@
147
192
  .mc-bar i.bad { background: #d08a3c; }
148
193
  .mc-foot { margin-top: 8px; text-align: right; }
149
194
 
150
- @media (max-width: 900px) {
151
- .market-body { grid-template-columns: 1fr; }
152
- .market-aside { position: static; max-height: none; }
153
- .logs-body { grid-template-columns: 1fr; }
154
- }
155
-
156
- /* 使用日志:两栏铺满 */
195
+ /* 使用日志:仿站点日志页,筛选条 + 统计条 + 全宽明细表 */
157
196
  .logs { width: 100%; }
158
- .logs-head > .row { padding: 12px 14px; border: 1px solid var(--nt-line); border-radius: 12px; margin-bottom: 12px; }
159
- .logs-body { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); gap: 12px; align-items: start; }
160
- .logs-body > .card { margin: 0; }
161
- .log-dist h3 { font-size: 12px; margin: 0 0 12px; }
162
- .dist-row { display: grid; grid-template-columns: 110px minmax(0, 1fr) 92px; gap: 8px; align-items: center; margin: 8px 0; font-size: 10.5px; }
197
+ .logs-head > .row { padding: 10px 12px; border: 1px solid var(--nt-line); border-radius: 12px; margin-bottom: 10px; }
198
+
199
+ .logs-toolbar {
200
+ display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
201
+ padding: 10px 12px; border: 1px solid var(--nt-line); border-radius: 12px; margin-bottom: 8px;
202
+ background: rgba(255,255,255,.02);
203
+ }
204
+ .lt-field { display: inline-flex; align-items: center; gap: 8px; min-width: 200px; height: 32px; padding: 0 10px;
205
+ border: 1px solid var(--nt-line); border-radius: 9px; color: var(--nt-ink); font-size: 11.5px; }
206
+ .lt-ico { color: var(--nt-muted); }
207
+ .lt-input { height: 32px; flex: 1 1 160px; min-width: 120px; padding: 0 10px;
208
+ border: 1px solid var(--nt-line); border-radius: 9px; background: rgba(0,0,0,.2);
209
+ color: var(--nt-ink); font-size: 11.5px; }
210
+ .lt-input::placeholder { color: var(--nt-faint); }
211
+ .lt-select { height: 32px; flex: 0 1 150px; padding: 0 8px;
212
+ border: 1px solid var(--nt-line); border-radius: 9px; background: #1c2027;
213
+ color: var(--nt-ink); font-size: 11.5px; }
214
+ .lt-btn { height: 32px; padding: 0 11px; border: 1px solid var(--nt-line); border-radius: 9px;
215
+ background: transparent; color: var(--nt-ink); font-size: 11.5px; white-space: nowrap; }
216
+ .lt-btn:hover { border-color: var(--nt-accent); }
217
+ .lt-btn.primary { background: var(--nt-accent); border-color: var(--nt-accent); color: #fff; }
218
+
219
+ .logs-statbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
220
+ padding: 8px 12px; border: 1px solid var(--nt-line); border-radius: 12px; margin-bottom: 10px; }
221
+ .ls-chips { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; flex: 1; }
222
+ .ls-chip { display: inline-flex; align-items: center; gap: 5px; height: 26px; padding: 0 10px;
223
+ border: 1px solid var(--nt-line); border-radius: 999px; font-size: 10.5px; color: var(--nt-muted); }
224
+ .ls-chip b { color: var(--nt-ink); font-family: var(--nt-code); font-variant-numeric: tabular-nums; }
225
+ .ls-dot { width: 2px; height: 12px; border-radius: 1px; display: inline-block; }
226
+ .ls-dot.blue { background: #5b8def; }
227
+ .ls-dot.red { background: #d05a6a; }
228
+ .ls-actions { display: flex; align-items: center; gap: 6px; }
229
+
230
+ .logs-table-wrap { width: 100%; border: 1px solid var(--nt-line); border-radius: 12px; overflow: hidden; }
231
+ .logs-table { width: 100%; }
232
+ .lt-row { display: grid; grid-template-columns: 90px 90px minmax(140px, 1.6fr) 70px 100px 110px 100px 80px 70px;
233
+ gap: 8px; align-items: center; padding: 9px 12px; border-bottom: 1px solid var(--nt-line); font-size: 11px; }
234
+ .lt-row:last-child { border-bottom: 0; }
235
+ .lt-row.head { color: var(--nt-muted); font-size: 10.5px; background: rgba(255,255,255,.02); }
236
+ .lt-row .who { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--nt-ink); }
237
+ .lt-row .num { text-align: right; font-family: var(--nt-code); font-variant-numeric: tabular-nums; color: var(--nt-muted); }
238
+ .lt-row > span:not(.who):not(.num) { color: var(--nt-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
239
+ .lt-empty { padding: 26px; text-align: center; color: var(--nt-muted); font-size: 11.5px; }
240
+
241
+ .logs-dist { padding: 10px 12px; border: 1px solid var(--nt-line); border-radius: 12px; margin-bottom: 10px; }
242
+ .dist-row { display: grid; grid-template-columns: 140px minmax(0, 1fr) 110px; gap: 8px; align-items: center; margin: 8px 0; font-size: 10.5px; }
163
243
  .dist-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--nt-muted); }
164
244
  .dist-track { height: 8px; background: rgba(255,255,255,.05); border-radius: 5px; overflow: hidden; }
165
245
  .dist-track i { display: block; height: 100%; border-radius: 5px; }
166
- .dist-val { text-align: right; font-family: var(--nt-code); color: var(--nt-muted); font-variant-numeric: tabular-nums; }
246
+ .dist-val { text-align: right; font-family: var(--nt-code); color: var(--nt-muted); font-variant-numeric: tabular-nums; }
247
+ .logs > .note { margin: 8px 2px 0; }
248
+
249
+ @media (max-width: 900px) {
250
+ .market-body { grid-template-columns: 1fr; }
251
+ .market-aside { position: static; max-height: none; }
252
+ .lt-row { grid-template-columns: 80px minmax(120px, 1.5fr) 90px 90px 60px; }
253
+ .lt-row > span:nth-child(2), .lt-row > span:nth-child(4), .lt-row > span:nth-child(6), .lt-row > span:nth-child(8) { display: none; }
254
+ }
package/page/style.css CHANGED
@@ -42,10 +42,18 @@ html, body {
42
42
  padding: 20px 22px 48px;
43
43
  }
44
44
 
45
- /* 首页铺满:两张卡一行,随窗口宽度伸缩 */
45
+ /* 首页铺满:两张卡一行,随窗口宽度伸缩;以账户卡为基准等高锁定,
46
+ 内容超出时卡片内部滚动,不把整页撑长。 */
46
47
  .home-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; align-items: start; }
47
- .home-grid > .card { margin: 0; }
48
- @media (max-width: 720px) { .home-grid { grid-template-columns: 1fr; } }
48
+ .home-grid > .card { margin: 0; overflow-y: auto; scrollbar-width: thin; }
49
+ .home-grid > .card.home-wide {
50
+ grid-column: 1 / -1;
51
+ overflow: visible; /* 长条卡不参与等高锁死 */
52
+ height: auto !important;
53
+ }
54
+ .home-grid > .card::-webkit-scrollbar { width: 6px; }
55
+ .home-grid > .card::-webkit-scrollbar-thumb { background: var(--nt-line); border-radius: 3px; }
56
+ @media (max-width: 720px) { .home-grid { grid-template-columns: 1fr; } .home-grid > .card { height: auto !important; } }
49
57
 
50
58
  /* 顶部:品牌 + 连接状态。整页只这里允许一点装饰。 */
51
59
  .head {
package/page/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // 由 scripts/stamp.mjs 生成,别手改:版本号在 package.json。
2
- self.NEARTOKEN_VERSION = "0.4.0"
2
+ self.NEARTOKEN_VERSION = "0.5.0"