@xdxer/dingtalk-agent 0.1.4-beta.8 → 0.1.4-beta.9
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/CHANGELOG.md +35 -0
- package/README.en.md +395 -0
- package/README.md +466 -35
- package/dist/bin/dingtalk-agent.js +1058 -10
- package/dist/bin/dingtalk-agent.js.map +1 -1
- package/dist/src/actions.js +98 -14
- package/dist/src/actions.js.map +1 -1
- package/dist/src/agent-audit.js +460 -0
- package/dist/src/agent-audit.js.map +1 -0
- package/dist/src/agent-bindings.js +132 -0
- package/dist/src/agent-bindings.js.map +1 -0
- package/dist/src/agent-definition.js +182 -0
- package/dist/src/agent-definition.js.map +1 -0
- package/dist/src/agent-enhance.js +678 -0
- package/dist/src/agent-enhance.js.map +1 -0
- package/dist/src/bootstrap.js +125 -17
- package/dist/src/bootstrap.js.map +1 -1
- package/dist/src/development-workspace.js +729 -0
- package/dist/src/development-workspace.js.map +1 -0
- package/dist/src/dws.js +145 -0
- package/dist/src/dws.js.map +1 -1
- package/dist/src/eval-evidence.js +193 -0
- package/dist/src/eval-evidence.js.map +1 -0
- package/dist/src/invocation.js +36 -0
- package/dist/src/invocation.js.map +1 -0
- package/dist/src/lab.js +679 -0
- package/dist/src/lab.js.map +1 -0
- package/dist/src/lease.js +100 -0
- package/dist/src/lease.js.map +1 -0
- package/dist/src/memory/candidates.js +451 -0
- package/dist/src/memory/candidates.js.map +1 -0
- package/dist/src/memory/completion-evidence.js +536 -0
- package/dist/src/memory/completion-evidence.js.map +1 -0
- package/dist/src/memory/operational.js +263 -0
- package/dist/src/memory/operational.js.map +1 -0
- package/dist/src/memory/remote-state.js +478 -0
- package/dist/src/memory/remote-state.js.map +1 -0
- package/dist/src/memory/task-checkpoints.js +204 -0
- package/dist/src/memory/task-checkpoints.js.map +1 -0
- package/dist/src/multica-deploy.js +1480 -0
- package/dist/src/multica-deploy.js.map +1 -0
- package/dist/src/multica-provider.js +685 -0
- package/dist/src/multica-provider.js.map +1 -0
- package/dist/src/opencode-evals.js +1062 -0
- package/dist/src/opencode-evals.js.map +1 -0
- package/dist/src/opencode-provider.js +531 -0
- package/dist/src/opencode-provider.js.map +1 -0
- package/dist/src/opencode-workspace.js +197 -0
- package/dist/src/opencode-workspace.js.map +1 -0
- package/dist/src/perception.js +225 -0
- package/dist/src/perception.js.map +1 -0
- package/dist/src/personal-event-evals.js +595 -0
- package/dist/src/personal-event-evals.js.map +1 -0
- package/dist/src/promotion.js +786 -0
- package/dist/src/promotion.js.map +1 -0
- package/dist/src/remote-semantic-state-live-evals.js +888 -0
- package/dist/src/remote-semantic-state-live-evals.js.map +1 -0
- package/dist/src/remote-semantic-state-worker.js +38 -0
- package/dist/src/remote-semantic-state-worker.js.map +1 -0
- package/dist/src/remote-state-evals.js +501 -0
- package/dist/src/remote-state-evals.js.map +1 -0
- package/dist/src/response-gate.js +51 -0
- package/dist/src/response-gate.js.map +1 -0
- package/dist/src/robot-evals.js +770 -0
- package/dist/src/robot-evals.js.map +1 -0
- package/dist/src/sessions.js +66 -105
- package/dist/src/sessions.js.map +1 -1
- package/dist/src/skill-manager.js +25 -16
- package/dist/src/skill-manager.js.map +1 -1
- package/dist/src/skills.js.map +1 -1
- package/dist/src/storage-evals.js +26 -0
- package/dist/src/storage-evals.js.map +1 -0
- package/dist/src/types.js.map +1 -1
- package/dist/src/waits.js +5 -1
- package/dist/src/waits.js.map +1 -1
- package/dist/src/workspace.js +28 -3
- package/dist/src/workspace.js.map +1 -1
- package/docs/INSTALLATION.md +47 -0
- package/docs/SECOND-AGENT-ACCEPTANCE.md +62 -0
- package/docs/architecture/agent-memory-topology.png +0 -0
- package/docs/architecture/agent-memory-topology.svg +132 -0
- package/docs/architecture/general-agent-kernel-topology.png +0 -0
- package/docs/architecture/general-agent-kernel-topology.svg +149 -0
- package/docs/architecture/provider-bound-development-workspace.png +0 -0
- package/docs/architecture/provider-bound-development-workspace.svg +141 -0
- package/docs/architecture/task-completion-gate.png +0 -0
- package/docs/architecture/task-completion-gate.svg +191 -0
- package/docs/schemas/agent-audit-load-evidence.schema.json +14 -0
- package/docs/schemas/agent-audit.schema.json +92 -0
- package/docs/schemas/agent-bindings.schema.json +54 -0
- package/docs/schemas/agent-definition.schema.json +78 -0
- package/docs/schemas/agent-enhancement-plan.schema.json +88 -0
- package/docs/schemas/agent-enhancement-receipt.schema.json +37 -0
- package/docs/schemas/enriched-invocation.schema.json +46 -0
- package/docs/schemas/eval-candidate-plan.schema.json +28 -0
- package/docs/schemas/eval-candidate-result.schema.json +20 -0
- package/docs/schemas/eval-candidate.schema.json +30 -0
- package/docs/schemas/invocation.schema.json +19 -0
- package/docs/schemas/memory-candidate-proposal.schema.json +18 -0
- package/docs/schemas/memory-candidate.schema.json +76 -0
- package/docs/schemas/memory-publish-target.schema.json +25 -0
- package/docs/schemas/multica-deployment-list.schema.json +29 -0
- package/docs/schemas/multica-deployment-operation.schema.json +51 -0
- package/docs/schemas/multica-deployment-plan.schema.json +70 -0
- package/docs/schemas/multica-deployment-receipt.schema.json +87 -0
- package/docs/schemas/multica-deployment-status.schema.json +23 -0
- package/docs/schemas/multica-workspace-inspection.schema.json +77 -0
- package/docs/schemas/multica-workspace-plan.schema.json +68 -0
- package/docs/schemas/multica-workspace-resource-list.schema.json +27 -0
- package/docs/schemas/multica-workspace-status.schema.json +34 -0
- package/docs/schemas/observation.schema.json +21 -0
- package/docs/schemas/operational-memory-provider.schema.json +36 -0
- package/docs/schemas/operational-memory-record.schema.json +24 -0
- package/docs/schemas/perception-input.schema.json +56 -0
- package/docs/schemas/project.schema.json +112 -0
- package/docs/schemas/promotion-list.schema.json +36 -0
- package/docs/schemas/promotion-plan.schema.json +60 -0
- package/docs/schemas/promotion-policy.schema.json +29 -0
- package/docs/schemas/promotion-receipt.schema.json +43 -0
- package/docs/schemas/promotion-status.schema.json +23 -0
- package/docs/schemas/release-readiness.schema.json +65 -0
- package/docs/schemas/remote-semantic-state-live-eval.schema.json +60 -0
- package/docs/schemas/remote-semantic-state-manifest.schema.json +79 -0
- package/docs/schemas/remote-semantic-state-provider.schema.json +98 -0
- package/docs/schemas/response-gate.schema.json +20 -0
- package/docs/schemas/task-checkpoint.schema.json +71 -0
- package/docs/schemas/task-completion-evidence.schema.json +154 -0
- package/docs/schemas/workspace-doctor.schema.json +56 -0
- package/docs/schemas/workspace-state.schema.json +39 -0
- package/evals/baselines/2026-07-16/opencode-basic-010-completion-summary.json +123 -0
- package/evals/baselines/2026-07-16/opencode-basic-skill-required-summary.json +69 -0
- package/evals/baselines/2026-07-16/opencode-multi-surface-summary.json +63 -0
- package/evals/baselines/2026-07-16/remote-state-live-summary.json +70 -0
- package/evals/baselines/2026-07-17/agent-enhance-opencode-dogfood-summary.json +98 -0
- package/evals/baselines/2026-07-17/personal-event-live-readiness-summary.json +68 -0
- package/examples/agents/fde-coach/AGENTS.md +26 -0
- package/examples/agents/fde-coach/MEMORY.md +3 -0
- package/examples/agents/fde-coach/fields/default/field.json +24 -0
- package/examples/agents/fde-coach/knowledge/INDEX.md +4 -0
- package/examples/agents/fde-coach/skills/fde-coach/SKILL.md +13 -0
- package/examples/agents/release-manager/AGENTS.md +26 -0
- package/examples/agents/release-manager/MEMORY.md +3 -0
- package/examples/agents/release-manager/fields/default/field.json +24 -0
- package/examples/agents/release-manager/knowledge/INDEX.md +4 -0
- package/examples/agents/release-manager/skills/release-manager/SKILL.md +13 -0
- package/lab/README.md +109 -0
- package/lab/agent-eval/catalog.json +91 -0
- package/lab/agent-eval/classic-failures.json +177 -0
- package/lab/agent-eval/completion-gate-regression.json +99 -0
- package/lab/agent-eval/personal-event-live.example.json +94 -0
- package/lab/agent-eval/remote-semantic-state-live.example.json +70 -0
- package/lab/agent-eval/remote-semantic-state-provider.fixture.json +47 -0
- package/lab/agent-eval/remote-state-workspace/AGENTS.md +8 -0
- package/lab/agent-eval/remote-state-workspace/opencode.json +7 -0
- package/lab/agent-eval/remote-state-workspace/skills/remote-state-operator/SKILL.md +13 -0
- package/lab/agent-eval/remote-state.example.json +31 -0
- package/lab/agent-eval/workspace/AGENTS.md +7 -0
- package/lab/agent-eval/workspace/MEMORY.md +4 -0
- package/lab/agent-eval/workspace/artifacts/pending-review.md +3 -0
- package/lab/agent-eval/workspace/knowledge/INDEX.md +4 -0
- package/lab/agent-eval/workspace/opencode.json +20 -0
- package/lab/manifest.example.json +27 -0
- package/lab/manifest.personal-event.example.json +27 -0
- package/lab/project-workspace/README.md +11 -0
- package/lab/project-workspace/fake-multica-provider.mjs +266 -0
- package/lab/project-workspace/multica-deploy.fixture.json +29 -0
- package/lab/project-workspace/multica-readonly.fixture.json +69 -0
- package/lab/project-workspace/observation.fixture.json +14 -0
- package/lab/project-workspace/opencode-provider-suite.json +65 -0
- package/lab/project-workspace/project.fixture.json +44 -0
- package/lab/project-workspace/promotion-policy.fixture.json +15 -0
- package/lab/robot-eval/pool.example.json +30 -0
- package/lab/robot-eval/suite.json +123 -0
- package/lab/robot-eval/workspace/AGENTS.md +21 -0
- package/lab/robot-eval/workspace/MEMORY.md +3 -0
- package/lab/robot-eval/workspace/knowledge/INDEX.md +5 -0
- package/lab/robot-eval/workspace/opencode.json +22 -0
- package/lab/schemas/agent-eval-catalog.schema.json +47 -0
- package/lab/schemas/lab-manifest.schema.json +70 -0
- package/lab/schemas/personal-event-eval.schema.json +91 -0
- package/lab/schemas/remote-state-eval.schema.json +66 -0
- package/lab/schemas/robot-eval-suite.schema.json +184 -0
- package/lab/schemas/robot-pool.schema.json +56 -0
- package/package.json +21 -3
- package/skills/dingtalk-agent-boot-multica/SKILL.md +40 -0
- package/skills/dingtalk-agent-compose/SKILL.md +110 -0
- package/skills/dingtalk-agent-compose/assets/AGENTS.template.md +26 -0
- package/skills/dingtalk-agent-compose/assets/agent.bindings.dingtalk-doc.template.json +13 -0
- package/skills/dingtalk-agent-compose/assets/agent.bindings.local.template.json +13 -0
- package/skills/dingtalk-agent-compose/assets/opencode.template.json +12 -0
- package/skills/dingtalk-agent-compose/assets/role-skill.template.md +24 -0
- package/skills/dingtalk-agent-compose/evals/evals.json +94 -0
- package/skills/dingtalk-agent-compose/references/agent-definition-contract.md +55 -0
- package/skills/dingtalk-agent-compose/references/opencode-host-contract.md +65 -0
- package/skills/dingtalk-agent-compose/references/storage-routing.md +20 -0
- package/skills/dingtalk-agent-deploy/SKILL.md +60 -0
- package/skills/dingtalk-agent-deploy/references/multica-deployment-contract.md +49 -0
- package/skills/dingtalk-agent-deploy/references/promotion-observation-contract.md +49 -0
- package/skills/dingtalk-agent-eval/SKILL.md +116 -0
- package/skills/dingtalk-agent-eval/assets/eval-catalog.template.json +18 -0
- package/skills/dingtalk-agent-eval/evals/evals.json +61 -0
- package/skills/dingtalk-agent-eval/references/eval-topology.md +34 -0
- package/skills/dingtalk-agent-eval/references/evidence-contract.md +31 -0
- package/skills/dingtalk-agent-eval/references/scenario-taxonomy.md +25 -0
- package/skills/dingtalk-agent-eval/references/storage-modes.md +75 -0
- package/skills/dingtalk-basic-behavior/SKILL.md +63 -3
- package/skills/dingtalk-basic-behavior/assets/memory-candidate-proposal.json +10 -0
- package/skills/dingtalk-basic-behavior/assets/{task-checkpoint.md → task-checkpoint.json} +2 -21
- package/skills/dingtalk-basic-behavior/references/action-contract.md +2 -0
- package/skills/dingtalk-basic-behavior/references/memory-and-evolution.md +15 -1
- package/skills/dingtalk-basic-behavior/references/perception-and-gates.md +28 -0
- package/skills/dingtalk-basic-behavior/references/task-lifecycle.md +32 -7
|
@@ -0,0 +1,770 @@
|
|
|
1
|
+
// 可复用机器人评测:机器人是稳定资源,connector/workspace/run 是一次性测试实例。
|
|
2
|
+
// 所有真实钉钉动作仍只走 dws;本模块固定前缀、身份、模型、预算、marker、回读与 teardown。
|
|
3
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync, } from 'node:fs';
|
|
4
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
5
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
6
|
+
import { resolvePackageRoot } from './package-root.js';
|
|
7
|
+
import * as dws from './dws.js';
|
|
8
|
+
import { bindRequiredBasicSkill, relativeBinding, verifyRequiredBasicSkill, } from './opencode-workspace.js';
|
|
9
|
+
const sleep = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
10
|
+
export function runRobotEvaluation(root, poolFile, suiteFile, options = {}) {
|
|
11
|
+
const pool = loadRobotPool(root, poolFile);
|
|
12
|
+
const suite = loadRobotEvalSuite(root, suiteFile);
|
|
13
|
+
const selection = selectEvaluation(pool, suite, options.lanes || []);
|
|
14
|
+
const plan = {
|
|
15
|
+
suite: suite.id,
|
|
16
|
+
prefix: pool.prefix,
|
|
17
|
+
lanes: [...new Set(selection.cases.map((item) => item.lane))],
|
|
18
|
+
robots: selection.slots.map((slot) => ({
|
|
19
|
+
id: slot.id, name: slot.name, lane: slot.lane,
|
|
20
|
+
appName: slot.appName,
|
|
21
|
+
channel: slot.channel, model: slot.model, agentMemory: slot.agentMemory,
|
|
22
|
+
workspaceTemplate: slot.workspaceTemplate,
|
|
23
|
+
})),
|
|
24
|
+
cases: selection.cases.map((item) => ({
|
|
25
|
+
id: item.id, category: item.category, lane: item.lane,
|
|
26
|
+
scenario: item.scenario || null, risk: item.risk || null,
|
|
27
|
+
kind: item.kind || 'behavior', capability: item.capability || 'unspecified',
|
|
28
|
+
robot: selection.assignments.get(item.id).name,
|
|
29
|
+
continuationOf: item.continuationOf || null,
|
|
30
|
+
})),
|
|
31
|
+
hostContract: {
|
|
32
|
+
host: 'opencode',
|
|
33
|
+
requiredSkill: 'dingtalk-basic-behavior',
|
|
34
|
+
loadStrategy: 'opencode.json#instructions',
|
|
35
|
+
preflightBeforeMessage: true,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
if (!options.live) {
|
|
39
|
+
return {
|
|
40
|
+
$schema: 'dingtalk-agent/robot-eval-plan@1', ready: true,
|
|
41
|
+
sideEffect: false, liveRequired: true, plan,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (!options.yes) {
|
|
45
|
+
throw new Error('Robot Eval 会启动 connector 并以当前用户身份真实发消息;必须同时传 --live --yes');
|
|
46
|
+
}
|
|
47
|
+
const runId = `robot_eval_${Date.now()}_${randomUUID().slice(0, 8)}`;
|
|
48
|
+
const evidencePath = safeInside(root, join(suite.evidenceRoot, runId), false);
|
|
49
|
+
mkdirSync(evidencePath, { recursive: true });
|
|
50
|
+
writeJson(join(evidencePath, 'suite.json'), suite);
|
|
51
|
+
writeJson(join(evidencePath, 'pool.redacted.json'), redactPool(pool));
|
|
52
|
+
writeJson(join(evidencePath, 'plan.json'), plan);
|
|
53
|
+
const workspaces = new Map();
|
|
54
|
+
const connectorReports = [];
|
|
55
|
+
const teardownReports = [];
|
|
56
|
+
const started = [];
|
|
57
|
+
const results = [];
|
|
58
|
+
let fatal = null;
|
|
59
|
+
try {
|
|
60
|
+
// 所有需要的 connector 先启动完成,最多 3 个可同时保持在线;随后才开始发送题目。
|
|
61
|
+
for (const slot of selection.slots) {
|
|
62
|
+
const workspace = materializeWorkspace(root, runId, slot);
|
|
63
|
+
workspaces.set(slot.id, workspace);
|
|
64
|
+
const preflight = process.env.DTA_LAB_FAKE_DWS === '1'
|
|
65
|
+
? {
|
|
66
|
+
...relativeBinding(workspace.binding),
|
|
67
|
+
verification: 'static-contract',
|
|
68
|
+
packageSourceHash: workspace.packageSourceHash,
|
|
69
|
+
}
|
|
70
|
+
: {
|
|
71
|
+
...relativeBinding(verifyRequiredBasicSkill(workspace.binding)),
|
|
72
|
+
verification: 'opencode-resolved',
|
|
73
|
+
packageSourceHash: workspace.packageSourceHash,
|
|
74
|
+
};
|
|
75
|
+
writeJson(join(evidencePath, 'workspaces', `${safeName(slot.id)}.json`), preflight);
|
|
76
|
+
const report = startConnector(pool, slot, workspace.path, preflight);
|
|
77
|
+
connectorReports.push(report);
|
|
78
|
+
started.push(slot);
|
|
79
|
+
}
|
|
80
|
+
writeJson(join(evidencePath, 'connectors.json'), connectorReports);
|
|
81
|
+
const orderedCases = [
|
|
82
|
+
...selection.cases.filter((item) => item.kind === 'load-probe'),
|
|
83
|
+
...selection.cases.filter((item) => item.kind !== 'load-probe'),
|
|
84
|
+
];
|
|
85
|
+
for (const testCase of orderedCases) {
|
|
86
|
+
const slot = selection.assignments.get(testCase.id);
|
|
87
|
+
const result = runCase(pool, suite, testCase, slot, evidencePath);
|
|
88
|
+
results.push(result);
|
|
89
|
+
if (testCase.kind === 'load-probe' && !result.passed) {
|
|
90
|
+
throw new Error(`Basic Skill load probe 失败: case=${testCase.id};后续行为题已中止`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
fatal = error;
|
|
96
|
+
writeJson(join(evidencePath, 'fatal.json'), {
|
|
97
|
+
message: fatal.message, at: new Date().toISOString(),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
for (const slot of [...started].reverse()) {
|
|
102
|
+
teardownReports.push(stopConnector(pool, slot));
|
|
103
|
+
}
|
|
104
|
+
writeJson(join(evidencePath, 'teardown.json'), {
|
|
105
|
+
status: 'complete', connectors: teardownReports,
|
|
106
|
+
stoppedOnly: started.map((slot) => slot.name), completedAt: new Date().toISOString(),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
const passedCases = results.filter((item) => item.passed).length;
|
|
110
|
+
const selectedLoadProbes = selection.cases.filter((item) => item.kind === 'load-probe');
|
|
111
|
+
const evaluatedLoadProbes = results.filter((item) => item.kind === 'load-probe');
|
|
112
|
+
const loadGate = evaluatedLoadProbes.length === selectedLoadProbes.length &&
|
|
113
|
+
evaluatedLoadProbes.every((item) => item.passed);
|
|
114
|
+
const capabilitySummary = Object.fromEntries([...new Set(results.map((item) => item.capability))].map((capability) => {
|
|
115
|
+
const capabilityResults = results.filter((item) => item.capability === capability);
|
|
116
|
+
return [capability, {
|
|
117
|
+
total: capabilityResults.length,
|
|
118
|
+
passed: capabilityResults.filter((item) => item.passed).length,
|
|
119
|
+
}];
|
|
120
|
+
}));
|
|
121
|
+
const report = {
|
|
122
|
+
$schema: 'dingtalk-agent/robot-eval-report@1',
|
|
123
|
+
passed: !fatal && passedCases === selection.cases.length,
|
|
124
|
+
sideEffect: true,
|
|
125
|
+
runId,
|
|
126
|
+
evidencePath: relative(root, evidencePath).split('\\').join('/'),
|
|
127
|
+
plan,
|
|
128
|
+
summary: {
|
|
129
|
+
total: selection.cases.length,
|
|
130
|
+
passed: passedCases,
|
|
131
|
+
failed: selection.cases.length - passedCases,
|
|
132
|
+
platformVisible: results.filter((item) => item.platformVisible).length,
|
|
133
|
+
exactAttributed: results.filter((item) => item.exactAttributed).length,
|
|
134
|
+
loadGate,
|
|
135
|
+
capabilities: capabilitySummary,
|
|
136
|
+
manualReview: 'not-collected',
|
|
137
|
+
},
|
|
138
|
+
results,
|
|
139
|
+
fatal: fatal?.message || null,
|
|
140
|
+
teardown: teardownReports.map((item) => ({ slot: item.slot, stopped: item.stopped })),
|
|
141
|
+
};
|
|
142
|
+
writeJson(join(evidencePath, 'report.json'), report);
|
|
143
|
+
return report;
|
|
144
|
+
}
|
|
145
|
+
export function loadRobotPool(root, file) {
|
|
146
|
+
const value = readJsonFile(safeInside(root, file, true), 'Robot Pool');
|
|
147
|
+
return validatePool(root, value);
|
|
148
|
+
}
|
|
149
|
+
export function loadRobotEvalSuite(root, file) {
|
|
150
|
+
const value = readJsonFile(safeInside(root, file, true), 'Robot Eval Suite');
|
|
151
|
+
return validateSuite(root, value);
|
|
152
|
+
}
|
|
153
|
+
function selectEvaluation(pool, suite, lanes) {
|
|
154
|
+
const requested = lanes.length ? new Set(lanes) : new Set(suite.cases.map((item) => item.lane));
|
|
155
|
+
const cases = suite.cases.filter((item) => requested.has(item.lane));
|
|
156
|
+
if (!cases.length)
|
|
157
|
+
throw new Error('Robot Eval 选择后没有 case');
|
|
158
|
+
const localOnly = cases.filter((item) => item.execution?.host === 'opencode-only');
|
|
159
|
+
if (localOnly.length) {
|
|
160
|
+
throw new Error(`Robot Eval 不执行 opencode-only case: ${localOnly.map((item) => item.id).join(',')}`);
|
|
161
|
+
}
|
|
162
|
+
if (cases.length > pool.budgets.maxCases) {
|
|
163
|
+
throw new Error(`Robot Eval case 超过预算: ${cases.length}/${pool.budgets.maxCases}`);
|
|
164
|
+
}
|
|
165
|
+
const assignments = new Map();
|
|
166
|
+
const selected = new Map();
|
|
167
|
+
for (const testCase of cases) {
|
|
168
|
+
const matches = pool.slots.filter((slot) => slot.lane === testCase.lane);
|
|
169
|
+
if (matches.length !== 1) {
|
|
170
|
+
throw new Error(`lane=${testCase.lane} 必须恰好绑定一个机器人,当前 ${matches.length} 个`);
|
|
171
|
+
}
|
|
172
|
+
assignments.set(testCase.id, matches[0]);
|
|
173
|
+
selected.set(matches[0].id, matches[0]);
|
|
174
|
+
}
|
|
175
|
+
const slots = [...selected.values()];
|
|
176
|
+
if (slots.length > pool.budgets.maxRobots || slots.length > 3) {
|
|
177
|
+
throw new Error(`Robot Eval 同时机器人超过上限: ${slots.length}`);
|
|
178
|
+
}
|
|
179
|
+
return { cases, slots, assignments };
|
|
180
|
+
}
|
|
181
|
+
function startConnector(pool, slot, workspace, basicSkillLoad) {
|
|
182
|
+
const app = dws.run([
|
|
183
|
+
'dev', 'app', 'get', '--unified-app-id', slot.unifiedAppId,
|
|
184
|
+
...profileArgs(pool.dws.profile),
|
|
185
|
+
], { retries: 0 });
|
|
186
|
+
const appJson = JSON.stringify(app);
|
|
187
|
+
if (!appJson.includes(slot.unifiedAppId) || !appJson.includes(slot.appName)) {
|
|
188
|
+
throw new Error(`机器人 ${slot.name} 的 unifiedAppId 无法按应用详情回读`);
|
|
189
|
+
}
|
|
190
|
+
const bot = dws.run([
|
|
191
|
+
'chat', 'bot', 'find', '--query', slot.name, '--limit', '20',
|
|
192
|
+
...profileArgs(pool.dws.profile),
|
|
193
|
+
], { retries: 0 });
|
|
194
|
+
const botJson = JSON.stringify(bot);
|
|
195
|
+
if (!botJson.includes(slot.name) || !botJson.includes(slot.botOpenDingTalkId)) {
|
|
196
|
+
throw new Error(`机器人 ${slot.name} 的 openDingTalkId 无法按名称回读`);
|
|
197
|
+
}
|
|
198
|
+
const before = dws.run([
|
|
199
|
+
'dev', 'connect', 'status', '--robot-client-id', slot.robotClientId, '--json',
|
|
200
|
+
...profileArgs(pool.dws.profile),
|
|
201
|
+
], { allowFail: true, retries: 0 });
|
|
202
|
+
if (connectorHealthy(before)) {
|
|
203
|
+
throw new Error(`机器人 ${slot.name} 已有未知配置的 healthy connector;先显式停止,避免复用错误模型/工作区`);
|
|
204
|
+
}
|
|
205
|
+
let owned = false;
|
|
206
|
+
try {
|
|
207
|
+
const start = dws.startConnectDaemon([
|
|
208
|
+
'dev', 'connect', '--unified-app-id', slot.unifiedAppId,
|
|
209
|
+
'--channel', slot.channel,
|
|
210
|
+
'--agent-model', slot.model,
|
|
211
|
+
'--agent-workdir', workspace,
|
|
212
|
+
'--allowed-users', pool.dws.expectedUserId,
|
|
213
|
+
'--agent-permission-mode', 'ask',
|
|
214
|
+
'--agent-approval-mode', 'ask',
|
|
215
|
+
`--agent-memory=${String(slot.agentMemory)}`,
|
|
216
|
+
'--reply-card=false',
|
|
217
|
+
'--user-rate-limit', '10',
|
|
218
|
+
'--agent-timeout', String(pool.budgets.maxWaitSeconds),
|
|
219
|
+
'--daemon', '--yes',
|
|
220
|
+
...profileArgs(pool.dws.profile),
|
|
221
|
+
], { timeout: 60_000 });
|
|
222
|
+
owned = true;
|
|
223
|
+
const deadline = Date.now() + 30_000;
|
|
224
|
+
let status = null;
|
|
225
|
+
while (Date.now() < deadline) {
|
|
226
|
+
status = dws.run([
|
|
227
|
+
'dev', 'connect', 'status', '--robot-client-id', slot.robotClientId, '--json',
|
|
228
|
+
...profileArgs(pool.dws.profile),
|
|
229
|
+
], { allowFail: true, retries: 0 });
|
|
230
|
+
if (connectorHealthy(status))
|
|
231
|
+
break;
|
|
232
|
+
sleep(1000);
|
|
233
|
+
}
|
|
234
|
+
if (!connectorHealthy(status))
|
|
235
|
+
throw new Error(`机器人 ${slot.name} connector 未进入 healthy`);
|
|
236
|
+
return {
|
|
237
|
+
slot: slot.name, lane: slot.lane, channel: slot.channel, model: slot.model,
|
|
238
|
+
agentMemory: slot.agentMemory, workspace, started: Boolean(start), healthy: true,
|
|
239
|
+
basicSkillLoad,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
// daemon 进程一旦由本次 Run 创建,就归本次 Run 所有;即使健康检查失败也要精确回收。
|
|
244
|
+
if (owned) {
|
|
245
|
+
stopConnector(pool, slot);
|
|
246
|
+
}
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function stopConnector(pool, slot) {
|
|
251
|
+
let command = null;
|
|
252
|
+
let error = '';
|
|
253
|
+
try {
|
|
254
|
+
command = dws.stopConnectDaemon([
|
|
255
|
+
'dev', 'connect', 'stop', '--robot-client-id', slot.robotClientId,
|
|
256
|
+
'--yes', ...profileArgs(pool.dws.profile),
|
|
257
|
+
]);
|
|
258
|
+
}
|
|
259
|
+
catch (caught) {
|
|
260
|
+
error = String(caught.message || caught);
|
|
261
|
+
}
|
|
262
|
+
const deadline = Date.now() + 10_000;
|
|
263
|
+
let status = null;
|
|
264
|
+
while (Date.now() < deadline) {
|
|
265
|
+
status = dws.run([
|
|
266
|
+
'dev', 'connect', 'status', '--robot-client-id', slot.robotClientId, '--json',
|
|
267
|
+
...profileArgs(pool.dws.profile),
|
|
268
|
+
], { allowFail: true, retries: 0 });
|
|
269
|
+
if (connectorNotRunning(status))
|
|
270
|
+
break;
|
|
271
|
+
sleep(500);
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
slot: slot.name,
|
|
275
|
+
stopped: connectorNotRunning(status),
|
|
276
|
+
commandAccepted: Boolean(command),
|
|
277
|
+
status,
|
|
278
|
+
error: error || null,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function runCase(pool, suite, testCase, slot, evidenceRoot) {
|
|
282
|
+
const uuid = randomUUID();
|
|
283
|
+
const marker = `${suite.markerPrefix}${safeName(suite.id)}-${safeName(testCase.id)}-${uuid}]`;
|
|
284
|
+
const outbound = `${marker}\n评测协议:回复第一行原样保留上面的 marker,不要解释评测协议。\n${testCase.prompt}`;
|
|
285
|
+
if (outbound.length > pool.budgets.maxTextCharacters) {
|
|
286
|
+
throw new Error(`case=${testCase.id} 超过文本预算 ${pool.budgets.maxTextCharacters}`);
|
|
287
|
+
}
|
|
288
|
+
const casePath = join(evidenceRoot, 'cases', safeName(testCase.id));
|
|
289
|
+
mkdirSync(casePath, { recursive: true });
|
|
290
|
+
const startedAt = Date.now();
|
|
291
|
+
writeJson(join(casePath, 'request.json'), {
|
|
292
|
+
case: testCase, robot: slot.name, marker, outbound, uuid, startedAt: new Date(startedAt).toISOString(),
|
|
293
|
+
});
|
|
294
|
+
let send = null;
|
|
295
|
+
let readback = null;
|
|
296
|
+
let reply = '';
|
|
297
|
+
let candidateReply = '';
|
|
298
|
+
let error = '';
|
|
299
|
+
try {
|
|
300
|
+
send = dws.run([
|
|
301
|
+
'chat', 'message', 'send', '--open-dingtalk-id', slot.botOpenDingTalkId,
|
|
302
|
+
'--text', outbound, '--uuid', uuid, '--yes', ...profileArgs(pool.dws.profile),
|
|
303
|
+
], { retries: 0 });
|
|
304
|
+
const deadline = Date.now() + pool.budgets.maxWaitSeconds * 1000;
|
|
305
|
+
while (Date.now() < deadline) {
|
|
306
|
+
readback = dws.run([
|
|
307
|
+
'chat', 'message', 'list', '--open-dingtalk-id', slot.botOpenDingTalkId,
|
|
308
|
+
'--time', formatLocalTime(new Date(startedAt - 10_000)),
|
|
309
|
+
'--direction', 'newer', '--limit', '100', ...profileArgs(pool.dws.profile),
|
|
310
|
+
], { retries: 0 });
|
|
311
|
+
reply = extractReply(readback, marker, outbound);
|
|
312
|
+
if (reply)
|
|
313
|
+
break;
|
|
314
|
+
candidateReply = extractCandidateReply(readback, uuid, outbound);
|
|
315
|
+
if (candidateReply) {
|
|
316
|
+
error = '平台已回收到同 UUID 回复,但 marker 被改写,不能作为本 case 的精确归因证据';
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
sleep(3000);
|
|
320
|
+
}
|
|
321
|
+
if (!reply && !candidateReply) {
|
|
322
|
+
error = `等待 ${pool.budgets.maxWaitSeconds}s 未回收到带 marker 的机器人回复`;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
catch (caught) {
|
|
326
|
+
error = String(caught.message || caught);
|
|
327
|
+
}
|
|
328
|
+
const answer = reply
|
|
329
|
+
? reply.replace(marker, '').trim()
|
|
330
|
+
: stripEvalMarker(candidateReply);
|
|
331
|
+
const grading = gradeAnswer(answer, testCase.expectations);
|
|
332
|
+
const platformVisible = Boolean(reply || candidateReply);
|
|
333
|
+
const exactAttributed = Boolean(reply);
|
|
334
|
+
const attribution = exactAttributed ? 'exact-marker' : candidateReply ? 'marker-mismatch' : 'missing';
|
|
335
|
+
const passed = exactAttributed && grading.every((item) => item.passed);
|
|
336
|
+
const result = {
|
|
337
|
+
id: testCase.id,
|
|
338
|
+
category: testCase.category,
|
|
339
|
+
lane: testCase.lane,
|
|
340
|
+
kind: testCase.kind || 'behavior',
|
|
341
|
+
capability: testCase.capability || 'unspecified',
|
|
342
|
+
robot: slot.name,
|
|
343
|
+
marker,
|
|
344
|
+
platformVisible,
|
|
345
|
+
exactAttributed,
|
|
346
|
+
attribution,
|
|
347
|
+
passed,
|
|
348
|
+
reply: answer,
|
|
349
|
+
grading,
|
|
350
|
+
manualReview: testCase.manualChecks.map((text) => ({ text, status: 'not-collected' })),
|
|
351
|
+
error: error || null,
|
|
352
|
+
durationMs: Date.now() - startedAt,
|
|
353
|
+
};
|
|
354
|
+
writeJson(join(casePath, 'send.json'), send || { status: 'not-sent', error });
|
|
355
|
+
writeJson(join(casePath, 'platform-readback.json'), readback || { status: 'unverified', error });
|
|
356
|
+
writeJson(join(casePath, 'result.json'), result);
|
|
357
|
+
return result;
|
|
358
|
+
}
|
|
359
|
+
export function gradeAnswer(answer, expectations) {
|
|
360
|
+
const normalized = answer.toLocaleLowerCase();
|
|
361
|
+
return expectations.map((expectation) => {
|
|
362
|
+
if (expectation.type === 'equals') {
|
|
363
|
+
return {
|
|
364
|
+
id: expectation.id,
|
|
365
|
+
passed: answer.trim() === expectation.value,
|
|
366
|
+
detail: `equals: actual=${JSON.stringify(answer.trim())}, expected=${JSON.stringify(expectation.value)}`,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
if (expectation.type === 'maxQuestions') {
|
|
370
|
+
const count = (answer.match(/[??]/g) || []).length;
|
|
371
|
+
return {
|
|
372
|
+
id: expectation.id, passed: count <= expectation.max,
|
|
373
|
+
detail: `questions=${count}, max=${expectation.max}`,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
if (expectation.type === 'matches' || expectation.type === 'notMatches') {
|
|
377
|
+
const regex = new RegExp(expectation.pattern, expectation.flags || 'u');
|
|
378
|
+
const matched = regex.test(answer);
|
|
379
|
+
return {
|
|
380
|
+
id: expectation.id,
|
|
381
|
+
passed: expectation.type === 'matches' ? matched : !matched,
|
|
382
|
+
detail: `${expectation.type}: pattern=${JSON.stringify(expectation.pattern)}, matched=${matched}`,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
const textExpectation = expectation;
|
|
386
|
+
const values = textExpectation.values.map((value) => value.toLocaleLowerCase());
|
|
387
|
+
const hits = values.filter((value) => normalized.includes(value));
|
|
388
|
+
const passed = textExpectation.type === 'includesAny'
|
|
389
|
+
? hits.length > 0
|
|
390
|
+
: textExpectation.type === 'includesAll'
|
|
391
|
+
? hits.length === values.length
|
|
392
|
+
: hits.length === 0;
|
|
393
|
+
return {
|
|
394
|
+
id: textExpectation.id, passed,
|
|
395
|
+
detail: `${textExpectation.type}: hits=${JSON.stringify(hits)}, expected=${JSON.stringify(textExpectation.values)}`,
|
|
396
|
+
};
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
function materializeWorkspace(root, runId, slot) {
|
|
400
|
+
const source = safeInside(root, slot.workspaceTemplate, true);
|
|
401
|
+
const destination = safeInside(root, join('.dingtalk-agent', 'robot-eval-workspaces', runId, safeName(slot.id)), false);
|
|
402
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
403
|
+
cpSync(source, destination, { recursive: true, errorOnExist: true });
|
|
404
|
+
const packageRoot = resolvePackageRoot(import.meta.url);
|
|
405
|
+
const basicSource = join(packageRoot, 'skills', 'dingtalk-basic-behavior');
|
|
406
|
+
const basicTarget = join(destination, '.agents', 'skills', 'dingtalk-basic-behavior');
|
|
407
|
+
mkdirSync(dirname(basicTarget), { recursive: true });
|
|
408
|
+
cpSync(basicSource, basicTarget, { recursive: true, errorOnExist: true });
|
|
409
|
+
const binding = bindRequiredBasicSkill(destination);
|
|
410
|
+
const packageSourceHash = createHash('sha256')
|
|
411
|
+
.update(readFileSync(join(basicSource, 'SKILL.md'), 'utf8')).digest('hex');
|
|
412
|
+
if (packageSourceHash !== binding.skill.hash) {
|
|
413
|
+
throw new Error('Robot Eval 物化后的 Basic Skill 与当前 package source hash 不一致');
|
|
414
|
+
}
|
|
415
|
+
return { path: destination, binding, packageSourceHash };
|
|
416
|
+
}
|
|
417
|
+
function validatePool(root, value) {
|
|
418
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
419
|
+
throw new Error('Robot Pool 必须是 JSON object');
|
|
420
|
+
}
|
|
421
|
+
const input = value;
|
|
422
|
+
if (input.$schema !== 'dingtalk-agent/robot-pool@1')
|
|
423
|
+
throw new Error('Robot Pool schema 非法');
|
|
424
|
+
if (!String(input.prefix || '').trim())
|
|
425
|
+
throw new Error('Robot Pool 缺少 prefix');
|
|
426
|
+
if (!input.dws || typeof input.dws.profile !== 'string' || !String(input.dws.expectedUserId || '').trim()) {
|
|
427
|
+
throw new Error('Robot Pool dws 必须固定 profile 与 expectedUserId');
|
|
428
|
+
}
|
|
429
|
+
const budget = input.budgets || {};
|
|
430
|
+
if (!Number.isInteger(budget.maxRobots) || budget.maxRobots < 1 || budget.maxRobots > 3 ||
|
|
431
|
+
!Number.isInteger(budget.maxCases) || budget.maxCases < 1 || budget.maxCases > 30 ||
|
|
432
|
+
!Number.isInteger(budget.maxTextCharacters) || budget.maxTextCharacters < 1 || budget.maxTextCharacters > 4000 ||
|
|
433
|
+
!Number.isInteger(budget.maxWaitSeconds) || budget.maxWaitSeconds < 10 || budget.maxWaitSeconds > 300) {
|
|
434
|
+
throw new Error('Robot Pool budgets 非法(robots 1..3 / cases 1..30 / text 1..4000 / wait 10..300)');
|
|
435
|
+
}
|
|
436
|
+
if (!Array.isArray(input.slots) || input.slots.length < 1 || input.slots.length > 3) {
|
|
437
|
+
throw new Error('Robot Pool slots 必须为 1..3 个');
|
|
438
|
+
}
|
|
439
|
+
const seen = {
|
|
440
|
+
id: new Set(), name: new Set(), app: new Set(), task: new Set(),
|
|
441
|
+
client: new Set(), bot: new Set(), lane: new Set(),
|
|
442
|
+
};
|
|
443
|
+
for (const slot of input.slots) {
|
|
444
|
+
for (const key of ['id', 'appName', 'name', 'lane', 'unifiedAppId', 'robotClientId', 'botOpenDingTalkId', 'workspaceTemplate', 'channel', 'model']) {
|
|
445
|
+
if (!String(slot?.[key] || '').trim())
|
|
446
|
+
throw new Error(`Robot Pool slot 缺少 ${key}`);
|
|
447
|
+
}
|
|
448
|
+
if (!slot.name.startsWith(input.prefix))
|
|
449
|
+
throw new Error(`机器人名不符合 prefix: ${slot.name}`);
|
|
450
|
+
if (!['stateless', 'stateful', 'role'].includes(slot.lane))
|
|
451
|
+
throw new Error(`机器人 lane 非法: ${slot.lane}`);
|
|
452
|
+
if (slot.channel !== 'opencode')
|
|
453
|
+
throw new Error(`Robot Eval 只允许 opencode: ${slot.channel}`);
|
|
454
|
+
if (!slot.model.startsWith('deepseek/'))
|
|
455
|
+
throw new Error(`Robot Eval 模型必须是 deepseek/*: ${slot.model}`);
|
|
456
|
+
if (typeof slot.agentMemory !== 'boolean')
|
|
457
|
+
throw new Error(`机器人 ${slot.name} 缺少 agentMemory boolean`);
|
|
458
|
+
if (slot.lane === 'stateful' && slot.agentMemory !== true)
|
|
459
|
+
throw new Error('stateful lane 必须 agentMemory=true');
|
|
460
|
+
if (slot.lane === 'stateless' && slot.agentMemory !== false)
|
|
461
|
+
throw new Error('stateless lane 必须 agentMemory=false');
|
|
462
|
+
const uniqueKeys = [
|
|
463
|
+
['id', slot.id], ['name', slot.name],
|
|
464
|
+
['app', slot.unifiedAppId],
|
|
465
|
+
...(slot.creationTaskId ? [['task', slot.creationTaskId]] : []),
|
|
466
|
+
['client', slot.robotClientId], ['bot', slot.botOpenDingTalkId], ['lane', slot.lane],
|
|
467
|
+
];
|
|
468
|
+
for (const [kind, key] of uniqueKeys) {
|
|
469
|
+
if (seen[kind].has(key))
|
|
470
|
+
throw new Error(`Robot Pool ${kind} 重复: ${key}`);
|
|
471
|
+
seen[kind].add(key);
|
|
472
|
+
}
|
|
473
|
+
safeInside(root, slot.workspaceTemplate, true);
|
|
474
|
+
}
|
|
475
|
+
return input;
|
|
476
|
+
}
|
|
477
|
+
function validateSuite(root, value) {
|
|
478
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
479
|
+
throw new Error('Robot Eval Suite 必须是 JSON object');
|
|
480
|
+
}
|
|
481
|
+
const input = value;
|
|
482
|
+
if (input.$schema !== 'dingtalk-agent/robot-eval-suite@1')
|
|
483
|
+
throw new Error('Robot Eval Suite schema 非法');
|
|
484
|
+
if (!String(input.id || '').trim() || !String(input.markerPrefix || '').startsWith('[DTA-EVAL-')) {
|
|
485
|
+
throw new Error('Robot Eval Suite 缺少 id 或合法 markerPrefix');
|
|
486
|
+
}
|
|
487
|
+
if (!String(input.evidenceRoot || '').trim())
|
|
488
|
+
throw new Error('Robot Eval Suite 缺少 evidenceRoot');
|
|
489
|
+
safeInside(root, input.evidenceRoot, false);
|
|
490
|
+
if (input.comparisonPolicy !== undefined) {
|
|
491
|
+
const policy = input.comparisonPolicy;
|
|
492
|
+
const keys = policy && typeof policy === 'object' && !Array.isArray(policy)
|
|
493
|
+
? Object.keys(policy).sort()
|
|
494
|
+
: [];
|
|
495
|
+
const expectedKeys = [
|
|
496
|
+
'maximumRegressedCases', 'minimumImprovedCases',
|
|
497
|
+
'minimumOverallPassRateDelta', 'minimumRunsPerConfiguration',
|
|
498
|
+
];
|
|
499
|
+
if (JSON.stringify(keys) !== JSON.stringify(expectedKeys) ||
|
|
500
|
+
!Number.isInteger(policy.minimumRunsPerConfiguration) ||
|
|
501
|
+
policy.minimumRunsPerConfiguration < 3 || policy.minimumRunsPerConfiguration > 10 ||
|
|
502
|
+
typeof policy.minimumOverallPassRateDelta !== 'number' ||
|
|
503
|
+
policy.minimumOverallPassRateDelta <= 0 || policy.minimumOverallPassRateDelta > 1 ||
|
|
504
|
+
!Number.isInteger(policy.minimumImprovedCases) || policy.minimumImprovedCases < 1 ||
|
|
505
|
+
!Number.isInteger(policy.maximumRegressedCases) || policy.maximumRegressedCases < 0) {
|
|
506
|
+
throw new Error('Robot Eval Suite comparisonPolicy 非法');
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (!Array.isArray(input.cases) || input.cases.length < 1 || input.cases.length > 30) {
|
|
510
|
+
throw new Error('Robot Eval Suite cases 必须为 1..30 个');
|
|
511
|
+
}
|
|
512
|
+
const ids = new Set();
|
|
513
|
+
for (const testCase of input.cases) {
|
|
514
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/.test(testCase?.id || ''))
|
|
515
|
+
throw new Error(`case id 非法: ${testCase?.id}`);
|
|
516
|
+
if (ids.has(testCase.id))
|
|
517
|
+
throw new Error(`case id 重复: ${testCase.id}`);
|
|
518
|
+
ids.add(testCase.id);
|
|
519
|
+
if (!['stateless', 'stateful', 'role'].includes(testCase.lane) || !String(testCase.prompt || '').trim()) {
|
|
520
|
+
throw new Error(`case=${testCase.id} 缺少合法 lane/prompt`);
|
|
521
|
+
}
|
|
522
|
+
if (testCase.kind !== undefined && !['load-probe', 'behavior'].includes(testCase.kind)) {
|
|
523
|
+
throw new Error(`case=${testCase.id} kind 非法`);
|
|
524
|
+
}
|
|
525
|
+
if (testCase.capability !== undefined &&
|
|
526
|
+
!['agent-body', 'basic-skill', 'role-skill', 'session-memory', 'knowledge',
|
|
527
|
+
'workspace', 'artifact', 'platform'].includes(testCase.capability)) {
|
|
528
|
+
throw new Error(`case=${testCase.id} capability 非法`);
|
|
529
|
+
}
|
|
530
|
+
if (testCase.completionClaim !== undefined &&
|
|
531
|
+
!['forbidden', 'permitted'].includes(testCase.completionClaim)) {
|
|
532
|
+
throw new Error(`case=${testCase.id} completionClaim 非法`);
|
|
533
|
+
}
|
|
534
|
+
if (testCase.scenario !== undefined &&
|
|
535
|
+
!['host-loading', 'conversation-contract', 'authority-and-egress',
|
|
536
|
+
'task-lifecycle', 'memory-and-storage', 'workspace-and-artifacts',
|
|
537
|
+
'platform-reliability'].includes(testCase.scenario)) {
|
|
538
|
+
throw new Error(`case=${testCase.id} scenario 非法`);
|
|
539
|
+
}
|
|
540
|
+
if (testCase.risk !== undefined && !['safety', 'reliability', 'quality'].includes(testCase.risk)) {
|
|
541
|
+
throw new Error(`case=${testCase.id} risk 非法`);
|
|
542
|
+
}
|
|
543
|
+
if (testCase.kind === 'load-probe' && testCase.capability !== 'basic-skill') {
|
|
544
|
+
throw new Error(`case=${testCase.id} load-probe 必须归类为 basic-skill`);
|
|
545
|
+
}
|
|
546
|
+
if (!Array.isArray(testCase.expectations) || !testCase.expectations.length ||
|
|
547
|
+
!Array.isArray(testCase.manualChecks)) {
|
|
548
|
+
throw new Error(`case=${testCase.id} 缺少 expectations/manualChecks`);
|
|
549
|
+
}
|
|
550
|
+
if (testCase.execution !== undefined &&
|
|
551
|
+
(testCase.execution.host !== 'opencode-only' ||
|
|
552
|
+
!['none', 'workspace-read', 'workspace-write',
|
|
553
|
+
'workspace-write-verify'].includes(testCase.execution.tools))) {
|
|
554
|
+
throw new Error(`case=${testCase.id} execution 非法`);
|
|
555
|
+
}
|
|
556
|
+
if (testCase.execution?.tools !== undefined && testCase.execution.tools !== 'none' &&
|
|
557
|
+
(!Array.isArray(testCase.postconditions) || !testCase.postconditions.length)) {
|
|
558
|
+
throw new Error(`case=${testCase.id} 文件工具模式必须声明 postconditions`);
|
|
559
|
+
}
|
|
560
|
+
if (testCase.postconditions !== undefined) {
|
|
561
|
+
if (!Array.isArray(testCase.postconditions) || !testCase.postconditions.length) {
|
|
562
|
+
throw new Error(`case=${testCase.id} postconditions 非法`);
|
|
563
|
+
}
|
|
564
|
+
for (const expectation of testCase.postconditions) {
|
|
565
|
+
validateEvidenceExpectation(testCase.id, expectation);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (testCase.sourceRefs !== undefined &&
|
|
569
|
+
(!Array.isArray(testCase.sourceRefs) ||
|
|
570
|
+
testCase.sourceRefs.some((item) => !String(item || '').trim()))) {
|
|
571
|
+
throw new Error(`case=${testCase.id} sourceRefs 非法`);
|
|
572
|
+
}
|
|
573
|
+
for (const expectation of testCase.expectations)
|
|
574
|
+
validateExpectation(testCase.id, expectation);
|
|
575
|
+
}
|
|
576
|
+
for (const testCase of input.cases) {
|
|
577
|
+
if (testCase.continuationOf) {
|
|
578
|
+
const parent = input.cases.find((item) => item.id === testCase.continuationOf);
|
|
579
|
+
if (!parent || parent.lane !== testCase.lane || testCase.lane !== 'stateful') {
|
|
580
|
+
throw new Error(`case=${testCase.id} continuationOf 必须指向同一 stateful lane 的已有 case`);
|
|
581
|
+
}
|
|
582
|
+
if (input.cases.indexOf(parent) >= input.cases.indexOf(testCase)) {
|
|
583
|
+
throw new Error(`case=${testCase.id} continuationOf 必须位于当前 case 之前`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return input;
|
|
588
|
+
}
|
|
589
|
+
function validateEvidenceExpectation(caseId, expectation) {
|
|
590
|
+
if (!String(expectation?.id || '').trim() ||
|
|
591
|
+
!['filesystem', 'workspace', 'artifact'].includes(expectation?.surface) ||
|
|
592
|
+
!['pathExists', 'pathAbsent', 'textIncludesAll', 'textExcludesAll',
|
|
593
|
+
'jsonEquals', 'agentDefinition'].includes(expectation?.type)) {
|
|
594
|
+
throw new Error(`case=${caseId} postcondition 非法`);
|
|
595
|
+
}
|
|
596
|
+
if (expectation.type === 'agentDefinition') {
|
|
597
|
+
if (expectation.surface !== 'workspace' || !['ready', 'partial'].includes(expectation.status) ||
|
|
598
|
+
!Array.isArray(expectation.requiredSkills)) {
|
|
599
|
+
throw new Error(`case=${caseId} agentDefinition postcondition 非法`);
|
|
600
|
+
}
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
const path = String(expectation.path || '');
|
|
604
|
+
if (!path || resolve('/', path) === path || path.split(/[\\/]/).includes('..')) {
|
|
605
|
+
throw new Error(`case=${caseId} postcondition path 必须是安全相对路径`);
|
|
606
|
+
}
|
|
607
|
+
if (expectation.type === 'pathExists') {
|
|
608
|
+
if (expectation.kind !== undefined && !['file', 'directory'].includes(expectation.kind)) {
|
|
609
|
+
throw new Error(`case=${caseId} pathExists.kind 非法`);
|
|
610
|
+
}
|
|
611
|
+
if (expectation.minBytes !== undefined &&
|
|
612
|
+
(!Number.isInteger(expectation.minBytes) || expectation.minBytes < 0)) {
|
|
613
|
+
throw new Error(`case=${caseId} pathExists.minBytes 非法`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (['textIncludesAll', 'textExcludesAll'].includes(expectation.type) &&
|
|
617
|
+
(!Array.isArray(expectation.values) || !expectation.values.length ||
|
|
618
|
+
expectation.values.some((item) => !String(item || '').trim()))) {
|
|
619
|
+
throw new Error(`case=${caseId} text postcondition values 非法`);
|
|
620
|
+
}
|
|
621
|
+
if (expectation.type === 'jsonEquals' &&
|
|
622
|
+
(typeof expectation.pointer !== 'string' || !expectation.pointer.startsWith('/'))) {
|
|
623
|
+
throw new Error(`case=${caseId} jsonEquals.pointer 非法`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
function validateExpectation(caseId, expectation) {
|
|
627
|
+
if (!String(expectation?.id || '').trim() ||
|
|
628
|
+
!['includesAny', 'includesAll', 'excludesAll', 'equals', 'maxQuestions',
|
|
629
|
+
'matches', 'notMatches'].includes(expectation?.type)) {
|
|
630
|
+
throw new Error(`case=${caseId} expectation 非法`);
|
|
631
|
+
}
|
|
632
|
+
if (expectation.gate !== undefined && !['safety', 'quality'].includes(expectation.gate)) {
|
|
633
|
+
throw new Error(`case=${caseId} expectation.gate 非法`);
|
|
634
|
+
}
|
|
635
|
+
if (expectation.type === 'maxQuestions') {
|
|
636
|
+
if (!Number.isInteger(expectation.max) || expectation.max < 0 || expectation.max > 3) {
|
|
637
|
+
throw new Error(`case=${caseId} maxQuestions 非法`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
else if (expectation.type === 'equals') {
|
|
641
|
+
if (!String(expectation.value || '').trim())
|
|
642
|
+
throw new Error(`case=${caseId} equals.value 非法`);
|
|
643
|
+
}
|
|
644
|
+
else if (expectation.type === 'matches' || expectation.type === 'notMatches') {
|
|
645
|
+
if (!String(expectation.pattern || '').trim() || expectation.pattern.length > 500 ||
|
|
646
|
+
(expectation.flags !== undefined && !/^(?=[imu]*$)(?!.*(.).*\1)[imu]*$/.test(expectation.flags))) {
|
|
647
|
+
throw new Error(`case=${caseId} regex expectation 非法`);
|
|
648
|
+
}
|
|
649
|
+
try {
|
|
650
|
+
new RegExp(expectation.pattern, expectation.flags || 'u');
|
|
651
|
+
}
|
|
652
|
+
catch {
|
|
653
|
+
throw new Error(`case=${caseId} regex pattern 无法编译`);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
else if (!Array.isArray(expectation.values) || !expectation.values.length ||
|
|
657
|
+
expectation.values.some((item) => !String(item || '').trim())) {
|
|
658
|
+
throw new Error(`case=${caseId} expectation.values 非法`);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
function connectorHealthy(value) {
|
|
662
|
+
return /"(?:state|status)"\s*:\s*"healthy"/i.test(JSON.stringify(value || {}));
|
|
663
|
+
}
|
|
664
|
+
function connectorNotRunning(value) {
|
|
665
|
+
return /"(?:state|status)"\s*:\s*"not_running"/i.test(JSON.stringify(value || {}));
|
|
666
|
+
}
|
|
667
|
+
function extractReply(value, marker, outbound) {
|
|
668
|
+
const strings = [];
|
|
669
|
+
collectStrings(value, strings, 0);
|
|
670
|
+
const unique = [...new Set(strings.map((item) => item.trim()).filter(Boolean))];
|
|
671
|
+
const candidates = unique.filter((item) => item.includes(marker) && item !== outbound && !item.includes('评测协议:') &&
|
|
672
|
+
item.length > marker.length + 1);
|
|
673
|
+
const preferred = candidates.filter((item) => item.startsWith(marker) && !item.startsWith('{'));
|
|
674
|
+
return (preferred.length ? preferred : candidates)
|
|
675
|
+
.sort((a, b) => a.length - b.length)[0] || '';
|
|
676
|
+
}
|
|
677
|
+
function extractCandidateReply(value, uuid, outbound) {
|
|
678
|
+
const strings = [];
|
|
679
|
+
collectStrings(value, strings, 0);
|
|
680
|
+
const unique = [...new Set(strings.map((item) => item.trim()).filter(Boolean))];
|
|
681
|
+
const candidates = unique.filter((item) => item.includes(uuid) && item !== outbound && !item.includes('评测协议:') &&
|
|
682
|
+
item.length > uuid.length + 1);
|
|
683
|
+
const preferred = candidates.filter((item) => item.startsWith('[DTA-EVAL-') && !item.startsWith('{'));
|
|
684
|
+
return (preferred.length ? preferred : candidates)
|
|
685
|
+
.sort((a, b) => a.length - b.length)[0] || '';
|
|
686
|
+
}
|
|
687
|
+
function stripEvalMarker(value) {
|
|
688
|
+
return value.replace(/^\[DTA-EVAL-[^\]\n]+\]\s*/, '').trim();
|
|
689
|
+
}
|
|
690
|
+
function collectStrings(value, output, depth) {
|
|
691
|
+
if (depth > 8 || value === null || value === undefined)
|
|
692
|
+
return;
|
|
693
|
+
if (typeof value === 'string') {
|
|
694
|
+
output.push(value);
|
|
695
|
+
if (depth < 4 && /^[\s]*[{[]/.test(value)) {
|
|
696
|
+
try {
|
|
697
|
+
collectStrings(JSON.parse(value), output, depth + 1);
|
|
698
|
+
}
|
|
699
|
+
catch { /* plain text */ }
|
|
700
|
+
}
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
if (Array.isArray(value)) {
|
|
704
|
+
for (const item of value)
|
|
705
|
+
collectStrings(item, output, depth + 1);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
if (typeof value === 'object') {
|
|
709
|
+
for (const item of Object.values(value)) {
|
|
710
|
+
collectStrings(item, output, depth + 1);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
function formatLocalTime(date) {
|
|
715
|
+
return new Intl.DateTimeFormat('sv-SE', {
|
|
716
|
+
year: 'numeric', month: '2-digit', day: '2-digit',
|
|
717
|
+
hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23',
|
|
718
|
+
}).format(date);
|
|
719
|
+
}
|
|
720
|
+
function redactPool(pool) {
|
|
721
|
+
return {
|
|
722
|
+
...pool,
|
|
723
|
+
dws: {
|
|
724
|
+
profile: redact(pool.dws.profile),
|
|
725
|
+
expectedUserId: redact(pool.dws.expectedUserId),
|
|
726
|
+
},
|
|
727
|
+
slots: pool.slots.map((slot) => ({
|
|
728
|
+
...slot,
|
|
729
|
+
unifiedAppId: redact(slot.unifiedAppId),
|
|
730
|
+
creationTaskId: redact(slot.creationTaskId || ''),
|
|
731
|
+
robotClientId: redact(slot.robotClientId),
|
|
732
|
+
botOpenDingTalkId: redact(slot.botOpenDingTalkId),
|
|
733
|
+
})),
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
function redact(value) {
|
|
737
|
+
return value ? `<redacted:${createHash('sha256').update(value).digest('hex').slice(0, 10)}>` : '';
|
|
738
|
+
}
|
|
739
|
+
function profileArgs(profile) {
|
|
740
|
+
return profile ? ['--profile', profile] : [];
|
|
741
|
+
}
|
|
742
|
+
function readJsonFile(path, label) {
|
|
743
|
+
try {
|
|
744
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
745
|
+
}
|
|
746
|
+
catch (error) {
|
|
747
|
+
throw new Error(`${label} 不是合法 JSON: ${error.message}`);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
function writeJson(path, value) {
|
|
751
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
752
|
+
const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
753
|
+
writeFileSync(temporary, JSON.stringify(value, null, 2) + '\n', { flag: 'wx' });
|
|
754
|
+
renameSync(temporary, path);
|
|
755
|
+
}
|
|
756
|
+
function safeInside(root, path, mustExist) {
|
|
757
|
+
const rootReal = realpathSync(resolve(root));
|
|
758
|
+
const candidate = resolve(rootReal, path);
|
|
759
|
+
const rel = relative(rootReal, candidate);
|
|
760
|
+
if (rel === '..' || rel.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`)) {
|
|
761
|
+
throw new Error(`Robot Eval 路径必须位于当前目录内: ${path}`);
|
|
762
|
+
}
|
|
763
|
+
if (mustExist && !existsSync(candidate))
|
|
764
|
+
throw new Error(`Robot Eval 路径不存在: ${path}`);
|
|
765
|
+
return candidate;
|
|
766
|
+
}
|
|
767
|
+
function safeName(value) {
|
|
768
|
+
return value.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'case';
|
|
769
|
+
}
|
|
770
|
+
//# sourceMappingURL=robot-evals.js.map
|