Files
workinf_Blender_Wasm/my-tools/view_wbv2_progress.py
mes123456 f3e21c6b98
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
chore: checkpoint web Blender migration source, tools and docs
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>
2026-11-10 11:18:01 +08:00

706 lines
42 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""只读查看 Blender → Web Blender UI 对标执行状态与最新功能交接。
不会启动执行器、构建、部署或运行功能验证;候选源码对应关系仍明确标记为未知。
"""
from __future__ import annotations
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 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=__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()
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__':
raise SystemExit(main())