Files
Neo-ZQYY/.kiro/state/.audit_context.json

191 lines
71 KiB
JSON
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.
{
"built_at": "2026-03-09T00:48:01.403432+08:00",
"prompt_id": "P20260309-004313",
"prompt_at": "2026-03-09T00:43:13.639535+08:00",
"audit_required": true,
"db_docs_required": true,
"reasons": [
"root-file",
"dir:admin-web",
"dir:backend",
"dir:etl",
"dir:miniprogram",
"dir:db",
"db-schema-change",
"dir:shared"
],
"changed_files": [
".env",
".env.template",
".gitignore",
"_tmp_replace2.py",
"apps/admin-web/README.md",
"apps/admin-web/src/App.tsx",
"apps/admin-web/src/__tests__/taskLogParser.test.ts",
"apps/admin-web/src/api/businessDay.ts",
"apps/admin-web/src/api/schedules.ts",
"apps/admin-web/src/components/BusinessDayHint.tsx",
"apps/admin-web/src/components/LogStream.tsx",
"apps/admin-web/src/components/ScheduleHistoryDrawer.tsx",
"apps/admin-web/src/components/ScheduleTab.tsx",
"apps/admin-web/src/components/TaskLogViewer.tsx",
"apps/admin-web/src/pages/LogViewer.tsx",
"apps/admin-web/src/pages/TaskConfig.tsx",
"apps/admin-web/src/pages/TaskManager.tsx",
"apps/admin-web/src/store/businessDayStore.ts",
"apps/admin-web/src/types/index.ts",
"apps/admin-web/src/utils/",
"apps/admin-web/tsconfig.tsbuildinfo",
"apps/backend/README.md",
"apps/backend/app/ai/",
"apps/backend/app/config.py",
"apps/backend/app/main.py",
"apps/backend/app/middleware/permission.py",
"apps/backend/app/routers/admin_applications.py",
"apps/backend/app/routers/business_day.py",
"apps/backend/app/routers/execution.py",
"apps/backend/app/routers/member_retention_clue.py",
"apps/backend/app/routers/ops_panel.py",
"apps/backend/app/routers/schedules.py",
"apps/backend/app/routers/tasks.py",
"apps/backend/app/routers/xcx_auth.py",
"apps/backend/app/routers/xcx_notes.py",
"apps/backend/app/routers/xcx_tasks.py",
"apps/backend/app/schemas/execution.py",
"apps/backend/app/schemas/member_retention_clue.py",
"apps/backend/app/schemas/schedules.py",
"apps/backend/app/schemas/tasks.py",
"apps/backend/app/schemas/xcx_auth.py",
"apps/backend/app/schemas/xcx_notes.py",
"apps/backend/app/schemas/xcx_tasks.py",
"apps/backend/app/services/application.py",
"apps/backend/app/services/cli_builder.py",
"apps/backend/app/services/note_reclassifier.py",
"apps/backend/app/services/note_service.py",
"apps/backend/app/services/recall_detector.py",
"apps/backend/app/services/scheduler.py",
"apps/backend/app/services/task_executor.py",
"apps/backend/tests/test_ai_cache.py"
],
"high_risk_files": [
"apps/admin-web/src/App.tsx",
"apps/admin-web/src/__tests__/taskLogParser.test.ts",
"apps/admin-web/src/api/businessDay.ts",
"apps/admin-web/src/api/schedules.ts",
"apps/admin-web/src/components/BusinessDayHint.tsx",
"apps/admin-web/src/components/LogStream.tsx",
"apps/admin-web/src/components/ScheduleHistoryDrawer.tsx",
"apps/admin-web/src/components/ScheduleTab.tsx",
"apps/admin-web/src/components/TaskLogViewer.tsx",
"apps/admin-web/src/pages/LogViewer.tsx",
"apps/admin-web/src/pages/TaskConfig.tsx",
"apps/admin-web/src/pages/TaskManager.tsx",
"apps/admin-web/src/store/businessDayStore.ts",
"apps/admin-web/src/types/index.ts",
"apps/admin-web/src/utils/",
"apps/backend/app/ai/",
"apps/backend/app/config.py",
"apps/backend/app/main.py",
"apps/backend/app/middleware/permission.py",
"apps/backend/app/routers/admin_applications.py",
"apps/backend/app/routers/business_day.py",
"apps/backend/app/routers/execution.py",
"apps/backend/app/routers/member_retention_clue.py",
"apps/backend/app/routers/ops_panel.py",
"apps/backend/app/routers/schedules.py",
"apps/backend/app/routers/tasks.py",
"apps/backend/app/routers/xcx_auth.py",
"apps/backend/app/routers/xcx_notes.py",
"apps/backend/app/routers/xcx_tasks.py",
"apps/backend/app/schemas/execution.py",
"apps/backend/app/schemas/member_retention_clue.py",
"apps/backend/app/schemas/schedules.py",
"apps/backend/app/schemas/tasks.py",
"apps/backend/app/schemas/xcx_auth.py",
"apps/backend/app/schemas/xcx_notes.py",
"apps/backend/app/schemas/xcx_tasks.py",
"apps/backend/app/services/application.py",
"apps/backend/app/services/cli_builder.py",
"apps/backend/app/services/note_reclassifier.py",
"apps/backend/app/services/note_service.py",
"apps/backend/app/services/recall_detector.py",
"apps/backend/app/services/scheduler.py",
"apps/backend/app/services/task_executor.py"
],
"session_diff": {
"added": [
"apps/backend/tests/test_ai_cache.py",
"docs/audit/prompt_logs/prompt_log_20260309_004313.md",
"docs/audit/session_logs/2026-03/08/50_81ac54f5_211524/main_01_7a544eac.md",
"docs/audit/session_logs/2026-03/09/03_81f427f3_003443/main_01_fbe02a11.md",
"docs/audit/session_logs/2026-03/09/_ref_81ac54f5.md"
],
"modified": [
"docs/audit/session_logs/2026-02/11/_day_index.json",
"docs/audit/session_logs/2026-02/11/_day_index_full.json",
"docs/audit/session_logs/2026-02/12/_day_index.json",
"docs/audit/session_logs/2026-02/12/_day_index_full.json",
"docs/audit/session_logs/2026-02/13/_day_index.json",
"docs/audit/session_logs/2026-02/13/_day_index_full.json",
"docs/audit/session_logs/2026-02/14/_day_index.json",
"docs/audit/session_logs/2026-02/14/_day_index_full.json",
"docs/audit/session_logs/2026-02/15/_day_index.json",
"docs/audit/session_logs/2026-02/15/_day_index_full.json",
"docs/audit/session_logs/2026-02/16/_day_index.json",
"docs/audit/session_logs/2026-02/16/_day_index_full.json",
"docs/audit/session_logs/2026-02/17/_day_index.json",
"docs/audit/session_logs/2026-02/17/_day_index_full.json",
"docs/audit/session_logs/2026-02/18/_day_index.json",
"docs/audit/session_logs/2026-02/18/_day_index_full.json",
"docs/audit/session_logs/2026-02/19/_day_index.json",
"docs/audit/session_logs/2026-02/19/_day_index_full.json",
"docs/audit/session_logs/2026-02/20/_day_index.json",
"docs/audit/session_logs/2026-02/20/_day_index_full.json",
"docs/audit/session_logs/2026-02/21/_day_index.json",
"docs/audit/session_logs/2026-02/21/_day_index_full.json",
"docs/audit/session_logs/2026-02/22/_day_index.json",
"docs/audit/session_logs/2026-02/22/_day_index_full.json",
"docs/audit/session_logs/2026-02/23/_day_index.json",
"docs/audit/session_logs/2026-02/23/_day_index_full.json",
"docs/audit/session_logs/2026-02/24/_day_index.json",
"docs/audit/session_logs/2026-02/24/_day_index_full.json",
"docs/audit/session_logs/2026-02/25/_day_index.json",
"docs/audit/session_logs/2026-02/25/_day_index_full.json",
"docs/audit/session_logs/2026-02/26/_day_index.json",
"docs/audit/session_logs/2026-02/26/_day_index_full.json",
"docs/audit/session_logs/2026-02/27/_day_index.json",
"docs/audit/session_logs/2026-02/27/_day_index_full.json",
"docs/audit/session_logs/2026-02/28/_day_index.json",
"docs/audit/session_logs/2026-02/28/_day_index_full.json",
"docs/audit/session_logs/2026-03/01/_day_index.json",
"docs/audit/session_logs/2026-03/01/_day_index_full.json",
"docs/audit/session_logs/2026-03/02/_day_index.json",
"docs/audit/session_logs/2026-03/02/_day_index_full.json",
"docs/audit/session_logs/2026-03/03/_day_index.json",
"docs/audit/session_logs/2026-03/03/_day_index_full.json",
"docs/audit/session_logs/2026-03/04/_day_index.json",
"docs/audit/session_logs/2026-03/04/_day_index_full.json",
"docs/audit/session_logs/2026-03/05/_day_index.json",
"docs/audit/session_logs/2026-03/05/_day_index_full.json",
"docs/audit/session_logs/2026-03/06/_day_index.json",
"docs/audit/session_logs/2026-03/06/_day_index_full.json",
"docs/audit/session_logs/2026-03/07/_day_index.json",
"docs/audit/session_logs/2026-03/07/_day_index_full.json"
],
"deleted": [
"docs/audit/session_logs/2026-03/08/50_81ac54f5_211524/main_01_ac8de71d.md"
]
},
"compliance": {
"code_without_docs": [],
"new_migration_sql": [],
"has_bd_manual": false,
"has_audit_record": false,
"has_ddl_baseline": false,
"api_changed": false,
"openapi_spec_stale": false
},
"diff_stat": ".env | 41 +\n .env.template | 57 +-\n .gitignore | 9 +\n .kiro/.audit_context.json | 526 -\n .kiro/.compliance_state.json | 74 -\n .kiro/.git_snapshot.json | 106 -\n .kiro/.gitkeep | 0\n .kiro/.last_prompt_id.json | 4 -\n .kiro/agents/audit-writer.md | 109 +-\n .kiro/hooks/agent-on-stop.kiro.hook | 3 +-\n .kiro/hooks/audit-flagger.kiro.hook | 15 -\n .kiro/hooks/audit-reminder.kiro.hook | 15 -\n .kiro/hooks/change-compliance.kiro.hook | 15 -\n .kiro/hooks/dataflow-analyze.kiro.hook | 15 -\n .kiro/hooks/db-docs-sync.kiro.hook | 15 -\n .kiro/hooks/etl-data-consistency.kiro.hook | 15 -\n .kiro/hooks/prompt-audit-log.kiro.hook | 15 -\n .kiro/hooks/prompt-on-submit.kiro.hook | 2 +-\n .kiro/hooks/run-audit-writer.kiro.hook | 6 +-\n .kiro/hooks/session-log.kiro.hook | 15 -\n .kiro/scripts/agent_on_stop.py | 506 +-\n .kiro/scripts/audit_flagger.py | 6 +-\n .kiro/scripts/audit_reminder.py | 4 +-\n .kiro/scripts/build_audit_context.py | 18 +-\n .kiro/scripts/change_compliance_prescan.py | 6 +-\n .kiro/scripts/prompt_audit_log.py | 4 +-\n .kiro/scripts/prompt_on_submit.py | 70 +-\n .kiro/scripts/session_log.py | 13 +-\n .kiro/settings/mcp.json | 24 +-\n .../assets/schema-changelog-template.md | 2 +-\n .../assets/table-structure-template.md | 2 +-\n .../assets/audit-record-template.md | 2 +-\n .../assets/file-changelog-templates.md | 16 +-\n .../specs/02-etl-dws-miniapp-extensions/design.md | 2 +-\n .kiro/specs/03-miniapp-auth-system/tasks.md | 78 +-\n .kiro/specs/[ETL]-fullstack-integration/design.md | 4 +-\n .../[ETL]-fullstack-integration/requirements.md | 2 +-\n .kiro/specs/[ETL]-fullstack-integration/tasks.md | 28 +-\n .kiro/specs/admin-web-console/requirements.md | 32 +-\n .kiro/specs/etl-aggregation-fix/.config.kiro | 1 -\n .kiro/specs/etl-aggregation-fix/design.md | 464 -\n .kiro/specs/etl-aggregation-fix/requirements.md | 84 -\n .kiro/specs/etl-aggregation-fix/tasks.md | 228 -\n .kiro/specs/etl-dws-flow-refactor/design.md | 1 +\n .kiro/specs/etl-dws-flow-refactor/requirements.md | 2 +-\n .kiro/specs/miniapp-core-business/.config.kiro | 1 -\n .kiro/specs/miniapp-core-business/requirements.md | 189 -\n .kiro/steering/doc-map.md | 2 +-\n .kiro/steering/export-paths.md | 51 +-\n .kiro/steering/governance.md | 27 -\n .kiro/steering/language-zh.md | 19 +-\n .kiro/steering/product.md | 22 -\n .kiro/steering/steering-readme-maintainer.md | 2 +-\n .kiro/steering/structure-lite.md | 34 -\n .kiro/steering/structure.md | 2 +-\n .kiro/steering/tech.md | 24 +-\n .kiro/steering/testing-env.md | 28 +-\n apps/admin-web/README.md | 17 +-\n apps/admin-web/src/App.tsx | 6 +-\n apps/admin-web/src/api/schedules.ts | 21 +-\n apps/admin-web/src/components/LogStream.tsx | 15 +-\n apps/admin-web/src/components/ScheduleTab.tsx | 144 +-\n apps/admin-web/src/pages/LogViewer.tsx | 45 +-\n apps/admin-web/src/pages/TaskConfig.tsx | 296 +-\n apps/admin-web/src/pages/TaskManager.tsx | 138 +-\n apps/admin-web/src/types/index.ts | 7 +\n apps/admin-web/tsconfig.tsbuildinfo | 2 +-\n apps/backend/README.md | 162 +-\n apps/backend/app/config.py | 133 +-\n apps/backend/app/main.py | 71 +-\n apps/backend/app/routers/execution.py | 4 +-\n apps/backend/app/routers/ops_panel.py | 7 +-\n apps/backend/app/routers/schedules.py | 111 +-\n apps/backend/app/routers/tasks.py | 12 +-\n apps/backend/app/routers/xcx_auth.py | 352 +-\n apps/backend/app/schemas/execution.py | 1 +\n apps/backend/app/schemas/schedules.py | 1 +\n apps/backend/app/schemas/tasks.py | 5 +\n apps/backend/app/schemas/xcx_auth.py | 106 +\n apps/backend/app/services/application.py | 9 +\n apps/backend/app/services/cli_builder.py | 10 +\n apps/backend/app/services/scheduler.py | 2 +-\n apps/backend/app/services/task_executor.py | 72 +-\n apps/backend/app/services/task_queue.py | 55 +-\n apps/backend/app/services/task_registry.py | 2 +\n apps/backend/app/services/wechat.py | 6 +-\n apps/backend/docs/API-REFERENCE.md | 101 +-\n apps/backend/pyproject.toml | 1 +\n apps/etl/connectors/feiqiu/.env | 11 +-\n apps/etl/connectors/feiqiu/api/client.py | 12 +\n apps/etl/connectors/feiqiu/api/recording_client.py | 11 +\n apps/etl/connectors/feiqiu/cli/main.py | 36 +\n apps/etl/connectors/feiqiu/config/defaults.py | 3 +-\n apps/etl/connectors/feiqiu/config/env_parser.py | 4 +\n apps/etl/connectors/feiqiu/config/settings.py | 6 +\n apps/etl/connectors/feiqiu/database/connection.py | 10 +-\n apps/etl/connectors/feiqiu/database/operations.py | 23 +-\n .../feiqiu/docs/business-rules/dws_metrics.md | 2 +-\n .../DWD/Ex/BD_manual_dim_groupbuy_package_ex.md | 6 +\n .../Ex/BD_manual_dwd_assistant_service_log_ex.md | 2 +-\n .../database/DWD/main/BD_manual_billiards_dwd.md | 4 +-\n .../docs/database/DWD/main/BD_manual_dim_table.md | 2 +-\n .../DWD/main/BD_manual_dwd_settlement_head.md | 21 +-\n .../DWS/main/BD_manual_cfg_area_category.md | 91 +-\n .../main/BD_manual_dws_assistant_daily_detail.md | 36 +-\n .../main/BD_manual_dws_finance_daily_summary.md | 55 +-\n .../main/BD_manual_dws_finance_discount_detail.md | 15 +-\n .../main/BD_manual_dws_finance_income_structure.md | 39 +-\n .../main/BD_manual_dws_finance_recharge_summary.md | 12 +-\n .../BD_manual_dws_member_consumption_summary.md | 68 +-\n .../DWS/main/BD_manual_dws_member_visit_detail.md | 59 +-\n .../DWS/main/BD_manual_dws_order_summary.md | 30 +-\n .../etl/connectors/feiqiu/docs/etl_tasks/README.md | 5 +-\n .../connectors/feiqiu/docs/etl_tasks/dwd_tasks.md | 50 +-\n .../connectors/feiqiu/docs/etl_tasks/dws_tasks.md | 192 +-\n .../docs/etl_tasks/ods_task_params_matrix.md | 3 +-\n .../connectors/feiqiu/docs/etl_tasks/ods_tasks.md | 34 +-\n .../feiqiu/docs/etl_tasks/utility_tasks.md | 1 -\n .../connectors/feiqiu/orchestration/flow_runner.py | 9 +-\n .../feiqiu/orchestration/task_executor.py | 7 +\n .../feiqiu/orchestration/task_registry.py | 6 +\n .../feiqiu/quality/consistency_checker.py | 136 +-\n .../connectors/feiqiu/scripts/check_json_vs_md.py | 3 +\n .../connectors/feiqiu/scripts/compare_api_ods.py | 3 +\n .../connectors/feiqiu/scripts/compare_ddl_db.py | 3 +\n .../feiqiu/scripts/compare_ods_vs_summary_v2.py | 3 +\n .../feiqiu/scripts/debug/debug_blackbox.py | 9 +-\n .../feiqiu/scripts/full_api_refresh_v2.py | 3 +\n .../feiqiu/scripts/gen_audit_dashboard.py | 3 +\n .../etl/connectors/feiqiu/scripts/ods_columns.json | 21 -\n .../feiqiu/scripts/refresh_json_and_audit.py | 4 +-\n .../connectors/feiqiu/scripts/run_compare_v3.py | 5 +-\n .../feiqiu/scripts/run_compare_v3_fixed.py | 4 +-\n apps/etl/connectors/feiqiu/scripts/run_update.py | 41 +-\n .../feiqiu/scripts/validate_bd_manual.py | 3 +\n .../feiqiu/scripts/verify_dws_extensions.py | 2 +\n apps/etl/connectors/feiqiu/tasks/base_task.py | 15 +-\n .../connectors/feiqiu/tasks/dwd/dwd_load_task.py | 262 +-\n apps/etl/connectors/feiqiu/tasks/dws/__init__.py | 4 +\n .../feiqiu/tasks/dws/assistant_customer_task.py | 9 +-\n .../feiqiu/tasks/dws/assistant_daily_task.py | 30 +-\n .../feiqiu/tasks/dws/assistant_finance_task.py | 17 +-\n .../feiqiu/tasks/dws/assistant_monthly_task.py | 21 +-\n .../tasks/dws/assistant_order_contribution_task.py | 31 +-\n .../connectors/feiqiu/tasks/dws/base_dws_task.py | 157 +-\n .../feiqiu/tasks/dws/finance_base_task.py | 66 +-\n .../feiqiu/tasks/dws/finance_daily_task.py | 18 +-\n .../feiqiu/tasks/dws/finance_discount_task.py | 40 +-\n .../feiqiu/tasks/dws/finance_income_task.py | 76 +-\n .../feiqiu/tasks/dws/finance_recharge_task.py | 12 +-\n .../feiqiu/tasks/dws/goods_stock_daily_task.py | 26 +-\n .../feiqiu/tasks/dws/goods_stock_monthly_task.py | 28 +-\n .../feiqiu/tasks/dws/goods_stock_weekly_task.py | 28 +-\n .../feiqiu/tasks/dws/index/member_index_base.py | 8 +-\n .../feiqiu/tasks/dws/index/relation_index_task.py | 2 +-\n .../tasks/dws/index/spending_power_index_task.py | 64 +-\n .../feiqiu/tasks/dws/maintenance_task.py | 4 +\n .../feiqiu/tasks/dws/member_consumption_task.py | 113 +-\n .../feiqiu/tasks/dws/member_visit_task.py | 146 +-\n apps/etl/connectors/feiqiu/tasks/ods/ods_tasks.py | 501 +-\n .../tasks/utility/dws_build_order_summary_task.py | 3 +-\n .../feiqiu/tasks/utility/manual_ingest_task.py | 2 -\n .../feiqiu/tasks/verification/dws_verifier.py | 4 +-\n .../feiqiu/tasks/verification/index_verifier.py | 2 +-\n .../connectors/feiqiu/tests/unit/test_config.py | 29 +\n apps/etl/connectors/feiqiu/utils/json_store.py | 1 -\n apps/miniprogram/README.md | 66 +-\n apps/miniprogram/doc/prd.md | 997 -\n apps/miniprogram/miniprogram/app.json | 59 +-\n apps/miniprogram/miniprogram/app.ts | 76 +-\n apps/miniprogram/miniprogram/app.wxss | 101 +\n .../miniprogram/miniprogram/pages/index/index.wxml | 2 +\n apps/miniprogram/miniprogram/pages/logs/logs.wxml | 2 +\n apps/miniprogram/miniprogram/pages/mvp/mvp.wxml | 2 +\n apps/miniprogram/package-lock.json | 4622 ++++-\n apps/miniprogram/package.json | 6 +-\n apps/miniprogram/project.private.config.json | 4 +-\n apps/miniprogram/typings/index.d.ts | 18 +-\n db/etl_feiqiu/seeds/seed_dws_config.sql | 76 +-\n db/fdw/setup_fdw_reverse.sql | 63 +-\n db/fdw/setup_fdw_reverse_test.sql | 66 +-\n docs/DOCUMENTATION-MAP.md | 84 +-\n docs/README.md | 31 +-\n docs/audit/README.md | 14 +-\n docs/audit/audit_dashboard.md | 86 +-\n docs/contracts/openapi/backend-api.json | 5096 ++++-\n .../BD_Manual_assistant_service_records.md | 1 +\n .../BD_Manual_dws_assistant_order_contribution.md | 4 +-\n docs/database/README.md | 1 +\n docs/database/ddl/etl_feiqiu__app.sql | 10 +-\n docs/database/ddl/etl_feiqiu__core.sql | 2 +-\n docs/database/ddl/etl_feiqiu__dwd.sql | 27 +-\n docs/database/ddl/etl_feiqiu__dws.sql | 107 +-\n docs/database/ddl/etl_feiqiu__meta.sql | 2 +-\n docs/database/ddl/etl_feiqiu__ods.sql | 28 +-\n docs/database/ddl/fdw.sql | 2 +-\n docs/database/ddl/zqyy_app__auth.sql | 6 +-\n docs/database/ddl/zqyy_app__public.sql | 27 +-\n docs/deployment/LAUNCH-CHECKLIST.md | 43 +-\n docs/etl-feiqiu-architecture.md | 743 -\n docs/h5_ui/css/ai-icons.css | 5 +\n docs/h5_ui/css/task-detail.css | 81 +\n docs/h5_ui/js/ai-icons.js | 7 -\n docs/h5_ui/js/task-detail-notes.js | 78 +-\n docs/h5_ui/pages/ai-icon-demo.html | 12 +-\n docs/h5_ui/pages/apply.html | 169 +-\n docs/h5_ui/pages/customer-detail.html | 170 +-\n docs/h5_ui/pages/feiqiu-ETL.code-workspace | 13 +\n docs/h5_ui/pages/performance.html | 322 +-\n docs/h5_ui/pages/task-detail-callback.html | 104 +-\n docs/h5_ui/pages/task-detail-priority.html | 104 +-\n docs/h5_ui/pages/task-detail-relationship.html | 104 +-\n docs/h5_ui/pages/task-detail.html | 322 +-\n docs/h5_ui/pages/task-list.html | 13 +-\n \"docs/prd/PRD\\345\\256\\241\\351\\230\\205-Q&A-R2.md\" | 4 +-\n ...264\\271\\345\\212\\233\\346\\214\\207\\346\\225\\260.md\" | 2 +-\n ...276\\235\\350\\265\\226\\347\\237\\251\\351\\230\\265.md\" | 25 +-\n ...213\\206\\345\\210\\206\\346\\200\\273\\350\\247\\210.md\" | 101 +-\n docs/prd/specs/P10-tenant-admin-web.md | 43 +-\n docs/prd/specs/P2-etl-dws-miniapp-extensions.md | 9 +-\n docs/prd/specs/P3-miniapp-auth-system.md | 13 +\n docs/prd/specs/P4-miniapp-core-business.md | 42 +-\n docs/prd/specs/P5-miniapp-ai-integration.md | 476 +-\n docs/prd/specs/P6-miniapp-fe-tasks.md | 35 +-\n docs/prd/specs/P7-miniapp-fe-performance.md | 23 +-\n docs/prd/specs/P8-miniapp-fe-boards.md | 54 +-\n docs/prd/specs/P9-miniapp-fe-details.md | 68 +-\n .../assistant_cancellation_records.json | 40 -\n .../dataflow_analysis/collection_manifest.json | 36 +-\n .../dataflow_2026-02-21_154548.md | 4464 -----\n .../db_schemas/dwd_dim_member.json | 8 +\n .../db_schemas/dwd_dim_store_goods_ex.json | 32 +\n .../db_schemas/dwd_dim_tenant_goods_ex.json | 4 +-\n .../db_schemas/dwd_dwd_assistant_trash_event.json | 95 -\n .../dwd_dwd_assistant_trash_event_ex.json | 39 -\n .../ods_assistant_cancellation_records.json | 158 -\n .../db_schemas/ods_member_profiles.json | 8 +\n .../db_schemas/ods_store_goods_master.json | 32 +\n .../assistant_accounts_master.json | 1454 --\n .../assistant_cancellation_records.json | 508 -\n .../assistant_service_records.json | 1646 --\n .../field_mappings_new/goods_stock_movements.json | 323 -\n .../field_mappings_new/goods_stock_summary.json | 243 -\n .../field_mappings_new/group_buy_packages.json | 940 -\n .../group_buy_redemption_records.json | 1178 --\n .../field_mappings_new/member_balance_changes.json | 657 -\n .../field_mappings_new/member_profiles.json | 537 -\n .../member_stored_value_cards.json | 1712 --\n .../field_mappings_new/payment_transactions.json | 448 -\n .../platform_coupon_redemption_records.json | 766 -\n .../field_mappings_new/recharge_settlements.json | 1668 --\n .../field_mappings_new/refund_transactions.json | 898 -\n .../field_mappings_new/settlement_records.json | 1668 --\n .../field_mappings_new/site_tables_master.json | 667 -\n .../stock_goods_category_tree.json | 350 -\n .../field_mappings_new/store_goods_master.json | 1179 --\n .../store_goods_sales_records.json | 1169 --\n .../table_fee_discount_records.json | 829 -\n .../field_mappings_new/table_fee_transactions.json | 1430 --\n .../field_mappings_new/tenant_goods_master.json | 792 -\n .../json_trees/assistant_cancellation_records.json | 480 -\n .../json_trees/assistant_service_records.json | 240 +-\n .../json_trees/goods_stock_movements.json | 142 +-\n .../json_trees/goods_stock_summary.json | 12 +-\n .../json_trees/group_buy_redemption_records.json | 242 +-\n .../json_trees/member_balance_changes.json | 172 +-\n .../json_trees/payment_transactions.json | 97 +-\n .../platform_coupon_redemption_records.json | 154 +-\n .../json_trees/recharge_settlements.json | 370 +-\n .../json_trees/refund_transactions.json | 230 +-\n .../json_trees/settlement_records.json | 304 +-\n .../json_trees/site_tables_master.json | 21 +-\n .../json_trees/store_goods_master.json | 20 +-\n .../json_trees/store_goods_sales_records.json | 260 +-\n .../json_trees/table_fee_discount_records.json | 162 +-\n .../json_trees/table_fee_transactions.json | 333 +-\n packages/shared/README.md | 14 +-\n packages/shared/src/neozqyy_shared/__init__.py | 6 +\n .../shared/src/neozqyy_shared/datetime_utils.py | 92 +\n scripts/ops/_archive_etl_db_docs.py | 90 -\n scripts/ops/_archive_phase2.py | 93 -\n scripts/ops/_consistency_output.txt | 53 -\n scripts/ops/_env_paths.py | 31 +\n scripts/ops/_etl_log_temp.txt | 5 -\n scripts/ops/_run_migrations_20260224.py | 65 -\n scripts/ops/_tmp_execution_logs.json | 5 -\n scripts/ops/batch_h5_updates.py | 3 +\n scripts/ops/clone_output.txt | Bin 2618 -> 0 bytes\n scripts/ops/etl_consistency_check.py | 54 +-\n scripts/ops/etl_integration_report.py | 18 +-\n scripts/ops/export_bug_report.py | 207 -\n scripts/ops/export_etl_result.py | 4 +-\n scripts/ops/export_v8_report.py | 2 -\n scripts/ops/fix_board_coach_dims.py | 3 +\n scripts/ops/fix_date_dividers.py | 3 +\n scripts/ops/gen_consolidated_ddl.py | 4 +-\n scripts/ops/gen_integration_report.py | 13 +-\n scripts/ops/monitor_etl_run.py | 11 +-\n scripts/ops/redesign_board_customer.py | 3 +\n scripts/ops/run_migration_assistant_no_int.py | 36 -\n scripts/ops/seed_dws_config.py | 73 +-\n scripts/ops/start-admin.ps1 | 112 +-\n scripts/ops/update_board_coach.py | 3 +\n scripts/ops/update_board_coach_v2.py | 3 +\n scripts/ops/update_board_customer.py | 3 +\n scripts/ops/update_coach_detail.py | 3 +\n scripts/ops/update_customer_detail.py | 3 +\n start-admin.bat | 3 +\n tmp/_gen.py | 2 -\n tmp/_write_test.py | 112 -\n tmp/api_samples/assistant_accounts_master.json | 4386 -----\n .../assistant_cancellation_records.json | 3320 ----\n tmp/api_samples/assistant_service_records.json | 19002 ------------------\n tmp/api_samples/goods_stock_movements.json | 4202 ----\n tmp/api_samples/goods_stock_summary.json | 2770 ---\n tmp/api_samples/group_buy_packages.json | 830 -\n tmp/api_samples/group_buy_redemption_records.json | 10802 ----------\n tmp/api_samples/member_balance_changes.json | 6002 ------\n tmp/api_samples/member_profiles.json | 4402 -----\n tmp/api_samples/member_stored_value_cards.json | 15396 ---------------\n tmp/api_samples/payment_transactions.json | 8002 --------\n .../platform_coupon_redemption_records.json | 11002 -----------\n tmp/api_samples/recharge_settlements.json | 19602 -------------------\n tmp/api_samples/refund_transactions.json | 2198 ---\n tmp/api_samples/settlement_records.json | 19602 -------------------\n tmp/api_samples/site_tables_master.json | 2074 --\n tmp/api_samples/stock_goods_category_tree.json | 349 -\n tmp/api_samples/store_goods_master.json | 9863 ----------\n tmp/api_samples/store_goods_sales_records.json | 10602 ----------\n tmp/api_samples/table_fee_discount_records.json | 12202 ------------\n tmp/api_samples/table_fee_transactions.json | 14202 --------------\n tmp/api_samples/tenant_goods_master.json | 6237 ------\n tmp/finalize_err.txt | 0\n tmp/finalize_out.txt | 134 -\n tmp/finalize_output.txt | Bin 10412 -> 0 bytes\n uv.lock | 148 +\n 336 files changed, 18947 insertions(+), 221402 deletions(-)",
"high_risk_diff": "diff --git a/apps/admin-web/src/App.tsx b/apps/admin-web/src/App.tsx\nindex fe8079d..b9d1762 100644\n--- a/apps/admin-web/src/App.tsx\n+++ b/apps/admin-web/src/App.tsx\n@@ -21,6 +21,7 @@ import {\n } from \"@ant-design/icons\";\n import type { MenuProps } from \"antd\";\n import { useAuthStore } from \"./store/authStore\";\n+import { useBusinessDayStore } from \"./store/businessDayStore\";\n import { fetchQueue } from \"./api/execution\";\n import type { QueuedTask } from \"./types\";\n import Login from \"./pages/Login\";\n@@ -179,12 +180,15 @@ const AppLayout: React.FC = () => {\n \n const App: React.FC = () => {\n const hydrate = useAuthStore((s) => s.hydrate);\n+ const initBusinessDay = useBusinessDayStore((s) => s.init);\n const [hydrated, setHydrated] = useState(false);\n \n useEffect(() => {\n hydrate();\n setHydrated(true);\n- }, [hydrate]);\n+ // 启动时请求一次营业日配置,降级策略在 store 内部处理\n+ initBusinessDay();\n+ }, [hydrate, initBusinessDay]);\n \n /* hydrate 完成前不渲染路由,避免 PrivateRoute 误判跳转到 /login */\n if (!hydrated) return <Spin style={{ display: \"flex\", justifyContent: \"center\", marginTop: 120 }} />;\n--- /dev/null\n+++ b/apps/admin-web/src/__tests__/taskLogParser.test.ts\n@@ -0,0 +1 @@\n/**\n * 任务日志解析与分组测试\n *\n * **Validates: Requirements 10.2, 10.5, 10.6**\n *\n * 验证 parseLogLine / groupLogsByTask / filterTaskGroups 的正确性。\n */\n\nimport { describe, it, expect } from \"vitest\";\nimport {\n parseLogLine,\n groupLogsByTask,\n filterTaskGroups,\n} from \"../utils/taskLogParser\";\n\n/* ------------------------------------------------------------------ */\n/* parseLogLine */\n/* ------------------------------------------------------------------ */\n\ndescribe(\"parseLogLine — 单行日志解析\", () => {\n it(\"解析标准格式日志行\", () => {\n const line = \"[ODS_MEMBER] 2024-06-01 12:00:00 INFO 开始拉取会员数据\";\n const result = parseLogLine(line);\n expect(result).not.toBeNull();\n expect(result!.taskCode).toBe(\"ODS_MEMBER\");\n expect(result!.timestamp).toBe(\"2024-06-01 12:00:00\");\n expect(result!.level).toBe(\"INFO\");\n expect(result!.message).toBe(\"开始拉取会员数据\");\n });\n\n it(\"解析带毫秒的时间戳\", () => {\n const line = \"[ODS_ORDER] 2024-06-01 12:00:00,123 ERROR 请求失败\";\n const result = parseLogLine(line);\n expect(result).not.toBeNull();\n expect(result!.taskCode).toBe(\"ODS_ORDER\");\n expect(result!.timestamp).toBe(\"2024-06-01 12:00:00,123\");\n expect(result!.level).toBe(\"ERROR\");\n });\n\n it(\"解析 WARNING 级别\", () => {\n const line = \"[DWD_LOAD_FROM_ODS] 2024-06-01 12:00:00 WARNING 队列积压\";\n const result = parseLogLine(line);\n expect(result).not.toBeNull();\n expect(result!.level).toBe(\"WARNING\");\n });\n\n it(\"解析 DEBUG 级别\", () => {\n const line = \"[ODS_MEMBER] 2024-06-01 12:00:00 DEBUG 调试信息\";\n const result = parseLogLine(line);\n expect(result).not.toBeNull();\n expect(result!.level).toBe(\"DEBUG\");\n });\n\n it(\"无法解析的行返回 null\", () => {\n expect(parseLogLine(\"这是一行普通文本\")).toBeNull();\n expect(parseLogLine(\"\")).toBeNull();\n expect(parseLogLine(\"[lowercase] 2024-06-01 12:00:00 INFO msg\")).toBeNull();\n });\n});\n\n/* ------------------------------------------------------------------ */\n/* groupLogsByTask */\n/* ------------------------------------------------------------------ */\n\ndescribe(\"groupLogsByTask — 按任务分组\", () => {\n const sampleLines = [\n \"[ODS_MEMBER] 2024-06-01 12:00:00 INFO 开始拉取\",\n \"[ODS_MEMBER] 2024-06-01 12:00:05 INFO 拉取完成\",\n \"[ODS_ORDER] 2024-06-01 12:00:10 INFO 开始拉取订单\",\n \"[ODS_ORDER] 2024-06-01 12:00:20 ERROR 请求超时\",\n \"[ODS_MEMBER] 2024-06-01 12:00:25 INFO 写入完成 success\",\n ];\n\n it(\"正确分组到不同任务\", () => {\n const groups = groupLogsByTask(sampleLines);\n expect(groups).toHaveLength(2);\n const memberGroup = groups.find((g) => g.taskCode === \"ODS_MEMBER\");\n const orderGroup = groups.find((g) => g.taskCode === \"ODS_ORDER\");\n expect(memberGroup).toBeDefined();\n expect(orderGroup).toBeDefined();\n expect(memberGroup!.entries).toHaveLength(3);\n expect(orderGroup!.entries).toHaveLength(2);\n });\n\n it(\"提取正确的开始/结束时间\", () => {\n const groups = groupLogsByTask(sampleLines);\n const memberGroup = groups.find((g) => g.taskCode === \"ODS_MEMBER\")!;\n expect(memberGroup.startTime).toBe(\"2024-06-01 12:00:00\");\n expect(memberGroup.endTime).toBe(\"2024-06-01 12:00:25\");\n });\n\n it(\"有 ERROR 的任务状态为 failed\", () => {\n const groups = groupLogsByTask(sampleLines);\n const orderGroup = groups.find((g) => g.taskCode === \"ODS_ORDER\")!;\n expect(orderGroup.status).toBe(\"failed\");\n expect(orderGroup.counts.error).toBe(1);\n });\n\n it(\"最后一条含 success 的任务状态为 success\", () => {\n const groups = groupLogsByTask(sampleLines);\n const memberGroup = groups.find((g) => g.taskCode === \"ODS_MEMBER\")!;\n expect(memberGroup.status).toBe(\"success\");\n });\n\n it(\"空行被忽略\", () => {\n const groups = groupLogsByTask([\"\", \" \", \"[ODS_A] 2024-01-01 00:00:00 INFO ok\"]);\n expect(groups).toHaveLength(1);\n expect(groups[0].taskCode).toBe(\"ODS_A\");\n });\n\n it(\"无法解析的行归入 _UNKNOWN\", () => {\n const groups = groupLogsByTask([\"普通文本行\", \"另一行\"]);\n expect(groups).toHaveLength(1);\n expect(groups[0].taskCode).toBe(\"_UNKNOWN\");\n expect(groups[0].entries).toHaveLength(2);\n });\n\n it(\"空数组返回空分组\", () => {\n expect(groupLogsByTask([])).toEqual([]);\n });\n\n it(\"日志计数正确\", () => {\n const lines = [\n \"[T1] 2024-01-01 00:00:00 INFO a\",\n \"[T1] 2024-01-01 00:00:01 WARNING b\",\n \"[T1] 2024-01-01 00:00:02 ERROR c\",\n \"[T1] 2024-01-01 00:00:03 DEBUG d\",\n \"[T1] 2024-01-01 00:00:04 INFO e\",\n ];\n const groups = groupLogsByTask(lines);\n expect(groups[0].counts).toEqual({ info: 2, warning: 1, error: 1, debug: 1 });\n });\n});\n\n/* ------------------------------------------------------------------ */\n/* filterTaskGroups */\n/* ------------------------------------------------------------------ */\n\ndescribe(\"filterTaskGroups — 按任务代码过滤\", () => {\n const lines = [\n \"[ODS_MEMBER] 2024-06-01 12:00:00 INFO a\",\n \"[ODS_ORDER] 2024-06-01 12:00:01 INFO b\",\n \"[DWD_LOAD_FROM_ODS] 2024-06-01 12:00:02 INFO c\",\n ];\n\n it(\"空关键词返回全部\", () => {\n const groups = groupLogsByTask(lines);\n expect(filterTaskGroups(groups, \"\")).toHaveLength(3);\n expect(filterTaskGroups(groups, \" \")).toHaveLength(3);\n });\n\n it(\"精确匹配任务代码\", () => {\n const groups = groupLogsByTask(lines);\n const filtered = filterTaskGroups(groups, \"ODS_MEMBER\");\n expect(filtered).toHaveLength(1);\n expect(filtered[0].taskCode).toBe(\"ODS_MEMBER\");\n });\n\n it(\"部分匹配(大小写不敏感)\", () => {\n const groups = groupLogsByTask(lines);\n const filtered = filterTaskGroups(groups, \"ods\");\n // \"ODS_MEMBER\", \"ODS_ORDER\", \"DWD_LOAD_FROM_ODS\" 都包含 \"ods\"\n expect(filtered).toHaveLength(3);\n });\n\n it(\"无匹配返回空数组\", () => {\n const groups = groupLogsByTask(lines);\n expect(filterTaskGroups(groups, \"NONEXISTENT\")).toEqual([]);\n });\n});\n\n--- /dev/null\n+++ b/apps/admin-web/src/api/businessDay.ts\n@@ -0,0 +1 @@\n/**\n * 营业日配置 API。\n *\n * 从后端 /api/config/business-day 获取营业日分割点小时值。\n */\n\nimport { apiClient } from \"./client\";\n\nexport interface BusinessDayConfig {\n business_day_start_hour: number;\n}\n\n/** 获取营业日分割点配置 */\nexport async function fetchBusinessDayConfig(): Promise<BusinessDayConfig> {\n const { data } = await apiClient.get<BusinessDayConfig>(\"/config/business-day\");\n return data;\n}\n\ndiff --git a/apps/admin-web/src/api/schedules.ts b/apps/admin-web/src/api/schedules.ts\nindex d9a05c7..81ac40f 100644\n--- a/apps/admin-web/src/api/schedules.ts\n+++ b/apps/admin-web/src/api/schedules.ts\n@@ -3,7 +3,7 @@\n */\n \n import { apiClient } from './client';\n-import type { ScheduledTask, ScheduleConfig, TaskConfig } from '../types';\n+import type { ScheduledTask, ScheduleConfig, TaskConfig, ExecutionLog } from '../types';\n \n /** 获取调度任务列表 */\n export async function fetchSchedules(): Promise<ScheduledTask[]> {\n@@ -17,6 +17,7 @@ export async function createSchedule(payload: {\n task_codes: string[];\n task_config: TaskConfig;\n schedule_config: ScheduleConfig;\n+ run_immediately?: boolean;\n }): Promise<ScheduledTask> {\n const { data } = await apiClient.post<ScheduledTask>('/schedules', payload);\n return data;\n@@ -46,3 +47,21 @@ export async function toggleSchedule(id: string): Promise<ScheduledTask> {\n const { data } = await apiClient.patch<ScheduledTask>(`/schedules/${id}/toggle`);\n return data;\n }\n+\n+/** 手动执行调度任务一次(不更新调度间隔) */\n+export async function runScheduleNow(id: string): Promise<{ message: string; task_id: string }> {\n+ const { data } = await apiClient.post<{ message: string; task_id: string }>(`/schedules/${id}/run`);\n+ return data;\n+}\n+\n+/** 获取调度任务的执行历史 */\n+export async function fetchScheduleHistory(\n+ id: string,\n+ page = 1,\n+ pageSize = 50,\n+): Promise<ExecutionLog[]> {\n+ const { data } = await apiClient.get<ExecutionLog[]>(`/schedules/${id}/history`, {\n+ params: { page, page_size: pageSize },\n+ });\n+ return data;\n+}\n--- /dev/null\n+++ b/apps/admin-web/src/components/BusinessDayHint.tsx\n@@ -0,0 +1 @@\n/**\n * 营业日口径提示组件。\n *\n * 在日期选择器旁显示 Tooltip + 文字标注,说明当前营业日分割点。\n * 例如「营业日08:00 起」\n */\n\nimport React from \"react\";\nimport { Tooltip, Typography } from \"antd\";\nimport { InfoCircleOutlined } from \"@ant-design/icons\";\nimport { useBusinessDayStore } from \"../store/businessDayStore\";\n\nconst { Text } = Typography;\n\nconst BusinessDayHint: React.FC = () => {\n const startHour = useBusinessDayStore((s) => s.startHour);\n const hh = String(startHour).padStart(2, \"0\");\n\n return (\n <Tooltip title={`统计日期按营业日口径划分:每天 ${hh}:00 至次日 ${hh}:00`}>\n <Text type=\"secondary\" style={{ fontSize: 12, marginLeft: 4 }}>\n <InfoCircleOutlined style={{ marginRight: 2 }} />\n 营业日:{hh}:00 起\n </Text>\n </Tooltip>\n );\n};\n\nexport default BusinessDayHint;\n\ndiff --git a/apps/admin-web/src/components/LogStream.tsx b/apps/admin-web/src/components/LogStream.tsx\nindex a3e6947..d02f202 100644\n--- a/apps/admin-web/src/components/LogStream.tsx\n+++ b/apps/admin-web/src/components/LogStream.tsx\n@@ -64,11 +64,16 @@ const LogStream: React.FC<LogStreamProps> = ({ lines }) => {\n {lines.length === 0 ? (\n <div style={{ color: \"#888\" }}>暂无日志</div>\n ) : (\n- lines.map((line, i) => (\n- <div key={i} style={{ whiteSpace: \"pre-wrap\", wordBreak: \"break-all\" }}>\n- {line}\n- </div>\n- ))\n+ lines.map((line, i) => {\n+ let color = \"#d4d4d4\";\n+ if (/\\bERROR\\b/i.test(line)) color = \"#f56c6c\";\n+ else if (/\\bWARN(?:ING)?\\b/i.test(line)) color = \"#e6a23c\";\n+ return (\n+ <div key={i} style={{ whiteSpace: \"pre-wrap\", wordBreak: \"break-all\", color }}>\n+ {line}\n+ </div>\n+ );\n+ })\n )}\n <div ref={bottomRef} />\n </div>\n--- /dev/null\n+++ b/apps/admin-web/src/components/ScheduleHistoryDrawer.tsx\n@@ -0,0 +1 @@\n/**\n * 调度任务执行历史抽屉组件。\n *\n * - 表格展示执行记录ID、状态、开始时间、耗时、退出码50 条/页分页\n * - 点击行打开详情(复用 LogStream 展示日志running 任务走 WebSocket\n */\n\nimport React, { useEffect, useState, useCallback, useRef } from 'react';\nimport {\n Drawer, Table, Tag, Typography, Descriptions, Spin, Space, message,\n} from 'antd';\nimport { FileTextOutlined } from '@ant-design/icons';\nimport type { ColumnsType } from 'antd/es/table';\nimport type { ExecutionLog } from '../types';\nimport { fetchScheduleHistory } from '../api/schedules';\nimport { apiClient } from '../api/client';\nimport LogStream from './LogStream';\n\nconst { Text } = Typography;\n\nconst STATUS_COLOR: Record<string, string> = {\n pending: 'default',\n running: 'processing',\n success: 'success',\n failed: 'error',\n cancelled: 'warning',\n};\n\nfunction fmtTime(iso: string | null | undefined): string {\n if (!iso) return '—';\n return new Date(iso).toLocaleString('zh-CN');\n}\n\nfunction fmtDuration(ms: number | null | undefined): string {\n if (ms == null) return '—';\n if (ms < 1000) return `${ms}ms`;\n const sec = ms / 1000;\n if (sec < 60) return `${sec.toFixed(1)}s`;\n const min = Math.floor(sec / 60);\n const remainSec = Math.round(sec % 60);\n return `${min}m${remainSec}s`;\n}\n\ninterface Props {\n open: boolean;\n scheduleId: string | null;\n scheduleName: string;\n onClose: () => void;\n}\n\nconst ScheduleHistoryDrawer: React.FC<Props> = ({ open, scheduleId, scheduleName, onClose }) => {\n const [data, setData] = useState<ExecutionLog[]>([]);\n const [loading, setLoading] = useState(false);\n const [page, setPage] = useState(1);\n const [detail, setDetail] = useState<ExecutionLog | null>(null);\n const [logLines, setLogLines] = useState<string[]>([]);\n const [logLoading, setLogLoading] = useState(false);\n const [wsConnected, setWsConnected] = useState(false);\n const wsRef = useRef<WebSocket | null>(null);\n\n const closeWs = useCallback(() => {\n wsRef.current?.close();\n wsRef.current = null;\n setWsConnected(false);\n }, []);\n\n const load = useCallback(async (p: number) => {\n if (!scheduleId) return;\n setLoading(true);\n try {\n setData(await fetchScheduleHistory(scheduleId, p, 50));\n } catch {\n message.error('加载执行历史失败');\n } finally {\n setLoading(false);\n }\n }, [scheduleId]);\n\n useEffect(() => {\n if (open && scheduleId) {\n setPage(1);\n setDetail(null);\n load(1);\n }\n return () => { closeWs(); };\n }, [open, scheduleId, load, closeWs]);\n\n const handleRowClick = useCallback(async (record: ExecutionLog) => {\n setDetail(record);\n setLogLines([]);\n setLogLoading(true);\n closeWs();\n\n if (record.status === 'running') {\n const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';\n const host = window.location.host;\n const ws = new WebSocket(`${protocol}//${host}/ws/logs/${record.id}`);\n wsRef.current = ws;\n ws.onopen = () => { setWsConnected(true); setLogLoading(false); };\n ws.onmessage = (event) => { setLogLines((prev) => [...prev, event.data]); };\n ws.onclose = () => { setWsConnected(false); };\n ws.onerror = () => {\n setWsConnected(false);\n apiClient.get<{ output_log: string | null; error_log: string | null }>(\n `/execution/${record.id}/logs`,\n ).then(({ data: logData }) => {\n const parts: string[] = [];\n if (logData.output_log) parts.push(logData.output_log);\n if (logData.error_log) parts.push(logData.error_log);\n setLogLines(parts.join('\\n').split('\\n').filter(Boolean));\n }).catch(() => {}).finally(() => setLogLoading(false));\n };\n } else {\n try {\n const { data: logData } = await apiClient.get<{\n output_log: string | null; error_log: string | null;\n }>(`/execution/${record.id}/logs`);\n const parts: string[] = [];\n if (logData.output_log) parts.push(logData.output_log);\n if (logData.error_log) parts.push(logData.error_log);\n setLogLines(parts.join('\\n').split('\\n').filter(Boolean));\n } catch { /* 静默 */ }\n finally { setLogLoading(false); }\n }\n }, [closeWs]);\n\n const columns: ColumnsType<ExecutionLog> = [\n {\n title: '执行 ID', dataIndex: 'id', key: 'id', width: 120,\n render: (id: string) => (\n <Text copyable={{ text: id }} style={{ fontSize: 11 }}>\n {id.slice(0, 8)}…\n </Text>\n ),\n },\n {\n title: '状态', dataIndex: 'status', key: 'status', width: 90,\n render: (s: string) => <Tag color={STATUS_COLOR[s] ?? 'default'}>{s}</Tag>,\n },\n { title: '开始时间', dataIndex: 'started_at', key: 'started_at', width: 170, render: fmtTime },\n { title: '时长', dataIndex: 'duration_ms', key: 'duration_ms', width: 90, render: fmtDuration },\n {\n title: '退出码', dataIndex: 'exit_code', key: 'exit_code', width: 70, align: 'center',\n render: (v: number | null) => v != null\n ? <Tag color={v === 0 ? 'success' : 'error'}>{v}</Tag>\n : '—',\n },\n ];\n\n return (\n <Drawer\n title={`执行历史 — ${scheduleName}`}\n open={open}\n onClose={() => { closeWs(); setDetail(null); onClose(); }}\n width={800}\n styles={{ body: { padding: 12 } }}\n >\n <Table<ExecutionLog>\n rowKey=\"id\"\n columns={columns}\n dataSource={data}\n loading={loading}\n size=\"small\"\n pagination={{\n current: page,\n pageSize: 50,\n onChange: (p) => { setPage(p); load(p); },\n showTotal: (t) => `共 ${t} 条`,\n }}\n onRow={(record) => ({\n onClick: () => handleRowClick(record),\n style: { cursor: 'pointer' },\n })}\n />\n\n {/* 执行详情 */}\n <Drawer\n title={\n <Space>\n <span>执行详情</span>\n {detail?.status === 'running' && (\n wsConnected ? <Tag color=\"processing\">实时连接中</Tag> : <Tag>未连接</Tag>\n )}\n </Space>\n }\n open={!!detail}\n onClose={() => { closeWs(); setDetail(null); }}\n width={700}\n styles={{ body: { padding: 12 } }}\n >\n {detail && (\n <>\n <Descriptions column={1} bordered size=\"small\" style={{ marginBottom: 16 }}>\n <Descriptions.Item label=\"执行 ID\">{detail.id}</Descriptions.Item>\n <Descriptions.Item label=\"任务\">{detail.task_codes?.join(', ')}</Descriptions.Item>\n <Descriptions.Item label=\"状态\">\n <Tag color={STATUS_COLOR[detail.status] ?? 'default'}>{detail.status}</Tag>\n </Descriptions.Item>\n <Descriptions.Item label=\"开始时间\">{fmtTime(detail.started_at)}</Descriptions.Item>\n <Descriptions.Item label=\"结束时间\">{fmtTime(detail.finished_at)}</Descriptions.Item>\n <Descriptions.Item label=\"时长\">{fmtDuration(detail.duration_ms)}</Descriptions.Item>\n <Descriptions.Item label=\"退出码\">\n {detail.exit_code != null\n ? <Tag color={detail.exit_code === 0 ? 'success' : 'error'}>{detail.exit_code}</Tag>\n : '—'}\n </Descriptions.Item>\n <Descriptions.Item label=\"命令\">\n <code style={{ wordBreak: 'break-all', fontSize: 12 }}>{detail.command || '—'}</code>\n </Descriptions.Item>\n </Descriptions>\n <div style={{ marginBottom: 8, display: 'flex', alignItems: 'center', gap: 8 }}>\n <FileTextOutlined />\n <Text strong>执行日志</Text>\n {logLoading && <Spin size=\"small\" />}\n </div>\n <div style={{ height: 400 }}>\n <LogStream executionId={detail.id} lines={logLines} />\n </div>\n </>\n )}\n </Drawer>\n </Drawer>\n );\n};\n\nexport default ScheduleHistoryDrawer;\n\ndiff --git a/apps/admin-web/src/components/ScheduleTab.tsx b/apps/admin-web/src/components/ScheduleTab.tsx\nindex a81c8f1..47b195b 100644\n--- a/apps/admin-web/src/components/ScheduleTab.tsx\n+++ b/apps/admin-web/src/components/ScheduleTab.tsx\n@@ -10,19 +10,22 @@\n import React, { useEffect, useState, useCallback } from 'react';\n import {\n Table, Tag, Button, Switch, Popconfirm, Space, Modal, Form,\n- Input, Select, InputNumber, TimePicker, Checkbox, message,\n+ Input, Select, InputNumber, TimePicker, Checkbox, message, Typography,\n } from 'antd';\n-import { PlusOutlined, ReloadOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';\n+import { ReloadOutlined, EditOutlined, DeleteOutlined, HistoryOutlined, PlayCircleOutlined } from '@ant-design/icons';\n import type { ColumnsType } from 'antd/es/table';\n import dayjs from 'dayjs';\n import type { ScheduledTask, ScheduleConfig } from '../types';\n import {\n fetchSchedules,\n- createSchedule,\n updateSchedule,\n deleteSchedule,\n toggleSchedule,\n+ runScheduleNow,\n } from '../api/schedules';\n+import ScheduleHistoryDrawer from './ScheduleHistoryDrawer';\n+\n+const { Text } = Typography;\n \n /* ------------------------------------------------------------------ */\n /* 常量 & 工具 */\n@@ -150,6 +153,11 @@ const ScheduleTab: React.FC = () => {\n const [scheduleType, setScheduleType] = useState<string>('daily');\n const [form] = Form.useForm();\n \n+ /* 执行历史抽屉状态 */\n+ const [historyOpen, setHistoryOpen] = useState(false);\n+ const [historyScheduleId, setHistoryScheduleId] = useState<string | null>(null);\n+ const [historyScheduleName, setHistoryScheduleName] = useState('');\n+\n /* 加载列表 */\n const load = useCallback(async () => {\n setLoading(true);\n@@ -164,25 +172,6 @@ const ScheduleTab: React.FC = () => {\n \n useEffect(() => { load(); }, [load]);\n \n- /* 打开创建 Modal */\n- const openCreate = () => {\n- setEditing(null);\n- form.resetFields();\n- form.setFieldsValue({\n- schedule_config: {\n- schedule_type: 'daily',\n- interval_value: 1,\n- interval_unit: 'hours',\n- daily_time: dayjs('04:00', 'HH:mm'),\n- weekly_days: [1],\n- weekly_time: dayjs('04:00', 'HH:mm'),\n- cron_expression: '0 4 * * *',\n- },\n- });\n- setScheduleType('daily');\n- setModalOpen(true);\n- };\n-\n /* 打开编辑 Modal */\n const openEdit = (record: ScheduledTask) => {\n setEditing(record);\n@@ -199,13 +188,20 @@ const ScheduleTab: React.FC = () => {\n setModalOpen(true);\n };\n \n- /* 提交创建/编辑 */\n+ /* 打开执行历史 */\n+ const openHistory = (record: ScheduledTask) => {\n+ setHistoryScheduleId(record.id);\n+ setHistoryScheduleName(record.name);\n+ setHistoryOpen(true);\n+ };\n+\n+ /* 提交编辑 */\n const handleSubmit = async () => {\n+ if (!editing) return;\n try {\n const values = await form.validateFields();\n setSubmitting(true);\n \n- // 将 dayjs 对象转为字符串\n const cfg = { ...values.schedule_config };\n if (cfg.daily_time && typeof cfg.daily_time !== 'string') {\n cfg.daily_time = cfg.daily_time.format('HH:mm');\n@@ -227,47 +223,16 @@ const ScheduleTab: React.FC = () => {\n end_date: null,\n };\n \n- if (editing) {\n- await updateSchedule(editing.id, {\n- name: values.name,\n- schedule_config: scheduleConfig,\n- });\n- message.success('调度任务已更新');\n- } else {\n- // 创建时使用默认 task_config简化实现\n- await createSchedule({\n- name: values.name,\n- task_codes: [],\n- task_config: {\n- tasks: [],\n- flow: 'api_full',\n- processing_mode: 'increment_only',\n- pipeline_flow: 'FULL',\n- dry_run: false,\n- window_mode: 'lookback',\n- window_start: null,\n- window_end: null,\n- window_split: null,\n- window_split_days: null,\n- lookback_hours: 24,\n- overlap_seconds: 600,\n- fetch_before_verify: false,\n- skip_ods_when_fetch_before_verify: false,\n- ods_use_local_json: false,\n- store_id: null,\n- dwd_only_tables: null,\n- force_full: false,\n- extra_args: {},\n- },\n- schedule_config: scheduleConfig,\n- });\n- message.success('调度任务已创建');\n- }\n+ await updateSchedule(editing.id, {\n+ name: values.name,\n+ schedule_config: scheduleConfig,\n+ });\n+ message.success('调度任务已更新');\n \n setModalOpen(false);\n load();\n } catch {\n- // 表单验证失败,不做额外处理\n+ // 表单验证失败\n } finally {\n setSubmitting(false);\n }\n@@ -294,8 +259,29 @@ const ScheduleTab: React.FC = () => {\n }\n };\n \n+ /* 手动执行一次(不更新调度间隔) */\n+ const handleRunNow = async (id: string) => {\n+ try {\n+ await runScheduleNow(id);\n+ message.success('已提交到执行队列');\n+ } catch {\n+ message.error('执行失败');\n+ }\n+ };\n+\n /* 表格列定义 */\n const columns: ColumnsType<ScheduledTask> = [\n+ {\n+ title: '调度 ID',\n+ dataIndex: 'id',\n+ key: 'id',\n+ width: 120,\n+ render: (id: string) => (\n+ <Text copyable={{ text: id }} style={{ fontSize: 11 }}>\n+ {id.slice(0, 8)}…\n+ </Text>\n+ ),\n+ },\n {\n title: '名称',\n dataIndex: 'name',\n@@ -338,9 +324,17 @@ const ScheduleTab: React.FC = () => {\n {\n title: '操作',\n key: 'action',\n- width: 140,\n+ width: 300,\n render: (_: unknown, record: ScheduledTask) => (\n <Space size=\"small\">\n+ <Popconfirm title=\"确认立即执行一次?(不影响调度间隔)\" onConfirm={() => handleRunNow(record.id)}>\n+ <Button type=\"link\" icon={<PlayCircleOutlined />} size=\"small\">\n+ 立即执行\n+ </Button>\n+ </Popconfirm>\n+ <Button type=\"link\" icon={<HistoryOutlined />} size=\"small\" onClick={() => openHistory(record)}>\n+ 执行历史\n+ </Button>\n <Button type=\"link\" icon={<EditOutlined />} size=\"small\" onClick={() => openEdit(record)}>\n 编辑\n </Button>\n@@ -356,15 +350,11 @@ const ScheduleTab: React.FC = () => {\n \n return (\n <>\n- <div style={{ marginBottom: 12 }}>\n- <Space>\n- <Button type=\"primary\" icon={<PlusOutlined />} onClick={openCreate}>\n- 新建调度\n- </Button>\n- <Button icon={<ReloadOutlined />} onClick={load} loading={loading}>\n- 刷新\n- </Button>\n- </Space>\n+ <div style={{ marginBottom: 12, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>\n+ <Text type=\"secondary\">共 {data.length} 个调度任务</Text>\n+ <Button icon={<ReloadOutlined />} onClick={load} loading={loading}>\n+ 刷新\n+ </Button>\n </div>\n \n <Table<ScheduledTask>\n@@ -376,9 +366,9 @@ const ScheduleTab: React.FC = () => {\n size=\"middle\"\n />\n \n- {/* 创建/编辑 Modal */}\n+ {/* 编辑 Modal */}\n <Modal\n- title={editing ? '编辑调度任务' : '新建调度任务'}\n+ title=\"编辑调度任务\"\n open={modalOpen}\n onOk={handleSubmit}\n onCancel={() => setModalOpen(false)}\n@@ -400,6 +390,14 @@ const ScheduleTab: React.FC = () => {\n <ScheduleConfigFields scheduleType={scheduleType} />\n </Form>\n </Modal>\n+\n+ {/* 执行历史抽屉 */}\n+ <ScheduleHistoryDrawer\n+ open={historyOpen}\n+ scheduleId={historyScheduleId}\n+ scheduleName={historyScheduleName}\n+ onClose={() => setHistoryOpen(false)}\n+ />\n </>\n );\n };\n--- /dev/null\n+++ b/apps/admin-web/src/components/TaskLogViewer.tsx\n@@ -0,0 +1 @@\n/**\n * 按任务分组的日志展示组件。\n *\n * - 顶部:任务执行时间线概览(可点击跳转)\n * - 中部:按任务代码过滤搜索框\n * - 主体Collapse 折叠面板,每个任务一个区块\n * - 展开后显示该任务的完整日志(时间戳、级别、消息)\n *\n * 需求: 10.2, 10.5, 10.6\n */\n\nimport React, { useMemo, useRef, useCallback, useState } from \"react\";\nimport {\n Collapse,\n Timeline,\n Tag,\n Input,\n Empty,\n Badge,\n Space,\n Typography,\n} from \"antd\";\nimport {\n CheckCircleOutlined,\n CloseCircleOutlined,\n ClockCircleOutlined,\n QuestionCircleOutlined,\n SearchOutlined,\n} from \"@ant-design/icons\";\nimport {\n groupLogsByTask,\n filterTaskGroups,\n type TaskLogGroup,\n type ParsedLogEntry,\n} from \"../utils/taskLogParser\";\n\nconst { Text } = Typography;\n\n/* ------------------------------------------------------------------ */\n/* 状态 → 颜色/图标映射 */\n/* ------------------------------------------------------------------ */\n\nconst STATUS_CONFIG: Record<\n TaskLogGroup[\"status\"],\n { color: string; icon: React.ReactNode; label: string }\n> = {\n success: { color: \"green\", icon: <CheckCircleOutlined />, label: \"成功\" },\n failed: { color: \"red\", icon: <CloseCircleOutlined />, label: \"失败\" },\n running: { color: \"blue\", icon: <ClockCircleOutlined />, label: \"运行中\" },\n unknown: { color: \"default\", icon: <QuestionCircleOutlined />, label: \"未知\" },\n};\n\nconst LEVEL_COLOR: Record<string, string> = {\n ERROR: \"#ff4d4f\",\n CRITICAL: \"#ff4d4f\",\n WARNING: \"#faad14\",\n INFO: \"#52c41a\",\n DEBUG: \"#8c8c8c\",\n};\n\n/* ------------------------------------------------------------------ */\n/* 日志级别标签 */\n/* ------------------------------------------------------------------ */\n\nconst LevelTag: React.FC<{ level: string }> = ({ level }) => (\n <span\n style={{\n color: LEVEL_COLOR[level] ?? \"#8c8c8c\",\n fontWeight: level === \"ERROR\" || level === \"CRITICAL\" ? 600 : 400,\n fontFamily: \"monospace\",\n fontSize: 12,\n minWidth: 56,\n display: \"inline-block\",\n }}\n >\n {level}\n </span>\n);\n\n/* ------------------------------------------------------------------ */\n/* 单条日志行 */\n/* ------------------------------------------------------------------ */\n\nconst LogLine: React.FC<{ entry: ParsedLogEntry }> = ({ entry }) => (\n <div\n style={{\n fontFamily: \"'Cascadia Code', 'Fira Code', Consolas, monospace\",\n fontSize: 12,\n lineHeight: 1.7,\n padding: \"1px 0\",\n whiteSpace: \"pre-wrap\",\n wordBreak: \"break-all\",\n }}\n >\n {entry.timestamp && (\n <Text type=\"secondary\" style={{ fontSize: 12, marginRight: 8, fontFamily: \"monospace\" }}>\n {entry.timestamp}\n </Text>\n )}\n <LevelTag level={entry.level} />\n <span style={{ marginLeft: 8 }}>{entry.message}</span>\n </div>\n);\n\n/* ------------------------------------------------------------------ */\n/* 任务面板头部 */\n/* ------------------------------------------------------------------ */\n\nconst TaskPanelHeader: React.FC<{ group: TaskLogGroup }> = ({ group }) => {\n const cfg = STATUS_CONFIG[group.status];\n return (\n <Space size={12}>\n <Text strong style={{ fontFamily: \"monospace\" }}>{group.taskCode}</Text>\n <Tag color={cfg.color} icon={cfg.icon}>{cfg.label}</Tag>\n <Text type=\"secondary\" style={{ fontSize: 12 }}>\n {group.entries.length} 条日志\n </Text>\n {group.counts.error > 0 && (\n <Badge count={group.counts.error} size=\"small\" title=\"错误数\" />\n )}\n {group.counts.warning > 0 && (\n <Badge\n count={group.counts.warning}\n size=\"small\"\n color=\"#faad14\"\n title=\"警告数\"\n />\n )}\n {group.startTime && (\n <Text type=\"secondary\" style={{ fontSize: 11 }}>\n {group.startTime}\n {group.endTime && group.endTime !== group.startTime\n ? ` → ${group.endTime}`\n : \"\"}\n </Text>\n )}\n </Space>\n );\n};\n\n/* ------------------------------------------------------------------ */\n/* 时间线概览 */\n/* ------------------------------------------------------------------ */\n\nconst TaskTimeline: React.FC<{\n groups: TaskLogGroup[];\n onClickTask: (taskCode: string) => void;\n}> = ({ groups, onClickTask }) => {\n if (groups.length === 0) return null;\n\n const items = groups.map((g) => {\n const cfg = STATUS_CONFIG[g.status];\n return {\n color: cfg.color === \"default\" ? \"gray\" : cfg.color,\n children: (\n <div\n style={{ cursor: \"pointer\" }}\n onClick={() => onClickTask(g.taskCode)}\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => {\n if (e.key === \"Enter\") onClickTask(g.taskCode);\n }}\n >\n <Space size={8}>\n <Text strong style={{ fontFamily: \"monospace\", fontSize: 13 }}>\n {g.taskCode}\n </Text>\n <Tag color={cfg.color} style={{ fontSize: 11 }}>\n {cfg.label}\n </Tag>\n {g.startTime && (\n <Text type=\"secondary\" style={{ fontSize: 11 }}>\n {g.startTime}\n {g.endTime && g.endTime !== g.startTime\n ? ` → ${g.endTime}`\n : \"\"}\n </Text>\n )}\n <Text type=\"secondary\" style={{ fontSize: 11 }}>\n ({g.entries.length} 条)\n </Text>\n </Space>\n </div>\n ),\n };\n });\n\n return (\n <div style={{ marginBottom: 12, padding: \"8px 12px\", background: \"#fafafa\", borderRadius: 4 }}>\n <Text type=\"secondary\" style={{ fontSize: 12, marginBottom: 8, display: \"block\" }}>\n 任务执行时间线(点击跳转)\n </Text>\n <Timeline items={items} style={{ marginBottom: 0, paddingTop: 8 }} />\n </div>\n );\n};\n\n/* ------------------------------------------------------------------ */\n/* 主组件 */\n/* ------------------------------------------------------------------ */\n\nexport interface TaskLogViewerProps {\n /** 原始日志行数组 */\n lines: string[];\n}\n\nconst TaskLogViewer: React.FC<TaskLogViewerProps> = ({ lines }) => {\n const [taskFilter, setTaskFilter] = useState(\"\");\n const [activeKeys, setActiveKeys] = useState<string[]>([]);\n const panelRefs = useRef<Record<string, HTMLDivElement | null>>({});\n\n // 解析并分组\n const allGroups = useMemo(() => groupLogsByTask(lines), [lines]);\n const filteredGroups = useMemo(\n () => filterTaskGroups(allGroups, taskFilter),\n [allGroups, taskFilter],\n );\n\n // 点击时间线跳转到对应任务面板\n const handleTimelineClick = useCallback((taskCode: string) => {\n // 展开目标面板\n setActiveKeys((prev) =>\n prev.includes(taskCode) ? prev : [...prev, taskCode],\n );\n // 滚动到面板位置\n requestAnimationFrame(() => {\n const el = panelRefs.current[taskCode];\n if (el) {\n el.scrollIntoView({ behavior: \"smooth\", block: \"start\" });\n }\n });\n }, []);\n\n const setPanelRef = useCallback(\n (taskCode: string) => (el: HTMLDivElement | null) => {\n panelRefs.current[taskCode] = el;\n },\n [],\n );\n\n if (lines.length === 0) {\n return <Empty description=\"暂无日志数据\" />;\n }\n\n // 如果没有可解析的任务分组(全是无法解析的行),回退到普通展示\n if (allGroups.length === 0) {\n return <Empty description=\"无法解析任务分组\" />;\n }\n\n const collapseItems = filteredGroups.map((group) => ({\n key: group.taskCode,\n label: <TaskPanelHeader group={group} />,\n children: (\n <div\n style={{\n maxHeight: 400,\n overflow: \"auto\",\n background: \"#1e1e1e\",\n color: \"#d4d4d4\",\n padding: 12,\n borderRadius: 4,\n }}\n >\n {group.entries.map((entry, i) => (\n <LogLine key={i} entry={entry} />\n ))}\n </div>\n ),\n }));\n\n return (\n <div>\n {/* 时间线概览 */}\n <TaskTimeline groups={allGroups} onClickTask={handleTimelineClick} />\n\n {/* 任务代码过滤 */}\n {allGroups.length > 1 && (\n <div style={{ marginBottom: 12 }}>\n <Input\n prefix={<SearchOutlined />}\n placeholder=\"按任务代码过滤...\"\n value={taskFilter}\n onChange={(e) => setTaskFilter(e.target.value)}\n allowClear\n style={{ width: 280 }}\n />\n <Text type=\"secondary\" style={{ marginLeft: 12, fontSize: 12 }}>\n {filteredGroups.length} / {allGroups.length} 个任务\n </Text>\n </div>\n )}\n\n {/* 折叠面板 */}\n {filteredGroups.length === 0 ? (\n <Empty description=\"无匹配的任务\" />\n ) : (\n <>\n {filteredGroups.map((g) => (\n <div key={g.taskCode} ref={setPanelRef(g.taskCode)} />\n ))}\n <Collapse\n activeKey={activeKeys}\n onChange={(keys) => setActiveKeys(keys as string[])}\n items={collapseItems}\n />\n </>\n )}\n </div>\n );\n};\n\nexport default TaskLogViewer;\n\n\n[TRUNCATED: diff exceeds 30KB]",
"latest_prompt_log": "- [P20260309-004313] 2026-03-09 00:43:13 +0800\n - summary: CONTEXT TRANSFER: We are continuing a conversation that had gotten too long. Here is a summary: --- ## TASK 1: Review an…\n - prompt:\n```text\nCONTEXT TRANSFER: We are continuing a conversation that had gotten too long. Here is a summary:\n\n---\n## TASK 1: Review and optimize MIGRATION-PLAYBOOK.md document structure\n\n**STATUS**: done\n\n**USER QUERIES**: Previous conversations (carried over from earlier context transfers)\n\n**DETAILS**: Fixed chapter renumbering, condensed sections, fixed cross-references throughout the ~1960 line document.\n\n**FILEPATHS**: `docs/prd/MIGRATION-PLAYBOOK.md`\n\n---\n\n## TASK 2: Create Spec for H5→微信小程序 Migration Engineering\n\n**STATUS**: in-progress (design phase)\n\n**USER QUERIES**: Multiple across conversations — feature spec creation, requirements writing, user feedback on input material batching, AI icon color system discovery, migration strategy additions\n\n**DETAILS**:\n\n### Spec Configuration\n- Feature name: `h5-miniprogram-migration`\n- Workflow: requirements-first (Requirements → Design → Tasks)\n- Config: `.kiro/specs/h5-miniprogram-migration/.config.kiro` with `{\"specId\": \"cd30e87b-ce7a-4ff5-8587-f5ae75013e58\", \"workflowType\": \"requirements-first\", \"specType\": \"feature\"}`\n\n### Requirements Document (COMPLETED - 33 requirements)\n- `requirements.md` has 33 requirements covering all aspects of MIGRATION-PLAYBOOK.md\n- Key user corrections already applied:\n - **Input materials split into 2 batches** (Req 3): Batch 1 for structure migration (Step 1-5), Batch 2 for pixel tuning (Step 6-7)\n - **Migration strategy rules** (Req 2, items 5-7): Abandon patching when diff >15%, complex banners → SVG export, complex icons → SVG export\n - **AI icon color system** (Req 32): 6 color schemes (red/orange/yellow/blue/indigo/purple), random per-page selection, two series (ai-inline-icon + ai-title-badge), float button excluded from random\n - **Filter-bar height constraint** (Req 11, item 8): 70 logical pixels for all board pages\n - **borderRadius**: simple ×2 rule (not ×2×0.875), verified by A/B comparison\n\n### Design Document (NEEDS CREATION)\n- User explicitly requested: \"清除Task list。按照要求生成 Design再生成 Task list\"\n- Both `design.md` and `tasks.md` were deleted\n- **Must create `design.md` FIRST**, then create `tasks.md` after\n- This is a requirements-first workflow: Requirements ✅ → Design ⬜ → Tasks ⬜\n\n### Tasks Document (NEEDS CREATION AFTER DESIGN)\n- A previous tasks.md was created but user wanted it deleted and redone properly (design first)\n- The deleted tasks.md had 24 top-level tasks organized by batch (A-G) with checkpoints\n- When recreating, the structure can be similar but must follow from the design document\n\n### User's Page Migration Philosophy (from earlier feedback)\nUser described their preferred 4-step approach per page:\n1. Screenshot + requirements doc → confirm page length, how many screens, sub-pages/variants → estimate workload\n2. Per minimum unit (one screen/\n[TRUNCATED]"
}