#!/usr/bin/env python3
"""代码浏览器 — 只读,为 /root/solution-1.0/ 提供 GitHub 风格的文件浏览"""
import http.server
import os
import urllib.parse
import json
ROOT = '/root/solution-1.0'
PORT = 8082
# 文件类型图标
ICON_MAP = {
'.py': '🐍', '.js': '🟨', '.html': '🌐', '.css': '🎨',
'.md': '📝', '.json': '📋', '.yaml': '⚙️', '.yml': '⚙️',
'.sh': '💻', '.conf': '⚙️', '.txt': '📄', '.env': '🔒',
'.gitignore': '🚫', '.example': '📋',
}
def get_icon(name):
_, ext = os.path.splitext(name)
return ICON_MAP.get(ext, '📄')
class CodeBrowserHandler(http.server.SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=ROOT, **kwargs)
def log_message(self, format, *args):
pass # 安静日志
TEXT_EXTS = {'.py','.js','.html','.css','.md','.json','.yaml','.yml',
'.sh','.conf','.txt','.env','.example','.gitignore','.toml'}
LANG_MAP = {'.py':'python','.js':'javascript','.html':'html','.css':'css',
'.md':'markdown','.json':'json','.yaml':'yaml','.yml':'yaml',
'.sh':'bash','.conf':'nginx','.toml':'toml'}
def send_head(self):
path = self.translate_path(self.path)
if os.path.isdir(path):
return self.list_directory_html(path)
# 代码文件 → 生成语法高亮 HTML(在发 headers 之前完成)
if os.path.isfile(path):
_, ext = os.path.splitext(path)
if ext in self.TEXT_EXTS or not ext:
html = self._render_source_page(path, ext)
if html:
encoded = html.encode('utf-8')
self.send_response(200)
self.send_header('Content-Type', 'text/html; charset=utf-8')
self.send_header('Content-Length', str(len(encoded)))
self.send_header('Cache-Control', 'no-cache')
self.end_headers()
return self._buffer(encoded)
return super().send_head()
def _render_source_page(self, path, ext):
"""生成语法高亮 HTML 源码页"""
try:
content = open(path, 'rb').read()
text = content.decode('utf-8')
except:
return None
lang = self.LANG_MAP.get(ext, 'plaintext')
rel = os.path.relpath(path, ROOT)
breadcrumb = self._breadcrumb(os.path.dirname(rel))
escaped = text.replace('&','&').replace('<','<').replace('>','>')
return f'''<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="utf-8">
<title>{os.path.basename(path)} - solution-1.0</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
<style>
* {{ margin:0; padding:0; box-sizing:border-box; }}
body {{ background:#0d1117; color:#c9d1d9; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; }}
.header {{ background:#161b22; border-bottom:1px solid #30363d; padding:16px 24px; }}
.header h1 a {{ color:#58a6ff; text-decoration:none; font-size:16px; }}
.breadcrumb {{ padding:8px 24px; font-size:14px; color:#58a6ff; }}
.breadcrumb a {{ color:#58a6ff; text-decoration:none; }}
.breadcrumb a:hover {{ text-decoration:underline; }}
.breadcrumb .sep {{ color:#484f58; padding:0 4px; }}
.code-view pre {{ padding:16px 24px; font-size:13px; line-height:1.5; tab-size:4; margin:0; overflow-x:auto; }}
.code-view pre code {{ background:transparent !important; padding:0 !important; }}
.back {{ display:block; padding:8px 24px; color:#58a6ff; font-size:14px; text-decoration:none; }}
.back:hover {{ text-decoration:underline; }}
</style>
</head>
<body>
<div class="header"><h1><a href="/">📂 solution-1.0</a></h1></div>
<div class="breadcrumb">{breadcrumb}</div>
<a class="back" href="{os.path.dirname(self.path)}">← 返回上级</a>
<div class="code-view"><pre><code class="language-{lang}">{escaped}</code></pre></div>
</body>
</html>'''
def list_directory_html(self, path):
"""生成 GitHub 风格的文件列表页面"""
entries = []
try:
for name in sorted(os.listdir(path)):
if name.startswith('.'):
continue
full = os.path.join(path, name)
is_dir = os.path.isdir(full)
size = os.path.getsize(full) if not is_dir else ''
icon = get_icon(name) if not is_dir else '📁'
entries.append((name, is_dir, size, icon))
except OSError:
pass
rel = os.path.relpath(path, ROOT)
breadcrumb = self._breadcrumb(rel)
html = f'''<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{'solution-1.0/' + rel if rel != '.' else 'solution-1.0/'}</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0d1117; color: #c9d1d9; }}
.header {{ background: #161b22; border-bottom: 1px solid #30363d; padding: 16px 24px; }}
.header h1 {{ font-size: 16px; font-weight: 600; }}
.header h1 a {{ color: #58a6ff; text-decoration: none; }}
.header h1 a:hover {{ text-decoration: underline; }}
.header .sub {{ color: #8b949e; font-size: 12px; margin-top: 4px; }}
.breadcrumb {{ padding: 8px 24px; background: #0d1117; border-bottom: 1px solid #21262d;
font-size: 14px; color: #58a6ff; }}
.breadcrumb a {{ color: #58a6ff; text-decoration: none; }}
.breadcrumb a:hover {{ text-decoration: underline; }}
.breadcrumb .sep {{ color: #484f58; padding: 0 4px; }}
.file-list {{ margin: 0 24px; }}
.file-row {{ display: flex; align-items: center; padding: 8px 0; border-bottom: 1px solid #21262d;
font-size: 14px; cursor: pointer; }}
.file-row:hover {{ background: #161b22; }}
.file-row .icon {{ width: 24px; text-align: center; font-size: 16px; }}
.file-row .name {{ flex: 1; margin-left: 8px; }}
.file-row .name a {{ color: #c9d1d9; text-decoration: none; }}
.file-row:hover .name a {{ color: #58a6ff; }}
.file-row .size {{ color: #8b949e; font-size: 12px; width: 80px; text-align: right; }}
.code-view {{ padding: 0; max-width: 100%; overflow-x: auto; }}
.code-view pre {{ padding: 16px 24px; font-size: 13px; line-height: 1.5; tab-size: 4; }}
.code-view pre code {{ background: transparent; padding: 0; }}
.back-link {{ display: inline-block; margin: 16px 24px; color: #58a6ff; text-decoration: none; font-size: 14px; }}
.back-link:hover {{ text-decoration: underline; }}
</style>
</head>
<body>
<div class="header">
<h1><a href="/">📂 solution-1.0</a></h1>
<div class="sub">Hermes Agent Solution · 只读代码浏览器</div>
</div>
<div class="breadcrumb">{breadcrumb}</div>
<div class="file-list">
'''
# 父目录
if rel != '.':
parent = os.path.dirname(rel)
if parent == '.':
parent = ''
html += f'''<div class="file-row">
<div class="icon">📂</div>
<div class="name"><a href="/{parent}">..</a></div>
<div class="size"></div>
</div>'''
for name, is_dir, size, icon in entries:
href = urllib.parse.quote(name)
if is_dir:
href += '/'
size_str = self._format_size(size) if size != '' else ''
html += f'''<div class="file-row">
<div class="icon">{icon}</div>
<div class="name"><a href="{href}">{name}</a></div>
<div class="size">{size_str}</div>
</div>'''
html += '''</div>
</body>
</html>'''
encoded = html.encode('utf-8')
self.send_response(200)
self.send_header('Content-Type', 'text/html; charset=utf-8')
self.send_header('Content-Length', str(len(encoded)))
self.send_header('Cache-Control', 'no-cache')
self.end_headers()
return self._buffer(html.encode('utf-8'))
def _buffer(self, data):
import io
buf = io.BytesIO(data)
buf.seek(0)
return buf
def _breadcrumb(self, rel):
parts = rel.split(os.sep) if rel != '.' else []
html = '<a href="/">solution-1.0</a>'
path = ''
for p in parts:
path = os.path.join(path, p)
html += f'<span class="sep">/</span><a href="/{path}">{p}</a>'
return html
def _format_size(self, size):
for unit in ['B', 'KB', 'MB', 'GB']:
if size < 1024:
return f'{size:.1f} {unit}' if unit != 'B' else f'{size} B'
size /= 1024
return f'{size:.1f} TB'
def copyfile(self, source, outputfile):
"""委托给父类(所有逻辑已移到 send_head)"""
return super().copyfile(source, outputfile)
if __name__ == '__main__':
server = http.server.HTTPServer(('127.0.0.1', PORT), CodeBrowserHandler)
print(f'📂 Code Browser: http://127.0.0.1:{PORT}/')
print(f'📁 Serving: {ROOT}')
server.serve_forever()