dsh-eval-guard 0.2.3 → 0.3.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/README.md +91 -13
- package/assets/commit0/instance.json +30 -0
- package/assets/commit0/workspace.zip +0 -0
- package/assets/featurebench/hidden/test_regression.py +690 -0
- package/assets/featurebench/instance.json +16 -0
- package/assets/featurebench/workspace.zip +0 -0
- package/assets/swe-bench-pro/hidden/test.patch +36 -0
- package/assets/swe-bench-pro/instance.json +10 -0
- package/assets/swe-bench-pro/workspace.zip +0 -0
- package/eval/README.md +18 -0
- package/eval/arena/arena/README.md +21 -0
- package/eval/arena/arena/bt.py +104 -0
- package/eval/arena/arena/clock.py +15 -0
- package/eval/arena/arena/core.py +265 -0
- package/eval/arena/arena/run.cmd +24 -0
- package/eval/arena/arena/run.sh +11 -0
- package/eval/arena/arena/schedule.py +79 -0
- package/eval/arena/arena/tournament.py +183 -0
- package/eval/arena/bomberman/bots/README.md +16 -0
- package/eval/arena/bomberman/bots/deepseek-v4.1-flash.cpp +447 -0
- package/eval/arena/bomberman/bots/doubao-seed-2.1.cpp +542 -0
- package/eval/arena/bomberman/bots/glm-5.3.cpp +406 -0
- package/eval/arena/bomberman/bots/gpt-6-astra.cpp +768 -0
- package/eval/arena/bomberman/bots/gpt-6-sol.cpp +444 -0
- package/eval/arena/bomberman/bots/kimi-k3.cpp +432 -0
- package/eval/arena/bomberman/bots/mimo-v2.6-pro.cpp +529 -0
- package/eval/arena/bomberman/bots/opus-5.5.cpp +548 -0
- package/eval/arena/bomberman/bots/qwen-3.8-max.cpp +836 -0
- package/eval/arena/bomberman/tournament/README.md +11 -0
- package/eval/arena/bomberman/tournament/bt.py +56 -0
- package/eval/arena/bomberman/tournament/evaluate.py +199 -0
- package/eval/arena/bomberman/tournament/run.py +79 -0
- package/eval/arena/bomberman/tournament/schedule-2to10.json.gz +0 -0
- package/eval/arena/bomberman/workspace/MANIFEST.json +13 -0
- package/eval/arena/bomberman/workspace/README.md +29 -0
- package/eval/arena/bomberman/workspace/RULES.md +100 -0
- package/eval/arena/bomberman/workspace/baseline.cpp +240 -0
- package/eval/arena/bomberman/workspace/clock.py +25 -0
- package/eval/arena/bomberman/workspace/engine/arena.py +198 -0
- package/eval/arena/bomberman/workspace/engine/build.py +35 -0
- package/eval/arena/bomberman/workspace/engine/engine.py +256 -0
- package/eval/arena/bomberman/workspace/engine/verify_replay.py +24 -0
- package/eval/arena/bomberman/workspace/run.cmd +28 -0
- package/eval/arena/bomberman/workspace/run.sh +16 -0
- package/eval/arena/bomberman/workspace/viewer/index.html +42 -0
- package/eval/arena/ctf/bots/README.md +18 -0
- package/eval/arena/ctf/bots/deepseek-v4.1-flash.cpp +601 -0
- package/eval/arena/ctf/bots/doubao-seed-2.1-turbo.cpp +320 -0
- package/eval/arena/ctf/bots/fable-5.1.cpp +634 -0
- package/eval/arena/ctf/bots/glm-5.3.cpp +689 -0
- package/eval/arena/ctf/bots/gpt-6-astra.cpp +657 -0
- package/eval/arena/ctf/bots/gpt-6.1-sol.cpp +725 -0
- package/eval/arena/ctf/bots/kimi-k3.cpp +667 -0
- package/eval/arena/ctf/bots/mimo-v2.6-pro.cpp +533 -0
- package/eval/arena/ctf/bots/opus-5.5.cpp +645 -0
- package/eval/arena/ctf/bots/qwen-3.8-max.cpp +726 -0
- package/eval/arena/ctf/bots/sonnet-5.5.cpp +569 -0
- package/eval/arena/ctf/tournament/README.md +15 -0
- package/eval/arena/ctf/tournament/formal-2to8.json +31 -0
- package/eval/arena/ctf/tournament/formal-9to15.json +31 -0
- package/eval/arena/ctf/workspace/README.md +41 -0
- package/eval/arena/ctf/workspace/RULES.md +119 -0
- package/eval/arena/ctf/workspace/arena.py +186 -0
- package/eval/arena/ctf/workspace/baseline.cpp +257 -0
- package/eval/arena/ctf/workspace/clock.py +15 -0
- package/eval/arena/ctf/workspace/core.py +265 -0
- package/eval/arena/ctf/workspace/engine.py +389 -0
- package/eval/arena/ctf/workspace/run.cmd +24 -0
- package/eval/arena/ctf/workspace/run.sh +11 -0
- package/eval/arena/ctf/workspace/viewer.html +71 -0
- package/eval/arena/land/bots/README.md +17 -0
- package/eval/arena/land/bots/deepseek-4.1f@workbuddy#1/bot.cpp +608 -0
- package/eval/arena/land/bots/glm-5.3@workbuddy#1/bot.cpp +521 -0
- package/eval/arena/land/bots/gpt-6-astra@codex#1/bot.cpp +373 -0
- package/eval/arena/land/bots/gpt-6-sol@codex#1/bot.cpp +341 -0
- package/eval/arena/land/bots/kimi-k3@workbuddy#1/bot.cpp +492 -0
- package/eval/arena/land/bots/mimo-2.6-pro@zcode#1/bot.cpp +652 -0
- package/eval/arena/land/bots/mystery-model@zcode#1/bot.cpp +466 -0
- package/eval/arena/land/bots/opus-5.5@claude-code#1/bot.cpp +630 -0
- package/eval/arena/land/bots/sonnet-5@claude-code#1/bot.cpp +619 -0
- package/eval/arena/land/tournament/make_replay_page.py +17 -0
- package/eval/arena/land/tournament/rate.py +290 -0
- package/eval/arena/land/tournament/round_robin.py +172 -0
- package/eval/arena/land/tournament/sparring.cpp +356 -0
- package/eval/arena/land/workspace/RULES.md +114 -0
- package/eval/arena/land/workspace/TASK.md +46 -0
- package/eval/arena/land/workspace/anchors/.build/greedy_f2e6d2dea5ca +0 -0
- package/eval/arena/land/workspace/anchors/greedy.cpp +194 -0
- package/eval/arena/land/workspace/anchors/random.cpp +80 -0
- package/eval/arena/land/workspace/clock.py +64 -0
- package/eval/arena/land/workspace/engine/build.py +33 -0
- package/eval/arena/land/workspace/engine/game.py +273 -0
- package/eval/arena/land/workspace/engine/referee.py +308 -0
- package/eval/arena/land/workspace/starter/.build/bot_be32b45b4699 +0 -0
- package/eval/arena/land/workspace/starter/bot.cpp +82 -0
- package/eval/arena/land/workspace/viewer/viewer.html +210 -0
- package/eval/arena/tank/bots/README.md +24 -0
- package/eval/arena/tank/bots/half1/deepseek-v4.1-flash.cpp +367 -0
- package/eval/arena/tank/bots/half1/doubao-seed-evolving.cpp +402 -0
- package/eval/arena/tank/bots/half1/gemini-4-argon.cpp +1132 -0
- package/eval/arena/tank/bots/half1/glm-5.3.cpp +551 -0
- package/eval/arena/tank/bots/half1/gpt-6-astra.cpp +233 -0
- package/eval/arena/tank/bots/half1/gpt-6.1-sol-ultrafast.cpp +350 -0
- package/eval/arena/tank/bots/half1/gpt-6.1-sol.cpp +78 -0
- package/eval/arena/tank/bots/half1/grok-4.7.cpp +1099 -0
- package/eval/arena/tank/bots/half1/haiku-5.5.cpp +210 -0
- package/eval/arena/tank/bots/half1/kimi-k3.cpp +353 -0
- package/eval/arena/tank/bots/half1/mimo-v2.6-pro.cpp +336 -0
- package/eval/arena/tank/bots/half1/muse-wonder-full.cpp +560 -0
- package/eval/arena/tank/bots/half1/opus-5.5.cpp +435 -0
- package/eval/arena/tank/bots/half1/qwen-3.8-max.cpp +537 -0
- package/eval/arena/tank/bots/half2/deepseek-v4.1-flash.cpp +425 -0
- package/eval/arena/tank/bots/half2/doubao-seed-evolving.cpp +308 -0
- package/eval/arena/tank/bots/half2/gemini-4-argon.cpp +1390 -0
- package/eval/arena/tank/bots/half2/glm-5.3.cpp +651 -0
- package/eval/arena/tank/bots/half2/gpt-6-astra.cpp +241 -0
- package/eval/arena/tank/bots/half2/gpt-6.1-sol-ultrafast.cpp +356 -0
- package/eval/arena/tank/bots/half2/gpt-6.1-sol.cpp +87 -0
- package/eval/arena/tank/bots/half2/grok-4.7.cpp +1205 -0
- package/eval/arena/tank/bots/half2/haiku-5.5.cpp +210 -0
- package/eval/arena/tank/bots/half2/kimi-k3.cpp +487 -0
- package/eval/arena/tank/bots/half2/mimo-v2.6-pro.cpp +336 -0
- package/eval/arena/tank/bots/half2/muse-wonder-full.cpp +560 -0
- package/eval/arena/tank/bots/half2/opus-5.5.cpp +476 -0
- package/eval/arena/tank/bots/half2/qwen-3.8-max.cpp +392 -0
- package/eval/arena/tank/tournament/README.md +24 -0
- package/eval/arena/tank/tournament/bt.py +218 -0
- package/eval/arena/tank/tournament/final.json +202 -0
- package/eval/arena/tank/tournament/halftime.json +146 -0
- package/eval/arena/tank/tournament/progress.json +911 -0
- package/eval/arena/tank/tournament/schedule.py +100 -0
- package/eval/arena/tank/tournament/sparring.cpp +928 -0
- package/eval/arena/tank/tournament/tournament.py +1426 -0
- package/eval/arena/tank/workspace/README.md +169 -0
- package/eval/arena/tank/workspace/RULES.md +443 -0
- package/eval/arena/tank/workspace/arena.py +862 -0
- package/eval/arena/tank/workspace/baseline.cpp +259 -0
- package/eval/arena/tank/workspace/clock.py +103 -0
- package/eval/arena/tank/workspace/core.py +712 -0
- package/eval/arena/tank/workspace/engine.py +573 -0
- package/eval/arena/tank/workspace/history/.lock +0 -0
- package/eval/arena/tank/workspace/history/20261017-193610-08715287f91f-bot.cpp +109 -0
- package/eval/arena/tank/workspace/history/20261017-193619-f00b2d5635fa-gpt-6-astra.cpp +241 -0
- package/eval/arena/tank/workspace/history/20261017-193643-63319e0a2b1a-gpt-6_1-sol-ultrafast.cpp +356 -0
- package/eval/arena/tank/workspace/history/20261017-193647-41c1341d0a8a-gemini-4-argon.cpp +1390 -0
- package/eval/arena/tank/workspace/history/20261017-193652-e34c0f45539a-gpt-6_1-sol.cpp +87 -0
- package/eval/arena/tank/workspace/history/20261017-193654-c026348ce96e-opus-5_5.cpp +476 -0
- package/eval/arena/tank/workspace/history/20261017-193655-4a21366e94d1-muse-wonder-full.cpp +560 -0
- package/eval/arena/tank/workspace/history/20261017-193657-dbc4e9969c52-mimo-v2_6-pro.cpp +336 -0
- package/eval/arena/tank/workspace/history/20261017-193703-723cd036eb31-grok-4_7.cpp +1205 -0
- package/eval/arena/tank/workspace/history/20261017-193707-1ebbb227be68-deepseek-v4_1-flash.cpp +425 -0
- package/eval/arena/tank/workspace/history/20261017-193709-aeb0e7202ad5-doubao-seed-evolving.cpp +308 -0
- package/eval/arena/tank/workspace/history/20261017-193710-b21ebeb9bc76-kimi-k3.cpp +487 -0
- package/eval/arena/tank/workspace/history/20261017-193712-54cd86531905-haiku-5_5.cpp +210 -0
- package/eval/arena/tank/workspace/history/20261017-193714-efdb8e35c0bf-qwen-3_8-max.cpp +392 -0
- package/eval/arena/tank/workspace/history/20261017-193715-681423c23fdb-glm-5_3.cpp +651 -0
- package/eval/arena/tank/workspace/history/20261017-193717-1a8b3c75181d-sparring.cpp +928 -0
- package/eval/arena/tank/workspace/history/20261017-193720-1e0803a541e8-baseline.cpp +259 -0
- package/eval/arena/tank/workspace/history/log.jsonl +103 -0
- package/eval/arena/tank/workspace/judge/cycles.hpp +53 -0
- package/eval/arena/tank/workspace/judge/fast.cpp +337 -0
- package/eval/arena/tank/workspace/judge/referee.cpp +125 -0
- package/eval/arena/tank/workspace/judge/speed.cpp +99 -0
- package/eval/arena/tank/workspace/run.cmd +31 -0
- package/eval/arena/tank/workspace/run.sh +23 -0
- package/eval/arena/tank/workspace/tank.hpp +1762 -0
- package/eval/arena/tank/workspace/tank_main.cpp +215 -0
- package/eval/arena/tank/workspace/viewer.html +1150 -0
- package/eval/arena/warehouse/workspace/SPEC.md +130 -0
- package/eval/arena/warehouse/workspace/TASK.md +71 -0
- package/eval/arena/warehouse/workspace/baseline/common.h +88 -0
- package/eval/arena/warehouse/workspace/baseline/main.cpp +27 -0
- package/eval/arena/warehouse/workspace/baseline/old_stuff.cpp +87 -0
- package/eval/arena/warehouse/workspace/baseline/sim.cpp +427 -0
- package/eval/arena/warehouse/workspace/baseline/util.cpp +220 -0
- package/eval/arena/warehouse/workspace/clock.py +42 -0
- package/eval/arena/warehouse/workspace/src/common.h +88 -0
- package/eval/arena/warehouse/workspace/src/main.cpp +27 -0
- package/eval/arena/warehouse/workspace/src/old_stuff.cpp +87 -0
- package/eval/arena/warehouse/workspace/src/sim.cpp +427 -0
- package/eval/arena/warehouse/workspace/src/util.cpp +220 -0
- package/eval/arena/warehouse/workspace/tools/bench.py +119 -0
- package/eval/arena/warehouse/workspace/tools/check_input.py +161 -0
- package/eval/arena/warehouse/workspace/tools/gen.py +132 -0
- package/eval/arena/warehouse/workspace/tools/measure.py +107 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_crowded.in +1420 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_crowded.out +9162 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_dynamic.in +1465 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_dynamic.out +3439 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_flood.in +2757 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_flood.out +3126 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_small.in +731 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_small.out +2578 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_wide.in +528 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_wide.out +2026 -0
- package/eval/bomberman.py +20 -0
- package/eval/commit0.py +21 -0
- package/eval/common.py +61 -0
- package/eval/ctf.py +20 -0
- package/eval/featurebench.py +25 -0
- package/eval/land.py +20 -0
- package/eval/legacy/bomberman-prices.json +4 -0
- package/eval/legacy/bomberman.py +161 -0
- package/eval/legacy/commit0.py +91 -0
- package/eval/legacy/ctf-prices.json +4 -0
- package/eval/legacy/ctf.py +178 -0
- package/eval/legacy/featurebench.py +103 -0
- package/eval/legacy/land-prices.json +4 -0
- package/eval/legacy/land.py +177 -0
- package/eval/legacy/swe_bench_pro.py +116 -0
- package/eval/legacy/tank-prices.json +3 -0
- package/eval/legacy/tank.py +408 -0
- package/eval/legacy/warehouse-prices.json +4 -0
- package/eval/legacy/warehouse.py +365 -0
- package/eval/swe-bench-pro.py +25 -0
- package/eval/tank.py +20 -0
- package/eval/warehouse.py +20 -0
- package/lib/client/index.js +376 -63
- package/lib/client/index.js.map +1 -1
- package/lib/index.js +684 -27
- package/package.json +2 -1
package/lib/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { spawn } from "node:child_process";
|
|
7
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
8
8
|
//#region src/config.ts
|
|
9
9
|
/**
|
|
10
10
|
* Plugin configuration. The Settings page writes this document through the
|
|
@@ -18,7 +18,13 @@ const Config = z.object({
|
|
|
18
18
|
timeLimitMinutes: z.number().default(90).volatile(),
|
|
19
19
|
forbiddenPaths: z.string().default("").volatile(),
|
|
20
20
|
toolBlacklistUseDefault: z.boolean().default(true).volatile(),
|
|
21
|
-
toolBlacklist: z.string().default("").volatile()
|
|
21
|
+
toolBlacklist: z.string().default("").volatile(),
|
|
22
|
+
cloneSource: z.string().default("github").volatile(),
|
|
23
|
+
customCloneUrl: z.string().default("").volatile(),
|
|
24
|
+
pipIndex: z.string().default("pypi").volatile(),
|
|
25
|
+
customPipUrl: z.string().default("").volatile(),
|
|
26
|
+
gitProxyUrl: z.string().default("").volatile(),
|
|
27
|
+
pipProxyUrl: z.string().default("").volatile()
|
|
22
28
|
});
|
|
23
29
|
/** Clamp and normalize a live configuration object. */
|
|
24
30
|
function resolveConfig(value) {
|
|
@@ -29,7 +35,13 @@ function resolveConfig(value) {
|
|
|
29
35
|
timeLimitMinutes: Math.min(1440, Math.max(1, Math.round(minutes))),
|
|
30
36
|
forbiddenPaths: typeof value?.forbiddenPaths === "string" ? value.forbiddenPaths : "",
|
|
31
37
|
toolBlacklistUseDefault: value?.toolBlacklistUseDefault !== false,
|
|
32
|
-
toolBlacklist: typeof value?.toolBlacklist === "string" ? value.toolBlacklist : ""
|
|
38
|
+
toolBlacklist: typeof value?.toolBlacklist === "string" ? value.toolBlacklist : "",
|
|
39
|
+
cloneSource: typeof value?.cloneSource === "string" && value.cloneSource !== "" ? value.cloneSource : "github",
|
|
40
|
+
customCloneUrl: typeof value?.customCloneUrl === "string" ? value.customCloneUrl : "",
|
|
41
|
+
pipIndex: typeof value?.pipIndex === "string" && value.pipIndex !== "" ? value.pipIndex : "pypi",
|
|
42
|
+
customPipUrl: typeof value?.customPipUrl === "string" ? value.customPipUrl : "",
|
|
43
|
+
gitProxyUrl: typeof value?.gitProxyUrl === "string" ? value.gitProxyUrl : "",
|
|
44
|
+
pipProxyUrl: typeof value?.pipProxyUrl === "string" ? value.pipProxyUrl : ""
|
|
33
45
|
};
|
|
34
46
|
}
|
|
35
47
|
//#endregion
|
|
@@ -42,6 +54,10 @@ const PROMPT_ROUTE = "/api/eval-guard/prompt";
|
|
|
42
54
|
const EVENTS_ROUTE = "/api/eval-guard/events";
|
|
43
55
|
const REVEAL_ROUTE = "/api/eval-guard/reveal";
|
|
44
56
|
const MANUAL_ROUTE = "/api/eval-guard/manual";
|
|
57
|
+
const JOBS_ROUTE = "/api/eval-guard/jobs";
|
|
58
|
+
const JOB_ROUTE = "/api/eval-guard/job";
|
|
59
|
+
const JOB_CANCEL_ROUTE = "/api/eval-guard/job/cancel";
|
|
60
|
+
const EVAL_ROUTE = "/api/eval-guard/evaluate";
|
|
45
61
|
/** Supported task ids. */
|
|
46
62
|
const TANK_TASK = "tank";
|
|
47
63
|
const TANK_TASK_LABEL = "第 6 集 · 坦克大战";
|
|
@@ -52,6 +68,45 @@ const LAND_TASK_LABEL = "第 1 集 · 圈地";
|
|
|
52
68
|
const BOMBERMAN_TASK = "bomberman";
|
|
53
69
|
const BOMBERMAN_TASK_LABEL = "第 2 集 · 炸弹人";
|
|
54
70
|
const CTF_TASK_LABEL = "第 3 集 · 夺旗";
|
|
71
|
+
const COMMIT0_TASK = "commit0";
|
|
72
|
+
const COMMIT0_TASK_LABEL = "Commit0 · 单库实现";
|
|
73
|
+
const SWE_BENCH_PRO_TASK = "swe-bench-pro";
|
|
74
|
+
const SWE_BENCH_PRO_TASK_LABEL = "SWE-Bench Pro · 单任务";
|
|
75
|
+
const FEATUREBENCH_TASK = "featurebench";
|
|
76
|
+
const FEATUREBENCH_TASK_LABEL = "FeatureBench · 单 feature";
|
|
77
|
+
/** Common pip indexes offered by the settings page. */
|
|
78
|
+
const PIP_MIRRORS = [
|
|
79
|
+
{
|
|
80
|
+
id: "pypi",
|
|
81
|
+
label: "PyPI",
|
|
82
|
+
url: "https://pypi.org/simple"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: "tsinghua",
|
|
86
|
+
label: "清华 TUNA",
|
|
87
|
+
url: "https://pypi.tuna.tsinghua.edu.cn/simple"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: "aliyun",
|
|
91
|
+
label: "阿里云",
|
|
92
|
+
url: "https://mirrors.aliyun.com/pypi/simple"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: "tencent",
|
|
96
|
+
label: "腾讯云",
|
|
97
|
+
url: "https://mirrors.cloud.tencent.com/pypi/simple"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: "bfsu",
|
|
101
|
+
label: "北外",
|
|
102
|
+
url: "https://mirrors.bfsu.edu.cn/pypi/web/simple"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: "sjtu",
|
|
106
|
+
label: "上海交大",
|
|
107
|
+
url: "https://mirror.sjtu.edu.cn/pypi/web/simple"
|
|
108
|
+
}
|
|
109
|
+
];
|
|
55
110
|
//#endregion
|
|
56
111
|
//#region src/policy.ts
|
|
57
112
|
/**
|
|
@@ -755,7 +810,13 @@ var Guard = class {
|
|
|
755
810
|
timeLimitMinutes: read("timeLimitMinutes"),
|
|
756
811
|
forbiddenPaths: read("forbiddenPaths"),
|
|
757
812
|
toolBlacklistUseDefault: read("toolBlacklistUseDefault"),
|
|
758
|
-
toolBlacklist: read("toolBlacklist")
|
|
813
|
+
toolBlacklist: read("toolBlacklist"),
|
|
814
|
+
cloneSource: read("cloneSource"),
|
|
815
|
+
customCloneUrl: read("customCloneUrl"),
|
|
816
|
+
pipIndex: read("pipIndex"),
|
|
817
|
+
customPipUrl: read("customPipUrl"),
|
|
818
|
+
gitProxyUrl: read("gitProxyUrl"),
|
|
819
|
+
pipProxyUrl: read("pipProxyUrl")
|
|
759
820
|
});
|
|
760
821
|
}
|
|
761
822
|
/** Apply a new configuration document. */
|
|
@@ -770,8 +831,6 @@ var Guard = class {
|
|
|
770
831
|
const wasEnabled = this.enabled;
|
|
771
832
|
if (!wasEnabled && next.enabled) this.enable();
|
|
772
833
|
else if (wasEnabled && !next.enabled) this.disable();
|
|
773
|
-
else if (this.enabled && !next.timeLimitEnabled) this.deadlines.clear();
|
|
774
|
-
else if (this.enabled && next.timeLimitEnabled) this.rearmRunning();
|
|
775
834
|
}
|
|
776
835
|
/** Whether the guard is active. */
|
|
777
836
|
get active() {
|
|
@@ -898,7 +957,6 @@ var Guard = class {
|
|
|
898
957
|
};
|
|
899
958
|
};
|
|
900
959
|
this.enabled = true;
|
|
901
|
-
this.rearmRunning();
|
|
902
960
|
for (const agent of this.services.agents.list()) if (this.isMain(agent.session)) this.forceApproval(agent.session);
|
|
903
961
|
this.record({
|
|
904
962
|
kind: "notice",
|
|
@@ -919,15 +977,6 @@ var Guard = class {
|
|
|
919
977
|
detail: "评测守护已停用。"
|
|
920
978
|
});
|
|
921
979
|
}
|
|
922
|
-
/** Re-arm deadlines for running main conversations after an enable/config change. */
|
|
923
|
-
rearmRunning() {
|
|
924
|
-
if (!this.enabled || !this.configuration.timeLimitEnabled) return;
|
|
925
|
-
for (const agent of this.services.agents.list()) {
|
|
926
|
-
if (!this.isMain(agent.session)) continue;
|
|
927
|
-
const jobs = (this.services.jobs?.list(agent.id) ?? []).filter((job) => job.status === "running" || job.status === "stopping");
|
|
928
|
-
if (agent.status === "running" || jobs.length > 0) this.arm(agent.session.id);
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
980
|
/** Set the deadline for one session. */
|
|
932
981
|
arm(sessionId) {
|
|
933
982
|
const config = this.configuration;
|
|
@@ -985,7 +1034,8 @@ var Guard = class {
|
|
|
985
1034
|
if (event.type === "user/message") {
|
|
986
1035
|
if ((event.data?.source)?.kind === "user") {
|
|
987
1036
|
this.startedAt.set(session.id, Date.now());
|
|
988
|
-
this.arm(session.id);
|
|
1037
|
+
if (this.configuration.timeLimitEnabled) this.arm(session.id);
|
|
1038
|
+
else this.deadlines.delete(session.id);
|
|
989
1039
|
}
|
|
990
1040
|
} else if (event.type === "assistant/message") {
|
|
991
1041
|
const usage = event.data?.usage;
|
|
@@ -1318,6 +1368,204 @@ var Guard = class {
|
|
|
1318
1368
|
}
|
|
1319
1369
|
};
|
|
1320
1370
|
//#endregion
|
|
1371
|
+
//#region src/jobs.ts
|
|
1372
|
+
/** Persistent background jobs for task setup and single-result evaluation. */
|
|
1373
|
+
function now() {
|
|
1374
|
+
return Date.now();
|
|
1375
|
+
}
|
|
1376
|
+
function newId() {
|
|
1377
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/[-:.TZ]/g, "").slice(0, 14) + "-" + Math.random().toString(16).slice(2, 8);
|
|
1378
|
+
}
|
|
1379
|
+
/** Jobs live under the guard root and survive settings unmounts and page reloads. */
|
|
1380
|
+
var JobManager = class {
|
|
1381
|
+
root;
|
|
1382
|
+
jobs = /* @__PURE__ */ new Map();
|
|
1383
|
+
children = /* @__PURE__ */ new Map();
|
|
1384
|
+
constructor() {
|
|
1385
|
+
this.root = path.join(guardRoot(), "jobs");
|
|
1386
|
+
fs.mkdirSync(this.root, { recursive: true });
|
|
1387
|
+
for (const name of fs.readdirSync(this.root)) {
|
|
1388
|
+
const file = path.join(this.root, name, "job.json");
|
|
1389
|
+
if (!fs.existsSync(file)) continue;
|
|
1390
|
+
try {
|
|
1391
|
+
const stored = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
1392
|
+
if (stored.status === "running" || stored.status === "queued") {
|
|
1393
|
+
stored.status = "interrupted";
|
|
1394
|
+
stored.error = "服务重启,任务已中断";
|
|
1395
|
+
stored.endedAt = now();
|
|
1396
|
+
this.write(stored);
|
|
1397
|
+
}
|
|
1398
|
+
this.jobs.set(stored.id, stored);
|
|
1399
|
+
} catch {}
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
dir(idValue) {
|
|
1403
|
+
return path.join(this.root, idValue);
|
|
1404
|
+
}
|
|
1405
|
+
write(job) {
|
|
1406
|
+
fs.mkdirSync(this.dir(job.id), { recursive: true });
|
|
1407
|
+
fs.writeFileSync(path.join(this.dir(job.id), "job.json"), JSON.stringify(job, void 0, 2));
|
|
1408
|
+
}
|
|
1409
|
+
/** Start one child process and stream its output to a persistent log. */
|
|
1410
|
+
start(options) {
|
|
1411
|
+
const jobId = newId();
|
|
1412
|
+
const directory = this.dir(jobId);
|
|
1413
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
1414
|
+
const logFile = path.join(directory, "log.txt");
|
|
1415
|
+
fs.writeFileSync(logFile, "");
|
|
1416
|
+
const job = {
|
|
1417
|
+
id: jobId,
|
|
1418
|
+
kind: options.kind,
|
|
1419
|
+
task: options.task,
|
|
1420
|
+
workspaceId: options.workspaceId,
|
|
1421
|
+
status: "running",
|
|
1422
|
+
createdAt: now(),
|
|
1423
|
+
startedAt: now(),
|
|
1424
|
+
endedAt: null,
|
|
1425
|
+
updatedAt: now(),
|
|
1426
|
+
command: options.command,
|
|
1427
|
+
cwd: options.cwd,
|
|
1428
|
+
logFile,
|
|
1429
|
+
resultFile: path.join(directory, "result.json")
|
|
1430
|
+
};
|
|
1431
|
+
this.jobs.set(jobId, job);
|
|
1432
|
+
this.write(job);
|
|
1433
|
+
let child;
|
|
1434
|
+
try {
|
|
1435
|
+
child = spawn(options.command[0], options.command.slice(1), {
|
|
1436
|
+
cwd: options.cwd,
|
|
1437
|
+
env: {
|
|
1438
|
+
...process.env,
|
|
1439
|
+
...options.env ?? {},
|
|
1440
|
+
PYTHONUNBUFFERED: "1",
|
|
1441
|
+
EVAL_JOB_DIR: directory,
|
|
1442
|
+
EVAL_RESULT_FILE: job.resultFile
|
|
1443
|
+
},
|
|
1444
|
+
detached: true,
|
|
1445
|
+
stdio: [
|
|
1446
|
+
"ignore",
|
|
1447
|
+
"pipe",
|
|
1448
|
+
"pipe"
|
|
1449
|
+
]
|
|
1450
|
+
});
|
|
1451
|
+
} catch (error) {
|
|
1452
|
+
job.status = "failed";
|
|
1453
|
+
job.error = error instanceof Error ? error.message : String(error);
|
|
1454
|
+
job.endedAt = now();
|
|
1455
|
+
job.updatedAt = now();
|
|
1456
|
+
this.write(job);
|
|
1457
|
+
return this.info(job);
|
|
1458
|
+
}
|
|
1459
|
+
this.children.set(jobId, child);
|
|
1460
|
+
const append = (chunk) => {
|
|
1461
|
+
try {
|
|
1462
|
+
fs.appendFileSync(logFile, chunk);
|
|
1463
|
+
} catch {}
|
|
1464
|
+
};
|
|
1465
|
+
child.stdout?.on("data", (chunk) => append(chunk));
|
|
1466
|
+
child.stderr?.on("data", (chunk) => append(chunk));
|
|
1467
|
+
child.on("error", (error) => {
|
|
1468
|
+
job.status = "failed";
|
|
1469
|
+
job.error = error.message;
|
|
1470
|
+
job.endedAt = now();
|
|
1471
|
+
job.updatedAt = now();
|
|
1472
|
+
this.write(job);
|
|
1473
|
+
});
|
|
1474
|
+
child.on("exit", (code, signal) => {
|
|
1475
|
+
this.children.delete(jobId);
|
|
1476
|
+
if (job.status === "cancelled") return;
|
|
1477
|
+
if (code === 0) try {
|
|
1478
|
+
const extra = options.onSuccess?.(this.detail(jobId));
|
|
1479
|
+
if (extra !== void 0) {
|
|
1480
|
+
if (extra.prompt !== void 0) job.prompt = extra.prompt;
|
|
1481
|
+
if (extra.runId !== void 0) job.runId = extra.runId;
|
|
1482
|
+
if (extra.result !== void 0) {
|
|
1483
|
+
job.result = extra.result;
|
|
1484
|
+
fs.writeFileSync(job.resultFile, JSON.stringify(extra.result, void 0, 2));
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
if (fs.existsSync(job.resultFile)) try {
|
|
1488
|
+
job.result = JSON.parse(fs.readFileSync(job.resultFile, "utf8"));
|
|
1489
|
+
} catch {}
|
|
1490
|
+
job.status = "succeeded";
|
|
1491
|
+
} catch (error) {
|
|
1492
|
+
job.status = "failed";
|
|
1493
|
+
job.error = error instanceof Error ? error.message : String(error);
|
|
1494
|
+
}
|
|
1495
|
+
else {
|
|
1496
|
+
job.status = "failed";
|
|
1497
|
+
job.error = signal !== null ? "进程被信号 " + signal + " 终止" : "退出码 " + String(code);
|
|
1498
|
+
}
|
|
1499
|
+
job.endedAt = now();
|
|
1500
|
+
job.updatedAt = now();
|
|
1501
|
+
this.write(job);
|
|
1502
|
+
});
|
|
1503
|
+
return this.info(job);
|
|
1504
|
+
}
|
|
1505
|
+
/** Cancel a running job and its process group. */
|
|
1506
|
+
cancel(jobId) {
|
|
1507
|
+
const job = this.jobs.get(jobId);
|
|
1508
|
+
if (job === void 0) return void 0;
|
|
1509
|
+
if (job.status !== "running" && job.status !== "queued") return this.info(job);
|
|
1510
|
+
job.status = "cancelled";
|
|
1511
|
+
job.endedAt = now();
|
|
1512
|
+
job.updatedAt = now();
|
|
1513
|
+
job.error = "用户取消";
|
|
1514
|
+
this.write(job);
|
|
1515
|
+
const child = this.children.get(jobId);
|
|
1516
|
+
if (child?.pid !== void 0) try {
|
|
1517
|
+
process.kill(-child.pid, "SIGTERM");
|
|
1518
|
+
} catch {
|
|
1519
|
+
try {
|
|
1520
|
+
child.kill("SIGTERM");
|
|
1521
|
+
} catch {}
|
|
1522
|
+
}
|
|
1523
|
+
return this.info(job);
|
|
1524
|
+
}
|
|
1525
|
+
/** Current state, including a bounded log tail and optional result. */
|
|
1526
|
+
get(jobId) {
|
|
1527
|
+
if (!this.jobs.has(jobId)) return void 0;
|
|
1528
|
+
return this.detail(jobId);
|
|
1529
|
+
}
|
|
1530
|
+
/** Recent jobs, optionally filtered by workspace and task. */
|
|
1531
|
+
list(workspaceId, task) {
|
|
1532
|
+
return [...this.jobs.values()].filter((job) => workspaceId === void 0 || job.workspaceId === workspaceId).filter((job) => task === void 0 || job.task === task).sort((a, b) => b.createdAt - a.createdAt).slice(0, 20).map((job) => this.info(job));
|
|
1533
|
+
}
|
|
1534
|
+
info(job) {
|
|
1535
|
+
return {
|
|
1536
|
+
id: job.id,
|
|
1537
|
+
kind: job.kind,
|
|
1538
|
+
task: job.task,
|
|
1539
|
+
workspaceId: job.workspaceId,
|
|
1540
|
+
status: job.status,
|
|
1541
|
+
createdAt: job.createdAt,
|
|
1542
|
+
startedAt: job.startedAt,
|
|
1543
|
+
endedAt: job.endedAt,
|
|
1544
|
+
updatedAt: job.updatedAt,
|
|
1545
|
+
error: job.error
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
detail(jobId) {
|
|
1549
|
+
const job = this.jobs.get(jobId);
|
|
1550
|
+
let log = "";
|
|
1551
|
+
try {
|
|
1552
|
+
log = fs.readFileSync(job.logFile, "utf8").slice(-16e3);
|
|
1553
|
+
} catch {}
|
|
1554
|
+
let result;
|
|
1555
|
+
if (job.result !== void 0) result = job.result;
|
|
1556
|
+
else if (fs.existsSync(job.resultFile)) try {
|
|
1557
|
+
result = JSON.parse(fs.readFileSync(job.resultFile, "utf8"));
|
|
1558
|
+
} catch {}
|
|
1559
|
+
return {
|
|
1560
|
+
...this.info(job),
|
|
1561
|
+
log,
|
|
1562
|
+
prompt: job.prompt,
|
|
1563
|
+
runId: job.runId,
|
|
1564
|
+
result
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
};
|
|
1568
|
+
//#endregion
|
|
1321
1569
|
//#region src/task.ts
|
|
1322
1570
|
/**
|
|
1323
1571
|
* Task packs: packaged workspace.zip install, original prompt construction, and
|
|
@@ -1329,27 +1577,100 @@ const TASKS = [
|
|
|
1329
1577
|
{
|
|
1330
1578
|
id: TANK_TASK,
|
|
1331
1579
|
label: TANK_TASK_LABEL,
|
|
1332
|
-
description: "1v1 坦克大战 bot;解压 workspace.zip,提交 workspace/submission/bot.cpp。"
|
|
1580
|
+
description: "1v1 坦克大战 bot;解压 workspace.zip,提交 workspace/submission/bot.cpp。",
|
|
1581
|
+
timeLimitMinutes: 90,
|
|
1582
|
+
timeLimitEnabled: true,
|
|
1583
|
+
needsSetup: false
|
|
1333
1584
|
},
|
|
1334
1585
|
{
|
|
1335
1586
|
id: WAREHOUSE_TASK,
|
|
1336
1587
|
label: WAREHOUSE_TASK_LABEL,
|
|
1337
|
-
description: "单人单次仓储调度优化;解压 workspace.zip,提交 workspace/src/。"
|
|
1588
|
+
description: "单人单次仓储调度优化;解压 workspace.zip,提交 workspace/src/。",
|
|
1589
|
+
timeLimitMinutes: 90,
|
|
1590
|
+
timeLimitEnabled: true,
|
|
1591
|
+
needsSetup: false
|
|
1338
1592
|
},
|
|
1339
1593
|
{
|
|
1340
1594
|
id: LAND_TASK,
|
|
1341
1595
|
label: LAND_TASK_LABEL,
|
|
1342
|
-
description: "圈地 1v1/4 人混战 bot;解压 workspace.zip,提交 workspace/submission/bot.cpp。"
|
|
1596
|
+
description: "圈地 1v1/4 人混战 bot;解压 workspace.zip,提交 workspace/submission/bot.cpp。",
|
|
1597
|
+
timeLimitMinutes: 90,
|
|
1598
|
+
timeLimitEnabled: true,
|
|
1599
|
+
needsSetup: false
|
|
1343
1600
|
},
|
|
1344
1601
|
{
|
|
1345
1602
|
id: BOMBERMAN_TASK,
|
|
1346
1603
|
label: BOMBERMAN_TASK_LABEL,
|
|
1347
|
-
description: "炸弹人 2–10 人 bot;解压 workspace.zip,提交 workspace/submission/bot.cpp。"
|
|
1604
|
+
description: "炸弹人 2–10 人 bot;解压 workspace.zip,提交 workspace/submission/bot.cpp。",
|
|
1605
|
+
timeLimitMinutes: 90,
|
|
1606
|
+
timeLimitEnabled: true,
|
|
1607
|
+
needsSetup: false
|
|
1348
1608
|
},
|
|
1349
1609
|
{
|
|
1350
1610
|
id: "ctf",
|
|
1351
1611
|
label: CTF_TASK_LABEL,
|
|
1352
|
-
description: "夺旗 2–15 人 bot;解压 workspace.zip,提交 workspace/submission/bot.cpp。"
|
|
1612
|
+
description: "夺旗 2–15 人 bot;解压 workspace.zip,提交 workspace/submission/bot.cpp。",
|
|
1613
|
+
timeLimitMinutes: 90,
|
|
1614
|
+
timeLimitEnabled: true,
|
|
1615
|
+
needsSetup: false
|
|
1616
|
+
},
|
|
1617
|
+
{
|
|
1618
|
+
id: COMMIT0_TASK,
|
|
1619
|
+
label: COMMIT0_TASK_LABEL,
|
|
1620
|
+
description: "Commit0 单库实现:按规格从零实现一个 Python 库,单元测试评分。",
|
|
1621
|
+
timeLimitMinutes: 480,
|
|
1622
|
+
timeLimitEnabled: false,
|
|
1623
|
+
needsSetup: true,
|
|
1624
|
+
repoUrl: "https://github.com/msiemens/tinydb",
|
|
1625
|
+
remotes: [{
|
|
1626
|
+
id: "github",
|
|
1627
|
+
label: "GitHub",
|
|
1628
|
+
url: "https://github.com/msiemens/tinydb.git"
|
|
1629
|
+
}]
|
|
1630
|
+
},
|
|
1631
|
+
{
|
|
1632
|
+
id: SWE_BENCH_PRO_TASK,
|
|
1633
|
+
label: SWE_BENCH_PRO_TASK_LABEL,
|
|
1634
|
+
description: "SWE-Bench Pro 单任务:在真实仓库中修复一个多文件 issue,隐藏测试评分。",
|
|
1635
|
+
timeLimitMinutes: 1440,
|
|
1636
|
+
timeLimitEnabled: false,
|
|
1637
|
+
needsSetup: true,
|
|
1638
|
+
repoUrl: "https://github.com/ansible/ansible",
|
|
1639
|
+
remotes: [{
|
|
1640
|
+
id: "github",
|
|
1641
|
+
label: "GitHub",
|
|
1642
|
+
url: "https://github.com/ansible/ansible.git"
|
|
1643
|
+
}, {
|
|
1644
|
+
id: "gitee",
|
|
1645
|
+
label: "Gitee 镜像",
|
|
1646
|
+
url: "https://gitee.com/mirrors/ansible.git"
|
|
1647
|
+
}]
|
|
1648
|
+
},
|
|
1649
|
+
{
|
|
1650
|
+
id: FEATUREBENCH_TASK,
|
|
1651
|
+
label: FEATUREBENCH_TASK_LABEL,
|
|
1652
|
+
description: "FeatureBench 单 feature:实现一个跨文件功能,隐藏测试评分。",
|
|
1653
|
+
timeLimitMinutes: 1440,
|
|
1654
|
+
timeLimitEnabled: false,
|
|
1655
|
+
needsSetup: true,
|
|
1656
|
+
repoUrl: "https://github.com/mwaskom/seaborn",
|
|
1657
|
+
remotes: [
|
|
1658
|
+
{
|
|
1659
|
+
id: "github",
|
|
1660
|
+
label: "GitHub",
|
|
1661
|
+
url: "https://github.com/mwaskom/seaborn.git"
|
|
1662
|
+
},
|
|
1663
|
+
{
|
|
1664
|
+
id: "gitee",
|
|
1665
|
+
label: "Gitee 镜像",
|
|
1666
|
+
url: "https://gitee.com/mirrors/seaborn.git"
|
|
1667
|
+
},
|
|
1668
|
+
{
|
|
1669
|
+
id: "gitcode",
|
|
1670
|
+
label: "GitCode 镜像",
|
|
1671
|
+
url: "https://gitcode.com/gh_mirrors/se/seaborn.git"
|
|
1672
|
+
}
|
|
1673
|
+
]
|
|
1353
1674
|
}
|
|
1354
1675
|
];
|
|
1355
1676
|
/** The tank task definition (original prompt; only halftime mechanics removed). */
|
|
@@ -1358,6 +1679,9 @@ const TANK_DEFINITION = {
|
|
|
1358
1679
|
label: TANK_TASK_LABEL,
|
|
1359
1680
|
description: TASKS[0].description,
|
|
1360
1681
|
asset: path.join(packageRoot(), "assets", "tank", "workspace.zip"),
|
|
1682
|
+
timeLimitMinutes: 90,
|
|
1683
|
+
timeLimitEnabled: true,
|
|
1684
|
+
needsSetup: false,
|
|
1361
1685
|
prompt() {
|
|
1362
1686
|
return [
|
|
1363
1687
|
"按照要求编写尽可能强的坦克大战 bot。你的对手是其他顶尖 AI 模型写的 bot,它们可能主动设计战术。基准是一个中等强度、思路常规的 bot,它在正式比赛中是 0 分线;战胜基准不代表足够强,正式对手远强于基准。正式赛是 1 对 1 循环赛,用 Bradley-Terry 模型拟合实力值。",
|
|
@@ -1384,6 +1708,9 @@ const WAREHOUSE_DEFINITION = {
|
|
|
1384
1708
|
label: WAREHOUSE_TASK_LABEL,
|
|
1385
1709
|
description: TASKS[1].description,
|
|
1386
1710
|
asset: path.join(packageRoot(), "assets", "warehouse", "workspace.zip"),
|
|
1711
|
+
timeLimitMinutes: 90,
|
|
1712
|
+
timeLimitEnabled: true,
|
|
1713
|
+
needsSetup: false,
|
|
1387
1714
|
prompt() {
|
|
1388
1715
|
return [
|
|
1389
1716
|
"这是一场 AI 编程赛。你接手了一个代码混乱的老系统:src/ 里是一个仓储机器人调度仿真系统,功能正确,但运行很慢。你的任务:在输出完全不变的前提下,让它尽可能快。",
|
|
@@ -1407,6 +1734,9 @@ const LAND_DEFINITION = {
|
|
|
1407
1734
|
label: LAND_TASK_LABEL,
|
|
1408
1735
|
description: TASKS[2].description,
|
|
1409
1736
|
asset: path.join(packageRoot(), "assets", "land", "workspace.zip"),
|
|
1737
|
+
timeLimitMinutes: 90,
|
|
1738
|
+
timeLimitEnabled: true,
|
|
1739
|
+
needsSetup: false,
|
|
1410
1740
|
prompt() {
|
|
1411
1741
|
return [
|
|
1412
1742
|
"这是一场正式的 AI 编程对抗赛,你的对手是其他顶尖 AI 模型写的 bot,它们会主动切断你、抢你的地。比赛结果会公开排名,请拿出你的最高水平。",
|
|
@@ -1421,6 +1751,9 @@ const BOMBERMAN_DEFINITION = {
|
|
|
1421
1751
|
label: BOMBERMAN_TASK_LABEL,
|
|
1422
1752
|
description: TASKS[3].description,
|
|
1423
1753
|
asset: path.join(packageRoot(), "assets", "bomberman", "workspace.zip"),
|
|
1754
|
+
timeLimitMinutes: 90,
|
|
1755
|
+
timeLimitEnabled: true,
|
|
1756
|
+
needsSetup: false,
|
|
1424
1757
|
prompt() {
|
|
1425
1758
|
return ["按照要求编写尽可能强bot,你的对手是全球其他顶尖ai,可能主动设计战术。基准只是很弱的bot,它在正式比赛为0分线。正式赛会对战多轮,用Bradley-Terry模型拟合实力值。", "解压 workspace.zip,阅读 README.md 和 RULES.md,编写支持2–10人的炸弹人 bot。尽量60分钟完成,90分钟必须交卷。允许本地工具、对战和复盘,禁止联网、调用其他AI或查看其他选手资料。提交 submission/bot.cpp,简述策略。"].join("\n");
|
|
1426
1759
|
}
|
|
@@ -1431,25 +1764,173 @@ const CTF_DEFINITION = {
|
|
|
1431
1764
|
label: CTF_TASK_LABEL,
|
|
1432
1765
|
description: TASKS[4].description,
|
|
1433
1766
|
asset: path.join(packageRoot(), "assets", "ctf", "workspace.zip"),
|
|
1767
|
+
timeLimitMinutes: 90,
|
|
1768
|
+
timeLimitEnabled: true,
|
|
1769
|
+
needsSetup: false,
|
|
1434
1770
|
prompt() {
|
|
1435
1771
|
return ["按照要求编写尽可能强的 bot,你的对手是全球其他顶尖 AI,可能主动设计战术。基准是一个中等强度、思路常规的 bot,它在正式比赛是 0 分线;战胜基准不代表足够强,正式对手远强于基准。正式赛会对战多轮,用 Bradley-Terry 模型拟合实力值。", "解压 workspace.zip,阅读 README.md 和 RULES.md,编写支持 2–15 方的夺旗 bot。你有 60 分钟,只看最终 bot 的强度,提前交卷不加分,请充分利用时间持续改进;可以适当超时,但到 90 分钟会强制交卷。允许本地工具、对战和复盘,禁止联网、调用其他 AI 或查看其他选手资料。提交 workspace/submission/bot.cpp,简述策略。"].join("\n");
|
|
1436
1772
|
}
|
|
1437
1773
|
};
|
|
1774
|
+
/** Run the task's host-side setup script with network access. */
|
|
1775
|
+
function runSetup(workspacePath) {
|
|
1776
|
+
execFileSync("bash", ["setup.sh"], {
|
|
1777
|
+
cwd: workspacePath,
|
|
1778
|
+
stdio: "inherit",
|
|
1779
|
+
env: {
|
|
1780
|
+
...process.env,
|
|
1781
|
+
GIT_TERMINAL_PROMPT: "0"
|
|
1782
|
+
}
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
/** Commit0 single-library task. */
|
|
1786
|
+
const COMMIT0_DEFINITION = {
|
|
1787
|
+
id: COMMIT0_TASK,
|
|
1788
|
+
label: COMMIT0_TASK_LABEL,
|
|
1789
|
+
description: TASKS[5].description,
|
|
1790
|
+
asset: path.join(packageRoot(), "assets", "commit0", "workspace.zip"),
|
|
1791
|
+
prepare: runSetup,
|
|
1792
|
+
timeLimitMinutes: 480,
|
|
1793
|
+
timeLimitEnabled: false,
|
|
1794
|
+
needsSetup: true,
|
|
1795
|
+
repoUrl: "https://github.com/msiemens/tinydb",
|
|
1796
|
+
remotes: [{
|
|
1797
|
+
id: "github",
|
|
1798
|
+
label: "GitHub",
|
|
1799
|
+
url: "https://github.com/msiemens/tinydb.git"
|
|
1800
|
+
}],
|
|
1801
|
+
prompt() {
|
|
1802
|
+
return [
|
|
1803
|
+
"这是 Commit0 的一个单库实现任务:从零实现一个 Python 库,使其通过仓库自带的完整单元测试。",
|
|
1804
|
+
"",
|
|
1805
|
+
"当前 workspace 已经由安装器准备好:包含该库的测试、规格文档和项目配置,但库的实现源码已被移除。请阅读 TASK.md 和规格文档,自己实现库,直到单元测试全部通过。",
|
|
1806
|
+
"",
|
|
1807
|
+
"要求:",
|
|
1808
|
+
"- 只能修改当前工作区内的文件;工作区外不可访问,包括 /tmp;",
|
|
1809
|
+
"- 禁止联网、禁止调用其他 AI 或子代理;",
|
|
1810
|
+
"- 使用工作区里的 .venv 运行测试,例如 .venv/bin/python -m pytest tests;",
|
|
1811
|
+
"- 不要删除、修改或伪造测试;评分以隐藏的测试执行为准;",
|
|
1812
|
+
"- 完成后用几句话总结实现思路。"
|
|
1813
|
+
].join("\n");
|
|
1814
|
+
}
|
|
1815
|
+
};
|
|
1816
|
+
/** SWE-Bench Pro single-task task. */
|
|
1817
|
+
const SWE_BENCH_PRO_DEFINITION = {
|
|
1818
|
+
id: SWE_BENCH_PRO_TASK,
|
|
1819
|
+
label: SWE_BENCH_PRO_TASK_LABEL,
|
|
1820
|
+
description: TASKS[6].description,
|
|
1821
|
+
asset: path.join(packageRoot(), "assets", "swe-bench-pro", "workspace.zip"),
|
|
1822
|
+
prepare: runSetup,
|
|
1823
|
+
timeLimitMinutes: 1440,
|
|
1824
|
+
timeLimitEnabled: false,
|
|
1825
|
+
needsSetup: true,
|
|
1826
|
+
repoUrl: "https://github.com/ansible/ansible",
|
|
1827
|
+
remotes: [{
|
|
1828
|
+
id: "github",
|
|
1829
|
+
label: "GitHub",
|
|
1830
|
+
url: "https://github.com/ansible/ansible.git"
|
|
1831
|
+
}, {
|
|
1832
|
+
id: "gitee",
|
|
1833
|
+
label: "Gitee 镜像",
|
|
1834
|
+
url: "https://gitee.com/mirrors/ansible.git"
|
|
1835
|
+
}],
|
|
1836
|
+
prompt() {
|
|
1837
|
+
return [
|
|
1838
|
+
"这是 SWE-Bench Pro 的一个单任务:在真实的 Python 仓库中完成一个多文件 issue 修复。",
|
|
1839
|
+
"",
|
|
1840
|
+
"当前 workspace 已经由安装器准备好:包含目标仓库在指定 commit 的源码和依赖环境。请阅读 TASK.md 中的 issue 描述与要求,修改源码完成修复。",
|
|
1841
|
+
"",
|
|
1842
|
+
"要求:",
|
|
1843
|
+
"- 只能修改当前工作区内的文件;工作区外不可访问,包括 /tmp;",
|
|
1844
|
+
"- 禁止联网、禁止调用其他 AI 或子代理;",
|
|
1845
|
+
"- 使用工作区里的 .venv 运行测试,例如 .venv/bin/python -m pytest <测试文件>;",
|
|
1846
|
+
"- 不要删除、修改或伪造测试;评分会在你的代码上应用隐藏测试;",
|
|
1847
|
+
"- 完成后用几句话总结修改。"
|
|
1848
|
+
].join("\n");
|
|
1849
|
+
}
|
|
1850
|
+
};
|
|
1851
|
+
/** FeatureBench single-feature task. */
|
|
1852
|
+
const FEATUREBENCH_DEFINITION = {
|
|
1853
|
+
id: FEATUREBENCH_TASK,
|
|
1854
|
+
label: FEATUREBENCH_TASK_LABEL,
|
|
1855
|
+
description: TASKS[7].description,
|
|
1856
|
+
asset: path.join(packageRoot(), "assets", "featurebench", "workspace.zip"),
|
|
1857
|
+
prepare: runSetup,
|
|
1858
|
+
timeLimitMinutes: 1440,
|
|
1859
|
+
timeLimitEnabled: false,
|
|
1860
|
+
needsSetup: true,
|
|
1861
|
+
repoUrl: "https://github.com/mwaskom/seaborn",
|
|
1862
|
+
remotes: [
|
|
1863
|
+
{
|
|
1864
|
+
id: "github",
|
|
1865
|
+
label: "GitHub",
|
|
1866
|
+
url: "https://github.com/mwaskom/seaborn.git"
|
|
1867
|
+
},
|
|
1868
|
+
{
|
|
1869
|
+
id: "gitee",
|
|
1870
|
+
label: "Gitee 镜像",
|
|
1871
|
+
url: "https://gitee.com/mirrors/seaborn.git"
|
|
1872
|
+
},
|
|
1873
|
+
{
|
|
1874
|
+
id: "gitcode",
|
|
1875
|
+
label: "GitCode 镜像",
|
|
1876
|
+
url: "https://gitcode.com/gh_mirrors/se/seaborn.git"
|
|
1877
|
+
}
|
|
1878
|
+
],
|
|
1879
|
+
prompt() {
|
|
1880
|
+
return [
|
|
1881
|
+
"这是 FeatureBench 的一个单 feature 任务:在真实 Python 仓库中实现一个跨文件功能,并保持已有功能不退化。",
|
|
1882
|
+
"",
|
|
1883
|
+
"当前 workspace 已经由安装器准备好:包含目标仓库源码和依赖环境。请阅读 TASK.md 中的功能需求,修改源码完成实现。",
|
|
1884
|
+
"",
|
|
1885
|
+
"要求:",
|
|
1886
|
+
"- 只能修改当前工作区内的文件;工作区外不可访问,包括 /tmp;",
|
|
1887
|
+
"- 禁止联网、禁止调用其他 AI 或子代理;",
|
|
1888
|
+
"- 使用工作区里的 .venv 运行测试;",
|
|
1889
|
+
"- 不要删除、修改或伪造测试;评分会在你的代码上运行隐藏测试和回归测试;",
|
|
1890
|
+
"- 完成后用几句话总结实现思路。"
|
|
1891
|
+
].join("\n");
|
|
1892
|
+
}
|
|
1893
|
+
};
|
|
1438
1894
|
/** All task definitions keyed by id. */
|
|
1439
1895
|
const TASK_DEFINITIONS = {
|
|
1440
1896
|
[TANK_TASK]: TANK_DEFINITION,
|
|
1441
1897
|
[WAREHOUSE_TASK]: WAREHOUSE_DEFINITION,
|
|
1442
1898
|
[LAND_TASK]: LAND_DEFINITION,
|
|
1443
1899
|
[BOMBERMAN_TASK]: BOMBERMAN_DEFINITION,
|
|
1444
|
-
["ctf"]: CTF_DEFINITION
|
|
1900
|
+
["ctf"]: CTF_DEFINITION,
|
|
1901
|
+
[COMMIT0_TASK]: COMMIT0_DEFINITION,
|
|
1902
|
+
[SWE_BENCH_PRO_TASK]: SWE_BENCH_PRO_DEFINITION,
|
|
1903
|
+
[FEATUREBENCH_TASK]: FEATUREBENCH_DEFINITION
|
|
1445
1904
|
};
|
|
1446
|
-
/** Install, check, and prompt services for the task packs. */
|
|
1447
1905
|
var TaskService = class {
|
|
1448
1906
|
guard;
|
|
1449
1907
|
registry;
|
|
1908
|
+
jobs;
|
|
1450
1909
|
constructor(guard, registry) {
|
|
1451
1910
|
this.guard = guard;
|
|
1452
1911
|
this.registry = registry;
|
|
1912
|
+
this.jobs = new JobManager();
|
|
1913
|
+
}
|
|
1914
|
+
/** The clone URL for a task, honouring the selected source. */
|
|
1915
|
+
cloneUrl(definition, options) {
|
|
1916
|
+
const repo = definition.repoUrl ?? "";
|
|
1917
|
+
const source = options.cloneSource ?? "github";
|
|
1918
|
+
if (source === "custom" && options.customCloneUrl !== void 0 && options.customCloneUrl.trim() !== "") return options.customCloneUrl.trim().replace(/\/$/, "") + "/" + repo.replace(/^https?:\/\/github\.com\//, "");
|
|
1919
|
+
const remote = definition.remotes?.find((item) => item.id === source);
|
|
1920
|
+
if (remote !== void 0) return remote.url;
|
|
1921
|
+
return definition.remotes?.[0]?.url ?? repo;
|
|
1922
|
+
}
|
|
1923
|
+
/** Normalize a user-entered proxy address, or return an empty string. */
|
|
1924
|
+
proxyValue(raw) {
|
|
1925
|
+
const value = (raw ?? "").trim();
|
|
1926
|
+
if (value === "") return "";
|
|
1927
|
+
return /^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : "http://" + value;
|
|
1928
|
+
}
|
|
1929
|
+
/** The pip index URL for a task, honouring the selected mirror. */
|
|
1930
|
+
pipIndexUrl(options) {
|
|
1931
|
+
const index = options.pipIndex ?? "pypi";
|
|
1932
|
+
if (index === "custom") return (options.customPipUrl ?? "").trim() || "https://pypi.org/simple";
|
|
1933
|
+
return PIP_MIRRORS.find((item) => item.id === index)?.url ?? "https://pypi.org/simple";
|
|
1453
1934
|
}
|
|
1454
1935
|
/** Check one workspace before install. */
|
|
1455
1936
|
check(workspaceId) {
|
|
@@ -1487,6 +1968,118 @@ var TaskService = class {
|
|
|
1487
1968
|
definition(taskId) {
|
|
1488
1969
|
return TASK_DEFINITIONS[taskId];
|
|
1489
1970
|
}
|
|
1971
|
+
/** Start a non-blocking install. The settings page polls the returned job. */
|
|
1972
|
+
startInstall(workspaceId, taskId, options = {}) {
|
|
1973
|
+
const definition = TASK_DEFINITIONS[taskId];
|
|
1974
|
+
if (definition === void 0) return {
|
|
1975
|
+
ok: false,
|
|
1976
|
+
error: "未知任务:" + taskId
|
|
1977
|
+
};
|
|
1978
|
+
const checked = this.check(workspaceId);
|
|
1979
|
+
if (!checked.ok) return {
|
|
1980
|
+
ok: false,
|
|
1981
|
+
error: checked.error
|
|
1982
|
+
};
|
|
1983
|
+
if (!checked.empty) return {
|
|
1984
|
+
ok: false,
|
|
1985
|
+
error: "工作区不是空的;未执行任何操作。"
|
|
1986
|
+
};
|
|
1987
|
+
if (!this.guard.active) return {
|
|
1988
|
+
ok: false,
|
|
1989
|
+
error: "请先启用评测守护,再安装评测任务。"
|
|
1990
|
+
};
|
|
1991
|
+
try {
|
|
1992
|
+
fs.copyFileSync(this.materialZip(taskId), path.join(checked.path, "workspace.zip"));
|
|
1993
|
+
} catch (error) {
|
|
1994
|
+
return {
|
|
1995
|
+
ok: false,
|
|
1996
|
+
error: "复制素材失败:" + (error instanceof Error ? error.message : String(error))
|
|
1997
|
+
};
|
|
1998
|
+
}
|
|
1999
|
+
const env = {
|
|
2000
|
+
EVAL_CLONE_URL: this.cloneUrl(definition, options),
|
|
2001
|
+
EVAL_PIP_INDEX_URL: this.pipIndexUrl(options),
|
|
2002
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
2003
|
+
EVAL_GIT_PROXY: this.proxyValue(options.gitProxyUrl),
|
|
2004
|
+
EVAL_PIP_PROXY: this.proxyValue(options.pipProxyUrl)
|
|
2005
|
+
};
|
|
2006
|
+
const command = definition.needsSetup ? ["bash", "setup.sh"] : [
|
|
2007
|
+
"bash",
|
|
2008
|
+
"-c",
|
|
2009
|
+
"true"
|
|
2010
|
+
];
|
|
2011
|
+
return {
|
|
2012
|
+
ok: true,
|
|
2013
|
+
jobId: this.jobs.start({
|
|
2014
|
+
kind: "install",
|
|
2015
|
+
task: taskId,
|
|
2016
|
+
workspaceId,
|
|
2017
|
+
command,
|
|
2018
|
+
cwd: checked.path,
|
|
2019
|
+
env,
|
|
2020
|
+
onSuccess: () => {
|
|
2021
|
+
const runId = this.newRunId();
|
|
2022
|
+
const prompt = this.guard.promptFor(checked.path, this.materialZip(taskId), definition);
|
|
2023
|
+
const run = {
|
|
2024
|
+
runId,
|
|
2025
|
+
task: definition.id,
|
|
2026
|
+
workspaceId,
|
|
2027
|
+
workspacePath: checked.path,
|
|
2028
|
+
sessionId: null,
|
|
2029
|
+
createdAt: Date.now(),
|
|
2030
|
+
startedAt: null,
|
|
2031
|
+
endedAt: null,
|
|
2032
|
+
status: "prepared",
|
|
2033
|
+
prompt,
|
|
2034
|
+
timeLimitMinutes: this.guard.configuration.timeLimitMinutes
|
|
2035
|
+
};
|
|
2036
|
+
this.guard.registerRun(run);
|
|
2037
|
+
return {
|
|
2038
|
+
runId,
|
|
2039
|
+
prompt
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2042
|
+
}).id
|
|
2043
|
+
};
|
|
2044
|
+
}
|
|
2045
|
+
/** Start a single-workspace evaluation job. */
|
|
2046
|
+
startEval(workspaceId, taskId) {
|
|
2047
|
+
if (TASK_DEFINITIONS[taskId] === void 0) return {
|
|
2048
|
+
ok: false,
|
|
2049
|
+
error: "未知任务:" + taskId
|
|
2050
|
+
};
|
|
2051
|
+
const checked = this.check(workspaceId);
|
|
2052
|
+
if (!checked.ok) return {
|
|
2053
|
+
ok: false,
|
|
2054
|
+
error: checked.error
|
|
2055
|
+
};
|
|
2056
|
+
const script = path.join(packageRoot(), "eval", taskId + ".py");
|
|
2057
|
+
if (!fs.existsSync(script)) return {
|
|
2058
|
+
ok: false,
|
|
2059
|
+
error: "评测脚本不存在:" + script
|
|
2060
|
+
};
|
|
2061
|
+
return {
|
|
2062
|
+
ok: true,
|
|
2063
|
+
jobId: this.jobs.start({
|
|
2064
|
+
kind: "eval",
|
|
2065
|
+
task: taskId,
|
|
2066
|
+
workspaceId,
|
|
2067
|
+
command: [
|
|
2068
|
+
"python3",
|
|
2069
|
+
"-X",
|
|
2070
|
+
"utf8",
|
|
2071
|
+
script,
|
|
2072
|
+
"--workspace",
|
|
2073
|
+
checked.path
|
|
2074
|
+
],
|
|
2075
|
+
cwd: packageRoot(),
|
|
2076
|
+
env: {
|
|
2077
|
+
PYTHONPATH: "",
|
|
2078
|
+
PYTHONHOME: ""
|
|
2079
|
+
}
|
|
2080
|
+
}).id
|
|
2081
|
+
};
|
|
2082
|
+
}
|
|
1490
2083
|
/** Copy the packaged workspace.zip to a stable material path for install/manual use. */
|
|
1491
2084
|
materialZip(taskId = TANK_TASK) {
|
|
1492
2085
|
const definition = TASK_DEFINITIONS[taskId];
|
|
@@ -1533,12 +2126,13 @@ var TaskService = class {
|
|
|
1533
2126
|
};
|
|
1534
2127
|
try {
|
|
1535
2128
|
fs.copyFileSync(this.materialZip(taskId), path.join(checked.path, "workspace.zip"));
|
|
2129
|
+
if (definition.prepare !== void 0) definition.prepare(checked.path);
|
|
1536
2130
|
} catch (error) {
|
|
1537
2131
|
return {
|
|
1538
2132
|
ok: false,
|
|
1539
2133
|
workspaceId,
|
|
1540
2134
|
workspacePath: checked.path,
|
|
1541
|
-
error: "
|
|
2135
|
+
error: "任务准备失败:" + (error instanceof Error ? error.message : String(error))
|
|
1542
2136
|
};
|
|
1543
2137
|
}
|
|
1544
2138
|
const runId = this.newRunId();
|
|
@@ -1645,7 +2239,8 @@ function registerRoutes(ctx, guard, tasks) {
|
|
|
1645
2239
|
defaultBlacklist: guard.defaultBlacklist(),
|
|
1646
2240
|
generatedDefault: guard.configuration.toolBlacklistUseDefault,
|
|
1647
2241
|
workspaceRoot: dshHome(),
|
|
1648
|
-
runsRoot: path.join(guardRoot(), "runs")
|
|
2242
|
+
runsRoot: path.join(guardRoot(), "runs"),
|
|
2243
|
+
pipMirrors: [...PIP_MIRRORS]
|
|
1649
2244
|
});
|
|
1650
2245
|
}
|
|
1651
2246
|
});
|
|
@@ -1673,7 +2268,69 @@ function registerRoutes(ctx, guard, tasks) {
|
|
|
1673
2268
|
const workspaceId = typeof body.workspaceId === "string" ? body.workspaceId : "";
|
|
1674
2269
|
const taskId = typeof body.task === "string" && body.task !== "" ? body.task : TANK_TASK;
|
|
1675
2270
|
if (workspaceId === "") return json({ error: "workspaceId is required" }, 400);
|
|
1676
|
-
|
|
2271
|
+
const config = guard.configuration;
|
|
2272
|
+
return json(tasks.startInstall(workspaceId, taskId, {
|
|
2273
|
+
cloneSource: typeof body.cloneSource === "string" ? body.cloneSource : config.cloneSource,
|
|
2274
|
+
customCloneUrl: typeof body.customCloneUrl === "string" ? body.customCloneUrl : config.customCloneUrl,
|
|
2275
|
+
pipIndex: typeof body.pipIndex === "string" ? body.pipIndex : config.pipIndex,
|
|
2276
|
+
customPipUrl: typeof body.customPipUrl === "string" ? body.customPipUrl : config.customPipUrl,
|
|
2277
|
+
gitProxyUrl: typeof body.gitProxyUrl === "string" ? body.gitProxyUrl : config.gitProxyUrl,
|
|
2278
|
+
pipProxyUrl: typeof body.pipProxyUrl === "string" ? body.pipProxyUrl : config.pipProxyUrl
|
|
2279
|
+
}));
|
|
2280
|
+
}
|
|
2281
|
+
});
|
|
2282
|
+
add({
|
|
2283
|
+
path: EVAL_ROUTE,
|
|
2284
|
+
methods: ["POST"],
|
|
2285
|
+
requestBody: "buffered",
|
|
2286
|
+
fetch: async (request) => {
|
|
2287
|
+
let body = {};
|
|
2288
|
+
try {
|
|
2289
|
+
body = await request.json();
|
|
2290
|
+
} catch {
|
|
2291
|
+
return json({ error: "invalid body" }, 400);
|
|
2292
|
+
}
|
|
2293
|
+
const workspaceId = typeof body.workspaceId === "string" ? body.workspaceId : "";
|
|
2294
|
+
const taskId = typeof body.task === "string" && body.task !== "" ? body.task : TANK_TASK;
|
|
2295
|
+
if (workspaceId === "") return json({ error: "workspaceId is required" }, 400);
|
|
2296
|
+
return json(tasks.startEval(workspaceId, taskId));
|
|
2297
|
+
}
|
|
2298
|
+
});
|
|
2299
|
+
add({
|
|
2300
|
+
path: JOBS_ROUTE,
|
|
2301
|
+
methods: ["GET"],
|
|
2302
|
+
requestBody: "buffered",
|
|
2303
|
+
fetch: async (request) => {
|
|
2304
|
+
const workspaceId = query(request, "workspaceId");
|
|
2305
|
+
const taskId = query(request, "task");
|
|
2306
|
+
return json(tasks.jobs.list(workspaceId === "" ? void 0 : workspaceId, taskId === "" ? void 0 : taskId));
|
|
2307
|
+
}
|
|
2308
|
+
});
|
|
2309
|
+
add({
|
|
2310
|
+
path: JOB_ROUTE,
|
|
2311
|
+
methods: ["GET"],
|
|
2312
|
+
requestBody: "buffered",
|
|
2313
|
+
fetch: async (request) => {
|
|
2314
|
+
const jobId = query(request, "id");
|
|
2315
|
+
const job = tasks.jobs.get(jobId);
|
|
2316
|
+
return job === void 0 ? json({ error: "job not found" }, 404) : json(job);
|
|
2317
|
+
}
|
|
2318
|
+
});
|
|
2319
|
+
add({
|
|
2320
|
+
path: JOB_CANCEL_ROUTE,
|
|
2321
|
+
methods: ["POST"],
|
|
2322
|
+
requestBody: "buffered",
|
|
2323
|
+
fetch: async (request) => {
|
|
2324
|
+
let body = {};
|
|
2325
|
+
try {
|
|
2326
|
+
body = await request.json();
|
|
2327
|
+
} catch {
|
|
2328
|
+
return json({ error: "invalid body" }, 400);
|
|
2329
|
+
}
|
|
2330
|
+
const jobId = typeof body.id === "string" ? body.id : "";
|
|
2331
|
+
if (jobId === "") return json({ error: "id is required" }, 400);
|
|
2332
|
+
const job = tasks.jobs.cancel(jobId);
|
|
2333
|
+
return job === void 0 ? json({ error: "job not found" }, 404) : json(job);
|
|
1677
2334
|
}
|
|
1678
2335
|
});
|
|
1679
2336
|
add({
|