@fish-under-sea/magpie-neartoken 0.1.9 → 0.2.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/README.md CHANGED
@@ -41,6 +41,16 @@
41
41
 
42
42
  模型目录读的是站点的 `/api/pricing`(公开、全量),不用 `/v1/models`——那个端点按令牌分组过滤,很多账号拿到的是空列表,magpie 的「重新获取模型」就会报 `no models listed`。这里不碰它。
43
43
 
44
+ ## 三个视角
45
+
46
+ 页面上方三个标签:
47
+
48
+ | 标签 | 内容 |
49
+ | --- | --- |
50
+ | 原本界面 | 账户、余额与代金券、消耗看板、分组开关、充值、供应商说明 |
51
+ | 模型广场 | 按站点返回的分组重绘:低价 / 满血 / 图片 / 视频 / 语音 / 向量,每组一枚可点的开关(关掉即从 magpie 的模型列表里移除) |
52
+ | 使用日志 | 这个窗口里每个模型花了多少、用了多少 token、占比多少,可按日 / 周 / 月切 |
53
+
44
54
  ## 页面怎么用
45
55
 
46
56
  1. 装上包,侧边栏「插件」→「NearToken」。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fish-under-sea/magpie-neartoken",
3
- "version": "0.1.9",
3
+ "version": "0.2.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
@@ -21,6 +21,8 @@
21
21
  /** 插件自己的开关:关掉的分组(写进 magpie 的 plugin options,provider 插件按它过滤)。 */
22
22
  let options = {}
23
23
  let disabledGroups = new Set()
24
+ /** 当前视角:home / market / logs。 */
25
+ let tab = 'home'
24
26
  const pick = { amount: null, method: null, custom: '' }
25
27
  /** 正在等宿主的哪个动作。 */
26
28
  let pendingAct = ''
@@ -95,6 +97,9 @@
95
97
  // ---------- 主流程 ----------
96
98
 
