@remixmate/cli 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -83
- package/README.zh-CN.md +183 -0
- package/dist/handlers/gen-digital-human.d.ts +12 -0
- package/dist/handlers/gen-digital-human.js +157 -0
- package/dist/handlers/gen-image.js +5 -41
- package/dist/handlers/gen-video.d.ts +18 -0
- package/dist/handlers/gen-video.js +153 -0
- package/dist/handlers/gen-voice.js +2 -12
- package/dist/handlers/index.js +4 -2
- package/dist/handlers/shared.d.ts +26 -0
- package/dist/handlers/shared.js +66 -0
- package/dist/http.d.ts +4 -2
- package/dist/http.js +5 -3
- package/dist/manifest.json +27 -24
- package/dist/registry.d.ts +5 -16
- package/dist/registry.js +11 -17
- package/dist/skill-schema.d.ts +47 -0
- package/dist/skill-schema.js +49 -0
- package/package.json +5 -4
- package/skills/export-jianying/scripts/gen_jianying_draft.py +4 -4
- package/skills/gen-digital-human/SKILL.md +23 -46
- package/skills/gen-digital-human/skill.json +3 -2
- package/skills/gen-digital-human/version.json +1 -1
- package/skills/gen-image/SKILL.md +2 -2
- package/skills/gen-image/skill.json +2 -1
- package/skills/gen-image/version.json +1 -1
- package/skills/gen-script/SKILL.md +13 -13
- package/skills/gen-script/scripts/gen_script.py +11 -12
- package/skills/gen-video/SKILL.md +22 -33
- package/skills/gen-video/skill.json +3 -2
- package/skills/gen-video/version.json +1 -1
- package/skills/gen-voice/SKILL.md +1 -1
- package/skills/gen-voice/version.json +1 -1
- package/skills/prepare-video-assets/SKILL.md +2 -2
- package/skills/prepare-video-assets/skill.json +1 -1
- package/skills/render-video/scripts/render_video.py +8 -8
- package/skills/{template-bind → template-registry}/README.md +4 -4
- package/skills/{template-bind → template-registry}/SKILL.md +14 -12
- package/skills/template-registry/scripts/list_templates.py +129 -0
- package/skills/{template-bind → template-registry}/scripts/match_template.py +4 -3
- package/skills/{template-bind → template-registry}/scripts/registry_loader.py +1 -1
- package/skills/{template-bind → template-registry}/scripts/template_paths.py +7 -7
- package/skills/{template-bind → template-registry}/skill.json +6 -5
- package/skills/template-registry/version.json +7 -0
- package/skills/{template-bind → template-registry}/video_dsl/README.md +2 -2
- package/skills/{template-bind → template-registry}/video_dsl/runtime/__init__.py +1 -1
- package/skills/video-parser/SKILL.md +47 -160
- package/skills/video-parser/skill.json +3 -4
- package/skills/video-parser/version.json +1 -1
- package/skills/web-capture/SKILL.md +407 -116
- package/skills/web-capture/scripts/_media_screenshot/__init__.py +21 -0
- package/skills/web-capture/scripts/_media_screenshot/bootstrap.py +34 -0
- package/skills/web-capture/scripts/_media_screenshot/browser.py +86 -0
- package/skills/web-capture/scripts/_media_screenshot/cli_args.py +111 -0
- package/skills/web-capture/scripts/_media_screenshot/js/arrow.js +86 -0
- package/skills/web-capture/scripts/_media_screenshot/js/caption.js +27 -0
- package/skills/web-capture/scripts/_media_screenshot/js/clear_overlay.js +5 -0
- package/skills/web-capture/scripts/_media_screenshot/js/cursor_bootstrap.js +26 -0
- package/skills/web-capture/scripts/_media_screenshot/js/cursor_hide.js +6 -0
- package/skills/web-capture/scripts/_media_screenshot/js/cursor_move.js +12 -0
- package/skills/web-capture/scripts/_media_screenshot/js/cursor_ripple.js +25 -0
- package/skills/web-capture/scripts/_media_screenshot/js/element_zoom.js +72 -0
- package/skills/web-capture/scripts/_media_screenshot/js/element_zoom_restore.js +21 -0
- package/skills/web-capture/scripts/_media_screenshot/js/fade_out_overlays.js +10 -0
- package/skills/web-capture/scripts/_media_screenshot/js/focus.js +38 -0
- package/skills/web-capture/scripts/_media_screenshot/js/highlight.js +97 -0
- package/skills/web-capture/scripts/_media_screenshot/js/overlay_bootstrap.js +19 -0
- package/skills/web-capture/scripts/_media_screenshot/js/page_zoom_in.js +54 -0
- package/skills/web-capture/scripts/_media_screenshot/js/page_zoom_out.js +11 -0
- package/skills/web-capture/scripts/_media_screenshot/js/page_zoom_restore.js +13 -0
- package/skills/web-capture/scripts/_media_screenshot/js/redact.js +33 -0
- package/skills/web-capture/scripts/_media_screenshot/js/scroll.js +26 -0
- package/skills/web-capture/scripts/_media_screenshot/js/scroll_through.js +18 -0
- package/skills/web-capture/scripts/_media_screenshot/js/sequence.js +76 -0
- package/skills/web-capture/scripts/_media_screenshot/js/settled_check.js +15 -0
- package/skills/web-capture/scripts/_media_screenshot/js/title_card.js +71 -0
- package/skills/web-capture/scripts/_media_screenshot/js_loader.py +14 -0
- package/skills/web-capture/scripts/_media_screenshot/overlay.py +17 -0
- package/skills/web-capture/scripts/_media_screenshot/recording.py +100 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/__init__.py +39 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/arrow.py +50 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/caption.py +20 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/click.py +35 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/code_lines.py +54 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/focus.py +24 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/highlight.py +54 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/hover.py +28 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/redact.py +32 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/scroll.py +32 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/sequence.py +43 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/title_card.py +20 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/type.py +42 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/wait.py +9 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/zoom.py +72 -0
- package/skills/web-capture/scripts/_media_screenshot/screenshot.py +166 -0
- package/skills/web-capture/scripts/_media_screenshot/storyboard.py +88 -0
- package/skills/web-capture/scripts/_media_screenshot/template.py +119 -0
- package/skills/web-capture/scripts/_media_screenshot/trim.py +82 -0
- package/skills/web-capture/scripts/record.py +186 -0
- package/skills/web-capture/scripts/screenshot.py +94 -0
- package/skills/web-capture/skill.json +14 -14
- package/skills/web-capture/templates/github-code-tour.json +59 -0
- package/skills/web-capture/templates/github-readme-tour.json +77 -0
- package/skills/web-capture/templates/github-repo-intro.json +74 -0
- package/skills/web-capture/version.json +2 -2
- package/dist/handlers/template-bind.d.ts +0 -15
- package/dist/handlers/template-bind.js +0 -149
- package/skills/gen-digital-human/scripts/gen_digital_human_video.py +0 -574
- package/skills/gen-video/scripts/gen_video.py +0 -591
- package/skills/template-bind/version.json +0 -7
- package/skills/web-capture/scripts/web_capture.py +0 -461
- /package/skills/{template-bind → template-registry}/scripts/check_contracts.py +0 -0
- /package/skills/{template-bind → template-registry}/scripts/render_job_client.py +0 -0
- /package/skills/{template-bind → template-registry}/scripts/sync_registry.py +0 -0
- /package/skills/{template-bind → template-registry}/video_dsl/runtime/dsl_validator.py +0 -0
- /package/skills/{template-bind → template-registry}/video_dsl/runtime/prompt_enhancer.py +0 -0
- /package/skills/{template-bind → template-registry}/video_dsl/runtime/template_binder.py +0 -0
- /package/skills/{template-bind → template-registry}/video_dsl/runtime/timeline_compiler.py +0 -0
- /package/skills/{template-bind → template-registry}/video_dsl/schema/render-plan-v1alpha1.json +0 -0
- /package/skills/{template-bind → template-registry}/video_dsl/schema/template-binding-v1alpha1.json +0 -0
- /package/skills/{template-bind → template-registry}/video_dsl/schema/template-definition-v1alpha1.json +0 -0
- /package/skills/{template-bind → template-registry}/video_dsl/schema/video-dsl-v1alpha1.json +0 -0
|
@@ -9,10 +9,9 @@
|
|
|
9
9
|
* - Async response (generationId + status:'generating'/'pending') is polled
|
|
10
10
|
* against /model/getImgStatus until completed/failed/timeout
|
|
11
11
|
*/
|
|
12
|
-
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
13
|
-
import path from 'node:path';
|
|
14
12
|
import { mmPost, pollUntil, resolveHttpContext, SkillError } from '../http.js';
|
|
15
13
|
import { emitProgress } from '../progress.js';
|
|
14
|
+
import { isTrue, resolveImageInput, toNumber, toStringArray } from './shared.js';
|
|
16
15
|
const DEFAULT_IMAGE_MODEL = 'doubao/doubao-seedream-5-0-260128';
|
|
17
16
|
// Maps aspect-ratio shorthand → (width, height) for Seedream models.
|
|
18
17
|
const SIZE_PRESETS = {
|
|
@@ -23,13 +22,6 @@ const SIZE_PRESETS = {
|
|
|
23
22
|
'3:4': [1728, 2304],
|
|
24
23
|
'2:3': [1664, 2496],
|
|
25
24
|
};
|
|
26
|
-
const MIME_BY_EXT = {
|
|
27
|
-
'.png': 'image/png',
|
|
28
|
-
'.jpg': 'image/jpeg',
|
|
29
|
-
'.jpeg': 'image/jpeg',
|
|
30
|
-
'.webp': 'image/webp',
|
|
31
|
-
'.gif': 'image/gif',
|
|
32
|
-
};
|
|
33
25
|
// Per-model reference-image caps (matches ab-api validation).
|
|
34
26
|
const SEEDREAM_REF_LIMIT = 14;
|
|
35
27
|
const GEMINI_REF_LIMIT = 4;
|
|
@@ -37,38 +29,6 @@ function isSeedreamModel(model) {
|
|
|
37
29
|
const lower = model.toLowerCase();
|
|
38
30
|
return lower.includes('seedream') || lower.includes('seedance');
|
|
39
31
|
}
|
|
40
|
-
function resolveImageInput(pathOrUrl) {
|
|
41
|
-
const trimmed = pathOrUrl.trim();
|
|
42
|
-
if (!trimmed)
|
|
43
|
-
return '';
|
|
44
|
-
const lower = trimmed.toLowerCase();
|
|
45
|
-
if (lower.startsWith('http://') || lower.startsWith('https://') || lower.startsWith('data:')) {
|
|
46
|
-
return trimmed;
|
|
47
|
-
}
|
|
48
|
-
if (!existsSync(trimmed) || !statSync(trimmed).isFile()) {
|
|
49
|
-
throw new SkillError(`❌ image path does not exist or is not a file: ${trimmed}`);
|
|
50
|
-
}
|
|
51
|
-
const ext = path.extname(trimmed).toLowerCase();
|
|
52
|
-
const mime = MIME_BY_EXT[ext] ?? 'image/jpeg';
|
|
53
|
-
const b64 = readFileSync(trimmed).toString('base64');
|
|
54
|
-
return `data:${mime};base64,${b64}`;
|
|
55
|
-
}
|
|
56
|
-
/** Coerce a CLI flag value (string|string[]|boolean) into an array of strings. */
|
|
57
|
-
function toStringArray(value) {
|
|
58
|
-
if (value === undefined || value === null)
|
|
59
|
-
return [];
|
|
60
|
-
if (Array.isArray(value))
|
|
61
|
-
return value.map(String);
|
|
62
|
-
return [String(value)];
|
|
63
|
-
}
|
|
64
|
-
function toNumber(value, name) {
|
|
65
|
-
if (value === undefined || value === null)
|
|
66
|
-
return undefined;
|
|
67
|
-
const n = Number(value);
|
|
68
|
-
if (!Number.isFinite(n))
|
|
69
|
-
throw new SkillError(`❌ ${name} must be a number, received: ${value}`);
|
|
70
|
-
return n;
|
|
71
|
-
}
|
|
72
32
|
/** Resolve {width, height} for Seedream from --size (preset key or WxH). */
|
|
73
33
|
function resolveSeedreamSize(size) {
|
|
74
34
|
if (SIZE_PRESETS[size]) {
|
|
@@ -153,6 +113,10 @@ export async function genImage(input, ctxIn) {
|
|
|
153
113
|
if (imageUrls.length === 0) {
|
|
154
114
|
throw new SkillError(`❌ API response did not contain any image URL: ${JSON.stringify(data)}`);
|
|
155
115
|
}
|
|
116
|
+
if (isTrue(input.json_output)) {
|
|
117
|
+
process.stdout.write(JSON.stringify({ urls: imageUrls }) + '\n');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
156
120
|
process.stdout.write(`\n📸 Generated ${imageUrls.length} image(s)\n`);
|
|
157
121
|
process.stdout.write(`\n🔗 Image URLs (renders directly, no download needed):\n`);
|
|
158
122
|
for (const url of imageUrls)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gen-video handler — POST ab-api /model/genVideo, then poll /model/getVideoStatus.
|
|
3
|
+
*
|
|
4
|
+
* Ported from the previous `scripts/gen_video.py` (entry.type python →
|
|
5
|
+
* entry.type http) so it shares auth / base-URL / business-code / polling with
|
|
6
|
+
* the other atomic generators via http.ts instead of re-implementing them.
|
|
7
|
+
*
|
|
8
|
+
* Model families (aligned with the ab-web AI_VIDEO_MODELS dropdown):
|
|
9
|
+
* - Seedance 1.5 Pro (Volcano): audio, first/last frame, fixed camera,
|
|
10
|
+
* adaptive ratio, duration 4–12s
|
|
11
|
+
* - Veo 3.1 / Veo 3.1 Fast (Google): first/last frame, reference images,
|
|
12
|
+
* negative prompt, 4K, duration 4/6/8s
|
|
13
|
+
*
|
|
14
|
+
* Local image paths for first/last/reference frames are read and base64-encoded
|
|
15
|
+
* into a data: URI; https / data: URIs pass through unchanged.
|
|
16
|
+
*/
|
|
17
|
+
import type { HandlerContext, HandlerInput } from './index.js';
|
|
18
|
+
export declare function genVideo(input: HandlerInput, ctxIn: HandlerContext): Promise<void>;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gen-video handler — POST ab-api /model/genVideo, then poll /model/getVideoStatus.
|
|
3
|
+
*
|
|
4
|
+
* Ported from the previous `scripts/gen_video.py` (entry.type python →
|
|
5
|
+
* entry.type http) so it shares auth / base-URL / business-code / polling with
|
|
6
|
+
* the other atomic generators via http.ts instead of re-implementing them.
|
|
7
|
+
*
|
|
8
|
+
* Model families (aligned with the ab-web AI_VIDEO_MODELS dropdown):
|
|
9
|
+
* - Seedance 1.5 Pro (Volcano): audio, first/last frame, fixed camera,
|
|
10
|
+
* adaptive ratio, duration 4–12s
|
|
11
|
+
* - Veo 3.1 / Veo 3.1 Fast (Google): first/last frame, reference images,
|
|
12
|
+
* negative prompt, 4K, duration 4/6/8s
|
|
13
|
+
*
|
|
14
|
+
* Local image paths for first/last/reference frames are read and base64-encoded
|
|
15
|
+
* into a data: URI; https / data: URIs pass through unchanged.
|
|
16
|
+
*/
|
|
17
|
+
import { mmPost, pollUntil, resolveHttpContext, SkillError } from '../http.js';
|
|
18
|
+
import { emitProgress } from '../progress.js';
|
|
19
|
+
import { isTrue, resolveImageInput, toNumber, toStringArray } from './shared.js';
|
|
20
|
+
const DEFAULT_VIDEO_MODEL = 'doubao-seedance-1-5-pro-251215';
|
|
21
|
+
// CLI shorthand → full LiteLLM / gateway model id (matches the frontend dropdown).
|
|
22
|
+
const MODEL_ALIASES = {
|
|
23
|
+
seedance: 'doubao-seedance-1-5-pro-251215',
|
|
24
|
+
'seedance-1.5': 'doubao-seedance-1-5-pro-251215',
|
|
25
|
+
'seedance-1.5-pro': 'doubao-seedance-1-5-pro-251215',
|
|
26
|
+
veo: 'veo-3.1-generate-001',
|
|
27
|
+
'veo-3.1': 'veo-3.1-generate-001',
|
|
28
|
+
'veo-3.1-generate': 'veo-3.1-generate-001',
|
|
29
|
+
'veo-fast': 'veo-3.1-fast-generate-001',
|
|
30
|
+
'veo-3.1-fast': 'veo-3.1-fast-generate-001',
|
|
31
|
+
'veo-3.1-fast-generate': 'veo-3.1-fast-generate-001',
|
|
32
|
+
};
|
|
33
|
+
const SEEDANCE_RATIOS = new Set(['adaptive', '16:9', '4:3', '1:1', '3:4', '9:16', '21:9']);
|
|
34
|
+
const SEEDANCE_RESOLUTIONS = new Set(['480p', '720p', '1080p']);
|
|
35
|
+
const VEO_RATIOS = new Set(['16:9', '9:16']);
|
|
36
|
+
const VEO_RESOLUTIONS = new Set(['720p', '1080p', '4k']);
|
|
37
|
+
const VEO_DURATIONS = new Set([4, 6, 8]);
|
|
38
|
+
const VEO_MAX_REFERENCE_IMAGES = 3;
|
|
39
|
+
function normalizeModelId(model) {
|
|
40
|
+
const key = model.trim().toLowerCase();
|
|
41
|
+
return MODEL_ALIASES[key] ?? model.trim();
|
|
42
|
+
}
|
|
43
|
+
function isSeedanceModel(model) {
|
|
44
|
+
return model.toLowerCase().includes('seedance');
|
|
45
|
+
}
|
|
46
|
+
function isVeoModel(model) {
|
|
47
|
+
return model.toLowerCase().includes('veo');
|
|
48
|
+
}
|
|
49
|
+
function validateParamsForModel(model, duration, ratio, resolution, refCount) {
|
|
50
|
+
if (isSeedanceModel(model)) {
|
|
51
|
+
if (duration < 4 || duration > 12) {
|
|
52
|
+
throw new SkillError(`❌ Seedance duration must be 4–12 s, got: ${duration}`);
|
|
53
|
+
}
|
|
54
|
+
if (!SEEDANCE_RATIOS.has(ratio)) {
|
|
55
|
+
throw new SkillError(`❌ Seedance aspect ratio must be one of: ${[...SEEDANCE_RATIOS].join(', ')}; got: ${ratio}`);
|
|
56
|
+
}
|
|
57
|
+
if (!SEEDANCE_RESOLUTIONS.has(resolution)) {
|
|
58
|
+
throw new SkillError(`❌ Seedance resolution must be one of: ${[...SEEDANCE_RESOLUTIONS].join(', ')}; got: ${resolution}`);
|
|
59
|
+
}
|
|
60
|
+
if (refCount > 0) {
|
|
61
|
+
throw new SkillError('❌ Seedance 1.5 Pro does not support a reference-image array (first / last frame only); use --first-frame / --last-frame instead');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else if (isVeoModel(model)) {
|
|
65
|
+
if (!VEO_DURATIONS.has(duration)) {
|
|
66
|
+
throw new SkillError(`❌ Veo duration must be one of: ${[...VEO_DURATIONS].join(', ')} s; got: ${duration}`);
|
|
67
|
+
}
|
|
68
|
+
if (!VEO_RATIOS.has(ratio)) {
|
|
69
|
+
throw new SkillError(`❌ Veo aspect ratio must be one of: ${[...VEO_RATIOS].join(', ')}; got: ${ratio}`);
|
|
70
|
+
}
|
|
71
|
+
if (!VEO_RESOLUTIONS.has(resolution)) {
|
|
72
|
+
throw new SkillError(`❌ Veo resolution must be one of: ${[...VEO_RESOLUTIONS].join(', ')}; got: ${resolution}`);
|
|
73
|
+
}
|
|
74
|
+
if (refCount > VEO_MAX_REFERENCE_IMAGES) {
|
|
75
|
+
throw new SkillError(`❌ Veo accepts at most ${VEO_MAX_REFERENCE_IMAGES} reference images; got: ${refCount}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function pollVideoStatus(ctx, taskId) {
|
|
80
|
+
return pollUntil(async () => {
|
|
81
|
+
const data = await mmPost(ctx, '/model/getVideoStatus', { taskId }, { timeoutMs: 30_000 });
|
|
82
|
+
const status = data.status ?? 'unknown';
|
|
83
|
+
emitProgress({ phase: 'gen-video:poll', taskId, status });
|
|
84
|
+
if (status === 'succeeded' || status === 'completed') {
|
|
85
|
+
return data.videoUrl ?? '';
|
|
86
|
+
}
|
|
87
|
+
if (status === 'failed') {
|
|
88
|
+
const err = data.errorMsg ?? data.error ?? data.message ?? 'unknown error';
|
|
89
|
+
throw new SkillError(`❌ video generation failed: ${err}`);
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}, { intervalMs: 10_000, timeoutMs: 300_000 });
|
|
93
|
+
}
|
|
94
|
+
export async function genVideo(input, ctxIn) {
|
|
95
|
+
const model = normalizeModelId(input.model ?? process.env.MM_VIDEO_MODEL ?? DEFAULT_VIDEO_MODEL);
|
|
96
|
+
const firstFrame = input.first_frame ? resolveImageInput(String(input.first_frame)) : '';
|
|
97
|
+
const lastFrame = input.last_frame ? resolveImageInput(String(input.last_frame)) : '';
|
|
98
|
+
const references = toStringArray(input.reference).map(resolveImageInput).filter(Boolean);
|
|
99
|
+
const prompt = (input.prompt ?? '').trim();
|
|
100
|
+
const hasVisual = Boolean(firstFrame || lastFrame || references.length > 0);
|
|
101
|
+
if (!prompt && !hasVisual) {
|
|
102
|
+
throw new SkillError('❌ Provide at least one of: a non-empty --prompt, or --first-frame / --last-frame / --reference');
|
|
103
|
+
}
|
|
104
|
+
const ratio = input.ratio ?? '16:9';
|
|
105
|
+
const resolution = input.resolution ?? '720p';
|
|
106
|
+
const duration = toNumber(input.duration, 'duration') ?? (isVeoModel(model) ? 8 : 5);
|
|
107
|
+
const seed = toNumber(input.seed, 'seed');
|
|
108
|
+
validateParamsForModel(model, duration, ratio, resolution, references.length);
|
|
109
|
+
const ctx = resolveHttpContext(ctxIn.skillName, {
|
|
110
|
+
apiBaseUrl: input.api_base_url,
|
|
111
|
+
privateToken: input.priv_token,
|
|
112
|
+
});
|
|
113
|
+
const payload = {
|
|
114
|
+
model,
|
|
115
|
+
duration,
|
|
116
|
+
ratio,
|
|
117
|
+
resolution,
|
|
118
|
+
generateAudio: isTrue(input.generate_audio),
|
|
119
|
+
cameraFixed: isTrue(input.camera_fixed),
|
|
120
|
+
};
|
|
121
|
+
if (prompt)
|
|
122
|
+
payload.prompt = prompt;
|
|
123
|
+
if (firstFrame)
|
|
124
|
+
payload.firstFrameImage = firstFrame;
|
|
125
|
+
if (lastFrame)
|
|
126
|
+
payload.lastFrameImage = lastFrame;
|
|
127
|
+
if (references.length > 0)
|
|
128
|
+
payload.referenceImages = references;
|
|
129
|
+
if (input.negative_prompt && String(input.negative_prompt).trim()) {
|
|
130
|
+
payload.negativePrompt = String(input.negative_prompt).trim();
|
|
131
|
+
}
|
|
132
|
+
if (seed !== undefined)
|
|
133
|
+
payload.seed = seed;
|
|
134
|
+
if (input.person_generation)
|
|
135
|
+
payload.personGeneration = String(input.person_generation);
|
|
136
|
+
emitProgress({ phase: 'gen-video:request', model, ratio, resolution, duration });
|
|
137
|
+
const data = await mmPost(ctx, '/model/genVideo', payload, { timeoutMs: 120_000 });
|
|
138
|
+
const taskId = data.taskId;
|
|
139
|
+
if (!taskId) {
|
|
140
|
+
throw new SkillError(`❌ could not parse the task id; API returned: ${JSON.stringify(data)}`);
|
|
141
|
+
}
|
|
142
|
+
emitProgress({ phase: 'gen-video:async', taskId });
|
|
143
|
+
const videoUrl = await pollVideoStatus(ctx, taskId);
|
|
144
|
+
if (!videoUrl) {
|
|
145
|
+
throw new SkillError(`❌ video generated but no URL was returned, task id: ${taskId}`);
|
|
146
|
+
}
|
|
147
|
+
if (isTrue(input.json_output)) {
|
|
148
|
+
process.stdout.write(JSON.stringify({ url: videoUrl }) + '\n');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
process.stdout.write(`\n🔗 Video URL (renders directly, no download needed):\n${videoUrl}\n`);
|
|
152
|
+
process.stdout.write(`\n🎉 Video generation done!\n`);
|
|
153
|
+
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { mmPost, resolveHttpContext, SkillError } from '../http.js';
|
|
16
16
|
import { emitProgress } from '../progress.js';
|
|
17
|
+
import { isTrue, toNumber } from './shared.js';
|
|
17
18
|
const DEFAULT_VOICE_ID = 'Chinese (Mandarin)_Male_Announcer';
|
|
18
19
|
/**
|
|
19
20
|
* Fallback voice catalog printed when `--list-voices --local` is passed.
|
|
@@ -27,17 +28,6 @@ const LOCAL_VOICE_CATALOG = [
|
|
|
27
28
|
// TODO: replace once gen-voice service confirms an English default voice id.
|
|
28
29
|
{ id: 'english-male-friendly-01', language: 'en', name: 'Friendly Male (English, TBD)' },
|
|
29
30
|
];
|
|
30
|
-
function toNumber(value, name, defaultValue) {
|
|
31
|
-
if (value === undefined || value === null || value === '')
|
|
32
|
-
return defaultValue;
|
|
33
|
-
const n = Number(value);
|
|
34
|
-
if (!Number.isFinite(n))
|
|
35
|
-
throw new SkillError(`❌ ${name} must be a number, received: ${value}`);
|
|
36
|
-
return n;
|
|
37
|
-
}
|
|
38
|
-
function isTrue(value) {
|
|
39
|
-
return value === true || value === 'true';
|
|
40
|
-
}
|
|
41
31
|
async function listVoicesRemote(ctx) {
|
|
42
32
|
const data = await mmPost(ctx, '/voice/page', { current: 1, pageSize: 100 }, { timeoutMs: 30_000 });
|
|
43
33
|
const voices = data.list ?? [];
|
|
@@ -75,7 +65,7 @@ export async function genVoice(input, ctxIn) {
|
|
|
75
65
|
throw new SkillError('❌ Please provide --text to specify the text to synthesize');
|
|
76
66
|
}
|
|
77
67
|
const voiceId = input.voice_id ?? DEFAULT_VOICE_ID;
|
|
78
|
-
const speed = toNumber(input.speed, 'speed'
|
|
68
|
+
const speed = toNumber(input.speed, 'speed') ?? 1.0;
|
|
79
69
|
if (speed < 0.5 || speed > 2.0) {
|
|
80
70
|
throw new SkillError('❌ speed must be in the range 0.5~2.0');
|
|
81
71
|
}
|
package/dist/handlers/index.js
CHANGED
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { genImage } from './gen-image.js';
|
|
8
8
|
import { genVoice } from './gen-voice.js';
|
|
9
|
-
import {
|
|
9
|
+
import { genVideo } from './gen-video.js';
|
|
10
|
+
import { genDigitalHuman } from './gen-digital-human.js';
|
|
10
11
|
export const HANDLERS = {
|
|
11
12
|
'gen-image': genImage,
|
|
12
13
|
'gen-voice': genVoice,
|
|
13
|
-
'
|
|
14
|
+
'gen-video': genVideo,
|
|
15
|
+
'gen-digital-human': genDigitalHuman,
|
|
14
16
|
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for TS skill handlers.
|
|
3
|
+
*
|
|
4
|
+
* These were previously copy-pasted across gen-image / gen-video /
|
|
5
|
+
* gen-digital-human / gen-voice. Centralizing them keeps argument coercion and
|
|
6
|
+
* image-input resolution identical across every atomic generator.
|
|
7
|
+
*
|
|
8
|
+
* Lives under handlers/ (not src/ root) so the compiled `dist/handlers/shared.js`
|
|
9
|
+
* travels with the rest of the handler bundle when ab-agent copies `dist/`.
|
|
10
|
+
*/
|
|
11
|
+
/** Truthy check tolerant of CLI string values (`--flag` → true, `--flag true` → 'true'). */
|
|
12
|
+
export declare function isTrue(value: unknown): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Coerce a flag value into a finite number, or `undefined` when absent/empty.
|
|
15
|
+
* Throws SkillError on a non-numeric value so callers fail fast with a clear msg.
|
|
16
|
+
*/
|
|
17
|
+
export declare function toNumber(value: unknown, name: string): number | undefined;
|
|
18
|
+
/** Coerce a repeatable flag (string | string[] | undefined) into a string array. */
|
|
19
|
+
export declare function toStringArray(value: unknown): string[];
|
|
20
|
+
/**
|
|
21
|
+
* Resolve an image reference into a value the backend accepts:
|
|
22
|
+
* - http(s) URLs and data: URIs pass through unchanged
|
|
23
|
+
* - local file paths are read and base64-encoded into a `data:<mime>;base64,…` URI
|
|
24
|
+
* Throws SkillError when a local path does not exist or is not a file.
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveImageInput(pathOrUrl: string): string;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for TS skill handlers.
|
|
3
|
+
*
|
|
4
|
+
* These were previously copy-pasted across gen-image / gen-video /
|
|
5
|
+
* gen-digital-human / gen-voice. Centralizing them keeps argument coercion and
|
|
6
|
+
* image-input resolution identical across every atomic generator.
|
|
7
|
+
*
|
|
8
|
+
* Lives under handlers/ (not src/ root) so the compiled `dist/handlers/shared.js`
|
|
9
|
+
* travels with the rest of the handler bundle when ab-agent copies `dist/`.
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { SkillError } from '../http.js';
|
|
14
|
+
const MIME_BY_EXT = {
|
|
15
|
+
'.png': 'image/png',
|
|
16
|
+
'.jpg': 'image/jpeg',
|
|
17
|
+
'.jpeg': 'image/jpeg',
|
|
18
|
+
'.webp': 'image/webp',
|
|
19
|
+
'.gif': 'image/gif',
|
|
20
|
+
};
|
|
21
|
+
/** Truthy check tolerant of CLI string values (`--flag` → true, `--flag true` → 'true'). */
|
|
22
|
+
export function isTrue(value) {
|
|
23
|
+
return value === true || value === 'true';
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Coerce a flag value into a finite number, or `undefined` when absent/empty.
|
|
27
|
+
* Throws SkillError on a non-numeric value so callers fail fast with a clear msg.
|
|
28
|
+
*/
|
|
29
|
+
export function toNumber(value, name) {
|
|
30
|
+
if (value === undefined || value === null || value === '')
|
|
31
|
+
return undefined;
|
|
32
|
+
const n = Number(value);
|
|
33
|
+
if (!Number.isFinite(n))
|
|
34
|
+
throw new SkillError(`❌ ${name} must be a number, received: ${value}`);
|
|
35
|
+
return n;
|
|
36
|
+
}
|
|
37
|
+
/** Coerce a repeatable flag (string | string[] | undefined) into a string array. */
|
|
38
|
+
export function toStringArray(value) {
|
|
39
|
+
if (value === undefined || value === null)
|
|
40
|
+
return [];
|
|
41
|
+
if (Array.isArray(value))
|
|
42
|
+
return value.map(String);
|
|
43
|
+
return [String(value)];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Resolve an image reference into a value the backend accepts:
|
|
47
|
+
* - http(s) URLs and data: URIs pass through unchanged
|
|
48
|
+
* - local file paths are read and base64-encoded into a `data:<mime>;base64,…` URI
|
|
49
|
+
* Throws SkillError when a local path does not exist or is not a file.
|
|
50
|
+
*/
|
|
51
|
+
export function resolveImageInput(pathOrUrl) {
|
|
52
|
+
const trimmed = pathOrUrl.trim();
|
|
53
|
+
if (!trimmed)
|
|
54
|
+
return '';
|
|
55
|
+
const lower = trimmed.toLowerCase();
|
|
56
|
+
if (lower.startsWith('http://') || lower.startsWith('https://') || lower.startsWith('data:')) {
|
|
57
|
+
return trimmed;
|
|
58
|
+
}
|
|
59
|
+
if (!existsSync(trimmed) || !statSync(trimmed).isFile()) {
|
|
60
|
+
throw new SkillError(`❌ image path does not exist or is not a file: ${trimmed}`);
|
|
61
|
+
}
|
|
62
|
+
const ext = path.extname(trimmed).toLowerCase();
|
|
63
|
+
const mime = MIME_BY_EXT[ext] ?? 'image/jpeg';
|
|
64
|
+
const b64 = readFileSync(trimmed).toString('base64');
|
|
65
|
+
return `data:${mime};base64,${b64}`;
|
|
66
|
+
}
|
package/dist/http.d.ts
CHANGED
|
@@ -43,8 +43,10 @@ export declare function mmPost<T = unknown>(ctx: HttpContext, pathOrUrl: string,
|
|
|
43
43
|
timeoutMs?: number;
|
|
44
44
|
}): Promise<T>;
|
|
45
45
|
/**
|
|
46
|
-
* Generic polling helper.
|
|
47
|
-
* until it returns a non-null value or `timeoutMs` elapses.
|
|
46
|
+
* Generic polling helper. Invokes `probe()` immediately, then every
|
|
47
|
+
* `intervalMs`, until it returns a non-null value or `timeoutMs` elapses.
|
|
48
|
+
* Probing first (rather than sleeping first) avoids a wasted initial interval
|
|
49
|
+
* when the backend job is already complete.
|
|
48
50
|
*/
|
|
49
51
|
export declare function pollUntil<T>(probe: () => Promise<T | null>, opts?: {
|
|
50
52
|
intervalMs?: number;
|
package/dist/http.js
CHANGED
|
@@ -93,18 +93,20 @@ export async function mmPost(ctx, pathOrUrl, body, opts = {}) {
|
|
|
93
93
|
return parsed.data ?? undefined;
|
|
94
94
|
}
|
|
95
95
|
/**
|
|
96
|
-
* Generic polling helper.
|
|
97
|
-
* until it returns a non-null value or `timeoutMs` elapses.
|
|
96
|
+
* Generic polling helper. Invokes `probe()` immediately, then every
|
|
97
|
+
* `intervalMs`, until it returns a non-null value or `timeoutMs` elapses.
|
|
98
|
+
* Probing first (rather than sleeping first) avoids a wasted initial interval
|
|
99
|
+
* when the backend job is already complete.
|
|
98
100
|
*/
|
|
99
101
|
export async function pollUntil(probe, opts = {}) {
|
|
100
102
|
const intervalMs = opts.intervalMs ?? 5_000;
|
|
101
103
|
const timeoutMs = opts.timeoutMs ?? 120_000;
|
|
102
104
|
const start = Date.now();
|
|
103
105
|
while (Date.now() - start < timeoutMs) {
|
|
104
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
105
106
|
const result = await probe();
|
|
106
107
|
if (result !== null)
|
|
107
108
|
return result;
|
|
109
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
108
110
|
}
|
|
109
111
|
throw new SkillError(`❌ polling timed out (${Math.round(timeoutMs / 1000)}s)`);
|
|
110
112
|
}
|
package/dist/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"generatedAt": "2026-06-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"generatedAt": "2026-06-08T05:59:50.921Z",
|
|
5
5
|
"skills": [
|
|
6
6
|
{
|
|
7
7
|
"id": "export-jianying",
|
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
"Use Jimeng or HiFly to drive an avatar"
|
|
38
38
|
],
|
|
39
39
|
"entry": {
|
|
40
|
-
"type": "
|
|
41
|
-
"
|
|
40
|
+
"type": "http",
|
|
41
|
+
"handler": "gen-digital-human"
|
|
42
42
|
},
|
|
43
|
-
"runtime": "
|
|
43
|
+
"runtime": "node",
|
|
44
44
|
"envVars": [
|
|
45
45
|
"PRIV_TOKEN",
|
|
46
46
|
"MM_API_BASE_URL",
|
|
@@ -103,10 +103,10 @@
|
|
|
103
103
|
"Image-to-video, first-frame / last-frame, reference-image-to-video"
|
|
104
104
|
],
|
|
105
105
|
"entry": {
|
|
106
|
-
"type": "
|
|
107
|
-
"
|
|
106
|
+
"type": "http",
|
|
107
|
+
"handler": "gen-video"
|
|
108
108
|
},
|
|
109
|
-
"runtime": "
|
|
109
|
+
"runtime": "node",
|
|
110
110
|
"envVars": [
|
|
111
111
|
"PRIV_TOKEN",
|
|
112
112
|
"MM_API_BASE_URL",
|
|
@@ -190,20 +190,18 @@
|
|
|
190
190
|
]
|
|
191
191
|
},
|
|
192
192
|
{
|
|
193
|
-
"id": "template-
|
|
193
|
+
"id": "template-registry",
|
|
194
194
|
"tier": "orchestration",
|
|
195
|
-
"title": "Template Registry
|
|
195
|
+
"title": "Video Template Registry",
|
|
196
196
|
"summary": "List all available video templates (templateId / name / aspect ratio / style tags). Template-to-DSL binding is no longer exposed as a separate step — once prepare_video_assets receives a template_id it runs the full DSL→RenderPlan pipeline internally.",
|
|
197
197
|
"triggers": [
|
|
198
|
-
"View available templates / list every template"
|
|
199
|
-
"Bind a template to a DSL (a template-id must be supplied)",
|
|
200
|
-
"Agent has a Video DSL and needs a TemplateBinding"
|
|
198
|
+
"View available templates / list every template"
|
|
201
199
|
],
|
|
202
200
|
"entry": {
|
|
203
|
-
"type": "
|
|
204
|
-
"
|
|
201
|
+
"type": "python",
|
|
202
|
+
"scriptPath": "scripts/list_templates.py"
|
|
205
203
|
},
|
|
206
|
-
"runtime": "
|
|
204
|
+
"runtime": "python",
|
|
207
205
|
"envVars": [
|
|
208
206
|
"PRIV_TOKEN",
|
|
209
207
|
"VIDEO_TEMPLATE_REGISTRY",
|
|
@@ -227,25 +225,30 @@
|
|
|
227
225
|
},
|
|
228
226
|
"runtime": "python",
|
|
229
227
|
"envVars": [
|
|
228
|
+
"RENDER_API_URL",
|
|
230
229
|
"PRIV_TOKEN",
|
|
231
|
-
"
|
|
232
|
-
"AGENT_NAME",
|
|
233
|
-
"RENDER_API_URL"
|
|
230
|
+
"CONVERSATION_ID"
|
|
234
231
|
]
|
|
235
232
|
},
|
|
236
233
|
{
|
|
237
234
|
"id": "web-capture",
|
|
238
235
|
"tier": "tool",
|
|
239
236
|
"title": "Web Page Capture",
|
|
240
|
-
"summary": "Drive a headless browser (Playwright) to capture
|
|
237
|
+
"summary": "Drive a headless browser (Playwright Python) to capture any URL to local files: full-page / element / region screenshots, fixed-duration / condition-triggered / auto-scroll recordings, and storyboard videos (highlight / focus / zoom / scroll / virtual-cursor click / caption / title-card / arrow / numbered sequence / redact / code-line highlight). Two entry scripts: scripts/screenshot.py (stills) and scripts/record.py (webm video, storyboards, templates).",
|
|
241
238
|
"triggers": [
|
|
242
|
-
"
|
|
243
|
-
"
|
|
244
|
-
"
|
|
239
|
+
"网页截图、网页截屏、整页截图、长截图、full page screenshot",
|
|
240
|
+
"截某个元素 / 区域、局部截屏、focus 某个区域",
|
|
241
|
+
"录屏、网页录制、录制视频、录一段操作、生成 webm",
|
|
242
|
+
"滚动录屏、页面从头划到尾的视频",
|
|
243
|
+
"多场景视频、分镜视频、storyboard、按时间线编排",
|
|
244
|
+
"演示点击 / 输入 / 悬停(虚拟鼠标 + 涟漪 / 打字机 / 触发 tooltip)",
|
|
245
|
+
"加箭头、加字幕条、加标题卡 / 片头片尾、按顺序编号高亮多个区域",
|
|
246
|
+
"截图前隐藏元素 / 涂盖打码、给静态截图加注释(红框 / 箭头 / 标签)",
|
|
247
|
+
"移动端 / 设备模拟、带 cookie / 登录态截屏 / 录屏"
|
|
245
248
|
],
|
|
246
249
|
"entry": {
|
|
247
250
|
"type": "python",
|
|
248
|
-
"scriptPath": "scripts/
|
|
251
|
+
"scriptPath": "scripts/screenshot.py"
|
|
249
252
|
},
|
|
250
253
|
"runtime": "python",
|
|
251
254
|
"envVars": [
|
package/dist/registry.d.ts
CHANGED
|
@@ -1,24 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Skill registry — scan skills/ and load skill.json definitions.
|
|
3
3
|
*
|
|
4
|
-
* skill.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* `entry` is the dispatcher hint. Missing entry falls back to the legacy
|
|
8
|
-
* top-level scriptPath, treated as { type:'python', scriptPath } so older
|
|
9
|
-
* skills keep working without a touch.
|
|
4
|
+
* Validation and entry-normalization live in `skill-schema.ts` (shared with
|
|
5
|
+
* the build-time manifest generator and smoke test). A record that fails
|
|
6
|
+
* validation is skipped with a warning rather than crashing the whole CLI.
|
|
10
7
|
*/
|
|
8
|
+
import { type SkillEntry } from './skill-schema.js';
|
|
11
9
|
export declare const SKILLS_DIR: string;
|
|
12
|
-
export type SkillEntry
|
|
13
|
-
type: 'python';
|
|
14
|
-
scriptPath: string;
|
|
15
|
-
} | {
|
|
16
|
-
type: 'http';
|
|
17
|
-
handler: string;
|
|
18
|
-
} | {
|
|
19
|
-
type: 'builtin';
|
|
20
|
-
handler: string;
|
|
21
|
-
};
|
|
10
|
+
export type { SkillEntry } from './skill-schema.js';
|
|
22
11
|
export interface SkillDef {
|
|
23
12
|
name: string;
|
|
24
13
|
toolName: string;
|
package/dist/registry.js
CHANGED
|
@@ -1,29 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Skill registry — scan skills/ and load skill.json definitions.
|
|
3
3
|
*
|
|
4
|
-
* skill.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* `entry` is the dispatcher hint. Missing entry falls back to the legacy
|
|
8
|
-
* top-level scriptPath, treated as { type:'python', scriptPath } so older
|
|
9
|
-
* skills keep working without a touch.
|
|
4
|
+
* Validation and entry-normalization live in `skill-schema.ts` (shared with
|
|
5
|
+
* the build-time manifest generator and smoke test). A record that fails
|
|
6
|
+
* validation is skipped with a warning rather than crashing the whole CLI.
|
|
10
7
|
*/
|
|
11
8
|
import { readdirSync, readFileSync, existsSync, statSync } from 'node:fs';
|
|
12
9
|
import path from 'node:path';
|
|
13
10
|
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { normalizeEntry, validateSkillJson, } from './skill-schema.js';
|
|
14
12
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
13
|
export const SKILLS_DIR = path.resolve(__dirname, '..', 'skills');
|
|
16
|
-
function normalizeEntry(raw) {
|
|
17
|
-
if (raw.entry)
|
|
18
|
-
return raw.entry;
|
|
19
|
-
if (raw.scriptPath)
|
|
20
|
-
return { type: 'python', scriptPath: raw.scriptPath };
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
14
|
function loadOne(skillDir) {
|
|
24
15
|
const skillJsonPath = path.join(skillDir, 'skill.json');
|
|
25
16
|
if (!existsSync(skillJsonPath))
|
|
26
17
|
return null;
|
|
18
|
+
const skillId = path.basename(skillDir);
|
|
27
19
|
let raw;
|
|
28
20
|
try {
|
|
29
21
|
raw = JSON.parse(readFileSync(skillJsonPath, 'utf-8'));
|
|
@@ -32,16 +24,18 @@ function loadOne(skillDir) {
|
|
|
32
24
|
console.error(`[remixmate] failed to parse ${skillJsonPath}:`, err);
|
|
33
25
|
return null;
|
|
34
26
|
}
|
|
35
|
-
const
|
|
36
|
-
if (
|
|
37
|
-
console.warn(`[remixmate] skipping ${
|
|
27
|
+
const errors = validateSkillJson(raw, skillId);
|
|
28
|
+
if (errors.length > 0) {
|
|
29
|
+
console.warn(`[remixmate] skipping ${skillId}:\n${errors.map((e) => ` - ${e}`).join('\n')}`);
|
|
38
30
|
return null;
|
|
39
31
|
}
|
|
32
|
+
// validateSkillJson guarantees a resolvable entry.
|
|
33
|
+
const entry = normalizeEntry(raw);
|
|
40
34
|
const def = {
|
|
41
35
|
name: raw.name,
|
|
42
36
|
toolName: raw.toolName,
|
|
43
37
|
description: raw.description,
|
|
44
|
-
parameters: raw.parameters,
|
|
38
|
+
parameters: raw.parameters ?? {},
|
|
45
39
|
entry,
|
|
46
40
|
skillDir,
|
|
47
41
|
};
|