第4讲我们实现了自然语言转代码------MiniCopilot 能根据用户描述生成代码。但生成的代码质量如何?有没有隐藏的 Bug?这一讲,我们要让 MiniCopilot 学会审查代码,像一位经验丰富的代码评审员一样,自动发现潜在问题。
一、代码审查的核心维度
| 维度 | 说明 | 示例 |
|---|---|---|
| 语法错误 | 代码无法通过编译/解释 | 缺少括号、缩进错误 |
| 逻辑错误 | 代码能运行但结果不对 | 除零、无限循环、空指针 |
| 安全漏洞 | 可能存在安全隐患 | SQL 注入、XSS、硬编码密钥 |
| 性能问题 | 代码效率低下 | 不必要的循环、重复计算 |
| 代码异味 | 代码可维护性差 | 过长函数、魔法数字、重复代码 |
| 最佳实践 | 违反语言惯例 | 命名不规范、缺少类型注解 |
二、静态分析引擎
2.1 基于规则的检测器
# engine/analyzer/rules/base.py
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Optional
@dataclass
class Issue:
"""代码问题"""
rule_id: str
severity: str # error / warning / info
message: str
line: int
column: int
suggestion: Optional[str] = None
code_snippet: Optional[str] = None
class Rule(ABC):
"""检测规则基类"""
@abstractmethod
def check(self, node, code: str) -> list[Issue]:
"""检查代码节点,返回发现的问题"""
pass
@property
@abstractmethod
def rule_id(self) -> str:
pass
@property
@abstractmethod
def description(self) -> str:
pass
2.2 具体规则实现
# engine/analyzer/rules/python_rules.py
from .base import Rule, Issue
class BareExceptRule(Rule):
"""检测裸 except"""
rule_id = "E001"
description = "检测没有指定异常类型的 except 语句"
def check(self, node, code: str) -> list[Issue]:
issues = []
if node.type == "except_clause" and not any(
child.type == "type" for child in node.children
):
issues.append(Issue(
rule_id=self.rule_id,
severity="warning",
message="使用裸 except 会捕获所有异常,包括 SystemExit 和 KeyboardInterrupt",
line=node.range.start.line + 1,
column=node.range.start.column,
suggestion="改为 except Exception as e: 来捕获特定异常"
))
return issues
class HardcodedPasswordRule(Rule):
"""检测硬编码密码"""
rule_id = "S001"
description = "检测代码中硬编码的密码或密钥"
# 常见的密码变量名
SENSITIVE_NAMES = {"password", "passwd", "secret", "api_key", "token", "credential"}
def check(self, node, code: str) -> list[Issue]:
issues = []
# 检查赋值语句
if node.type == "assignment":
left = node.children[0] if node.children else None
right = node.children[-1] if len(node.children) > 1 else None
if left and left.type == "identifier":
var_name = left.text.lower()
if var_name in self.SENSITIVE_NAMES:
issues.append(Issue(
rule_id=self.rule_id,
severity="error",
message=f"检测到可能的硬编码凭据: {left.text}",
line=node.range.start.line + 1,
column=node.range.start.column,
suggestion="使用环境变量或密钥管理服务来存储敏感信息"
))
return issues
class LongFunctionRule(Rule):
"""检测过长函数"""
rule_id = "C001"
description = "检测过长的函数(超过50行)"
def check(self, node, code: str) -> list[Issue]:
issues = []
if node.type == "function_definition":
func_lines = node.range.end.line - node.range.start.line
if func_lines > 50:
# 获取函数名
func_name = ""
for child in node.children:
if child.type == "identifier":
func_name = child.text
break
issues.append(Issue(
rule_id=self.rule_id,
severity="warning",
message=f"函数 '{func_name}' 过长({func_lines}行),建议拆分",
line=node.range.start.line + 1,
column=node.range.start.column,
suggestion="考虑将函数拆分为多个小函数,每个函数只做一件事"
))
return issues
class MagicNumberRule(Rule):
"""检测魔法数字"""
rule_id = "C002"
description = "检测没有命名的硬编码数值"
def check(self, node, code: str) -> list[Issue]:
issues = []
# 检测数字字面量(排除 0, 1, -1, True, False 等常见值)
if node.type == "integer" or node.type == "float":
try:
value = float(node.text)
# 跳过常见的无害数字
if value in (0, 1, -1, 100) or value == int(value):
return issues
# 检查是否在比较表达式中
if node.parent and node.parent.type in ("comparison_operator", "binary_operator"):
issues.append(Issue(
rule_id=self.rule_id,
severity="info",
message=f"魔法数字 {node.text},建议定义为命名常量",
line=node.range.start.line + 1,
column=node.range.start.column,
suggestion=f"例如: MAX_RETRIES = {node.text}"
))
except:
pass
return issues
class TodoCommentRule(Rule):
"""检测 TODO 注释"""
rule_id = "C003"
description = "检测遗留的 TODO/FIXME 注释"
def check(self, node, code: str) -> list[Issue]:
issues = []
if node.type == "comment":
text = node.text.lower()
if "todo" in text or "fixme" in text or "hack" in text:
issues.append(Issue(
rule_id=self.rule_id,
severity="info",
message=f"发现遗留标记: {node.text.strip()}",
line=node.range.start.line + 1,
column=node.range.start.column,
suggestion="在提交前处理此标记"
))
return issues
2.3 规则引擎
# engine/analyzer/rules/engine.py
from engine.parser.python_parser import PythonParser
from .python_rules import (
BareExceptRule, HardcodedPasswordRule,
LongFunctionRule, MagicNumberRule, TodoCommentRule
)
class RuleEngine:
"""规则引擎:遍历 AST 并应用所有规则"""
def __init__(self, language: str = "python"):
self.language = language
self.parser = PythonParser() if language == "python" else None
# 注册规则
self.rules = self._register_rules()
def _register_rules(self) -> list:
"""注册所有规则"""
return [
BareExceptRule(),
HardcodedPasswordRule(),
LongFunctionRule(),
MagicNumberRule(),
TodoCommentRule(),
]
def analyze(self, code: str) -> list[Issue]:
"""
分析代码,返回所有发现的问题
参数:
code: 源代码
返回:
[Issue, ...] 按行号排序的问题列表
"""
if not self.parser:
return []
# 解析 AST
ast = self.parser.parse(code)
# 遍历 AST 并应用规则
all_issues = []
self._walk_and_check(ast, code, all_issues)
# 按行号排序
all_issues.sort(key=lambda x: (x.line, x.column))
return all_issues
def _walk_and_check(self, node, code: str, issues: list):
"""递归遍历 AST 节点并应用规则"""
# 对当前节点应用所有规则
for rule in self.rules:
try:
rule_issues = rule.check(node, code)
issues.extend(rule_issues)
except Exception as e:
print(f"规则 {rule.rule_id} 执行出错: {e}")
# 递归子节点
for child in node.children:
self._walk_and_check(child, code, issues)
def get_summary(self, issues: list[Issue]) -> dict:
"""获取问题摘要"""
summary = {
"total": len(issues),
"by_severity": {"error": 0, "warning": 0, "info": 0},
"by_rule": {}
}
for issue in issues:
summary["by_severity"][issue.severity] = \
summary["by_severity"].get(issue.severity, 0) + 1
if issue.rule_id not in summary["by_rule"]:
summary["by_rule"][issue.rule_id] = 0
summary["by_rule"][issue.rule_id] += 1
return summary
三、LLM 驱动的深度审查
静态规则能发现已知模式的问题,但对于复杂的逻辑错误、设计问题,需要 LLM 的理解能力。
# engine/analyzer/llm_reviewer.py
import requests
import json
class LLMCodeReviewer:
"""基于 LLM 的代码审查"""
def __init__(self, api_key: str):
self.api_key = api_key
def review(self, code: str, language: str = "python",
context: str = "") -> dict:
"""
深度审查代码
返回:
{
"overall_assessment": "总体评价",
"bugs": [{"severity", "description", "line", "suggestion"}],
"security_issues": [...],
"performance_issues": [...],
"design_issues": [...],
"improvements": [...]
}
"""
prompt = self._build_review_prompt(code, language, context)
response = requests.post(
"https://api.deepseek.com/v1/chat/completions",
headers={"Authorization": f"Bearer {self.api_key}"},
json={
"model": "deepseek-chat",
"messages": [
{"role": "system", "content": "你是一位资深的代码审查专家。请全面审查代码,发现 Bug、安全漏洞、性能问题和设计缺陷。"},
{"role": "user", "content": prompt}
],
"response_format": {"type": "json_object"},
"temperature": 0.2,
"max_tokens": 4096
}
)
result = response.json()
content = result["choices"][0]["message"]["content"]
try:
review_result = json.loads(content)
return review_result
except:
return {
"overall_assessment": "审查失败",
"bugs": [{"severity": "error", "description": "无法解析 LLM 响应"}],
"security_issues": [],
"performance_issues": [],
"design_issues": [],
"improvements": []
}
def _build_review_prompt(self, code: str, language: str, context: str) -> str:
return f"""请全面审查以下 {language} 代码。
代码:
{language}
{code}
{context}
请输出 JSON 格式的审查结果,包含以下字段:
1. overall_assessment: 总体评价(优秀/良好/一般/需改进)
2. bugs: Bug 列表,每个包含 severity(critical/major/minor)、description、line_number(如果可确定)、suggestion
3. security_issues: 安全问题列表
4. performance_issues: 性能问题列表
5. design_issues: 设计问题列表(如单一职责、耦合度等)
6. improvements: 改进建议列表
重点关注:
- 潜在的运行时错误(空指针、除零、类型错误)
- 并发安全问题(竞态条件、死锁)
- 资源泄漏(文件句柄、数据库连接未关闭)
- 逻辑错误(边界条件处理不当)
- 安全漏洞(注入、XSS、敏感信息泄露)
- 性能瓶颈(不必要的循环、重复计算)
- 代码可维护性(过长函数、过度嵌套、魔法数字)"""
四、混合审查引擎
结合静态规则和 LLM 的优势:
# engine/analyzer/hybrid_reviewer.py
from .rules.engine import RuleEngine
from .llm_reviewer import LLMCodeReviewer
class HybridReviewer:
"""混合审查引擎"""
def __init__(self, api_key: str):
self.rule_engine = RuleEngine()
self.llm_reviewer = LLMCodeReviewer(api_key)
def review(self, code: str, language: str = "python",
deep_analysis: bool = True) -> dict:
"""
混合审查代码
策略:
1. 先用规则引擎快速扫描(< 100ms)
2. 如果发现问题较多或需要深度分析,调用 LLM
3. 合并结果,去重
"""
result = {
"static_issues": [],
"deep_review": None,
"summary": {},
"quality_score": 0
}
# 1. 静态规则检查
static_issues = self.rule_engine.analyze(code)
result["static_issues"] = [
{
"rule_id": i.rule_id,
"severity": i.severity,
"message": i.message,
"line": i.line,
"suggestion": i.suggestion
}
for i in static_issues
]
# 2. LLM 深度审查(可选)
if deep_analysis:
deep_review = self.llm_reviewer.review(code, language)
result["deep_review"] = deep_review
# 3. 计算综合评分
result["quality_score"] = self._calculate_score(result)
# 4. 生成摘要
result["summary"] = self._generate_summary(result)
return result
def _calculate_score(self, result: dict) -> int:
"""计算代码质量评分(0-100)"""
score = 100
# 静态规则扣分
for issue in result["static_issues"]:
if issue["severity"] == "error":
score -= 15
elif issue["severity"] == "warning":
score -= 8
else:
score -= 3
# LLM 审查扣分
deep = result.get("deep_review", {})
if deep:
score -= len(deep.get("bugs", [])) * 10
score -= len(deep.get("security_issues", [])) * 12
score -= len(deep.get("performance_issues", [])) * 5
return max(0, min(100, score))
def _generate_summary(self, result: dict) -> dict:
"""生成审查摘要"""
static_count = len(result["static_issues"])
error_count = sum(1 for i in result["static_issues"] if i["severity"] == "error")
warning_count = sum(1 for i in result["static_issues"] if i["severity"] == "warning")
summary = {
"total_issues": static_count,
"errors": error_count,
"warnings": warning_count,
"quality_score": result["quality_score"]
}
# 添加 LLM 审查摘要
deep = result.get("deep_review", {})
if deep:
summary["bugs_found"] = len(deep.get("bugs", []))
summary["security_issues"] = len(deep.get("security_issues", []))
summary["performance_issues"] = len(deep.get("performance_issues", []))
summary["overall_assessment"] = deep.get("overall_assessment", "未知")
return summary
五、Bug 检测实战
5.1 常见 Bug 模式检测
# engine/analyzer/bug_detector.py
import re
class BugDetector:
"""Bug 检测器:专门检测常见 Bug 模式"""
def detect(self, code: str, language: str = "python") -> list[dict]:
"""检测代码中的 Bug"""
bugs = []
detectors = [
self._check_division_by_zero,
self._check_infinite_loop,
self._check_none_dereference,
self._check_resource_leak,
self._check_race_condition,
]
for detector in detectors:
try:
found = detector(code)
bugs.extend(found)
except Exception as e:
print(f"检测器 {detector.__name__} 出错: {e}")
return bugs
def _check_division_by_zero(self, code: str) -> list[dict]:
"""检测可能的除零操作"""
bugs = []
# 模式:直接除以字面量 0
for match in re.finditer(r'/[\s]*0[\s]*[^.]', code):
line_num = code[:match.start()].count('\n') + 1
bugs.append({
"type": "division_by_zero",
"severity": "critical",
"message": "可能的除零操作",
"line": line_num,
"suggestion": "在进行除法前检查除数是否为0"
})
# 模式:变量可能为0但没有检查
div_pattern = re.finditer(r'/(\w+)', code)
for match in div_pattern:
var_name = match.group(1)
# 检查这个变量之前有没有非零检查
line_start = max(0, match.start() - 200)
before = code[line_start:match.start()]
if var_name not in before: # 简单启发式
line_num = code[:match.start()].count('\n') + 1
bugs.append({
"type": "potential_division_by_zero",
"severity": "major",
"message": f"变量 '{var_name}' 用作除数但未检查是否为0",
"line": line_num,
"suggestion": f"添加 if {var_name} == 0: 的处理"
})
return bugs
def _check_infinite_loop(self, code: str) -> list[dict]:
"""检测可能的无限循环"""
bugs = []
# 检测 while True 没有 break
lines = code.split('\n')
for i, line in enumerate(lines):
stripped = line.strip()
if stripped == 'while True:' or stripped == 'while 1:':
# 检查后续代码是否有 break
following = '\n'.join(lines[i:i+30])
if 'break' not in following:
bugs.append({
"type": "infinite_loop",
"severity": "critical",
"message": "while True 循环中没有找到 break 语句",
"line": i + 1,
"suggestion": "添加循环退出条件或 break 语句"
})
# 检测 for 循环中修改迭代变量
for match in re.finditer(r'for (\w+) in', code):
var_name = match.group(1)
after = code[match.end():match.end()+500]
# 检查迭代变量是否被修改
if re.search(rf'\b{var_name}\s*=', after):
line_num = code[:match.start()].count('\n') + 1
bugs.append({
"type": "modified_iterator",
"severity": "warning",
"message": f"循环变量 '{var_name}' 在循环体内被修改,可能导致意外行为",
"line": line_num,
"suggestion": "使用不同的变量名来存储修改后的值"
})
return bugs
def _check_none_dereference(self, code: str) -> list[dict]:
"""检测可能的空指针/None 解引用"""
bugs = []
# 检测函数返回值可能为 None 但没有检查
none_return_functions = ['find', 'get', 'first_or_null', 'optional']
for func in none_return_functions:
pattern = rf'{func}\(.*?\)\.'
for match in re.finditer(pattern, code):
before = code[max(0, match.start()-100):match.start()]
# 检查前面是否有 None 检查
if 'is None' not in before and 'is not None' not in before:
line_num = code[:match.start()].count('\n') + 1
bugs.append({
"type": "none_dereference",
"severity": "critical",
"message": f"'{func}()' 的返回值可能为 None,直接访问属性可能引发 AttributeError",
"line": line_num,
"suggestion": f"先检查返回值是否为 None: result = {func}(...); if result is not None:"
})
return bugs
def _check_resource_leak(self, code: str) -> list[dict]:
"""检测资源泄漏"""
bugs = []
# 检测 open() 没有使用 with 语句
for match in re.finditer(r'\bopen\(', code):
before = code[:match.start()]
# 检查是否在 with 语句中
line_start = before.rfind('\n') + 1 if '\n' in before else 0
line_prefix = before[line_start:]
if 'with' not in line_prefix:
line_num = code[:match.start()].count('\n') + 1
bugs.append({
"type": "resource_leak",
"severity": "major",
"message": "文件打开操作未使用 with 语句,可能导致文件句柄泄漏",
"line": line_num,
"suggestion": "使用 'with open(...) as f:' 确保文件自动关闭"
})
return bugs
def _check_race_condition(self, code: str) -> list[dict]:
"""检测可能的竞态条件"""
bugs = []
# 检测文件操作没有加锁
if 'threading' in code or 'Thread' in code:
file_ops = re.finditer(r'(open|write|read)\s*\(', code)
has_lock = 'Lock' in code or 'RLock' in code
if file_ops and not has_lock:
for match in file_ops:
line_num = code[:match.start()].count('\n') + 1
bugs.append({
"type": "race_condition",
"severity": "major",
"message": "多线程环境下进行文件操作但没有使用锁",
"line": line_num,
"suggestion": "使用 threading.Lock() 保护共享资源的访问"
})
break # 只报告一次
return bugs
5.2 完整审查演示
# test_reviewer.py
from engine.analyzer.hybrid_reviewer import HybridReviewer
from engine.analyzer.bug_detector import BugDetector
import json
# 初始化
api_key = "your-api-key"
reviewer = HybridReviewer(api_key)
bug_detector = BugDetector()
# 测试代码(故意包含各种问题)
test_code = """
import os
def process_user_data(user_input):
# TODO: 添加输入验证
query = "SELECT * FROM users WHERE id = " + user_input
os.system("echo " + user_input)
result = find_user(user_input)
return result.name
def calculate_discount(price, rate):
result = price / rate
return result
def read_config():
file = open("config.txt")
data = file.read()
return data
def complex_function(a, b, c, d, e, f, g, h, i, j):
x = a + b
y = c * d
z = e - f
w = g / h
v = i ** j
result = x + y + z + w + v
result = result * 42
result = result / 7
result = result - 12345
return result
password = "supersecret123"
while True:
print("Running...")
"""
print("=" * 60)
print("🔍 代码审查报告")
print("=" * 60)
# 1. 静态规则检查
print("\n📋 静态规则检查:")
result = reviewer.review(test_code, deep_analysis=False)
for issue in result["static_issues"]:
icon = {"error": "❌", "warning": "⚠️", "info": "ℹ️"}
print(f" {icon.get(issue['severity'], '•')} [{issue['severity'].upper()}] "
f"第{issue['line']}行: {issue['message']}")
if issue.get('suggestion'):
print(f" 💡 {issue['suggestion']}")
print(f"\n📊 统计: {result['summary']['total_issues']} 个问题 "
f"(错误: {result['summary']['errors']}, "
f"警告: {result['summary']['warnings']})")
# 2. Bug 检测
print("\n🐛 Bug 检测:")
bugs = bug_detector.detect(test_code)
for bug in bugs:
print(f" [{bug['severity'].upper()}] 第{bug['line']}行: {bug['message']}")
print(f" 💡 {bug['suggestion']}")
# 3. LLM 深度审查
print("\n🧠 LLM 深度审查:")
deep_result = reviewer.review(test_code, deep_analysis=True)
if deep_result.get("deep_review"):
dr = deep_result["deep_review"]
print(f"\n总体评价: {dr.get('overall_assessment', '未知')}")
for bug in dr.get("bugs", []):
print(f" ❌ [{bug.get('severity', 'unknown')}] {bug.get('description', '')}")
if bug.get('suggestion'):
print(f" 💡 {bug['suggestion']}")
for sec in dr.get("security_issues", []):
print(f" 🔒 {sec}")
for perf in dr.get("performance_issues", []):
print(f" ⚡ {perf}")
# 4. 最终评分
print(f"\n{'=' * 60}")
print(f"🏆 代码质量评分: {result['quality_score']}/100")
if result['quality_score'] >= 80:
print("✅ 代码质量良好")
elif result['quality_score'] >= 60:
print("⚠️ 代码需要改进")
else:
print("❌ 代码存在严重问题")
六、IDE 集成:代码诊断
# plugin/diagnostics.py
class DiagnosticProvider:
"""IDE 诊断提供者"""
def __init__(self, reviewer):
self.reviewer = reviewer
self.diagnostics = {}
def update_diagnostics(self, file_path: str, code: str):
"""更新文件的诊断信息"""
result = self.reviewer.review(code, deep_analysis=False)
diagnostics = []
# 转换问题为 IDE 诊断格式
for issue in result["static_issues"]:
severity_map = {
"error": 1, # Error
"warning": 2, # Warning
"info": 3, # Information
}
diagnostic = {
"range": {
"start": {"line": issue["line"] - 1, "character": 0},
"end": {"line": issue["line"] - 1, "character": 100}
},
"severity": severity_map.get(issue["severity"], 3),
"message": issue["message"],
"source": "MiniCopilot",
"code": issue.get("rule_id", "unknown"),
"relatedInformation": []
}
if issue.get("suggestion"):
diagnostic["relatedInformation"].append({
"location": diagnostic["range"],
"message": f"💡 {issue['suggestion']}"
})
diagnostics.append(diagnostic)
self.diagnostics[file_path] = diagnostics
return diagnostics
def get_diagnostics(self, file_path: str) -> list:
"""获取文件的诊断信息"""
return self.diagnostics.get(file_path, [])
七、性能优化
7.1 增量审查
class IncrementalReviewer:
"""增量审查:只审查变化的部分"""
def __init__(self, reviewer):
self.reviewer = reviewer
self.file_versions = {}
def review_change(self, file_path: str, old_code: str, new_code: str):
"""审查文件的变化"""
if file_path not in self.file_versions:
# 首次审查,全量检查
result = self.reviewer.review(new_code)
self.file_versions[file_path] = new_code
return result
# 计算差异
import difflib
diff = list(difflib.unified_diff(
old_code.splitlines(keepends=True),
new_code.splitlines(keepends=True)
))
# 如果改动很小,只审查变化行
if len(diff) < 20:
changed_lines = set()
for line in diff:
if line.startswith('@@'):
# 解析行号
match = re.search(r'\+(\d+)', line)
if match:
changed_lines.add(int(match.group(1)))
# 只审查变化行附近的代码
context_lines = set()
for line in changed_lines:
for offset in range(-5, 6):
context_lines.add(line + offset)
lines = new_code.split('\n')
relevant_code = '\n'.join(
lines[i] for i in range(len(lines))
if i + 1 in context_lines
)
result = self.reviewer.review(relevant_code)
else:
# 改动较大,全量审查
result = self.reviewer.review(new_code)
self.file_versions[file_path] = new_code
return result
八、常见错误 & 排坑指南
-
误报太多
-
问题:规则引擎过于严格,产生大量误报
-
解决:配置规则阈值,允许用户忽略某些规则
-
-
LLM 审查不稳定
-
问题:相同代码每次审查结果不同
-
解决:降低 temperature,多次审查取交集
-
-
大型文件审查超时
-
问题:超过千行的文件审查耗时过长
-
解决:分段审查,只审查变更部分
-
-
安全规则误伤测试代码
-
问题:测试代码中的硬编码密码被误报
-
解决:根据文件路径自动调整规则集
-
九、课后作业
-
实现自定义规则:写一条规则检测"函数参数超过5个",这是代码坏味道之一。
-
添加安全规则:实现 SQL 注入检测规则,识别拼接 SQL 查询的代码。
-
挑战题:实现"自动修复建议"------对于常见问题(如裸 except),自动生成修复后的代码。
十、总结
这一讲我们实现了代码审查与 Bug 检测:
-
静态规则引擎:基于 AST 的规则检查,快速发现已知模式的问题
-
LLM 深度审查:利用大模型的语义理解能力,发现复杂逻辑错误
-
Bug 检测器:专门检测除零、无限循环、空指针、资源泄漏等问题
-
混合审查策略:规则引擎保速度,LLM 保深度
-
IDE 集成:将审查结果转换为 IDE 诊断信息
现在,MiniCopilot 不仅能写代码,还能像资深工程师一样审查代码质量。
下一讲,我们将实现单元测试自动生成------让 MiniCopilot 自动为代码生成测试用例。
🧰 开发之余,处理 Base64、JWT 解析、JSON 格式化、Crontab 计算、PDF 合并压缩这些碎片需求,我常用一个纯前端本地工具箱:zz365.top (子页 PDF 大师:PDF 大师 - zz365工具箱)。所有计算在浏览器完成,文件不上传服务器,关页即清。免费、无登录、无广告,适合开发者当常驻标签页。