@weibaohui/context-razor 0.4.0 → 0.4.2

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
@@ -10,8 +10,10 @@
10
10
  ## 核心功能
11
11
 
12
12
  - **逐条列出**:当前会话模型可见的每条上下文(用户/助手/工具结果)按序排列,带预览、字符数与 ≈token 估算,点 ⋯ 查看全文与真实 usage
13
+ - **占比标注**:每条的 token 徽章同时显示其占全部上下文的百分比(如 `≈5728 · 15%`),一眼看出谁是上下文大头
14
+ - **类型筛选**:按类型一键过滤——用户 / 注入 / 助手,以及各工具名(bash、skill、read、edit…按会话实际出现的动态生成)。多选 toggle,与 token 量级筛选正交叠加
13
15
  - **超阈值标红**:可调阈值(默认 500 token,本地记忆),超限条目红色高亮;「只看超阈值」过滤 + 「token 高→低」排序,大头一眼可见
14
- - **精确裁剪**:勾选任意条目一键删除——连续段自动合并处理。删除走宿主 surface replace 协议(与官方 compaction 同款机制),**不经 LLM 总结**,删了什么、删了多少完全由你决定
16
+ - **精确裁剪**:勾选任意条目一键删除——连续段自动合并处理。删除走宿主 surface replace 协议(与官方 compaction 同款机制),**不经 LLM 总结**,删了什么、删了多少完全由你决定。每行尾部带 ✕ 快捷按钮,**单条直接删**与勾选后**多选批量删**走同一确认弹窗
15
17
  - **不重排**:本插件不会改变上下文条目顺序,只会剔除选中条目;「token 高→低」排序仅影响页面显示,模型可见顺序始终由会话 surface 决定
16
18
  - **安全护栏**:会话运行中禁止裁剪(等当前回合结束);每次替换附带一条 notice 标记消息,模型知道这段历史被你移除了,需要时会主动向你确认
17
19
  - **非破坏**:append-only 日志保留全部痕迹(宿主机制如此)。「删除」= 从模型视野与界面投影中移除
