@weibaohui/dsh-code-poem 0.3.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 ADDED
@@ -0,0 +1,81 @@
1
+ # @weibaohui/dsh-code-poem
2
+
3
+ [![DSH plugin](https://img.shields.io/badge/dsh-plugin-green)](https://github.com/topics/dsh-plugin)
4
+
5
+ **代码如诗**:写代码的间隙,右下角翻开一张宣纸诗笺——随机一条古诗词、成语或《龙文鞭影》典故,共 **4598 条**。
6
+
7
+ ## 数据集
8
+
9
+ | 类别 | 条数 | 来源 |
10
+ |---|---|---|
11
+ | 典故 | 2064 | 《龙文鞭影》初集+二集(萧良有辑·杨臣诤增订),逐四字句,含白话释义/古注/简注 |
12
+ | 成语 | 1930 | 《中华成语故事大全集》(巴城)+《中华成语典故大全》(图说天下),两书合并 |
13
+ | 诗/词/文 | 604 | 陈引驰《你应该熟读的中国古诗/古词/古文》,含注释与赏析 |
14
+
15
+ 数据统一为一条目一记录的 JSON schema(`data/entries.json`,约 6.8MB,随包自带):
16
+
17
+ ```json
18
+ {
19
+ "id": "lw-00012",
20
+ "kind": "典故", // 典故 | 成语 | 诗 | 词 | 文
21
+ "title": "太宗怀鹞",
22
+ "sub": "《龙文鞭影》·01 一东",
23
+ "author": "明·萧良有 辑",
24
+ "front": "太宗怀鹞,桓典乘骢。", // 卡片正面
25
+ "digest": "《隋唐嘉话》记载:一次,唐太宗……", // 一句话释义
26
+ "sections": [ { "h": "白话释义", "t": "……" }, { "h": "古注", "t": "……" } ] // 展开区
27
+ }
28
+ ```
29
+
30
+ ## 功能
31
+
32
+ - **事件联动自动浮现(v0.3.0)**:订阅会话事件流——回合结束【功行圆满,且读一句】、工具报错【路遇荆棘,诗以解之】、中止【收势小憩】、用户提问(25% 概率)时自动弹出一张诗笺,带"缘起"引子,14 秒无操作自动收起;90 秒内不重复打扰;🔔 可随时关闭联动
33
+ - **低频批量拉取**:客户端每 5 分钟(或收到回合开始事件时)一次 `GET /bundle` 拉 30 条存本地池,事件触发与手动换笺都从池里取,几乎不产生请求压力;事件流轮询 15 秒一次,响应仅几百字节
34
+ - **随机诗笺**:点右下角 📜 卷轴按钮手动翻开卡片;内容随机,近期 10 条不重复
35
+ - **类别过滤**:随机 / 诗 / 词 / 文 / 成语 / 典故,选择持久化
36
+ - **❤ 收藏**:喜欢的句子点爱心,收藏进 localStorage;⭐ 打开收藏夹,点击回看(可取消)
37
+ - **展开看更多**:卡片正面只给一句话释义,"展开 释义 · 故事"后看全部分节——白话释义、古注、出处、故事、注释、赏析
38
+ - **宣纸美学**:米色宣纸卡片、墨字楷体、朱砂印章类别戳(诗/词/文/语/典),与暗色调 Web UI 形成对比
39
+
40
+ ## 安装
41
+
42
+ ```bash
43
+ dsh plugin --profile web add @weibaohui/dsh-code-poem -w
44
+ ```
45
+
46
+ 装完重启 `dsh web` 即生效,数据集已内置。
47
+
48
+ ## HTTP API(/dsh-code-poem/api/*)
49
+
50
+ | 接口 | 说明 |
51
+ |---|---|
52
+ | `GET /draw?kind=all\|诗\|词\|文\|成语\|典故&exclude=id1,id2` | 随机抽一条(exclude 优先避开) |
53
+ | `GET /bundle?kind=&n=30&exclude=` | 一次随机取 n 条,客户端低频批量拉取 |
54
+ | `GET /entry?id=` | 按 id 取整条 |
55
+ | `GET /feed?after=N` | 会话事件流(user_msg/tool_call/tool_error/turn_end…),供客户端联动 |
56
+ | `GET /status` | 数据集概况 |
57
+ | `GET /reload` | 数据文件更新后重建索引 |
58
+
59
+ ## 数据更新
60
+
61
+ 三个知识库(`~/Desktop/不要了/` 下的 文学/龙文鞭影工作区、成语/成语工作区、古诗/古诗工作区)随逐本解析持续完善,重新导出即可:
62
+
63
+ ```bash
64
+ npm run export:data # 跑 scripts/export_data.py 重新生成 data/entries.json
65
+ curl "http://127.0.0.1:19080/dsh-code-poem/api/reload" # 热刷新
66
+ ```
67
+
68
+ 也可用插件配置 `dataDir` 指向外部数据目录(目录下放 `entries.json`)。
69
+
70
+ ## 开发
71
+
72
+ ```bash
73
+ npm run export:data # 从三个知识库导出数据集
74
+ npm run build:client # 重建 client/bundle-mc.js
75
+ npm run check # 语法检查
76
+ npm test # smoke 测试
77
+ ```
78
+
79
+ ## 设计说明
80
+
81
+ 条目 schema 与三个上游知识库("一条目一 md + 变更记录")解耦:`scripts/export_data.py` 负责把三种 md 版式解析成统一 schema,上游任何修订重跑导出即可,插件侧不感知来源差异。
@@ -0,0 +1,350 @@
1
+ /* Generated from client/index.js by scripts/build-client.mjs — do not edit by hand.
2
+ * Regenerate with: npm run build:client
3
+ */
4
+ window.__ModuleLoader__.load({
5
+ id: "@weibaohui/dsh-code-poem",
6
+ factory: (require) => {
7
+ var module = { exports: {} }
8
+ var exports = module.exports
9
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" })
10
+ var React = require("react")
11
+ 'use strict'
12
+
13
+ /**
14
+ * dsh-code-poem — Client half
15
+ *
16
+ * 代码如诗:右下角一枚"诗笺"卷轴按钮,点开宣纸卡片,随机展示一条
17
+ * 古诗词 / 成语 / 龙文鞭影典故。
18
+ * - ❤ 收藏(localStorage 持久化,⭐ 打开收藏夹)
19
+ * - 🎲 换一条(可按 诗/词/文/成语/典故 过滤,近期不重复)
20
+ * - 「释义 · 故事」展开区:白话释义 / 古注 / 出处 / 故事 / 注释 / 赏析
21
+ */
22
+
23
+ const STYLE = `
24
+ @keyframes cp-pop{0%{transform:scale(.92) translateY(8px);opacity:0}100%{transform:scale(1) translateY(0);opacity:1}}
25
+ @keyframes cp-flip{0%{transform:rotateY(90deg);opacity:0}100%{transform:rotateY(0);opacity:1}}
26
+ .cp-root{position:fixed;right:26px;bottom:20px;z-index:1200;display:flex;flex-direction:column;align-items:flex-end;gap:10px;
27
+ font:13px/1.75 "Kaiti SC","STKaiti","KaiTi","FangSong","Songti SC","SimSun",serif;user-select:none}
28
+ /* 卷轴按钮 */
29
+ .cp-toggle{width:46px;height:46px;border-radius:50%;border:1.5px solid #b5543b;cursor:pointer;
30
+ background:linear-gradient(145deg,#f8f2e2,#efe6cf);color:#8c3226;font-size:22px;line-height:1;
31
+ box-shadow:0 6px 18px rgba(0,0,0,.45),inset 0 0 0 3px rgba(140,50,38,.12);display:flex;align-items:center;justify-content:center}
32
+ .cp-toggle:hover{transform:translateY(-2px);box-shadow:0 9px 22px rgba(0,0,0,.5),inset 0 0 0 3px rgba(140,50,38,.22)}
33
+ /* 诗笺卡片:宣纸 */
34
+ .cp-card{position:relative;width:352px;max-height:min(76vh,640px);display:flex;flex-direction:column;overflow:hidden;
35
+ background:linear-gradient(160deg,#faf5e8 0%,#f4ecd8 55%,#efe4cb 100%);color:#33291f;border-radius:6px;
36
+ border:1px solid #c9b98f;box-shadow:0 14px 44px rgba(0,0,0,.5),inset 0 0 0 4px rgba(181,84,59,.07),inset 0 0 0 5px rgba(120,98,60,.18);
37
+ animation:cp-pop .22s ease}
38
+ .cp-card::before{content:"";position:absolute;inset:6px;border:1px solid rgba(140,50,38,.18);border-radius:4px;pointer-events:none}
39
+ /* 顶部:印章 + 标题 */
40
+ .cp-head{display:flex;align-items:center;gap:10px;padding:12px 14px 6px 16px}
41
+ .cp-seal{flex-shrink:0;width:34px;height:34px;background:linear-gradient(150deg,#a4342a,#8c2a22);color:#f6ead2;border-radius:5px;
42
+ display:flex;align-items:center;justify-content:center;font-size:15px;font-weight:700;letter-spacing:0;
43
+ box-shadow:0 2px 6px rgba(140,42,34,.45);transform:rotate(-3deg);writing-mode:vertical-rl;line-height:1.1}
44
+ .cp-htext{flex:1;min-width:0}
45
+ .cp-title{font-size:19px;font-weight:700;color:#2f2418;letter-spacing:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
46
+ .cp-sub{font-size:11px;color:#8a7455;letter-spacing:.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
47
+ .cp-close{flex-shrink:0;width:24px;height:24px;border:none;background:transparent;color:#a08c66;font-size:15px;cursor:pointer;border-radius:6px}
48
+ .cp-close:hover{background:rgba(140,50,38,.1);color:#8c3226}
49
+ /* 正文 */
50
+ .cp-body{flex:1;overflow-y:auto;padding:8px 20px 10px;scrollbar-width:thin;scrollbar-color:#c9b98f transparent}
51
+ .cp-front{white-space:pre-wrap;text-align:center;font-size:15.5px;line-height:2.05;color:#33291f;letter-spacing:1.5px;
52
+ padding:6px 0 10px;animation:cp-flip .3s ease}
53
+ .cp-front.cp-left{text-align:left;font-size:14px;line-height:1.95;letter-spacing:.5px}
54
+ .cp-digest{position:relative;font-size:12.5px;color:#6f5c3f;line-height:1.8;border-top:1px dashed #cbbb92;padding-top:8px;margin-top:2px;
55
+ display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
56
+ .cp-digest.cp-open{-webkit-line-clamp:unset}
57
+ /* 展开区 */
58
+ .cp-more{margin-top:10px;border-top:1px solid #cbbb92;padding-top:8px;animation:cp-pop .2s ease}
59
+ .cp-sec{margin-bottom:10px}
60
+ .cp-sec h4{margin:0 0 4px;font-size:12px;color:#8c3226;letter-spacing:2px;font-weight:700}
61
+ .cp-sec h4::before{content:"◆ ";font-size:9px;vertical-align:2px}
62
+ .cp-sec p{margin:0;font-size:12.5px;line-height:1.85;color:#4a3d2c;white-space:pre-wrap}
63
+ /* 展开按钮 */
64
+ .cp-toggle-more{width:100%;border:none;background:transparent;color:#8c3226;font-size:12px;letter-spacing:2px;cursor:pointer;
65
+ padding:7px 0 2px;font-family:inherit}
66
+ .cp-toggle-more:hover{color:#5e1f16}
67
+ /* 底部工具栏 */
68
+ .cp-foot{display:flex;align-items:center;gap:4px;padding:8px 12px;border-top:1px solid #d5c69d;background:rgba(233,221,192,.55)}
69
+ .cp-btn{border:1px solid transparent;background:transparent;color:#5f4d33;font-size:14px;cursor:pointer;border-radius:8px;
70
+ padding:4px 8px;line-height:1;font-family:inherit}
71
+ .cp-btn:hover{background:rgba(140,50,38,.09)}
72
+ .cp-btn.cp-loved{color:#b5362a}
73
+ .cp-chips{flex:1;display:flex;gap:2px;justify-content:flex-end;flex-wrap:wrap}
74
+ .cp-chip{border:1px solid #c9b98f;background:transparent;color:#7a6748;font-size:10.5px;border-radius:9px;padding:2px 7px;
75
+ cursor:pointer;font-family:inherit;letter-spacing:1px}
76
+ .cp-chip.cp-on{background:#8c3226;border-color:#8c3226;color:#f6ead2}
77
+ /* 收藏夹面板 */
78
+ .cp-favs{flex:1;overflow-y:auto;padding:4px 12px 10px;scrollbar-width:thin;scrollbar-color:#c9b98f transparent}
79
+ .cp-favrow{display:flex;align-items:center;gap:8px;padding:6px 4px;border-bottom:1px dashed #d5c69d;cursor:pointer}
80
+ .cp-favrow:hover{background:rgba(140,50,38,.06)}
81
+ .cp-favrow .cp-fk{flex-shrink:0;font-size:10px;color:#a4342a;border:1px solid rgba(164,52,42,.5);border-radius:4px;padding:1px 5px}
82
+ .cp-favrow .cp-ft{flex:1;font-size:13.5px;color:#33291f;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
83
+ .cp-favrow .cp-fd{flex-shrink:0;color:#b0a184;font-size:11px}
84
+ .cp-favempty{text-align:center;color:#a08c66;font-size:12.5px;padding:26px 0}
85
+ .cp-hint{font-size:10.5px;color:#a8946e;text-align:center;padding:0 0 6px;letter-spacing:1px}
86
+ .cp-origin{display:flex;align-items:center;gap:6px;padding:5px 16px 0;font-size:11px;color:#8c3226;letter-spacing:1.5px}
87
+ .cp-origin::before{content:"";width:5px;height:5px;background:#a4342a;border-radius:1px;transform:rotate(45deg);flex-shrink:0}
88
+ .cp-card.cp-auto{animation:cp-pop .3s ease}
89
+ `
90
+
91
+ const KINDS = ['诗', '词', '文', '成语', '典故']
92
+ const SEAL = { '诗': '诗', '词': '词', '文': '文', '成语': '语', '典故': '典' }
93
+ const LS_FAVS = 'cp-favs'
94
+ const LS_KIND = 'cp-kind'
95
+ const LS_RECENT = 'cp-recent'
96
+ const LS_AUTO = 'cp-auto'
97
+
98
+ /** 会话事件 → 诗笺缘起语。turn 类必触发;user_msg 按概率。 */
99
+ const ORIGINS = {
100
+ turn_end: '功行圆满,且读一句——',
101
+ tool_error: '路遇荆棘,诗以解之——',
102
+ turn_abort: '收势小憩,翻页片刻——',
103
+ user_msg: '道友起意,赠笺一张——',
104
+ }
105
+
106
+ const favStore = {
107
+ list() { try { return JSON.parse(localStorage.getItem(LS_FAVS) || '[]') } catch { return [] } },
108
+ save(list) { try { localStorage.setItem(LS_FAVS, JSON.stringify(list)) } catch {} },
109
+ }
110
+
111
+ const recentStore = {
112
+ list() { try { return JSON.parse(localStorage.getItem(LS_RECENT) || '[]') } catch { return [] } },
113
+ push(id) {
114
+ const list = this.list().filter((x) => x !== id)
115
+ list.unshift(id)
116
+ try { localStorage.setItem(LS_RECENT, JSON.stringify(list.slice(0, 10))) } catch {}
117
+ },
118
+ }
119
+
120
+ module.exports = {
121
+ inject: ['slots'],
122
+
123
+ apply(ctx) {
124
+ const slots = ctx.get('slots')
125
+ if (slots === undefined) return
126
+ const React = require('react')
127
+ const { useState, useEffect, useCallback } = React
128
+
129
+ const api = (p) => fetch(`/dsh-code-poem/api/${p}`).then((r) => r.json())
130
+
131
+ function App() {
132
+ const [open, setOpen] = useState(false)
133
+ const [entry, setEntry] = useState(null)
134
+ const [kind, setKind] = useState(() => localStorage.getItem(LS_KIND) || 'all')
135
+ const [loved, setLoved] = useState(false)
136
+ const [favs, setFavs] = useState(() => favStore.list())
137
+ const [showFavs, setShowFavs] = useState(false)
138
+ const [expanded, setExpanded] = useState(false)
139
+ const [auto, setAuto] = useState(() => localStorage.getItem(LS_AUTO) !== 'off')
140
+ const [origin, setOrigin] = useState(null) // 本次弹出的缘起(null=手动)
141
+ const lastFeedId = React.useRef(0)
142
+ const lastAutoAt = React.useRef(0)
143
+ const autoRef = React.useRef(auto)
144
+ autoRef.current = auto
145
+ const kindRef = React.useRef(kind)
146
+ kindRef.current = kind
147
+
148
+ // 本地诗笺池:每 5 分钟或收到回合开始事件时批量拉 30 条,
149
+ // 事件触发/手动换一条都从池里取,几乎不再打 API
150
+ const poolRef = React.useRef([])
151
+ const refreshBundle = useCallback(async (k) => {
152
+ try {
153
+ const exclude = recentStore.list().join(',')
154
+ const r = await api(`bundle?kind=${encodeURIComponent(k || 'all')}&n=30&exclude=${encodeURIComponent(exclude)}`)
155
+ if (r && r.entries && r.entries.length) {
156
+ poolRef.current = r.entries
157
+ return true
158
+ }
159
+ } catch {}
160
+ return false
161
+ }, [])
162
+
163
+ const draw = useCallback(async (k) => {
164
+ if (!poolRef.current.length) await refreshBundle(k || kindRef.current)
165
+ const e = poolRef.current.shift()
166
+ if (e) {
167
+ setEntry(e)
168
+ setExpanded(false)
169
+ recentStore.push(e.id)
170
+ return
171
+ }
172
+ try { // 池子空且批量拉取失败时的兜底单抽
173
+ const exclude = recentStore.list().join(',')
174
+ const r = await api(`draw?kind=${encodeURIComponent(k || 'all')}&exclude=${encodeURIComponent(exclude)}`)
175
+ if (r && r.entry) {
176
+ setEntry(r.entry)
177
+ setExpanded(false)
178
+ recentStore.push(r.entry.id)
179
+ }
180
+ } catch {}
181
+ }, [refreshBundle])
182
+
183
+ useEffect(() => { if (open && !entry) draw(kind) }, [open])
184
+
185
+ // ── 事件联动:轮询会话事件流,随机自动浮现诗笺 ──
186
+ React.useEffect(() => {
187
+ const react = (ev) => {
188
+ if (!autoRef.current) return
189
+ let why = null
190
+ if (ev.kind === 'turn_end' || ev.kind === 'tool_error' || ev.kind === 'turn_abort') why = ev.kind
191
+ else if (ev.kind === 'user_msg' && Math.random() < 0.25) why = ev.kind
192
+ if (!why) return
193
+ const now = Date.now()
194
+ if (now - lastAutoAt.current < 90000) return // 90 秒内不重复打扰
195
+ lastAutoAt.current = now
196
+ setOrigin(ORIGINS[why])
197
+ setExpanded(false)
198
+ setOpen(true)
199
+ draw(kindRef.current)
200
+ }
201
+ const tick = () => {
202
+ api(`feed?after=${lastFeedId.current}`).then((r) => {
203
+ for (const ev of r.events || []) {
204
+ lastFeedId.current = Math.max(lastFeedId.current, ev.id)
205
+ if (ev.kind === 'turn_start') refreshBundle(kindRef.current) // 回合开始 → 顺手换一批
206
+ react(ev)
207
+ }
208
+ }).catch(() => {})
209
+ }
210
+ refreshBundle(kindRef.current) // 启动先取一批
211
+ const iv = setInterval(tick, 15000) // 事件流 15 秒一轮(响应很小)
212
+ const bv = setInterval(() => refreshBundle(kindRef.current), 300000) // 5 分钟换一批
213
+ return () => { clearInterval(iv); clearInterval(bv) }
214
+ }, [draw, refreshBundle])
215
+
216
+ // 自动浮现的卡片:14 秒无操作自动收起(展开或手动交互后不收)
217
+ React.useEffect(() => {
218
+ if (!open || !origin) return
219
+ const t = setTimeout(() => { if (!expanded) setOpen(false) }, 14000)
220
+ return () => clearTimeout(t)
221
+ }, [open, origin, expanded, entry])
222
+
223
+ const autoOff = () => { setOrigin(null) } // 用户触摸卡片 → 视为手动模式,不再自动收
224
+
225
+ useEffect(() => {
226
+ setLoved(!!entry && favs.some((f) => f.id === entry.id))
227
+ }, [entry, favs])
228
+
229
+ const toggleFav = () => {
230
+ if (!entry) return
231
+ setFavs((prev) => {
232
+ const has = prev.some((f) => f.id === entry.id)
233
+ const next = has
234
+ ? prev.filter((f) => f.id !== entry.id)
235
+ : [{ id: entry.id, title: entry.title, kind: entry.kind, front: entry.front, digest: entry.digest }, ...prev]
236
+ favStore.save(next)
237
+ return next
238
+ })
239
+ }
240
+
241
+ const openFav = async (f) => {
242
+ try {
243
+ const r = await api(`entry?id=${encodeURIComponent(f.id)}`)
244
+ if (r && r.entry) { setEntry(r.entry); setExpanded(true); setShowFavs(false) }
245
+ else { setEntry({ id: f.id, kind: f.kind, title: f.title, sub: '(收藏快照)', author: '', front: f.front, digest: f.digest, sections: [] }) ; setExpanded(false); setShowFavs(false) }
246
+ } catch {}
247
+ }
248
+
249
+ const pickKind = (k) => {
250
+ setKind(k)
251
+ try { localStorage.setItem(LS_KIND, k) } catch {}
252
+ poolRef.current = [] // 类别变了,旧池子作废
253
+ draw(k)
254
+ }
255
+
256
+ const frontCls = entry && (entry.kind === '文' || entry.kind === '成语') ? 'cp-front cp-left' : 'cp-front'
257
+ const sections = (entry && entry.sections) || []
258
+
259
+ return React.createElement('div', { className: 'cp-root' },
260
+ open && React.createElement('div', {
261
+ className: 'cp-card' + (origin ? ' cp-auto' : ''),
262
+ onMouseDown: autoOff,
263
+ },
264
+ React.createElement('div', { className: 'cp-head' },
265
+ React.createElement('div', { className: 'cp-seal' }, entry ? (SEAL[entry.kind] || '诗') : '诗'),
266
+ React.createElement('div', { className: 'cp-htext' },
267
+ React.createElement('div', { className: 'cp-title' }, entry ? entry.title : '诗笺'),
268
+ React.createElement('div', { className: 'cp-sub' },
269
+ entry ? [entry.sub, entry.author].filter(Boolean).join(' · ') : '代码如诗')),
270
+ React.createElement('button', { className: 'cp-close', title: '收起', onClick: () => setOpen(false) }, '✕')),
271
+ origin && React.createElement('div', { className: 'cp-origin' }, ORIGINS[origin] || '缘起'),
272
+ showFavs
273
+ ? React.createElement('div', { className: 'cp-favs' },
274
+ favs.length === 0 && React.createElement('div', { className: 'cp-favempty' }, '还没有收藏,遇到喜欢的句子点个 ❤ 吧'),
275
+ favs.map((f) => React.createElement('div', {
276
+ key: f.id, className: 'cp-favrow',
277
+ onClick: () => openFav(f), title: f.digest || '',
278
+ },
279
+ React.createElement('span', { className: 'cp-fk' }, SEAL[f.kind] || f.kind),
280
+ React.createElement('span', { className: 'cp-ft' }, f.title),
281
+ React.createElement('span', {
282
+ className: 'cp-fd', title: '取消收藏',
283
+ onClick: (ev) => {
284
+ ev.stopPropagation()
285
+ const next = favs.filter((x) => x.id !== f.id)
286
+ favStore.save(next); setFavs(next)
287
+ },
288
+ }, '✖'))))
289
+ : React.createElement('div', { className: 'cp-body' },
290
+ entry && React.createElement('div', { key: entry.id, className: 'cp-front ' + frontCls.split(' ').slice(1).join(' ') || 'cp-front' },
291
+ entry.front),
292
+ entry && entry.digest && React.createElement('div', {
293
+ className: 'cp-digest' + (expanded ? ' cp-open' : ''),
294
+ onClick: () => setExpanded(true), title: '点击展开',
295
+ }, entry.digest),
296
+ expanded && React.createElement('div', { className: 'cp-more', key: 'more' },
297
+ ...(sections.length
298
+ ? sections.map((s, i) => React.createElement('div', { className: 'cp-sec', key: i },
299
+ React.createElement('h4', null, s.h),
300
+ React.createElement('p', null, s.t)))
301
+ : [React.createElement('div', { className: 'cp-favempty', key: 'empty' }, '这条没有更多释义了')])),
302
+ !expanded && React.createElement('button', {
303
+ className: 'cp-toggle-more',
304
+ onClick: () => setExpanded(true),
305
+ }, sections.length ? '—— 展开 释义 · 故事 ▾ ——' : '—— 展开 全文 ▾ ——')),
306
+ React.createElement('div', { className: 'cp-foot' },
307
+ React.createElement('button', {
308
+ className: 'cp-btn' + (loved ? ' cp-loved' : ''),
309
+ title: loved ? '取消收藏' : '收藏', onClick: toggleFav,
310
+ }, loved ? '❤️' : '🤍'),
311
+ React.createElement('button', { className: 'cp-btn', title: '换一条', onClick: () => draw(kind) }, '🎲'),
312
+ React.createElement('button', {
313
+ className: 'cp-btn', title: '收藏夹',
314
+ onClick: () => setShowFavs((v) => !v),
315
+ }, showFavs ? '📖' : `⭐${favs.length || ''}`),
316
+ React.createElement('button', {
317
+ className: 'cp-btn', title: auto ? '事件联动:开(回合结束/报错时自动浮现)' : '事件联动:关',
318
+ onClick: () => {
319
+ const next = !auto
320
+ setAuto(next)
321
+ try { localStorage.setItem(LS_AUTO, next ? 'on' : 'off') } catch {}
322
+ },
323
+ }, auto ? '🔔' : '🔕'),
324
+ React.createElement('div', { className: 'cp-chips' },
325
+ React.createElement('button', {
326
+ className: 'cp-chip' + (kind === 'all' ? ' cp-on' : ''),
327
+ onClick: () => pickKind('all'),
328
+ }, '随机'),
329
+ KINDS.map((k) => React.createElement('button', {
330
+ key: k, className: 'cp-chip' + (kind === k ? ' cp-on' : ''),
331
+ onClick: () => pickKind(k),
332
+ }, k))))),
333
+ React.createElement('button', {
334
+ className: 'cp-toggle', title: '代码如诗:随机一条诗词/成语/典故',
335
+ onClick: () => setOpen((v) => !v),
336
+ }, '📜'),
337
+ open && !origin && React.createElement('div', { className: 'cp-hint' }, '诗词 · 成语 · 典故'),
338
+ )
339
+ }
340
+
341
+ slots.inject('sidebar.footer.action', () => slots.register(
342
+ { name: 'sidebar.footer.action', id: '@weibaohui/dsh-code-poem', order: 98 },
343
+ () => React.createElement(App, null)
344
+ ))
345
+ },
346
+ }
347
+
348
+ return module.exports
349
+ }
350
+ })