[split-upload 1/6] f2fd4fa backup via proxy

This commit is contained in:
Simon
2026-08-11 14:15:36 +08:00
parent 6be361fb63
commit 268bf914ce
75 changed files with 5734 additions and 84 deletions
+292
View File
@@ -0,0 +1,292 @@
# agent-browser-auth-wait.ps1
#
# 功能:使用 agent-browser 打开登录页(含二维码),导出二维码图片,
# 等待用户扫码完成后继续执行,自动保存认证状态供下次复用。
#
# 设计要点:
# 1. 复用 agent-browser v0.27.0+ 的 state save/load 能力(内置),
# 不自造轮询 cookie 的轮子。
# 2. 使用 eval -b <base64> 注入 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
}
+30
View File
@@ -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!"
+27
View File
@@ -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())
+64
View File
@@ -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"
+16
View File
@@ -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)
+6
View File
@@ -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;
+36
View File
@@ -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}")
+57
View File
@@ -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: <dify_base_url>|<api_key>|<app_type>
# 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')}")
+79
View File
@@ -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}")
+29
View File
@@ -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())
+2
View File
@@ -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';
+31
View File
@@ -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!')
+136
View File
@@ -0,0 +1,136 @@
#!/bin/bash
# =============================================================================
# deploy_inspection_to_jumpserver.sh — 巡检报告 HTML 发布到 jumpserver
# =============================================================================
# 流程:
# 1. 扫描 docs/07-项目管理/巡检报告/ 下所有 HTML 报告
# 2. 调用 jumpserver-V2 skill 的 v2_ops.py upload 上传到 /tmp/
# 3. 用 v2_ops.py exec 按月份目录归档到 /opt/wecom-it-desk/docs-public/inspection/
# 4. (可选)触发 nginx reload(仅当挂载点在容器外时)
#
# 用法:
# bash scripts/deploy_inspection_to_jumpserver.sh # 默认发布全部
# bash scripts/deploy_inspection_to_jumpserver.sh --dry-run # 仅 dry-run
# bash scripts/deploy_inspection_to_jumpserver.sh --month 2026-08 # 仅发布指定月份
#
# 前置:
# - jumpserver-V2 skill 已登录(v2_ops.py status 返回有效)
# - jumpserver 上有静态目录 /opt/wecom-it-desk/docs-public/inspection/
# - jumpserver 上有 nginx 路由 /docs/inspection/ → 该目录
#
# ⚠️ 本脚本默认不修改 nginx 配置(避免误改生产)。
# =============================================================================
set -e
# 颜色
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'
info() { echo -e "${BLUE}[INFO]${NC} $1"; }
ok() { echo -e "${GREEN}[OK]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
# 路径常量
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
SRC_DIR="$PROJECT_ROOT/docs/07-项目管理/巡检报告"
# jumpserver-V2 skill
V2_OPS="C:\Users\simon\.workbuddy\skills\jumpserver-V2\scripts\v2_ops.py"
PYTHON_BIN="C:\Users\simon\.workbuddy\binaries\python\versions\3.13.12\python.exe"
# 服务器路径
REMOTE_BASE="/opt/wecom-it-desk/docs-public/inspection"
# 参数
DRY_RUN=false
ONLY_MONTH=""
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=true ;;
--month) ONLY_MONTH="$2"; shift 2 ;;
*) error "未知参数:$arg" ;;
esac
done
# ============================================================
# Step 1: 扫描 HTML 文件
# ============================================================
info "[1/4] 扫描 $SRC_DIR ..."
[ -d "$SRC_DIR" ] || error "源目录不存在:$SRC_DIR"
# 收集所有 .html 文件(按月份分组)
mapfile -t ALL_FILES < <(find "$SRC_DIR" -name "*.html" -type f 2>/dev/null | sort)
[ ${#ALL_FILES[@]} -gt 0 ] || error "未找到任何 HTML 文件"
# 按月份过滤
declare -A MONTH_FILES
for f in "${ALL_FILES[@]}"; do
# 提取 YYYY-MM(相对路径第一段)
rel="${f#$SRC_DIR/}"
month="$(echo "$rel" | cut -d'/' -f1)"
if [ -n "$ONLY_MONTH" ] && [ "$month" != "$ONLY_MONTH" ]; then
continue
fi
MONTH_FILES["$month"]+="$f"$'\n'
done
[ ${#MONTH_FILES[@]} -gt 0 ] || error "指定月份 $ONLY_MONTH 下无 HTML 文件"
info "发现 ${#MONTH_FILES[@]} 个月份,共 $(printf '%s\n' "${ALL_FILES[@]}" | grep -c "\.html") 个文件"
for month in "${!MONTH_FILES[@]}"; do
count=$(echo "${MONTH_FILES[$month]}" | grep -c "\.html" || echo 0)
info " 📅 $month: $count 个报告"
done
# ============================================================
# Step 2: jumpserver 会话状态
# ============================================================
info "[2/4] 检查 jumpserver 会话..."
"$PYTHON_BIN" "$V2_OPS" status 2>&1 | grep -E "✅|❌" || error "jumpserver 会话无效,请先 login"
# ============================================================
# Step 3: 上传 + 归档(按月份)
# ============================================================
info "[3/4] 上传文件到 jumpserver..."
UPLOADED=0
for month in "${!MONTH_FILES[@]}"; do
while IFS= read -r local_file; do
[ -z "$local_file" ] && continue
basename="$(basename "$local_file")"
ts="$(date +%Y%m%d%H%M%S)"
# 中文路径 → jumpserver 必须走 ASCII 临时文件名
# 但 v2_ops.py upload 已自动处理(内部 ASCII staging
info " 📤 $month/$basename"
if [ "$DRY_RUN" = true ]; then
info " (dry-run 跳过)"
else
"$PYTHON_BIN" "$V2_OPS" upload "$local_file" "inspection_${month}_${ts}_${basename}" \
|| error "上传失败:$basename"
# 远端 mkdir + mv 到正确月份目录
remote_cmd="mkdir -p ${REMOTE_BASE}/${month} && mv /tmp/inspection_${month}_${ts}_${basename} ${REMOTE_BASE}/${month}/${basename} && chmod 644 ${REMOTE_BASE}/${month}/${basename}"
"$PYTHON_BIN" "$V2_OPS" exec "$remote_cmd" \
|| error "归档失败:$basename"
UPLOADED=$((UPLOADED + 1))
fi
done <<< "${MONTH_FILES[$month]}"
done
# ============================================================
# Step 4: 完成
# ============================================================
info "[4/4] 部署完成"
echo ""
ok "========== 发布总结 =========="
info "月份数: ${#MONTH_FILES[@]}"
info "上传文件: $UPLOADED"
echo ""
info "📍 访问 URL(前提:nginx 已加 /docs/inspection/ 路由)"
info " https://itsupport.servyou.com.cn/docs/inspection/2026-08/2026-08-05-早班.html"
echo ""
info "🔁 重新发布:直接重跑本脚本即可(脚本会自动覆盖)"
echo ""
info "💡 巡检报告 HTML 由早班巡检自动化任务生成 + 自动调用本脚本发布"
info " (详见 .workbuddy/automations/automation-1782986180887/memory.md 标准步骤清单)"
+44
View File
@@ -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())
+98
View File
@@ -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 "===== 验证完成 ====="