dsh-trpg-scenario 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +59 -2
- package/cordis.patch.yml +4 -0
- package/icon.svg +8 -0
- package/index.js +222 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +21 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 畔川俱乐部
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,60 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-trpg-scenario
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
DeepSeek Harness 的 AI 跑团插件:导入剧本包、开局、评分。
|
|
4
|
+
|
|
5
|
+
## 装
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
dsh plugin --profile desktop add dsh-trpg-scenario
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
或在 DSH 里让 Agent 用 `plugin_manager` 的 `install_bundle` 装本地目录。
|
|
12
|
+
|
|
13
|
+
装完重启 DSH 一次。
|
|
14
|
+
|
|
15
|
+
## 用
|
|
16
|
+
|
|
17
|
+
| 命令 | 作用 |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `/trpg list` | 列出已装剧本 |
|
|
20
|
+
| `/trpg import <zip>` | 导入一个剧本包 |
|
|
21
|
+
| `/trpg remove <名称>` | 删除剧本 |
|
|
22
|
+
| `/trpg play <名称>` | 开一局 |
|
|
23
|
+
| `/trpg judge <名称>` | 开评分会话 |
|
|
24
|
+
|
|
25
|
+
剧本库默认在 `~/.dsh/trpg/scenarios`。
|
|
26
|
+
|
|
27
|
+
## 剧本包格式
|
|
28
|
+
|
|
29
|
+
一个 zip,里面正好六个文件:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
manifest.yaml id / title / kind / minutes
|
|
33
|
+
system.md 玩法与规则
|
|
34
|
+
setting.md 背景、角色、初始困境
|
|
35
|
+
opening.md 开场场景
|
|
36
|
+
rubric.md 评分维度(每维 0–10)
|
|
37
|
+
judge.md 评委系统提示词
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
缺一个就会被拒收,并告诉你缺哪个。
|
|
41
|
+
|
|
42
|
+
## 上下文怎么进
|
|
43
|
+
|
|
44
|
+
`play` 时把三个文件作为**三个独立段落**注入系统提示词:
|
|
45
|
+
|
|
46
|
+
- `trpg/rules` ← `system.md`
|
|
47
|
+
- `trpg/world` ← `setting.md`
|
|
48
|
+
- `trpg/opening` ← `opening.md`
|
|
49
|
+
|
|
50
|
+
规则、世界、开场互不污染,换剧本只替换后两段。
|
|
51
|
+
|
|
52
|
+
## 限制
|
|
53
|
+
|
|
54
|
+
- 目前只有命令入口,没有图形界面的上传面板
|
|
55
|
+
- 依赖系统的 `unzip` 命令解包
|
|
56
|
+
- 未在 Windows 上验证
|
|
57
|
+
|
|
58
|
+
## 许可
|
|
59
|
+
|
|
60
|
+
MIT
|
package/cordis.patch.yml
ADDED
package/icon.svg
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
|
|
2
|
+
<rect x="4" y="10" width="56" height="44" rx="6" fill="#1f2937"/>
|
|
3
|
+
<path d="M4 20 L60 20" stroke="#4b5563" stroke-width="2"/>
|
|
4
|
+
<circle cx="12" cy="15" r="2.5" fill="#ef4444"/>
|
|
5
|
+
<circle cx="20" cy="15" r="2.5" fill="#f59e0b"/>
|
|
6
|
+
<circle cx="28" cy="15" r="2.5" fill="#10b981"/>
|
|
7
|
+
<text x="32" y="46" font-family="monospace" font-size="20" fill="#e5e7eb" text-anchor="middle">>_</text>
|
|
8
|
+
</svg>
|
package/index.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process'
|
|
2
|
+
import { mkdir, readdir, readFile, rm, stat } from 'node:fs/promises'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { promisify } from 'node:util'
|
|
6
|
+
|
|
7
|
+
const run = promisify(execFile)
|
|
8
|
+
|
|
9
|
+
const REQUIRED = ['manifest.yaml', 'system.md', 'setting.md', 'opening.md', 'rubric.md', 'judge.md']
|
|
10
|
+
|
|
11
|
+
export const name = 'dsh-trpg-scenario'
|
|
12
|
+
export const inject = ['commands', 'agents']
|
|
13
|
+
|
|
14
|
+
function parseManifest(text) {
|
|
15
|
+
const out = {}
|
|
16
|
+
for (const line of text.split('\n')) {
|
|
17
|
+
const m = /^([A-Za-z_][\w-]*)\s*:\s*(.*)$/.exec(line.trim())
|
|
18
|
+
if (m) out[m[1]] = m[2].replace(/^["']|["']$/g, '').trim()
|
|
19
|
+
}
|
|
20
|
+
return out
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function apply(ctx, config = {}) {
|
|
24
|
+
const root = config.root ?? join(homedir(), '.dsh', 'trpg', 'scenarios')
|
|
25
|
+
const ok = (text) => ({ kind: 'success', text })
|
|
26
|
+
const err = (text) => ({ kind: 'error', text })
|
|
27
|
+
|
|
28
|
+
async function installedNames() {
|
|
29
|
+
try {
|
|
30
|
+
const entries = await readdir(root, { withFileTypes: true })
|
|
31
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort()
|
|
32
|
+
} catch {
|
|
33
|
+
return []
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function loadScenario(dir) {
|
|
38
|
+
const meta = parseManifest(await readFile(join(dir, 'manifest.yaml'), 'utf8'))
|
|
39
|
+
const [system, setting, opening, rubric, judge] = await Promise.all(
|
|
40
|
+
['system.md', 'setting.md', 'opening.md', 'rubric.md', 'judge.md'].map((f) =>
|
|
41
|
+
readFile(join(dir, f), 'utf8'),
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
return { meta, system, setting, opening, rubric, judge }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function cmdList() {
|
|
48
|
+
const names = await installedNames()
|
|
49
|
+
if (!names.length) return ok(`剧本库为空:${root}\n用 /trpg import <zip 路径> 导入。`)
|
|
50
|
+
const lines = []
|
|
51
|
+
for (const n of names) {
|
|
52
|
+
try {
|
|
53
|
+
const { meta } = await loadScenario(join(root, n))
|
|
54
|
+
lines.push(`· ${n} ${meta.title ?? ''} ${meta.kind ?? ''} ${meta.minutes ?? ''} 分钟`)
|
|
55
|
+
} catch {
|
|
56
|
+
lines.push(`· ${n} (损坏,缺少文件)`)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return ok(`剧本库(${root})\n${lines.join('\n')}`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function cmdImport(rawPath) {
|
|
63
|
+
const zip = rawPath.trim().replace(/^["']|["']$/g, '')
|
|
64
|
+
if (!zip) return err('用法:/trpg import <zip 路径>')
|
|
65
|
+
try {
|
|
66
|
+
await stat(zip)
|
|
67
|
+
} catch {
|
|
68
|
+
return err(`找不到文件:${zip}`)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const tmp = join(root, '.incoming')
|
|
72
|
+
await rm(tmp, { recursive: true, force: true })
|
|
73
|
+
await mkdir(tmp, { recursive: true })
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
await run('unzip', ['-o', '-q', zip, '-d', tmp])
|
|
77
|
+
} catch (e) {
|
|
78
|
+
await rm(tmp, { recursive: true, force: true })
|
|
79
|
+
return err(`解压失败:${e.message}`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 兼容压缩包里多套一层目录
|
|
83
|
+
let base = tmp
|
|
84
|
+
const top = await readdir(tmp, { withFileTypes: true })
|
|
85
|
+
if (top.length === 1 && top[0].isDirectory()) base = join(tmp, top[0].name)
|
|
86
|
+
|
|
87
|
+
const missing = []
|
|
88
|
+
for (const f of REQUIRED) {
|
|
89
|
+
try {
|
|
90
|
+
await stat(join(base, f))
|
|
91
|
+
} catch {
|
|
92
|
+
missing.push(f)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (missing.length) {
|
|
96
|
+
await rm(tmp, { recursive: true, force: true })
|
|
97
|
+
return err(`剧本包缺少文件:${missing.join('、')}\n需要的文件:${REQUIRED.join('、')}`)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let meta
|
|
101
|
+
try {
|
|
102
|
+
meta = parseManifest(await readFile(join(base, 'manifest.yaml'), 'utf8'))
|
|
103
|
+
} catch {
|
|
104
|
+
meta = {}
|
|
105
|
+
}
|
|
106
|
+
const slug = (meta.id ?? meta.name ?? 'scenario').replace(/[^\w\u4e00-\u9fa5-]/g, '-')
|
|
107
|
+
|
|
108
|
+
const dest = join(root, slug)
|
|
109
|
+
await rm(dest, { recursive: true, force: true })
|
|
110
|
+
await run('mv', [base, dest])
|
|
111
|
+
await rm(tmp, { recursive: true, force: true })
|
|
112
|
+
|
|
113
|
+
return ok(`已导入:${slug}${meta.title ? `(${meta.title})` : ''}\n用 /trpg play ${slug} 开局。`)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function cmdRemove(rawName) {
|
|
117
|
+
const nm = rawName.trim()
|
|
118
|
+
if (!nm) return err('用法:/trpg remove <名称>')
|
|
119
|
+
const dest = join(root, nm)
|
|
120
|
+
if (!dest.startsWith(root)) return err('名称不合法')
|
|
121
|
+
try {
|
|
122
|
+
await stat(dest)
|
|
123
|
+
} catch {
|
|
124
|
+
return err(`没有这个剧本:${nm}`)
|
|
125
|
+
}
|
|
126
|
+
await rm(dest, { recursive: true, force: true })
|
|
127
|
+
return ok(`已删除:${nm}`)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function nextSessionId(kind) {
|
|
131
|
+
const rand = Math.random().toString(36).slice(2, 8)
|
|
132
|
+
return `trpg-${kind}-${Date.now().toString(36)}-${rand}`
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function cmdPlay(rawName) {
|
|
136
|
+
const nm = rawName.trim()
|
|
137
|
+
if (!nm) return err('用法:/trpg play <名称>')
|
|
138
|
+
const dir = join(root, nm)
|
|
139
|
+
let sc
|
|
140
|
+
try {
|
|
141
|
+
sc = await loadScenario(dir)
|
|
142
|
+
} catch {
|
|
143
|
+
return err(`剧本 ${nm} 不存在或缺少文件,先 /trpg list 看看。`)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const handle = await ctx.agents.create({
|
|
147
|
+
sessionId: nextSessionId('play'),
|
|
148
|
+
agentOptions: { provider: config.provider, model: config.model },
|
|
149
|
+
setup(agentCtx) {
|
|
150
|
+
agentCtx.systemPrompt.section({
|
|
151
|
+
name: 'trpg/rules',
|
|
152
|
+
order: 100,
|
|
153
|
+
text: sc.system,
|
|
154
|
+
})
|
|
155
|
+
agentCtx.systemPrompt.section({
|
|
156
|
+
name: 'trpg/world',
|
|
157
|
+
order: 110,
|
|
158
|
+
text: sc.setting,
|
|
159
|
+
})
|
|
160
|
+
agentCtx.systemPrompt.section({
|
|
161
|
+
name: 'trpg/opening',
|
|
162
|
+
order: 120,
|
|
163
|
+
text: `【开场场景】\n${sc.opening}`,
|
|
164
|
+
})
|
|
165
|
+
},
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
return ok(
|
|
169
|
+
`已开启剧本「${sc.meta.title ?? nm}」。\n\n开场:\n${sc.opening}\n\n直接说你的第一句话即可开始。`,
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function cmdJudge(rawName) {
|
|
174
|
+
const nm = rawName.trim()
|
|
175
|
+
if (!nm) return err('用法:/trpg judge <名称>')
|
|
176
|
+
let sc
|
|
177
|
+
try {
|
|
178
|
+
sc = await loadScenario(join(root, nm))
|
|
179
|
+
} catch {
|
|
180
|
+
return err(`剧本 ${nm} 不存在或缺少文件。`)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
await ctx.agents.create({
|
|
184
|
+
sessionId: nextSessionId('judge'),
|
|
185
|
+
agentOptions: { provider: config.provider, model: config.model },
|
|
186
|
+
setup(agentCtx) {
|
|
187
|
+
agentCtx.systemPrompt.section({ name: 'trpg/judge', order: 100, text: sc.judge })
|
|
188
|
+
agentCtx.systemPrompt.section({ name: 'trpg/rubric', order: 110, text: sc.rubric })
|
|
189
|
+
agentCtx.systemPrompt.section({ name: 'trpg/world', order: 120, text: sc.setting })
|
|
190
|
+
},
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
return ok(`已开启评分会话(${sc.meta.title ?? nm})。\n把各组导出的 Session 日志拖进来,或告诉我存档路径。`)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const register = ctx.effect ?? ((fn) => fn())
|
|
197
|
+
register(() => ctx.commands.register({
|
|
198
|
+
name: 'trpg',
|
|
199
|
+
description: 'AI 跑团:列出剧本、导入剧本包、开局、评分',
|
|
200
|
+
input: { hint: 'list | import <zip> | remove <名称> | play <名称> | judge <名称>' },
|
|
201
|
+
async handler({ rawInput }) {
|
|
202
|
+
const line = String(rawInput ?? '').trim()
|
|
203
|
+
const [sub, ...rest] = line.split(/\s+/)
|
|
204
|
+
const arg = rest.join(' ')
|
|
205
|
+
switch ((sub || 'list').toLowerCase()) {
|
|
206
|
+
case 'list':
|
|
207
|
+
return cmdList()
|
|
208
|
+
case 'import':
|
|
209
|
+
return cmdImport(arg)
|
|
210
|
+
case 'remove':
|
|
211
|
+
case 'rm':
|
|
212
|
+
return cmdRemove(arg)
|
|
213
|
+
case 'play':
|
|
214
|
+
return cmdPlay(arg)
|
|
215
|
+
case 'judge':
|
|
216
|
+
return cmdJudge(arg)
|
|
217
|
+
default:
|
|
218
|
+
return err('未知子命令。用 /trpg list|import|remove|play|judge')
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
}))
|
|
222
|
+
}
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-trpg-scenario",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "AI 跑团剧本加载器:导入剧本包、开局、评分",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./index.js",
|
|
8
|
+
"./locale/*.json": "./locale/*.json",
|
|
9
|
+
"./icon": "./icon.svg"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"index.js",
|
|
13
|
+
"cordis.patch.yml",
|
|
14
|
+
"locale/*.json",
|
|
15
|
+
"icon.svg",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"dsh": {
|
|
19
|
+
"bundle": {
|
|
20
|
+
"patch": "./cordis.patch.yml"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT"
|
|
6
24
|
}
|