@qwen-code/arena-agent 0.0.8-dev.5 → 0.0.9-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 +41 -15
- package/README.md +128 -29
- package/dist/agent.d.ts +8 -17
- package/dist/agent.js +60 -75
- package/dist/agent.js.map +1 -1
- package/dist/cli.js +12 -4
- package/dist/cli.js.map +1 -1
- package/dist/compaction.js +2 -1
- package/dist/compaction.js.map +1 -1
- package/dist/config.d.ts +11 -0
- package/dist/config.js +73 -9
- package/dist/config.js.map +1 -1
- package/dist/conversation-metrics.d.ts +61 -0
- package/dist/conversation-metrics.js +433 -0
- package/dist/conversation-metrics.js.map +1 -0
- package/dist/document-reader.js +9 -0
- package/dist/document-reader.js.map +1 -1
- package/dist/history.d.ts +31 -2
- package/dist/history.js +53 -3
- package/dist/history.js.map +1 -1
- package/dist/metrics.d.ts +61 -0
- package/dist/metrics.js +578 -0
- package/dist/metrics.js.map +1 -0
- package/dist/mock.js +153 -0
- package/dist/mock.js.map +1 -1
- package/dist/protocol.d.ts +2 -3
- package/dist/protocol.js +9 -5
- package/dist/protocol.js.map +1 -1
- package/dist/provider.js +22 -8
- package/dist/provider.js.map +1 -1
- package/dist/runtime-assets.d.ts +3 -0
- package/dist/runtime-assets.js +16 -0
- package/dist/runtime-assets.js.map +1 -0
- package/dist/sdk.d.ts +28 -1
- package/dist/sdk.js +38 -0
- package/dist/sdk.js.map +1 -1
- package/dist/server.js +486 -36
- package/dist/server.js.map +1 -1
- package/dist/side-query.d.ts +31 -0
- package/dist/side-query.js +138 -0
- package/dist/side-query.js.map +1 -0
- package/dist/spec.d.ts +1 -1
- package/dist/spec.js +6 -4
- package/dist/spec.js.map +1 -1
- package/dist/tools.d.ts +3 -8
- package/dist/tools.js +149 -200
- package/dist/tools.js.map +1 -1
- package/dist/tui.js +4 -3
- package/dist/tui.js.map +1 -1
- package/dist/types.d.ts +155 -5
- package/dist/web/app.d.ts +1 -0
- package/dist/web/app.js +333 -10
- package/dist/web/app.js.map +1 -1
- package/docs/SDK.md +42 -0
- package/package.json +10 -20
- package/web/index.html +25 -0
- package/web/styles.css +109 -3
- package/dist/index.d.ts +0 -9
- package/dist/index.js +0 -9
- package/dist/index.js.map +0 -1
package/dist/tools.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { existsSync, mkdirSync, realpathSync } from "node:fs";
|
|
3
|
-
import {
|
|
2
|
+
import { existsSync, mkdirSync, realpathSync, } from "node:fs";
|
|
3
|
+
import { open, readFile, realpath, stat, writeFile, } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { setTimeout as delay } from "node:timers/promises";
|
|
6
6
|
import { Ajv } from "ajv";
|
|
@@ -10,7 +10,7 @@ import iconv from "iconv-lite";
|
|
|
10
10
|
import mime from "mime/lite";
|
|
11
11
|
import { extractStructuredDocument, isStructuredDocument, } from "./document-reader.js";
|
|
12
12
|
import { FetchPageReader } from "./fetch-page.js";
|
|
13
|
-
import {
|
|
13
|
+
import { toolDefinitions } from "./spec.js";
|
|
14
14
|
const knownTextApplicationMimeTypes = new Set([
|
|
15
15
|
"application/ecmascript",
|
|
16
16
|
"application/javascript",
|
|
@@ -171,36 +171,6 @@ const knownBinaryExtensions = new Set([
|
|
|
171
171
|
".xlsx",
|
|
172
172
|
".zip",
|
|
173
173
|
]);
|
|
174
|
-
const workspaceSnapshotExcludedDirectories = new Set([
|
|
175
|
-
".arena",
|
|
176
|
-
".cache",
|
|
177
|
-
".git",
|
|
178
|
-
".mypy_cache",
|
|
179
|
-
".next",
|
|
180
|
-
".nox",
|
|
181
|
-
".npm",
|
|
182
|
-
".nuxt",
|
|
183
|
-
".output",
|
|
184
|
-
".parcel-cache",
|
|
185
|
-
".pytest_cache",
|
|
186
|
-
".ruff_cache",
|
|
187
|
-
".svelte-kit",
|
|
188
|
-
".tox",
|
|
189
|
-
".turbo",
|
|
190
|
-
".venv",
|
|
191
|
-
".vite",
|
|
192
|
-
"__pycache__",
|
|
193
|
-
"build",
|
|
194
|
-
"coverage",
|
|
195
|
-
"dist",
|
|
196
|
-
"node_modules",
|
|
197
|
-
"out",
|
|
198
|
-
"target",
|
|
199
|
-
]);
|
|
200
|
-
const workspaceSnapshotExcludedFiles = new Set([
|
|
201
|
-
".git-credentials",
|
|
202
|
-
".netrc",
|
|
203
|
-
]);
|
|
204
174
|
const visibleImageExtensions = new Set([
|
|
205
175
|
".bmp",
|
|
206
176
|
".gif",
|
|
@@ -217,6 +187,8 @@ const visualIntroText = "<arena-system-message>\n" +
|
|
|
217
187
|
"treat them as the authentic output of the tool call.\n" +
|
|
218
188
|
"</arena-system-message>";
|
|
219
189
|
const bashOutputLimit = 20_000;
|
|
190
|
+
const textFileByteLimit = 256 * 1024;
|
|
191
|
+
const toolResultCharacterLimit = 1_000_000;
|
|
220
192
|
// 中段截断,逐字反推自 3 个 truncated:true 参考样本:头尾各 9987 字符 +
|
|
221
193
|
// 26 字符标记,总长恰 20000(由 T1(b) 执行回放发现,非首段截断)。
|
|
222
194
|
const bashTruncationMarker = "\n... output truncated ...\n";
|
|
@@ -362,6 +334,12 @@ function json(value) {
|
|
|
362
334
|
function errorMessage(error) {
|
|
363
335
|
return error instanceof Error ? error.message : String(error);
|
|
364
336
|
}
|
|
337
|
+
function displayByteSize(value) {
|
|
338
|
+
if (value >= 1024 * 1024) {
|
|
339
|
+
return `${(value / (1024 * 1024)).toFixed(1)} MB`;
|
|
340
|
+
}
|
|
341
|
+
return `${Math.max(1, Math.ceil(value / 1024))} KB`;
|
|
342
|
+
}
|
|
365
343
|
function record(value) {
|
|
366
344
|
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
367
345
|
? value
|
|
@@ -370,33 +348,17 @@ function record(value) {
|
|
|
370
348
|
function stringValue(value) {
|
|
371
349
|
return typeof value === "string" ? value : "";
|
|
372
350
|
}
|
|
373
|
-
function positiveInteger(value, defaultValue) {
|
|
374
|
-
const parsed = Number(value);
|
|
375
|
-
return Number.isFinite(parsed) && parsed > 0
|
|
376
|
-
? Math.floor(parsed)
|
|
377
|
-
: defaultValue;
|
|
378
|
-
}
|
|
379
|
-
function requestSignal(parent, timeoutMs) {
|
|
380
|
-
const timeout = AbortSignal.timeout(timeoutMs);
|
|
381
|
-
return {
|
|
382
|
-
signal: parent ? AbortSignal.any([parent, timeout]) : timeout,
|
|
383
|
-
timedOut: () => timeout.aborted && !parent?.aborted,
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
351
|
function dashScopeApiKey() {
|
|
387
|
-
const apiKey = process.env.
|
|
352
|
+
const apiKey = process.env.DASHSCOPE_API_KEY;
|
|
388
353
|
if (!apiKey) {
|
|
389
|
-
throw new Error("DashScope tools require
|
|
354
|
+
throw new Error("DashScope tools require DASHSCOPE_API_KEY");
|
|
390
355
|
}
|
|
391
356
|
return apiKey;
|
|
392
357
|
}
|
|
393
358
|
function dashScopeRoot() {
|
|
394
|
-
return (process.env.
|
|
395
|
-
process.env.BAILIAN_API_HOST ||
|
|
396
|
-
process.env.DASHSCOPE_API_BASE ||
|
|
359
|
+
return (process.env.DASHSCOPE_API_BASE ||
|
|
397
360
|
process.env.DASHSCOPE_BASE_URL ||
|
|
398
361
|
"https://dashscope.aliyuncs.com")
|
|
399
|
-
.replace(/^([^:/?#]+\.)/i, "https://$1")
|
|
400
362
|
.replace(/\/+$/, "")
|
|
401
363
|
.replace(/\/(?:api\/v1|compatible-mode\/v1)$/i, "");
|
|
402
364
|
}
|
|
@@ -408,22 +370,10 @@ function dashScopeUrl(protocol, pathname) {
|
|
|
408
370
|
async function dashScopeFetch(protocol, pathname, init, label) {
|
|
409
371
|
const headers = new Headers(init.headers);
|
|
410
372
|
headers.set("authorization", `Bearer ${dashScopeApiKey()}`);
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
response = await fetch(dashScopeUrl(protocol, pathname), {
|
|
416
|
-
...init,
|
|
417
|
-
headers,
|
|
418
|
-
signal: request.signal,
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
|
-
catch (error) {
|
|
422
|
-
if (request.timedOut()) {
|
|
423
|
-
throw new Error(`${label} timed out after ${timeoutMs}ms`);
|
|
424
|
-
}
|
|
425
|
-
throw error;
|
|
426
|
-
}
|
|
373
|
+
const response = await fetch(dashScopeUrl(protocol, pathname), {
|
|
374
|
+
...init,
|
|
375
|
+
headers,
|
|
376
|
+
});
|
|
427
377
|
if (response.ok)
|
|
428
378
|
return response;
|
|
429
379
|
const body = await response.text();
|
|
@@ -464,7 +414,6 @@ export class ToolExecutor {
|
|
|
464
414
|
validators = new Map();
|
|
465
415
|
pendingVisualContent = new Map();
|
|
466
416
|
askUserHandler;
|
|
467
|
-
deferUserInput;
|
|
468
417
|
signal;
|
|
469
418
|
workspaceContext;
|
|
470
419
|
pageReader;
|
|
@@ -479,26 +428,25 @@ export class ToolExecutor {
|
|
|
479
428
|
mkdirSync(this.workspace, { recursive: true });
|
|
480
429
|
this.workspaceReal = realpathSync(this.workspace);
|
|
481
430
|
this.askUserHandler = options.askUserHandler;
|
|
482
|
-
this.
|
|
483
|
-
this.askUserEnabled =
|
|
484
|
-
Boolean(options.askUserHandler) || this.deferUserInput;
|
|
431
|
+
this.askUserEnabled = Boolean(options.askUserHandler);
|
|
485
432
|
this.signal = options.signal;
|
|
486
433
|
this.workspaceContext = options.workspaceContext;
|
|
487
434
|
this.pageReader = new FetchPageReader({ signal: options.signal });
|
|
488
435
|
addFormats(this.ajv);
|
|
489
|
-
const
|
|
490
|
-
for (const definition of toolDefinitions(this.workspaceContext, toolNames)) {
|
|
436
|
+
for (const definition of toolDefinitions(this.workspaceContext)) {
|
|
491
437
|
this.validators.set(definition.function.name, this.ajv.compile(definition.function.parameters));
|
|
492
438
|
}
|
|
493
439
|
}
|
|
494
440
|
// The tool schemas offered to the model for this session. Identical to the
|
|
495
441
|
// canonical trace set, adjusted for the selected workspace runtime and minus
|
|
496
442
|
// ask_user when no interactive handler is wired.
|
|
497
|
-
availableTools(
|
|
498
|
-
|
|
499
|
-
if (this.askUserEnabled)
|
|
500
|
-
|
|
501
|
-
|
|
443
|
+
availableTools() {
|
|
444
|
+
let definitions = toolDefinitions(this.workspaceContext);
|
|
445
|
+
if (!this.askUserEnabled) {
|
|
446
|
+
definitions = definitions.filter((definition) => definition.function.name !== "ask_user");
|
|
447
|
+
}
|
|
448
|
+
const disallowed = new Set(this.config.disallowedTools || []);
|
|
449
|
+
return definitions.filter((definition) => !disallowed.has(definition.function.name));
|
|
502
450
|
}
|
|
503
451
|
async execute(call) {
|
|
504
452
|
const callId = String(call.id || "");
|
|
@@ -508,6 +456,9 @@ export class ToolExecutor {
|
|
|
508
456
|
// 监听器不会补发已发生的 abort:已取消的 signal 必须在产生任何副作用
|
|
509
457
|
// 之前短路,否则命令会照常 spawn 直到自然结束。
|
|
510
458
|
this.signal?.throwIfAborted();
|
|
459
|
+
if (this.config.disallowedTools?.includes(name)) {
|
|
460
|
+
throw new Error(`tool is disallowed: ${name}`);
|
|
461
|
+
}
|
|
511
462
|
const argumentsValue = JSON.parse(typeof call.function?.arguments === "string"
|
|
512
463
|
? call.function.arguments
|
|
513
464
|
: "{}");
|
|
@@ -521,10 +472,23 @@ export class ToolExecutor {
|
|
|
521
472
|
throw new Error(`invalid arguments: ${errors}`);
|
|
522
473
|
}
|
|
523
474
|
const result = await this.dispatch(name, argumentsValue, callId);
|
|
475
|
+
const content = typeof result === "string" ? result : json(result);
|
|
476
|
+
if (content.length > toolResultCharacterLimit) {
|
|
477
|
+
return {
|
|
478
|
+
role: "tool",
|
|
479
|
+
tool_call_id: callId,
|
|
480
|
+
content: json({
|
|
481
|
+
status: "error",
|
|
482
|
+
message: `Tool result from ${name} (${content.length} characters) exceeds the ` +
|
|
483
|
+
`maximum allowed size (${toolResultCharacterLimit} characters). ` +
|
|
484
|
+
"Retry with narrower arguments or inspect the data in smaller portions.",
|
|
485
|
+
}),
|
|
486
|
+
};
|
|
487
|
+
}
|
|
524
488
|
return {
|
|
525
489
|
role: "tool",
|
|
526
490
|
tool_call_id: callId,
|
|
527
|
-
content
|
|
491
|
+
content,
|
|
528
492
|
};
|
|
529
493
|
}
|
|
530
494
|
catch (error) {
|
|
@@ -555,52 +519,6 @@ export class ToolExecutor {
|
|
|
555
519
|
content: [{ text: visualIntroText }, ...images],
|
|
556
520
|
};
|
|
557
521
|
}
|
|
558
|
-
async workspaceSnapshot() {
|
|
559
|
-
const files = [];
|
|
560
|
-
let capturedBytes = 0;
|
|
561
|
-
const perFileLimit = Math.max(1024 * 1024, Number(process.env.ARENA_DB_FILE_LIMIT_BYTES || 8 * 1024 * 1024));
|
|
562
|
-
const totalLimit = Math.max(perFileLimit, Number(process.env.ARENA_DB_WORKSPACE_LIMIT_BYTES || 32 * 1024 * 1024));
|
|
563
|
-
const visit = async (directory) => {
|
|
564
|
-
if (files.length >= 2_000)
|
|
565
|
-
return;
|
|
566
|
-
const entries = await readdir(directory, { withFileTypes: true });
|
|
567
|
-
for (const entry of entries) {
|
|
568
|
-
if (files.length >= 2_000)
|
|
569
|
-
break;
|
|
570
|
-
if (entry.isSymbolicLink())
|
|
571
|
-
continue;
|
|
572
|
-
const target = path.join(directory, entry.name);
|
|
573
|
-
if (entry.isDirectory()) {
|
|
574
|
-
if (!workspaceSnapshotExcludedDirectories.has(entry.name)) {
|
|
575
|
-
await visit(target);
|
|
576
|
-
}
|
|
577
|
-
continue;
|
|
578
|
-
}
|
|
579
|
-
if (!entry.isFile() ||
|
|
580
|
-
workspaceSnapshotExcludedFiles.has(entry.name)) {
|
|
581
|
-
continue;
|
|
582
|
-
}
|
|
583
|
-
const metadata = await stat(target);
|
|
584
|
-
const relative = this.relativePath(target);
|
|
585
|
-
if (metadata.size > perFileLimit ||
|
|
586
|
-
capturedBytes + metadata.size > totalLimit) {
|
|
587
|
-
throw new Error(`workspace database snapshot limit exceeded at ${relative}`);
|
|
588
|
-
}
|
|
589
|
-
const data = await readFile(target);
|
|
590
|
-
capturedBytes += data.length;
|
|
591
|
-
const kind = workspaceFileKind(target, data.subarray(0, 4096));
|
|
592
|
-
const mimeType = mime.getType(target) || "application/octet-stream";
|
|
593
|
-
files.push({
|
|
594
|
-
path: relative,
|
|
595
|
-
content: kind === "text"
|
|
596
|
-
? decodeText(data)
|
|
597
|
-
: `data:${mimeType};base64,${data.toString("base64")}`,
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
|
-
};
|
|
601
|
-
await visit(this.workspaceReal);
|
|
602
|
-
return files;
|
|
603
|
-
}
|
|
604
522
|
async dispatch(name, values, toolCallId) {
|
|
605
523
|
switch (name) {
|
|
606
524
|
case "bash":
|
|
@@ -822,13 +740,6 @@ export class ToolExecutor {
|
|
|
822
740
|
if (this.askUserHandler) {
|
|
823
741
|
return this.askUserHandler(toolCallId, questions);
|
|
824
742
|
}
|
|
825
|
-
if (this.deferUserInput) {
|
|
826
|
-
return {
|
|
827
|
-
status: "needs_input",
|
|
828
|
-
questions,
|
|
829
|
-
message: "The current request is waiting for interactive input.",
|
|
830
|
-
};
|
|
831
|
-
}
|
|
832
743
|
// ask_user is not offered in non-interactive sessions; if the model still
|
|
833
744
|
// emits it (e.g. resumed history or a hallucinated call), report an error
|
|
834
745
|
// so the turn continues instead of stalling on needs_input.
|
|
@@ -859,9 +770,23 @@ export class ToolExecutor {
|
|
|
859
770
|
}
|
|
860
771
|
async readWorkspaceFile(rawPath, toolCallId) {
|
|
861
772
|
const target = await this.confinedPath(rawPath);
|
|
862
|
-
let
|
|
773
|
+
let fileSize;
|
|
774
|
+
let sample;
|
|
863
775
|
try {
|
|
864
|
-
|
|
776
|
+
const metadata = await stat(target);
|
|
777
|
+
if (!metadata.isFile())
|
|
778
|
+
throw new Error("not a regular file");
|
|
779
|
+
fileSize = metadata.size;
|
|
780
|
+
sample = Buffer.alloc(Math.min(fileSize, 4096));
|
|
781
|
+
if (sample.length) {
|
|
782
|
+
const handle = await open(target, "r");
|
|
783
|
+
try {
|
|
784
|
+
await handle.read(sample, 0, sample.length, 0);
|
|
785
|
+
}
|
|
786
|
+
finally {
|
|
787
|
+
await handle.close();
|
|
788
|
+
}
|
|
789
|
+
}
|
|
865
790
|
}
|
|
866
791
|
catch {
|
|
867
792
|
// 逐字对齐生产错误文案,路径按调用方原样回显。
|
|
@@ -871,7 +796,29 @@ export class ToolExecutor {
|
|
|
871
796
|
};
|
|
872
797
|
}
|
|
873
798
|
const extension = path.extname(target).toLowerCase();
|
|
874
|
-
const kind = workspaceFileKind(target,
|
|
799
|
+
const kind = workspaceFileKind(target, sample);
|
|
800
|
+
if (kind === "text" && fileSize > textFileByteLimit) {
|
|
801
|
+
return {
|
|
802
|
+
status: "error",
|
|
803
|
+
message: `File content (${displayByteSize(fileSize)}) exceeds the maximum allowed size ` +
|
|
804
|
+
`(${displayByteSize(textFileByteLimit)}). Use bash with sed/head/tail to read ` +
|
|
805
|
+
"specific portions of the file, or grep/rg to search for specific content " +
|
|
806
|
+
"instead of reading the whole file.",
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
if (kind === "image" && fileSize > 20 * 1024 * 1024) {
|
|
810
|
+
throw new Error(`image exceeds 20 MB: ${rawPath}`);
|
|
811
|
+
}
|
|
812
|
+
let data;
|
|
813
|
+
try {
|
|
814
|
+
data = await readFile(target);
|
|
815
|
+
}
|
|
816
|
+
catch {
|
|
817
|
+
return {
|
|
818
|
+
status: "error",
|
|
819
|
+
message: `Failed to read workspace file: ${rawPath}`,
|
|
820
|
+
};
|
|
821
|
+
}
|
|
875
822
|
if (isStructuredDocument(target)) {
|
|
876
823
|
// 文档提取无参考样本(known-unknown),保留本地扩展键。
|
|
877
824
|
const result = {
|
|
@@ -909,15 +856,12 @@ export class ToolExecutor {
|
|
|
909
856
|
mimeTypes[extension] ||
|
|
910
857
|
"application/octet-stream";
|
|
911
858
|
if (kind === "image") {
|
|
912
|
-
if (data.length > 20 * 1024 * 1024) {
|
|
913
|
-
throw new Error(`image exceeds 20 MB: ${rawPath}`);
|
|
914
|
-
}
|
|
915
859
|
this.pendingVisualContent.set(toolCallId, [
|
|
916
860
|
{
|
|
917
861
|
image: `data:${mimeType};base64,${data.toString("base64")}`,
|
|
918
862
|
},
|
|
919
863
|
]);
|
|
920
|
-
return [{ text: imageResultPlaceholder }];
|
|
864
|
+
return [{ type: "text", text: imageResultPlaceholder }];
|
|
921
865
|
}
|
|
922
866
|
return {
|
|
923
867
|
status: "success",
|
|
@@ -981,13 +925,13 @@ export class ToolExecutor {
|
|
|
981
925
|
};
|
|
982
926
|
}
|
|
983
927
|
async imageSearch(query, count) {
|
|
984
|
-
const model = process.env.ARENA_IMAGE_SEARCH_MODEL || "qwen3.7-
|
|
928
|
+
const model = process.env.ARENA_IMAGE_SEARCH_MODEL || "qwen3.7-plus";
|
|
985
929
|
const payload = await dashScopeJson("compatible", "/responses", {
|
|
986
930
|
method: "POST",
|
|
987
931
|
headers: { "content-type": "application/json" },
|
|
988
932
|
body: JSON.stringify({
|
|
989
933
|
model,
|
|
990
|
-
input:
|
|
934
|
+
input: query,
|
|
991
935
|
tools: [{ type: "web_search_image" }],
|
|
992
936
|
}),
|
|
993
937
|
signal: this.signal,
|
|
@@ -1015,53 +959,40 @@ export class ToolExecutor {
|
|
|
1015
959
|
.toLowerCase()
|
|
1016
960
|
.replace(/[^a-z0-9]+/g, "-")
|
|
1017
961
|
.slice(0, 40);
|
|
1018
|
-
const
|
|
962
|
+
for (const value of results) {
|
|
963
|
+
if (saved.length >= count)
|
|
964
|
+
break;
|
|
1019
965
|
const item = record(value);
|
|
1020
966
|
const sourceUrl = stringValue(item.url) ||
|
|
1021
967
|
stringValue(item.original_url) ||
|
|
1022
968
|
stringValue(item.image_url) ||
|
|
1023
969
|
stringValue(item.thumbnail_url);
|
|
1024
970
|
if (!sourceUrl)
|
|
1025
|
-
|
|
971
|
+
continue;
|
|
1026
972
|
try {
|
|
1027
|
-
const
|
|
1028
|
-
const imageResponse = await fetch(sourceUrl, {
|
|
1029
|
-
signal: download.signal,
|
|
1030
|
-
});
|
|
973
|
+
const imageResponse = await fetch(sourceUrl, { signal: this.signal });
|
|
1031
974
|
if (!imageResponse.ok)
|
|
1032
|
-
|
|
975
|
+
continue;
|
|
1033
976
|
const contentType = imageResponse.headers.get("content-type") || "image/jpeg";
|
|
1034
977
|
if (!contentType.toLowerCase().startsWith("image/"))
|
|
1035
|
-
|
|
978
|
+
continue;
|
|
1036
979
|
const normalizedType = contentType.split(";")[0]?.trim().toLowerCase();
|
|
1037
980
|
const extension = Object.entries(mimeTypes).find(([, mime]) => mime === normalizedType)?.[0] || ".jpg";
|
|
1038
|
-
|
|
981
|
+
const target = await this.confinedPath(`image-search/${slug}-${saved.length + 1}${extension}`);
|
|
982
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
983
|
+
await writeFile(target, await responseBytes(imageResponse));
|
|
984
|
+
saved.push({
|
|
1039
985
|
title: stringValue(item.title),
|
|
986
|
+
file_path: this.relativePath(target),
|
|
1040
987
|
source_url: sourceUrl,
|
|
1041
988
|
thumbnail_url: stringValue(item.thumbnail_url) || sourceUrl,
|
|
1042
|
-
|
|
1043
|
-
bytes: await responseBytes(imageResponse),
|
|
1044
|
-
};
|
|
989
|
+
});
|
|
1045
990
|
}
|
|
1046
991
|
catch (error) {
|
|
1047
992
|
if (this.signal?.aborted)
|
|
1048
993
|
throw error;
|
|
1049
994
|
// A single inaccessible search result should not fail the whole search.
|
|
1050
|
-
return undefined;
|
|
1051
995
|
}
|
|
1052
|
-
}));
|
|
1053
|
-
for (const image of downloaded) {
|
|
1054
|
-
if (!image || saved.length >= count)
|
|
1055
|
-
continue;
|
|
1056
|
-
const target = await this.confinedPath(`image-search/${slug}-${saved.length + 1}${image.extension}`);
|
|
1057
|
-
mkdirSync(path.dirname(target), { recursive: true });
|
|
1058
|
-
await writeFile(target, image.bytes);
|
|
1059
|
-
saved.push({
|
|
1060
|
-
title: image.title,
|
|
1061
|
-
file_path: this.relativePath(target),
|
|
1062
|
-
source_url: image.source_url,
|
|
1063
|
-
thumbnail_url: image.thumbnail_url,
|
|
1064
|
-
});
|
|
1065
996
|
}
|
|
1066
997
|
return { status: "success", results: saved };
|
|
1067
998
|
}
|
|
@@ -1077,7 +1008,8 @@ export class ToolExecutor {
|
|
|
1077
1008
|
throw new Error("file_path must end in .jpg, .jpeg, or .png");
|
|
1078
1009
|
}
|
|
1079
1010
|
mkdirSync(path.dirname(target), { recursive: true });
|
|
1080
|
-
const model = process.env.ARENA_IMAGE_MODEL || "
|
|
1011
|
+
const model = process.env.ARENA_IMAGE_MODEL || "qwen-image-2.0-pro";
|
|
1012
|
+
const isQwenImage2 = model.startsWith("qwen-image-2.0");
|
|
1081
1013
|
const content = [];
|
|
1082
1014
|
for (const imagePath of images || []) {
|
|
1083
1015
|
const source = await this.confinedPath(imagePath);
|
|
@@ -1096,18 +1028,28 @@ export class ToolExecutor {
|
|
|
1096
1028
|
}
|
|
1097
1029
|
content.push({ text: prompt });
|
|
1098
1030
|
const parameters = {
|
|
1099
|
-
size: process.env.ARENA_IMAGE_SIZE ||
|
|
1100
|
-
|
|
1031
|
+
size: process.env.ARENA_IMAGE_SIZE ||
|
|
1032
|
+
(isQwenImage2 ? "2048*2048" : "2K"),
|
|
1101
1033
|
watermark: false,
|
|
1102
1034
|
};
|
|
1103
|
-
if (
|
|
1104
|
-
parameters.
|
|
1105
|
-
|
|
1035
|
+
if (isQwenImage2) {
|
|
1036
|
+
parameters.prompt_extend = true;
|
|
1037
|
+
}
|
|
1038
|
+
else {
|
|
1039
|
+
parameters.n = 1;
|
|
1040
|
+
if (!images?.length)
|
|
1041
|
+
parameters.thinking_mode = true;
|
|
1042
|
+
}
|
|
1043
|
+
const headers = {
|
|
1044
|
+
"content-type": "application/json",
|
|
1045
|
+
};
|
|
1046
|
+
if (!isQwenImage2)
|
|
1047
|
+
headers["x-dashscope-async"] = "enable";
|
|
1048
|
+
const submitted = await dashScopeJson("api", isQwenImage2
|
|
1049
|
+
? "/services/aigc/multimodal-generation/generation"
|
|
1050
|
+
: "/services/aigc/image-generation/generation", {
|
|
1106
1051
|
method: "POST",
|
|
1107
|
-
headers
|
|
1108
|
-
"content-type": "application/json",
|
|
1109
|
-
"x-dashscope-async": "enable",
|
|
1110
|
-
},
|
|
1052
|
+
headers,
|
|
1111
1053
|
body: JSON.stringify({
|
|
1112
1054
|
model,
|
|
1113
1055
|
input: {
|
|
@@ -1117,31 +1059,38 @@ export class ToolExecutor {
|
|
|
1117
1059
|
}),
|
|
1118
1060
|
signal: this.signal,
|
|
1119
1061
|
}, "generate_image");
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
const
|
|
1130
|
-
const
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1062
|
+
let completed = isQwenImage2
|
|
1063
|
+
? submitted
|
|
1064
|
+
: undefined;
|
|
1065
|
+
if (!isQwenImage2) {
|
|
1066
|
+
const taskId = stringValue(record(submitted.output).task_id);
|
|
1067
|
+
if (!taskId) {
|
|
1068
|
+
throw new Error(`generate_image returned no task_id: ${responseMessage(submitted)}`);
|
|
1069
|
+
}
|
|
1070
|
+
const timeoutMs = Number(process.env.DASHSCOPE_TASK_TIMEOUT_MS || 5 * 60 * 1000);
|
|
1071
|
+
const pollIntervalMs = Number(process.env.DASHSCOPE_TASK_POLL_INTERVAL_MS || 1000);
|
|
1072
|
+
const deadline = Date.now() + timeoutMs;
|
|
1073
|
+
while (Date.now() <= deadline) {
|
|
1074
|
+
const task = await dashScopeJson("api", `/tasks/${encodeURIComponent(taskId)}`, { method: "GET", signal: this.signal }, "generate_image status");
|
|
1075
|
+
const output = record(task.output);
|
|
1076
|
+
const taskStatus = stringValue(output.task_status).toUpperCase();
|
|
1077
|
+
if (taskStatus === "SUCCEEDED") {
|
|
1078
|
+
completed = task;
|
|
1079
|
+
break;
|
|
1080
|
+
}
|
|
1081
|
+
if (["FAILED", "CANCELED", "UNKNOWN"].includes(taskStatus)) {
|
|
1082
|
+
throw new Error(`generate_image task ${taskStatus}: ${responseMessage(task)}`);
|
|
1083
|
+
}
|
|
1084
|
+
if (Date.now() + pollIntervalMs > deadline)
|
|
1085
|
+
break;
|
|
1086
|
+
await delay(pollIntervalMs, undefined, { signal: this.signal });
|
|
1135
1087
|
}
|
|
1136
|
-
if (
|
|
1137
|
-
throw new Error(`generate_image task ${
|
|
1088
|
+
if (!completed) {
|
|
1089
|
+
throw new Error(`generate_image timed out waiting for task ${taskId}`);
|
|
1138
1090
|
}
|
|
1139
|
-
if (Date.now() + pollIntervalMs > deadline)
|
|
1140
|
-
break;
|
|
1141
|
-
await delay(pollIntervalMs, undefined, { signal: this.signal });
|
|
1142
1091
|
}
|
|
1143
1092
|
if (!completed) {
|
|
1144
|
-
throw new Error(
|
|
1093
|
+
throw new Error("generate_image returned no response");
|
|
1145
1094
|
}
|
|
1146
1095
|
const output = record(completed.output);
|
|
1147
1096
|
const choices = Array.isArray(output.choices) ? output.choices : [];
|
|
@@ -1156,7 +1105,7 @@ export class ToolExecutor {
|
|
|
1156
1105
|
const imageUrl = stringValue(generated?.image) ||
|
|
1157
1106
|
stringValue(record(legacyResults[0]).url);
|
|
1158
1107
|
if (!imageUrl) {
|
|
1159
|
-
throw new Error("generate_image
|
|
1108
|
+
throw new Error("generate_image response did not include an image URL");
|
|
1160
1109
|
}
|
|
1161
1110
|
const imageResponse = await fetch(imageUrl, { signal: this.signal });
|
|
1162
1111
|
if (!imageResponse.ok) {
|