@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 +81 -0
- package/client/bundle-mc.js +350 -0
- package/client/index.js +336 -0
- package/cordis.patch.yml +11 -0
- package/data/entries.json +1 -0
- package/package.json +63 -0
- package/src/index.js +214 -0
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
|
+
}
|