SLOPSHOPPER

xhs-count

小红书字数哨兵:选中文字实时统计,band 提示够不够标题(≤20)、正文超没超(≤1000)

newbandcommandtimer
v0.1.0MITupdated 2026-10-05eddiezhan/xhs-count-mod
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · xhs-count
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /xhs-count 🔍 xhs字数哨兵 · 全屏选中一段文字,我帮你卡标题(≤20)/正文(≤1000)字数 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
🔍 xhs字数哨兵 · 全屏选中一段文字,我帮你卡标题(≤20)/正文(≤1000)字数
README

xhs-count · 小红书字数哨兵

一个 Claude Code mod:在 prompt 上方的 band 里实时统计你选中的文字,告诉你——

  • 🟢 够标题(≤20 字):还差几个字拉满悬念
  • 🟡 适合正文:已用 N/1000
  • 🔴 正文超了:超几个字,删

写小红书帖子再也不用肉眼数格子。

它是怎么工作的

选中文字(fullscreen)
   ↓  $.clock.every(800ms) 轮询 $.ui.selection()
统计码点数(中文/emoji 算 1 字)
   ↓  $.ui.invalidate('ui.render')
band 重绘(ui.render · AbovePrompt)

整个 mod 就一个 register.js,约 90 行:一个轮询钩子 + 一个 band 绘制钩子,无依赖、无网络请求、无文件读写。

安装

需要 Claude Code v2.1.287+。

# 方式一:单次试运行
git clone https://github.com/eddiezhan/xhs-count-mod
claude --plugin-dir ./xhs-count-mod

# 方式二:正式安装(在 Claude Code 会话里)
/plugin marketplace add eddiezhan/xhs-count-mod
/plugin install xhs-count@xhs-count-mod

使用

  1. 全屏模式下用鼠标选中一段文字
  2. 看 prompt 上方的 band:
  3. 未选中时显示灰色提示
  4. 选中后按字数变绿 / 黄 / 红
  5. 也可以跑 /xhs-count 查看命令是否注册成功

验证

claude plugin validate ./xhs-count-mod
# hooks: session.start, ui.render{component=AbovePrompt}
# calls: $.clock.every, $.command.register, $.ui.invalidate, $.ui.resolve, $.ui.selection

已知限制

  • $.ui.selection() 只在全屏(fullscreen)模式下返回选区,普通滚动模式选不中
  • band 的显示由所有 mod 共享,装了多个带 band 的 mod 会叠在一起

为什么开源这个

小红书 HOWTO 活动「我这行的AI路」的参赛作品:把我这行(独立开发博主)每天卡字数的痛点,沉淀成一个 90 行的 mod。别人的行业痛点——合同审查、排班、备课——同样值得一个 mod。

License

MIT

Source 1 files
hooks/register.js 75 lines
1// xhs-count —— 小红书字数哨兵
2// 选中一段文字,prompt 上方的 band 实时告诉你:
3// 够不够标题(≤20 字)、适不适合正文(≤1000 字)、超了几个字要删。
4
5const LIMIT_TITLE = 20
6const LIMIT_BODY = 1000
7const POLL_MS = 800
8
9// 上一次统计到的选区与结果
10let lastText = ''
11let lastStats = null
12
13function statsFor(text) {
14  // 中文按 Unicode 码点数,emoji 也算一个字
15  const count = [...text].length
16  if (count <= LIMIT_TITLE) {
17    return {
18      count,
19      label: `够标题(≤${LIMIT_TITLE}),还差 ${LIMIT_TITLE - count} 字拉满悬念`,
20      color: 'green',
21    }
22  }
23  if (count <= LIMIT_BODY) {
24    return {
25      count,
26      label: `适合正文,已用 ${count}/${LIMIT_BODY}`,
27      color: 'yellow',
28    }
29  }
30  return {
31    count,
32    label: `正文超了 ${count - LIMIT_BODY} 字,删!`,
33    color: 'red',
34  }
35}
36
37export function register(on) {
38  on('session.start', async ($, e, next) => {
39    await $.command.register({
40      name: 'xhs-count',
41      description: '小红书字数哨兵:选中文字,band 实时统计够不够标题/正文',
42    })
43    // 定时轮询选区;$.ui.selection() 在 fullscreen 下返回最近选中的文字
44    $.clock.every(POLL_MS, async () => {
45      try {
46        const sel = await $.ui.selection()
47        const text = typeof sel === 'string' ? sel : (sel?.text ?? '')
48        if (!text || text === lastText) return
49        lastText = text
50        lastStats = statsFor(text)
51        $.ui.invalidate('ui.render')
52      } catch {
53        // 选区不可用时保持上一次结果
54      }
55    })
56    return next(e)
57  })
58
59  // 常驻 band:prompt 上方那条
60  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
61    const { Box, Text } = $.ui.resolve(e)
62    if (!lastStats) {
63      return Box({ flexDirection: 'row', children: [
64        Text({
65          children: ['🔍 xhs字数哨兵 · 全屏选中一段文字,我帮你卡标题(≤20)/正文(≤1000)字数'],
66          dimColor: true,
67        }),
68      ]})
69    }
70    return Box({ flexDirection: 'row', columnGap: 2, children: [
71      Text({ children: [`🔍 选中 ${lastStats.count} 字 · ${lastStats.label}`], color: lastStats.color }),
72    ]})
73  })
74}
75