feat: add page map recognition and grouped task planning

This commit is contained in:
admin committed 2026-10-06 13:24:25 +08:00
1 parent 386198e8e0
commit f7628ab41e
76 files changed
+12437 -49

No files matched your search

+146
View File
@@ -0,0 +1,146 @@
"""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': '分组供后续调度迁移使用;现有脚本仍从首页启动并自行导航,不能直接送到子页面执行。此命令不会操作游戏。',
}