feat: complete production workflow migration

This commit is contained in:
2026-08-06 14:29:57 +08:00
parent 7f215e79c4
commit 8df5266abb
448 changed files with 56937 additions and 14619 deletions
@@ -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 ownerbot 身份在 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 明天到货` | ✅(明天自动换算) |
| `104650075月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.
@@ -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`
@@ -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`
@@ -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%