@playcraft/cli 0.1.56-beta → 0.1.58-beta
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/dist/assets/client.js +6 -4
- package/dist/commands/tools-3d.js +15 -0
- package/dist/commands/tools-generation.js +3 -2
- package/dist/commands/workspace.js +43 -0
- package/dist/utils/agent-api-client.js +31 -8
- package/dist/utils/agent-generation-provenance.js +45 -0
- package/dist/utils/generation-workspace-scope.js +50 -0
- package/dist/utils/image-generation-retry.js +7 -0
- package/dist/utils/tool-operation-journal.js +34 -4
- package/dist/workspace/messages.js +10 -0
- package/dist/workspace-runtime/batch/deliver.js +140 -80
- package/dist/workspace-runtime/batch/freeze.js +18 -6
- package/dist/workspace-runtime/batch/generate.js +68 -64
- package/dist/workspace-runtime/batch/prepare.js +109 -137
- package/dist/workspace-runtime/batch/records.js +4 -0
- package/dist/workspace-runtime/batch/routes.js +10 -2
- package/dist/workspace-runtime/batch/source-candidate.js +2 -0
- package/dist/workspace-runtime/batch/source-capture.js +1 -0
- package/dist/workspace-runtime/batch/source-failure.js +2 -0
- package/dist/workspace-runtime/batch/source-publish.js +3 -0
- package/dist/workspace-runtime/batch/writer-proof.js +37 -0
- package/dist/workspace-runtime/codex/app-server.js +1 -0
- package/dist/workspace-runtime/codex/config-toml.js +4 -0
- package/dist/workspace-runtime/codex/delivery-tools.js +7 -5
- package/dist/workspace-runtime/codex/generation-provenance-broker.js +80 -0
- package/dist/workspace-runtime/codex/launcher-client.js +18 -0
- package/dist/workspace-runtime/codex/model-broker.js +23 -4
- package/dist/workspace-runtime/persistence/journal.js +34 -0
- package/dist/workspace-runtime/persistence/sqlite-worker.js +68 -6
- package/dist/workspace-runtime/previews/output.js +2 -0
- package/dist/workspace-runtime/previews/publish.js +20 -5
- package/dist/workspace-runtime/previews/runtime-preview.js +4 -0
- package/dist/workspace-runtime/processes/managed-writes.js +47 -5
- package/dist/workspace-runtime/serve.js +19 -32
- package/dist/workspace-runtime/server/accept-operation.js +22 -2
- package/dist/workspace-runtime/server/directory-access.js +414 -0
- package/dist/workspace-runtime/server/dispatch.js +61 -23
- package/dist/workspace-runtime/server/endpoints.js +1 -0
- package/dist/workspace-runtime/server/http.js +52 -10
- package/dist/workspace-runtime/server/operation-record.js +2 -0
- package/dist/workspace-runtime/server/terminal-host.js +43 -8
- package/dist/workspace-runtime/server/workspace-transfer.js +3 -1
- package/dist/workspace-runtime/workspaces/append-only-log.js +3 -0
- package/dist/workspace-runtime/workspaces/artifact-events.js +5 -2
- package/dist/workspace-runtime/workspaces/artifact-files.js +14 -8
- package/dist/workspace-runtime/workspaces/artifact-receipts.js +7 -25
- package/dist/workspace-runtime/workspaces/artifacts.js +4 -1
- package/dist/workspace-runtime/workspaces/asset-library-import.js +9 -4
- package/dist/workspace-runtime/workspaces/asset-publication-access.js +7 -9
- package/dist/workspace-runtime/workspaces/asset-transfer-publish.js +3 -0
- package/dist/workspace-runtime/workspaces/asset-transfer-storage.js +17 -2
- package/dist/workspace-runtime/workspaces/attachments.js +9 -0
- package/dist/workspace-runtime/workspaces/batch-commit.js +5 -3
- package/dist/workspace-runtime/workspaces/binary-write.js +12 -7
- package/dist/workspace-runtime/workspaces/cli-context.js +2 -0
- package/dist/workspace-runtime/workspaces/cli-credentials.js +1 -0
- package/dist/workspace-runtime/workspaces/delivery-build.js +30 -11
- package/dist/workspace-runtime/workspaces/delivery-store.js +2 -0
- package/dist/workspace-runtime/workspaces/dependencies.js +105 -71
- package/dist/workspace-runtime/workspaces/developer-command.js +12 -1
- package/dist/workspace-runtime/workspaces/directory-access.js +13 -18
- package/dist/workspace-runtime/workspaces/directory-attachments.js +8 -0
- package/dist/workspace-runtime/workspaces/directory-create.js +18 -5
- package/dist/workspace-runtime/workspaces/directory-maintenance.js +46 -35
- package/dist/workspace-runtime/workspaces/directory-mutations.js +8 -3
- package/dist/workspace-runtime/workspaces/files.js +4 -0
- package/dist/workspace-runtime/workspaces/git-credential-broker.js +3 -0
- package/dist/workspace-runtime/workspaces/git-direct-store.js +2 -0
- package/dist/workspace-runtime/workspaces/json5-edit.js +36 -0
- package/dist/workspace-runtime/workspaces/library-attachments.js +4 -0
- package/dist/workspace-runtime/workspaces/parameter-agent-protocol.js +2 -0
- package/dist/workspace-runtime/workspaces/parameter-agent-store.js +2 -0
- package/dist/workspace-runtime/workspaces/parameter-agent.js +40 -11
- package/dist/workspace-runtime/workspaces/parameter-assets.js +1 -0
- package/dist/workspace-runtime/workspaces/parameter-initialization-skill-transaction.js +141 -0
- package/dist/workspace-runtime/workspaces/parameter-initialization-skill.js +329 -0
- package/dist/workspace-runtime/workspaces/parameter-initialization.js +63 -0
- package/dist/workspace-runtime/workspaces/parameter-production.js +124 -0
- package/dist/workspace-runtime/workspaces/parameters.js +95 -3
- package/dist/workspace-runtime/workspaces/platform-export.js +190 -0
- package/dist/workspace-runtime/workspaces/prepare.js +4 -0
- package/dist/workspace-runtime/workspaces/registry.js +11 -0
- package/dist/workspace-runtime/workspaces/remix-references.js +5 -0
- package/dist/workspace-runtime/workspaces/shared-write.js +187 -0
- package/dist/workspace-runtime/workspaces/skills.js +14 -15
- package/dist/workspace-runtime/workspaces/transfer.js +4 -1
- package/dist/workspace-runtime/workspaces/workspace-shares.js +235 -0
- package/dist/workspace-runtime/workspaces/worktree-attachments.js +2 -0
- package/dist/workspace-runtime/workspaces/worktree-checkout.js +11 -6
- package/package.json +5 -4
- package/project-template/.claude/skills/playcraft-runtime-parameters/SKILL.md +32 -0
- package/project-template/.claude/skills/playcraft-runtime-parameters/references/platform-protocol.md +21 -0
- package/project-template/.claude/skills/playcraft-runtime-parameters/references/preview-integration.md +57 -0
- package/project-template-v2/.playcraft/skills.lock.json +15 -8
- package/templates/preview-runtime/v1/README.md +4 -0
- package/templates/preview-runtime/v1/manifest.json +6 -6
- package/templates/preview-runtime/v1/runtime.mjs +368 -22
- package/templates/preview-runtime/v1/types/adapters.d.ts +4 -1
- package/templates/preview-runtime/v1/types/preview.d.ts +1 -0
- package/templates/preview-runtime/v1/types/protocol.d.ts +19 -0
- package/templates/preview-runtime/v1/types/runtime.d.ts +2 -1
package/dist/assets/client.js
CHANGED
|
@@ -120,7 +120,9 @@ export async function downloadAssetTask(client, scope, taskId, output, versionId
|
|
|
120
120
|
const staging = await mkdtemp(join(dirname(destination), '.playcraft-asset-'));
|
|
121
121
|
let createdDestination = false;
|
|
122
122
|
try {
|
|
123
|
-
const
|
|
123
|
+
const multiple = !bundle && !versionId && Boolean(task.outputs);
|
|
124
|
+
const directory = Boolean(bundle || multiple);
|
|
125
|
+
const files = bundle?.files ?? (multiple ? task.outputs.map(file => ({ ...file, path: file.name })) : [{ ...entry, path: 'output', fileId: undefined }]);
|
|
124
126
|
const paths = [];
|
|
125
127
|
for (const file of files) {
|
|
126
128
|
const temporary = resolve(staging, file.path);
|
|
@@ -128,11 +130,11 @@ export async function downloadAssetTask(client, scope, taskId, output, versionId
|
|
|
128
130
|
if (!inside || inside === '..' || inside.startsWith(`..${sep}`))
|
|
129
131
|
throw new Error(t('invalidContent'));
|
|
130
132
|
await mkdir(dirname(temporary), { recursive: true });
|
|
131
|
-
await client.download(assetContentRoute({ ...scope, taskId, ...(
|
|
133
|
+
await client.download(assetContentRoute({ ...scope, taskId, ...(directory ? { fileId: file.fileId } : {}), ...(versionId ? { versionId } : {}) }), temporary);
|
|
132
134
|
await verifyFile(temporary, file);
|
|
133
|
-
paths.push(
|
|
135
|
+
paths.push(directory ? join(destination, file.path) : destination);
|
|
134
136
|
}
|
|
135
|
-
if (
|
|
137
|
+
if (directory) {
|
|
136
138
|
// mkdir is exclusive; an existing directory or symbolic link cannot be replaced.
|
|
137
139
|
await mkdir(destination);
|
|
138
140
|
createdDestination = true;
|
|
@@ -46,6 +46,7 @@ export function register3DCommands(tools) {
|
|
|
46
46
|
.description('Generate a 3D model from a text prompt or input image (uses HunyuanTo3D or configured provider)')
|
|
47
47
|
.option('--prompt <text>', 'Text description of the 3D model to generate')
|
|
48
48
|
.option('--input <path>', 'Input image (local path or URL) for image-to-3D generation')
|
|
49
|
+
.option('--view-image <view=path>', 'Additional local reference for hy-3d-3.1: left|right|back|top|bottom|left_front|right_front; repeat for multiple views', (value, previous) => [...previous, value], [])
|
|
49
50
|
.requiredOption('--output <path>', 'Output model file path')
|
|
50
51
|
.option('--model <ref>', 'Model ref in provider/model-id format (e.g. tencent-cloud/hy-3d-3.1)')
|
|
51
52
|
.option('--format <fmt>', 'Output format: glb|obj|stl|fbx|usdz (default: glb)', 'glb')
|
|
@@ -77,6 +78,20 @@ export function register3DCommands(tools) {
|
|
|
77
78
|
payload.enablePBR = true;
|
|
78
79
|
if (opts.polygonType)
|
|
79
80
|
payload.polygonType = opts.polygonType;
|
|
81
|
+
if (opts.viewImage.length) {
|
|
82
|
+
const views = new Set(['left', 'right', 'back', 'top', 'bottom', 'left_front', 'right_front']);
|
|
83
|
+
const seen = new Set();
|
|
84
|
+
if (!opts.input || opts.viewImage.length > 7 || opts.model && opts.model !== 'tencent-cloud/hy-3d-3.1')
|
|
85
|
+
throw new Error('--view-image requires --input and hy-3d-3.1 (at most 7 unique views)');
|
|
86
|
+
payload.multiViewImages = opts.viewImage.map((value) => {
|
|
87
|
+
const separator = value.indexOf('='), view = value.slice(0, separator), imagePath = value.slice(separator + 1);
|
|
88
|
+
if (separator < 1 || !imagePath || !views.has(view) || seen.has(view))
|
|
89
|
+
throw new Error('--view-image expects a unique view=path, e.g. back=./back.png');
|
|
90
|
+
seen.add(view);
|
|
91
|
+
return { view: view,
|
|
92
|
+
imageBase64: readFileSync(imagePath).toString('base64'), mimeType: mimeTypeForImagePath(imagePath) };
|
|
93
|
+
});
|
|
94
|
+
}
|
|
80
95
|
if (opts.input) {
|
|
81
96
|
let inputImageBase64;
|
|
82
97
|
let inputImageMimeType = opts.inputMime;
|
|
@@ -6,6 +6,7 @@ import { captureGeneratedArtifact } from '../workspace-runtime/workspaces/artifa
|
|
|
6
6
|
import { resolveImageBackgroundRequirement, validateImageDeliveryOptions } from '@playcraft/common/prefab';
|
|
7
7
|
import { assertImageAssetOutputPaths, readImageAssetContext } from '../utils/image-asset-context.js';
|
|
8
8
|
import { AgentApiClient } from '../utils/agent-api-client.js';
|
|
9
|
+
import { isUnsafeImageRetryError } from '../utils/image-generation-retry.js';
|
|
9
10
|
import { MAX_REFERENCE_IMAGES, collectReferenceImagePaths, parseImageTargetFieldPath, collectReferenceImagePayloads, sniffImageExtension, extensionForImageMime, resolveImageOutputPath, handleError, raceWithTimeout, } from './tools-utils.js';
|
|
10
11
|
function isMasterCompositeStripRequest(opts) {
|
|
11
12
|
if (opts.aspectRatio === '45:16')
|
|
@@ -285,7 +286,7 @@ export function registerGenerationCommands(tools) {
|
|
|
285
286
|
lastError = e;
|
|
286
287
|
const msg = e instanceof Error ? e.message : String(e);
|
|
287
288
|
const isTimeout = msg.includes('timed out');
|
|
288
|
-
const cannotRetry =
|
|
289
|
+
const cannotRetry = isUnsafeImageRetryError(e);
|
|
289
290
|
if (isTimeout || cannotRetry) {
|
|
290
291
|
console.error(`Attempt ${attempt + 1} stopped: ${msg}`);
|
|
291
292
|
break;
|
|
@@ -433,7 +434,7 @@ export function registerGenerationCommands(tools) {
|
|
|
433
434
|
tools.command('generate-sfx')
|
|
434
435
|
.description('AI 生成音效(SFX),使用 ElevenLabs Sound Effects')
|
|
435
436
|
.requiredOption('--prompt <text>', '音效描述,仅英文,如 "crisp UI click, short tail, no voice"')
|
|
436
|
-
.option('--duration <seconds>', '时长(秒,0.5-
|
|
437
|
+
.option('--duration <seconds>', '时长(秒,0.5-22;省略时由供应商自动决定)', parseFloat)
|
|
437
438
|
.option('--loop', '生成可循环的音效', false)
|
|
438
439
|
.requiredOption('--output <path>', '保存路径,如 ./assets/audio/click.mp3')
|
|
439
440
|
.action(async (opts) => {
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { stat } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { diagnoseWorkspaceShares, WORKSPACE_SHARED_PATHS } from '../workspace-runtime/workspaces/workspace-shares.js';
|
|
1
4
|
import { workspaceClone, workspacePush } from '../workspace/transfer-client.js';
|
|
2
5
|
import { workspaceMessage as message } from '../workspace/messages.js';
|
|
3
6
|
import { normalizeWorkspaceTransferError, WorkspaceCliTransferError } from '../workspace/transfer-errors.js';
|
|
@@ -22,7 +25,47 @@ export function registerWorkspaceCommands(program) {
|
|
|
22
25
|
process.exitCode = 1;
|
|
23
26
|
}
|
|
24
27
|
};
|
|
28
|
+
const doctor = async (options) => {
|
|
29
|
+
const root = resolve(options.dir ?? process.cwd());
|
|
30
|
+
const info = await stat(root).catch(() => undefined);
|
|
31
|
+
if (!info?.isDirectory()) {
|
|
32
|
+
if (options.json)
|
|
33
|
+
console.log(JSON.stringify({ ok: false, error: 'workspace_root_missing', root }));
|
|
34
|
+
else
|
|
35
|
+
console.error(message('doctorRootMissing'));
|
|
36
|
+
process.exitCode = 1;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const drift = await diagnoseWorkspaceShares(root);
|
|
40
|
+
if (options.json) {
|
|
41
|
+
console.log(JSON.stringify({
|
|
42
|
+
ok: drift.length === 0,
|
|
43
|
+
root,
|
|
44
|
+
shared: [...WORKSPACE_SHARED_PATHS],
|
|
45
|
+
drift: drift.map((entry) => ({
|
|
46
|
+
path: entry.path.slice(root.length + 1),
|
|
47
|
+
missing: entry.missing,
|
|
48
|
+
owner: entry.owner,
|
|
49
|
+
})),
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
else if (drift.length === 0) {
|
|
53
|
+
console.log(message('doctorHealthy'));
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
for (const entry of drift) {
|
|
57
|
+
console.log(`${entry.missing}\t${entry.owner}\t${entry.path.slice(root.length + 1)}`);
|
|
58
|
+
}
|
|
59
|
+
console.log(message('doctorSummary', { count: drift.length }));
|
|
60
|
+
console.log(message('doctorHint'));
|
|
61
|
+
}
|
|
62
|
+
process.exitCode = drift.length === 0 ? 0 : 1;
|
|
63
|
+
};
|
|
25
64
|
const workspace = program.command('workspace').description(message('description'));
|
|
65
|
+
workspace.command('doctor').description(message('doctor'))
|
|
66
|
+
.option('--dir <path>', message('doctorTarget'), process.cwd())
|
|
67
|
+
.option('--json', message('json'))
|
|
68
|
+
.action(doctor);
|
|
26
69
|
workspace.command('clone').description(message('clone'))
|
|
27
70
|
.requiredOption('--workspace-id <id>', message('workspaceId'))
|
|
28
71
|
.option('--directory <name>', message('directory'), 'basic')
|
|
@@ -9,8 +9,10 @@ import { homedir } from 'os';
|
|
|
9
9
|
import { loadGlobalConfig } from '../config.js';
|
|
10
10
|
import { DEFAULT_PLAYCRAFT_CLOUD_BASE } from '../commands/login.js';
|
|
11
11
|
import { AGENT_TOOL_OPERATION_PROTOCOL, AGENT_TOOL_OPERATION_HEADERS, AGENT_TOOL_OPERATION_CAPABILITIES_PATH, } from '@playcraft/common/agent-tools';
|
|
12
|
-
import { beginToolOperation } from './tool-operation-journal.js';
|
|
12
|
+
import { beginToolOperation, findToolOperation } from './tool-operation-journal.js';
|
|
13
13
|
import { businessEndpoint } from './environment-endpoints.js';
|
|
14
|
+
import { cloudRuntimeCredentialHint, isWorkspaceGenerationPath, withGenerationWorkspaceScope } from './generation-workspace-scope.js';
|
|
15
|
+
import { agentGenerationProvenanceHeaders } from './agent-generation-provenance.js';
|
|
14
16
|
import { redactDiagnosticText } from './error-diagnostics.js';
|
|
15
17
|
import { AccountActivationError, parseErrorPayload, pendingAccountActivationError, } from '../auth/account-activation.js';
|
|
16
18
|
/**
|
|
@@ -37,6 +39,7 @@ export class AgentApiClient {
|
|
|
37
39
|
token;
|
|
38
40
|
credentialFile;
|
|
39
41
|
playcraftEnv;
|
|
42
|
+
cloudRuntimeCredential;
|
|
40
43
|
constructor() {
|
|
41
44
|
const localConfig = AgentApiClient.loadLocalConfig();
|
|
42
45
|
const globalConfig = loadGlobalConfig();
|
|
@@ -59,6 +62,7 @@ export class AgentApiClient {
|
|
|
59
62
|
this.token = token;
|
|
60
63
|
this.credentialFile = localConfig.credentialFile;
|
|
61
64
|
this.playcraftEnv = (process.env.PLAYCRAFT_ENV || localConfig.env || '').trim();
|
|
65
|
+
this.cloudRuntimeCredential = cloudRuntimeCredentialHint(token, token === localConfig.token ? localConfig : undefined);
|
|
62
66
|
}
|
|
63
67
|
/** 根据 token 类型构造鉴权 headers */
|
|
64
68
|
authHeaders() {
|
|
@@ -85,12 +89,26 @@ export class AgentApiClient {
|
|
|
85
89
|
}
|
|
86
90
|
async post(path, body, options) {
|
|
87
91
|
const url = this.toolsRequestUrl(path);
|
|
88
|
-
const
|
|
89
|
-
if (
|
|
92
|
+
const originalBody = JSON.stringify(body);
|
|
93
|
+
if (originalBody === undefined)
|
|
90
94
|
throw new Error('Tool request body is required');
|
|
91
95
|
const recoverId = options?.requestId ?? process.env.PLAYCRAFT_TOOL_REQUEST_ID;
|
|
96
|
+
const newRequest = options?.newRequest ?? process.env.PLAYCRAFT_TOOL_NEW_REQUEST === '1';
|
|
97
|
+
const originalReceipt = recoverId !== undefined && !newRequest && isWorkspaceGenerationPath(path)
|
|
98
|
+
? findToolOperation(url.pathname, originalBody, recoverId) : undefined;
|
|
99
|
+
let serializedBody = originalReceipt ? originalBody : JSON.stringify(await withGenerationWorkspaceScope(path, body, this.cloudRuntimeCredential));
|
|
100
|
+
if (serializedBody === undefined)
|
|
101
|
+
throw new Error('Tool request body is required');
|
|
102
|
+
if (serializedBody !== originalBody && !newRequest) {
|
|
103
|
+
const previous = findToolOperation(url.pathname, originalBody, recoverId);
|
|
104
|
+
if (previous && recoverId !== undefined)
|
|
105
|
+
serializedBody = originalBody;
|
|
106
|
+
else if (previous)
|
|
107
|
+
throw new Error(`Previous unscoped tool request ${previous.requestId} is still unconfirmed. ` +
|
|
108
|
+
`Rerun this command with PLAYCRAFT_TOOL_REQUEST_ID=${previous.requestId} to read its settled receipt before starting a workspace generation. No generation was sent.`);
|
|
109
|
+
}
|
|
92
110
|
const operation = beginToolOperation(url.pathname, serializedBody, recoverId, {
|
|
93
|
-
newRequest
|
|
111
|
+
newRequest,
|
|
94
112
|
});
|
|
95
113
|
const unknownOutcome = `Tool request ${operation.requestId}: the outcome is unknown. Rerun the same command to ` +
|
|
96
114
|
`continue this request: it returns the result if the request finished, or finishes it if ` +
|
|
@@ -135,6 +153,7 @@ export class AgentApiClient {
|
|
|
135
153
|
headers: {
|
|
136
154
|
'Content-Type': 'application/json',
|
|
137
155
|
...this.authHeaders(),
|
|
156
|
+
...await agentGenerationProvenanceHeaders(path, serializedBody, operation.requestId, this.cloudRuntimeCredential, options?.signal),
|
|
138
157
|
[AGENT_TOOL_OPERATION_HEADERS.protocol]: AGENT_TOOL_OPERATION_PROTOCOL,
|
|
139
158
|
[AGENT_TOOL_OPERATION_HEADERS.requestId]: operation.requestId,
|
|
140
159
|
...(operation.settledOnly ? { [AGENT_TOOL_OPERATION_HEADERS.settledOnly]: 'true' } : {}),
|
|
@@ -163,11 +182,15 @@ export class AgentApiClient {
|
|
|
163
182
|
const pending = pendingAccountActivationError(res.status, parseErrorPayload(text));
|
|
164
183
|
if (pending)
|
|
165
184
|
throw pending;
|
|
185
|
+
if (isWorkspaceGenerationPath(path) && res.headers.get('x-playcraft-generation-outcome') === 'failed') {
|
|
186
|
+
operation.reject();
|
|
187
|
+
throw new ToolRequestError(`API ${res.status}: ${text}`);
|
|
188
|
+
}
|
|
166
189
|
const code = errorCode(text);
|
|
167
|
-
if (res.status === 409 && code === 'business_operation_in_progress')
|
|
190
|
+
if (res.status === 409 && (code === 'business_operation_in_progress' || code === 'asset_generation_in_progress'))
|
|
168
191
|
throw new ToolRequestError(`API 409: ${text}\nTool request ${operation.requestId} is still running. Rerun the ` +
|
|
169
192
|
'same command later to get its result.');
|
|
170
|
-
if (res.status === 409 && code === 'business_operation_requires_reconciliation')
|
|
193
|
+
if (res.status === 409 && (code === 'business_operation_requires_reconciliation' || code === 'asset_generation_outcome_unknown'))
|
|
171
194
|
throw new ToolRequestError(recoverId !== undefined
|
|
172
195
|
? // A receipt query never runs the request, even for a tool that can be safely redone.
|
|
173
196
|
`API 409: ${text}\nTool request ${operation.requestId} has no settled result yet. ` +
|
|
@@ -331,8 +354,8 @@ function responseErrorReason(text) {
|
|
|
331
354
|
}
|
|
332
355
|
function errorCode(text) {
|
|
333
356
|
try {
|
|
334
|
-
const
|
|
335
|
-
return typeof message === 'string' ? message : undefined;
|
|
357
|
+
const payload = JSON.parse(text);
|
|
358
|
+
return typeof payload.code === 'string' ? payload.code : typeof payload.message === 'string' ? payload.message : undefined;
|
|
336
359
|
}
|
|
337
360
|
catch {
|
|
338
361
|
return undefined;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { hashOperationPayload, AGENT_GENERATION_PROVENANCE_HEADER } from '@playcraft/common/cloud-workspace/runtime';
|
|
2
|
+
import { isWorkspaceGenerationPath } from './generation-workspace-scope.js';
|
|
3
|
+
/** Attribution is optional; only the controlled local broker can attest an execution. */
|
|
4
|
+
export async function agentGenerationProvenanceHeaders(path, body, requestId, cloudRuntime, signal) {
|
|
5
|
+
if (!cloudRuntime)
|
|
6
|
+
return {};
|
|
7
|
+
const normalized = path.startsWith('/') ? path : `/${path}`;
|
|
8
|
+
let payload;
|
|
9
|
+
try {
|
|
10
|
+
payload = JSON.parse(body);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
const assetCreate = normalized === '/workspace-assets' && payload?.action === 'create';
|
|
16
|
+
if (!isWorkspaceGenerationPath(normalized) && !assetCreate)
|
|
17
|
+
return {};
|
|
18
|
+
const endpoint = process.env.PLAYCRAFT_AGENT_GENERATION_ENDPOINT;
|
|
19
|
+
const capability = process.env.PLAYCRAFT_AGENT_GENERATION_CAPABILITY;
|
|
20
|
+
if (!endpoint || !capability || capability.length > 128 || endpoint.length > 1024)
|
|
21
|
+
return {};
|
|
22
|
+
try {
|
|
23
|
+
const url = new URL(endpoint);
|
|
24
|
+
if (url.protocol !== 'http:' || url.hostname !== '127.0.0.1' || !url.port || url.username || url.password
|
|
25
|
+
|| url.search || url.hash || !/\/[0-9a-f-]{36}\/[0-9a-f]{32}\/generation-provenance$/.test(url.pathname))
|
|
26
|
+
return {};
|
|
27
|
+
const response = await fetch(url, {
|
|
28
|
+
method: 'POST', redirect: 'error',
|
|
29
|
+
signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(15_000)]) : AbortSignal.timeout(15_000),
|
|
30
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${capability}` },
|
|
31
|
+
body: JSON.stringify({ toolPath: `/agent/tools${normalized}`,
|
|
32
|
+
toolRequestId: assetCreate ? payload.clientRequestId : requestId, payloadHash: hashOperationPayload(payload) }),
|
|
33
|
+
});
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
await response.body?.cancel();
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
const value = await response.json();
|
|
39
|
+
return typeof value.proof === 'string' && value.proof.length > 0 && value.proof.length <= 8192
|
|
40
|
+
? { [AGENT_GENERATION_PROVENANCE_HEADER]: value.proof } : {};
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { basename, dirname } from 'node:path';
|
|
2
|
+
import { workspaceDirectoryRegistrationSchema } from '@playcraft/common/cloud-workspace/runtime';
|
|
3
|
+
import { generatedWorkspace } from '../workspace-runtime/workspaces/artifact-receipts.js';
|
|
4
|
+
const generationPaths = new Set([
|
|
5
|
+
'/generate-image', '/generate-sfx', '/generate-bgm', '/generate-3d',
|
|
6
|
+
'/generate-texture', '/generate-motion', '/generate-avatar',
|
|
7
|
+
]);
|
|
8
|
+
const workspaceScopeSchema = workspaceDirectoryRegistrationSchema.pick({ workspaceId: true, directoryName: true });
|
|
9
|
+
export function isWorkspaceGenerationPath(path) {
|
|
10
|
+
return generationPaths.has(path.startsWith('/') ? path : `/${path}`);
|
|
11
|
+
}
|
|
12
|
+
/** Credential metadata only selects routing; the business service still authenticates and authorizes it. */
|
|
13
|
+
export function cloudRuntimeCredentialHint(token, metadata) {
|
|
14
|
+
if (token.startsWith('playcraft_pat_'))
|
|
15
|
+
return false;
|
|
16
|
+
if (metadata?.kind === 'cloud-runtime-cli' || typeof metadata?.runtimeId === 'string' && metadata.runtimeId.trim())
|
|
17
|
+
return true;
|
|
18
|
+
const parts = token.split('.');
|
|
19
|
+
if (parts.length !== 3 || parts[1].length > 16 * 1024)
|
|
20
|
+
return false;
|
|
21
|
+
try {
|
|
22
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
|
|
23
|
+
return Boolean(payload && typeof payload === 'object' && 'kind' in payload && payload.kind === 'cloud-runtime-cli');
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Add the current engineering checkout before the durable operation hashes its request. */
|
|
30
|
+
export async function withGenerationWorkspaceScope(path, body, requiresWorkspace, cwd = process.cwd()) {
|
|
31
|
+
if (!isWorkspaceGenerationPath(path))
|
|
32
|
+
return body;
|
|
33
|
+
const context = await generatedWorkspace(cwd);
|
|
34
|
+
if (!context) {
|
|
35
|
+
if (requiresWorkspace)
|
|
36
|
+
throw new Error('Cloud workspace generation requires a current engineering directory; run this command from basic or a remix directory. No generation was sent.');
|
|
37
|
+
return body;
|
|
38
|
+
}
|
|
39
|
+
const parsed = workspaceScopeSchema.safeParse({ workspaceId: context.workspaceId, directoryName: basename(context.root) });
|
|
40
|
+
if (!parsed.success || basename(dirname(context.root)) !== context.workspaceId)
|
|
41
|
+
throw new Error('Cloud workspace generation requires a valid basic or remix checkout under its workspace. No generation was sent.');
|
|
42
|
+
if (!body || typeof body !== 'object' || Array.isArray(body))
|
|
43
|
+
throw new Error('Tool generation request must be an object');
|
|
44
|
+
if ('workspace' in body && body.workspace !== undefined) {
|
|
45
|
+
const explicit = workspaceScopeSchema.safeParse(body.workspace);
|
|
46
|
+
if (!explicit.success || explicit.data.workspaceId !== parsed.data.workspaceId || explicit.data.directoryName !== parsed.data.directoryName)
|
|
47
|
+
throw new Error('Tool generation workspace does not match the current engineering directory. No generation was sent.');
|
|
48
|
+
}
|
|
49
|
+
return { ...body, workspace: parsed.data };
|
|
50
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** An uncertain generation may still be billed; retrying must never create another operation. */
|
|
2
|
+
export function isUnsafeImageRetryError(error) {
|
|
3
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4
|
+
return /source_transparency_invalid|output_transparency_invalid|transparency_unsupported|"retryable"\s*:\s*false/.test(message)
|
|
5
|
+
|| /business_operation_(?:in_progress|requires_reconciliation)|asset_generation_(?:in_progress|outcome_unknown)/.test(message)
|
|
6
|
+
|| /outcome is unknown|outcome remains unconfirmed|still unconfirmed|needs reconciliation|no settled result|settled result is not available/i.test(message);
|
|
7
|
+
}
|
|
@@ -97,9 +97,40 @@ export function beginToolOperation(path, serializedBody, recoverId, options = {}
|
|
|
97
97
|
release: () => inFlight.delete(requestId),
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
|
-
|
|
100
|
+
/** Read an existing identity without creating an intent or changing its receipt state. */
|
|
101
|
+
export function findToolOperation(path, serializedBody, requestId) {
|
|
102
|
+
const directory = process.env.PLAYCRAFT_TOOL_REQUEST_DIR ?? join(homedir(), '.playcraft', 'tool-requests');
|
|
103
|
+
if (!isAbsolute(directory))
|
|
104
|
+
throw new Error('PLAYCRAFT_TOOL_REQUEST_DIR must be an absolute path');
|
|
105
|
+
const inputHash = createHash('sha256').update(serializedBody).digest('hex');
|
|
106
|
+
if (requestId === undefined)
|
|
107
|
+
return findUnconfirmed(directory, path, inputHash, true);
|
|
108
|
+
if (!isAgentToolRequestId(requestId))
|
|
109
|
+
return undefined;
|
|
110
|
+
try {
|
|
111
|
+
const entry = JSON.parse(readFileSync(join(directory, `${requestId.toLowerCase()}.json`), 'utf8'));
|
|
112
|
+
if (entry.version === 1 && entry.requestId === requestId.toLowerCase() && entry.path === path && entry.inputHash === inputHash
|
|
113
|
+
&& ['unconfirmed', 'completed', 'rejected'].includes(entry.state))
|
|
114
|
+
return entry;
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (error.code !== 'ENOENT')
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
function findUnconfirmed(directory, path, inputHash, includeActive = false) {
|
|
101
123
|
let latest;
|
|
102
|
-
|
|
124
|
+
let files;
|
|
125
|
+
try {
|
|
126
|
+
files = readdirSync(directory);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
if (error.code === 'ENOENT')
|
|
130
|
+
return undefined;
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
for (const name of files) {
|
|
103
134
|
if (!name.endsWith('.json'))
|
|
104
135
|
continue;
|
|
105
136
|
try {
|
|
@@ -112,8 +143,7 @@ function findUnconfirmed(directory, path, inputHash) {
|
|
|
112
143
|
(!latest || entry.updatedAt > latest.updatedAt) &&
|
|
113
144
|
// Still being sent, here or by another live process: that run is not interrupted.
|
|
114
145
|
// Checked last, so only this command's candidates cost a process probe.
|
|
115
|
-
!inFlight.has(entry.requestId) &&
|
|
116
|
-
!holderAlive(entry.holder))
|
|
146
|
+
(includeActive || !inFlight.has(entry.requestId) && !holderAlive(entry.holder)))
|
|
117
147
|
latest = entry;
|
|
118
148
|
}
|
|
119
149
|
catch {
|
|
@@ -31,6 +31,11 @@ const messages = {
|
|
|
31
31
|
preflight: 'Push preflight passed: {files} files, {bytes} of content. Preparing incremental transfer.',
|
|
32
32
|
upload: 'Uploading incremental transfer: {bytes}.',
|
|
33
33
|
login: 'Log in to the target environment using playcraft login --url',
|
|
34
|
+
doctor: 'List shared workspace paths that lost development-group access',
|
|
35
|
+
doctorTarget: 'Workspace checkout to inspect', doctorRootMissing: 'The target directory does not exist.',
|
|
36
|
+
doctorHealthy: 'All shared workspace paths are readable and writable by the development group.',
|
|
37
|
+
doctorSummary: '{count} shared path(s) need repair.',
|
|
38
|
+
doctorHint: 'Reconnect the workspace to repair entries owned by this user; entries owned by the other identity are repaired when that identity writes again.',
|
|
34
39
|
},
|
|
35
40
|
zh: {
|
|
36
41
|
description: '下载云工作区,并将本地提交推送回来', clone: '克隆当前工作区文件,包含尚未提交的修改',
|
|
@@ -59,6 +64,11 @@ const messages = {
|
|
|
59
64
|
preflight: '推送预检查通过:{files} 个文件,内容共 {bytes}。正在准备增量传输。',
|
|
60
65
|
upload: '正在上传增量传输包:{bytes}。',
|
|
61
66
|
login: '请使用 playcraft login --url 登录目标环境',
|
|
67
|
+
doctor: '列出失去开发组访问权限的共享工作区路径',
|
|
68
|
+
doctorTarget: '要检查的工作区目录', doctorRootMissing: '目标目录不存在。',
|
|
69
|
+
doctorHealthy: '所有共享工作区路径都能被开发组读写。',
|
|
70
|
+
doctorSummary: '有 {count} 个共享路径需要修复。',
|
|
71
|
+
doctorHint: '重新连接工作区可修复属于当前用户的条目;属于另一个身份的条目会在该身份下次写入时修复。',
|
|
62
72
|
},
|
|
63
73
|
};
|
|
64
74
|
export function workspaceMessage(key, values = {}) {
|