@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,595 @@
|
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync, } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
4
|
+
import { must as loadConfig } from './config.js';
|
|
5
|
+
import { digest, normalizeEvent, stableStringify } from './events.js';
|
|
6
|
+
import { doctorLab, loadLabManifest, runLab, teardownLab, verifyLab, } from './lab.js';
|
|
7
|
+
import { isCancelCommand } from './waits.js';
|
|
8
|
+
import { readJson } from './sessions.js';
|
|
9
|
+
import * as dws from './dws.js';
|
|
10
|
+
const SCENARIOS = new Set([
|
|
11
|
+
'host-loading', 'conversation-contract', 'authority-and-egress', 'task-lifecycle',
|
|
12
|
+
'memory-and-storage', 'workspace-and-artifacts', 'platform-reliability',
|
|
13
|
+
]);
|
|
14
|
+
const ACTIONS = new Set(['reply', 'ask', 'silence']);
|
|
15
|
+
const INVOCATION_MODES = new Set(['mention', 'dm', 'ambient', 'continuation']);
|
|
16
|
+
const RESPONSE_DECISIONS = new Set(['engage', 'silent', 'inspect']);
|
|
17
|
+
const RESUME_MODES = new Set(['start', 'resume', 'cancel']);
|
|
18
|
+
const SILENCE_REASONS = new Set([
|
|
19
|
+
'unmentioned', 'already_answered', 'no_value', 'policy_denied', 'unsupported',
|
|
20
|
+
'heartbeat_noop',
|
|
21
|
+
]);
|
|
22
|
+
/**
|
|
23
|
+
* Phase 9 只编排可信 personal-event → Prepared Run → Action Gate → DWS → readback。
|
|
24
|
+
* 事件订阅仍由外部 Adapter 负责,模型表达也继续由 L1/OpenCode 单独评测。
|
|
25
|
+
*/
|
|
26
|
+
export function runPersonalEventEvaluation(root, workspacePath, suitePath, options = {}) {
|
|
27
|
+
const workspace = safeInside(root, workspacePath, true, 'Workspace');
|
|
28
|
+
const suiteFile = safeInside(root, suitePath, true, 'suite');
|
|
29
|
+
const suite = validateSuite(readJsonFile(suiteFile, 'personal-event suite'));
|
|
30
|
+
const manifest = loadLabManifest(workspace, suite.manifest);
|
|
31
|
+
if (!['mock-integration', 'personal-event'].includes(manifest.mode)) {
|
|
32
|
+
throw new Error('personal-event eval 只接受 mock-integration 或 personal-event manifest');
|
|
33
|
+
}
|
|
34
|
+
if (resolve(workspace, suite.evidenceRoot) === resolve(workspace, manifest.evidenceRoot)) {
|
|
35
|
+
throw new Error('personal-event eval evidenceRoot 必须与单 case Lab evidenceRoot 分开');
|
|
36
|
+
}
|
|
37
|
+
const doctor = doctorLab(workspace, suite.manifest);
|
|
38
|
+
if (!doctor.ready)
|
|
39
|
+
throw new Error(`personal-event Lab preflight 失败: ${summarizeDoctor(doctor)}`);
|
|
40
|
+
const prepared = prepareCases(workspace, suite, manifest);
|
|
41
|
+
const expectedOutward = prepared.reduce((sum, item) => sum + (item.spec.duplicateOf ? 0 : item.spec.expected.newOutwardEffects), 0);
|
|
42
|
+
if (expectedOutward !== suite.maxOutwardActions) {
|
|
43
|
+
throw new Error(`personal-event suite outward budget 不一致: cases=${expectedOutward}, suite=${suite.maxOutwardActions}`);
|
|
44
|
+
}
|
|
45
|
+
const physicalRuns = prepared.filter((item) => !item.spec.duplicateOf).length;
|
|
46
|
+
if (physicalRuns > manifest.budgets.maxRuns) {
|
|
47
|
+
throw new Error(`personal-event suite 需要 ${physicalRuns} 个 Run,超过 manifest ${manifest.budgets.maxRuns}`);
|
|
48
|
+
}
|
|
49
|
+
const plan = {
|
|
50
|
+
$schema: 'dingtalk-agent/personal-event-eval-plan@1',
|
|
51
|
+
ready: manifest.mode === 'mock-integration' || suite.liveAuthorized === 'yes',
|
|
52
|
+
executeRequired: true,
|
|
53
|
+
sideEffect: false,
|
|
54
|
+
dingtalkSideEffect: false,
|
|
55
|
+
id: suite.id,
|
|
56
|
+
workspace: relative(realpathSync(root), workspace) || '.',
|
|
57
|
+
suite: relative(realpathSync(root), suiteFile),
|
|
58
|
+
mode: manifest.mode,
|
|
59
|
+
liveAuthorized: suite.liveAuthorized,
|
|
60
|
+
egressOwner: manifest.egressOwner,
|
|
61
|
+
eventAdapter: 'external-dws-event-consume',
|
|
62
|
+
triggerOwnedByDta: false,
|
|
63
|
+
executionGate: manifest.mode === 'mock-integration'
|
|
64
|
+
? '--execute --yes with verified fake DWS'
|
|
65
|
+
: 'suite.liveAuthorized=yes + --execute --live --yes',
|
|
66
|
+
maxOutwardActions: suite.maxOutwardActions,
|
|
67
|
+
physicalRuns,
|
|
68
|
+
requiredSkills: suite.requiredSkills,
|
|
69
|
+
cases: prepared.map((item) => ({
|
|
70
|
+
id: item.spec.id,
|
|
71
|
+
scenario: item.spec.scenario,
|
|
72
|
+
risk: item.spec.risk,
|
|
73
|
+
action: item.spec.action,
|
|
74
|
+
duplicateOf: item.spec.duplicateOf || null,
|
|
75
|
+
continuationOf: item.spec.continuationOf || null,
|
|
76
|
+
expected: item.spec.expected,
|
|
77
|
+
marker: manifest.mode === 'personal-event' ? '<validated-live-marker>' : '<generated-at-run>',
|
|
78
|
+
})),
|
|
79
|
+
gates: [
|
|
80
|
+
'suite liveAuthorized + CLI execute/live/yes',
|
|
81
|
+
'manifest/workspace identity and single egress',
|
|
82
|
+
'tenant/conversation/actor allowlist',
|
|
83
|
+
'Definition and Basic/Role Skill snapshot',
|
|
84
|
+
'Response Gate and expected action',
|
|
85
|
+
'Intent/Attempt/Receipt journal',
|
|
86
|
+
'same message id + exact text independent readback',
|
|
87
|
+
'duplicate Receipt replay with unchanged attempt',
|
|
88
|
+
'Wait resume/cancel Session continuity',
|
|
89
|
+
'teardown with no Lab-owned connector',
|
|
90
|
+
],
|
|
91
|
+
doctor,
|
|
92
|
+
};
|
|
93
|
+
if (!options.execute)
|
|
94
|
+
return plan;
|
|
95
|
+
const mock = manifest.mode === 'mock-integration';
|
|
96
|
+
if (mock) {
|
|
97
|
+
if (options.live)
|
|
98
|
+
throw new Error('mock personal-event eval 不接受 --live');
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
if (suite.liveAuthorized !== 'yes') {
|
|
102
|
+
throw new Error('personal-event Live 未授权:suite.liveAuthorized 必须是 yes');
|
|
103
|
+
}
|
|
104
|
+
if (!options.live || !options.yes) {
|
|
105
|
+
throw new Error('personal-event Live 必须同时传 --execute --live --yes');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (!options.yes)
|
|
109
|
+
throw new Error('personal-event eval 执行必须显式传 --yes');
|
|
110
|
+
const evaluationId = `personal_event_${Date.now()}_${randomUUID().slice(0, 8)}`;
|
|
111
|
+
const evidenceRoot = safeEvidenceRoot(workspace, options.out || suite.evidenceRoot);
|
|
112
|
+
const evaluationRoot = join(evidenceRoot, evaluationId);
|
|
113
|
+
mkdirSync(evaluationRoot, { recursive: true });
|
|
114
|
+
writeJson(join(evaluationRoot, 'plan.json'), plan);
|
|
115
|
+
writeJson(join(evaluationRoot, 'suite.redacted.json'), redactSuite(suite));
|
|
116
|
+
const cfg = loadConfig(workspace);
|
|
117
|
+
const results = [];
|
|
118
|
+
const completed = new Map();
|
|
119
|
+
const uniqueEvidence = new Set();
|
|
120
|
+
const teardowns = [];
|
|
121
|
+
let outwardEffects = 0;
|
|
122
|
+
let failure = null;
|
|
123
|
+
let activeCaseId = 'pre-case';
|
|
124
|
+
let identityVerified = false;
|
|
125
|
+
try {
|
|
126
|
+
activeCaseId = 'identity-preflight';
|
|
127
|
+
const identityReadback = dws.getSelf(manifest.dws.profile);
|
|
128
|
+
writeJson(join(evaluationRoot, 'identity-readback.json'), identityReadback);
|
|
129
|
+
const actualUserId = findString(identityReadback, ['userId', 'userid', 'user_id']);
|
|
130
|
+
if (!actualUserId || actualUserId !== manifest.dws.expectedUserId) {
|
|
131
|
+
throw new Error('personal-event DWS 执行身份与 manifest expectedUserId 不一致');
|
|
132
|
+
}
|
|
133
|
+
identityVerified = true;
|
|
134
|
+
for (const item of prepared) {
|
|
135
|
+
activeCaseId = item.spec.id;
|
|
136
|
+
const relation = item.spec.duplicateOf
|
|
137
|
+
? (completed.get(item.spec.duplicateOf) || null)
|
|
138
|
+
: item.spec.continuationOf
|
|
139
|
+
? (completed.get(item.spec.continuationOf) || null)
|
|
140
|
+
: null;
|
|
141
|
+
if ((item.spec.duplicateOf || item.spec.continuationOf) && !relation) {
|
|
142
|
+
throw new Error(`case=${item.spec.id} 引用的前置 case 没有完成`);
|
|
143
|
+
}
|
|
144
|
+
const source = item.spec.duplicateOf
|
|
145
|
+
? prepared.find((candidate) => candidate.spec.id === item.spec.duplicateOf)
|
|
146
|
+
: item;
|
|
147
|
+
const sourceSpec = source.spec;
|
|
148
|
+
const beforeAttemptHash = relation?.evidencePath
|
|
149
|
+
? fileHash(join(relation.evidencePath, 'action-attempt.json')) : '';
|
|
150
|
+
let output;
|
|
151
|
+
try {
|
|
152
|
+
output = runLab(workspace, cfg, suite.manifest, source.rawEvent, {
|
|
153
|
+
caseId: sourceSpec.id,
|
|
154
|
+
action: sourceSpec.action,
|
|
155
|
+
replyText: sourceSpec.text,
|
|
156
|
+
silenceReason: sourceSpec.silenceReason,
|
|
157
|
+
perception: source.perception,
|
|
158
|
+
useEventCorrelation: true,
|
|
159
|
+
markerOverride: mock
|
|
160
|
+
? (item.spec.duplicateOf ? relation?.marker : item.marker)
|
|
161
|
+
: undefined,
|
|
162
|
+
live: mock ? false : options.live,
|
|
163
|
+
yes: true,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
const failedEvidence = String(error.evidencePath || '');
|
|
168
|
+
if (failedEvidence)
|
|
169
|
+
uniqueEvidence.add(failedEvidence);
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
uniqueEvidence.add(output.evidencePath);
|
|
173
|
+
const result = evaluateCase(workspace, suite, item.spec, output, relation, beforeAttemptHash);
|
|
174
|
+
results.push(result);
|
|
175
|
+
completed.set(item.spec.id, result);
|
|
176
|
+
outwardEffects += result.newOutwardEffects;
|
|
177
|
+
writeJson(join(evaluationRoot, 'cases', `${safeName(item.spec.id)}.json`), result);
|
|
178
|
+
if (!result.passed)
|
|
179
|
+
throw new Error(`case=${item.spec.id} 硬门禁失败`);
|
|
180
|
+
if (outwardEffects > suite.maxOutwardActions) {
|
|
181
|
+
throw new Error(`personal-event outward budget 超限: ${outwardEffects}/${suite.maxOutwardActions}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
failure = {
|
|
187
|
+
stage: activeCaseId,
|
|
188
|
+
error: String(error.message || error),
|
|
189
|
+
occurredAt: new Date().toISOString(),
|
|
190
|
+
};
|
|
191
|
+
writeJson(join(evaluationRoot, 'failure.json'), failure);
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
for (const evidencePath of uniqueEvidence) {
|
|
195
|
+
try {
|
|
196
|
+
const teardown = teardownLab(workspace, suite.manifest, evidencePath);
|
|
197
|
+
const verified = verifyLab(workspace, evidencePath);
|
|
198
|
+
teardowns.push({ evidencePath: relative(workspace, evidencePath), teardown, verified });
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
teardowns.push({
|
|
202
|
+
evidencePath: relative(workspace, evidencePath),
|
|
203
|
+
error: String(error.message || error),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
writeJson(join(evaluationRoot, 'teardown.json'), teardowns);
|
|
208
|
+
}
|
|
209
|
+
const teardownPassed = teardowns.length === physicalRuns &&
|
|
210
|
+
teardowns.every((item) => item.teardown?.passed === true && item.verified?.passed === true);
|
|
211
|
+
const allPassed = !failure && results.length === suite.cases.length &&
|
|
212
|
+
results.every((item) => item.passed) && outwardEffects === suite.maxOutwardActions &&
|
|
213
|
+
teardownPassed;
|
|
214
|
+
const report = {
|
|
215
|
+
$schema: 'dingtalk-agent/personal-event-eval-report@1',
|
|
216
|
+
passed: allPassed,
|
|
217
|
+
sideEffect: mock ? 'fake-dws-only' : 'dedicated-personal-event-resources',
|
|
218
|
+
dingtalkSideEffect: !mock,
|
|
219
|
+
evaluationId,
|
|
220
|
+
evidencePath: relative(workspace, evaluationRoot),
|
|
221
|
+
mode: manifest.mode,
|
|
222
|
+
liveAuthorized: suite.liveAuthorized,
|
|
223
|
+
eventAdapter: 'external-dws-event-consume',
|
|
224
|
+
triggerOwnedByDta: false,
|
|
225
|
+
identityVerified,
|
|
226
|
+
outwardEffects,
|
|
227
|
+
maxOutwardActions: suite.maxOutwardActions,
|
|
228
|
+
results,
|
|
229
|
+
teardown: { passed: teardownPassed, results: teardowns },
|
|
230
|
+
failure,
|
|
231
|
+
limitations: [
|
|
232
|
+
'Event subscription/capture is external and is not created by this runner.',
|
|
233
|
+
'Scripted actions validate the kernel and platform path; model expression remains an L1/OpenCode gate.',
|
|
234
|
+
'User feedback is not inferred from delivery receipts.',
|
|
235
|
+
],
|
|
236
|
+
};
|
|
237
|
+
writeJson(join(evaluationRoot, 'report.json'), report);
|
|
238
|
+
return report;
|
|
239
|
+
}
|
|
240
|
+
function prepareCases(workspace, suite, manifest) {
|
|
241
|
+
const prepared = [];
|
|
242
|
+
const byId = new Map();
|
|
243
|
+
for (const spec of suite.cases) {
|
|
244
|
+
if (spec.duplicateOf) {
|
|
245
|
+
const source = byId.get(spec.duplicateOf);
|
|
246
|
+
if (!source)
|
|
247
|
+
throw new Error(`case=${spec.id} duplicateOf 必须引用前面的 case`);
|
|
248
|
+
prepared.push({ ...source, spec, marker: source.marker });
|
|
249
|
+
byId.set(spec.id, prepared.at(-1));
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (!spec.eventFile)
|
|
253
|
+
throw new Error(`case=${spec.id} 缺少 eventFile`);
|
|
254
|
+
const eventPath = safeInside(workspace, spec.eventFile, true, `case=${spec.id} eventFile`);
|
|
255
|
+
const rawEvent = readJsonFile(eventPath, `case=${spec.id} event`);
|
|
256
|
+
const event = normalizeEvent(rawEvent);
|
|
257
|
+
const marker = extractMarker(event.message.text);
|
|
258
|
+
if (manifest.mode === 'personal-event') {
|
|
259
|
+
const markers = event.message.text.match(/\[DTA-LAB-[^\]\r\n]{1,180}\]/gu) || [];
|
|
260
|
+
if (markers.length !== 1 || !marker.startsWith(`${manifest.markerPrefix}${spec.id}-`)) {
|
|
261
|
+
throw new Error(`case=${spec.id} Live event 必须有且只有一个绑定 case id 的 marker`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
assertAllowlisted(manifest, event, spec.id);
|
|
265
|
+
let perception;
|
|
266
|
+
if (spec.perceptionFile) {
|
|
267
|
+
perception = readJsonFile(safeInside(workspace, spec.perceptionFile, true, `case=${spec.id} perceptionFile`), `case=${spec.id} perception`);
|
|
268
|
+
}
|
|
269
|
+
if (spec.expected.resumeMode === 'cancel' && !isCancelCommand(event.message.text)) {
|
|
270
|
+
throw new Error(`case=${spec.id} 期望 cancel,但 event 不是显式 /stop 或 /cancel`);
|
|
271
|
+
}
|
|
272
|
+
const item = {
|
|
273
|
+
spec, rawEvent, perception,
|
|
274
|
+
marker: marker || `${manifest.markerPrefix}${spec.id}-${randomUUID()}]`,
|
|
275
|
+
};
|
|
276
|
+
prepared.push(item);
|
|
277
|
+
byId.set(spec.id, item);
|
|
278
|
+
}
|
|
279
|
+
assertCoverage(prepared);
|
|
280
|
+
return prepared;
|
|
281
|
+
}
|
|
282
|
+
function evaluateCase(workspace, suite, spec, output, relation, beforeAttemptHash) {
|
|
283
|
+
const evidencePath = String(output.evidencePath || '');
|
|
284
|
+
const invocation = readJson(join(evidencePath, 'invocation.json'));
|
|
285
|
+
const enriched = readJson(join(evidencePath, 'enriched-invocation.json'));
|
|
286
|
+
const responseGate = readJson(join(evidencePath, 'response-gate.json'));
|
|
287
|
+
const resume = readJson(join(evidencePath, 'resume.json'));
|
|
288
|
+
const definition = readJson(join(evidencePath, 'agent-definition.json'));
|
|
289
|
+
const skillManifest = readJson(join(evidencePath, 'skill-manifest.json'));
|
|
290
|
+
const receipt = readJson(join(evidencePath, 'action-receipt.json'));
|
|
291
|
+
const assertions = readJson(join(evidencePath, 'assertions.json'));
|
|
292
|
+
const attemptHash = fileHash(join(evidencePath, 'action-attempt.json'));
|
|
293
|
+
const skillNames = new Set((skillManifest.entries || []).map((entry) => entry.name));
|
|
294
|
+
const perceptionChecks = (spec.expected.perception || []).map((name) => ({
|
|
295
|
+
name,
|
|
296
|
+
passed: enriched.enrichments?.[name]?.status === 'available',
|
|
297
|
+
status: enriched.enrichments?.[name]?.status || 'missing',
|
|
298
|
+
}));
|
|
299
|
+
const checks = [
|
|
300
|
+
{
|
|
301
|
+
id: 'lab-assertions', passed: assertions.passed === true,
|
|
302
|
+
detail: stableStringify(assertions.checks || []),
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
id: 'invocation-mode', passed: invocation.mode === spec.expected.invocationMode,
|
|
306
|
+
detail: `${invocation.mode}/${spec.expected.invocationMode}`,
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
id: 'response-gate', passed: responseGate.decision === spec.expected.responseDecision,
|
|
310
|
+
detail: `${responseGate.decision}/${spec.expected.responseDecision}`,
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
id: 'resume-mode', passed: resume.mode === spec.expected.resumeMode,
|
|
314
|
+
detail: `${resume.mode}/${spec.expected.resumeMode}`,
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
id: 'definition-and-skills', passed: Boolean(definition.hash) &&
|
|
318
|
+
suite.requiredSkills.every((name) => skillNames.has(name)),
|
|
319
|
+
detail: `${definition.hash || 'missing'}; ${[...skillNames].join(',')}`,
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
id: 'receipt', passed: receipt.outcome === 'confirmed' && receipt.verified === true &&
|
|
323
|
+
receipt.kind === spec.action,
|
|
324
|
+
detail: `${receipt.kind}/${receipt.outcome}/${String(receipt.verified)}`,
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
id: 'marker-readback', passed: assertions.markerCount === spec.expected.markerCount,
|
|
328
|
+
detail: `${assertions.markerCount}/${spec.expected.markerCount}`,
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
id: 'perception', passed: perceptionChecks.every((item) => item.passed),
|
|
332
|
+
detail: stableStringify(perceptionChecks),
|
|
333
|
+
},
|
|
334
|
+
];
|
|
335
|
+
if (spec.duplicateOf) {
|
|
336
|
+
checks.push({
|
|
337
|
+
id: 'duplicate-receipt',
|
|
338
|
+
passed: output.dispatch?.duplicate === true && receipt.duplicate === true &&
|
|
339
|
+
output.dispatch?.runId === relation?.runId && receipt.actionId === relation?.actionId &&
|
|
340
|
+
beforeAttemptHash === attemptHash,
|
|
341
|
+
detail: stableStringify({
|
|
342
|
+
dispatchDuplicate: output.dispatch?.duplicate, receiptDuplicate: receipt.duplicate,
|
|
343
|
+
runId: output.dispatch?.runId, expectedRunId: relation?.runId,
|
|
344
|
+
actionId: receipt.actionId, expectedActionId: relation?.actionId,
|
|
345
|
+
beforeAttemptHash, attemptHash,
|
|
346
|
+
}),
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
if (spec.continuationOf) {
|
|
350
|
+
checks.push({
|
|
351
|
+
id: 'session-continuity',
|
|
352
|
+
passed: output.dispatch?.sessionId === relation?.sessionId &&
|
|
353
|
+
output.dispatch?.runId !== relation?.runId,
|
|
354
|
+
detail: `${relation?.sessionId}/${relation?.runId} -> ${output.dispatch?.sessionId}/${output.dispatch?.runId}`,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
return {
|
|
358
|
+
id: spec.id,
|
|
359
|
+
scenario: spec.scenario,
|
|
360
|
+
risk: spec.risk,
|
|
361
|
+
passed: checks.every((item) => item.passed),
|
|
362
|
+
action: spec.action,
|
|
363
|
+
marker: output.marker,
|
|
364
|
+
markerCount: assertions.markerCount,
|
|
365
|
+
newOutwardEffects: spec.duplicateOf ? 0 : spec.expected.newOutwardEffects,
|
|
366
|
+
sessionId: output.dispatch?.sessionId,
|
|
367
|
+
runId: output.dispatch?.runId,
|
|
368
|
+
actionId: receipt.actionId,
|
|
369
|
+
duplicate: Boolean(receipt.duplicate),
|
|
370
|
+
definitionHash: definition.hash,
|
|
371
|
+
skillManifestHash: skillManifest.hash,
|
|
372
|
+
invocationMode: invocation.mode,
|
|
373
|
+
responseDecision: responseGate.decision,
|
|
374
|
+
resumeMode: resume.mode,
|
|
375
|
+
attemptHash,
|
|
376
|
+
evidencePath,
|
|
377
|
+
evidenceRelative: relative(workspace, evidencePath),
|
|
378
|
+
checks,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
function validateSuite(value) {
|
|
382
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
383
|
+
throw new Error('personal-event suite 必须是 JSON object');
|
|
384
|
+
}
|
|
385
|
+
const suite = value;
|
|
386
|
+
if (suite.$schema !== 'dingtalk-agent/personal-event-eval@1') {
|
|
387
|
+
throw new Error('personal-event suite schema 非法');
|
|
388
|
+
}
|
|
389
|
+
if (!String(suite.id || '').trim() || !String(suite.manifest || '').trim() ||
|
|
390
|
+
!String(suite.evidenceRoot || '').trim()) {
|
|
391
|
+
throw new Error('personal-event suite 缺少 id/manifest/evidenceRoot');
|
|
392
|
+
}
|
|
393
|
+
if (!['yes', 'no'].includes(suite.liveAuthorized)) {
|
|
394
|
+
throw new Error('personal-event suite liveAuthorized 必须是 yes 或 no');
|
|
395
|
+
}
|
|
396
|
+
if (!Array.isArray(suite.requiredSkills) || !suite.requiredSkills.includes('dingtalk-basic-behavior') ||
|
|
397
|
+
suite.requiredSkills.some((item) => !String(item || '').trim()) ||
|
|
398
|
+
new Set(suite.requiredSkills).size !== suite.requiredSkills.length) {
|
|
399
|
+
throw new Error('personal-event suite requiredSkills 必须包含 dingtalk-basic-behavior');
|
|
400
|
+
}
|
|
401
|
+
if (!Number.isInteger(suite.maxOutwardActions) || suite.maxOutwardActions < 1 ||
|
|
402
|
+
suite.maxOutwardActions > 20) {
|
|
403
|
+
throw new Error('personal-event suite maxOutwardActions 必须为 1..20');
|
|
404
|
+
}
|
|
405
|
+
if (!Array.isArray(suite.cases) || !suite.cases.length || suite.cases.length > 20) {
|
|
406
|
+
throw new Error('personal-event suite cases 必须为 1..20');
|
|
407
|
+
}
|
|
408
|
+
const ids = new Set();
|
|
409
|
+
const prior = new Map();
|
|
410
|
+
for (const item of suite.cases) {
|
|
411
|
+
if (!item || typeof item !== 'object' || Array.isArray(item) ||
|
|
412
|
+
!/^[a-z0-9][a-z0-9._-]*$/.test(String(item.id || '')) || ids.has(item.id)) {
|
|
413
|
+
throw new Error(`personal-event case id 非法或重复: ${item?.id || 'missing'}`);
|
|
414
|
+
}
|
|
415
|
+
ids.add(item.id);
|
|
416
|
+
if (!SCENARIOS.has(item.scenario) || !['safety', 'reliability', 'quality'].includes(item.risk) ||
|
|
417
|
+
!ACTIONS.has(item.action)) {
|
|
418
|
+
throw new Error(`case=${item.id} scenario/risk/action 非法`);
|
|
419
|
+
}
|
|
420
|
+
if (item.duplicateOf && item.continuationOf) {
|
|
421
|
+
throw new Error(`case=${item.id} 不能同时声明 duplicateOf 和 continuationOf`);
|
|
422
|
+
}
|
|
423
|
+
const sourceId = item.duplicateOf || item.continuationOf;
|
|
424
|
+
const source = sourceId ? prior.get(sourceId) : null;
|
|
425
|
+
if (sourceId && !source) {
|
|
426
|
+
throw new Error(`case=${item.id} 必须引用前面已经声明的 case: ${sourceId}`);
|
|
427
|
+
}
|
|
428
|
+
if (item.duplicateOf && (item.action !== source?.action ||
|
|
429
|
+
item.expected?.invocationMode !== source?.expected?.invocationMode ||
|
|
430
|
+
item.expected?.responseDecision !== source?.expected?.responseDecision ||
|
|
431
|
+
item.expected?.resumeMode !== source?.expected?.resumeMode)) {
|
|
432
|
+
throw new Error(`case=${item.id} duplicateOf 必须保持原 action/invocation/decision/resume`);
|
|
433
|
+
}
|
|
434
|
+
if (item.continuationOf && source?.action !== 'ask') {
|
|
435
|
+
throw new Error(`case=${item.id} continuationOf 必须引用 ask case`);
|
|
436
|
+
}
|
|
437
|
+
if (!item.duplicateOf && !String(item.eventFile || '').trim()) {
|
|
438
|
+
throw new Error(`case=${item.id} 缺少 eventFile`);
|
|
439
|
+
}
|
|
440
|
+
if (item.action !== 'silence' && !String(item.text || '').trim()) {
|
|
441
|
+
throw new Error(`case=${item.id} reply/ask 必须声明 text`);
|
|
442
|
+
}
|
|
443
|
+
if (item.action === 'silence' && !SILENCE_REASONS.has(String(item.silenceReason || ''))) {
|
|
444
|
+
throw new Error(`case=${item.id} silence 必须声明合法 silenceReason`);
|
|
445
|
+
}
|
|
446
|
+
if (!item.expected || !INVOCATION_MODES.has(item.expected.invocationMode) ||
|
|
447
|
+
!RESPONSE_DECISIONS.has(item.expected.responseDecision) ||
|
|
448
|
+
!RESUME_MODES.has(item.expected.resumeMode) ||
|
|
449
|
+
![0, 1].includes(item.expected.newOutwardEffects) ||
|
|
450
|
+
![0, 1].includes(item.expected.markerCount)) {
|
|
451
|
+
throw new Error(`case=${item.id} expected 非法`);
|
|
452
|
+
}
|
|
453
|
+
const shouldOutward = item.action === 'reply' || item.action === 'ask';
|
|
454
|
+
if (!item.duplicateOf && Number(shouldOutward) !== item.expected.newOutwardEffects) {
|
|
455
|
+
throw new Error(`case=${item.id} newOutwardEffects 与 action 不一致`);
|
|
456
|
+
}
|
|
457
|
+
if (item.expected.markerCount !== Number(shouldOutward)) {
|
|
458
|
+
throw new Error(`case=${item.id} markerCount 与 action 不一致`);
|
|
459
|
+
}
|
|
460
|
+
if (!Array.isArray(item.sourceRefs) || !item.sourceRefs.length) {
|
|
461
|
+
throw new Error(`case=${item.id} 必须声明 sourceRefs`);
|
|
462
|
+
}
|
|
463
|
+
if (item.expected.perception !== undefined &&
|
|
464
|
+
(!Array.isArray(item.expected.perception) ||
|
|
465
|
+
item.expected.perception.some((name) => !['quote', 'burst', 'identity'].includes(String(name))))) {
|
|
466
|
+
throw new Error(`case=${item.id} expected.perception 非法`);
|
|
467
|
+
}
|
|
468
|
+
prior.set(item.id, item);
|
|
469
|
+
}
|
|
470
|
+
return suite;
|
|
471
|
+
}
|
|
472
|
+
function assertCoverage(cases) {
|
|
473
|
+
const mentionReply = cases.some((item) => item.spec.action === 'reply' &&
|
|
474
|
+
item.spec.expected.invocationMode === 'mention' && !item.spec.duplicateOf);
|
|
475
|
+
const ambientSilence = cases.some((item) => item.spec.action === 'silence' &&
|
|
476
|
+
item.spec.expected.invocationMode === 'ambient');
|
|
477
|
+
const duplicate = cases.some((item) => Boolean(item.spec.duplicateOf));
|
|
478
|
+
const continuation = cases.some((item) => Boolean(item.spec.continuationOf) &&
|
|
479
|
+
item.spec.expected.resumeMode === 'resume');
|
|
480
|
+
const cancel = cases.some((item) => Boolean(item.spec.continuationOf) &&
|
|
481
|
+
item.spec.expected.resumeMode === 'cancel');
|
|
482
|
+
const perception = cases.some((item) => ['quote', 'burst', 'identity'].every((name) => item.spec.expected.perception?.includes(name)));
|
|
483
|
+
const missing = Object.entries({ mentionReply, ambientSilence, duplicate, continuation, cancel, perception })
|
|
484
|
+
.filter(([, value]) => !value).map(([name]) => name);
|
|
485
|
+
if (missing.length)
|
|
486
|
+
throw new Error(`personal-event suite 缺少经典场景: ${missing.join(', ')}`);
|
|
487
|
+
}
|
|
488
|
+
function assertAllowlisted(manifest, event, caseId) {
|
|
489
|
+
const actorIds = [event.actor.id, event.actor.userId, event.actor.openDingTalkId].filter(Boolean);
|
|
490
|
+
if (!manifest.allowlist.tenantIds.includes(event.tenant.id) ||
|
|
491
|
+
!manifest.allowlist.conversationIds.includes(event.conversation.id) ||
|
|
492
|
+
!actorIds.some((id) => manifest.allowlist.actorIds.includes(id))) {
|
|
493
|
+
throw new Error(`case=${caseId} event tenant/conversation/actor 不在 manifest allowlist`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
function redactSuite(suite) {
|
|
497
|
+
return {
|
|
498
|
+
...suite,
|
|
499
|
+
manifest: '<gitignored-local-manifest>',
|
|
500
|
+
cases: suite.cases.map((item) => ({
|
|
501
|
+
...item,
|
|
502
|
+
eventFile: item.eventFile ? '<gitignored-event-file>' : undefined,
|
|
503
|
+
perceptionFile: item.perceptionFile ? '<gitignored-perception-file>' : undefined,
|
|
504
|
+
text: item.text ? `<redacted:${digest(item.text).slice(0, 10)}>` : undefined,
|
|
505
|
+
})),
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
function summarizeDoctor(doctor) {
|
|
509
|
+
return (doctor.checks || []).filter((item) => !item.passed)
|
|
510
|
+
.map((item) => `${item.id}:${item.detail}`).join('; ') || 'not ready';
|
|
511
|
+
}
|
|
512
|
+
function extractMarker(text) {
|
|
513
|
+
return text.match(/\[DTA-LAB-[^\]\r\n]{1,180}\]/u)?.[0] || '';
|
|
514
|
+
}
|
|
515
|
+
function fileHash(path) {
|
|
516
|
+
return existsSync(path) ? digest(readFileSync(path).toString('base64')) : '';
|
|
517
|
+
}
|
|
518
|
+
function readJsonFile(path, label) {
|
|
519
|
+
try {
|
|
520
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
521
|
+
}
|
|
522
|
+
catch (error) {
|
|
523
|
+
throw new Error(`${label} 不是合法 JSON: ${error.message}`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
function writeJson(path, value) {
|
|
527
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
528
|
+
const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
529
|
+
writeFileSync(temporary, JSON.stringify(value, null, 2) + '\n', { flag: 'wx' });
|
|
530
|
+
renameSync(temporary, path);
|
|
531
|
+
}
|
|
532
|
+
function safeInside(root, value, mustExist, label) {
|
|
533
|
+
const rootReal = realpathSync(resolve(root));
|
|
534
|
+
const candidate = resolve(rootReal, value);
|
|
535
|
+
const rel = relative(rootReal, candidate);
|
|
536
|
+
if (rel === '..' || rel.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`)) {
|
|
537
|
+
throw new Error(`${label} 必须位于 Workspace 内: ${value}`);
|
|
538
|
+
}
|
|
539
|
+
if (mustExist && !existsSync(candidate))
|
|
540
|
+
throw new Error(`${label} 不存在: ${value}`);
|
|
541
|
+
assertNoSymlinkAncestors(rootReal, candidate, label);
|
|
542
|
+
if (existsSync(candidate)) {
|
|
543
|
+
if (lstatSync(candidate).isSymbolicLink())
|
|
544
|
+
throw new Error(`${label} 不允许符号链接: ${value}`);
|
|
545
|
+
const actual = realpathSync(candidate);
|
|
546
|
+
const actualRel = relative(rootReal, actual);
|
|
547
|
+
if (actualRel === '..' || actualRel.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`)) {
|
|
548
|
+
throw new Error(`${label} 真实路径越界: ${value}`);
|
|
549
|
+
}
|
|
550
|
+
return actual;
|
|
551
|
+
}
|
|
552
|
+
return candidate;
|
|
553
|
+
}
|
|
554
|
+
function safeEvidenceRoot(workspace, value) {
|
|
555
|
+
if (!String(value || '').trim() || value.split(/[\\/]/).includes('..')) {
|
|
556
|
+
throw new Error('evidenceRoot 非法');
|
|
557
|
+
}
|
|
558
|
+
const target = safeInside(workspace, value, false, 'evidenceRoot');
|
|
559
|
+
const rel = relative(realpathSync(resolve(workspace)), target).split('\\').join('/');
|
|
560
|
+
if (!rel.startsWith('.dingtalk-agent/')) {
|
|
561
|
+
throw new Error('personal-event 原始证据必须位于 Workspace 的 gitignored .dingtalk-agent/ 下');
|
|
562
|
+
}
|
|
563
|
+
return target;
|
|
564
|
+
}
|
|
565
|
+
function assertNoSymlinkAncestors(root, path, label) {
|
|
566
|
+
const rel = relative(resolve(root), resolve(path));
|
|
567
|
+
let current = resolve(root);
|
|
568
|
+
for (const part of rel.split(/[\\/]/).filter(Boolean)) {
|
|
569
|
+
current = join(current, part);
|
|
570
|
+
if (!existsSync(current))
|
|
571
|
+
break;
|
|
572
|
+
if (lstatSync(current).isSymbolicLink()) {
|
|
573
|
+
throw new Error(`${label} 不允许穿过符号链接`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
function safeName(value) {
|
|
578
|
+
return value.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'case';
|
|
579
|
+
}
|
|
580
|
+
function findString(value, keys) {
|
|
581
|
+
if (!value || typeof value !== 'object')
|
|
582
|
+
return '';
|
|
583
|
+
for (const key of keys) {
|
|
584
|
+
const found = value[key];
|
|
585
|
+
if (found !== undefined && found !== null && String(found).trim())
|
|
586
|
+
return String(found).trim();
|
|
587
|
+
}
|
|
588
|
+
for (const child of Object.values(value)) {
|
|
589
|
+
const found = findString(child, keys);
|
|
590
|
+
if (found)
|
|
591
|
+
return found;
|
|
592
|
+
}
|
|
593
|
+
return '';
|
|
594
|
+
}
|
|
595
|
+
//# sourceMappingURL=personal-event-evals.js.map
|