@pippit-dev/cli 1.0.38 → 1.0.41
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 +84 -1
- package/checksums.txt +6 -6
- package/cmd/auth/auth.go +4 -3
- package/cmd/marketing.go +5 -4
- package/cmd/marketing_test.go +58 -0
- package/cmd/root.go +40 -14
- package/cmd/root_test.go +63 -0
- package/cmd/source_test.go +2 -3
- package/cmd/submit_run_test.go +20 -81
- package/cmd/update/update.go +36 -10
- package/cmd/update/update_test.go +73 -3
- package/dist/checksums.txt +6 -6
- package/dist/xyq-canvas-command-runtime.cjs +14 -14
- package/dist/xyq-canvas-command-runtime.cjs.LEGAL.txt +0 -206
- package/dist/xyq-canvas-command-runtime.cjs.sha256 +2 -2
- package/internal/adapter/adapter.go +73 -0
- package/internal/adapter/ppe.go +36 -0
- package/internal/adaptercli/commands.go +27 -0
- package/internal/adaptercli/commands_test.go +41 -0
- package/internal/auth/manager.go +25 -13
- package/internal/canvas/apply.go +3 -0
- package/internal/canvas/media_identity.go +300 -0
- package/internal/canvas/media_identity_test.go +160 -0
- package/internal/canvas/upload.go +30 -1
- package/internal/commandnames/names.go +9 -0
- package/internal/common/adapter_test.go +74 -0
- package/internal/common/client.go +37 -12
- package/internal/config/config.go +41 -0
- package/internal/config/config_test.go +41 -1
- package/internal/generate_video/generate_video.go +1 -0
- package/internal/generate_video/generate_video_test.go +25 -3
- package/internal/models/models.go +6 -2
- package/internal/models/models_test.go +7 -0
- package/package.json +2 -2
- package/scripts/canvas-command.js +4 -2
- package/skills/short-drama/SKILL.md +7 -4
- package/skills/short-drama/commands/download-result.md +9 -3
- package/skills/short-drama/commands/get-thread.md +2 -0
- package/skills/short-drama/commands/list-thread-file.md +12 -6
- package/skills/short-drama/commands/submit-run.md +3 -1
- package/skills/short-drama/examples/create-and-deliver.md +6 -2
- package/skills/short-drama/examples/reference-and-continue.md +20 -1
- package/skills/short-drama/workflows/creation.md +39 -14
- package/skills/short-drama/workflows/poll-and-deliver.md +27 -9
- package/skills/xyq-nest-skill/commands/canvas-assets.md +9 -1
package/README.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
面向 Pippit / 小云雀工作流的命令行工具与智能体技能集合。
|
|
4
4
|
|
|
5
|
+
## 本地运行配置
|
|
6
|
+
|
|
7
|
+
CLI 每次启动时读取**当前工作目录**中的 `config.toml`。在仓库根目录执行 CLI 时使用仓库配置,修改后下一次执行直接生效,无需重新编译:
|
|
8
|
+
|
|
9
|
+
```toml
|
|
10
|
+
xyq_api_url = "https://xyq.jianying.com"
|
|
11
|
+
x-tt-env = ""
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`x-tt-env` 填写本次实际 PPE 环境名,格式为 `ppe_[A-Za-z0-9_]+`。非空时,发往 `xyq_api_url` 的提交、查询、上传等 API 请求自动携带 `x-tt-env: <环境名>` 和 `x-use-ppe: 1`;外部媒体下载不携带这两个 header。留空时不添加 PPE header。仓库默认留空,避免默认路由到某个个人泳道。
|
|
15
|
+
|
|
16
|
+
文件不存在时继续使用线上默认配置;不会搜索父目录、用户目录或二进制目录。文件格式、字段名或值错误会终止执行。旧的 `PIPPIT_CLI_PPE_ENV` 不再生效;`XYQ_ACCESS_KEY` 等认证设置保持原有行为。
|
|
17
|
+
|
|
18
|
+
`tests/` 中的验证脚本随源码仓库维护,不包含在 npm 发布包中,也不接入正式 CLI 二进制入口。
|
|
19
|
+
|
|
5
20
|
## 技能列表
|
|
6
21
|
|
|
7
22
|
本仓库在 `skills/` 目录下包含三个智能体技能:
|
|
@@ -79,6 +94,45 @@ Canvas 任务使用 `ensure-cli.js --canvas`,额外返回 `canvas_entry`;原
|
|
|
79
94
|
- 文档使用 Skill 内相对链接,打包时保留结构。规范副本位于 `skills/xyq-nest-skill/`,项目发现入口 `.agents/skills/xyq-skill` 指向该目录。
|
|
80
95
|
- 修改后运行 `node scripts/skills.test.js` 与 `node scripts/install-cli.test.js`,检查引用完整、保留命令与安装检查一致及缺命令升级/缓存复用;Agent 行为用例见 [测试场景](skills/xyq-nest-skill/tests/agent_test_cases.md)。这些检查不代表真实生成已验证。
|
|
81
96
|
|
|
97
|
+
## CLI 更新渠道与发布
|
|
98
|
+
|
|
99
|
+
普通用户使用稳定渠道(npm `latest`);试用用户主动选择 `canary`:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
pippit-tool-cli update # 默认安装 latest
|
|
103
|
+
pippit-tool-cli update --channel canary # 本次安装 canary,并更新配套 Skills
|
|
104
|
+
pippit-tool-cli update --channel stable # 本次安装 latest,并更新配套 Skills
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`--channel` 只影响本次更新,不保存渠道偏好。下一次不带参数的 `update` 仍走 `latest`,因此可能从试用版退回当前稳定版。显式 `--channel` 优先于 `PIPPIT_CLI_INSTALL_PACKAGE`;不传渠道时保留该环境变量指定包版本的既有行为。新版本提示仍检查稳定渠道。
|
|
108
|
+
|
|
109
|
+
首次安装试用版,或旧 CLI 尚不支持 `--channel` 时,使用现有安装入口:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
npx --prefer-online @pippit-dev/cli@canary install
|
|
113
|
+
pippit-tool-cli --version
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
该命令会替换全局 CLI 和配套 Skills。`canary` 是公开 npm 分发渠道,适合主动参与的小范围试用,不限制谁可以下载。Skill 的 `ensure-cli.js` 在需要下载时仍使用 `latest`;希望试用的用户应先安装 canary。
|
|
117
|
+
|
|
118
|
+
维护者发布步骤:
|
|
119
|
+
|
|
120
|
+
1. 保持 `package.json`、`package-lock.json` 与 `vX.Y.Z` tag 的版本一致;使用普通版本号,不使用 `-beta` / `-rc` 后缀(现有安装器按普通版本号定位二进制)。
|
|
121
|
+
2. 推送 tag 后,`Release` workflow 构建 GitHub Release 产物并执行 `npm publish --tag canary`,保留原 `latest`。GitHub Release 产物仍公开可见,本流程控制的是 npm 分发渠道。
|
|
122
|
+
3. 试用用户安装 canary,核对版本并验证实际业务流程。
|
|
123
|
+
4. 验证通过后,在 GitHub Actions 的 `Promote canary to latest` workflow 中点击 `Run workflow`,填写已验证的 `X.Y.Z`。该 workflow 须先合入默认分支才会显示手动入口,并复用仓库已有的 `NPM_TOKEN`。
|
|
124
|
+
5. 提升流程要求输入版本与当前 `canary` 一致,只移动 npm `latest` 指针并回读确认,不重新构建或发布 npm 包。如果 canary 已更新,需要验证新的候选版本后再提升。
|
|
125
|
+
|
|
126
|
+
维护者回滚步骤:
|
|
127
|
+
|
|
128
|
+
1. 在 [GitHub Actions](https://github.com/Pippit-dev/cli/actions) 选择 `Rollback CLI`,点击 `Run workflow`。该 workflow 须先合入默认分支;复用 `NPM_TOKEN` 修改 npm 标签,使用只读 `GITHUB_TOKEN` 检查 Release。
|
|
129
|
+
2. 填写 `channel`(`stable` 对应 `latest`,或 `canary`)、`expected_version`(预期渠道当前版本)和 `target_version`(已验证的目标旧版本)。两个版本均须为普通 `X.Y.Z`,目标必须更低;例如 `stable / 1.0.39 / 1.0.38`,仅在确认 `1.0.38` 可用后操作。
|
|
130
|
+
3. 流水线核对渠道当前值、目标 npm 版本及 GitHub Release 元数据:版本 tag 匹配、已公开发布、六种平台/架构的压缩包及 `checksums.txt` 均已上传且非空。该检查不运行旧版程序,不替代业务验证;回滚依赖这些历史产物继续保留。
|
|
131
|
+
4. 写入前再次核对当前渠道,随后只移动所选 npm 标签并回读确认。不会重新构建、发包、修改 Git tag 或改变另一渠道。Actions 日志和运行摘要会记录目标及确认结果;写入或回读失败时,先用 `npm view @pippit-dev/cli dist-tags --prefer-online` 核实实际状态,再决定是否重试。
|
|
132
|
+
5. 已安装问题版本的用户需要主动执行 `pippit-tool-cli update --channel stable`(回滚 canary 时使用 `--channel canary`)并核对 `pippit-tool-cli --version`。旧 CLI 可执行默认 `update` 获取 `latest`,但须确保未设置 `PIPPIT_CLI_INSTALL_PACKAGE` 覆盖变量。已运行的进程及 Skill 独立缓存中的 CLI 不会被渠道回滚自动替换。
|
|
133
|
+
|
|
134
|
+
发布、提升和回滚共用并发组,避免这些 workflow 同时修改渠道;不取消正在运行的任务。GitHub 默认只保留一个等待中的同组任务,连续触发时请检查是否有等待任务被替换。并发组不能锁住维护者在终端执行的 npm 写入,操作期间应避免其它渠道修改。此阶段不提供百分比放量或自动回滚,普通客户端也不会因指针变化而自动安装;显式指定问题版本的安装仍然可用。
|
|
135
|
+
|
|
82
136
|
## 短剧工作流技能
|
|
83
137
|
|
|
84
138
|
包发布后可以通过 npm 安装。安装器会按当前系统下载匹配的预构建二进制文件,支持 macOS、Linux 和 Windows:
|
|
@@ -349,6 +403,35 @@ pippit-tool-cli query-result \
|
|
|
349
403
|
|
|
350
404
|
`query-result` 会查询指定 Run 并输出 JSON。Run 成功完成后下载视频和图片产物,`completed=true`,`videos` 和 `images` 中各包含 `download_url` 和 `output_path`;图片扩展名取自产物 `metadata.format`,缺省时兜底 `.png`。Run 失败也视为终态,`completed=true` 且填充 `error_message`;Run 未到终态时 `completed=false`。
|
|
351
405
|
|
|
406
|
+
## Adapter 接入边界
|
|
407
|
+
|
|
408
|
+
公共接口位于 `internal/adapter`,通过实例注入适用于当前运行环境的命令和 HTTP 请求策略;公共命令与 HTTP 层只依赖接口,不依赖具体实现。接口提供 `CLIOptions`、`PrepareAPIRequest`、`WrapTransport`、`StripSensitiveHeaders` 四个方法,`adapter.Default{}` 提供默认行为。
|
|
409
|
+
|
|
410
|
+
- 根入口 `main.go` 调用 `cmd.Execute()`,使用完整命令、本机认证和原始 API 路由。
|
|
411
|
+
- 同进程集成可使用 `cmd.NewRootCommandWithAdapter(stdout, stderr, policy)` 或 `common.NewHTTPClientWithAdapter(baseURL, timeout, authorizer, policy)`;传 nil 使用默认 adapter,不存在全局 adapter 注册。
|
|
412
|
+
- 同源判断由公共 HTTP client 按 scheme、hostname、有效端口执行;只有同源 API 请求才调用 `PrepareAPIRequest`。非同源请求清除默认认证和 adapter 管理的敏感字段;API 跨域重定向会被拒绝。
|
|
413
|
+
- `AuthProvided` 只表示 adapter 已附加认证材料,不代表服务端验证通过;失败立即返回,不回退到本机认证。`UseDefaultAuth` 才调用默认认证器,未知决策会被拒绝。
|
|
414
|
+
- CLI 策略与 Cobra 解耦。`DefaultAllowRootCommands` 明确指定是否默认放行一级业务命令;为 false 时只开放 `AllowedRootCommands` 白名单,nil 和空列表都表示没有白名单命令。默认 adapter 将该开关设为 true,本机认证和自更新仍受各自开关控制;标准帮助和版本行为仍由 Cobra 提供。可通过 `auth.WithCLIOptions` 将本机认证能力配置应用于独立 Manager。
|
|
415
|
+
- 业务 client 与更新遥测 client 都在构造时包装 Transport;遥测 client 属于命令实例,避免使用不同 adapter 的实例共享可变客户端。
|
|
416
|
+
|
|
417
|
+
例如,自定义 adapter 可以复用默认实现,仅调整命令能力,并在自己的程序入口注入:
|
|
418
|
+
|
|
419
|
+
```go
|
|
420
|
+
type customAdapter struct {
|
|
421
|
+
adapter.Default
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
func (customAdapter) CLIOptions() adapter.CLIOptions {
|
|
425
|
+
options := adapter.Default{}.CLIOptions()
|
|
426
|
+
options.SelfUpdateEnabled = false
|
|
427
|
+
return options
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// 程序入口:cmd.ExecuteWithAdapter(customAdapter{})
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
新增业务请求应复用 `Runner.Client`;需要独立 `http.Client` 时,通过所注入策略的 `WrapTransport` 包装。直接创建未包装的客户端不会自动应用策略。
|
|
434
|
+
|
|
352
435
|
## HTTP 客户端
|
|
353
436
|
|
|
354
437
|
命令模块通过 `common.Runner` 发起服务调用。运行时配置,例如基础地址、HTTP 超时时间和接口路径,由 `internal/config` 加载,并在运行器中与 `common.Client` 组合使用。
|
|
@@ -375,7 +458,7 @@ pippit-tool-cli update --source HOST
|
|
|
375
458
|
|
|
376
459
|
### 宿主来源统计
|
|
377
460
|
|
|
378
|
-
生成提交命令均支持可选 `--source`:`
|
|
461
|
+
生成提交命令均支持可选 `--source`:`generate-image`、`generate-video`、`video-super-resolution`、`erase-video-subtitle`、`short-drama +submit-run`、`marketing generate`。
|
|
379
462
|
|
|
380
463
|
由宿主 Agent 根据实际环境静默填写稳定标识,例如豆包办公 `doubao_office`、WorkBuddy `workbuddy`、Codex `codex`。其它来源可使用其真实产品标识;来源未知时省略,不询问用户,也不从 prompt 猜测。该值去掉首尾空白后写入请求顶层 `platform`,仅供统计,不参与创作、模型选择或鉴权;无法解析来源或显式空值时不发送该字段。省略 `--source` 时,CLI 按以下顺序尽力补齐来源:
|
|
381
464
|
|
package/checksums.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
6f7daba79b3423b148e30373bb87e7e8998df3d76be2196e39f84af209f0ea50 pippit-tool-cli-1.0.41-darwin-amd64.tar.gz
|
|
2
|
+
9f03ae01c213a76f45b0dfa110272b45e5b5ecd54b188313c9d256ccbe512980 pippit-tool-cli-1.0.41-darwin-arm64.tar.gz
|
|
3
|
+
ea603412d1c5b0baa42efd34bec92fcd0907333babd64a6e08514d4e2dcb86e5 pippit-tool-cli-1.0.41-linux-amd64.tar.gz
|
|
4
|
+
cebd1f0f34157624c0fc8203eec306bd6c7e31a2db44089f52bca9c74c57ee03 pippit-tool-cli-1.0.41-linux-arm64.tar.gz
|
|
5
|
+
729f413373d185964a42c178a7e0eeafc3fca490670fb753dc36fb38bb47aaf7 pippit-tool-cli-1.0.41-windows-amd64.zip
|
|
6
|
+
b49491ae751e72f994da9388777a43f1b6b5781399dc2da15ced6d74f48dad6d pippit-tool-cli-1.0.41-windows-arm64.zip
|
package/cmd/auth/auth.go
CHANGED
|
@@ -9,6 +9,7 @@ import (
|
|
|
9
9
|
"time"
|
|
10
10
|
|
|
11
11
|
internal_auth "github.com/Pippit-dev/pippit-cli/internal/auth"
|
|
12
|
+
"github.com/Pippit-dev/pippit-cli/internal/commandnames"
|
|
12
13
|
"github.com/Pippit-dev/pippit-cli/internal/common"
|
|
13
14
|
"github.com/spf13/cobra"
|
|
14
15
|
)
|
|
@@ -31,7 +32,7 @@ type logoutResult struct {
|
|
|
31
32
|
func NewLoginCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
|
|
32
33
|
var forceRefresh bool
|
|
33
34
|
command := &cobra.Command{
|
|
34
|
-
Use:
|
|
35
|
+
Use: commandnames.Login,
|
|
35
36
|
Short: "通过浏览器登录小云雀 CLI",
|
|
36
37
|
Args: cobra.NoArgs,
|
|
37
38
|
RunE: func(command *cobra.Command, _ []string) error {
|
|
@@ -66,7 +67,7 @@ func NewLoginCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Com
|
|
|
66
67
|
// NewStatusCommand creates the top-level `pippit-tool-cli status` command.
|
|
67
68
|
func NewStatusCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
|
|
68
69
|
command := &cobra.Command{
|
|
69
|
-
Use:
|
|
70
|
+
Use: commandnames.Status,
|
|
70
71
|
Short: "查看小云雀 CLI 登录状态",
|
|
71
72
|
Args: cobra.NoArgs,
|
|
72
73
|
RunE: func(command *cobra.Command, _ []string) error {
|
|
@@ -95,7 +96,7 @@ func NewStatusCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Co
|
|
|
95
96
|
// modified or revoked by this command.
|
|
96
97
|
func NewLogoutCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
|
|
97
98
|
command := &cobra.Command{
|
|
98
|
-
Use:
|
|
99
|
+
Use: commandnames.Logout,
|
|
99
100
|
Short: "清除本机小云雀 CLI 登录(不撤销远程 Access Key)",
|
|
100
101
|
Args: cobra.NoArgs,
|
|
101
102
|
RunE: func(command *cobra.Command, _ []string) error {
|
package/cmd/marketing.go
CHANGED
|
@@ -11,6 +11,7 @@ import (
|
|
|
11
11
|
"strings"
|
|
12
12
|
"time"
|
|
13
13
|
|
|
14
|
+
"github.com/Pippit-dev/pippit-cli/internal/adapter"
|
|
14
15
|
"github.com/Pippit-dev/pippit-cli/internal/common"
|
|
15
16
|
"github.com/Pippit-dev/pippit-cli/internal/config"
|
|
16
17
|
"github.com/spf13/cobra"
|
|
@@ -25,17 +26,17 @@ var marketingPaths = map[string]string{
|
|
|
25
26
|
|
|
26
27
|
// Marketing uses the same AuthManager as login/status and all other commands.
|
|
27
28
|
// Only fixed public endpoints are exposed; credentials never leave the CLI.
|
|
28
|
-
func newMarketingCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
|
|
29
|
+
func newMarketingCommand(stdout, stderr io.Writer, runner *common.Runner, policy adapter.HTTPAdapter) *cobra.Command {
|
|
29
30
|
root := &cobra.Command{Use: "marketing", Short: "Marketing API using the shared CLI login"}
|
|
30
31
|
root.SetOut(stdout)
|
|
31
32
|
root.SetErr(stderr)
|
|
32
33
|
for _, action := range []string{"generate", "query", "upload", "balance"} {
|
|
33
|
-
root.AddCommand(newMarketingAction(action, stdout, stderr, runner))
|
|
34
|
+
root.AddCommand(newMarketingAction(action, stdout, stderr, runner, policy))
|
|
34
35
|
}
|
|
35
36
|
return root
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
func newMarketingAction(action string, stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
|
|
39
|
+
func newMarketingAction(action string, stdout, stderr io.Writer, runner *common.Runner, policy adapter.HTTPAdapter) *cobra.Command {
|
|
39
40
|
var requestFile, file, threadID, runID, source string
|
|
40
41
|
var execute bool
|
|
41
42
|
var timeout time.Duration
|
|
@@ -123,7 +124,7 @@ func newMarketingAction(action string, stdout, stderr io.Writer, runner *common.
|
|
|
123
124
|
}
|
|
124
125
|
ctx, cancel := context.WithTimeout(cmd.Context(), timeout)
|
|
125
126
|
defer cancel()
|
|
126
|
-
client := common.
|
|
127
|
+
client := common.NewNonRedirectingHTTPClientWithAdapter(runner.Config.BaseURL, timeout, newRunnerAuthorizer(runner), adapter.WithPPE(policy, runner.Config.PPEEnv))
|
|
127
128
|
var result map[string]json.RawMessage
|
|
128
129
|
var err error
|
|
129
130
|
if action == "upload" {
|
package/cmd/marketing_test.go
CHANGED
|
@@ -14,6 +14,7 @@ import (
|
|
|
14
14
|
"testing"
|
|
15
15
|
"time"
|
|
16
16
|
|
|
17
|
+
"github.com/Pippit-dev/pippit-cli/internal/adapter"
|
|
17
18
|
"github.com/Pippit-dev/pippit-cli/internal/auth"
|
|
18
19
|
"github.com/Pippit-dev/pippit-cli/internal/config"
|
|
19
20
|
)
|
|
@@ -34,6 +35,63 @@ func (s *marketingCredentialStore) Load(context.Context) (*auth.Credential, erro
|
|
|
34
35
|
|
|
35
36
|
const marketingRequest = `{"message":"make an ad","general_agent_settings":{"video_model":"chosen-model","show_subtitle":false}}`
|
|
36
37
|
|
|
38
|
+
type marketingTestAdapter struct{}
|
|
39
|
+
|
|
40
|
+
func (marketingTestAdapter) CLIOptions() adapter.CLIOptions {
|
|
41
|
+
return adapter.CLIOptions{DefaultAllowRootCommands: true}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
func (marketingTestAdapter) PrepareAPIRequest(_ context.Context, req *http.Request) (adapter.AuthDecision, error) {
|
|
45
|
+
req.Header.Set("X-Test-Adapter-Auth", "present")
|
|
46
|
+
return adapter.AuthProvided, nil
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func (marketingTestAdapter) WrapTransport(next http.RoundTripper) http.RoundTripper {
|
|
50
|
+
if next == nil {
|
|
51
|
+
next = http.DefaultTransport
|
|
52
|
+
}
|
|
53
|
+
return marketingTestTransport{next: next}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
func (marketingTestAdapter) StripSensitiveHeaders(*http.Request) {}
|
|
57
|
+
|
|
58
|
+
type marketingTestTransport struct{ next http.RoundTripper }
|
|
59
|
+
|
|
60
|
+
func (t marketingTestTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
61
|
+
cloned := req.Clone(req.Context())
|
|
62
|
+
cloned.Header.Set("X-Test-Adapter-Transport", "present")
|
|
63
|
+
return t.next.RoundTrip(cloned)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
func TestMarketingUsesRootAdapterForIndependentClient(t *testing.T) {
|
|
67
|
+
called := 0
|
|
68
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
69
|
+
called++
|
|
70
|
+
if r.Header.Get("X-Test-Adapter-Auth") != "present" || r.Header.Get("X-Test-Adapter-Transport") != "present" {
|
|
71
|
+
t.Errorf("marketing request bypassed root adapter: %#v", r.Header)
|
|
72
|
+
}
|
|
73
|
+
if r.Header.Get("x-tt-env") != "ppe_fixture" || r.Header.Get("x-use-ppe") != "1" {
|
|
74
|
+
t.Error("marketing request bypassed runtime PPE config")
|
|
75
|
+
}
|
|
76
|
+
fmt.Fprint(w, `{"ret":"0","data":{}}`)
|
|
77
|
+
}))
|
|
78
|
+
defer server.Close()
|
|
79
|
+
|
|
80
|
+
cfg := config.Load()
|
|
81
|
+
cfg.BaseURL = server.URL
|
|
82
|
+
cfg.PPEEnv = "ppe_fixture"
|
|
83
|
+
policy := marketingTestAdapter{}
|
|
84
|
+
runner := newRootRunnerWithAdapter(cfg, policy)
|
|
85
|
+
root := newRootCommandWithAdapter(io.Discard, io.Discard, runner, policy)
|
|
86
|
+
root.SetArgs([]string{"marketing", "balance"})
|
|
87
|
+
if err := root.Execute(); err != nil {
|
|
88
|
+
t.Fatal(err)
|
|
89
|
+
}
|
|
90
|
+
if called != 1 {
|
|
91
|
+
t.Fatalf("marketing requests = %d, want 1", called)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
37
95
|
func TestMarketingUsesSharedBrowserAuth(t *testing.T) {
|
|
38
96
|
for _, action := range []string{"generate", "query", "upload", "balance"} {
|
|
39
97
|
t.Run(action, func(t *testing.T) {
|
package/cmd/root.go
CHANGED
|
@@ -14,6 +14,8 @@ import (
|
|
|
14
14
|
"github.com/Pippit-dev/pippit-cli/cmd/short_drama"
|
|
15
15
|
updatecmd "github.com/Pippit-dev/pippit-cli/cmd/update"
|
|
16
16
|
"github.com/Pippit-dev/pippit-cli/cmd/video_tool"
|
|
17
|
+
"github.com/Pippit-dev/pippit-cli/internal/adapter"
|
|
18
|
+
"github.com/Pippit-dev/pippit-cli/internal/adaptercli"
|
|
17
19
|
internal_auth "github.com/Pippit-dev/pippit-cli/internal/auth"
|
|
18
20
|
"github.com/Pippit-dev/pippit-cli/internal/common"
|
|
19
21
|
"github.com/Pippit-dev/pippit-cli/internal/config"
|
|
@@ -22,24 +24,42 @@ import (
|
|
|
22
24
|
)
|
|
23
25
|
|
|
24
26
|
// Execute runs the pippit-tool-cli command tree.
|
|
25
|
-
func Execute() error {
|
|
26
|
-
|
|
27
|
+
func Execute() error { return ExecuteWithAdapter(adapter.Default{}) }
|
|
28
|
+
|
|
29
|
+
func ExecuteWithAdapter(policy adapter.Adapter) error {
|
|
30
|
+
cfg, err := config.LoadFile(config.FileName)
|
|
31
|
+
if err != nil {
|
|
32
|
+
return err
|
|
33
|
+
}
|
|
34
|
+
policy = adapter.OrDefault(policy)
|
|
35
|
+
runner := newRootRunnerWithAdapter(cfg, policy)
|
|
36
|
+
return newRootCommandWithAdapter(os.Stdout, os.Stderr, runner, policy).Execute()
|
|
27
37
|
}
|
|
28
38
|
|
|
29
39
|
func NewRootCommand(stdout, stderr io.Writer) *cobra.Command {
|
|
40
|
+
return NewRootCommandWithAdapter(stdout, stderr, adapter.Default{})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
func NewRootCommandWithAdapter(stdout, stderr io.Writer, policy adapter.Adapter) *cobra.Command {
|
|
44
|
+
policy = adapter.OrDefault(policy)
|
|
30
45
|
cfg := config.Load()
|
|
31
|
-
runner :=
|
|
32
|
-
return
|
|
46
|
+
runner := newRootRunnerWithAdapter(cfg, policy)
|
|
47
|
+
return newRootCommandWithAdapter(stdout, stderr, runner, policy)
|
|
33
48
|
}
|
|
34
49
|
|
|
35
50
|
func newRootRunner(cfg *config.Config) *common.Runner {
|
|
51
|
+
return newRootRunnerWithAdapter(cfg, adapter.Default{})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
func newRootRunnerWithAdapter(cfg *config.Config, policy adapter.Adapter) *common.Runner {
|
|
36
55
|
runner := common.NewRunner(cfg, nil)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
cfg.
|
|
41
|
-
newRunnerAuthorizer(runner)
|
|
42
|
-
|
|
56
|
+
var authorizer common.RequestAuthorizer
|
|
57
|
+
options := policy.CLIOptions()
|
|
58
|
+
if options.LocalCredentialsEnabled {
|
|
59
|
+
runner.Auth = internal_auth.NewManager(cfg, internal_auth.WithCLIOptions(options))
|
|
60
|
+
authorizer = newRunnerAuthorizer(runner)
|
|
61
|
+
}
|
|
62
|
+
runner.Client = common.NewHTTPClientWithAdapter(cfg.BaseURL, cfg.HTTPTimeout, authorizer, adapter.WithPPE(policy, cfg.PPEEnv))
|
|
43
63
|
return runner
|
|
44
64
|
}
|
|
45
65
|
|
|
@@ -53,10 +73,14 @@ func newRunnerAuthorizer(runner *common.Runner) common.RequestAuthorizer {
|
|
|
53
73
|
}
|
|
54
74
|
|
|
55
75
|
func newRootCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
|
|
76
|
+
return newRootCommandWithAdapter(stdout, stderr, runner, adapter.Default{})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
func newRootCommandWithAdapter(stdout, stderr io.Writer, runner *common.Runner, policy adapter.Adapter) *cobra.Command {
|
|
56
80
|
root := &cobra.Command{
|
|
57
81
|
Use: "pippit-tool-cli",
|
|
58
82
|
Short: "Pippit CLI",
|
|
59
|
-
Long: "Pippit CLI generates and processes videos and images, queries credit balances, submits
|
|
83
|
+
Long: "Pippit CLI generates and processes videos and images, queries credit balances, submits short-drama workflows, downloads generated assets, and updates the installed CLI package.\n\n" +
|
|
60
84
|
canvascmd.CommandDiscoveryHelp + "\n\nInstallation help (npm launcher): pippit-tool-cli install --help",
|
|
61
85
|
Version: version.Current(),
|
|
62
86
|
SilenceUsage: true,
|
|
@@ -84,9 +108,10 @@ func newRootCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Comm
|
|
|
84
108
|
root.AddCommand(newDownloadResultCommand(stdout, stderr, runner))
|
|
85
109
|
root.AddCommand(newGetCreditBalanceCommand(stdout, stderr, runner))
|
|
86
110
|
root.AddCommand(newModelCommand(stdout, stderr, runner))
|
|
87
|
-
root.AddCommand(newMarketingCommand(stdout, stderr, runner))
|
|
111
|
+
root.AddCommand(newMarketingCommand(stdout, stderr, runner, policy))
|
|
88
112
|
root.AddCommand(newGetThreadCommand(stdout, stderr, runner))
|
|
89
|
-
|
|
113
|
+
// 暂停对外提供通用综合 Agent 提交入口,保留实现供后续恢复。
|
|
114
|
+
// root.AddCommand(newSubmitRunCommand(stdout, stderr, runner))
|
|
90
115
|
root.AddCommand(newUploadFileCommand(stdout, stderr, runner))
|
|
91
116
|
root.AddCommand(newListThreadFileCommand(stdout, stderr, runner))
|
|
92
117
|
root.AddCommand(generate_image.NewCommand(stdout, stderr, runner))
|
|
@@ -95,7 +120,8 @@ func newRootCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Comm
|
|
|
95
120
|
root.AddCommand(video_tool.NewSuperResolutionCommand(stdout, stderr, runner))
|
|
96
121
|
root.AddCommand(video_tool.NewEraseSubtitleCommand(stdout, stderr, runner))
|
|
97
122
|
root.AddCommand(short_drama.NewCommand(stdout, stderr, runner))
|
|
98
|
-
root.AddCommand(updatecmd.
|
|
123
|
+
root.AddCommand(updatecmd.NewCommandWithAdapter(stdout, stderr, policy))
|
|
124
|
+
adaptercli.Configure(root, policy.CLIOptions())
|
|
99
125
|
localizeFlagErrors(root)
|
|
100
126
|
return root
|
|
101
127
|
}
|
package/cmd/root_test.go
CHANGED
|
@@ -3,15 +3,78 @@ package cmd
|
|
|
3
3
|
import (
|
|
4
4
|
"bytes"
|
|
5
5
|
"context"
|
|
6
|
+
"fmt"
|
|
6
7
|
"net/http"
|
|
7
8
|
"net/http/httptest"
|
|
9
|
+
"os"
|
|
10
|
+
"path/filepath"
|
|
8
11
|
"strings"
|
|
9
12
|
"testing"
|
|
10
13
|
"time"
|
|
11
14
|
|
|
15
|
+
"github.com/Pippit-dev/pippit-cli/internal/common"
|
|
12
16
|
"github.com/Pippit-dev/pippit-cli/internal/config"
|
|
13
17
|
)
|
|
14
18
|
|
|
19
|
+
func TestRootRunnerUsesRuntimePPERouting(t *testing.T) {
|
|
20
|
+
headers := make(chan http.Header, 1)
|
|
21
|
+
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
22
|
+
headers <- r.Header.Clone()
|
|
23
|
+
_, _ = w.Write([]byte(`{}`))
|
|
24
|
+
})
|
|
25
|
+
api := httptest.NewServer(handler)
|
|
26
|
+
defer api.Close()
|
|
27
|
+
media := httptest.NewServer(handler)
|
|
28
|
+
defer media.Close()
|
|
29
|
+
dir := t.TempDir()
|
|
30
|
+
configPath, mediaPath := filepath.Join(dir, config.FileName), filepath.Join(dir, "sample.txt")
|
|
31
|
+
if err := os.WriteFile(mediaPath, []byte("fixture"), 0600); err != nil {
|
|
32
|
+
t.Fatal(err)
|
|
33
|
+
}
|
|
34
|
+
// Re-read the same file after changes, including clearing the PPE setting.
|
|
35
|
+
for _, env := range []string{"ppe_first", "ppe_second", ""} {
|
|
36
|
+
contents := fmt.Sprintf("xyq_api_url = %q\nx-tt-env = %q\n", api.URL, env)
|
|
37
|
+
if err := os.WriteFile(configPath, []byte(contents), 0600); err != nil {
|
|
38
|
+
t.Fatal(err)
|
|
39
|
+
}
|
|
40
|
+
cfg, err := config.LoadFile(configPath)
|
|
41
|
+
if err != nil {
|
|
42
|
+
t.Fatal(err)
|
|
43
|
+
}
|
|
44
|
+
cfg.AccessKey = "fixture-key"
|
|
45
|
+
runner := newRootRunner(cfg)
|
|
46
|
+
for _, request := range []struct {
|
|
47
|
+
name string
|
|
48
|
+
external bool
|
|
49
|
+
send func() error
|
|
50
|
+
}{
|
|
51
|
+
{"submit", false, func() error {
|
|
52
|
+
return runner.Client.SendRequest(context.Background(), config.SubmitRunPath, map[string]any{}, nil)
|
|
53
|
+
}},
|
|
54
|
+
{"query", false, func() error { return runner.Client.SendRequest(context.Background(), config.GetThreadPath, nil, nil) }},
|
|
55
|
+
{"upload", false, func() error {
|
|
56
|
+
return runner.Client.SendMultipartRequest(context.Background(), config.UploadFilePath, nil, common.MultipartFile{FieldName: "file", Path: mediaPath}, nil)
|
|
57
|
+
}},
|
|
58
|
+
{"media", true, func() error { return runner.Client.SendRequest(context.Background(), media.URL, nil, nil) }},
|
|
59
|
+
} {
|
|
60
|
+
if err := request.send(); err != nil {
|
|
61
|
+
t.Fatalf("%s: %v", request.name, err)
|
|
62
|
+
}
|
|
63
|
+
got := <-headers
|
|
64
|
+
wantEnv, wantUsePPE := env, ""
|
|
65
|
+
if request.external {
|
|
66
|
+
wantEnv = ""
|
|
67
|
+
}
|
|
68
|
+
if wantEnv != "" {
|
|
69
|
+
wantUsePPE = "1"
|
|
70
|
+
}
|
|
71
|
+
if got.Get("x-tt-env") != wantEnv || got.Get("x-use-ppe") != wantUsePPE {
|
|
72
|
+
t.Fatalf("%s with env %q: unexpected PPE headers", request.name, env)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
15
78
|
func TestRootRunnerReadsUpdatedAccessKeyForEveryRequest(t *testing.T) {
|
|
16
79
|
received := make([]string, 0, 2)
|
|
17
80
|
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
package/cmd/source_test.go
CHANGED
|
@@ -23,14 +23,13 @@ func TestSkillSubmitSourcePreservesCreativeRequest(t *testing.T) {
|
|
|
23
23
|
t.Setenv("HOME", t.TempDir())
|
|
24
24
|
t.Setenv("XDG_CACHE_HOME", t.TempDir())
|
|
25
25
|
t.Setenv("LocalAppData", t.TempDir())
|
|
26
|
-
for _, name := range []string{"
|
|
26
|
+
for _, name := range []string{"generate-image", "generate-video", "video-super-resolution", "erase-video-subtitle", "short-drama"} {
|
|
27
27
|
t.Run(name, func(t *testing.T) {
|
|
28
28
|
video := filepath.Join(t.TempDir(), "input.mp4")
|
|
29
29
|
if err := os.WriteFile(video, []byte("test-video"), 0600); err != nil {
|
|
30
30
|
t.Fatal(err)
|
|
31
31
|
}
|
|
32
32
|
args := map[string][]string{
|
|
33
|
-
"submit-run": {"submit-run", "--message", " 保留原文\nworkbuddy 只是内容 ", "--thread-id", "skill_existing", "--asset-ids", "asset_1"},
|
|
34
33
|
"generate-image": {"generate-image", "--prompt", "cat", "--model", "Seedream 5.0 Pro", "--ratio", "6", "--resolution", "4K"},
|
|
35
34
|
"generate-video": {"generate-video", "--prompt", "cat", "--model", "Seedance_2.0_mini", "--duration", "4", "--ratio", "16:9"},
|
|
36
35
|
"video-super-resolution": {"video-super-resolution", "--video", video, "--output-resolution", "1080p"},
|
|
@@ -192,7 +191,7 @@ func TestSkillSubmitSourcePreservesCreativeRequest(t *testing.T) {
|
|
|
192
191
|
|
|
193
192
|
func TestSourceHelpWithoutCredentials(t *testing.T) {
|
|
194
193
|
for _, args := range [][]string{
|
|
195
|
-
{"
|
|
194
|
+
{"generate-image"}, {"generate-video"},
|
|
196
195
|
{"video-super-resolution"}, {"erase-video-subtitle"}, {"short-drama", "+submit-run"},
|
|
197
196
|
} {
|
|
198
197
|
var stdout, stderr bytes.Buffer
|
package/cmd/submit_run_test.go
CHANGED
|
@@ -2,102 +2,41 @@ package cmd
|
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"bytes"
|
|
5
|
-
"encoding/json"
|
|
6
|
-
"net/http"
|
|
7
|
-
"net/http/httptest"
|
|
8
|
-
"reflect"
|
|
9
5
|
"strings"
|
|
10
6
|
"testing"
|
|
11
|
-
|
|
12
|
-
"github.com/Pippit-dev/pippit-cli/internal/config"
|
|
13
7
|
)
|
|
14
8
|
|
|
15
|
-
func
|
|
16
|
-
for _, existing := range []bool{false, true} {
|
|
17
|
-
t.Run(map[bool]string{false: "new", true: "existing_with_assets"}[existing], func(t *testing.T) {
|
|
18
|
-
message := " 根据参考素材生成视频\n保留原文 "
|
|
19
|
-
want := map[string]any{"message": message}
|
|
20
|
-
args := []string{"submit-run", "--message", message, "--source", ""}
|
|
21
|
-
if existing {
|
|
22
|
-
want["thread_id"] = "skill_original_thread"
|
|
23
|
-
want["asset_ids"] = []any{"asset_original_1", "asset_original_2"}
|
|
24
|
-
args = append(args, "--thread-id", "skill_original_thread", "--asset-ids", "asset_original_1", "--asset-ids", "asset_original_2")
|
|
25
|
-
}
|
|
26
|
-
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
27
|
-
if r.Method != http.MethodPost || r.URL.Path != config.SubmitRunPath {
|
|
28
|
-
t.Errorf("request = %s %s", r.Method, r.URL.Path)
|
|
29
|
-
}
|
|
30
|
-
if r.Header.Get("Authorization") != "Bearer test-token" {
|
|
31
|
-
t.Error("missing bearer authorization")
|
|
32
|
-
}
|
|
33
|
-
var got map[string]any
|
|
34
|
-
if err := json.NewDecoder(r.Body).Decode(&got); err != nil {
|
|
35
|
-
t.Error(err)
|
|
36
|
-
}
|
|
37
|
-
if !reflect.DeepEqual(got, want) {
|
|
38
|
-
t.Errorf("body = %#v, want %#v", got, want)
|
|
39
|
-
}
|
|
40
|
-
_, _ = w.Write([]byte(`{"ret":"0","data":{"run":{"thread_id":"thread_1","run_id":"run_1"},"web_thread_link":"https://xyq.jianying.com/thread_1"}}`))
|
|
41
|
-
}))
|
|
42
|
-
defer server.Close()
|
|
43
|
-
var stdout, stderr bytes.Buffer
|
|
44
|
-
root := newTestRootCommand(t, &stdout, &stderr, server.URL)
|
|
45
|
-
root.SetArgs(args)
|
|
46
|
-
if err := root.Execute(); err != nil {
|
|
47
|
-
t.Fatal(err)
|
|
48
|
-
}
|
|
49
|
-
if got := stdout.String(); got != "{\"thread_id\":\"thread_1\",\"run_id\":\"run_1\",\"web_thread_link\":\"https://xyq.jianying.com/thread_1\"}\n" {
|
|
50
|
-
t.Fatalf("unexpected output: %s", got)
|
|
51
|
-
}
|
|
52
|
-
})
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
func TestSubmitRunCommandValidatesBeforeRequest(t *testing.T) {
|
|
57
|
-
for _, args := range [][]string{
|
|
58
|
-
{"submit-run"},
|
|
59
|
-
{"submit-run", "--message", " \n "},
|
|
60
|
-
{"submit-run", "--message", "hello", "unexpected"},
|
|
61
|
-
} {
|
|
62
|
-
var stdout, stderr bytes.Buffer
|
|
63
|
-
root := newRootCommand(&stdout, &stderr, nil)
|
|
64
|
-
root.SetArgs(args)
|
|
65
|
-
if err := root.Execute(); err == nil {
|
|
66
|
-
t.Fatalf("Execute(%v) should fail before accessing the runner", args)
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
func TestSubmitRunHelpDoesNotRequireCredentials(t *testing.T) {
|
|
9
|
+
func TestSubmitRunIsNotExposedInRootHelp(t *testing.T) {
|
|
72
10
|
var stdout, stderr bytes.Buffer
|
|
73
11
|
root := newRootCommand(&stdout, &stderr, nil)
|
|
74
|
-
root.SetArgs([]string{"
|
|
12
|
+
root.SetArgs([]string{"--help"})
|
|
75
13
|
if err := root.Execute(); err != nil {
|
|
76
14
|
t.Fatal(err)
|
|
77
15
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
16
|
+
if strings.Contains(stdout.String(), "submit-run") || strings.Contains(stdout.String(), "creative conversations") {
|
|
17
|
+
t.Fatalf("root help still exposes creative conversations: %s", stdout.String())
|
|
18
|
+
}
|
|
19
|
+
command, _, err := root.Find([]string{"short-drama", "+submit-run"})
|
|
20
|
+
if err != nil || command == nil || command.Name() != "+submit-run" {
|
|
21
|
+
t.Fatalf("short-drama submission must remain available: command=%v, err=%v", command, err)
|
|
82
22
|
}
|
|
83
23
|
}
|
|
84
24
|
|
|
85
|
-
func
|
|
86
|
-
for _,
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
25
|
+
func TestSubmitRunIsUnavailable(t *testing.T) {
|
|
26
|
+
for _, args := range [][]string{
|
|
27
|
+
{"submit-run"},
|
|
28
|
+
{"submit-run", "--message", "生成一个小猫在花园散步的视频"},
|
|
29
|
+
{"submit-run", "--help"},
|
|
90
30
|
} {
|
|
91
|
-
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
92
|
-
_, _ = w.Write([]byte(response))
|
|
93
|
-
}))
|
|
94
31
|
var stdout, stderr bytes.Buffer
|
|
95
|
-
root :=
|
|
96
|
-
root.SetArgs(
|
|
32
|
+
root := newRootCommand(&stdout, &stderr, nil)
|
|
33
|
+
root.SetArgs(args)
|
|
97
34
|
err := root.Execute()
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
35
|
+
if err == nil || !strings.Contains(err.Error(), "unknown command") {
|
|
36
|
+
t.Fatalf("Execute(%v) = %v, want unknown command", args, err)
|
|
37
|
+
}
|
|
38
|
+
if stdout.Len() != 0 {
|
|
39
|
+
t.Fatalf("disabled command produced output: %s", stdout.String())
|
|
101
40
|
}
|
|
102
41
|
}
|
|
103
42
|
}
|