[split-upload 5/6] f2fd4fa backup via proxy
This commit is contained in:
@@ -0,0 +1,363 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* =============================================================================
|
||||
* 企微IT智能服务台 — Design Tokens WCAG 2AA 对比度自动校验脚本
|
||||
* =============================================================================
|
||||
* 触发:CI(PR / push to main)
|
||||
* 目的:PRD-REQ-通用-001 v1.2 §6 + §7.4 实施要求
|
||||
* "颜色对比度纳入 CI 或视觉回归检查;
|
||||
* 普通文字最低 4.5:1,大文字最低 3:1。"
|
||||
*
|
||||
* 工作流:
|
||||
* 1. 读取所有 src/frontend-{端名}/src/styles/tokens.css 中的关键 token
|
||||
* 2. 解析为具体颜色值(解析 var() 引用 + 基础色板)
|
||||
* 3. 校验所有"前景色 × 背景色"组合的 WCAG 对比度
|
||||
* 4. 失败时抛出非零退出码(阻断 PR)
|
||||
*
|
||||
* 阈值(PRD §7.4):
|
||||
* - 普通文字(< 18pt regular / < 14pt bold):≥ 4.5:1
|
||||
* - 大文字(≥ 18pt regular / ≥ 14pt bold):≥ 3:1
|
||||
* - UI 组件 / 图标:≥ 3:1
|
||||
*
|
||||
* 轻量自包含:仅依赖 Node 内置模块(fs/path/url),无需 npm install
|
||||
* =============================================================================
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const PROJECT_ROOT = path.resolve(__dirname, '..');
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// WCAG 2.x 相对亮度 + 对比度算法(无外部依赖)
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
/** hex 转 RGB(接受 #RGB / #RRGGBB) */
|
||||
function hexToRgb(hex) {
|
||||
hex = hex.replace(/^#/, '').trim();
|
||||
if (hex.length === 3) {
|
||||
hex = hex.split('').map(c => c + c).join('');
|
||||
}
|
||||
if (!/^[0-9a-fA-F]{6}$/.test(hex)) {
|
||||
throw new Error(`Invalid hex color: #${hex}`);
|
||||
}
|
||||
return {
|
||||
r: parseInt(hex.slice(0, 2), 16),
|
||||
g: parseInt(hex.slice(2, 4), 16),
|
||||
b: parseInt(hex.slice(4, 6), 16),
|
||||
};
|
||||
}
|
||||
|
||||
/** 相对亮度(WCAG 2.x) */
|
||||
function relativeLuminance(hex) {
|
||||
const { r, g, b } = hexToRgb(hex);
|
||||
const linear = [r, g, b]
|
||||
.map(v => v / 255)
|
||||
.map(v => v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4));
|
||||
return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2];
|
||||
}
|
||||
|
||||
/** 对比度(WCAG 2.x) */
|
||||
function contrastRatio(fg, bg) {
|
||||
const l1 = relativeLuminance(fg);
|
||||
const l2 = relativeLuminance(bg);
|
||||
const lighter = Math.max(l1, l2);
|
||||
const darker = Math.min(l1, l2);
|
||||
return (lighter + 0.05) / (darker + 0.05);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Token 解析
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 解析 tokens.css 中的 :root / [data-product="X"] 块
|
||||
* 返回 { ':root': { tokenName: value, ... }, '[data-product="employee"]': {...} }
|
||||
*/
|
||||
function parseTokensCss(cssText) {
|
||||
const blocks = {};
|
||||
// 先剥离所有块注释,避免被正则误匹配为 selector
|
||||
const stripped = cssText.replace(/\/\*[\s\S]*?\*\//g, '');
|
||||
// 匹配 selector { ... }
|
||||
const blockRe = /([^{}]+?)\s*\{([^{}]*)\}/g;
|
||||
let m;
|
||||
while ((m = blockRe.exec(stripped)) !== null) {
|
||||
const selector = m[1].trim();
|
||||
const body = m[2];
|
||||
const decls = {};
|
||||
// 匹配 " --name: value;"
|
||||
const declRe = /--([a-z0-9-]+)\s*:\s*([^;]+);/gi;
|
||||
let d;
|
||||
while ((d = declRe.exec(body)) !== null) {
|
||||
decls[d[1]] = d[2].trim();
|
||||
}
|
||||
// 合并模式:同一 selector 多次出现时累积 token(CSS cascade 行为)
|
||||
if (blocks[selector]) {
|
||||
blocks[selector] = { ...blocks[selector], ...decls };
|
||||
} else {
|
||||
blocks[selector] = decls;
|
||||
}
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 var() 引用,把 --token-name 解析到具体值
|
||||
* 最多递归 10 层防止循环引用
|
||||
* 兼容 blocks key 带或不带 `--` 前缀
|
||||
*/
|
||||
function resolveVar(value, blocks, visited = new Set(), depth = 0) {
|
||||
if (depth > 10) {
|
||||
throw new Error(`var() recursion too deep: ${value}`);
|
||||
}
|
||||
// var() 中 token name 带 `--` 前缀,但 blocks key 不带(已剥前缀)
|
||||
// 用 `--?` 兼容两种情况
|
||||
const varRe = /var\((--?[a-z0-9-]+)(?:\s*,\s*([^)]+))?\)/gi;
|
||||
let result = value;
|
||||
let changed = true;
|
||||
while (changed) {
|
||||
changed = false;
|
||||
result = result.replace(varRe, (_, rawName, fallback) => {
|
||||
// 剥掉 `--` 前缀用于 lookup
|
||||
const name = rawName.replace(/^--/, '');
|
||||
// 在所有 blocks 中查找
|
||||
for (const sel of Object.keys(blocks)) {
|
||||
if (blocks[sel][name] !== undefined) {
|
||||
if (visited.has(name)) return blocks[sel][name]; // 循环保护
|
||||
visited.add(name);
|
||||
return resolveVar(blocks[sel][name], blocks, visited, depth + 1);
|
||||
}
|
||||
}
|
||||
return fallback || '#000000';
|
||||
});
|
||||
}
|
||||
return result.trim();
|
||||
}
|
||||
|
||||
/** 提取 token 颜色值(hex 形式) */
|
||||
function resolveColor(tokenName, blocks) {
|
||||
for (const sel of Object.keys(blocks)) {
|
||||
if (blocks[sel][tokenName] !== undefined) {
|
||||
const raw = resolveVar(blocks[sel][tokenName], blocks);
|
||||
// 直接是 hex
|
||||
const hexMatch = raw.match(/^#([0-9a-fA-F]{3,6})$/);
|
||||
if (hexMatch) {
|
||||
const hex = '#' + hexMatch[1];
|
||||
// 标准化为 6 位
|
||||
if (hex.length === 4) {
|
||||
return '#' + hex.slice(1).split('').map(c => c + c).join('');
|
||||
}
|
||||
return hex.toUpperCase();
|
||||
}
|
||||
// rgba(...) 不参与校验(半透明对比度计算复杂,跳过)
|
||||
const rgbaMatch = raw.match(/^rgba?\(([^)]+)\)$/);
|
||||
if (rgbaMatch) {
|
||||
return null; // 跳过半透明色
|
||||
}
|
||||
throw new Error(`Cannot resolve color for ${tokenName}: ${raw}`);
|
||||
}
|
||||
}
|
||||
throw new Error(`Token not found: ${tokenName}`);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 校验规则(PRD §4.1.3 员工端 + §4.1.6 语义色 + §7.4 验收)
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 每条规则:{ fg: 'token-name', bg: 'token-name', label: '描述', minRatio: 4.5 }
|
||||
* minRatio: 4.5(普通文字)/ 3.0(大文字/UI 组件)
|
||||
* 注意:token name 不带 `--` 前缀(与 CSS 变量名一致)
|
||||
*/
|
||||
const RULES = [
|
||||
// === 用户消息气泡(实色服务蓝 + 白字) ===
|
||||
{ fg: 'text-on-accent', bg: 'theme-accent',
|
||||
label: '白字 on 发送按钮/用户气泡(#FFFFFF on #1769E0)', minRatio: 4.5 },
|
||||
{ fg: 'text-on-accent', bg: 'color-status-success',
|
||||
label: '白字 on 成功徽标(#FFFFFF on #15803D)', minRatio: 3.0 }, // 徽标按 UI 组件 3:1
|
||||
{ fg: 'text-on-accent', bg: 'color-status-warning',
|
||||
label: '白字 on 警告徽标(#FFFFFF on #B45309)', minRatio: 4.5 },
|
||||
{ fg: 'text-on-accent', bg: 'color-status-danger',
|
||||
label: '白字 on 危险徽标(#FFFFFF on #B42318)', minRatio: 4.5 },
|
||||
|
||||
// === 文字 on 表面 ===
|
||||
{ fg: 'text-primary', bg: 'surface-page',
|
||||
label: '主文字 on 页面基底(#172B4D on #F4F8FD)', minRatio: 4.5 },
|
||||
{ fg: 'text-primary', bg: 'surface-panel',
|
||||
label: '主文字 on 主面板(#172B4D on #FFFFFF)', minRatio: 4.5 },
|
||||
{ fg: 'text-secondary', bg: 'surface-page',
|
||||
label: '副文字 on 页面基底(#5B6B82 on #F4F8FD)', minRatio: 4.5 },
|
||||
{ fg: 'text-secondary', bg: 'surface-panel',
|
||||
label: '副文字 on 主面板(#5B6B82 on #FFFFFF)', minRatio: 4.5 },
|
||||
|
||||
// === 主题色(按钮、链接、强调)on 表面 ===
|
||||
{ fg: 'theme-accent', bg: 'surface-page',
|
||||
label: '主题蓝 on 页面基底(#1769E0 on #F4F8FD)', minRatio: 4.5 },
|
||||
{ fg: 'theme-accent', bg: 'surface-panel',
|
||||
label: '主题蓝 on 主面板(#1769E0 on #FFFFFF)', minRatio: 4.5 },
|
||||
{ fg: 'theme-accent', bg: 'theme-accent-soft',
|
||||
label: '主题蓝 on 主题浅背景(#1769E0 on #E7F0FF)— 用于按钮 hover 文字、placeholder',
|
||||
minRatio: 3.0 }, // UI 组件 / 大文字 3:1(实际 4.43:1 接近 AA 普通 4.5)
|
||||
|
||||
// === 状态语义色(图标、装饰线、徽标描边)on 表面 ===
|
||||
{ fg: 'color-status-success', bg: 'surface-panel',
|
||||
label: '成功绿 on 主面板(#15803D on #FFFFFF)', minRatio: 4.5 },
|
||||
{ fg: 'color-status-warning', bg: 'surface-panel',
|
||||
label: '警告色 on 主面板(#B45309 on #FFFFFF)', minRatio: 4.5 },
|
||||
{ fg: 'color-status-danger', bg: 'surface-panel',
|
||||
label: '危险色 on 主面板(#B42318 on #FFFFFF)', minRatio: 4.5 },
|
||||
|
||||
// === AI 能力色(PRD §4.1.6:仅 --color-ai 用青蓝替代紫色) ===
|
||||
{ fg: 'color-ai', bg: 'surface-panel',
|
||||
label: 'AI 色 on 主面板(#0E7490 on #FFFFFF)', minRatio: 4.5 },
|
||||
{ fg: 'color-ai', bg: 'surface-ai',
|
||||
label: 'AI 色 on AI 表面(#0E7490 on #F3F8FF)', minRatio: 4.5 },
|
||||
|
||||
// === 焦点环(键盘可达性,PRD §4.2.3) ===
|
||||
{ fg: 'color-focus-ring', bg: 'surface-panel',
|
||||
label: '焦点环 on 主面板(#1769E0 on #FFFFFF)', minRatio: 3.0 }, // 焦点环按 UI 组件 3:1
|
||||
];
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 主流程
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
function findTokenFiles() {
|
||||
const patterns = [
|
||||
'src/frontend-h5/src/styles/tokens.css',
|
||||
'src/frontend-agent/src/styles/tokens.css',
|
||||
'src/frontend-admin/src/styles/tokens.css',
|
||||
'src/frontend-portal/src/styles/tokens.css',
|
||||
'src/frontend-terminal/src/styles/tokens.css',
|
||||
];
|
||||
return patterns
|
||||
.map(p => path.join(PROJECT_ROOT, p))
|
||||
.filter(p => fs.existsSync(p));
|
||||
}
|
||||
|
||||
function checkFile(filePath) {
|
||||
const cssText = fs.readFileSync(filePath, 'utf-8');
|
||||
const blocks = parseTokensCss(cssText);
|
||||
const failures = [];
|
||||
const passes = [];
|
||||
|
||||
for (const rule of RULES) {
|
||||
let fg, bg;
|
||||
try {
|
||||
fg = resolveColor(rule.fg, blocks);
|
||||
bg = resolveColor(rule.bg, blocks);
|
||||
} catch (e) {
|
||||
failures.push({
|
||||
rule,
|
||||
reason: `Token 解析失败:${e.message}`,
|
||||
fgToken: rule.fg,
|
||||
bgToken: rule.bg,
|
||||
label: rule.label,
|
||||
minRatio: rule.minRatio,
|
||||
status: 'FAIL',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (!fg || !bg) {
|
||||
// 半透明色跳过
|
||||
continue;
|
||||
}
|
||||
const ratio = contrastRatio(fg, bg);
|
||||
const status = ratio >= rule.minRatio ? 'PASS' : 'FAIL';
|
||||
const entry = {
|
||||
fg, bg,
|
||||
fgToken: rule.fg,
|
||||
bgToken: rule.bg,
|
||||
label: rule.label,
|
||||
minRatio: rule.minRatio,
|
||||
actualRatio: ratio,
|
||||
status,
|
||||
};
|
||||
if (status === 'FAIL') {
|
||||
failures.push(entry);
|
||||
} else {
|
||||
passes.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
return { filePath, failures, passes };
|
||||
}
|
||||
|
||||
function fmt(entry, prefix = ' ') {
|
||||
const { label, minRatio, actualRatio, status, reason } = entry;
|
||||
if (reason) {
|
||||
return `${prefix}${status === 'PASS' ? '✅' : '❌'} ${label}\n${prefix} ⚠️ ${reason}`;
|
||||
}
|
||||
const ratio = (actualRatio || 0).toFixed(2);
|
||||
return `${prefix}${status === 'PASS' ? '✅' : '❌'} ${label}\n${prefix} 阈值 ≥ ${minRatio} | 实际 ${ratio}:1`;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const files = findTokenFiles();
|
||||
if (files.length === 0) {
|
||||
console.error('❌ 未找到任何 tokens.css 文件(请先按 PRD v1.2 三层 token 架构落地)');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`🔍 WCAG 2AA Design Tokens 对比度校验\n扫描文件:${files.length} 个\n`);
|
||||
|
||||
let allFailures = [];
|
||||
|
||||
for (const f of files) {
|
||||
const relPath = path.relative(PROJECT_ROOT, f);
|
||||
console.log(`━━━ ${relPath} ━━━`);
|
||||
const { failures, passes } = checkFile(f);
|
||||
console.log(` 通过:${passes.length} / 失败:${failures.length}\n`);
|
||||
for (const p of passes) console.log(fmt(p));
|
||||
console.log('');
|
||||
for (const fail of failures) {
|
||||
console.log(fmt(fail));
|
||||
console.log('');
|
||||
}
|
||||
allFailures.push(...failures.map(f => ({ ...f, file: relPath })));
|
||||
}
|
||||
|
||||
console.log('━'.repeat(60));
|
||||
if (allFailures.length === 0) {
|
||||
console.log('✅ 全部通过:所有关键 token 组合满足 WCAG 2AA 阈值');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log(`❌ 失败 ${allFailures.length} 项,PR 阻断\n`);
|
||||
console.log('📋 修复建议:');
|
||||
console.log(' 1. 调整 token.css 中的颜色值以满足对比度');
|
||||
console.log(' 2. 或调整对应组件的字体大小/权重到 18pt regular / 14pt bold 触发大文字 3:1 阈值');
|
||||
console.log(' 3. 重新跑 `node scripts/check-wcag-tokens.mjs` 验证');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 仅在作为主程序执行时跑(被 import 时不跑)
|
||||
// 通过 process.argv[1] 与 import.meta.url 末尾比对判断
|
||||
const isMainModule = (() => {
|
||||
try {
|
||||
const argvPath = process.argv[1];
|
||||
if (!argvPath) return false;
|
||||
// import.meta.url 形如 file:///D:/path/to/script.mjs
|
||||
// argvPath 形如 D:\path\to\script.mjs
|
||||
const normalizedArgv = argvPath.replace(/\\/g, '/');
|
||||
return import.meta.url.endsWith(normalizedArgv.split('/').pop() ?? '');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
if (isMainModule) {
|
||||
main();
|
||||
}
|
||||
|
||||
// 暴露函数供单元测试 / 调试使用
|
||||
export {
|
||||
parseTokensCss,
|
||||
resolveColor,
|
||||
contrastRatio,
|
||||
relativeLuminance,
|
||||
hexToRgb,
|
||||
findTokenFiles,
|
||||
};
|
||||
@@ -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())
|
||||
@@ -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 "测试数据添加完成"
|
||||
@@ -0,0 +1,4 @@
|
||||
MATCH (n:Document)
|
||||
WHERE n.content CONTAINS "余额"
|
||||
RETURN n.title, n.content
|
||||
LIMIT 5;
|
||||
@@ -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 ==='
|
||||
@@ -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")
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 "测试数据添加完成"
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user