feat: complete production workflow migration
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Archived content-marketing Windows launchers
|
||||
|
||||
These BAT and PowerShell files are retained only as migration history. They are
|
||||
not production commands, are not registered by GYXX Flow, and are not used by
|
||||
the Python scheduler. The active workflow steps are declared in
|
||||
`config/workflows.json` and scheduled through the Python scheduler service.
|
||||
@@ -0,0 +1,44 @@
|
||||
@echo off
|
||||
if not defined GYXX_PYTHON set "GYXX_PYTHON=python"
|
||||
REM Daily SKU marketing operations report: Hermes analysis + Feishu dashboard and full report
|
||||
REM Historical launcher retained for provenance; production scheduling is owned by gyxx schedule run.
|
||||
|
||||
setlocal
|
||||
|
||||
for %%I in ("%~dp0..\..") do set "PROJECT_DIR=%%~fI"
|
||||
if not defined GYXX_DATA_ROOT (
|
||||
if not defined GYXX_PROJECT_ROOT (
|
||||
echo ERROR: GYXX_DATA_ROOT or GYXX_PROJECT_ROOT must be defined. 1>&2
|
||||
exit /b 3
|
||||
)
|
||||
set "GYXX_DATA_ROOT=%GYXX_PROJECT_ROOT%\var"
|
||||
)
|
||||
set LOG_DIR=%GYXX_DATA_ROOT%\logs\content_marketing\scheduled
|
||||
set PYTHON=%GYXX_PYTHON%
|
||||
where %PYTHON% >nul 2>&1 || set PYTHON=python
|
||||
|
||||
set PYTHONIOENCODING=utf-8
|
||||
set LARK_CLI_NO_PROXY=1
|
||||
set LARKSUITE_CLI_NO_UPDATE_NOTIFIER=1
|
||||
set LARKSUITE_CLI_NO_SKILLS_NOTIFIER=1
|
||||
|
||||
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
|
||||
|
||||
set TS=%date:~0,4%%date:~5,2%%date:~8,2%_%time:~0,2%%time:~3,2%%time:~6,2%
|
||||
set TS=%TS: =0%
|
||||
set LOG_FILE=%LOG_DIR%\daily_marketing_report_%TS%.log
|
||||
|
||||
echo === Daily marketing report started at %date% %time% === > "%LOG_FILE%"
|
||||
echo PROJECT_DIR=%PROJECT_DIR% >> "%LOG_FILE%"
|
||||
|
||||
REM Exposure collection finishes at 22:00. The 10:00 report reads the latest
|
||||
REM completed database snapshot and must not wait for a same-day collection.
|
||||
|
||||
cd /d "%PROJECT_DIR%"
|
||||
call %PYTHON% -u -X utf8 daily_marketing_report.py --send >> "%LOG_FILE%" 2>&1
|
||||
set RC=%ERRORLEVEL%
|
||||
|
||||
echo daily_marketing_report exit=%RC% >> "%LOG_FILE%"
|
||||
echo === Daily marketing report finished at %date% %time% (rc=%RC%) === >> "%LOG_FILE%"
|
||||
|
||||
endlocal & exit /b %RC%
|
||||
@@ -0,0 +1,67 @@
|
||||
@echo off
|
||||
if not defined GYXX_PYTHON set "GYXX_PYTHON=python"
|
||||
REM Daily 22:00 task: collect collaborator + self-operated exposure, write back to Feishu, then sync cmt_notes.
|
||||
REM Historical launcher retained for provenance; production scheduling is owned by gyxx schedule run.
|
||||
|
||||
setlocal
|
||||
|
||||
for %%I in ("%~dp0..\..") do set "PROJECT_DIR=%%~fI"
|
||||
if not defined GYXX_DATA_ROOT (
|
||||
if not defined GYXX_PROJECT_ROOT (
|
||||
echo ERROR: GYXX_DATA_ROOT or GYXX_PROJECT_ROOT must be defined. 1>&2
|
||||
exit /b 3
|
||||
)
|
||||
set "GYXX_DATA_ROOT=%GYXX_PROJECT_ROOT%\var"
|
||||
)
|
||||
set LOG_DIR=%GYXX_DATA_ROOT%\logs\content_marketing\scheduled
|
||||
REM The Python scheduler service may inject a project-specific interpreter.
|
||||
set PYTHON=%GYXX_PYTHON%
|
||||
where %PYTHON% >nul 2>&1 || set PYTHON=python
|
||||
|
||||
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
|
||||
|
||||
set TS=%date:~0,4%%date:~5,2%%date:~8,2%_%time:~0,2%%time:~3,2%%time:~6,2%
|
||||
set TS=%TS: =0%
|
||||
|
||||
set LOG_FILE=%LOG_DIR%\daily_run_%TS%.log
|
||||
|
||||
echo === Daily run started at %date% %time% === > "%LOG_FILE%"
|
||||
echo PROJECT_DIR=%PROJECT_DIR% >> "%LOG_FILE%"
|
||||
|
||||
cd /d "%PROJECT_DIR%"
|
||||
|
||||
echo --- [step 1] run_all.py (collaborator only; includes collaborator mapping refresh) --- >> "%LOG_FILE%"
|
||||
call %PYTHON% run_all.py --daily-scope >> "%LOG_FILE%" 2>&1
|
||||
set RC_RUN=%ERRORLEVEL%
|
||||
echo run_all exit=%RC_RUN% >> "%LOG_FILE%"
|
||||
|
||||
echo --- [step 2] refresh_self_mapping.py --- >> "%LOG_FILE%"
|
||||
call %PYTHON% data\tools\refresh_self_mapping.py >> "%LOG_FILE%" 2>&1
|
||||
set RC_SELF_MAP=%ERRORLEVEL%
|
||||
echo refresh_self_mapping exit=%RC_SELF_MAP% >> "%LOG_FILE%"
|
||||
|
||||
echo --- [step 3] self_bilibili_scraper.py --- >> "%LOG_FILE%"
|
||||
call %PYTHON% self_bilibili_scraper.py >> "%LOG_FILE%" 2>&1
|
||||
set RC_SELF_BILI=%ERRORLEVEL%
|
||||
echo self_bilibili exit=%RC_SELF_BILI% >> "%LOG_FILE%"
|
||||
|
||||
echo --- [step 4] chanmama_scraper.py (self-operated Douyin) --- >> "%LOG_FILE%"
|
||||
call %PYTHON% chanmama_scraper.py >> "%LOG_FILE%" 2>&1
|
||||
set RC_SELF_DOUYIN=%ERRORLEVEL%
|
||||
echo chanmama exit=%RC_SELF_DOUYIN% >> "%LOG_FILE%"
|
||||
|
||||
echo --- [step 5] sync_metrics_to_cmt_notes.py --- >> "%LOG_FILE%"
|
||||
call %PYTHON% data\tools\sync_metrics_to_cmt_notes.py >> "%LOG_FILE%" 2>&1
|
||||
set RC_SYNC=%ERRORLEVEL%
|
||||
echo sync exit=%RC_SYNC% >> "%LOG_FILE%"
|
||||
|
||||
set FINAL_RC=0
|
||||
if not "%RC_RUN%"=="0" set FINAL_RC=1
|
||||
if not "%RC_SELF_MAP%"=="0" set FINAL_RC=1
|
||||
if not "%RC_SELF_BILI%"=="0" set FINAL_RC=1
|
||||
if not "%RC_SELF_DOUYIN%"=="0" set FINAL_RC=1
|
||||
if not "%RC_SYNC%"=="0" set FINAL_RC=1
|
||||
|
||||
echo === Daily run finished at %date% %time% (run=%RC_RUN%, self_map=%RC_SELF_MAP%, self_bili=%RC_SELF_BILI%, self_douyin=%RC_SELF_DOUYIN%, sync=%RC_SYNC%, final=%FINAL_RC%) === >> "%LOG_FILE%"
|
||||
|
||||
endlocal & exit /b %FINAL_RC%
|
||||
@@ -0,0 +1,49 @@
|
||||
@echo off
|
||||
if not defined GYXX_PYTHON set "GYXX_PYTHON=python"
|
||||
REM Monday task: run 3-platform V2 + sync metrics to cmt_notes
|
||||
REM Register: schtasks /Create /SC WEEKLY /D MON /TN YingxiaoYunying_MondayBackfill /TR %PROJECT_DIR%\data\tools\daily_run_with_backfill.bat /ST 06:00 /F
|
||||
|
||||
setlocal
|
||||
|
||||
for %%I in ("%~dp0..\..") do set "PROJECT_DIR=%%~fI"
|
||||
if not defined GYXX_DATA_ROOT (
|
||||
if not defined GYXX_PROJECT_ROOT (
|
||||
echo ERROR: GYXX_DATA_ROOT or GYXX_PROJECT_ROOT must be defined. 1>&2
|
||||
exit /b 3
|
||||
)
|
||||
set "GYXX_DATA_ROOT=%GYXX_PROJECT_ROOT%\var"
|
||||
)
|
||||
set LOG_DIR=%GYXX_DATA_ROOT%\logs\content_marketing\scheduled
|
||||
REM Task Scheduler (SYSTEM account) has no user PATH -> lock to venv python
|
||||
set PYTHON=%GYXX_PYTHON%
|
||||
where %PYTHON% >nul 2>&1 || set PYTHON=python
|
||||
|
||||
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
|
||||
|
||||
set TS=%date:~0,4%%date:~5,2%%date:~8,2%_%time:~0,2%%time:~3,2%%time:~6,2%
|
||||
set TS=%TS: =0%
|
||||
|
||||
set LOG_FILE=%LOG_DIR%\monday_run_%TS%.log
|
||||
|
||||
echo === Monday run started at %date% %time% === > "%LOG_FILE%"
|
||||
echo PROJECT_DIR=%PROJECT_DIR% >> "%LOG_FILE%"
|
||||
|
||||
cd /d "%PROJECT_DIR%"
|
||||
|
||||
echo --- [step 1] run_all.py (includes feishu_mapping refresh) --- >> "%LOG_FILE%"
|
||||
call %PYTHON% run_all.py --daily-scope >> "%LOG_FILE%" 2>&1
|
||||
set RC_RUN=%ERRORLEVEL%
|
||||
echo run_all exit=%RC_RUN% >> "%LOG_FILE%"
|
||||
|
||||
echo --- [step 2] sync_metrics_to_cmt_notes.py --- >> "%LOG_FILE%"
|
||||
call %PYTHON% data\tools\sync_metrics_to_cmt_notes.py >> "%LOG_FILE%" 2>&1
|
||||
set RC_SYNC=%ERRORLEVEL%
|
||||
echo sync exit=%RC_SYNC% >> "%LOG_FILE%"
|
||||
|
||||
set FINAL_RC=0
|
||||
if not "%RC_RUN%"=="0" set FINAL_RC=1
|
||||
if not "%RC_SYNC%"=="0" set FINAL_RC=1
|
||||
|
||||
echo === Monday run finished at %date% %time% (run=%RC_RUN%, sync=%RC_SYNC%, final=%FINAL_RC%) === >> "%LOG_FILE%"
|
||||
|
||||
endlocal & exit /b %FINAL_RC%
|
||||
@@ -0,0 +1,39 @@
|
||||
@echo off
|
||||
if not defined GYXX_PYTHON set "GYXX_PYTHON=python"
|
||||
REM Friday relogin: 4 QR scans — B站, 蒲公英, 星图(+同步到抖音评论), 小红书评论
|
||||
REM 星图登录完成后自动把 cookie 同步到抖音评论 scraper,不需要单独再扫抖音码
|
||||
REM Register: schtasks /Create /SC WEEKLY /D FRI /TN YingxiaoYunying_FridayRelogin /TR %PROJECT_DIR%\data\tools\friday_relogin.bat /ST 10:00 /F
|
||||
|
||||
setlocal
|
||||
|
||||
for %%I in ("%~dp0..\..") do set "PROJECT_DIR=%%~fI"
|
||||
if not defined GYXX_DATA_ROOT (
|
||||
if not defined GYXX_PROJECT_ROOT (
|
||||
echo ERROR: GYXX_DATA_ROOT or GYXX_PROJECT_ROOT must be defined. 1>&2
|
||||
exit /b 3
|
||||
)
|
||||
set "GYXX_DATA_ROOT=%GYXX_PROJECT_ROOT%\var"
|
||||
)
|
||||
set LOG_DIR=%GYXX_DATA_ROOT%\logs\content_marketing\scheduled
|
||||
set PYTHON=%GYXX_PYTHON%
|
||||
where %PYTHON% >nul 2>&1 || set PYTHON=python
|
||||
|
||||
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
|
||||
|
||||
set TS=%date:~0,4%%date:~5,2%%date:~8,2%_%time:~0,2%%time:~3,2%%time:~6,2%
|
||||
set TS=%TS: =0%
|
||||
|
||||
set LOG_FILE=%LOG_DIR%\friday_relogin_%TS%.log
|
||||
|
||||
echo === Friday relogin started at %date% %time% === > "%LOG_FILE%"
|
||||
echo PROJECT_DIR=%PROJECT_DIR% >> "%LOG_FILE%"
|
||||
|
||||
cd /d "%PROJECT_DIR%"
|
||||
|
||||
echo --- Launch 5 QR relogins in parallel; send screenshots; retry up to 3 rounds --- >> "%LOG_FILE%"
|
||||
call %PYTHON% data\tools\friday_relogin_parallel.py --max-attempts 3 --round-timeout 300 --screenshot-delay 60 >> "%LOG_FILE%" 2>&1
|
||||
set FINAL_RC=%ERRORLEVEL%
|
||||
|
||||
echo === Friday relogin finished at %date% %time% (exit=%FINAL_RC%) === >> "%LOG_FILE%"
|
||||
|
||||
endlocal & exit /b %FINAL_RC%
|
||||
@@ -0,0 +1,7 @@
|
||||
$moduleRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
|
||||
$dataRoot = if ($env:GYXX_DATA_ROOT) { $env:GYXX_DATA_ROOT } else { Join-Path $moduleRoot 'var' }
|
||||
$profileRoot = Join-Path $dataRoot 'state\content_marketing\browser-profiles'
|
||||
Get-WmiObject Win32_Process | Where-Object { $_.Name -eq 'chrome.exe' -and $_.CommandLine -like "*$profileRoot*" } | ForEach-Object { Write-Host "Killing PID $($_.ProcessId)"; Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
|
||||
Start-Sleep -Seconds 3
|
||||
$procs = Get-WmiObject Win32_Process | Where-Object { $_.Name -eq 'chrome.exe' -and $_.CommandLine -like "*$profileRoot*" }
|
||||
if ($procs) { Write-Host 'Remaining project chrome processes:'; $procs | Select-Object ProcessId,CommandLine } else { Write-Host 'No project chrome processes remaining' }
|
||||
@@ -0,0 +1,4 @@
|
||||
$moduleRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
|
||||
$dataRoot = if ($env:GYXX_DATA_ROOT) { $env:GYXX_DATA_ROOT } else { Join-Path $moduleRoot 'var' }
|
||||
$profileRoot = Join-Path $dataRoot 'state\content_marketing\browser-profiles'
|
||||
Get-WmiObject Win32_Process | Where-Object { $_.Name -eq 'chrome.exe' -and $_.CommandLine -like "*$profileRoot*" } | Select-Object ProcessId, ParentProcessId, @{Name='CmdStart';Expression={$_.CommandLine.Substring(0, [Math]::Min(200, $_.CommandLine.Length))}}
|
||||
@@ -0,0 +1,55 @@
|
||||
@echo off
|
||||
if not defined GYXX_PYTHON set "GYXX_PYTHON=python"
|
||||
REM Monday 13:00 self-operated pipeline: Bilibili + Chanmama -> sync to cmt_notes (with INSERT)
|
||||
REM Feishu write-back happens inside each scraper; sync step writes PostgreSQL.
|
||||
REM Register: schtasks /Create /SC WEEKLY /D MON /TN YingxiaoYunying_MondaySelf /TR %PROJECT_DIR%\data\tools\monday_self_run.bat /ST 13:00 /F
|
||||
|
||||
setlocal
|
||||
|
||||
for %%I in ("%~dp0..\..") do set "PROJECT_DIR=%%~fI"
|
||||
if not defined GYXX_DATA_ROOT (
|
||||
if not defined GYXX_PROJECT_ROOT (
|
||||
echo ERROR: GYXX_DATA_ROOT or GYXX_PROJECT_ROOT must be defined. 1>&2
|
||||
exit /b 3
|
||||
)
|
||||
set "GYXX_DATA_ROOT=%GYXX_PROJECT_ROOT%\var"
|
||||
)
|
||||
set LOG_DIR=%GYXX_DATA_ROOT%\logs\content_marketing\scheduled
|
||||
set PYTHON=%GYXX_PYTHON%
|
||||
if not exist "%PYTHON%" set PYTHON=python
|
||||
|
||||
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
|
||||
|
||||
set TS=%date:~0,4%%date:~5,2%%date:~8,2%_%time:~0,2%%time:~3,2%%time:~6,2%
|
||||
set TS=%TS: =0%
|
||||
|
||||
set LOG_FILE=%LOG_DIR%\monday_self_%TS%.log
|
||||
|
||||
echo === Monday self-operated run started at %date% %time% === > "%LOG_FILE%"
|
||||
echo PROJECT_DIR=%PROJECT_DIR% >> "%LOG_FILE%"
|
||||
|
||||
cd /d "%PROJECT_DIR%"
|
||||
|
||||
echo --- [step 1] self_bilibili_scraper.py --- >> "%LOG_FILE%"
|
||||
call %PYTHON% self_bilibili_scraper.py >> "%LOG_FILE%" 2>&1
|
||||
set RC_BILI=%ERRORLEVEL%
|
||||
echo self_bilibili exit=%RC_BILI% >> "%LOG_FILE%"
|
||||
|
||||
echo --- [step 2] chanmama_scraper.py --- >> "%LOG_FILE%"
|
||||
call %PYTHON% chanmama_scraper.py >> "%LOG_FILE%" 2>&1
|
||||
set RC_CM=%ERRORLEVEL%
|
||||
echo chanmama exit=%RC_CM% >> "%LOG_FILE%"
|
||||
|
||||
echo --- [step 3] sync_metrics_to_cmt_notes.py (with INSERT) --- >> "%LOG_FILE%"
|
||||
call %PYTHON% data\tools\sync_metrics_to_cmt_notes.py >> "%LOG_FILE%" 2>&1
|
||||
set RC_SYNC=%ERRORLEVEL%
|
||||
echo sync exit=%RC_SYNC% >> "%LOG_FILE%"
|
||||
|
||||
set FINAL_RC=0
|
||||
if not "%RC_BILI%"=="0" set FINAL_RC=1
|
||||
if not "%RC_CM%"=="0" set FINAL_RC=1
|
||||
if not "%RC_SYNC%"=="0" set FINAL_RC=1
|
||||
|
||||
echo === Monday self-operated run finished at %date% %time% (bili=%RC_BILI%, cm=%RC_CM%, sync=%RC_SYNC%, final=%FINAL_RC%) === >> "%LOG_FILE%"
|
||||
|
||||
endlocal & exit /b %FINAL_RC%
|
||||
@@ -0,0 +1,41 @@
|
||||
@echo off
|
||||
if not defined GYXX_PYTHON set "GYXX_PYTHON=python"
|
||||
REM Monthly creator report: run on 1st of each month at 08:30
|
||||
REM 1. Read cmt_cooperations + cmt_creators + cmt_styles from PG
|
||||
REM 2. Generate 9-section analysis report (Markdown)
|
||||
REM 3. Create Feishu doc + save to cmt_creator_report
|
||||
REM Register: schtasks /Create /SC MONTHLY /D 1 /TN YingxiaoYunying_MonthlyCreatorReport /TR %PROJECT_DIR%\data\tools\monthly_creator_report.bat /ST 08:30 /F
|
||||
|
||||
setlocal
|
||||
|
||||
for %%I in ("%~dp0..\..") do set "PROJECT_DIR=%%~fI"
|
||||
if not defined GYXX_DATA_ROOT (
|
||||
if not defined GYXX_PROJECT_ROOT (
|
||||
echo ERROR: GYXX_DATA_ROOT or GYXX_PROJECT_ROOT must be defined. 1>&2
|
||||
exit /b 3
|
||||
)
|
||||
set "GYXX_DATA_ROOT=%GYXX_PROJECT_ROOT%\var"
|
||||
)
|
||||
set LOG_DIR=%GYXX_DATA_ROOT%\logs\content_marketing\scheduled
|
||||
set PYTHON=%GYXX_PYTHON%
|
||||
where %PYTHON% >nul 2>&1 || set PYTHON=python
|
||||
|
||||
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
|
||||
|
||||
set TS=%date:~0,4%%date:~5,2%%date:~8,2%_%time:~0,2%%time:~3,2%%time:~6,2%
|
||||
set TS=%TS: =0%
|
||||
|
||||
set LOG_FILE=%LOG_DIR%\monthly_creator_report_%TS%.log
|
||||
|
||||
echo === Monthly creator report started at %date% %time% === > "%LOG_FILE%"
|
||||
echo PROJECT_DIR=%PROJECT_DIR% >> "%LOG_FILE%"
|
||||
|
||||
cd /d "%PROJECT_DIR%"
|
||||
|
||||
call %PYTHON% -u -X utf8 data/tools/generate_creator_report.py >> "%LOG_FILE%" 2>&1
|
||||
set RC=%ERRORLEVEL%
|
||||
echo monthly_creator_report exit=%RC% >> "%LOG_FILE%"
|
||||
|
||||
echo === Monthly creator report finished at %date% %time% (rc=%RC%) === >> "%LOG_FILE%"
|
||||
|
||||
endlocal
|
||||
@@ -0,0 +1,41 @@
|
||||
@echo off
|
||||
if not defined GYXX_PYTHON set "GYXX_PYTHON=python"
|
||||
REM Monthly summary: run on 1st of each month at 08:00
|
||||
REM 1. Compute last month date range
|
||||
REM 2. Query PG for notes per style
|
||||
REM 3. For each style: note analysis + LLM cross-compare + Feishu doc + write back + save to DB
|
||||
REM Register: schtasks /Create /SC MONTHLY /D 1 /TN YingxiaoYunying_MonthlySummary /TR %PROJECT_DIR%\data\tools\monthly_summary.bat /ST 08:00 /F
|
||||
|
||||
setlocal
|
||||
|
||||
for %%I in ("%~dp0..\..") do set "PROJECT_DIR=%%~fI"
|
||||
if not defined GYXX_DATA_ROOT (
|
||||
if not defined GYXX_PROJECT_ROOT (
|
||||
echo ERROR: GYXX_DATA_ROOT or GYXX_PROJECT_ROOT must be defined. 1>&2
|
||||
exit /b 3
|
||||
)
|
||||
set "GYXX_DATA_ROOT=%GYXX_PROJECT_ROOT%\var"
|
||||
)
|
||||
set LOG_DIR=%GYXX_DATA_ROOT%\logs\content_marketing\scheduled
|
||||
set PYTHON=%GYXX_PYTHON%
|
||||
where %PYTHON% >nul 2>&1 || set PYTHON=python
|
||||
|
||||
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
|
||||
|
||||
set TS=%date:~0,4%%date:~5,2%%date:~8,2%_%time:~0,2%%time:~3,2%%time:~6,2%
|
||||
set TS=%TS: =0%
|
||||
|
||||
set LOG_FILE=%LOG_DIR%\monthly_summary_%TS%.log
|
||||
|
||||
echo === Monthly summary started at %date% %time% === > "%LOG_FILE%"
|
||||
echo PROJECT_DIR=%PROJECT_DIR% >> "%LOG_FILE%"
|
||||
|
||||
cd /d "%PROJECT_DIR%"
|
||||
|
||||
call %PYTHON% -u -X utf8 monthly_summary_all.py --max-workers 4 >> "%LOG_FILE%" 2>&1
|
||||
set RC=%ERRORLEVEL%
|
||||
echo monthly_summary exit=%RC% >> "%LOG_FILE%"
|
||||
|
||||
echo === Monthly summary finished at %date% %time% (rc=%RC%) === >> "%LOG_FILE%"
|
||||
|
||||
endlocal
|
||||
@@ -0,0 +1,37 @@
|
||||
@echo off
|
||||
if not defined GYXX_PYTHON set "GYXX_PYTHON=python"
|
||||
REM Sync creator attributes and cooperation records from Feishu to PostgreSQL
|
||||
REM Runs after daily scrape to pick up new creators/notes/cooperations
|
||||
REM Register: schtasks /Create /SC DAILY /TN YingxiaoYunying_SyncCooperations /TR %PROJECT_DIR%\data\tools\sync_cooperations.bat /ST 09:00 /F
|
||||
|
||||
setlocal
|
||||
|
||||
for %%I in ("%~dp0..\..") do set "PROJECT_DIR=%%~fI"
|
||||
if not defined GYXX_DATA_ROOT (
|
||||
if not defined GYXX_PROJECT_ROOT (
|
||||
echo ERROR: GYXX_DATA_ROOT or GYXX_PROJECT_ROOT must be defined. 1>&2
|
||||
exit /b 3
|
||||
)
|
||||
set "GYXX_DATA_ROOT=%GYXX_PROJECT_ROOT%\var"
|
||||
)
|
||||
set LOG_DIR=%GYXX_DATA_ROOT%\logs\content_marketing\scheduled
|
||||
set PYTHON=%GYXX_PYTHON%
|
||||
where %PYTHON% >nul 2>&1 || set PYTHON=python
|
||||
|
||||
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
|
||||
|
||||
set TS=%date:~0,4%%date:~5,2%%date:~8,2%_%time:~0,2%%time:~3,2%%time:~6,2%
|
||||
set TS=%TS: =0%
|
||||
|
||||
set LOG_FILE=%LOG_DIR%\sync_cooperations_%TS%.log
|
||||
|
||||
echo === sync_cooperations started at %date% %time% === > "%LOG_FILE%"
|
||||
|
||||
cd /d "%PROJECT_DIR%"
|
||||
|
||||
call %PYTHON% data\tools\sync_cooperations.py --refresh-mapping >> "%LOG_FILE%" 2>&1
|
||||
set RC=%ERRORLEVEL%
|
||||
|
||||
echo === sync_cooperations finished at %date% %time% exit=%RC% === >> "%LOG_FILE%"
|
||||
|
||||
endlocal & exit /b %RC%
|
||||
@@ -0,0 +1,80 @@
|
||||
@echo off
|
||||
if not defined GYXX_PYTHON set "GYXX_PYTHON=python"
|
||||
REM Weekly three-platform comment scraper wrapper.
|
||||
REM Runs bilibili, xiaohongshu, douyin in parallel via PowerShell.
|
||||
REM Register: schtasks /Create /SC WEEKLY /D SUN /TN YingxiaoYunying_WeeklyCommentScrape /TR %PROJECT_DIR%\data\tools\weekly_comment_scrape.bat /ST 12:00 /F
|
||||
|
||||
setlocal
|
||||
|
||||
for %%I in ("%~dp0..\..") do set "PROJECT_DIR=%%~fI"
|
||||
if not defined GYXX_DATA_ROOT (
|
||||
if not defined GYXX_PROJECT_ROOT (
|
||||
echo ERROR: GYXX_DATA_ROOT or GYXX_PROJECT_ROOT must be defined. 1>&2
|
||||
exit /b 3
|
||||
)
|
||||
set "GYXX_DATA_ROOT=%GYXX_PROJECT_ROOT%\var"
|
||||
)
|
||||
set "PYTHON_EXE=%GYXX_PYTHON%"
|
||||
set "LOG_DIR=%GYXX_DATA_ROOT%\logs\content_marketing"
|
||||
set "TMP_DIR=%GYXX_DATA_ROOT%\tmp\content_marketing"
|
||||
|
||||
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
|
||||
if not exist "%TMP_DIR%" mkdir "%TMP_DIR%"
|
||||
|
||||
for /f "tokens=2 delims==" %%I in ('wmic os get localdatetime /value') do set "NOW=%%I"
|
||||
set "STAMP=%NOW:~0,4%-%NOW:~4,2%-%NOW:~6,2%_%NOW:~8,2%-%NOW:~10,2%-%NOW:~12,2%"
|
||||
|
||||
set "MASTER_LOG=%LOG_DIR%\weekly_%STAMP%.log"
|
||||
echo === weekly scrape started %STAMP% > "%MASTER_LOG%"
|
||||
|
||||
set "DOUYIN_LOG=%LOG_DIR%\weekly_%STAMP%_douyin.log"
|
||||
set "XHS_LOG=%LOG_DIR%\weekly_%STAMP%_xiaohongshu.log"
|
||||
set "BILI_LOG=%LOG_DIR%\weekly_%STAMP%_bilibili.log"
|
||||
|
||||
echo [%STAMP%] launching douyin, xiaohongshu, bilibili in parallel >> "%MASTER_LOG%"
|
||||
|
||||
set "PS_SCRIPT=%GYXX_DATA_ROOT%\tmp\content_marketing\weekly_scrape_%STAMP%.ps1"
|
||||
> "%PS_SCRIPT%" echo $ErrorActionPreference = 'Continue'
|
||||
>> "%PS_SCRIPT%" echo $env:PYTHONIOENCODING = 'utf-8'
|
||||
>> "%PS_SCRIPT%" echo $env:PYTHONUTF8 = '1'
|
||||
>> "%PS_SCRIPT%" echo Set-Location -LiteralPath '%PROJECT_DIR%'
|
||||
>> "%PS_SCRIPT%" echo $py = '%GYXX_PYTHON%'
|
||||
>> "%PS_SCRIPT%" echo $stamp = '%STAMP%'
|
||||
>> "%PS_SCRIPT%" echo $logDir = '%GYXX_DATA_ROOT%\logs\content_marketing'
|
||||
>> "%PS_SCRIPT%" echo $jobs = @(
|
||||
>> "%PS_SCRIPT%" echo [pscustomobject]@{ Name='bilibili'; Script='%PROJECT_DIR%\data\tools\batch_rescrape_bilibili.py'; Log=Join-Path $logDir ("weekly_${stamp}_bilibili.log") },
|
||||
>> "%PS_SCRIPT%" echo [pscustomobject]@{ Name='xiaohongshu'; Script='%PROJECT_DIR%\data\tools\batch_rescrape_xiaohongshu.py'; Log=Join-Path $logDir ("weekly_${stamp}_xiaohongshu.log") },
|
||||
>> "%PS_SCRIPT%" echo [pscustomobject]@{ Name='douyin'; Script='%PROJECT_DIR%\data\tools\batch_rescrape_douyin.py'; Log=Join-Path $logDir ("weekly_${stamp}_douyin.log") }
|
||||
>> "%PS_SCRIPT%" echo )
|
||||
>> "%PS_SCRIPT%" echo $started = @{}
|
||||
>> "%PS_SCRIPT%" echo foreach ($job in $jobs) {
|
||||
>> "%PS_SCRIPT%" echo $logPath = $job.Log
|
||||
>> "%PS_SCRIPT%" echo $header = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] === $($job.Name) launch ==="
|
||||
>> "%PS_SCRIPT%" echo $cmd = "`"$py`" -X utf8 `"$($job.Script)`" >> `"$logPath`" 2>&1"
|
||||
>> "%PS_SCRIPT%" echo Add-Content -LiteralPath $logPath -Value $header -Encoding utf8
|
||||
>> "%PS_SCRIPT%" echo $proc = Start-Process -FilePath cmd.exe -ArgumentList '/c', "`"$cmd`"" -PassThru -WindowStyle Hidden
|
||||
>> "%PS_SCRIPT%" echo if ($null -eq $proc) {
|
||||
>> "%PS_SCRIPT%" echo Add-Content -LiteralPath $logPath -Value "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] !!! Start-Process returned null for $($job.Name) !!!" -Encoding utf8
|
||||
>> "%PS_SCRIPT%" echo continue
|
||||
>> "%PS_SCRIPT%" echo }
|
||||
>> "%PS_SCRIPT%" echo $started[$job.Name] = @{ Proc=$proc; Log=$logPath }
|
||||
>> "%PS_SCRIPT%" echo }
|
||||
>> "%PS_SCRIPT%" echo $exitCodes = @{}
|
||||
>> "%PS_SCRIPT%" echo foreach ($job in $jobs) {
|
||||
>> "%PS_SCRIPT%" echo $entry = $started[$job.Name]
|
||||
>> "%PS_SCRIPT%" echo $entry.Proc.WaitForExit()
|
||||
>> "%PS_SCRIPT%" echo $rc = $entry.Proc.ExitCode
|
||||
>> "%PS_SCRIPT%" echo $exitCodes[$job.Name] = $rc
|
||||
>> "%PS_SCRIPT%" echo Add-Content -LiteralPath $entry.Log -Value "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] === $($job.Name) done, exit=$rc ===" -Encoding utf8
|
||||
>> "%PS_SCRIPT%" echo }
|
||||
>> "%PS_SCRIPT%" echo foreach ($k in $exitCodes.Keys) { Write-Output ("{0}={1}" -f $k, $exitCodes[$k]) }
|
||||
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%PS_SCRIPT%" >> "%MASTER_LOG%" 2>&1
|
||||
set "RC=%ERRORLEVEL%"
|
||||
|
||||
del "%PS_SCRIPT%" 2>nul
|
||||
|
||||
echo [%STAMP%] all three scrapers finished, ps exit=%RC% >> "%MASTER_LOG%"
|
||||
echo === weekly scrape done %STAMP% >> "%MASTER_LOG%"
|
||||
|
||||
endlocal & exit /b %RC%
|
||||
@@ -0,0 +1,42 @@
|
||||
@echo off
|
||||
if not defined GYXX_PYTHON set "GYXX_PYTHON=python"
|
||||
REM Weekly summary: 每周一 10:00 跑上周各款周笔记汇总
|
||||
REM 1. 动态算上周一到上周日时间范围
|
||||
REM 2. PG 查上周有笔记的款
|
||||
REM 3. 对每个款:跑单品分析 + LLM 4 维度综合 + 创建飞书文档 + 写入生命进程表
|
||||
REM Register: schtasks /Create /SC WEEKLY /D TUE /TN YingxiaoYunying_WeeklySummary /TR %PROJECT_DIR%\data\tools\weekly_summary.bat /ST 10:00 /F
|
||||
|
||||
setlocal
|
||||
|
||||
for %%I in ("%~dp0..\..") do set "PROJECT_DIR=%%~fI"
|
||||
if not defined GYXX_DATA_ROOT (
|
||||
if not defined GYXX_PROJECT_ROOT (
|
||||
echo ERROR: GYXX_DATA_ROOT or GYXX_PROJECT_ROOT must be defined. 1>&2
|
||||
exit /b 3
|
||||
)
|
||||
set "GYXX_DATA_ROOT=%GYXX_PROJECT_ROOT%\var"
|
||||
)
|
||||
set LOG_DIR=%GYXX_DATA_ROOT%\logs\content_marketing\scheduled
|
||||
set PYTHON=%GYXX_PYTHON%
|
||||
where %PYTHON% >nul 2>&1 || set PYTHON=python
|
||||
|
||||
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
|
||||
|
||||
set TS=%date:~0,4%%date:~5,2%%date:~8,2%_%time:~0,2%%time:~3,2%%time:~6,2%
|
||||
set TS=%TS: =0%
|
||||
|
||||
set LOG_FILE=%LOG_DIR%\weekly_summary_%TS%.log
|
||||
|
||||
echo === Weekly summary started at %date% %time% === > "%LOG_FILE%"
|
||||
echo PROJECT_DIR=%PROJECT_DIR% >> "%LOG_FILE%"
|
||||
|
||||
cd /d "%PROJECT_DIR%"
|
||||
|
||||
REM 跑周汇总(4 路并发,动态时间范围)
|
||||
call %PYTHON% -u -X utf8 weekly_summary_all.py --max-workers 4 >> "%LOG_FILE%" 2>&1
|
||||
set RC=%ERRORLEVEL%
|
||||
echo weekly_summary exit=%RC% >> "%LOG_FILE%"
|
||||
|
||||
echo === Weekly summary finished at %date% %time% (rc=%RC%) === >> "%LOG_FILE%"
|
||||
|
||||
endlocal
|
||||
@@ -0,0 +1,15 @@
|
||||
# Product-commerce migration history
|
||||
|
||||
Product-commerce production code now lives directly in
|
||||
`src/gyxx_flow/modules/product_commerce/`. The module is fully owned by
|
||||
`gyxx-flow`; it does not import executable code from the legacy checkout.
|
||||
|
||||
`launchers_reference/` is provenance only. Those files document the old task
|
||||
arguments and must never be registered as final scheduled-task actions.
|
||||
|
||||
The migrated regression tests live in `tests/modules/product_commerce/` and are
|
||||
part of the repository's normal pytest collection.
|
||||
|
||||
The authoritative source/target hashes, transformations, and six intentionally
|
||||
excluded diagnostic entries are recorded in
|
||||
`config/source-manifests/product_commerce.json`.
|
||||
@@ -0,0 +1,8 @@
|
||||
# Launcher provenance only
|
||||
|
||||
These launchers preserve the arguments and sequencing discovered during the
|
||||
read-only audit. Their project-root literals were replaced by the inert
|
||||
`GYXX_RUNTIME_ROOT` placeholder during migration.
|
||||
|
||||
Do not register or invoke these files as production entry points. Final tasks
|
||||
must call the native `gyxx-flow` CLI and its workflow registry.
|
||||
@@ -0,0 +1,35 @@
|
||||
@echo off
|
||||
REM Daily 23:00 sales-decline alert -- independent of 08:40 daily collect.
|
||||
REM
|
||||
REM Detection window: previous 9 days from today, split into 3 segments
|
||||
REM (strict decline: seg1 > seg2 > seg3 > 0).
|
||||
REM Data source: data/<plat>/<safe_name>_<date>/data.json (PG first, JSON fallback).
|
||||
REM Notify: POST Hermes analyzer chat completions, analyzer @ recipient by openid.
|
||||
REM Recipient: ou_7ad5fc8012e2f741afc5346e05ffd447.
|
||||
REM
|
||||
REM Independence rationale:
|
||||
REM - daily collect at 08:40 uses "yesterday" as window end (9d back: 5.31-6.8)
|
||||
REM - this job at 23:00 uses "today" (9d back: 6.1-6.9) to see the latest trend
|
||||
REM - even if daily collect fails, the alert still triggers
|
||||
REM
|
||||
REM Exit codes:
|
||||
REM 0 = 0 events, or successful POST
|
||||
REM 1 = detection logic failed
|
||||
REM 3 = Hermes POST failed (events still persisted)
|
||||
|
||||
setlocal
|
||||
cd /d %GYXX_RUNTIME_ROOT%
|
||||
|
||||
set "PY=%GYXX_RUNTIME_ROOT%\.venv\Scripts\python.exe"
|
||||
|
||||
if not exist data\logs mkdir data\logs
|
||||
|
||||
for /f %%i in ('powershell -NoProfile -Command "Get-Date -Format yyyy-MM-dd"') do set TODAY=%%i
|
||||
|
||||
echo [%TODAY% 23:00] === sales alert job start === >> data\logs\alerts.log
|
||||
|
||||
"%PY%" run_alerts_with_retry.py --end-date %TODAY% --openid ou_7ad5fc8012e2f741afc5346e05ffd447 >> data\logs\alerts.log 2>&1
|
||||
set RC=%ERRORLEVEL%
|
||||
|
||||
echo [%TODAY% 23:00] === sales alert job end (rc=%RC%) === >> data\logs\alerts.log
|
||||
exit /b %RC%
|
||||
@@ -0,0 +1,17 @@
|
||||
@echo off
|
||||
REM Daily 08:40 trigger -- collect (ERP + 3 platforms) + analyze (date-first summary)
|
||||
REM + export (data/ready_for_bitable/<date>/bitable_records.json)
|
||||
REM + insert (推飞书多维表格)
|
||||
REM Skips notify/decline (告警由 run_alerts.bat 23:00 跑; decline 暂未自动触发)
|
||||
REM Uses project venv Python to ensure scrapling/playwright are available
|
||||
REM 08:40 选在 JD/TM 当日数据已刷新、且赶在工作日开始前完成批跑
|
||||
|
||||
setlocal
|
||||
cd /d %GYXX_RUNTIME_ROOT%
|
||||
|
||||
set "PY=%GYXX_RUNTIME_ROOT%\.venv\Scripts\python.exe"
|
||||
|
||||
if not exist data\logs mkdir data\logs
|
||||
|
||||
"%PY%" orchestrate_daily_collection.py --stages collect,analyze,export,insert >> data\logs\daily_collect.log 2>&1
|
||||
exit /b %ERRORLEVEL%
|
||||
@@ -0,0 +1,38 @@
|
||||
@echo off
|
||||
REM run_daily_import.bat
|
||||
REM Triggered by Windows Task Scheduler daily at 19:00.
|
||||
REM Imports previous day's data via import_product_daily.py.
|
||||
REM Log goes to logs/daily_import_<timestamp>.log.
|
||||
REM Note: file is ASCII-only on purpose - cmd's default codepage 936 (GBK)
|
||||
REM misparses UTF-8 Chinese in REM/echo lines as commands.
|
||||
|
||||
setlocal EnableDelayedExpansion
|
||||
set "PROJECT_DIR=%GYXX_RUNTIME_ROOT%"
|
||||
set "PY=%PROJECT_DIR%\.venv\Scripts\python.exe"
|
||||
set "SCRIPT=%PROJECT_DIR%\import_product_daily.py"
|
||||
set "LOG_DIR=%PROJECT_DIR%\logs"
|
||||
|
||||
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
|
||||
|
||||
REM yesterday's date (cmd has no date math, use PowerShell with usebackq to avoid quote conflict)
|
||||
for /f "usebackq" %%D in (`powershell -NoProfile -Command "(Get-Date).AddDays(-1).ToString('yyyy-MM-dd')"`) do set "YESTERDAY=%%D"
|
||||
|
||||
REM timestamp YYYYMMDD_HHMMSS
|
||||
set "STAMP=%date:~0,4%%date:~5,2%%date:~8,2%_%time:~0,2%%time:~3,2%%time:~6,2%"
|
||||
set "STAMP=%STAMP: =0%"
|
||||
|
||||
set "LOG_FILE=%LOG_DIR%\daily_import_%STAMP%.log"
|
||||
|
||||
REM banner (ASCII only)
|
||||
echo [%date% %time:~0,8%] START daily import target_date=%YESTERDAY% log=%LOG_FILE%> "%LOG_FILE%"
|
||||
|
||||
cd /d "%PROJECT_DIR%"
|
||||
"%PY%" "%SCRIPT%" --date %YESTERDAY% >> "%LOG_FILE%" 2>&1
|
||||
set "EXIT_CODE=%ERRORLEVEL%"
|
||||
|
||||
echo [%date% %time:~0,8%] exit_code=%EXIT_CODE% >> "%LOG_FILE%"
|
||||
|
||||
REM keep 30 days of logs, delete older
|
||||
forfiles /p "%LOG_DIR%" /m "daily_import_*.log" /d -30 /c "cmd /c del @file" 2>nul
|
||||
|
||||
endlocal & exit /b %EXIT_CODE%
|
||||
@@ -0,0 +1,49 @@
|
||||
# run_daily_import.ps1
|
||||
# 每日 19:00 由 Windows Task Scheduler 触发。
|
||||
# 计算昨天的日期, 调 import_product_daily.py 入库, 日志落到 logs/。
|
||||
|
||||
$ErrorActionPreference = "Continue"
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
$PSDefaultParameterValues['Out-File:Encoding'] = 'utf8'
|
||||
|
||||
$ProjectDir = $env:GYXX_RUNTIME_ROOT
|
||||
$LogDir = Join-Path $ProjectDir "logs"
|
||||
|
||||
if (-not (Test-Path $LogDir)) {
|
||||
New-Item -ItemType Directory -Path $LogDir -Force | Out-Null
|
||||
}
|
||||
|
||||
$Stamp = Get-Date -Format "yyyyMMdd_HHmmss"
|
||||
$Yesterday = (Get-Date).AddDays(-1).ToString("yyyy-MM-dd")
|
||||
$LogFile = Join-Path $LogDir ("daily_import_" + $Stamp + ".log")
|
||||
|
||||
# 把 stdout/stderr 全部 >> 到日志 (用 cmd 重定向, 避免 PowerShell pipe 跟 Python 的编码冲突)
|
||||
@"
|
||||
[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] 启动每日导入 | 目标日期=$Yesterday | 日志=$LogFile
|
||||
"@ | Out-File -FilePath $LogFile -Encoding utf8 -Append
|
||||
|
||||
Set-Location $ProjectDir
|
||||
$Python = Join-Path $ProjectDir ".venv\Scripts\python.exe"
|
||||
$Script = Join-Path $ProjectDir "import_product_daily.py"
|
||||
|
||||
if (-not (Test-Path $Python)) {
|
||||
Add-Content -Path $LogFile -Value "[FATAL] 找不到 python: $Python" -Encoding utf8
|
||||
exit 2
|
||||
}
|
||||
if (-not (Test-Path $Script)) {
|
||||
Add-Content -Path $LogFile -Value "[FATAL] 找不到脚本: $Script" -Encoding utf8
|
||||
exit 2
|
||||
}
|
||||
|
||||
# 用 cmd 重定向 (2>&1 把 stderr 合并) - 兼容 Python 输出
|
||||
cmd /c "`"$Python`" `"$Script`" --date $Yesterday >> `"$LogFile`" 2>&1"
|
||||
$Exit = $LASTEXITCODE
|
||||
|
||||
Add-Content -Path $LogFile -Value "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] 退出码: $Exit" -Encoding utf8
|
||||
|
||||
# 保留 30 天日志, 删旧的
|
||||
Get-ChildItem -Path $LogDir -Filter "daily_import_*.log" |
|
||||
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-30) } |
|
||||
ForEach-Object { Remove-Item $_.FullName -Force }
|
||||
|
||||
exit $Exit
|
||||
@@ -0,0 +1,16 @@
|
||||
@echo off
|
||||
REM Daily 10:00 trigger -- 三平台人群画像并行采集 + 回填飞书表
|
||||
REM 天猫: collect_persona_to_bitable.py (达摩盘)
|
||||
REM 抖音: collect_dy_persona_to_bitable.py (罗盘成交人群)
|
||||
REM 京东: collect_jd_persona_to_bitable.py (商智人群画像)
|
||||
REM 三个脚本并行启动,各自独立写日志
|
||||
|
||||
setlocal
|
||||
cd /d %GYXX_RUNTIME_ROOT%
|
||||
|
||||
set "PY=%GYXX_RUNTIME_ROOT%\.venv\Scripts\python.exe"
|
||||
|
||||
if not exist data\logs mkdir data\logs
|
||||
|
||||
"%PY%" run_daily_persona.py >> data\logs\daily_persona_launcher.log 2>&1
|
||||
exit /b %ERRORLEVEL%
|
||||
@@ -0,0 +1,13 @@
|
||||
@echo off
|
||||
REM Runs independently every 3 days at 11:00. The analyzer defaults to yesterday
|
||||
REM as the window end, so each run covers the latest three complete calendar days.
|
||||
|
||||
setlocal
|
||||
cd /d %GYXX_RUNTIME_ROOT%
|
||||
|
||||
set "PY=%GYXX_RUNTIME_ROOT%\.venv\Scripts\python.exe"
|
||||
|
||||
if not exist data\logs mkdir data\logs
|
||||
|
||||
"%PY%" -u analyze_style_with_hermes.py --all-styles --days 3 --min-interval-days 3 --skip-existing >> data\logs\style_analysis_3d.log 2>&1
|
||||
exit /b %ERRORLEVEL%
|
||||
@@ -0,0 +1,12 @@
|
||||
@echo off
|
||||
REM Weekly JD main image pipeline. Keep this file ASCII-only for Task Scheduler.
|
||||
setlocal
|
||||
cd /d %GYXX_RUNTIME_ROOT%
|
||||
|
||||
set "PY=%GYXX_RUNTIME_ROOT%\.venv\Scripts\python.exe"
|
||||
set "PYTHONUNBUFFERED=1"
|
||||
|
||||
if not exist data\logs mkdir data\logs
|
||||
|
||||
"%PY%" -u run_weekly_jd_main_image.py --headless >> data\logs\weekly_jd_main_image.log 2>&1
|
||||
exit /b %ERRORLEVEL%
|
||||
@@ -0,0 +1,12 @@
|
||||
@echo off
|
||||
REM Weekly main image pipeline. Keep this file ASCII-only for Task Scheduler.
|
||||
setlocal
|
||||
cd /d %GYXX_RUNTIME_ROOT%
|
||||
|
||||
set "PY=%GYXX_RUNTIME_ROOT%\.venv\Scripts\python.exe"
|
||||
set "PYTHONUNBUFFERED=1"
|
||||
|
||||
if not exist data\logs mkdir data\logs
|
||||
|
||||
"%PY%" -u run_weekly_main_image.py --headless >> data\logs\weekly_main_image.log 2>&1
|
||||
exit /b %ERRORLEVEL%
|
||||
@@ -0,0 +1,14 @@
|
||||
@echo off
|
||||
REM Weekly Monday 10:00 -- collect TM/JD/DY market ranks, create Feishu docs,
|
||||
REM and archive each platform's latest document URL in PostgreSQL.
|
||||
|
||||
setlocal
|
||||
cd /d %GYXX_RUNTIME_ROOT%
|
||||
|
||||
set "PY=%GYXX_RUNTIME_ROOT%\.venv\Scripts\python.exe"
|
||||
set "PYTHONUNBUFFERED=1"
|
||||
|
||||
if not exist data\logs mkdir data\logs
|
||||
|
||||
"%PY%" orchestrate_market_rank_collection.py >> data\logs\market_rank_weekly.log 2>&1
|
||||
exit /b %ERRORLEVEL%
|
||||
@@ -0,0 +1,21 @@
|
||||
#Requires -RunAsAdministrator
|
||||
param(
|
||||
[string]$ProjectRoot = "",
|
||||
[string]$NssmPath = "nssm.exe"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
if ([string]::IsNullOrWhiteSpace($ProjectRoot)) {
|
||||
$ProjectRoot = if ($env:GYXX_PROJECT_ROOT) {
|
||||
$env:GYXX_PROJECT_ROOT
|
||||
} else {
|
||||
(Resolve-Path (Join-Path $PSScriptRoot "..\..\..\..\..\..")).Path
|
||||
}
|
||||
}
|
||||
$uninstaller = Join-Path $ProjectRoot "deploy\windows-service\uninstall.ps1"
|
||||
if (-not (Test-Path -LiteralPath $uninstaller -PathType Leaf)) {
|
||||
throw "Server scheduler uninstaller is missing: $uninstaller"
|
||||
}
|
||||
|
||||
& $uninstaller -NssmPath $NssmPath
|
||||
exit $LASTEXITCODE
|
||||
@@ -0,0 +1,22 @@
|
||||
#Requires -RunAsAdministrator
|
||||
param(
|
||||
[string]$ProjectRoot = "",
|
||||
[string]$DataRoot = "",
|
||||
[string]$NssmPath = "nssm.exe"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
if ([string]::IsNullOrWhiteSpace($ProjectRoot)) {
|
||||
$ProjectRoot = if ($env:GYXX_PROJECT_ROOT) {
|
||||
$env:GYXX_PROJECT_ROOT
|
||||
} else {
|
||||
(Resolve-Path (Join-Path $PSScriptRoot "..\..\..\..\..\..")).Path
|
||||
}
|
||||
}
|
||||
$installer = Join-Path $ProjectRoot "deploy\windows-service\install.ps1"
|
||||
if (-not (Test-Path -LiteralPath $installer -PathType Leaf)) {
|
||||
throw "Server scheduler installer is missing: $installer"
|
||||
}
|
||||
|
||||
& $installer -ProjectRoot $ProjectRoot -DataRoot $DataRoot -NssmPath $NssmPath
|
||||
exit $LASTEXITCODE
|
||||
@@ -0,0 +1,190 @@
|
||||
---
|
||||
name: data-analyzer
|
||||
description: 分析端。接收用户消息触发采购单更新,发送采购单更新通知,处理编排器发来的工作流分析指令。
|
||||
metadata:
|
||||
hermes:
|
||||
skillKey: data-analyzer
|
||||
profile: analyzer
|
||||
os: ["win32"]
|
||||
requires:
|
||||
bins: ["python"]
|
||||
---
|
||||
|
||||
# Data Analyzer
|
||||
|
||||
## Mission
|
||||
你是 `data-analyzer`,职责:
|
||||
1. 读取 `shared-data` 产物做分析/汇总。
|
||||
2. 发送最终业务通知(给业务对象和 owner)。
|
||||
3. 接收用户飞书消息,触发采购单日期更新工作流。
|
||||
|
||||
## 采购单日期更新触发
|
||||
|
||||
当用户发来包含 **8位SKU编码** 和 **日期** 的消息时,触发采购单更新工作流。
|
||||
|
||||
### 触发条件
|
||||
消息中同时包含:
|
||||
- 至少一个 8 位数字 SKU(如 `10439030`)
|
||||
- 一个日期(如 `5月25日`、`2026-05-25`、`明天`)
|
||||
|
||||
### 触发方式
|
||||
使用终端工具执行(**必须传递 --sender-open-id**,脚本内部校验白名单):
|
||||
```bash
|
||||
python -m gyxx_flow.modules.supply_chain.runtime.orchestrator.scripts.trigger_purchase_order_update --message "<用户消息原文>" --sender-open-id "<发送人open_id>"
|
||||
```
|
||||
|
||||
允许触发的 sender_open_id 在 `orchestrator/config.py` → `FEISHU_CONFIG.trigger.purchase_order_update.allowed_sender_open_ids` 中配置。当前允许:`<ANALYZER_OWNER_OPEN_ID>`。
|
||||
|
||||
### 执行后
|
||||
- 工作流触发后,采集端执行 ERP 操作,结果写入 `shared-data/purchase-order-update/update_result.json`
|
||||
- **通知发送需要分析端手动执行**(见下节),工作流本身不自动发送飞书消息
|
||||
- 如果触发成功,回复用户"采购单更新已触发,SKU: xxx,目标日期: xxx"
|
||||
- 如果触发失败(权限不足、解析失败等),回复用户具体原因
|
||||
- 如果消息不包含 SKU 或日期,按普通对话处理,不触发工作流
|
||||
|
||||
## 采购单更新后发送通知
|
||||
|
||||
工作流执行完成后,分析端需要手动发送通知到两个目标:
|
||||
1. **审单群**(chat_id: `oc_6e95333db779b07524e1c361099c7aec`)
|
||||
2. **owner 个人**(open_id: `ou_7ad5fc8012e2f741afc5346e05ffd447`)
|
||||
|
||||
### 前置条件
|
||||
- 分析端飞书机器人(`cli_aa8c4fc918b85cce`)必须已是审单群成员,才能发送到群
|
||||
- 如果机器人不在群里,会返回 `code=230002: Bot/User can NOT be out of the chat`
|
||||
|
||||
### 发送方式(直接调用飞书 Open API)
|
||||
|
||||
```python
|
||||
import requests, json
|
||||
from datetime import datetime
|
||||
|
||||
APP_ID = "cli_aa8c4fc918b85cce"
|
||||
APP_SECRET = "${GYXX_SUPPLY_ANALYZER_APP_SECRET}"
|
||||
FEISHU_OPENAPI_BASE = "https://open.feishu.cn/open-apis"
|
||||
SHENDAN_GROUP_CHAT_ID = "oc_6e95333db779b07524e1c361099c7aec"
|
||||
MY_OPEN_ID = "ou_7ad5fc8012e2f741afc5346e05ffd447"
|
||||
|
||||
# 获取 token
|
||||
token_resp = requests.post(
|
||||
f"{FEISHU_OPENAPI_BASE}/auth/v3/tenant_access_token/internal/",
|
||||
json=dict((("app_id", APP_ID), ("app_secret", APP_SECRET))), timeout=10
|
||||
)
|
||||
token = token_resp.json()["tenant_access_token"]
|
||||
|
||||
# 构建交互式卡片 (兼容写法:top-level elements + header,避开 body 嵌套翻车)
|
||||
# records 每条形如 {"sku": "...", "name": "...", "spec": "..."}
|
||||
header = {
|
||||
"template": "blue",
|
||||
"title": {"tag": "plain_text", "content": "📌 采购单更新通知"},
|
||||
}
|
||||
elements = [
|
||||
{
|
||||
"tag": "div",
|
||||
"fields": [
|
||||
{"is_short": True, "text": {"tag": "lark_md", "content": f"**更新时间**\n{datetime.now().strftime('%Y-%m-%d %H:%M')}"}},
|
||||
{"is_short": True, "text": {"tag": "lark_md", "content": f"**到货日期**\n{target_date}"}},
|
||||
],
|
||||
},
|
||||
{"tag": "div", "text": {"tag": "lark_md", "content": f"✅ **成功更新 {total_updated} 条采购单记录**"}},
|
||||
{"tag": "hr"},
|
||||
]
|
||||
for r in records:
|
||||
elements.append({
|
||||
"tag": "div",
|
||||
"text": {"tag": "lark_md", "content": f"- **SKU:** {r['sku']} **品名:** {r['name']} **规格:** {r['spec']}"},
|
||||
})
|
||||
card = {
|
||||
"config": {"wide_screen_mode": True},
|
||||
"header": header,
|
||||
# ⚠️ 2026-06-29:不要把 elements 嵌套在 body 下!Feishu 存盘会 strip 掉 body,
|
||||
# 用户收到空卡。用 top-level elements(详见上文避坑)。
|
||||
"elements": elements,
|
||||
}
|
||||
|
||||
# 发送到审单群
|
||||
requests.post(
|
||||
f"{FEISHU_OPENAPI_BASE}/im/v1/messages",
|
||||
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
|
||||
params={"receive_id_type": "chat_id"},
|
||||
json={"receive_id": SHENDAN_GROUP_CHAT_ID, "msg_type": "interactive",
|
||||
"content": json.dumps(card, ensure_ascii=False)},
|
||||
timeout=10
|
||||
)
|
||||
|
||||
# 发送到个人
|
||||
requests.post(
|
||||
f"{FEISHU_OPENAPI_BASE}/im/v1/messages",
|
||||
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
|
||||
params={"receive_id_type": "open_id"},
|
||||
json={"receive_id": MY_OPEN_ID, "msg_type": "interactive",
|
||||
"content": json.dumps(card, ensure_ascii=False)},
|
||||
timeout=10
|
||||
)
|
||||
```
|
||||
|
||||
### 通知内容要求(来自工作流规则)
|
||||
必须包含:SKU列表、商品名称、规格颜色、到货日期、更新记录数。
|
||||
|
||||
### 如果机器人不在审单群
|
||||
1. 先发送通知到个人账户告知结果
|
||||
2. 告知用户需要将分析端机器人加入审单群
|
||||
|
||||
### 备选:lark-cli 发送(不推荐主用)
|
||||
|
||||
如果不想手写 `requests.post` token 流程,可改用 `lark-cli`(已配 `hermes-analyzer` profile):
|
||||
|
||||
```bash
|
||||
# 1. 把 card 写到临时文件
|
||||
cat > /tmp/card.json << 'JSON'
|
||||
{
|
||||
"config": {"wide_screen_mode": true},
|
||||
"header": {"template": "blue", "title": {"tag": "plain_text", "content": "📌 采购单更新通知"}},
|
||||
"elements": [...]
|
||||
}
|
||||
JSON
|
||||
|
||||
# 2. 发给个人(analyzer owner,bot 身份在 hermes-analyzer profile 下)
|
||||
lark-cli --profile hermes-analyzer im +messages-send \
|
||||
--as bot --user-id ou_7ad5fc8012e2f741afc5346e05ffd447 \
|
||||
--msg-type interactive --content "$(cat /tmp/card.json)"
|
||||
|
||||
# 3. 发给审单群(用 --chat-id + bot 身份)
|
||||
lark-cli --profile hermes-analyzer im +messages-send \
|
||||
--as bot --chat-id oc_6e95333db779b07524e1c361099c7aec \
|
||||
--msg-type interactive --content "$(cat /tmp/card.json)"
|
||||
```
|
||||
|
||||
**关键点**:
|
||||
- 必须在命令前加 `--profile hermes-analyzer`(lark-cli 默认 `currentApp` 是采集端 `cli_aa8c4fb4c4f81cd3`,发给 analyzer owner 会 `code: 99992361 "open_id cross app"`)。
|
||||
- 卡片 JSON 用 **top-level `elements`**(不要再用 `body.elements` 嵌套,详见上文 Hard Rules 避坑段)。`header.template` + `header.title` 控制标题颜色和文字。
|
||||
- `--as bot` 不需要额外权限;`--as user` 需要 `im:message.send_as_user` scope,分析端应用未配置。
|
||||
|
||||
## 工作流分析(由编排器调用)
|
||||
|
||||
当收到编排器发来的工作流分析指令时:
|
||||
1. 读取 `shared-data` 下对应工作流的输出文件
|
||||
2. 分析数据并发送最终业务通知
|
||||
3. 不发送过程/进度通知
|
||||
|
||||
## 参考资料
|
||||
- `references/purchase-order-update-config.md` — 飞书应用配置、审单群 chat_id、触发白名单、常见错误码
|
||||
|
||||
## Hard Rules
|
||||
- 不运行采集脚本(`collect_*.ps1`)。
|
||||
- 不发送采集心跳、进度播报给业务用户。
|
||||
- 最终业务通知如果内容过长,必须拆分为多条消息,使用 `[1/N]...[N/N]` 前缀。
|
||||
- 禁止截断通知内容。
|
||||
- **禁止用本地 Python/PowerShell 脚本发送飞书业务通知**,必须通过 `execute_code` 直接调飞书 Open API。
|
||||
- 例外:**Schema 2.0 卡片发送**统一走 `orchestrator/scripts/send_card_notification.py`(2026-06-29 起,LLM 直接调 lark-cli im +messages-send --file 翻车)。其它通知场景仍按上面铁律。
|
||||
- **业务通知必须使用 interactive card**(`msg_type="interactive"`, `content` 为卡片 JSON 字符串),header 配色按场景选 blue/green/red;不要发 plain text 或 post 富文本。
|
||||
- **【避坑 2026-06-29】卡片 elements 不要用 `body.elements` 嵌套!** 实测 Feishu `/im/v1/messages` POST 返回 200 OK,但存盘时把整个 `body` 字段丢掉,用户收到"只有标题的空卡片"。**正确写法:top-level elements + header(template+title)**,`send_card_notification.py` 已加 body→top-level 兜底,但 LLM 应该直接发对。
|
||||
```json
|
||||
// ✓ 推荐 (兼容写法,header 控色,elements 在 top-level)
|
||||
{"config": {"wide_screen_mode": true},
|
||||
"header": {"template": "blue", "title": {"tag": "plain_text", "content": "📌 通知标题"}},
|
||||
"elements": [{"tag": "div", "text": {"tag": "lark_md", "content": "**正文**"}}]}
|
||||
// ✗ 会翻车 (Schema 2.0 嵌套,body 存盘被 strip)
|
||||
{"header": {...}, "body": {"elements": [...]}}
|
||||
```
|
||||
- 用 lark-cli 发卡时必须加 `--profile hermes-analyzer`,否则 lark-cli 用采集端 bot 身份,发给分析端用户会 `code: 99992361 "open_id cross app"`。卡片 JSON 用 **top-level elements**(不要再用 `body.elements` 嵌套,见上面避坑)。
|
||||
- **【避坑 2026-06-29】不要在卡片里用 `tag: "table"` 元素**。`/im/v1/messages` 报 400 ErrCode 200906 「table columns is empty」(加 column_widths 也不解决);改用 cells/td 又触发 200621 「parse card json err」;改用 column name 映射 rows 能 send(200)但飞书客户端显示「请升级至最新版本」的占位图。**结论:表格用 `div + lark_md` 包 ```markdown|...|...|``` 代码块渲染,稳定可靠。**
|
||||
@@ -0,0 +1,151 @@
|
||||
---
|
||||
name: purchase-order-update
|
||||
description: 采购单日期更新工作流 — 触发工作流更新ERP中SKU的协议到货日期,然后发送飞书通知到审单群和用户
|
||||
---
|
||||
|
||||
# Purchase Order Update Workflow(采购单日期更新工作流)
|
||||
|
||||
## 触发条件
|
||||
用户发送消息包含 **8位SKU** 和 **到货日期**,支持多种自然语言格式。
|
||||
|
||||
### 支持的消息格式示例
|
||||
|
||||
| 示例消息 | 说明 |
|
||||
|---------|------|
|
||||
| `10465007 更新到货日期 5月19日` | ✅ |
|
||||
| `10465007 到货日期改为 2026-05-19` | ✅ |
|
||||
| `采购单 10465007 5月19日到货` | ✅ |
|
||||
| `10465007 明天到货` | ✅(明天自动换算) |
|
||||
| `10465007,5月19日,采购到货` | ✅ |
|
||||
| `10439048 10439047 更新到货日期 5月19日` | ✅(多SKU) |
|
||||
| `10465007 更新 5月19日` | ✅ |
|
||||
|
||||
### 日期格式支持
|
||||
- `YYYY-MM-DD` → `2026-05-19`
|
||||
- `YYYY/MM/DD` → `2026/05/19`
|
||||
- `5月19日` / `5月19号`
|
||||
- `今天` / `明天` / `后天` / `大后天`
|
||||
|
||||
### 关键词忽略
|
||||
`采购`、`到货`、`日期`、`更新`、`协议` 等字词会被自动过滤,不影响解析。
|
||||
|
||||
## 执行步骤
|
||||
|
||||
### 1. 准备任务文件
|
||||
由于 `orchestrator.config` 模块当前不可用,直接写入 task.json:
|
||||
```bash
|
||||
mkdir -p ${GYXX_PROJECT_ROOT}/shared-data/purchase-order-update
|
||||
# 写入 task.json:
|
||||
{
|
||||
"sku_list": ["<SKU1>", "<SKU2>"],
|
||||
"target_date": "2026-05-19",
|
||||
"remark": "",
|
||||
"created_at": "<ISO时间>",
|
||||
"source": "analyzer_message_trigger"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 直接运行采购单更新脚本
|
||||
```bash
|
||||
cd ${GYXX_PROJECT_ROOT} && .venv/Scripts/python.exe orchestrator/scripts/PurchaseOrderUpdate.py --task-file ${GYXX_PROJECT_ROOT}/shared-data/purchase-order-update/task.json
|
||||
```
|
||||
- 超时:300秒
|
||||
- 成功标志:日志含 `[LOG] SCRIPT_COMPLETED`
|
||||
- 结果写入:`orchestrator/scripts/data/update_result.json`
|
||||
|
||||
### 3. 发送飞书通知
|
||||
|
||||
#### 3.1 获取 tenant_access_token
|
||||
```python
|
||||
import requests
|
||||
APP_ID = "cli_aa8c4fc918b85cce"
|
||||
APP_SECRET = "${GYXX_SUPPLY_ANALYZER_APP_SECRET}"
|
||||
token = requests.post(
|
||||
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/",
|
||||
json=dict((("app_id", APP_ID), ("app_secret", APP_SECRET))),
|
||||
timeout=10
|
||||
).json()["tenant_access_token"]
|
||||
```
|
||||
|
||||
#### 3.2 构建通知内容
|
||||
```python
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
current_time = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
|
||||
# Schema 2.0 交互式卡片
|
||||
header = {
|
||||
"template": "blue",
|
||||
"title": {"tag": "plain_text", "content": "📌 采购单更新通知"},
|
||||
}
|
||||
elements = [
|
||||
{
|
||||
"tag": "div",
|
||||
"fields": [
|
||||
{"is_short": True, "text": {"tag": "lark_md", "content": f"**更新时间**\n{current_time}"}},
|
||||
{"is_short": True, "text": {"tag": "lark_md", "content": f"**到货日期**\n{target_date}"}},
|
||||
],
|
||||
},
|
||||
{"tag": "div", "text": {"tag": "lark_md", "content": f"✅ **成功更新 {total_updated} 条采购单记录**"}},
|
||||
{"tag": "hr"},
|
||||
]
|
||||
# 每条记录一行:- SKU:{sku},品名:{name},规格:{spec}
|
||||
for r in records:
|
||||
elements.append({
|
||||
"tag": "div",
|
||||
"text": {"tag": "lark_md", "content": f"- **SKU:** {r['sku']} **品名:** {r['name']} **规格:** {r['spec']}"},
|
||||
})
|
||||
|
||||
card = {
|
||||
"config": {"wide_screen_mode": True},
|
||||
"header": header,
|
||||
"body": {"elements": elements}, # Schema 2.0 规范:elements 嵌套在 body 下
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.3 发送消息
|
||||
```python
|
||||
SHENDAN_GROUP_CHAT_ID = "oc_6e95333db779b07524e1c361099c7aec"
|
||||
MY_OPEN_ID = "ou_7ad5fc8012e2f741afc5346e05ffd447"
|
||||
|
||||
# 发送审单群
|
||||
requests.post(
|
||||
"https://open.feishu.cn/open-apis/im/v1/messages",
|
||||
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
|
||||
params={"receive_id_type": "chat_id"},
|
||||
json={
|
||||
"receive_id": SHENDAN_GROUP_CHAT_ID,
|
||||
"msg_type": "interactive",
|
||||
"content": json.dumps(card, ensure_ascii=False)
|
||||
},
|
||||
timeout=10
|
||||
)
|
||||
|
||||
# 发送本人
|
||||
requests.post(
|
||||
"https://open.feishu.cn/open-apis/im/v1/messages",
|
||||
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
|
||||
params={"receive_id_type": "open_id"},
|
||||
json={
|
||||
"receive_id": MY_OPEN_ID,
|
||||
"msg_type": "interactive",
|
||||
"content": json.dumps(card, ensure_ascii=False)
|
||||
},
|
||||
timeout=10
|
||||
)
|
||||
```
|
||||
|
||||
## 白名单配置
|
||||
`${GYXX_PROJECT_ROOT}\orchestrator\config.py` → `FEISHU_CONFIG.trigger.purchase_order_update.allowed_sender_open_ids`
|
||||
- `ou_7ad5fc8012e2f741afc5346e05ffd447`(用户)
|
||||
- `ou_339c1d396b397c97b08e1bf377963d40`
|
||||
- `ou_54141d75b24d22ddc8c1c4d66e00b6e9`
|
||||
|
||||
## 注意事项
|
||||
- `orchestrator.config` 模块缺失(WORKFLOWS / FEISHU_CONFIG 未定义),trigger 脚本因此不可用;改用直接脚本执行可正常工作
|
||||
- 发送审单群时需确认分析端机器人已在群中
|
||||
- 工作流会自动更新 ERP 系统中对应 SKU 采购单的协议到货日期
|
||||
- Playwright 浏览器依赖若未安装(`Executable doesn't exist`),需先运行:`.venv\Scripts\python.exe -m playwright install chromium`
|
||||
- 卡片 JSON 用 `body.elements` 嵌套(Schema 2.0 规范);top-level `elements` 也兼容但官方不推荐
|
||||
- 若改用 lark-cli 发送,命令前必须加 `--profile hermes-analyzer`(lark-cli 默认采集端 bot 身份),否则 `code: 99992361 "open_id cross app"`
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
name: data-collector-guardrail
|
||||
description: "Guardrail for collector: collect + heartbeat only."
|
||||
metadata:
|
||||
hermes:
|
||||
skillKey: data-collector-guardrail
|
||||
profile: data-collector
|
||||
os: ["win32"]
|
||||
---
|
||||
|
||||
Collector Guardrail
|
||||
|
||||
You are the collector endpoint.
|
||||
|
||||
Do:
|
||||
- Run collection scripts.
|
||||
- Publish fresh artifacts to shared-data.
|
||||
- (Heartbeat is now sent automatically by `orchestrator/mcp_workflow.py` as a Schema 2.0 interactive card to `ou_8ee224968aa26a74c7d30ba27fed5eeb`. Do NOT call send_message / curl / any Feishu API to relay heartbeats.)
|
||||
- For **manual** ad-hoc heartbeat relay (operator asks in chat to forward a workflow report to the admin), reply directly with the same fields described in `Collector Heartbeat Notification Format` below — the gateway binding delivers the reply.
|
||||
|
||||
Do not:
|
||||
- Send final business notifications.
|
||||
- Run analyzer-stage actions.
|
||||
- Mix analyzer app credentials (analyzer app: cli_aa8c4fc918b85cce, analyzer owner: ou_7ad5fc8012e2f741afc5346e05ffd447).
|
||||
- Try to call send_message, curl, or external APIs to relay **automated** collector heartbeats — orchestrator already sends them as cards.
|
||||
|
||||
Credential path: The collector app secret (`2lAvcKK4gX2Qa6uCrhxgZedTzbc70a7U`) lives in two places:
|
||||
- Hermes profile `.env`: `${HERMES_STATE_ROOT}\profiles\data-collector\.env` under `FEISHU_APP_SECRET` (for cron / LLM-direct paths).
|
||||
- `orchestrator/config.py` → `FEISHU_CONFIG.apps.collector.app_secret` (env override `AUTOFLOW_COLLECTOR_APP_SECRET`). Used by `mcp_workflow.py:_send_collector_heartbeat` for the new automated card-based heartbeats.
|
||||
|
||||
It is NOT in `orchestrator/scripts/config.json` → `feishu.app_secret` / `feishu_analyzer.app_secret` (those are analyzer app credentials). Scripts like `send_collector_notify.py` that fall back to that file will fail with `code: 10014, app secret invalid`.
|
||||
|
||||
**Automated heartbeat (post-2026-06-27):** The orchestrator sends Schema 2.0 interactive cards via `mcp_workflow.py:_send_collector_heartbeat` → `requests.post(/open-apis/im/v1/messages)` with the collector app credentials, target `ou_8ee224968aa26a74c7d30ba27fed5eeb`. Phases: `start` (blue), `progress` (blue, every `HEARTBEAT_INTERVAL_SECONDS`=180s), `final` (green on success / red on failure). The collector LLM is NOT involved — do not duplicate.
|
||||
|
||||
**Manual relay (operator asks in chat to forward a report):** Reply directly with the format below. The gateway binding delivers it. No tool calls needed.
|
||||
|
||||
Pitfalls:
|
||||
| Collector app owner open_id | `ou_8ee224968aa26a74c7d30ba27fed5eeb` — the admin/reporter who receives collector heartbeat notifications via collector Feishu app |
|
||||
| Notification target (admin/reporter) | `ou_8ee224968aa26a74c7d30ba27fed5eeb` — this profile receives workflow heartbeat reports (NOT the analyzer app owner) |
|
||||
| Analyzer app owner open_id | `ou_7ad5fc8012e2f741afc5346e05ffd447` — NOT the collector heartbeat target; this belongs to the analyzer app scope (`cli_aa8c4fc918b85cce`) |
|
||||
- UTF-8 BOM manifest error: If a workflow fails with `Invalid run manifest: Unexpected UTF-8 BOM (decode using utf-8-sig): line 1 column 1`, the manifest JSON file was saved with a BOM (common on Windows editors like Notepad). Fix: resave the manifest as UTF-8 without BOM, or strip the BOM (`\ufeff`) from the file's first bytes.
|
||||
- batch_process.py scripts must be saved as UTF-8 encoding. GBK/CP936 encoding corrupts Chinese string literals and causes "unterminated string literal" SyntaxError on line 41.
|
||||
- Yuanbao toolsets are listed in platform_toolsets.cli but the `yb` CLI binary and `yb_send_dm`/`yb_query_group_members` functions are NOT available on data-collector — `yb_send_dm: command not found` is the expected result. Do not search for the `yb` binary, attempt `hermes tools enable yuanbao` (it will succeed but expose nothing usable), or try to curl localhost:8123/yuanbao/send_dm. Yuanbao adapter is also absent from channel_directory.json (yuanbao: []). Always fall back to direct reply immediately — the API server delivers collector heartbeat reports without any Yuanbao integration.
|
||||
- Cron jobs running collector workflows (deliver: local): output IS the notification — do NOT try to call Feishu REST APIs from cron. The cron output goes to the gateway chat; the gateway relays to Feishu. If you need to send Feishu from cron, you need the app_secret accessible in the cron env (`FEISHU_APP_SECRET`) — collector app secrets are often gateway-runtime-injected and NOT stored in the codebase. Use `FEISHU_APP_SECRET` (not `COLLECTOR_FEISHU_APP_SECRET`).
|
||||
- **`execute_code` filesystem access is limited to the sandbox directory.** `execute_code` runs in an isolated sandbox — it cannot access host paths like `${GYXX_PROJECT_ROOT}\shared-data\` or `${HERMES_STATE_ROOT}\...`. Attempting to open host paths via `Path()` or `read_bytes()` will raise `FileNotFoundError`. Use `terminal` (which inherits the MSYS/Git-Bash environment with proper path mapping) for all file operations on host paths. The `execute_code` sandbox is only suitable for pure in-memory computation or accessing paths that the sandbox itself creates.
|
||||
- **`execute_code` can make outbound HTTP calls** — the "cannot call Feishu REST APIs" restriction is outdated. `requests` + `FEISHU_APP_SECRET` works fine from `execute_code` in the agent process. The failure scenario is cron job scripts running in a subprocess (where `FEISHU_APP_SECRET` is not injected). Always check `os.environ.get("FEISHU_APP_SECRET")` before assuming it works or fails.
|
||||
- **`terminal` + inline Python script** is the most reliable pattern for Feishu REST calls. `python -c "..."` via terminal inherits the shell env, works in both agent and cron contexts.
|
||||
- **Manual relay only (operator asks in chat to forward a report) — reply DIRECTLY with no tool calls.** Your reply text IS the notification. Do NOT query channel_directory, search for yuanbao group codes, call send_message, or look up credentials. This is for **manual / ad-hoc** forwarding only — automated heartbeats are sent by the orchestrator as cards.
|
||||
- Common mistake: trying to relay an automated heartbeat (orchestrator already sent it) or calling send_message for manual relay. Both are unnecessary.
|
||||
- Cron jobs with **gateway Feishu binding** (deliver: origin/local): reply directly — the gateway's own Feishu binding delivers the reply. (For automated heartbeats the orchestrator sends the card itself, so this is now mostly relevant for operator-side messages, not workflow status.)
|
||||
- Scripts run via **terminal** without gateway binding: use `python -c "..."` to call Feishu REST APIs with `FEISHU_APP_SECRET`. (Same caveat — only relevant for paths outside the orchestrator's automated heartbeat flow.)
|
||||
**Cron job skills that don't exist cause a skip warning but don't fail the job.** When a cron job's `skills` list contains a nonexistent skill, the scheduler logs `WARNING Cron job 'X': skill not found, skipping — Skill 'feishu_doc' not found.` The job still runs with the remaining skills or no skills. If the cron job silently does nothing or hits an unexpected code path, check `logs/errors.log` for this warning — the missing skill is the likely cause. To fix: remove the nonexistent skill from the cron job's `skills` list, or create the skill.
|
||||
- Example: A `purchase-confirmation` workflow monitor cron job listed `feishu_doc` in skills but the skill was deleted. The job's `feishu_doc_read` tool call failed because the skill wasn't loaded, producing a partial/incomplete result with no error surfaced to the user.
|
||||
|
||||
## Collector Heartbeat Notification Format
|
||||
|
||||
**Automated heartbeats are sent by the orchestrator as Schema 2.0 cards** (see "Automated heartbeat" under Credential path). The LLM does not need to format or send them.
|
||||
|
||||
**Manual relay only** (operator asks in chat to forward a workflow report) — include all key fields:
|
||||
|
||||
```
|
||||
[采集端心跳] <工作流名称>
|
||||
|
||||
Workflow ID: <id>
|
||||
运行 ID: <run_id>
|
||||
状态: <status>
|
||||
当前阶段: <phase>
|
||||
采集尝试次数: <n>
|
||||
分析尝试次数: <n>
|
||||
执行结论: <conclusion>
|
||||
```
|
||||
|
||||
Manual relay pattern (for owner-only reports):
|
||||
- User asks: "notify me only, not business targets" → reply directly as your response text (API server delivers it).
|
||||
- The reply content IS the notification — no extra tools needed.
|
||||
- Include all key fields: workflow ID, run ID, phase, status, attempts, error details, collection summary.
|
||||
- Do NOT also call send_message / lark-cli / lark-cli / Feishu REST API — that would double-send.
|
||||
|
||||
Reference: `references/feishu-open-id-cross-app.md` — Feishu open_id cross-app failure and correct routing. Collector heartbeat target is `ou_8ee224968aa26a74c7d30ba27fed5eeb` via collector app (`cli_aa8c4fb4c4f81cd3`). `ou_7ad5fc8012e2f741afc5346e05ffd447` is the analyzer app owner — NOT the collector heartbeat target. Full REST call pattern included.
|
||||
Reference: `references/collector-notification-credentials.md` — Collector Feishu notification credential sources: direct-reply pattern vs REST API pattern, and the known bug where `send_collector_notify.py` reads the wrong app secret from `config.json`.
|
||||
Reference: `references/workflow-state-querying.md` — How to query collector workflow state. **Primary source: `shared-data/` artifacts + agent logs.** The orchestrator's `state.db` (LangChain checkpoints) only stores `replenishment-alert` runs — NOT collector runs. See the reference file for the full state querying strategy.
|
||||
|
||||
**Reusable monitoring script:** `scripts/monitor_purchase_confirmation.py` — drop-in script that queries collector session DB + shared-data, then sends Feishu admin notification. Run with `python scripts/monitor_purchase_confirmation.py <RUN_ID>`. Handles `orchestrator/state.db` ≠ collector workflow source correctly.
|
||||
|
||||
Final check:
|
||||
- Collection only.
|
||||
- No business-user final notify.
|
||||
- Shared-data contains only this run outputs.
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: purchase-confirmation-workflow
|
||||
description: 触发采购确认通知工作流 (purchase-confirmation)
|
||||
triggers:
|
||||
- manual: 用户要求"执行/触发 purchase-confirmation"
|
||||
- scheduled: 通过 cron 定时触发
|
||||
---
|
||||
|
||||
# Purchase Confirmation Workflow
|
||||
|
||||
Trigger the `purchase-confirmation` (采购确认通知工作流) from the `${GYXX_PROJECT_ROOT}` orchestrator.
|
||||
|
||||
## ⚠️ Important Execution Notes
|
||||
|
||||
- **执行时间长**:此工作流包含浏览器自动化(Chrome CDP),采集脚本执行时间约 **5-15 分钟**,请耐心等待,**不要**超时后随意清理锁和进程
|
||||
- **不要主动终止**:看到超时不要清理锁/杀进程,工作流可能仍在正常执行
|
||||
- **判断完成**:检查 `shared-data/purchase-confirmation/` 目录中是否有 CSV 文件(如 `采购确认通知汇总_YYYYMMDD_HHMMSS.csv`)
|
||||
|
||||
## Workflow Command
|
||||
|
||||
```bash
|
||||
cd ${GYXX_PROJECT_ROOT} && .venv/Scripts/python.exe run.py mcp-run purchase-confirmation
|
||||
```
|
||||
|
||||
工作流通过 Hermes MCP 网关执行,包含采集端和分析端两个阶段。
|
||||
|
||||
## 创建定时任务
|
||||
|
||||
```python
|
||||
cronjob(action='create',
|
||||
prompt='执行 purchase-confirmation 工作流。运行命令:cd ${GYXX_PROJECT_ROOT} && .venv/Scripts/python.exe run.py mcp-run purchase-confirmation',
|
||||
schedule='0 9 * * *', # 每天早上9点
|
||||
name='purchase-confirmation-daily')
|
||||
```
|
||||
|
||||
## 通知规则(关键)
|
||||
|
||||
⚠️ **采集端通知规则**:采集端 Hermes 只通知运营,不通知业务目标用户。
|
||||
- 采集端通知对象:运营(飞书 OpenID `ou_7ad5fc8012e2f741afc5346e05ffd447`)
|
||||
- 业务最终通知:由分析端 Hermes 负责
|
||||
- 通知方式:通过当前会话直接响应(不调用 send_message/yuanbao 派)
|
||||
|
||||
当用户请求"只通知我(不要通知业务目标用户)"时:
|
||||
1. 直接在当前会话中以文本形式输出报告
|
||||
2. 说明通知范围:仅运营,业务目标用户由分析端 Hermes 负责
|
||||
|
||||
## 工作流配置参考
|
||||
|
||||
- **工作流名称**:purchase-confirmation
|
||||
- **显示名称**:采购确认通知工作流
|
||||
- **采集脚本**:`${GYXX_PROJECT_ROOT}/orchestrator/scripts/collect_confirmation.ps1`
|
||||
- **输出文件**:`采购确认通知汇总_*.csv`(xlsx 格式)
|
||||
- **通知目标**(业务用户):
|
||||
- `ou_7ad5fc8012e2f741afc5346e05ffd447`
|
||||
- `ou_339c1d396b397c97b08e1bf377963d40`
|
||||
- `ou_54141d75b24d22ddc8c1c4d66e00b6e9`
|
||||
- **采集端绑定应用**:`cli_aa8c4fb4c4f81cd3`
|
||||
- **分析端绑定应用**:`cli_aa8c4fc918b85cce`
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
---
|
||||
name: purchase-order-update-workflow
|
||||
description: 触发采购单日期更新工作流 (purchase-order-update)
|
||||
category: workflow
|
||||
triggers:
|
||||
- manual: 用户要求"执行/触发 purchase-order-update"
|
||||
- scheduled: 通过 cron 定时触发
|
||||
---
|
||||
|
||||
# Purchase Order Update Workflow
|
||||
|
||||
Trigger the `purchase-order-update`(采购单日期更新工作流)from the `${GYXX_PROJECT_ROOT}` orchestrator.
|
||||
|
||||
## Workflow Command
|
||||
|
||||
```bash
|
||||
cd ${GYXX_PROJECT_ROOT} && .venv/Scripts/python.exe run.py mcp-run purchase-order-update
|
||||
```
|
||||
|
||||
## 工作流配置(来源 bytecode 反向提取)
|
||||
|
||||
| 字段 | 值 |
|
||||
|------|-----|
|
||||
| **工作流名称** | purchase-order-update |
|
||||
| **显示名称** | 采购单日期更新工作流 |
|
||||
| **采集脚本** | `collect_purchase_order_update.ps1` |
|
||||
| **输出文件** | `update_result.json` |
|
||||
| **分析端通知方式** | `plugin:feishu.im.send_message` |
|
||||
| **采集端绑定应用** | `cli_aa8c4fb4c4f81cd3` |
|
||||
| **分析端绑定应用** | `cli_aa8c4fc918b85cce` |
|
||||
|
||||
## ⚠️ config.py 源文件已丢失
|
||||
|
||||
此工作流配置从 bytecode 反向提取,如需修改通知目标,需先重建 `config.py`。
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: replenishment-alert-workflow
|
||||
description: 触发库存预警通知工作流 (replenishment-alert)
|
||||
triggers:
|
||||
- manual: 用户要求"执行/触发 replenishment-alert"
|
||||
- scheduled: 通过 cron 定时触发
|
||||
---
|
||||
|
||||
# Replenishment Alert Workflow
|
||||
|
||||
Trigger the `replenishment-alert` (库存预警通知工作流) from the `${GYXX_PROJECT_ROOT}` orchestrator.
|
||||
|
||||
## Trigger Conditions
|
||||
|
||||
- **手动触发**:用户明确要求"执行/触发 replenishment-alert"
|
||||
- **定时触发**:通过 cron 任务定期运行
|
||||
|
||||
## Workflow Command
|
||||
|
||||
```bash
|
||||
cd ${GYXX_PROJECT_ROOT} && .venv/Scripts/python.exe run.py mcp-run replenishment-alert
|
||||
```
|
||||
|
||||
工作流通过 Hermes MCP 网关执行,包含采集端和分析端两个阶段。
|
||||
|
||||
## 已知问题
|
||||
|
||||
⚠️ **通知重复问题**:如需修复,从 `config.py` 的 `notification_targets.threshold_alert` 中移除 analyzer owner。但注意:`config.py` 源文件已丢失(仅 pyc 缓存),修改前需先重建配置。
|
||||
|
||||
⚠️ **config.py 源文件已丢失**:仅存 `__pycache__/config.cpython-311.pyc`,如需修改通知目标,需从 bytecode 反向提取常量后再重建文件。
|
||||
|
||||
## 工作流配置参考
|
||||
|
||||
- **工作流名称**:replenishment-alert
|
||||
- **显示名称**:库存预警通知工作流
|
||||
- **采集脚本**:`${GYXX_PROJECT_ROOT}/orchestrator/scripts/collect_replenishment.ps1`
|
||||
- **输出文件**:`threshold_alert_data.json`
|
||||
- **通知目标**(业务用户,仅 `threshold_alert`,来源 bytecode const[53]):
|
||||
- `ou_b76e4cbdb24fe28cebd45ad091b60224`(黄坤平)
|
||||
- **采集端绑定应用**:`cli_aa8c4fb4c4f81cd3`
|
||||
- **分析端绑定应用**:`cli_aa8c4fc918b85cce`
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
name: replenishment-workflow
|
||||
description: 触发补货建议工作流 (replenishment)
|
||||
triggers:
|
||||
- manual: 用户要求"执行/触发 replenishment"
|
||||
- scheduled: 通过 cron 定时触发
|
||||
---
|
||||
|
||||
# Replenishment Workflow
|
||||
|
||||
Trigger the `replenishment` (补货建议工作流) from the `${GYXX_PROJECT_ROOT}` orchestrator.
|
||||
|
||||
## ⚠️ Important Execution Notes
|
||||
|
||||
- **执行时间长**:此工作流包含浏览器自动化(Chrome CDP),采集脚本执行时间约 **5-15 分钟**,请耐心等待
|
||||
- **判断完成**:检查 `shared-data/replenishment/` 目录中是否有 `pending_insert.json` 和 `notify_data.json`
|
||||
- **后台执行**:建议使用 `background=true` + `notify_on_complete=true` 执行,避免超时
|
||||
|
||||
## Workflow Command
|
||||
|
||||
```bash
|
||||
cd ${GYXX_PROJECT_ROOT} && .venv/Scripts/python.exe run.py mcp-run replenishment
|
||||
```
|
||||
|
||||
## 创建定时任务
|
||||
|
||||
```python
|
||||
cronjob(action='create',
|
||||
prompt='执行 replenishment 工作流。运行命令:cd ${GYXX_PROJECT_ROOT} && .venv/Scripts/python.exe run.py mcp-run replenishment',
|
||||
schedule='0 9 * * *', # 每天早上9点
|
||||
name='replenishment-daily')
|
||||
```
|
||||
|
||||
## 通知目标(分析端)
|
||||
|
||||
仅通知运营(`ou_7ad5fc8012e2f741afc5346e05ffd447`)。不通知黄坤平,不通知审单群。
|
||||
|
||||
## 工作流配置参考
|
||||
|
||||
- **工作流名称**:replenishment
|
||||
- **显示名称**:补货建议工作流
|
||||
- **采集脚本**:`${GYXX_PROJECT_ROOT}/orchestrator/scripts/collect_replenishment.ps1`
|
||||
- **输出文件**:`pending_insert.json`, `notify_data.json`
|
||||
- **多维表**:应用 `cli_aa8c4fc918b85cce` 需对多维表 `Th0jbMHHQa7a8Lse3CicfSMBnxb` 有写入权限
|
||||
- **采集端绑定应用**:`cli_aa8c4fb4c4f81cd3`
|
||||
- **分析端绑定应用**:`cli_aa8c4fc918b85cce`
|
||||
|
||||
## 已知问题
|
||||
|
||||
⚠️ **多维表写入权限**:如遇以下错误,需在飞书开放平台为应用 `cli_aa8c4fc918b85cce` 开启多维表写入权限:
|
||||
- `91403 Forbidden` — 无权限访问多维表
|
||||
- `99992402 field validation failed` — 字段验证失败(常见于 app 无写入权限或字段类型不匹配)
|
||||
|
||||
写入失败**不影响通知发送**(符合 HARD RULES),但待处理数据需手动补充。
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
name: workflow-trigger-only
|
||||
description: 工作流触发原则:只执行既有命令,不自主生成代码修改流程
|
||||
triggers:
|
||||
- manual: 用户要求将某个操作原则沉淀为 skill 时创建
|
||||
---
|
||||
|
||||
# Workflow Trigger Only
|
||||
|
||||
## 核心原则
|
||||
|
||||
**只触发既有工作流,不自主生成或修改流程代码。**
|
||||
|
||||
## 执行方式
|
||||
|
||||
```
|
||||
cd ${GYXX_PROJECT_ROOT} && .venv/Scripts/python.exe run.py mcp-run <工作流名>
|
||||
```
|
||||
|
||||
支持的 `<工作流名>`:
|
||||
- `replenishment-alert` — 库存预警通知
|
||||
- `purchase-confirmation` — 采购确认通知
|
||||
- `replenishment` — 补货建议
|
||||
|
||||
## 规则
|
||||
|
||||
1. **只运行既有命令** — 使用 `run.py mcp-run` 触发,不写 Python 脚本绕过后续流程
|
||||
2. **不自主修改流程代码** — 不修改 `mcp_workflow.py`、通知脚本、采集脚本等,除非用户明确要求
|
||||
3. **不自主生成新代码** — 不创建新脚本自行完成通知/写表等操作
|
||||
4. **编排逻辑由既有代码决定** — 采集→分析→通知的分段流程和通知对象由 `mcp_workflow.py` 和分析端 Hermes 控制
|
||||
|
||||
## 例外:手动转发心跳/执行报告
|
||||
|
||||
"只触发既有命令"规则**仅适用于 orchestrator 工作流执行**。当用户要求将某个工作流的心跳/执行报告**手动转发**给运营时:
|
||||
|
||||
1. 构造心跳消息内容(包含工作流 ID、运行 ID、状态、错误信息等)
|
||||
2. 通过飞书采集端应用(`cli_aa8c4fb4c4f81cd3`)将报告以 **Feishu DM** 形式发送给运营(`ou_8ee224968aa26a74c7d30ba27fed5eeb`)
|
||||
3. **不**通知业务目标用户,不走 yuanbao group
|
||||
4. 使用 `feishu` skill 中的 temp 脚本模式发送(写临时脚本 → terminal 运行 → 删除),或直接 reply 让 gateway 转发
|
||||
|
||||
> 注意:**自动心跳已由 `orchestrator/mcp_workflow.py` 用 Schema 2.0 interactive card 发送**(target 同样是 `ou_8ee224968aa26a74c7d30ba27fed5eeb`),本例外仅适用于**人工额外转发**场景(比如重发、转发给第三方等),不要重复发送已自动发出的心跳。
|
||||
|
||||
这是人工操作员在对话中转发报告的合法场景,与 orchestrator 自动执行工作流是两回事。
|
||||
|
||||
## 触发命令参考
|
||||
|
||||
| 工作流 | 命令 |
|
||||
|--------|------|
|
||||
| 库存预警通知 | `cd ${GYXX_PROJECT_ROOT} && .venv/Scripts/python.exe run.py mcp-run replenishment-alert` |
|
||||
| 采购确认通知 | `cd ${GYXX_PROJECT_ROOT} && .venv/Scripts/python.exe run.py mcp-run purchase-confirmation` |
|
||||
| 补货建议 | `cd ${GYXX_PROJECT_ROOT} && .venv/Scripts/python.exe run.py mcp-run replenishment` |
|
||||
|
||||
## 为什么这样做
|
||||
|
||||
- 工作流的编排逻辑(采集、分析、通知分段执行)由 `mcp_workflow.py` 控制
|
||||
- 通知对象由 `config.py` 和分析端 prompt 决定,不应在触发层硬编码
|
||||
- 自主生成代码容易出错且难以追踪
|
||||
@@ -0,0 +1,6 @@
|
||||
@echo off
|
||||
setlocal
|
||||
if defined GYXX_PROJECT_ROOT (set "PROJECT_ROOT=%GYXX_PROJECT_ROOT%") else (set "PROJECT_ROOT=%~dp0\..\..\..\..\..\..\..")
|
||||
for %%I in ("%PROJECT_ROOT%") do set "PROJECT_ROOT=%%~fI"
|
||||
"%PROJECT_ROOT%\.venv\Scripts\python.exe" -m gyxx_flow.cli run supply.purchase_confirmation.daily
|
||||
endlocal & exit /b %ERRORLEVEL%
|
||||
@@ -0,0 +1,6 @@
|
||||
@echo off
|
||||
setlocal
|
||||
if defined GYXX_PROJECT_ROOT (set "PROJECT_ROOT=%GYXX_PROJECT_ROOT%") else (set "PROJECT_ROOT=%~dp0\..\..\..\..\..\..\..")
|
||||
for %%I in ("%PROJECT_ROOT%") do set "PROJECT_ROOT=%%~fI"
|
||||
"%PROJECT_ROOT%\.venv\Scripts\python.exe" -m gyxx_flow.cli run supply.replenishment_alert.daily
|
||||
endlocal & exit /b %ERRORLEVEL%
|
||||
@@ -0,0 +1,6 @@
|
||||
@echo off
|
||||
setlocal
|
||||
if defined GYXX_PROJECT_ROOT (set "PROJECT_ROOT=%GYXX_PROJECT_ROOT%") else (set "PROJECT_ROOT=%~dp0\..\..\..\..\..\..\..")
|
||||
for %%I in ("%PROJECT_ROOT%") do set "PROJECT_ROOT=%%~fI"
|
||||
"%PROJECT_ROOT%\.venv\Scripts\python.exe" -m gyxx_flow.cli run supply.replenishment.weekly
|
||||
endlocal & exit /b %ERRORLEVEL%
|
||||
Reference in New Issue
Block a user