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.
@@ -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">设备 <b id="resolution">准备中…</b></span></div>
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">运行时场景树 <small id="nodeCount">0 节点</small></div><div class="tree" id="sceneTree"></div><div class="panel-head">服务端变量 <small id="varCount">0 项</small></div><div class="vars" id="serverVars"></div></aside>
33
- <div class="log-stack"><main class="panel log-panel"><div class="panel-head">客户端脚本日志 <small>最近 100 条</small></div><div class="log" id="log"></div></main><aside class="panel log-panel"><div class="panel-head">服务端日志 <small>变量 / 信号</small></div><div class="log" id="serverLog"></div></aside><section class="panel case-panel"><div class="panel-head">自动测用例 <small id="caseHint">先试玩,再保存回放</small></div><div class="case-body"><textarea id="caseAsserts" spellcheck="false" placeholder='断言 JSON 数组,例如 [{"kind":"log","contains":"clicked"}]'></textarea><button id="saveCase">保存用例</button><button id="runCase">回放用例</button></div></section></div>
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 数组,例如 [{&quot;kind&quot;:&quot;log&quot;,&quot;contains&quot;:&quot;clicked&quot;}]"></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 = (i + 1) + ' 人'
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 同时负责交付后的协作:引导用户完成导入/挂载、核对真实索引、选择脚本更新方式;需要时绑定导入后的脚本目录。AI 准备修改、保存和差异预览,用户在编辑器确认复制并在千星沙箱保存试玩;手动修改也要同步回项目。详见 [真机交付步骤](skills/qxqy-game-studio/references/07-device-validation.md)及[实机脚本同步能力](../../studio/docs/script-sync.md)。
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. Your working directory is {{cwd}}.
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 可配置/预览同步,Web 与 MCP 是独立进程,入口脚本尚待挂载。
157
+ 前置:用户刚导入 GIA,提供目标控件的新索引和本机关卡脚本目录;MCP 支持配置、保存、预览和复制,Web 与 MCP 是独立进程,入口脚本尚待挂载。
158
158
 
159
159
  提示:
160
160
 
161
161
  > 已经导入了,帮我对好索引并绑定这个目录,以后修 bug 就更新这里。接下来我还要做什么?
162
162
 
163
- Must:复用已给索引和路径;列出本项目剩余的脚本映射/挂载、索引核验、复制与试玩操作;修复内联与路径源码,核验后确认变更;保存目录配置与工程,准备差异。指引用户在 Web 编辑器加载同一存档、重新检查差异并确认复制,随后在千星沙箱保存并试玩;区分已配置、已复制、真机已验证。
163
+ Must:复用已给索引和路径;列出本项目剩余的脚本映射/挂载、索引核验、复制与试玩操作;修复内联与路径源码,核验后确认变更;保存目录配置与工程并预览差异。仅要求绑定时准备更新而不复制;已要求同步时,在同一 handle 用 preview 的计划和选定脚本 apply,检查逐文件结果与备份位置,无需另到 Web 点击。随后指引用户在千星沙箱保存并试玩;区分已配置、已复制、真机已验证。
164
164
 
165
- Must-not:再次索取相同路径/索引;虚构 AI 工具可直接 apply;以 shell 或人工接口绕过本次复制确认;把目录绑定说成实时双向同步;承诺新增脚本无需映射或挂载。
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
- | 更新脚本 | 在 Web/DSH 的“Lua 脚本 → 实机脚本同步”检查源文件、目标路径和差异,选择本次文件并确认复制;也可按交付说明手动更新。 | 完成源码与索引修复、保存工程,指出新增脚本还需要建立的映射/挂载。 |
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 可调用当前可用的 `qxqy_script_sync` 的 `discover/status/configure/preview` 发现目录、读取/保存配置和预览差异;MCP 工程操作带 `handle`,配置操作用最新 revision。调用前以当前工具 schema 为准。
63
+ AI 可调用 `qxqy_script_sync` 的 `discover/status/configure/save/preview/apply` 发现目录、配置、保存、预览及复制;MCP 工程操作带同一个 `handle`,Harness 使用同一会话。调用前以当前工具 schema 为准。
64
64
 
65
65
  1. **对齐当前版本。** 先核对索引变更、工作区源码、存档内联源码及已知的实机手改。非空白内联 `source` 优先,否则读取 `path`;不能只改磁盘文件后默认同步的是新内容。发现实机独有修改先比较并合并回项目,有真实冲突再请用户判断。
66
- 2. **保存并准备差异。** 完成引用修复和已核验变更的确认,保存完整存档,再预览每个脚本的来源、目标和新增/覆盖内容。AI 报告“已准备更新”,不能在复制前说“已同步到真机”。
67
- 3. **用户确认复制。** 当前工具只向 AI 开放配置与预览。请用户在 Web/DSH 编辑器“Lua 脚本 → 实机脚本同步”执行“保存存档并查看更新”,选择文件并点击“确认复制”。MCP 与 Web 各有会话:先在 Web `/editor` 加载 AI 保存的同一存档,再重新预览;只读预览页不能执行复制,MCP 的预览计划不能跨进程使用。AI 不调用人工复制端点或用 shell、目录联接绕过本次确认。
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
  用户选择手动更新时,提供本次改动文件和准确目标路径,提醒保留实机已有修改;手改后将最终版本同步回项目,以免下一轮被旧存档覆盖。复制到磁盘、编辑器保存/重新加载、真机验证分别记录,不承诺“绑定一次以后自动生效”。
@@ -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` 配置或检查实机脚本目录。configure 的 args 为 `{ config: { version: 1, workspaceDir, clientImportRoot, clientSubdir }, expectedRevision }`;MCP 另带 handle。路径位于宿主机器,配置随完整存档保存。`workspaceDir` 与 `clientSubdir` 通常同值,以保留 GIA 映射的相对路径;不要按文件名平铺。
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 文件;两者不同须核对,不能只改磁盘后默认复制新内容。保存完整存档后,请用户在 Web/DSH「Lua 脚本 → 实机脚本同步」检查差异并点击「确认复制」。MCP 与 Web 各有会话,需先在 Web 编辑器加载 AI 保存的存档。AI 不调用人工复制端点或用 shell 绕过本次确认;不得建立目录联接替代复制。
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.1.1",
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
  }