@playcraft/cli 0.0.39 → 0.0.41
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 +66 -3
- package/dist/atom-plan/validate-atom-plan.js +298 -0
- package/dist/cli-root-help.js +1 -1
- package/dist/commands/3d.js +363 -0
- package/dist/commands/create.js +337 -0
- package/dist/commands/fix-ids.js +17 -3
- package/dist/commands/fix-ids.test.js +264 -0
- package/dist/commands/image.js +1337 -43
- package/dist/commands/login.js +60 -2
- package/dist/commands/recommend.js +1 -1
- package/dist/commands/remix.js +213 -0
- package/dist/commands/skills.js +1379 -0
- package/dist/commands/tools-3d.js +473 -0
- package/dist/commands/tools-generation.js +454 -0
- package/dist/commands/tools-project.js +400 -0
- package/dist/commands/tools-research.js +37 -0
- package/dist/commands/tools-research.test.js +216 -0
- package/dist/commands/tools-utils.js +164 -0
- package/dist/commands/tools.js +7 -616
- package/dist/config.js +2 -0
- package/dist/index.js +20 -2
- package/dist/utils/agent-api-client.js +52 -16
- package/package.json +9 -3
- package/project-template/.claude/agents/designer.md +116 -0
- package/project-template/.claude/agents/developer.md +133 -0
- package/project-template/.claude/agents/pm.md +164 -0
- package/project-template/.claude/agents/refs/README.md +67 -0
- package/project-template/.claude/agents/refs/designer-art-style-catalog.md +533 -0
- package/project-template/.claude/agents/refs/designer-color-audio-recipes.md +153 -0
- package/project-template/.claude/agents/refs/designer-deliverable-spec.md +167 -0
- package/project-template/.claude/agents/refs/designer-dimension-axis.md +27 -0
- package/project-template/.claude/agents/refs/designer-handoff-v2-checklist.md +68 -0
- package/project-template/.claude/agents/refs/designer-master-composite-recipes.md +216 -0
- package/project-template/.claude/agents/refs/designer-style-exploration-flow.md +37 -0
- package/project-template/.claude/agents/refs/developer-dev-handoff.md +109 -0
- package/project-template/.claude/agents/refs/developer-impl-cookbook.md +134 -0
- package/project-template/.claude/agents/refs/developer-phase1-flow.md +211 -0
- package/project-template/.claude/agents/refs/pm-workflow-detail.md +545 -0
- package/project-template/.claude/agents/refs/reviewer-six-dimension-eval.md +286 -0
- package/project-template/.claude/agents/refs/ta-3d-flip-recipe.md +85 -0
- package/project-template/.claude/agents/refs/ta-atlas-deliverable-standard.md +46 -0
- package/project-template/.claude/agents/refs/ta-batch-pipeline-recipes.md +120 -0
- package/project-template/.claude/agents/refs/ta-image-generation-detail.md +356 -0
- package/project-template/.claude/agents/refs/ta-image-ops-reference.md +495 -0
- package/project-template/.claude/agents/refs/ta-pipeline-cookbook.md +699 -0
- package/project-template/.claude/agents/refs/ta-tools-reference.md +111 -0
- package/project-template/.claude/agents/refs/ta-vfx-preset-catalog.md +365 -0
- package/project-template/.claude/agents/reviewer.md +103 -0
- package/project-template/.claude/agents/technical-artist.md +111 -0
- package/project-template/.claude/hooks/README.md +36 -0
- package/project-template/.claude/hooks/validate-atom-plan.mjs +224 -0
- package/project-template/.claude/hooks/validate-workflow-stop.mjs +258 -0
- package/project-template/.claude/settings.json +32 -0
- package/project-template/.claude/settings.local.json +4 -0
- package/project-template/.claude/skills/playcraft-ad-psychology/SKILL.md +182 -0
- package/project-template/.claude/skills/playcraft-art-style-guide/SKILL.md +123 -0
- package/project-template/.claude/skills/playcraft-asset-state-sheet/SKILL.md +141 -0
- package/project-template/.claude/skills/playcraft-audio-generation/SKILL.md +280 -0
- package/project-template/.claude/skills/playcraft-batch-pipeline/SKILL.md +184 -0
- package/project-template/.claude/skills/playcraft-build-optimizer/SKILL.md +306 -0
- package/project-template/.claude/skills/playcraft-image-generation/SKILL.md +229 -0
- package/project-template/.claude/skills/playcraft-image-generation/reference/build-sprite-sheet.template.mjs +123 -0
- package/project-template/.claude/skills/playcraft-image-generation/reference/compare-style.template.mjs +254 -0
- package/project-template/.claude/skills/playcraft-image-generation/reference/gen-batch-sprite.template.mjs +235 -0
- package/project-template/.claude/skills/playcraft-image-generation/reference/gen-batch.template.mjs +97 -0
- package/project-template/.claude/skills/playcraft-image-generation/reference/gen-edit-variants.template.mjs +118 -0
- package/project-template/.claude/skills/playcraft-image-generation/reference/process-batch.template.mjs +137 -0
- package/project-template/.claude/skills/playcraft-image-generation/reference/prompt-cookbook.md +397 -0
- package/project-template/.claude/skills/playcraft-image-generation/reference/validate-sprite-sheet.template.mjs +296 -0
- package/project-template/.claude/skills/playcraft-image-ops/SKILL.md +122 -0
- package/project-template/.claude/skills/playcraft-masking/SKILL.md +373 -0
- package/project-template/.claude/skills/playcraft-research/SKILL.md +212 -0
- package/project-template/.claude/skills/playcraft-sprite-generation/SKILL.md +423 -0
- package/project-template/.claude/skills/playcraft-storyboard/SKILL.md +148 -0
- package/project-template/.claude/skills/playcraft-style-qa/SKILL.md +270 -0
- package/project-template/.claude/skills/playcraft-text-rendering/SKILL.md +236 -0
- package/project-template/.claude/skills/playcraft-vfx-animation/SKILL.md +130 -0
- package/project-template/.claude/skills/playcraft-workflow/SKILL.md +396 -0
- package/project-template/.cursor/hooks.json +17 -0
- package/project-template/.cursor/rules/playcraft-orchestrator.mdc +87 -0
- package/project-template/.cursor/rules/playcraft-subagent-boundary.mdc +18 -0
- package/project-template/CLAUDE.md +240 -0
- package/project-template/assets/audio/bgm/.gitkeep +0 -0
- package/project-template/assets/audio/sfx/.gitkeep +0 -0
- package/project-template/assets/bundles/.gitkeep +0 -0
- package/project-template/assets/images/bg/.gitkeep +0 -0
- package/project-template/assets/images/reference/.gitkeep +0 -0
- package/project-template/assets/images/storyboard/.gitkeep +0 -0
- package/project-template/assets/images/tiles/.gitkeep +0 -0
- package/project-template/assets/images/ui/.gitkeep +0 -0
- package/project-template/assets/images/vfx/.gitkeep +0 -0
- package/project-template/assets/models/.gitkeep +0 -0
- package/project-template/docs/team/agent-conduct.md +105 -0
- package/project-template/docs/team/agent-runtime-matrix.md +62 -0
- package/project-template/docs/team/atom-plan-format.md +74 -0
- package/project-template/docs/team/collaboration.md +288 -0
- package/project-template/docs/team/core-model.md +50 -0
- package/project-template/docs/team/platform-capabilities.md +15 -0
- package/project-template/docs/team/workflow-changelog.md +51 -0
- package/project-template/docs/team/workflow-consistency-checklist.md +128 -0
- package/project-template/game/config/.gitkeep +0 -0
- package/project-template/game/gameplay/.gitkeep +0 -0
- package/project-template/game/scenes/.gitkeep +0 -0
- package/project-template/logs/.gitkeep +0 -0
- package/project-template/ta-workspace/logs/.gitkeep +0 -0
- package/project-template/ta-workspace/scripts/.gitkeep +0 -0
- package/project-template/ta-workspace/tmp/.gitkeep +0 -0
- package/project-template/templates/atom-plan.template.json +26 -0
- package/project-template/templates/atom-plan.template.md +76 -0
- package/project-template/templates/design-brief.template.md +195 -0
- package/project-template/templates/design-lens-checklist.reference.md +117 -0
- package/project-template/templates/design-methodology.md +99 -0
- package/project-template/templates/designer-log.template.md +98 -0
- package/project-template/templates/developer-log.template.md +140 -0
- package/project-template/templates/five-axis-framework.md +186 -0
- package/project-template/templates/intent-clarifications.template.md +58 -0
- package/project-template/templates/layout-spec.template.md +132 -0
- package/project-template/templates/project-state.template.md +219 -0
- package/project-template/templates/review-report.template.md +166 -0
- package/project-template/templates/style-exploration.template.md +93 -0
- package/project-template/templates/ta-log.template.md +205 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { AgentApiClient } from '../utils/agent-api-client.js';
|
|
2
|
+
import { researchCommand, fetchUrlCommand } from './tools-research.js';
|
|
3
|
+
import { writeResult, handleError } from './tools-utils.js';
|
|
4
|
+
export function registerProjectCommands(tools) {
|
|
5
|
+
// ─── Project Pipeline ───────────────────────────────────────
|
|
6
|
+
tools.command('save-to-git')
|
|
7
|
+
.description('同步沙箱内容到 Git 仓库')
|
|
8
|
+
.requiredOption('--project-id <id>', '项目数字 ID', parseInt)
|
|
9
|
+
.requiredOption('--message <text>', '提交信息')
|
|
10
|
+
.option('--branch <name>', '分支名', 'main')
|
|
11
|
+
.action(async (opts) => {
|
|
12
|
+
try {
|
|
13
|
+
const client = new AgentApiClient();
|
|
14
|
+
const result = await client.post('/save-to-git', {
|
|
15
|
+
projectId: opts.projectId,
|
|
16
|
+
branch: opts.branch,
|
|
17
|
+
commitMessage: opts.message,
|
|
18
|
+
});
|
|
19
|
+
if (result.success) {
|
|
20
|
+
console.log(`Saved to git: commit ${result.commitHash ?? 'unknown'}`);
|
|
21
|
+
}
|
|
22
|
+
else if (result.conflict) {
|
|
23
|
+
console.log(`Conflict detected in ${result.conflictFiles?.length ?? 0} file(s)`);
|
|
24
|
+
if (result.conflictFiles?.length) {
|
|
25
|
+
for (const f of result.conflictFiles)
|
|
26
|
+
console.log(` - ${f}`);
|
|
27
|
+
}
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
console.error(`Error: ${result.error ?? 'Save failed'}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
handleError(e);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
tools.command('build-project')
|
|
40
|
+
.description('在沙箱中执行项目构建')
|
|
41
|
+
.requiredOption('--project-id <id>', '项目数字 ID', parseInt)
|
|
42
|
+
.option('--branch <name>', '分支名', 'main')
|
|
43
|
+
.action(async (opts) => {
|
|
44
|
+
try {
|
|
45
|
+
const client = new AgentApiClient();
|
|
46
|
+
const result = await client.post('/build-project', {
|
|
47
|
+
projectId: opts.projectId,
|
|
48
|
+
branch: opts.branch,
|
|
49
|
+
});
|
|
50
|
+
if (result.success === false) {
|
|
51
|
+
if (result.output) {
|
|
52
|
+
const logPath = writeResult('build', { output: result.output, error: result.error });
|
|
53
|
+
console.error(`Build failed. Log: ${logPath}`);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
console.error(`Error: ${result.error ?? 'Build failed'}`);
|
|
57
|
+
}
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
if (result.output) {
|
|
61
|
+
const logPath = writeResult('build', result);
|
|
62
|
+
console.log(`Build succeeded`);
|
|
63
|
+
console.log(`Log: ${logPath}`);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
console.log('Build succeeded');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
handleError(e);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
tools.command('publish')
|
|
74
|
+
.description('将构建产物上传到 COS,获取预览 URL')
|
|
75
|
+
.requiredOption('--project-id <id>', '项目数字 ID', parseInt)
|
|
76
|
+
.option('--branch <name>', '分支名', 'main')
|
|
77
|
+
.action(async (opts) => {
|
|
78
|
+
try {
|
|
79
|
+
const client = new AgentApiClient();
|
|
80
|
+
const result = await client.post('/publish-to-cos', {
|
|
81
|
+
projectId: opts.projectId,
|
|
82
|
+
branch: opts.branch,
|
|
83
|
+
});
|
|
84
|
+
if (result.success === false) {
|
|
85
|
+
console.error(`Error: ${result.error ?? 'Publish failed'}`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
if (result.previewUrl) {
|
|
89
|
+
console.log(`Published: ${result.previewUrl}`);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
console.log('Published successfully');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
handleError(e);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
tools.command('create-remix')
|
|
100
|
+
.description('基于项目创建 Remix 分支')
|
|
101
|
+
.requiredOption('--project-id <id>', '项目数字 ID')
|
|
102
|
+
.requiredOption('--name <name>', 'Remix 显示名称')
|
|
103
|
+
.option('--source-branch <name>', '源分支', 'main')
|
|
104
|
+
.option('--description <text>', '描述')
|
|
105
|
+
.action(async (opts) => {
|
|
106
|
+
try {
|
|
107
|
+
const client = new AgentApiClient();
|
|
108
|
+
const projectId = Number(opts.projectId);
|
|
109
|
+
if (!Number.isInteger(projectId) || projectId <= 0) {
|
|
110
|
+
console.error(`Error: --project-id must be a positive integer, got: ${opts.projectId}`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
const result = await client.post('/create-remix', {
|
|
114
|
+
projectId,
|
|
115
|
+
sourceBranch: opts.sourceBranch,
|
|
116
|
+
name: opts.name,
|
|
117
|
+
description: opts.description,
|
|
118
|
+
});
|
|
119
|
+
if (result.success === false) {
|
|
120
|
+
console.error(`Error: ${result.error ?? 'Create remix failed'}`);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
console.log(`Remix created: ${opts.name} (project ${result.projectId}, branch ${result.branchName})`);
|
|
124
|
+
if (result.editorUrl)
|
|
125
|
+
console.log(`Editor: ${result.editorUrl}`);
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
handleError(e);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
// ─── Queries ────────────────────────────────────────────────
|
|
132
|
+
tools.command('list-templates')
|
|
133
|
+
.description('列出可用项目模板')
|
|
134
|
+
.option('--tag <filter>', '按标签过滤')
|
|
135
|
+
.action(async (opts) => {
|
|
136
|
+
try {
|
|
137
|
+
const client = new AgentApiClient();
|
|
138
|
+
const params = {};
|
|
139
|
+
if (opts.tag)
|
|
140
|
+
params.tagFilter = opts.tag;
|
|
141
|
+
const result = await client.get('/list-templates', params);
|
|
142
|
+
const detailsPath = writeResult('list-templates', result);
|
|
143
|
+
if (opts.tag) {
|
|
144
|
+
console.log(`Found ${result.templateCount} templates matching "${opts.tag}" (${result.totalCount} total)`);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
console.log(`Found ${result.templateCount} templates`);
|
|
148
|
+
}
|
|
149
|
+
console.log(`Details: ${detailsPath}`);
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
handleError(e);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
tools.command('list-assets')
|
|
156
|
+
.description('列出项目资产')
|
|
157
|
+
.requiredOption('--project-id <id>', '项目数字 ID')
|
|
158
|
+
.option('--branch <name>', '分支名')
|
|
159
|
+
.option('--types <list>', '资产类型,逗号分隔(texture,script,audio,model...)')
|
|
160
|
+
.option('--search <keyword>', '名称搜索')
|
|
161
|
+
.option('--limit <n>', '最大数量', '30')
|
|
162
|
+
.option('--skip <n>', '偏移', '0')
|
|
163
|
+
.action(async (opts) => {
|
|
164
|
+
try {
|
|
165
|
+
const client = new AgentApiClient();
|
|
166
|
+
const params = { projectId: opts.projectId };
|
|
167
|
+
if (opts.branch)
|
|
168
|
+
params.branch = opts.branch;
|
|
169
|
+
if (opts.types)
|
|
170
|
+
params.types = opts.types;
|
|
171
|
+
if (opts.search)
|
|
172
|
+
params.search = opts.search;
|
|
173
|
+
if (opts.limit)
|
|
174
|
+
params.limit = opts.limit;
|
|
175
|
+
if (opts.skip)
|
|
176
|
+
params.skip = opts.skip;
|
|
177
|
+
const result = await client.get('/list-project-assets', params);
|
|
178
|
+
const detailsPath = writeResult('list-assets', result);
|
|
179
|
+
const typeSummary = Object.entries(result.byTypeCount)
|
|
180
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
181
|
+
.join(', ');
|
|
182
|
+
console.log(`Found ${result.total} assets (${typeSummary})`);
|
|
183
|
+
console.log(`Details: ${detailsPath}`);
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
handleError(e);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
tools.command('list-remixes')
|
|
190
|
+
.description('列出可访问的 Remix 项目')
|
|
191
|
+
.option('--scope <scope>', 'my 或 gallery', 'my')
|
|
192
|
+
.option('--search <keyword>', '搜索')
|
|
193
|
+
.option('--limit <n>', '最大数量', '20')
|
|
194
|
+
.option('--page <n>', '页码', '1')
|
|
195
|
+
.action(async (opts) => {
|
|
196
|
+
try {
|
|
197
|
+
const client = new AgentApiClient();
|
|
198
|
+
const params = {};
|
|
199
|
+
if (opts.scope)
|
|
200
|
+
params.scope = opts.scope;
|
|
201
|
+
if (opts.search)
|
|
202
|
+
params.search = opts.search;
|
|
203
|
+
if (opts.limit)
|
|
204
|
+
params.limit = opts.limit;
|
|
205
|
+
if (opts.page)
|
|
206
|
+
params.page = opts.page;
|
|
207
|
+
const result = await client.get('/list-remixes', params);
|
|
208
|
+
const detailsPath = writeResult('list-remixes', result);
|
|
209
|
+
console.log(`Found ${result.total} remixes (page ${result.page}/${result.totalPages})`);
|
|
210
|
+
console.log(`Details: ${detailsPath}`);
|
|
211
|
+
}
|
|
212
|
+
catch (e) {
|
|
213
|
+
handleError(e);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
// ─── Publish ────────────────────────────────────────────────
|
|
217
|
+
tools.command('publish-prefab')
|
|
218
|
+
.description('发布 Remixable Prefab 到资产库')
|
|
219
|
+
.requiredOption('--remix-project-id <id>', 'Remix 项目 ID', parseInt)
|
|
220
|
+
.requiredOption('--template <name>', '模板名')
|
|
221
|
+
.requiredOption('--variant <name>', '变体名')
|
|
222
|
+
.requiredOption('--prefab <name>', '预制件名')
|
|
223
|
+
.requiredOption('--staging-root <path>', 'Staging 目录路径')
|
|
224
|
+
.option('--visibility <v>', 'public 或 team', 'team')
|
|
225
|
+
.option('--team-id <id>', '团队 ID')
|
|
226
|
+
.action(async (opts) => {
|
|
227
|
+
try {
|
|
228
|
+
const client = new AgentApiClient();
|
|
229
|
+
const result = await client.post('/publish-prefab', {
|
|
230
|
+
remixProjectId: opts.remixProjectId,
|
|
231
|
+
templateName: opts.template,
|
|
232
|
+
variantName: opts.variant,
|
|
233
|
+
prefabName: opts.prefab,
|
|
234
|
+
stagingRoot: opts.stagingRoot,
|
|
235
|
+
visibility: opts.visibility,
|
|
236
|
+
teamId: opts.teamId,
|
|
237
|
+
});
|
|
238
|
+
if (result.success === false) {
|
|
239
|
+
console.error(`Error: ${result.error ?? 'Publish prefab failed'}`);
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
console.log(`Prefab published: ${result.bundlePath} (${result.uploaded} files)`);
|
|
243
|
+
}
|
|
244
|
+
catch (e) {
|
|
245
|
+
handleError(e);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
// ─── Builds ─────────────────────────────────────────────────
|
|
249
|
+
tools.command('list-builds')
|
|
250
|
+
.description('查看云端构建列表')
|
|
251
|
+
.option('--project-id <id>', '项目数字 ID(不填则查看当前用户所有项目)')
|
|
252
|
+
.option('--branch <name>', '按分支过滤')
|
|
253
|
+
.option('--status <status>', '按状态过滤 (queued|running|success|failed)')
|
|
254
|
+
.option('--platform <platform>', '按平台过滤 (facebook|snapchat|playcraft...)')
|
|
255
|
+
.option('--search <keyword>', '按名称/提交信息搜索')
|
|
256
|
+
.option('--limit <n>', '最大数量', '20')
|
|
257
|
+
.option('--skip <n>', '偏移', '0')
|
|
258
|
+
.action(async (opts) => {
|
|
259
|
+
try {
|
|
260
|
+
const client = new AgentApiClient();
|
|
261
|
+
const params = {};
|
|
262
|
+
if (opts.projectId)
|
|
263
|
+
params.projectId = opts.projectId;
|
|
264
|
+
if (opts.branch)
|
|
265
|
+
params.branchName = opts.branch;
|
|
266
|
+
if (opts.status)
|
|
267
|
+
params.status = opts.status;
|
|
268
|
+
if (opts.platform)
|
|
269
|
+
params.platform = opts.platform;
|
|
270
|
+
if (opts.search)
|
|
271
|
+
params.search = opts.search;
|
|
272
|
+
if (opts.limit)
|
|
273
|
+
params.limit = opts.limit;
|
|
274
|
+
if (opts.skip)
|
|
275
|
+
params.skip = opts.skip;
|
|
276
|
+
const result = await client.get('/list-builds', params);
|
|
277
|
+
const detailsPath = writeResult('list-builds', result);
|
|
278
|
+
const statusCounts = {};
|
|
279
|
+
for (const item of result.items) {
|
|
280
|
+
statusCounts[item.status] = (statusCounts[item.status] ?? 0) + 1;
|
|
281
|
+
}
|
|
282
|
+
const statusSummary = Object.entries(statusCounts).map(([s, n]) => `${s}: ${n}`).join(', ');
|
|
283
|
+
console.log(`Found ${result.total} builds (${statusSummary || 'none'})`);
|
|
284
|
+
console.log(`Details: ${detailsPath}`);
|
|
285
|
+
}
|
|
286
|
+
catch (e) {
|
|
287
|
+
handleError(e);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
tools.command('get-build')
|
|
291
|
+
.description('查看单次构建详情(状态、产物、校验结果等)')
|
|
292
|
+
.requiredOption('--id <buildId>', '构建 ID')
|
|
293
|
+
.action(async (opts) => {
|
|
294
|
+
try {
|
|
295
|
+
const client = new AgentApiClient();
|
|
296
|
+
const result = await client.get('/get-build', { id: opts.id });
|
|
297
|
+
if ('error' in result && result.error) {
|
|
298
|
+
console.error(`Error: ${result.error}`);
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
const detailsPath = writeResult('get-build', result);
|
|
302
|
+
const statusLine = result.status === 'running' && result.progress != null
|
|
303
|
+
? `${result.status} (${result.progress}%)`
|
|
304
|
+
: result.status;
|
|
305
|
+
const sizeMB = result.outputSize ? `${(result.outputSize / 1024 / 1024).toFixed(2)}MB` : 'unknown size';
|
|
306
|
+
const nameLabel = result.name ? ` "${result.name}"` : '';
|
|
307
|
+
console.log(`Build ${result.id}${nameLabel}: ${statusLine} [${result.platform}/${result.format}] ${sizeMB}`);
|
|
308
|
+
if (result.error)
|
|
309
|
+
console.log(`Error: ${result.error}`);
|
|
310
|
+
if (result.downloadUrl)
|
|
311
|
+
console.log(`Download: ${result.downloadUrl}`);
|
|
312
|
+
if (result.previewUrl)
|
|
313
|
+
console.log(`Preview: ${result.previewUrl}`);
|
|
314
|
+
if (result.validationPassed === false) {
|
|
315
|
+
const errors = (result.validationWarnings ?? []).filter((w) => w.severity === 'error');
|
|
316
|
+
console.log(`Validation: FAILED (${errors.length} error(s))`);
|
|
317
|
+
}
|
|
318
|
+
else if (result.validationPassed === true) {
|
|
319
|
+
console.log(`Validation: PASSED`);
|
|
320
|
+
}
|
|
321
|
+
console.log(`Details: ${detailsPath}`);
|
|
322
|
+
}
|
|
323
|
+
catch (e) {
|
|
324
|
+
handleError(e);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
tools.command('get-build-download')
|
|
328
|
+
.description('获取构建产物下载地址')
|
|
329
|
+
.requiredOption('--id <buildId>', '构建 ID')
|
|
330
|
+
.action(async (opts) => {
|
|
331
|
+
try {
|
|
332
|
+
const client = new AgentApiClient();
|
|
333
|
+
const result = await client.get('/get-build-download', { id: opts.id });
|
|
334
|
+
if (result.error) {
|
|
335
|
+
console.error(`Error: ${result.error}`);
|
|
336
|
+
process.exit(1);
|
|
337
|
+
}
|
|
338
|
+
console.log(`Download URL: ${result.downloadUrl}`);
|
|
339
|
+
if (result.previewUrl)
|
|
340
|
+
console.log(`Preview URL: ${result.previewUrl}`);
|
|
341
|
+
const sizeMB = result.size ? ` (${(result.size / 1024 / 1024).toFixed(2)}MB)` : '';
|
|
342
|
+
console.log(`Platform: ${result.platform}/${result.format}${sizeMB}`);
|
|
343
|
+
}
|
|
344
|
+
catch (e) {
|
|
345
|
+
handleError(e);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
// ─── Research & Fetch ───────────────────────────────────────
|
|
349
|
+
tools.command('research')
|
|
350
|
+
.description('使用 AI 搜索网络研究游戏设计、艺术风格、技术实现等主题(基于 Gemini + Google Search)')
|
|
351
|
+
.requiredOption('--query <query>', '研究问题或搜索关键词')
|
|
352
|
+
.option('--focus <focus>', '研究领域:gameplay | visual | audio | code | compliance')
|
|
353
|
+
.option('--json', '输出 JSON 格式(供 Agent 消费)')
|
|
354
|
+
.action(async (opts) => {
|
|
355
|
+
try {
|
|
356
|
+
const result = await researchCommand({ query: opts.query, focus: opts.focus });
|
|
357
|
+
if (opts.json) {
|
|
358
|
+
console.log(JSON.stringify(result, null, 2));
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
console.log('\n' + result.answer);
|
|
362
|
+
if (result.citations?.length) {
|
|
363
|
+
console.log('\n--- Sources ---');
|
|
364
|
+
for (const c of result.citations) {
|
|
365
|
+
console.log(`• ${c.title || c.url}`);
|
|
366
|
+
console.log(` ${c.url}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
catch (e) {
|
|
372
|
+
handleError(e);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
tools.command('fetch-url')
|
|
376
|
+
.description('让 AI 读取并理解指定 URL 的内容(官方文档、教程、GitHub 页面等)')
|
|
377
|
+
.requiredOption('--url <url>', '要读取的 URL')
|
|
378
|
+
.option('--query <query>', '对该 URL 内容的具体问题(可选)')
|
|
379
|
+
.option('--json', '输出 JSON 格式(供 Agent 消费)')
|
|
380
|
+
.action(async (opts) => {
|
|
381
|
+
try {
|
|
382
|
+
const result = await fetchUrlCommand({ url: opts.url, query: opts.query });
|
|
383
|
+
if (opts.json) {
|
|
384
|
+
console.log(JSON.stringify(result, null, 2));
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
console.log('\n' + result.answer);
|
|
388
|
+
if (result.citations?.length) {
|
|
389
|
+
console.log('\n--- Sources ---');
|
|
390
|
+
for (const c of result.citations) {
|
|
391
|
+
console.log(`• ${c.title || c.url}: ${c.url}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
catch (e) {
|
|
397
|
+
handleError(e);
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tools research / fetch-url 命令的核心逻辑
|
|
3
|
+
*
|
|
4
|
+
* 提取为独立函数,方便单元测试和 Commander 注册。
|
|
5
|
+
*/
|
|
6
|
+
import { AgentApiClient } from '../utils/agent-api-client.js';
|
|
7
|
+
/** 为测试注入自定义凭据的轻量 AgentApiClient 工厂 */
|
|
8
|
+
function makeClient(opts) {
|
|
9
|
+
if (opts.apiUrl || opts.token) {
|
|
10
|
+
if (opts.apiUrl)
|
|
11
|
+
process.env.PLAYCRAFT_API_URL = opts.apiUrl;
|
|
12
|
+
if (opts.token)
|
|
13
|
+
process.env.PLAYCRAFT_SANDBOX_TOKEN = opts.token;
|
|
14
|
+
}
|
|
15
|
+
return new AgentApiClient();
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 调用 POST /api/agent/tools/research,返回研究结果。
|
|
19
|
+
* 测试时通过 apiUrl / token 注入,生产时从环境变量 / .playcraft.json 读取。
|
|
20
|
+
*/
|
|
21
|
+
export async function researchCommand(opts) {
|
|
22
|
+
const client = makeClient({ apiUrl: opts.apiUrl, token: opts.token });
|
|
23
|
+
return client.post('/research', {
|
|
24
|
+
query: opts.query,
|
|
25
|
+
focus: opts.focus,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 调用 POST /api/agent/tools/fetch-url,返回 URL 内容摘要。
|
|
30
|
+
*/
|
|
31
|
+
export async function fetchUrlCommand(opts) {
|
|
32
|
+
const client = makeClient({ apiUrl: opts.apiUrl, token: opts.token });
|
|
33
|
+
return client.post('/fetch-url', {
|
|
34
|
+
url: opts.url,
|
|
35
|
+
query: opts.query,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* playcraft tools research / fetch-url 命令测试
|
|
3
|
+
*
|
|
4
|
+
* 覆盖:
|
|
5
|
+
* - 正确路径:research 返回 answer + citations
|
|
6
|
+
* - 正确路径:fetch-url 返回 answer + citations
|
|
7
|
+
* - focus 参数透传
|
|
8
|
+
* - query 参数透传
|
|
9
|
+
* - 后端返回 4xx / 5xx 时抛出错误
|
|
10
|
+
* - 鉴权头:sandbox JWT → X-Sandbox-Token;PAT → Authorization: Bearer
|
|
11
|
+
* - citation 为空时不崩溃
|
|
12
|
+
*/
|
|
13
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi, } from 'vitest';
|
|
14
|
+
import http from 'node:http';
|
|
15
|
+
import { researchCommand, fetchUrlCommand } from './tools-research.js';
|
|
16
|
+
// ─── Mock loadGlobalConfig ─────────────────────────────────────────────────────
|
|
17
|
+
vi.mock('../config.js', () => ({
|
|
18
|
+
loadGlobalConfig: vi.fn(() => ({})),
|
|
19
|
+
saveGlobalConfig: vi.fn(),
|
|
20
|
+
loadConfig: vi.fn(),
|
|
21
|
+
}));
|
|
22
|
+
let server;
|
|
23
|
+
let baseUrl;
|
|
24
|
+
let lastRequest = null;
|
|
25
|
+
let responseStatus = 200;
|
|
26
|
+
let responseBody = {
|
|
27
|
+
answer: 'Test answer from Gemini',
|
|
28
|
+
citations: [],
|
|
29
|
+
};
|
|
30
|
+
beforeAll(async () => {
|
|
31
|
+
server = http.createServer((req, res) => {
|
|
32
|
+
let body = '';
|
|
33
|
+
req.on('data', (chunk) => { body += chunk.toString(); });
|
|
34
|
+
req.on('end', () => {
|
|
35
|
+
lastRequest = {
|
|
36
|
+
url: req.url ?? '',
|
|
37
|
+
method: req.method ?? '',
|
|
38
|
+
headers: req.headers,
|
|
39
|
+
body,
|
|
40
|
+
};
|
|
41
|
+
res.writeHead(responseStatus, { 'Content-Type': 'application/json' });
|
|
42
|
+
res.end(JSON.stringify(responseBody));
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
await new Promise((resolve) => {
|
|
46
|
+
server.listen(0, '127.0.0.1', () => resolve());
|
|
47
|
+
});
|
|
48
|
+
const addr = server.address();
|
|
49
|
+
baseUrl = `http://127.0.0.1:${addr.port}`;
|
|
50
|
+
});
|
|
51
|
+
afterAll(async () => {
|
|
52
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
53
|
+
});
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
lastRequest = null;
|
|
56
|
+
responseStatus = 200;
|
|
57
|
+
responseBody = { answer: 'Test answer', citations: [] };
|
|
58
|
+
delete process.env.PLAYCRAFT_API_URL;
|
|
59
|
+
delete process.env.PLAYCRAFT_SANDBOX_TOKEN;
|
|
60
|
+
});
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
delete process.env.PLAYCRAFT_API_URL;
|
|
63
|
+
delete process.env.PLAYCRAFT_SANDBOX_TOKEN;
|
|
64
|
+
});
|
|
65
|
+
// ─── Helper ───────────────────────────────────────────────────────────────────
|
|
66
|
+
/** 解析测试服务器收到的请求 body */
|
|
67
|
+
function parseBody() {
|
|
68
|
+
return JSON.parse(lastRequest?.body ?? '{}');
|
|
69
|
+
}
|
|
70
|
+
// ─── researchCommand ──────────────────────────────────────────────────────────
|
|
71
|
+
describe('researchCommand', () => {
|
|
72
|
+
it('发送 POST 到 /api/agent/tools/research', async () => {
|
|
73
|
+
const result = await researchCommand({
|
|
74
|
+
query: 'match-3 level design',
|
|
75
|
+
apiUrl: baseUrl,
|
|
76
|
+
token: 'test-sandbox-token',
|
|
77
|
+
});
|
|
78
|
+
expect(lastRequest?.url).toBe('/api/agent/tools/research');
|
|
79
|
+
expect(lastRequest?.method).toBe('POST');
|
|
80
|
+
expect(result.answer).toBe('Test answer');
|
|
81
|
+
});
|
|
82
|
+
it('将 query 和 focus 正确写入请求 body', async () => {
|
|
83
|
+
await researchCommand({
|
|
84
|
+
query: 'pixel art color palette',
|
|
85
|
+
focus: 'visual',
|
|
86
|
+
apiUrl: baseUrl,
|
|
87
|
+
token: 'test-sandbox-token',
|
|
88
|
+
});
|
|
89
|
+
const body = parseBody();
|
|
90
|
+
expect(body.query).toBe('pixel art color palette');
|
|
91
|
+
expect(body.focus).toBe('visual');
|
|
92
|
+
});
|
|
93
|
+
it('不传 focus 时 body 中 focus 为 undefined(不发送该字段)', async () => {
|
|
94
|
+
await researchCommand({
|
|
95
|
+
query: 'game mechanics',
|
|
96
|
+
apiUrl: baseUrl,
|
|
97
|
+
token: 'test-sandbox-token',
|
|
98
|
+
});
|
|
99
|
+
const body = parseBody();
|
|
100
|
+
expect(body.query).toBe('game mechanics');
|
|
101
|
+
expect(body.focus).toBeUndefined();
|
|
102
|
+
});
|
|
103
|
+
it('所有 focus 枚举值均可正常透传', async () => {
|
|
104
|
+
const focuses = ['gameplay', 'visual', 'audio', 'code', 'compliance'];
|
|
105
|
+
for (const focus of focuses) {
|
|
106
|
+
await researchCommand({ query: 'test', focus, apiUrl: baseUrl, token: 'tok' });
|
|
107
|
+
expect(parseBody().focus).toBe(focus);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
it('返回包含 citations 的完整结果', async () => {
|
|
111
|
+
responseBody = {
|
|
112
|
+
answer: 'Answer with citations',
|
|
113
|
+
citations: [
|
|
114
|
+
{ url: 'https://example.com/1', title: 'Source 1' },
|
|
115
|
+
{ url: 'https://example.com/2', title: 'Source 2', excerpt: 'excerpt text' },
|
|
116
|
+
],
|
|
117
|
+
};
|
|
118
|
+
const result = await researchCommand({
|
|
119
|
+
query: 'test query',
|
|
120
|
+
apiUrl: baseUrl,
|
|
121
|
+
token: 'test-token',
|
|
122
|
+
});
|
|
123
|
+
expect(result.answer).toBe('Answer with citations');
|
|
124
|
+
expect(result.citations).toHaveLength(2);
|
|
125
|
+
expect(result.citations[0].url).toBe('https://example.com/1');
|
|
126
|
+
expect(result.citations[1].excerpt).toBe('excerpt text');
|
|
127
|
+
});
|
|
128
|
+
it('citations 为空数组时不崩溃', async () => {
|
|
129
|
+
responseBody = { answer: 'No citations answer', citations: [] };
|
|
130
|
+
const result = await researchCommand({
|
|
131
|
+
query: 'test',
|
|
132
|
+
apiUrl: baseUrl,
|
|
133
|
+
token: 'test-token',
|
|
134
|
+
});
|
|
135
|
+
expect(result.citations).toEqual([]);
|
|
136
|
+
});
|
|
137
|
+
it('使用沙箱 JWT 时发送 X-Sandbox-Token 头', async () => {
|
|
138
|
+
await researchCommand({ query: 'test', apiUrl: baseUrl, token: 'sandbox-jwt-eyJ...' });
|
|
139
|
+
expect(lastRequest?.headers['x-sandbox-token']).toBe('sandbox-jwt-eyJ...');
|
|
140
|
+
expect(lastRequest?.headers['authorization']).toBeUndefined();
|
|
141
|
+
});
|
|
142
|
+
it('使用 PAT 时发送 Authorization: Bearer 头', async () => {
|
|
143
|
+
await researchCommand({ query: 'test', apiUrl: baseUrl, token: 'playcraft_pat_abc123' });
|
|
144
|
+
expect(lastRequest?.headers['authorization']).toBe('Bearer playcraft_pat_abc123');
|
|
145
|
+
expect(lastRequest?.headers['x-sandbox-token']).toBeUndefined();
|
|
146
|
+
});
|
|
147
|
+
it('后端返回 400 时抛出错误', async () => {
|
|
148
|
+
responseStatus = 400;
|
|
149
|
+
responseBody = { error: 'query is required' };
|
|
150
|
+
await expect(researchCommand({ query: 'test', apiUrl: baseUrl, token: 'tok' })).rejects.toThrow(/400/);
|
|
151
|
+
});
|
|
152
|
+
it('后端返回 500 时抛出错误', async () => {
|
|
153
|
+
responseStatus = 500;
|
|
154
|
+
responseBody = { error: 'Internal server error' };
|
|
155
|
+
await expect(researchCommand({ query: 'test', apiUrl: baseUrl, token: 'tok' })).rejects.toThrow(/500/);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
// ─── fetchUrlCommand ──────────────────────────────────────────────────────────
|
|
159
|
+
describe('fetchUrlCommand', () => {
|
|
160
|
+
it('发送 POST 到 /api/agent/tools/fetch-url', async () => {
|
|
161
|
+
const result = await fetchUrlCommand({
|
|
162
|
+
url: 'https://phaser.io/docs/3.60.0',
|
|
163
|
+
apiUrl: baseUrl,
|
|
164
|
+
token: 'test-token',
|
|
165
|
+
});
|
|
166
|
+
expect(lastRequest?.url).toBe('/api/agent/tools/fetch-url');
|
|
167
|
+
expect(lastRequest?.method).toBe('POST');
|
|
168
|
+
expect(result.answer).toBe('Test answer');
|
|
169
|
+
});
|
|
170
|
+
it('将 url 和 query 正确写入请求 body', async () => {
|
|
171
|
+
await fetchUrlCommand({
|
|
172
|
+
url: 'https://phaser.io/docs/3.60.0',
|
|
173
|
+
query: 'how does TileMap collision work',
|
|
174
|
+
apiUrl: baseUrl,
|
|
175
|
+
token: 'test-token',
|
|
176
|
+
});
|
|
177
|
+
const body = parseBody();
|
|
178
|
+
expect(body.url).toBe('https://phaser.io/docs/3.60.0');
|
|
179
|
+
expect(body.query).toBe('how does TileMap collision work');
|
|
180
|
+
});
|
|
181
|
+
it('不传 query 时请求 body 中 query 为 undefined', async () => {
|
|
182
|
+
await fetchUrlCommand({
|
|
183
|
+
url: 'https://example.com',
|
|
184
|
+
apiUrl: baseUrl,
|
|
185
|
+
token: 'tok',
|
|
186
|
+
});
|
|
187
|
+
const body = parseBody();
|
|
188
|
+
expect(body.url).toBe('https://example.com');
|
|
189
|
+
expect(body.query).toBeUndefined();
|
|
190
|
+
});
|
|
191
|
+
it('返回包含 citations 的完整结果', async () => {
|
|
192
|
+
responseBody = {
|
|
193
|
+
answer: 'Content from URL',
|
|
194
|
+
citations: [{ url: 'https://phaser.io/docs', title: 'Phaser Docs' }],
|
|
195
|
+
};
|
|
196
|
+
const result = await fetchUrlCommand({
|
|
197
|
+
url: 'https://phaser.io/docs',
|
|
198
|
+
apiUrl: baseUrl,
|
|
199
|
+
token: 'tok',
|
|
200
|
+
});
|
|
201
|
+
expect(result.answer).toBe('Content from URL');
|
|
202
|
+
expect(result.citations[0].title).toBe('Phaser Docs');
|
|
203
|
+
});
|
|
204
|
+
it('使用 PAT 时发送 Authorization: Bearer 头', async () => {
|
|
205
|
+
await fetchUrlCommand({
|
|
206
|
+
url: 'https://example.com',
|
|
207
|
+
apiUrl: baseUrl,
|
|
208
|
+
token: 'playcraft_pat_xyz',
|
|
209
|
+
});
|
|
210
|
+
expect(lastRequest?.headers['authorization']).toBe('Bearer playcraft_pat_xyz');
|
|
211
|
+
});
|
|
212
|
+
it('后端返回 400 时抛出错误', async () => {
|
|
213
|
+
responseStatus = 400;
|
|
214
|
+
await expect(fetchUrlCommand({ url: 'https://example.com', apiUrl: baseUrl, token: 'tok' })).rejects.toThrow(/400/);
|
|
215
|
+
});
|
|
216
|
+
});
|