Compare commits

...
Author SHA1 Message Date
Brandon Thomas cfa8264642 debug tool 2026-09-18 18:54:10 -04:00
Brandon Thomas fcb567ec3c Merge pull request #25 from storytold/gpt-image-qualities
gpt image 2.5: new qualities
2026-09-18 14:13:57 -04:00
10 changed files with 286 additions and 88 deletions

No files matched your search

+17 -1
View File
@@ -13,9 +13,25 @@ cargo run -p support-tool -- kinovi_web generate_video --prompt "A corgi at the
Requires a `.env-support-tool-secrets` file (or env vars) with:
- `SEEDANCE2PRO_COOKIES` — session cookies for Kinovi/KinoviWeb API auth
- `SEEDANCE2PRO_VOLCENGINE_COOKIES` — Volcengine cookies (legacy fallback: `SEEDANCE2PRO_COOKIES`)
- `SEEDANCE2PRO_BYTEPLUS_COOKIES` — BytePlus cookies (legacy fallback: `SEEDANCE2PRO_ALT_COOKIES`)
- `SEEDANCE2PRO_BYTEPLUS_ULTRA_COOKIES` — BytePlus Ultra cookies
- `ARTCRAFT_COOKIES` — session cookies for ArtCraft API auth (format: `session=...; visitor=...`)
All `kinovi_web` commands (also available as `kinovi` / `seedance2pro`) accept
`--kinovi-account volcengine|byteplus|byteplusultra`, defaulting to `volcengine`.
Use `--cookies-env ENV_VAR_NAME` to explicitly override that account's cookie
configuration. The override has no default; when omitted, only the selected
account's variables and its legacy fallback are checked. Either flag can appear
before or after the subcommand. CSV account labels default to the resolved cookie
env var name; `--account` still overrides the output label.
```bash
support-tool kinovi_web account_info --kinovi-account byteplus
support-tool seedance2pro find_job --token <order_id> --kinovi-account byteplusultra
support-tool kinovi_web account_info --kinovi-account byteplus --cookies-env CUSTOM_COOKIES
```
## Architecture
- Entry point: `src/main.rs` → normalizes args (underscores optional) → dispatches
@@ -0,0 +1,20 @@
use clap::ValueEnum;
#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
#[value(rename_all = "lowercase")]
pub enum KinoviAccount {
Volcengine,
BytePlus,
BytePlusUltra,
}
impl KinoviAccount {
/// Cookie configuration and legacy fallbacks used by seedance2-pro-job.
pub fn cookie_env_vars(self) -> &'static [&'static str] {
match self {
Self::Volcengine => &["SEEDANCE2PRO_VOLCENGINE_COOKIES", "SEEDANCE2PRO_COOKIES"],
Self::BytePlus => &["SEEDANCE2PRO_BYTEPLUS_COOKIES", "SEEDANCE2PRO_ALT_COOKIES"],
Self::BytePlusUltra => &["SEEDANCE2PRO_BYTEPLUS_ULTRA_COOKIES"],
}
}
}
@@ -1,6 +1,6 @@
use anyhow::anyhow;
use clap::Subcommand;
use clap::{Args, Subcommand};
use super::account::KinoviAccount;
use super::state::KinoviWebState;
use super::subcommands;
@@ -16,6 +16,20 @@ pub const SUBCOMMAND_NAMES: &[&str] = &[
"generate_video",
];
#[derive(Args)]
pub struct KinoviWebArgs {
/// Kinovi account whose cookie configuration to use.
#[arg(long, global = true, value_enum, ignore_case = true, default_value = "volcengine")]
pub kinovi_account: KinoviAccount,
/// Read cookies from this env var, overriding --kinovi-account only when supplied.
#[arg(long = "cookies-env", global = true, value_name = "ENV_VAR_NAME")]
pub maybe_cookies_env: Option<String>,
#[command(subcommand)]
pub command: KinoviWebCommand,
}
#[derive(Subcommand)]
#[command(rename_all = "snake_case")]
pub enum KinoviWebCommand {
@@ -44,13 +58,10 @@ pub enum KinoviWebCommand {
GenerateVideo(subcommands::generate_video::GenerateVideoArgs),
}
pub async fn run(command: KinoviWebCommand) -> anyhow::Result<()> {
let cookies = easyenv::get_env_string_required("SEEDANCE2PRO_COOKIES")
.map_err(|err| anyhow!("Missing SEEDANCE2PRO_COOKIES env var: {:?}", err))?;
pub async fn run(args: KinoviWebArgs) -> anyhow::Result<()> {
let state = KinoviWebState::from_env(args.kinovi_account, args.maybe_cookies_env.as_deref())?;
let state = KinoviWebState { cookies };
match command {
match args.command {
KinoviWebCommand::AccountInfo(args) => subcommands::account_info::run(&state, args).await,
KinoviWebCommand::AuditCredits(args) => subcommands::audit_credits::run(&state, args).await,
KinoviWebCommand::AuditOrders(args) => subcommands::audit_orders::run(&state, args).await,
@@ -60,3 +71,85 @@ pub async fn run(command: KinoviWebCommand) -> anyhow::Result<()> {
KinoviWebCommand::GenerateVideo(args) => subcommands::generate_video::run(&state, args).await,
}
}
#[cfg(test)]
mod tests {
use clap::{error::ErrorKind, Parser};
use super::*;
use crate::commands::run::{all_canonical_names, Cli, TopLevelCommand};
use crate::utils::normalize_subcommands::normalize_subcommand_args;
const COMMANDS: &[&[&str]] = &[
&["account_info"],
&["audit_credits", "--out", "/tmp/credits.csv"],
&["audit_orders", "--out", "/tmp/orders.csv"],
&["audit_payments", "--out", "/tmp/payments.csv"],
&["find_job", "--token", "test-order"],
&["failed_job_histogram"],
&["generate_video", "--prompt", "A corgi at the lake"],
];
mod cli_parsing {
use super::*;
#[test]
fn every_command_defaults_to_volcengine_without_a_cookie_override() {
for command in COMMANDS {
let args = parse_kinovi(command);
assert_eq!(args.kinovi_account, KinoviAccount::Volcengine);
assert!(args.maybe_cookies_env.is_none());
}
}
#[test]
fn every_command_accepts_all_accounts_and_cookie_overrides_before_or_after_subcommand() {
for (value, account) in [
("volcengine", KinoviAccount::Volcengine),
("byteplus", KinoviAccount::BytePlus),
("byteplusultra", KinoviAccount::BytePlusUltra),
] {
for command in COMMANDS {
let flags = ["--kinovi-account", value, "--cookies-env", "CUSTOM_COOKIES"];
for args in [
[flags.as_slice(), *command].concat(),
[*command, flags.as_slice()].concat(),
] {
let args = parse_kinovi(&args);
assert_eq!(args.kinovi_account, account);
assert_eq!(args.maybe_cookies_env.as_deref(), Some("CUSTOM_COOKIES"));
}
}
}
}
#[test]
fn aliases_and_underscore_normalization_preserve_account_selection() {
for alias in ["kinovi_web", "kinoviweb", "kinovi", "seedance2pro", "seedance2_pro"] {
let args = ["support-tool", alias, "accountinfo", "--kinovi-account", "BYTEPLUSULTRA"];
let args = normalize_subcommand_args(args.map(String::from), &all_canonical_names());
let cli = Cli::try_parse_from(args).unwrap();
let TopLevelCommand::KinoviWeb(args) = cli.command else {
panic!("expected Kinovi command");
};
assert_eq!(args.kinovi_account, KinoviAccount::BytePlusUltra);
}
}
#[test]
fn rejects_unknown_accounts() {
let err = Cli::try_parse_from([
"support-tool", "kinovi_web", "account_info", "--kinovi-account", "unknown",
]).err().expect("unknown accounts should be rejected");
assert_eq!(err.kind(), ErrorKind::InvalidValue);
}
}
fn parse_kinovi(args: &[&str]) -> KinoviWebArgs {
let cli = Cli::try_parse_from([&["support-tool", "kinovi_web"], args].concat()).unwrap();
match cli.command {
TopLevelCommand::KinoviWeb(args) => args,
_ => panic!("expected Kinovi command"),
}
}
}
@@ -1,6 +1,7 @@
mod account;
pub mod dispatch;
mod state;
mod subcommands;
pub use dispatch::KinoviWebCommand;
pub use dispatch::KinoviWebArgs;
pub use dispatch::run;
@@ -1,3 +1,131 @@
use anyhow::anyhow;
use super::account::KinoviAccount;
pub struct KinoviWebState {
pub cookies: String,
pub cookies_env: String,
}
impl KinoviWebState {
pub fn from_env(account: KinoviAccount, maybe_cookies_env: Option<&str>) -> anyhow::Result<Self> {
Self::from_env_lookup(account, maybe_cookies_env, easyenv::get_env_string_optional)
}
fn from_env_lookup(
account: KinoviAccount,
maybe_cookies_env: Option<&str>,
mut lookup: impl FnMut(&str) -> Option<String>,
) -> anyhow::Result<Self> {
// An explicit override must resolve on its own, without requiring or falling
// back to credentials for the selected account.
let override_vars;
let cookie_env_vars = match maybe_cookies_env {
Some(var) => {
override_vars = [var];
&override_vars[..]
},
None => account.cookie_env_vars(),
};
for &var in cookie_env_vars {
if let Some(cookies) = lookup(var) {
return Ok(Self {
cookies,
cookies_env: var.to_string(),
});
}
}
Err(anyhow!("Missing Kinovi cookies: set {}", cookie_env_vars.join(" or ")))
}
}
#[cfg(test)]
mod tests {
use super::*;
const COOKIES: &[(&str, &str)] = &[
("SEEDANCE2PRO_VOLCENGINE_COOKIES", "volcengine-cookies"),
("SEEDANCE2PRO_BYTEPLUS_COOKIES", "byteplus-cookies"),
("SEEDANCE2PRO_BYTEPLUS_ULTRA_COOKIES", "ultra-cookies"),
("SEEDANCE2PRO_COOKIES", "legacy-cookies"),
("SEEDANCE2PRO_ALT_COOKIES", "legacy-alt-cookies"),
("CUSTOM_COOKIES", "override-cookies"),
];
mod account_selection {
use super::*;
#[test]
fn selects_each_account_even_when_legacy_cookies_are_set() {
for (account, var, cookies) in [
(KinoviAccount::Volcengine, "SEEDANCE2PRO_VOLCENGINE_COOKIES", "volcengine-cookies"),
(KinoviAccount::BytePlus, "SEEDANCE2PRO_BYTEPLUS_COOKIES", "byteplus-cookies"),
(KinoviAccount::BytePlusUltra, "SEEDANCE2PRO_BYTEPLUS_ULTRA_COOKIES", "ultra-cookies"),
] {
for env in [COOKIES, &[(var, cookies)]] {
let state = resolve(account, None, env).unwrap();
assert_eq!(state.cookies, cookies);
assert_eq!(state.cookies_env, var);
}
}
}
#[test]
fn falls_back_to_the_matching_legacy_variable() {
let env = &COOKIES[3..5];
for (account, var, cookies) in [
(KinoviAccount::Volcengine, "SEEDANCE2PRO_COOKIES", "legacy-cookies"),
(KinoviAccount::BytePlus, "SEEDANCE2PRO_ALT_COOKIES", "legacy-alt-cookies"),
] {
let state = resolve(account, None, env).unwrap();
assert_eq!(state.cookies, cookies);
assert_eq!(state.cookies_env, var);
}
}
#[test]
fn missing_account_cookies_never_use_another_accounts_credentials() {
for (account, expected_var) in [
(KinoviAccount::BytePlus, "SEEDANCE2PRO_BYTEPLUS_COOKIES"),
(KinoviAccount::BytePlusUltra, "SEEDANCE2PRO_BYTEPLUS_ULTRA_COOKIES"),
] {
let err = resolve(account, None, &COOKIES[3..4]).err().expect("account cookies are missing");
assert!(err.to_string().contains(expected_var));
}
}
}
mod cookie_overrides {
use super::*;
#[test]
fn explicit_override_wins_with_or_without_account_cookies() {
for account in [KinoviAccount::Volcengine, KinoviAccount::BytePlus, KinoviAccount::BytePlusUltra] {
for env in [COOKIES, &COOKIES[5..]] {
let state = resolve(account, Some("CUSTOM_COOKIES"), env).unwrap();
assert_eq!(state.cookies, "override-cookies");
assert_eq!(state.cookies_env, "CUSTOM_COOKIES");
}
}
}
#[test]
fn missing_explicit_override_errors_instead_of_using_account_cookies() {
let err = resolve(KinoviAccount::Volcengine, Some("MISSING_COOKIES"), COOKIES)
.err().expect("explicit override must exist");
assert_eq!(err.to_string(), "Missing Kinovi cookies: set MISSING_COOKIES");
}
}
fn resolve(
account: KinoviAccount,
maybe_override: Option<&str>,
env: &[(&str, &str)],
) -> anyhow::Result<KinoviWebState> {
KinoviWebState::from_env_lookup(account, maybe_override, |var| {
env.iter().find(|(name, _)| *name == var).map(|(_, value)| value.to_string())
})
}
}
@@ -11,28 +11,14 @@ use super::super::state::KinoviWebState;
/// single CSV line: account,email,credits,available_credits.
#[derive(Args)]
pub struct AccountInfoArgs {
/// Read session cookies from this env var instead of SEEDANCE2PRO_COOKIES.
#[arg(long)]
pub cookies_env: Option<String>,
/// Account label for the output line. Defaults to the cookies env var name.
#[arg(long)]
pub account: Option<String>,
}
pub async fn run(state: &KinoviWebState, args: AccountInfoArgs) -> anyhow::Result<()> {
let (cookies, account_label) = match &args.cookies_env {
Some(var) => (
easyenv::get_env_string_required(var)
.map_err(|err| anyhow!("Missing {} env var: {:?}", var, err))?,
args.account.clone().unwrap_or_else(|| var.clone()),
),
None => (
state.cookies.clone(),
args.account.clone().unwrap_or_else(|| "SEEDANCE2PRO_COOKIES".to_string()),
),
};
let session = KinoviWebSession::from_cookies_string(cookies);
let account_label = args.account.as_deref().unwrap_or(&state.cookies_env);
let session = KinoviWebSession::from_cookies_string(state.cookies.clone());
let details = get_user_auth_details(GetUserAuthDetailsArgs {
session: &session,
@@ -44,10 +44,6 @@ pub struct AuditCreditsArgs {
#[arg(long, default_value_t = 250)]
pub delay_ms: u64,
/// Read session cookies from this env var instead of SEEDANCE2PRO_COOKIES.
#[arg(long)]
pub cookies_env: Option<String>,
/// Account label written into each CSV row. Defaults to the cookies env
/// var name.
#[arg(long)]
@@ -55,18 +51,8 @@ pub struct AuditCreditsArgs {
}
pub async fn run(state: &KinoviWebState, args: AuditCreditsArgs) -> anyhow::Result<()> {
let (cookies, account_label) = match &args.cookies_env {
Some(var) => (
easyenv::get_env_string_required(var)
.map_err(|err| anyhow!("Missing {} env var: {:?}", var, err))?,
args.account.clone().unwrap_or_else(|| var.clone()),
),
None => (
state.cookies.clone(),
args.account.clone().unwrap_or_else(|| "SEEDANCE2PRO_COOKIES".to_string()),
),
};
let session = KinoviWebSession::from_cookies_string(cookies);
let account_label = args.account.as_deref().unwrap_or(&state.cookies_env);
let session = KinoviWebSession::from_cookies_string(state.cookies.clone());
let file_is_new = std::fs::metadata(&args.out).map(|m| m.len() == 0).unwrap_or(true);
let mut out = OpenOptions::new().create(true).append(true).open(&args.out)
@@ -101,7 +87,7 @@ pub async fn run(state: &KinoviWebState, args: AuditCreditsArgs) -> anyhow::Resu
let oldest_day = page.entries.last().map(|e| day_of(e)).unwrap_or_default();
for entry in &page.entries {
write_entry_row(&mut out, &account_label, entry)?;
write_entry_row(&mut out, account_label, entry)?;
rows += 1;
let tally = type_tallies.entry(entry.entry_type.as_str().to_string()).or_insert((0, 0));
tally.0 += 1;
@@ -43,31 +43,15 @@ pub struct AuditOrdersArgs {
#[arg(long, default_value_t = 250)]
pub delay_ms: u64,
/// Read session cookies from this env var instead of SEEDANCE2PRO_COOKIES
/// (e.g. SEEDANCE2PRO_VOLCENGINE_COOKIES / SEEDANCE2PRO_BYTEPLUS_COOKIES /
/// SEEDANCE2PRO_BYTEPLUS_ULTRA_COOKIES to audit each Kinovi account).
#[arg(long)]
pub cookies_env: Option<String>,
/// Account label written into each CSV row. Defaults to the cookies env
/// var name (or "SEEDANCE2PRO_COOKIES").
/// var name.
#[arg(long)]
pub account: Option<String>,
}
pub async fn run(state: &KinoviWebState, args: AuditOrdersArgs) -> anyhow::Result<()> {
let (cookies, account_label) = match &args.cookies_env {
Some(var) => (
easyenv::get_env_string_required(var)
.map_err(|err| anyhow!("Missing {} env var: {:?}", var, err))?,
args.account.clone().unwrap_or_else(|| var.clone()),
),
None => (
state.cookies.clone(),
args.account.clone().unwrap_or_else(|| "SEEDANCE2PRO_COOKIES".to_string()),
),
};
let session = KinoviWebSession::from_cookies_string(cookies);
let account_label = args.account.as_deref().unwrap_or(&state.cookies_env);
let session = KinoviWebSession::from_cookies_string(state.cookies.clone());
let file_is_new = std::fs::metadata(&args.out).map(|m| m.len() == 0).unwrap_or(true);
let mut out = OpenOptions::new().create(true).append(true).open(&args.out)
@@ -106,7 +90,7 @@ pub async fn run(state: &KinoviWebState, args: AuditOrdersArgs) -> anyhow::Resul
stats.skipped_before_window += 1;
continue;
}
write_order_row(&mut out, &account_label, order)?;
write_order_row(&mut out, account_label, order)?;
stats.record(order);
}
out.flush()?;
@@ -133,7 +117,7 @@ pub async fn run(state: &KinoviWebState, args: AuditOrdersArgs) -> anyhow::Resul
tokio::time::sleep(Duration::from_millis(args.delay_ms)).await;
}
stats.print_summary(&account_label, cursor);
stats.print_summary(account_label, cursor);
Ok(())
}
@@ -29,10 +29,6 @@ pub struct AuditPaymentsArgs {
#[arg(long, default_value_t = 250)]
pub delay_ms: u64,
/// Read session cookies from this env var instead of SEEDANCE2PRO_COOKIES.
#[arg(long)]
pub cookies_env: Option<String>,
/// Account label written into each CSV row. Defaults to the cookies env
/// var name.
#[arg(long)]
@@ -40,18 +36,8 @@ pub struct AuditPaymentsArgs {
}
pub async fn run(state: &KinoviWebState, args: AuditPaymentsArgs) -> anyhow::Result<()> {
let (cookies, account_label) = match &args.cookies_env {
Some(var) => (
easyenv::get_env_string_required(var)
.map_err(|err| anyhow!("Missing {} env var: {:?}", var, err))?,
args.account.clone().unwrap_or_else(|| var.clone()),
),
None => (
state.cookies.clone(),
args.account.clone().unwrap_or_else(|| "SEEDANCE2PRO_COOKIES".to_string()),
),
};
let session = KinoviWebSession::from_cookies_string(cookies);
let account_label = args.account.as_deref().unwrap_or(&state.cookies_env);
let session = KinoviWebSession::from_cookies_string(state.cookies.clone());
let file_is_new = std::fs::metadata(&args.out).map(|m| m.len() == 0).unwrap_or(true);
let mut out = OpenOptions::new().create(true).append(true).open(&args.out)
@@ -82,7 +68,7 @@ pub async fn run(state: &KinoviWebState, args: AuditPaymentsArgs) -> anyhow::Res
}
for payment in &page.payments {
write_payment_row(&mut out, &account_label, payment)?;
write_payment_row(&mut out, account_label, payment)?;
rows += 1;
total_amount += payment.amount_usd;
total_credits += payment.credits_earned;
@@ -6,7 +6,7 @@ use super::kinovi_web;
/// All canonical subcommand names across all modules.
/// Used by the underscore-insensitive arg normalizer.
pub fn all_canonical_names() -> Vec<&'static str> {
let mut names: Vec<&str> = vec!["kinovi_web", "artcraft"];
let mut names: Vec<&str> = vec!["kinovi_web", "kinovi", "seedance2pro", "artcraft"];
names.extend_from_slice(kinovi_web::dispatch::SUBCOMMAND_NAMES);
names.extend_from_slice(artcraft::dispatch::SUBCOMMAND_NAMES);
names
@@ -23,10 +23,8 @@ pub struct Cli {
#[command(rename_all = "snake_case")]
pub enum TopLevelCommand {
/// Seedance2 Pro support commands (direct Kinovi API)
KinoviWeb {
#[command(subcommand)]
command: kinovi_web::KinoviWebCommand,
},
#[command(visible_aliases = ["kinovi", "seedance2pro"])]
KinoviWeb(kinovi_web::KinoviWebArgs),
/// ArtCraft support commands (omni API)
Artcraft(artcraft::dispatch::ArtcraftArgs),
@@ -34,8 +32,8 @@ pub enum TopLevelCommand {
pub async fn run(cli: Cli) -> anyhow::Result<()> {
match cli.command {
TopLevelCommand::KinoviWeb { command } => {
kinovi_web::run(command).await
TopLevelCommand::KinoviWeb(args) => {
kinovi_web::run(args).await
}
TopLevelCommand::Artcraft(args) => {
artcraft::dispatch(args).await