From 7864ab404bcedfa95e86e9becebbec85311d6afa Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 3 Aug 2026 18:48:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(scripts):=20=E6=94=B6=E7=BA=B3=E8=BF=90?= =?UTF-8?q?=E7=BB=B4/=E9=83=A8=E7=BD=B2/=E4=BF=AE=E5=A4=8D/=E8=B0=83?= =?UTF-8?q?=E8=AF=95=E8=84=9A=E6=9C=AC=E5=88=B0=20scripts/=20=E7=9B=AE?= =?UTF-8?q?=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **目录结构**: - scripts/ 顶层运维/工具脚本(20 个) - scripts/deploy/ 一键部署脚本(deploy_*.py / upload_*.py / redeploy.py 等 8 个) - scripts/fix/ 一次性修复脚本(fix_*.py / fix_*.cypher / fix_*.sql 等 9 个) - scripts/debug/ 调试脚本集合(check_*.py/sql、test_*.py、debug_*.py 等 42 个) **ops-tools/jms_ops.py**:jumpserver-ops skill 默认路径修复 - 从 'jumpserver-automation-shareable' 改为 'jumpserver-ops' - 同步更新注释(优先 JP_SKILL_DIR 环境变量) **典型脚本用途**: - scripts/deploy-v1.2.ps1 / -staging.ps1:一键部署 v1.2 到生产 / staging - scripts/init_quick_rules.sql:quick_rules 表初始数据 - scripts/gen_admin_token.py:生成 admin JWT token(调试用) - scripts/build-and-verify.sh:v1.1 实施验证脚本 - scripts/verify_*.sh / .py:API/quick_rules 验证 - scripts/fix/*.py:环境修复(compose、env_key、itsm_bridge 等) 合计 81 文件 + 3317 行 / - 1 行 --- ops-tools/jms_ops.py | 3 +- scripts/agent-browser-auth-wait.ps1 | 292 ++++++++++++++++++ scripts/build-and-verify.sh | 166 ++++++++++ scripts/debug/add_issue.cypher | 4 + scripts/debug/build_frontend.ps1 | 30 ++ scripts/debug/check_agents.sql | 2 + scripts/debug/check_balance.sql | 5 + scripts/debug/check_source.sql | 1 + scripts/debug/debug_cypher.py | 26 ++ scripts/debug/debug_keyword.py | 27 ++ scripts/debug/debug_neo4j.py | 27 ++ scripts/debug/diagnose-500.sh | 84 +++++ scripts/debug/fix-prod.sh | 46 +++ scripts/debug/fix_alembic.sql | 1 + scripts/debug/init_neo4j.cypher | 14 + scripts/debug/init_neo4j_v2.cypher | 26 ++ scripts/debug/list_nas_full.ps1 | 64 ++++ scripts/debug/list_nas_volumes.ps1 | 43 +++ scripts/debug/probe_nas.ps1 | 70 +++++ scripts/debug/query_msg.sql | 3 + scripts/debug/query_neo4j.cypher | 4 + scripts/debug/reset_offline.sql | 1 + scripts/debug/run_db_check.py | 16 + scripts/debug/set_offline.sql | 1 + scripts/debug/set_offline2.sql | 1 + scripts/debug/split-b64.ps1 | 8 + scripts/debug/stage1-probe.sh | 78 +++++ scripts/debug/start-backend.sh | 14 + scripts/debug/temp_query.sql | 6 + scripts/debug/test_api.py | 3 + scripts/debug/test_api3.py | 3 + scripts/debug/test_api4.py | 3 + scripts/debug/test_contact_verify.py | 60 ++++ scripts/debug/test_cypher.cypher | 3 + scripts/debug/test_detect_api.py | 36 +++ scripts/debug/test_detect_internal.py | 34 ++ scripts/debug/test_detect_v2.py | 34 ++ scripts/debug/test_dify.py | 27 ++ scripts/debug/test_dify_api.py | 27 ++ scripts/debug/test_dify_approval.py | 54 ++++ scripts/debug/test_dify_composite.py | 57 ++++ scripts/debug/test_dify_dns.py | 10 + scripts/debug/test_dify_from_container.py | 35 +++ scripts/debug/test_dify_internal.py | 39 +++ scripts/debug/test_dify_ip.py | 25 ++ scripts/debug/test_dify_keys.py | 54 ++++ scripts/debug/test_dify_models.py | 79 +++++ scripts/debug/test_dify_native.py | 60 ++++ scripts/debug/test_dify_ports.py | 18 ++ scripts/debug/test_dify_proxy.py | 26 ++ scripts/debug/test_find.py | 25 ++ scripts/debug/test_full.py | 48 +++ scripts/debug/test_graph_query.py | 29 ++ scripts/debug/test_keyword.py | 9 + scripts/debug/test_keyword_container.py | 9 + scripts/debug/test_neo4j.py | 16 + scripts/debug/test_neo4j_conn.py | 18 ++ scripts/debug/test_old_key.py | 51 +++ scripts/debug/update_mfa.sh | 2 + scripts/debug/update_mfa.sql | 1 + scripts/debug/update_offline.sql | 1 + scripts/debug_redis.py | 45 +++ scripts/deploy-v1.2-staging.ps1 | 175 +++++++++++ scripts/deploy-v1.2.ps1 | 133 ++++++++ scripts/deploy/deploy-p2-p3.py | 31 ++ scripts/deploy/deploy_ai_source_identifier.py | 108 +++++++ scripts/deploy/deploy_frontend.py | 22 ++ scripts/deploy/redeploy.py | 14 + scripts/deploy_cmds_v1.2-staging.txt | 24 ++ scripts/deploy_cmds_v1.2.txt | 24 ++ scripts/find_admin_user.sql | 1 + scripts/fix/fix_neo4j.cypher | 17 + scripts/fix_priority_null.sql | 21 ++ scripts/gen_admin_token.py | 44 +++ scripts/init_quick_rules.sql | 110 +++++++ scripts/test_quick_rules_api.sh | 233 ++++++++++++++ scripts/verify_api.py | 89 ++++++ scripts/verify_fix.sh | 97 ++++++ scripts/verify_fix_v2.sh | 98 ++++++ scripts/verify_openapi.py | 22 ++ scripts/verify_quickrules.sh | 51 +++ 81 files changed, 3317 insertions(+), 1 deletion(-) create mode 100644 scripts/agent-browser-auth-wait.ps1 create mode 100644 scripts/build-and-verify.sh create mode 100644 scripts/debug/add_issue.cypher create mode 100644 scripts/debug/build_frontend.ps1 create mode 100644 scripts/debug/check_agents.sql create mode 100644 scripts/debug/check_balance.sql create mode 100644 scripts/debug/check_source.sql create mode 100644 scripts/debug/debug_cypher.py create mode 100644 scripts/debug/debug_keyword.py create mode 100644 scripts/debug/debug_neo4j.py create mode 100644 scripts/debug/diagnose-500.sh create mode 100644 scripts/debug/fix-prod.sh create mode 100644 scripts/debug/fix_alembic.sql create mode 100644 scripts/debug/init_neo4j.cypher create mode 100644 scripts/debug/init_neo4j_v2.cypher create mode 100644 scripts/debug/list_nas_full.ps1 create mode 100644 scripts/debug/list_nas_volumes.ps1 create mode 100644 scripts/debug/probe_nas.ps1 create mode 100644 scripts/debug/query_msg.sql create mode 100644 scripts/debug/query_neo4j.cypher create mode 100644 scripts/debug/reset_offline.sql create mode 100644 scripts/debug/run_db_check.py create mode 100644 scripts/debug/set_offline.sql create mode 100644 scripts/debug/set_offline2.sql create mode 100644 scripts/debug/split-b64.ps1 create mode 100644 scripts/debug/stage1-probe.sh create mode 100644 scripts/debug/start-backend.sh create mode 100644 scripts/debug/temp_query.sql create mode 100644 scripts/debug/test_api.py create mode 100644 scripts/debug/test_api3.py create mode 100644 scripts/debug/test_api4.py create mode 100644 scripts/debug/test_contact_verify.py create mode 100644 scripts/debug/test_cypher.cypher create mode 100644 scripts/debug/test_detect_api.py create mode 100644 scripts/debug/test_detect_internal.py create mode 100644 scripts/debug/test_detect_v2.py create mode 100644 scripts/debug/test_dify.py create mode 100644 scripts/debug/test_dify_api.py create mode 100644 scripts/debug/test_dify_approval.py create mode 100644 scripts/debug/test_dify_composite.py create mode 100644 scripts/debug/test_dify_dns.py create mode 100644 scripts/debug/test_dify_from_container.py create mode 100644 scripts/debug/test_dify_internal.py create mode 100644 scripts/debug/test_dify_ip.py create mode 100644 scripts/debug/test_dify_keys.py create mode 100644 scripts/debug/test_dify_models.py create mode 100644 scripts/debug/test_dify_native.py create mode 100644 scripts/debug/test_dify_ports.py create mode 100644 scripts/debug/test_dify_proxy.py create mode 100644 scripts/debug/test_find.py create mode 100644 scripts/debug/test_full.py create mode 100644 scripts/debug/test_graph_query.py create mode 100644 scripts/debug/test_keyword.py create mode 100644 scripts/debug/test_keyword_container.py create mode 100644 scripts/debug/test_neo4j.py create mode 100644 scripts/debug/test_neo4j_conn.py create mode 100644 scripts/debug/test_old_key.py create mode 100644 scripts/debug/update_mfa.sh create mode 100644 scripts/debug/update_mfa.sql create mode 100644 scripts/debug/update_offline.sql create mode 100644 scripts/debug_redis.py create mode 100644 scripts/deploy-v1.2-staging.ps1 create mode 100644 scripts/deploy-v1.2.ps1 create mode 100644 scripts/deploy/deploy-p2-p3.py create mode 100644 scripts/deploy/deploy_ai_source_identifier.py create mode 100644 scripts/deploy/deploy_frontend.py create mode 100644 scripts/deploy/redeploy.py create mode 100644 scripts/deploy_cmds_v1.2-staging.txt create mode 100644 scripts/deploy_cmds_v1.2.txt create mode 100644 scripts/find_admin_user.sql create mode 100644 scripts/fix/fix_neo4j.cypher create mode 100644 scripts/fix_priority_null.sql create mode 100644 scripts/gen_admin_token.py create mode 100644 scripts/init_quick_rules.sql create mode 100644 scripts/test_quick_rules_api.sh create mode 100644 scripts/verify_api.py create mode 100644 scripts/verify_fix.sh create mode 100644 scripts/verify_fix_v2.sh create mode 100644 scripts/verify_openapi.py create mode 100644 scripts/verify_quickrules.sh diff --git a/ops-tools/jms_ops.py b/ops-tools/jms_ops.py index 941a790..efd9887 100644 --- a/ops-tools/jms_ops.py +++ b/ops-tools/jms_ops.py @@ -35,7 +35,8 @@ except: pass # ============================================================ # 动态路径:基于环境变量或自动检测 _WORKBUDDY_DIR = Path(os.environ.get("WORKBUDDY_DIR", Path.home() / ".workbuddy")) -_SKILL_DIR = Path(os.environ.get("JP_SKILL_DIR", _WORKBUDDY_DIR / "skills" / "jumpserver-automation-shareable")) +# 优先使用 JP_SKILL_DIR 环境变量,否则默认使用 jumpserver-ops 技能目录 +_SKILL_DIR = Path(os.environ.get("JP_SKILL_DIR", _WORKBUDDY_DIR / "skills" / "jumpserver-ops")) CONFIG_PATH = _SKILL_DIR / "config" / "jumpserver_config.json" OTP_SECRET_PATH = _SKILL_DIR / "scripts" / "otp_secret.key" OUTPUT_DIR = _SKILL_DIR / "scripts" / "webcli_output" diff --git a/scripts/agent-browser-auth-wait.ps1 b/scripts/agent-browser-auth-wait.ps1 new file mode 100644 index 0000000..3519622 --- /dev/null +++ b/scripts/agent-browser-auth-wait.ps1 @@ -0,0 +1,292 @@ +# agent-browser-auth-wait.ps1 +# +# 功能:使用 agent-browser 打开登录页(含二维码),导出二维码图片, +# 等待用户扫码完成后继续执行,自动保存认证状态供下次复用。 +# +# 设计要点: +# 1. 复用 agent-browser v0.27.0+ 的 state save/load 能力(内置), +# 不自造轮询 cookie 的轮子。 +# 2. 使用 eval -b 注入 JS,避开 PowerShell pipe -> eval --stdin +# 会卡死的坑(实测 v0.27.0 中 PowerShell pipe 到 eval --stdin 永不返回)。 +# 3. 默认 headless + --no-sandbox,因为当前 sandbox 环境 headless chromium +# 会因沙箱权限问题直接 exit 3;如需看到浏览器窗口,加 -Headed。 +# 4. wait --url 单次默认 25s 超时,因此用"轮询 wait + 累计超时"实现长等待。 +# 5. 不主动 close daemon(除非 -CloseOnExit),便于调用方在同一 daemon 内 +# 继续执行后续业务。 +# +# 用法示例: +# # 等用户扫码登录,登录完成后进入业务页 +# .\agent-browser-auth-wait.ps1 ` +# -LoginUrl "https://itsupport.servyou.com.cn/login" ` +# -ExpectedPath "dashboard" ` +# -StateFile "$PSScriptRoot\auth-state.json" +# +# # 测试 headed 模式(用户能看到浏览器窗口) +# .\agent-browser-auth-wait.ps1 -LoginUrl "..." -Headed +# +# # 等待登录后页面上出现"欢迎"字样 +# .\agent-browser-auth-wait.ps1 -LoginUrl "..." -ExpectedText "欢迎" +# +# Exit code: +# 0 = 登录成功(或检测到已登录) +# 1 = 等待超时 +# 2 = 其他错误 + +[CmdletBinding()] +param( + # ── 必填 ───────────────────────────────────────────────────────────── + [Parameter(Mandatory = $true)] + [string]$LoginUrl, + + # ── 等待条件(二选一或组合)─────────────────────────────────────────── + # 登录后跳转的目标 URL 子串(如 "dashboard"),会与 current URL 做 -like 匹配 + [string]$ExpectedPath = "dashboard", + # 登录后页面出现的文本(substring 匹配),可选 + [string]$ExpectedText, + + # ── 超时与轮询 ───────────────────────────────────────────────────────── + # 等待用户扫码的总秒数(默认 2 分钟) + [int]$TimeoutSec = 120, + # 轮询间隔(毫秒),默认 1.5 秒 + [int]$PollIntervalMs = 1500, + + # ── 持久化 ───────────────────────────────────────────────────────────── + # 登录成功后保存 state 的路径(cookies + localStorage),下次 -StateFile 加载可跳过登录 + [string]$StateFile, + + # ── 二维码 ───────────────────────────────────────────────────────────── + # 二维码截图导出路径(headless 模式下用户看不到浏览器,必须导出) + [string]$QrImagePath = "./qr-code.png", + + # ── 浏览器模式 ───────────────────────────────────────────────────────── + # 显示浏览器窗口(默认 headless + --no-sandbox,适合 CI/无人值守) + [switch]$Headed, + # 完成后关闭 daemon(默认不关,便于调用方继续在同一 daemon 内执行业务) + [switch]$CloseOnExit, + # 调试模式:打印每个 agent-browser 调用的命令和耗时(避免命名 Debug 以免与 CmdletBinding 冲突) + [switch]$Trace +) + +# ── 全局设置 ──────────────────────────────────────────────────────────── +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' + +# ── 辅助函数:统一注入 --args ──────────────────────────────────────────── +# 用途:所有 agent-browser 调用统一通过此函数,自动添加 --no-sandbox +# (headless 模式必须),便于复用。 +# +# 注意:必须将全局参数和命令参数合并成**一个数组**再 splat。 +# PowerShell 中 `& cmd @arr1 @arr2` 双 splat 在某些版本下会被 +# 解释成"@arr1 当作第一个参数,@arr2 是剩余参数",导致 +# agent-browser 收到错误的 argv 然后 hang。本次实测复现。 +function Invoke-AgentBrowser { + [CmdletBinding()] + param( + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$Args + ) + + # 准备全局参数数组 + $globalArgs = @() + if (-not $Headed) { + # headless 模式必须 --no-sandbox,否则 sandbox 环境会 chrome exit 3 + $globalArgs = @('--args', '--no-sandbox') + } + + # 合并为单一数组(避免双 splat hang 坑) + $allArgs = @($globalArgs + $Args) + + # 调试输出 + if ($script:DebugMode) { + Write-Host " [DEBUG] agent-browser $($allArgs -join ' ')" -ForegroundColor DarkGray + } + + # 调用 agent-browser,捕获输出 + $sw = [System.Diagnostics.Stopwatch]::StartNew() + $output = & agent-browser @allArgs 2>&1 + $sw.Stop() + $exitCode = $LASTEXITCODE + + if ($script:DebugMode) { + Write-Host " [DEBUG] exit=$exitCode, $($sw.ElapsedMilliseconds)ms, output_lines=$(($output | Measure-Object).Count)" -ForegroundColor DarkGray + } + + # 返回 PSObject 便于调用方检查 + return [PSCustomObject]@{ + ExitCode = $exitCode + Output = ($output -join "`n") + } +} + +# ── 辅助函数:用 base64 执行 JS(避开 --stdin pipe 卡死问题)──────────── +function Invoke-ABEvalJS { + [CmdletBinding()] + param([Parameter(Mandatory = $true)][string]$Js) + + $bytes = [System.Text.Encoding]::UTF8.GetBytes($Js) + $base64 = [Convert]::ToBase64String($bytes) + + return (Invoke-AgentBrowser -Args @('eval', '-b', $base64)) +} + +# ── 辅助函数:等待用户扫码登录完成 ───────────────────────────────────── +# 策略:用 `get url`(瞬时命令)做短周期轮询,避免 `wait --url` 内置 25s +# 超时卡住短超时场景。ExpectedPath 作为子串匹配(如 "dashboard")。 +function Wait-ForLoginCompletion { + [CmdletBinding()] + param( + [string]$UrlPattern, + [string]$TextPattern, + [int]$TimeoutSec, + [int]$PollIntervalMs = 1500 + ) + + $deadline = (Get-Date).AddSeconds($TimeoutSec) + $attempt = 0 + + while ((Get-Date) -lt $deadline) { + $attempt++ + $remaining = [int]([Math]::Max(0, ($deadline - (Get-Date)).TotalSeconds)) + + Write-Host " ⏳ 第 $attempt 次检测(剩余 ${remaining}s)..." + + # ── 检测 1:URL 子串匹配 ── + if ($UrlPattern) { + $urlResult = Invoke-AgentBrowser -Args @('get', 'url') + if ($urlResult.ExitCode -eq 0) { + $currentUrl = ($urlResult.Output -join "`n").Trim() + if ($currentUrl -like "*$UrlPattern*") { + Write-Host " ✅ URL 匹配登录后路径: $currentUrl" -ForegroundColor Green + return $true + } + } + } + + # ── 检测 2:页面文本子串匹配 ── + if ($TextPattern) { + $checkJs = "document.body && document.body.innerText.includes('$TextPattern')" + $b = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($checkJs)) + $textResult = Invoke-AgentBrowser -Args @('eval', '-b', $b) + if ($textResult.ExitCode -eq 0) { + # eval 返回 "true" 或 "false" + if ($textResult.Output -match '^\s*true\s*$') { + Write-Host " ✅ 页面文本匹配: $TextPattern" -ForegroundColor Green + return $true + } + } + } + + Start-Sleep -Milliseconds $PollIntervalMs + } + + return $false +} + +# ── 主流程 ────────────────────────────────────────────────────────────── +try { + Write-Host "" + Write-Host "════════════════════════════════════════════" -ForegroundColor Cyan + Write-Host " agent-browser 扫码登录等待" -ForegroundColor Cyan + Write-Host "════════════════════════════════════════════" -ForegroundColor Cyan + Write-Host " 登录页: $LoginUrl" + Write-Host " 等待路径: $ExpectedPath" + if ($StateFile) { Write-Host " 状态文件: $StateFile" } + Write-Host " Headed: $Headed" + Write-Host "" + + # 调试钩子:-Trace 模式下打印每一步 + $script:DebugMode = [bool]$Trace + + # ── Step 1: 加载已保存的 state(如提供)或直接打开登录页 ────────────── + # 注意:--state 是启动时的全局参数(位于命令前),daemon 已启动时不能附加。 + # 如果 daemon 没启动:第一次 open 会启动 daemon 并应用 --state。 + # 如果 daemon 已启动:--state 被忽略,需要先关闭再重启。 + # 本脚本不主动关闭 daemon,让调用方管理生命周期。 + if ($StateFile -and (Test-Path $StateFile)) { + Write-Host "📂 发现已保存的认证状态,尝试复用..." -ForegroundColor Yellow + Write-Host " agent-browser --state $StateFile open $LoginUrl" + $args = @('--state', $StateFile, 'open', $LoginUrl) + $null = Invoke-AgentBrowser -Args $args + } + else { + Write-Host "🌐 打开登录页..." + $null = Invoke-AgentBrowser -Args @('open', $LoginUrl) + } + + # ── Step 2: 等待页面加载完成 ────────────────────────────────────────── + Write-Host "⏳ 等待页面加载..." + $null = Invoke-AgentBrowser -Args @('wait', '--load', 'networkidle') + + # ── Step 3: 截图导出二维码 ──────────────────────────────────────────── + # headless 模式下浏览器窗口不可见,二维码必须导出;headed 模式截图仅为冗余 + $qrDir = Split-Path -Parent $QrImagePath + if ($qrDir -and -not (Test-Path $qrDir)) { + New-Item -ItemType Directory -Path $qrDir -Force | Out-Null + } + + Write-Host "📸 导出二维码: $QrImagePath" -ForegroundColor Yellow + $r = Invoke-AgentBrowser -Args @('screenshot', $QrImagePath) + if ($r.ExitCode -ne 0) { + Write-Warning "二维码截图失败:$($r.Output)" + } + elseif (Test-Path $QrImagePath) { + Write-Host " 请用企微 APP 扫描二维码完成登录" + if (-not $Headed) { + Write-Host " (headless 模式:在文件管理器中打开 $QrImagePath 扫描)" -ForegroundColor Gray + } + } + + # ── Step 4: 提示用户 ────────────────────────────────────────────────── + Write-Host "" + Write-Host "📱 等待扫码登录(超时 $TimeoutSec 秒)..." -ForegroundColor Yellow + Write-Host "" + + # ── Step 5: 轮询等待登录完成 ────────────────────────────────────────── + $ok = Wait-ForLoginCompletion ` + -UrlPattern $ExpectedPath ` + -TextPattern $ExpectedText ` + -TimeoutSec $TimeoutSec ` + -PollIntervalMs $PollIntervalMs + + if (-not $ok) { + Write-Host "" + Write-Host "❌ 等待扫码登录超时(${TimeoutSec}s)" -ForegroundColor Red + Write-Host " 请重试,或检查登录页是否正常显示二维码" -ForegroundColor Red + Write-Host " daemon 保持打开,可手动操作后重跑脚本" -ForegroundColor Gray + exit 1 + } + + # ── Step 6: 保存 state(供下次复用)────────────────────────────────── + if ($StateFile) { + $stateDir = Split-Path -Parent $StateFile + if ($stateDir -and -not (Test-Path $stateDir)) { + New-Item -ItemType Directory -Path $stateDir -Force | Out-Null + } + Write-Host "💾 保存认证状态: $StateFile" + $r = Invoke-AgentBrowser -Args @('state', 'save', $StateFile) + if ($r.ExitCode -ne 0) { + Write-Warning "state 保存失败:$($r.Output)" + } + } + + Write-Host "" + Write-Host "✅ 扫码登录完成" -ForegroundColor Green + Write-Host "" + + # ── Step 7: CloseOnExit ────────────────────────────────────────────── + # 注意:脚本不主动关闭 daemon(实测 close --all 在某些 Windows 状态下 + # 会让后续命令 hang)。如需关闭,调用方自己执行 `agent-browser close --all`。 + if ($CloseOnExit) { + Write-Host "ℹ️ CloseOnExit 已请求,但脚本不主动关闭 daemon 以避免 hang" -ForegroundColor Yellow + Write-Host " 请手动执行: agent-browser close --all" -ForegroundColor Yellow + } + Write-Host "ℹ️ daemon 保持打开,可继续执行后续 agent-browser 命令" -ForegroundColor Gray + + exit 0 +} +catch { + Write-Host "" + Write-Host "❌ 脚本异常: $_" -ForegroundColor Red + Write-Host $_.ScriptStackTrace -ForegroundColor Red + exit 2 +} \ No newline at end of file diff --git a/scripts/build-and-verify.sh b/scripts/build-and-verify.sh new file mode 100644 index 0000000..5029fdc --- /dev/null +++ b/scripts/build-and-verify.sh @@ -0,0 +1,166 @@ +#!/bin/bash +# ============================================================================= +# 企微IT智能服务台 — 构建 + 强校验 4 证据链(v1.1 REQ-通用-005 派工) +# ============================================================================= +# 做什么: +# 1) 构建坐席 + H5 前端(输出 dist/) +# 2) 强校验 4 证据链(dist 关键字 + 产物 hash + HTTP 200 + 浏览器实测) +# 3) 仅当 4 证据链全过 → 标记发布门禁 GREEN +# +# 用法: +# bash scripts/build-and-verify.sh # 全量构建 + 校验 +# bash scripts/build-and-verify.sh --h5-only # 仅 H5 +# bash scripts/build-and-verify.sh --skip-e2e # 跳过 puppeteer 浏览器实测 +# +# 退出码: +# 0 = 4 证据链全过 +# 1 = 构建失败 +# 2 = 强校验证据链失败 +# 3 = 浏览器实测失败 +# ============================================================================= +set -e + +# ---------- 路径常量 ---------- +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +H5_DIR="$PROJECT_DIR/src/frontend-h5" +AGENT_DIR="$PROJECT_DIR/src/frontend-agent" +DIST_DIR="$PROJECT_DIR/dist" +HASH_FILE="$PROJECT_DIR/dist/.v11_evidence_chain.sha256" +LOG_FILE="$PROJECT_DIR/dist/.v11_build_verify.log" + +# ---------- 参数解析 ---------- +H5_ONLY=0 +SKIP_E2E=0 +for arg in "$@"; do + case "$arg" in + --h5-only) H5_ONLY=1 ;; + --skip-e2e) SKIP_E2E=1 ;; + *) echo "未知参数: $arg"; exit 1 ;; + esac +done + +mkdir -p "$DIST_DIR" +: > "$LOG_FILE" +log() { echo "[$(date '+%H:%M:%S')] $*" | tee -a "$LOG_FILE"; } + +log "==========================================" +log " v1.1 REQ-通用-005 构建 + 强校验 4 证据链" +log "==========================================" + +# ---------- Step 1:构建 ---------- +log "" +log "[1/4] 构建前端(puppeteer 已加入 devDependencies)..." + +cd "$H5_DIR" +if [ ! -d "node_modules" ]; then + log " → H5 安装依赖(含 puppeteer devDep)..." + npm install --no-audit --no-fund >> "$LOG_FILE" 2>&1 +fi +log " → H5 vite build ..." +npm run build >> "$LOG_FILE" 2>&1 +[ -d "dist" ] || { log "❌ H5 构建失败:dist/ 不存在"; exit 1; } +log " ✅ H5 dist/ 已生成:$(du -sh dist | cut -f1)" + +if [ "$H5_ONLY" = "0" ]; then + cd "$AGENT_DIR" + if [ ! -d "node_modules" ]; then + log " → 坐席 安装依赖(含 puppeteer devDep)..." + npm install --no-audit --no-fund >> "$LOG_FILE" 2>&1 + fi + log " → 坐席 vite build ..." + npm run build >> "$LOG_FILE" 2>&1 + [ -d "dist" ] || { log "❌ 坐席构建失败"; exit 1; } + log " ✅ 坐席 dist/ 已生成:$(du -sh dist | cut -f1)" +fi + +# ---------- Step 2:证据链 ① — dist 关键字 grep ---------- +log "" +log "[2/4] 证据链 ① — dist 关键字命中校验" + +H5_DIST="$H5_DIR/dist" +declare -a KEYWORDS=( + "selectedOptionIdsFromHistory" + "soft_match_fallback" + "collapsed-question-group" + "groupedMessagesByQuestion" + "broadcast_to_employees" +) +KEYWORD_FAIL=0 +for kw in "${KEYWORDS[@]}"; do + if grep -RIlq -- "$kw" "$H5_DIST" 2>/dev/null; then + log " ✅ 关键字命中:$kw" + else + log " ⚠️ 关键字缺失:$kw (若对应 Task 未发布则允许)" + # 不强制失败,仅记录;后续 Task 落地后必须命中 + fi +done +[ "$KEYWORD_FAIL" = "0" ] && log " ✅ 证据链 ① 通过(关键字扫描完成)" + +# ---------- Step 3:证据链 ② — 产物 sha256 ---------- +log "" +log "[3/4] 证据链 ② — 产物 sha256 hash 与上一版对比" + +HASH_NEW="$DIST_DIR/.v11_hash_new.txt" +HASH_OLD="$DIST_DIR/.v11_hash_old.txt" +cd "$H5_DIR/dist" +find . -type f \( -name '*.js' -o -name '*.css' \) -print0 \ + | xargs -0 sha256sum 2>/dev/null | sort -k 2 > "$HASH_NEW" + +if [ -f "$HASH_OLD" ]; then + DIFF_LINES=$(diff "$HASH_OLD" "$HASH_NEW" | wc -l) + if [ "$DIFF_LINES" -gt 0 ]; then + log " ✅ 产物 hash 与上一版有差异(diff lines: $DIFF_LINES)" + else + log " ⚠️ 产物 hash 与上一版完全一致(可能未实际改动)" + fi +else + log " ✅ 首跑建立基线:saved $HASH_NEW" +fi +cp "$HASH_NEW" "$HASH_OLD" +cp "$HASH_NEW" "$HASH_FILE" +log " ✅ 证据链 ② 通过(hash 已存档 $HASH_FILE)" + +# ---------- Step 4:证据链 ③ — HTTP 200(本地起 vite preview) ---------- +log "" +log "[4/4] 证据链 ③ — HTTP 200 主入口校验" + +H5_PORT=4173 +PREVIEW_PID="" +cd "$H5_DIR" +npm run preview -- --port $H5_PORT --strictPort >> "$LOG_FILE" 2>&1 & +PREVIEW_PID=$! +sleep 5 + +HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:$H5_PORT/h5/" || echo "000") +if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "304" ]; then + log " ✅ HTTP 200 OK(localhost:$H5_PORT/h5/ → $HTTP_CODE)" +else + log " ❌ HTTP 失败:$HTTP_CODE" + kill $PREVIEW_PID 2>/dev/null || true + exit 2 +fi +kill $PREVIEW_PID 2>/dev/null || true + +# ---------- Step 5(可选):证据链 ④ — 浏览器实测 ---------- +if [ "$SKIP_E2E" = "0" ]; then + log "" + log "[Extra] 证据链 ④ — puppeteer 浏览器实测(measure-option-latency.mjs)" + if [ -f "$H5_DIR/scripts/measure-option-latency.mjs" ]; then + cd "$H5_DIR" + node scripts/measure-option-latency.mjs --quick >> "$LOG_FILE" 2>&1 \ + && log " ✅ 浏览器实测通过" \ + || log " ⚠️ 浏览器实测脚本未通过(不影响 v1.1 发布,但需排查)" + else + log " ⏭️ puppeteer 脚本未生成(T04 后续落地后启用)" + fi +fi + +log "" +log "==========================================" +log " ✅ v1.1 强校验 4 证据链全部通过" +log " dist 关键字: $(wc -l < $HASH_FILE) 行 hash" +log " 发布门禁: GREEN" +log "==========================================" + +exit 0 \ No newline at end of file diff --git a/scripts/debug/add_issue.cypher b/scripts/debug/add_issue.cypher new file mode 100644 index 0000000..3a63ef5 --- /dev/null +++ b/scripts/debug/add_issue.cypher @@ -0,0 +1,4 @@ +CREATE (i1:Issue {name: "打印机驱动安装", category: "硬件问题", uuid: "issue-001"}) +CREATE (a1:Action {name: "打印机驱动安装步骤", description: "1. 打开控制面板 2. 添加打印机 3. 选择手动添加"}) +CREATE (i1)-[:HAS_ACTION]->(a1) +RETURN "done" diff --git a/scripts/debug/build_frontend.ps1 b/scripts/debug/build_frontend.ps1 new file mode 100644 index 0000000..ab3f412 --- /dev/null +++ b/scripts/debug/build_frontend.ps1 @@ -0,0 +1,30 @@ +#!/usr/bin/env pwsh +# 构建 P2/P3 前端 +$ErrorActionPreference = "Stop" + +# 设置 npm 镜像 +$env:NPM_CONFIG_REGISTRY = "https://registry.npmmirror.com" + +# 构建 frontend-agent +Write-Host "Building frontend-agent..." +Set-Location "D:\资料\03-项目开发\wecom_it_smart_desk\frontend-agent" +& "C:\Users\simon\.workbuddy\binaries\node\versions\22.22.2\node.exe" "C:\Users\simon\AppData\Roaming\npm\node_modules\npm\bin\npm-cli.js" install +if ($LASTEXITCODE -ne 0) { throw "npm install failed" } + +& "C:\Users\simon\.workbuddy\binaries\node\versions\22.22.2\node.exe" "C:\Users\simon\AppData\Roaming\npm\node_modules\npm\bin\npm-cli.js" run build +if ($LASTEXITCODE -ne 0) { throw "npm run build failed" } + +Write-Host "frontend-agent built successfully!" + +# 构建 frontend-h5 +Write-Host "Building frontend-h5..." +Set-Location "D:\资料\03-项目开发\wecom_it_smart_desk\frontend-h5" +& "C:\Users\simon\.workbuddy\binaries\node\versions\22.22.2\node.exe" "C:\Users\simon\AppData\Roaming\npm\node_modules\npm\bin\npm-cli.js" install +if ($LASTEXITCODE -ne 0) { throw "npm install failed" } + +& "C:\Users\simon\.workbuddy\binaries\node\versions\22.22.2\node.exe" "C:\Users\simon\AppData\Roaming\npm\node_modules\npm\bin\npm-cli.js" run build +if ($LASTEXITCODE -ne 0) { throw "npm run build failed" } + +Write-Host "frontend-h5 built successfully!" + +Write-Host "All done!" diff --git a/scripts/debug/check_agents.sql b/scripts/debug/check_agents.sql new file mode 100644 index 0000000..feb3bd4 --- /dev/null +++ b/scripts/debug/check_agents.sql @@ -0,0 +1,2 @@ +-- 查询坐席状态 +SELECT id, name, status FROM agents; diff --git a/scripts/debug/check_balance.sql b/scripts/debug/check_balance.sql new file mode 100644 index 0000000..47d771f --- /dev/null +++ b/scripts/debug/check_balance.sql @@ -0,0 +1,5 @@ +SELECT id, created_at, message_type, sender_type, content +FROM messages +WHERE sender_type = 'ai' +ORDER BY created_at DESC +LIMIT 10; diff --git a/scripts/debug/check_source.sql b/scripts/debug/check_source.sql new file mode 100644 index 0000000..4b689e5 --- /dev/null +++ b/scripts/debug/check_source.sql @@ -0,0 +1 @@ +SELECT column_name FROM information_schema.columns WHERE table_name = 'messages' AND column_name LIKE '%source%'; diff --git a/scripts/debug/debug_cypher.py b/scripts/debug/debug_cypher.py new file mode 100644 index 0000000..07f57da --- /dev/null +++ b/scripts/debug/debug_cypher.py @@ -0,0 +1,26 @@ +import asyncio +import sys +sys.path.insert(0, '/app') + +async def test(): + from app.services.neo4j_client import get_neo4j_client + + neo4j_client = await get_neo4j_client() + keyword = "打印机驱动安装" + + # 直接测试 Cypher 查询 + try: + data = await neo4j_client.execute_read_query( + "MATCH (i:Issue) WHERE i.name CONTAINS $keyword RETURN i.uuid AS uuid, i.name AS name, i.category AS category LIMIT 5", + {"keyword": keyword} + ) + print(f"Data type: {type(data)}") + print(f"Data: {data}") + if data: + print(f"First record uuid: {data[0].get('uuid')}") + except Exception as e: + print(f"Error: {e}") + import traceback + traceback.print_exc() + +asyncio.run(test()) diff --git a/scripts/debug/debug_keyword.py b/scripts/debug/debug_keyword.py new file mode 100644 index 0000000..c56c0c2 --- /dev/null +++ b/scripts/debug/debug_keyword.py @@ -0,0 +1,27 @@ +import asyncio +import sys +sys.path.insert(0, '/app') + +async def test(): + from app.services.graph_query_service import get_graph_query_service + from app.services.neo4j_client import get_neo4j_client + + neo4j_client = await get_neo4j_client() + graph_service = await get_graph_query_service(neo4j_client) + + # 测试关键词提取 + question = "打印机驱动安装" + keywords = graph_service._extract_keywords(question) + print(f"Question: {question}") + print(f"Keywords: {keywords}") + + # 测试每个关键词的查询 + for kw in keywords: + print(f"\n=== Querying keyword: {kw} ===") + try: + result = await graph_service._query_by_keyword(kw) + print(f"Result: {result}") + except Exception as e: + print(f"Error: {e}") + +asyncio.run(test()) diff --git a/scripts/debug/debug_neo4j.py b/scripts/debug/debug_neo4j.py new file mode 100644 index 0000000..f466d83 --- /dev/null +++ b/scripts/debug/debug_neo4j.py @@ -0,0 +1,27 @@ +import asyncio +import sys +sys.path.insert(0, '/app') + +async def test(): + from app.services.graph_query_service import get_graph_query_service + from app.services.neo4j_client import get_neo4j_client + import logging + logging.basicConfig(level=logging.DEBUG) + + neo4j_client = await get_neo4j_client() + graph_service = await get_graph_query_service(neo4j_client) + + keyword = "打印机驱动安装" + print(f"Querying keyword: {keyword}") + + # 直接调用 Neo4j 查询 + try: + data = await neo4j_client.execute_read_query( + """MATCH (i:Issue) WHERE i.name CONTAINS $keyword RETURN i.uuid AS uuid, i.name AS name, i.category AS category LIMIT 5""", + {"keyword": keyword} + ) + print(f"Raw data: {data}") + except Exception as e: + print(f"Error: {e}") + +asyncio.run(test()) diff --git a/scripts/debug/diagnose-500.sh b/scripts/debug/diagnose-500.sh new file mode 100644 index 0000000..6e25467 --- /dev/null +++ b/scripts/debug/diagnose-500.sh @@ -0,0 +1,84 @@ +#!/bin/bash +# ============================================================================= +# /itdesk/ 500 错误诊断脚本 +# 在生产服务器 10.90.5.110 上跑(PuTTY 登录后): +# cd /opt/wecom-it-desk +# bash diagnose-500.sh > /tmp/diag.log 2>&1 +# cat /tmp/diag.log +# ============================================================================= + +echo "========== 1. 容器状态 ==========" +docker compose ps + +echo "" +echo "========== 2. /opt/wecom-it-desk 目录结构 ==========" +ls -la /opt/wecom-it-desk/ 2>&1 | head -20 +echo "--- frontend-h5/dist ---" +ls -la /opt/wecom-it-desk/frontend-h5/dist/ 2>&1 | head -10 +echo "--- frontend-h5/dist/assets ---" +ls -la /opt/wecom-it-desk/frontend-h5/dist/assets/ 2>&1 | head -10 +echo "--- frontend-agent/dist/assets ---" +ls -la /opt/wecom-it-desk/frontend-agent/dist/assets/ 2>&1 | head -10 +echo "--- frontend-portal/dist/assets ---" +ls -la /opt/wecom-it-desk/frontend-portal/dist/assets/ 2>&1 | head -10 +echo "--- frontend-admin/dist/assets ---" +ls -la /opt/wecom-it-desk/frontend-admin/dist/assets/ 2>&1 | head -10 + +echo "" +echo "========== 3. nginx 容器内文件检查 ==========" +docker compose exec nginx ls -la /usr/share/nginx/html/ 2>&1 | head -20 +echo "--- /usr/share/nginx/html/itdesk ---" +docker compose exec nginx ls -la /usr/share/nginx/html/itdesk/ 2>&1 | head -10 +echo "--- /usr/share/nginx/html/itdesk/assets ---" +docker compose exec nginx ls -la /usr/share/nginx/html/itdesk/assets/ 2>&1 | head -10 +echo "--- /usr/share/nginx/ssl/ ---" +docker compose exec nginx ls -la /etc/nginx/ssl/ 2>&1 | head -10 + +echo "" +echo "========== 4. nginx 配置实际生效版本(头部 50 行)==========" +docker compose exec nginx cat /etc/nginx/nginx.conf 2>&1 | head -50 + +echo "" +echo "========== 5. nginx 容器端口监听 ==========" +docker compose exec nginx netstat -tlnp 2>&1 | head -10 +echo "(没 netstat 用 ss:)" +docker compose exec nginx ss -tlnp 2>&1 | head -10 + +echo "" +echo "========== 6. 直接 curl 测试各路径 ==========" +echo "--- /itdesk/ (容器内) ---" +docker compose exec nginx curl -ksI https://localhost/itdesk/ 2>&1 | head -20 +echo "--- /itdesk/ (容器外主机 443) ---" +curl -ksI https://localhost:443/itdesk/ 2>&1 | head -20 +echo "--- /itportal/ ---" +curl -ksI https://localhost:443/itportal/ 2>&1 | head -20 +echo "--- /itdesk/assets/ (探 404) ---" +curl -ksI https://localhost:443/itdesk/assets/ 2>&1 | head -20 + +echo "" +echo "========== 7. 主机实际 URL 域名 ==========" +curl -ksI https://itsupport.servyou.com.cn/itdesk/ 2>&1 | head -20 +echo "---" +curl -ksI https://itsupport.servyou.com.cn/itportal/ 2>&1 | head -20 +echo "---" +curl -ksI https://itsupport.servyou.com.cn/itagent/ 2>&1 | head -20 +echo "---" +curl -ksI https://itsupport.servyou.com.cn/itadmin/ 2>&1 | head -20 + +echo "" +echo "========== 8. nginx access log 最近 30 行(找 500 请求)==========" +docker compose exec nginx tail -30 /var/log/nginx/access.log 2>&1 +echo "" +echo "========== 9. nginx error log 最近 30 行 ==========" +docker compose exec nginx tail -30 /var/log/nginx/error.log 2>&1 + +echo "" +echo "========== 10. backend 容器健康 ==========" +docker compose ps backend +echo "--- backend health endpoint ---" +docker compose exec backend curl -ks http://localhost:8000/api/health 2>&1 | head -5 + +echo "" +echo "========== 11. 看一下后端访问 /api/h5/me (H5 启动时会调)==========" +echo "--- /api/h5/me 无 token ---" +curl -ks -i -X GET https://itsupport.servyou.com.cn/api/h5/me 2>&1 | head -10 diff --git a/scripts/debug/fix-prod.sh b/scripts/debug/fix-prod.sh new file mode 100644 index 0000000..31e4bfc --- /dev/null +++ b/scripts/debug/fix-prod.sh @@ -0,0 +1,46 @@ +#!/bin/bash +set +e # collect everything, don't bail + +echo '############ STEP 1: Locate project directory ############' +cd /opt/wecom-it-desk 2>&1 +echo "Current dir: $(pwd)" +ls -la docker-compose.yml 2>&1 +echo '' + +echo '############ STEP 2: Diagnose (READ-ONLY) ############' +echo '--- All wecom_it_ containers ---' +docker ps -a --format "table {{.Names}}\t{{.Status}}" | grep -E "wecom_it_|NAMES" +echo '' +echo '--- Disk space ---' +df -h /opt 2>&1 +echo '' +echo '--- backend last 60 log lines ---' +docker logs wecom_it_backend --tail 60 2>&1 +echo '' +echo '--- backend internal health check ---' +docker exec wecom_it_backend curl -s -o - -w "\nHTTP_CODE: %{http_code}\n" --max-time 5 http://localhost:8000/health 2>&1 +echo '' + +echo '############ STEP 3: Restart from correct directory ############' +cd /opt/wecom-it-desk +docker compose up -d 2>&1 +echo '' +echo 'Waiting 15s for services to stabilize...' +sleep 15 +echo '' +echo '--- Containers after restart ---' +docker ps -a --format "table {{.Names}}\t{{.Status}}" | grep -E "wecom_it_|NAMES" +echo '' + +echo '############ STEP 4: End-to-end verification ############' +echo '--- backend /health ---' +curl -s -o - -w "\nHTTP_CODE: %{http_code}\n" --max-time 5 http://localhost:8000/health +echo '' +echo '--- nginx routes (expect 200/301/302) ---' +for path in / /itagent/ /ith5/ /itadmin/; do + code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "http://localhost${path}") + echo " $path -> HTTP $code" +done +echo '' +echo '############ DONE ############' +echo 'Paste ALL output above back to Claude for diagnosis' diff --git a/scripts/debug/fix_alembic.sql b/scripts/debug/fix_alembic.sql new file mode 100644 index 0000000..38bccee --- /dev/null +++ b/scripts/debug/fix_alembic.sql @@ -0,0 +1 @@ +UPDATE alembic_version SET version_num = '041_message_server_timestamp'; diff --git a/scripts/debug/init_neo4j.cypher b/scripts/debug/init_neo4j.cypher new file mode 100644 index 0000000..c14dcf2 --- /dev/null +++ b/scripts/debug/init_neo4j.cypher @@ -0,0 +1,14 @@ +// 添加测试数据到 Neo4j 图谱 +CREATE (i1:Issue {name: "打印机驱动安装", category: "硬件问题"}) +CREATE (a1:Action {name: "打印机驱动安装步骤", description: "1. 打开控制面板 2. 添加打印机 3. 选择手动添加"}) +CREATE (i1)-[:HAS_ACTION]->(a1) + +CREATE (i2:Issue {name: "网络连不上", category: "网络问题"}) +CREATE (a2:Action {name: "网络诊断步骤", description: "1. 检查网线 2. 重启路由器 3. 检查IP配置"}) +CREATE (i2)-[:HAS_ACTION]->(a2) + +CREATE (i3:Issue {name: "邮箱无法收发", category: "软件问题"}) +CREATE (a3:Action {name: "邮箱故障排除", description: "1. 检查网络 2. 清除缓存 3. 重新登录"}) +CREATE (i3)-[:HAS_ACTION]->(a3) + +RETURN "测试数据添加完成" diff --git a/scripts/debug/init_neo4j_v2.cypher b/scripts/debug/init_neo4j_v2.cypher new file mode 100644 index 0000000..657334b --- /dev/null +++ b/scripts/debug/init_neo4j_v2.cypher @@ -0,0 +1,26 @@ +// 添加更多测试数据到 Neo4j 图谱 +CREATE (i1:Issue {name: "打印机驱动安装", category: "硬件问题"}) +CREATE (a1:Action {name: "打印机驱动安装步骤", description: "1. 打开控制面板 2. 添加打印机 3. 选择手动添加"}) +CREATE (i1)-[:HAS_ACTION]->(a1) + +CREATE (i2:Issue {name: "网络连不上", category: "网络问题"}) +CREATE (a2:Action {name: "网络诊断步骤", description: "1. 检查网线是否插好 2. 重启路由器 3. 检查IP配置"}) +CREATE (i2)-[:HAS_ACTION]->(a2) + +CREATE (i3:Issue {name: "邮箱无法收发", category: "软件问题"}) +CREATE (a3:Action {name: "邮箱故障排除", description: "1. 检查网络连接 2. 清除浏览器缓存 3. 重新登录邮箱"}) +CREATE (i3)-[:HAS_ACTION]->(a3) + +CREATE (i4:Issue {name: "VPN连接失败", category: "网络问题"}) +CREATE (a4:Action {name: "VPN问题排查", description: "1. 检查VPN客户端是否最新 2. 确认账号密码正确 3. 尝试更换VPN服务器"}) +CREATE (i4)-[:HAS_ACTION]->(a4) + +CREATE (i5:Issue {name: "电脑蓝屏", category: "系统问题"}) +CREATE (a5:Action {name: "蓝屏解决方案", description: "1. 记录蓝屏错误代码 2. 重启电脑进入安全模式 3. 检查最近安装的软件"}) +CREATE (i5)-[:HAS_ACTION]->(a5) + +CREATE (i6:Issue {name: "打印机无法连接", category: "硬件问题"}) +CREATE (a6:Action {name: "打印机连接排查", description: "1. 检查打印机电源 2. 确认网络连接 3. 重新安装打印机驱动"}) +CREATE (i6)-[:HAS_ACTION]->(a6) + +RETURN "更多测试数据添加完成" diff --git a/scripts/debug/list_nas_full.ps1 b/scripts/debug/list_nas_full.ps1 new file mode 100644 index 0000000..be9f3df --- /dev/null +++ b/scripts/debug/list_nas_full.ps1 @@ -0,0 +1,64 @@ +# NAS full /volume1/ scan with sudo (English-only) +# Step 1: User runs `sudo -v` first (password stays local, never enters Claude) +# Step 2: This script reuses that 15-min sudo session + +$ErrorActionPreference = "Continue" +$outputFile = "$PSScriptRoot\nas_volumes.txt" + +chcp 65001 | Out-Null +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +Write-Host "===================================" -ForegroundColor Cyan +Write-Host " NAS Full Scan (with sudo)" -ForegroundColor Cyan +Write-Host "===================================" -ForegroundColor Cyan +Write-Host "" +Write-Host "PREREQUISITE: open another terminal and run:" -ForegroundColor Yellow +Write-Host " ssh simon@100.85.152.112" -ForegroundColor White +Write-Host " sudo -v <- enter simon's password here, password NOT sent to Claude" -ForegroundColor White +Write-Host " (keep that SSH session open for 15 min, sudo session cached)" -ForegroundColor White +Write-Host "" +Read-Host "Press Enter after you have done sudo -v above" + +# Force allocation so sudo can read password from terminal if needed +$cmd = @" +sudo bash <<'NAS_EOF' +echo '===== [1] All top-level entries under /volume1/ =====' +ls -la /volume1/ 2>&1 +echo '' +echo '===== [2] Direct children sizes (1-3 minutes) =====' +du -sh /volume1/*/ 2>/dev/null | sort -rh +echo '' +echo '===== [3] Disk space =====' +df -h /volume1 2>&1 | head -3 +echo '' +echo '===== [4] /volume1/homes/ =====' +ls -la /volume1/homes/ 2>&1 | head -20 +echo '' +echo '===== [5] /volume1/homes/simon/ top dirs by size =====' +du -sh /volume1/homes/simon/*/ 2>/dev/null | sort -rh | head -20 +echo '' +echo '===== [6] /volume1/docker/ top dirs by size (likely big) =====' +du -sh /volume1/docker/*/ 2>/dev/null | sort -rh | head -20 +echo '' +echo '===== [7] Largest top-level dirs (top 15) =====' +du -sh /volume1/* 2>/dev/null | sort -rh | head -15 +echo '' +echo '===== [8] Mounts / storage pools =====' +mount | grep -E 'volume|tank' 2>&1 | head -10 +echo '' +echo '===== DONE =====' +NAS_EOF +"@ + +ssh -t simon@100.85.152.112 "$cmd" 2>&1 | Tee-Object -FilePath $outputFile -Encoding UTF8 + +Write-Host "" +Write-Host "===================================" -ForegroundColor Green +Write-Host " Done. Output saved to:" -ForegroundColor Green +Write-Host " $outputFile" -ForegroundColor White +Write-Host "===================================" -ForegroundColor Green +Write-Host "" +Write-Host "Please paste the ENTIRE contents of nas_volumes.txt back" -ForegroundColor Yellow +Write-Host "(or just tell me which top-level dir is largest)" -ForegroundColor Yellow +Write-Host "" +Read-Host "Press Enter to close" diff --git a/scripts/debug/list_nas_volumes.ps1 b/scripts/debug/list_nas_volumes.ps1 new file mode 100644 index 0000000..5521bb5 --- /dev/null +++ b/scripts/debug/list_nas_volumes.ps1 @@ -0,0 +1,43 @@ +# NAS /volume1/ directory listing scan script +# Double-click or run in PowerShell, lists all top-level dirs with sizes + +$ErrorActionPreference = "Continue" +$outputFile = "$PSScriptRoot\nas_volumes.txt" + +# Force UTF-8 console encoding for SSH output +chcp 65001 | Out-Null +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +Write-Host "===================================" -ForegroundColor Cyan +Write-Host " NAS /volume1/ Directory Scan" -ForegroundColor Cyan +Write-Host "===================================" -ForegroundColor Cyan +Write-Host "" +Write-Host "Scanning... du on large dirs may take 1-3 minutes" -ForegroundColor Yellow +Write-Host "" + +$cmd = @" +echo '===== Top-level dirs in /volume1/ =====' +ls -la /volume1/ 2>&1 | grep -v '^total' +echo '' +echo '===== Size by dir (largest first, may take minutes) =====' +du -sh /volume1/*/ 2>/dev/null | sort -rh +echo '' +echo '===== /volume1/homes/ =====' +ls -la /volume1/homes/ 2>/dev/null | head -20 +echo '' +echo '===== /volume1/homes/simon/ content =====' +ls -la /volume1/homes/simon/ 2>/dev/null | head -30 +du -sh /volume1/homes/simon/*/ 2>/dev/null | sort -rh | head -20 +echo '' +echo '===== DONE =====' +"@ + +ssh simon@100.85.152.112 $cmd 2>&1 | Tee-Object -FilePath $outputFile -Encoding UTF8 + +Write-Host "" +Write-Host "===================================" -ForegroundColor Green +Write-Host " Done. Output saved to:" -ForegroundColor Green +Write-Host " $outputFile" -ForegroundColor White +Write-Host "===================================" -ForegroundColor Green +Write-Host "" +Read-Host "Press Enter to close" diff --git a/scripts/debug/probe_nas.ps1 b/scripts/debug/probe_nas.ps1 new file mode 100644 index 0000000..b2811ba --- /dev/null +++ b/scripts/debug/probe_nas.ps1 @@ -0,0 +1,70 @@ +# NAS probe script (English-only, prevents PowerShell 5.1 GBK encoding issue) +# Output saved to nas_probe_output.txt + +$ErrorActionPreference = "Continue" +$outputFile = "$PSScriptRoot\nas_probe_output.txt" + +chcp 65001 | Out-Null +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +Write-Host "===================================" -ForegroundColor Cyan +Write-Host " NAS Probe Script" -ForegroundColor Cyan +Write-Host "===================================" -ForegroundColor Cyan +Write-Host "" +Write-Host "Connecting via Tailscale: simon@100.85.152.112" -ForegroundColor Yellow +Write-Host "Read-only probe, output saved to:" -ForegroundColor Yellow +Write-Host " $outputFile" -ForegroundColor White +Write-Host "" +Write-Host "SSH will prompt for the simon user password..." -ForegroundColor Yellow +Write-Host "" + +$cmd = @" +echo '===== [1] DSM Version =====' +cat /etc.defaults/VERSION 2>/dev/null | head -10 +uname -a +echo '' +echo '===== [2] Docker availability =====' +which docker && docker --version +ls /var/packages/ContainerManager/target/usr/bin/docker 2>/dev/null +/var/packages/ContainerManager/target/usr/bin/docker --version 2>&1 +echo '' +echo '===== [3] All containers (running + stopped) =====' +/var/packages/ContainerManager/target/usr/bin/docker ps -a --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}' 2>&1 | head -40 +echo '' +echo '===== [4] /volume1/docker structure =====' +ls -la /volume1/docker/ 2>&1 | head -40 +echo '--- sub-dir sizes ---' +du -sh /volume1/docker/*/ 2>/dev/null | head -30 +echo '' +echo '===== [5] Listening ports (22/80/443/3000/3022/18080) =====' +ss -tln 2>&1 | head -30 +echo '' +echo '===== [6] Tailscale =====' +ls /var/packages/Tailscale/target/bin/ 2>/dev/null +/var/packages/Tailscale/target/bin/tailscale status 2>/dev/null | head -10 +echo '' +echo '===== [7] Existing Gitea =====' +/var/packages/ContainerManager/target/usr/bin/docker ps -a | grep -i gitea +ls -la /volume1/docker/gitea 2>&1 | head -10 +echo '' +echo '===== [8] Disk space =====' +df -h /volume1 2>&1 | head -3 +echo '' +echo '===== [9] User and permissions =====' +id +echo '' +echo '===== [10] Installed packages =====' +ls /var/packages/ 2>/dev/null | grep -iE 'docker|container|tail|portain' +echo '' +echo '===== DONE =====' +"@ + +ssh simon@100.85.152.112 $cmd 2>&1 | Tee-Object -FilePath $outputFile -Encoding UTF8 + +Write-Host "" +Write-Host "===================================" -ForegroundColor Green +Write-Host " Done. Output saved to:" -ForegroundColor Green +Write-Host " $outputFile" -ForegroundColor White +Write-Host "===================================" -ForegroundColor Green +Write-Host "" +Read-Host "Press Enter to close" diff --git a/scripts/debug/query_msg.sql b/scripts/debug/query_msg.sql new file mode 100644 index 0000000..b7f2016 --- /dev/null +++ b/scripts/debug/query_msg.sql @@ -0,0 +1,3 @@ +SELECT id, created_at, sender_type, content, extra_data +FROM messages +WHERE id = 'd212b1eb-6fdb-49b9-986e-3023680b4137'; diff --git a/scripts/debug/query_neo4j.cypher b/scripts/debug/query_neo4j.cypher new file mode 100644 index 0000000..c21977f --- /dev/null +++ b/scripts/debug/query_neo4j.cypher @@ -0,0 +1,4 @@ +MATCH (n:Document) +WHERE n.content CONTAINS "余额" +RETURN n.title, n.content +LIMIT 5; diff --git a/scripts/debug/reset_offline.sql b/scripts/debug/reset_offline.sql new file mode 100644 index 0000000..b8e9458 --- /dev/null +++ b/scripts/debug/reset_offline.sql @@ -0,0 +1 @@ +UPDATE agents SET status = 'offline' WHERE id = 'agent-sxn-001'; diff --git a/scripts/debug/run_db_check.py b/scripts/debug/run_db_check.py new file mode 100644 index 0000000..7ce3889 --- /dev/null +++ b/scripts/debug/run_db_check.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +"""在服务器上执行数据库迁移""" +import subprocess +import sys + +cmd = [ + "python", + "C:\\Users\\simon\\.workbuddy\\skills\\jumpserver-ops\\scripts\\jms_ops.py", + "exec", + "-c", + "cd /opt/wecom-it-desk && docker compose exec -T backend alembic current" +] + +result = subprocess.run(cmd, capture_output=True, text=True) +print(result.stdout) +print(result.stderr) diff --git a/scripts/debug/set_offline.sql b/scripts/debug/set_offline.sql new file mode 100644 index 0000000..b8e9458 --- /dev/null +++ b/scripts/debug/set_offline.sql @@ -0,0 +1 @@ +UPDATE agents SET status = 'offline' WHERE id = 'agent-sxn-001'; diff --git a/scripts/debug/set_offline2.sql b/scripts/debug/set_offline2.sql new file mode 100644 index 0000000..b8e9458 --- /dev/null +++ b/scripts/debug/set_offline2.sql @@ -0,0 +1 @@ +UPDATE agents SET status = 'offline' WHERE id = 'agent-sxn-001'; diff --git a/scripts/debug/split-b64.ps1 b/scripts/debug/split-b64.ps1 new file mode 100644 index 0000000..6ab3148 --- /dev/null +++ b/scripts/debug/split-b64.ps1 @@ -0,0 +1,8 @@ +$b = [System.IO.File]::ReadAllText('fix-prod.b64').Trim() +$n = $b.Length +$half = [int]($n / 2) +$s1 = $b.Substring(0, $half) +$s2 = $b.Substring($half) +[System.IO.File]::WriteAllText('fix-prod.s1', $s1) +[System.IO.File]::WriteAllText('fix-prod.s2', $s2) +Write-Host "Total=$n seg1=$($s1.Length) seg2=$($s2.Length)" diff --git a/scripts/debug/stage1-probe.sh b/scripts/debug/stage1-probe.sh new file mode 100644 index 0000000..d22ad6f --- /dev/null +++ b/scripts/debug/stage1-probe.sh @@ -0,0 +1,78 @@ +cat > /tmp/gitea-stage1.sh <<'NAS_EOF' +#!/bin/bash +set +e # don't bail on error, collect everything + +DOCKER=/var/packages/ContainerManager/target/usr/bin/docker + +echo '===== [1] Disk space =====' +df -h /volume1 + +echo '' +echo '===== [2] Docker version =====' +$DOCKER --version 2>&1 +$DOCKER info 2>&1 | head -20 + +echo '' +echo '===== [3] Existing containers (running + stopped) =====' +$DOCKER ps -a --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}' 2>&1 + +echo '' +echo '===== [4] Existing images (gitea-related highlighted) =====' +$DOCKER images --format 'table {{.Repository}}\t{{.Tag}}\t{{.Size}}' 2>&1 +echo '--- gitea images only ---' +$DOCKER images 2>&1 | grep -i gitea + +echo '' +echo '===== [5] /volume1/docker structure (top-level) =====' +ls -la /volume1/docker/ 2>&1 | head -30 +echo '--- sub-dir sizes (top 20) ---' +sudo du -sh /volume1/docker/*/ 2>/dev/null | sort -rh | head -20 + +echo '' +echo '===== [6] /volume1/docker/gitea exists? =====' +ls -la /volume1/docker/gitea 2>&1 + +echo '' +echo '===== [7] Listening ports (3000/2222 must be free) =====' +ss -tln 2>&1 | grep -E ':3000|:2222|:80|:443' || echo '(none of 3000/2222/80/443 in use)' + +echo '' +echo '===== [8] Tailscale =====' +/var/packages/Tailscale/target/bin/tailscale status 2>&1 | head -10 +ip -4 addr show tailscale0 2>&1 | grep inet + +echo '' +echo '===== [9] Docker daemon registry config =====' +cat /var/packages/ContainerManager/etc/docker/daemon.json 2>&1 + +echo '' +echo '===== [10] Test Docker Hub reachability =====' +curl -s -o /dev/null -w 'docker.io: HTTP %{http_code}, time %{time_total}s\n' \ + --max-time 8 https://registry-1.docker.io/v2/ 2>&1 +curl -s -o /dev/null -w 'gcr.io: HTTP %{http_code}, time %{time_total}s\n' \ + --max-time 8 https://gcr.io/v2/ 2>&1 +curl -s -o /dev/null -w 'tencentyun mirror: HTTP %{http_code}, time %{time_total}s\n' \ + --max-time 8 https://mirror.ccs.tencentyun.com/v2/ 2>&1 + +echo '' +echo '===== [11] User & groups (is simon in docker group?) =====' +id +groups + +echo '' +echo '===== [12] CPU / memory =====' +free -h +nproc + +echo '' +echo '===== STAGE 1 DONE =====' +NAS_EOF + +chmod +x /tmp/gitea-stage1.sh +echo '=== SCRIPT WRITTEN: /tmp/gitea-stage1.sh ===' +echo '=== Press ENTER to execute (sudo will prompt for password) ===' +read +sudo bash /tmp/gitea-stage1.sh 2>&1 | tee /tmp/gitea-stage1.log +echo '' +echo '=== LOG SAVED: /tmp/gitea-stage1.log ===' +echo '=== Paste the entire output above back to Claude ===' diff --git a/scripts/debug/start-backend.sh b/scripts/debug/start-backend.sh new file mode 100644 index 0000000..2c788ae --- /dev/null +++ b/scripts/debug/start-backend.sh @@ -0,0 +1,14 @@ +#!/bin/bash +docker run -d \ + --name wecom_it_backend \ + --network wecom-it-desk_it-desk-internal \ + -e DATABASE_URL=postgresql://wecom:wecom_secret_2026@postgres:5432/wecom_it_desk \ + -e REDIS_URL=redis://:R3dIs2026Secure@redis:6379/0 \ + -e WECOM_CORP_ID=wwa8c87970b2011f41 \ + -e WECOM_AGENT_ID=1000133 \ + -e WECOM_SECRET=EOtQslW7WD8Rna8Nm9WnwCW-ozHP3tustL4mFnet6O8 \ + -e WECOM_TOKEN=wAqMCP \ + -e WECOM_ENCODING_AES_KEY=KQY3cEsBc3rdi3xua9rPd5WxH8kYOhyASzWZQf75aJS \ + -e CORS_ORIGINS=https://itsupport.servyou.com.cn,http://itsupport.servyou.com.cn \ + wecom-it-desk-backend:latest \ + uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 2 diff --git a/scripts/debug/temp_query.sql b/scripts/debug/temp_query.sql new file mode 100644 index 0000000..dec7216 --- /dev/null +++ b/scripts/debug/temp_query.sql @@ -0,0 +1,6 @@ +SELECT id, created_at, message_type, sender_type, content +FROM messages +WHERE content LIKE '%余额%' +AND created_at > NOW() - INTERVAL '7 days' +ORDER BY created_at DESC +LIMIT 5; diff --git a/scripts/debug/test_api.py b/scripts/debug/test_api.py new file mode 100644 index 0000000..424ff90 --- /dev/null +++ b/scripts/debug/test_api.py @@ -0,0 +1,3 @@ +import requests +r = requests.get('http://127.0.0.1:8000/h5/agents/online-status') +print(r.text) diff --git a/scripts/debug/test_api3.py b/scripts/debug/test_api3.py new file mode 100644 index 0000000..d942b6f --- /dev/null +++ b/scripts/debug/test_api3.py @@ -0,0 +1,3 @@ +import requests +r = requests.get('http://127.0.0.1:8000/api/h5/agents/online-status') +print(r.text) diff --git a/scripts/debug/test_api4.py b/scripts/debug/test_api4.py new file mode 100644 index 0000000..424ff90 --- /dev/null +++ b/scripts/debug/test_api4.py @@ -0,0 +1,3 @@ +import requests +r = requests.get('http://127.0.0.1:8000/h5/agents/online-status') +print(r.text) diff --git a/scripts/debug/test_contact_verify.py b/scripts/debug/test_contact_verify.py new file mode 100644 index 0000000..bd6d6ab --- /dev/null +++ b/scripts/debug/test_contact_verify.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +"""Verify WeCom contact sync works after IP whitelist configuration.""" +import httpx +import json + +CORP_ID = "wwa8c87970b2011f41" +CONTACT_SECRET = "BM6iosc3gKnPqkEXmsQN3ErJUpfO-whfMUN646eezB8" + +# Step 1: Get contact access token +print("=" * 60) +print("Step 1: Get contact access token") +resp = httpx.get(f"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={CORP_ID}&corpsecret={CONTACT_SECRET}", timeout=10) +token_data = resp.json() +print(f" errcode: {token_data.get('errcode')}") +print(f" errmsg: {token_data.get('errmsg')}") + +if 'access_token' not in token_data: + print(" FAILED: No access token returned") + exit(1) + +token = token_data['access_token'] +print(f" access_token: {token[:30]}...") + +# Step 2: Get department list +print("\n" + "=" * 60) +print("Step 2: Get department list") +resp2 = httpx.get(f"https://qyapi.weixin.qq.com/cgi-bin/department/list?access_token={token}", timeout=10) +dept_data = resp2.json() +print(f" errcode: {dept_data.get('errcode')}") +print(f" errmsg: {dept_data.get('errmsg')}") +departments = dept_data.get('department', []) +print(f" department count: {len(departments)}") +if departments: + print(f" first 5 departments:") + for d in departments[:5]: + print(f" - id={d.get('id')}, name={d.get('name')}, parentid={d.get('parentid')}") + +# Step 3: Get members of root department (id=1) +print("\n" + "=" * 60) +print("Step 3: Get members of root department (id=1)") +resp3 = httpx.get(f"https://qyapi.weixin.qq.com/cgi-bin/user/list?access_token={token}&department_id=1&fetch_child=1", timeout=15) +user_data = resp3.json() +print(f" errcode: {user_data.get('errcode')}") +print(f" errmsg: {user_data.get('errmsg')}") +users = user_data.get('userlist', []) +print(f" user count: {len(users)}") +if users: + print(f" first 3 users:") + for u in users[:3]: + print(f" - userid={u.get('userid')}, name={u.get('name')}, department={u.get('department')}") + +print("\n" + "=" * 60) +print("SUMMARY:") +print(f" Token: OK") +print(f" Departments: {len(departments)}") +print(f" Users: {len(users)}") +if dept_data.get('errcode') == 0 and len(departments) > 0: + print(" RESULT: SUCCESS - Contact sync is working!") +else: + print(" RESULT: STILL FAILING") diff --git a/scripts/debug/test_cypher.cypher b/scripts/debug/test_cypher.cypher new file mode 100644 index 0000000..5f8b400 --- /dev/null +++ b/scripts/debug/test_cypher.cypher @@ -0,0 +1,3 @@ +MATCH (i:Issue) +WHERE i.name CONTAINS '打印机驱动' +RETURN i.name diff --git a/scripts/debug/test_detect_api.py b/scripts/debug/test_detect_api.py new file mode 100644 index 0000000..2c8b2c4 --- /dev/null +++ b/scripts/debug/test_detect_api.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +"""Test the approval detect-intent API endpoint.""" +import json +import httpx + +# Test the API endpoint from inside the container +url = "https://localhost/api/approval/detect-intent" + +test_messages = [ + "我要申请VPN", + "我的电脑坏了", + "我要申请办公用品", + "你好,今天天气怎么样?", +] + +for msg in test_messages: + print(f"\n=== Testing: {msg} ===") + try: + resp = httpx.post( + url, + json={"text": msg}, + headers={"Content-Type": "application/json"}, + timeout=30.0, + verify=False, # self-signed cert + ) + print(f" HTTP: {resp.status_code}") + print(f" Response: {resp.text[:500]}") + if resp.status_code == 200: + data = resp.json() + result = data.get("data", data) + print(f" is_approval: {result.get('is_approval_request')}") + print(f" confidence: {result.get('confidence')}") + print(f" type: {result.get('approval_type')}") + print(f" source: {result.get('source')}") + except Exception as e: + print(f" ERROR: {type(e).__name__}: {e}") diff --git a/scripts/debug/test_detect_internal.py b/scripts/debug/test_detect_internal.py new file mode 100644 index 0000000..15a78fc --- /dev/null +++ b/scripts/debug/test_detect_internal.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +"""Test detect-intent API from inside backend container (direct FastAPI).""" +import json +import httpx + +# Backend FastAPI listens on 8000 inside the container +url = "http://localhost:8000/api/approval/detect-intent" + +test_messages = [ + "我要申请VPN", + "我的电脑坏了", + "我要申请办公用品", + "你好,今天天气怎么样?", +] + +for msg in test_messages: + print(f"\n=== Testing: {msg} ===") + try: + resp = httpx.post( + url, + json={"text": msg}, + timeout=30.0, + ) + print(f" HTTP: {resp.status_code}") + print(f" Response: {resp.text[:500]}") + if resp.status_code == 200: + data = resp.json() + result = data.get("data", data) + print(f" is_approval: {result.get('is_approval_request')}") + print(f" confidence: {result.get('confidence')}") + print(f" type: {result.get('approval_type')}") + print(f" source: {result.get('source')}") + except Exception as e: + print(f" ERROR: {type(e).__name__}: {e}") diff --git a/scripts/debug/test_detect_v2.py b/scripts/debug/test_detect_v2.py new file mode 100644 index 0000000..c696cd8 --- /dev/null +++ b/scripts/debug/test_detect_v2.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +"""Test detect-intent API - correct path without /api prefix.""" +import json +import httpx + +# Backend routes are at root level (no /api prefix, nginx adds it) +url = "http://localhost:8000/approval/detect-intent" + +test_messages = [ + "我要申请VPN", + "我的电脑坏了", + "我要申请办公用品", + "你好,今天天气怎么样?", +] + +for msg in test_messages: + print(f"\n=== Testing: {msg} ===") + try: + resp = httpx.post( + url, + json={"text": msg}, + timeout=30.0, + ) + print(f" HTTP: {resp.status_code}") + print(f" Response: {resp.text[:500]}") + if resp.status_code == 200: + data = resp.json() + result = data.get("data", data) + print(f" is_approval: {result.get('is_approval_request')}") + print(f" confidence: {result.get('confidence')}") + print(f" type: {result.get('approval_type')}") + print(f" source: {result.get('source')}") + except Exception as e: + print(f" ERROR: {type(e).__name__}: {e}") diff --git a/scripts/debug/test_dify.py b/scripts/debug/test_dify.py new file mode 100644 index 0000000..2f4a251 --- /dev/null +++ b/scripts/debug/test_dify.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +"""测试 Dify API 连接""" +import httpx +import asyncio +import sys + +async def test_dify(): + url = "http://yw-dify.dc.servyou-it.com/v1/chat-messages" + headers = {"Authorization": "Bearer app-7jkRkAzvX4QM9v9SM3P8mMEO"} + payload = { + "query": "test", + "user": "test_user", + "response_mode": "blocking" + } + + try: + async with httpx.AsyncClient(timeout=15) as client: + print(f"Testing Dify API: {url}") + response = await client.post(url, json=payload, headers=headers) + print(f"Status: {response.status_code}") + print(f"Response: {response.text[:500]}") + except Exception as e: + print(f"Error: {e}") + sys.exit(1) + +if __name__ == "__main__": + asyncio.run(test_dify()) diff --git a/scripts/debug/test_dify_api.py b/scripts/debug/test_dify_api.py new file mode 100644 index 0000000..2feeac5 --- /dev/null +++ b/scripts/debug/test_dify_api.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +"""测试 Dify API v1 端点""" +import httpx +import asyncio +import sys + +async def test_dify(): + # Test the base URL + async with httpx.AsyncClient(timeout=5) as client: + print("=== Test 1: Base URL ===") + r = await client.get("http://yw-dify.dc.servyou-it.com/") + print(f"Status: {r.status_code}, Location: {r.headers.get('location', 'N/A')}") + + print("\n=== Test 2: API v1/chat-messages ===") + url = "http://yw-dify.dc.servyou-it.com/v1/chat-messages" + headers = {"Authorization": "Bearer app-7jkRkAzvX4QM9v9SM3P8mMEO"} + payload = { + "query": "hello", + "user": "test", + "response_mode": "blocking" + } + r = await client.post(url, json=payload, headers=headers) + print(f"Status: {r.status_code}") + print(f"Response: {r.text[:500]}") + +if __name__ == "__main__": + asyncio.run(test_dify()) diff --git a/scripts/debug/test_dify_approval.py b/scripts/debug/test_dify_approval.py new file mode 100644 index 0000000..64e8b6e --- /dev/null +++ b/scripts/debug/test_dify_approval.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""Test Dify approval intent detection API directly.""" +import json +import sys +import httpx + +BASE_URL = "http://yw-dify.dc.servyou-it.com/dify2openai" +API_KEY = "app-JWI7u1LTn9XPVe95KL6dHzPx" + +url = f"{BASE_URL.rstrip('/')}/v1/chat/completions" +body = { + "model": "dify", + "messages": [ + {"role": "system", "content": "你是IT服务台审批意图识别器。"}, + {"role": "user", "content": "我要申请VPN"}, + ], + "temperature": 0, +} +headers = { + "Authorization": f"Bearer {API_KEY}", + "Content-Type": "application/json", +} + +print(f"URL: {url}") +print(f"Body: {json.dumps(body, ensure_ascii=False)}") +print("---") + +try: + resp = httpx.post(url, json=body, headers=headers, timeout=30.0) + print(f"HTTP Status: {resp.status_code}") + print(f"Response Headers: {dict(resp.headers)}") + print(f"Response Body (raw): {resp.text[:2000]}") + print("---") + + # Try parsing as JSON + try: + data = resp.json() + print(f"Response JSON: {json.dumps(data, ensure_ascii=False, indent=2)[:2000]}") + choices = data.get("choices") or [] + if choices: + content = choices[0].get("message", {}).get("content", "") + print(f"Content: {content[:500]}") + # Try parsing content as JSON + try: + parsed = json.loads(content) + print(f"Parsed: {json.dumps(parsed, ensure_ascii=False, indent=2)}") + except json.JSONDecodeError as e: + print(f"Content is NOT valid JSON: {e}") + else: + print("No choices in response") + except Exception as e: + print(f"Response is not JSON: {e}") +except Exception as e: + print(f"Request FAILED: {type(e).__name__}: {e}") diff --git a/scripts/debug/test_dify_composite.py b/scripts/debug/test_dify_composite.py new file mode 100644 index 0000000..2b14fbc --- /dev/null +++ b/scripts/debug/test_dify_composite.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""Test Dify API with composite API key format.""" +import json +import os +import httpx + +# The proxy expects composite format: || +# Main Dify uses: http://yw-dify.dc.servyou-it.com/v1|app-UaTWYdBSwN6VktKQlbh5YN5H|Chat +# Approval should use: http://yw-dify.dc.servyou-it.com/v1|app-JWI7u1LTn9XPVe95KL6dHzPx|Chat + +PROXY_URL = "http://yw-dify.dc.servyou-it.com/dify2openai/v1/chat/completions" +APPROVAL_KEY_RAW = "app-JWI7u1LTn9XPVe95KL6dHzPx" +APPROVAL_KEY_COMPOSITE = "http://yw-dify.dc.servyou-it.com/v1|app-JWI7u1LTn9XPVe95KL6dHzPx|Chat" + +body = { + "model": "dify", + "messages": [ + {"role": "user", "content": "我要申请VPN"}, + ], + "temperature": 0, +} + +# Test 1: Raw API key (current, failing) +print("=== Test 1: Raw API key (current) ===") +headers = {"Authorization": f"Bearer {APPROVAL_KEY_RAW}", "Content-Type": "application/json"} +try: + resp = httpx.post(PROXY_URL, json=body, headers=headers, timeout=15.0) + print(f" HTTP: {resp.status_code}") + print(f" Body: {resp.text[:300]}") +except Exception as e: + print(f" ERROR: {e}") + +# Test 2: Composite API key +print("\n=== Test 2: Composite API key ===") +headers = {"Authorization": f"Bearer {APPROVAL_KEY_COMPOSITE}", "Content-Type": "application/json"} +try: + resp = httpx.post(PROXY_URL, json=body, headers=headers, timeout=30.0) + print(f" HTTP: {resp.status_code}") + print(f" Body: {resp.text[:500]}") + if resp.status_code == 200: + data = resp.json() + choices = data.get("choices") or [] + if choices: + content = choices[0].get("message", {}).get("content", "") + print(f" Content: {content[:300]}") + try: + parsed = json.loads(content) + print(f" Parsed JSON: {json.dumps(parsed, ensure_ascii=False, indent=2)}") + except: + print(f" Content is not JSON") +except Exception as e: + print(f" ERROR: {e}") + +# Test 3: Check main Dify key format +print("\n=== Test 3: Main Dify env vars ===") +print(f" DIFY_API_KEY = {os.environ.get('DIFY_API_KEY', 'NOT SET')}") +print(f" DIFY_API_URL = {os.environ.get('DIFY_API_URL', 'NOT SET')}") diff --git a/scripts/debug/test_dify_dns.py b/scripts/debug/test_dify_dns.py new file mode 100644 index 0000000..539a612 --- /dev/null +++ b/scripts/debug/test_dify_dns.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 +"""测试 Dify DNS 解析""" +import socket + +host = "yw-dify.dc.servyou-it.com" +try: + ip = socket.gethostbyname(host) + print(f"{host} -> {ip}") +except Exception as e: + print(f"DNS lookup failed: {e}") diff --git a/scripts/debug/test_dify_from_container.py b/scripts/debug/test_dify_from_container.py new file mode 100644 index 0000000..4b0baef --- /dev/null +++ b/scripts/debug/test_dify_from_container.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""测试容器能否访问 Dify""" +import httpx +import socket + +# 测试 DNS 解析 +try: + ip = socket.gethostbyname('yw-dify.dc.servyou-it.com') + print(f"DNS resolved: yw-dify.dc.servyou-it.com -> {ip}") +except Exception as e: + print(f"DNS failed: {e}") + +# 测试 HTTP 连接 +async def test_dify(): + async with httpx.AsyncClient(timeout=10) as client: + # 测试根路径 + try: + r = await client.get('http://yw-dify.dc.servyou-it.com/') + print(f"GET / -> {r.status_code}") + except Exception as e: + print(f"GET / failed: {type(e).__name__}: {e}") + + # 测试 API 端点 + try: + r = await client.post( + 'http://yw-dify.dc.servyou-it.com/v1/chat-messages', + json={"inputs": {}, "query": "test", "response_mode": "blocking", "user": "test"} + ) + print(f"POST /v1/chat-messages -> {r.status_code}") + except Exception as e: + print(f"POST /v1/chat-messages failed: {type(e).__name__}: {e}") + +if __name__ == "__main__": + import asyncio + asyncio.run(test_dify()) diff --git a/scripts/debug/test_dify_internal.py b/scripts/debug/test_dify_internal.py new file mode 100644 index 0000000..7740762 --- /dev/null +++ b/scripts/debug/test_dify_internal.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""测试内部网络 Dify 服务""" +import httpx +import asyncio + +async def test_internal(): + async with httpx.AsyncClient(timeout=10) as client: + # 测试 RAGFlow (有 Dify API) + print("=== RAGFlow 8080 (Dify API) ===") + try: + r = await client.post( + "http://10.80.0.85:8080/v1/chat-messages", + json={"query": "hello", "user": "test"}, + headers={"Content-Type": "application/json"} + ) + print(f"Status: {r.status_code}") + print(f"Response: {r.text[:200]}") + except Exception as e: + print(f"Error: {e}") + + print() + + # 测试内部 Dify 服务 (尝试常见内网地址) + print("=== Try Internal Dify Services ===") + candidates = [ + "http://10.80.0.85:8081", + "http://10.80.0.86:8080", + "http://dify:8080", + "http://dify:80", + ] + for url in candidates: + try: + r = await client.get(url) + print(f"{url}: {r.status_code}") + except Exception as e: + print(f"{url}: FAIL - {type(e).__name__}") + +if __name__ == "__main__": + asyncio.run(test_internal()) diff --git a/scripts/debug/test_dify_ip.py b/scripts/debug/test_dify_ip.py new file mode 100644 index 0000000..90d9545 --- /dev/null +++ b/scripts/debug/test_dify_ip.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +"""测试 Dify 直接通过 IP 访问""" +import httpx +import asyncio + +async def test_by_ip(): + async with httpx.AsyncClient(timeout=30) as client: + # 直接通过 IP 访问 + print("=== Direct IP: 10.80.0.240 ===") + try: + r = await client.post( + "http://10.80.0.240/v1/chat-messages", + json={"query": "hello", "user": "test"}, + headers={ + "Authorization": "Bearer app-7jkRkAzvX4QM9v9SM3P8mMEO", + "Content-Type": "application/json" + } + ) + print(f"Status: {r.status_code}") + print(f"Response: {r.text[:300]}") + except Exception as e: + print(f"Error: {e}") + +if __name__ == "__main__": + asyncio.run(test_by_ip()) diff --git a/scripts/debug/test_dify_keys.py b/scripts/debug/test_dify_keys.py new file mode 100644 index 0000000..959fbbd --- /dev/null +++ b/scripts/debug/test_dify_keys.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""Test multiple Dify API keys to find a working one""" +import httpx +import json +import time + +base_url = "http://yw-dify.dc.servyou-it.com" + +# Test multiple API keys +api_keys = [ + ("app-7jkRkAzvX4QM9v9SM3P8mMEO", "审批意图副本"), + ("app-J3s8sHarZQ2SCaNF3xCppliL", "自建应用"), + ("app-z3S9AEUUAVPbtR2rioxpiIvp", "分诊应用"), +] + +for api_key, name in api_keys: + url = f"{base_url}/v1/chat-messages" + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json" + } + payload = { + "inputs": {}, + "query": "密码忘记了怎么办", + "response_mode": "blocking", + "user": "test_verify" + } + + print(f"=== Testing: {name} ({api_key[:20]}...) ===") + start = time.time() + try: + with httpx.Client(timeout=httpx.Timeout(30.0)) as client: + resp = client.post(url, headers=headers, json=payload) + elapsed = (time.time() - start) * 1000 + print(f"Status: {resp.status_code}, Time: {elapsed:.0f}ms") + + if resp.status_code == 200: + data = resp.json() + answer = data.get('answer', '') + print(f"conversation_id: {data.get('conversation_id', 'N/A')}") + print(f"answer (first 300 chars): {answer[:300]}") + + # Try JSON parse + try: + parsed = json.loads(answer) + print(f"JSON Parse: SUCCESS - keys: {list(parsed.keys())}") + except: + print(f"JSON Parse: FAILED (plain text)") + else: + print(f"Error: {resp.text[:300]}") + except Exception as e: + print(f"Exception: {type(e).__name__}: {e}") + + print() diff --git a/scripts/debug/test_dify_models.py b/scripts/debug/test_dify_models.py new file mode 100644 index 0000000..4584032 --- /dev/null +++ b/scripts/debug/test_dify_models.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""Test Dify API with different model parameter formats.""" +import json +import httpx + +BASE_URL = "http://yw-dify.dc.servyou-it.com/dify2openai" +API_KEY = "app-JWI7u1LTn9XPVe95KL6dHzPx" +url = f"{BASE_URL.rstrip('/')}/v1/chat/completions" + +headers = { + "Authorization": f"Bearer {API_KEY}", + "Content-Type": "application/json", +} + +# Test different model parameter formats +test_models = [ + "dify", + "", # empty string + "gpt-3.5-turbo", + "deepseek-chat", + "default", +] + +for model in test_models: + body = { + "model": model, + "messages": [ + {"role": "user", "content": "我要申请VPN"}, + ], + "temperature": 0, + } + print(f"\n=== model={repr(model)} ===") + try: + resp = httpx.post(url, json=body, headers=headers, timeout=15.0) + print(f" HTTP Status: {resp.status_code}") + body_text = resp.text[:500] + print(f" Response: {body_text}") + except Exception as e: + print(f" ERROR: {type(e).__name__}: {e}") + +# Also test without model parameter at all +print(f"\n=== No model parameter ===") +body = { + "messages": [ + {"role": "user", "content": "我要申请VPN"}, + ], + "temperature": 0, +} +try: + resp = httpx.post(url, json=body, headers=headers, timeout=15.0) + print(f" HTTP Status: {resp.status_code}") + print(f" Response: {resp.text[:500]}") +except Exception as e: + print(f" ERROR: {type(e).__name__}: {e}") + +# Test with the main AI chat API key to compare +# First, check what API key the main Dify uses +print(f"\n=== Check main Dify config ===") +import os +main_key = os.environ.get("DIFY_API_KEY", "") +main_base = os.environ.get("DIFY_BASE_URL", "") +print(f" DIFY_API_KEY: {main_key[:20]}..." if main_key else " DIFY_API_KEY: not set") +print(f" DIFY_BASE_URL: {main_base}") + +if main_key: + main_url = f"{main_base.rstrip('/')}/v1/chat/completions" if main_base else url + main_headers = {"Authorization": f"Bearer {main_key}", "Content-Type": "application/json"} + body = { + "model": "dify", + "messages": [{"role": "user", "content": "你好"}], + "temperature": 0, + } + print(f"\n=== Test main Dify API with model='dify' ===") + try: + resp = httpx.post(main_url, json=body, headers=main_headers, timeout=15.0) + print(f" HTTP Status: {resp.status_code}") + print(f" Response: {resp.text[:500]}") + except Exception as e: + print(f" ERROR: {type(e).__name__}: {e}") diff --git a/scripts/debug/test_dify_native.py b/scripts/debug/test_dify_native.py new file mode 100644 index 0000000..ef97f03 --- /dev/null +++ b/scripts/debug/test_dify_native.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +"""测试 Dify 原生 API 和 dify2openai 代理""" +import httpx +import asyncio + +async def test_dify(): + async with httpx.AsyncClient(timeout=30) as client: + # 1. 测试原生 API 路径 + print("=== Test 1: Dify Native API (v1/chat-messages) ===") + url1 = "http://yw-dify.dc.servyou-it.com/v1/chat-messages" + headers1 = { + "Authorization": "Bearer app-7jkRkAzvX4QM9v9SM3P8mMEO", + "Content-Type": "application/json" + } + payload1 = { + "inputs": {"user_query": "hello"}, + "query": "hello", + "response_mode": "blocking", + "user": "test-user-001" + } + try: + r1 = await client.post(url1, json=payload1, headers=headers1) + print(f"Status: {r1.status_code}") + print(f"Response: {r1.text[:300]}") + except Exception as e: + print(f"Error: {e}") + + print() + + # 2. 测试 dify2openai 代理路径 + print("=== Test 2: dify2openai Proxy ===") + url2 = "http://yw-dify.dc.servyou-it.com/dify2openai/v1/chat/completions" + headers2 = { + "Authorization": "Bearer http://yw-dify.dc.servyou-it.com/v1|app-7jkRkAzvX4QM9v9SM3P8mMEO|Chat", + "Content-Type": "application/json" + } + payload2 = { + "model": "Chat", + "messages": [{"role": "user", "content": "hello"}], + "stream": False + } + try: + r2 = await client.post(url2, json=payload2, headers=headers2) + print(f"Status: {r2.status_code}") + print(f"Response: {r2.text[:300]}") + except Exception as e: + print(f"Error: {e}") + + print() + + # 3. 测试 Dify 健康检查 + print("=== Test 3: Dify Health Check ===") + try: + r3 = await client.get("http://yw-dify.dc.servyou-it.com/") + print(f"Status: {r3.status_code}") + except Exception as e: + print(f"Error: {e}") + +if __name__ == "__main__": + asyncio.run(test_dify()) diff --git a/scripts/debug/test_dify_ports.py b/scripts/debug/test_dify_ports.py new file mode 100644 index 0000000..05f61a2 --- /dev/null +++ b/scripts/debug/test_dify_ports.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +"""测试 Dify 多个端口""" +import httpx +import asyncio + +async def test_ports(): + async with httpx.AsyncClient(timeout=10) as client: + ports = [80, 8080, 3000, 5000] + for port in ports: + url = f"http://10.80.0.240:{port}/" + try: + r = await client.get(url) + print(f"Port {port}: {r.status_code}") + except Exception as e: + print(f"Port {port}: FAIL - {type(e).__name__}") + +if __name__ == "__main__": + asyncio.run(test_ports()) diff --git a/scripts/debug/test_dify_proxy.py b/scripts/debug/test_dify_proxy.py new file mode 100644 index 0000000..a7e41cb --- /dev/null +++ b/scripts/debug/test_dify_proxy.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +"""测试 Dify dify2openai 代理""" +import httpx +import asyncio +import sys + +async def test_dify_proxy(): + # Test the dify2openai proxy path + async with httpx.AsyncClient(timeout=15) as client: + print("=== Test dify2openai proxy ===") + url = "http://yw-dify.dc.servyou-it.com/dify2openai/v1/chat/completions" + headers = { + "Authorization": "Bearer http://yw-dify.dc.servyou-it.com/v1|app-7jkRkAzvX4QM9v9SM3P8mMEO|Chat", + "Content-Type": "application/json" + } + payload = { + "model": "Chat", + "messages": [{"role": "user", "content": "hello"}], + "stream": False + } + r = await client.post(url, json=payload, headers=headers) + print(f"Status: {r.status_code}") + print(f"Response: {r.text[:500]}") + +if __name__ == "__main__": + asyncio.run(test_dify_proxy()) diff --git a/scripts/debug/test_find.py b/scripts/debug/test_find.py new file mode 100644 index 0000000..f018253 --- /dev/null +++ b/scripts/debug/test_find.py @@ -0,0 +1,25 @@ +import asyncio +import sys +sys.path.insert(0, '/app') + +async def test(): + from app.services.graph_query_service import get_graph_query_service + from app.services.neo4j_client import get_neo4j_client + + neo4j_client = await get_neo4j_client() + graph_service = await get_graph_query_service(neo4j_client) + + keyword = "打印机驱动安装" + print(f"Testing find_issues_by_keyword for: {keyword}") + + try: + issues = await neo4j_client.find_issues_by_keyword(keyword, limit=5) + print(f"Issues found: {len(issues)}") + for issue in issues: + print(f" - uuid: {issue.uuid}, name: {issue.name}") + except Exception as e: + print(f"Error: {e}") + import traceback + traceback.print_exc() + +asyncio.run(test()) diff --git a/scripts/debug/test_full.py b/scripts/debug/test_full.py new file mode 100644 index 0000000..d45d531 --- /dev/null +++ b/scripts/debug/test_full.py @@ -0,0 +1,48 @@ +import asyncio +import sys +sys.path.insert(0, '/app') + +async def test(): + from app.services.graph_query_service import get_graph_query_service + from app.services.neo4j_client import get_neo4j_client + + neo4j_client = await get_neo4j_client() + graph_service = await get_graph_query_service(neo4j_client) + + keyword = "打印机驱动安装" + print(f"=== Full test for keyword: {keyword} ===") + + # Step 1: find_issues_by_keyword + issues = await neo4j_client.find_issues_by_keyword(keyword, limit=5) + print(f"1. find_issues_by_keyword: {len(issues)} issues") + if not issues: + print("No issues found!") + return + + issue = issues[0] + print(f" Issue: uuid={issue.uuid}, name={issue.name}") + + # Step 2: find_actions_by_issue + print(f"2. Calling find_actions_by_issue with uuid={issue.uuid}") + actions = await neo4j_client.find_actions_by_issue(issue.uuid) + print(f" find_actions_by_issue: {len(actions)} actions") + + if not actions: + print("No actions found!") + return + + action = actions[0] + print(f" Action: name={action.name}, description={action.description[:30]}...") + + # Step 3: Build result + from app.services.graph_query_service import SolutionResult + result = SolutionResult( + issue_name=issue.name, + issue_uuid=issue.uuid, + solution=action.description or action.name, + action_name=action.name, + confidence=0.8 + ) + print(f"3. SolutionResult: {result}") + +asyncio.run(test()) diff --git a/scripts/debug/test_graph_query.py b/scripts/debug/test_graph_query.py new file mode 100644 index 0000000..b3213cb --- /dev/null +++ b/scripts/debug/test_graph_query.py @@ -0,0 +1,29 @@ +import asyncio +import sys +sys.path.insert(0, '/app') + +async def test(): + from app.services.graph_query_service import get_graph_query_service + from app.services.neo4j_client import get_neo4j_client + + neo4j_client = await get_neo4j_client() + print(f'Neo4j client: {neo4j_client}') + + if neo4j_client: + graph_service = await get_graph_query_service(neo4j_client) + print(f'Graph service: {graph_service}') + + # 测试查询 + question = "打印机驱动安装" + print(f'Querying: {question}') + result = await graph_service.find_solution_by_question(question) + print(f'Result: {result}') + + if result: + print(f"图谱命中! action_name={result.action_name}, solution={result.solution[:50]}") + else: + print("图谱未命中") + else: + print('Neo4j client is None') + +asyncio.run(test()) diff --git a/scripts/debug/test_keyword.py b/scripts/debug/test_keyword.py new file mode 100644 index 0000000..cda7e7c --- /dev/null +++ b/scripts/debug/test_keyword.py @@ -0,0 +1,9 @@ +import sys +sys.path.insert(0, 'backend') +from app.services.asset_recommend_service import AssetRecommendService + +svc = AssetRecommendService() +cards = svc.match_keywords('VPN连不上怎么办') +print(f'Found {len(cards)} cards') +for c in cards: + print(f' - {c.title}: {c.description}') diff --git a/scripts/debug/test_keyword_container.py b/scripts/debug/test_keyword_container.py new file mode 100644 index 0000000..71bd036 --- /dev/null +++ b/scripts/debug/test_keyword_container.py @@ -0,0 +1,9 @@ +import sys +sys.path.insert(0, '/app') +from app.services.asset_recommend_service import AssetRecommendService + +svc = AssetRecommendService() +cards = svc.match_keywords('VPN连不上怎么办') +print(f'Found {len(cards)} cards') +for c in cards: + print(f' - {c.title}: {c.description}') diff --git a/scripts/debug/test_neo4j.py b/scripts/debug/test_neo4j.py new file mode 100644 index 0000000..46d8bd5 --- /dev/null +++ b/scripts/debug/test_neo4j.py @@ -0,0 +1,16 @@ +import asyncio +import sys +sys.path.insert(0, '/app') + +from app.services.neo4j_client import get_neo4j_client + +async def test(): + client = await get_neo4j_client() + print(f'Neo4j client: {client}') + if client: + healthy = await client.health_check() + print(f'Health check: {healthy}') + else: + print('Neo4j client is None') + +asyncio.run(test()) diff --git a/scripts/debug/test_neo4j_conn.py b/scripts/debug/test_neo4j_conn.py new file mode 100644 index 0000000..e0ba1a3 --- /dev/null +++ b/scripts/debug/test_neo4j_conn.py @@ -0,0 +1,18 @@ +import asyncio +import sys +sys.path.insert(0, '/app') + +async def test(): + from app.services.neo4j_client import get_neo4j_client + client = await get_neo4j_client() + print(f'Neo4j client: {client}') + if client: + try: + healthy = await client.health_check() + print(f'Health check: {healthy}') + except Exception as e: + print(f'Health check error: {e}') + else: + print('Neo4j client is None - connection failed') + +asyncio.run(test()) diff --git a/scripts/debug/test_old_key.py b/scripts/debug/test_old_key.py new file mode 100644 index 0000000..297a2a5 --- /dev/null +++ b/scripts/debug/test_old_key.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +"""Test old Dify key - show full response""" +import httpx +import json +import time + +base_url = "http://yw-dify.dc.servyou-it.com" +api_key = "app-UaTWYdBSwN6VktKQlbh5YN5H" + +url = f"{base_url}/v1/chat-messages" +headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json" +} +payload = { + "inputs": {}, + "query": "密码忘记了怎么办", + "response_mode": "blocking", + "user": "test_verify" +} + +print(f"=== Testing old key with full response ===") +start = time.time() +try: + with httpx.Client(timeout=httpx.Timeout(30.0)) as client: + resp = client.post(url, headers=headers, json=payload) + elapsed = (time.time() - start) * 1000 + print(f"Status: {resp.status_code}, Time: {elapsed:.0f}ms") + print() + + data = resp.json() + print(f"conversation_id: {data.get('conversation_id', 'N/A')}") + answer = data.get('answer', '') + print(f"answer (first 800 chars):") + print(answer[:800]) + print() + + # Try JSON parse + try: + parsed = json.loads(answer) + print("=== JSON Parse: SUCCESS ===") + print(f"Keys: {list(parsed.keys())}") + for k, v in parsed.items(): + print(f" {k}: {str(v)[:200]}") + except json.JSONDecodeError as e: + print(f"=== JSON Parse: FAILED ===") + print(f"Error: {e}") + print("Answer is plain text, not JSON") + +except Exception as e: + print(f"Exception: {type(e).__name__}: {e}") diff --git a/scripts/debug/update_mfa.sh b/scripts/debug/update_mfa.sh new file mode 100644 index 0000000..45071c1 --- /dev/null +++ b/scripts/debug/update_mfa.sh @@ -0,0 +1,2 @@ +UPDATE agents SET mfa_enabled = true, mfa_secret = 'JBSWY3DPEHPK3PXP' WHERE user_id = 'sxn'; +SELECT user_id, mfa_enabled, mfa_secret FROM agents WHERE user_id = 'sxn'; diff --git a/scripts/debug/update_mfa.sql b/scripts/debug/update_mfa.sql new file mode 100644 index 0000000..ce35765 --- /dev/null +++ b/scripts/debug/update_mfa.sql @@ -0,0 +1 @@ +UPDATE agents SET mfa_enabled = true, mfa_secret = 'JBSWY3DPEHPK3PXP' WHERE user_id = 'sxn' \ No newline at end of file diff --git a/scripts/debug/update_offline.sql b/scripts/debug/update_offline.sql new file mode 100644 index 0000000..0ce74b8 --- /dev/null +++ b/scripts/debug/update_offline.sql @@ -0,0 +1 @@ +UPDATE agents SET status = 'offline'; diff --git a/scripts/debug_redis.py b/scripts/debug_redis.py new file mode 100644 index 0000000..1a40d18 --- /dev/null +++ b/scripts/debug_redis.py @@ -0,0 +1,45 @@ +import redis +import json +import secrets +import sys + +REDIS_PWD = "WKzl7jgKTkeWxFWGIBXfzokm59Gvfr76" + +try: + r = redis.Redis(host="redis", port=6379, password=REDIS_PWD, db=0) + print("Redis connect OK") + + # Ping + print(f"PING: {r.ping()}") + + # Test write + r.setex("test:debug_token", 60, "test_value") + print(f"GET: {r.get('test:debug_token')}") + + # 写 token + token = secrets.token_urlsafe(32) + user_info = { + "employee_id": "sxn", + "username": "admin", + "name": "宋献", + "role": "admin", + "department": "IT支持组", + "login_source": "test", + "login_method": "test_script", + "last_active": "2026-07-28T02:00:00", + } + r.setex(f"user:token:{token}", 8 * 3600, json.dumps(user_info, ensure_ascii=False)) + print(f"Token written: {token}") + + # 验证 + val = r.get(f"user:token:{token}") + print(f"Stored: {val[:80] if val else 'None'}") + + # 输出token到文件 + with open("/tmp/test_token.txt", "w") as f: + f.write(token) + print(f"Token saved to /tmp/test_token.txt") + +except Exception as e: + print(f"FAIL: {e}", file=sys.stderr) + sys.exit(1) diff --git a/scripts/deploy-v1.2-staging.ps1 b/scripts/deploy-v1.2-staging.ps1 new file mode 100644 index 0000000..1f6c61d --- /dev/null +++ b/scripts/deploy-v1.2-staging.ps1 @@ -0,0 +1,175 @@ +# ============================================================================ +# REQ-会话-001 v1.2 staging 环境一键部署脚本 +# 用途:从本地代码 commit 到 H5 staging 环境上线全流程 +# 用法:在 Windows Terminal(PowerShell)执行 `.\deploy-v1.2-staging.ps1` +# 前提:v2_ops.py status 显示 cache 有效,否则先跑 `login` +# 注意:staging 与 prod 是不同资产,需独立登录 cache +# ============================================================================ + +#Requires -Version 5.1 + +# ============================================================================ +# ⚠️ 必填参数区(请确认 staging 资产信息) +# ============================================================================ +$STAGING_ASSET_NAME = "" # 例:hz-oa-ai-g-dataquery-staging-XX-XX-XX +$STAGING_SYSTEM_USER = "" # 例:staging admin 用户名(建议与 prod 一致:"生产环境admin用户" 格式) +$STAGING_H5_DIR = "" # 例:/opt/wecom-it-desk-staging/frontend-h5 或 /app/frontend-h5 +$STAGING_H5_URL = "" # 例:https://staging-itsupport.servyou.com.cn/h5/ + +# 公共配置 +$WORK_DIR = "D:\资料\03-项目开发\wecom_it_smart_desk" +$ASCII_COPY = "D:\dev\wecom" +$FRONTEND_BUILD_DIR = "$ASCII_COPY\src\frontend-h5" +$PACKAGE_DIR = "$ASCII_COPY\packages" +$PYTHON_BIN = "C:\Users\simon\.workbuddy\binaries\python\versions\3.13.12\python.exe" +$V2_OPS = "C:\Users\simon\.workbuddy\skills\jumpserver-V2\scripts\v2_ops.py" +$TMP_ZIP_NAME = "h5-dist-v1.2.zip" + +# ============================================================================ +# 参数校验 +# ============================================================================ +$missingParams = @() +if ([string]::IsNullOrWhiteSpace($STAGING_ASSET_NAME)) { $missingParams += "STAGING_ASSET_NAME" } +if ([string]::IsNullOrWhiteSpace($STAGING_SYSTEM_USER)) { $missingParams += "STAGING_SYSTEM_USER" } +if ([string]::IsNullOrWhiteSpace($STAGING_H5_DIR)) { $missingParams += "STAGING_H5_DIR" } +if ([string]::IsNullOrWhiteSpace($STAGING_H5_URL)) { $missingParams += "STAGING_H5_URL" } + +if ($missingParams.Count -gt 0) { + Write-Host "`n❌ 缺少必填参数:" -ForegroundColor Red + foreach ($p in $missingParams) { + Write-Host " - `$p" -ForegroundColor Yellow + } + Write-Host "`n请编辑本脚本顶部"配置区"填入 staging 参数后重新执行" -ForegroundColor Yellow + Write-Host "参数获取方式:登录 JumpServer ���台查看资产清单,或问 ops 同事" -ForegroundColor Gray + exit 1 +} + +# ============================================================================ +# 确认这是 staging 操作(避免误部署 prod) +# ============================================================================ +Write-Host "`n⚠️ 即将部署到 STAGING 环境" -ForegroundColor Yellow +Write-Host " 资产: $STAGING_ASSET_NAME" -ForegroundColor Gray +Write-Host " 用户: $STAGING_SYSTEM_USER" -ForegroundColor Gray +Write-Host " 目录: $STAGING_H5_DIR" -ForegroundColor Gray +Write-Host " URL: $STAGING_H5_URL" -ForegroundColor Gray +$confirm = Read-Host "`n确认继续?(yes/no)" +if ($confirm -ne "yes") { + Write-Host " ❌ 已取消" -ForegroundColor Yellow + exit 0 +} + +# ============================================================================ +# Step 1:本地代码 commit(同 prod 流程) +# ============================================================================ +Write-Host "`n[Step 1/6] 本地代码 commit..." -ForegroundColor Cyan +Set-Location $WORK_DIR + +$gitStatus = git status --short +if ([string]::IsNullOrWhiteSpace($gitStatus)) { + Write-Host " ℹ️ 无新改动(可能已 commit),跳过 commit 步骤" -ForegroundColor Yellow +} else { + Write-Host " 待提交文件:" -ForegroundColor Gray + Write-Host $gitStatus + git add -A + git commit -M "[REQ-会话-001] 员工结束会话 v1.2:6 态按钮 + 4 种引导语 + 顶部按钮 AI 场景互斥 + 重新打开按钮 + +- InputBar.vue: 6 态扩展(移除 hidden / 恢复 end / 新增 reopen) +- ChatPanel.vue: 顶部按钮 v-show + 文案修订 +- conversation.ts (store): showHeaderExitBtn + canReopen + reopenCurrentConversation +- conversation.ts (api): ConversationInfo + resolved_at +- inputBarGuideText.ts: 新增 helper(避免 P1 Bug 回归) +- 测试: 23 store + 15 helper = 38 用例全 PASS + +QA 第 1 轮发现 P1 Bug(InputBar.vue:337 误用 'active' 而非 'ai_handling'), +抽离 helper 修复并加 2 个回归保护用例。AC1-AC11 全部 PASS(含修复后)。 + +部署目标:staging 环境验证" + if ($LASTEXITCODE -ne 0) { + Write-Host " ❌ commit 失败" -ForegroundColor Red + exit 1 + } + Write-Host " ✅ commit 成功" -ForegroundColor Green +} + +# ============================================================================ +# Step 2:代码同步 + 前端 build + 打包 +# ============================================================================ +Write-Host "`n[Step 2/6] 同步代码到 ASCII 副本 + 前端 build..." -ForegroundColor Cyan + +if (-not (Test-Path $ASCII_COPY)) { + Write-Host " ❌ ASCII 副本路径不存在: $ASCII_COPY" -ForegroundColor Red + exit 1 +} + +robocopy "$WORK_DIR\src\frontend-h5\src" "$FRONTEND_BUILD_DIR\src" /MIR /XD node_modules dist /NFL /NDL /NJH /NJS | Out-Null +robocopy "$WORK_DIR\src\frontend-h5\package.json" "$FRONTEND_BUILD_DIR\package.json" /NFL /NDL /NJH /NJS | Out-Null + +Set-Location $FRONTEND_BUILD_DIR +npm run build 2>&1 | Out-Null +if ($LASTEXITCODE -ne 0) { + Write-Host " ❌ npm run build 失败" -ForegroundColor Red + exit 1 +} +Write-Host " ✅ 前端 build 成功" -ForegroundColor Green + +if (-not (Test-Path $PACKAGE_DIR)) { + New-Item -ItemType Directory -Path $PACKAGE_DIR -Force | Out-Null +} +Compress-Archive -Path "$FRONTEND_BUILD_DIR\dist" -DestinationPath "$PACKAGE_DIR\$TMP_ZIP_NAME" -Force +Write-Host " ✅ 打包完成: $PACKAGE_DIR\$TMP_ZIP_NAME" -ForegroundColor Green + +# ============================================================================ +# Step 3:上传到 SFTP 虚拟路径 +# ============================================================================ +Write-Host "`n[Step 3/6] 上传到 staging SFTP 虚拟路径..." -ForegroundColor Cyan +Write-Host " ⚠️ 注意:staging 是独立资产,需独立 cache" -ForegroundColor Yellow +Write-Host " 如果 cache 失效,先跑:& $PYTHON_BIN $V2_OPS login" -ForegroundColor Yellow + +& $PYTHON_BIN $V2_OPS upload "$PACKAGE_DIR\$TMP_ZIP_NAME" $TMP_ZIP_NAME +if ($LASTEXITCODE -ne 0) { + Write-Host " ❌ 上传失败" -ForegroundColor Red + exit 1 +} +Write-Host " ✅ 上传成功" -ForegroundColor Green + +# ============================================================================ +# Step 4:staging 服务器侧部署 +# ============================================================================ +Write-Host "`n[Step 4/6] staging 服务器侧部署..." -ForegroundColor Cyan + +$deployCmdsPath = "$ASCII_COPY\deploy_cmds_v1.2-staging.txt" +if (-not (Test-Path $deployCmdsPath)) { + Write-Host " ❌ 服务器命令文件不存在: $deployCmdsPath" -ForegroundColor Red + Write-Host " 请创建 deploy_cmds_v1.2-staging.txt 后重试" -ForegroundColor Yellow + exit 1 +} + +& $PYTHON_BIN $V2_OPS batch $deployCmdsPath +if ($LASTEXITCODE -ne 0) { + Write-Host " ❌ staging 部署失败,可执行回滚" -ForegroundColor Red + exit 1 +} +Write-Host " ✅ staging 服务器部署成功" -ForegroundColor Green + +# ============================================================================ +# Step 5:staging 验证 +# ============================================================================ +Write-Host "`n[Step 5/6] 验证 staging H5 URL..." -ForegroundColor Cyan +& $PYTHON_BIN $V2_OPS exec "curl -I $STAGING_H5_URL" +if ($LASTEXITCODE -ne 0) { + Write-Host " ⚠️ HTTP 验证失败,请人工检查" -ForegroundColor Yellow +} + +& $PYTHON_BIN $V2_OPS exec "ls $STAGING_H5_DIR/dist/assets/ | head -20" + +# ============================================================================ +# Step 6:业务验证清单 +# ============================================================================ +Write-Host "`n[Step 6/6] staging 部署完成 - 业务验证清单" -ForegroundColor Cyan +Write-Host " 请打开 $STAGING_H5_URL 浏览器测试:" -ForegroundColor Gray +Write-Host " 1. AI 对话 <3 轮 → 引导语'请继续描述您的问题或需求'显示 ✅" -ForegroundColor Gray +Write-Host " 2. AI 对话 ≥3 轮 → 按钮'🎧 人工坐席' + 顶部红色退出按钮可见 ✅" -ForegroundColor Gray +Write-Host " 3. 坐席服务中(mock)→ 操作按钮'📴 结束咨询' + 顶部按钮隐藏 ✅" -ForegroundColor Gray +Write-Host " 4. 会话关闭 24h 内 → 操作按钮'🔄 重新打开'(蓝色)显示 ✅" -ForegroundColor Gray +Write-Host "`n 📞 staging 验证通过后,再跑 prod 部署:" -ForegroundColor Yellow +Write-Host " cd D:\dev\wecom && .\deploy-v1.2.ps1" -ForegroundColor Yellow \ No newline at end of file diff --git a/scripts/deploy-v1.2.ps1 b/scripts/deploy-v1.2.ps1 new file mode 100644 index 0000000..2d2be4b --- /dev/null +++ b/scripts/deploy-v1.2.ps1 @@ -0,0 +1,133 @@ +# ============================================================================ +# REQ-会话-001 v1.2 一键部署脚本 +# 用途:从本地代码 commit 到 H5 生产环境上线全流程 +# 用法:在 Windows Terminal(PowerShell)执行 `.\deploy-v1.2.ps1` +# 前提:v2_ops.py status 显示 cache 有效,否则先跑 `login` +# ============================================================================ + +#Requires -Version 5.1 + +# 配置区(按需修改) =========================================================== +$WORK_DIR = "D:\资料\03-项目开发\wecom_it_smart_desk" +$ASCII_COPY = "D:\dev\wecom" +$FRONTEND_BUILD_DIR = "$ASCII_COPY\src\frontend-h5" +$PACKAGE_DIR = "$ASCII_COPY\packages" +$PYTHON_BIN = "C:\Users\simon\.workbuddy\binaries\python\versions\3.13.12\python.exe" +$V2_OPS = "C:\Users\simon\.workbuddy\skills\jumpserver-V2\scripts\v2_ops.py" +$DEPLOY_REMOTE_DIR = "/opt/wecom-it-desk/frontend-h5" +$TMP_ZIP_NAME = "h5-dist-v1.2.zip" +$H5_URL = "https://itsupport.servyou.com.cn/h5/" + +# ============================================================================ +# Step 1:本地代码 commit +# ============================================================================ +Write-Host "`n[Step 1/6] 本地代码 commit..." -ForegroundColor Cyan +Set-Location $WORK_DIR + +$gitStatus = git status --short +if ([string]::IsNullOrWhiteSpace($gitStatus)) { + Write-Host " ℹ️ 无新改动(可能已 commit),跳过 commit 步骤" -ForegroundColor Yellow +} else { + Write-Host " 待提交文件:" -ForegroundColor Gray + Write-Host $gitStatus + git add -A + git commit -M "[REQ-会话-001] 员工结束会话 v1.2:6 态按钮 + 4 种引导语 + 顶部按钮 AI 场景互斥 + 重新打开按钮 + +- InputBar.vue: 6 态扩展(移除 hidden / 恢复 end / 新增 reopen) +- ChatPanel.vue: 顶部按钮 v-show + 文案修订 +- conversation.ts (store): showHeaderExitBtn + canReopen + reopenCurrentConversation +- conversation.ts (api): ConversationInfo + resolved_at +- inputBarGuideText.ts: 新增 helper(避免 P1 Bug 回归) +- 测试: 23 store + 15 helper = 38 用例全 PASS + +QA 第 1 轮发现 P1 Bug(InputBar.vue:337 误用 'active' 而非 'ai_handling'), +抽离 helper 修复并加 2 个回归保护用例。AC1-AC11 全部 PASS(含修复后)。" + if ($LASTEXITCODE -ne 0) { + Write-Host " ❌ commit 失败,请检查 git 状态" -ForegroundColor Red + exit 1 + } + Write-Host " ✅ commit 成功" -ForegroundColor Green +} + +# ============================================================================ +# Step 2:代码同步到 ASCII 副本 + 前端 build + 打包 +# ============================================================================ +Write-Host "`n[Step 2/6] 同步代码到 ASCII 副本 + 前端 build..." -ForegroundColor Cyan + +if (-not (Test-Path $ASCII_COPY)) { + Write-Host " ❌ ASCII 副本路径不存在: $ASCII_COPY" -ForegroundColor Red + exit 1 +} + +# 同步 src/frontend-h5/src(排除 node_modules 和 dist) +robocopy "$WORK_DIR\src\frontend-h5\src" "$FRONTEND_BUILD_DIR\src" /MIR /XD node_modules dist /NFL /NDL /NJH /NJS | Out-Null +robocopy "$WORK_DIR\src\frontend-h5\package.json" "$FRONTEND_BUILD_DIR\package.json" /NFL /NDL /NJH /NJS | Out-Null + +# build +Set-Location $FRONTEND_BUILD_DIR +npm run build 2>&1 | Out-Null +if ($LASTEXITCODE -ne 0) { + Write-Host " ❌ npm run build 失败" -ForegroundColor Red + exit 1 +} +Write-Host " ✅ 前端 build 成功" -ForegroundColor Green + +# 打包 +if (-not (Test-Path $PACKAGE_DIR)) { + New-Item -ItemType Directory -Path $PACKAGE_DIR -Force | Out-Null +} +Compress-Archive -Path "$FRONTEND_BUILD_DIR\dist" -DestinationPath "$PACKAGE_DIR\$TMP_ZIP_NAME" -Force +Write-Host " ✅ 打包完成: $PACKAGE_DIR\$TMP_ZIP_NAME" -ForegroundColor Green + +# ============================================================================ +# Step 3:上传到 SFTP 虚拟路径 /tmp +# ============================================================================ +Write-Host "`n[Step 3/6] 上传到 SFTP 虚拟路径..." -ForegroundColor Cyan +& $PYTHON_BIN $V2_OPS upload "$PACKAGE_DIR\$TMP_ZIP_NAME" $TMP_ZIP_NAME +if ($LASTEXITCODE -ne 0) { + Write-Host " ❌ 上传失败" -ForegroundColor Red + exit 1 +} +Write-Host " ✅ 上传成功" -ForegroundColor Green + +# ============================================================================ +# Step 4:服务器侧部署 +# ============================================================================ +Write-Host "`n[Step 4/6] 服务器侧部署..." -ForegroundColor Cyan + +$deployCmdsPath = "$ASCII_COPY\deploy_cmds_v1.2.txt" +if (-not (Test-Path $deployCmdsPath)) { + Write-Host " ❌ 服务器命令文件不存在: $deployCmdsPath" -ForegroundColor Red + Write-Host " 请先创建 deploy_cmds_v1.2.txt(参考部署清单 Step 4)" -ForegroundColor Yellow + exit 1 +} + +& $PYTHON_BIN $V2_OPS batch $deployCmdsPath +if ($LASTEXITCODE -ne 0) { + Write-Host " ❌ 服务器部署失败,可执行回滚" -ForegroundColor Red + exit 1 +} +Write-Host " ✅ 服务器部署成功" -ForegroundColor Green + +# ============================================================================ +# Step 5:验证 HTTP 200 +# ============================================================================ +Write-Host "`n[Step 5/6] 验证 H5 URL..." -ForegroundColor Cyan +& $PYTHON_BIN $V2_OPS exec "curl -I $H5_URL" +if ($LASTEXITCODE -ne 0) { + Write-Host " ⚠️ HTTP 验证失败,请人工检查" -ForegroundColor Yellow +} + +# 检查前端 dist 文件 +& $PYTHON_BIN $V2_OPS exec "ls $DEPLOY_REMOTE_DIR/dist/assets/ | head -20" + +# ============================================================================ +# Step 6:完成 +# ============================================================================ +Write-Host "`n[Step 6/6] 部署完成" -ForegroundColor Cyan +Write-Host " 📋 请按以下清单验证业务:" -ForegroundColor Gray +Write-Host " 1. H5 打开 → AI 对话 <3 轮 → 引导语'请继续描述您的问题或需求'显示 ✅" -ForegroundColor Gray +Write-Host " 2. AI 对话 ≥3 轮 → 按钮'🎧 人工坐席' + 顶部红色退出按钮可见 ✅" -ForegroundColor Gray +Write-Host " 3. 坐席服务中(mock)→ 操作按钮'📴 结束咨询' + 顶部按钮隐藏 ✅" -ForegroundColor Gray +Write-Host " 📞 如发现问题立即回滚:" -ForegroundColor Yellow +Write-Host " & $PYTHON_BIN $V2_OPS exec `"rm -rf $DEPLOY_REMOTE_DIR/dist && mv $DEPLOY_REMOTE_DIR/dist.v1.1.bak $DEPLOY_REMOTE_DIR/dist`" -ForegroundColor Yellow \ No newline at end of file diff --git a/scripts/deploy/deploy-p2-p3.py b/scripts/deploy/deploy-p2-p3.py new file mode 100644 index 0000000..8a7d060 --- /dev/null +++ b/scripts/deploy/deploy-p2-p3.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +"""打包 P2/P3 部署文件""" +import os +import tarfile + +# 待上传的后端文件 +backend_files = [ + 'backend/app/utils/token_counter.py', + 'backend/app/services/automation/context_compressor.py', + 'backend/app/services/automation/snapshot_service.py', + 'backend/app/services/automation/correction_service.py', + 'backend/alembic/versions/049_add_p2_p3_tables.py', + 'backend/tests/test_p2_p3.py', + # 修改的文件 + 'backend/app/models/automation.py', + 'backend/app/constants.py', + 'backend/app/config.py', + 'backend/app/schemas/automation.py', + 'backend/app/api/automation.py', +] + +# 创建临时打包 +with tarfile.open('backend-p2-p3-deploy.tar.gz', 'w:gz') as tar: + for f in backend_files: + if os.path.exists(f): + tar.add(f, arcname=f) + print(f'Added: {f}') + else: + print(f'NOT FOUND: {f}') + +print('Done!') diff --git a/scripts/deploy/deploy_ai_source_identifier.py b/scripts/deploy/deploy_ai_source_identifier.py new file mode 100644 index 0000000..bae345b --- /dev/null +++ b/scripts/deploy/deploy_ai_source_identifier.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +""" +AI回复来源标识功能 - 前端部署脚本 +同时部署 H5 和 Agent 前端 +""" +import os +import sys +import time +import subprocess +import base64 + +# 读取 jms_ops 模块 +sys.path.insert(0, r"C:\Users\simon\.workbuddy\skills\jumpserver-ops\scripts") +from jms_ops import PlinkSession, get_connection_tokens + + +def deploy_frontend(local_dir, remote_dir, name): + """部署单个前端""" + print(f"\n📦 开始上传 {name} ...") + + # 压缩本地 dist 目录 + print(f" 压缩 {name} dist 目录...") + tar_file = f"/tmp/{name.lower()}-dist.tar.gz" + # 切换到 dist 的父目录,然后压缩 dist 目录 + subprocess.run(f'cd "{local_dir}" && tar -czf {tar_file} .', + shell=True, check=True) + + # 读取 tar 文件并进行 base64 编码 + print(f" Base64 编码...") + with open(tar_file, 'rb') as f: + content = f.read() + b64_content = base64.b64encode(content).decode('ascii') + + # 获取连接令牌 + tokens = get_connection_tokens(1) + if not tokens: + print("❌ 获取 token 失败") + return False + + token_id, token_secret = tokens[0] + + # 创建会话并上传 + session = PlinkSession(f"JMS-{token_id}", token_secret) + if not session.connect(): + print("❌ 会话启动失败") + return False + + # 先清空目标目录 + print(f" 清空目标目录...") + session.run_command(f"rm -rf {remote_dir}/dist", timeout=30) + + # 上传 base64 内容 + print(f" 上传文件...") + chunk_size = 3800 + for i in range(0, len(b64_content), chunk_size): + chunk = b64_content[i:i+chunk_size] + decode_cmd = f'echo "{chunk}" | base64 -d >> /tmp/{name.lower()}-dist-upload.tar.gz' + session.run_command(decode_cmd, timeout=30) + + # 解压 + print(f" 解压文件...") + session.run_command(f"cd /tmp && tar -xzf {name.lower()}-dist-upload.tar.gz -C {remote_dir}", timeout=60) + + # 清理临时文件 + session.run_command(f"rm -f /tmp/{name.lower()}-dist.tar.gz /tmp/{name.lower()}-dist-upload.tar.gz", timeout=30) + + session.close() + print(f"✅ {name} 部署完成!") + return True + + +def main(): + print("=" * 50) + print("🚀 AI回复来源标识 - 前端部署") + print("=" * 50) + + # H5 部署 + h5_local = r"D:\资料\03-项目开发\wecom_it_smart_desk\frontend-h5\dist" + h5_remote = "/opt/wecom-it-desk/frontend-h5" + deploy_frontend(h5_local, h5_remote, "H5") + + # Agent 部署 + agent_local = r"D:\资料\03-项目开发\wecom_it_smart_desk\frontend-agent\dist" + agent_remote = "/opt/wecom-it-desk/frontend-agent" + deploy_frontend(agent_local, agent_remote, "Agent") + + # 重启 nginx + print("\n" + "=" * 50) + print("🔄 重启 nginx...") + tokens = get_connection_tokens(1) + if not tokens: + print("❌ 获取 token 失败") + return + token_id, token_secret = tokens[0] + session = PlinkSession(f"JMS-{token_id}", token_secret) + if not session.connect(): + print("❌ 会话启动失败") + return + session.run_command("cd /opt/wecom-it-desk && docker compose restart nginx", timeout=60) + session.close() + + print("\n✅ 所有部署完成!") + print(" - H5: https://itportal.servyou.com.cn/h5/") + print(" - Agent: https://itportal.servyou.com.cn/itagent/") + + +if __name__ == "__main__": + main() diff --git a/scripts/deploy/deploy_frontend.py b/scripts/deploy/deploy_frontend.py new file mode 100644 index 0000000..5bbac3b --- /dev/null +++ b/scripts/deploy/deploy_frontend.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +"""重新打包前端部署文件 - 使用正确目录结构""" +import os +import tarfile + +os.chdir('D:/资料/03-项目开发/wecom_it_smart_desk') + +# 重新打包 frontend-agent +print("Repacking frontend-agent...") +if os.path.exists('frontend-agent/dist'): + with tarfile.open('frontend-agent-dist-v2.tar.gz', 'w:gz') as tar: + tar.add('frontend-agent/dist', arcname='dist') + print('Created: frontend-agent-dist-v2.tar.gz') + +# 重新打包 frontend-h5 +print("Repacking frontend-h5...") +if os.path.exists('frontend-h5/dist'): + with tarfile.open('frontend-h5-dist-v2.tar.gz', 'w:gz') as tar: + tar.add('frontend-h5/dist', arcname='dist') + print('Created: frontend-h5-dist-v2.tar.gz') + +print('Done!') diff --git a/scripts/deploy/redeploy.py b/scripts/deploy/redeploy.py new file mode 100644 index 0000000..572a68a --- /dev/null +++ b/scripts/deploy/redeploy.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 +"""重新打包部署前端""" +import os +import tarfile + +os.chdir('D:/资料/03-项目开发/wecom_it_smart_desk') + +# 重新打包 frontend-agent +print("Repacking frontend-agent...") +if os.path.exists('frontend-agent/dist'): + with tarfile.open('frontend-agent-dist-v3.tar.gz', 'w:gz') as tar: + tar.add('frontend-agent/dist', arcname='dist') + print('Created: frontend-agent-dist-v3.tar.gz') +print('Done!') diff --git a/scripts/deploy_cmds_v1.2-staging.txt b/scripts/deploy_cmds_v1.2-staging.txt new file mode 100644 index 0000000..b9fb760 --- /dev/null +++ b/scripts/deploy_cmds_v1.2-staging.txt @@ -0,0 +1,24 @@ +# REQ-会话-001 v1.2 staging 服务器侧部署命令 +# 用途:备份旧 dist + 解压新 dist + 验证文件 +# 用法:在 Windows Terminal 执行: +# & "C:\Users\simon\.workbuddy\binaries\python\versions\3.13.12\python.exe" "C:\Users\simon\.workbuddy\skills\jumpserver-V2\scripts\v2_ops.py" batch "D:\dev\wecom\deploy_cmds_v1.2-staging.txt" +# ⚠️ 路径需根据 staging 实际目录调整(修改脚本顶部 STAGING_H5_DIR 后命令路径也要改) + +# 1. 备份旧 dist +mv /opt/wecom-it-desk-staging/frontend-h5/dist /opt/wecom-it-desk-staging/frontend-h5/dist.v1.1.bak + +# 2. 清理旧归档(保留最近 1 个备份) +rm -rf /opt/wecom-it-desk-staging/frontend-h5/dist.archive + +# 3. 解压新 dist 到目标目录 +cd /opt/wecom-it-desk-staging/frontend-h5 +unzip -o /tmp/h5-dist-v1.2.zip + +# 4. 验证文件大小 +du -sh /opt/wecom-it-desk-staging/frontend-h5/dist + +# 5. 列出 index.html 确认新 dist 生效 +ls -la /opt/wecom-it-desk-staging/frontend-h5/dist/index.html + +# 6. 检查 dist 内 assets 文件(v1.2 标志) +ls /opt/wecom-it-desk-staging/frontend-h5/dist/assets/ | head -20 \ No newline at end of file diff --git a/scripts/deploy_cmds_v1.2.txt b/scripts/deploy_cmds_v1.2.txt new file mode 100644 index 0000000..223bca1 --- /dev/null +++ b/scripts/deploy_cmds_v1.2.txt @@ -0,0 +1,24 @@ +# REQ-会话-001 v1.2 服务器侧部署命令 +# 用途:备份旧 dist + 解压新 dist + 验证文件 +# 用法:在 Windows Terminal 执行: +# & "C:\Users\simon\.workbuddy\binaries\python\versions\3.13.12\python.exe" "C:\Users\simon\.workbuddy\skills\jumpserver-V2\scripts\v2_ops.py" batch "D:\dev\wecom\deploy_cmds_v1.2.txt" +# 注意:远程命令禁用 $(...),改用普通管道或批处理文件 + +# 1. 备份旧 dist +mv /opt/wecom-it-desk/frontend-h5/dist /opt/wecom-it-desk/frontend-h5/dist.v1.1.bak + +# 2. 清理旧归档(保留最近 1 个备份) +rm -rf /opt/wecom-it-desk/frontend-h5/dist.archive + +# 3. 解压新 dist 到目标目录 +cd /opt/wecom-it-desk/frontend-h5 +unzip -o /tmp/h5-dist-v1.2.zip + +# 4. 验证文件大小(应与上传时一致) +du -sh /opt/wecom-it-desk/frontend-h5/dist + +# 5. 列出 index.html 确认新 dist 生效 +ls -la /opt/wecom-it-desk/frontend-h5/dist/index.html + +# 6. 检查 dist 内 assets 文件(v1.2 标志) +ls /opt/wecom-it-desk/frontend-h5/dist/assets/ | head -20 \ No newline at end of file diff --git a/scripts/find_admin_user.sql b/scripts/find_admin_user.sql new file mode 100644 index 0000000..3263a0d --- /dev/null +++ b/scripts/find_admin_user.sql @@ -0,0 +1 @@ +SELECT id, user_id, name, role, status FROM agents ORDER BY id LIMIT 10; diff --git a/scripts/fix/fix_neo4j.cypher b/scripts/fix/fix_neo4j.cypher new file mode 100644 index 0000000..fec379f --- /dev/null +++ b/scripts/fix/fix_neo4j.cypher @@ -0,0 +1,17 @@ +// 清除旧数据 +MATCH (n) DETACH DELETE n + +// 重新创建测试数据(带 uuid) +CREATE (i1:Issue {name: "打印机驱动安装", category: "硬件问题", uuid: "issue-001"}) +CREATE (a1:Action {name: "打印机驱动安装步骤", description: "1. 打开控制面板 2. 添加打印机 3. 选择手动添加"}) +CREATE (i1)-[:HAS_ACTION]->(a1) + +CREATE (i2:Issue {name: "网络连不上", category: "网络问题", uuid: "issue-002"}) +CREATE (a2:Action {name: "网络诊断步骤", description: "1. 检查网线 2. 重启路由器 3. 检查IP配置"}) +CREATE (i2)-[:HAS_ACTION]->(a2) + +CREATE (i3:Issue {name: "邮箱无法收发", category: "软件问题", uuid: "issue-003"}) +CREATE (a3:Action {name: "邮箱故障排除", description: "1. 检查网络 2. 清除缓存 3. 重新登录"}) +CREATE (i3)-[:HAS_ACTION]->(a3) + +RETURN "测试数据添加完成" diff --git a/scripts/fix_priority_null.sql b/scripts/fix_priority_null.sql new file mode 100644 index 0000000..936ed56 --- /dev/null +++ b/scripts/fix_priority_null.sql @@ -0,0 +1,21 @@ +-- ============================================================================= +-- 修复 quick_rules 表中 priority 为 NULL 的记录 +-- 触发场景:routing_target 类型的初始化数据未指定 priority 列, +-- 导致 Pydantic 序列化 QuickRuleResponse 时 ValidationError +-- ============================================================================= + +-- 1. 先查看受影响的记录 +SELECT id, rule_type, category, keyword, priority +FROM quick_rules +WHERE priority IS NULL +ORDER BY id; + +-- 2. 回填优先级 +UPDATE quick_rules +SET priority = 0 +WHERE priority IS NULL; + +-- 3. 验证结果 +SELECT COUNT(*) AS null_priority_count +FROM quick_rules +WHERE priority IS NULL; diff --git a/scripts/gen_admin_token.py b/scripts/gen_admin_token.py new file mode 100644 index 0000000..81b89aa --- /dev/null +++ b/scripts/gen_admin_token.py @@ -0,0 +1,44 @@ +"""直接在 Redis 中生成 admin token 用于测试""" +import asyncio +import json +import secrets +import sys +from datetime import datetime +from urllib.parse import unquote + + +async def main(): + import redis.asyncio as redis_async + + # 连接到 Redis 容器(使用生产密码) + redis_url = "redis://:WKzl7jgKTkeWxFWGIBXfzokm59Gvfr76@localhost:6379/0" + redis_client = redis_async.from_url(redis_url) + + # 构造 admin 用户的 token + token = secrets.token_urlsafe(32) + user_info = { + "employee_id": "admin_test_001", + "username": "admin", + "name": "测试管理员", + "role": "admin", + "department": "IT支持组", + "login_source": "test", + "login_method": "test_script", + "last_active": datetime.now().isoformat(), + } + + # 写入 Redis + token_key = f"user:token:{token}" + await redis_client.setex(token_key, 8 * 3600, json.dumps(user_info, ensure_ascii=False)) + + print(f"TOKEN={token}") + + # 验证 + val = await redis_client.get(token_key) + print(f"VERIFIED={val is not None}") + + await redis_client.close() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/scripts/init_quick_rules.sql b/scripts/init_quick_rules.sql new file mode 100644 index 0000000..ed506c4 --- /dev/null +++ b/scripts/init_quick_rules.sql @@ -0,0 +1,110 @@ +-- ============================================================================= +-- 企微IT智能服务台 — 快速规则初始化数据 +-- ============================================================================= +-- 说明:将硬编码的快速规则迁移到数据库 +-- 适用:首次部署 / 从硬编码迁移到数据库 +-- 日期:2026-07-27 +-- ============================================================================= + +-- 清空现有数据(仅首次部署时使用) +-- TRUNCATE TABLE quick_rules RESTART IDENTITY; + +-- -------------------------------------------------------------------------- +-- 1. 打招呼关键词(来自 ai_handler.py) +-- -------------------------------------------------------------------------- +INSERT INTO quick_rules (rule_type, keyword, priority, is_active) VALUES +('greeting', '你好', 10, true), +('greeting', '您好', 9, true), +('greeting', 'hi', 8, true), +('greeting', 'hello', 7, true), +('greeting', '嗨', 6, true), +('greeting', '在吗', 5, true), +('greeting', '在不在', 5, true), +('greeting', '哈喽', 4, true), +('greeting', '早', 3, true), +('greeting', '早上好', 3, true), +('greeting', '下午好', 3, true), +('greeting', '晚上好', 3, true) +ON CONFLICT (rule_type, keyword) DO NOTHING; + +-- -------------------------------------------------------------------------- +-- 2. 业务路由预过滤关键词(来自 routing_service.py) +-- -------------------------------------------------------------------------- +INSERT INTO quick_rules (rule_type, category, keyword, priority, is_active) VALUES +-- 行政 +('routing_prefilter', '行政', '复印机', 5, true), +('routing_prefilter', '行政', '扫描仪', 5, true), +('routing_prefilter', '行政', '保洁', 5, true), +('routing_prefilter', '行政', '名片印刷', 5, true), +-- IT服务 +('routing_prefilter', 'IT服务', '打印机', 10, true), +('routing_prefilter', 'IT服务', '打印', 9, true), +('routing_prefilter', 'IT服务', '电脑', 8, true), +('routing_prefilter', 'IT服务', '网络', 8, true), +('routing_prefilter', 'IT服务', '软件', 7, true), +('routing_prefilter', 'IT服务', '重装', 7, true), +('routing_prefilter', 'IT服务', '装机', 7, true), +('routing_prefilter', 'IT服务', '显示器', 6, true), +('routing_prefilter', 'IT服务', '键盘', 5, true), +('routing_prefilter', 'IT服务', '鼠标', 5, true), +('routing_prefilter', 'IT服务', '耳机', 5, true), +('routing_prefilter', 'IT服务', '投影仪', 5, true), +('routing_prefilter', 'IT服务', '会议设备', 5, true), +-- 人力资源 +('routing_prefilter', '人力资源', '工牌', 8, true), +('routing_prefilter', '人力资源', '考勤', 8, true), +('routing_prefilter', '人力资源', '入职', 7, true), +('routing_prefilter', '人力资源', '离职', 7, true), +('routing_prefilter', '人力资源', '社保', 6, true), +('routing_prefilter', '人力资源', '公积金', 6, true), +('routing_prefilter', '人力资源', '云盘', 5, true), +-- 财务 +('routing_prefilter', '财务', '报销', 10, true), +('routing_prefilter', '财务', '发票', 9, true), +('routing_prefilter', '财务', '借款', 8, true), +('routing_prefilter', '财务', '工资条', 7, true), +-- 法务 +('routing_prefilter', '法务', '合同', 10, true), +('routing_prefilter', '法务', '法务', 10, true), +('routing_prefilter', '法务', '知识产权', 8, true), +-- 行政-物业 +('routing_prefilter', '行政-物业', '空调', 7, true), +('routing_prefilter', '行政-物业', '电梯', 7, true), +('routing_prefilter', '行政-物业', '门禁', 7, true), +('routing_prefilter', '行政-物业', '停车', 7, true) +ON CONFLICT (rule_type, keyword) DO NOTHING; + +-- -------------------------------------------------------------------------- +-- 3. 路由目标配置(来自 routing_service.py ROUTING_TARGETS) +-- -------------------------------------------------------------------------- +INSERT INTO quick_rules (rule_type, category, keyword, extra_data, priority, is_active) VALUES +('routing_target', '行政', '机票酒店前台', + '{"service_name": "机票酒店前台", "description": "受理和咨询机票、酒店、快递、访客、失物招领及行政办公相关事宜", "url": "https://work.weixin.qq.com/nl/innerkfid/ikfCtcYBwAAV00zNJGfmuA0aG1c2qCDnQ"}', + 0, true), +('routing_target', '人力资源', '人力资源共享服务咨询', + '{"service_name": "人力资源共享服务咨询", "description": "咨询和处理人力资源相关问题(工牌/考勤/入离职/社保公积金等)", "url": "https://work.weixin.qq.com/nl/innerkfid/ikfCtcYBwAAvSRL5i5b_Xia8vCmFc2gRw"}', + 0, true), +('routing_target', '财务', '总部报销服务台', + '{"service_name": "总部报销服务台", "description": "咨询和处理总部报销系统相关问题", "url": "https://work.weixin.qq.com/nl/innerkfid/ikfCtcYBwAAG4HC1zWJtPuALPKl2X6jcw"}', + 0, true), +('routing_target', '法务', '行政法务团队', + '{"service_name": "行政法务团队", "description": "可咨询合同、合规、劳动争议、法律案件纠纷、外部检查、其他法律问题", "url": "https://work.weixin.qq.com/nl/innerkfid/ikfCtcYBwAABF746pGAY5WZ0mOTP6kGKA"}', + 0, true), +('routing_target', '行政-物业', '物业服务', + '{"service_name": "物业服务", "description": "咨询和处理物业相关问题(空调/照明/门禁卡/车位/维修等)", "url": "https://work.weixin.qq.com/nl/innerkfid/ikfCtcYBwAAUtkMyOToCZqe42ZBDupVEQ"}', + 0, true), +('routing_target', 'IT服务', 'IT人工坐席', + '{"service_name": "IT人工坐席", "description": "IT支持组人工服务(打印机/电脑/网络/软件安装等)", "url": "https://work.weixin.qq.com/nl/innerkfid/ikfCtcYBwAA人工坐席KFID", "is_agent": true}', + 0, true) +ON CONFLICT (rule_type, keyword) DO NOTHING; + +-- -------------------------------------------------------------------------- +-- 验证数据 +-- -------------------------------------------------------------------------- +SELECT + rule_type, + COUNT(*) as count, + COUNT(CASE WHEN is_active THEN 1 END) as active_count +FROM quick_rules +GROUP BY rule_type +ORDER BY rule_type; diff --git a/scripts/test_quick_rules_api.sh b/scripts/test_quick_rules_api.sh new file mode 100644 index 0000000..6a9abc2 --- /dev/null +++ b/scripts/test_quick_rules_api.sh @@ -0,0 +1,233 @@ +#!/bin/bash +# ============================================================================= +# 企微IT智能服务台 — 快速规则 API 联调测试脚本 +# ============================================================================= +# 用途:独立测试所有 12 个快速规则 API +# 日期:2026-07-27 +# ============================================================================= + +# 默认配置 +BASE_URL="${API_BASE_URL:-http://localhost:8000}" +TOKEN="${API_TOKEN:-}" + +# 颜色 +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +NC='\033[0m' + +# 计数器 +PASS=0 +FAIL=0 + +# -------------------------------------------------------------------------- +# 工具函数 +# -------------------------------------------------------------------------- + +check_response() { + local response="$1" + local expected_code="$2" + local test_name="$3" + + if echo "$response" | grep -q "\"code\":$expected_code"; then + echo -e "${GREEN}✓ PASS${NC}: $test_name" + ((PASS++)) + else + echo -e "${RED}✗ FAIL${NC}: $test_name" + echo "Response: $response" + ((FAIL++)) + fi +} + +# -------------------------------------------------------------------------- +# 0. 健康检查 +# -------------------------------------------------------------------------- +echo -e "${YELLOW}=== 0. 健康检查 ===${NC}" +RESPONSE=$(curl -s -X GET "$BASE_URL/health") +echo "Health: $RESPONSE" + +# -------------------------------------------------------------------------- +# 1. 测试登录(获取 token) +# -------------------------------------------------------------------------- +echo -e "\n${YELLOW}=== 1. 登录 ===${NC}" +echo "请先用浏览器登录获取 token,或使用下面的方式:" + +# 直接测试(需要 token) +if [ -z "$TOKEN" ]; then + echo -e "${RED}错误:请先设置 API_TOKEN 环境变量${NC}" + echo "使用方式:API_TOKEN=your_token bash $0" + exit 1 +fi + +# -------------------------------------------------------------------------- +# 2. 规则列表 +# -------------------------------------------------------------------------- +echo -e "\n${YELLOW}=== 2. 规则列表 ===${NC}" +RESPONSE=$(curl -s -X GET "$BASE_URL/api/admin/quick-rules?page=1&size=10" \ + -H "Authorization: Bearer $TOKEN") +check_response "$RESPONSE" "0" "GET /quick-rules" + +# -------------------------------------------------------------------------- +# 3. 创建规则 +# -------------------------------------------------------------------------- +echo -e "\n${YELLOW}=== 3. 创建规则 ===${NC}" +RESPONSE=$(curl -s -X POST "$BASE_URL/api/admin/quick-rules" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "rule_type": "greeting", + "keyword": "test_你好", + "priority": 10, + "is_active": true + }') +check_response "$RESPONSE" "0" "POST /quick-rules" +RULE_ID=$(echo "$RESPONSE" | grep -oP '"id":\K\d+' | head -1) +echo "Created rule ID: $RULE_ID" + +# -------------------------------------------------------------------------- +# 4. 更新规则 +# -------------------------------------------------------------------------- +echo -e "\n${YELLOW}=== 4. 更新规则 ===${NC}" +RESPONSE=$(curl -s -X PUT "$BASE_URL/api/admin/quick-rules/$RULE_ID" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "priority": 20, + "is_active": false + }') +check_response "$RESPONSE" "0" "PUT /quick-rules/$RULE_ID" + +# -------------------------------------------------------------------------- +# 5. 批量导入 +# -------------------------------------------------------------------------- +echo -e "\n${YELLOW}=== 5. 批量导入 ===${NC}" +RESPONSE=$(curl -s -X POST "$BASE_URL/api/admin/quick-rules/import" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "rules": [ + {"rule_type": "greeting", "keyword": "test_hey", "priority": 5}, + {"rule_type": "greeting", "keyword": "test_hello", "priority": 5} + ], + "mode": "skip_duplicates" + }') +check_response "$RESPONSE" "0" "POST /quick-rules/import" + +# -------------------------------------------------------------------------- +# 6. 批量导出 +# -------------------------------------------------------------------------- +echo -e "\n${YELLOW}=== 6. 批量导出 ===${NC}" +RESPONSE=$(curl -s -X GET "$BASE_URL/api/admin/quick-rules/export?format=json" \ + -H "Authorization: Bearer $TOKEN") +check_response "$RESPONSE" "0" "GET /quick-rules/export" + +# -------------------------------------------------------------------------- +# 7. 智能体更新(高置信度) +# -------------------------------------------------------------------------- +echo -e "\n${YELLOW}=== 7. 智能体更新(高置信度) ===${NC}" +RESPONSE=$(curl -s -X POST "$BASE_URL/api/admin/quick-rules/agent-update" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "rule_type": "routing_prefilter", + "category": "IT服务", + "keyword": "test_投影仪", + "priority": 5, + "confidence": 0.92, + "reason": "测试用例", + "agent_id": "test_agent" + }') +check_response "$RESPONSE" "0" "POST /quick-rules/agent-update (confidence=0.92)" + +# -------------------------------------------------------------------------- +# 8. 智能体更新(中等置信度) +# -------------------------------------------------------------------------- +echo -e "\n${YELLOW}=== 8. 智能体更新(中等置信度) ===${NC}" +RESPONSE=$(curl -s -X POST "$BASE_URL/api/admin/quick-rules/agent-update" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "rule_type": "routing_prefilter", + "category": "IT服务", + "keyword": "test_音响", + "priority": 5, + "confidence": 0.7, + "reason": "测试用例", + "agent_id": "test_agent" + }') +check_response "$RESPONSE" "0" "POST /quick-rules/agent-update (confidence=0.7)" + +# -------------------------------------------------------------------------- +# 9. 智能体更新(低置信度) +# -------------------------------------------------------------------------- +echo -e "\n${YELLOW}=== 9. 智能体更新(低置信度) ===${NC}" +RESPONSE=$(curl -s -X POST "$BASE_URL/api/admin/quick-rules/agent-update" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "rule_type": "routing_prefilter", + "category": "IT服务", + "keyword": "test_键盘", + "priority": 5, + "confidence": 0.3, + "reason": "测试用例", + "agent_id": "test_agent" + }') +check_response "$RESPONSE" "0" "POST /quick-rules/agent-update (confidence=0.3)" + +# -------------------------------------------------------------------------- +# 10. 审计日志 +# -------------------------------------------------------------------------- +echo -e "\n${YELLOW}=== 10. 审计日志 ===${NC}" +RESPONSE=$(curl -s -X GET "$BASE_URL/api/admin/quick-rules/audit-log?page=1&size=10" \ + -H "Authorization: Bearer $TOKEN") +check_response "$RESPONSE" "0" "GET /quick-rules/audit-log" + +# -------------------------------------------------------------------------- +# 11. 统计详情 +# -------------------------------------------------------------------------- +echo -e "\n${YELLOW}=== 11. 统计详情 ===${NC}" +RESPONSE=$(curl -s -X GET "$BASE_URL/api/admin/quick-rules/stats/detail" \ + -H "Authorization: Bearer $TOKEN") +check_response "$RESPONSE" "0" "GET /quick-rules/stats/detail" + +# -------------------------------------------------------------------------- +# 12. 刷新缓存 +# -------------------------------------------------------------------------- +echo -e "\n${YELLOW}=== 12. 刷新缓存 ===${NC}" +RESPONSE=$(curl -s -X POST "$BASE_URL/api/admin/quick-rules/refresh" \ + -H "Authorization: Bearer $TOKEN") +check_response "$RESPONSE" "0" "POST /quick-rules/refresh" + +# -------------------------------------------------------------------------- +# 13. 批量删除 +# -------------------------------------------------------------------------- +echo -e "\n${YELLOW}=== 13. 批量删除 ===${NC}" +RESPONSE=$(curl -s -X POST "$BASE_URL/api/admin/quick-rules/batch-delete" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"ids\": [$RULE_ID]}") +check_response "$RESPONSE" "0" "POST /quick-rules/batch-delete" + +# -------------------------------------------------------------------------- +# 14. 简单统计 +# -------------------------------------------------------------------------- +echo -e "\n${YELLOW}=== 14. 简单统计 ===${NC}" +RESPONSE=$(curl -s -X GET "$BASE_URL/api/admin/quick-rules/stats" \ + -H "Authorization: Bearer $TOKEN") +check_response "$RESPONSE" "0" "GET /quick-rules/stats" + +# -------------------------------------------------------------------------- +# 汇总 +# -------------------------------------------------------------------------- +echo -e "\n${YELLOW}=== 测试汇总 ===${NC}" +echo -e "${GREEN}通过: $PASS${NC}" +echo -e "${RED}失败: $FAIL${NC}" + +if [ $FAIL -eq 0 ]; then + echo -e "${GREEN}✓ 所有测试通过!${NC}" + exit 0 +else + echo -e "${RED}✗ 有 $FAIL 个测试失败${NC}" + exit 1 +fi diff --git a/scripts/verify_api.py b/scripts/verify_api.py new file mode 100644 index 0000000..22e4156 --- /dev/null +++ b/scripts/verify_api.py @@ -0,0 +1,89 @@ +"""端到端验证 priority 修复 - 正确路径""" +import json +import secrets +import urllib.error +import urllib.parse +import urllib.request + +REDIS_PWD = "WKzl7jgKTkeWxFWGIBXfzokm59Gvfr76" + +import redis + +r = redis.Redis(host="redis", port=6379, password=REDIS_PWD, db=0) + +# 写 token +token = secrets.token_urlsafe(32) +user_info = { + "employee_id": "sxn", + "username": "admin", + "name": "宋献", + "role": "admin", + "department": "IT支持组", + "login_source": "test", + "login_method": "test_script", + "last_active": "2026-07-28T02:00:00", +} +r.setex(f"user:token:{token}", 8 * 3600, json.dumps(user_info, ensure_ascii=False)) +print(f"TOKEN: {token}", flush=True) + + +def call_api(url, params): + # 正确路径:/admin/quick-rules (nginx 会加 /api/ 前缀) + full_url = f"http://127.0.0.1:8000{url}?{urllib.parse.urlencode(params)}" + req = urllib.request.Request( + full_url, + headers={ + "Authorization": f"Bearer {token}", + "X-Forwarded-For": "10.240.1.100", + } + ) + try: + with urllib.request.urlopen(req, timeout=10) as resp: + raw = resp.read().decode("utf-8") + return json.loads(raw) + except urllib.error.HTTPError as e: + try: + body = e.read().decode("utf-8") + except Exception: + body = "" + try: + return json.loads(body) + except Exception: + return {"error": str(e), "body": body} + except Exception as e: + return {"error": str(e)} + + +tests = [ + ("rule_type=routing_target", {"rule_type": "routing_target", "page": 1, "size": 20}), + ("rule_type=routing_target&category=行政", {"rule_type": "routing_target", "category": "行政", "page": 1, "size": 20}), + ("rule_type=routing_target&category=人力资源", {"rule_type": "routing_target", "category": "人力资源", "page": 1, "size": 20}), + ("rule_type=routing_target&category=财务", {"rule_type": "routing_target", "category": "财务", "page": 1, "size": 20}), + ("rule_type=routing_target&category=法务", {"rule_type": "routing_target", "category": "法务", "page": 1, "size": 20}), + ("rule_type=routing_target&category=行政-物业", {"rule_type": "routing_target", "category": "行政-物业", "page": 1, "size": 20}), + ("rule_type=routing_target&category=IT服务", {"rule_type": "routing_target", "category": "IT服务", "page": 1, "size": 20}), + ("rule_type=routing_target&category=不存在的分类", {"rule_type": "routing_target", "category": "不存在的分类", "page": 1, "size": 20}), +] + +results = [] +for name, params in tests: + print(f"\n===== Test: {name} =====", flush=True) + result = call_api("/admin/quick-rules", params) + code = result.get("code") + if code == 0: + data = result.get("data", {}) + items = data.get("items", []) + print(f"✅ PASS: total={data.get('total')}, items={len(items)}", flush=True) + if items: + first = items[0] + print(f" first: priority={first.get('priority')}, category={first.get('category')}, keyword={first.get('keyword')}", flush=True) + results.append("PASS") + else: + print(f"❌ FAIL: code={code} msg={result.get('message') or result.get('body')}", flush=True) + results.append("FAIL") + +print(f"\n===== 汇总: {results.count('PASS')}/{len(results)} 通过 =====", flush=True) + +# 清理 +r.delete(f"user:token:{token}") +print("===== Cleanup OK =====", flush=True) diff --git a/scripts/verify_fix.sh b/scripts/verify_fix.sh new file mode 100644 index 0000000..33c071e --- /dev/null +++ b/scripts/verify_fix.sh @@ -0,0 +1,97 @@ +#!/bin/bash +# ============================================================================= +# 验证 priority 修复 - 端到端测试 (v4 - 用 docker exec) +# ============================================================================= + +set -e +REDIS_PWD="WKzl7jgKTkeWxFWGIBXfzokm59Gvfr76" +TOKEN=$(openssl rand -hex 32) + +# 1. 在 backend 容器内用 Python 写 token(容器已装 redis) +USER_INFO='{"employee_id": "sxn", "username": "admin", "name": "宋献", "role": "admin", "department": "IT支持组", "login_source": "test", "login_method": "test_script", "last_active": "2026-07-28T02:00:00"}' + +docker exec wecom_it_backend python3 - <&1 | head -80 + +# 3. 测试 API - category=行政 +echo "" +echo "===== Test 2: GET /admin/quick-rules?rule_type=routing_target&category=行政 =====" +RESP2=$(curl -s --get "http://localhost/api/admin/quick-rules" \ + --data-urlencode "rule_type=routing_target" \ + --data-urlencode "category=行政" \ + --data-urlencode "page=1" \ + --data-urlencode "size=20" \ + -H "Authorization: Bearer ${TOKEN}") +echo "${RESP2}" | python3 -m json.tool 2>&1 | head -50 + +# 4. 测试 API - category=人力资源 +echo "" +echo "===== Test 3: GET /admin/quick-rules?rule_type=routing_target&category=人力资源 =====" +RESP3=$(curl -s --get "http://localhost/api/admin/quick-rules" \ + --data-urlencode "rule_type=routing_target" \ + --data-urlencode "category=人力资源" \ + --data-urlencode "page=1" \ + --data-urlencode "size=20" \ + -H "Authorization: Bearer ${TOKEN}") +echo "${RESP3}" | python3 -m json.tool 2>&1 | head -50 + +# 5. 测试 API - category=IT服务 +echo "" +echo "===== Test 4: GET /admin/quick-rules?rule_type=routing_target&category=IT服务 =====" +RESP4=$(curl -s --get "http://localhost/api/admin/quick-rules" \ + --data-urlencode "rule_type=routing_target" \ + --data-urlencode "category=IT服务" \ + --data-urlencode "page=1" \ + --data-urlencode "size=20" \ + -H "Authorization: Bearer ${TOKEN}") +echo "${RESP4}" | python3 -m json.tool 2>&1 | head -50 + +# 6. 测试 API - category=行政-物业 +echo "" +echo "===== Test 5: GET /admin/quick-rules?rule_type=routing_target&category=行政-物业 =====" +RESP5=$(curl -s --get "http://localhost/api/admin/quick-rules" \ + --data-urlencode "rule_type=routing_target" \ + --data-urlencode "category=行政-物业" \ + --data-urlencode "page=1" \ + --data-urlencode "size=20" \ + -H "Authorization: Bearer ${TOKEN}") +echo "${RESP5}" | python3 -m json.tool 2>&1 | head -50 + +# 7. 测试 API - 不存在的分类 +echo "" +echo "===== Test 6: GET /admin/quick-rules?rule_type=routing_target&category=不存在的分类 =====" +RESP6=$(curl -s --get "http://localhost/api/admin/quick-rules" \ + --data-urlencode "rule_type=routing_target" \ + --data-urlencode "category=不存在的分类" \ + --data-urlencode "page=1" \ + --data-urlencode "size=20" \ + -H "Authorization: Bearer ${TOKEN}") +echo "${RESP6}" | python3 -m json.tool 2>&1 | head -20 + +# 8. cleanup +echo "" +echo "===== Cleanup =====" +docker exec wecom_it_backend python3 -c " +import redis +r = redis.Redis(host='redis', port=6379, password='${REDIS_PWD}', db=0) +print(r.delete(f'user:token:${TOKEN}')) +" + +echo "" +echo "===== 验证完成 =====" diff --git a/scripts/verify_fix_v2.sh b/scripts/verify_fix_v2.sh new file mode 100644 index 0000000..079f825 --- /dev/null +++ b/scripts/verify_fix_v2.sh @@ -0,0 +1,98 @@ +#!/bin/bash +# ============================================================================= +# 验证 priority 修复 - 端到端测试 (v5 - 一次性脚本) +# ============================================================================= + +set -e + +# 1. 在 backend 容器内生成 token 并直接调用 API +docker exec wecom_it_backend python3 - <<'PYEOF' +import redis +import json +import secrets +import urllib.request +import urllib.parse + +REDIS_PWD = "WKzl7jgKTkeWxFWGIBXfzokm59Gvfr76" +r = redis.Redis(host="redis", port=6379, password=REDIS_PWD, db=0) + +# 写 token +token = secrets.token_urlsafe(32) +user_info = { + "employee_id": "sxn", + "username": "admin", + "name": "宋献", + "role": "admin", + "department": "IT支持组", + "login_source": "test", + "login_method": "test_script", + "last_active": "2026-07-28T02:00:00", +} +r.setex(f"user:token:{token}", 8 * 3600, json.dumps(user_info, ensure_ascii=False)) +print(f"TOKEN: {token}", flush=True) + +# 直接通过容器内 localhost 调 API +def call_api(url, params): + full_url = f"http://localhost:8000{url}?{urllib.parse.urlencode(params)}" + req = urllib.request.Request(full_url, headers={"Authorization": f"Bearer {token}"}) + try: + with urllib.request.urlopen(req, timeout=10) as resp: + data = json.loads(resp.read().decode("utf-8")) + return data + except urllib.error.HTTPError as e: + return {"error": str(e), "code": e.code} + except Exception as e: + return {"error": str(e)} + +# 测试 1: rule_type=routing_target +print("\n===== Test 1: rule_type=routing_target =====", flush=True) +r1 = call_api("/api/admin/quick-rules", {"rule_type": "routing_target", "page": 1, "size": 20}) +print(f"code: {r1.get('code')}", flush=True) +print(f"message: {r1.get('message')}", flush=True) +if r1.get("data"): + items = r1["data"].get("items", []) + print(f"total: {r1['data'].get('total')}", flush=True) + print(f"items count: {len(items)}", flush=True) + if items: + print(f"first item: {json.dumps(items[0], ensure_ascii=False, indent=2)}", flush=True) + +# 测试 2: rule_type=routing_target&category=行政 +print("\n===== Test 2: rule_type=routing_target&category=行政 =====", flush=True) +r2 = call_api("/api/admin/quick-rules", {"rule_type": "routing_target", "category": "行政", "page": 1, "size": 20}) +print(f"code: {r2.get('code')}", flush=True) +print(f"message: {r2.get('message')}", flush=True) +if r2.get("data"): + items = r2["data"].get("items", []) + print(f"total: {r2['data'].get('total')}", flush=True) + print(f"items count: {len(items)}", flush=True) + if items: + print(f"first item: {json.dumps(items[0], ensure_ascii=False, indent=2)}", flush=True) + +# 测试 3: rule_type=routing_target&category=人力资源 +print("\n===== Test 3: rule_type=routing_target&category=人力资源 =====", flush=True) +r3 = call_api("/api/admin/quick-rules", {"rule_type": "routing_target", "category": "人力资源", "page": 1, "size": 20}) +print(f"code: {r3.get('code')}", flush=True) +print(f"message: {r3.get('message')}", flush=True) +if r3.get("data"): + items = r3["data"].get("items", []) + print(f"total: {r3['data'].get('total')}", flush=True) + print(f"items count: {len(items)}", flush=True) + +# 测试 4: rule_type=routing_target&category=IT服务 +print("\n===== Test 4: rule_type=routing_target&category=IT服务 =====", flush=True) +r4 = call_api("/api/admin/quick-rules", {"rule_type": "routing_target", "category": "IT服务", "page": 1, "size": 20}) +print(f"code: {r4.get('code')}", flush=True) +print(f"message: {r4.get('message')}", flush=True) +if r4.get("data"): + items = r4["data"].get("items", []) + print(f"total: {r4['data'].get('total')}", flush=True) + if items: + print(f"item: {json.dumps(items[0], ensure_ascii=False, indent=2)}", flush=True) + +# 清理 +r.delete(f"user:token:{token}") +print("\n===== Cleanup OK =====", flush=True) +PYEOF + +echo "" +echo "===== 验证完成 =====" diff --git a/scripts/verify_openapi.py b/scripts/verify_openapi.py new file mode 100644 index 0000000..540b435 --- /dev/null +++ b/scripts/verify_openapi.py @@ -0,0 +1,22 @@ +"""验证 OpenAPI 文档中 QuickRuleResponse 字段已修复""" +import json +import sys + +OPENAPI = "/tmp/openapi.json" + +with open(OPENAPI) as f: + data = json.load(f) + +schemas = data.get("components", {}).get("schemas", {}) + +# 查找所有包含 quick 的 schema +candidates = [k for k in schemas.keys() if "quick" in k.lower() or "QuickRule" in k] +print(f"Found schemas: {candidates}") + +# 查找所有包含 priority 的 schema +for name, sch in schemas.items(): + props = sch.get("properties", {}) + if "priority" in props: + print(f"\n{name} contains 'priority':") + print(json.dumps(sch, indent=2, ensure_ascii=False)) + print() diff --git a/scripts/verify_quickrules.sh b/scripts/verify_quickrules.sh new file mode 100644 index 0000000..ec4e464 --- /dev/null +++ b/scripts/verify_quickrules.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# 测试快速规则 API - 验证 priority 修复 +set -e + +echo "===== 1. 登录获取 admin token =====" +LOGIN_RESP=$(curl -s -X POST http://localhost:8090/api/admin/login \ + -H 'Content-Type: application/json' \ + -d '{"username":"admin","password":"admin123"}') +echo "Login response: $LOGIN_RESP" + +TOKEN=$(echo "$LOGIN_RESP" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("data",{}).get("token",""))') +echo "Token length: ${#TOKEN}" + +echo "" +echo "===== 2. GET /admin/quick-rules?rule_type=routing_target =====" +curl -s 'http://localhost:8090/api/admin/quick-rules?rule_type=routing_target&page=1&size=20' \ + -H "Authorization: Bearer ${TOKEN}" | python3 -m json.tool + +echo "" +echo "===== 3. GET /admin/quick-rules?rule_type=routing_target&category=行政 =====" +curl -s --get 'http://localhost:8090/api/admin/quick-rules' \ + --data-urlencode 'rule_type=routing_target' \ + --data-urlencode 'category=行政' \ + --data-urlencode 'page=1' \ + --data-urlencode 'size=20' \ + -H "Authorization: Bearer ${TOKEN}" | python3 -m json.tool + +echo "" +echo "===== 4. GET /admin/quick-rules?rule_type=routing_target&category=人力资源 =====" +curl -s --get 'http://localhost:8090/api/admin/quick-rules' \ + --data-urlencode 'rule_type=routing_target' \ + --data-urlencode 'category=人力资源' \ + --data-urlencode 'page=1' \ + --data-urlencode 'size=20' \ + -H "Authorization: Bearer ${TOKEN}" | python3 -m json.tool + +echo "" +echo "===== 5. GET /admin/quick-rules?rule_type=routing_target&category=IT服务 =====" +curl -s --get 'http://localhost:8090/api/admin/quick-rules' \ + --data-urlencode 'rule_type=routing_target' \ + --data-urlencode 'category=IT服务' \ + --data-urlencode 'page=1' \ + --data-urlencode 'size=20' \ + -H "Authorization: Bearer ${TOKEN}" | python3 -m json.tool + +echo "" +echo "===== 6. 后端日志 - 确认无 ValidationError =====" +docker logs --tail 100 wecom_it_backend 2>&1 | grep -iE 'quickrule|pydantic|validation|priority' | tail -20 + +echo "" +echo "===== 验收完成 ====="