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 //go:embed logo.jpg
var logoImage []byte 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 // clientConn wraps a WebSocket connection with a write mutex for safe concurrent writes
type clientConn struct { type clientConn struct {
conn *websocket.Conn conn *websocket.Conn
@ -290,6 +305,11 @@ func (c *Channel) Start(ctx context.Context) error {
mux.HandleFunc("/ws", c.handleWebSocket) mux.HandleFunc("/ws", c.handleWebSocket)
mux.HandleFunc("/", c.handleIndex) mux.HandleFunc("/", c.handleIndex)
mux.HandleFunc("/assets/logo.jpg", c.handleLogo) 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) addr := fmt.Sprintf("%s:%d", c.config.Host, c.config.Port)
c.server = &http.Server{ 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.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write(chatHTML) 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="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="format-detection" content="telephone=no"> <meta name="format-detection" content="telephone=no">
<title>PicoClaw · Intelligent Conversation</title> <title>PicoClaw · Intelligent Conversation</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="stylesheet" href="/static/css/fontawesome.min.css">
<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" />
<style> <style>
:root { :root {
--bg-primary: #0f0f0f; --bg-primary: #0f0f0f;
@ -51,7 +48,7 @@
} }
body { 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); background: var(--bg-primary);
color: var(--text-primary); color: var(--text-primary);
height: 100vh; height: 100vh;
@ -517,7 +514,7 @@
border: none; border: none;
font-size: 14px; font-size: 14px;
color: var(--text-primary); color: var(--text-primary);
font-family: 'Inter', sans-serif; font-family: inherit;
outline: none; outline: none;
resize: none; resize: none;
line-height: 1.5; line-height: 1.5;
@ -541,7 +538,7 @@
border-radius: 6px; border-radius: 6px;
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 600;
font-family: 'Inter', sans-serif; font-family: inherit;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
white-space: nowrap; white-space: nowrap;
@ -879,46 +876,41 @@
</div> </div>
</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> <script>
// 配置 marked // Simple markdown-like formatting without external dependencies
marked.setOptions({ function parseMarkdown(text) {
breaks: true, // Escape HTML to prevent XSS
gfm: true, let html = text
headerIds: true, .replace(/&/g, '&amp;')
mangle: false .replace(/</g, '&lt;')
}); .replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
// 配置 DOMPurify 以提供更强的 XSS 防护 .replace(/'/g, '&#039;');
const DOMPURIFY_CONFIG = {
ALLOWED_TAGS: [ // Code blocks (```)
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', html = html.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
'p', 'br', 'hr',
'strong', 'em', 'b', 'i', 'u', 'del', 's', 'code', 'pre', // Bold (**text**)
'ul', 'ol', 'li', html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
'blockquote',
'a', 'img', // Italic (*text*)
'table', 'thead', 'tbody', 'tr', 'th', 'td', html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
'span', 'div'
], // Inline code (`code`)
ALLOWED_ATTR: [ html = html.replace(/`(.+?)`/g, '<code>$1</code>');
'href', 'title', 'alt', 'src',
'class', 'id' // Links [text](url)
], html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
ALLOW_DATA_ATTR: false,
ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp|data):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i, // Headers
FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed', 'form', 'input', 'button'], html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onfocus', 'onblur'], html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
KEEP_CONTENT: true, html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
RETURN_DOM: false,
RETURN_DOM_FRAGMENT: false, // Line breaks
RETURN_TRUSTED_TYPE: false html = html.replace(/\n/g, '<br>');
};
return html;
// 创建安全的 sanitize 函数
function sanitizeHtml(html) {
return DOMPurify.sanitize(html, DOMPURIFY_CONFIG);
} }
let ws; let ws;
@ -1344,9 +1336,7 @@
// 根据发送者类型决定是否渲染 Markdown // 根据发送者类型决定是否渲染 Markdown
if (sender === 'assistant' || sender === 'system') { if (sender === 'assistant' || sender === 'system') {
// AI 回复和系统消息渲染 Markdown // AI 回复和系统消息渲染 Markdown
const rawHtml = marked.parse(content); contentDiv.innerHTML = parseMarkdown(content);
const cleanHtml = sanitizeHtml(rawHtml);
contentDiv.innerHTML = cleanHtml;
} else { } else {
// 用户消息保持纯文本 // 用户消息保持纯文本
contentDiv.textContent = content; contentDiv.textContent = content;
@ -1367,9 +1357,7 @@
const contentDiv = reconnectMessageElement.querySelector('.message-content'); const contentDiv = reconnectMessageElement.querySelector('.message-content');
if (contentDiv) { if (contentDiv) {
// Render Markdown for system messages // Render Markdown for system messages
const rawHtml = marked.parse(newContent); contentDiv.innerHTML = parseMarkdown(newContent);
const cleanHtml = sanitizeHtml(rawHtml);
contentDiv.innerHTML = cleanHtml;
} }
} }
} }
@ -1390,11 +1378,9 @@
contentDiv.style.cursor = 'pointer'; contentDiv.style.cursor = 'pointer';
contentDiv.style.userSelect = 'none'; contentDiv.style.userSelect = 'none';
const rawHtml = marked.parse( contentDiv.innerHTML = parseMarkdown(
t.clickToRetry + '\n\n**[' + t.retryButton + ']**' t.clickToRetry + '\n\n**[' + t.retryButton + ']**'
); );
const cleanHtml = sanitizeHtml(rawHtml);
contentDiv.innerHTML = cleanHtml;
messageDiv.appendChild(contentDiv); messageDiv.appendChild(contentDiv);
messagesDiv.insertBefore(messageDiv, typingIndicator); messagesDiv.insertBefore(messageDiv, typingIndicator);

File diff suppressed because one or more lines are too long