diff --git a/README.md b/README.md index 0e3e53e..63c0145 100644 --- a/README.md +++ b/README.md @@ -9,210 +9,50 @@ | 技能 | 说明 | 路径 | |-------|-------------|------| | `xyq-short-drama-skill` | 短剧工作流技能,支持提交创作任务、上传参考文件、查询进度、列出会话文件和下载产物。 | `skills/short-drama/` | -| `xyq-skill` | 通用创作技能,支持 NestAgent 图片/视频生成与编辑、个人积分余额查询,并在视频模型直出时调用 `pippit-tool-cli generate-video`。 | `skills/xyq-nest-skill/` | +| `xyq-skill` | 图片生成与参考图编辑、视频生成、视频超分与擦字幕、异步结果交付、个人 Canvas 编辑、积分查询及登录授权。 | `skills/xyq-nest-skill/` | ### 技能路由 -- 小云雀积分余额、剩余积分或 credits 查询由 `xyq-skill` 调用 `pippit-tool-cli get-credit-balance`,直接展示个人有效积分余额,无需创建会话或轮询。 -- 通用图片/视频生成、编辑和复杂参考素材编排使用 `xyq-skill`。 -- 用户明确要求视频模型直出、指定视频模型或直接调用 CLI 时,由 `xyq-skill` 调用 `pippit-tool-cli generate-video`,再用 `query-result` 查询和下载结果。 -- 短剧生成、续写、改写、人物设定、分集创作和短剧会话文件处理使用 `xyq-short-drama-skill`,不要与通用创作流程混用。 +- 图片生成与参考图编辑、视频生成(含首尾帧和参考素材)、视频超分、擦字幕、结果查询、个人 Canvas 编辑、积分和授权由 `xyq-skill` 处理。 +- 短剧生成、续写、改写、人物设定、分集创作和短剧会话文件处理使用 `xyq-short-drama-skill`。 -两份技能需要用户补充、选择或确认时,优先调用宿主的结构化提问工具:Codex 使用 `request_user_input`,WorkBuddy 使用 `ask_user_question`,Trae 和其他宿主使用实际暴露的同类工具;没有同类工具时退回普通聊天提问。 +需要补充、选择或确认时,使用宿主实际暴露且当前模式允许的工具:Codex 的 `request_user_input` / `request_user_input_async`、WorkBuddy 的 `ask_user_question`;不可用时用普通聊天。 -## 通用 NestAgent 技能 +## 小云雀图片、视频与媒体处理技能 -`xyq-skill` 通过接入小云雀 NestAgent 的综合创作能力,实现 AI 图片/视频生成、编辑、风格转换、图片/视频/mp3或wav音频文件上传、进度查询和结果下载;视频模型直出请求直接使用 `pippit-tool-cli generate-video`。 +入口:[skills/xyq-nest-skill/SKILL.md](skills/xyq-nest-skill/SKILL.md)。普通生成请求也直接使用对应 CLI;素材路径交给命令内部上传,异步查询自动下载,最后通过宿主交付真实媒体附件。 -### 功能特性 +| 操作 | CLI | 文档 | +| --- | --- | --- | +| 登录授权 | `status` / `login` / `logout` | [授权](skills/xyq-nest-skill/commands/auth.md) | +| 个人 Canvas 画布与节点编辑 | `canvas` | [画布](skills/xyq-nest-skill/commands/canvas.md) | +| 生图、参考图编辑 | `generate-image` | [图片](skills/xyq-nest-skill/commands/generate-image.md) | +| 生视频、首尾帧 | `generate-video` | [视频](skills/xyq-nest-skill/commands/generate-video.md) | +| 视频超分 | `video-super-resolution` | [超分](skills/xyq-nest-skill/commands/video-super-resolution.md) | +| 擦字幕 | `erase-video-subtitle` | [擦字幕](skills/xyq-nest-skill/commands/erase-video-subtitle.md) | +| 查询并下载结果 | `query-result` | [查询](skills/xyq-nest-skill/commands/query-result.md) | +| 查积分 | `get-credit-balance` | [积分](skills/xyq-nest-skill/commands/get-credit-balance.md) | -| 功能 | 说明 | -|------|------| -| 创建会话 / 发送消息 | 向小云雀发送自然语言指令,生成图片或视频。 | -| 查询会话进展 | 增量拉取会话消息,轮询创作进度和产物结果。 | -| 积分余额查询 | 调用 `get-credit-balance`,读取 `total_remain_amount` 并展示个人有效积分余额。 | -| 上传文件 | 上传图片/视频/mp3或wav音频到小云雀资产库,获取 `asset_id` 用于编辑和参考。 | -| 下载结果 | 批量下载生成的图片/视频到本地,支持并行下载。 | -| 视频模型直出 | 调用 `generate-video` 提交请求,展示 `web_thread_link`,再用 `query-result` 查询并下载视频。 | - -小云雀平台能力覆盖: - -- 生成:文生图、文生视频、图生视频、视频续写。 -- 编辑:局部修改、元素替换、镜头调整、风格迁移。 -- 复杂创作:复刻视频/TVC/宣传片、音乐 MV 生成、产品展示片制作。 - -### 配置 - -单独上传 `xyq-skill` ZIP 时,保留 Skill 内的 `scripts/ensure-cli.js`。每次开始执行 Skill 任务先运行: +### 安装与执行 ```bash node /path/to/xyq-skill/scripts/ensure-cli.js ``` -环境需要 Node.js 16+ 和 Python 3;首次安装或自动升级时需要 npm、`curl`、系统解压工具(macOS/Linux 的 `tar`,Windows 的 PowerShell)及访问 npm 源和 GitHub Release 的网络。脚本优先复用 PATH 或自身缓存中命令齐全的 CLI;均不存在或缺少必需命令时获取 `@pippit-dev/cli@latest`,安装到 `~/.cache/pippit-tool-cli/xyq-skill/<平台>-<架构>/current`,返回 `{ "cli_path": "CLI绝对路径", "version": "实际安装版本" }`。后续示例中的 `pippit-tool-cli` 替换为该绝对路径;同一任务内复用,不在轮询时重复安装。已有 CLI 缺少关键命令时自动升级;升级后的缓存可被后续任务复用,避免 PATH 旧版本触发重复下载。每次调用最多下载安装一次,升级失败保留原安装;最新版本仍缺少必需命令时报告阻塞。 - -新入口 `node scripts/install-cli.js` 只安装 npm 包对应版本的 CLI 二进制,不安装或清理全局 Skill。ZIP 安装脚本先以 `--ignore-scripts` 获取 npm 包,再调用这个入口。发布包含新入口的 npm 包及对应 GitHub Release 后,ZIP 的最新版本安装流程才能完整使用。 - -`submit-run` 和 `upload-file` 使用原生 CLI 登录凭证(`pippit-tool-cli login`),也可通过 `XYQ_ACCESS_KEY` 显式覆盖。保留的查询脚本 `get_thread.py` 仍需配置同一用户的 Bearer 凭证: - -```bash -export XYQ_ACCESS_KEY="" -``` - -CLI 和 Python 脚本携带用户密钥的 API 请求地址固定为 `https://xyq.jianying.com`,不接受 `XYQ_OPENAPI_BASE` / `XYQ_BASE_URL` 覆盖。CLI 拒绝 API 跨域重定向,Python API 脚本禁止自动重定向。上传只在 Authorization 请求头携带密钥。 - -### 创建会话 / 发送消息 - -```bash -# 创建新会话 -pippit-tool-cli submit-run --message "生一个动漫视频" - -# 向已有会话发送消息 -pippit-tool-cli submit-run \ - --message "再生成一个故事视频" \ - --thread-id THREAD_ID - -# 携带参考文件发送 -pippit-tool-cli submit-run \ - --message "参考这个视频做修改" \ - --asset-ids asset_id1 --asset-ids asset_id2 -``` - -| 参数 | 必填 | 说明 | -|------|------|------| -| `--message` | 是 | 非空白的创作指令,原样发送。 | -| `--thread-id` | 否 | 已有会话 ID,不传则创建新会话。 | -| `--asset-ids` | 否 | 每次传一个资产 ID;多个素材重复该参数。 | - -返回示例: - -```json -{ - "thread_id": "90f05e0c-...", - "run_id": "abc123-...", - "web_thread_link": "https://xyq.jianying.com/..." -} -``` - -### 查询会话进展 - -```bash -python3 skills/xyq-nest-skill/scripts/get_thread.py \ - --thread-id THREAD_ID \ - --run-id RUN_ID \ - --after-seq 0 -``` - -| 参数 | 必填 | 说明 | -|------|------|------| -| `--thread-id` | 是 | 会话 ID。 | -| `--run-id` | 否 | 运行 ID。 | -| `--after-seq` | 否 | 增量拉取起始序号,默认 `0`。 | - -脚本会返回会话消息和产物条目。后续轮询时,根据已获取消息更新 `after_seq`。 - -### 上传文件 - -使用顶层 `upload-file --path`,每次上传一个本地图片、视频或 MP3/WAV 音频文件,文件须小于 500 MB(500000000 字节)。成功返回 `{"asset_id":"..."}`,可直接传给 `submit-run --asset-ids`。 - -```bash -# 上传图片 -pippit-tool-cli upload-file --path /path/to/image.png - -# 上传视频 -pippit-tool-cli upload-file --path /path/to/video.mp4 - -# 上传音频 -pippit-tool-cli upload-file --path /path/to/audio.mp3 -``` - -仅支持 `image/*`、`video/*` 和 `.mp3/.wav` 音频文件,单文件大小限制 500 MB。 - -返回示例: - -```json -{ - "asset_id": "asset_xxx" -} -``` - -### 下载结果 - -每个产物 URL 调用一次 CLI,`--output-path` 必须包含文件名: +保存返回的 `cli_path`,后续用带引号的绝对路径替换示例中的命令名。同一任务复用路径;已有命令齐全的 CLI 不下载,缺少必需命令时自动升级。ZIP 应包含整个 Skill 目录,具体环境条件与故障处理见 [安装说明](skills/xyq-nest-skill/scripts/install.md)。`node scripts/install-cli.js` 是 npm 包内仅安装 CLI 的入口,不安装或清理全局 Skill。 -```bash -pippit-tool-cli download-result \ - --url "URL1" \ - --output-path "./xyq_output/storyboard_01.png" -``` - -| 参数 | 必填 | 说明 | -|------|------|------| -| `--url` | 是 | 单个产物的下载 URL。 | -| `--output-path` | 是 | 包含文件名的本地目标路径。 | -| `--updated-at` | 否 | 远端文件真实更新时间(Unix 秒),用于判断是否覆盖已有文件。 | -| `--workers` | 否 | 下载 worker 数,默认 `5`;当前单 URL 调用实际只使用一个 worker。 | - -Skill 沿用用户指定的输出目录,未指定时使用 `./xyq_output`,按 URL 列表顺序从 `01` 编号,组成 `前缀_01.ext`(无前缀时为 `01.ext`)。扩展名优先取 URL 查询参数 `filename`,其次取 URL 路径,无法取得时使用 `.bin`。多文件逐项调用,可最多并行执行 5 个命令;重试保持原目标路径。 - -下载成功返回示例: - -```json -{ - "output_path": "./xyq_output/storyboard_01.png", - "downloaded": ["./xyq_output/storyboard_01.png"] -} -``` +Canvas 任务使用 `ensure-cli.js --canvas`,额外返回 `canvas_entry`;原生资产命令使用 `cli_path`,语义命令通过 `node "CANVAS_ENTRY" canvas command ...` 执行。检查会真实加载 npm 内的离线命令目录,避免把原生帮助误当作运行时已就绪。画布编辑使用独立的 [查询、编辑与回读流程](skills/xyq-nest-skill/workflows/canvas-edit.md),不套用媒体轮询。 -未传 `--updated-at` 时,CLI 默认跳过已有文件,返回 `already_exist`,此时 `downloaded` 为 `null`;传入真实更新时间后,仅当本地文件修改时间早于该时间时覆盖更新。跳过不代表已校验本地内容与远端一致。 - -下载失败时命令以非零退出码返回错误,不保证输出 JSON。Skill 汇总各次调用的下载成功、已存在跳过和失败项,只对失败项重试一次;仍有失败时明确报告未完整交付。 - -### 典型示例 - -文生视频: - -```text -1. pippit-tool-cli submit-run --message "生成一个赛博朋克风格的城市夜景视频" -2. 每 10 秒轮询: - get_thread.py --thread-id THREAD_ID --run-id RUN_ID --after-seq SEQUENCE -3. 拿到产物 URL 后下载: - pippit-tool-cli download-result --url "URL1" --output-path "./output/cyberpunk_01.mp4" - pippit-tool-cli download-result --url "URL2" --output-path "./output/cyberpunk_02.mp4" -``` - -编辑已有视频: - -```text -1. pippit-tool-cli upload-file --path /path/to/video.mp4 -2. pippit-tool-cli submit-run --message "把背景换成星空" --asset-ids asset_id -3. 按文生视频流程轮询和下载。 -``` - -多参考图/视频生成: - -```text -1. pippit-tool-cli upload-file --path /path/to/ref1.png -2. pippit-tool-cli upload-file --path /path/to/ref2.png -3. pippit-tool-cli upload-file --path /path/to/ref3.mp4 -4. pippit-tool-cli submit-run --message "根据参考图和视频生成科普故事视频" --asset-ids asset_id1 --asset-ids asset_id2 --asset-ids asset_id3 -5. 按文生视频流程轮询和下载。 -``` - -在已有会话中追加需求: - -```text -1. pippit-tool-cli submit-run --message "把刚才的视频加个片头" --thread-id EXISTING_THREAD_ID -2. 使用新的 run_id 轮询和下载。 -``` +登录后选择生成或处理命令,统一接入 [异步结果与媒体交付](skills/xyq-nest-skill/workflows/async-delivery.md)。完整基础案例见 [生成一张图并交付](skills/xyq-nest-skill/examples/generate-and-deliver.md),组合案例由入口按需引导。 -轮询策略: +### 模块维护 -- 间隔:每 10 秒查询一次。 -- 增量拉取:首次 `--after-seq 0`,后续根据已获取消息数更新 seq。 -- 意图确认:如果智能体追问用户,先展示问题,再用同一个 `thread_id` 提交用户回复。 -- 超时:连续轮询 48 小时无结果则停止。 -- 错误重试:单次失败可重试 1 次,连续 3 次失败则停止。 +- `SKILL.md` 维护能力边界、意图到命令的路由及必要执行规则。 +- `commands/` 每个模块维护适用场景、必填与可选参数、最小调用、真实返回契约及失败处理;授权相关命令合并在同一文档。 +- `workflows/` 维护共用轮询与媒体交付规则;`examples/` 展示基础完整流程及易混淆的组合场景,引用规则,不复制参数手册。 +- 新增 CLI 时补命令文档、入口路由、`ensure-cli.js` 必需命令集合和安装测试;声明是同步结果还是异步任务,是否需要附加运行时及其检查方式,按需接入交付流程,补正常、缺输入和易混淆场景用例。 +- 文档使用 Skill 内相对链接,打包时保留结构。规范副本位于 `skills/xyq-nest-skill/`,项目发现入口 `.agents/skills/xyq-skill` 指向该目录。 +- 修改后运行 `node scripts/skills.test.js` 与 `node scripts/install-cli.test.js`,检查引用完整、保留命令与安装检查一致及缺命令升级/缓存复用;Agent 行为用例见 [测试场景](skills/xyq-nest-skill/tests/agent_test_cases.md)。这些检查不代表真实生成已验证。 ## 短剧工作流技能 @@ -451,4 +291,4 @@ pippit-tool-cli query-result \ 原生 CLI 命令通过 `pippit-tool-cli login` 打开小云雀网页授权,并把本机设备专属凭证保存到系统安全凭证库;Access Key 不会显示在终端。可用 `pippit-tool-cli status` 查看状态、`pippit-tool-cli logout` 清除本机登录。 -CI 或 Agent 可继续显式设置 `XYQ_ACCESS_KEY`,它会覆盖本机网页登录凭证;配置错误时不会静默回退到个人登录。`skills/xyq-nest-skill/scripts` 下的独立 Python 脚本尚未接入原生 CLI 凭证库,当前仍需要该环境变量。 +CI 或 Agent 可继续显式设置 `XYQ_ACCESS_KEY`,它会覆盖本机网页登录凭证;配置错误时不会静默回退到个人登录。会话提交和查询共享上述凭据。 diff --git a/package-lock.json b/package-lock.json index abd61fd..707d14d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@pippit-dev/cli", - "version": "1.0.23", + "version": "1.0.25", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pippit-dev/cli", - "version": "1.0.23", + "version": "1.0.25", "hasInstallScript": true, "license": "MIT", "bin": { diff --git a/package.json b/package.json index 4a9bcda..c9ce5aa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pippit-dev/cli", - "version": "1.0.23", + "version": "1.0.25", "description": "Pippit CLI", "bin": { "pippit-tool-cli": "scripts/run.js" diff --git a/scripts/install-cli.test.js b/scripts/install-cli.test.js index 67ef1c7..7a4b21d 100644 --- a/scripts/install-cli.test.js +++ b/scripts/install-cli.test.js @@ -95,7 +95,7 @@ function checkInstaller() { assert.deepStrictEqual(effects, []); } -function bootstrapFixture({ failure, platform = "linux", version = "9.9.9" } = {}) { +function bootstrapFixture({ failure, platform = "linux", version = "9.9.9", missingCommand = "generate-image", canvas = false } = {}) { const dirs = []; const calls = []; const home = fs.mkdtempSync(path.join(root, "user-")); @@ -108,7 +108,7 @@ function bootstrapFixture({ failure, platform = "linux", version = "9.9.9" } = { dirs, calls, npmDir, options: { main: true, - process: { platform, execPath: nodePath, env: { XYQ_ACCESS_KEY: "test-key", PATH: npmDir } }, + process: { platform, execPath: nodePath, argv: [nodePath, bootstrap, ...(canvas ? ["--canvas"] : [])], env: { XYQ_ACCESS_KEY: "test-key", PATH: npmDir } }, modules: { os: { homedir: () => home }, child_process: { @@ -134,22 +134,31 @@ function bootstrapFixture({ failure, platform = "linux", version = "9.9.9" } = { fs.mkdirSync(path.join(pkg, "scripts"), { recursive: true }); fs.writeFileSync(path.join(pkg, "package.json"), JSON.stringify({ version })); if (failure !== "missing-installer") fs.writeFileSync(path.join(pkg, "scripts/install-cli.js"), "fixture"); + if (failure !== "missing-canvas-entry") fs.writeFileSync(path.join(pkg, "scripts/run.js"), "fixture"); } else if (args[0].endsWith("install-cli.js")) { assert.strictEqual(command, nodePath); if (failure === "download") throw Object.assign(new Error("network"), { status: 1 }); const bin = path.resolve(path.dirname(args[0]), "../bin"); fs.mkdirSync(bin, { recursive: true }); fs.writeFileSync(path.join(bin, platform === "win32" ? "pippit-tool-cli.exe" : "pippit-tool-cli"), "binary fixture"); + } else if (args[0].endsWith("run.js")) { + assert.strictEqual(command, nodePath); + assert.deepStrictEqual(Array.from(args.slice(1)), ["canvas", "command", "list"]); + if (failure === "canvas-runtime") throw Object.assign(new Error("runtime missing"), { status: 1 }); + if (failure === "canvas-json") return Buffer.from("invalid JSON"); + const commands = failure === "canvas-catalog" ? [] : [{ name: "get_snapshot" }, { name: "create_biz_node" }]; + return Buffer.from(JSON.stringify({ commands })); } else if (args[0] === "--version") { assert(path.isAbsolute(command)); return Buffer.from(failure === "version" ? "0.0.1\n" : `${version}\n`); } else { - assert.strictEqual(args[1], "--help"); - if (args[0] === "upload-file" && fs.existsSync(command) + assert.strictEqual(args[args.length - 1], "--help"); + const commandName = args.slice(0, -1).join(" "); + if (commandName === missingCommand && fs.existsSync(command) && fs.readFileSync(command, "utf8").includes("missing-command")) { throw Object.assign(new Error("unknown command"), { status: 1 }); } - if (failure === args[0]) throw Object.assign(new Error("unknown command"), { status: 1 }); + if (failure === commandName) throw Object.assign(new Error("unknown command"), { status: 1 }); } return Buffer.from(""); }, @@ -172,7 +181,7 @@ function checkBootstrap() { assert(result.cli_path.endsWith(platform === "win32" ? "pippit-tool-cli.exe" : "pippit-tool-cli")); assert.strictEqual(result.cli_path, JSON.parse(next.output[0]).cli_path); assert.strictEqual(fixture.dirs.length, 1, "Subsequent invocations must reuse the cached CLI"); - for (const command of ["login", "submit-run", "upload-file", "download-result", "query-result", + for (const command of ["status", "login", "logout", "query-result", "generate-image", "generate-video", "video-super-resolution", "erase-video-subtitle", "get-credit-balance"]) { assert.strictEqual(fixture.calls.filter((call) => call.args[0] === command).length, 2); } @@ -202,6 +211,31 @@ function checkBootstrap() { assert.strictEqual(fixture.dirs.length, missingCommand ? 1 : 0, "An old PATH CLI must not cause repeated upgrades when cache is usable"); } } + // Every retained command participates in compatibility checks and cache reuse. + for (const missingCommand of ["status", "login", "logout", "query-result", "generate-image", + "generate-video", "video-super-resolution", "erase-video-subtitle", "get-credit-balance"]) { + const fixture = bootstrapFixture({ missingCommand }); + const existing = path.join(fixture.npmDir, "pippit-tool-cli"); + fs.writeFileSync(existing, "missing-command"); + const upgraded = load(bootstrap, fixture.options); + assert.strictEqual(upgraded.proc.exitCode, 0, upgraded.errors.join("\n")); + assert.notStrictEqual(JSON.parse(upgraded.output[0]).cli_path, existing); + assert.strictEqual(fixture.dirs.length, 1); + assert.strictEqual(load(bootstrap, fixture.options).proc.exitCode, 0); + assert.strictEqual(fixture.dirs.length, 1, `Reuse cache after upgrading ${missingCommand}`); + } + // Removed Skill commands must not force an otherwise usable CLI to upgrade. + for (const missingCommand of ["submit-run", "get-thread", "upload-file", "download-result"]) { + const fixture = bootstrapFixture({ missingCommand }); + const existing = path.join(fixture.npmDir, "pippit-tool-cli"); + fs.writeFileSync(existing, "missing-command"); + const reused = load(bootstrap, fixture.options); + assert.strictEqual(reused.proc.exitCode, 0, reused.errors.join("\n")); + assert.strictEqual(JSON.parse(reused.output[0]).cli_path, existing); + assert.strictEqual(fixture.dirs.length, 0); + assert(!fixture.calls.some((call) => call.args[0] === missingCommand)); + } + const outdatedCache = bootstrapFixture(); const initial = load(bootstrap, outdatedCache.options); const cachedPath = JSON.parse(initial.output[0]).cli_path; @@ -222,13 +256,13 @@ function checkBootstrap() { assert.strictEqual(load(bootstrap, outdatedCache.options).proc.exitCode, 1); assert.strictEqual(fs.readFileSync(cachedPath, "utf8"), "missing-command", "Failed upgrades must preserve the original cache"); - const failedUpgrade = bootstrapFixture({ failure: "upload-file" }); + const failedUpgrade = bootstrapFixture({ failure: "generate-image" }); const oldPath = path.join(failedUpgrade.npmDir, "pippit-tool-cli"); fs.writeFileSync(oldPath, "missing-command"); assert.strictEqual(load(bootstrap, failedUpgrade.options).proc.exitCode, 1); assert.strictEqual(failedUpgrade.dirs.length, 1, "An incompatible latest release must fail without an upgrade loop"); assert.strictEqual(fs.readFileSync(oldPath, "utf8"), "missing-command"); - for (const failure of ["npm", "missing-installer", "download", "version", "upload-file"]) { + for (const failure of ["npm", "missing-installer", "download", "version", "status", "logout", "generate-image", "query-result"]) { const fixture = bootstrapFixture({ failure }); const result = load(bootstrap, fixture.options); assert.strictEqual(result.proc.exitCode, 1, failure); @@ -242,9 +276,81 @@ function checkBootstrap() { assert.strictEqual(help.calls.length, 0, "Help must not install or download anything"); } +function checkCanvasBootstrap() { + for (const platform of ["darwin", "linux", "win32"]) { + const fixture = bootstrapFixture({ platform, canvas: true }); + const first = load(bootstrap, fixture.options); + assert.strictEqual(first.proc.exitCode, 0, first.errors.join("\n")); + const result = JSON.parse(first.output[0]); + assert(path.isAbsolute(result.canvas_entry)); + assert(fs.existsSync(result.canvas_entry), "Return the relocated npm entry, not the temporary install path"); + const next = load(bootstrap, fixture.options); + assert.strictEqual(next.proc.exitCode, 0, next.errors.join("\n")); + assert.strictEqual(JSON.parse(next.output[0]).canvas_entry, result.canvas_entry); + assert.strictEqual(fixture.dirs.length, 1); + for (const command of ["create", "get", "allocate", "upload", "apply"]) { + assert.strictEqual(fixture.calls.filter((call) => call.args.join(" ") === `canvas ${command} --help`).length, 2); + } + assert.strictEqual(fixture.calls.filter((call) => call.args[0].endsWith("run.js")).length, 2); + + // Removing only the runtime entry upgrades Canvas, while normal media use still reuses the binary. + fs.rmSync(result.canvas_entry); + const mediaOptions = { ...fixture.options, process: { ...fixture.options.process, argv: ["node", bootstrap] } }; + assert.strictEqual(load(bootstrap, mediaOptions).proc.exitCode, 0); + assert.strictEqual(fixture.dirs.length, 1); + const repaired = load(bootstrap, fixture.options); + assert.strictEqual(repaired.proc.exitCode, 0, repaired.errors.join("\n")); + assert(fs.existsSync(JSON.parse(repaired.output[0]).canvas_entry)); + assert.strictEqual(fixture.dirs.length, 2); + + // A complete npm package found on PATH must not be reinstalled. + fixture.options.process.env.PATH = path.dirname(result.cli_path); + assert.strictEqual(load(bootstrap, fixture.options).proc.exitCode, 0); + assert.strictEqual(fixture.dirs.length, 2); + } + const standalone = bootstrapFixture({ canvas: true }); + const nativePath = path.join(standalone.npmDir, "pippit-tool-cli"); + fs.writeFileSync(nativePath, "existing standalone binary"); + const upgraded = load(bootstrap, standalone.options); + assert.strictEqual(upgraded.proc.exitCode, 0, upgraded.errors.join("\n")); + assert.notStrictEqual(JSON.parse(upgraded.output[0]).cli_path, nativePath); + assert.strictEqual(standalone.dirs.length, 1); + assert.strictEqual(fs.readFileSync(nativePath, "utf8"), "existing standalone binary"); + assert.strictEqual(load(bootstrap, standalone.options).proc.exitCode, 0); + assert.strictEqual(standalone.dirs.length, 1); + + for (const failure of ["missing-canvas-entry", "canvas-runtime", "canvas-json", "canvas-catalog", "canvas get"]) { + const fixture = bootstrapFixture({ canvas: true, failure }); + const failed = load(bootstrap, fixture.options); + assert.strictEqual(failed.proc.exitCode, 1, failure); + assert.strictEqual(fixture.dirs.length, 1, "An incompatible latest package must stop after one attempt"); + assert.strictEqual(failed.output.length, 0); + } + + const failure = bootstrapFixture({ canvas: true }); + const initial = load(bootstrap, failure.options); + const original = JSON.parse(initial.output[0]); + const execute = failure.options.modules.child_process.execFileSync; + failure.options.modules.child_process.execFileSync = (command, args, options) => { + if (args[0].endsWith("run.js")) throw Object.assign(new Error("timeout"), { status: null }); + return execute(command, args, options); + }; + assert.strictEqual(load(bootstrap, failure.options).proc.exitCode, 1); + assert.strictEqual(failure.dirs.length, 1, "Runtime timeouts must not trigger an upgrade"); + failure.options.modules.child_process.execFileSync = (command, args, options) => { + if (args[0].endsWith("run.js")) throw Object.assign(new Error("invalid runtime"), { status: 1 }); + return execute(command, args, options); + }; + assert.strictEqual(load(bootstrap, failure.options).proc.exitCode, 1); + assert.strictEqual(failure.dirs.length, 2); + assert(fs.existsSync(original.canvas_entry), "Failed Canvas upgrade must preserve the previous npm package"); + assert(fs.existsSync(original.cli_path)); +} + try { checkInstaller(); checkBootstrap(); + checkCanvasBootstrap(); const pkg = require("../package.json"); assert(pkg.files.includes("scripts/install-cli.js"), "npm package must ship the CLI-only entry"); console.log("CLI-only installer and Skill bootstrap checks passed"); diff --git a/scripts/skills.test.js b/scripts/skills.test.js index 880fc28..2020b8d 100644 --- a/scripts/skills.test.js +++ b/scripts/skills.test.js @@ -32,21 +32,69 @@ assert.ok( "xyq-short-drama-skill must remain user-invocable", ); -for (const requiredText of [ - "pippit-tool-cli generate-video", - "pippit-tool-cli query-result", - "pippit-tool-cli login", - "XYQ_ACCESS_KEY", - "pippit-tool-cli submit-run", - "pippit-tool-cli upload-file", - "web_thread_link", - "request_user_input", - "ask_user_question", -]) { - assert.ok(generalSkill.includes(requiredText), `xyq-skill missing contract: ${requiredText}`); +// The Skill is a self-contained document graph: follow only the selected module +// at runtime, but verify all shipped references and examples offline here. +const skillRoot = path.dirname(generalSkillPath); +const visited = new Set(); +function visitDocument(filePath) { + filePath = path.resolve(filePath); + assert(filePath.startsWith(skillRoot + path.sep), `Skill reference escapes its package: ${filePath}`); + if (visited.has(filePath)) return; + visited.add(filePath); + const content = readRequiredFile(filePath); + for (const match of content.matchAll(/\[[^\]]*\]\(([^)]+)\)/g)) { + const target = match[1].split("#")[0]; + if (!target || /^[a-z]+:/i.test(target)) continue; + const resolved = path.resolve(path.dirname(filePath), target); + assert(fs.existsSync(resolved), `Broken Skill link in ${filePath}: ${target}`); + if (resolved.endsWith(".md")) visitDocument(resolved); + } } +visitDocument(generalSkillPath); +const skillDocuments = [...visited].map((file) => readRequiredFile(file)).join("\n"); +const commandModules = { + auth: ["status", "login", "logout"], + canvas: ["canvas"], + "generate-image": ["generate-image"], + "generate-video": ["generate-video"], + "video-super-resolution": ["video-super-resolution"], + "erase-video-subtitle": ["erase-video-subtitle"], + "query-result": ["query-result"], + "get-credit-balance": ["get-credit-balance"], +}; +for (const [moduleName, commands] of Object.entries(commandModules)) { + const file = path.join(skillRoot, "commands", `${moduleName}.md`); + assert(visited.has(file), `Module is not reachable from SKILL.md: ${moduleName}`); + for (const command of commands) { + assert(readRequiredFile(file).includes(`pippit-tool-cli ${command}`), `Module missing usage: ${command}`); + } +} +const documentedCommands = [...new Set([...skillDocuments.matchAll(/\bpippit-tool-cli ([a-z][a-z-]*)\b/g)].map((match) => match[1]))].sort(); +assert.deepStrictEqual(documentedCommands, Object.values(commandModules).flat().sort(), "Skill must document exactly its supported CLI commands"); +for (const requiredText of ["XYQ_ACCESS_KEY", "web_thread_link", "request_user_input", "ask_user_question"]) { + assert(skillDocuments.includes(requiredText), `xyq-skill missing contract: ${requiredText}`); +} +for (const folder of ["commands", "workflows", "examples"]) { + for (const file of fs.readdirSync(path.join(skillRoot, folder))) { + if (file.endsWith(".md")) { + assert(visited.has(path.join(skillRoot, folder, file)), `Unreachable Skill document: ${folder}/${file}`); + } + } +} +assert(visited.has(path.join(skillRoot, "examples", "generate-and-deliver.md")), "Keep the basic end-to-end example"); -assert.ok(!generalSkill.includes("xyq-short-drama-skill"), "xyq-skill must not depend on the short-drama Skill"); +function checkSkillFiles(dir) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const file = path.join(dir, entry.name); + if (entry.isDirectory()) checkSkillFiles(file); + else if (/\.(md|js)$/.test(entry.name)) { + const content = readRequiredFile(file); + assert(!/submit[-_]run|get[-_]thread|upload[-_]file|download[-_]results?/.test(content), `Removed command still in Skill: ${file}`); + assert(!content.includes("xyq-short-drama-skill"), `Unexpected Skill dependency: ${file}`); + } + } +} +checkSkillFiles(skillRoot); for (const requiredText of ["request_user_input", "ask_user_question", "credits"]) { assert.ok( @@ -65,7 +113,7 @@ for (const requiredText of [ assert.ok(readme.includes(requiredText), `README missing skill contract: ${requiredText}`); } -for (const script of ["submit_run.py", "upload_file.py", "download_results.py"]) { +for (const script of ["submit_run.py", "upload_file.py", "download_results.py", "get_thread.py", "xyq_common.py"]) { assert.ok(!generalSkill.includes(script), `xyq-skill must migrate ${script} to CLI`); assert.ok(!readme.includes(script), `README must migrate ${script} to CLI`); assert.strictEqual( diff --git a/scripts/xyq-security.test.py b/scripts/xyq-security.test.py deleted file mode 100644 index e6ffc10..0000000 --- a/scripts/xyq-security.test.py +++ /dev/null @@ -1,128 +0,0 @@ -"""Offline regression checks for the remaining authenticated Python scripts.""" - -import contextlib -import importlib -import io -import os -from pathlib import Path -import subprocess -import sys -import unittest -from unittest import mock -import urllib.error -import urllib.request -import urllib.response -from email.message import Message - -SCRIPT_DIR = Path(__file__).resolve().parents[1] / "skills/xyq-nest-skill/scripts" -sys.path.insert(0, str(SCRIPT_DIR)) -TEST_KEY = "offline-test-access-key" -with mock.patch.dict(os.environ, { - "XYQ_ACCESS_KEY": TEST_KEY, - "XYQ_OPENAPI_BASE": "https://untrusted.example", - "XYQ_BASE_URL": "http://untrusted.example", -}): - common = importlib.import_module("xyq_common") - - -class SecurityTests(unittest.TestCase): - def test_help_and_argument_validation_without_credentials(self): - env = os.environ.copy() - env.pop("XYQ_ACCESS_KEY", None) - for args, exit_code, expected in ( - (["--help"], 0, "--thread-id"), - ([], 2, "--thread-id"), - (["--thread-id", "thread_test", "--after-seq", "invalid"], 2, "--after-seq"), - ): - with self.subTest(args=args): - result = subprocess.run( - [sys.executable, "-B", str(SCRIPT_DIR / "get_thread.py"), *args], - env=env, capture_output=True, text=True, timeout=10, - ) - self.assertEqual(result.returncode, exit_code, result.stderr) - self.assertIn(expected, result.stdout + result.stderr) - self.assertNotIn("错误:请设置 XYQ_ACCESS_KEY", result.stderr) - - def test_missing_credentials_block_requests_before_network(self): - for method in ("GET", "POST"): - stderr = io.StringIO() - with self.subTest(method=method), mock.patch.object(common, "ACCESS_KEY", ""): - with mock.patch.object(common, "authenticated_open") as send: - with contextlib.redirect_stderr(stderr), self.assertRaises(SystemExit) as raised: - if method == "GET": - common.api_get(common.GET_THREAD_PATH) - else: - common.get_thread("thread_test") - self.assertEqual(raised.exception.code, 1) - send.assert_not_called() - self.assertIn("请设置 XYQ_ACCESS_KEY", stderr.getvalue()) - - def test_environment_cannot_change_authenticated_origin(self): - self.assertEqual(common.XYQ_BASE, "https://xyq.jianying.com") - with mock.patch.object(common, "authenticated_open", return_value=io.BytesIO(b'{}')) as send: - common.api_post(common.GET_THREAD_PATH, {"thread_id": "thread_original"}) - request = send.call_args.args[0] - self.assertEqual(request.full_url, "https://xyq.jianying.com/api/biz/v1/skill/get_thread") - self.assertEqual(request.get_header("Authorization"), f"Bearer {TEST_KEY}") - - def test_untrusted_targets_rejected_before_network(self): - for target in ( - "http://xyq.jianying.com/api", - "https://untrusted.example/api", - "https://xyq.jianying.com.untrusted.example/api", - "https://xyq.jianying.com:444/api", - "https://user@xyq.jianying.com/api", - ): - with self.subTest(target=target), mock.patch.object(urllib.request, "build_opener") as build: - with self.assertRaises(urllib.error.URLError): - common.authenticated_open(urllib.request.Request(target)) - build.assert_not_called() - - def test_redirects_never_forward_authorization_or_body(self): - for status in (301, 302, 303, 307, 308): - for target in ("https://untrusted.example/steal", "https://xyq.jianying.com/next"): - requests = [] - - class RedirectServer(urllib.request.BaseHandler): - handler_order = 100 - - def https_open(self, req): - requests.append(req) - headers = Message() - headers["Location"] = target - response = urllib.response.addinfourl(io.BytesIO(b""), headers, req.full_url, status) - response.msg = "Redirect" - return response - - opener = urllib.request.build_opener(RedirectServer(), common._NoRedirect()) - request = urllib.request.Request( - common.XYQ_BASE + common.GET_THREAD_PATH, - data=b'{"private":"body"}', - headers={"Authorization": f"Bearer {TEST_KEY}"}, - ) - with self.subTest(status=status, target=target): - with mock.patch.object(urllib.request, "build_opener", return_value=opener): - with self.assertRaises(urllib.error.HTTPError): - common.authenticated_open(request) - self.assertEqual(len(requests), 1) - - def test_error_response_does_not_echo_key(self): - for error in ( - urllib.error.HTTPError(common.XYQ_BASE, 500, "failure", {}, io.BytesIO(TEST_KEY.encode())), - urllib.error.URLError(TEST_KEY), - ): - stderr = io.StringIO() - with mock.patch.object(common, "authenticated_open", side_effect=error): - with contextlib.redirect_stderr(stderr), self.assertRaises(SystemExit): - common.api_post(common.GET_THREAD_PATH, {}) - self.assertNotIn(TEST_KEY, stderr.getvalue()) - self.assertIn("[REDACTED]", stderr.getvalue()) - - stderr = io.StringIO() - with contextlib.redirect_stderr(stderr), self.assertRaises(SystemExit): - common.parse_response({"ret": "1", "errmsg": TEST_KEY}) - self.assertNotIn(TEST_KEY, stderr.getvalue()) - - -if __name__ == "__main__": - unittest.main() diff --git a/skills/xyq-nest-skill/SKILL.md b/skills/xyq-nest-skill/SKILL.md index cf8f74a..d14e508 100644 --- a/skills/xyq-nest-skill/SKILL.md +++ b/skills/xyq-nest-skill/SKILL.md @@ -1,510 +1,59 @@ --- name: xyq-skill -description: 通过小云雀的 AI 能力进行综合创作,支持生成和编辑图片/视频,并在用户明确要求图片或视频模型直出、指定图片或视频模型或直接调用 CLI 时使用 pippit-tool-cli generate-image / generate-video;用户要求视频超分、提升视频清晰度、擦字幕或去字幕时,使用 video-super-resolution / erase-video-subtitle。覆盖文生图、文生视频、图生视频、首尾帧生视频、视频编辑、风格转换、视频续写、视频复刻、TVC、宣传片、音乐 MV、产品广告、分镜和教育短视频等场景。当用户提到小云雀、xyq、上传参考图/视频/mp3或wav音频、查看生成进度,或查询小云雀积分余额、剩余积分、credits 时也应触发;积分查询使用 pippit-tool-cli get-credit-balance。 +description: 使用小云雀 pippit-tool-cli 生成或编辑图片、生成视频、超分和擦字幕,查询结果并交付媒体;操作小云雀个人 Canvas 画布、节点、布局、连线、角色/场景、生成提示词、3D 导演台与多轨草稿;查询积分及管理授权。用户提到小云雀、xyq 并需要这些操作时使用。 user-invocable: true metadata: - { - "openclaw": - { - "emoji": "💬", - "requires": - { - "bins": ["python3", "node"], - "env": ["XYQ_ACCESS_KEY"] - }, - "primaryEnv": "XYQ_ACCESS_KEY" - } - } + {"openclaw": {"emoji": "💬", "requires": {"bins": ["node"]}}} --- -# 小云雀创作、图片/视频模型直出、视频处理与积分查询 +# 小云雀媒体创作与画布操作 -通过 小云雀的API 创建会话、发送消息(生图、生视频、编辑视频等)、上传图片/视频/mp3或wav音频文件,并查询会话消息进展;通过 CLI 查询个人有效积分余额。 +通过 CLI 完成生成、处理、结果下载与媒体交付。支持下表中的操作;不提供多轮会话续写或自动拆分剧本、分镜并编排成片的能力。复杂需求先确认能由所列命令完成的具体操作,不承诺未覆盖的流程。 -小云雀是一个 AI 综合创作平台,同时为人类创作者和 Agent 设计。Agent 通过 Skill 入口理解任务、调用模型并自动编排工作流。 +## 开始执行 -每次开始执行本技能任务时,先按“前置要求”运行 `scripts/ensure-cli.js`,检查已有 CLI,不存在或缺少必需命令时获取最新版本。本文命令中的 `pippit-tool-cli` 均代表该脚本返回的 `cli_path`;实际执行时替换为带引号的绝对路径。 +1. 画布任务运行 `node "{baseDir}/scripts/ensure-cli.js" --canvas`,其他任务运行 `node "{baseDir}/scripts/ensure-cli.js"`。保存返回的 `cli_path`;Canvas 还需保存 `canvas_entry`。文档中的 `pippit-tool-cli` 替换为带引号的 `cli_path`;画布语义命令按模块说明通过 Node 入口执行。同一任务复用,安装细节见 [安装说明](scripts/install.md)。 +2. 按下表选择操作,只读取命中的命令文档。执行需要鉴权的操作前,按 [授权说明](commands/auth.md) 检查登录;有效登录可复用。 +3. 生成、视频处理和查询已有媒体结果时,还必须读取 [异步结果与媒体交付](workflows/async-delivery.md)。画布任务使用 [画布查询、编辑与验证](workflows/canvas-edit.md),不把画布编辑当作媒体生成。积分与授权操作直接返回结果。 -**平台核心能力:** -- **生成**:文生图、文生视频、图生视频、视频续写 -- **编辑**:局部修改、元素替换、镜头调整、风格迁移 -- **视频处理**:视频超分、提升视频清晰度、擦字幕 -- **复杂创作**:复刻已有视频风格做 TVC/宣传片、用音乐生成 MV、产品展示片制作 +参数是否存在、命令语法以当前 `cli_path` 对应的 `--help` 为准;返回字段和成功判断按命令文档及真实响应核对。文档与实际不一致时说明差异,不猜参数、不绕过 CLI 自行调用 HTTP。 -除“图片/视频模型直出”和“视频超分/擦字幕”外,创作和编辑需求通过发送自然语言消息来完成,后端 Agent 会自主编排工作流。复杂任务耗时较长,需耐心轮询。 +## 意图路由 -## 执行路由(必须先判断) +按用户要做的操作选择命令,不能只看素材类型。有任务标识且用户只要求查询或取件时,复用该任务,不重新生成。 -积分余额查询优先走路由 E,不进入创作或视频处理工作流。 +| 用户意图 | CLI | 必读文档 | +| --- | --- | --- | +| 查看登录状态、登录、退出或切换账号 | `status` / `login` / `logout` | [授权](commands/auth.md) | +| 创建或查询小云雀个人画布,编辑节点、布局、连线、角色/场景、提示词、3D 或多轨草稿 | `canvas` | [Canvas 能力与命令发现](commands/canvas.md) | +| 生成图片,或基于参考图修改图片 | `generate-image` | [生图与图片编辑](commands/generate-image.md) | +| 生成视频,使用图/视频/音频参考,首尾帧生视频 | `generate-video` | [生视频](commands/generate-video.md) | +| 提升已有视频分辨率、视频超分 | `video-super-resolution` | [超分](commands/video-super-resolution.md) | +| 去除已有视频字幕 | `erase-video-subtitle` | [擦字幕](commands/erase-video-subtitle.md) | +| 查询已有任务进度、下载生成结果 | `query-result` | [查询结果](commands/query-result.md) | +| 查询个人积分余额、剩余 credits | `get-credit-balance` | [积分](commands/get-credit-balance.md) | -### 路由 A:图片模型直出 +- 普通生图、生视频也走对应生成命令,无需用户额外声明“模型直出”。 +- 明确要求修改现有画布或其中节点时优先走 Canvas;普通生图、生视频不自动创建画布。“修改节点提示词”只修改配置,不隐含生成;指定节点生成或导出须先确认当前命令目录有对应能力。 +- “参考这个视频生成新的”走生视频;“把这个视频变清晰”走超分。意图不清时先问清。 +- 同时提出多个明确操作时,分别选模块;有输入依赖则顺序执行。仅在用户请求包含多个步骤时组合,不自动增加收费处理。 +- 后续修改某张结果图片时,将对应本地文件作为新一次图片编辑的参考图;需要隐式会话上下文时,先补齐具体素材和指令。 -满足任一条件时,必须直接使用 `pippit-tool-cli generate-image`,不要改走 `pippit-tool-cli submit-run`: +## 执行总则 -- 用户明确说“图片模型直出”、“直接调图片模型”或明确要求用 CLI 生图。 -- 用户指定了具体图片模型(如 `seedream_5.0_pro`),并希望单次直接生成图片。 -- 上游流程已明确将任务标记为图片 direct-model / 模型直出。 +- 保留用户原始 prompt,不擅自扩写、润色、翻译或增加风格词;参数转换按对应命令文档执行。生成和视频处理只传用户给定的可选创作参数,缺少必填项先询问。画布输入还需使用实际查询的 ID、版本和当前 schema。模型和参数最终合法性由服务端判断。 +- 用户明确要求生成或处理,即可在该范围内执行;仅咨询用法、费用或方案时不提交。范围、必填信息或消耗 credits 的授权不明确时,先确认,不重复索要已给出的授权。 +- 提问优先使用宿主实际提供且当前模式允许的工具:Codex 的 `request_user_input` 或 `request_user_input_async`,WorkBuddy 的 `ask_user_question`;不可用时用普通聊天。需要答案时等待答复。 +- 素材参数接收本地文件路径,CLI 内部上传。远程链接不能冒充本地路径;缺少可访问文件时先解决素材获取。单文件必须小于 500 MB(500000000 字节)。 +- 提交成功后立即展示真实 `web_thread_link`;未返回链接时如实说明,保留任务 ID。后续查询和下载失败不能触发重复生成。 +- 每个最终图片/视频都通过宿主文件交付或媒体渲染能力展示为真实附件或可预览媒体。URL、路径列表仅作补充;详细完成标准见共用交付流程。 -执行原则: +## 按需参考的完整场景 -1. 执行前完成“前置要求”的CLI 安装检查,使用返回的 `cli_path`;失败时报告阻塞,不要悄悄降级到会话 API。 -2. 真实提交会消耗 credits;如果用户本轮尚未明确确认生成,按“用户确认与反问”规则征得明确确认后再运行。 -3. 保留用户原始 prompt,不要自行扩写、润色、翻译或增加风格词。 -4. `--model` 必填;用户未提供图片模型时,先询问使用哪个模型。只添加用户已经给出的 `--ratio`、`--resolution`、`--generate-image-count`、`--image` 参数,不补默认值。 -5. `--resolution` 的使用说明是:仅 `seedream_5.0_pro` 支持 `1K`、`2K`、`4K`。不要在 skill 侧维护额外 allowlist 或自行改写用户值,实际合法性由服务端决定。 -6. `generate-image` 返回后,保存 `thread_id`、`run_id`,并立即向用户展示 `web_thread_link`。 -7. 每隔 10 秒调用 `query-result`,直到 `completed=true`。出现 `error_message` 时停止并报告;成功时展示并下载 `images[].output_path`。 +命令文档含最小调用示例;需要了解从需求到交付的组合过程时,再读对应场景: -```bash -pippit-tool-cli generate-image \ - --prompt "用户原始描述" \ - --model IMAGE_MODEL - --ratio RATIO - --resolution RESOLUTION - --generate-image-count COUNT - --image 参考图路径 - -pippit-tool-cli query-result \ - --thread-id THREAD_ID \ - --run-id RUN_ID \ - --download-dir OUTPUT_DIR -``` - -### 路由 B:视频模型直出 - -满足任一条件时,必须直接使用 `pippit-tool-cli generate-video`,不要改走 `pippit-tool-cli submit-run`: - -- 用户明确说“视频模型直出”、“直接调模型”或“直接调用 CLI”。 -- 用户指定了具体视频模型(如 `Seedance_2.5`),并希望单次直接生成视频。 -- 用户明确要求“首尾帧生视频”、指定首帧和尾帧,或要求从第一张图过渡到第二张图。 -- 上游流程已明确将任务标记为 direct-model / 模型直出。 - -执行原则: - -1. 执行前完成“前置要求”的CLI 安装检查,使用返回的 `cli_path`;失败时报告阻塞,不要悄悄降级到会话 API。 -2. 真实提交会消耗 credits;如果用户本轮尚未明确确认生成,按“用户确认与反问”规则征得明确确认后再运行。 -3. 保留用户原始 prompt,不要自行扩写、润色、翻译或增加风格词。 -4. 只添加用户已经给出的 `--model`、`--duration`、`--ratio`、`--resolution`、`--image`、`--video`、`--audio`、`--generate-type` 参数;未给参数交给 CLI 默认值。 -5. 普通用户支持模型 `Seedance_2.0_mini_lite`;VIP 专属模型包括 `seedance2.0_vision`、`seedance2.0_fast_vision`、`Seedance_2.0_mini` 和 `Seedance_2.5`。该列表仅用于指导用户选择和传入准确的 `--model` 值,不要在 skill 侧增加模型枚举校验,实际合法性由服务端决定。 -6. 首尾帧请求固定传 `--generate-type 1`,并按首帧、尾帧顺序传入两次 `--image`,不得重排。用户未明确两张图片的角色或缺少任一张时,先询问用户;不要在 skill 侧维护额外的 `generate_type` 枚举 allowlist,其他值原样交给服务端处理。 -7. `generate-video` 返回后,保存 `thread_id`、`run_id`,并立即向用户展示 `web_thread_link`。 -8. 每隔 10 秒调用 `query-result`,直到 `completed=true`。出现 `error_message` 时停止并报告;成功时展示并下载 `videos[].output_path`。 - -```bash -pippit-tool-cli generate-video --prompt "用户原始描述" --model "Seedance_2.5" - -pippit-tool-cli generate-video \ - --prompt "用户原始描述" \ - --image FIRST_FRAME_PATH \ - --image LAST_FRAME_PATH \ - --generate-type 1 - -pippit-tool-cli query-result \ - --thread-id THREAD_ID \ - --run-id RUN_ID \ - --download-dir OUTPUT_DIR -``` - -`--image` 最多重复 9 次,`--video` 和 `--audio` 最多各重复 3 次。 - -### 路由 C:视频超分和擦字幕 - -用户明确要求视频超分、提升视频清晰度、擦字幕或去字幕时,直接调用对应的 `pippit-tool-cli` 视频处理命令,不要改走 `pippit-tool-cli submit-run`: - -- 视频超分、提升视频清晰度:`video-super-resolution` -- 擦字幕、去字幕:`erase-video-subtitle` - -执行原则: - -1. 执行前完成“前置要求”的CLI 安装检查,使用返回的 `cli_path`;失败时报告阻塞,不要悄悄降级到会话 API。 -2. 真实提交会消耗 credits;如果用户本轮尚未明确确认处理,按“用户确认与反问”规则征得明确确认后再运行。 -3. 把用户提供的本地视频路径和处理参数直接交给对应 CLI;缺少必填输入时先询问用户。 -4. 命令返回后,保存 `thread_id`、`run_id`,并立即向用户展示 `web_thread_link`。 -5. 每隔 10 秒调用 `query-result`,直到 `completed=true`。出现 `error_message` 时停止并报告;成功时展示并下载 `videos[].output_path`。 - -```bash -pippit-tool-cli video-super-resolution \ - --video VIDEO_PATH \ - --output-resolution OUTPUT_RESOLUTION - -pippit-tool-cli erase-video-subtitle \ - --video VIDEO_PATH - -pippit-tool-cli query-result \ - --thread-id THREAD_ID \ - --run-id RUN_ID \ - --download-dir OUTPUT_DIR -``` - -### 路由 D:小云雀后端 Agent 编排 - -需要意图确认、脚本/分镜拆解、MV、TVC、局部编辑、复杂参考素材编排,或者用户未明确要求模型直出的创作需求,使用 `pippit-tool-cli submit-run` 提交消息,再用 `get_thread.py` 查询会话进展;明确的首尾帧请求走路由 B,明确的视频超分和擦字幕请求走路由 C;积分余额查询走路由 E。 - -提交前完成“前置要求”的CLI 安装检查,使用返回的 `cli_path` 调用 `submit-run`;失败时报告版本或安装阻塞。 - -### 路由 E:积分余额查询 - -用户询问小云雀“积分余额”、“还剩多少积分”、“剩余 credits”或要求查询个人有效积分时,直接使用 `pippit-tool-cli get-credit-balance`。 - -执行原则: - -1. 执行前完成“前置要求”的CLI 安装检查,使用返回的 `cli_path`;不可用或版本不支持该命令时报告阻塞,不要改走 `pippit-tool-cli submit-run`。 -2. 使用当前 CLI 登录凭证或显式配置的 `XYQ_ACCESS_KEY` 查询凭证所属用户的个人有效积分余额;无需传入用户 ID、`thread_id` 或 `run_id`。鉴权要求见“前置要求”。 -3. 这是只读查询,不需要积分消耗确认;不创建会话,不提交生成任务,也不调用 `get_thread.py` 或 `query-result` 轮询。 -4. 成功时读取 JSON 中字符串类型的 `total_remain_amount`,向用户展示当前有效积分余额;`"0"` 是有效的零余额。查询失败或缺少余额字段时报告错误,不得当作零余额。 -5. 需要排查请求时可添加 `--with-log-id`,同时保留返回的 `log_id`。该命令只返回总余额,不提供积分明细、到期时间或生成任务的费用预估。 - -```bash -pippit-tool-cli get-credit-balance - -# 排查请求时同时返回 log_id -pippit-tool-cli get-credit-balance --with-log-id -``` - -默认输出示例: - -```json -{"total_remain_amount":"123"} -``` - -## 用户确认与反问 - -后端返回意图确认问题、真实提交前需要 credits 确认,或缺少无法安全推断的必需信息时,暂停执行并向用户提问。 - -1. 优先使用当前 Agent 宿主提供的结构化用户提问或确认工具。 - - **Codex**:准确工具名是 `request_user_input`。仅在工具已暴露且当前模式允许时调用;不可用时退回普通聊天提问。不要在 Codex 中调用 `ask_user_question`。 - - **WorkBuddy**:准确工具名是 `ask_user_question`(Ask User Question)。需要用户补充、选择或确认时优先调用;工具未暴露时才退回普通聊天提问。 - - **Trae 及其他宿主**:先查看当前宿主实际暴露的工具,再使用同类结构化提问、确认或表单工具;不要臆造具体工具名。没有同类工具时退回普通聊天提问。 -2. 涉及 credits 消耗、真实生成、外部提交或不可逆操作时,必须等待用户明确答复;不要默认同意或超时后继续。路由 E 的只读积分余额查询不需要额外确认。 -3. 后端已经给出问题或选项时,保持原意传给用户,不要代替用户回答。 -4. 当前宿主没有结构化提问工具,或当前模式不允许调用时,使用一条简洁的普通聊天问题并暂停。 -5. 收到回复后,把用户答案原样发回同一 `thread_id`,获取新的 `run_id`,再继续轮询;不要新开会话。 - -## 功能 - -1. **创建会话 / 发消息** - 创建新会话或向已有会话发送一条消息(如「创作一个视频」) -2. **查询会话进展** - 根据 `thread_id` 、 `run_id`、`after_seq` 增量拉取该会话的消息列表,用于轮询创作过程的消息和最终产物结果 -3. **上传文件** - 支持上传`单张图片`、`单个视频文件`或`单个mp3/wav音频文件`到小云雀资产库,得到文件对应的 `asset_id`(编辑或参考已有图片/视频/音频时需要先上传) -4. **下载结果** - 将会话中生成的图片/视频批量下载到本地,支持指定输出目录和文件名前缀。 -5. **图片模型直出** - 使用 `pippit-tool-cli generate-image` 直接调用图片模型,使用 `query-result` 查询并下载图片结果。 -6. **视频模型直出** - 使用 `pippit-tool-cli generate-video` 直接调用视频模型,使用 `query-result` 查询并下载视频结果。 -7. **视频处理** - 使用 `pippit-tool-cli video-super-resolution` 或 `erase-video-subtitle` 处理本地视频,使用 `query-result` 查询并下载视频结果。 -8. **积分余额查询** - 使用 `pippit-tool-cli get-credit-balance` 查询当前凭证所属用户的个人有效积分余额,直接展示 `total_remain_amount`。 - - -## 前置要求 - -### 检查并按需安装 CLI - -运行环境需要 Python 3、Node.js 16+,支持执行本地程序。首次安装或自动升级 CLI 时需要 npm、可写的用户缓存目录、访问 npm 源及 GitHub Release 的网络、`curl` 和解压工具(macOS/Linux 的 `tar`,Windows 的 PowerShell)。复用已有 CLI 不需要 npm 或下载网络;安装或升级缺少这些能力时,报告具体安装阻塞。 - -开始执行本技能任务时运行以下脚本。它先检查 PATH 中的 CLI,再检查自身的安装缓存;找到命令齐全的 CLI 就复用,不访问 npm 或下载二进制。两处均不存在 CLI,或已有 CLI 的必需命令帮助检查返回非零退出码时,获取 npm `latest` 并安装或升级。PATH 中的旧版本缺少命令但缓存可用时,直接复用缓存,不重复升级。同一任务内的提交、轮询、上传和下载复用返回路径。 - -```bash -node "{baseDir}/scripts/ensure-cli.js" -``` - -需要安装或升级时,脚本跳过 npm 生命周期脚本获取 `@pippit-dev/cli@latest`,再调用包内的 `scripts/install-cli.js` 只安装 CLI。新版本通过全部检查后保存在 `~/.cache/pippit-tool-cli/xyq-skill/<平台>-<架构>/current`,供后续任务复用。它不会安装、清理全局 Skill,也不要求全局 npm 写入权限。安装与检查日志写入 stderr,成功时 stdout 返回 JSON: - -```json -{"cli_path":"/absolute/cache/path/current/node_modules/@pippit-dev/cli/bin/pippit-tool-cli","version":"实际安装版本"} -``` - -保存 `cli_path`,后续用它替换所有示例中的 `pippit-tool-cli`。不要依赖上一次 shell 调用中的临时环境变量;Windows PowerShell 用 `& "绝对路径" 参数` 调用。保留安装缓存以便后续任务复用;如果路径已被清理,重新运行安装脚本。 - -脚本验证 CLI 版本命令,以及 `login`、`submit-run`、`upload-file`、`download-result`、`query-result`、`generate-image`、`generate-video`、`video-super-resolution`、`erase-video-subtitle`、`get-credit-balance` 的 `--help`。检查不发送创作请求,也不需要凭据。已有 CLI 缺少必需命令时自动升级;版本命令无法运行或检查超时时报告运行错误。单次调用最多下载安装一次,最新版本仍不支持必需命令时停止并报告,不反复升级。升级成功前保留原安装;下载失败或最新包缺少只安装 CLI 的入口时,报告安装阻塞。 - -### 配置凭据 - -创建会话/发送消息、媒体上传、图片/视频模型直出、视频处理和积分余额查询(路由 A/B/C/D/E)使用原生 CLI。首次使用时运行网页登录,CLI 会自动申请或复用本机专属凭证,并保存到系统安全凭证库: - -```bash -pippit-tool-cli login -``` - -`XYQ_ACCESS_KEY` 仅作为原生 CLI 在 CI、Agent 等非交互环境中的显式覆盖。如果该环境变量已经设置但无效,CLI 不会静默改用网页登录凭证,应先修正或取消该环境变量。 - -路由 D 使用 CLI 提交消息和上传素材;查询进展仍使用独立 Python 脚本 `get_thread.py`。该脚本尚未接入 CLI 的系统安全凭证库,使用前必须配置同一用户的凭证: - -```bash -export XYQ_ACCESS_KEY="your-access-key" -``` - -原生 CLI 和保留的 Python 脚本携带用户密钥的 API 请求固定发往 `https://xyq.jianying.com`,不接受 `XYQ_OPENAPI_BASE` 或 `XYQ_BASE_URL` 覆盖。CLI 拒绝 API 跨域重定向,Python API 脚本禁止自动重定向;上传只通过 Authorization 请求头携带密钥。 - -所有 CLI 路由使用本次安装检查返回的 `cli_path`。保留的 Python 脚本仅使用标准库;安装检查脚本仅使用 Node.js 内置模块。 - -## 使用方法 - -### 1. 创建会话 / 发送消息 - -```bash -# 创建新会话并发送「生一个动漫视频」 -pippit-tool-cli submit-run --message "生一个动漫视频" - -# 向已有会话发送消息 -pippit-tool-cli submit-run --message "再生成一个故事视频" --thread-id THREAD_ID - -# 携带多个已上传的素材,每个 ID 重复一次参数 -pippit-tool-cli submit-run --message "根据参考素材生成视频" --asset-ids ASSET_ID1 --asset-ids ASSET_ID2 -``` - -`--message` 必填且不能全为空白,内容原样发送。`--thread-id` 可选;`--asset-ids` 每次接收一个 ID,多个素材必须重复该参数,不能在一次参数后以空格罗列多个 ID。 - -### 2. 查询会话进展 - -```bash -# 查询会话消息列表 -python3 {baseDir}/scripts/get_thread.py --thread-id THREAD_ID --run-id RUN_ID --after-seq SEQUENCE -``` - -> `run_id` 由 `submit_run` 返回,用于指定查询某次具体运行的结果。 - -### 3. 上传文件 - -先完成“前置要求”的CLI 安装检查,再使用返回的 `cli_path` 调用 `upload-file`;缺少命令时报告版本或安装阻塞。该命令使用 CLI 登录凭证或显式设置的 `XYQ_ACCESS_KEY`,成功输出 `{"asset_id":"..."}`。 - -- 当用户提供了参考的文件地址时,先进行文件上传,仅支持图片、视频、`.mp3/.wav` 音频。 -- 单次指令执行仅支持单个文件,多个文件可并行调用,单个文件必须小于 500 MB(500000000 字节,达到上限会拒绝上传)。 - -```bash -# 上传图片 -pippit-tool-cli upload-file --path /path/to/image.png - -# 上传视频 -pippit-tool-cli upload-file --path /path/to/video.mp4 - -# 上传音频 -pippit-tool-cli upload-file --path /path/to/audio.mp3 -``` - -### 4. 下载结果 - -会话 API 路由从 `get_thread.py` 返回的 `messages` 中提取产物 URL,逐文件调用 `pippit-tool-cli download-result` 下载到本地。 - -- 输出目录沿用用户指定的目录,未指定时使用 `./xyq_output`。 -- 保留原有命名规则:按产物 URL 列表顺序从 `01` 开始编号,有前缀时为 `前缀_01.ext`,无前缀时为 `01.ext`。扩展名优先取 URL 查询参数 `filename` 中的扩展名,其次取 URL 路径的扩展名,无法取得时使用 `.bin`。 -- 将目录和文件名拼成完整的 `--output-path`;每个 URL 调用一次,可最多并行执行 5 个下载命令。重试时保持 URL 与目标路径的对应关系。 - -```bash -# 示例:输出目录 ./xyq_output,前缀 artifact,两个 URL 的扩展名分别为 .png 和 .mp4 -pippit-tool-cli download-result --url "URL1" --output-path "./xyq_output/artifact_01.png" -pippit-tool-cli download-result --url "URL2" --output-path "./xyq_output/artifact_02.mp4" -``` - -CLI 默认跳过已存在的目标文件,返回 `already_exist`。仅在来源提供真实的文件更新时间时传入 `--updated-at`(Unix 秒),让 CLI 根据本地文件修改时间决定是否重新下载;不要用当前时间代替远端更新时间。跳过不代表已校验本地内容与远端一致,不能将已知属于其他产物的同名文件当作本次结果。 - -逐项收集下载结果;单项失败不阻断其他文件,只对失败项重试一次,仍失败则记录该产物、目标路径及 CLI 返回的错误。 - -## 典型工作流 - -理解这些工作流,才能正确组合上面的 CLI 和脚本完成用户需求。 - -### 场景 1:用户要求生成图片或视频(非模型直出) - -``` -1. pippit-tool-cli submit-run --message "用户的描述" → 拿到 thread_id、run_id 和 web_thread_link -2. **立即**将 `web_thread_link` 展示给用户(如"任务已提交,可在此查看:{web_thread_link}") -3. 每隔 `10` 秒钟调用 get_thread.py --thread-id THREAD_ID --run-id RUN_ID --after-seq SEQUENCE 进行轮询 -4. 检查 messages: - - 当任务还在创作中: - - 将过程创作信息展示给用户,继续轮询 - - 当任务完成(run 结束): - - 如果涉及意图确认/流程中断(如"请回答以下问题"): - → 优先调用当前宿主的结构化用户提问工具展示问题,等待用户回复 - → 使用 `thread_id` 重新提交任务(保持同一会话,产生新的 run_id) - → 回到步骤 2 继续轮询(可能多轮,直到不再意图确认) - - 如果 content 中包含产物 URL: - → 信息展示 → 下载产物 → 结果展示 -5. 自动下载:按“下载结果”的目录、前缀和编号规则,为每个产物 URL 调用 pippit-tool-cli download-result --url URL --output-path 完整文件路径 -6. 汇总每次调用的下载成功、已存在跳过和失败结果,向用户展示产物链接及对应的本地文件 -``` - -### 场景 2:用户明确要求图片模型直出 - -``` -1. 按“前置要求”检查并按需安装 CLI,后续使用返回的 cli_path -2. 检查图片模型:用户未提供时先询问,不要自行选择 -3. pippit-tool-cli generate-image --prompt "用户原始描述" --model IMAGE_MODEL [仅添加用户已给出的其他参数] -4. 拿到 thread_id、run_id 和 web_thread_link,立即展示 web_thread_link -5. 每隔 10 秒调用 query-result --thread-id THREAD_ID --run-id RUN_ID --download-dir OUTPUT_DIR -6. completed=true 后展示并下载 images[].output_path;出现 error_message 时停止并报告 -``` - -### 场景 3:用户明确要求视频模型直出(含首尾帧) - -``` -1. 按“前置要求”检查并按需安装 CLI,后续使用返回的 cli_path -2. 普通视频模型直出:pippit-tool-cli generate-video --prompt "用户原始描述" [仅添加用户已给出的其他参数] -3. 首尾帧直出:确认两张图片的首帧/尾帧角色,按顺序执行 generate-video --image FIRST_FRAME_PATH --image LAST_FRAME_PATH --generate-type 1 -4. 拿到 thread_id、run_id 和 web_thread_link,立即展示 web_thread_link -5. 每隔 10 秒调用 query-result --thread-id THREAD_ID --run-id RUN_ID --download-dir OUTPUT_DIR -6. completed=true 后展示并下载 videos[].output_path;出现 error_message 时停止并报告 -``` - -### 场景 4:用户提供图片/视频/音频要求编辑修改或作为参考(如"参考这个视频做一个新的"、"用这首歌做MV") - -``` -1. pippit-tool-cli upload-file --path /path/to/video.mp4 → 拿到 asset_id1 -2. pippit-tool-cli upload-file --path /path/to/audio.mp3 → 拿到 asset_id2 -3. pippit-tool-cli submit-run --message "参考这个视频并用这首歌做一个新的" --asset-ids asset_id1 --asset-ids asset_id2 → 拿到 thread_id、run_id、web_thread_link -4. 后续同场景 1 的步骤 2-6 -``` - -用户给了文件路径 + 编辑指令 = 先上传文件,再把编辑指令和 所有asset_id 一起发送。 - -### 场景 5:用户提供参考图/视频/音频要求生成新内容 - -``` -1. pippit-tool-cli upload-file --path /path/to/ref1.png → 拿到 asset_id1 -2. pippit-tool-cli upload-file --path /path/to/ref2.mp4 → 拿到 asset_id2 -3. pippit-tool-cli upload-file --path /path/to/ref3.mp3 → 拿到 asset_id3 -4. 直到所有文件上传完成,拿到所有 asset_id -5. pippit-tool-cli submit-run --message "根据参考图、视频、音频生成xxx" --asset-ids asset_id1 --asset-ids asset_id2 --asset-ids asset_id3 → 拿到 thread_id、run_id、web_thread_link -6. 后续同场景 1 的步骤 2-6 -``` - -### 场景 6:在已有会话中追加新需求 - -``` -1. pippit-tool-cli submit-run --message "新的描述" --thread-id THREAD_ID → 拿到 thread_id、run_id、web_thread_link -2. 后续同场景 1 的步骤 2-6 -``` - -### 场景 7:用户要求视频超分或擦字幕 - -``` -1. 按“前置要求”检查并按需安装 CLI,后续使用返回的 cli_path -2. 根据用户意图调用 video-super-resolution 或 erase-video-subtitle,并传入用户提供的本地视频路径和处理参数 -3. 拿到 thread_id、run_id 和 web_thread_link,立即展示 web_thread_link -4. 每隔 10 秒调用 query-result --thread-id THREAD_ID --run-id RUN_ID --download-dir OUTPUT_DIR -5. completed=true 后展示并下载 videos[].output_path;出现 error_message 时停止并报告 -``` - -### 轮询策略 - -- **间隔**:每 10 秒查询一次 -- **增量拉取**:首次用 --after-seq 0,后续根据messages消息列表长度,计算新的 seq 值 -- **完成判断**:当创作任务完成且messages的content中包含产物结果 URL(图片/视频地址) -- **超时**:连续轮询 `48 小时`仍无结果,告知用户"生成时间较长,可稍后查看",不再继续轮询 -- **错误重试**:单次查询失败可重试 1 次,连续 3 次失败则停止并告知用户 - -## 输出格式 - -**pippit-tool-cli submit-run** 返回: -```json -{ - "thread_id": "90f05e0c-...", - "run_id": "abc123-...", - "web_thread_link": "https://xyq.jianying.com/..." -} -``` - -**get_thread** 返回: -```json -{ - "messages": [ - {"id": "1", "role": "user", "content": "生一个动漫视频"}, - {"id": "2", "role": "assistant", "content": [ - { - "type": "{type}", - "subtype": "{sub_type}", - "data": {...} - } - ]}, - {"id": "3", "role": "assistant", "content": [ - { - "type": "{type}", - "subtype": "{sub_type}", - "data": {..., "url": "{url}"....} - } - ]} - ] -} -``` - -**pippit-tool-cli upload-file** 返回: -```json -{ - "asset_id": "{asset_id}" -} -``` - -**pippit-tool-cli download-result** 每次下载一个文件,成功返回: -```json -{ - "output_path": "./xyq_output/artifact_01.png", - "downloaded": ["./xyq_output/artifact_01.png"] -} -``` - -目标文件已存在而跳过时返回: -```json -{ - "output_path": "./xyq_output/artifact_01.png", - "downloaded": null, - "already_exist": ["./xyq_output/artifact_01.png"] -} -``` - -单文件下载失败时,命令以非零退出码返回错误,不保证输出 JSON;不能只检查 JSON 中是否有 `errors` 来判断成功。由用户侧 Agent 汇总各次调用的 `downloaded`、`already_exist` 和失败项,不再依赖批量返回的 `output_dir`、`total`。 - -## 会话 API 路由的下载完成标准 - -- run 结束后,先处理意图确认或流程中断;收到产物 URL 后才进入下载交付。 -- 每个待交付产物都要有对应结果:本次下载成功、已存在而跳过,或下载失败。只有所有产物均已下载或明确复用已有文件时,才能报告本地交付完成。 -- 已存在跳过的文件须单独说明,不能计为本次新下载;仍有失败项时报告“生成已完成,部分产物下载失败”,列出失败项和原始产物链接,不宣称全部下载完成。 - -## 向用户展示内容 - -- 任务提交后:立即将 `web_thread_link` 展示给用户,方便用户直接打开浏览器查看任务页面 -- 任务在创作中: - - 展示过程中的创作信息等,继续轮询 -- 任务完成(run 结束): - - 若涉及意图确认/流程中断(如"请回答以下问题")→ 按“用户确认与反问”规则优先调用结构化提问工具 → 等待用户回复 → 使用同一 `thread_id` 重新提交任务 → 继续轮询(可能多轮) - - 若 content 中包含产物 URL:展示来自 `get_thread` 返回的 `messages` 的产物链接,以及对应本地文件的可点击绝对路径;区分本次下载、已存在跳过和下载失败,并按上述完成标准说明交付状态。 - -## 核心原则:用户侧不做创作,只做传话 - -你(用户侧 Agent)的职责是**搬运工**,不是创作者。会话 API 路由由后端 Agent 负责理解需求、拆解分镜、编排工作流、选模型、写 prompt;图片/视频模型直出和视频处理路由把用户原始参数传给 CLI。积分余额查询按路由 E 直接查询并展示余额;以下步骤适用于创作和视频处理任务: - -1. **准备素材**:会话 API 路由用 `pippit-tool-cli upload-file` 把本地文件转为 asset_id;图片/视频模型直出和视频处理路由把本地路径直接交给对应 CLI;首尾帧任务固定传 `--generate-type 1` 并保持首帧、尾帧顺序 -2. **提交任务**:先按“执行路由”判断;图片模型直出调用 `pippit-tool-cli generate-image`,视频模型直出调用 `pippit-tool-cli generate-video`,视频超分和擦字幕调用对应的视频处理命令,其余通用创作任务把用户的原始描述 + asset_id 原封不动发给 `pippit-tool-cli submit-run` -3. **传话**:根据 `get_thread.py` 返回的消息列表,展示过程中的意图询问、创作信息等 -4. **取件**:会话 API 路由用 `get_thread.py` 轮询,图片/视频模型直出和视频处理路由用 `query-result` 轮询 → 检查结果 → 下载产物 → 结果展示给用户 - -**绝对不要做的事:** -- 不要替用户扩写、润色、翻译 prompt(用户说"帮我推演分镜",就直接传"帮我推演分镜",不要自己先写个分镜表再逐条发) -- 不要自行编排镜头描述、剧情推演、风格分析 -- 不要在消息中添加自己编的 prompt(如"超写实风格,电影级光影,8K分辨率"之类的描述词) - -后端 Agent 对模型能力、参数配置、prompt 工程远比用户侧更专业。用户侧越俎代庖只会降低生成质量,换个弱模型更是灾难。 - -**正确示例:** -``` -用户说:「根据多张参考图,做个科普故事视频」 -用户给了参考图:/path/to/ref1.png, /path/to/ref2.png, /path/to/ref3.png - -→ pippit-tool-cli upload-file --path /path/to/ref1.png → 拿到 asset_id1 -→ pippit-tool-cli upload-file --path /path/to/ref2.png → 拿到 asset_id2 -→ pippit-tool-cli upload-file --path /path/to/ref3.png → 拿到 asset_id3 -→ pippit-tool-cli submit-run --message "根据参考图、视频生成xxx" --asset-ids asset_id1 --asset-ids asset_id2 --asset-ids asset_id3 → 拿到 web_thread_link,立即展示给用户 -→ 轮询 ─┬─ 意图确认 → 用户确认 → 使用 thread_id 重新提交 → 继续轮询 - └─ 无意图确认 → 信息展示 → 下载产物 → 结果展示 -``` - -**错误示例:** -``` -❌ 用户侧自己先写了个九宫格分镜表(对峙、交锋、危机...) -❌ 然后把自己编的描述发给后端 -❌ 或者拆成9次 submit_run 分别发送 -``` - -## 注意事项 - -- 独立 Python 会话 API 脚本的鉴权方式为请求头 `Authorization: Bearer ` -- 创建会话时 `message` 是用户的指令要求,不能为空 -- 查询会话时可用 --after-seq 做增量拉取,便于轮询新消息(含 assistant 回复与生图/生视频结果) -- 上传文件仅支持图片(image/*)、视频(video/*)和 `.mp3/.wav` 音频文件,其他类型会被拒绝,文件必须小于 500 MB(500000000 字节) -- 生成过程中将过程中的创作信息展示给用户;任务完成后给出**产物结果(图片/视频)URL链接**和下载的**本地文件列表**。 -- 图片/视频模型直出和视频处理任务必须保留 CLI 返回的 `thread_id` / `run_id`,并用 `query-result` 取回最终图片或视频。 +- [基础生图到交付](examples/generate-and-deliver.md):第一次执行完整生成流程。 +- [参考图编辑](examples/image-edit.md):保留底图与参考图的角色。 +- [首尾帧生视频](examples/first-last-frame.md):保持素材顺序。 +- [擦字幕后超分](examples/video-process-chain.md):前一步结果作为下一步输入。 +- [画布内创建角色节点](examples/canvas-role.md):发现契约、查询真实 ID、编辑后回读。 diff --git a/skills/xyq-nest-skill/commands/auth.md b/skills/xyq-nest-skill/commands/auth.md new file mode 100644 index 0000000..064f93d --- /dev/null +++ b/skills/xyq-nest-skill/commands/auth.md @@ -0,0 +1,35 @@ +# 登录授权:status / login / logout + +适用于检查登录、首次授权、退出和切换账号。所有业务命令共享 CLI 凭据,无需为查询或媒体处理单独设置密钥。 + +## 调用与结果 + +```bash +pippit-tool-cli status +``` + +读取 JSON 的 `logged_in`、`source`,以及存在时的 `uid`、`expires_at`。`logged_in=true` 表示 CLI 找到了可用凭据,不保证服务端尚未撤销凭据或具有某个模型的使用权限。 + +未登录时执行: + +```bash +pippit-tool-cli login +``` + +引导用户在 CLI 提供的浏览器页面完成授权,等待命令成功返回 `logged_in=true`。CLI 自动申请或复用本机凭据并保存到系统安全凭证库。无浏览器交互能力且没有可用凭据时,报告授权阻塞。 + +用户要求退出时执行: + +```bash +pippit-tool-cli logout +``` + +`logged_out=true` 表示清除了本机浏览器登录凭据;`remote_credential_preserved=true` 表示远端密钥未撤销。切换账号时先退出,再重新登录,在新授权页选择目标账号;不要刷新旧授权页。 + +## 显式凭据覆盖与故障处理 + +- `XYQ_ACCESS_KEY` 是 CI、Agent 等环境的显式覆盖,优先于网页登录凭据。已设置但无效时不会自动回退;应修正该环境的配置或取消覆盖,再重试原操作。 +- `logout` 不清除环境变量;返回 `environment_still_active=true` 时,显式密钥仍生效。不要把本机退出解释成所有凭据均已失效。 +- 浏览器登录凭据被服务端拒绝时可使用 `pippit-tool-cli login --force` 轮换本机密钥;不要作为每次调用的例行步骤。 +- 不展示、回显或把密钥写入文档和命令参数。错误信息中出现凭据时,向用户展示前须隐藏凭据。 +- 带凭据的 API 请求固定发往 `https://xyq.jianying.com`,不通过修改 API 域名修复鉴权问题。 diff --git a/skills/xyq-nest-skill/commands/canvas-assets.md b/skills/xyq-nest-skill/commands/canvas-assets.md new file mode 100644 index 0000000..c4478d5 --- /dev/null +++ b/skills/xyq-nest-skill/commands/canvas-assets.md @@ -0,0 +1,32 @@ +# Canvas 原生资产命令 + +本页命令通过 `cli_path` 执行。仅面向个人画布,所有 ID 保持原始字符串,不能转换成 JavaScript Number;`project_id`、`canvas_asset_id`、节点 ID 和媒体 `pippit_asset_id` 不可混用。 + +| 命令 | 输入 | 返回与完成判断 | +| --- | --- | --- | +| `canvas create` | `--title` 最多 50 字符;`--request-id` 可选;`--wait` 等初始化;可调 `--poll-interval`、`--timeout` | 保存 `project_id`、`canvas_asset_id`、`web_url`、`request_id`;检查 `state`、`warning` 和退出码 | +| `canvas get` | `--asset-id` 必填,可重复 | 返回 `requested_asset_ids`、`assets` 及可用的 `log_id`;核对实际返回资产 | +| `canvas upload` | `--path` 本地文件;可调 `--poll-interval`、`--timeout` | 返回 `pippit_asset_id`、`locator`、`state`、`warning`;素材可查询不代表已插入画布 | +| `canvas allocate` | `--count` 分配数量 | 返回 `asset_ids[]`;只预留 ID,不创建节点或资产 | +| `canvas apply` | `--project-id` 项目 ID;`--file` JSON 文件,默认 `-` 读 stdin | 一个 transaction,可含多条 patch;检查事务 ACK 和目标资产版本 | + +```bash +pippit-tool-cli canvas create --title "产品方案" --wait +pippit-tool-cli canvas get --asset-id CANVAS_ASSET_ID +pippit-tool-cli canvas upload --path "/path/to/reference.png" +``` + +示例代表不同操作,不应因为读到示例就全部执行。创建、上传可能以退出码 0 返回 `creating` / `processing` 和 `warning`:保留已返回的 ID,稍后用 `canvas get` 回查资产可见性;不要重复创建或上传。画布初始化概览尚未完成时,回查根资产仅证明资产可见,不足以宣称所有初始化完成,应保留 `web_url` 和原始状态说明限制。`request_id` 用于追踪,不能假定跨故障重试严格幂等。 + +## 低层事务的使用边界 + +普通节点和领域编辑优先使用 [语义命令](canvas.md),由 SDK 分配 ID 并构造事务。仅在用户确实提供或需要底层资产事务且契约已核实时执行: + +```bash +pippit-tool-cli canvas allocate --count 2 +pippit-tool-cli canvas apply --project-id PROJECT_ID --file "/path/to/validated-patch.json" +``` + +JSON 根包含 `batch_id`、`client_id`、`transactions`,可含 `root_pippit_asset_id` 和 `Base`;单一 transaction 含 `transaction_id`、`patches`。每个 patch 使用实际 `asset_id`、`op`、`path`、所需 `value` 与适用的 `base_asset_version`,具体版本及内容必须来自真实查询或已核实调用契约。保留调用方原有 batch/transaction ID,不擅自改前缀;不手工绕过语义命令的业务校验。 + +写入结果不明确、超时或版本冲突时,先查询受影响资产,再决定如何恢复;不能照原请求盲目重放。不要使用隐藏传输参数绕过 ACK 校验。 diff --git a/skills/xyq-nest-skill/commands/canvas.md b/skills/xyq-nest-skill/commands/canvas.md new file mode 100644 index 0000000..3018c88 --- /dev/null +++ b/skills/xyq-nest-skill/commands/canvas.md @@ -0,0 +1,50 @@ +# Canvas:个人画布与命令发现 + +用于小云雀个人漫剧画布操作,不适用于其他产品的画布或团队空间。与 [生图](generate-image.md)、[生视频](generate-video.md) 区分:修改画布文档不等于生成媒体,独立生成也不会自动写回指定节点。 + +## 两个入口 + +先运行 `node "{baseDir}/scripts/ensure-cli.js" --canvas`。返回: + +```json +{"cli_path":"/absolute/package/bin/pippit-tool-cli","version":"实际版本","canvas_entry":"/absolute/package/scripts/run.js"} +``` + +- 原生资产命令:`pippit-tool-cli canvas ...`,命令名替换为 `cli_path`。 +- SDK 语义命令:`node "CANVAS_ENTRY" canvas command ...`,`CANVAS_ENTRY` 替换为返回的 `canvas_entry`。不要把 Go 二进制的帮助输出当作语义命令可执行的证明。 +- 切换 shell 时仍使用保存的绝对路径;Windows PowerShell 原生命令用 `& "CLI_PATH" ...`,Node 命令用 `node "CANVAS_ENTRY" ...`。授权复用 [CLI 凭据](auth.md)。 + +## 按用户意图发现操作 + +先读取精简目录,仅展开要用的命令;以下名称用于定位,以当前安装版本返回的目录为准。 + +| 操作 | 入口或可定位的语义命令 | 要点 | +| --- | --- | --- | +| 创建画布、查资产、上传素材、分配 ID、提交事务 | `canvas create/get/upload/allocate/apply` | 见 [原生资产命令](canvas-assets.md) | +| 读取画布和资产 | `get_snapshot`、`get_asset`、`get_permissions` | 取得真实节点/资产 ID,不用名称猜 ID | +| 创建业务节点 | `create_biz_node` | 支持类型和初始字段用 `--node-kind` 查看;不要手拼业务节点结构 | +| 节点位置、大小、样式、布局、分组 | `move_nodes`、`resize_node`、`set_node_style`、`align_nodes`、`arrange_nodes`、`group_nodes` 等 | 先查询目标和归属;删除与重排仅限用户请求范围 | +| 连线和画布配置 | `create_edge`、`reconnect_edge`、`delete_edges`、`set_title`、`set_cover` 等 | 连线两端来自当前画布查询 | +| 角色与场景资料 | `xyq.role.updateDescription/updateAppearance/updateSourceInfos`、`xyq.scene.updateDescription` | 用完整命令名分别 describe,遵守具体字段 | +| 图片/视频节点提示词和已有引用 | `xyq.generation.update_prompt` | 先读 `guide prompt-references`;保留其他生成参数,不触发生成 | +| 3D 导演台对象、摄像机、关键帧、动作 | `xyq.scene3d.query` / `xyq.scene3d.apply` | 先读 `guide scene3d` 和 `guide time` | +| 多轨草稿、轨道、片段、输出尺寸 | `xyq.timeline.query` / `xyq.timeline.apply` | 先读 `guide timeline` 和 `guide time`,使用最新 `expectedRevision` | +| 检查点创建、列表、比较、恢复 | `create_checkpoint`、`list_checkpoints`、`compare_checkpoint`、`restore_checkpoint` | 需要网页登录的 credential_scope;本地检查点按账号与画布隔离,不是跨机器备份 | + +```bash +node "CANVAS_ENTRY" canvas command list +node "CANVAS_ENTRY" canvas command describe create_biz_node --node-kind role +node "CANVAS_ENTRY" canvas command describe xyq.timeline.apply --operation set_output_size +node "CANVAS_ENTRY" canvas command schema xyq.timeline.apply +node "CANVAS_ENTRY" canvas command guide +``` + +`list --category 类别` 缩小范围;`describe --path schema.path` 展开字段。`describe` 是摘要视图,嵌套分支不完整;构造复杂输入时用 `schema 命令名` 取得完整契约。不要默认导出所有 schema。需要的命令不存在时报告当前版本的能力限制,不调用猜测的命令。 + +## 边界与易混淆点 + +- `create_biz_node` 可建立文字、图片、视频、音频、角色、场景、3D、多轨等业务节点;具体 `nodeKind` 以 schema 为准。创建节点本身不执行生成。 +- 提示词标签只解析已有画布节点及目标草稿中的引用。只有上传得到的裸 ID 不能自动加入草稿;无法解析时停止,不能用临时 URL 或虚构引用替代。普通提示词更新不等于故事板脚本编辑。 +- 目前没有公开的故事板镜头增删排序、脚本保存、指定镜头生成领域命令;`guide storyboard` 是说明,不是执行能力。底层补丁不能替代这些业务流程。 +- 3D 和多轨命令编辑文档,不提供截图、渲染或最终视频导出。多轨时间为整数微秒,3D 动画时间区分帧和源动画秒;先查字段 schema。 +- 编辑后按 [画布查询、编辑与验证](../workflows/canvas-edit.md) 回读确认;无需使用媒体异步查询来判断画布写入成功。只有实际取得图片/视频文件时才进入媒体交付标准。 diff --git a/skills/xyq-nest-skill/commands/erase-video-subtitle.md b/skills/xyq-nest-skill/commands/erase-video-subtitle.md new file mode 100644 index 0000000..d059169 --- /dev/null +++ b/skills/xyq-nest-skill/commands/erase-video-subtitle.md @@ -0,0 +1,13 @@ +# erase-video-subtitle:擦字幕 + +用于去除已有视频字幕;不代表支持删除任意水印、标志或画面对象。 + +必填参数 `--video` 接收一个本地视频路径,命令内部上传;没有可用视频文件时先补齐输入。 + +```bash +pippit-tool-cli erase-video-subtitle --video "/path/to/source.mp4" +``` + +成功返回 `thread_id`、`run_id`、`web_thread_link`,继续 [异步结果与媒体交付](../workflows/async-delivery.md)。失败时报告原因,不把其他生成命令当作字幕处理的自动降级方案。 + +用户还明确要求超分时,参考 [擦字幕后超分](../examples/video-process-chain.md),用第一步下载得到的实际视频路径衔接第二步。 diff --git a/skills/xyq-nest-skill/commands/generate-image.md b/skills/xyq-nest-skill/commands/generate-image.md new file mode 100644 index 0000000..4f75131 --- /dev/null +++ b/skills/xyq-nest-skill/commands/generate-image.md @@ -0,0 +1,32 @@ +# generate-image:生图与参考图编辑 + +适用于普通文生图、指定模型生图和基于参考图片的编辑。目标是生成视频时读取 [生视频](generate-video.md)。 + +## 输入与参数 + +| 参数 | 必填 | 规则 | +| --- | --- | --- | +| `--prompt` | 是 | 用户原始描述,不能全为空白 | +| `--model` | 是 | 用户选择的图片模型;缺少时先询问 | +| `--image` | 否 | 本地参考图路径,多张图重复此参数,保留用户指定的角色与顺序 | +| `--ratio` | 否 | 整数枚举,按下表转换明确的比例要求 | +| `--resolution` | 否 | 仅 `seedream_5.0_pro` 支持 `1K`、`2K`、`4K` 选项 | +| `--generate-image-count` | 否 | 用户指定的生成数量 | + +比例映射:`0=原始比例/自动`、`2=16:9`、`13=21:9`、`3=9:16`、`4=4:3`、`5=3:4`、`6=1:1`。此命令接收整数,不传 `--ratio "16:9"`。用户未给比例时省略;不明确的比例先确认,不猜枚举。 + +当前 CLI 帮助列出的模型包括 `seedream_5.0_pro`、`seedream_5.0`、`seedream_4.3`、`nova2`、`seedream_4.5`、`seedream_4.1`、`seedream_4`。用于提示选择,实际支持情况以当前帮助和服务端为准,不在 Skill 中增加模型白名单校验。 + +本地图片后缀支持 `.jpg/.jpeg/.png/.gif/.bmp/.webp/.svg`。参考图由命令内部上传,不需要自行获取资产 ID。 + +## 最小调用 + +```bash +pippit-tool-cli generate-image --prompt "用户原始描述" --model IMAGE_MODEL +``` + +只追加用户已提供的可选参数。多图编辑见 [参考图编辑示例](../examples/image-edit.md)。 + +## 返回与处理 + +成功返回 JSON 中的 `thread_id`、`run_id`、`web_thread_link`;随后执行 [异步结果与媒体交付](../workflows/async-delivery.md)。参数错误、上传失败或服务端拒绝时停止并说明原因,不切换模型或重新提交。仅在已明确失败、问题已修正且原授权仍适用时重试;提交结果不确定时避免重复收费。 diff --git a/skills/xyq-nest-skill/commands/generate-video.md b/skills/xyq-nest-skill/commands/generate-video.md new file mode 100644 index 0000000..ec24d9d --- /dev/null +++ b/skills/xyq-nest-skill/commands/generate-video.md @@ -0,0 +1,33 @@ +# generate-video:生视频 + +适用于文生视频、参考图/视频/音频生成新视频以及首尾帧生成。仅处理已有视频的清晰度或字幕时,分别使用 [超分](video-super-resolution.md)、[擦字幕](erase-video-subtitle.md)。 + +## 输入与参数 + +| 参数 | 必填 | 规则 | +| --- | --- | --- | +| `--prompt` | 是 | 用户原始描述,不能全为空白 | +| `--model` | 否 | 用户指定的模型;未提供时省略,由服务端处理默认配置 | +| `--image` | 否 | 本地图片路径,重复参数,最多 9 张 | +| `--video` | 否 | 本地参考视频路径,重复参数,最多 3 个 | +| `--audio` | 否 | 本地 `.mp3/.wav` 音频路径,重复参数,最多 3 个 | +| `--duration` | 否 | 整数秒;用户只给时长范围时先确认具体秒数 | +| `--ratio` | 否 | 比例字符串,如 `9:16`、`16:9`、`3:4`、`4:3`;不转换为生图枚举 | +| `--resolution` | 否 | 用户指定值,如 `720p`、`1080p` | +| `--generate-type` | 否 | 首尾帧任务传 `1`;其他显式值交服务端处理 | + +普通用户模型为 `Seedance_2.0_mini_lite`;VIP 模型包括 `seedance2.0_vision`、`seedance2.0_fast_vision`、`Seedance_2.0_mini`、`Seedance_2.5`。该列表仅用于选择提示,以当前帮助和服务端为准,不自行新增模型或分辨率组合校验。 + +本地图片支持 `.jpg/.jpeg/.png/.gif/.bmp/.webp/.svg`;视频支持 `.mp4/.avi/.mov/.wmv/.flv/.webm/.mkv/.m4v`。CLI 内部上传参考素材。 + +## 最小调用 + +```bash +pippit-tool-cli generate-video --prompt "用户原始描述" +``` + +首尾帧场景:明确两张图片的角色,按首帧、尾帧顺序传两次 `--image`,固定传 `--generate-type 1`。缺少图片、角色不清或无法确定原始描述时先询问。完整示例见 [首尾帧生视频](../examples/first-last-frame.md)。 + +## 返回与处理 + +成功返回 `thread_id`、`run_id`、`web_thread_link`,继续 [异步结果与媒体交付](../workflows/async-delivery.md)。素材、权限或参数失败时说明原因,不自动降低用户指定的模型、分辨率或删减素材。无法确认提交是否成功时,不重复生成。 diff --git a/skills/xyq-nest-skill/commands/get-credit-balance.md b/skills/xyq-nest-skill/commands/get-credit-balance.md new file mode 100644 index 0000000..85ce71c --- /dev/null +++ b/skills/xyq-nest-skill/commands/get-credit-balance.md @@ -0,0 +1,17 @@ +# get-credit-balance:个人积分余额 + +用于查询当前 CLI 凭据所属用户的个人有效积分余额。无需用户 ID 或任务 ID,不创建生成任务、不轮询,也不需要消耗积分的确认。 + +```bash +pippit-tool-cli get-credit-balance +``` + +成功输出示例: + +```json +{"total_remain_amount":"123"} +``` + +读取字符串 `total_remain_amount` 展示余额,`"0"` 是有效零余额。失败或缺少字段时报告查询失败,不能当作余额为零。 + +排查请求时使用 `pippit-tool-cli get-credit-balance --with-log-id`,保留返回的 `log_id`。该命令不提供积分明细、到期时间或生成任务费用估算。鉴权失败按 [授权说明](auth.md) 处理。 diff --git a/skills/xyq-nest-skill/commands/query-result.md b/skills/xyq-nest-skill/commands/query-result.md new file mode 100644 index 0000000..085268b --- /dev/null +++ b/skills/xyq-nest-skill/commands/query-result.md @@ -0,0 +1,47 @@ +# query-result:查询异步结果并下载 + +用于生成和视频处理命令返回的任务,也用于用户要求查询的已有任务。三个参数均必填: + +| 参数 | 含义 | +| --- | --- | +| `--thread-id` | 原任务的 `thread_id` | +| `--run-id` | 要查询的那一次运行的 `run_id`,不得混用其他运行 | +| `--download-dir` | 本地输出目录,命令在任务成功时自动下载产物 | + +用户未指定目录时使用 `./xyq_output`。已知历史任务但缺少任一 ID 时,从当前上下文获取,无法确定则询问;不要通过重新生成来补 ID。 + +```bash +pippit-tool-cli query-result --thread-id THREAD_ID --run-id RUN_ID --download-dir "./xyq_output" +``` + +## 输出契约 + +stdout 是 JSON;命令可能将错误编码进 JSON 并以退出码 0 返回,因此必须先检查 `error_message`。 + +| 字段 | 含义 | +| --- | --- | +| `completed` | 是否结束;失败时也可能为 `true`,不等于成功 | +| `error_message` | 非空即错误,不能因为 `completed=false` 而忽略 | +| `thread_id` / `run_id` | 对应的查询任务 | +| `images[]` / `videos[]` | 成功后获取的媒体,每项含 `download_url`、`output_path` | + +成功示例(ID、URL 和文件名仅为示意): + +```json +{ + "completed": true, + "thread_id": "THREAD_ID", + "run_id": "RUN_ID", + "error_message": "", + "images": [{"download_url": "https://example.com/image.jpeg", "output_path": "./xyq_output/asset.jpeg"}], + "videos": [] +} +``` + +任务尚未完成时通常返回 `completed=false`、空错误、空媒体数组。命令不提供完整会话消息、用户反问或可区分的所有状态;不能仅凭这个响应断言具体进度、取消状态或等待用户输入。轮询停止条件见 [共用流程](../workflows/async-delivery.md)。 + +## 下载行为 + +文件名由 CLI 根据产物信息生成,以 `output_path` 为准,不自行拼接编号或推测扩展名。同目录已有同名文件可能被复用;复用不证明内容相同,也不代表本次新下载。发现同名文件属于其他产物时,选择用户认可范围内的未冲突目录再查询,不删除已有文件。 + +找不到产物、链接缺失或下载失败时会返回错误。当前任一文件下载失败可能使整次查询只返回错误,无法据此认定其他文件都没下载或已完整交付。复查本次结果,不把输出目录里的任意旧文件当作本次产物。 diff --git a/skills/xyq-nest-skill/commands/video-super-resolution.md b/skills/xyq-nest-skill/commands/video-super-resolution.md new file mode 100644 index 0000000..ca1f130 --- /dev/null +++ b/skills/xyq-nest-skill/commands/video-super-resolution.md @@ -0,0 +1,17 @@ +# video-super-resolution:视频超分 + +用于提升已有视频的分辨率或清晰度。将视频作为新内容参考时使用 [生视频](generate-video.md)。 + +| 参数 | 必填 | 规则 | +| --- | --- | --- | +| `--video` | 是 | 一个本地视频文件,命令内部上传 | +| `--output-resolution` | 是 | 用户选择的目标分辨率;帮助列出 `720p`、`1080p`、`2k`、`4k` | +| `--tool-version` | 否 | 用户指定时传入;帮助列出 `standard`、`professional_v1`、`professional_v2` | + +用户只说“变清晰”且未给目标分辨率时先询问。模型版本或分辨率最终合法性由服务端判断。 + +```bash +pippit-tool-cli video-super-resolution --video "/path/to/source.mp4" --output-resolution 1080p +``` + +示例以用户要求 1080p 为前提。成功返回 `thread_id`、`run_id`、`web_thread_link`,按 [异步结果与媒体交付](../workflows/async-delivery.md) 查询、下载并交付。输入失败或服务端拒绝时停止,不擅自切换版本或分辨率。 diff --git a/skills/xyq-nest-skill/examples/canvas-role.md b/skills/xyq-nest-skill/examples/canvas-role.md new file mode 100644 index 0000000..e99f29f --- /dev/null +++ b/skills/xyq-nest-skill/examples/canvas-role.md @@ -0,0 +1,26 @@ +# 场景:在已有画布创建角色节点 + +用户请求:“在这个小云雀画布里新增一个叫小雨的角色节点。”用户已提供真实画布资产 ID,目标是新增资料节点,不是生成人物图片。 + +先读 [Canvas 模块](../commands/canvas.md),运行 `ensure-cli.js --canvas` 并完成授权。将 `CANVAS_ENTRY` 替换为返回的 Node 入口路径,将 `CANVAS_ASSET_ID` 替换为用户画布资产 ID。 + +```bash +node "CANVAS_ENTRY" canvas command list +node "CANVAS_ENTRY" canvas command describe create_biz_node --node-kind role +node "CANVAS_ENTRY" canvas command describe get_snapshot +node "CANVAS_ENTRY" canvas command run get_snapshot --canvas-id CANVAS_ASSET_ID --input '{}' +``` + +确认目标画布与既有节点,按当前 role schema 核对 `initialData.nodeName` 后执行: + +```bash +node "CANVAS_ENTRY" canvas command run create_biz_node --canvas-id CANVAS_ASSET_ID --input '{"nodeKind":"role","initialData":{"nodeName":"小雨"}}' +``` + +由业务工厂分配节点及配套资产,不自行编 ID。检查实际执行结果,再回读: + +```bash +node "CANVAS_ENTRY" canvas command run get_snapshot --canvas-id CANVAS_ASSET_ID --input '{}' +``` + +确认新增角色及名称,只报告“已新增角色节点”,附画布链接(已有时)和查询到的 ID。不调用独立生图命令,也不把新节点认定为已生成图片。失败或响应不确定时按 [画布编辑流程](../workflows/canvas-edit.md) 回读后恢复,避免创建重复节点。 diff --git a/skills/xyq-nest-skill/examples/first-last-frame.md b/skills/xyq-nest-skill/examples/first-last-frame.md new file mode 100644 index 0000000..7aead78 --- /dev/null +++ b/skills/xyq-nest-skill/examples/first-last-frame.md @@ -0,0 +1,15 @@ +# 场景:从首帧过渡到尾帧 + +用户请求:“以 opening.png 为首帧、ending.png 为尾帧,生成从白天过渡到夜晚的视频。” + +完成 [入口](../SKILL.md) 的前置步骤,确认两张图片实际位于下面的路径,读取 [生视频命令](../commands/generate-video.md)。 + +```bash +pippit-tool-cli generate-video \ + --prompt "以 opening.png 为首帧、ending.png 为尾帧,生成从白天过渡到夜晚的视频。" \ + --image "/path/to/opening.png" \ + --image "/path/to/ending.png" \ + --generate-type 1 +``` + +第一个 `--image` 是首帧,第二个是尾帧,不按文件名重新排序。用户未指定模型、时长、比例和分辨率,省略这些参数。若用户仅给两张图片而未明确首尾角色,应先确认。随后执行 [异步结果与媒体交付](../workflows/async-delivery.md)。 diff --git a/skills/xyq-nest-skill/examples/generate-and-deliver.md b/skills/xyq-nest-skill/examples/generate-and-deliver.md new file mode 100644 index 0000000..e57db8c --- /dev/null +++ b/skills/xyq-nest-skill/examples/generate-and-deliver.md @@ -0,0 +1,19 @@ +# 基础示例:生成一张图并交付 + +用户请求:“用 seedream_5.0_pro 生成一张白底红色马克杯图片。” + +先按 [入口](../SKILL.md) 完成安装检查与登录,读取 [生图命令](../commands/generate-image.md) 和 [异步交付流程](../workflows/async-delivery.md)。下列命令名替换为检查返回的 `cli_path`。 + +```bash +pippit-tool-cli generate-image --prompt "用 seedream_5.0_pro 生成一张白底红色马克杯图片。" --model seedream_5.0_pro --generate-image-count 1 +``` + +此处模型与数量均来自用户请求,不添加比例或分辨率。保存实际返回的任务 ID 并展示任务链接,然后将下面的占位符替换为真实值: + +```bash +pippit-tool-cli query-result --thread-id THREAD_ID --run-id RUN_ID --download-dir "./xyq_output" +``` + +按共用流程继续查询。成功后读取 `images[].output_path`,检查文件存在且非空,用宿主的附件工具或媒体渲染能力展示图片。回复可以是“已生成”加实际图片附件;只发送“文件位于 ./xyq_output/…”不算交付。 + +如果用户只问“怎么生成一张马克杯图片”,解释用法即可,不执行此收费流程。基础视频生成同样复用交付流程,提交参数改读 [生视频命令](../commands/generate-video.md)。 diff --git a/skills/xyq-nest-skill/examples/image-edit.md b/skills/xyq-nest-skill/examples/image-edit.md new file mode 100644 index 0000000..d1afacc --- /dev/null +++ b/skills/xyq-nest-skill/examples/image-edit.md @@ -0,0 +1,15 @@ +# 场景:保留多张参考图的角色 + +用户请求:“用 seedream_5.0_pro,图1是底图,图2只提供猫的形象,把图1的猫换成图2的猫,背景和其他物体不变。” + +用户明确图1为 `/path/to/scene.png`,图2为 `/path/to/cat.png`。完成 [入口](../SKILL.md) 的前置步骤后,读取 [生图命令](../commands/generate-image.md)。 + +```bash +pippit-tool-cli generate-image \ + --prompt "用 seedream_5.0_pro,图1是底图,图2只提供猫的形象,把图1的猫换成图2的猫,背景和其他物体不变。" \ + --model seedream_5.0_pro \ + --image "/path/to/scene.png" \ + --image "/path/to/cat.png" +``` + +保留用户原文与素材顺序,不将两张图都理解成可自由混合的风格参考。图片角色或文件映射不明确时先确认。继续 [异步结果与媒体交付](../workflows/async-delivery.md),逐项展示实际结果;具备看图能力时检查用户要求保留的主要元素,不能未经查看就宣称完全满足编辑要求。 diff --git a/skills/xyq-nest-skill/examples/video-process-chain.md b/skills/xyq-nest-skill/examples/video-process-chain.md new file mode 100644 index 0000000..8cd276e --- /dev/null +++ b/skills/xyq-nest-skill/examples/video-process-chain.md @@ -0,0 +1,21 @@ +# 场景:擦字幕后超分 + +用户请求:“把 /path/to/source.mp4 先擦掉字幕,再超分到 1080p。” + +用户已明确授权这两个步骤及顺序。完成 [入口](../SKILL.md) 的前置步骤,读取 [擦字幕](../commands/erase-video-subtitle.md)、[超分](../commands/video-super-resolution.md) 和 [异步交付流程](../workflows/async-delivery.md)。 + +第一步: + +```bash +pippit-tool-cli erase-video-subtitle --video "/path/to/source.mp4" +pippit-tool-cli query-result --thread-id FIRST_THREAD_ID --run-id FIRST_RUN_ID --download-dir "./xyq_output" +``` + +查询使用第一步真实返回的 ID。按共用流程等待成功,核对下载的视频文件。将下面的 `FIRST_OUTPUT_PATH` 替换为对应 `videos[].output_path`,不能继续传最初的带字幕视频: + +```bash +pippit-tool-cli video-super-resolution --video "FIRST_OUTPUT_PATH" --output-resolution 1080p +pippit-tool-cli query-result --thread-id SECOND_THREAD_ID --run-id SECOND_RUN_ID --download-dir "./xyq_output" +``` + +使用第二步的新 ID 查询,并交付最终视频附件。第一步失败时不提交第二步;第二步失败时说明组合任务尚未完成,可交付明确标注“仅完成擦字幕”的中间视频。用户只要求擦字幕时,到第一步结束,不自行增加超分。 diff --git a/skills/xyq-nest-skill/scripts/ensure-cli.js b/skills/xyq-nest-skill/scripts/ensure-cli.js index 072fe6c..7a620a1 100644 --- a/skills/xyq-nest-skill/scripts/ensure-cli.js +++ b/skills/xyq-nest-skill/scripts/ensure-cli.js @@ -7,7 +7,7 @@ const os = require("os"); const path = require("path"); const REQUIRED_COMMANDS = [ - "login", "submit-run", "upload-file", "download-result", "query-result", + "status", "login", "logout", "query-result", "generate-image", "generate-video", "video-super-resolution", "erase-video-subtitle", "get-credit-balance", ]; @@ -44,7 +44,7 @@ function findCLIOnPath() { return null; } -function ensureCLI() { +function ensureCLI({ canvas = false } = {}) { if (Number(process.versions.node.split(".")[0]) < 16) { throw new Error("需要 Node.js 16+ 和 npm。"); } @@ -79,17 +79,40 @@ function ensureCLI() { if (expectedVersion && version !== expectedVersion) { throw new Error(`CLI 版本 ${version} 与 npm 包版本 ${expectedVersion} 不一致。`); } - for (const command of REQUIRED_COMMANDS) { + const commands = REQUIRED_COMMANDS.map((command) => [command]); + if (canvas) { + for (const command of ["create", "get", "allocate", "upload", "apply"]) commands.push(["canvas", command]); + } + for (const command of commands) { try { - run(cliPath, [command, "--help"], `检查 ${command} 命令`, true); + run(cliPath, [...command, "--help"], `检查 ${command.join(" ")} 命令`, true); } catch (err) { if (Number.isInteger(err.exitStatus) && err.exitStatus !== 0) { - err.missingCommand = command; + err.missingCommand = command.join(" "); } throw err; } } - return { cli_path: cliPath, version }; + const result = { cli_path: cliPath, version }; + if (canvas) { + // Semantic Canvas commands live in the npm package, not the Go binary. + const entry = path.resolve(path.dirname(fs.realpathSync(cliPath)), "../scripts/run.js"); + try { + if (!fs.existsSync(entry)) throw new Error("缺少 npm 入口"); + const catalog = JSON.parse(run(process.execPath, [entry, "canvas", "command", "list"], "检查 Canvas 运行时", true)); + if (!Array.isArray(catalog.commands) || !["get_snapshot", "create_biz_node"].every( + (name) => catalog.commands.some((command) => command.name === name), + )) throw new Error("Canvas 命令目录不完整"); + } catch (err) { + // Runtime timeouts are execution failures; do not repeatedly install to mask them. + if (err.exitStatus === null) throw err; + const failure = new Error("Canvas npm 入口或运行时不可用,请检查 CLI 安装。"); + failure.missingCommand = "canvas command runtime"; + throw failure; + } + result.canvas_entry = entry; + } + return result; } const candidates = new Set([findCLIOnPath(), fs.existsSync(cachedCLI) ? cachedCLI : null]); @@ -123,7 +146,10 @@ function ensureCLI() { // Preserve the previous installation until the replacement passes all checks. fs.rmSync(installedDir, { recursive: true, force: true }); fs.renameSync(installDir, installedDir); - return { ...result, cli_path: cachedCLI }; + return { + ...result, cli_path: cachedCLI, + ...(canvas ? { canvas_entry: fs.realpathSync(path.join(installedDir, "node_modules", "@pippit-dev", "cli", "scripts", "run.js")) } : {}), + }; } catch (err) { fs.rmSync(installDir, { recursive: true, force: true }); throw err; @@ -132,13 +158,13 @@ function ensureCLI() { if (require.main === module) { if (process.argv.length === 3 && process.argv[2] === "--help") { - console.log("Usage: node ensure-cli.js\n优先复用 PATH 或缓存中命令齐全的 CLI,不存在或缺少必需命令时安装 npm latest,成功输出 {cli_path, version} JSON。"); - } else if (process.argv.length !== 2) { - console.error("不支持的参数。用法:node ensure-cli.js"); + console.log("Usage: node ensure-cli.js [--canvas]\n优先复用 PATH 或缓存中命令齐全的 CLI,不存在或缺少必需命令时安装 npm latest,成功输出 {cli_path, version} JSON。--canvas 额外验证画布原生命令和 npm 运行时,并返回 canvas_entry。"); + } else if (process.argv.length !== 2 && !(process.argv.length === 3 && process.argv[2] === "--canvas")) { + console.error("不支持的参数。用法:node ensure-cli.js [--canvas]"); process.exitCode = 1; } else { try { - console.log(JSON.stringify(ensureCLI())); + console.log(JSON.stringify(ensureCLI({ canvas: process.argv[2] === "--canvas" }))); } catch (err) { console.error(err.message); process.exitCode = 1; diff --git a/skills/xyq-nest-skill/scripts/get_thread.py b/skills/xyq-nest-skill/scripts/get_thread.py deleted file mode 100644 index 4d50c92..0000000 --- a/skills/xyq-nest-skill/scripts/get_thread.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env python3 -"""查询会话进展:POST /api/biz/v1/skill/get_thread,返回消息列表""" - -import argparse -import json -import sys -import os - -sys.path.insert(0, os.path.dirname(__file__)) -from xyq_common import extract_entries_from_run -from xyq_common import get_thread - - -def main(): - parser = argparse.ArgumentParser( - description="查询会话消息列表(会话进展)", - epilog=""" -环境变量: - XYQ_ACCESS_KEY 必填,Bearer 鉴权 - API 地址固定为 https://xyq.jianying.com,不支持环境变量覆盖 - -示例: - python3 get_thread.py --thread-id abc123 --run-id def456 --after-seq 0 - """, - formatter_class=argparse.RawDescriptionHelpFormatter, - ) - parser.add_argument( - "--thread-id", - required=True, - help="会话 ID(由 submit_run 返回)", - ) - parser.add_argument( - "--run-id", - default="", - help="运行 ID(由 submit_run 返回)", - ) - parser.add_argument( - "--after-seq", - type=int, - default=0, - help="只返回 seq 大于等于该值的消息,用于增量拉取(默认 0)", - ) - args = parser.parse_args() - - run = get_thread(args.thread_id, run_id=args.run_id, after_seq=args.after_seq) - # 从run中提取Message和Artifact - entries = extract_entries_from_run(run) - out = {"messages": entries} - print(json.dumps(out, ensure_ascii=False, indent=2)) - - -if __name__ == "__main__": - main() diff --git a/skills/xyq-nest-skill/scripts/install.md b/skills/xyq-nest-skill/scripts/install.md new file mode 100644 index 0000000..1e4da1c --- /dev/null +++ b/skills/xyq-nest-skill/scripts/install.md @@ -0,0 +1,39 @@ +# 检查与按需安装 CLI + +普通任务开始时运行与本文同目录的脚本: + +```bash +node "{baseDir}/scripts/ensure-cli.js" +``` + +`{baseDir}` 是当前 Skill 的根目录。运行环境需 Node.js 16+,并允许执行本地程序。首次安装或自动升级还需要 npm、可写的用户缓存目录、访问 npm 源和 GitHub Release 的网络、`curl` 与解压工具(macOS/Linux 的 `tar`,Windows 的 PowerShell)。复用已有 CLI 不需要下载网络或 npm。 + +## 查找与复用 + +脚本依次检查 PATH 中的 CLI 和自身缓存,验证版本及本 Skill 使用命令的 `--help`;命令集合维护在脚本的 `REQUIRED_COMMANDS`。帮助检查不调用生成服务,也不需要凭据,不证明账号权限或服务端运行状态。 + +命令齐全则直接复用,不检查最新版本;不存在或缺少必需命令时,获取 `@pippit-dev/cli@latest`。PATH 旧版本缺少命令但缓存完整时复用缓存,避免每次升级。版本命令不能运行或检查超时则报告运行错误。 + +需要安装时,脚本跳过 npm 生命周期脚本获取包,调用包内 `scripts/install-cli.js` 只安装 CLI。成功后缓存到 `~/.cache/pippit-tool-cli/xyq-skill/<平台>-<架构>/current`。不要求全局 npm 写入权限,也不安装或清理全局 Skill。 + +## 返回与使用 + +日志写入 stderr,成功时 stdout 为 JSON: + +```json +{"cli_path":"/absolute/path/to/pippit-tool-cli","version":"实际版本"} +``` + +后续所有命令使用返回的 `cli_path`,路径加引号;Windows PowerShell 使用 `& "绝对路径" 参数`。不要依赖前一次 shell 中的临时变量,同一任务复用返回路径,路径被清理后再运行脚本。 + +每次最多安装一次,升级成功前保留旧缓存。下载失败、最新包缺少安装入口或仍缺必需命令时停止并报告,不重复升级、不输出可用路径。仅将升级到可用版本作为恢复方式,不绕过缺失命令的检查。 + +独立 ZIP 必须包含整个 Skill 的命令文档、共用流程、示例和本脚本,保持相对路径;安装入口来自下载的 npm 包,不依赖本机源码仓库。 + +## Canvas 运行时检查 + +画布任务改用 `node "{baseDir}/scripts/ensure-cli.js" --canvas`。除原有检查外,再验证五个原生资产子命令的帮助,并通过同一个 npm 包的 Node 入口真实执行离线 `canvas command list`,确认运行时可加载且含基本查询与节点创建能力。该检查不登录、不访问画布、不写入远端。 + +成功额外返回 `canvas_entry`,供 `node "CANVAS_ENTRY" canvas command ...` 使用;`cli_path` 仍用于原生命令。语义操作的实际支持范围以当前目录为准,检查通过不代表所有业务操作或服务端权限都可用。 + +独立 Go 二进制没有 npm 入口,或包内运行时缺失/损坏时,Canvas 模式按原有规则检查缓存并至多安装一次最新完整 npm 包,保留旧安装直到新版本通过。普通媒体任务不要求 Canvas 运行时,也不会因为缺少它而升级。不要把缓存内的 `run.js` 单独复制出来,它依赖相邻模块与 `dist` 运行时。 diff --git a/skills/xyq-nest-skill/scripts/xyq_common.py b/skills/xyq-nest-skill/scripts/xyq_common.py deleted file mode 100644 index 91d2c90..0000000 --- a/skills/xyq-nest-skill/scripts/xyq_common.py +++ /dev/null @@ -1,162 +0,0 @@ -"""小云雀 agent-im OpenAPI 公共模块:查询会话(鉴权为 Authorization: Bearer )""" - -import json -import os -import sys -import urllib.request -import urllib.error -import urllib.parse - -# Credentials may only be sent to the fixed production HTTPS origin. -XYQ_BASE = "https://xyq.jianying.com" -ACCESS_KEY = os.environ.get("XYQ_ACCESS_KEY", "") - -# API 路径常量 -GET_THREAD_PATH = "/api/biz/v1/skill/get_thread" -HTTP_TIMEOUT_SECONDS = 30 * 60 - - -class _NoRedirect(urllib.request.HTTPRedirectHandler): - def redirect_request(self, req, fp, code, msg, headers, newurl): - # Never forward credentials or request bodies through a redirect. - raise urllib.error.HTTPError(req.full_url, code, "API 重定向已拒绝", headers, fp) - - -def authenticated_open(req): - target = urllib.parse.urlsplit(req.full_url) - if (target.scheme != "https" or target.hostname != "xyq.jianying.com" - or target.port not in (None, 443) or target.username is not None - or target.password is not None): - raise urllib.error.URLError("仅允许小云雀生产 HTTPS 地址") - return urllib.request.build_opener(_NoRedirect()).open(req, timeout=HTTP_TIMEOUT_SECONDS) - - -def redact_error(value): - text = str(value) - return text.replace(ACCESS_KEY, "[REDACTED]") if ACCESS_KEY else text - - -def _headers(): - if not ACCESS_KEY: - print("错误:请设置 XYQ_ACCESS_KEY 环境变量", file=sys.stderr) - sys.exit(1) - return { - "Authorization": f"Bearer {ACCESS_KEY}", - "Content-Type": "application/json", - } - - -def api_post(path: str, body: dict) -> dict: - """POST 请求 agent-im OpenAPI""" - url = f"{XYQ_BASE.rstrip('/')}{path}" - data = json.dumps(body).encode("utf-8") - req = urllib.request.Request( - url, - data=data, - method="POST", - headers=_headers(), - ) - try: - with authenticated_open(req) as resp: - return json.loads(resp.read().decode("utf-8")) - except urllib.error.HTTPError as e: - err_body = e.read().decode("utf-8") if e.fp else "" - print(f"API 错误 {e.code}: {redact_error(err_body)}", file=sys.stderr) - sys.exit(1) - except urllib.error.URLError as e: - print(f"网络错误: {redact_error(e.reason)}", file=sys.stderr) - sys.exit(1) - - -def api_get(path: str) -> dict: - """GET 请求 agent-im OpenAPI""" - url = f"{XYQ_BASE.rstrip('/')}{path}" - req = urllib.request.Request(url, method="GET", headers=_headers()) - try: - with authenticated_open(req) as resp: - return json.loads(resp.read().decode("utf-8")) - except urllib.error.HTTPError as e: - err_body = e.read().decode("utf-8") if e.fp else "" - print(f"API 错误 {e.code}: {redact_error(err_body)}", file=sys.stderr) - sys.exit(1) - except urllib.error.URLError as e: - print(f"网络错误: {redact_error(e.reason)}", file=sys.stderr) - sys.exit(1) - - -def parse_response(resp: dict) -> dict: - """ - 分析 API 响应。 - 响应结构:{"ret":"0","errmsg":"","data":{}} - 如果 ret 不是 "0",打印错误信息并退出;否则返回 data。 - """ - ret = resp.get("ret", "") - if ret != "0": - errmsg = resp.get("errmsg", "未知错误") - print(f"错误码: {redact_error(ret)}, 错误信息: {redact_error(errmsg)}", file=sys.stderr) - sys.exit(1) - return resp.get("data", {}) - - -def get_thread(thread_id: str, run_id: str = "", after_seq: int = 0) -> dict: - """ - 查询会话消息列表。 - 返回 data: { messages: [...] }。 - """ - body = {} - if thread_id: - body["thread_id"] = thread_id - if run_id: - body["run_id"] = run_id - body["after_seq"] = after_seq - resp = api_post(GET_THREAD_PATH, body) - resp = parse_response(resp) - thread = resp.get("thread", {}) - run_list = thread.get("run_list", []) - if len(run_list) == 0: - print("错误:未返回 run_list", file=sys.stderr) - sys.exit(1) - run = run_list[0] - run_state = run.get("state", "") - - # 判断 run_state - if run_state == 3: - # 成功 - print("成功:本次创作已完成", file=sys.stderr) - return run - elif run_state == 4: - # 失败 - fail_reason = run.get("fail_reason", "未知失败原因") - print(f"错误:{redact_error(fail_reason)}", file=sys.stderr) - sys.exit(1) - elif run_state == 5: - # 取消 - print("错误:本次创作已被终止", file=sys.stderr) - sys.exit(1) - else: - print("本次创作进行中", file=sys.stdout) - return run - - -def extract_entries_from_run(run: dict) -> list: - """ - 从 Run 的 EntryList 中提取符合条件的 entry。 - """ - matched = [] - for entry in run.get("entry_list") or []: - e = {} - message = entry.get("message") - artifact = entry.get("artifact") - if message: - e["id"] = message.get("message_id", "") - e["role"] = message.get("role", "") - e["content"] = message.get("content", []) - client_tool_calls = message.get("client_tool_calls", []) - if len(client_tool_calls) > 0: - e["content"].extend(client_tool_calls) - if artifact: - e["id"] = artifact.get("artifact_id", "") - e["role"] = artifact.get("role", "") - e["content"] = artifact.get("content", []) - matched.append(e) - return matched diff --git a/skills/xyq-nest-skill/tests/agent_test_cases.md b/skills/xyq-nest-skill/tests/agent_test_cases.md new file mode 100644 index 0000000..430686e --- /dev/null +++ b/skills/xyq-nest-skill/tests/agent_test_cases.md @@ -0,0 +1,64 @@ +# xyq-skill Agent 行为验证场景 + +用于检查模块选择、参数映射、异步查询与媒体交付。以下是待执行用例,不代表已通过真实 Agent 或服务端验证。离线推演不调用收费服务;真实生成须取得对应授权,并记录实际结果。 + +## 路由与输入 + +| 场景 | 用户输入/条件 | 期望行为 | +| --- | --- | --- | +| 基础生图 | 用 seedream_5.0_pro 生成一张白底红色马克杯图片 | 读取生图模块与异步交付流程,保留原文,模型和数量来自请求,不补比例/分辨率 | +| 缺少模型 | 生成一张猫咪图片 | 询问图片模型;答案到达前不提交 | +| 仅咨询 | 怎么生成一张猫咪图片? | 解释命令,不发起收费生成 | +| 图片编辑 | 图1是底图,图2只提供猫形象,替换猫但保留背景;已指定模型 | 两次 --image 顺序与用户角色一致,原始指令不改写 | +| 生图比例 | 用 seedream_5.0_pro,16:9 生图 | --ratio 2;不把视频比例字符串用在生图命令中 | +| 普通生视频 | 生成一个猫咪跳舞的视频 | 读取生视频模块,不要求用户说“模型直出”,未指定模型时省略 | +| 视频参数 | 用 Seedance_2.5,9:16,720p,8 秒生成猫咪视频 | 保留 prompt,--ratio 9:16、--duration 8,不改成整数比例或时长范围 | +| 时长范围 | 生成一个 5 到 10 秒的视频 | 先询问具体秒数,不臆造范围参数 | +| 首尾帧 | A 是首帧,B 是尾帧,从白天过渡到夜晚 | --image A --image B --generate-type 1,保留用户描述 | +| 首尾角色不明 | 这两张图做首尾帧视频 | 先确认角色,再执行;不按文件名自行排序 | +| 视频作为参考 | 参考这个视频做一段新的猫咪视频 | 走 generate-video,视频路径传 --video,不走超分 | +| 超分 | 把本地 source.mp4 超分到 1080p | 走 video-super-resolution;输出分辨率必填 | +| 超分缺参 | 把本地 source.mp4 变清晰 | 询问目标分辨率,不默认 1080p | +| 擦字幕 | 去除 source.mp4 的字幕 | 走 erase-video-subtitle,不添加超分步骤 | +| 组合处理 | 先擦字幕,再超分到 1080p | 先查询并检查第一步视频,再传其 output_path;使用第二步新 ID 查询 | +| 查询已有任务 | 提供一对任务 ID,只问结果 | 直接 query-result,不重新生成;缺 ID 时询问 | +| 积分 | 小云雀还剩多少积分? | 直接查积分,字符串 "0" 正常显示,不轮询、不要求消费确认 | +| 授权 | 查看登录状态 | 只执行 status,不自动退出、轮换凭据或生成内容 | + +## 输出、失败与交付 + +| 条件 | 期望行为 | +| --- | --- | +| 查询退出码 0,error_message 非空 | 按错误处理,不能判成功,也不能因 completed=false 继续空轮询 | +| completed=true 且有错误 | 失败,保留原任务 ID,不重新生成 | +| completed=false、空错误 | 仅说明暂未取得最终结果,按共用流程有界等待,不编造进度或具体状态 | +| completed=true、空错误、空产物 | 报告结果异常,不宣称交付完成 | +| 成功返回多张图/多个视频 | 检查所有 output_path,通过宿主逐项展示真实媒体 | +| 本地缺文件、空文件或已知是同名旧文件 | 不视为成功交付,报告对应项目;不得凭目录中的任意文件补结果 | +| 部分媒体不能展示 | 交付其余可用媒体,明确未交付项,不宣称全部完成 | +| 宿主不支持附件或预览 | 说明交付限制,链接/路径仅作补充 | +| 第二步处理失败 | 不宣称组合完成,可交付标明仅完成第一步的中间视频 | +| 用户停止或宿主等待期限到达 | 停止本地轮询,保留 ID;不声称服务端已取消或后台持续监控 | + +## 安装与发现(离线检查) + +- Node 安装测试覆盖缺 CLI 时安装、完整 CLI 复用、缺当前所需命令时升级、旧 PATH 配合新缓存复用、失败保留原缓存。 +- Skill 文档检查覆盖相对链接、模块可达性、基础完整示例以及命令范围。 +- `.agents/skills/xyq-skill` 应指向项目规范目录;独立目录复制后文档链接与安装入口仍完整。 + +## Canvas 路由与边界 + +| 场景 | 期望行为 | +| --- | --- | +| 在指定小云雀画布新增角色小雨 | Canvas 安装检查返回 Node 入口;读取节点 schema,查询目标后创建并回读;不生成图片 | +| 普通生图请求 | 维持原生路径,不要求 Canvas 运行时、不创建画布 | +| 移动现有节点或修改连线 | 查询实际节点 ID 和连线关系,仅编辑用户范围 | +| 修改图片节点提示词 | 使用专用提示词命令,保留其他参数;不浅覆盖整个 generation,也不触发生成 | +| 只有新上传的媒体 ID,要求引用 | 检查目标草稿是否已有引用;不能把未知裸 ID 认定为已建立的参考素材 | +| 修改多轨输出尺寸 | 查询外层节点和独立草稿,使用最新 revision,核对字段单位;写后回读,不声称已导出视频 | +| 移动 3D 对象或关键帧 | 区分外层节点 ID 与导演文档对象 ID,按 schema 使用米/角度/帧等单位 | +| 要求故事板排序、指定镜头生成或渲染导出 | 查实际目录,未支持时明确能力边界,不用底层 patch 或独立生成冒充完成 | +| create/upload 退出码 0 但 state 未 ready | 保留 ID 和 warning,查询原资产,不重复创建或上传 | +| 语义编辑失败或事务结果不明 | 先回读当前状态,不自动重跑、恢复检查点或删除本地恢复记录 | +| 只有 Go 二进制或 npm 运行时损坏 | Canvas 模式检查缓存/至多升级一次;普通媒体模式仍可复用原生 CLI | +| 语义 dryRun 成功 | 仅报告预演通过;实际写入和回读后才报告修改完成 | diff --git a/skills/xyq-nest-skill/workflows/async-delivery.md b/skills/xyq-nest-skill/workflows/async-delivery.md new file mode 100644 index 0000000..87c8190 --- /dev/null +++ b/skills/xyq-nest-skill/workflows/async-delivery.md @@ -0,0 +1,29 @@ +# 异步结果与媒体交付 + +生成、视频处理、查询已有结果共用此流程。开始前读取 [查询命令契约](../commands/query-result.md),不得只按退出码或 `completed` 判断成功。 + +## 保存任务与查询 + +1. 提交成功后保存 `thread_id`、`run_id` 和 `web_thread_link`,立即向用户展示任务链接。已有任务直接使用其标识进入查询,不再次生成。 +2. 使用同一 `cli_path` 和同一对任务 ID 调用查询命令,明确 `--download-dir`。每隔 10 秒查询一次,不在轮询中重新安装 CLI。 +3. 先检查命令是否可执行、输出是否为有效 JSON,再读取非空 `error_message`;有错误就进入下面的失败处理。 +4. 无错误且 `completed=false` 时继续轮询,只说明尚未取得最终结果,不编造创作消息或完成百分比。 +5. 无错误且 `completed=true` 时,要求媒体数组中有待交付产物,逐项核对 `output_path` 后进入交付。空结果或缺少必要字段时报告异常,不空轮询。 + +## 停止与恢复 + +- 明确的任务失败、鉴权失败、参数错误或产物缺失:停止并报告错误及可用的任务 ID、链接、LogID,不重新提交生成。 +- 可识别的暂时网络错误或下载错误:间隔 10 秒后仅重试同一次查询一次;仍失败则报告阻塞。无法判别错误类别时停止,避免盲目重试。 +- 用户要求停止时停止本地轮询;这不等于已取消服务端任务。持续未完成时遵守用户或宿主的等待时限,最迟在连续 48 小时后停止等待,保留 ID 供后续恢复查询。宿主无法持续执行时如实说明,不承诺后台监控。 +- 当前查询输出无法区分所有非成功状态;若长时间未完成或任务页面显示需要交互,报告当前查询能力的限制,不代答或新建任务。 +- 恢复时继续查询原任务,复用该任务已确定的输出目录,对已交付产物去重。 + +## 媒体交付完成标准 + +- 对 `images[]`、`videos[]` 中每个待交付文件确认本地存在且非空;同名旧文件只有明确对应本次产物时才能复用,不能计为本次新下载。 +- 使用宿主实际提供的文件交付工具,逐项展示真实图片/视频附件。宿主支持内置媒体渲染时按其规定引用文件,例如要求绝对路径时,先解析 `output_path` 为绝对路径。 +- 产物 URL、任务链接或本地文件列表只能作为补充,不能替代媒体附件或可预览媒体。 +- 所有待交付产物均经宿主交付成功后,才能宣称“交付完成”。生成、下载、附件交付分别判断。 +- 某项下载或展示失败时明确未交付项目及原因,仍交付其他可确认属于本任务的可用媒体。宿主不支持媒体展示时如实说明限制,不宣称全部交付完成。 + +组合处理时,中间产物下载并检查成功后才可作为下一步输入;最终结果按上述标准交付。用户要求中间产物时也逐项交付。 diff --git a/skills/xyq-nest-skill/workflows/canvas-edit.md b/skills/xyq-nest-skill/workflows/canvas-edit.md new file mode 100644 index 0000000..0f1680f --- /dev/null +++ b/skills/xyq-nest-skill/workflows/canvas-edit.md @@ -0,0 +1,19 @@ +# 画布查询、编辑与验证 + +适用于 [Canvas](../commands/canvas.md) 任务。该流程以画布状态为交付对象,创建、编辑不会自动产生可交付媒体文件。 + +1. **准备入口**:执行 Canvas 安装检查,保存 `cli_path` 和 `canvas_entry`,按授权文档确认身份。只查命令目录和离线指南无需登录;查询真实画布、执行编辑需要授权。 +2. **确定目标**:已有画布使用真实 `canvas_asset_id` 作为语义命令的 `--canvas-id`。用户提供的信息无法唯一定位时先补齐,不创建替代画布。新画布仅在用户要求创建时使用原生 `canvas create`,检查其返回状态。 +3. **发现与查询**:`list → describe/schema` 定位操作。用 `get_snapshot` 查节点,用 `get_asset` 查具体资产;3D/多轨再查询其独立文档。`run` 是统一执行入口,其中 query/get 类操作是读取,不等于所有 run 都是写入。 +4. **准备输入**:使用查询所得的节点、子对象和资产 ID。多轨 `expectedRevision` 来自最新查询。复杂输入写入 JSON 文件后传 `--file`;不要同时传 `--file` 和 `--input`。只操作用户指定范围。 +5. **预演与写入**:当前 schema 支持 `dryRun` 时可先预演,再在已有授权范围内执行真实修改。不能给所有命令强加此字段:领域命令的 dryRun 验证具体编辑,通用 `apply_mutations.dryRun` 不能替代领域预演。删除、恢复等操作仅按用户明确要求执行。 +6. **验证结果**:检查退出码与 JSON 的 `ok`、错误信息,`ok=false` 即失败。`dryRun=true` 成功不代表落盘;实际写入成功后,用对应查询回读目标字段及关联关系,确认没有误改其他对象。 +7. **反馈交付**:说明已验证的画布变化,保留已有/返回的画布链接及目标 ID。只完成文档编辑时不要声称图片/视频已生成;确有媒体文件待交付时遵循 [媒体交付标准](async-delivery.md)。 + +语义执行骨架(占位符替换为当前目录中的命令、真实 ID 和已准备的输入文件): + +```bash +node "CANVAS_ENTRY" canvas command run COMMAND_NAME --canvas-id CANVAS_ASSET_ID --file "/path/to/input.json" +``` + +写请求失败或返回“未确认事务已隔离”时保留错误上下文,先回读状态,不自动重跑、不删除本地恢复记录。多轨版本冲突需重新查询并基于新状态准备操作;检查点恢复会改变画布,不能作为所有失败的自动回滚方式。