@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/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@weibaohui/dsh-code-poem",
3
+ "version": "0.3.0",
4
+ "description": "dsh 插件 · 代码如诗:4598 条古诗词、成语、龙文鞭影典故随机成笺,随会话事件自动浮现(回合结束/工具报错时送你一句),可 ❤ 收藏、展开看释义与故事,宣纸墨色的阅读卡片。",
5
+ "license": "MIT",
6
+ "keywords": [
7
+ "dsh",
8
+ "deepseek-harness",
9
+ "cordis",
10
+ "plugin",
11
+ "poem",
12
+ "idiom",
13
+ "allusion",
14
+ "dsh-plugin"
15
+ ],
16
+ "main": "src/index.js",
17
+ "exports": {
18
+ ".": "./src/index.js",
19
+ "./client": "./client/bundle-mc.js",
20
+ "./package.json": "./package.json"
21
+ },
22
+ "dsh": {
23
+ "bundle": {
24
+ "patch": "./cordis.patch.yml"
25
+ },
26
+ "client": {
27
+ "platform": "web"
28
+ }
29
+ },
30
+ "files": [
31
+ "src",
32
+ "client",
33
+ "data",
34
+ "cordis.patch.yml"
35
+ ],
36
+ "scripts": {
37
+ "check": "node --check src/index.js && node --check client/index.js",
38
+ "build:client": "node scripts/build-client.mjs",
39
+ "export:data": "python3 scripts/export_data.py",
40
+ "prepublishOnly": "npm run build:client && npm run check",
41
+ "test": "node --test test/*.test.mjs"
42
+ },
43
+ "engines": {
44
+ "node": ">=22.5"
45
+ },
46
+ "peerDependencies": {
47
+ "@deepseek-ai/cordis": "^4.0.1"
48
+ },
49
+ "devDependencies": {
50
+ "@deepseek-ai/cordis": "^4.0.1"
51
+ },
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "git+https://github.com/weibaohui/dsh-code-poem.git"
55
+ },
56
+ "homepage": "https://github.com/weibaohui/dsh-code-poem#readme",
57
+ "bugs": {
58
+ "url": "https://github.com/weibaohui/dsh-code-poem/issues"
59
+ },
60
+ "publishConfig": {
61
+ "access": "public"
62
+ }
63
+ }
package/src/index.js ADDED
@@ -0,0 +1,214 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * dsh-code-poem — Host half
5
+ *
6
+ * 代码如诗:从 4598 条诗笺(古诗词 604 / 成语 1930 / 龙文鞭影典故 2064)
7
+ * 中随机抽一条,供 web 客户端的诗笺卡片消费。数据集随 npm 包自带
8
+ * (data/entries.json,统一 schema:id/kind/title/sub/author/front/digest/sections),
9
+ * 也可用插件配置 `dataDir` 指向外部数据集(目录下放 entries.json)。
10
+ *
11
+ * HTTP API(/dsh-code-poem/api/*):
12
+ * GET /draw?kind=all|诗|词|文|成语|典故&exclude=id1,id2
13
+ * 随机抽一条(exclude 里的 id 优先避开)
14
+ * GET /bundle?kind=&n=30&exclude=
15
+ * 一次随机取 n 条(客户端低频批量拉取,本地池消费)
16
+ * GET /entry?id= 按 id 取整条
17
+ * GET /status 数据集概况
18
+ * GET /reload 重新读取数据文件
19
+ * GET /feed?after=N 会话事件流(turn_end/tool_error/user_msg...),供客户端联动
20
+ *
21
+ * 零 npm 依赖:只用 node 内置模块,经注入的 `webServer` 挂路由。
22
+ */
23
+
24
+ const fs = require('node:fs')
25
+ const path = require('node:path')
26
+
27
+ const BUNDLED_DATA_DIR = path.join(__dirname, '..', 'data')
28
+
29
+ /** 读取数据文件并构建索引。 */
30
+ class PoemStore {
31
+ constructor(dataDir) {
32
+ this.dataDir = dataDir
33
+ this.file = path.join(dataDir, 'entries.json')
34
+ this.entries = []
35
+ this.byIdMap = new Map()
36
+ this.byKind = new Map()
37
+ }
38
+
39
+ build() {
40
+ const raw = fs.readFileSync(this.file, 'utf8')
41
+ const list = JSON.parse(raw)
42
+ if (!Array.isArray(list) || !list.length) throw new Error('entries.json 为空或格式不对')
43
+ this.entries = list
44
+ this.byIdMap = new Map(list.map((e) => [e.id, e]))
45
+ this.byKind = new Map()
46
+ for (const e of list) {
47
+ if (!this.byKind.has(e.kind)) this.byKind.set(e.kind, [])
48
+ this.byKind.get(e.kind).push(e)
49
+ }
50
+ return list.length
51
+ }
52
+
53
+ kinds() { return [...this.byKind.keys()] }
54
+
55
+ draw(kind, exclude = []) {
56
+ const ex = new Set(exclude)
57
+ let pool
58
+ if (!kind || kind === 'all' || kind === '全部') pool = this.entries
59
+ else pool = this.byKind.get(kind)
60
+ if (!pool || !pool.length) return undefined
61
+ if (ex.size) {
62
+ const fresh = pool.filter((e) => !ex.has(e.id))
63
+ if (fresh.length) pool = fresh
64
+ }
65
+ return pool[Math.floor(Math.random() * pool.length)]
66
+ }
67
+
68
+ byId(id) { return this.byIdMap.get(id) }
69
+
70
+ status() {
71
+ const kinds = {}
72
+ for (const e of this.entries) kinds[e.kind] = (kinds[e.kind] || 0) + 1
73
+ return {
74
+ total: this.entries.length,
75
+ kinds,
76
+ dataFile: this.file,
77
+ mtime: fs.existsSync(this.file) ? fs.statSync(this.file).mtime.toISOString() : undefined,
78
+ }
79
+ }
80
+ }
81
+
82
+ module.exports = {
83
+ name: 'dsh-code-poem',
84
+ inject: ['webServer'],
85
+
86
+ __test: { PoemStore, BUNDLED_DATA_DIR },
87
+
88
+ apply(ctx, rawConfig) {
89
+ const config = rawConfig && typeof rawConfig === 'object' ? rawConfig : {}
90
+ const dataDir = config.dataDir || BUNDLED_DATA_DIR
91
+ const store = new PoemStore(dataDir)
92
+ try {
93
+ const n = store.build()
94
+ ctx.logger?.info?.(`[dsh-code-poem] 诗笺数据加载完成:${n} 条`)
95
+ } catch (err) {
96
+ ctx.logger?.warn?.(`[dsh-code-poem] 数据加载失败:${err.message}(${dataDir})`)
97
+ }
98
+
99
+ // ── 诗笺缘起:订阅会话事件流,供客户端随机触发诗笺 ──────────────────
100
+ const feed = []
101
+ let feedSeq = 0
102
+ const FEED_MAX = 60
103
+ const feedPush = (kind, extra = {}) => {
104
+ feedSeq += 1
105
+ feed.push({ id: feedSeq, at: new Date().toISOString(), kind, ...extra })
106
+ if (feed.length > FEED_MAX) feed.splice(0, feed.length - FEED_MAX)
107
+ }
108
+
109
+ ctx.effect(() => {
110
+ const onSessionEvent = (session, event) => {
111
+ try {
112
+ const sessionId = session && session.id
113
+ const base = { sessionId: typeof sessionId === 'string' ? sessionId : undefined }
114
+ switch (event && event.type) {
115
+ case 'user/message': {
116
+ const src = event.data && event.data.source
117
+ if (src && src.kind !== 'user') return
118
+ const text = typeof (event.data && event.data.content) === 'string'
119
+ ? event.data.content
120
+ : ''
121
+ feedPush('user_msg', { ...base, text: text.replace(/\s+/g, ' ').slice(0, 80) })
122
+ break
123
+ }
124
+ case 'tool/call': {
125
+ const tool = (event.data && event.data.name) || 'tool'
126
+ feedPush('tool_call', { ...base, tool })
127
+ break
128
+ }
129
+ case 'tool/result': {
130
+ const tool = (event.data && event.data.name) || ''
131
+ const failed = !!(event.data && ((event.data.message && event.data.message.isError === true) || event.data.error !== undefined))
132
+ feedPush(failed ? 'tool_error' : 'tool_ok', { ...base, tool })
133
+ break
134
+ }
135
+ case 'turn/end':
136
+ feedPush('turn_end', base)
137
+ break
138
+ case 'turn/start':
139
+ feedPush('turn_start', base)
140
+ break
141
+ }
142
+ } catch (e) {
143
+ ctx.logger?.warn?.(`[dsh-code-poem] session/event handler: ${e && e.message}`)
144
+ }
145
+ }
146
+ const dispose = ctx.on('session/event', onSessionEvent)
147
+ return () => { try { dispose() } catch {} }
148
+ }, 'dsh-code-poem: session/event subscription')
149
+
150
+ const sendJson = (res, status, payload) => {
151
+ res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
152
+ res.end(JSON.stringify(payload))
153
+ }
154
+
155
+ ctx.effect(() => ctx.webServer.register({
156
+ kind: 'prefix',
157
+ path: '/dsh-code-poem/api',
158
+ handler: async (req, res) => {
159
+ try {
160
+ const url = new URL(req.url || '/', 'http://dsh.local')
161
+ const p = url.pathname.replace(/\/+$/, '')
162
+ if (req.method === 'GET' && p.endsWith('/dsh-code-poem/api/draw')) {
163
+ if (!store.entries.length) { sendJson(res, 503, { error: '诗笺索引为空' }); return }
164
+ const exclude = (url.searchParams.get('exclude') || '').split(',').filter(Boolean)
165
+ const e = store.draw(url.searchParams.get('kind'), exclude)
166
+ if (!e) { sendJson(res, 404, { error: '该类别下没有诗笺' }); return }
167
+ sendJson(res, 200, { entry: e })
168
+ return
169
+ }
170
+ if (req.method === 'GET' && p.endsWith('/dsh-code-poem/api/bundle')) {
171
+ if (!store.entries.length) { sendJson(res, 503, { error: '诗笺索引为空' }); return }
172
+ const kind = url.searchParams.get('kind') || 'all'
173
+ const n = Math.min(60, Math.max(1, Number(url.searchParams.get('n') || 30)))
174
+ const ex = new Set((url.searchParams.get('exclude') || '').split(',').filter(Boolean))
175
+ let pool = (!kind || kind === 'all' || kind === '全部') ? store.entries : store.byKind.get(kind) || []
176
+ const fresh = ex.size ? pool.filter((e) => !ex.has(e.id)) : pool
177
+ if (fresh.length >= n) pool = fresh
178
+ const picked = pool.slice()
179
+ for (let i = picked.length - 1; i > 0; i--) {
180
+ const j = Math.floor(Math.random() * (i + 1))
181
+ ;[picked[i], picked[j]] = [picked[j], picked[i]]
182
+ }
183
+ sendJson(res, 200, { entries: picked.slice(0, n) })
184
+ return
185
+ }
186
+ if (req.method === 'GET' && p.endsWith('/dsh-code-poem/api/entry')) {
187
+ const e = store.byId(url.searchParams.get('id') || '')
188
+ if (!e) { sendJson(res, 404, { error: '诗笺不存在' }); return }
189
+ sendJson(res, 200, { entry: e })
190
+ return
191
+ }
192
+ if (req.method === 'GET' && p.endsWith('/dsh-code-poem/api/status')) {
193
+ sendJson(res, 200, store.status())
194
+ return
195
+ }
196
+ if (req.method === 'GET' && p.endsWith('/dsh-code-poem/api/feed')) {
197
+ const after = Number(url.searchParams.get('after') || 0)
198
+ const events = feed.filter((ev) => ev.id > after)
199
+ sendJson(res, 200, { events })
200
+ return
201
+ }
202
+ if (req.method === 'GET' && p.endsWith('/dsh-code-poem/api/reload')) {
203
+ const n = store.build()
204
+ sendJson(res, 200, { reloaded: true, total: n })
205
+ return
206
+ }
207
+ sendJson(res, 404, { error: 'unknown api' })
208
+ } catch (err) {
209
+ sendJson(res, 500, { error: String(err && err.message || err) })
210
+ }
211
+ },
212
+ }), 'dsh-code-poem: api route')
213
+ },
214
+ }