Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ python3 skills/xyq-nest-skill/scripts/download_results.py \

```bash
npx @pippit-dev/cli@latest install
export XYQ_ACCESS_KEY="<access-key>"
pippit-tool-cli login
pippit-tool-cli --version
pippit-tool-cli short-drama +submit-run --message "写一个赛博朋克短剧开头"
pippit-tool-cli short-drama +upload-file --path ./reference.doc
Expand All @@ -222,6 +222,26 @@ pippit-tool-cli download-result --output-path ./thread_123/results/result.mp4 --

短剧命令的错误日志会追加写入本地每日日志文件:`~/.pippit_tool_cli/logs/yyyy-mm-dd.log`。日志路径会基于当前用户主目录和系统路径分隔符生成,因此可在 macOS、Linux 和 Windows 上使用。

## Canvas 原子命令

CLI 提供个人漫剧画布的通用原子命令,不包含特定来源的导入或转换逻辑:

```bash
# 首次使用时打开小云雀网页授权
pippit-tool-cli login
pippit-tool-cli status

# 创建、查询、上传与提交单个画布 transaction
pippit-tool-cli canvas create --title "CLI Canvas" --wait
pippit-tool-cli canvas get --asset-id PIPPIT_ASSET_ID
pippit-tool-cli canvas upload --path ./reference.png
pippit-tool-cli canvas apply --project-id PROJECT_ID --file ./patch.json
```

仅测试 PPE 时,在命令前增加 `--ppe-env ppe_cli_canvas_ak`;生产环境不要设置该参数。PPE 只影响登录完成后的同源业务请求,不改变登录账号或本机凭证。

四个命令均输出单行 JSON,资源 ID 保持字符串。`create` 的 `request_id` 用于追踪,不是跨服务崩溃窗口的严格幂等键;写请求结果不明确时不要盲目重放,应先使用 `canvas get` 回读确认。`apply` 当前只接受一个 transaction,但该 transaction 可以包含多个 patches;CLI 会严格检查 transaction ACK 和每个目标资产的新版本。

## 生图 CLI

`generate-image` 会上传本地参考图片,然后向综合 Nest Agent 提交生图请求:
Expand Down Expand Up @@ -333,4 +353,6 @@ pippit-tool-cli query-result \

## 鉴权

`short-drama +submit-run`、`get-thread`、`list-thread-file`、`short-drama +upload-file` 以及 `xyq-skill` Python 脚本都使用 `Authorization: Bearer <XYQ_ACCESS_KEY>` 鉴权。OAuth 命令代码仍保留在仓库中,但短剧运行时请求不使用 OAuth。
原生 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 凭证库,当前仍需要该环境变量。
278 changes: 136 additions & 142 deletions cmd/auth/auth.go
Original file line number Diff line number Diff line change
@@ -1,144 +1,138 @@
package authcmd

//import (
// "fmt"
// "io"
// "strings"
// "time"
//
// "github.com/Pippit-dev/pippit-cli/internal/auth"
// "github.com/Pippit-dev/pippit-cli/internal/common"
// "github.com/bytedance/sonic"
// "github.com/spf13/cobra"
//)
//
//type checkResult struct {
// Pending bool `json:"pending"`
// State any `json:"state,omitempty"`
//}
//
//func NewCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
// cmd := &cobra.Command{
// Use: "auth",
// Short: "Manage Pippit OAuth login state",
// }
// cmd.SetOut(stdout)
// cmd.SetErr(stderr)
// cmd.AddCommand(newLoginCommand(stdout, stderr, runner))
// cmd.AddCommand(newCheckCommand(stdout, stderr, runner))
// cmd.AddCommand(newStatusCommand(stdout, stderr, runner))
// cmd.AddCommand(newLogoutCommand(stdout, stderr, runner))
// return cmd
//}
//
//func newLoginCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
// cmd := &cobra.Command{
// Use: "login",
// Short: "Start an OAuth device login flow",
// Args: cobra.NoArgs,
// RunE: func(cmd *cobra.Command, _ []string) error {
// if runner == nil || runner.AuthAuthorizer == nil {
// return fmt.Errorf("auth manager is required")
// }
// flow, err := runner.AuthAuthorizer.NewLoginFlow(cmd.Context())
// if err != nil {
// return err
// }
// return writeJSON(stdout, flow)
// },
// }
// cmd.SetOut(stdout)
// cmd.SetErr(stderr)
// return cmd
//}
//
//func newCheckCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
// var deviceCode string
// cmd := &cobra.Command{
// Use: "check",
// Short: "Check whether an OAuth device login has completed",
// Args: cobra.NoArgs,
// RunE: func(cmd *cobra.Command, _ []string) error {
// deviceCode = strings.TrimSpace(deviceCode)
// if deviceCode == "" {
// return fmt.Errorf("--device-code is required")
// }
// if runner == nil || runner.AuthAuthorizer == nil {
// return fmt.Errorf("auth manager is required")
// }
// state, err := runner.AuthAuthorizer.CheckLogin(cmd.Context(), deviceCode)
// if auth.IsLoginPending(err) {
// return writeJSON(stdout, checkResult{Pending: true})
// }
// if err != nil {
// return err
// }
// v := map[string]any{
// "logged_in": state.LoggedIn,
// "expires_at": state.ExpiresAt.Format(time.RFC3339),
// }
// return writeJSON(stdout, checkResult{State: v})
// },
// }
// cmd.SetOut(stdout)
// cmd.SetErr(stderr)
// cmd.Flags().StringVar(&deviceCode, "device-code", "", "device code returned by auth login")
// return cmd
//}
//
//func newStatusCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
// cmd := &cobra.Command{
// Use: "status",
// Short: "Show current OAuth login state",
// Args: cobra.NoArgs,
// RunE: func(cmd *cobra.Command, _ []string) error {
// if runner == nil || runner.AuthAuthorizer == nil {
// return fmt.Errorf("auth manager is required")
// }
// state, err := runner.AuthAuthorizer.State(cmd.Context())
// if err != nil {
// return err
// }
// if !state.LoggedIn {
// return fmt.Errorf("not logged in")
// }
// v := map[string]any{
// "logged_in": state.LoggedIn,
// "expires_at": state.ExpiresAt.Format(time.RFC3339),
// }
// return writeJSON(stdout, v)
// },
// }
// cmd.SetOut(stdout)
// cmd.SetErr(stderr)
// return cmd
//}
//
//func newLogoutCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
// cmd := &cobra.Command{
// Use: "logout",
// Short: "Clear local OAuth login state",
// Args: cobra.NoArgs,
// RunE: func(cmd *cobra.Command, _ []string) error {
// if runner == nil || runner.AuthAuthorizer == nil {
// return fmt.Errorf("auth manager is required")
// }
// if err := runner.AuthAuthorizer.Logout(cmd.Context()); err != nil {
// return err
// }
// return writeJSON(stdout, map[string]bool{"logged_out": true})
// },
// }
// cmd.SetOut(stdout)
// cmd.SetErr(stderr)
// return cmd
//}
//
//func writeJSON(w io.Writer, v any) error {
// data, err := sonic.Marshal(v)
// if err != nil {
// return err
// }
// _, err = fmt.Fprintln(w, string(data))
// return err
//}
import (
"encoding/json"
"errors"
"fmt"
"io"
"strings"
"time"

internal_auth "github.com/Pippit-dev/pippit-cli/internal/auth"
"github.com/Pippit-dev/pippit-cli/internal/common"
"github.com/spf13/cobra"
)

type loginResult struct {
LoggedIn bool `json:"logged_in"`
Source string `json:"source"`
UID string `json:"uid,omitempty"`
CredentialScope string `json:"credential_scope,omitempty"`
ExpiresAt string `json:"expires_at,omitempty"`
}

type logoutResult struct {
LoggedOut bool `json:"logged_out"`
EnvironmentStillActive bool `json:"environment_still_active,omitempty"`
RemoteCredentialPreserved bool `json:"remote_credential_preserved"`
}

// NewLoginCommand creates the top-level `pippit-tool-cli login` command.
func NewLoginCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
var forceRefresh bool
command := &cobra.Command{
Use: "login",
Short: "通过浏览器登录小云雀 CLI",
Args: cobra.NoArgs,
RunE: func(command *cobra.Command, _ []string) error {
manager, err := requireAuthManager(runner)
if err != nil {
return err
}
credential, err := manager.Login(command.Context(), internal_auth.LoginOptions{
Progress: stderr, ForceRefresh: forceRefresh,
})
if err != nil {
return err
}
if runner.Config != nil && strings.TrimSpace(runner.Config.AccessKey) != "" {
_, _ = fmt.Fprintln(stderr, "提示:当前进程设置了 XYQ_ACCESS_KEY,它会继续优先于刚保存的浏览器登录凭证。")
}
return writeJSON(stdout, loginResult{
LoggedIn: true,
Source: "browser",
UID: credential.UID,
CredentialScope: credential.CredentialScope,
ExpiresAt: time.Unix(credential.ExpiredAt, 0).Format(time.RFC3339),
})
},
}
command.SetOut(stdout)
command.SetErr(stderr)
command.Flags().BoolVar(&forceRefresh, "force", false, "强制轮换当前设备的 CLI Access Key(仅在旧密钥被拒绝时使用)")
return command
}

// NewStatusCommand creates the top-level `pippit-tool-cli status` command.
func NewStatusCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
command := &cobra.Command{
Use: "status",
Short: "查看小云雀 CLI 登录状态",
Args: cobra.NoArgs,
RunE: func(command *cobra.Command, _ []string) error {
manager, err := requireAuthManager(runner)
if err != nil {
return err
}
status, err := manager.Status(command.Context())
if err != nil {
return err
}
result := loginResult{LoggedIn: status.LoggedIn, Source: status.Source, UID: status.UID, CredentialScope: status.CredentialScope}
if !status.ExpiresAt.IsZero() {
result.ExpiresAt = status.ExpiresAt.Format(time.RFC3339)
}
return writeJSON(stdout, result)
},
}
command.SetOut(stdout)
command.SetErr(stderr)
return command
}

// NewLogoutCommand clears only the browser-managed local credential. An
// explicit XYQ_ACCESS_KEY belongs to the caller's environment and is never
// modified or revoked by this command.
func NewLogoutCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
command := &cobra.Command{
Use: "logout",
Short: "清除本机小云雀 CLI 登录(不撤销远程 Access Key)",
Args: cobra.NoArgs,
RunE: func(command *cobra.Command, _ []string) error {
manager, err := requireAuthManager(runner)
if err != nil {
return err
}
if err := manager.Logout(command.Context(), false); err != nil && !errors.Is(err, internal_auth.ErrCredentialNotFound) {
return err
}
environmentActive := runner.Config != nil && strings.TrimSpace(runner.Config.AccessKey) != ""
if environmentActive {
_, _ = fmt.Fprintln(stderr, "提示:XYQ_ACCESS_KEY 仍由当前 shell 提供,CLI 无法替你清除该环境变量。")
}
_, _ = fmt.Fprintln(stderr, "已清除本机登录密钥;远程 Access Key 未撤销,并保留非秘密设备标识供下次登录安全复用。")
return writeJSON(stdout, logoutResult{
LoggedOut: true, EnvironmentStillActive: environmentActive, RemoteCredentialPreserved: true,
})
},
}
command.SetOut(stdout)
command.SetErr(stderr)
return command
}

func requireAuthManager(runner *common.Runner) (common.AuthManager, error) {
if runner == nil || runner.Auth == nil {
return nil, fmt.Errorf("小云雀 CLI 浏览器授权尚未配置")
}
return runner.Auth, nil
}

func writeJSON(writer io.Writer, value any) error {
payload, err := json.Marshal(value)
if err != nil {
return fmt.Errorf("编码登录结果失败: %w", err)
}
_, err = fmt.Fprintln(writer, string(payload))
return err
}
Loading