chore: checkpoint web Blender migration source, tools and docs
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled

Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer,
web workflow engine and protocol, tooling, docs and task records.
Large generated evidence, user-testing archives, build snapshots and
imported test assets remain untracked.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
mes123456
2026-11-10 11:18:01 +08:00
parent 122142c26c
commit f3e21c6b98
6887 changed files with 257154 additions and 12294 deletions

View File

@@ -1,53 +1,705 @@
#!/usr/bin/env python3
"""Read-only WBV2 migration progress dashboard.
"""只读查看 Blender → Web Blender UI 对标执行状态与最新功能交接。
The dashboard deliberately separates implementation, build, and verification;
build receipts never count as completed business behavior.
不会启动执行器、构建、部署或运行功能验证;候选源码对应关系仍明确标记为未知。
"""
from __future__ import annotations
import argparse, json, re
import argparse
from datetime import datetime, timezone
import json
import math
from pathlib import Path
import re
import sys
import time
from wbv2_gap_progress import resolve_gap
ROOT = Path(__file__).resolve().parents[1]
FOCUS_NAMES = {'editors': '桌面与编辑器', 'gpu': '原生绘制与计算', 'bpy': 'Python/操作入口'}
STATE_NAMES = {
'running': '执行中', 'starting': '正在启动工作轮次', 'handed_off': '已交接,准备继续',
'recovering': '恢复中', 'waiting_retry': '等待重试', 'reloading': '安全交接后加载新执行逻辑',
'stopped': '已停止', 'awaiting_user_verification': '执行器报告实现已交接,等待用户使用',
}
WORK_NAMES = {
'reading_sources': '定位原生源码', 'source_changes_observed': '已有源码改动',
'executing_command': '执行实现命令', 'waiting_executor': '等待执行器动作',
'waiting_provider_retry': '服务退避后重试', 'waiting_business_retry': '业务轮次重试等待',
}
def read(path, default=None):
try: return json.loads((ROOT / path).read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError): return default
def snapshot():
queue = (ROOT / "docs/EXECUTION_QUEUE.md").read_text(encoding="utf-8")
task = re.search(r"\| 当前任务 \| `([^`]+)`", queue)
parent = re.search(r"\| parent manifest \| `([^`]+)`", queue)
backlog = read("tests/golden/WBV2/web-product-backlog.json", {}) or {}
state = read("tests/golden/WBV2/autopilot/state.json", {}) or {}
assignments = read("tests/golden/WBV2/autopilot/local-task-assignments.json", {}) or {}
jobs = state.get("jobs", {})
tasks = backlog.get("tasks", [])
counts = {}
for item in tasks: counts[item.get("status", "unknown")] = counts.get(item.get("status", "unknown"), 0) + 1
needs = [a for a in assignments.get("assignments", []) if a.get("state") == "needs_implementation"]
builds = {k: v for k, v in jobs.items() if v.get("state") == "built_pending_verification"}
verified = [k for k, v in jobs.items() if v.get("state") == "verified"]
return {"task": task.group(1) if task else None, "parentManifest": parent.group(1) if parent else None,
"runner": {"state": state.get("state"), "pid": state.get("pid"), "activeJob": state.get("activeJob"),
"verificationTiming": state.get("verificationTiming")},
"backlog": {"total": len(tasks), "byStatus": counts, "needsImplementation": [x.get("id") for x in tasks if x.get("status") == "pending"]},
"business": {"needsImplementation": [x.get("job") for x in needs], "readyForValidation": len(builds), "verified": verified},
"builds": {"pendingVerification": sorted(builds), "stale": sum(1 for v in jobs.values() if v.get("state") == "stale")},
"recommendations": [f"实现并绑定: {x.get('job')}" for x in needs] or ["没有新的本地实现缺口;由代理优先完成自动验证"]}
def obj(value):
return value if isinstance(value, dict) else {}
def rows(value):
return [item for item in value if isinstance(item, dict)] if isinstance(value, list) else []
def number(value):
return value if isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value) else None
def read_record(path, notes, required=False):
try:
# Only small state/manifest/result files; never load full inventory or logs.
with path.open(encoding='utf-8') as stream:
raw = stream.read(2 * 1024 * 1024 + 1)
if len(raw) > 2 * 1024 * 1024:
raise ValueError('超过进度记录大小上限')
data = json.loads(raw)
if not isinstance(data, dict):
raise ValueError('记录不是 JSON 对象')
return data
except FileNotFoundError:
if required:
notes.append(f'尚无记录:{path.relative_to(ROOT)}')
except (OSError, ValueError) as error:
notes.append(f'暂时无法读取 {path.relative_to(ROOT)}:{error}')
return {}
def timestamp(value):
try:
result = datetime.fromisoformat(value.replace('Z', '+00:00'))
return result.timestamp() if result.tzinfo else None
except (AttributeError, TypeError, ValueError, OverflowError):
return None
def local_time(value):
stamp = timestamp(value)
return datetime.fromtimestamp(stamp).astimezone().strftime('%m-%d %H:%M:%S %Z') if stamp is not None else '未知'
def duration(seconds):
seconds = number(seconds)
if seconds is None:
return '未知'
value = max(0, int(seconds))
if value >= 3600:
return f'{value // 3600}小时{value % 3600 // 60}分'
return f'{value // 60}分{value % 60}秒' if value >= 60 else f'{value}秒'
def cycle_from_path(value):
"""Extract a cycle number from a candidate/report path when available."""
match = re.search(r'(?:cycle-|through-|candidate-)(\d+)', str(value or ''))
return int(match.group(1)) if match else None
def cadence_label(cadence):
average = number(cadence.get('averageSeconds'))
if average is None:
return '暂无足够交接样本'
return f"最近 {cadence.get('sampleCount', 0)} 轮平均 {duration(average)},上一轮 {duration(cadence.get('lastSeconds'))}"
def assigned_ui_module(delivery_stage):
"""Read the module explicitly named by the current delivery assignment."""
assignment = obj(delivery_stage).get('assignment')
modules = re.findall(r'UI-[A-Z]+-\d{3}', assignment or '')
return modules[0] if len(set(modules)) == 1 else None
def process_status(pid, expected_identity):
if not isinstance(pid, int) or isinstance(pid, bool) or pid <= 0:
return 'not_recorded'
try:
fields = Path(f'/proc/{pid}/stat').read_text().rsplit(')', 1)[1].split()
if fields[0] in {'Z', 'X'}:
return 'exited'
if expected_identity is None:
return 'identity_unknown'
return 'alive' if fields[19] == str(expected_identity) else 'pid_reused'
except FileNotFoundError:
return 'exited' if Path('/proc').exists() else 'unknown'
except (OSError, IndexError):
return 'unknown'
def snapshot(recent_limit=3):
notes = []
now = time.time()
try:
queue = (ROOT / 'docs/EXECUTION_QUEUE.md').read_text(encoding='utf-8')
except OSError as error:
queue = ''
notes.append(f'执行队列无法读取:{error}')
match = re.search(r'\| 当前任务 \| `([A-Z0-9_-]+)`', queue)
task = match.group(1) if match else None
data = {'observedAt': datetime.now(timezone.utc).isoformat(), 'task': task,
'notes': notes, 'basis': '既有实现交接;只读查询,未执行任何功能验证'}
if not task:
notes.append('未找到当前任务指针,不能判断完成情况。')
return data
base = ROOT / 'tests/golden' / task
state = read_record(base / 'unattended-implementation/state.json', notes, True)
if state and state.get('task') != task:
notes.append('执行状态所属任务与队列不一致,未将旧状态计入当前进度。')
state = {}
delivery = read_record(base / 'desktop-delivery-manifest.json', notes, True)
for record in (delivery,):
if record and record.get('task') != task:
notes.append('计划所属任务不一致,忽略该计划。')
record.clear()
first_group = obj(delivery.get('firstGroupExecution'))
gap_policy = obj(delivery.get('gapScheduling'))
cadence_policy = obj(first_group.get('candidateCadence'))
refresh_gap = cadence_policy.get('sourceIncrementGap') or gap_policy.get('candidateRefreshSourceGap') or 0
first_group_view = {
'stageId': first_group.get('stageId') or 'main-desktop',
'status': first_group.get('status') or obj(delivery.get('stages')[0] if rows(delivery.get('stages')) else {}).get('status', 'in_progress'),
'scope': first_group.get('scope') or 'DW01-DW03 concrete operator workflows',
'planPath': first_group.get('planPath') or gap_policy.get('firstGroupScopeReport'),
'currentWave': first_group.get('currentWave') or 'unspecified',
'nextWaves': first_group.get('nextWaves') or [],
'coherentFocusTurns': cadence_policy.get('coherentFocusTurns') or gap_policy.get('coherentFocusTurns') or 0,
'candidateRefreshSourceGap': refresh_gap,
'buildPolicy': first_group.get('buildPolicy') or {'mode': 'unknown'},
}
current = obj(state.get('deliveryStage'))
progress = obj(state.get('deliveryStages'))
stages = []
for stage in rows(delivery.get('stages')):
stage_id = stage.get('id')
recorded = obj(obj(progress.get(stage_id)).get('focuses'))
order = stage.get('focusOrder') if isinstance(stage.get('focusOrder'), list) else []
order = [name for name in order if isinstance(name, str)]
done = bool(order) and all(obj(recorded.get(name)).get('reportedImplemented') is True for name in order)
status = 'reported_implemented_unverified' if done else (
'in_progress' if recorded or current.get('id') == stage_id else 'no_stage_completion_record')
stages.append({'id': stage_id, 'title': stage.get('title') or stage_id,
'status': status, 'current': current.get('id') == stage_id,
'focusesReportedImplemented': sum(obj(recorded.get(name)).get('reportedImplemented') is True for name in order),
'focusCount': len(order)})
supervisor = process_status(state.get('pid'), state.get('identity'))
worker = process_status(state.get('childPid'), state.get('childIdentity'))
updated = timestamp(state.get('updatedAt'))
age = max(0, now - updated) if updated is not None else None
recorded_state = state.get('state', 'not_started')
health = 'normal'
if not state:
health = 'unknown'
elif supervisor in {'exited', 'pid_reused', 'not_recorded'}:
health = 'stopped' if recorded_state in {'stopped', 'awaiting_user_verification'} else 'process_missing'
elif supervisor != 'alive':
health = 'process_unknown'
elif age is None or age > 30:
health = 'heartbeat_stale'
elif recorded_state == 'running' and worker == 'exited':
health = 'worker_exited_pending_handoff'
provider = obj(state.get('provider'))
retry = number(provider.get('retryAt'))
issues = [item for item in obj(obj(state.get('efficiency')).get('issues')).values()
if isinstance(item, dict) and item.get('status') == 'open']
focus_records = obj(obj(progress.get(current.get('id'))).get('focuses'))
current_plan = next((item for item in rows(delivery.get('stages'))
if item.get('id') == current.get('id')), {})
concrete_gaps = {}
findings = obj(obj(state.get('efficiency')).get('issues'))
for name, seeds in obj(current_plan.get('gapItems')).items():
concrete_gaps[name] = []
focus_delivery = obj(obj(obj(progress.get(current.get('id'))).get('focuses')).get(name))
for seed in rows(seeds):
key = f"{current.get('id')}:{name}:{seed.get('id')}"
finding = obj(findings.get(key))
item = resolve_gap(seed, finding, focus_delivery)
handoff = obj(seed.get('implementationHandoff'))
# Supersede only explicitly named old residuals, below the recorded
# cycle boundary. A later worker observation always remains visible.
boundary = number(handoff.get('throughCycle'))
latest_cycle = max(number(finding.get('cycle')) or 0,
number(focus_delivery.get('cycle')) or 0)
covered = set(handoff.get('supersededRoutes') or [])
if handoff.get('reportPath') and boundary is not None and latest_cycle <= boundary:
previous = item.get('remainingSubroutes') or []
remaining = [route for route in previous if route not in covered]
if covered and previous and len(remaining) < len(previous):
remaining.extend(handoff.get('remainingRoutes') or [])
item['remainingSubroutes'] = list(dict.fromkeys(remaining))
item['completedSubroutes'] = list(dict.fromkeys(
item.get('completedSubroutes', []) + handoff.get('completedRoutes', [])))
item['status'] = 'open' if remaining else 'implemented_unverified'
item['statusSource'] = 'explicit_implementation_handoff'
item['latestFinding'] = {
'cycle': None, 'reason': '\n'.join(item['remainingSubroutes']),
'action': handoff.get('nextAction'),
'sourcePaths': handoff.get('sourcePaths', []),
}
item['handoffReportPath'] = handoff['reportPath']
handoff_cycles = [value for value in (handoff.get('throughCycles') or [])
if isinstance(value, int) and not isinstance(value, bool)]
handoff_boundary = max(handoff_cycles) if handoff_cycles else None
if (handoff.get('reportPath') and handoff.get('candidatePath') and
handoff_boundary is not None and latest_cycle <= handoff_boundary):
residuals = list(dict.fromkeys(handoff.get('remainingRoutes') or []))
item['completedSubroutes'] = list(dict.fromkeys(
item.get('completedSubroutes', []) + (handoff.get('completedRoutes') or [])))
item['remainingSubroutes'] = residuals
item['missing'] = '\n'.join(residuals)
item['nextAction'] = handoff.get('nextAction', item.get('nextAction', ''))
item['status'] = 'open' if residuals else 'implemented_unverified'
item['statusSource'] = 'explicit_candidate_implementation_handoff'
item['latestFinding'] = {
'cycle': handoff_boundary,
'reason': item.get('missing'),
'action': item.get('nextAction'),
'sourcePaths': handoff.get('sourcePaths', []),
}
item['handoffReportPath'] = handoff['reportPath']
concrete_gaps[name].append(item)
focuses = []
for name in dict.fromkeys([*obj(state.get('focusHandoffs')), *focus_records]):
record = obj(focus_records.get(name))
branch = obj(obj(state.get('focusHandoffs')).get(name))
next_retry = number(branch.get('retryAt'))
circuit_deadline = number(branch.get('circuitOpenUntil'))
circuit_seconds = (max(0, int(circuit_deadline - now))
if circuit_deadline is not None
and branch.get('circuitStageId') == current.get('id') else 0)
focuses.append({'id': name, 'title': FOCUS_NAMES.get(name, name), 'cycle': record.get('cycle'),
'deferredPathSeconds': circuit_seconds,
'deferredPathId': branch.get('stalledScope'),
'completedRoutes': record.get('completedRoutes', []),
'remainingRoutes': record.get('remainingRoutes', []),
'reportPath': record.get('reportPath'),
'blockingRoute': record.get('blockingRoute'), 'nextAction': record.get('nextAction'),
'repeatedBlockerRounds': record.get('sameBusinessBlockerRounds', 0),
'noProgressRounds': branch.get('noProgressRounds', 0),
'retrySeconds': max(0, int(next_retry - now)) if next_retry is not None else 0})
source_updates = []
for stage in rows(delivery.get('stages')):
for seeds in obj(stage.get('gapItems')).values():
for seed in rows(seeds):
handoff = obj(seed.get('implementationHandoff'))
if handoff.get('reportPath'):
source_updates.append({**handoff, 'gapId': seed.get('id'), 'stageId': stage.get('id')})
source_updates.sort(key=lambda item: timestamp(item.get('recordedAt')) or 0, reverse=True)
recent = []
cycle = state.get('cycle')
current_attempt = {}
if isinstance(cycle, int) and not isinstance(cycle, bool):
current_attempt = read_record(base / 'unattended-implementation' / f'cycle-{cycle:06d}' / 'attempt.json', notes)
if (current_attempt.get('cycle') != cycle or current_attempt.get('focus') != state.get('focus')
or current_attempt.get('deliveryStage') != current.get('id')):
current_attempt = {}
# Read only the requested recent reports, within a 24-cycle window.
for index in range(cycle, max(0, cycle - 24), -1):
if len(recent) >= recent_limit:
break
directory = base / 'unattended-implementation' / f'cycle-{index:06d}'
result_path = directory / 'result.json'
result = read_record(result_path, notes)
if not result:
continue
process = read_record(directory / 'process.json', notes)
recent.append({'cycle': index, 'status': result.get('status'),
'businessEntry': result.get('businessEntry'), 'summary': result.get('summary'),
'elapsedSeconds': process.get('elapsedSeconds'),
'exitCode': process.get('exitCode'), 'reason': process.get('reason'),
'changedProductionFiles': process.get('changedProductionFiles', []),
'processRecorded': bool(process),
'buildStatus': obj(result.get('build')).get('status'),
'reportPath': str(result_path.relative_to(ROOT))})
# A report mtime interval includes pauses, retries and skipped cycles. It is
# not execution speed. Expose only explicitly recorded process durations.
measured = [item['elapsedSeconds'] for item in recent if number(item.get('elapsedSeconds')) is not None]
cadence = {'sampleCount': len(measured), 'averageSeconds': None,
'lastSeconds': measured[0] if measured else None,
'basis': 'process.json elapsedSeconds; no throughput estimate'}
build = obj(state.get('sharedBuild'))
# Only the current scheduler's explicit candidate is an application handoff.
# A directory's mtime cannot associate an arbitrary candidate with this task.
candidate = None
recorded_candidate = obj(state.get('applicationCandidate'))
preferred = recorded_candidate.get('path')
preferred_path = (ROOT / preferred).resolve() if isinstance(preferred, str) and preferred else None
chosen = None
if preferred_path and preferred_path.is_file():
try:
chosen = (preferred_path.stat().st_mtime, preferred_path, 'state.applicationCandidate')
except OSError:
chosen = None
if preferred and chosen is None:
notes.append(f'当前候选记录不可读:{preferred};未用其它目录替代。')
if chosen:
modified, path, selection_basis = chosen
record = read_record(path, notes)
if record:
latest_impl = obj(state.get('latestImplementation'))
latest_at = timestamp(latest_impl.get('at'))
latest_cycle = latest_impl.get('cycle') if isinstance(latest_impl.get('cycle'), int) else cycle
candidate_cycle = (recorded_candidate.get('cycle')
if selection_basis == 'state.applicationCandidate'
and isinstance(recorded_candidate.get('cycle'), int) else cycle_from_path(path))
cycle_gap = max(0, latest_cycle - candidate_cycle) if isinstance(latest_cycle, int) and isinstance(candidate_cycle, int) else None
source_gap = None
count_basis = 'not_recorded'
pending = state.get('pendingSourceCycles')
if selection_basis == 'state.applicationCandidate' and isinstance(pending, list):
pending = sorted({n for n in pending if isinstance(n, int) and not isinstance(n, bool)})
source_gap = len(pending)
count_basis = 'recorded_source_change_rounds'
candidate = {'path': str(path.relative_to(ROOT)), 'status': record.get('status'),
'scopeComplete': record.get('scopeComplete'), 'validation': record.get('validation'),
'deployment': record.get('deployment'),
'recordedAt': datetime.fromtimestamp(modified, timezone.utc).isoformat(),
'candidateCycle': candidate_cycle, 'latestImplementationCycle': latest_cycle,
'sourceGap': source_gap,
'pendingSourceCycles': pending if isinstance(pending, list) else None,
'pendingHistoryIncomplete': bool(state.get('pendingSourceHistoryIncomplete')),
'externalIntegrationUnconfirmed': [item for item in source_updates if not item.get('candidatePath')],
'cycleGap': cycle_gap, 'countBasis': count_basis,
'refreshGap': refresh_gap,
'refreshDue': bool(source_gap is not None and refresh_gap and source_gap >= refresh_gap)
or (selection_basis == 'state.applicationCandidate'
and bool(state.get('pendingSourceHistoryIncomplete'))),
'selectionBasis': selection_basis,
'newerImplementationHandoff': ((source_gap > 0) if source_gap is not None
else (latest_at is not None and latest_at > modified)),
'sourceParity': 'unknown_not_checked', 'taskAssociation': 'candidate does not declare task; local directory reference only'}
current_focus = next((focus for focus in focuses if focus.get('id') == state.get('focus')), None)
source_idle = number(state.get('sourceIdleSeconds'))
last_source_age = number(state.get('lastSourceChangeAgeSeconds'))
alerts = []
if health != 'normal':
alerts.append(health)
if provider.get('reconnecting') or (number(provider.get('consecutiveFailures')) or 0) > 0:
alerts.append('provider_retry_or_reconnect')
if runner_active := (state.get('activeArtifactCommandCount') or 0):
alerts.append('artifact_command_active')
if candidate and candidate.get('refreshDue'):
alerts.append('candidate_refresh_due')
if (source_idle is not None and source_idle > 180
and state.get('activity') not in {'command_execution', 'artifact_generation'}):
alerts.append('source_idle_over_3m')
if last_source_age is not None and last_source_age > 600:
alerts.append('source_progress_stale')
if current_focus and ((number(current_focus.get('noProgressRounds')) or 0) >= 2 or
(number(current_focus.get('repeatedBlockerRounds')) or 0) >= 2):
alerts.append('focus_repeated_no_progress')
done_count = sum(stage['status'] == 'reported_implemented_unverified' for stage in stages)
ui_plan = obj(delivery.get('uiParityExecution'))
ui_modules = []
ownership = obj(ui_plan.get('moduleGapOwnership'))
for module in rows(delivery.get('uiParityModules')):
handoff = obj(module.get('progressHandoff'))
latest = obj(handoff.get('latestImplementation'))
if latest.get('reportPath'):
source_updates.append({**latest, 'moduleId': module.get('id'),
'stageId': current.get('id'), 'summary': latest.get('completedRoutes', [''])[0]})
focus = module.get('focus')
items = concrete_gaps.get(focus, [])
gap_ids = ownership.get(module.get('id'), module.get('gapIds', [])) or []
selected = [item for item in items if item.get('id') in set(gap_ids)]
handoff = obj(module.get('progressHandoff'))
counts = {
'implemented_unverified': sum(item.get('status') == 'implemented_unverified' for item in selected),
'locate': sum(item.get('phase') == 'locate' and item.get('status') != 'implemented_unverified' for item in selected),
'open': sum(item.get('status') != 'implemented_unverified' and item.get('phase') != 'locate' for item in selected),
}
if not selected:
if handoff:
status = handoff.get('status', '已登记,状态未完成')
counts = {
'implemented_unverified': len(handoff.get('completedRoutes') or []),
'locate': 1 if handoff.get('phase') == 'locate' else 0,
'open': len(handoff.get('remainingRoutes') or []),
}
else:
status = '尚无模块专属交接'
elif counts['open'] == 0 and counts['locate'] == 0:
status = '登记子项已补漏,整模块未确认'
elif counts['implemented_unverified'] or counts['locate']:
status = '部分已补漏/定位中'
else:
status = '待处理'
related = obj(focus_records.get(focus))
ui_modules.append({**module, 'status': status, 'counts': counts, 'gaps': selected,
'progressHandoff': handoff or None,
'scopeComplete': False, 'visualParity': '未有样式对标交接',
'relatedHandoffCycle': related.get('cycle') if selected else None,
'relatedHandoffPath': related.get('reportPath') if selected else None,
'gapStatuses': [{'id': item.get('id'), 'phase': item.get('phase', 'implement'),
'status': item.get('status'), 'entry': item.get('entry')}
for item in selected]})
source_updates.sort(key=lambda item: timestamp(item.get('recordedAt')) or 0, reverse=True)
current_module = current.get('moduleId') or assigned_ui_module(current)
concrete_open_count = sum(
1 for items in concrete_gaps.values() for item in items
if item.get('status') != 'implemented_unverified' or item.get('remainingSubroutes')
)
ui_parity = {**ui_plan, 'modules': ui_modules,
# Focus names are implementation roles, not unique UI modules.
'currentModule': current_module,
'currentModuleSource': ('deliveryStage.assignment' if not current.get('moduleId') and current_module else 'state.moduleId'),
'associatedModules': [module.get('id') for module in ui_modules
if module.get('focus') == state.get('focus') and module.get('gaps')],
'routeFields': ui_plan.get('routeFields', []),
'validation': 'NOT_RUN', 'acceptance': 'NOT_CLAIMED'}
data.update({'runner': {'recordedState': recorded_state, 'health': health, 'pid': state.get('pid'),
'supervisor': supervisor, 'childPid': state.get('childPid'), 'worker': worker,
'cycle': cycle, 'focus': state.get('focus'), 'workState': state.get('workState'),
'startedAt': current_attempt.get('startedAt'),
'assignment': current.get('assignment'),
'executionMode': state.get('executionMode'),
'sourceProgressDeadlineSeconds': state.get('sourceProgressDeadlineSeconds'),
'activity': state.get('activity'), 'noProgressCycles': state.get('noProgressCycles', 0),
'elapsedSeconds': state.get('elapsedSeconds'),
'lastActionAgeSeconds': state.get('lastActionAgeSeconds'),
'activeCommandCount': state.get('activeCommandCount', 0),
'observedProductionFiles': state.get('observedProductionFiles', []),
'sourceIdleSeconds': state.get('sourceIdleSeconds'),
'sourceReadCommands': state.get('sourceReadCommands'),
'sourceReadCommandBudget': state.get('sourceReadCommandBudget'),
'implementationProgressDeadlineSeconds': state.get('implementationProgressDeadlineSeconds'),
'lastSourceChangeAgeSeconds': state.get('lastSourceChangeAgeSeconds'),
'workflowContinuation': state.get('workflowContinuation'),
'updatedAt': state.get('updatedAt'), 'heartbeatAgeSeconds': age,
'reason': state.get('reason'), 'activeArtifactCommands': state.get('activeArtifactCommandCount', 0)},
'integrationUrgent': state.get('integrationUrgent'),
'progress': {'stagesReportedImplemented': done_count, 'stageTotal': len(stages),
'allStagesReportedImplemented': bool(stages) and done_count == len(stages),
'featureCompletionPercent': None, 'stages': stages,
'currentStage': current.get('id'), 'focuses': focuses,
'concreteGaps': concrete_gaps,
'openRecordedIssueCount': len(issues),
'openConcreteGapCount': concrete_open_count,
'scope': '仅统计当前 manifest 固定缺口与最新交接,不代表全部功能比例'},
'provider': {'error': provider.get('errorKind'), 'consecutiveFailures': provider.get('consecutiveFailures', 0),
'retrySeconds': max(0, int(retry - now)) if retry is not None else 0,
'reconnecting': provider.get('reconnecting', False)},
'firstGroup': first_group_view, 'latestImplementation': obj(state.get('latestImplementation')), 'recent': recent,
'uiParity': ui_parity,
'cadence': cadence, 'alerts': alerts, 'sourceUpdates': source_updates,
'build': {key: build.get(key) for key in ['cycle', 'status', 'logPath', 'artifactPaths', 'blockingSymbols']},
'candidate': candidate, 'validation': state.get('validation', 'UNKNOWN'),
'acceptance': state.get('acceptance', 'UNKNOWN')})
return data
HEALTH_NAMES = {
'normal': '进程存活、心跳更新(不代表功能完成)',
'unknown': '缺少运行记录', 'stopped': '执行器已停止',
'process_missing': '执行器退出或 PID 已复用',
'process_unknown': '无法确认进程身份',
'heartbeat_stale': '心跳过期;下方活动为旧记录',
'worker_exited_pending_handoff': '工作进程已退出,等待交接',
}
def lines(value):
return list(dict.fromkeys(line.strip() for line in str(value or '').splitlines() if line.strip()))
def display(data, details=False):
print(f"桌面 Blender UI → Web Blender UI | {local_time(data['observedAt'])}")
print(f"任务:{data.get('task') or '未知'};实现状态:尚未完整交付")
runner = data.get('runner') or {}
if not runner:
for note in data.get('notes', []):
print(f'提示:{note}')
return
health = runner.get('health')
print(f"执行:{STATE_NAMES.get(runner.get('recordedState'), runner.get('recordedState'))};"
f"{HEALTH_NAMES.get(health, health)}")
print(f"第 {runner.get('cycle')} 轮 | {FOCUS_NAMES.get(runner.get('focus'), runner.get('focus'))} | "
f"{WORK_NAMES.get(runner.get('workState'), runner.get('workState') or '活动未记录')}")
print(f"本轮记录用时 {duration(runner.get('elapsedSeconds'))};最近动作距心跳 {duration(runner.get('lastActionAgeSeconds'))};"
f"心跳距今 {duration(runner.get('heartbeatAgeSeconds'))}")
print(f"活动命令 {runner.get('activeCommandCount', 0)};构建/打包 {runner.get('activeArtifactCommands', 0)};"
f"本轮观察到生产文件修改 {len(runner.get('observedProductionFiles') or [])} 个")
if runner.get('sourceIdleSeconds') is not None:
print(f"未观察到源码修改 {duration(runner['sourceIdleSeconds'])}(截至心跳,不单独判为空转)")
if runner.get('sourceReadCommands') is not None:
print(f"读取观察 {runner.get('sourceReadCommands')} 次;阈值 {runner.get('sourceReadCommandBudget')} 次且无进展计时达 "
f"{duration(runner.get('implementationProgressDeadlineSeconds'))} 后,在命令结束处收束;读取数不是硬上限")
if runner.get('executionMode'):
print('本轮工作:' + {'INTEGRATE_PENDING': '先组合已有源码并交接候选',
'IMPLEMENT_FAMILY': '实现同一业务族的具体缺口'}.get(
runner['executionMode'], runner['executionMode']))
if details:
print(f"进程:{runner.get('pid')} [{runner.get('supervisor')}];工作进程:"
f"{runner.get('childPid')} [{runner.get('worker')}];记录时间 {local_time(runner.get('updatedAt'))}")
if runner.get('startedAt'):
print(f"本轮开始:{local_time(runner['startedAt'])}")
for path in runner.get('observedProductionFiles') or []:
print(f" 本轮改动:{path}")
if runner.get('reason'):
print(f"执行原因:{runner['reason']}")
urgent = data.get('integrationUrgent')
if urgent:
reason = {'coherent_batch_ready': '业务批次源码已闭合',
'source_batch_limit': '待组合批次数到限',
'pending_delivery_deferred': '待交付跨轮积压',
'source_history_needs_reconciliation': '部分源码交接待对齐'}.get(
urgent.get('reason'), '候选刷新到期')
print(f"集成优先:{reason};待组合源码轮次 {urgent.get('pendingSourceCycles', [])};先生成匹配候选")
ui = data.get('uiParity') or {}
module_by_id = {module.get('id'): module for module in ui.get('modules', [])}
module_id = ui.get('currentModule')
if module_id:
print(f"当前模块:{module_id} {module_by_id.get(module_id, {}).get('title', '')}(当前任务指派)")
else:
print('当前精确模块未记录;不由执行方向推断模块完成度。')
progress = data.get('progress') or {}
focus = next((item for item in progress.get('focuses', []) if item.get('id') == runner.get('focus')), {})
if focus.get('nextAction'):
print(f"本批接续(第 {focus.get('cycle')} 轮交接):{focus['nextAction']}")
elif runner.get('assignment'):
print(f"任务指派:{runner['assignment']}")
if details and runner.get('assignment') and focus.get('nextAction'):
print(f"任务指派:{runner['assignment']}")
provider = data.get('provider') or {}
if provider.get('retrySeconds') or provider.get('reconnecting') or provider.get('consecutiveFailures'):
print(f"服务等待:退避 {duration(provider.get('retrySeconds'))};重连 {'是' if provider.get('reconnecting') else '否'};"
f"连续失败 {provider.get('consecutiveFailures', 0)};分类 {provider.get('error') or '未记录'}")
for item in progress.get('focuses', []):
if item.get('deferredPathSeconds') or item.get('retrySeconds'):
print(f"路径暂缓:{item.get('title')} / {item.get('deferredPathId') or '未记录路径'};"
f"暂缓剩余 {duration(item.get('deferredPathSeconds', 0))},重试剩余 {duration(item.get('retrySeconds', 0))}")
if (number(focus.get('noProgressRounds')) or 0) >= 2 or (number(focus.get('repeatedBlockerRounds')) or 0) >= 2:
print(f"推进警示:当前方向连续无源码进展 {focus.get('noProgressRounds', 0)} 轮;"
f"重复阻塞 {focus.get('repeatedBlockerRounds', 0)} 轮。")
print('\n最新交接(源码实现报告,未验证):')
latest = data.get('latestImplementation') or {}
if latest:
stamp = timestamp(latest.get('at'))
age = (timestamp(data.get('observedAt')) or 0) - stamp if stamp is not None else None
print(f" 最近调度器源码交接:第 {latest.get('cycle')} 轮,{local_time(latest.get('at'))},距今 {duration(age)}")
print(f" {latest.get('businessEntry') or latest.get('workflow') or '未提供业务入口'}")
else:
print(' 尚无调度器源码交接记录。')
for item in data.get('recent', []):
measured = duration(item.get('elapsedSeconds'))
if item.get('processRecorded') and (item.get('exitCode') != 0 or item.get('reason')):
label = f"退出 {item.get('exitCode')} / {item.get('reason') or '原因未记录'};报告未视为完成"
else:
label = '已写交接' if item.get('processRecorded') else '仅结果记录,进程交接未记录'
print(f" [{item['cycle']}] {item.get('businessEntry') or item.get('summary') or '未提供业务入口'};{label};工作进程用时 {measured}")
if details:
print(f" {item.get('summary') or '未提供摘要'}")
print(f" 记录:{item.get('reportPath')}")
for item in data.get('sourceUpdates', []):
print(f" [根会话补缺 {local_time(item.get('recordedAt'))}] {item.get('summary')}")
if item.get('candidatePath'):
print(f" 候选:{item['candidatePath']};部署:{item.get('deployment', 'not_performed')};验证:{item.get('validation', 'NOT_RUN')}")
if details:
print(f" 记录:{item.get('reportPath')}")
print('\n最新记录余项(不是全功能缺口总表):')
displayed = False
# Show the active batch first. Completed fixed IDs remain in JSON/details;
# an old fixed inventory is not the current executable work list.
focuses = sorted(progress.get('focuses', []), key=lambda item: item.get('id') != runner.get('focus'))
for item in focuses:
gaps = progress.get('concreteGaps', {}).get(item.get('id'), [])
open_gaps = [gap for gap in gaps if gap.get('status') != 'implemented_unverified' or gap.get('remainingSubroutes')]
known_routes = []
for gap in open_gaps:
finding = obj(gap.get('latestFinding'))
residuals = gap.get('remainingSubroutes') or lines(finding.get('reason') or gap.get('missing'))
known_routes.extend(residuals)
displayed = True
origin = f"第 {finding['cycle']} 轮" if finding.get('cycle') is not None else gap.get('statusSource', 'manifest')
print(f" [{gap.get('id')}] {gap.get('entry')}(来源:{origin})")
for residual in residuals:
for line in lines(residual):
print(f" {line}")
if details:
print(f" 下一步:{finding.get('action') or gap.get('nextAction') or '未记录'}")
if gap.get('handoffReportPath'):
print(f" 交接:{gap['handoffReportPath']}")
# Native drawing/region work often uses DW IDs outside the old 7-item
# inventory. Keep these current residuals instead of showing only G01.
for route in item.get('remainingRoutes') or []:
if route in known_routes:
continue
if any(gap.get('id') and gap['id'] in route for gap in gaps):
continue
displayed = True
print(f" [{item.get('title')},第 {item.get('cycle')} 轮] {route}")
if details:
for gap in gaps:
if gap not in open_gaps:
print(f" [{gap.get('id')}] 已报告实现、未验证:{gap.get('entry')}")
if not displayed:
print(' 当前交接未列明确余项;不能据此认定整条 UI 主线完成。')
no_handoff = [module for module in ui.get('modules', [])
if not module.get('gaps') and not module.get('progressHandoff')]
if no_handoff:
print(' 尚无模块专属进度记录:' + '、'.join(module['id'] for module in no_handoff) + '(状态未知,不显示零进度)')
if details:
for module in ui.get('modules', []):
print(f" 模块 {module.get('id')} / {module.get('title')}:{module.get('status')};完整样式与入口对标未确认")
candidate = data.get('candidate')
print('\n应用候选与源码集成:')
if candidate:
print(f" 调度器指定候选:{candidate.get('path')}")
pending = candidate.get('pendingSourceCycles')
if pending is None:
print(' 待组合源码批次:未记录;不使用轮次差估算。')
else:
cycle_list = '、'.join(map(str, pending)) or '无'
print(f" 调度器待组合 {len(pending)} 批:{cycle_list}")
if candidate.get('pendingHistoryIncomplete'):
print(' 待组合历史不完整,不能认定候选已包含所有源码。')
external = candidate.get('externalIntegrationUnconfirmed') or []
if external:
print(' 另有根会话补缺未记录候选归属:' + '、'.join(item.get('id', '未知') for item in external))
if candidate.get('refreshDue'):
print(' 已达到记录中的候选刷新条件。')
print(f" 部署记录:{candidate.get('deployment') or '未知'};源码包含关系及运行结果未验证。")
else:
print(' 当前任务尚无可读取候选记录。')
build = data.get('build') or {}
if details or build.get('blockingSymbols') or build.get('status') in {'failed', 'blocked'}:
print(f" 最近构件交接:第 {build.get('cycle', '—')} 轮 / {build.get('status') or '未记录'}")
if build.get('logPath'):
print(f" 构件日志:{build['logPath']}")
for symbol in (build.get('blockingSymbols') or [])[:8]:
print(f" 构建卡点:{symbol}")
print(f"验证:{data.get('validation')};验收:{data.get('acceptance')}。轮次、心跳和构建不计作功能完成。")
for note in data.get('notes', []):
print(f'提示:{note}')
def interval_value(value):
interval = float(value)
if not math.isfinite(interval) or interval < 1:
raise argparse.ArgumentTypeError('刷新间隔必须是至少 1 秒的有限数字')
return interval
def main():
parser = argparse.ArgumentParser(description="查看 Blender Web 移植真实进度")
parser.add_argument("--json", action="store_true", help="输出机器可读 JSON")
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--details', action='store_true', help='展开源码/进程/交接/已实现条目;默认只显示当前执行与实际余项')
parser.add_argument('--json', action='store_true', help='输出 JSON;配合 --watch 时每行一个快照')
parser.add_argument('--watch', action='store_true', help='持续刷新,Ctrl+C 退出')
parser.add_argument('--interval', type=interval_value, default=5, metavar='秒', help='刷新间隔,默认 5 秒')
parser.add_argument('--recent', type=int, choices=range(0, 11), default=3, metavar='0..10', help='显示最近交接条数,默认 3,最多回看 24 轮')
args = parser.parse_args()
data = snapshot()
if args.json:
print(json.dumps(data, ensure_ascii=False, indent=2)); return
print(f"WBV2 task: {data['task']} parent: {data['parentManifest']}")
print(f"Runner: {data['runner']['state']} pid={data['runner']['pid']} active={data['runner']['activeJob'] or '-'} verification={data['runner']['verificationTiming']}")
print("Backlog: " + ", ".join(f"{k}={v}" for k, v in sorted(data['backlog']['byStatus'].items())) + f" total={data['backlog']['total']}")
print(f"Business: needs_implementation={len(data['business']['needsImplementation'])} ready_for_validation={data['business']['readyForValidation']} verified={len(data['business']['verified'])}")
print(f"Builds: pending_verification={len(data['builds']['pendingVerification'])} stale={data['builds']['stale']}")
for item in data["recommendations"]: print(f"Next: {item}")
try:
while True:
data = snapshot(args.recent)
if args.json:
print(json.dumps(data, ensure_ascii=False, indent=None if args.watch else 2), flush=True)
else:
if args.watch and sys.stdout.isatty():
print('\033[2J\033[H', end='')
display(data, details=args.details)
sys.stdout.flush()
if not args.watch:
return 0 if data.get('task') else 1
time.sleep(args.interval)
except (KeyboardInterrupt, BrokenPipeError):
return 0
if __name__ == "__main__": main()
if __name__ == '__main__':
raise SystemExit(main())