@qwen-code/arena-agent 0.0.9-image-input-fallback.0 → 0.0.10-dev.0
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/.env.example +34 -13
- package/README.md +156 -53
- package/dist/agent.d.ts +4 -2
- package/dist/agent.js +142 -20
- package/dist/agent.js.map +1 -1
- package/dist/anthropic.d.ts +1 -0
- package/dist/anthropic.js +78 -19
- package/dist/anthropic.js.map +1 -1
- package/dist/canonical-data.js +2 -2
- package/dist/canonical-data.js.map +1 -1
- package/dist/cli.js +44 -13
- package/dist/cli.js.map +1 -1
- package/dist/code-tools.js +18 -161
- package/dist/code-tools.js.map +1 -1
- package/dist/compaction.js +8 -2
- package/dist/compaction.js.map +1 -1
- package/dist/config.d.ts +22 -0
- package/dist/config.js +53 -1
- package/dist/config.js.map +1 -1
- package/dist/history.d.ts +3 -2
- package/dist/history.js +7 -2
- package/dist/history.js.map +1 -1
- package/dist/image.d.ts +2 -0
- package/dist/image.js +88 -7
- package/dist/image.js.map +1 -1
- package/dist/model-capabilities.js +12 -2
- package/dist/model-capabilities.js.map +1 -1
- package/dist/model-diagnostics.js +11 -3
- package/dist/model-diagnostics.js.map +1 -1
- package/dist/model-router-media.d.ts +26 -0
- package/dist/model-router-media.js +274 -0
- package/dist/model-router-media.js.map +1 -0
- package/dist/openai-responses.d.ts +28 -0
- package/dist/openai-responses.js +473 -0
- package/dist/openai-responses.js.map +1 -0
- package/dist/process-manager.d.ts +38 -0
- package/dist/process-manager.js +486 -0
- package/dist/process-manager.js.map +1 -0
- package/dist/prompts.js +3 -1
- package/dist/prompts.js.map +1 -1
- package/dist/provider.d.ts +7 -0
- package/dist/provider.js +259 -14
- package/dist/provider.js.map +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.js +37 -18
- package/dist/server.js.map +1 -1
- package/dist/spec.js +15 -1
- package/dist/spec.js.map +1 -1
- package/dist/tools.js +160 -237
- package/dist/tools.js.map +1 -1
- package/dist/tui.d.ts +2 -0
- package/dist/tui.js +15 -11
- package/dist/tui.js.map +1 -1
- package/dist/types.d.ts +7 -0
- package/dist/voice-registry.d.ts +30 -0
- package/dist/voice-registry.js +82 -0
- package/dist/voice-registry.js.map +1 -0
- package/dist/web/app.js +8 -0
- package/dist/web/app.js.map +1 -1
- package/docs/SDK.md +4 -3
- package/package.json +7 -2
- package/web/styles.css +6 -0
- package/dist/audit.d.ts +0 -14
- package/dist/audit.js +0 -31
- package/dist/audit.js.map +0 -1
- package/dist/dashscope.d.ts +0 -5
- package/dist/dashscope.js +0 -55
- package/dist/dashscope.js.map +0 -1
- package/dist/fetch-page/aparat.d.ts +0 -30
- package/dist/fetch-page/aparat.js +0 -209
- package/dist/fetch-page/aparat.js.map +0 -1
- package/dist/fetch-page/json-path.d.ts +0 -12
- package/dist/fetch-page/json-path.js +0 -111
- package/dist/fetch-page/json-path.js.map +0 -1
- package/dist/fetch-page/sites.d.ts +0 -26
- package/dist/fetch-page/sites.js +0 -39
- package/dist/fetch-page/sites.js.map +0 -1
- package/dist/fetch-page/text.d.ts +0 -5
- package/dist/fetch-page/text.js +0 -38
- package/dist/fetch-page/text.js.map +0 -1
- package/dist/fetch-page/types.d.ts +0 -17
- package/dist/fetch-page/types.js +0 -3
- package/dist/fetch-page/types.js.map +0 -1
- package/dist/fetch-page/youtube.d.ts +0 -35
- package/dist/fetch-page/youtube.js +0 -282
- package/dist/fetch-page/youtube.js.map +0 -1
- package/dist/web-search.d.ts +0 -5
- package/dist/web-search.js +0 -465
- package/dist/web-search.js.map +0 -1
package/dist/tools.js
CHANGED
|
@@ -2,7 +2,6 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { existsSync, mkdirSync, realpathSync, } from "node:fs";
|
|
3
3
|
import { open, readFile, realpath, stat, writeFile, } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { setTimeout as delay } from "node:timers/promises";
|
|
6
5
|
import { Ajv } from "ajv";
|
|
7
6
|
import addFormats from "ajv-formats";
|
|
8
7
|
import { detect as detectEncoding } from "chardet";
|
|
@@ -14,12 +13,15 @@ import { CodeToolExecutor } from "./code-tools.js";
|
|
|
14
13
|
import { supportsImageInput } from "./config.js";
|
|
15
14
|
import { extractStructuredDocument, isStructuredDocument, } from "./document-reader.js";
|
|
16
15
|
import { FetchPageReader } from "./fetch-page.js";
|
|
17
|
-
import { prepareImageForModel } from "./image.js";
|
|
16
|
+
import { generatedImageBytesForExtension, prepareImageForModel, } from "./image.js";
|
|
18
17
|
import { imageTextFromFallbackModel } from "./image-input-fallback.js";
|
|
19
18
|
import { isCodeArenaMode } from "./mode.js";
|
|
19
|
+
import { generateImageWithModelRouter, generateSpeechWithModelRouter, } from "./model-router-media.js";
|
|
20
|
+
import { processManagerForWorkspace, } from "./process-manager.js";
|
|
20
21
|
import { HttpModelClient } from "./provider.js";
|
|
21
22
|
import { RoutifyFetchPageReader, routifyWebFetchEnabled, } from "./routify-web-fetch.js";
|
|
22
23
|
import { toolDefinitions } from "./spec.js";
|
|
24
|
+
import { voiceRegistryForWorkspace, } from "./voice-registry.js";
|
|
23
25
|
const knownTextApplicationMimeTypes = new Set([
|
|
24
26
|
"application/ecmascript",
|
|
25
27
|
"application/javascript",
|
|
@@ -361,58 +363,6 @@ function record(value) {
|
|
|
361
363
|
function stringValue(value) {
|
|
362
364
|
return typeof value === "string" ? value : "";
|
|
363
365
|
}
|
|
364
|
-
function dashScopeApiKey() {
|
|
365
|
-
const apiKey = process.env.DASHSCOPE_API_KEY;
|
|
366
|
-
if (!apiKey) {
|
|
367
|
-
throw new Error("DashScope tools require DASHSCOPE_API_KEY");
|
|
368
|
-
}
|
|
369
|
-
return apiKey;
|
|
370
|
-
}
|
|
371
|
-
function dashScopeRoot() {
|
|
372
|
-
return (process.env.DASHSCOPE_API_BASE ||
|
|
373
|
-
process.env.DASHSCOPE_BASE_URL ||
|
|
374
|
-
"https://dashscope.aliyuncs.com")
|
|
375
|
-
.replace(/\/+$/, "")
|
|
376
|
-
.replace(/\/(?:api\/v1|compatible-mode\/v1)$/i, "");
|
|
377
|
-
}
|
|
378
|
-
function dashScopeUrl(protocol, pathname) {
|
|
379
|
-
const prefix = protocol === "api" ? "/api/v1" : "/compatible-mode/v1";
|
|
380
|
-
const normalized = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
|
381
|
-
return `${dashScopeRoot()}${prefix}${normalized}`;
|
|
382
|
-
}
|
|
383
|
-
async function dashScopeFetch(protocol, pathname, init, label) {
|
|
384
|
-
const headers = new Headers(init.headers);
|
|
385
|
-
headers.set("authorization", `Bearer ${dashScopeApiKey()}`);
|
|
386
|
-
const response = await fetch(dashScopeUrl(protocol, pathname), {
|
|
387
|
-
...init,
|
|
388
|
-
headers,
|
|
389
|
-
});
|
|
390
|
-
if (response.ok)
|
|
391
|
-
return response;
|
|
392
|
-
const body = await response.text();
|
|
393
|
-
let detail = body;
|
|
394
|
-
try {
|
|
395
|
-
const payload = record(JSON.parse(body));
|
|
396
|
-
detail =
|
|
397
|
-
stringValue(payload.message) ||
|
|
398
|
-
stringValue(record(payload.error).message) ||
|
|
399
|
-
body;
|
|
400
|
-
}
|
|
401
|
-
catch {
|
|
402
|
-
// Keep the response text as the diagnostic.
|
|
403
|
-
}
|
|
404
|
-
throw new Error(`${label} failed (${response.status} ${response.statusText}): ${detail.slice(0, 2000)}`);
|
|
405
|
-
}
|
|
406
|
-
async function dashScopeJson(protocol, pathname, init, label) {
|
|
407
|
-
const response = await dashScopeFetch(protocol, pathname, init, label);
|
|
408
|
-
return record(await response.json());
|
|
409
|
-
}
|
|
410
|
-
function responseMessage(payload) {
|
|
411
|
-
return (stringValue(payload.message) ||
|
|
412
|
-
stringValue(record(payload.output).message) ||
|
|
413
|
-
stringValue(payload.code) ||
|
|
414
|
-
"unknown provider error");
|
|
415
|
-
}
|
|
416
366
|
async function responseBytes(response, limit = 4 * 1024 * 1024) {
|
|
417
367
|
const bytes = Buffer.from(await response.arrayBuffer());
|
|
418
368
|
if (bytes.length > limit) {
|
|
@@ -432,16 +382,20 @@ class ArenaToolExecutor {
|
|
|
432
382
|
workspaceContext;
|
|
433
383
|
pageReader;
|
|
434
384
|
routifyPageReader;
|
|
385
|
+
processManager;
|
|
386
|
+
voiceRegistry;
|
|
435
387
|
ajv = new Ajv({ allErrors: true, strict: false });
|
|
436
388
|
// ask_user needs a handler: TUI/WebUI collect real user input, while
|
|
437
|
-
// headless `-p` may delegate to a configured remote User Agent.
|
|
438
|
-
//
|
|
389
|
+
// headless `-p` may delegate to a configured remote User Agent. add_voice
|
|
390
|
+
// remains available without one and deterministically registers a fallback.
|
|
439
391
|
askUserEnabled;
|
|
440
392
|
constructor(config, options = {}) {
|
|
441
393
|
this.config = config;
|
|
442
394
|
this.workspace = path.resolve(config.workspace);
|
|
443
395
|
mkdirSync(this.workspace, { recursive: true });
|
|
444
396
|
this.workspaceReal = realpathSync(this.workspace);
|
|
397
|
+
this.processManager = processManagerForWorkspace(this.workspaceReal);
|
|
398
|
+
this.voiceRegistry = voiceRegistryForWorkspace(this.workspaceReal);
|
|
445
399
|
this.askUserHandler = options.askUserHandler;
|
|
446
400
|
this.askUserEnabled = Boolean(options.askUserHandler);
|
|
447
401
|
if (config.imageInputFallbackModel?.trim()) {
|
|
@@ -461,7 +415,7 @@ class ArenaToolExecutor {
|
|
|
461
415
|
}
|
|
462
416
|
// The tool schemas offered to the model for this session. Identical to the
|
|
463
417
|
// canonical trace set, adjusted for the selected workspace runtime and minus
|
|
464
|
-
// ask_user when no handler is wired.
|
|
418
|
+
// ask_user when no handler is wired. add_voice stays available headlessly.
|
|
465
419
|
availableTools() {
|
|
466
420
|
let definitions = toolDefinitions(this.workspaceContext);
|
|
467
421
|
if (!this.askUserEnabled) {
|
|
@@ -557,6 +511,8 @@ class ArenaToolExecutor {
|
|
|
557
511
|
: "/home/user")), Number(values.timeout || 30));
|
|
558
512
|
case "ask_user":
|
|
559
513
|
return this.askUser(values.questions, toolCallId, context);
|
|
514
|
+
case "add_voice":
|
|
515
|
+
return this.addVoice(String(values.text), String(values.language), values.voice_identity, toolCallId, context);
|
|
560
516
|
case "edit_file":
|
|
561
517
|
return this.editFile(String(values.path), String(values.old_text), String(values.new_text));
|
|
562
518
|
case "read_file":
|
|
@@ -573,10 +529,28 @@ class ArenaToolExecutor {
|
|
|
573
529
|
return this.imageSearch(String(values.query), Number(values.count));
|
|
574
530
|
case "present_file":
|
|
575
531
|
return this.presentFile(String(values.path));
|
|
532
|
+
case "stop_process":
|
|
533
|
+
return this.processManager.stop(String(values.process_id));
|
|
534
|
+
case "start_process":
|
|
535
|
+
return this.startProcess(String(values.name || ""), String(values.command), String(values.cwd ||
|
|
536
|
+
(this.workspaceContext
|
|
537
|
+
? path.resolve(this.workspaceContext.cwd)
|
|
538
|
+
: "/home/user")), Number(values.startup_wait || 5));
|
|
576
539
|
case "generate_image":
|
|
577
540
|
return this.generateImage(String(values.file_path), String(values.prompt), values.images);
|
|
578
541
|
case "generate_speech":
|
|
579
|
-
return this.generateSpeech(String(values.file_path), String(values.text), String(values.
|
|
542
|
+
return this.generateSpeech(String(values.file_path), String(values.text), String(values.voice_id), values.language === undefined ? undefined : String(values.language));
|
|
543
|
+
case "get_process_output":
|
|
544
|
+
return this.processManager.get({
|
|
545
|
+
processId: String(values.process_id),
|
|
546
|
+
tailLines: Number(values.tail_lines || 200),
|
|
547
|
+
waitFor: values.wait_for,
|
|
548
|
+
waitPattern: values.wait_pattern === undefined
|
|
549
|
+
? undefined
|
|
550
|
+
: String(values.wait_pattern),
|
|
551
|
+
waitTimeoutSeconds: Number(values.wait_timeout || 60),
|
|
552
|
+
signal: this.signal,
|
|
553
|
+
});
|
|
580
554
|
default:
|
|
581
555
|
throw new Error(`unknown tool: ${name}`);
|
|
582
556
|
}
|
|
@@ -585,12 +559,15 @@ class ArenaToolExecutor {
|
|
|
585
559
|
if (!raw)
|
|
586
560
|
throw new Error("path must not be empty");
|
|
587
561
|
let candidate;
|
|
588
|
-
if (
|
|
562
|
+
if (raw === "/home/user") {
|
|
589
563
|
candidate = this.workspace;
|
|
590
564
|
}
|
|
591
|
-
else if (
|
|
565
|
+
else if (raw.startsWith("/home/user/")) {
|
|
592
566
|
candidate = path.join(this.workspace, raw.slice("/home/user/".length));
|
|
593
567
|
}
|
|
568
|
+
else if (!this.workspaceContext && raw === "~") {
|
|
569
|
+
candidate = this.workspace;
|
|
570
|
+
}
|
|
594
571
|
else if (!this.workspaceContext && raw.startsWith("~/")) {
|
|
595
572
|
candidate = path.join(this.workspace, raw.slice(2));
|
|
596
573
|
}
|
|
@@ -636,6 +613,17 @@ class ArenaToolExecutor {
|
|
|
636
613
|
}
|
|
637
614
|
return virtualized;
|
|
638
615
|
}
|
|
616
|
+
sandboxEnvironment() {
|
|
617
|
+
const env = {
|
|
618
|
+
...process.env,
|
|
619
|
+
ARENA_WORKSPACE: this.workspace,
|
|
620
|
+
LANG: "C.UTF-8",
|
|
621
|
+
LC_ALL: "C.UTF-8",
|
|
622
|
+
};
|
|
623
|
+
delete env.FORCE_COLOR;
|
|
624
|
+
delete env.CLICOLOR_FORCE;
|
|
625
|
+
return env;
|
|
626
|
+
}
|
|
639
627
|
async bash(command, cwd, timeoutSeconds) {
|
|
640
628
|
const workdir = await this.confinedPath(cwd);
|
|
641
629
|
if (!(await stat(workdir)).isDirectory()) {
|
|
@@ -647,18 +635,7 @@ class ArenaToolExecutor {
|
|
|
647
635
|
? command
|
|
648
636
|
: command.replaceAll("/home/user", "\"$ARENA_WORKSPACE\"");
|
|
649
637
|
return new Promise((resolve, reject) => {
|
|
650
|
-
const env =
|
|
651
|
-
...process.env,
|
|
652
|
-
ARENA_WORKSPACE: this.workspace,
|
|
653
|
-
// 生产 sandbox 是 C collation(参考 find|sort 为逐字节序),宿主
|
|
654
|
-
// 常见 en_US.UTF-8 会翻转大小写次序;统一成 C.UTF-8 对齐生产。
|
|
655
|
-
LANG: "C.UTF-8",
|
|
656
|
-
LC_ALL: "C.UTF-8",
|
|
657
|
-
};
|
|
658
|
-
// 交互终端(node --test、npm 等)会向子进程注入强制着色变量,
|
|
659
|
-
// 管道输出被 ANSI 色码污染;生产轨迹无色码,剥离之。
|
|
660
|
-
delete env.FORCE_COLOR;
|
|
661
|
-
delete env.CLICOLOR_FORCE;
|
|
638
|
+
const env = this.sandboxEnvironment();
|
|
662
639
|
// detached:管道成员(npm、tail…)与 bash 同进程组。只杀 bash 会留下
|
|
663
640
|
// 孤儿持有 stdout/stderr 管道,close 永不触发,整个 turn 挂死;
|
|
664
641
|
// 必须杀整组,并在宽限期后强制结算兜底。
|
|
@@ -673,7 +650,6 @@ class ArenaToolExecutor {
|
|
|
673
650
|
let stderr = "";
|
|
674
651
|
let timedOut = false;
|
|
675
652
|
let settled = false;
|
|
676
|
-
let killRequested = false;
|
|
677
653
|
let forceSettle;
|
|
678
654
|
const killTree = (signal) => {
|
|
679
655
|
try {
|
|
@@ -692,33 +668,26 @@ class ArenaToolExecutor {
|
|
|
692
668
|
throw error;
|
|
693
669
|
}
|
|
694
670
|
};
|
|
695
|
-
const finish = (code) => {
|
|
671
|
+
const finish = (code, signal) => {
|
|
696
672
|
if (settled)
|
|
697
673
|
return;
|
|
698
674
|
settled = true;
|
|
699
675
|
clearTimeout(timeout);
|
|
700
676
|
if (forceSettle)
|
|
701
677
|
clearTimeout(forceSettle);
|
|
702
|
-
//
|
|
703
|
-
//
|
|
704
|
-
//
|
|
705
|
-
|
|
706
|
-
if (killRequested)
|
|
707
|
-
killTree("SIGKILL");
|
|
678
|
+
// bash is one-shot in v2. Any descendant left in the process group is
|
|
679
|
+
// cleaned up even when the shell itself exited normally; persistent
|
|
680
|
+
// servers belong in start_process.
|
|
681
|
+
killTree("SIGKILL");
|
|
708
682
|
this.signal?.removeEventListener("abort", abort);
|
|
709
683
|
if (this.signal?.aborted) {
|
|
710
684
|
reject(this.signal.reason || new Error("operation cancelled"));
|
|
711
685
|
return;
|
|
712
686
|
}
|
|
713
|
-
// 生产契约:status 恒为 "completed"(包括非零退出码),超时无参考样本,
|
|
714
|
-
// 按最小猜测同样返回 completed + exit_code null,stderr 附超时说明。
|
|
715
|
-
if (timedOut) {
|
|
716
|
-
stderr += (stderr ? "\n" : "") + `command exceeded ${timeoutSeconds}s`;
|
|
717
|
-
}
|
|
718
687
|
const out = capBashOutput(this.virtualizeBashOutput(stdout));
|
|
719
688
|
const err = capBashOutput(this.virtualizeBashOutput(stderr));
|
|
720
689
|
resolve({
|
|
721
|
-
status: "completed",
|
|
690
|
+
status: timedOut ? "timeout" : signal ? "killed" : "completed",
|
|
722
691
|
stderr: err.text,
|
|
723
692
|
stdout: out.text,
|
|
724
693
|
exit_code: timedOut ? null : code,
|
|
@@ -732,17 +701,15 @@ class ArenaToolExecutor {
|
|
|
732
701
|
const armForceSettle = () => {
|
|
733
702
|
forceSettle ??= setTimeout(() => {
|
|
734
703
|
killTree("SIGKILL");
|
|
735
|
-
finish(null);
|
|
704
|
+
finish(null, "SIGKILL");
|
|
736
705
|
}, 2000);
|
|
737
706
|
};
|
|
738
707
|
const timeout = setTimeout(() => {
|
|
739
708
|
timedOut = true;
|
|
740
|
-
killRequested = true;
|
|
741
709
|
killTree("SIGKILL");
|
|
742
710
|
armForceSettle();
|
|
743
711
|
}, timeoutSeconds * 1000);
|
|
744
712
|
const abort = () => {
|
|
745
|
-
killRequested = true;
|
|
746
713
|
killTree("SIGTERM");
|
|
747
714
|
armForceSettle();
|
|
748
715
|
};
|
|
@@ -763,7 +730,24 @@ class ArenaToolExecutor {
|
|
|
763
730
|
this.signal?.removeEventListener("abort", abort);
|
|
764
731
|
reject(error);
|
|
765
732
|
});
|
|
766
|
-
child.once("close", (code) => finish(code));
|
|
733
|
+
child.once("close", (code, signal) => finish(code, signal));
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
async startProcess(name, command, cwd, startupWaitSeconds) {
|
|
737
|
+
const workdir = await this.confinedPath(cwd);
|
|
738
|
+
if (!(await stat(workdir)).isDirectory()) {
|
|
739
|
+
throw new Error(`cwd is not a directory: ${cwd}`);
|
|
740
|
+
}
|
|
741
|
+
const translatedCommand = this.workspaceContext
|
|
742
|
+
? command
|
|
743
|
+
: command.replaceAll("/home/user", '"$ARENA_WORKSPACE"');
|
|
744
|
+
return this.processManager.start({
|
|
745
|
+
name,
|
|
746
|
+
command: translatedCommand,
|
|
747
|
+
cwd: workdir,
|
|
748
|
+
startupWaitSeconds,
|
|
749
|
+
env: this.sandboxEnvironment(),
|
|
750
|
+
signal: this.signal,
|
|
767
751
|
});
|
|
768
752
|
}
|
|
769
753
|
async askUser(questions, toolCallId, context) {
|
|
@@ -783,6 +767,58 @@ class ArenaToolExecutor {
|
|
|
783
767
|
"Proceed with reasonable defaults and state any assumptions you make.",
|
|
784
768
|
};
|
|
785
769
|
}
|
|
770
|
+
async addVoice(text, language, voiceIdentity = {}, toolCallId, context) {
|
|
771
|
+
if (!this.askUserHandler) {
|
|
772
|
+
const selection = this.voiceRegistry.registerVirtual(language, voiceIdentity);
|
|
773
|
+
return {
|
|
774
|
+
status: "success",
|
|
775
|
+
message: `Battle "${selection.battleId}": no interactive voice picker was ` +
|
|
776
|
+
`available, so a default voice was selected. Pass voice_id ` +
|
|
777
|
+
`"${selection.voiceId}" to generate_speech to speak with it.`,
|
|
778
|
+
voice_id: selection.voiceId,
|
|
779
|
+
battle_id: selection.battleId,
|
|
780
|
+
virtual: true,
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
const battle = await this.voiceRegistry.audition(text, language, voiceIdentity, this.signal);
|
|
784
|
+
const response = await this.askUserHandler(toolCallId, [
|
|
785
|
+
{
|
|
786
|
+
id: `voice-battle-${battle.battleId}`,
|
|
787
|
+
question: "Which voice should be used for this speaker?",
|
|
788
|
+
options: battle.candidates.map((candidate) => ({
|
|
789
|
+
id: candidate.id,
|
|
790
|
+
label: candidate.label,
|
|
791
|
+
description: "Play the audition, then select this voice.",
|
|
792
|
+
audioDataUrl: candidate.audioDataUrl,
|
|
793
|
+
})),
|
|
794
|
+
allowCustomResponse: false,
|
|
795
|
+
},
|
|
796
|
+
], context
|
|
797
|
+
? {
|
|
798
|
+
...context,
|
|
799
|
+
signal: this.signal || context.signal,
|
|
800
|
+
}
|
|
801
|
+
: undefined);
|
|
802
|
+
if (response.status === "needs_input")
|
|
803
|
+
return response;
|
|
804
|
+
if (response.skipped === true) {
|
|
805
|
+
return { status: "error", message: "The user skipped voice selection." };
|
|
806
|
+
}
|
|
807
|
+
const answers = Array.isArray(response.answers) ? response.answers : [];
|
|
808
|
+
const selectedOptionId = record(answers[0]).selectedOptionId;
|
|
809
|
+
if (typeof selectedOptionId !== "string") {
|
|
810
|
+
throw new Error("voice selection did not choose a candidate");
|
|
811
|
+
}
|
|
812
|
+
const voiceId = this.voiceRegistry.register(battle, selectedOptionId);
|
|
813
|
+
return {
|
|
814
|
+
status: "success",
|
|
815
|
+
message: `Battle "${battle.battleId}": the user selected a voice. ` +
|
|
816
|
+
`Pass voice_id "${voiceId}" to generate_speech to speak with it. ` +
|
|
817
|
+
"Continue the task now — if you need another voice, call add_voice again immediately.",
|
|
818
|
+
voice_id: voiceId,
|
|
819
|
+
battle_id: battle.battleId,
|
|
820
|
+
};
|
|
821
|
+
}
|
|
786
822
|
async editFile(rawPath, oldText, newText) {
|
|
787
823
|
const target = await this.confinedPath(rawPath);
|
|
788
824
|
const source = await readFile(target, "utf8");
|
|
@@ -1012,13 +1048,13 @@ class ArenaToolExecutor {
|
|
|
1012
1048
|
}
|
|
1013
1049
|
async generateImage(rawPath, prompt, images) {
|
|
1014
1050
|
const target = await this.confinedPath(rawPath);
|
|
1015
|
-
|
|
1051
|
+
const extension = path.extname(target).toLowerCase();
|
|
1052
|
+
if (![".jpg", ".jpeg", ".png"].includes(extension)) {
|
|
1016
1053
|
throw new Error("file_path must end in .jpg, .jpeg, or .png");
|
|
1017
1054
|
}
|
|
1018
1055
|
mkdirSync(path.dirname(target), { recursive: true });
|
|
1019
|
-
const model = process.env.ARENA_IMAGE_MODEL || "
|
|
1020
|
-
const
|
|
1021
|
-
const content = [];
|
|
1056
|
+
const model = process.env.ARENA_IMAGE_MODEL || "gemini-2.5-flash-image";
|
|
1057
|
+
const references = [];
|
|
1022
1058
|
for (const imagePath of images || []) {
|
|
1023
1059
|
const source = await this.confinedPath(imagePath);
|
|
1024
1060
|
const extension = path.extname(source).toLowerCase();
|
|
@@ -1030,169 +1066,56 @@ class ArenaToolExecutor {
|
|
|
1030
1066
|
if (bytes.length > 20 * 1024 * 1024) {
|
|
1031
1067
|
throw new Error(`reference image exceeds 20 MB: ${imagePath}`);
|
|
1032
1068
|
}
|
|
1033
|
-
|
|
1034
|
-
|
|
1069
|
+
references.push({
|
|
1070
|
+
bytes,
|
|
1071
|
+
fileName: path.basename(source),
|
|
1072
|
+
mimeType,
|
|
1035
1073
|
});
|
|
1036
1074
|
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
watermark: false,
|
|
1042
|
-
};
|
|
1043
|
-
if (isQwenImage2) {
|
|
1044
|
-
parameters.prompt_extend = true;
|
|
1045
|
-
}
|
|
1046
|
-
else {
|
|
1047
|
-
parameters.n = 1;
|
|
1048
|
-
if (!images?.length)
|
|
1049
|
-
parameters.thinking_mode = true;
|
|
1050
|
-
}
|
|
1051
|
-
const headers = {
|
|
1052
|
-
"content-type": "application/json",
|
|
1053
|
-
};
|
|
1054
|
-
if (!isQwenImage2)
|
|
1055
|
-
headers["x-dashscope-async"] = "enable";
|
|
1056
|
-
const submitted = await dashScopeJson("api", isQwenImage2
|
|
1057
|
-
? "/services/aigc/multimodal-generation/generation"
|
|
1058
|
-
: "/services/aigc/image-generation/generation", {
|
|
1059
|
-
method: "POST",
|
|
1060
|
-
headers,
|
|
1061
|
-
body: JSON.stringify({
|
|
1062
|
-
model,
|
|
1063
|
-
input: {
|
|
1064
|
-
messages: [{ role: "user", content }],
|
|
1065
|
-
},
|
|
1066
|
-
parameters,
|
|
1067
|
-
}),
|
|
1075
|
+
const generated = await generateImageWithModelRouter({
|
|
1076
|
+
images: references,
|
|
1077
|
+
model,
|
|
1078
|
+
prompt,
|
|
1068
1079
|
signal: this.signal,
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
: undefined;
|
|
1073
|
-
if (!isQwenImage2) {
|
|
1074
|
-
const taskId = stringValue(record(submitted.output).task_id);
|
|
1075
|
-
if (!taskId) {
|
|
1076
|
-
throw new Error(`generate_image returned no task_id: ${responseMessage(submitted)}`);
|
|
1077
|
-
}
|
|
1078
|
-
const timeoutMs = Number(process.env.DASHSCOPE_TASK_TIMEOUT_MS || 5 * 60 * 1000);
|
|
1079
|
-
const pollIntervalMs = Number(process.env.DASHSCOPE_TASK_POLL_INTERVAL_MS || 1000);
|
|
1080
|
-
const deadline = Date.now() + timeoutMs;
|
|
1081
|
-
while (Date.now() <= deadline) {
|
|
1082
|
-
const task = await dashScopeJson("api", `/tasks/${encodeURIComponent(taskId)}`, { method: "GET", signal: this.signal }, "generate_image status");
|
|
1083
|
-
const output = record(task.output);
|
|
1084
|
-
const taskStatus = stringValue(output.task_status).toUpperCase();
|
|
1085
|
-
if (taskStatus === "SUCCEEDED") {
|
|
1086
|
-
completed = task;
|
|
1087
|
-
break;
|
|
1088
|
-
}
|
|
1089
|
-
if (["FAILED", "CANCELED", "UNKNOWN"].includes(taskStatus)) {
|
|
1090
|
-
throw new Error(`generate_image task ${taskStatus}: ${responseMessage(task)}`);
|
|
1091
|
-
}
|
|
1092
|
-
if (Date.now() + pollIntervalMs > deadline)
|
|
1093
|
-
break;
|
|
1094
|
-
await delay(pollIntervalMs, undefined, { signal: this.signal });
|
|
1095
|
-
}
|
|
1096
|
-
if (!completed) {
|
|
1097
|
-
throw new Error(`generate_image timed out waiting for task ${taskId}`);
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
if (!completed) {
|
|
1101
|
-
throw new Error("generate_image returned no response");
|
|
1102
|
-
}
|
|
1103
|
-
const output = record(completed.output);
|
|
1104
|
-
const choices = Array.isArray(output.choices) ? output.choices : [];
|
|
1105
|
-
const message = record(record(choices[0]).message);
|
|
1106
|
-
const generatedContent = Array.isArray(message.content)
|
|
1107
|
-
? message.content
|
|
1108
|
-
: [];
|
|
1109
|
-
const generated = generatedContent
|
|
1110
|
-
.map(record)
|
|
1111
|
-
.find((item) => typeof item.image === "string");
|
|
1112
|
-
const legacyResults = Array.isArray(output.results) ? output.results : [];
|
|
1113
|
-
const imageUrl = stringValue(generated?.image) ||
|
|
1114
|
-
stringValue(record(legacyResults[0]).url);
|
|
1115
|
-
if (!imageUrl) {
|
|
1116
|
-
throw new Error("generate_image response did not include an image URL");
|
|
1117
|
-
}
|
|
1118
|
-
const imageResponse = await fetch(imageUrl, { signal: this.signal });
|
|
1119
|
-
if (!imageResponse.ok) {
|
|
1120
|
-
throw new Error(`image download failed (${imageResponse.status})`);
|
|
1121
|
-
}
|
|
1122
|
-
await writeFile(target, await responseBytes(imageResponse, 25 * 1024 * 1024));
|
|
1080
|
+
size: process.env.ARENA_IMAGE_SIZE || "1024*1024",
|
|
1081
|
+
});
|
|
1082
|
+
await writeFile(target, await generatedImageBytesForExtension(generated.bytes, generated.mimeType, extension));
|
|
1123
1083
|
return {
|
|
1124
1084
|
status: "success",
|
|
1125
1085
|
file_path: this.relativePath(target),
|
|
1126
1086
|
};
|
|
1127
1087
|
}
|
|
1128
|
-
async generateSpeech(rawPath, text,
|
|
1088
|
+
async generateSpeech(rawPath, text, voiceId, language) {
|
|
1129
1089
|
const target = await this.confinedPath(rawPath);
|
|
1130
1090
|
const extension = path.extname(target).toLowerCase();
|
|
1131
1091
|
const format = {
|
|
1132
1092
|
".mp3": "mp3",
|
|
1133
1093
|
".wav": "wav",
|
|
1094
|
+
".ogg": "opus",
|
|
1134
1095
|
".opus": "opus",
|
|
1135
1096
|
}[extension];
|
|
1136
1097
|
if (!format) {
|
|
1137
|
-
throw new Error("
|
|
1098
|
+
throw new Error("ModelRouter Doubao TTS output supports .mp3, .wav, .ogg, or .opus");
|
|
1138
1099
|
}
|
|
1139
1100
|
mkdirSync(path.dirname(target), { recursive: true });
|
|
1140
|
-
const
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
const voices = primaryLanguage === "en"
|
|
1144
|
-
? feminine
|
|
1145
|
-
? ["loongeva_v3.6", "longanhuan_v3.6"]
|
|
1146
|
-
: ["loongjohn", "longjielidou_v3.6"]
|
|
1147
|
-
: feminine
|
|
1148
|
-
? ["longanhuan_v3.6"]
|
|
1149
|
-
: ["longjielidou_v3.6"];
|
|
1150
|
-
const requestedIndex = Math.max(0, Math.trunc(Number(voiceIdentity.index || 0)));
|
|
1151
|
-
const voice = process.env.DASHSCOPE_SPEECH_VOICE ||
|
|
1152
|
-
voices[requestedIndex % voices.length] ||
|
|
1153
|
-
voices[0];
|
|
1154
|
-
const supportedHints = new Set([
|
|
1155
|
-
"ar",
|
|
1156
|
-
"de",
|
|
1157
|
-
"en",
|
|
1158
|
-
"es",
|
|
1159
|
-
"fil",
|
|
1160
|
-
"fr",
|
|
1161
|
-
"id",
|
|
1162
|
-
"it",
|
|
1163
|
-
"ja",
|
|
1164
|
-
"ko",
|
|
1165
|
-
"ms",
|
|
1166
|
-
"pt",
|
|
1167
|
-
"ru",
|
|
1168
|
-
"th",
|
|
1169
|
-
"vi",
|
|
1170
|
-
"zh",
|
|
1171
|
-
]);
|
|
1172
|
-
const input = {
|
|
1173
|
-
text,
|
|
1174
|
-
voice,
|
|
1175
|
-
format,
|
|
1176
|
-
sample_rate: 24000,
|
|
1177
|
-
};
|
|
1178
|
-
if (supportedHints.has(primaryLanguage)) {
|
|
1179
|
-
input.language_hints = [primaryLanguage];
|
|
1180
|
-
}
|
|
1181
|
-
const payload = await dashScopeJson("api", "/services/audio/tts/SpeechSynthesizer", {
|
|
1182
|
-
method: "POST",
|
|
1183
|
-
headers: { "content-type": "application/json" },
|
|
1184
|
-
body: JSON.stringify({ model, input }),
|
|
1185
|
-
signal: this.signal,
|
|
1186
|
-
}, "generate_speech");
|
|
1187
|
-
const audioUrl = stringValue(record(record(payload.output).audio).url);
|
|
1188
|
-
if (!audioUrl) {
|
|
1189
|
-
throw new Error(`generate_speech returned no audio URL: ${responseMessage(payload)}`);
|
|
1101
|
+
const registered = this.voiceRegistry.resolve(voiceId);
|
|
1102
|
+
if (!registered) {
|
|
1103
|
+
throw new Error(`unknown voice_id: ${voiceId}. Call add_voice and use the returned voice_id.`);
|
|
1190
1104
|
}
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1105
|
+
if (language &&
|
|
1106
|
+
language.toLowerCase().split(/[-_]/)[0] !==
|
|
1107
|
+
registered.language.toLowerCase().split(/[-_]/)[0]) {
|
|
1108
|
+
throw new Error(`voice ${voiceId} was registered for ${registered.language}; ` +
|
|
1109
|
+
`register a voice for ${language} with add_voice`);
|
|
1194
1110
|
}
|
|
1195
|
-
|
|
1111
|
+
const voice = process.env.ARENA_SPEECH_VOICE || registered.providerVoice;
|
|
1112
|
+
await writeFile(target, await generateSpeechWithModelRouter({
|
|
1113
|
+
format,
|
|
1114
|
+
model: process.env.ARENA_SPEECH_MODEL || "doubao-tts",
|
|
1115
|
+
signal: this.signal,
|
|
1116
|
+
text,
|
|
1117
|
+
voice,
|
|
1118
|
+
}));
|
|
1196
1119
|
return {
|
|
1197
1120
|
status: "success",
|
|
1198
1121
|
file_path: this.relativePath(target),
|