147 lines
7.6 KiB
Python
147 lines
7.6 KiB
Python
"""Configuration-only task audit and development plan. No desktop input."""
|
|
from collections import Counter
|
|
from pathlib import Path
|
|
|
|
PAGE_ORDER = ('home', 'shop', 'territory', 'guild', 'any_primary')
|
|
|
|
|
|
class TaskPlanner:
|
|
def __init__(self, data, root):
|
|
self.data = data
|
|
self.root = Path(root)
|
|
self.pages = {page['id']: page for page in data['pages']}
|
|
|
|
def ancestry(self, page_id):
|
|
result = []
|
|
while page_id is not None and page_id not in result:
|
|
if page_id not in self.pages:
|
|
break
|
|
result.append(page_id)
|
|
page_id = self.pages[page_id]['parent']
|
|
return list(reversed(result))
|
|
|
|
def audit(self):
|
|
issues = []
|
|
for key in ('pages', 'edges', 'tasks', 'planned_tasks'):
|
|
ids = [item['id'] for item in self.data[key]]
|
|
issues.extend(f'{key}: duplicate {ident}' for ident, n in Counter(ids).items() if n > 1)
|
|
task_ids = [task['id'] for task in self.data['tasks'] + self.data['planned_tasks']]
|
|
issues.extend(f'tasks: duplicate across lists {ident}'
|
|
for ident, n in Counter(task_ids).items() if n > 1)
|
|
for page in self.data['pages']:
|
|
parent = page['parent']
|
|
if parent is not None and parent not in self.pages:
|
|
issues.append(f"{page['id']}: missing parent {parent}")
|
|
seen, current = set(), page['id']
|
|
while current in self.pages:
|
|
if current in seen:
|
|
issues.append(f"{page['id']}: cyclic parent")
|
|
break
|
|
seen.add(current)
|
|
current = self.pages[current]['parent']
|
|
for variant in page.get('recognition', []):
|
|
for spec in variant:
|
|
if not (self.root / 'assets' / spec['template']).is_file():
|
|
issues.append(f"{page['id']}: missing template {spec['template']}")
|
|
for edge in self.data['edges']:
|
|
if edge['from'] not in self.pages or edge['to'] not in self.pages:
|
|
issues.append(f"{edge['id']}: missing endpoint")
|
|
for task in self.data['tasks'] + self.data['planned_tasks']:
|
|
if task.get('group_page') not in PAGE_ORDER:
|
|
issues.append(f"{task['id']}: unknown group page {task.get('group_page')}")
|
|
page_id = task.get('task_page')
|
|
if page_id is None and task.get('group_page') == 'any_primary':
|
|
continue
|
|
if page_id not in self.pages:
|
|
issues.append(f"{task['id']}: missing task page")
|
|
for ident in self.ancestry(page_id):
|
|
if self.pages[ident].get('automation', {}).get('enabled') is False:
|
|
issues.append(f"{task['id']}: disabled ancestor {ident}")
|
|
script = task.get('script')
|
|
if script and not (self.root / script).is_file():
|
|
issues.append(f"{task['id']}: missing script {script}")
|
|
return {
|
|
'valid': not issues, 'issues': issues,
|
|
'counts': {key: len(self.data[key]) for key in ('pages', 'edges', 'tasks', 'planned_tasks')},
|
|
'recognized_pages': sum(bool(page.get('recognition')) for page in self.pages.values()),
|
|
'verified_edges': sum(edge['status'] in ('live_verified', 'workflow_verified')
|
|
for edge in self.data['edges']),
|
|
}
|
|
|
|
def hub(self, task):
|
|
page = task.get('task_page')
|
|
group = task['group_page']
|
|
if group == 'any_primary':
|
|
return 'any_primary'
|
|
if page and page.startswith('offer.'):
|
|
return 'home.offer'
|
|
ancestors = self.ancestry(page)
|
|
# Siblings share their first entrance below the primary page.
|
|
return ancestors[1] if len(ancestors) > 1 and ancestors[0] == group else page
|
|
|
|
def family(self, task):
|
|
if task.get('reusable_pattern'):
|
|
return task['reusable_pattern']
|
|
ident, page = task['id'], task.get('task_page') or ''
|
|
if 'purchases' in ident or ident == 'shop_daily_coin_ticket':
|
|
return 'priority_purchase'
|
|
if ident in ('sweep', 'planet', 'relic_daily_sweep'):
|
|
return 'sweep'
|
|
if ident == 'wall_command_build':
|
|
return 'explicit_command_build'
|
|
if 'task' in page or ident in ('tasks', 'lizhu', 'pet_challenge_rewards'):
|
|
return 'scrolling_reward_list'
|
|
if ident in ('offer_diamond_fund_rewards', 'offer_relic_pass_rewards', 'offer_pet_pass_rewards'):
|
|
return 'reward_icon_list'
|
|
if 'store' in page or 'free' in ident or ident == 'shop_totem_free_rewards':
|
|
return 'free_offer'
|
|
return 'page_specific'
|
|
|
|
def plan(self, include_deferred=False, include_manual=False, group_page=None):
|
|
rows, omitted = [], []
|
|
existing = {task['id'] for task in self.data['tasks']}
|
|
for task in self.data['tasks'] + self.data['planned_tasks']:
|
|
if group_page and task['group_page'] != group_page:
|
|
continue
|
|
deferred = 'deferred' in task.get('status', '')
|
|
manual = task['id'] in ('sweep', 'wall_command_build') or task.get('frequency') in ('on_demand', 'explicit_user_command')
|
|
if deferred and not include_deferred or manual and not include_manual:
|
|
omitted.append({'id': task['id'], 'reason': 'deferred' if deferred else 'explicit_command'})
|
|
continue
|
|
page_id = task.get('task_page')
|
|
is_existing = task['id'] in existing
|
|
blockers = []
|
|
if not is_existing:
|
|
blockers.append('任务执行器尚未实现')
|
|
if not self.pages.get(page_id, {}).get('recognition'):
|
|
blockers.append('任务页面尚无识别模板')
|
|
if task.get('consumes_resources') is None:
|
|
blockers.append('资源消耗需核实')
|
|
if 'rules_incomplete' in task.get('status', ''):
|
|
blockers.append('业务规则仍待确认')
|
|
if deferred:
|
|
blockers.append('用户要求延期完善')
|
|
rows.append({
|
|
'id': task['id'], 'name': task['name'], 'group_page': task['group_page'],
|
|
'hub': self.hub(task), 'task_page': page_id, 'family': self.family(task),
|
|
'implementation': 'existing_script' if is_existing else 'planned',
|
|
'script': task.get('script'), 'frequency': task.get('frequency'),
|
|
'consumes_resources': task.get('consumes_resources'),
|
|
'requires_explicit_command': manual, 'blockers': blockers,
|
|
'script_start': task.get('current_script_start'),
|
|
'script_finish': task.get('current_script_finish'),
|
|
'navigation_mode': 'legacy_script_owns_navigation' if is_existing else 'future_page_local_task',
|
|
})
|
|
rows.sort(key=lambda row: (PAGE_ORDER.index(row['group_page']), row['hub'] or '',
|
|
row['implementation'] != 'existing_script', row['id']))
|
|
visits = []
|
|
for row in rows:
|
|
if not visits or (visits[-1]['group_page'], visits[-1]['hub']) != (row['group_page'], row['hub']):
|
|
visits.append({'group_page': row['group_page'], 'hub': row['hub'], 'tasks': []})
|
|
visits[-1]['tasks'].append(row['id'])
|
|
return {
|
|
'mode': 'read_only_development_plan', 'tasks': rows, 'visit_groups': visits,
|
|
'omitted': omitted,
|
|
'note': '分组供后续调度迁移使用;现有脚本仍从首页启动并自行导航,不能直接送到子页面执行。此命令不会操作游戏。',
|
|
}
|