@playcraft/cli 0.1.85-beta → 0.1.86
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/commands/tools-request.js +24 -0
- package/dist/commands/tools.js +2 -0
- package/dist/utils/agent-api-client.js +57 -18
- package/dist/utils/tool-operation-journal.js +3 -3
- package/dist/workspace-runtime/codex/model-broker.js +82 -269
- package/package.json +3 -4
- package/templates/preview-runtime/v1/manifest.json +1 -1
- package/templates/preview-runtime/v1/runtime.mjs +1 -1
- package/dist/workspace-runtime/codex/capture-isolation.js +0 -10
- package/dist/workspace-runtime/codex/model-capture.js +0 -675
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { AgentApiClient } from '../utils/agent-api-client.js';
|
|
4
|
+
export async function toolRequestCommand(operation, serializedBody) {
|
|
5
|
+
if (!['generate-image', 'generate-bgm', 'generate-sfx'].includes(operation))
|
|
6
|
+
throw new Error('Unsupported generation operation');
|
|
7
|
+
const body = JSON.parse(serializedBody);
|
|
8
|
+
if (!body || typeof body !== 'object' || Array.isArray(body))
|
|
9
|
+
throw new Error('Generation request must be a JSON object');
|
|
10
|
+
return new AgentApiClient({
|
|
11
|
+
configFile: process.env.PLAYCRAFT_SANDBOX_CONFIG_PATH || join(process.cwd(), '.playcraft.json'),
|
|
12
|
+
operationNamespace: 'opencode',
|
|
13
|
+
}).post(`/${operation}`, body);
|
|
14
|
+
}
|
|
15
|
+
/** Static sandbox plugins cannot import command-line modules; use this client for transport. */
|
|
16
|
+
export function registerToolRequestCommand(tools) {
|
|
17
|
+
tools
|
|
18
|
+
.command('request <operation>', { hidden: true })
|
|
19
|
+
.description('Send a generation request from JSON on stdin and print its durable result as JSON')
|
|
20
|
+
.action(async (operation) => {
|
|
21
|
+
const result = await toolRequestCommand(operation, readFileSync(0, 'utf8'));
|
|
22
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
23
|
+
});
|
|
24
|
+
}
|
package/dist/commands/tools.js
CHANGED
|
@@ -6,11 +6,13 @@ import { registerDetectCommand } from './tools-detect.js';
|
|
|
6
6
|
import { registerVideoReviewCommand } from './tools-video-review.js';
|
|
7
7
|
import { registerImageContextCommand } from './tools-image-context.js';
|
|
8
8
|
import { registerContextCommands } from './tools-context.js';
|
|
9
|
+
import { registerToolRequestCommand } from './tools-request.js';
|
|
9
10
|
export function registerToolsCommands(program) {
|
|
10
11
|
const tools = program
|
|
11
12
|
.command('tools')
|
|
12
13
|
.description('后端 /api/agent/tools;运行 playcraft login 后即可直接使用,也支持 .playcraft.json 或 PLAYCRAFT_SANDBOX_TOKEN 环境变量');
|
|
13
14
|
registerGenerationCommands(tools);
|
|
15
|
+
registerToolRequestCommand(tools);
|
|
14
16
|
registerImageContextCommand(tools);
|
|
15
17
|
registerProjectCommands(tools);
|
|
16
18
|
registerBatchCommands(tools);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from 'fs';
|
|
2
2
|
import { createWriteStream } from 'node:fs';
|
|
3
3
|
import { link, mkdir, stat, unlink } from 'node:fs/promises';
|
|
4
|
-
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
5
5
|
import { Readable } from 'node:stream';
|
|
6
6
|
import { pipeline } from 'node:stream/promises';
|
|
7
|
-
import { dirname, join } from 'path';
|
|
7
|
+
import { dirname, join, resolve } from 'path';
|
|
8
8
|
import { homedir } from 'os';
|
|
9
9
|
import { loadGlobalConfig } from '../config.js';
|
|
10
10
|
import { DEFAULT_PLAYCRAFT_CLOUD_BASE } from '../commands/login.js';
|
|
@@ -41,20 +41,28 @@ export class AgentApiClient {
|
|
|
41
41
|
credentialFile;
|
|
42
42
|
playcraftEnv;
|
|
43
43
|
cloudRuntimeCredential;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
explicitConfig;
|
|
45
|
+
operationDirectory;
|
|
46
|
+
constructor(options = {}) {
|
|
47
|
+
const localConfig = options.configFile
|
|
48
|
+
? AgentApiClient.requireLocalConfig(options.configFile)
|
|
49
|
+
: AgentApiClient.loadLocalConfig();
|
|
50
|
+
const globalConfig = options.configFile ? {} : loadGlobalConfig();
|
|
47
51
|
// URL: 环境变量 → 本地 .playcraft.json → 全局 login 配置 → 公有云默认地址
|
|
48
|
-
const apiUrl = (
|
|
49
|
-
localConfig.apiUrl
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
const apiUrl = (options.configFile
|
|
53
|
+
? localConfig.apiUrl
|
|
54
|
+
: process.env.PLAYCRAFT_API_URL ||
|
|
55
|
+
localConfig.apiUrl ||
|
|
56
|
+
globalConfig.backendUrl ||
|
|
57
|
+
globalConfig.url ||
|
|
58
|
+
DEFAULT_PLAYCRAFT_CLOUD_BASE).replace(/\/+$/, '');
|
|
53
59
|
// Token: 环境变量 → 沙箱全局配置 → 项目配置 → 用户登录配置
|
|
54
|
-
const token =
|
|
55
|
-
localConfig.token
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
const token = options.configFile
|
|
61
|
+
? localConfig.token
|
|
62
|
+
: process.env.PLAYCRAFT_SANDBOX_TOKEN ||
|
|
63
|
+
localConfig.token ||
|
|
64
|
+
globalConfig.token ||
|
|
65
|
+
'';
|
|
58
66
|
if (!token) {
|
|
59
67
|
throw new Error('Missing PlayCraft API credentials. ' +
|
|
60
68
|
'Run `playcraft login` to authenticate, or set PLAYCRAFT_SANDBOX_TOKEN / create a .playcraft.json file.');
|
|
@@ -62,14 +70,29 @@ export class AgentApiClient {
|
|
|
62
70
|
this.apiUrl = apiUrl;
|
|
63
71
|
this.token = token;
|
|
64
72
|
this.credentialFile = localConfig.credentialFile;
|
|
65
|
-
this.
|
|
73
|
+
this.explicitConfig = Boolean(options.configFile);
|
|
74
|
+
this.playcraftEnv = (options.configFile
|
|
75
|
+
? localConfig.env || process.env.PLAYCRAFT_ENV || ''
|
|
76
|
+
: process.env.PLAYCRAFT_ENV || localConfig.env || '').trim();
|
|
66
77
|
this.cloudRuntimeCredential = cloudRuntimeCredentialHint(token, token === localConfig.token ? localConfig : undefined);
|
|
78
|
+
if (options.operationNamespace) {
|
|
79
|
+
if (!options.configFile)
|
|
80
|
+
throw new Error('Operation namespace requires an explicit sandbox configuration');
|
|
81
|
+
const scope = JSON.stringify([
|
|
82
|
+
resolve(options.configFile),
|
|
83
|
+
apiUrl,
|
|
84
|
+
this.playcraftEnv,
|
|
85
|
+
localConfig.projectId,
|
|
86
|
+
localConfig.branch,
|
|
87
|
+
]);
|
|
88
|
+
this.operationDirectory = join(process.env.PLAYCRAFT_TOOL_REQUEST_DIR ?? join(homedir(), '.playcraft', 'tool-requests'), options.operationNamespace, createHash('sha256').update(scope).digest('hex'));
|
|
89
|
+
}
|
|
67
90
|
}
|
|
68
91
|
/** 根据 token 类型构造鉴权 headers */
|
|
69
92
|
authHeaders() {
|
|
70
93
|
let token = this.token;
|
|
71
94
|
// Polling commands can outlive a credential: read the atomically refreshed file per request.
|
|
72
|
-
if (this.credentialFile && !process.env.PLAYCRAFT_SANDBOX_TOKEN) {
|
|
95
|
+
if (this.credentialFile && (this.explicitConfig || !process.env.PLAYCRAFT_SANDBOX_TOKEN)) {
|
|
73
96
|
try {
|
|
74
97
|
token = JSON.parse(readFileSync(this.credentialFile, 'utf8')).token || token;
|
|
75
98
|
}
|
|
@@ -96,12 +119,12 @@ export class AgentApiClient {
|
|
|
96
119
|
const recoverId = options?.requestId ?? process.env.PLAYCRAFT_TOOL_REQUEST_ID;
|
|
97
120
|
const newRequest = options?.newRequest ?? process.env.PLAYCRAFT_TOOL_NEW_REQUEST === '1';
|
|
98
121
|
const originalReceipt = recoverId !== undefined && !newRequest && isWorkspaceGenerationPath(path)
|
|
99
|
-
? findToolOperation(url.pathname, originalBody, recoverId) : undefined;
|
|
122
|
+
? findToolOperation(url.pathname, originalBody, recoverId, this.operationDirectory) : undefined;
|
|
100
123
|
let serializedBody = originalReceipt ? originalBody : JSON.stringify(await withGenerationWorkspaceScope(path, body, this.cloudRuntimeCredential));
|
|
101
124
|
if (serializedBody === undefined)
|
|
102
125
|
throw new Error('Tool request body is required');
|
|
103
126
|
if (serializedBody !== originalBody && !newRequest) {
|
|
104
|
-
const previous = findToolOperation(url.pathname, originalBody, recoverId);
|
|
127
|
+
const previous = findToolOperation(url.pathname, originalBody, recoverId, this.operationDirectory);
|
|
105
128
|
if (previous && recoverId !== undefined)
|
|
106
129
|
serializedBody = originalBody;
|
|
107
130
|
else if (previous)
|
|
@@ -110,6 +133,7 @@ export class AgentApiClient {
|
|
|
110
133
|
}
|
|
111
134
|
const operation = beginToolOperation(url.pathname, serializedBody, recoverId, {
|
|
112
135
|
newRequest,
|
|
136
|
+
directory: this.operationDirectory,
|
|
113
137
|
});
|
|
114
138
|
const unknownOutcome = `Tool request ${operation.requestId}: the outcome is unknown. Rerun the same command to ` +
|
|
115
139
|
`continue this request: it returns the result if the request finished, or finishes it if ` +
|
|
@@ -384,6 +408,21 @@ export class AgentApiClient {
|
|
|
384
408
|
}
|
|
385
409
|
return { apiUrl: '', token: '' };
|
|
386
410
|
}
|
|
411
|
+
/** A static plugin's slot must never fall back to another workspace or a global login. */
|
|
412
|
+
static requireLocalConfig(file) {
|
|
413
|
+
try {
|
|
414
|
+
const config = JSON.parse(readFileSync(file, 'utf8'));
|
|
415
|
+
if (typeof config?.apiUrl !== 'string' ||
|
|
416
|
+
!config.apiUrl.trim() ||
|
|
417
|
+
typeof config.token !== 'string' ||
|
|
418
|
+
!config.token.trim())
|
|
419
|
+
throw new Error('invalid');
|
|
420
|
+
return { ...config, credentialFile: file };
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
throw new Error('A valid project .playcraft.json with apiUrl and token is required; no generation request was sent');
|
|
424
|
+
}
|
|
425
|
+
}
|
|
387
426
|
}
|
|
388
427
|
/** A failure whose meaning is settled; no recovery hint applies. */
|
|
389
428
|
class ToolRequestError extends Error {
|
|
@@ -29,7 +29,7 @@ function holderAlive(holder) {
|
|
|
29
29
|
}
|
|
30
30
|
/** Persist an identity before sending. Store neither credentials nor request/response bodies. */
|
|
31
31
|
export function beginToolOperation(path, serializedBody, recoverId, options = {}) {
|
|
32
|
-
const directory = process.env.PLAYCRAFT_TOOL_REQUEST_DIR ?? join(homedir(), '.playcraft', 'tool-requests');
|
|
32
|
+
const directory = options.directory ?? process.env.PLAYCRAFT_TOOL_REQUEST_DIR ?? join(homedir(), '.playcraft', 'tool-requests');
|
|
33
33
|
if (!isAbsolute(directory))
|
|
34
34
|
throw new Error('PLAYCRAFT_TOOL_REQUEST_DIR must be an absolute path');
|
|
35
35
|
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
@@ -98,8 +98,8 @@ export function beginToolOperation(path, serializedBody, recoverId, options = {}
|
|
|
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');
|
|
101
|
+
export function findToolOperation(path, serializedBody, requestId, directoryOverride) {
|
|
102
|
+
const directory = directoryOverride ?? process.env.PLAYCRAFT_TOOL_REQUEST_DIR ?? join(homedir(), '.playcraft', 'tool-requests');
|
|
103
103
|
if (!isAbsolute(directory))
|
|
104
104
|
throw new Error('PLAYCRAFT_TOOL_REQUEST_DIR must be an absolute path');
|
|
105
105
|
const inputHash = createHash('sha256').update(serializedBody).digest('hex');
|