@pippit-dev/cli 1.0.21 → 1.0.22
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 +65 -1
- package/checksums.txt +6 -6
- package/cmd/canvas/canvas.go +2 -0
- package/cmd/canvas/canvas_test.go +2 -2
- package/cmd/canvas/command_help.go +40 -0
- package/cmd/root.go +4 -3
- package/cmd/root_test.go +38 -0
- package/dist/checksums.txt +6 -6
- package/dist/xyq-canvas-command-runtime.cjs +14 -14
- package/dist/xyq-canvas-command-runtime.cjs.LEGAL.txt +235 -3
- package/dist/xyq-canvas-command-runtime.cjs.sha256 +2 -2
- package/package.json +7 -3
- package/scripts/canvas-command-discovery.js +202 -0
- package/scripts/canvas-command-guides.js +171 -0
- package/scripts/canvas-command.js +100 -23
- package/scripts/install-wizard.js +46 -22
- package/scripts/run.js +58 -42
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Offline usage notes. The installed runtime's list/describe/schema remain the
|
|
4
|
+
// source of truth for available commands and their accepted input.
|
|
5
|
+
const GUIDES = [
|
|
6
|
+
{
|
|
7
|
+
name: "storyboard",
|
|
8
|
+
summary: "故事板原生脚本标签、时长与引用,以及当前 CLI 的能力边界。",
|
|
9
|
+
content: [
|
|
10
|
+
"故事板脚本保存在镜头资产 content.generation.prompt。现有数据可能是 biz/shot,或 biz/video 且 bizVariant 为 storyboard-shot;故事板 brief 另有镜头列表与顺序。普通 video 节点不等于一个完整故事板镜头。",
|
|
11
|
+
"<duration-ms> 中填写正数毫秒。一个 prompt 中所有有效正数标签累加;有标签总和时优先采用该总和,否则回退到正数 generation.durationMs。空值、非数值或非正数不计入总和。例子中的 2000 + 3000 = 5000 ms。",
|
|
12
|
+
"时间码和起止区间文字只是 prompt 内容,不是可执行的台词调度。标签求和也不保证模型精确按秒完成每个动作或台词。生成会将镜头总毫秒数向上取整为秒,再按当前故事板模型策略校验;可用时长、分辨率组合和最小时长取决于当前模型目录,不能从通用 generate-video 的帮助推断。",
|
|
13
|
+
"node-asset 填真实节点资产 ID,角色形象选择沿用 data-reference-selection 属性。独立媒体可以没有节点,使用 pippit-asset-id,但需已有可解析的引用草稿;只有裸 ID 不代表素材已就绪。先查询真实资产与当前引用,不要从名称猜 ID。",
|
|
14
|
+
"当前没有公开的故事板脚本编辑、镜头增删排序或指定镜头生成领域 command。Canvas 的 xyq.generation.update_prompt 不等同于故事板的脚本保存流程;generate-video 不会自动设置故事板身份或写回指定镜头。通用资产补丁能传输数据,不提供这些业务校验,不能作为故事板领域操作的替代。此处 XML 用于说明既有格式,不是可直接执行的故事板命令。",
|
|
15
|
+
"下一步先用 canvas command list 确认安装版本能力,用 canvas get 读取已知镜头资产;故事板编辑与生成继续使用现有 Web 故事板入口。所有示例 ID 都须替换为当前项目真实 ID。",
|
|
16
|
+
].join("\n\n"),
|
|
17
|
+
next_commands: [
|
|
18
|
+
"pippit-tool-cli canvas command list",
|
|
19
|
+
"pippit-tool-cli canvas get --asset-id SHOT_ASSET_ID",
|
|
20
|
+
"pippit-tool-cli canvas command guide prompt-references",
|
|
21
|
+
"pippit-tool-cli canvas command guide time",
|
|
22
|
+
],
|
|
23
|
+
examples: [
|
|
24
|
+
{
|
|
25
|
+
description: "现有故事板 prompt:两个正数毫秒段,总时长 5000 ms;不是 CLI 编辑指令。",
|
|
26
|
+
prompt:
|
|
27
|
+
"<duration-ms>2000</duration-ms><node-asset>ROLE_NODE_ASSET_ID</node-asset>走入雨中的街道。\n<duration-ms>3000</duration-ms><node-asset>ROLE_NODE_ASSET_ID</node-asset>停下脚步,说:我们到了。",
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
related: ["prompt-references", "time"],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: "prompt-references",
|
|
34
|
+
summary: "图片/视频 prompt 标签、角色选材与没有节点的独立素材。",
|
|
35
|
+
content: [
|
|
36
|
+
"先用 list 确认 xyq.generation.update_prompt 可用,再 describe 获取参数。该命令接收 nodeId、prompt、可选 dryRun;nodeId 是当前 Canvas 的目标图片或视频节点 ID。get_snapshot 可查节点,get_asset 可读节点资产与 generation.references;用 describe 查看这两个查询命令的输入,避免混淆 Canvas ID、节点 ID 与媒体 Pippit ID。",
|
|
37
|
+
'<node-asset>NODE_ASSET_ID</node-asset> 引用已有节点。标签 ID 来自查询结果中的真实节点资产身份;不要用展示名称代替。CLI 复用 Web 标签匹配、节点连边和角色默认选材,保留标签属性。角色已有 data-reference-selection="IMAGE_ID,VOICE_ID" 属性使用逗号分隔选择 ID,需与角色已有资料匹配,不另传 selectedIds 或 reference 数组。',
|
|
38
|
+
"独立上传/素材库媒体可以没有节点。对已在目标 generation.references 中的素材,用 <pippit-asset-id>MEDIA_PIPPIT_ASSET_ID</pippit-asset-id>,ID 取自该草稿的 pippitAssetId。视频目标可使用已有图片、视频、音频候选;图片目标须符合自身支持的引用类型。无须新增 referenceSource 入参。",
|
|
39
|
+
"只有 canvas upload 返回的 ID 还不够:本命令不会查询未知媒体并加入草稿。canvas get 只查询,不添加引用。找不到的普通标签返回 UNRESOLVED_PROMPT_REFERENCE,不写文档。相同媒体若匹配到已有 Canvas 源节点,沿用 Web 节点优先规则连边。新独立素材先通过现有 Web 上传/素材库流程建立草稿。",
|
|
40
|
+
"dryRun:true 在隔离文档预演,不改变原文档。成功编辑保留其他 generation 参数及 caption/title/name;空 prompt 清空文本并保留现有引用与连边。本命令不触发生成。不要浅覆盖整个 generation。故事板有自己的脚本与引用流程,见 storyboard 指南。",
|
|
41
|
+
].join("\n\n"),
|
|
42
|
+
next_commands: [
|
|
43
|
+
"pippit-tool-cli canvas command describe xyq.generation.update_prompt",
|
|
44
|
+
"pippit-tool-cli canvas command describe get_snapshot",
|
|
45
|
+
"pippit-tool-cli canvas command describe get_asset",
|
|
46
|
+
"pippit-tool-cli canvas command describe xyq.generation.update_prompt --path properties.prompt",
|
|
47
|
+
],
|
|
48
|
+
examples: [
|
|
49
|
+
{
|
|
50
|
+
description: "已有 Canvas 节点引用的预演;将两个节点 ID 换成查询到的真实值。",
|
|
51
|
+
command:
|
|
52
|
+
"pippit-tool-cli canvas command run xyq.generation.update_prompt --canvas-id CANVAS_ID --file ./prompt-input.json",
|
|
53
|
+
input: {
|
|
54
|
+
nodeId: "TARGET_IMAGE_NODE_ID",
|
|
55
|
+
prompt: "参考 <node-asset>REFERENCE_NODE_ASSET_ID</node-asset> 的人物,改为雨夜街景。",
|
|
56
|
+
dryRun: true,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
description: "没有节点、但已存在于目标草稿中的独立图片引用。",
|
|
61
|
+
input: {
|
|
62
|
+
nodeId: "TARGET_IMAGE_NODE_ID",
|
|
63
|
+
prompt: "参考 <pippit-asset-id>PIPPIT_ASSET_ID_IN_DRAFT</pippit-asset-id> 的构图。",
|
|
64
|
+
dryRun: true,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
related: ["storyboard", "time"],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "time",
|
|
72
|
+
summary: "区分故事板毫秒、多轨微秒、3D 帧与源动画秒。",
|
|
73
|
+
content: [
|
|
74
|
+
"各领域单位不同,先 describe 具体字段;不要把一个领域的数值直接复制到另一个领域。此指南只解释单位,不触发写入或生成。",
|
|
75
|
+
"故事板 <duration-ms> 与 generation.durationMs 使用毫秒:1 秒 = 1000 ms。多个正数标签累加;没有有效标签才回退到 generation.durationMs。提交视频时对总毫秒数向上取整为秒,随后遵循当前故事板模型策略。时间码文字不是执行调度,详见 storyboard。",
|
|
76
|
+
"多轨 TimelineDraft 的时间字段使用整数微秒:1 秒 = 1000000 µs。输出 width/height 使用像素。草稿 duration 由片段推导;frameRate 当前只读,没有设置帧率 command。expectedRevision 是草稿版本号,不是时间。",
|
|
77
|
+
"3D 关键帧与动作片段 startFrame/durationFrames 使用帧,timeline.fps 定义帧时长;move_keyframes.deltaFrames 是整数帧偏移。动作片段 trimStart/trimEnd 使用源动画秒数,可带小数,不是帧。",
|
|
78
|
+
"通用 generate-video --duration 使用整数秒,由该视频入口处理;它不会在本地把故事板 duration-ms 标签自动转换成该参数。其模型/时长能力不能替代故事板当前模型约束。",
|
|
79
|
+
"ID 从所属查询取得:Timeline 的轨道/片段/来源 ID 由 xyq.timeline.query 返回;3D 对象/动画 ID 由 xyq.scene3d.query 返回。外层 nodeId 始终从当前 Canvas 节点查询取得。",
|
|
80
|
+
].join("\n\n"),
|
|
81
|
+
next_commands: [
|
|
82
|
+
"pippit-tool-cli canvas command guide storyboard",
|
|
83
|
+
"pippit-tool-cli canvas command guide timeline",
|
|
84
|
+
"pippit-tool-cli canvas command guide scene3d",
|
|
85
|
+
"pippit-tool-cli canvas command describe xyq.scene3d.apply --operation move_keyframes",
|
|
86
|
+
],
|
|
87
|
+
examples: [
|
|
88
|
+
{
|
|
89
|
+
description: "仅作换算:在 30 fps 的 3D 时间轴上,持续 2 秒对应 60 帧;不是字段更新请求。",
|
|
90
|
+
conversion: { seconds: 2, milliseconds: 2000, microseconds: 2000000, fps: 30, frames: 60 },
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
related: ["storyboard", "timeline", "scene3d"],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: "timeline",
|
|
97
|
+
summary: "查询多轨节点、草稿版本和素材来源,再预演现有编辑命令。",
|
|
98
|
+
content: [
|
|
99
|
+
"多轨的外层 Canvas 节点类型是 biz/timeline-composition,创建工厂 nodeKind 是 timeline-composition。编辑内容位于节点引用的独立 TimelineDraft;create_biz_node 的 initialData 被该工厂忽略,工厂创建空草稿。先 list 确认命令可用,再按 node-kind 查看工厂参数。",
|
|
100
|
+
"已有节点先运行 xyq.timeline.query。nodeId 来自 Canvas 节点查询,expectedRevision 必须使用此次查询的 draft.revision,轨道/片段/来源 ID 使用草稿中的真实 ID。版本不匹配会中止写入;重新查询后基于新状态准备操作。",
|
|
101
|
+
"xyq.timeline.apply.commands 接受现有 Timeline SDK 命令的 {type,payload} 数组。用 describe --operation 按 type 查看单项参数,schema 显式导出完整集合。dryRun:true 执行同一业务校验与原子预演,不保存 Canvas 状态。",
|
|
102
|
+
"时间字段使用整数微秒,1 秒 = 1000000 µs;输出尺寸使用像素。总时长由片段推导,frameRate 当前只读。编辑器会按既有规则闭合主视频轨空隙并删除空副轨,不应假定编辑后草稿只改一处字段。",
|
|
103
|
+
"新增素材需复用已有来源,或关联真实 Canvas 素材节点;不能发明 source ID 或任意 URL 来替代素材解析。渲染、导出、上传与生成需要各自现有运行环境,不包含在草稿编辑命令内。",
|
|
104
|
+
].join("\n\n"),
|
|
105
|
+
next_commands: [
|
|
106
|
+
"pippit-tool-cli canvas command describe create_biz_node --node-kind timeline-composition",
|
|
107
|
+
'pippit-tool-cli canvas command run xyq.timeline.query --canvas-id CANVAS_ID --input \'{"nodeId":"TIMELINE_NODE_ID"}\'',
|
|
108
|
+
"pippit-tool-cli canvas command describe xyq.timeline.apply --operation set_output_size",
|
|
109
|
+
"pippit-tool-cli canvas command schema xyq.timeline.apply",
|
|
110
|
+
],
|
|
111
|
+
examples: [
|
|
112
|
+
{
|
|
113
|
+
description: "将查询得到的 draft.revision 替换示例 0,并填写真实 nodeId;先预演输出尺寸。",
|
|
114
|
+
command:
|
|
115
|
+
"pippit-tool-cli canvas command run xyq.timeline.apply --canvas-id CANVAS_ID --file ./timeline-input.json",
|
|
116
|
+
input: {
|
|
117
|
+
nodeId: "TIMELINE_NODE_ID",
|
|
118
|
+
expectedRevision: 0,
|
|
119
|
+
commands: [{ type: "set_output_size", payload: { width: 1920, height: 1080 } }],
|
|
120
|
+
dryRun: true,
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
related: ["time", "scene3d"],
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: "scene3d",
|
|
128
|
+
summary: "3D 导演文档、对象 ID、空间与动画单位及编辑边界。",
|
|
129
|
+
content: [
|
|
130
|
+
"3D 导演台由外层 Canvas 节点和独立导演文档组成。create_biz_node 使用 nodeKind: scene3d;先 list 确认当前安装版本支持的命令,再 describe --node-kind 查看工厂参数。",
|
|
131
|
+
"xyq.scene3d.query 的 nodeId 是外层 Canvas 节点 ID;返回的对象、摄像机、关键帧等 ID 属于导演文档,不能与 Canvas 节点 ID 混用。先查询文档与可动画属性通道,再引用已有 ID。创建新对象时选文档内未占用的 ID。",
|
|
132
|
+
"xyq.scene3d.apply.operations 是 {command,args} 数组,command 使用现有 Scene3D SDK 命令名。describe --operation 查看一种操作及其 args,schema 可导出完整输入。dryRun:true 预演编辑,不改变原文档。",
|
|
133
|
+
"transform.position 使用米,rotation 为 XYZ Euler 角度,scale 为无单位 XYZ 倍率;camera FOV 使用角度。几何体 theta/phi/arc 等参数使用弧度,以对应 kind 的 schema 为准。",
|
|
134
|
+
"关键帧与 startFrame/durationFrames 使用帧,帧时长取决于 timeline.fps;动作片段 trimStart/trimEnd 使用源动画秒数,可为小数。不要把微秒、毫秒或旋转弧度填入帧或角度字段。",
|
|
135
|
+
"这套命令编辑导演文档;截图、渲染导出、上传与生成需要相应运行环境和现有入口。资源/动作引用应来自已查询到的真实资源,不用临时 URL 或虚构 ID 代替。",
|
|
136
|
+
].join("\n\n"),
|
|
137
|
+
next_commands: [
|
|
138
|
+
"pippit-tool-cli canvas command describe create_biz_node --node-kind scene3d",
|
|
139
|
+
'pippit-tool-cli canvas command run xyq.scene3d.query --canvas-id CANVAS_ID --input \'{"nodeId":"DIRECTOR_NODE_ID"}\'',
|
|
140
|
+
"pippit-tool-cli canvas command describe xyq.scene3d.apply --operation create_node",
|
|
141
|
+
"pippit-tool-cli canvas command schema xyq.scene3d.apply",
|
|
142
|
+
],
|
|
143
|
+
examples: [
|
|
144
|
+
{
|
|
145
|
+
description: "填入真实外层 nodeId,并确保 camera-new 在导演文档内未占用,再预演创建摄像机。",
|
|
146
|
+
command:
|
|
147
|
+
"pippit-tool-cli canvas command run xyq.scene3d.apply --canvas-id CANVAS_ID --file ./scene3d-input.json",
|
|
148
|
+
input: {
|
|
149
|
+
nodeId: "DIRECTOR_NODE_ID",
|
|
150
|
+
operations: [{ command: "create_node", args: { kind: "camera", id: "camera-new", name: "Close-up" } }],
|
|
151
|
+
dryRun: true,
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
related: ["time", "timeline"],
|
|
156
|
+
},
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
function listGuides() {
|
|
160
|
+
return GUIDES.map(({ name, summary }) => ({ name, summary }));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function getGuide(name) {
|
|
164
|
+
const guide = GUIDES.find((item) => item.name === name);
|
|
165
|
+
if (!guide) {
|
|
166
|
+
throw new Error(`未知的画布指南:${String(name)}。运行 pippit-tool-cli canvas command guide 查看主题。`);
|
|
167
|
+
}
|
|
168
|
+
return JSON.parse(JSON.stringify(guide));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = { listGuides, getGuide };
|
|
@@ -8,6 +8,7 @@ const path = require("path");
|
|
|
8
8
|
const { spawn } = require("child_process");
|
|
9
9
|
const { createHash, randomBytes } = require("crypto");
|
|
10
10
|
const { pathToFileURL } = require("url");
|
|
11
|
+
const { assertOutputBudget, describeEntry, listCatalog, SCHEMA_VERSION } = require("./canvas-command-discovery");
|
|
11
12
|
|
|
12
13
|
const DEFAULT_SDK_MODULE = path.join(
|
|
13
14
|
__dirname,
|
|
@@ -36,9 +37,15 @@ const CANVAS_COMMAND_PERMISSIONS = [
|
|
|
36
37
|
];
|
|
37
38
|
|
|
38
39
|
const COMMAND_HELP = `用法:
|
|
39
|
-
pippit-tool-cli canvas command list
|
|
40
|
-
pippit-tool-cli canvas command describe <command>
|
|
40
|
+
pippit-tool-cli canvas command list [--category <category>]
|
|
41
|
+
pippit-tool-cli canvas command describe <command> [--operation <name>] [--node-kind <kind>] [--path <schema.path>]
|
|
42
|
+
pippit-tool-cli canvas command guide [topic]
|
|
43
|
+
pippit-tool-cli canvas command schema [command]
|
|
41
44
|
pippit-tool-cli canvas command run <command> --canvas-id <id> [--input <JSON> | --file <path|->]
|
|
45
|
+
|
|
46
|
+
list 返回简短索引;describe 按层展示字段和可选操作,使用 --path 展开 schema_path。
|
|
47
|
+
guide 按主题说明时间、引用和工作流;schema 显式导出完整 schema(可能很大)。
|
|
48
|
+
这些发现入口无需登录;run 需要目标画布的访问权限。
|
|
42
49
|
`;
|
|
43
50
|
|
|
44
51
|
function isCanvasCommand(args) {
|
|
@@ -53,17 +60,30 @@ function parseCanvasCommandArgs(args) {
|
|
|
53
60
|
}
|
|
54
61
|
|
|
55
62
|
const action = values[0];
|
|
56
|
-
if (!new Set(["list", "describe", "run"]).has(action)) {
|
|
63
|
+
if (!new Set(["list", "describe", "schema", "guide", "run"]).has(action)) {
|
|
57
64
|
throw new Error(`未知的 canvas command 子命令:${action}`);
|
|
58
65
|
}
|
|
59
66
|
let canvasId = "";
|
|
60
67
|
let commandName = "";
|
|
61
68
|
let filePath = "";
|
|
62
69
|
let input = "";
|
|
70
|
+
let hasRunOptions = false;
|
|
71
|
+
const discovery = {};
|
|
72
|
+
const discoveryFlags = { "--category": "category", "--operation": "operation", "--node-kind": "nodeKind", "--path": "schemaPath" };
|
|
63
73
|
for (let index = 1; index < values.length; index += 1) {
|
|
64
74
|
const value = values[index];
|
|
65
75
|
if (value === "--help" || value === "-h") return { action: "help" };
|
|
76
|
+
const flag = value.split("=", 1)[0];
|
|
77
|
+
if (discoveryFlags[flag]) {
|
|
78
|
+
const next = value.includes("=") ? value.slice(flag.length + 1) : values[++index];
|
|
79
|
+
if (!next || next.startsWith("--")) throw new Error(`参数 ${flag} 缺少取值`);
|
|
80
|
+
const key = discoveryFlags[flag];
|
|
81
|
+
if (discovery[key]) throw new Error(`重复参数:${flag}`);
|
|
82
|
+
discovery[key] = next;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
66
85
|
if (value === "--canvas-id" || value === "--file" || value === "--input") {
|
|
86
|
+
hasRunOptions = true;
|
|
67
87
|
const next = values[index + 1];
|
|
68
88
|
if (next === undefined || next.startsWith("--")) {
|
|
69
89
|
throw new Error(`参数 ${value} 缺少取值`);
|
|
@@ -75,14 +95,17 @@ function parseCanvasCommandArgs(args) {
|
|
|
75
95
|
continue;
|
|
76
96
|
}
|
|
77
97
|
if (value.startsWith("--canvas-id=")) {
|
|
98
|
+
hasRunOptions = true;
|
|
78
99
|
canvasId = value.slice("--canvas-id=".length).trim();
|
|
79
100
|
continue;
|
|
80
101
|
}
|
|
81
102
|
if (value.startsWith("--file=")) {
|
|
103
|
+
hasRunOptions = true;
|
|
82
104
|
filePath = value.slice("--file=".length);
|
|
83
105
|
continue;
|
|
84
106
|
}
|
|
85
107
|
if (value.startsWith("--input=")) {
|
|
108
|
+
hasRunOptions = true;
|
|
86
109
|
input = value.slice("--input=".length);
|
|
87
110
|
continue;
|
|
88
111
|
}
|
|
@@ -91,14 +114,22 @@ function parseCanvasCommandArgs(args) {
|
|
|
91
114
|
commandName = value;
|
|
92
115
|
}
|
|
93
116
|
|
|
117
|
+
const allowed = action === "list" ? ["category"] : action === "describe" ? ["operation", "nodeKind", "schemaPath"] : [];
|
|
118
|
+
for (const key of Object.keys(discovery)) {
|
|
119
|
+
if (!allowed.includes(key)) throw new Error(`canvas command ${action} 不接受参数 ${key}`);
|
|
120
|
+
}
|
|
94
121
|
if (action === "list") {
|
|
95
|
-
if (commandName ||
|
|
96
|
-
return { action };
|
|
122
|
+
if (commandName || hasRunOptions) throw new Error("canvas command list 不接受额外参数");
|
|
123
|
+
return { action, ...discovery };
|
|
124
|
+
}
|
|
125
|
+
if (action === "schema" || action === "guide") {
|
|
126
|
+
if (hasRunOptions) throw new Error(`canvas command ${action} 不接受运行参数`);
|
|
127
|
+
return { action, commandName };
|
|
97
128
|
}
|
|
98
129
|
if (!commandName) throw new Error(`canvas command ${action} 缺少 command 名称`);
|
|
99
130
|
if (action === "describe") {
|
|
100
|
-
if (
|
|
101
|
-
return { action, commandName };
|
|
131
|
+
if (hasRunOptions) throw new Error("canvas command describe 不接受运行参数");
|
|
132
|
+
return { action, commandName, ...discovery };
|
|
102
133
|
}
|
|
103
134
|
if (!canvasId) throw new Error("canvas command run 缺少必填参数 --canvas-id");
|
|
104
135
|
if (input && filePath) throw new Error("--input 和 --file 不能同时使用");
|
|
@@ -571,9 +602,16 @@ function definitionToJSON(name, definition) {
|
|
|
571
602
|
properties[argument] = schemaToJSON(schema);
|
|
572
603
|
if (!schema[OPTIONAL_SCHEMA]) required.push(argument);
|
|
573
604
|
}
|
|
605
|
+
const inputSchema = definition.inputSchema;
|
|
574
606
|
return {
|
|
575
607
|
description: definition.description,
|
|
576
|
-
input_schema: {
|
|
608
|
+
input_schema: inputSchema ? {
|
|
609
|
+
...inputSchema,
|
|
610
|
+
...(inputSchema.properties ? { properties: Object.fromEntries(Object.entries(inputSchema.properties).map(([key, schema]) => [key, {
|
|
611
|
+
...(properties[key]?.description ? { description: properties[key].description } : {}),
|
|
612
|
+
...schema,
|
|
613
|
+
}])) } : {}),
|
|
614
|
+
} : {
|
|
577
615
|
properties,
|
|
578
616
|
...(required.length ? { required } : {}),
|
|
579
617
|
type: "object",
|
|
@@ -584,6 +622,7 @@ function definitionToJSON(name, definition) {
|
|
|
584
622
|
|
|
585
623
|
function createDefinitions(sdk, runtime, allocateNodeId) {
|
|
586
624
|
const definitions = sdk.createXyqCanvasOpencodeToolDefinitions({
|
|
625
|
+
allocateAssetId: allocateNodeId,
|
|
587
626
|
allocateNodeId,
|
|
588
627
|
runtime,
|
|
589
628
|
schema: createSchemaFactory(),
|
|
@@ -663,13 +702,19 @@ function writeJSON(stream, value) {
|
|
|
663
702
|
stream.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
664
703
|
}
|
|
665
704
|
|
|
666
|
-
function decorateCatalogEntry(
|
|
705
|
+
function decorateCatalogEntry(entry) {
|
|
667
706
|
const output = { ...entry };
|
|
668
707
|
if (entry.name === "apply_mutations") {
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
708
|
+
const properties = entry.input_schema?.properties;
|
|
709
|
+
if (properties?.atomic) {
|
|
710
|
+
output.input_schema = {
|
|
711
|
+
...entry.input_schema,
|
|
712
|
+
properties: {
|
|
713
|
+
...properties,
|
|
714
|
+
atomic: { ...properties.atomic, default: true, description: "The CLI always selects atomic execution for apply_mutations." },
|
|
715
|
+
},
|
|
716
|
+
};
|
|
717
|
+
}
|
|
673
718
|
}
|
|
674
719
|
return output;
|
|
675
720
|
}
|
|
@@ -679,7 +724,7 @@ function createPublicCatalog(sdk, definitions) {
|
|
|
679
724
|
const routes = new Map();
|
|
680
725
|
const append = (entry, route) => {
|
|
681
726
|
if (routes.has(entry.name)) return;
|
|
682
|
-
entries.push(decorateCatalogEntry(
|
|
727
|
+
entries.push(decorateCatalogEntry(entry));
|
|
683
728
|
routes.set(entry.name, route);
|
|
684
729
|
};
|
|
685
730
|
for (const [name, definition] of Object.entries(definitions)) {
|
|
@@ -688,14 +733,14 @@ function createPublicCatalog(sdk, definitions) {
|
|
|
688
733
|
for (const definition of sdk.XYQ_CANVAS_OPENCODE_MUTATION_DEFINITIONS) {
|
|
689
734
|
append({
|
|
690
735
|
description: definition.description,
|
|
691
|
-
input_schema: { description: definition.input, type: "object" },
|
|
736
|
+
input_schema: definition.inputSchema || { description: definition.input, type: "object" },
|
|
692
737
|
name: definition.kind,
|
|
693
738
|
}, { kind: "mutation", name: definition.kind });
|
|
694
739
|
}
|
|
695
740
|
for (const definition of sdk.XYQ_CANVAS_REGISTERED_COMMAND_DEFINITIONS) {
|
|
696
741
|
append({
|
|
697
742
|
description: definition.description,
|
|
698
|
-
input_schema: { description: definition.input, type: "object" },
|
|
743
|
+
input_schema: definition.inputSchema || { description: definition.input, type: "object" },
|
|
699
744
|
name: definition.name,
|
|
700
745
|
}, { kind: "registered", name: definition.name });
|
|
701
746
|
}
|
|
@@ -738,6 +783,11 @@ async function runCanvasCommand(args, options = {}) {
|
|
|
738
783
|
stdout.write(COMMAND_HELP);
|
|
739
784
|
return 0;
|
|
740
785
|
}
|
|
786
|
+
if (parsed.action === "guide") {
|
|
787
|
+
const { getGuide, listGuides } = require("./canvas-command-guides");
|
|
788
|
+
writeJSON(stdout, parsed.commandName ? getGuide(parsed.commandName) : { guides: listGuides(), next: "pippit-tool-cli canvas command guide <topic>" });
|
|
789
|
+
return 0;
|
|
790
|
+
}
|
|
741
791
|
|
|
742
792
|
const sdk = await loadSdk(options);
|
|
743
793
|
assertSdkFunctions(sdk, ["createXyqCanvasOpencodeToolDefinitions"]);
|
|
@@ -754,13 +804,21 @@ async function runCanvasCommand(args, options = {}) {
|
|
|
754
804
|
);
|
|
755
805
|
const { entries: catalog, routes } = createPublicCatalog(sdk, catalogDefinitions);
|
|
756
806
|
if (parsed.action === "list") {
|
|
757
|
-
writeJSON(stdout,
|
|
807
|
+
writeJSON(stdout, assertOutputBudget(listCatalog(catalog, parsed.category), "list"));
|
|
808
|
+
return 0;
|
|
809
|
+
}
|
|
810
|
+
if (parsed.action === "schema" && !parsed.commandName) {
|
|
811
|
+
writeJSON(stdout, { schema_version: SCHEMA_VERSION, commands: catalog });
|
|
758
812
|
return 0;
|
|
759
813
|
}
|
|
760
814
|
const catalogEntry = catalog.find((entry) => entry.name === parsed.commandName);
|
|
761
815
|
if (!catalogEntry) throw new Error(`未知的画布 command:${parsed.commandName}`);
|
|
762
816
|
if (parsed.action === "describe") {
|
|
763
|
-
writeJSON(stdout, catalogEntry);
|
|
817
|
+
writeJSON(stdout, assertOutputBudget(describeEntry(catalogEntry, parsed, catalog), "describe"));
|
|
818
|
+
return 0;
|
|
819
|
+
}
|
|
820
|
+
if (parsed.action === "schema") {
|
|
821
|
+
writeJSON(stdout, { schema_version: SCHEMA_VERSION, ...catalogEntry });
|
|
764
822
|
return 0;
|
|
765
823
|
}
|
|
766
824
|
|
|
@@ -794,6 +852,12 @@ async function runCanvasCommand(args, options = {}) {
|
|
|
794
852
|
let saveError;
|
|
795
853
|
let serializedResult;
|
|
796
854
|
const allocatedAssetIds = [];
|
|
855
|
+
const allocateAssetId = async () => {
|
|
856
|
+
const [assetId] = await assetRuntime.client.ids.allocate(1);
|
|
857
|
+
if (!assetId) throw new Error("资产服务未返回新资产 ID");
|
|
858
|
+
allocatedAssetIds.push(assetId);
|
|
859
|
+
return assetId;
|
|
860
|
+
};
|
|
797
861
|
let standalone;
|
|
798
862
|
const persistence = authStatus.credential_scope
|
|
799
863
|
? createFilePersistence({
|
|
@@ -804,8 +868,14 @@ async function runCanvasCommand(args, options = {}) {
|
|
|
804
868
|
: createMemoryPersistence();
|
|
805
869
|
try {
|
|
806
870
|
standalone = sdk.createXyqCanvasCommandRuntime({
|
|
871
|
+
allocateAssetId,
|
|
807
872
|
canvasId: parsed.canvasId,
|
|
873
|
+
command: { name: parsed.commandName, input },
|
|
808
874
|
persistence,
|
|
875
|
+
queryAsset: async (assetId) => {
|
|
876
|
+
const asset = await assetRuntime.client.assets.getAsset({ mediaType: "text", pippitAssetId: assetId });
|
|
877
|
+
return { asset: asset?.text?.content ?? null, version: asset?.version };
|
|
878
|
+
},
|
|
809
879
|
sync: { flush: { maxAttempts: 1, maxBatchSize: 1 } },
|
|
810
880
|
transportFactory: createCanvasTransportFactory({ assetRuntime, loader }),
|
|
811
881
|
});
|
|
@@ -817,12 +887,19 @@ async function runCanvasCommand(args, options = {}) {
|
|
|
817
887
|
const definitions = createDefinitions(
|
|
818
888
|
sdk,
|
|
819
889
|
{ checkpoints: checkpointStore, permissions: CANVAS_COMMAND_PERMISSIONS, store: standalone.store },
|
|
820
|
-
|
|
821
|
-
const [assetId] = await assetRuntime.client.ids.allocate(1);
|
|
822
|
-
allocatedAssetIds.push(assetId);
|
|
823
|
-
return assetId;
|
|
824
|
-
}
|
|
890
|
+
allocateAssetId
|
|
825
891
|
);
|
|
892
|
+
if (typeof standalone.prepareCommand === "function") {
|
|
893
|
+
if (route.kind === "registered") {
|
|
894
|
+
await standalone.prepareCommand(route.name, input);
|
|
895
|
+
} else if (route.name === "apply_mutations" && !input.dryRun && Array.isArray(input.mutations)) {
|
|
896
|
+
for (const mutation of input.mutations) {
|
|
897
|
+
if (mutation?.kind === "invoke_command") {
|
|
898
|
+
await standalone.prepareCommand(mutation.name, mutation.args?.[0]);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
826
903
|
try {
|
|
827
904
|
serializedResult = await executePublicCommand(route, definitions, input);
|
|
828
905
|
} catch (error) {
|
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const fs = require(
|
|
4
|
-
const path = require(
|
|
5
|
-
const { isWindows, run, runSilent } = require(
|
|
6
|
-
const { DEFAULT_PKG, installGlobalPackageSkills } = require(
|
|
7
|
-
const { reportBundledSkillTelemetry } = require(
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { isWindows, run, runSilent } = require('./platform');
|
|
6
|
+
const { DEFAULT_PKG, installGlobalPackageSkills } = require('./skills');
|
|
7
|
+
const { reportBundledSkillTelemetry } = require('./telemetry');
|
|
8
8
|
|
|
9
|
-
const VERSION = require(
|
|
9
|
+
const VERSION = require('../package.json').version.replace(/-.*$/, '');
|
|
10
|
+
|
|
11
|
+
const INSTALL_HELP = `Install or update the global Pippit CLI and its bundled skills.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
pippit-tool-cli install
|
|
15
|
+
pippit-tool-cli install --help
|
|
16
|
+
npx @pippit-dev/cli install
|
|
17
|
+
|
|
18
|
+
Requires npm, npx, network access, and permission to write the global npm package
|
|
19
|
+
and skill directories. Installs the version of this package by default.
|
|
20
|
+
PIPPIT_CLI_INSTALL_PACKAGE can override the package/version to install.
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
-h, --help Show this help without installing, updating, or sending telemetry
|
|
24
|
+
`;
|
|
10
25
|
|
|
11
26
|
function defaultInstallPackage() {
|
|
12
27
|
return `${DEFAULT_PKG}@${VERSION}`;
|
|
@@ -18,9 +33,9 @@ function installPackage() {
|
|
|
18
33
|
|
|
19
34
|
function getGloballyInstalledVersion() {
|
|
20
35
|
try {
|
|
21
|
-
const out = runSilent(
|
|
36
|
+
const out = runSilent('npm', ['list', '-g', DEFAULT_PKG], { timeout: 15000 });
|
|
22
37
|
const match = out.toString().match(/@(\d+\.\d+\.\d+[^\s]*)/);
|
|
23
|
-
return match ? match[1] :
|
|
38
|
+
return match ? match[1] : 'unknown';
|
|
24
39
|
} catch (_) {
|
|
25
40
|
return null;
|
|
26
41
|
}
|
|
@@ -28,24 +43,33 @@ function getGloballyInstalledVersion() {
|
|
|
28
43
|
|
|
29
44
|
function whichPippitToolCli() {
|
|
30
45
|
try {
|
|
31
|
-
const prefix = runSilent(
|
|
46
|
+
const prefix = runSilent('npm', ['prefix', '-g'], { timeout: 15000 }).toString().trim();
|
|
32
47
|
const bin = isWindows
|
|
33
|
-
? path.join(prefix,
|
|
34
|
-
: path.join(prefix,
|
|
48
|
+
? path.join(prefix, 'pippit-tool-cli.cmd')
|
|
49
|
+
: path.join(prefix, 'bin', 'pippit-tool-cli');
|
|
35
50
|
if (fs.existsSync(bin)) return bin;
|
|
36
51
|
} catch (_) {
|
|
37
52
|
// Fall back to PATH lookup.
|
|
38
53
|
}
|
|
39
54
|
|
|
40
55
|
try {
|
|
41
|
-
const cmd = isWindows ?
|
|
42
|
-
return runSilent(cmd, [
|
|
56
|
+
const cmd = isWindows ? 'where' : 'which';
|
|
57
|
+
return runSilent(cmd, ['pippit-tool-cli']).toString().split('\n')[0].trim();
|
|
43
58
|
} catch (_) {
|
|
44
59
|
return null;
|
|
45
60
|
}
|
|
46
61
|
}
|
|
47
62
|
|
|
48
|
-
function main() {
|
|
63
|
+
function main(args = process.argv.slice(2)) {
|
|
64
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
65
|
+
console.log(INSTALL_HELP);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (args.length > 0) {
|
|
69
|
+
console.error(`Unknown install argument: ${args[0]}. Run pippit-tool-cli install --help.`);
|
|
70
|
+
process.exitCode = 1;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
49
73
|
const pkg = installPackage();
|
|
50
74
|
const installed = getGloballyInstalledVersion();
|
|
51
75
|
if (installed) {
|
|
@@ -53,33 +77,33 @@ function main() {
|
|
|
53
77
|
} else {
|
|
54
78
|
console.log(`Installing ${pkg} globally...`);
|
|
55
79
|
}
|
|
56
|
-
run(
|
|
80
|
+
run('npm', ['install', '-g', pkg], {
|
|
57
81
|
timeout: 120000,
|
|
58
|
-
env: { ...process.env, PIPPIT_CLI_SKIP_SKILLS:
|
|
82
|
+
env: { ...process.env, PIPPIT_CLI_SKIP_SKILLS: '1' },
|
|
59
83
|
});
|
|
60
84
|
|
|
61
|
-
console.log(
|
|
85
|
+
console.log('Installing pippit-tool-cli skills...');
|
|
62
86
|
try {
|
|
63
87
|
installGlobalPackageSkills(DEFAULT_PKG);
|
|
64
88
|
} catch (err) {
|
|
65
89
|
if (!installed) {
|
|
66
90
|
throw err;
|
|
67
91
|
}
|
|
68
|
-
console.log(
|
|
69
|
-
run(
|
|
92
|
+
console.log('Existing global package does not contain skills; reinstalling...');
|
|
93
|
+
run('npm', ['install', '-g', pkg], { timeout: 120000 });
|
|
70
94
|
installGlobalPackageSkills(DEFAULT_PKG);
|
|
71
95
|
}
|
|
72
96
|
|
|
73
97
|
const bin = whichPippitToolCli();
|
|
74
98
|
if (!bin) {
|
|
75
|
-
console.error(
|
|
99
|
+
console.error('pippit-tool-cli was installed, but no global command was found in npm prefix.');
|
|
76
100
|
console.error("Check that npm's global bin directory is in PATH.");
|
|
77
101
|
process.exit(1);
|
|
78
102
|
}
|
|
79
103
|
|
|
80
104
|
console.log(`pippit-tool-cli is ready: ${bin}`);
|
|
81
|
-
reportBundledSkillTelemetry(
|
|
82
|
-
console.log(
|
|
105
|
+
reportBundledSkillTelemetry('install', 'npx_install');
|
|
106
|
+
console.log('Try: pippit-tool-cli short-drama +submit-run --message "写一个短剧开头"');
|
|
83
107
|
}
|
|
84
108
|
|
|
85
109
|
if (require.main === module) {
|