dsh-plugin-beyond-simulator 2.1.1 → 2.2.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/README.md +69 -130
- package/dsh-plugin/cordis.patch.yml +1 -1
- package/dsh-plugin/dist/client.js +10 -6
- package/dsh-plugin/dist/index.js +52 -16
- package/dsh-plugin/dist/play-renderer.js +72 -71
- package/dsh-plugin/dist/worker.js +47 -11
- package/dsh-plugin/lib/play.html +56 -40
- package/dsh-plugin/presets/wonderland-lua-builder/README.md +9 -1
- package/dsh-plugin/presets/wonderland-lua-builder/agent.cordis.yml +1 -1
- package/dsh-plugin/presets/wonderland-lua-builder/evals/agent-behavior.md +4 -4
- package/dsh-plugin/presets/wonderland-lua-builder/skills/qxqy-game-studio/references/07-device-validation.md +4 -4
- package/dsh-plugin/skill.md +9 -2
- package/package.json +2 -2
package/dsh-plugin/lib/play.html
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
-
<title>千星沙箱 · 独立试玩</title>
|
|
6
|
+
<title data-i18n="千星沙箱 · 独立试玩">千星沙箱 · 独立试玩</title>
|
|
7
7
|
<style>
|
|
8
8
|
*{box-sizing:border-box}html,body{width:100%;height:100%;margin:0;overflow:hidden}
|
|
9
9
|
body{--bg:#0c1422;--panel:#101a2a;--panel2:#152238;--line:#263750;--text:#edf3ff;--muted:#8f9caf;--workspace:#111c2c;--input:#0d1726;--blue:#4382f3;color:var(--text);background:var(--bg);font:13px/1.45 Inter,"Microsoft YaHei UI","Microsoft YaHei",sans-serif}
|
|
@@ -19,27 +19,32 @@
|
|
|
19
19
|
@media(max-width:760px){.debug-view{grid-template-columns:1fr}.tree-panel{display:none}.brand strong{font-size:14px}.brand .devsel{width:104px;margin-left:6px}.actions button{padding:0 9px}.actions #theme{display:none}.view-tabs button{min-width:64px;padding:0 10px}}
|
|
20
20
|
</style>
|
|
21
21
|
</head>
|
|
22
|
-
<body>
|
|
22
|
+
<body data-qxqy-font-ui>
|
|
23
23
|
<div class="app">
|
|
24
24
|
<header class="bar">
|
|
25
|
-
<div class="brand"><span class="icon">UI</span><strong>模拟器</strong><select id="deviceSel" class="devsel hidden" aria-label="切换设备画布" title="切换设备画布:以新画布重建运行时,GetUICanvasSize 与预设像素一致"></select><select id="playerCountSel" class="playersel hidden" aria-label="试玩人数" title="试玩人数:最多 8 人,每人一份独立客户端"></select><select id="playerViewSel" class="viewsel hidden" aria-label="当前玩家视角" title="切换当前玩家视角:输入、画面和 PlayerSelf 都跟随该玩家"></select><span class="device">设备
|
|
26
|
-
<div class="view-tabs" role="tablist" aria-label="试玩页面"><button class="active" data-view="play" role="tab">试玩</button><button data-view="log" role="tab">日志</button></div>
|
|
27
|
-
<div class="actions"><button id="audio" data-audio-toggle title="点击解锁浏览器音效">开启声音</button><button id="pause">Ⅱ 暂停运行</button><button id="step" disabled title="暂停后可单帧调试">▷ 单帧调试</button><button class="stop" id="stop">■ 结束试玩</button><button class="primary hidden" id="restart">↻ 重新开始</button><button id="theme" title="切换亮暗主题">◐</button><button id="close">返回编辑</button></div>
|
|
25
|
+
<div class="brand"><span class="icon">UI</span><strong data-i18n="模拟器">模拟器</strong><select id="deviceSel" class="devsel hidden" aria-label="切换设备画布" data-i18n-aria-label="切换设备画布" title="切换设备画布:以新画布重建运行时,GetUICanvasSize 与预设像素一致" data-i18n-title="切换设备画布:以新画布重建运行时,GetUICanvasSize 与预设像素一致"></select><select id="playerCountSel" class="playersel hidden" aria-label="试玩人数" data-i18n-aria-label="试玩人数" title="试玩人数:最多 8 人,每人一份独立客户端" data-i18n-title="试玩人数:最多 8 人,每人一份独立客户端"></select><select id="playerViewSel" class="viewsel hidden" aria-label="当前玩家视角" data-i18n-aria-label="当前玩家视角" title="切换当前玩家视角:输入、画面和 PlayerSelf 都跟随该玩家" data-i18n-title="切换当前玩家视角:输入、画面和 PlayerSelf 都跟随该玩家"></select><span class="device"><span data-i18n="设备">设备 </span><b id="resolution" data-i18n="准备中…">准备中…</b></span></div>
|
|
26
|
+
<div class="view-tabs" role="tablist" aria-label="试玩页面" data-i18n-aria-label="试玩页面"><button class="active" data-view="play" role="tab" data-i18n="试玩">试玩</button><button data-view="log" role="tab" data-i18n="日志">日志</button></div>
|
|
27
|
+
<div class="actions"><button id="keyInput" data-key-input-toggle aria-pressed="true" title="转发官方键位的按下/抬起事件(含鼠标左右键);关闭不影响画布点击" data-i18n-title="转发官方键位的按下/抬起事件(含鼠标左右键);关闭不影响画布点击" data-i18n="键位响应:开">键位响应:开</button><button id="audio" data-audio-toggle title="点击解锁浏览器音效" data-i18n-title="点击解锁浏览器音效" data-i18n="开启声音">开启声音</button><button id="pause" data-i18n="Ⅱ 暂停运行">Ⅱ 暂停运行</button><button id="step" disabled title="暂停后可单帧调试" data-i18n-title="暂停后可单帧调试" data-i18n="▷ 单帧调试">▷ 单帧调试</button><button class="stop" id="stop" data-i18n="■ 结束试玩">■ 结束试玩</button><button class="primary hidden" id="restart" data-i18n="↻ 重新开始">↻ 重新开始</button><button id="theme" title="切换亮暗主题" data-i18n-title="切换亮暗主题">◐</button><button id="close" data-i18n="返回编辑">返回编辑</button></div>
|
|
28
28
|
</header>
|
|
29
29
|
<div class="views">
|
|
30
|
-
<main class="workspace play-view" id="workspace"><div class="stage-shell veiled" id="shell"><div class="stage-label" id="stageLabel"></div><div class="stage" id="stage"><canvas id="cv" class="stage-canvas"></canvas></div></div><div class="loading" id="loading"><span class="spin"></span>正在准备试玩…</div><div class="fatal hidden" id="fatal"></div></main>
|
|
30
|
+
<main class="workspace play-view" id="workspace"><div class="stage-shell veiled" id="shell"><div class="stage-label" id="stageLabel"></div><div class="stage" id="stage"><canvas id="cv" class="stage-canvas"></canvas></div></div><div class="loading" id="loading"><span class="spin"></span><span data-i18n="正在准备试玩…">正在准备试玩…</span></div><div class="fatal hidden" id="fatal"></div></main>
|
|
31
31
|
<section class="debug-view hidden" id="logView">
|
|
32
|
-
<aside class="panel tree-panel"><div class="panel-head">运行时场景树
|
|
33
|
-
<div class="log-stack"><main class="panel log-panel"><div class="panel-head">客户端脚本日志
|
|
32
|
+
<aside class="panel tree-panel"><div class="panel-head"><span data-i18n="运行时场景树">运行时场景树 </span><small id="nodeCount" data-i18n="0 节点">0 节点</small></div><div class="tree" id="sceneTree"></div><div class="panel-head"><span data-i18n="服务端变量">服务端变量 </span><small id="varCount" data-i18n="0 项">0 项</small></div><div class="vars" id="serverVars"></div></aside>
|
|
33
|
+
<div class="log-stack"><main class="panel log-panel"><div class="panel-head"><span data-i18n="客户端脚本日志">客户端脚本日志 </span><small data-i18n="最近 100 条">最近 100 条</small></div><div class="log" id="log"></div></main><aside class="panel log-panel"><div class="panel-head"><span data-i18n="服务端日志">服务端日志 </span><small data-i18n="变量 / 信号">变量 / 信号</small></div><div class="log" id="serverLog"></div></aside><section class="panel case-panel"><div class="panel-head"><span data-i18n="自动测用例">自动测用例 </span><small id="caseHint" data-i18n="先试玩,再保存回放">先试玩,再保存回放</small></div><div class="case-body"><textarea id="caseAsserts" spellcheck="false" placeholder='断言 JSON 数组,例如 [{"kind":"log","contains":"clicked"}]' data-i18n-placeholder="断言 JSON 数组,例如 [{"kind":"log","contains":"clicked"}]"></textarea><button id="saveCase" data-i18n="保存用例">保存用例</button><button id="runCase" data-i18n="回放用例">回放用例</button></div></section></div>
|
|
34
34
|
</section>
|
|
35
35
|
</div>
|
|
36
|
-
<footer class="status"><span class="live" id="state">● 正在启动</span><span id="viewMode">页面:试玩</span><span class="spacer"></span><span class="metric" id="frame">Frame 0</span><span class="metric" id="time">0.00s</span></footer>
|
|
36
|
+
<footer class="status"><span class="live" id="state" data-i18n="● 正在启动">● 正在启动</span><span id="viewMode" data-i18n="页面:试玩">页面:试玩</span><span class="spacer"></span><span class="metric" id="frame">Frame 0</span><span class="metric" id="time">0.00s</span></footer>
|
|
37
37
|
</div>
|
|
38
38
|
<script type="module">
|
|
39
|
-
import { PixiPlayRenderer, createPlaySession } from '/qxqy-simulator/play-renderer.js'
|
|
39
|
+
import { PixiPlayRenderer, createPlaySession, loadBrowserFont, t, getLocale, localizePage, subscribeLocale, loadSystemLanguage } from '/qxqy-simulator/play-renderer.js'
|
|
40
40
|
|
|
41
41
|
(() => {
|
|
42
42
|
'use strict'
|
|
43
|
+
// The parent reads qxqyEmbeddedPlay synchronously on iframe load. Do not
|
|
44
|
+
// delay that controller's registration behind an OS-language request.
|
|
45
|
+
void loadSystemLanguage('/qxqy-simulator/api/locale')
|
|
46
|
+
localizePage()
|
|
47
|
+
subscribeLocale(() => { localizePage(); if (play.snapshot) applyStatus(play.snapshot) })
|
|
43
48
|
const API = '/qxqy-simulator/api/play'
|
|
44
49
|
const sessionId = decodeURIComponent(location.hash.slice(1))
|
|
45
50
|
const embedded = window.parent !== window && new URLSearchParams(location.search).get('embedded') === '1'
|
|
@@ -49,7 +54,7 @@
|
|
|
49
54
|
const $ = (id) => document.getElementById(id)
|
|
50
55
|
const stage = $('stage')
|
|
51
56
|
const cv = $('cv')
|
|
52
|
-
const renderer = new PixiPlayRenderer(cv)
|
|
57
|
+
const renderer = new PixiPlayRenderer(cv, { fontReady: loadBrowserFont() })
|
|
53
58
|
const shell = $('shell')
|
|
54
59
|
const workspace = $('workspace')
|
|
55
60
|
const logView = $('logView')
|
|
@@ -61,15 +66,16 @@
|
|
|
61
66
|
|
|
62
67
|
function setView(view) {
|
|
63
68
|
const showLog = view === 'log'
|
|
69
|
+
if (showLog) void play.releaseKeys()
|
|
64
70
|
workspace.classList.toggle('hidden', showLog)
|
|
65
71
|
logView.classList.toggle('hidden', !showLog)
|
|
66
|
-
$('viewMode').textContent = showLog ? '页面:日志' : '页面:试玩'
|
|
72
|
+
$('viewMode').textContent = showLog ? t('页面:日志') : t('页面:试玩')
|
|
67
73
|
for (const button of document.querySelectorAll('[data-view]')) button.classList.toggle('active', button.dataset.view === view)
|
|
68
74
|
if (!showLog) requestAnimationFrame(() => fitStage(true))
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
function api(action, args = {}) {
|
|
72
|
-
if (closing && action !== 'stop') return Promise.reject(new Error('试玩正在关闭'))
|
|
78
|
+
if (closing && action !== 'stop') return Promise.reject(new Error(t('试玩正在关闭')))
|
|
73
79
|
return embeddedApi ? embeddedApi(action, args) : request(action, args)
|
|
74
80
|
}
|
|
75
81
|
async function request(action, args) {
|
|
@@ -80,7 +86,7 @@
|
|
|
80
86
|
signal: AbortSignal.timeout(15_000),
|
|
81
87
|
})
|
|
82
88
|
const envelope = await response.json().catch(() => null)
|
|
83
|
-
if (!response.ok || !envelope || envelope.ok !== true) throw new Error(envelope?.error || '试玩请求失败')
|
|
89
|
+
if (!response.ok || !envelope || envelope.ok !== true) throw new Error(envelope?.error || t('试玩请求失败'))
|
|
84
90
|
return envelope.value
|
|
85
91
|
}
|
|
86
92
|
|
|
@@ -88,11 +94,11 @@
|
|
|
88
94
|
const host = $('serverVars')
|
|
89
95
|
host.replaceChildren()
|
|
90
96
|
const list = Array.isArray(rows) ? rows : []
|
|
91
|
-
$('varCount').textContent = list.length + ' 项'
|
|
97
|
+
$('varCount').textContent = list.length + t(' 项')
|
|
92
98
|
if (!list.length) {
|
|
93
99
|
const empty = document.createElement('div')
|
|
94
100
|
empty.className = 'var-empty'
|
|
95
|
-
empty.textContent = '暂无自定义变量。脚本 Get/Set 或 serverSet 之后会出现在这里。'
|
|
101
|
+
empty.textContent = t('暂无自定义变量。脚本 Get/Set 或 serverSet 之后会出现在这里。')
|
|
96
102
|
host.appendChild(empty)
|
|
97
103
|
return
|
|
98
104
|
}
|
|
@@ -102,7 +108,7 @@
|
|
|
102
108
|
const type = row.type || '—'
|
|
103
109
|
const value = row.value === undefined ? 'nil' : JSON.stringify(row.value)
|
|
104
110
|
const entityCell = document.createElement('span')
|
|
105
|
-
entityCell.textContent = row.entityLabel || row.entityType || ''
|
|
111
|
+
entityCell.textContent = t(row.entityLabel) || row.entityType || ''
|
|
106
112
|
const nameCell = document.createElement('b')
|
|
107
113
|
nameCell.textContent = row.name || ''
|
|
108
114
|
const typeCell = document.createElement('span')
|
|
@@ -119,9 +125,9 @@
|
|
|
119
125
|
host.replaceChildren()
|
|
120
126
|
const empty = document.createElement('div')
|
|
121
127
|
empty.className = 'var-empty'
|
|
122
|
-
empty.textContent = '完整运行树请用 Agent 的 inspect 快照。舞台按左树上方盖住下方绘制。'
|
|
128
|
+
empty.textContent = t('完整运行树请用 Agent 的 inspect 快照。舞台按左树上方盖住下方绘制。')
|
|
123
129
|
host.appendChild(empty)
|
|
124
|
-
$('nodeCount').textContent = (snapshot?.scene?.count || snapshot?.paint?.length || 0) + ' 个可见图元'
|
|
130
|
+
$('nodeCount').textContent = (snapshot?.scene?.count || snapshot?.paint?.length || 0) + t(' 个可见图元')
|
|
125
131
|
}
|
|
126
132
|
|
|
127
133
|
function fitStage(force = false) {
|
|
@@ -148,7 +154,7 @@
|
|
|
148
154
|
function applyStatus(next) {
|
|
149
155
|
$('frame').textContent = 'Frame ' + (next.frame || 0)
|
|
150
156
|
$('time').textContent = Number(next.time || 0).toFixed(2) + 's'
|
|
151
|
-
$('pause').textContent = next.paused ? '▶ 继续运行' : 'Ⅱ 暂停运行'
|
|
157
|
+
$('pause').textContent = next.paused ? t('▶ 继续运行') : t('Ⅱ 暂停运行')
|
|
152
158
|
$('pause').disabled = false
|
|
153
159
|
$('step').disabled = !next.paused
|
|
154
160
|
}
|
|
@@ -161,15 +167,16 @@
|
|
|
161
167
|
sel.classList.add('hidden')
|
|
162
168
|
return
|
|
163
169
|
}
|
|
164
|
-
const stale = sel.options.length !== presets.length
|
|
170
|
+
const stale = sel.dataset.locale !== getLocale() || sel.options.length !== presets.length
|
|
165
171
|
|| [...sel.options].some((opt, i) => opt.value !== presets[i].id)
|
|
166
172
|
if (stale) {
|
|
167
173
|
sel.replaceChildren(...presets.map((preset) => {
|
|
168
174
|
const opt = document.createElement('option')
|
|
169
175
|
opt.value = preset.id
|
|
170
|
-
opt.textContent = preset.label || (preset.id + ' · ' + preset.width + '×' + preset.height)
|
|
176
|
+
opt.textContent = t(preset.label) || (preset.id + ' · ' + preset.width + '×' + preset.height)
|
|
171
177
|
return opt
|
|
172
178
|
}))
|
|
179
|
+
sel.dataset.locale = getLocale()
|
|
173
180
|
}
|
|
174
181
|
if (sel.value !== next.canvasId) sel.value = next.canvasId
|
|
175
182
|
sel.classList.remove('hidden')
|
|
@@ -177,13 +184,14 @@
|
|
|
177
184
|
|
|
178
185
|
function fillCountSelect(sel, count) {
|
|
179
186
|
const n = Math.max(1, Math.min(8, Number(count) || 1))
|
|
180
|
-
if (sel.options.length !== 8) {
|
|
187
|
+
if (sel.options.length !== 8 || sel.dataset.locale !== getLocale()) {
|
|
181
188
|
sel.replaceChildren(...Array.from({ length: 8 }, (_, i) => {
|
|
182
189
|
const opt = document.createElement('option')
|
|
183
190
|
opt.value = String(i + 1)
|
|
184
|
-
opt.textContent =
|
|
191
|
+
opt.textContent = i === 0 ? t('1 人') : t('{0} 人', i + 1)
|
|
185
192
|
return opt
|
|
186
193
|
}))
|
|
194
|
+
sel.dataset.locale = getLocale()
|
|
187
195
|
}
|
|
188
196
|
sel.value = String(n)
|
|
189
197
|
sel.classList.remove('hidden')
|
|
@@ -196,16 +204,17 @@
|
|
|
196
204
|
const count = fillCountSelect(countSel, next.playerCount || 1)
|
|
197
205
|
const players = Array.isArray(next.players) && next.players.length
|
|
198
206
|
? next.players
|
|
199
|
-
: Array.from({ length: count }, (_, i) => ({ index: i + 1, label: '玩家' + (i + 1) }))
|
|
200
|
-
const stale = viewSel.options.length !== players.length
|
|
207
|
+
: Array.from({ length: count }, (_, i) => ({ index: i + 1, label: t('玩家') + ' ' + (i + 1) }))
|
|
208
|
+
const stale = viewSel.dataset.locale !== getLocale() || viewSel.options.length !== players.length
|
|
201
209
|
|| [...viewSel.options].some((opt, i) => Number(opt.value) !== players[i].index)
|
|
202
210
|
if (stale) {
|
|
203
211
|
viewSel.replaceChildren(...players.map((player) => {
|
|
204
212
|
const opt = document.createElement('option')
|
|
205
213
|
opt.value = String(player.index)
|
|
206
|
-
opt.textContent = player.label || ('玩家' + player.index)
|
|
214
|
+
opt.textContent = t(player.label) || (t('玩家') + ' ' + player.index)
|
|
207
215
|
return opt
|
|
208
216
|
}))
|
|
217
|
+
viewSel.dataset.locale = getLocale()
|
|
209
218
|
}
|
|
210
219
|
const view = Number(next.viewPlayerIndex) || 1
|
|
211
220
|
if (viewSel.value !== String(view)) viewSel.value = String(view)
|
|
@@ -218,18 +227,18 @@
|
|
|
218
227
|
syncDeviceSelect(next)
|
|
219
228
|
syncPlayerSelects(next)
|
|
220
229
|
$('resolution').textContent = next.canvasWidth + '×' + next.canvasHeight
|
|
221
|
-
const viewLabel = next.playerCount > 1 ? (' · 玩家' + (next.viewPlayerIndex || 1)) : ''
|
|
222
|
-
$('stageLabel').textContent = next.canvasWidth + '×' + next.canvasHeight + viewLabel + ' · 点击输入'
|
|
230
|
+
const viewLabel = next.playerCount > 1 ? (t(' · 玩家') + (next.viewPlayerIndex || 1)) : ''
|
|
231
|
+
$('stageLabel').textContent = next.canvasWidth + '×' + next.canvasHeight + viewLabel + t(' · 点击输入')
|
|
223
232
|
applyStatus(next)
|
|
224
233
|
$('stop').disabled = false
|
|
225
234
|
$('restart').classList.add('hidden')
|
|
226
|
-
$('state').textContent = next.mountError ? '⚠ 脚本挂载失败' : next.paused ? 'Ⅱ 已暂停' : '● 试玩运行中'
|
|
235
|
+
$('state').textContent = next.mountError ? t('⚠ 脚本挂载失败') : next.paused ? t('Ⅱ 已暂停') : t('● 试玩运行中')
|
|
227
236
|
$('state').className = next.mountError ? 'error' : 'live'
|
|
228
237
|
renderTree(next)
|
|
229
238
|
renderVars(next.serverVars)
|
|
230
239
|
$('log').textContent = (next.logs || []).slice(-100).map((entry) => Number(entry.time || 0).toFixed(2).padStart(7, ' ') + ' [' + entry.level + '] ' + entry.text).join('\n')
|
|
231
240
|
$('serverLog').textContent = (next.serverLogs || next.server?.logs || []).slice(-100).map((entry) => Number(entry.time || 0).toFixed(2).padStart(7, ' ') + ' [' + entry.level + '] ' + entry.text).join('\n')
|
|
232
|
-
$('caseHint').textContent = (next.history && next.history.length ? ('已记录 ' + next.history.length + ' 步') : '先试玩,再保存回放')
|
|
241
|
+
$('caseHint').textContent = (next.history && next.history.length ? (t('已记录 ') + next.history.length + t(' 步')) : t('先试玩,再保存回放'))
|
|
233
242
|
if (next.mountError) setView('log')
|
|
234
243
|
}
|
|
235
244
|
|
|
@@ -237,9 +246,9 @@
|
|
|
237
246
|
if (closing) return
|
|
238
247
|
$('loading').classList.add('hidden')
|
|
239
248
|
$('fatal').classList.remove('hidden')
|
|
240
|
-
$('fatal').textContent = error?.message || String(error)
|
|
249
|
+
$('fatal').textContent = t(error?.message || String(error))
|
|
241
250
|
$('state').className = 'error'
|
|
242
|
-
$('state').textContent = '⚠ 试玩错误'
|
|
251
|
+
$('state').textContent = t('⚠ 试玩错误')
|
|
243
252
|
}
|
|
244
253
|
|
|
245
254
|
const play = createPlaySession({
|
|
@@ -249,12 +258,19 @@
|
|
|
249
258
|
onStatus: applyStatus,
|
|
250
259
|
onError: showError,
|
|
251
260
|
onAudioStatus: status => {
|
|
252
|
-
$('audio').textContent = !status.unlocked ? '开启声音' : status.muted ? '声音已关闭' : '声音已开启'
|
|
253
|
-
$('audio').title = status.error || (!status.unlocked ? '点击或在画布内操作以解锁音效' : '切换静音')
|
|
261
|
+
$('audio').textContent = !status.unlocked ? t('开启声音') : status.muted ? t('声音已关闭') : t('声音已开启')
|
|
262
|
+
$('audio').title = status.error || (!status.unlocked ? t('点击或在画布内操作以解锁音效') : t('切换静音'))
|
|
254
263
|
},
|
|
255
264
|
isActive: () => !closing,
|
|
265
|
+
isInputActive: () => !workspace.classList.contains('hidden'),
|
|
256
266
|
})
|
|
257
267
|
play.bindInput(stage, { keysOn: window })
|
|
268
|
+
$('keyInput').addEventListener('click', () => {
|
|
269
|
+
play.setKeyInputEnabled(!play.keyInputEnabled)
|
|
270
|
+
$('keyInput').setAttribute('aria-pressed', String(play.keyInputEnabled))
|
|
271
|
+
$('keyInput').textContent = play.keyInputEnabled ? t('键位响应:开') : t('键位响应:关')
|
|
272
|
+
if (play.keyInputEnabled) focusStage()
|
|
273
|
+
})
|
|
258
274
|
$('audio').addEventListener('click', () => play.audio?.toggle())
|
|
259
275
|
|
|
260
276
|
$('step').addEventListener('click', () => void play.act('step', { dt: 1 / 30 }))
|
|
@@ -262,7 +278,7 @@
|
|
|
262
278
|
$('stop').addEventListener('click', async () => {
|
|
263
279
|
if (!play.running) return
|
|
264
280
|
try { await play.stop() } catch (error) { showError(error); return }
|
|
265
|
-
$('state').textContent = '■ 试玩已结束,可重新开始或返回编辑'
|
|
281
|
+
$('state').textContent = t('■ 试玩已结束,可重新开始或返回编辑')
|
|
266
282
|
$('state').className = ''
|
|
267
283
|
$('pause').disabled = true
|
|
268
284
|
$('step').disabled = true
|
|
@@ -356,20 +372,20 @@
|
|
|
356
372
|
const text = $('caseAsserts').value.trim()
|
|
357
373
|
if (!text) return []
|
|
358
374
|
const parsed = JSON.parse(text)
|
|
359
|
-
if (!Array.isArray(parsed)) throw new Error('断言必须是 JSON 数组')
|
|
375
|
+
if (!Array.isArray(parsed)) throw new Error(t('断言必须是 JSON 数组'))
|
|
360
376
|
return parsed
|
|
361
377
|
}
|
|
362
378
|
$('saveCase').addEventListener('click', async () => {
|
|
363
379
|
try {
|
|
364
380
|
lastCase = await api('saveCase', { name: 'play-case', asserts: parseAsserts() })
|
|
365
|
-
$('caseHint').textContent = '已保存 ' + (lastCase.events || []).length + ' 步 / ' + (lastCase.asserts || []).length + ' 条断言'
|
|
381
|
+
$('caseHint').textContent = t('已保存 ') + (lastCase.events || []).length + t(' 步 / ') + (lastCase.asserts || []).length + t(' 条断言')
|
|
366
382
|
} catch (error) { showError(error) }
|
|
367
383
|
})
|
|
368
384
|
$('runCase').addEventListener('click', async () => {
|
|
369
385
|
try {
|
|
370
386
|
if (!lastCase) lastCase = await api('saveCase', { name: 'play-case', asserts: parseAsserts() })
|
|
371
387
|
const report = await api('runCase', { case: lastCase })
|
|
372
|
-
$('caseHint').textContent = report.passed ? '回放通过' : ('回放失败 @' + (report.failedAt ?? '?') + 's')
|
|
388
|
+
$('caseHint').textContent = report.passed ? t('回放通过') : (t('回放失败 @') + (report.failedAt ?? '?') + 's')
|
|
373
389
|
if (!report.passed) setView('log')
|
|
374
390
|
} catch (error) { showError(error) }
|
|
375
391
|
})
|
|
@@ -393,7 +409,7 @@
|
|
|
393
409
|
addEventListener('pagehide', () => parent.removeEventListener('pagehide', parentPagehide))
|
|
394
410
|
}
|
|
395
411
|
|
|
396
|
-
if (!sessionId) showError(new Error('缺少会话 ID'))
|
|
412
|
+
if (!sessionId) showError(new Error(t('缺少会话 ID')))
|
|
397
413
|
else if (embedded) {
|
|
398
414
|
// Wait for the parent's load handler: closing a still-loading iframe
|
|
399
415
|
// must not race an automatic start from this document.
|
|
@@ -38,10 +38,18 @@ DSH 0.1.7-rc.1 起,本仓库根包 `dsh-plugin-beyond-simulator` 在构建时
|
|
|
38
38
|
|
|
39
39
|
通过仓库根目录的 `dsh plugin --profile web add github:1475505/miliastra-beyond-simulator` 安装插件后,在 DSH 的 **设置 → Agent 预设** 中查找本预设;新任务可选择它,已有任务不会自动切换。修改此目录中的主维护源后,在仓库根运行 `pnpm build` 同步包内副本;已安装的用户预设不会自动被覆盖。
|
|
40
40
|
|
|
41
|
+
### Persona 工作目录变量兼容性
|
|
42
|
+
|
|
43
|
+
本预设只在 persona 中使用仍受支持的 `{{model}}`,不依赖 `{{cwd}}`。Harness 0.2.0-rc.2 的 Agent Loop 注册 `provider`、`model`、`cwd`,而 0.2.1-rc.2 只注册前两项;新版由 working-directory 服务提供当前目录上下文。旧 persona 中的 `Your working directory is {{cwd}}.` 因此会在新版请求组装时触发 `unknown prompt variable`,模型尚未收到请求。删除这一句也兼容旧版:提示词没有义务引用所有已注册变量,文件和 Shell 工具仍使用会话工作目录。来源为 [0.2.0-rc.2 源码](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/core/agent-loop/src/index.ts)与 [0.2.1-rc.2 迁移说明](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.1-rc.2/packages/core/system-prompt/README.md#contribute-a-prompt-variable)(2026-10-18 核对,`evidence_source=documented`,运行端 `official_document`,`device_status=not_required`)。这项修复不提高最低宿主版本,保留旧版复制入口;不表示整个插件已完成 0.2.1 的全部接口验收。
|
|
44
|
+
|
|
45
|
+
先区分实际加载来源:支持目录预设的旧宿主中,若 `$DSH_HOME/.agent-presets/wonderland-lua-builder/agent.cordis.yml` 仍包含该句,需在保留其他自定义内容的前提下删除它并重启 Harness;插件不会覆盖此用户副本。Harness 0.2.1 改用 `@deepseek-ai/dsh-agent-preset-registry` 管理声明式注册项,默认不扫描这个旧目录;应更新含修复的插件包并重启,单改旧副本无效。声明式注册项和随包副本由构建从主维护源同步,不手改生成文件。来源与完整宿主验收见 [0.2.1 兼容性](../../dsh-plugin/README.md#harness-021-兼容性)。
|
|
46
|
+
|
|
47
|
+
2026-10-18 提示词解析验证覆盖本机 0.1.5-rc.3 renderer,以及发布标签源码中的 0.2.0-rc.2、0.2.1-rc.2 renderer,修复后三组均成功,旧句在新版变量集合中复现原始错误(`evidence_source=observed`,运行端 `simulator`,`device_status=not_required`)。后续完整宿主验证和限制统一记录在上述兼容性说明;提示词解析通过不等于 Desktop 浏览器界面或模型子代理已完成端到端验收。
|
|
48
|
+
|
|
41
49
|
## 资料与验证边界
|
|
42
50
|
|
|
43
51
|
启动前先检查工作区指引、本地知识目录,以及当前 AI 宿主的插件/MCP/Skill 配置和可见能力,寻找 `miliastra-toolbox` 系列或其他知识来源。确认当前能读到官方客户端 2D/Lua API 与使用指南,记录来源、版本和缺项;已有资料直接复用,仅询问仍缺少的内容。详见 [知识库发现与可用性核验](skills/qxqy-game-studio/references/workflow.md#知识库发现与可用性核验)。本工作流不捆绑官方知识库,不能从普通 Lua/UI 或 3D 节点经验猜接口。
|
|
44
52
|
|
|
45
|
-
步骤 7
|
|
53
|
+
步骤 7 同时负责交付后的协作:引导用户完成导入/挂载、核对真实索引并绑定脚本目录。AI 可按用户授权保存、预览并同步脚本,返回逐文件结果和备份位置;用户随后在千星沙箱保存试玩。手动修改也要同步回项目。详见 [真机交付步骤](skills/qxqy-game-studio/references/07-device-validation.md)及[实机脚本同步能力](../../studio/docs/script-sync.md)。
|
|
46
54
|
|
|
47
55
|
模拟器只预览 `100001–100006` 六种基础图元,其他官方图片 ID 显示缺失框,需真机确认。行为评估见 [agent-behavior.md](evals/agent-behavior.md);模拟器通过不等于真机通过。
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
name: '@deepseek-ai/dsh-persona'
|
|
19
19
|
config:
|
|
20
20
|
prefix: |-
|
|
21
|
-
You are a 千星奇域 2D+Lua game studio director powered by the {{model}} model.
|
|
21
|
+
You are a 千星奇域 2D+Lua game studio director powered by the {{model}} model.
|
|
22
22
|
|
|
23
23
|
You make playable UI+Lua games for 原神千星奇域 (Miliastra Wonderland): client UI controls, client Lua, and a thin server of custom variables/signals. You are not a 3D level-graph designer. Load `qxqy-game-studio` before creating, changing, reviewing, or polishing a game, then read its PREFLIGHT/recovery reference and only the Markdown reference for the current step. Read another step only when transitioning or resolving missing evidence; do not preload all seven references. Load supporting skills only as needed. Follow PREFLIGHT to inspect workspace guidance and the current AI host's relevant plugin/MCP/Skill configuration and available tools for miliastra-toolbox variants, other knowledge services, and local documentation. Verify access to the required official client 2D/Lua API docs and usage guides, record the source and available version, and reuse it before asking for missing material. Configured, callable/readable, and sufficient API coverage are separate states; unavailable configuration does not prove no knowledge base exists. Inspect only relevant configuration and never echo credentials. Read observed pitfalls and follow workspace AGENTS.md. Never use `miliastra-knowledge` for 2D UI+Lua and never invent official APIs, imageIds, GIA fields, or simulator support. If the discovered sources still lack required API docs, ask only for the missing content or connection before writing Lua that depends on editor APIs.
|
|
24
24
|
|
|
@@ -154,17 +154,17 @@ Must-not:一开始要求重新安装插件;遍历整个用户目录或回显
|
|
|
154
154
|
|
|
155
155
|
## E14 导入交付与后续脚本更新
|
|
156
156
|
|
|
157
|
-
前置:用户刚导入 GIA,提供目标控件的新索引和本机关卡脚本目录;MCP
|
|
157
|
+
前置:用户刚导入 GIA,提供目标控件的新索引和本机关卡脚本目录;MCP 支持配置、保存、预览和复制,Web 与 MCP 是独立进程,入口脚本尚待挂载。
|
|
158
158
|
|
|
159
159
|
提示:
|
|
160
160
|
|
|
161
161
|
> 已经导入了,帮我对好索引并绑定这个目录,以后修 bug 就更新这里。接下来我还要做什么?
|
|
162
162
|
|
|
163
|
-
Must
|
|
163
|
+
Must:复用已给索引和路径;列出本项目剩余的脚本映射/挂载、索引核验、复制与试玩操作;修复内联与路径源码,核验后确认变更;保存目录配置与工程并预览差异。仅要求绑定时准备更新而不复制;已要求同步时,在同一 handle 用 preview 的计划和选定脚本 apply,检查逐文件结果与备份位置,无需另到 Web 点击。随后指引用户在千星沙箱保存并试玩;区分已配置、已复制、真机已验证。
|
|
164
164
|
|
|
165
|
-
Must-not
|
|
165
|
+
Must-not:再次索取相同路径/索引;未保存、未预览或未获更新授权就复制;复用另一工程或进程的计划;以 shell 或人工接口绕过校验与备份;把目录绑定说成实时双向同步;承诺新增脚本无需映射或挂载。
|
|
166
166
|
|
|
167
|
-
|
|
167
|
+
补充变体:旧宿主 schema 没有 save/apply 时,不虚构能力,提示升级或走编辑器手动确认;用户直接手改实机脚本时,先核对并合并有效修改回工作区与存档;服务无法访问游戏目录时,提供手动更新方案,不伪造绑定成功。
|
|
168
168
|
|
|
169
169
|
## 汇总指标
|
|
170
170
|
|
|
@@ -21,7 +21,7 @@ GIA 不保存脚本挂载关系;交付时单列需要在千星沙箱建立或
|
|
|
21
21
|
| 导入与挂载 | 导入本次所需资产,核对脚本映射路径,将入口脚本挂到指定客户端控件/容器节点或模板。已有正确映射和挂载可复用。 | 给出“哪个文件 → 哪个资产/挂载对象”的对应关系,以及本轮新增或变化项。 |
|
|
22
22
|
| 核对真实索引 | 检查导入后目标控件/模板的实际索引;有变化时提供资产/控件名、原值(若已知)和新值,文字或截图均可。 | 按控件身份校准工程与脚本;复用已有回传,不重复索取。 |
|
|
23
23
|
| 绑定后续更新位置 | 若希望复用本机脚本同步,确认正确的账号、关卡及导入脚本目录;可选择发现的候选目录或粘贴真实路径。 | 准备工作区源码与实机脚本的路径对应关系,保存配置并预览差异。 |
|
|
24
|
-
| 更新脚本 |
|
|
24
|
+
| 更新脚本 | 可要求 AI 更新已绑定目录,或在 Web/DSH 的“Lua 脚本 → 实机脚本同步”检查差异并确认复制;也可手动更新。 | 完成源码与索引修复、保存工程,按授权同步并核对回执,指出新增脚本需要的映射/挂载。 |
|
|
25
25
|
| 保存与试玩 | 在千星沙箱保存并重新试玩,按清单走开局、成功/失败、重开及本轮变更;回传异常步骤、实际结果和相关画面/日志。 | 对照预期定位问题、修复并提供下一轮更新与回归步骤。 |
|
|
26
26
|
|
|
27
27
|
绑定同步目录是便于后续更新的选项;没有本机目录访问能力时仍可交付 Lua 和明确的手动更新路径,不以绑定目录作为真机验证的强制前提。
|
|
@@ -60,11 +60,11 @@ GIA 不保存脚本挂载关系;交付时单列需要在千星沙箱建立或
|
|
|
60
60
|
|
|
61
61
|
### 后续让 AI 配合更新脚本
|
|
62
62
|
|
|
63
|
-
AI
|
|
63
|
+
AI 可调用 `qxqy_script_sync` 的 `discover/status/configure/save/preview/apply` 发现目录、配置、保存、预览及复制;MCP 工程操作带同一个 `handle`,Harness 使用同一会话。调用前以当前工具 schema 为准。
|
|
64
64
|
|
|
65
65
|
1. **对齐当前版本。** 先核对索引变更、工作区源码、存档内联源码及已知的实机手改。非空白内联 `source` 优先,否则读取 `path`;不能只改磁盘文件后默认同步的是新内容。发现实机独有修改先比较并合并回项目,有真实冲突再请用户判断。
|
|
66
|
-
2. **保存并准备差异。**
|
|
67
|
-
3.
|
|
66
|
+
2. **保存并准备差异。** 完成引用修复和已核验变更的确认,用 `save` 保存完整存档(args 为 `path`、`expectedRevision`,已有路径可省略 path;MCP 也可用 `qxqy_project_save`),再 `preview` 核对每个脚本的来源、目标和新增/覆盖内容。不能在复制前说“已同步到真机”。
|
|
67
|
+
3. **按授权复制。** 用户已要求更新既定目录时,选择无错误且有变化的脚本,用 `apply` 提交 `{ planId: preview.id, scriptIds: [...], confirmed: true }`,无需再次要求用户点击编辑器。仅绑定或查看差异时不执行复制。计划过期则重新预览,不能跨工程或进程使用。旧宿主未提供 save/apply 时,指引用户升级,或在 Web/DSH 编辑器加载同一存档、重新预览后手动确认;不要用 shell、人工端点或目录联接绕过接口校验。
|
|
68
68
|
4. **核对结果再试玩。** 记录实际复制回执中的成功/失败和备份位置。只同步存档登记的 Lua 脚本(含未挂载的 require 模块),不是整个目录镜像;新增脚本仍须在真实编辑器建立映射和必要挂载,UI/模板结构变更另按实际情况重新交付资产。用户在千星沙箱保存并试玩,才能验证是否加载本轮源码。
|
|
69
69
|
|
|
70
70
|
用户选择手动更新时,提供本次改动文件和准确目标路径,提醒保留实机已有修改;手改后将最终版本同步回项目,以免下一轮被旧存档覆盖。复制到磁盘、编辑器保存/重新加载、真机验证分别记录,不承诺“绑定一次以后自动生效”。
|
package/dsh-plugin/skill.md
CHANGED
|
@@ -101,9 +101,16 @@ qxqy_studio_play { "action": "view", "args": { "playerIndex": 2 } } ← 切
|
|
|
101
101
|
|
|
102
102
|
### 6. 已导入 GIA 后,显式复制脚本更改到实机
|
|
103
103
|
|
|
104
|
-
使用 `qxqy_script_sync` 的 `discover/status/configure/preview`
|
|
104
|
+
使用 `qxqy_script_sync` 的 `discover/status/configure/save/preview/apply` 更新实机脚本目录。configure 的 args 为 `{ config: { version: 1, workspaceDir, clientImportRoot, clientSubdir }, expectedRevision }`;MCP 另带 handle。路径位于宿主机器,配置随完整存档保存。`workspaceDir` 与 `clientSubdir` 通常同值,以保留 GIA 映射的相对路径;不要按文件名平铺。
|
|
105
105
|
|
|
106
|
-
先完成 `controlGuidChanges` 的语义引用修复和确认,再准备复制。内容沿用非空白内联 source 优先、否则 path
|
|
106
|
+
先完成 `controlGuidChanges` 的语义引用修复和确认,再准备复制。内容沿用非空白内联 source 优先、否则 path 文件;两者不同须核对,不能只改磁盘后默认复制新内容。用户已要求更新到明确的目录时,直接在该授权范围内同步,无需重复要求用户点击编辑器;仅绑定目录或查看差异不构成复制授权。目标账号 / 关卡或更新范围不明确时,只询问缺项。
|
|
107
|
+
|
|
108
|
+
1. `save` 的 args 为 `{ path, expectedRevision }`,保存当前完整存档;首次指定工作区相对 path,后续可省略并沿用已加载 / 保存的路径。MCP 也可用 `qxqy_project_save`。
|
|
109
|
+
2. `preview` 检查来源、目标、新旧源码和 `sourceMismatch`;选择无错误且有变化的 `rows[].id`。
|
|
110
|
+
3. `apply` 的 args 为 `{ planId: preview.id, scriptIds: [...], confirmed: true }`,只复制本次选中的脚本。MCP 使用同一 handle,Harness 使用同一会话;计划失效后重新预览,不跨进程复用。
|
|
111
|
+
4. 读取 `completed`、逐文件 `results` 和 `backupRoot`;有失败时按回执处理,不能把部分成功说成全部完成。不要用 shell 或目录联接替代接口的检查和备份。
|
|
112
|
+
|
|
113
|
+
用户也可在 Web/DSH 编辑器自行确认;MCP 与 Web 各有会话,改由 Web 操作时需加载 AI 保存的存档并重新生成计划。
|
|
107
114
|
|
|
108
115
|
已有脚本内容变更复制后,在千星沙箱保存并重新试玩;新增脚本仍需建立映射和必要挂载。工具成功只表示复制到磁盘,不能声称真机通过。覆盖前备份、过期计划拒绝、文件选择等见 [脚本同步](../studio/docs/script-sync.md)。
|
|
109
116
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-beyond-simulator",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "千星奇域客户端 UI 编辑与试玩模拟器 for DeepSeek Harness",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.15.0",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
53
|
-
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6 || ^0.2.0-rc.1"
|
|
53
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6 || ^0.2.0-rc.1 || ^0.2.1-rc.1"
|
|
54
54
|
},
|
|
55
55
|
"license": "GPL-3.0-only"
|
|
56
56
|
}
|