@playcraft/cli 0.1.65 → 0.1.67

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.
Files changed (102) hide show
  1. package/dist/assets/client.js +4 -2
  2. package/dist/assets/messages.js +2 -0
  3. package/dist/audio-source/messages.js +35 -0
  4. package/dist/commands/assets.js +1 -1
  5. package/dist/commands/audio-source.js +109 -0
  6. package/dist/commands/audio.js +11 -4
  7. package/dist/commands/build.js +4 -2
  8. package/dist/commands/tools-context.js +46 -0
  9. package/dist/commands/tools.js +2 -0
  10. package/dist/model-adaptation/project.js +1 -1
  11. package/dist/project-skills/builtin-import.js +69 -22
  12. package/dist/project-skills/commands.js +11 -2
  13. package/dist/project-skills/lifecycle.js +1 -1
  14. package/dist/project-skills/local-store.js +90 -31
  15. package/dist/project-skills/messages.js +16 -0
  16. package/dist/project-skills/migrate-lock.js +166 -0
  17. package/dist/project-skills/remote-cache.js +36 -5
  18. package/dist/utils/agent-api-client.js +18 -1
  19. package/dist/utils/agent-generation-provenance.js +8 -2
  20. package/dist/utils/audio-export.js +249 -0
  21. package/dist/utils/audio-processor.js +22 -99
  22. package/dist/utils/execution-context-bridge.js +28 -0
  23. package/dist/workspace-runtime/batch/control.js +13 -2
  24. package/dist/workspace-runtime/batch/generate.js +43 -2
  25. package/dist/workspace-runtime/batch/prepare.js +4 -0
  26. package/dist/workspace-runtime/batch/skill-artifacts.js +103 -0
  27. package/dist/workspace-runtime/batch/source-capture.js +6 -8
  28. package/dist/workspace-runtime/codex/app-server.js +26 -2
  29. package/dist/workspace-runtime/codex/execution-context-broker.js +61 -0
  30. package/dist/workspace-runtime/codex/execution-context-tools.js +37 -0
  31. package/dist/workspace-runtime/codex/generation-provenance-broker.js +9 -6
  32. package/dist/workspace-runtime/codex/level-editor-tools.js +18 -0
  33. package/dist/workspace-runtime/codex/model-broker.js +15 -2
  34. package/dist/workspace-runtime/codex/scheduled-task-tools.js +1 -1
  35. package/dist/workspace-runtime/previews/finalize.js +1 -1
  36. package/dist/workspace-runtime/previews/output.js +4 -1
  37. package/dist/workspace-runtime/previews/publish.js +5 -2
  38. package/dist/workspace-runtime/previews/runtime-preview.js +38 -3
  39. package/dist/workspace-runtime/serve.js +6 -0
  40. package/dist/workspace-runtime/server/accept-operation.js +85 -13
  41. package/dist/workspace-runtime/server/audio-assets.js +211 -0
  42. package/dist/workspace-runtime/server/audio-export.js +136 -0
  43. package/dist/workspace-runtime/server/directory-access.js +222 -67
  44. package/dist/workspace-runtime/server/dispatch.js +252 -29
  45. package/dist/workspace-runtime/server/http.js +97 -3
  46. package/dist/workspace-runtime/server/operation-record.js +1 -1
  47. package/dist/workspace-runtime/workspaces/artifact-files.js +10 -3
  48. package/dist/workspace-runtime/workspaces/artifacts.js +47 -6
  49. package/dist/workspace-runtime/workspaces/audio-assets/artifact-package.js +104 -0
  50. package/dist/workspace-runtime/workspaces/audio-assets/binding-usage.js +71 -0
  51. package/dist/workspace-runtime/workspaces/audio-assets/build-inputs.js +76 -0
  52. package/dist/workspace-runtime/workspaces/audio-assets/render-worker.js +117 -0
  53. package/dist/workspace-runtime/workspaces/audio-assets/renderer.js +128 -0
  54. package/dist/workspace-runtime/workspaces/audio-assets/representations.js +73 -0
  55. package/dist/workspace-runtime/workspaces/audio-assets/service.js +327 -0
  56. package/dist/workspace-runtime/workspaces/audio-assets/storage.js +125 -0
  57. package/dist/workspace-runtime/workspaces/build-isolation.js +2 -0
  58. package/dist/workspace-runtime/workspaces/composer-context.js +40 -1
  59. package/dist/workspace-runtime/workspaces/empty-template.js +1 -1
  60. package/dist/workspace-runtime/workspaces/execution-context.js +24 -0
  61. package/dist/workspace-runtime/workspaces/files.js +3 -5
  62. package/dist/workspace-runtime/workspaces/level-editor-agent-store.js +143 -0
  63. package/dist/workspace-runtime/workspaces/level-editor-agent.js +410 -0
  64. package/dist/workspace-runtime/workspaces/level-editor-atomic.js +424 -0
  65. package/dist/workspace-runtime/workspaces/level-editor-extensions.js +140 -0
  66. package/dist/workspace-runtime/workspaces/level-editor-format.js +75 -0
  67. package/dist/workspace-runtime/workspaces/level-editor-history-store.js +250 -0
  68. package/dist/workspace-runtime/workspaces/level-editor-json5.js +132 -0
  69. package/dist/workspace-runtime/workspaces/level-editor-requests.js +724 -0
  70. package/dist/workspace-runtime/workspaces/level-editor.js +454 -0
  71. package/dist/workspace-runtime/workspaces/parameter-initialization-skill.js +62 -22
  72. package/dist/workspace-runtime/workspaces/parameter-production.js +1 -1
  73. package/dist/workspace-runtime/workspaces/remix-reference-skill.js +4 -3
  74. package/dist/workspace-runtime/workspaces/skill-artifact-loader.js +19 -5
  75. package/dist/workspace-runtime/workspaces/skills.js +48 -13
  76. package/package.json +6 -4
  77. package/project-template/.claude/skills/playcraft-audio-generation/SKILL.md +74 -4
  78. package/project-template/.claude/skills/playcraft-level-editor/SKILL.md +34 -0
  79. package/project-template/.claude/skills/playcraft-level-editor/references/authoring-contract.json +3475 -0
  80. package/project-template/.claude/skills/playcraft-level-editor/references/extensions.md +37 -0
  81. package/project-template/.claude/skills/playcraft-level-editor/references/preview.md +15 -0
  82. package/project-template/.claude/skills/playcraft-level-editor/references/protocol-v1.md +35 -0
  83. package/project-template/vite.config.ts +1 -1
  84. package/project-template-v2/.claude/skills/playcraft-audio-generation/SKILL.md +74 -4
  85. package/project-template-v2/.playcraft/skills.lock.json +9 -2
  86. package/project-template-v2/vite.config.ts +1 -1
  87. package/project-templates/empty/AGENTS.md +8 -4
  88. package/project-templates/phaser/.claude/skills/playcraft-audio-generation/SKILL.md +74 -4
  89. package/project-templates/phaser/vite.config.ts +1 -1
  90. package/project-templates/playable-template-phaser/vite.config.ts +1 -1
  91. package/project-templates/threejs/.claude/skills/playcraft-audio-generation/SKILL.md +74 -4
  92. package/project-templates/threejs/vite.config.ts +1 -1
  93. package/templates/preview-runtime/v1/README.md +12 -0
  94. package/templates/preview-runtime/v1/build-preview.mjs +17 -8
  95. package/templates/preview-runtime/v1/manifest.json +9 -6
  96. package/templates/preview-runtime/v1/release-guard.mjs +1 -1
  97. package/templates/preview-runtime/v1/runtime.d.mts +1 -3
  98. package/templates/preview-runtime/v1/runtime.d.ts +1 -3
  99. package/templates/preview-runtime/v1/runtime.mjs +1142 -257
  100. package/templates/preview-runtime/v1/types/host.d.ts +29 -0
  101. package/templates/preview-runtime/v1/types/index.d.ts +6 -0
  102. package/templates/preview-runtime/v1/types/level-editor.d.ts +123 -0
