@mmmbuto/nexuscrew 0.8.24 → 0.8.26

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/lib/mcp/server.js CHANGED
@@ -25,7 +25,7 @@ const { loadConfig } = require('../config.js');
25
25
  const { readTokenSafe } = require('../auth/token.js');
26
26
  const { isValidSession } = require('../files/store.js');
27
27
  const VERSION = require('../../package.json').version;
28
- const { TOOLS } = require('./tools.js');
28
+ const { TOOLS, IDENTITY_CODE, IDENTITY_REMEDIATION } = require('./tools.js');
29
29
  const cells = require('./cells.js');
30
30
 
31
31
  // Versione protocollo di fallback se il client non ne dichiara una valida.
@@ -39,27 +39,83 @@ const METHOD_NOT_FOUND = -32601;
39
39
  const INVALID_PARAMS = -32602;
40
40
 
41
41
  // --- identita' cella mittente ------------------------------------------------
42
- // Ordine (design §1): $TMUX presente -> `tmux display-message -p '#S'` (nome
43
- // sessione reale); fallback env NEXUSCREW_MCP_SESSION; altrimenti null (i tool
44
- // che RICHIEDONO la sessione falliscono con errore chiaro, nc_notify degrada
45
- // a sender sconosciuto). execFile argv diretto: mai shell.
46
- function resolveSession({ env, tmuxBin, execFileImpl }) {
47
- const fallback = () => {
48
- const s = env.NEXUSCREW_MCP_SESSION;
49
- return (typeof s === 'string' && isValidSession(s.trim())) ? s.trim() : null;
42
+ // Ordine (design §1, INVARIATO): $TMUX presente -> `tmux display-message -p '#S'`
43
+ // (nome sessione reale); se fallisce/invalida -> fallback env NEXUSCREW_MCP_SESSION;
44
+ // altrimenti null. I tool che RICHIEDONO la sessione restano fail-closed.
45
+ // execFile argv diretto: mai shell.
46
+ //
47
+ // `resolveIdentity` rende OSSERVABILE la sorgente della risoluzione (P0):
48
+ // ritorna { session, source, code, envPresence, requiredEnvVars, remediation }
49
+ // senza cambiare la precedenza e senza esporre valori/segreti. `resolveSession`
50
+ // resta il wrapper pubblico Promise<string|null> invariato (compatibilita').
51
+ const IDENTITY_REQUIRED_ENV_VARS = Object.freeze(['TMUX', 'TMUX_PANE', 'NEXUSCREW_MCP_SESSION']);
52
+
53
+ function envPresenceOf(env) {
54
+ return {
55
+ TMUX: !!env.TMUX,
56
+ TMUX_PANE: !!env.TMUX_PANE,
57
+ NEXUSCREW_MCP_SESSION: !!(typeof env.NEXUSCREW_MCP_SESSION === 'string' && env.NEXUSCREW_MCP_SESSION.trim()),
58
+ };
59
+ }
60
+
61
+ function resolveIdentity({ env, tmuxBin, execFileImpl }) {
62
+ const e = env || {};
63
+ const envPresence = envPresenceOf(e);
64
+ const fallbackRaw = e.NEXUSCREW_MCP_SESSION;
65
+ const fallbackPresent = typeof fallbackRaw === 'string' && fallbackRaw.trim().length > 0;
66
+ const tmuxPresent = !!e.TMUX;
67
+
68
+ // Prova il fallback NEXUSCREW_MCP_SESSION: ritorna la sessione normalizzata se
69
+ // valida, `false` se presente ma invalida, `null` se assente.
70
+ const tryFallback = () => {
71
+ if (!fallbackPresent) return null;
72
+ const s = fallbackRaw.trim();
73
+ return isValidSession(s) ? s : false;
50
74
  };
51
- if (!env.TMUX) return Promise.resolve(fallback());
75
+
76
+ // code quando NON identificati: INVALID se c'e' un segnale di identita'
77
+ // (TMUX o NEXUSCREW_MCP_SESSION presente), MISSING altrimenti.
78
+ const codeWhenMissing = () => ((tmuxPresent || fallbackPresent)
79
+ ? IDENTITY_CODE.INVALID : IDENTITY_CODE.MISSING);
80
+
81
+ const ok = (session, source) => ({
82
+ session, source, code: IDENTITY_CODE.OK,
83
+ envPresence, requiredEnvVars: IDENTITY_REQUIRED_ENV_VARS, remediation: IDENTITY_REMEDIATION,
84
+ });
85
+ const missing = () => ({
86
+ session: null, source: 'missing', code: codeWhenMissing(),
87
+ envPresence, requiredEnvVars: IDENTITY_REQUIRED_ENV_VARS, remediation: IDENTITY_REMEDIATION,
88
+ });
89
+
52
90
  return new Promise((resolve) => {
91
+ if (!tmuxPresent) {
92
+ const fb = tryFallback();
93
+ return resolve(typeof fb === 'string' ? ok(fb, 'NEXUSCREW_MCP_SESSION') : missing());
94
+ }
53
95
  try {
54
96
  execFileImpl(tmuxBin, ['display-message', '-p', '#S'], { timeout: 3000 }, (err, stdout) => {
55
- if (err) return resolve(fallback());
56
- const name = String(stdout || '').trim();
57
- resolve(isValidSession(name) ? name : fallback());
97
+ if (!err) {
98
+ const name = String(stdout || '').trim();
99
+ if (isValidSession(name)) return resolve(ok(name, 'tmux'));
100
+ }
101
+ // tmux fallito/invalido -> precedenza preservata: fallback env.
102
+ const fb = tryFallback();
103
+ resolve(typeof fb === 'string' ? ok(fb, 'NEXUSCREW_MCP_SESSION') : missing());
58
104
  });
59
- } catch (_) { resolve(fallback()); }
105
+ } catch (_) {
106
+ const fb = tryFallback();
107
+ resolve(typeof fb === 'string' ? ok(fb, 'NEXUSCREW_MCP_SESSION') : missing());
108
+ }
60
109
  });
61
110
  }
62
111
 
112
+ // Wrapper pubblico STORICO: stessi parametri, stesso return Promise<string|null>.
113
+ // Mantiene i test esistenti e ogni chiamante esterno che dipende solo dal nome
114
+ // della sessione (o null). La diagnostica source/code vive in resolveIdentity.
115
+ function resolveSession(opts) {
116
+ return resolveIdentity(opts).then((i) => i.session);
117
+ }
118
+
63
119
  // --- server --------------------------------------------------------------------
64
120
  function createMcpServer(opts = {}) {
65
121
  const input = opts.input || process.stdin;
@@ -75,11 +131,15 @@ function createMcpServer(opts = {}) {
75
131
  const baseUrl = `http://127.0.0.1:${cfg.port}`;
76
132
 
77
133
  // Identita' risolta una volta e cacheata (la sessione tmux non cambia a runtime).
78
- let sessionP = null;
79
- const session = () => {
80
- if (!sessionP) sessionP = resolveSession({ env, tmuxBin: cfg.tmuxBin || 'tmux', execFileImpl });
81
- return sessionP;
134
+ // Una sola risoluzione condivisa: `identity()` per la diagnostica completa
135
+ // (source/code/presence), `session()` estrae solo il nome per gli handler
136
+ // storici (compatibilita'). Nessuna API/token coinvolta qui.
137
+ let identityP = null;
138
+ const identity = () => {
139
+ if (!identityP) identityP = resolveIdentity({ env, tmuxBin: cfg.tmuxBin || 'tmux', execFileImpl });
140
+ return identityP;
82
141
  };
142
+ const session = () => identity().then((i) => i.session);
83
143
 
84
144
  // Token letto ad OGNI chiamata (rotazione-friendly), MAI incluso negli errori.
85
145
  function readToken() {
@@ -113,6 +173,7 @@ function createMcpServer(opts = {}) {
113
173
 
114
174
  const ctx = {
115
175
  session,
176
+ identity,
116
177
  api,
117
178
  home: () => env.HOME || os.homedir(),
118
179
  fileExists: (p) => { try { return require('node:fs').statSync(p).isFile(); } catch (_) { return false; } },
@@ -252,7 +313,7 @@ function startMcp(opts = {}) {
252
313
  }
253
314
 
254
315
  module.exports = {
255
- createMcpServer, startMcp, resolveSession, TOOLS,
316
+ createMcpServer, startMcp, resolveSession, resolveIdentity, TOOLS,
256
317
  parseCellTarget: cells.parseCellTarget,
257
318
  normalizeCellPayload: cells.normalizeCellPayload,
258
319
  readCellDirectory: cells.readCellDirectory,
package/lib/mcp/tools.js CHANGED
@@ -27,10 +27,33 @@ function argString(args, key, { required = false, max = 4096 } = {}) {
27
27
  return v;
28
28
  }
29
29
 
30
- function requireSession(session, tool) {
30
+ // Codici stabili di identita' MCP (contratto P0). Valori EXACT, non sensibili,
31
+ // usati sia nella diagnostica read-only (`nc_identity`) sia nel messaggio umano
32
+ // dei tool identity-gated (isError=true preservato dal server). Non espongono
33
+ // valori/env/sessioni: solo la categoria del problema.
34
+ const IDENTITY_CODE = Object.freeze({
35
+ OK: 'OK',
36
+ MISSING: 'NEXUSCREW_MCP_IDENTITY_MISSING',
37
+ INVALID: 'NEXUSCREW_MCP_IDENTITY_INVALID',
38
+ });
39
+
40
+ // Remediation senza segreti/valori: nomi soltanto, compatibile con
41
+ // `codex-vl mcp add --env-var` (allowlist di nomi, nessun valore persistito).
42
+ const IDENTITY_REMEDIATION =
43
+ 'Nei client che ripuliscono l\'ambiente, allowlista i nomi delle variabili di identita nel server MCP stdio '
44
+ + '(codex-vl mcp add ... --env-var NEXUSCREW_MCP_SESSION --env-var TMUX --env-var TMUX_PANE) '
45
+ + 'oppure assicurati che il client MCP inoltri il contesto tmux al processo child: nessun valore '
46
+ + 'viene copiato nella CLI o nel file di configurazione.';
47
+
48
+ function requireSession(session, tool, code = IDENTITY_CODE.MISSING) {
31
49
  if (session) return session;
50
+ const stableCode = code === IDENTITY_CODE.INVALID ? IDENTITY_CODE.INVALID : IDENTITY_CODE.MISSING;
51
+ // Errore umano per il modello: messaggio chiaro + codice stabile fra parentesi
52
+ // quadre. isError=true e' impostato dal server (toolsCall); qui si propaga
53
+ // solo il testo. La regex storica /NEXUSCREW_MCP_SESSION/ resta soddisfatta.
32
54
  throw new Error(
33
- `${tool}: sessione tmux non identificata — serve $TMUX (dentro tmux) o NEXUSCREW_MCP_SESSION`,
55
+ `${tool}: sessione tmux non identificata — serve $TMUX (dentro tmux) o `
56
+ + `NEXUSCREW_MCP_SESSION [${stableCode}]`,
34
57
  );
35
58
  }
36
59
 
@@ -79,7 +102,8 @@ const TOOLS = [
79
102
  if (args.options !== undefined && !Array.isArray(args.options)) {
80
103
  throw new Error('options deve essere un array di stringhe');
81
104
  }
82
- const session = requireSession(await ctx.session(), 'nc_ask');
105
+ const identity = await ctx.identity();
106
+ const session = requireSession(identity.session, 'nc_ask', identity.code);
83
107
  const j = await ctx.api('POST', '/api/asks', {
84
108
  question, ...(args.options ? { options: args.options } : {}), session,
85
109
  });
@@ -109,7 +133,8 @@ const TOOLS = [
109
133
  if (!ctx.fileExists(p)) throw new Error(`file inesistente: ${p}`);
110
134
  const home = ctx.home();
111
135
  if (p !== home && !p.startsWith(home + path.sep)) throw new Error('path fuori da HOME');
112
- const session = requireSession(await ctx.session(), 'nc_send_file');
136
+ const identity = await ctx.identity();
137
+ const session = requireSession(identity.session, 'nc_send_file', identity.code);
113
138
  const j = await ctx.api('POST', '/api/files/outbox', {
114
139
  session, path: p, ...(caption ? { caption } : {}),
115
140
  });
@@ -145,7 +170,8 @@ const TOOLS = [
145
170
  inputSchema: { type: 'object', properties: {} },
146
171
  annotations: { readOnlyHint: true },
147
172
  async handler(_args, ctx) {
148
- const tmuxSession = requireSession(await ctx.session(), 'nc_deck');
173
+ const identity = await ctx.identity();
174
+ const tmuxSession = requireSession(identity.session, 'nc_deck', identity.code);
149
175
  const [config, topology, localDecks] = await Promise.all([
150
176
  ctx.api('GET', '/api/config'), ctx.api('GET', '/api/topology'), ctx.api('GET', '/api/decks'),
151
177
  ]);
@@ -257,7 +283,8 @@ const TOOLS = [
257
283
  if (code === 9 || code === 10 || code === 13) continue;
258
284
  if (code < 32 || code === 127) throw new Error('message contiene caratteri di controllo non ammessi');
259
285
  }
260
- const callerSession = requireSession(await ctx.session(), 'nc_send_cell');
286
+ const identity = await ctx.identity();
287
+ const callerSession = requireSession(identity.session, 'nc_send_cell', identity.code);
261
288
  const directory = await readCellDirectory(ctx, callerSession);
262
289
  const sender = directory.cells.find((cell) => cell.self && cell.active);
263
290
  if (!sender) throw new Error('nc_send_cell: la sessione chiamante non e\' una cella Fleet attiva locale');
@@ -290,11 +317,33 @@ const TOOLS = [
290
317
  inputSchema: { type: 'object', properties: {} },
291
318
  annotations: { readOnlyHint: true },
292
319
  async handler(_args, ctx) {
293
- const session = requireSession(await ctx.session(), 'nc_inbox');
320
+ const identity = await ctx.identity();
321
+ const session = requireSession(identity.session, 'nc_inbox', identity.code);
294
322
  const j = await ctx.api('GET', `/api/files?session=${encodeURIComponent(session)}`);
295
323
  return { inbox: Array.isArray(j.inbox) ? j.inbox : [] };
296
324
  },
297
325
  },
326
+ {
327
+ name: 'nc_identity',
328
+ description: 'Diagnostica read-only dell\'identita\' del chiamante MCP. Utilizzabile anche senza sessione tmux e senza token: restituisce SOLO dati non sensibili (presence delle env var, sorgente della risoluzione, codice stabile). Non chiama API HTTP e non legge il token. Usa questo tool quando gli altri tool nc_* falliscono con NEXUSCREW_MCP_IDENTITY_* per capire cosa manca.',
329
+ inputSchema: { type: 'object', properties: {} },
330
+ annotations: { readOnlyHint: true },
331
+ async handler(_args, ctx) {
332
+ const id = await ctx.identity();
333
+ // Output bounded e non sensibile: nessun valore/env, solo presence e codice.
334
+ // `session` solo se validata; `source` sempre fra i tre valori ammessi.
335
+ const out = {
336
+ identified: !!id.session,
337
+ source: id.source,
338
+ envPresence: id.envPresence,
339
+ requiredEnvVars: id.requiredEnvVars,
340
+ code: id.code,
341
+ remediation: id.remediation,
342
+ };
343
+ if (id.session) out.session = id.session;
344
+ return out;
345
+ },
346
+ },
298
347
  ];
299
348
 
300
- module.exports = { TOOLS, argString, requireSession };
349
+ module.exports = { TOOLS, argString, requireSession, IDENTITY_CODE, IDENTITY_REMEDIATION };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmmbuto/nexuscrew",
3
- "version": "0.8.24",
3
+ "version": "0.8.26",
4
4
  "description": "Faithful browser tmux client — attach to live sessions over a real PTY, localhost-only, mobile-easy",
5
5
  "main": "lib/server.js",
6
6
  "bin": {
@@ -12,6 +12,7 @@
12
12
  "skills/",
13
13
  "frontend/dist/",
14
14
  "frontend/index.html",
15
+ "CHANGELOG.md",
15
16
  "LICENSE",
16
17
  "README.md",
17
18
  "NOTICE"
@@ -19,7 +20,7 @@
19
20
  "scripts": {
20
21
  "start": "node bin/nexuscrew.js serve",
21
22
  "dev": "node bin/nexuscrew.js serve",
22
- "build": "cd frontend && npm install && npm run build && cd ..",
23
+ "build": "npm --prefix frontend ci && npm --prefix frontend run build",
23
24
  "test": "node tests/run-isolated.js && npm --prefix frontend test"
24
25
  },
25
26
  "dependencies": {
@@ -0,0 +1,133 @@
1
+ ---
2
+ name: alibaba-token-media
3
+ description: "Generate or edit images with Wan 2.7 and create videos with HappyHorse through an Alibaba Qwen Cloud Token Plan Personal subscription. Use when the user asks Claude Code, Codex, Codex-VL, or Pi to draw, generate, or edit an image with wan2.7-image or wan2.7-image-pro; create text-to-video or image-to-video media with happyhorse-1.1-t2v or happyhorse-1.1-i2v; inspect a submitted HappyHorse task; or validate the Token Plan media setup. Do not use this skill for chat, visual understanding, batch automation, backend services, or pay-as-you-go DashScope APIs."
4
+ ---
5
+
6
+ # Alibaba Token Media
7
+
8
+ Use the bundled, dependency-free CLI to access only the media models included
9
+ with the Lite guardrails of Alibaba Token Plan Personal. The same skill and Python script work
10
+ with Claude Code, Codex, Codex-VL, and Pi. Invoke the script directly from Pi;
11
+ do not assume that Pi provides a native MCP client.
12
+
13
+ Keep this workflow interactive. Every generation consumes plan Credits, and
14
+ the plan is not for unattended batch or backend workloads.
15
+
16
+ ## Safety contract
17
+
18
+ - Read the credential only from `ALIBABA_CODE_API_KEY`. Never request it in
19
+ chat, pass it on the command line, print it, persist it, or send it through
20
+ an agent bus.
21
+ - Use only the fixed Token Plan host and exact model allowlist implemented by
22
+ the script. Never fall back to DashScope pay-as-you-go endpoints.
23
+ - Run `status` and then a `--dry-run` before the first real request in a
24
+ session. Dry runs never require or reveal the key.
25
+ - Show the user the normalized model, operation, size or duration, resolution,
26
+ and confirmation flags before spending Credits. A real image request needs
27
+ `--confirm-credit-use`; a real video request additionally needs
28
+ `--confirm-expensive`.
29
+ - Require `--confirm-high-cost` for Wan Pro, 2K or 4K images, 1080P video, or
30
+ video longer than five seconds. Keep Lite defaults at one 1K base image or
31
+ one 720P three-second video.
32
+ - Never batch, enable sequential image sets, retry a generation POST
33
+ automatically, or run more than one generation submit concurrently. A task
34
+ status GET may be retried, but never with a tight polling loop.
35
+ - Accept local input only as a regular, non-symlink JPEG, PNG, BMP, or WEBP
36
+ file below 20 MiB and under the current user's home. Image-to-video accepts
37
+ JPEG, PNG, or WEBP first frames, not BMP. Accept remote input only
38
+ over public HTTPS; reject localhost, private IP literals, credentials, and
39
+ non-standard ports.
40
+ - Download signed result URLs immediately. Never paste signed URLs into chat,
41
+ logs, source files, or an agent bus; they expire after 24 hours.
42
+
43
+ ## Locate the CLI
44
+
45
+ Resolve `scripts/alibaba_token_media.py` relative to this `SKILL.md`, then run
46
+ it with Python 3:
47
+
48
+ ```bash
49
+ python3 <skill-dir>/scripts/alibaba_token_media.py status
50
+ ```
51
+
52
+ Keep this packaged skill as the source of truth; do not copy the script into a
53
+ project before using it.
54
+
55
+ ## Generate or edit an image
56
+
57
+ Start with a sanitized preview:
58
+
59
+ ```bash
60
+ python3 <skill-dir>/scripts/alibaba_token_media.py image \
61
+ --prompt "A quiet alpine lake at sunrise" \
62
+ --dry-run
63
+ ```
64
+
65
+ After explicit approval, repeat without `--dry-run` and add
66
+ `--confirm-credit-use`. For an edit, add one to nine `--image` values. Each
67
+ value may be a permitted local image under the current user's home or a public
68
+ HTTPS URL:
69
+
70
+ ```bash
71
+ python3 <skill-dir>/scripts/alibaba_token_media.py image \
72
+ --prompt "Preserve the subject; replace the background with a night city" \
73
+ --image /absolute/path/input.png \
74
+ --confirm-credit-use
75
+ ```
76
+
77
+ Use `--model wan2.7-image-pro --size 2K --confirm-high-cost` only after the
78
+ user selects that higher-cost variant. `4K` is allowed only for Pro
79
+ text-to-image with no input images. The script fixes `n=1` and disables image
80
+ sets. Image watermarking is off by default and enabled only with `--watermark`.
81
+
82
+ ## Submit a video
83
+
84
+ Text-to-video Lite preview:
85
+
86
+ ```bash
87
+ python3 <skill-dir>/scripts/alibaba_token_media.py video-submit \
88
+ --prompt "A paper model city slowly lights up at dusk" \
89
+ --dry-run
90
+ ```
91
+
92
+ For a real request, add both `--confirm-credit-use` and
93
+ `--confirm-expensive`. For image-to-video, select `happyhorse-1.1-i2v` and
94
+ supply exactly one first-frame image with `--image`. Do not supply `--ratio`
95
+ for image-to-video; its aspect ratio follows the input. Video watermarking is
96
+ on by default and can be disabled explicitly with `--no-watermark`.
97
+
98
+ ```bash
99
+ python3 <skill-dir>/scripts/alibaba_token_media.py video-submit \
100
+ --model happyhorse-1.1-i2v \
101
+ --image /absolute/path/first-frame.png \
102
+ --prompt "The camera slowly moves forward" \
103
+ --confirm-credit-use --confirm-expensive
104
+ ```
105
+
106
+ Return the task ID after a real submit. Do not hide submission success behind
107
+ polling.
108
+
109
+ ## Check and download a video
110
+
111
+ Query a task once:
112
+
113
+ ```bash
114
+ python3 <skill-dir>/scripts/alibaba_token_media.py video-status <task-id>
115
+ ```
116
+
117
+ When it is `SUCCEEDED`, repeat with `--download`. If recurring monitoring is
118
+ requested and the client exposes a loop facility, use an interval of at least
119
+ five minutes. Do not implement a private tight polling loop.
120
+
121
+ ```bash
122
+ python3 <skill-dir>/scripts/alibaba_token_media.py video-status <task-id> \
123
+ --download
124
+ ```
125
+
126
+ Default downloads go to
127
+ `~/Downloads/alibaba-token-plan/YYYY-MM-DD/` with unique names and no
128
+ overwrite. An explicit `--output` must remain under the current user's home.
129
+
130
+ ## Reference
131
+
132
+ Read [references/api-contract.md](references/api-contract.md) before changing
133
+ payloads, limits, endpoints, or model handling.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Alibaba Token Media"
3
+ short_description: "Generate Wan images and HappyHorse videos"
4
+ default_prompt: "Use $alibaba-token-media to generate or edit media safely with my Alibaba Token Plan."
@@ -0,0 +1,97 @@
1
+ # Alibaba Token Plan media API contract
2
+
3
+ Verified against the official Qwen Cloud documentation on 2026-07-19.
4
+
5
+ ## Fixed subscription surface
6
+
7
+ - Host: `https://token-plan.ap-southeast-1.maas.aliyuncs.com`
8
+ - Credential environment variable: `ALIBABA_CODE_API_KEY`
9
+ - Authorization: `Bearer <token>`
10
+ - Supported invocation surfaces: Claude Code, Codex, Codex-VL, and Pi through
11
+ the packaged skill and CLI. Pi is not assumed to support MCP natively.
12
+ - Token Plan Personal Lite guardrails: interactive use only, one submit at a
13
+ time, no batch generation, and no automatic retry of a generation POST.
14
+ - Never substitute the DashScope pay-as-you-go host.
15
+
16
+ The Token Plan documentation says coding tools cannot configure media models
17
+ as chat models. They must use a skill, command, agent, or equivalent extension.
18
+ The Personal plan is intended for interactive programming and agent-tool
19
+ work, not unattended scripts, backend services, or batch processing.
20
+
21
+ ## Wan 2.7 image generation and editing
22
+
23
+ Synchronous endpoint:
24
+
25
+ `POST /api/v1/services/aigc/multimodal-generation/generation`
26
+
27
+ Allowed plan models:
28
+
29
+ - `wan2.7-image`
30
+ - `wan2.7-image-pro`
31
+
32
+ Request shape:
33
+
34
+ ```json
35
+ {
36
+ "model": "wan2.7-image",
37
+ "input": {
38
+ "messages": [{
39
+ "role": "user",
40
+ "content": [{"text": "prompt"}, {"image": "data-or-https-url"}]
41
+ }]
42
+ },
43
+ "parameters": {
44
+ "n": 1,
45
+ "size": "1K",
46
+ "enable_sequential": false,
47
+ "watermark": false,
48
+ "thinking_mode": true
49
+ }
50
+ }
51
+ ```
52
+
53
+ The content array requires exactly one text object and accepts zero to nine
54
+ image objects. Prompt maximum is 5,000 characters. Inputs may be JPEG, PNG,
55
+ BMP, or WEBP and are limited to 20 MiB each. `wan2.7-image` supports 1K and 2K;
56
+ Pro also supports 4K for text-to-image only. The output image URL is under
57
+ `output.choices[*].message.content[*].image` and expires after 24 hours.
58
+
59
+ ## HappyHorse video generation
60
+
61
+ Submit endpoint:
62
+
63
+ `POST /api/v1/services/aigc/video-generation/video-synthesis`
64
+
65
+ Required submission header: `X-DashScope-Async: enable`.
66
+
67
+ Allowed plan models:
68
+
69
+ - `happyhorse-1.1-t2v`
70
+ - `happyhorse-1.1-i2v`
71
+
72
+ Text-to-video input contains `prompt`; parameters contain `resolution`,
73
+ `ratio`, and `duration`. Image-to-video input additionally contains exactly one
74
+ `media` item with `type: first_frame` and an HTTPS or Base64 image in `url`;
75
+ its parameters omit `ratio` because output follows the first frame. The CLI
76
+ enables video watermarking by default; image watermarking is disabled by
77
+ default. Image-to-video first frames accept JPEG, PNG, or WEBP, not BMP.
78
+
79
+ Duration is an integer from 3 through 15 seconds. Resolution is 720P or 1080P.
80
+ Text-to-video ratios are 16:9, 9:16, 1:1, 4:3, 3:4, 4:5, 5:4, 9:21, or 21:9.
81
+ The response task ID is `output.task_id`.
82
+
83
+ Status endpoint:
84
+
85
+ `GET /api/v1/tasks/{task_id}`
86
+
87
+ Terminal states are `SUCCEEDED`, `FAILED`, `CANCELED`, and `UNKNOWN`. On
88
+ success, `output.video_url` is an H.264 MP4 signed URL valid for 24 hours.
89
+
90
+ ## Official sources
91
+
92
+ - https://docs.qwencloud.com/token-plan/best-practices/integrate-multimodal-gen
93
+ - https://docs.qwencloud.com/token-plan/personal/token-plan-personal-overview
94
+ - https://docs.qwencloud.com/api-reference/image-generation/wan27-image-gen-edit/synchronous
95
+ - https://docs.qwencloud.com/api-reference/video-generation/happyhorse-text-to-video/create-task
96
+ - https://docs.qwencloud.com/api-reference/video-generation/happyhorse-image-to-video/create-task
97
+ - https://docs.qwencloud.com/api-reference/video-generation/happyhorse-text-to-video/query-result