package/client/bundle.js CHANGED
@@ -66,9 +66,11 @@ window.__ModuleLoader__.load({
66
66
  sortOrder: '上下文顺序',
67
67
  sortTokens: 'token 高→低',
68
68
  orderNote: '本插件不会改变上下文条目顺序,只会剔除选中条目。',
69
- selectVisible: '选中可见',
70
- selectVisibleHint: '选中当前显示的全部条目(配合色块筛选)',
71
- clearSelect: '清空选择',
69
+ legendTitle: '按 token 量级筛选(点选高亮某一档)',
70
+ kindFilterLabel: '类型',
71
+ selectVisible: '全选',
72
+ selectVisibleHint: '全选当前可见条目(配合类型/量级筛选)',
73
+ clearSelect: '取消全选',
72
74
  deleteSelected: '删除选中',
73
75
  selectedStats: '已选 {n} 条 / ≈{tokens} token',
74
76
  stats: '{nodes} 条 · 合计 ≈{tokens} token{mode}',
@@ -87,6 +89,8 @@ window.__ModuleLoader__.load({
87
89
  deleteTitle: '确认裁剪',
88
90
  deleteConfirm: '即将把 {n} 条消息(约 {tokens} token)从模型视野中移除,替换为一条标记消息。日志中保留痕迹,此操作不可恢复。继续?',
89
91
  deleteBusyHint: '会话正在运行,等当前回合结束后再裁剪',
92
+ deleteOne: '删除此条',
93
+ deleteOneHint: '删除此条(约 {tokens} token)',
90
94
  deleteOk: '执行',
91
95
  cancel: '取消',
92
96
  deletedToast: '已删除 {n} 条(约 {tokens} token)',
@@ -104,9 +108,11 @@ window.__ModuleLoader__.load({
104
108
  sortOrder: 'Context order',
105
109
  sortTokens: 'tokens high→low',
106
110
  orderNote: 'This plugin never reorders context entries; it only removes the selected ones.',
107
- selectVisible: 'Select shown',
108
- selectVisibleHint: 'Select every currently shown entry (pairs with tier filters)',
109
- clearSelect: 'Clear selection',
111
+ legendTitle: 'Filter by token tier (click to isolate a band)',
112
+ kindFilterLabel: 'Type',
113
+ selectVisible: 'All',
114
+ selectVisibleHint: 'Select all currently visible entries (pairs with type/tier filters)',
115
+ clearSelect: 'Clear all',
110
116
  deleteSelected: 'Delete selected',
111
117
  selectedStats: '{n} selected / ≈{tokens} tokens',
112
118
  stats: '{nodes} entries · total ≈{tokens} tokens{mode}',
@@ -125,6 +131,8 @@ window.__ModuleLoader__.load({
125
131
  deleteTitle: 'Confirm trim',
126
132
  deleteConfirm: 'About to remove {n} messages (≈{tokens} tokens) from the model view, replaced by one marker message. Traces stay in the log; this cannot be undone. Continue?',
127
133
  deleteBusyHint: 'Session is running — trim after the current turn finishes',
134
+ deleteOne: 'Delete this entry',
135
+ deleteOneHint: 'Delete this entry (≈{tokens} tokens)',
128
136
  deleteOk: 'Delete',
129
137
  cancel: 'Cancel',
130
138
  deletedToast: 'Deleted {n} entries (≈{tokens} tokens)',
@@ -174,6 +182,38 @@ window.__ModuleLoader__.load({
174
182
  return { kind: entry.kind, label: t(kindI18n(entry.kind)) }
175
183
  }
176
184
 
185
+ /**
186
+ * 筛选用稳定分类键,与 entryChip 的可见口径一致:
187
+ * tool → 'tool:<工具名>'(无工具名则 'tool:',按钮文案退回「工具」)
188
+ * user 注入 → 'injected'(sourceKind !== 'user' 的 user 消息,统一一档;
189
+ * 细分来源在 chip 的 title 与详情里已可见,筛选层不再切分,避免按钮爆炸)
190
+ * user / assistant → 自身
191
+ * 「按钮文案」与 chip 同源(categoryLabel),用户在两处看到的是同一个词。
192
+ */
193
+ function entryCategory(entry) {
194
+ if (entry.kind === 'tool') return 'tool:' + (typeof entry.tool === 'string' ? entry.tool : '')
195
+ if (entry.kind === 'user' && entry.sourceKind && entry.sourceKind !== 'user') return 'injected'
196
+ return entry.kind
197
+ }
198
+ function categoryLabel(cat, t) {
199
+ if (cat === 'user') return t('kindUser')
200
+ if (cat === 'assistant') return t('kindAssistant')
201
+ if (cat === 'injected') return t('kindInjected')
202
+ if (cat.lastIndexOf('tool:', 0) === 0) {
203
+ const name = cat.slice(5)
204
+ return name || t('kindTool')
205
+ }
206
+ return cat
207
+ }
208
+
209
+ /** 该条 token 占全部上下文的百分比文案;total 非正或 part 为 0 返回 null(不显示)。 */
210
+ function pctOf(part, total) {
211
+ if (!total || !part) return null
212
+ const p = part / total * 100
213
+ if (p < 0.1) return '<0.1%'
214
+ return (p >= 10 ? p.toFixed(0) : p.toFixed(1)) + '%'
215
+ }
216
+
177
217
  // ── 彩虹分级:颜色越暖 = 占用越多 ────────────────────────────────────────
178
218
  // 固定对数档位(相邻约 ×2.5),跨会话语义稳定:绿→黄绿→黄→橙→红→品红。
179
219
  // 档位是「这条消息吃掉多少典型上下文预算」的粗标尺,不随会话内最大值缩放。
@@ -224,6 +264,18 @@ window.__ModuleLoader__.load({
224
264
  .rz-swatch.tier-3{background:hsl(32,80%,45%)}
225
265
  .rz-swatch.tier-4{background:hsl(8,75%,48%)}
226
266
  .rz-swatch.tier-5{background:hsl(320,65%,50%)}
267
+ .rz-kindsbar{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
268
+ .rz-kinds{display:inline-flex;gap:4px;align-items:center;flex-wrap:wrap}
269
+ .rz-kinds.filtering .rz-kindbtn:not(.on){opacity:.45}
270
+ .rz-kindbtn{display:inline-flex;align-items:center;gap:5px;padding:2px 10px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);font-size:11.5px;cursor:pointer;font-family:var(--dsw-font-family);white-space:nowrap}
271
+ .rz-kindbtn:hover{border-color:var(--dsw-alias-border-l3);background:var(--dsw-alias-interactive-bg-hover)}
272
+ .rz-kindbtn.on{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary);font-weight:600}
273
+ .rz-kindcount{font-size:10px;opacity:.7}
274
+ .rz-thead{display:flex;gap:8px;align-items:center;flex-wrap:wrap;padding:6px 12px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-1)}
275
+ .rz-thead-l{display:inline-flex;gap:8px;align-items:center;flex-wrap:wrap}
276
+ .rz-sel-stats{color:var(--dsw-alias-label-secondary);font-size:12px;white-space:nowrap}
277
+ .rz-col-tok{flex:0 0 62px;display:flex;justify-content:flex-end}
278
+ .rz-col-pct{flex:0 0 44px;text-align:right;color:var(--dsw-alias-label-tertiary);font-size:11px;white-space:nowrap}
227
279
  .rz-list{display:flex;flex-direction:column;gap:6px}
228
280
  .rz-row{display:flex;gap:10px;align-items:center;padding:8px 12px;border-radius:10px;border:1px solid var(--dsw-alias-border-l1);border-left:3px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);cursor:pointer;text-align:left;width:100%}
229
281
  .rz-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
@@ -231,6 +283,9 @@ window.__ModuleLoader__.load({
231
283
  .rz-row-preview{flex:0 1 340px;min-width:100px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-primary);font-size:13px;text-decoration:none}
232
284
  .rz-row-preview:hover{text-decoration:underline;text-underline-offset:3px}
233
285
  .rz-row-meta{color:var(--dsw-alias-label-tertiary);font-size:11px;flex:none}
286
+ .rz-row-del{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;margin-left:4px;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1;cursor:pointer;flex:none}
287
+ .rz-row-del:hover:not(:disabled){color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary);background:hsla(0,75%,50%,.08)}
288
+ .rz-row-del:disabled{opacity:.35;cursor:not-allowed}
234
289
  .rz-badge.tier-0{color:hsl(120,55%,40%);border-color:hsla(120,55%,40%,.45);background:hsla(120,55%,40%,.10)}
235
290
  .rz-badge.tier-1{color:hsl(90,60%,38%);border-color:hsla(90,60%,38%,.45);background:hsla(90,60%,38%,.10)}
236
291
  .rz-badge.tier-2{color:hsl(60,70%,36%);border-color:hsla(60,70%,36%,.45);background:hsla(60,70%,36%,.12)}
@@ -281,14 +336,16 @@ window.__ModuleLoader__.load({
281
336
  }
282
337
 
283
338
  /** 单条全文弹窗(正文经 /entry 异步补全)。 */
284
- function DetailModal({ detail, t, onClose }) {
339
+ function DetailModal({ detail, t, total, onClose }) {
285
340
  if (!detail) return null
341
+ const pct = pctOf(detail.tokens, total)
286
342
  return h('div', { className: 'rz-dlg-backdrop', onClick: onClose },
287
343
  h('div', { className: 'rz-dlg', onClick: e => e.stopPropagation() },
288
344
  h('h3', null, t('detailTitle') + ' · ' + t('seqLabel', { seq: detail.seq })),
289
345
  h('div', { className: 'rz-stats', style: { marginBottom: 10 } },
290
346
  h(Chip, { ...entryChip(detail, t) }),
291
347
  h(TokenBadge, { entry: detail }),
348
+ pct && h('span', { className: 'rz-label', title: pct + ' of ≈' + formatNum(total) + ' token' }, pct),
292
349
  h('span', { className: 'rz-label', title: 'seq ' + detail.seq }, formatDateTime(detail.time)),
293
350
  h('span', { className: 'rz-label' }, t('detailChars', { chars: formatNum(detail.chars) })),
294
351
  detail.usage && h('span', { className: 'rz-label' }, t('detailUsage', {
@@ -319,10 +376,11 @@ window.__ModuleLoader__.load({
319
376
  const [ctxLoading, setCtxLoading] = useState(false)
320
377
  const [error, setError] = useState(null)
321
378
  const [tierFilter, setTierFilter] = useState(() => new Set())
379
+ const [kindFilter, setKindFilter] = useState(() => new Set())
322
380
  const [sortBy, setSortBy] = useState('order')
323
381
  const [selected, setSelected] = useState(() => new Set())
324
382
  const [detail, setDetail] = useState(null)
325
- const [confirming, setConfirming] = useState(false)
383
+ const [confirming, setConfirming] = useState(null) // null | number[] 待裁剪 seqs(单条/多选同源)
326
384
  const [deleting, setDeleting] = useState(false)
327
385
  const [toast, setToast] = useState(null)
328
386
  const sessionRef = useRef('')
@@ -353,16 +411,34 @@ window.__ModuleLoader__.load({
353
411
  if (!context) return []
354
412
  let rows = context.entries
355
413
  if (tierFilter.size > 0) rows = rows.filter(e => tierFilter.has(tierOf(e)))
414
+ if (kindFilter.size > 0) rows = rows.filter(e => kindFilter.has(entryCategory(e)))
356
415
  return sortEntries(rows, sortBy)
357
- }, [context, tierFilter, sortBy])
416
+ }, [context, tierFilter, kindFilter, sortBy])
358
417
 
359
- const selectedTokens = useMemo(() => {
360
- if (!context) return 0
361
- const bySeq = new Map(context.entries.map(e => [e.seq, e]))
418
+ // 当前上下文里实际出现的分类(cat/count/tokens),按条数降序——决定渲染哪些按钮。
419
+ const categories = useMemo(() => {
420
+ if (!context) return []
421
+ const seen = new Map()
422
+ for (const e of context.entries) {
423
+ const c = entryCategory(e)
424
+ const prev = seen.get(c) || { count: 0, tokens: 0 }
425
+ prev.count += 1
426
+ prev.tokens += (e.tokens || 0)
427
+ seen.set(c, prev)
428
+ }
429
+ return [...seen.entries()]
430
+ .map(([cat, v]) => ({ cat, count: v.count, tokens: v.tokens }))
431
+ .sort((a, b) => b.count - a.count || b.tokens - a.tokens)
432
+ }, [context])
433
+
434
+ const entriesBySeq = useMemo(() => context ? new Map(context.entries.map(e => [e.seq, e])) : null, [context])
435
+ const sumSeqs = (seqs) => {
362
436
  let sum = 0
363
- for (const seq of selected) { const e = bySeq.get(seq); if (e) sum += e.tokens }
437
+ if (entriesBySeq) for (const seq of seqs) { const e = entriesBySeq.get(seq); if (e) sum += e.tokens || 0 }
364
438
  return sum
365
- }, [context, selected])
439
+ }
440
+ const selectedTokens = useMemo(() => sumSeqs(selected), [selected, entriesBySeq])
441
+ const confirmTokens = useMemo(() => confirming ? sumSeqs(confirming) : 0, [confirming, entriesBySeq])
366
442
 
367
443
  const toggleRow = (seq) => setSelected(prev => {
368
444
  const next = new Set(prev)
@@ -376,6 +452,12 @@ window.__ModuleLoader__.load({
376
452
  else next.add(i)
377
453
  return next
378
454
  })
455
+ const toggleKind = (cat) => setKindFilter(prev => {
456
+ const next = new Set(prev)
457
+ if (next.has(cat)) next.delete(cat)
458
+ else next.add(cat)
459
+ return next
460
+ })
379
461
  const selectVisible = () => setSelected(new Set(visible.map(e => e.seq)))
380
462
 
381
463
  const openDetail = (entry) => {
@@ -386,17 +468,18 @@ window.__ModuleLoader__.load({
386
468
  }
387
469
 
388
470
  const doDelete = async () => {
471
+ if (!confirming || confirming.length === 0) return
389
472
  setDeleting(true)
390
473
  try {
391
- const r = await fetch(API + '/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session: sessionId, seqs: [...selected] }) })
474
+ const r = await fetch(API + '/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session: sessionId, seqs: confirming }) })
392
475
  const d = await r.json().catch(() => ({}))
393
476
  if (!r.ok) throw new Error(d.error || 'HTTP ' + r.status)
394
- setConfirming(false)
477
+ setConfirming(null)
395
478
  setSelected(new Set())
396
479
  showToast(t('deletedToast', { n: d.removed, tokens: formatNum(d.tokensRemoved) }))
397
480
  refreshAll()
398
481
  } catch (e) {
399
- setConfirming(false)
482
+ setConfirming(null)
400
483
  setError(e.message)
401
484
  } finally { setDeleting(false) }
402
485
  }
@@ -415,40 +498,59 @@ window.__ModuleLoader__.load({
415
498
  h('span', { className: 'rz-legend' + (tierFilter.size > 0 ? ' filtering' : ''), title: t('legendTitle') },
416
499
  RAZOR_TIERS.map((tr, i) => h('button', { key: i, className: 'rz-swatch tier-' + i + (tierFilter.has(i) ? ' on' : ''), title: tr.label + ' token', onClick: () => toggleTier(i) }, tr.label))),
417
500
  h('span', { className: 'rz-spacer' }),
418
- busy && h('span', { className: 'rz-badge' }, t('busyTag')),
419
- h('button', { className: 'rz-btn', onClick: selectVisible, disabled: visible.length === 0 || busy, title: t('selectVisibleHint') },
420
- t('selectVisible') + (visible.length ? ` (${visible.length})` : '')),
501
+ busy && h('span', { className: 'rz-badge' }, t('busyTag'))),
502
+ // 类型筛选:与 chip 同口径的可点选按钮(user/injected/assistant/tool:<名>),
503
+ // 多选 toggle,与 tier 量级筛选正交叠加。按钮带条数;hover token 占比。
504
+ categories.length > 0 && h('div', { key: 'kinds', className: 'rz-kindsbar' },
505
+ h('span', { className: 'rz-label' }, t('kindFilterLabel')),
506
+ h('span', { className: 'rz-kinds' + (kindFilter.size > 0 ? ' filtering' : '') },
507
+ categories.map(({ cat, count, tokens }) =>
508
+ h('button', { key: cat,
509
+ className: 'rz-kindbtn' + (kindFilter.has(cat) ? ' on' : ''),
510
+ title: categoryLabel(cat, t) + ' · ' + count + ' 条 · ≈' + formatNum(tokens) + ' token'
511
+ + (context.totalTokens ? '(' + (pctOf(tokens, context.totalTokens) || '0%') + ')' : ''),
512
+ onClick: () => toggleKind(cat) },
513
+ categoryLabel(cat, t),
514
+ h('span', { className: 'rz-kindcount' }, count))))),
515
+ // 表头(在类型筛选下方):左 刷新/全选,选中后追加 删除·取消全选;右 排序。
516
+ h('div', { key: 'thead', className: 'rz-thead' },
517
+ h('div', { className: 'rz-thead-l' },
518
+ h('button', { className: 'rz-btn', onClick: refreshAll, title: t('refresh') }, t('refresh')),
519
+ h('button', { className: 'rz-btn', onClick: selectVisible, disabled: visible.length === 0 || busy, title: t('selectVisibleHint') },
520
+ t('selectVisible') + (visible.length ? ` (${visible.length})` : '')),
521
+ selected.size > 0 && h('button', { className: 'rz-btn rz-btn-danger', disabled: !canDelete,
522
+ title: busy ? t('deleteBusyHint') : undefined,
523
+ onClick: () => setConfirming([...selected]) },
524
+ t('deleteSelected') + ` (${selected.size})`),
525
+ selected.size > 0 && h('button', { className: 'rz-btn', onClick: () => setSelected(new Set()) }, t('clearSelect'))),
526
+ h('span', { className: 'rz-spacer' }),
421
527
  h('select', { className: 'rz-select', value: sortBy, onChange: e => setSortBy(e.target.value), title: t('sortLabel'), 'aria-label': t('sortLabel') },
422
528
  h('option', { value: 'order' }, t('sortOrder')),
423
- h('option', { value: 'tokens' }, t('sortTokens'))),
424
- h('button', { className: 'rz-btn', onClick: refreshAll, title: t('refresh') }, t('refresh'))),
425
- selected.size > 0 && h('div', { key: 'sel', className: 'rz-stats' },
426
- h('span', null, t('selectedStats', { n: selected.size, tokens: formatNum(selectedTokens) })),
427
- h('span', { className: 'rz-spacer' }),
428
- h('div', { className: 'rz-footbtns' },
429
- h('button', { className: 'rz-btn', onClick: () => setSelected(new Set()) }, t('clearSelect')),
430
- h('button', { className: 'rz-btn rz-btn-danger', disabled: !canDelete,
431
- title: busy ? t('deleteBusyHint') : undefined,
432
- onClick: () => setConfirming(true) },
433
- t('deleteSelected') + ` (${selected.size})`))),
529
+ h('option', { value: 'tokens' }, t('sortTokens')))),
434
530
  visible.length === 0
435
531
  ? h('div', { key: 'empty', className: 'rz-empty' }, t('emptyContext'))
436
532
  : h('div', { key: 'list', className: 'rz-list' },
437
533
  h(PagedList, { items: visible, t, render: entry => {
438
534
  const tier = tierOf(entry)
535
+ const pct = pctOf(entry.tokens, context.totalTokens)
439
536
  return h('div', { key: entry.seq, className: 'rz-row tier-' + tier + (selected.has(entry.seq) ? ' checked' : ''),
440
537
  role: 'button', tabIndex: 0, onClick: () => toggleRow(entry.seq),
441
538
  onKeyDown: e => e.key === 'Enter' && toggleRow(entry.seq) },
442
539
  h('input', { type: 'checkbox', checked: selected.has(entry.seq), onClick: e => e.stopPropagation(), onChange: () => toggleRow(entry.seq) }),
443
- h(TokenBadge, { entry }),
540
+ h('span', { className: 'rz-col-tok' }, h(TokenBadge, { entry })),
541
+ h('span', { className: 'rz-col-pct', title: pct ? pct + ' of ≈' + formatNum(context.totalTokens) + ' token' : '' }, pct || ''),
444
542
  h(Chip, { ...entryChip(entry, t) }),
445
543
  h('span', { className: 'rz-row-preview', title: entry.preview,
446
544
  onClick: e => { e.stopPropagation(); openDetail(entry) } }, entry.preview || ' '),
447
- h('span', { className: 'rz-row-meta', title: 'seq ' + entry.seq }, formatDateTime(entry.time)))
545
+ h('span', { className: 'rz-row-meta', title: 'seq ' + entry.seq }, formatDateTime(entry.time)),
546
+ h('button', { className: 'rz-row-del', type: 'button', disabled: busy,
547
+ title: busy ? t('deleteBusyHint') : t('deleteOneHint', { tokens: formatNum(entry.tokens) }),
548
+ 'aria-label': t('deleteOne'),
549
+ onClick: e => { e.stopPropagation(); if (!busy) setConfirming([entry.seq]) } }, '✕'))
448
550
  } })),
449
- detail && h(DetailModal, { detail, t, onClose: () => setDetail(null) }),
450
- confirming && h(ConfirmDialog, { n: selected.size, tokens: formatNum(selectedTokens), deleting, t,
451
- onCancel: () => setConfirming(false), onOk: doDelete }),
551
+ detail && h(DetailModal, { detail, t, total: context.totalTokens, onClose: () => setDetail(null) }),
552
+ confirming && confirming.length > 0 && h(ConfirmDialog, { n: confirming.length, tokens: formatNum(confirmTokens), deleting, t,
553
+ onCancel: () => setConfirming(null), onOk: doDelete }),
452
554
  ],
453
555
  toast && h('div', { className: 'rz-toast' }, toast))
454
556
  }
@@ -460,7 +562,7 @@ window.__ModuleLoader__.load({
460
562
  module.exports = {
461
563
  name: CLIENT_NAME,
462
564
  inject: ['slots', 'locale'],
463
- __internals: { NS, ZH, EN, sortEntries, tierOf, RAZOR_TIERS, formatDateTime, entryChip },
565
+ __internals: { NS, ZH, EN, sortEntries, tierOf, RAZOR_TIERS, formatDateTime, entryChip, entryCategory, categoryLabel, pctOf },
464
566
  __boot(container, opts = {}) {
465
567
  ensureStyles()
466
568
  const t = opts.t || ((key, vars) => {
package/client/index.js CHANGED
@@ -56,9 +56,11 @@ const ZH = {
56
56
  sortOrder: '上下文顺序',
57
57
  sortTokens: 'token 高→低',
58
58
  orderNote: '本插件不会改变上下文条目顺序,只会剔除选中条目。',
59
- selectVisible: '选中可见',
60
- selectVisibleHint: '选中当前显示的全部条目(配合色块筛选)',
61
- clearSelect: '清空选择',
59
+ legendTitle: '按 token 量级筛选(点选高亮某一档)',
60
+ kindFilterLabel: '类型',
61
+ selectVisible: '全选',
62
+ selectVisibleHint: '全选当前可见条目(配合类型/量级筛选)',
63
+ clearSelect: '取消全选',
62
64
  deleteSelected: '删除选中',
63
65
  selectedStats: '已选 {n} 条 / ≈{tokens} token',
64
66
  stats: '{nodes} 条 · 合计 ≈{tokens} token{mode}',
@@ -77,6 +79,8 @@ const ZH = {
77
79
  deleteTitle: '确认裁剪',
78
80
  deleteConfirm: '即将把 {n} 条消息(约 {tokens} token)从模型视野中移除,替换为一条标记消息。日志中保留痕迹,此操作不可恢复。继续?',
79
81
  deleteBusyHint: '会话正在运行,等当前回合结束后再裁剪',
82
+ deleteOne: '删除此条',
83
+ deleteOneHint: '删除此条(约 {tokens} token)',
80
84
  deleteOk: '执行',
81
85
  cancel: '取消',
82
86
  deletedToast: '已删除 {n} 条(约 {tokens} token)',
@@ -94,9 +98,11 @@ const EN = {
94
98
  sortOrder: 'Context order',
95
99
  sortTokens: 'tokens high→low',
96
100
  orderNote: 'This plugin never reorders context entries; it only removes the selected ones.',
97
- selectVisible: 'Select shown',
98
- selectVisibleHint: 'Select every currently shown entry (pairs with tier filters)',
99
- clearSelect: 'Clear selection',
101
+ legendTitle: 'Filter by token tier (click to isolate a band)',
102
+ kindFilterLabel: 'Type',
103
+ selectVisible: 'All',
104
+ selectVisibleHint: 'Select all currently visible entries (pairs with type/tier filters)',
105
+ clearSelect: 'Clear all',
100
106
  deleteSelected: 'Delete selected',
101
107
  selectedStats: '{n} selected / ≈{tokens} tokens',
102
108
  stats: '{nodes} entries · total ≈{tokens} tokens{mode}',
@@ -115,6 +121,8 @@ const EN = {
115
121
  deleteTitle: 'Confirm trim',
116
122
  deleteConfirm: 'About to remove {n} messages (≈{tokens} tokens) from the model view, replaced by one marker message. Traces stay in the log; this cannot be undone. Continue?',
117
123
  deleteBusyHint: 'Session is running — trim after the current turn finishes',
124
+ deleteOne: 'Delete this entry',
125
+ deleteOneHint: 'Delete this entry (≈{tokens} tokens)',
118
126
  deleteOk: 'Delete',
119
127
  cancel: 'Cancel',
120
128
  deletedToast: 'Deleted {n} entries (≈{tokens} tokens)',
@@ -164,6 +172,38 @@ function entryChip(entry, t) {
164
172
  return { kind: entry.kind, label: t(kindI18n(entry.kind)) }
165
173
  }
166
174
 
175
+ /**
176
+ * 筛选用稳定分类键,与 entryChip 的可见口径一致:
177
+ * tool → 'tool:<工具名>'(无工具名则 'tool:',按钮文案退回「工具」)
178
+ * user 注入 → 'injected'(sourceKind !== 'user' 的 user 消息,统一一档;
179
+ * 细分来源在 chip 的 title 与详情里已可见,筛选层不再切分,避免按钮爆炸)
180
+ * user / assistant → 自身
181
+ * 「按钮文案」与 chip 同源(categoryLabel),用户在两处看到的是同一个词。
182
+ */
183
+ function entryCategory(entry) {
184
+ if (entry.kind === 'tool') return 'tool:' + (typeof entry.tool === 'string' ? entry.tool : '')
185
+ if (entry.kind === 'user' && entry.sourceKind && entry.sourceKind !== 'user') return 'injected'
186
+ return entry.kind
187
+ }
188
+ function categoryLabel(cat, t) {
189
+ if (cat === 'user') return t('kindUser')
190
+ if (cat === 'assistant') return t('kindAssistant')
191
+ if (cat === 'injected') return t('kindInjected')
192
+ if (cat.lastIndexOf('tool:', 0) === 0) {
193
+ const name = cat.slice(5)
194
+ return name || t('kindTool')
195
+ }
196
+ return cat
197
+ }
198
+
199
+ /** 该条 token 占全部上下文的百分比文案;total 非正或 part 为 0 返回 null(不显示)。 */
200
+ function pctOf(part, total) {
201
+ if (!total || !part) return null
202
+ const p = part / total * 100
203
+ if (p < 0.1) return '<0.1%'
204
+ return (p >= 10 ? p.toFixed(0) : p.toFixed(1)) + '%'
205
+ }
206
+
167
207
  // ── 彩虹分级:颜色越暖 = 占用越多 ────────────────────────────────────────
168
208
  // 固定对数档位(相邻约 ×2.5),跨会话语义稳定:绿→黄绿→黄→橙→红→品红。
169
209
  // 档位是「这条消息吃掉多少典型上下文预算」的粗标尺,不随会话内最大值缩放。
@@ -214,6 +254,18 @@ const STYLE = `<style>
214
254
  .rz-swatch.tier-3{background:hsl(32,80%,45%)}
215
255
  .rz-swatch.tier-4{background:hsl(8,75%,48%)}
216
256
  .rz-swatch.tier-5{background:hsl(320,65%,50%)}
257
+ .rz-kindsbar{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
258
+ .rz-kinds{display:inline-flex;gap:4px;align-items:center;flex-wrap:wrap}
259
+ .rz-kinds.filtering .rz-kindbtn:not(.on){opacity:.45}
260
+ .rz-kindbtn{display:inline-flex;align-items:center;gap:5px;padding:2px 10px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);font-size:11.5px;cursor:pointer;font-family:var(--dsw-font-family);white-space:nowrap}
261
+ .rz-kindbtn:hover{border-color:var(--dsw-alias-border-l3);background:var(--dsw-alias-interactive-bg-hover)}
262
+ .rz-kindbtn.on{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary);font-weight:600}
263
+ .rz-kindcount{font-size:10px;opacity:.7}
264
+ .rz-thead{display:flex;gap:8px;align-items:center;flex-wrap:wrap;padding:6px 12px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-1)}
265
+ .rz-thead-l{display:inline-flex;gap:8px;align-items:center;flex-wrap:wrap}
266
+ .rz-sel-stats{color:var(--dsw-alias-label-secondary);font-size:12px;white-space:nowrap}
267
+ .rz-col-tok{flex:0 0 62px;display:flex;justify-content:flex-end}
268
+ .rz-col-pct{flex:0 0 44px;text-align:right;color:var(--dsw-alias-label-tertiary);font-size:11px;white-space:nowrap}
217
269
  .rz-list{display:flex;flex-direction:column;gap:6px}
218
270
  .rz-row{display:flex;gap:10px;align-items:center;padding:8px 12px;border-radius:10px;border:1px solid var(--dsw-alias-border-l1);border-left:3px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);cursor:pointer;text-align:left;width:100%}
219
271
  .rz-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
@@ -221,6 +273,9 @@ const STYLE = `<style>
221
273
  .rz-row-preview{flex:0 1 340px;min-width:100px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-primary);font-size:13px;text-decoration:none}
222
274
  .rz-row-preview:hover{text-decoration:underline;text-underline-offset:3px}
223
275
  .rz-row-meta{color:var(--dsw-alias-label-tertiary);font-size:11px;flex:none}
276
+ .rz-row-del{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;margin-left:4px;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1;cursor:pointer;flex:none}
277
+ .rz-row-del:hover:not(:disabled){color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary);background:hsla(0,75%,50%,.08)}
278
+ .rz-row-del:disabled{opacity:.35;cursor:not-allowed}
224
279
  .rz-badge.tier-0{color:hsl(120,55%,40%);border-color:hsla(120,55%,40%,.45);background:hsla(120,55%,40%,.10)}
225
280
  .rz-badge.tier-1{color:hsl(90,60%,38%);border-color:hsla(90,60%,38%,.45);background:hsla(90,60%,38%,.10)}
226
281
  .rz-badge.tier-2{color:hsl(60,70%,36%);border-color:hsla(60,70%,36%,.45);background:hsla(60,70%,36%,.12)}
@@ -271,14 +326,16 @@ function PagedList({ items, render, t }) {
271
326
  }
272
327
 
273
328
  /** 单条全文弹窗(正文经 /entry 异步补全)。 */
274
- function DetailModal({ detail, t, onClose }) {
329
+ function DetailModal({ detail, t, total, onClose }) {
275
330
  if (!detail) return null
331
+ const pct = pctOf(detail.tokens, total)
276
332
  return h('div', { className: 'rz-dlg-backdrop', onClick: onClose },
277
333
  h('div', { className: 'rz-dlg', onClick: e => e.stopPropagation() },
278
334
  h('h3', null, t('detailTitle') + ' · ' + t('seqLabel', { seq: detail.seq })),
279
335
  h('div', { className: 'rz-stats', style: { marginBottom: 10 } },
280
336
  h(Chip, { ...entryChip(detail, t) }),
281
337
  h(TokenBadge, { entry: detail }),
338
+ pct && h('span', { className: 'rz-label', title: pct + ' of ≈' + formatNum(total) + ' token' }, pct),
282
339
  h('span', { className: 'rz-label', title: 'seq ' + detail.seq }, formatDateTime(detail.time)),
283
340
  h('span', { className: 'rz-label' }, t('detailChars', { chars: formatNum(detail.chars) })),
284
341
  detail.usage && h('span', { className: 'rz-label' }, t('detailUsage', {
@@ -309,10 +366,11 @@ function RazorPage({ t, fixedSessionId }) {
309
366
  const [ctxLoading, setCtxLoading] = useState(false)
310
367
  const [error, setError] = useState(null)
311
368
  const [tierFilter, setTierFilter] = useState(() => new Set())
369
+ const [kindFilter, setKindFilter] = useState(() => new Set())
312
370
  const [sortBy, setSortBy] = useState('order')
313
371
  const [selected, setSelected] = useState(() => new Set())
314
372
  const [detail, setDetail] = useState(null)
315
- const [confirming, setConfirming] = useState(false)
373
+ const [confirming, setConfirming] = useState(null) // null | number[] 待裁剪 seqs(单条/多选同源)
316
374
  const [deleting, setDeleting] = useState(false)
317
375
  const [toast, setToast] = useState(null)
318
376
  const sessionRef = useRef('')
@@ -343,16 +401,34 @@ function RazorPage({ t, fixedSessionId }) {
343
401
  if (!context) return []
344
402
  let rows = context.entries
345
403
  if (tierFilter.size > 0) rows = rows.filter(e => tierFilter.has(tierOf(e)))
404
+ if (kindFilter.size > 0) rows = rows.filter(e => kindFilter.has(entryCategory(e)))
346
405
  return sortEntries(rows, sortBy)
347
- }, [context, tierFilter, sortBy])
406
+ }, [context, tierFilter, kindFilter, sortBy])
348
407
 
349
- const selectedTokens = useMemo(() => {
350
- if (!context) return 0
351
- const bySeq = new Map(context.entries.map(e => [e.seq, e]))
408
+ // 当前上下文里实际出现的分类(cat/count/tokens),按条数降序——决定渲染哪些按钮。
409
+ const categories = useMemo(() => {
410
+ if (!context) return []
411
+ const seen = new Map()
412
+ for (const e of context.entries) {
413
+ const c = entryCategory(e)
414
+ const prev = seen.get(c) || { count: 0, tokens: 0 }
415
+ prev.count += 1
416
+ prev.tokens += (e.tokens || 0)
417
+ seen.set(c, prev)
418
+ }
419
+ return [...seen.entries()]
420
+ .map(([cat, v]) => ({ cat, count: v.count, tokens: v.tokens }))
421
+ .sort((a, b) => b.count - a.count || b.tokens - a.tokens)
422
+ }, [context])
423
+
424
+ const entriesBySeq = useMemo(() => context ? new Map(context.entries.map(e => [e.seq, e])) : null, [context])
425
+ const sumSeqs = (seqs) => {
352
426
  let sum = 0
353
- for (const seq of selected) { const e = bySeq.get(seq); if (e) sum += e.tokens }
427
+ if (entriesBySeq) for (const seq of seqs) { const e = entriesBySeq.get(seq); if (e) sum += e.tokens || 0 }
354
428
  return sum
355
- }, [context, selected])
429
+ }
430
+ const selectedTokens = useMemo(() => sumSeqs(selected), [selected, entriesBySeq])
431
+ const confirmTokens = useMemo(() => confirming ? sumSeqs(confirming) : 0, [confirming, entriesBySeq])
356
432
 
357
433
  const toggleRow = (seq) => setSelected(prev => {
358
434
  const next = new Set(prev)
@@ -366,6 +442,12 @@ function RazorPage({ t, fixedSessionId }) {
366
442
  else next.add(i)
367
443
  return next
368
444
  })
445
+ const toggleKind = (cat) => setKindFilter(prev => {
446
+ const next = new Set(prev)
447
+ if (next.has(cat)) next.delete(cat)
448
+ else next.add(cat)
449
+ return next
450
+ })
369
451
  const selectVisible = () => setSelected(new Set(visible.map(e => e.seq)))
370
452
 
371
453
  const openDetail = (entry) => {
@@ -376,17 +458,18 @@ function RazorPage({ t, fixedSessionId }) {
376
458
  }
377
459
 
378
460
  const doDelete = async () => {
461
+ if (!confirming || confirming.length === 0) return
379
462
  setDeleting(true)
380
463
  try {
381
- const r = await fetch(API + '/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session: sessionId, seqs: [...selected] }) })
464
+ const r = await fetch(API + '/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session: sessionId, seqs: confirming }) })
382
465
  const d = await r.json().catch(() => ({}))
383
466
  if (!r.ok) throw new Error(d.error || 'HTTP ' + r.status)
384
- setConfirming(false)
467
+ setConfirming(null)
385
468
  setSelected(new Set())
386
469
  showToast(t('deletedToast', { n: d.removed, tokens: formatNum(d.tokensRemoved) }))
387
470
  refreshAll()
388
471
  } catch (e) {
389
- setConfirming(false)
472
+ setConfirming(null)
390
473
  setError(e.message)
391
474
  } finally { setDeleting(false) }
392
475
  }
@@ -405,40 +488,59 @@ function RazorPage({ t, fixedSessionId }) {
405
488
  h('span', { className: 'rz-legend' + (tierFilter.size > 0 ? ' filtering' : ''), title: t('legendTitle') },
406
489
  RAZOR_TIERS.map((tr, i) => h('button', { key: i, className: 'rz-swatch tier-' + i + (tierFilter.has(i) ? ' on' : ''), title: tr.label + ' token', onClick: () => toggleTier(i) }, tr.label))),
407
490
  h('span', { className: 'rz-spacer' }),
408
- busy && h('span', { className: 'rz-badge' }, t('busyTag')),
409
- h('button', { className: 'rz-btn', onClick: selectVisible, disabled: visible.length === 0 || busy, title: t('selectVisibleHint') },
410
- t('selectVisible') + (visible.length ? ` (${visible.length})` : '')),
491
+ busy && h('span', { className: 'rz-badge' }, t('busyTag'))),
492
+ // 类型筛选:与 chip 同口径的可点选按钮(user/injected/assistant/tool:<名>),
493
+ // 多选 toggle,与 tier 量级筛选正交叠加。按钮带条数;hover token 占比。
494
+ categories.length > 0 && h('div', { key: 'kinds', className: 'rz-kindsbar' },
495
+ h('span', { className: 'rz-label' }, t('kindFilterLabel')),
496
+ h('span', { className: 'rz-kinds' + (kindFilter.size > 0 ? ' filtering' : '') },
497
+ categories.map(({ cat, count, tokens }) =>
498
+ h('button', { key: cat,
499
+ className: 'rz-kindbtn' + (kindFilter.has(cat) ? ' on' : ''),
500
+ title: categoryLabel(cat, t) + ' · ' + count + ' 条 · ≈' + formatNum(tokens) + ' token'
501
+ + (context.totalTokens ? '(' + (pctOf(tokens, context.totalTokens) || '0%') + ')' : ''),
502
+ onClick: () => toggleKind(cat) },
503
+ categoryLabel(cat, t),
504
+ h('span', { className: 'rz-kindcount' }, count))))),
505
+ // 表头(在类型筛选下方):左 刷新/全选,选中后追加 删除·取消全选;右 排序。
506
+ h('div', { key: 'thead', className: 'rz-thead' },
507
+ h('div', { className: 'rz-thead-l' },
508
+ h('button', { className: 'rz-btn', onClick: refreshAll, title: t('refresh') }, t('refresh')),
509
+ h('button', { className: 'rz-btn', onClick: selectVisible, disabled: visible.length === 0 || busy, title: t('selectVisibleHint') },
510
+ t('selectVisible') + (visible.length ? ` (${visible.length})` : '')),
511
+ selected.size > 0 && h('button', { className: 'rz-btn rz-btn-danger', disabled: !canDelete,
512
+ title: busy ? t('deleteBusyHint') : undefined,
513
+ onClick: () => setConfirming([...selected]) },
514
+ t('deleteSelected') + ` (${selected.size})`),
515
+ selected.size > 0 && h('button', { className: 'rz-btn', onClick: () => setSelected(new Set()) }, t('clearSelect'))),
516
+ h('span', { className: 'rz-spacer' }),
411
517
  h('select', { className: 'rz-select', value: sortBy, onChange: e => setSortBy(e.target.value), title: t('sortLabel'), 'aria-label': t('sortLabel') },
412
518
  h('option', { value: 'order' }, t('sortOrder')),
413
- h('option', { value: 'tokens' }, t('sortTokens'))),
414
- h('button', { className: 'rz-btn', onClick: refreshAll, title: t('refresh') }, t('refresh'))),
415
- selected.size > 0 && h('div', { key: 'sel', className: 'rz-stats' },
416
- h('span', null, t('selectedStats', { n: selected.size, tokens: formatNum(selectedTokens) })),
417
- h('span', { className: 'rz-spacer' }),
418
- h('div', { className: 'rz-footbtns' },
419
- h('button', { className: 'rz-btn', onClick: () => setSelected(new Set()) }, t('clearSelect')),
420
- h('button', { className: 'rz-btn rz-btn-danger', disabled: !canDelete,
421
- title: busy ? t('deleteBusyHint') : undefined,
422
- onClick: () => setConfirming(true) },
423
- t('deleteSelected') + ` (${selected.size})`))),
519
+ h('option', { value: 'tokens' }, t('sortTokens')))),
424
520
  visible.length === 0
425
521
  ? h('div', { key: 'empty', className: 'rz-empty' }, t('emptyContext'))
426
522
  : h('div', { key: 'list', className: 'rz-list' },
427
523
  h(PagedList, { items: visible, t, render: entry => {
428
524
  const tier = tierOf(entry)
525
+ const pct = pctOf(entry.tokens, context.totalTokens)
429
526
  return h('div', { key: entry.seq, className: 'rz-row tier-' + tier + (selected.has(entry.seq) ? ' checked' : ''),
430
527
  role: 'button', tabIndex: 0, onClick: () => toggleRow(entry.seq),
431
528
  onKeyDown: e => e.key === 'Enter' && toggleRow(entry.seq) },
432
529
  h('input', { type: 'checkbox', checked: selected.has(entry.seq), onClick: e => e.stopPropagation(), onChange: () => toggleRow(entry.seq) }),
433
- h(TokenBadge, { entry }),
530
+ h('span', { className: 'rz-col-tok' }, h(TokenBadge, { entry })),
531
+ h('span', { className: 'rz-col-pct', title: pct ? pct + ' of ≈' + formatNum(context.totalTokens) + ' token' : '' }, pct || ''),
434
532
  h(Chip, { ...entryChip(entry, t) }),
435
533
  h('span', { className: 'rz-row-preview', title: entry.preview,
436
534
  onClick: e => { e.stopPropagation(); openDetail(entry) } }, entry.preview || ' '),
437
- h('span', { className: 'rz-row-meta', title: 'seq ' + entry.seq }, formatDateTime(entry.time)))
535
+ h('span', { className: 'rz-row-meta', title: 'seq ' + entry.seq }, formatDateTime(entry.time)),
536
+ h('button', { className: 'rz-row-del', type: 'button', disabled: busy,
537
+ title: busy ? t('deleteBusyHint') : t('deleteOneHint', { tokens: formatNum(entry.tokens) }),
538
+ 'aria-label': t('deleteOne'),
539
+ onClick: e => { e.stopPropagation(); if (!busy) setConfirming([entry.seq]) } }, '✕'))
438
540
  } })),
439
- detail && h(DetailModal, { detail, t, onClose: () => setDetail(null) }),
440
- confirming && h(ConfirmDialog, { n: selected.size, tokens: formatNum(selectedTokens), deleting, t,
441
- onCancel: () => setConfirming(false), onOk: doDelete }),
541
+ detail && h(DetailModal, { detail, t, total: context.totalTokens, onClose: () => setDetail(null) }),
542
+ confirming && confirming.length > 0 && h(ConfirmDialog, { n: confirming.length, tokens: formatNum(confirmTokens), deleting, t,
543
+ onCancel: () => setConfirming(null), onOk: doDelete }),
442
544
  ],
443
545
  toast && h('div', { className: 'rz-toast' }, toast))
444
546
  }
@@ -450,7 +552,7 @@ const CLIENT_NAME = '@weibaohui/context-razor'
450
552
  module.exports = {
451
553
  name: CLIENT_NAME,
452
554
  inject: ['slots', 'locale'],
453
- __internals: { NS, ZH, EN, sortEntries, tierOf, RAZOR_TIERS, formatDateTime, entryChip },
555
+ __internals: { NS, ZH, EN, sortEntries, tierOf, RAZOR_TIERS, formatDateTime, entryChip, entryCategory, categoryLabel, pctOf },
454
556
  __boot(container, opts = {}) {
455
557
  ensureStyles()
456
558
  const t = opts.t || ((key, vars) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weibaohui/context-razor",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "dsh 插件 · 上下文剃刀:把当前会话上下文逐条列出(角色/预览/≈token,cl100k 估算),超阈值标红,勾选后精确裁剪——不经 LLM 总结,删了什么一目了然。",
5
5
  "license": "MIT",
6
6
  "keywords": [