#!/usr/bin/env python3 """Read-only Phase2 pending draft report. Offline by default; opt-in configuration generation only.""" import json import os import sys from datetime import datetime from pathlib import Path from zoneinfo import ZoneInfo ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from src.pending_pool import build_ordered_candidates, input_path, experience_order from src.config_params import build_config_params, config_params_source, SOURCE, GAP from src.queue_depth import queue_depth from src.task_add_client import build_payload TZ = ZoneInfo('Asia/Shanghai') def _load_candidates(path): data = json.loads(path.read_text()) return data if isinstance(data, list) else list(data['candidates']) def _render(candidates, ts, src): candidates = [c for c in candidates if not os.environ.get('PENDING_SECTION') or c.get('section', 'B') == os.environ['PENDING_SECTION'].strip().upper()] depth = queue_depth() lines = [f'# 代提候选 / 需 Victor 确认 — Dry-run — {ts} (Asia/Shanghai)', '', '> 模式:dry-run;零网络写;需 Victor 人工提交/确认', '> 积分:待平台核实', '> perf_order=unset;order_source=experience(经验序 / 待平台核实)', f'> configParams source: {SOURCE}', f'> configParams_source={config_params_source()}', f'> {GAP}', '', '## A→B 顺序草稿', '', f'输入:{src};候选条数:{len(candidates)}', f'队列深度:{depth if depth is not None else "未知,需人工确认"}', '队列≥95:禁 live;适配 W1 优先,不抢空位。', ''] if depth is not None and depth >= 95: lines.extend(['> **禁 live / 队列≥95**', '']) for i, c in enumerate(candidates, 1): model = c.get('model_address') or c.get('modelAddress') or c.get('model_id') or '' gpus = experience_order(c.get('try_next_gpus', [])) target = gpus[0] if gpus else c.get('targetGpu', '') status = c.get('status', 'needs_human') lines.extend([f'### {i}. Section {c.get("section", "B")} — `{model}`', '', f'状态:{status};needs_human={c.get("needs_human", True)}', f'过滤:{c.get("skip_reason") or "名称硬门未命中;元数据/唯一性待人工核实"}', '']) lines.extend([f'未适配卡(仅 pending 缺口):{", ".join(gpus) or "未提供,需确认"}', 'perf_order=unset;order_source=experience;天垓100 一般不扇出', '']) draft = build_payload(model, target, 'vllm', strategy_id='', config_params=build_config_params(target)) # Display only; this truncated draft must never be submitted. draft['configParams'] = draft['configParams'][:400] + '\n… [truncated; dry-run only]' lines.extend(['build-config query: gpuType=&framework=vllm&taskType=text-generation; POST empty body', 'POST 字段草稿(skipped 行禁止提交):', '```json', json.dumps(draft, ensure_ascii=False, indent=2), '```', '需 Victor 人工提交/确认;积分:待平台核实', '']) report = '\n'.join(lines) # Defensive redaction even if an input happens to contain an injected credential. secrets = [os.environ.get(k, '') for k in ('XC_TOKEN', 'MODELHUB_XC_TOKEN', 'EXTERNAL_SERVICE_TOKEN', 'Xc-Token')] if os.environ.get('XC_TOKEN_FILE'): try: secrets.append(Path(os.environ['XC_TOKEN_FILE']).read_text().strip()) except OSError: pass for secret in secrets: if secret: report = report.replace(secret, '[REDACTED]') return report def main(): md = input_path('PENDING_SUBMIT_MD', 'pending-submit.md') txt = input_path('PENDING_SUBMIT_TXT', 'pending-submit.txt') if md.is_file() or txt.is_file(): src = md if md.is_file() else txt candidates = build_ordered_candidates(md, txt) else: src = Path(os.environ.get('DRY_RUN_CANDIDATES', ROOT / 'config/candidates.example.json')) candidates = _load_candidates(src) now = datetime.now(TZ) out = ROOT / 'reports' / f'dry-run-{now:%Y%m%d-%H%M%S}.md' out.parent.mkdir(exist_ok=True) out.write_text(_render(candidates, now.strftime('%Y-%m-%d %H:%M:%S'), src), encoding='utf-8') print(f'Wrote {out}') if __name__ == '__main__': main()