{ "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 ;\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 {\n const { data } = await apiClient.get(\"/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 {\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 {\n const { data } = await apiClient.post('/schedules', payload);\n return data;\n@@ -46,3 +47,21 @@ export async function toggleSchedule(id: string): Promise {\n const { data } = await apiClient.patch(`/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 {\n+ const { data } = await apiClient.get(`/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 \n \n \n 营业日:{hh}:00 起\n \n \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 = ({ lines }) => {\n {lines.length === 0 ? (\n
暂无日志
\n ) : (\n- lines.map((line, i) => (\n-
\n- {line}\n-
\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+
\n+ {line}\n+
\n+ );\n+ })\n )}\n
\n
\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 = {\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 = ({ open, scheduleId, scheduleName, onClose }) => {\n const [data, setData] = useState([]);\n const [loading, setLoading] = useState(false);\n const [page, setPage] = useState(1);\n const [detail, setDetail] = useState(null);\n const [logLines, setLogLines] = useState([]);\n const [logLoading, setLogLoading] = useState(false);\n const [wsConnected, setWsConnected] = useState(false);\n const wsRef = useRef(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 = [\n {\n title: '执行 ID', dataIndex: 'id', key: 'id', width: 120,\n render: (id: string) => (\n \n {id.slice(0, 8)}…\n \n ),\n },\n {\n title: '状态', dataIndex: 'status', key: 'status', width: 90,\n render: (s: string) => {s},\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 ? {v}\n : '—',\n },\n ];\n\n return (\n { closeWs(); setDetail(null); onClose(); }}\n width={800}\n styles={{ body: { padding: 12 } }}\n >\n \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 \n 执行详情\n {detail?.status === 'running' && (\n wsConnected ? 实时连接中 : 未连接\n )}\n \n }\n open={!!detail}\n onClose={() => { closeWs(); setDetail(null); }}\n width={700}\n styles={{ body: { padding: 12 } }}\n >\n {detail && (\n <>\n \n {detail.id}\n {detail.task_codes?.join(', ')}\n \n {detail.status}\n \n {fmtTime(detail.started_at)}\n {fmtTime(detail.finished_at)}\n {fmtDuration(detail.duration_ms)}\n \n {detail.exit_code != null\n ? {detail.exit_code}\n : '—'}\n \n \n {detail.command || '—'}\n \n \n
\n \n 执行日志\n {logLoading && }\n
\n
\n \n
\n \n )}\n \n \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('daily');\n const [form] = Form.useForm();\n \n+ /* 执行历史抽屉状态 */\n+ const [historyOpen, setHistoryOpen] = useState(false);\n+ const [historyScheduleId, setHistoryScheduleId] = useState(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 = [\n+ {\n+ title: '调度 ID',\n+ dataIndex: 'id',\n+ key: 'id',\n+ width: 120,\n+ render: (id: string) => (\n+ \n+ {id.slice(0, 8)}…\n+ \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 \n+ handleRunNow(record.id)}>\n+ \n+ \n+ \n \n@@ -356,15 +350,11 @@ const ScheduleTab: React.FC = () => {\n \n return (\n <>\n-
\n- \n- \n- \n- \n+
\n+ 共 {data.length} 个调度任务\n+ \n
\n \n \n@@ -376,9 +366,9 @@ const ScheduleTab: React.FC = () => {\n size=\"middle\"\n />\n \n- {/* 创建/编辑 Modal */}\n+ {/* 编辑 Modal */}\n setModalOpen(false)}\n@@ -400,6 +390,14 @@ const ScheduleTab: React.FC = () => {\n \n \n \n+\n+ {/* 执行历史抽屉 */}\n+ 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: , label: \"成功\" },\n failed: { color: \"red\", icon: , label: \"失败\" },\n running: { color: \"blue\", icon: , label: \"运行中\" },\n unknown: { color: \"default\", icon: , label: \"未知\" },\n};\n\nconst LEVEL_COLOR: Record = {\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 \n {level}\n \n);\n\n/* ------------------------------------------------------------------ */\n/* 单条日志行 */\n/* ------------------------------------------------------------------ */\n\nconst LogLine: React.FC<{ entry: ParsedLogEntry }> = ({ entry }) => (\n \n {entry.timestamp && (\n \n {entry.timestamp}\n \n )}\n \n {entry.message}\n
\n);\n\n/* ------------------------------------------------------------------ */\n/* 任务面板头部 */\n/* ------------------------------------------------------------------ */\n\nconst TaskPanelHeader: React.FC<{ group: TaskLogGroup }> = ({ group }) => {\n const cfg = STATUS_CONFIG[group.status];\n return (\n \n {group.taskCode}\n {cfg.label}\n \n {group.entries.length} 条日志\n \n {group.counts.error > 0 && (\n \n )}\n {group.counts.warning > 0 && (\n \n )}\n {group.startTime && (\n \n {group.startTime}\n {group.endTime && group.endTime !== group.startTime\n ? ` → ${group.endTime}`\n : \"\"}\n \n )}\n \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 onClickTask(g.taskCode)}\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => {\n if (e.key === \"Enter\") onClickTask(g.taskCode);\n }}\n >\n \n \n {g.taskCode}\n \n \n {cfg.label}\n \n {g.startTime && (\n \n {g.startTime}\n {g.endTime && g.endTime !== g.startTime\n ? ` → ${g.endTime}`\n : \"\"}\n \n )}\n \n ({g.entries.length} 条)\n \n \n \n ),\n };\n });\n\n return (\n
\n \n 任务执行时间线(点击跳转)\n \n \n
\n );\n};\n\n/* ------------------------------------------------------------------ */\n/* 主组件 */\n/* ------------------------------------------------------------------ */\n\nexport interface TaskLogViewerProps {\n /** 原始日志行数组 */\n lines: string[];\n}\n\nconst TaskLogViewer: React.FC = ({ lines }) => {\n const [taskFilter, setTaskFilter] = useState(\"\");\n const [activeKeys, setActiveKeys] = useState([]);\n const panelRefs = useRef>({});\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 ;\n }\n\n // 如果没有可解析的任务分组(全是无法解析的行),回退到普通展示\n if (allGroups.length === 0) {\n return ;\n }\n\n const collapseItems = filteredGroups.map((group) => ({\n key: group.taskCode,\n label: ,\n children: (\n \n {group.entries.map((entry, i) => (\n \n ))}\n \n ),\n }));\n\n return (\n
\n {/* 时间线概览 */}\n \n\n {/* 任务代码过滤 */}\n {allGroups.length > 1 && (\n
\n }\n placeholder=\"按任务代码过滤...\"\n value={taskFilter}\n onChange={(e) => setTaskFilter(e.target.value)}\n allowClear\n style={{ width: 280 }}\n />\n \n {filteredGroups.length} / {allGroups.length} 个任务\n \n
\n )}\n\n {/* 折叠面板 */}\n {filteredGroups.length === 0 ? (\n \n ) : (\n <>\n {filteredGroups.map((g) => (\n
\n ))}\n setActiveKeys(keys as string[])}\n items={collapseItems}\n />\n \n )}\n
\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]" }