|
- import type { Page, Route } from '@playwright/test'
-
- export type TeachingIdentity = 'admin' | 'administrative' | 'teacher' | 'studentLeader' | 'studentOperator' | 'unionAdministrativeStudent'
- type Channel = 'VIRTUAL' | 'PHYSICAL' | 'CONFRONTATION'
- type AssignmentKind = 'TRAINING' | 'FORMAL_EXAM'
- type JsonRecord = Record<string, unknown>
- const stepCodeOf = (step: JsonRecord) => String(step.stepCode ?? step.code ?? step.id ?? '')
-
- export interface RecordedTeachingRequest {
- method: string
- path: string
- query: URLSearchParams
- body: JsonRecord
- }
-
- const allTeachingPages = [
- 'teaching.virtual', 'teaching.physical', 'teaching.confrontation', 'teaching.guides',
- 'teaching.immersive', 'teaching.exams', 'teaching.analytics',
- ]
- const managerActions = ['teaching.tasks.create', 'teaching.tasks.publish', 'teaching.tasks.withdraw', 'teaching.tasks.review', 'teaching.tasks.archive']
- const learnerActions = ['teaching.tasks.accept', 'teaching.tasks.submit', 'teaching.virtual.execute', 'teaching.physical.execute', 'teaching.confrontation.execute', 'teaching.immersive.execute']
-
- const permissions: Record<TeachingIdentity, string[]> = {
- admin: [...allTeachingPages, ...managerActions, ...learnerActions, 'teaching.confrontation.configure'],
- administrative: ['teaching.virtual', 'teaching.physical', 'teaching.confrontation', 'teaching.immersive', 'teaching.exams', 'teaching.analytics'],
- teacher: ['teaching.virtual', 'teaching.physical', 'teaching.confrontation', 'teaching.immersive', 'teaching.exams', 'teaching.analytics', ...managerActions, 'teaching.confrontation.configure', 'teaching.immersive.execute'],
- studentLeader: ['teaching.virtual', 'teaching.physical', 'teaching.confrontation', 'teaching.guides', 'teaching.immersive', 'teaching.exams', ...learnerActions],
- studentOperator: ['teaching.virtual', 'teaching.physical', 'teaching.confrontation', 'teaching.guides', 'teaching.immersive', 'teaching.exams', ...learnerActions],
- unionAdministrativeStudent: ['teaching.virtual', 'teaching.physical', 'teaching.confrontation', 'teaching.guides', 'teaching.immersive', 'teaching.exams', 'teaching.analytics', ...learnerActions],
- }
-
- const roleNames = { admin: '管理员', administrative: '行政', teacher: '教员', student: '学员' }
-
- const member = (
- id: string,
- userId: string,
- username: string,
- displayName: string,
- memberType: 'INSTRUCTOR' | 'LEARNER' | 'OBSERVER',
- positionCode: string,
- teamCode: 'NEUTRAL' | 'RED' | 'BLUE',
- groupCode: string,
- sortOrder: number,
- ) => ({
- id, userId, username, displayName, departmentId: '110', departmentName: '维修教研室',
- memberType, roleCode: memberType === 'INSTRUCTOR' ? 'teacher' : memberType === 'LEARNER' ? 'student' : 'observer',
- teamCode, groupCode, positionCode, stationCode: '', sortOrder, version: 1,
- })
-
- export const teachingMembers = {
- instructor: member('member-teacher', '2', 'teacher', '周教员', 'INSTRUCTOR', 'INSTRUCTOR', 'NEUTRAL', 'INSTRUCTORS', 0),
- leader: member('member-red-leader', '4', 'student.leader', '红方队长', 'LEARNER', 'TEAM_LEADER', 'RED', 'GROUP-RED', 1),
- operator: member('member-red-operator', '5', 'student.operator', '红方操作员', 'LEARNER', 'OPERATOR', 'RED', 'GROUP-RED', 2),
- commander: member('member-blue-commander', '7', 'student.commander', '蓝方指挥员', 'LEARNER', 'COMMANDER', 'BLUE', 'GROUP-BLUE', 3),
- observer: member('member-observer', '6', 'observer', '安全观察员', 'OBSERVER', 'OBSERVER', 'NEUTRAL', 'OBSERVERS', 4),
- }
-
- const twoStepDefinition = (channel: Channel) => ({
- schema: 'unreal-tran.training',
- version: '2.0',
- scenario: {
- schema: 'unreal-tran.scene', version: '2.0',
- objects: [{
- id: `${channel.toLowerCase()}-equipment`, type: 'model', name: '履带式挖掘机训练模型',
- assetUrl: 'content://sha256/contract-excavator', assetCode: 'EXCAVATOR_A',
- targetProjectId: 'asset-project-1', targetVersionId: 'asset-version-1',
- position: [0, 0, 0], rotation: [0, 0, 0], scale: [1, 1, 1], visible: true,
- semantic: { interactionId: 'excavator', operable: true },
- }],
- },
- steps: channel === 'PHYSICAL'
- ? [
- { code: 'STEP-01', title: '确认设备断电', publicSummary: '完成现场断电与挂牌。', targetId: `${channel.toLowerCase()}-equipment`, physicalExecution: { source: 'MANUAL', eventType: 'MANUAL_CONFIRM_POWER_OFF', manualConfirmAllowed: true } },
- { code: 'STEP-02', title: '确认液压卸压', publicSummary: '完成液压系统卸压。', targetId: `${channel.toLowerCase()}-equipment`, physicalExecution: { source: 'MANUAL', eventType: 'MANUAL_CONFIRM_PRESSURE_RELEASED', manualConfirmAllowed: true } },
- ]
- : channel === 'CONFRONTATION'
- ? [
- { code: 'STEP-01', title: '队长下达处置指令', publicSummary: '共享任务由队长发起。', actionCode: 'ISSUE_COMMAND', allowedPositions: ['TEAM_LEADER'], targetId: `${channel.toLowerCase()}-equipment` },
- { code: 'STEP-02', title: '操作员执行故障隔离', publicSummary: '操作员按指令隔离故障。', actionCode: 'ISOLATE_FAULT', requiredPositions: ['OPERATOR'], targetId: `${channel.toLowerCase()}-equipment` },
- ]
- : [
- { stepCode: 'STEP-01', title: '识别作业对象', publicSummary: '确认训练对象与安全边界。', actionCode: 'IDENTIFY_TARGET', targetId: `${channel.toLowerCase()}-equipment` },
- { stepCode: 'STEP-02', title: '完成检修操作', publicSummary: '按流程完成检修并复核。', actionCode: 'COMPLETE_REPAIR', targetId: `${channel.toLowerCase()}-equipment` },
- ],
- runtime: { submitPositionCodes: channel === 'CONFRONTATION' ? ['TEAM_LEADER', 'COMMANDER'] : [] },
- modeConfig: channel === 'CONFRONTATION' ? {
- objective: '红蓝双方按岗位协同完成液压故障隔离',
- roundDurationMinutes: 20,
- teamSize: 2,
- winRule: '在安全约束下按完成度与用时综合判定',
- redTeamName: '红方维修组',
- blueTeamName: '蓝方保障组',
- submitPositions: ['TEAM_LEADER', 'COMMANDER'],
- teams: [
- { code: 'RED', name: '红方维修组', color: '#d25c5c', size: 2 },
- { code: 'BLUE', name: '蓝方保障组', color: '#4b7ed6', size: 2 },
- ],
- } : {},
- })
-
- const assignment = (id: string, channel: Channel, kind: AssignmentKind, audienceType: 'ASSIGNED' | 'COMMON', collaborationMode: 'INDIVIDUAL' | 'TEAM') => ({
- id,
- code: kind === 'FORMAL_EXAM' ? 'EXAM-2026-001' : `TASK-${channel}-001`,
- name: kind === 'FORMAL_EXAM' ? `${channel === 'PHYSICAL' ? '实装' : channel === 'CONFRONTATION' ? '对抗' : '虚拟'}维修正式考核` : `${channel === 'PHYSICAL' ? '实装' : channel === 'CONFRONTATION' ? '对抗' : '虚拟'}维修训练`,
- description: '状态化 UI 契约测试任务。', channel, assignmentKind: kind,
- executionMode: kind === 'FORMAL_EXAM' ? 'EXAM' : 'PRACTICE', audienceType, collaborationMode,
- contentProjectId: 'training-project-1', contentVersionId: 'training-version-1',
- definitionSnapshot: twoStepDefinition(channel), definitionChecksum: `checksum-${id}`,
- digitalHumanAllowed: kind !== 'FORMAL_EXAM', ownerUserId: '2', ownerUsername: 'teacher', ownerName: '周教员',
- departmentId: '110', departmentName: '维修教研室', status: 'PUBLISHED',
- scheduleStartAt: 1786383600, dueAt: 1786473600, publishedAt: 1786380000,
- withdrawnAt: null, archivedAt: null, lifecycleReason: '', version: 3,
- addTime: 1786300800, updateTime: '2026-08-11T08:00:00+08:00', serverNow: 1786387200,
- members: audienceType === 'COMMON'
- ? [teachingMembers.instructor]
- : channel === 'CONFRONTATION'
- ? Object.values(teachingMembers)
- : [teachingMembers.instructor, teachingMembers.leader],
- runCount: 1, pendingReviewCount: 0,
- })
-
- const assignmentBrief = (item: JsonRecord) => ({
- id: item.id, code: item.code, name: item.name, channel: item.channel, assignmentKind: item.assignmentKind,
- executionMode: item.executionMode, audienceType: item.audienceType, collaborationMode: item.collaborationMode,
- digitalHumanAllowed: item.digitalHumanAllowed, status: item.status,
- scheduleStartAt: item.scheduleStartAt, dueAt: item.dueAt, serverNow: item.serverNow,
- })
-
- const runFor = (id: string, task: JsonRecord, status: 'PENDING' | 'ACCEPTED' | 'IN_PROGRESS' | 'SUBMITTED' | 'REVIEWED', learner = teachingMembers.leader) => ({
- id, code: `RUN-${String(task.channel)}-${id.slice(-3).toUpperCase()}`, assignmentId: task.id,
- memberId: learner.id, subjectType: task.collaborationMode === 'TEAM' ? 'TEAM' : 'MEMBER',
- subjectCode: task.collaborationMode === 'TEAM' ? learner.teamCode : learner.id,
- attemptNo: 1, status, progressPercent: status === 'SUBMITTED' || status === 'REVIEWED' ? 100 : 0,
- currentStepCode: 'STEP-01', checkpoint: {}, automaticResult: {}, submission: status === 'SUBMITTED' || status === 'REVIEWED' ? { summary: '已完成' } : null,
- score: status === 'REVIEWED' ? 88 : null, passed: status === 'REVIEWED' ? true : null,
- reviewFeedback: status === 'REVIEWED' ? '流程与操作符合要求。' : '', reviewRubric: {},
- acceptedAt: status === 'PENDING' ? null : 1786386000, startedAt: ['PENDING', 'ACCEPTED'].includes(status) ? null : 1786387200,
- submittedAt: status === 'SUBMITTED' || status === 'REVIEWED' ? 1786390800 : null,
- reviewedAt: status === 'REVIEWED' ? 1786394400 : null, reviewedByUserId: status === 'REVIEWED' ? '2' : null,
- reviewerName: status === 'REVIEWED' ? '周教员' : '', archivedAt: null, version: 2,
- addTime: 1786386000, updateTime: '2026-08-11T10:00:00+08:00', assignment: assignmentBrief(task),
- member: learner, members: task.collaborationMode === 'TEAM' ? Object.values(teachingMembers) : [teachingMembers.instructor, learner],
- })
-
- const envelope = (data: unknown, requestId = 'ute2e-teaching-state') => ({
- code: 200, message: '成功', data, timestamp: '2026-08-11T10:00:00+08:00', requestId,
- })
-
- const pageResult = (records: unknown[], page = 1, size = 10) => ({ records, total: records.length, page, size })
-
- const bodyOf = (route: Route): JsonRecord => {
- const text = route.request().postData()
- if (!text) return {}
- const value = JSON.parse(text) as unknown
- return value && typeof value === 'object' && !Array.isArray(value) ? value as JsonRecord : {}
- }
-
- const fulfill = (route: Route, data: unknown, requestId?: string) => route.fulfill({
- status: 200, contentType: 'application/json', body: JSON.stringify(envelope(data, requestId)),
- })
-
- const reject = (route: Route, status: number, message: string) => route.fulfill({
- status, contentType: 'application/json', body: JSON.stringify({ code: status * 100, message, data: null, timestamp: Date.now(), requestId: 'ute2e-rejected' }),
- })
-
- export class TeachingContractMock {
- identity: TeachingIdentity
- readonly requests: RecordedTeachingRequest[] = []
- readonly assignments = new Map<string, JsonRecord>()
- readonly runs = new Map<string, JsonRecord>()
- readonly events = new Map<string, JsonRecord[]>()
- readonly interventions: JsonRecord[] = []
- readonly deniedPermissions = new Set<string>()
- administrativeRunsRejected = 0
- administrativeAnalyticsRecordsRejected = 0
-
- constructor(identity: TeachingIdentity) {
- this.identity = identity
- const tasks = [
- assignment('assignment-virtual-common', 'VIRTUAL', 'TRAINING', 'COMMON', 'INDIVIDUAL'),
- assignment('assignment-physical', 'PHYSICAL', 'TRAINING', 'ASSIGNED', 'INDIVIDUAL'),
- assignment('assignment-confrontation', 'CONFRONTATION', 'TRAINING', 'ASSIGNED', 'TEAM'),
- assignment('assignment-exam', 'VIRTUAL', 'FORMAL_EXAM', 'ASSIGNED', 'INDIVIDUAL'),
- assignment('assignment-exam-physical', 'PHYSICAL', 'FORMAL_EXAM', 'ASSIGNED', 'INDIVIDUAL'),
- assignment('assignment-exam-confrontation', 'CONFRONTATION', 'FORMAL_EXAM', 'ASSIGNED', 'TEAM'),
- ]
- tasks.forEach((task) => this.assignments.set(String(task.id), task))
- this.runs.set('run-physical', runFor('run-physical', this.requireAssignment('assignment-physical'), 'ACCEPTED'))
- this.runs.set('run-confrontation', runFor('run-confrontation', this.requireAssignment('assignment-confrontation'), 'IN_PROGRESS'))
- this.runs.set('run-exam', runFor('run-exam', this.requireAssignment('assignment-exam'), 'IN_PROGRESS'))
- }
-
- setIdentity(identity: TeachingIdentity) { this.identity = identity }
-
- denyPermissions(...codes: string[]) { codes.forEach((code) => this.deniedPermissions.add(code)) }
-
- seedImmersiveRun() {
- const task = this.requireAssignment('assignment-virtual-common')
- const runtime = runFor('run-immersive', task, 'IN_PROGRESS')
- runtime.currentStepCode = 'STEP-01'
- this.runs.set(String(runtime.id), runtime)
- }
-
- seedVirtualReviewedRun() {
- const task = this.requireAssignment('assignment-virtual-common')
- const runtime = runFor('run-virtual-reviewed', task, 'REVIEWED')
- runtime.currentStepCode = 'STEP-02'
- runtime.progressPercent = 100
- runtime.score = 92
- runtime.passed = true
- runtime.reviewFeedback = '步骤完整,安全确认和检修复核符合要求。'
- runtime.checkpoint = { completedStepCodes: ['STEP-01', 'STEP-02'] }
- this.runs.set(String(runtime.id), runtime)
- this.events.set(String(runtime.id), [
- {
- id: 'event-virtual-reviewed-1', code: 'EVENT-VIRTUAL-001', assignmentId: task.id, runId: runtime.id,
- targetType: 'RUN', targetId: runtime.id, type: 'training.action.completed', title: '完成:识别作业对象',
- summary: '确认训练对象与安全边界。', source: 'web', visibility: 'PUBLIC', payload: { completedStepCode: 'STEP-01' },
- actorUserId: teachingMembers.leader.userId, actorDisplayName: teachingMembers.leader.displayName, occurredAt: 1786387260,
- },
- {
- id: 'event-virtual-reviewed-2', code: 'EVENT-VIRTUAL-002', assignmentId: task.id, runId: runtime.id,
- targetType: 'RUN', targetId: runtime.id, type: 'training.action.completed', title: '完成:完成检修操作',
- summary: '按流程完成检修并复核。', source: 'web', visibility: 'PUBLIC', payload: { completedStepCode: 'STEP-02' },
- actorUserId: teachingMembers.leader.userId, actorDisplayName: teachingMembers.leader.displayName, occurredAt: 1786387560,
- },
- ])
- return runtime
- }
-
- seedConfrontationMonitorRuns() {
- const task = this.requireAssignment('assignment-confrontation')
- const active = this.runs.get('run-confrontation')!
- active.status = 'IN_PROGRESS'
- active.subjectCode = 'RED'
- active.members = [teachingMembers.instructor, teachingMembers.leader, teachingMembers.operator]
- active.progressPercent = 50
- active.currentStepCode = 'STEP-02'
- this.events.set(String(active.id), [{
- id: 'event-confrontation-red-1', code: 'EVENT-CON-RED-001', assignmentId: task.id, runId: active.id,
- targetType: 'RUN', targetId: active.id, type: 'training.action.completed', title: '队长已下达处置指令',
- summary: '红方进入故障隔离阶段。', source: 'web', visibility: 'PUBLIC', payload: { completedStepCode: 'STEP-01' },
- actorUserId: teachingMembers.leader.userId, actorDisplayName: teachingMembers.leader.displayName, occurredAt: 1786387320,
- }])
-
- const blue = runFor('run-confrontation-blue', task, 'ACCEPTED', teachingMembers.commander)
- blue.subjectCode = 'BLUE'
- blue.members = [teachingMembers.instructor, teachingMembers.commander]
- blue.currentStepCode = 'STEP-01'
- blue.progressPercent = 0
- this.runs.set(String(blue.id), blue)
- this.events.set(String(blue.id), [])
- return { active, blue }
- }
-
- seedExpiredRun() {
- const task = this.requireAssignment('assignment-virtual-common')
- task.dueAt = Number(task.serverNow) - 60
- const runtime = runFor('run-timeout', task, 'IN_PROGRESS')
- runtime.startedAt = Number(task.serverNow) - 3_600
- runtime.progressPercent = 50
- this.runs.set(String(runtime.id), runtime)
- return runtime
- }
-
- seedTerminableRun() {
- const task = this.requireAssignment('assignment-virtual-common')
- task.dueAt = null
- const runtime = runFor('run-terminate', task, 'IN_PROGRESS')
- runtime.startedAt = Number(task.serverNow) - 600
- runtime.progressPercent = 25
- this.runs.set(String(runtime.id), runtime)
- return runtime
- }
-
- seedConfrontationTerminationCases() {
- const task = this.requireAssignment('assignment-confrontation')
- task.dueAt = null
- const active = this.runs.get('run-confrontation')!
- active.status = 'IN_PROGRESS'
- active.startedAt = Number(task.serverNow) - 21 * 60
- active.progressPercent = 50
- const accepted = runFor('run-confrontation-queued', task, 'ACCEPTED', teachingMembers.operator)
- this.runs.set(String(accepted.id), accepted)
- return { active, accepted }
- }
-
- keepOnlyRuns(...runIds: string[]) {
- const selected = runIds.map((id) => this.runs.get(id)).filter((item): item is JsonRecord => Boolean(item))
- this.runs.clear()
- selected.forEach((item) => this.runs.set(String(item.id), item))
- }
-
- count(method: string, path: string | RegExp) {
- return this.requests.filter((item) => item.method === method && (typeof path === 'string' ? item.path === path : path.test(item.path))).length
- }
-
- last(method: string, path: string | RegExp) {
- return [...this.requests].reverse().find((item) => item.method === method && (typeof path === 'string' ? item.path === path : path.test(item.path)))
- }
-
- private requireAssignment(id: string) {
- const item = this.assignments.get(id)
- if (!item) throw new Error(`测试任务不存在:${id}`)
- return item
- }
-
- private currentUserId() {
- if (this.identity === 'studentOperator') return '5'
- if (this.identity === 'studentLeader' || this.identity === 'unionAdministrativeStudent') return '4'
- if (this.identity === 'teacher') return '2'
- if (this.identity === 'administrative') return '3'
- return '1'
- }
-
- private profile() {
- const union = this.identity === 'unionAdministrativeStudent'
- const role = this.identity === 'teacher' ? 'teacher'
- : this.identity === 'administrative' || union ? 'administrative'
- : this.identity === 'studentLeader' || this.identity === 'studentOperator' ? 'student' : 'admin'
- const userId = this.currentUserId()
- const displayName = this.identity === 'studentOperator' ? '红方操作员' : this.identity === 'studentLeader' || union ? '红方队长' : roleNames[role]
- const roles = union
- ? [
- { id: 'role-administrative', code: 'administrative', name: '行政', shortName: '行', status: 1, builtIn: 1, isSuperAdmin: 0, dataScopeCode: 'DEPARTMENT' },
- { id: 'role-student', code: 'student', name: '学员', shortName: '学', status: 1, builtIn: 1, isSuperAdmin: 0, dataScopeCode: 'SELF' },
- ]
- : [{ id: `role-${role}`, code: role, name: roleNames[role], shortName: roleNames[role].slice(0, 1), status: 1, builtIn: 1, isSuperAdmin: role === 'admin' ? 1 : 0, dataScopeCode: role === 'admin' ? 'ALL' : role === 'student' ? 'SELF' : 'DEPARTMENT' }]
- return {
- user: { id: userId, username: this.identity, displayName, departmentId: '110', departmentName: '维修教研室', mustChangePassword: false, version: 1 },
- activeRoleId: union ? 'role-administrative' : `role-${role}`, roles,
- permissions: permissions[this.identity].filter((permission) => !this.deniedPermissions.has(permission)), authorizationMode: union ? 'UNION' : 'SINGLE_ACTIVE', loginTime: 1786387200,
- }
- }
-
- private currentMember(run: JsonRecord) {
- const values = Array.isArray(run.members) ? run.members as JsonRecord[] : []
- return values.find((item) => item.userId === this.currentUserId() && item.memberType === 'LEARNER')
- }
-
- private visibleRuns() {
- const values = [...this.runs.values()]
- if (this.identity === 'teacher' || this.identity === 'admin') return values
- const userId = this.currentUserId()
- return values.filter((item) => {
- const values = Array.isArray(item.members) ? item.members as JsonRecord[] : []
- return values.some((member) => member.userId === userId && member.memberType === 'LEARNER')
- })
- }
-
- private stepsFor(run: JsonRecord) {
- const task = this.requireAssignment(String(run.assignmentId))
- const definition = task.definitionSnapshot as JsonRecord
- return (definition.steps as JsonRecord[]) ?? []
- }
-
- private advanceRun(route: Route, run: JsonRecord, completedStepCode: string, actionCode?: string, respond = true) {
- const steps = this.stepsFor(run)
- const currentCode = String(run.currentStepCode)
- if (completedStepCode !== currentCode) return reject(route, 409, `完成步骤必须等于当前待办步骤 ${currentCode}`)
- const index = steps.findIndex((step) => stepCodeOf(step) === completedStepCode)
- if (index < 0) return reject(route, 400, '步骤不属于发布任务')
- const task = this.requireAssignment(String(run.assignmentId))
- if (task.channel === 'CONFRONTATION') {
- const current = this.currentMember(run)
- const allowed = (steps[index]!.allowedPositions ?? steps[index]!.requiredPositions ?? []) as string[]
- if (!current || !allowed.includes(String(current.positionCode))) return reject(route, 403, '当前岗位不能执行此动作')
- if (actionCode !== steps[index]!.actionCode) return reject(route, 400, 'actionCode 与当前步骤不匹配')
- }
- const next = steps[Math.min(index + 1, steps.length - 1)]!
- run.progressPercent = Math.round(((index + 1) * 100) / steps.length)
- run.currentStepCode = stepCodeOf(next)
- run.version = Number(run.version) + 1
- const events = this.events.get(String(run.id)) ?? []
- events.push({
- id: `event-${events.length + 1}`, code: `EVENT-${events.length + 1}`, assignmentId: run.assignmentId, runId: run.id,
- targetType: 'RUN', targetId: run.id, type: 'training.action.completed', title: `完成:${steps[index]!.title}`,
- summary: steps[index]!.publicSummary, source: 'web', visibility: 'PUBLIC', payload: { completedStepCode, actionCode },
- actorUserId: this.currentUserId(), actorDisplayName: this.profile().user.displayName, occurredAt: 1786387200 + events.length,
- })
- this.events.set(String(run.id), events)
- return respond ? fulfill(route, run, 'ute2e-progress-authoritative') : Promise.resolve()
- }
-
- async install(page: Page) {
- await page.addInitScript(() => sessionStorage.setItem('unreal-tran:web:access-token:v1', 'teaching-contract-token'))
- await page.route('**/api/auth/v1/auth/me', (route) => fulfill(route, this.profile(), 'ute2e-teaching-me'))
- await page.route('**/api/auth/v1/menus/navigation', (route) => fulfill(route, [], 'ute2e-teaching-menu'))
- await page.route('**/api/auth/v1/directory/teaching-members**', (route) => fulfill(route, [
- { id: '4', username: 'student.leader', displayName: '红方队长', departmentId: '110', departmentName: '维修教研室', roleCodes: ['student'] },
- { id: '5', username: 'student.operator', displayName: '红方操作员', departmentId: '110', departmentName: '维修教研室', roleCodes: ['student'] },
- ], 'ute2e-teaching-directory'))
- await page.route('**/api/tran/v1/content/catalog**', (route) => fulfill(route, pageResult([{
- projectId: 'training-project-1', versionId: 'training-version-1', type: 'TRAINING', code: 'TRAIN-001', name: '挖掘机液压系统训练', categoryCode: 'VIRTUAL', coverUri: '', versionNumber: 2, versionStatus: 'PUBLISHED', publishedAt: '2026-08-11T08:00:00+08:00', content: { steps: [] },
- }]), 'ute2e-teaching-catalog'))
- await page.route('**/api/tran/v1/teaching/**', (route) => this.handleTeaching(route))
- }
-
- private async handleTeaching(route: Route) {
- const request = route.request()
- const url = new URL(request.url())
- const path = url.pathname.replace(/^.*\/teaching/, '')
- const method = request.method()
- const body = bodyOf(route)
- this.requests.push({ method, path, query: new URLSearchParams(url.searchParams), body })
-
- if (method === 'GET' && path === '/guides') return fulfill(route, pageResult([{
- projectId: 'guide-project-1', versionId: 'guide-version-1', code: 'GUIDE-001', name: '挖掘机检修作业指导书', description: '标准维修作业流程', categoryCode: '维修作业', coverUri: '', equipment: '履带式挖掘机', specification: '教学训练型', chapterCount: 2, stepCount: 3, versionNumber: 3, versionCode: 'V3', publishedAt: 1786387200,
- }]), 'ute2e-guides')
- if (method === 'GET' && path === '/guides/guide-project-1') return fulfill(route, {
- projectId: 'guide-project-1', versionId: 'guide-version-1', code: 'GUIDE-001', name: '挖掘机检修作业指导书', description: '标准维修作业流程', categoryCode: '维修作业', coverUri: '', versionNumber: 3, versionCode: 'V3', publishedAt: 1786387200,
- content: {
- equipment: '履带式挖掘机', specification: '教学训练型',
- chapters: [
- {
- id: 'chapter-1', title: '安全准备', order: 1,
- steps: [
- { id: 'step-1', title: '断电隔离', summary: '确认停机、断电并完成安全隔离。', safety: '执行挂牌上锁。', acceptance: '设备处于零能量状态。', duration: '5分钟', type: 'CHECK', tools: '安全锁具', parameters: {}, media: '', part: '动力总成', anchor: '', animation: '', order: 1 },
- { id: 'step-2', title: '压力释放', summary: '缓慢释放液压系统残余压力。', safety: '佩戴护目镜并确认卸压方向无人。', acceptance: '压力表回零且系统无残压。', duration: '8分钟', type: 'SAFETY', tools: '压力表', parameters: { '目标压力': '0 MPa' }, media: '', part: '液压回路', anchor: '', animation: '', order: 2 },
- ],
- },
- {
- id: 'chapter-2', title: '检修复核', order: 2,
- steps: [{ id: 'step-3', title: '复装与复测', summary: '完成部件复装并按标准执行功能复测。', safety: '清点工具并恢复防护装置。', acceptance: '动作平稳、无泄漏且记录完整。', duration: '12分钟', type: 'VERIFY', tools: '扭矩扳手', parameters: { '复测次数': '2 次' }, media: '', part: '执行机构', anchor: '', animation: '', order: 1 }],
- },
- ],
- },
- }, 'ute2e-guide-detail')
-
- if (method === 'GET' && path === '/assignments/summary') {
- const channel = url.searchParams.get('channel')
- const kind = url.searchParams.get('assignmentKind') ?? 'TRAINING'
- const tasks = [...this.assignments.values()].filter((item) => (!channel || item.channel === channel) && item.assignmentKind === kind)
- return fulfill(route, { assignments: tasks.length, draft: 0, published: tasks.length, withdrawn: 0, archived: 0, pendingRuns: 0, activeRuns: this.visibleRuns().filter((item) => item.status === 'IN_PROGRESS').length, submittedRuns: this.visibleRuns().filter((item) => item.status === 'SUBMITTED').length, reviewedRuns: this.visibleRuns().filter((item) => item.status === 'REVIEWED').length, byChannel: Object.fromEntries(tasks.map((item) => [String(item.channel), 1])) }, 'ute2e-summary')
- }
- if (method === 'GET' && path === '/assignments') {
- const channel = url.searchParams.get('channel')
- const kind = url.searchParams.get('assignmentKind') ?? 'TRAINING'
- const tasks = [...this.assignments.values()].filter((item) => (!channel || item.channel === channel) && item.assignmentKind === kind)
- return fulfill(route, pageResult(tasks), 'ute2e-assignments')
- }
- if (method === 'POST' && path === '/assignments') {
- if (typeof body.scheduleStartAt !== 'number' || typeof body.dueAt !== 'number' || body.scheduleStartAt >= 1_000_000_000_000 || body.dueAt >= 1_000_000_000_000) return reject(route, 400, '正式考核时间必须是 Unix 秒')
- if (body.assignmentKind === 'FORMAL_EXAM' && (body.executionMode !== 'EXAM' || body.digitalHumanAllowed !== false)) return reject(route, 400, '正式考核必须使用 EXAM 且禁用数字教员')
- const created = { ...assignment('assignment-created-exam', String(body.channel) as Channel, String(body.assignmentKind) as AssignmentKind, 'ASSIGNED', 'INDIVIDUAL'), ...body, id: 'assignment-created-exam', version: 0, status: 'DRAFT' }
- this.assignments.set(String(created.id), created)
- return fulfill(route, created, 'ute2e-assignment-created')
- }
-
- const assignmentAssetMatch = path.match(/^\/assignments\/([^/]+)\/assets$/)
- if (method === 'GET' && assignmentAssetMatch) return fulfill(route, this.assetList(), 'ute2e-assignment-assets')
- const assignmentFaultMatch = path.match(/^\/assignments\/([^/]+)\/faults$/)
- if (method === 'GET' && assignmentFaultMatch) {
- if (this.identity === 'administrative') return reject(route, 403, '行政身份不可查看故障细节')
- return fulfill(route, [{
- id: 'assignment-fault-public', assignmentId: assignmentFaultMatch[1], faultId: this.identity === 'teacher' || this.identity === 'admin' ? 'fault-1' : null,
- faultCode: this.identity === 'teacher' || this.identity === 'admin' ? 'HYDRAULIC.VALVE.STUCK' : '',
- faultName: this.identity === 'teacher' || this.identity === 'admin' ? '液压阀卡滞' : '', severity: this.identity === 'teacher' || this.identity === 'admin' ? 'HIGH' : '',
- publicSymptom: '动臂响应迟缓且压力波动', privateTruth: this.identity === 'teacher' || this.identity === 'admin' ? { text: '液压阀芯卡滞真值' } : null,
- triggerConfig: this.identity === 'teacher' || this.identity === 'admin' ? { hidden: '内部触发条件' } : null,
- targetScope: 'TEAM', targetTeamCode: 'RED', targetGroupCode: 'GROUP-RED', sortOrder: 0,
- }], 'ute2e-assignment-faults')
- }
- const interventionListMatch = path.match(/^\/assignments\/([^/]+)\/interventions$/)
- if (interventionListMatch && method === 'GET') return fulfill(route, this.interventions, 'ute2e-interventions')
- if (interventionListMatch && method === 'POST') {
- if (!['teacher', 'admin'].includes(this.identity)) return reject(route, 403, '当前身份无教学干预权限')
- if (!['ASSIGNMENT', 'TEAM', 'MEMBER'].includes(String(body.targetScope))) return reject(route, 400, '干预作用范围不符合契约')
- if (body.targetScope === 'TEAM' && !['RED', 'BLUE', 'NEUTRAL'].includes(String(body.targetTeamCode))) return reject(route, 400, 'TEAM 干预必须携带目标队伍')
- if (body.targetScope === 'MEMBER' && !body.targetMemberId) return reject(route, 400, 'MEMBER 干预必须携带目标成员')
- if (typeof body.publicSummary !== 'string' || !body.publicSummary.trim()) return reject(route, 400, '干预必须提供公开说明')
- if (/privateTruth|triggerConfig|solution|answer|truth/i.test(JSON.stringify(body.publicPayload ?? {}))) return reject(route, 400, '公开干预载荷不能包含故障真值或解决方案')
- const privatePayload = body.privatePayload ?? {}
- const payload = Object.keys(privatePayload as JsonRecord).length
- ? { public: body.publicPayload ?? {}, private: privatePayload }
- : body.publicPayload ?? {}
- const item = {
- id: `intervention-${this.interventions.length + 1}`, assignmentId: interventionListMatch[1],
- code: body.code, type: body.type, targetScope: body.targetScope,
- targetMemberId: body.targetScope === 'MEMBER' ? body.targetMemberId : '',
- targetTeamCode: body.targetScope === 'TEAM' ? body.targetTeamCode : '',
- targetGroupCode: body.targetScope === 'TEAM' ? body.targetGroupCode ?? '' : '',
- publicSummary: body.publicSummary, payload, status: 'DRAFT', createdByUserId: '2', createdByName: '周教员', activatedAt: null, revokedAt: null, version: 0,
- }
- this.interventions.push(item)
- return fulfill(route, item, 'ute2e-intervention-created')
- }
- const assignmentDetailMatch = path.match(/^\/assignments\/([^/]+)$/)
- if (method === 'GET' && assignmentDetailMatch) {
- const item = this.requireAssignment(assignmentDetailMatch[1]!)
- const learnerProjection = !['admin', 'teacher'].includes(this.identity) && item.assignmentKind === 'FORMAL_EXAM'
- ? { ...item, definitionSnapshot: null }
- : item
- return fulfill(route, learnerProjection, 'ute2e-assignment-detail')
- }
- const acceptMatch = path.match(/^\/assignments\/([^/]+)\/accept$/)
- if (method === 'POST' && acceptMatch) {
- const task = this.requireAssignment(acceptMatch[1]!)
- if (body.assignmentVersion !== task.version || body.teamCode !== 'NEUTRAL' || body.groupCode !== 'INDIVIDUAL' || body.positionCode !== 'LEARNER') return reject(route, 400, '领取 DTO 不符合契约')
- const pending = [...this.runs.values()].find((item) => item.assignmentId === task.id && item.status === 'PENDING')
- if (pending) {
- pending.status = 'ACCEPTED'
- pending.acceptedAt = 1786386000
- pending.version = Number(pending.version) + 1
- return fulfill(route, pending, 'ute2e-assigned-task-accepted')
- }
- const created = runFor('run-virtual-common', task, 'ACCEPTED', teachingMembers.leader)
- this.runs.set(String(created.id), created)
- return fulfill(route, created, 'ute2e-assignment-accepted')
- }
-
- if (method === 'GET' && path === '/runs') {
- if (this.identity === 'administrative') { this.administrativeRunsRejected += 1; return reject(route, 403, '行政身份不可读取运行明细') }
- const assignmentId = url.searchParams.get('assignmentId')
- const channel = url.searchParams.get('channel')
- const kind = url.searchParams.get('assignmentKind')
- const values = this.visibleRuns().filter((item) => (!assignmentId || item.assignmentId === assignmentId) && (!channel || (item.assignment as JsonRecord).channel === channel) && (!kind || (item.assignment as JsonRecord).assignmentKind === kind))
- return fulfill(route, pageResult(values), 'ute2e-runs')
- }
- const runAssetsMatch = path.match(/^\/runs\/([^/]+)\/assets$/)
- if (method === 'GET' && runAssetsMatch) return fulfill(route, this.assetList(), 'ute2e-run-assets')
- const runExecutionMatch = path.match(/^\/runs\/([^/]+)\/execution$/)
- if (method === 'GET' && runExecutionMatch) {
- const item = this.runs.get(runExecutionMatch[1]!)
- if (!item) return reject(route, 404, '运行不存在')
- const task = this.requireAssignment(String(item.assignmentId))
- if (task.assignmentKind !== 'FORMAL_EXAM' || item.status !== 'IN_PROGRESS') return reject(route, 409, '仅正式考核进行中运行提供执行投影')
- const definition = task.definitionSnapshot as JsonRecord
- const steps = (definition.steps as JsonRecord[]) ?? []
- const index = Math.max(0, steps.findIndex((step) => stepCodeOf(step) === item.currentStepCode))
- return fulfill(route, {
- runId: item.id,
- assignmentId: item.assignmentId,
- currentStepCode: item.currentStepCode,
- currentStepIndex: index,
- totalSteps: steps.length,
- progressPercent: item.progressPercent,
- currentStep: steps[index] ?? null,
- resources: definition.resources ?? [],
- scenario: definition.scenario ?? {},
- tools: [],
- runtime: { showStepPanel: false, allowReplay: false, allowRollback: false, digitalHumanAllowed: false },
- modeConfig: definition.modeConfig ?? {},
- examPolicy: { hintsAllowed: false, demoAllowed: false, replayAllowed: false, rollbackAllowed: false, digitalHumanAllowed: false },
- deadlineAt: task.dueAt,
- serverNow: task.serverNow,
- }, 'ute2e-run-execution')
- }
- const runEventsMatch = path.match(/^\/runs\/([^/]+)\/events$/)
- if (method === 'GET' && runEventsMatch) return fulfill(route, this.events.get(runEventsMatch[1]!) ?? [], 'ute2e-events')
- const runDetailMatch = path.match(/^\/runs\/([^/]+)$/)
- if (method === 'GET' && runDetailMatch) {
- const item = this.runs.get(runDetailMatch[1]!)
- return item ? fulfill(route, item, 'ute2e-run-detail') : reject(route, 404, '运行不存在')
- }
- const startMatch = path.match(/^\/runs\/([^/]+)\/start$/)
- if (method === 'POST' && startMatch) {
- const item = this.runs.get(startMatch[1]!)
- if (!item || item.status !== 'ACCEPTED' || body.version !== item.version) return reject(route, 409, '运行版本或状态冲突')
- item.status = 'IN_PROGRESS'; item.startedAt = 1786387200; item.version = Number(item.version) + 1
- return fulfill(route, item, 'ute2e-run-started')
- }
- const progressMatch = path.match(/^\/runs\/([^/]+)\/progress$/)
- if (method === 'PUT' && progressMatch) {
- const item = this.runs.get(progressMatch[1]!)
- if (!item || item.status !== 'IN_PROGRESS' || body.version !== item.version) return reject(route, 409, '运行版本或状态冲突')
- if ('percent' in body || 'currentStepCode' in body) return reject(route, 400, '进度与下一步骤必须由服务端派生')
- if (typeof body.completedStepCode !== 'string') return reject(route, 400, '缺少 completedStepCode')
- return this.advanceRun(route, item, body.completedStepCode, typeof body.actionCode === 'string' ? body.actionCode : undefined)
- }
- const physicalMatch = path.match(/^\/runs\/([^/]+)\/physical-events$/)
- if (method === 'POST' && physicalMatch) {
- const item = this.runs.get(physicalMatch[1]!)
- if (!item || item.status !== 'IN_PROGRESS' || body.runVersion !== item.version) return reject(route, 409, '实装运行版本或状态冲突')
- if (body.source !== 'MANUAL' || body.manualConfirmed !== true || typeof body.eventId !== 'string' || body.stepCode !== item.currentStepCode) return reject(route, 400, '实装事件 DTO 不符合契约')
- if (typeof body.occurredAt !== 'number' || body.occurredAt >= 1_000_000_000_000) return reject(route, 400, '现场事件时间必须是 Unix 秒')
- const currentStep = this.stepsFor(item).find((step) => stepCodeOf(step) === body.stepCode)
- const execution = currentStep?.physicalExecution as JsonRecord | undefined
- if (!execution || execution.source !== body.source || execution.eventType !== body.eventType || execution.manualConfirmAllowed === false) return reject(route, 400, '现场事件与当前步骤执行规则不匹配')
- await this.advanceRun(route, item, String(body.stepCode), undefined, false)
- return fulfill(route, { id: `physical-${this.count('POST', /physical-events$/)}`, eventId: body.eventId, assignmentId: item.assignmentId, runId: item.id, source: body.source, eventType: body.eventType, stepCode: body.stepCode, outcome: 'MATCHED', matchedRule: true, progressApplied: true, occurredAt: body.occurredAt, runVersion: item.version }, 'ute2e-physical-event')
- }
- const submitMatch = path.match(/^\/runs\/([^/]+)\/submit$/)
- if (method === 'POST' && submitMatch) {
- const item = this.runs.get(submitMatch[1]!)
- if (!item || item.status !== 'IN_PROGRESS' || item.progressPercent !== 100 || body.version !== item.version) return reject(route, 409, '运行未完成或版本冲突')
- const task = this.requireAssignment(String(item.assignmentId))
- if (task.collaborationMode === 'TEAM') {
- const current = this.currentMember(item)
- if (!current || !['TEAM_LEADER', 'COMMANDER'].includes(String(current.positionCode))) return reject(route, 403, '共享运行仅负责人可提交')
- }
- item.status = 'SUBMITTED'; item.submittedAt = 1786390800; item.submission = { summary: body.summary }; item.version = Number(item.version) + 1
- return fulfill(route, item, 'ute2e-run-submitted')
- }
- const timeoutMatch = path.match(/^\/runs\/([^/]+)\/timeout$/)
- if (method === 'POST' && timeoutMatch) {
- const item = this.runs.get(timeoutMatch[1]!)
- if (!item || !['PENDING', 'ACCEPTED', 'IN_PROGRESS'].includes(String(item.status)) || body.version !== item.version) return reject(route, 409, '超时结算运行版本或状态冲突')
- if (!['teacher', 'admin'].includes(this.identity)) return reject(route, 403, '当前身份无评定权限')
- const task = this.requireAssignment(String(item.assignmentId))
- const now = Number(task.serverNow)
- const definition = task.definitionSnapshot as JsonRecord
- const modeConfig = (definition.modeConfig ?? {}) as JsonRecord
- const roundSeconds = Number(modeConfig.durationSeconds ?? 0)
- || Number(modeConfig.roundDurationMinutes ?? modeConfig.roundDuration ?? 0) * 60
- const dueExpired = typeof task.dueAt === 'number' && now >= task.dueAt
- const roundExpired = task.channel === 'CONFRONTATION' && typeof item.startedAt === 'number' && roundSeconds > 0 && now >= item.startedAt + roundSeconds
- if (!dueExpired && !roundExpired) return reject(route, 409, '运行尚未超时')
- item.status = 'SUBMITTED'
- item.submission = { summary: '运行已由服务端执行超时结算。', timeout: true }
- item.submittedAt = now
- item.version = Number(item.version) + 1
- return fulfill(route, item, 'ute2e-run-timeout-submitted')
- }
- const terminateMatch = path.match(/^\/runs\/([^/]+)\/terminate$/)
- if (method === 'POST' && terminateMatch) {
- const item = this.runs.get(terminateMatch[1]!)
- if (!item || !['PENDING', 'ACCEPTED', 'IN_PROGRESS'].includes(String(item.status)) || body.version !== item.version) return reject(route, 409, '结束运行版本或状态冲突')
- if (!['teacher', 'admin'].includes(this.identity)) return reject(route, 403, '当前身份无评定权限')
- const task = this.requireAssignment(String(item.assignmentId))
- if (task.assignmentKind !== 'TRAINING' || task.dueAt != null || (task.channel === 'CONFRONTATION' && item.status === 'IN_PROGRESS')) return reject(route, 409, '当前运行应使用超时结算')
- if (typeof body.reason !== 'string' || !body.reason.trim() || body.reason.length > 500) return reject(route, 400, '结束运行必须提供 1 至 500 字原因')
- item.status = 'SUBMITTED'
- item.submission = { summary: `运行由教员结束:${body.reason.trim()}`, terminated: true }
- item.submittedAt = Number(task.serverNow)
- item.version = Number(item.version) + 1
- return fulfill(route, item, 'ute2e-run-terminated-submitted')
- }
- const reviewMatch = path.match(/^\/runs\/([^/]+)\/review$/)
- if (method === 'POST' && reviewMatch) {
- const item = this.runs.get(reviewMatch[1]!)
- if (!item || item.status !== 'SUBMITTED' || body.version !== item.version || typeof body.score !== 'number') return reject(route, 409, '评定 DTO 或状态冲突')
- item.status = 'REVIEWED'; item.score = body.score; item.passed = body.passed; item.reviewFeedback = body.feedback; item.reviewerName = '周教员'; item.reviewedAt = 1786394400; item.version = Number(item.version) + 1
- return fulfill(route, item, 'ute2e-run-reviewed')
- }
-
- const activateMatch = path.match(/^\/interventions\/([^/]+)\/activate$/)
- if (method === 'POST' && activateMatch) {
- const item = this.interventions.find((value) => value.id === activateMatch[1])
- if (!item || body.version !== item.version) return reject(route, 409, '干预版本冲突')
- item.status = 'ACTIVE'; item.activatedAt = 1786387200; item.version = Number(item.version) + 1
- return fulfill(route, item, 'ute2e-intervention-active')
- }
-
- if (method === 'GET' && path === '/faults') return fulfill(route, pageResult([]), 'ute2e-faults')
- if (method === 'GET' && path === '/analytics') return fulfill(route, { assignmentCount: 6, participantCount: 12, runCount: 10, completedRunCount: 8, pendingReviewCount: 1, averageScore: 86.5, assignmentsByChannel: { VIRTUAL: 3, PHYSICAL: 2, CONFRONTATION: 1 }, runsByStatus: { ACCEPTED: 1, IN_PROGRESS: 1, REVIEWED: 8 }, membersByRole: { TEAM_LEADER: 3, OPERATOR: 5, COMMANDER: 2 } }, 'ute2e-analytics')
- if (method === 'GET' && path === '/analytics/records') {
- if (this.identity === 'administrative') {
- this.administrativeAnalyticsRecordsRejected += 1
- return reject(route, 403, '行政身份仅可读取部门聚合,不可读取个人运行明细')
- }
- if (this.identity === 'unionAdministrativeStudent' && url.searchParams.get('userId') !== this.currentUserId()) {
- return reject(route, 403, '受限学员只能读取本人训练明细')
- }
- return fulfill(route, pageResult([{
- runId: 'run-reviewed-analytics', runCode: 'RUN-ANALYTICS-001', assignmentId: 'assignment-exam', assignmentCode: 'EXAM-2026-001', assignmentName: '虚拟维修正式考核', assignmentKind: 'FORMAL_EXAM', channel: 'VIRTUAL', executionMode: 'EXAM', userId: '4', username: 'student.leader', displayName: '红方队长', departmentId: '110', departmentName: '维修教研室', status: 'REVIEWED', progressPercent: 100, score: 88, passed: true, acceptedAt: 1786386000, startedAt: 1786387200, submittedAt: 1786390800, reviewedAt: 1786394400, reviewerName: '周教员', serverNow: 1786394400,
- }]), 'ute2e-analytics-records')
- }
- if (method === 'GET' && path === '/xr/capabilities') return fulfill(route, { framework: 'three@0.185.0 + WebXR', sessionModes: ['INLINE', 'IMMERSIVE_VR'], referenceSpaces: ['LOCAL', 'LOCAL_FLOOR'], secureContextRequired: true, scoreAcceptedFromClient: false, resultPolicy: 'SERVER_AUTHORITATIVE', serverTimeSource: 'KINGBASE' }, 'ute2e-xr-capabilities')
- if (method === 'GET' && path === '/adapters') return fulfill(route, [{ id: 'adapter-dh', code: 'LOCAL_DIGITAL_HUMAN', name: '本地数字教员适配器', provider: 'LOCAL', mode: 'RESERVED', status: 'DISABLED', health: 'UNKNOWN', configSummary: {}, lastHealthAt: null, version: 1 }], 'ute2e-adapters')
- const xrStartMatch = path.match(/^\/runs\/([^/]+)\/xr-sessions$/)
- if (method === 'POST' && xrStartMatch) {
- const item = this.runs.get(xrStartMatch[1]!)
- if (!item || body.runVersion !== item.version || body.mode !== 'INLINE') return reject(route, 400, 'XR 会话 DTO 不符合契约')
- return fulfill(route, { id: 'xr-session-1', code: 'XR-SESSION-001', assignmentId: item.assignmentId, runId: item.id, userId: this.currentUserId(), mode: 'INLINE', referenceSpaceType: 'LOCAL', deviceName: body.deviceName, userAgentSummary: body.userAgentSummary, capabilities: body.capabilities, stateSnapshot: {}, status: 'ACTIVE', startedAt: 1786387200, lastHeartbeatAt: 1786387200, endedAt: null, failureReason: '', version: 0 }, 'ute2e-xr-session')
- }
- const xrEndMatch = path.match(/^\/xr-sessions\/([^/]+)\/end$/)
- if (method === 'PUT' && xrEndMatch) return fulfill(route, { id: xrEndMatch[1], code: 'XR-SESSION-001', assignmentId: 'assignment-virtual-common', runId: 'run-immersive', userId: this.currentUserId(), mode: 'INLINE', referenceSpaceType: 'LOCAL', deviceName: '桌面浏览器', userAgentSummary: '', capabilities: {}, stateSnapshot: {}, status: body.status, startedAt: 1786387200, lastHeartbeatAt: 1786387200, endedAt: 1786387260, failureReason: body.failureReason, version: 1 }, 'ute2e-xr-ended')
-
- return reject(route, 404, `未模拟教学端点 ${method} ${path}`)
- }
-
- private assetList() {
- return [{
- sourceProjectId: 'asset-project-1', sourceVersionId: 'asset-version-1', assetCode: 'EXCAVATOR_A',
- name: 'excavator-a.glb', type: 'MODEL_FILE', contentUri: 'content://sha256/contract-excavator',
- mimeType: 'model/gltf-binary', sizeBytes: 11919032, sha256: 'contract-sha256', downloadable: true,
- downloadUrl: '/models/excavator-a.glb',
- }]
- }
- }
|