97
99
  async function boot() {
100
+ // 预览用:?tab=market 可直接落在某个视角,生产里没这个参数就是原本界面
101
+ const want = new URLSearchParams(location.search).get('tab')
102
+ if (want === 'market' || want === 'logs' || want === 'home') tab = want
98
103
  if (!bridge || typeof bridge.call !== 'function') {
99
104
  fail('这个 magpie 还没有插件页的调用通道。')
100
105
  return
@@ -171,7 +176,10 @@
171
176
  frag.append(pendingCard())
172
177
  } else if (view.configured) {
173
178
  setStatus('已连接', 'on')
174
- frag.append(accountCard(), boardCard(), groupsCard(), topupCard(), providerCard())
179
+ frag.append(tabsBar())
180
+ if (tab === 'market') frag.append(marketCard())
181
+ else if (tab === 'logs') frag.append(logsCard())
182
+ else frag.append(accountCard(), boardCard(), groupsCard(), topupCard(), providerCard())
175
183
  } else {
176
184
  setStatus('未连接')
177
185
  frag.append(connectCard())
@@ -183,6 +191,25 @@
183
191
  if (view.pending) paintCountdown()
184
192
  }
185
193
 
194
+ /** tabsBar 是三个视角:原本界面、模型广场、使用日志。 */
195
+ function tabsBar() {
196
+ const tabs = [['home', '原本界面'], ['market', '模型广场'], ['logs', '使用日志']]
197
+ return el('div', { class: 'tabs', role: 'tablist' },
198
+ tabs.map(([key, label]) => el('button', {
199
+ role: 'tab',
200
+ 'aria-selected': String(tab === key),
201
+ class: tab === key ? 'on' : '',
202
+ onclick: () => {
203
+ if (tab === key) return
204
+ tab = key
205
+ render()
206
+ if (key !== 'home' && view.configured && !windows.has(span)) {
207
+ call('neartoken:usage', { range: span }).then((w) => { windows.set(span, w); render() }).catch(showError)
208
+ }
209
+ },
210
+ }, label)))
211
+ }
212
+
186
213
  /** 未连接:说清会发生什么,再给一个按钮。 */
187
214
  function connectCard() {
188
215
  return card('连接中转站',
@@ -340,6 +367,103 @@
340
367
  return card('充值', ...parts)
341
368
  }
342
369
 
370
+ /** 分组落在哪一类:低价 / 满血 / 图片 / 视频 / 语音 / 向量 / 其他。 */
371
+ function bucketOf(name) {
372
+ const n = String(name || '')
373
+ if (/生图|图片|image|画/i.test(n)) return '图片'
374
+ if (/视频|video/i.test(n)) return '视频'
375
+ if (/语音|tts|asr|转写|whisper|speech/i.test(n)) return '语音'
376
+ if (/向量|embedding/i.test(n)) return '向量'
377
+ if (/低价/.test(n)) return '低价'
378
+ if (/满血|官key|pro/i.test(n)) return '满血'
379
+ return '其他'
380
+ }
381
+ const BUCKET_ORDER = ['低价', '满血', '图片', '视频', '语音', '向量', '其他']
382
+
383
+ /** 模型广场:按站点返回的分组重绘,分组即为开关。 */
384
+ function marketCard() {
385
+ const list = Array.isArray(view.groupList) ? view.groupList : null
386
+ const head = el('div', { class: 'row' },
387
+ el('h2', { class: 'grow', text: '分组', style: 'margin:0' }),
388
+ el('a', { class: 'link', href: 'https://neartoken.cc/pricing', target: '_blank', rel: 'noreferrer' }, '站点广场 ↗'))
389
+ if (!list || !list.length) {
390
+ return card(null, head,
391
+ el('p', { class: 'note' },
392
+ '宿主还没把分组明细交给页面(`neartoken:state` 目前只回分组数)。'),
393
+ el('p', { class: 'note' },
394
+ '开关已接好:写进 magpie 的 plugin options,provider 插件按它过滤模型表;',
395
+ '宿主一回 groupList,这里就按站点分组重绘。先去 ',
396
+ el('a', { class: 'link', href: 'https://neartoken.cc/pricing', target: '_blank', rel: 'noreferrer' }, '站点广场'),
397
+ ' 看分组与倍率。'))
398
+ }
399
+ const buckets = new Map()
400
+ for (const g of list) {
401
+ const key = bucketOf(g?.name)
402
+ if (!buckets.has(key)) buckets.set(key, [])
403
+ buckets.get(key).push(g)
404
+ }
405
+ const parts = [head]
406
+ for (const key of BUCKET_ORDER) {
407
+ const groups = buckets.get(key)
408
+ if (!groups || !groups.length) continue
409
+ const chips = groups.map((g) => groupChip(g))
410
+ parts.push(el('div', { class: 'bucket' },
411
+ el('div', { class: 'bucket-name' }, key, el('span', { class: 'n', text: String(groups.length) })),
412
+ el('div', { class: 'groups' }, chips)))
413
+ }
414
+ const off = disabledGroups.size
415
+ parts.push(el('p', { class: 'note', text: off
416
+ ? '已关掉 ' + off + ' 个分组:它们的模型不再出现在 magpie 的模型列表里。'
417
+ : '全部开着。点一下关掉一组,它的模型就不再出现在 magpie 的模型列表里。' }))
418
+ return card(null, ...parts)
419
+ }
420
+
421
+ /** groupChip 是一枚分组开关:站点原文组名 + 倍率 + 模型数。 */
422
+ function groupChip(g) {
423
+ const name = String(g?.name ?? '')
424
+ const on = !disabledGroups.has(name)
425
+ const ratio = Number(g?.ratio) || 0
426
+ const label = ratio > 0 ? name + ' ' + ratioText(ratio) + 'x' : name
427
+ return el('button', {
428
+ class: 'group-chip',
429
+ 'aria-pressed': String(on),
430
+ title: g?.models ? g.models + ' 个模型' : '',
431
+ onclick: () => toggleGroup(name),
432
+ }, label, el('span', { class: 'n', text: String(g?.models || 0) }))
433
+ }
434
+
435
+ /** 使用日志:这个窗口里每个模型花了多少、用了多少 token。 */
436
+ function logsCard() {
437
+ const data = windows.get(span)
438
+ const seg = el('div', { class: 'seg' },
439
+ ['day', 'week', 'month'].map((key) =>
440
+ el('button', { 'aria-pressed': String(span === key), onclick: () => switchSpan(key) },
441
+ key === 'day' ? '日' : key === 'week' ? '周' : '月')))
442
+ const head = el('div', { class: 'row' },
443
+ el('h2', { class: 'grow', text: '使用日志', style: 'margin:0' }), seg)
444
+ if (!data) return card(null, head, el('p', { class: 'note', text: '正在读取用量…' }))
445
+ if (!data.available) {
446
+ return card(null, head, el('p', { class: 'note warn', text: data.reason === 'unreachable'
447
+ ? '暂时读不到站点的用量数据。' : '站点未提供用量统计。' }))
448
+ }
449
+ const rows = (data.models || []).slice().sort((a, b) => b.tokens - a.tokens)
450
+ const total = rows.reduce((sum, m) => sum + m.tokens, 0) || 1
451
+ const table = el('div', { class: 'log-table' },
452
+ el('div', { class: 'log-row head' },
453
+ el('span', { text: '模型' }), el('span', { text: 'Token' }), el('span', { text: '花费' }), el('span', { text: '占比' })),
454
+ rows.map((m) => el('div', { class: 'log-row' },
455
+ el('span', { class: 'who', title: m.model, text: m.model }),
456
+ el('span', { class: 'num', text: compact(m.tokens) }),
457
+ el('span', { class: 'num', text: m.costText || '' }),
458
+ el('span', { class: 'num', text: Math.round((m.tokens / total) * 100) + '%' }))))
459
+ return card(null, head,
460
+ el('div', { class: 'stats' },
461
+ el('div', { class: 'stat' }, el('div', { class: 'k', text: '消耗' }), el('div', { class: 'v', text: data.spent })),
462
+ el('div', { class: 'stat' }, el('div', { class: 'k', text: '当前余额' }), el('div', { class: 'v', text: data.balance || '—' })),
463
+ el('div', { class: 'stat' }, el('div', { class: 'k', text: 'Token' }), el('div', { class: 'v', text: compact(data.tokens) }))),
464
+ table,
465
+ el('p', { class: 'note', text: '数字来自站点用量端点;这个窗口里没有调用就不列模型。' }))
466
+ }
343
467
  /** 分组墙:站点原文组名 + 倍率 + 模型数,点一下启停(写进插件 options)。 */
344
468
  function groupsCard() {
345
469
  const list = Array.isArray(view.groupList) ? view.groupList : null
package/page/style.css CHANGED
@@ -416,4 +416,35 @@ select {
416
416
  .group-chip[aria-pressed="true"] .n { color: var(--nt-muted); }
417
417
 
418
418
  .link { color: var(--nt-accent); text-decoration: none; font-size: 11.5px; }
419
- .link:hover { text-decoration: underline; }
419
+ .link:hover { text-decoration: underline; }
420
+ /* 三个视角:原本界面 / 模型广场 / 使用日志 */
421
+ .tabs { display: inline-flex; gap: 2px; padding: 3px; margin: 14px 0 0; border: 1px solid var(--nt-line); border-radius: 10px; }
422
+
423
+ .tabs button {
424
+ border: 0; height: 26px; padding: 0 12px; border-radius: 8px;
425
+ color: var(--nt-muted); background: transparent; font-size: 12px;
426
+ }
427
+
428
+ .tabs button.on { color: var(--nt-ink); background: var(--nt-accent-soft); box-shadow: inset 0 0 0 1px var(--nt-accent); }
429
+
430
+ /* 模型广场:一类一组,组名在上,开关在下 */
431
+ .bucket { margin: 14px 0 0; }
432
+ .bucket-name { display: flex; align-items: baseline; gap: 7px; font-size: 11.5px; color: var(--nt-muted); margin-bottom: 8px; }
433
+ .bucket-name .n { font-family: var(--nt-code); font-size: 10.5px; color: var(--nt-faint); }
434
+
435
+ /* 使用日志 */
436
+ .log-table { margin-top: 12px; border-top: 1px solid var(--nt-line); }
437
+
438
+ .log-row {
439
+ display: grid;
440
+ grid-template-columns: 1fr 84px 92px 56px;
441
+ gap: 8px;
442
+ padding: 7px 0;
443
+ border-bottom: 1px solid var(--nt-line);
444
+ font-size: 12px;
445
+ align-items: baseline;
446
+ }
447
+
448
+ .log-row.head { color: var(--nt-muted); font-size: 11px; border-bottom-color: transparent; }
449
+ .log-row .who { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
450
+ .log-row .num { text-align: right; font-family: var(--nt-code); font-variant-numeric: tabular-nums; color: var(--nt-muted); }