@@ -42,7 +42,8 @@ export async function ensureWorkspaceRemixReferenceSkill(root, cache) {
42
42
  const archive = createCanonicalBundleArchive([{ path: 'SKILL.md', data: oldContent }]);
43
43
  if (createHash('sha256').update(oldContent).digest('hex') !== ORIGINAL_BUNDLED_GUIDE
44
44
  || current.sourceTreeHash !== locked.sourceTreeHash
45
- || createHash('sha256').update(archive).digest('hex') !== locked.revision.artifactHash)
45
+ || (previous.lock.lockVersion === 3 && createHash('sha256').update(archive).digest('hex') !== locked.revision.artifactHash)
46
+ || (previous.lock.lockVersion === 4 && current.sourceTreeHash !== locked.revision.contentHash))
46
47
  return;
47
48
  }
48
49
  else {
@@ -66,7 +67,7 @@ export async function ensureWorkspaceRemixReferenceSkill(root, cache) {
66
67
  const addition = buildLocalProjectSkillsSnapshot({
67
68
  declaration: { schemaVersion: 1, entries: [existing ?? { entryKey, source: { kind: 'repository', root: source }, enabled: true, surface: 'sandbox-agent' }] },
68
69
  discovered: [], sources: [{ entryKey, root: source, files, sourceTreeHash: sourceTreeDigest(files) }], issues: [],
69
- }, existing ? previous.lock : undefined);
70
+ }, existing ? previous.lock : undefined, { lockVersion: previous.lock.lockVersion });
70
71
  const next = structuredClone(previous);
