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

一个 Claude Code mod:在 prompt 上方的 band 里实时统计你选中的文字,告诉你——
写小红书帖子再也不用肉眼数格子。
选中文字(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
/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)模式下返回选区,普通滚动模式选不中小红书 HOWTO 活动「我这行的AI路」的参赛作品:把我这行(独立开发博主)每天卡字数的痛点,沉淀成一个 90 行的 mod。别人的行业痛点——合同审查、排班、备课——同样值得一个 mod。
MIT
hooks/register.js 75 lines1// 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