Fixed the issue of External CDN dependencies in embedded HTML, replaced Google Fonts with the system's native font stack, and replaced FontAwesome loaded via CDN with built-in files.

This commit is contained in:
likeaturtle 2026-02-23 11:11:19 +08:00
parent 3d1e0dc71c
commit a50bb97ab1
7 changed files with 105 additions and 55 deletions

View file

@ -27,6 +27,21 @@ var chatHTML []byte
//go:embed logo.jpg
var logoImage []byte
//go:embed static/css/fontawesome.min.css
var fontawesomeCSS []byte
//go:embed static/webfonts/fa-brands-400.woff2
var faBrandsFont []byte
//go:embed static/webfonts/fa-regular-400.woff2
var faRegularFont []byte
//go:embed static/webfonts/fa-solid-900.woff2
var faSolidFont []byte
//go:embed static/webfonts/fa-v4compatibility.woff2
var faV4CompatFont []byte
// clientConn wraps a WebSocket connection with a write mutex for safe concurrent writes
type clientConn struct {
conn *websocket.Conn
@ -290,6 +305,11 @@ func (c *Channel) Start(ctx context.Context) error {
mux.HandleFunc("/ws", c.handleWebSocket)
mux.HandleFunc("/", c.handleIndex)
mux.HandleFunc("/assets/logo.jpg", c.handleLogo)
mux.HandleFunc("/static/css/fontawesome.min.css", c.handleFontawesomeCSS)
mux.HandleFunc("/static/webfonts/fa-brands-400.woff2", c.handleFaBrandsFont)
mux.HandleFunc("/static/webfonts/fa-regular-400.woff2", c.handleFaRegularFont)
mux.HandleFunc("/static/webfonts/fa-solid-900.woff2", c.handleFaSolidFont)
mux.HandleFunc("/static/webfonts/fa-v4compatibility.woff2", c.handleFaV4CompatFont)
addr := fmt.Sprintf("%s:%d", c.config.Host, c.config.Port)
c.server = &http.Server{
@ -626,3 +646,38 @@ func (c *Channel) handleIndex(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write(chatHTML)
}
// handleFontawesomeCSS serves the FontAwesome CSS file
func (c *Channel) handleFontawesomeCSS(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/css")
w.Header().Set("Cache-Control", "public, max-age=86400") // Cache for 1 day
w.Write(fontawesomeCSS)
}
// handleFaBrandsFont serves the FontAwesome brands font file
func (c *Channel) handleFaBrandsFont(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "font/woff2")
w.Header().Set("Cache-Control", "public, max-age=86400") // Cache for 1 day
w.Write(faBrandsFont)
}
// handleFaRegularFont serves the FontAwesome regular font file
func (c *Channel) handleFaRegularFont(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "font/woff2")
w.Header().Set("Cache-Control", "public, max-age=86400") // Cache for 1 day
w.Write(faRegularFont)
}
// handleFaSolidFont serves the FontAwesome solid font file
func (c *Channel) handleFaSolidFont(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "font/woff2")
w.Header().Set("Cache-Control", "public, max-age=86400") // Cache for 1 day
w.Write(faSolidFont)
}
// handleFaV4CompatFont serves the FontAwesome v4 compatibility font file
func (c *Channel) handleFaV4CompatFont(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "font/woff2")
w.Header().Set("Cache-Control", "public, max-age=86400") // Cache for 1 day
w.Write(faV4CompatFont)
}

View file

@ -7,10 +7,7 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="format-detection" content="telephone=no">
<title>PicoClaw · Intelligent Conversation</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<link rel="stylesheet" href="/static/css/fontawesome.min.css">
<style>
:root {
--bg-primary: #0f0f0f;
@ -51,7 +48,7 @@
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
background: var(--bg-primary);
color: var(--text-primary);
height: 100vh;
@ -517,7 +514,7 @@
border: none;
font-size: 14px;
color: var(--text-primary);
font-family: 'Inter', sans-serif;
font-family: inherit;
outline: none;
resize: none;
line-height: 1.5;
@ -541,7 +538,7 @@
border-radius: 6px;
font-size: 13px;
font-weight: 600;
font-family: 'Inter', sans-serif;
font-family: inherit;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
@ -879,46 +876,41 @@
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.8/dist/purify.min.js"></script>
<script>
// 配置 marked
marked.setOptions({
breaks: true,
gfm: true,
headerIds: true,
mangle: false
});
// 配置 DOMPurify 以提供更强的 XSS 防护
const DOMPURIFY_CONFIG = {
ALLOWED_TAGS: [
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'p', 'br', 'hr',
'strong', 'em', 'b', 'i', 'u', 'del', 's', 'code', 'pre',
'ul', 'ol', 'li',
'blockquote',
'a', 'img',
'table', 'thead', 'tbody', 'tr', 'th', 'td',
'span', 'div'
],
ALLOWED_ATTR: [
'href', 'title', 'alt', 'src',
'class', 'id'
],
ALLOW_DATA_ATTR: false,
ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp|data):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i,
FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed', 'form', 'input', 'button'],
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onfocus', 'onblur'],
KEEP_CONTENT: true,
RETURN_DOM: false,
RETURN_DOM_FRAGMENT: false,
RETURN_TRUSTED_TYPE: false
};
// 创建安全的 sanitize 函数
function sanitizeHtml(html) {
return DOMPurify.sanitize(html, DOMPURIFY_CONFIG);
// Simple markdown-like formatting without external dependencies
function parseMarkdown(text) {
// Escape HTML to prevent XSS
let html = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
// Code blocks (```)
html = html.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
// Bold (**text**)
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
// Italic (*text*)
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
// Inline code (`code`)
html = html.replace(/`(.+?)`/g, '<code>$1</code>');
// Links [text](url)
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
// Headers
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
// Line breaks
html = html.replace(/\n/g, '<br>');
return html;
}
let ws;
@ -1344,9 +1336,7 @@
// 根据发送者类型决定是否渲染 Markdown
if (sender === 'assistant' || sender === 'system') {
// AI 回复和系统消息渲染 Markdown
const rawHtml = marked.parse(content);
const cleanHtml = sanitizeHtml(rawHtml);
contentDiv.innerHTML = cleanHtml;
contentDiv.innerHTML = parseMarkdown(content);
} else {
// 用户消息保持纯文本
contentDiv.textContent = content;
@ -1367,9 +1357,7 @@
const contentDiv = reconnectMessageElement.querySelector('.message-content');
if (contentDiv) {
// Render Markdown for system messages
const rawHtml = marked.parse(newContent);
const cleanHtml = sanitizeHtml(rawHtml);
contentDiv.innerHTML = cleanHtml;
contentDiv.innerHTML = parseMarkdown(newContent);
}
}
}
@ -1390,11 +1378,9 @@
contentDiv.style.cursor = 'pointer';
contentDiv.style.userSelect = 'none';
const rawHtml = marked.parse(
contentDiv.innerHTML = parseMarkdown(
t.clickToRetry + '\n\n**[' + t.retryButton + ']**'
);
const cleanHtml = sanitizeHtml(rawHtml);
contentDiv.innerHTML = cleanHtml;
messageDiv.appendChild(contentDiv);
messagesDiv.insertBefore(messageDiv, typingIndicator);

File diff suppressed because one or more lines are too long