71
72
  if (existing) {
72
73
  next.declaration.entries[next.declaration.entries.findIndex(entry => entry.entryKey === entryKey)] = addition.declaration.entries[0];
@@ -74,7 +75,7 @@ export async function ensureWorkspaceRemixReferenceSkill(root, cache) {
74
75
  }
75
76
  else {
76
77
  next.declaration.entries.push(...addition.declaration.entries);
77
- next.lock.entries.push(...addition.lock.entries);
78
+ next.lock = { ...next.lock, entries: [...next.lock.entries, ...addition.lock.entries] };
78
79
  }
79
80
  next.lock.declarationHash = declarationDigest(next.declaration);
80
81
  validateProjectSkillsSnapshot(next.declaration, next.lock);
@@ -1,4 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { extractBundleArchive, sourceTreeDigest } from '@playcraft/common/project-skills';
2
3
  import { workspaceRoot } from './registry.js';
3
4
  import { PLATFORM_ROUTES, platformEndpoint } from '@playcraft/common/cloud-workspace/runtime';
4
5
  const MAX_ARTIFACT_BYTES = 80 * 1024 * 1024;
@@ -40,12 +41,18 @@ async function requestWorkspaceSkillArtifact(state, workspaceId, entry) {
40
41
  await reader.cancel().catch(() => undefined);
41
42
  }
42
43
  const result = JSON.parse(Buffer.concat(chunks).toString('utf8'));
43
- if (typeof result.content !== 'string' || result.artifactHash !== entry.revision.artifactHash
44
+ if (typeof result.content !== 'string' || typeof result.artifactHash !== 'string' || !/^[a-f0-9]{64}$/.test(result.artifactHash)
45
+ || (!entry.revision.contentHash && result.artifactHash !== entry.revision.artifactHash)
44
46
  || result.content.length > Math.ceil(MAX_ARTIFACT_BYTES * 4 / 3) || result.content.length % 4 !== 0 || /[^A-Za-z0-9+/=]/.test(result.content))
45
47
  throw new Error('workspace_skills_digest_mismatch');
46
48
  const archive = Buffer.from(result.content, 'base64');
47
- if (archive.byteLength > MAX_ARTIFACT_BYTES || archive.toString('base64') !== result.content || createHash('sha256').update(archive).digest('hex') !== entry.revision.artifactHash)
49
+ if (archive.byteLength > MAX_ARTIFACT_BYTES || archive.toString('base64') !== result.content || createHash('sha256').update(archive).digest('hex') !== result.artifactHash)
48
50
  throw new Error('workspace_skills_digest_mismatch');
51
+ if (entry.revision.contentHash) {
52
+ const extracted = extractBundleArchive(archive);
53
+ if (extracted.issues.length || sourceTreeDigest(extracted.files.map(file => ({ path: file.path, content: file.data }))) !== entry.revision.contentHash)
54
+ throw new Error('workspace_skills_digest_mismatch');
55
+ }
49
56
  if (state.journal.destroyed() || epoch !== state.runtimeEpoch)
50
57
  throw new Error('workspace_skills_unavailable');
51
58
  return archive;
@@ -53,16 +60,23 @@ async function requestWorkspaceSkillArtifact(state, workspaceId, entry) {
53
60
  /** The platform independently authorizes the trusted binding, including an unpublished checkout. */
54
61
  export function workspacePlatformSkillAccess(state, workspaceId) {
55
62
  return {
56
- resolve: async (skillRefs) => {
63
+ resolve: async (skillRefs, lockVersion = 3, existingLockVersion) => {
57
64
  const epoch = state.runtimeEpoch;
58
65
  if (state.journal.destroyed())
59
66
  throw new Error('workspace_skills_unavailable');
60
- const response = await fetch(`${platformEndpoint(state.config.publicBaseUrl, PLATFORM_ROUTES.skillArtifacts)}/resolve`, {
67
+ const request = (includeVersion) => fetch(`${platformEndpoint(state.config.publicBaseUrl, PLATFORM_ROUTES.skillArtifacts)}/resolve`, {
61
68
  method: 'POST', redirect: 'error', signal: AbortSignal.timeout(30_000),
62
69
  headers: { authorization: `Bearer ${state.config.controlToken}`, 'content-type': 'application/json', 'playcraft-env': state.config.environment },
63
70
  body: JSON.stringify({ runtimeId: state.config.runtimeId, generation: state.config.generation,
64
- runtimeEpoch: epoch, workspaceId, skillRefs }),
71
+ runtimeEpoch: epoch, workspaceId, skillRefs, ...(includeVersion ? { lockVersion, ...(existingLockVersion ? { existingLockVersion } : {}) } : {}) }),
65
72
  });
73
+ let response = await request(lockVersion === 4);
74
+ // Older strict-schema servers report an unknown field as 401. Retry only to
75
+ // prove legacy capability with the same credentials; a second 401 remains fatal.
76
+ if (lockVersion === 4 && (response.status === 400 || response.status === 401)) {
77
+ await response.body?.cancel().catch(() => undefined);
78
+ response = await request(false);
79
+ }
66
80
  if (!response.ok) {
67
81
  await response.body?.cancel().catch(() => undefined);
68
82
  throw new Error(response.status === 401 || response.status === 403
@@ -82,7 +82,11 @@ async function lockedFiles(root, cache, entry, view, loadArtifact) {
82
82
  const persistent = await safeDirectory(root, '.playcraft/skill-artifacts');
83
83
  const saved = join(persistent, `${entry.revision.artifactHash}.tar.gz`);
84
84
  const path = join(cache, `${entry.revision.artifactHash}.tar.gz`);
85
+ const contentHash = view.lock.lockVersion === 4 ? entry.revision.contentHash : undefined;
86
+ const contentSaved = contentHash ? join(persistent, `content-${contentHash}.tar.gz`) : undefined;
87
+ const contentPath = contentHash ? join(cache, `content-${contentHash}.tar.gz`) : undefined;
85
88
  let archive;
89
+ let contentAddressed = false;
86
90
  try {
87
91
  const stat = await lstat(saved);
88
92
  if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > 80 * 1024 * 1024)
@@ -93,33 +97,64 @@ async function lockedFiles(root, cache, entry, view, loadArtifact) {
93
97
  if (error.code !== 'ENOENT')
94
98
  throw error;
95
99
  try {
96
- archive = await readFile(path);
100
+ if (!contentSaved)
101
+ throw error;
102
+ const stat = await lstat(contentSaved);
103
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > 80 * 1024 * 1024)
104
+ throw new Error('workspace_skills_invalid_bundle');
105
+ archive = await readFile(contentSaved);
106
+ contentAddressed = true;
97
107
  }
98
108
  catch (cacheError) {
99
109
  if (cacheError.code !== 'ENOENT')
100
110
  throw cacheError;
101
- const declaration = view.declaration.entries.find(item => item.entryKey === entry.entryKey);
102
- if (entry.revision.kind === 'source' && declaration.source.kind === 'repository') {
103
- const source = await readProjectSkillSource(root, declaration);
104
- if (source.sourceTreeHash !== entry.sourceTreeHash)
105
- throw new Error('workspace_skills_source_changed');
106
- archive = createCanonicalBundleArchive(source.files.map(file => ({ path: file.path, data: Buffer.from(file.content) })));
111
+ try {
112
+ archive = await readFile(path);
107
113
  }
108
- else {
109
- if (!loadArtifact)
110
- throw new Error('workspace_skills_artifact_unavailable');
111
- archive = await loadArtifact(entry);
114
+ catch (error) {
115
+ if (error.code !== 'ENOENT')
116
+ throw error;
117
+ try {
118
+ if (!contentPath)
119
+ throw error;
120
+ archive = await readFile(contentPath);
121
+ contentAddressed = true;
122
+ }
123
+ catch (contentError) {
124
+ if (contentError.code !== 'ENOENT')
125
+ throw contentError;
126
+ const declaration = view.declaration.entries.find(item => item.entryKey === entry.entryKey);
127
+ if (entry.revision.kind === 'source' && declaration.source.kind === 'repository') {
128
+ const source = await readProjectSkillSource(root, declaration);
129
+ if (source.sourceTreeHash !== entry.sourceTreeHash)
130
+ throw new Error('workspace_skills_source_changed');
131
+ if (contentHash) {
132
+ if (source.sourceTreeHash !== contentHash)
133
+ throw new Error('workspace_skills_digest_mismatch');
134
+ return source.files.map(file => ({ path: file.path, data: Buffer.from(file.content) }));
135
+ }
136
+ archive = createCanonicalBundleArchive(source.files.map(file => ({ path: file.path, data: Buffer.from(file.content) })));
137
+ }
138
+ else {
139
+ if (!loadArtifact)
140
+ throw new Error('workspace_skills_artifact_unavailable');
141
+ archive = await loadArtifact(entry);
142
+ contentAddressed = Boolean(contentHash);
143
+ }
144
+ }
112
145
  }
113
146
  }
114
147
  }
115
- if (createHash('sha256').update(archive).digest('hex') !== entry.revision.artifactHash)
148
+ if (!contentAddressed && createHash('sha256').update(archive).digest('hex') !== entry.revision.artifactHash)
116
149
  throw new Error('workspace_skills_digest_mismatch');
117
150
  const extracted = extractBundleArchive(archive);
118
151
  if (extracted.issues.length)
119
152
  throw new Error('workspace_skills_invalid_bundle');
153
+ if (contentHash && sourceTreeDigest(extracted.files.map(file => ({ path: file.path, content: file.data }))) !== contentHash)
154
+ throw new Error('workspace_skills_digest_mismatch');
120
155
  if (entry.revision.kind === 'source' && sourceTreeDigest(extracted.files.map(file => ({ path: file.path, content: file.data }))) !== entry.sourceTreeHash)
121
156
  throw new Error('workspace_skills_digest_mismatch');
122
- await writeSharedFile(root, saved, archive);
157
+ await writeSharedFile(root, contentAddressed ? contentSaved : saved, archive);
123
158
  return extracted.files;
124
159
  }
125
160
  function metadata(entryKey, content) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcraft/cli",
3
- "version": "0.1.65",
3
+ "version": "0.1.67",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -23,7 +23,7 @@
23
23
  ],
24
24
  "scripts": {
25
25
  "dev": "tsc -w",
26
- "build": "node scripts/generate-asset-skills.mjs && node scripts/generate-video-skills.mjs && node scripts/generate-parameter-skill.mjs && node scripts/generate-preview-runtime.mjs && tsc",
26
+ "build": "node scripts/generate-asset-skills.mjs && node scripts/generate-video-skills.mjs && node scripts/generate-parameter-skill.mjs && node scripts/generate-level-editor-skill.mjs && node scripts/generate-preview-runtime.mjs && tsc",
27
27
  "start": "node dist/index.js",
28
28
  "test": "pnpm build && vitest run",
29
29
  "test:agent": "node scripts/run-agent-tests.mjs --list",
@@ -42,6 +42,7 @@
42
42
  "generate:video-skills": "node scripts/generate-video-skills.mjs",
43
43
  "verify:video-skills": "node scripts/generate-video-skills.mjs --check && node --test scripts/generate-video-skills.test.mjs",
44
44
  "verify:parameter-skill": "node scripts/generate-parameter-skill.mjs --check && node --test scripts/generate-parameter-skill.test.mjs",
45
+ "verify:level-editor-skill": "node scripts/generate-level-editor-skill.mjs --check",
45
46
  "verify:asset-skills": "node scripts/generate-asset-skills.mjs --check && node --test scripts/generate-asset-skills.test.mjs",
46
47
  "verify:agent-stop": "node scripts/verify-agent-stop.mjs",
47
48
  "verify-sprite-overlay": "node scripts/verify-sprite-overlay.mjs",
@@ -53,8 +54,8 @@
53
54
  "@gltf-transform/core": "^4.3.0",
54
55
  "@gltf-transform/extensions": "^4.3.0",
55
56
  "@gltf-transform/functions": "^4.3.0",
56
- "@playcraft/build": "0.1.65",
57
- "@playcraft/common": "0.1.65",
57
+ "@playcraft/build": "0.1.67",
58
+ "@playcraft/common": "0.1.67",
58
59
  "better-sqlite3-pinned": "npm:better-sqlite3@13.0.3",
59
60
  "chokidar": "^4.0.3",
60
61
  "commander": "^13.1.0",
@@ -80,6 +81,7 @@
80
81
  "three": "0.183.2",
81
82
  "update-notifier": "^7.3.1",
82
83
  "ws": "^8.19.0",
84
+ "yaml": "2.8.3",
83
85
  "zod": "^3.25.76"
84
86
  },
85
87
  "devDependencies": {
@@ -3,7 +3,7 @@ name: playcraft-audio-generation
3
3
  description: Use when generating or replacing playable audio, reusing library audio, managing durable audio tasks, or locally compressing, trimming, looping, normalizing, and mixing sound.
4
4
  triggers: 生成音效,生成bgm,生成背景音乐,generate sfx,generate bgm,generate audio,音频,音效,背景音乐,sfx,bgm,loop音频,音频压缩,音频处理,audio,compress audio,fade,normalize,音频后处理,音频管线
5
5
  compatibility: agent,opencode
6
- version: 1.1.1
6
+ version: 1.3.0
7
7
  surfaces:
8
8
  - sandbox-agent
9
9
  ---
@@ -12,7 +12,77 @@ surfaces:
12
12
 
13
13
  ## 0. 开始前必读
14
14
 
15
- AI (Artificial Intelligence, 人工智能) 音频包含 SFX (Sound Effects, 音效) 与 BGM (Background Music, 背景音乐)。工作素材写入当前工程的 `assets/audio/` 或当前主题素材目录;最终交付导出到工程根目录 `dist/`。请求文件和工作记录保存在 `dist/` 外。
15
+ AI (Artificial Intelligence, 人工智能) 音频包含 SFX (Sound Effects, 音效) 与 BGM (Background Music, 背景音乐)。文件音频工作素材写入当前工程的 `assets/audio/` 或当前主题素材目录;最终交付导出到工程根目录 `dist/`。可编辑短音效源见下节。请求文件和工作记录保存在 `dist/` 外。
16
+
17
+ ### 云工作区的可编辑短音效
18
+
19
+ 仅在当前锁定技能版本与工作区能力支持时使用本段流程。先运行 `playcraft audio source capabilities`,检查 `enabled`、`writable`、对应 `operations`;生成与渲染还须 `renderer.ready`。能力缺失或关闭时保留源及已有试听,报告不可用,不替换工程技能锁,不移除声音上下文后继续执行。`synth-sfx` 保持短音效流程;仅在 `audioScoreVersion:1` 与 `audioScoreContextVersion:1` 均声明时使用下述乐谱/多轨流程。外部采样和动态混合仍不支持。
20
+
21
+ 默认从描述、试听、采用开始,只展示适用的时长、音量、音高、明亮度简单控件;用户要求细节后再展开精确参数。工程源在 `source/audio/` 等 `dist/` 外目录,最终表示包位于 `dist/audio/`。未采用候选保持私有,不因续改而强制写入工程。
22
+
23
+ ```bash
24
+ # short-sound-request.json: {"kind":"audio","representation":"synth-sfx","category":"sfx","description":"短促明亮的金币音","duration":0.2,"candidateCount":1}
25
+ playcraft audio source create --request short-sound-request.json --request-id <stable-request-id>
26
+ playcraft assets get <taskId>
27
+ playcraft assets wait <taskId>
28
+ playcraft audio source inspect --task <taskId> --candidate <candidateId>
29
+
30
+ # duration 是秒;volume 是主增益;pitch 是半音;brightness 是滤波截止频率倍数。
31
+ playcraft audio source edit --task <taskId> --candidate <candidateId> \
32
+ --revision <candidateRevision> --hash <sourceContentHash> --request-id <stable-edit-id> \
33
+ --macro duration --value 0.15
34
+ playcraft audio source render --task <taskId> --candidate <candidateId> \
35
+ --revision <newRevision> --hash <newSourceContentHash> --request-id <stable-render-id>
36
+
37
+ # 用户明确采用后才保存到工程;替换已有目标还须其真实修订和摘要。
38
+ playcraft audio source adopt --task <taskId> --candidate <candidateId> \
39
+ --revision <candidateRevision> --hash <sourceContentHash> --request-id <stable-adopt-id> \
40
+ --output source/audio/coin.pcaudio.json --destination-revision 0
41
+ playcraft audio source inspect --path source/audio/coin.pcaudio.json
42
+ ```
43
+
44
+ 这些命令使用同一工作区素材任务。候选身份是任务、候选、修订与源内容摘要;工程源身份是工作区、目录、路径、修订与摘要。保存完整源用 `edit/save --document <file>`,只改简单目标用 `--macro`。每次保存后重新渲染才获得对应试听。所有写命令都要求稳定 `--request-id`;响应不确定时使用原身份恢复,不重复生成。`assets download` 不会把参数候选伪装成音频文件,请走专用采用。
45
+
46
+ 接收 `composer.context.audio` 时先核对目录与精确版本,并保留 `selection`、`lockedVoiceIds`、`preserveTargets`。将保护项传给 `edit/save --locked-voices <逗号分隔声部> --preserve-targets <逗号分隔参数目标>`。锁定和曲线不可静默重置。`intent: propose` 只授权提出可比较方向;明确修改并保存才写入原目标;用于游戏需要接入授权。修订冲突表示用户已有新修改,保留结果供比较,不能刷新基准后覆盖。
47
+
48
+ 保存、试听就绪、实际引用、构建通过分别记录。`pending`、`failed`、`cancelled` 和 `render_peak_exceeded` 不能宣称完成;峰值超限时保留原源、说明需调整增益,不静默归一化。候选修改不会自动采用,收起精细视图也不能丢失源字段。
49
+
50
+ 用于游戏时读取采用回执的 `representation.modulePath`,其中 `sound.mjs` 与运行时、源和试听均是不可变包内相对引用。定位用户指定事件的实际调用点,或沿已有绑定替换;保存旧调用和绑定后,导入 `createSound`,在该交互中调用 `play()`,场景销毁调用 `dispose()`。共享播放器最多 8 个实例、32 个声部;播放需浏览器交互解锁,按工程既有方式处理失败。随后执行工程原有构建验证;没有明确调用点时保持待接入,不猜测事件映射。
51
+
52
+ ```javascript
53
+ import { createSound } from '<representation.modulePath 相对当前模块的路径>';
54
+ const coinSound = createSound();
55
+ // 放入用户指定的金币事件处理器;按项目方式处理播放失败。
56
+ await coinSound.play();
57
+ // 场景销毁:coinSound.dispose();
58
+ ```
59
+
60
+ `playcraft audio source bind --path <source> --revision <revision> --hash <hash> --event <event> --request-id <id>` 只创建固定版本的交接记录,返回 `integration_required`,不能证明代码已引用或构建成功。`rollback --path <source> --revision <current> --hash <hash> --version <historical> --request-id <id>` 追加历史源恢复修订,之后重新渲染。游戏回退还必须恢复旧模块调用及绑定,再次构建;不能只恢复源就宣称游戏回退。
61
+
62
+ ### 可编辑音乐与多轨协作
63
+
64
+ 音乐使用 `format: "playcraft.score"`、`content.kind: "score"` 的 `.pcscore.json`,引擎 `playcraft-webaudio-score@1.0.0`。先核对音乐能力、写权限与渲染就绪。默认让用户描述用途、试听候选,再调速度、整体强弱、乐器和循环;选轨、拍段或音符后才展开细节。符号音高采用 MIDI (Musical Instrument Digital Interface, 乐器数字接口) 音符号;时间是四分音符拍点,速度为 BPM (Beats Per Minute, 每分钟节拍数),不能混用秒和拍。
65
+
66
+ ```bash
67
+ # music-request.json: {"kind":"audio","representation":"score","category":"bgm","description":"轻快的两轨游戏循环音乐","duration":8,"bpm":120,"loop":true,"candidateCount":1}
68
+ playcraft audio source create --request music-request.json --request-id <stable-create-id>
69
+ playcraft assets wait <taskId>
70
+ playcraft audio source inspect --task <taskId> --candidate <candidateId>
71
+ playcraft audio source edit --task <taskId> --candidate <candidateId> \
72
+ --revision <revision> --hash <sourceContentHash> --request-id <stable-edit-id> --macro tempo --value 132
73
+ playcraft audio source render --task <taskId> --candidate <candidateId> \
74
+ --revision <newRevision> --hash <newHash> --request-id <stable-render-id>
75
+ playcraft audio source adopt --task <taskId> --candidate <candidateId> \
76
+ --revision <newRevision> --hash <newHash> --request-id <stable-adopt-id> --output source/audio/loop.pcscore.json
77
+ ```
78
+
79
+ 精细操作使用 `edit --adjustment <file>`:例如 `{"kind":"instrument","instrument":"bell","trackIds":["melody"]}`;或者在读取精确源后 `edit --document <file>` 保存完整源。内置音色限 `piano / bell / bass / pad / sine / drums`,不是外部采样乐器库。最多 8 轨、512 音符、32 并发音符、60 秒和 64 KiB 源;超限、无法表达的特性必须报告,不截断或改成另一种音频。
80
+
81
+ `composer.context.audio.version:2` 明确 `timeUnit:"beats"`,冻结 `selection.trackIds / noteIds / rangeBeats`、`protection.lockedTrackIds / preserveMelody`。把该选区原样写入工程外临时文档,保存时传 `--selection <file> --locked-tracks <ids> --preserve-melody` 中实际适用的项。只调整鼓轨第 5—8 小节时,按拍号计算精确拍段,保护其余轨和拍段;改变整轨音色、增益、自动化或全局速度会影响范围之外,不能把它当作局部修改。服务器会与可信上下文再次核对,命令自行删掉保护条件不会解除限制。
82
+
83
+ 修订不匹配时保留双方,不自行刷新后覆盖。网页可由用户显式合并没有依赖冲突的轨道或音符;速度、循环、全局混音、同轨音色/自动化及同音符冲突需要比较后重做。候选仍须独立采用,回退仍追加新源修订,游戏引用仍单独恢复。
84
+
85
+ 音乐完整包成员为 `sound.pcscore.json / audition.wav / runtime.mjs / sound.mjs / manifest.json`,只使用回执中的固定模块。`createSound().prepare()` 不默认展开整段音乐采样;`play()` 按乐谱和循环段调度,返回的 `stop()` 或播放器 `stop()/dispose()` 会结束循环并释放资源。场景退出、页面隐藏或切换试听时应调用停止,禁止遗留循环。单次播放可传 `play({loop:false})`;指定段落或轨道试听通过受支持的 `selection` 参数。试听渲染文件本身是完整单次表示,循环起止需随乐谱一起使用。
16
86
 
17
87
  ### 复用素材与持久化音频任务
18
88
 
@@ -199,7 +269,7 @@ playcraft audio info --input assets/audio/bgm/main_theme_loop.mp3
199
269
 
200
270
  ## 3. 本地音频后处理
201
271
 
202
- 所有 `playcraft audio` 子命令**完全本地运行**,无需网络。
272
+ 本节文件后处理子命令**完全本地运行**,无需网络;前述 `playcraft audio source` 使用云工作区服务。
203
273
 
204
274
  ### 3.1 常用处理命令
205
275
 
@@ -253,7 +323,7 @@ playcraft audio mix \
253
323
 
254
324
  ### 3.2 标准音频处理管线
255
325
 
256
- AI 生成后的**必做处理步骤**:
326
+ 文件音频生成后的处理步骤:
257
327
 
258
328
  ```
259
329
  SFX 管线:
@@ -0,0 +1,34 @@
1
+ ---
2
+ name: playcraft-level-editor
3
+ description: Create or evolve a project's declarative level editor by mapping its actual level files to platform editing components and connecting the same saved data to production and development playtests.
4
+ compatibility: agent,opencode
5
+ version: 1.1.0
6
+ surfaces:
7
+ - sandbox-agent
8
+ ---
9
+
10
+ # Project level editor
11
+
12
+ Generate a project-owned editing declaration, data schema and real game integration. The platform composes its editor from the declaration; do not build a game-specific platform page or infer a writable scene from arbitrary engine objects.
13
+
14
+ The runtime supplies an exact task identity, frozen component catalog and admission-report path. Read [the protocol](references/protocol-v1.md) and the installed component catalog before changing files. Use [preview integration](references/preview.md) when connecting the game.
15
+
16
+ Inspect the current production entry, level loading, authoring data, resource registration and lifecycle. Prefer existing native files and identifiers. Choose controls that address the user's actual request; preserve unrelated data, comments, layouts and existing editor customization. Repeated initialization evolves the current declaration, rather than resetting the game to an example.
17
+
18
+ Declare explicit collections, stable identities, selection, bindings, commands and view composition. Field names and panel layouts belong to the project. Expose only supported component and command versions; do not generate executable expressions inside declarations or game-specific platform frontend code. Use the installed extension contracts for project-owned dedicated views, validators and data providers. Ordinary edits use the declared commands without another Agent call.
19
+
20
+ Give every declared data-schema node an explicit single `type`; annotations such as `readOnly` cannot replace it. Preserve unknown author fields with their actual types and readonly boundaries rather than changing their values to fit a schema. Follow the exact node scope and subset rules in [the protocol](references/protocol-v1.md).
21
+
22
+ For paths, three-dimensional data, custom shapes, command compositions, dedicated views or nonstandard files, read [project extensions](references/extensions.md). Use only features reported by the current runtime; preserve existing frozen Skill locks.
23
+
24
+ Production and development playtests must consume the same neutral data resolver. Connect actual consumers: a declaration, getter or hand-written success report is insufficient. Editing changes authored initial data; playtest scores, deaths, motion and generated objects must not be written back to it. Restart the current level when the change cannot honestly apply in place.
25
+
26
+ Before changing a data contract, preserve the original files and account for existing user values and object identities. A layout-only change must not migrate level data. If a required migration cannot preserve meaning, leave the current data intact and report the missing work.
27
+
28
+ Add executable `test:level-editor` checks for defaults and at least two meaningful edited outcomes in real game consumers. Preserve an existing `typecheck` and successful production `build`. Production output belongs in `dist/` and excludes development editor code. Keep reviewable partial changes and report concrete unsupported work on failure.
29
+
30
+ These permanent checks must remain valid after ordinary legal author edits. Exercise controlled copies of current author data and actual consumers; do not pin the original level file hash, budget, coordinates, or object count as an immutable test expectation. Original-byte preservation belongs to the platform admission baseline and report, not a permanent restriction on the user's future levels.
31
+
32
+ Use the admission report's `missing` only for actual incomplete or failed engineering capabilities, such as schema, real consumers, adapter wiring, required scripts or production isolation. Post-authoring platform verification, managed preview context, browser handshake and Portal operations belong to their later authoritative phases; not having run them does not itself describe a code gap. `missing: []` claims no identified engineering gap, not checked, verified or connected status. Preserve unverified facts in the final reply or project README; follow [the protocol's phase boundaries](references/protocol-v1.md) without inventing pending report fields or platform results.
33
+
34
+ After editing, calculate the actual source-file hashes and write the exact admission report supplied by the runtime. End with the implemented scope and remaining work. The platform independently verifies sources and checks, then prepares the first development instance; only its real handshake establishes connected capability.