feat: 添加 Dashboard 完整日志监控系统 v1.1.0

 新增功能
- 完整的日志记录系统(6 种日志级别)
- 日志配置功能(可通过 config.json 控制)
- 性能监控装饰器和请求日志中间件
- 7 个管理工具脚本
- 完整的文档和使用指南

🛠️ 管理工具
- start-with-log.sh: 启动脚本(带日志)
- stop-dashboard.sh: 停止脚本
- view-logs.sh: 日志查看器
- monitor-logs.sh: 实时监控工具(支持多种过滤器)
- analyze-logs.sh: 日志分析工具(自动生成报告)
- demo-logging.sh: 功能演示脚本
- test-logging-config.sh: 配置测试工具

📊 日志特性
- 支持 INFO/SUCCESS/WARN/ERROR/DEBUG/PERF 6 种级别
- 自动记录启动过程、API 请求、性能统计
- 缓存命中情况追踪
- 分步性能监控
- 智能过滤器

⚙️ 配置功能
- 可控制是否启用日志(默认:true)
- 可设置日志级别(默认:INFO)
- 可控制文件/控制台输出
- 支持动态配置(重启生效)

📚 文档
- LOGGING_GUIDE.md: 完整使用指南
- LOGGING_CONFIG.md: 配置说明文档
- LOGGING_CONFIG_QUICK.md: 快速配置指南
- 多个中文说明文档

🔒 安全
- 添加 .gitignore 排除敏感信息
- config.json(含 Token)不提交
- 日志文件不提交
- 示例配置使用占位符

 测试
- 语法检查通过
- 功能完整性验证
- 配置控制测试通过
- 文档完整性检查

详见 CHANGELOG_v1.1.0.md

Made-with: Cursor
This commit is contained in:
fang
2026-03-11 11:37:35 +08:00
commit 5f14174bb9
65 changed files with 12754 additions and 0 deletions

View File

@@ -0,0 +1,149 @@
#!/usr/bin/env node
/**
* DuckDuckGo HTML 搜索 — 免费,无需 API Key
* 通过解析 DuckDuckGo HTML Lite 页面获取搜索结果
*/
function usage() {
console.error('Usage: search.mjs "query" [-n 10] [--region wt-wt]');
console.error(" -n <count> Number of results (default: 10)");
console.error(" --region <r> Region code: cn-zh, us-en, wt-wt (default)");
process.exit(2);
}
const args = process.argv.slice(2);
if (args.length === 0 || args[0] === "-h" || args[0] === "--help") usage();
const query = args[0];
let maxResults = 10;
let region = "wt-wt";
for (let i = 1; i < args.length; i++) {
if (args[i] === "-n") {
maxResults = parseInt(args[++i] || "10", 10);
} else if (args[i] === "--region") {
region = args[++i] || "wt-wt";
}
}
async function searchDDG(query, maxResults, region) {
const tokenUrl = "https://duckduckgo.com/";
const tokenResp = await fetch(tokenUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
},
body: `q=${encodeURIComponent(query)}`,
});
const tokenHtml = await tokenResp.text();
const vqdMatch = tokenHtml.match(/vqd=["']([^"']+)["']/);
if (!vqdMatch) {
const vqdAlt = tokenHtml.match(/vqd=([\d-]+)/);
if (!vqdAlt) {
throw new Error("Failed to get DuckDuckGo search token");
}
var vqd = vqdAlt[1];
} else {
var vqd = vqdMatch[1];
}
const searchUrl =
`https://links.duckduckgo.com/d.js?` +
`q=${encodeURIComponent(query)}` +
`&kl=${region}` +
`&vqd=${vqd}` +
`&o=json&dl=en&ct=US&sp=0&bpa=1&biaexp=b&msvrtexp=b`;
const searchResp = await fetch(searchUrl, {
headers: {
"User-Agent":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
Referer: "https://duckduckgo.com/",
},
});
const text = await searchResp.text();
let data;
try {
data = JSON.parse(text);
} catch {
return fallbackHTMLSearch(query, maxResults);
}
const results = (data.results || [])
.filter((r) => r.u && r.t && !r.u.startsWith("https://duckduckgo.com"))
.slice(0, maxResults)
.map((r) => ({
title: r.t.replace(/<\/?b>/g, ""),
url: r.u,
snippet: (r.a || "").replace(/<\/?b>/g, ""),
}));
return results;
}
async function fallbackHTMLSearch(query, maxResults) {
const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
const resp = await fetch(url, {
headers: {
"User-Agent":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
},
});
const html = await resp.text();
const results = [];
const linkRegex =
/<a[^>]*class="result__a"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/g;
const snippetRegex =
/<a[^>]*class="result__snippet"[^>]*>([\s\S]*?)<\/a>/g;
const links = [...html.matchAll(linkRegex)];
const snippets = [...html.matchAll(snippetRegex)];
for (let i = 0; i < Math.min(links.length, maxResults); i++) {
let rawUrl = links[i][1];
try {
const urlObj = new URL(rawUrl, "https://duckduckgo.com");
const uddg = urlObj.searchParams.get("uddg");
if (uddg) rawUrl = decodeURIComponent(uddg);
} catch {}
results.push({
title: links[i][2].replace(/<[^>]+>/g, "").trim(),
url: rawUrl,
snippet: (snippets[i]?.[1] || "").replace(/<[^>]+>/g, "").trim(),
});
}
return results;
}
try {
const results = await searchDDG(query, maxResults, region);
if (results.length === 0) {
console.log("未找到搜索结果。请尝试不同的关键词。");
process.exit(0);
}
console.log(`## 搜索结果:「${query}\n`);
console.log(`${results.length} 条结果\n`);
for (let i = 0; i < results.length; i++) {
const r = results[i];
console.log(`### ${i + 1}. ${r.title}`);
console.log(` URL: ${r.url}`);
if (r.snippet) {
console.log(` ${r.snippet}`);
}
console.log();
}
} catch (err) {
console.error(`搜索失败: ${err.message}`);
process.exit(1);
}