Files
wechatAiclaw/public/chat.html
丹尼尔 b7ef2569c4 fix: bug
2026-03-11 18:19:30 +08:00

318 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>实时消息 - WS 同步展示</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
:root {
color-scheme: light dark;
--bg: #0f172a;
--card: #020617;
--border: #1e293b;
--accent: #22c55e;
--accent-soft: rgba(34, 197, 94, 0.12);
--text: #e5e7eb;
--muted: #9ca3af;
--radius: 16px;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background: radial-gradient(circle at top, #1e293b 0, #020617 55%, #000 100%);
font-family: system-ui, -apple-system, sans-serif;
color: var(--text);
}
.nav {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
padding: 12px 24px;
border-bottom: 1px solid var(--border);
background: rgba(2, 6, 23, 0.95);
}
.nav-links { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.nav a {
color: var(--muted);
text-decoration: none;
font-size: 14px;
}
.nav a:hover { color: var(--accent); }
.nav a.current { color: var(--accent); font-weight: 500; }
.nav-banner { font-size: 18px; font-weight: 700; letter-spacing: 0.04em; color: var(--text); text-shadow: 0 0 20px rgba(34, 197, 94, 0.2); }
.nav-banner span { color: var(--accent); }
.container {
max-width: 720px;
margin: 0 auto;
padding: 24px;
}
.card {
border-radius: var(--radius);
border: 1px solid var(--border);
background: radial-gradient(circle at top, rgba(15, 23, 42, 0.9), #020617);
padding: 20px;
margin-bottom: 20px;
}
.card-title { font-size: 14px; font-weight: 600; margin-bottom: 16px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; }
.small-label { font-size: 12px; color: var(--muted); margin-bottom: 8px; }
.key-row { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
.key-row input { max-width: 280px; padding: 8px 12px; border: 1px solid var(--border); border-radius: 8px; background: rgba(15,23,42,0.9); color: var(--text); }
.msg-list {
max-height: 360px;
overflow: auto;
border: 1px solid var(--border);
border-radius: 10px;
background: rgba(15, 23, 42, 0.6);
padding: 8px;
margin-bottom: 16px;
}
.msg-item { font-size: 12px; padding: 8px 10px; border-bottom: 1px solid var(--border); display:flex; flex-direction:column; gap:4px; }
.msg-item:last-child { border-bottom: none; }
.msg-item .from { color: var(--accent); margin-right: 8px; }
.msg-item.out { opacity: 0.9; }
.msg-item.out .from { color: #94a3b8; }
.msg-item .time { font-size: 11px; color: var(--muted); margin-left: 8px; }
.msg-item .meta { display:flex; align-items:center; flex-wrap:wrap; }
.msg-item .content { margin-left: 0; font-size: 12px; word-break: break-all; }
.msg-item .content img { max-width: 100%; border-radius: 6px; margin-top: 4px; }
.msg-item .content audio,
.msg-item .content video { max-width: 100%; margin-top: 4px; }
.form-row { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; margin-bottom: 10px; }
.form-row label { display: block; font-size: 12px; color: var(--muted); margin-bottom: 4px; }
.form-row input { padding: 8px 12px; border: 1px solid var(--border); border-radius: 8px; background: rgba(15,23,42,0.9); color: var(--text); font-size: 13px; }
.form-row input[type="text"] { min-width: 180px; }
.form-row input[name="content"] { flex: 1; min-width: 200px; }
.primary { padding: 10px 20px; border-radius: 8px; background: var(--accent); color: #000; border: none; cursor: pointer; font-weight: 500; }
.secondary { padding: 8px 16px; border-radius: 8px; background: rgba(30,41,59,0.9); border: 1px solid var(--border); color: var(--text); cursor: pointer; font-size: 13px; }
</style>
</head>
<body>
<nav class="nav">
<div class="nav-banner">Wechat <span>智能托管服务</span></div>
<div class="nav-links">
<a href="manage.html">客户与消息管理</a>
<a href="chat.html" class="current">实时消息</a>
<a href="models.html">模型管理</a>
<a href="swagger.html" target="_blank">API 文档 (Swagger)</a>
</div>
</nav>
<div class="container">
<div class="card">
<div class="card-title">实时展示 WS 返回的消息</div>
<p class="small-label">后端通过 GetSyncMsg 连接微信服务,收到消息后写入存储;此处拉取并展示,非传统聊天界面。</p>
<div class="key-row">
<label for="key">账号 key</label>
<input type="text" id="key" placeholder="与登录页一致" autocomplete="off" />
</div>
<button type="button" class="secondary" id="btn-refresh-msg" style="margin-bottom:12px">刷新消息列表</button>
<div id="message-list" class="msg-list"></div>
</div>
<div class="card">
<div class="card-title">发送消息</div>
<div class="form-row">
<div>
<label for="send-to">对方 wxid / 用户名</label>
<input type="text" id="send-to" name="to" placeholder="wxid_xxx" />
</div>
<div style="flex:1;min-width:200px">
<label for="send-content">内容</label>
<input type="text" id="send-content" name="content" placeholder="消息内容" />
</div>
<button type="button" class="primary" id="btn-send-msg">发送</button>
</div>
</div>
</div>
<script>
const $ = (id) => document.getElementById(id);
// 相对路径,由 Node 代理到后端,适配 -p/-b 任意端口
const API_BASE = '';
const KEY_STORAGE = 'wechat_key';
function getToken() {
try {
return localStorage.getItem('auth_token') || '';
} catch (_) {
return '';
}
}
function redirectToLogin(msg) {
if (msg) alert(msg);
window.location.href = 'index.html';
}
async function callApi(path, options = {}) {
const url = API_BASE + path;
const token = getToken();
const headers = { 'Content-Type': 'application/json', ...(options.headers || {}) };
if (token) headers['Authorization'] = 'Bearer ' + token;
const res = await fetch(url, { ...options, headers });
let body = null;
try { body = await res.json(); } catch (_) {}
if (res.status === 401) {
redirectToLogin('登录已失效,请重新扫码登录');
throw new Error('unauthorized');
}
if (!res.ok) throw new Error((body && (body.detail || body.message)) || res.statusText || '请求失败');
return body;
}
function renderMessageContent(m) {
const msgType = m.MsgType ?? m.msgType;
const rawContent = m.Content || m.content || '';
const imageContent = m.ImageContent || m.imageContent || '';
const from = m.FromUserName || m.from || m.MsgId || '-';
// 链接检测
const isUrl = (s) => typeof s === 'string' && /^https?:\/\//i.test(s.trim());
const isBase64Like = (s) => typeof s === 'string' && /^[A-Za-z0-9+/=\s]+$/.test(s) && s.replace(/\s+/g, '').length > 60;
// 图片:上游通常提供 ImageContentbase64或 Content 为图片链接
if (imageContent || (msgType === 3) || (msgType === 0 && imageContent)) {
const src = isUrl(imageContent) ? imageContent :
(imageContent ? ('data:image/png;base64,' + imageContent.replace(/\s+/g, '')) :
(isUrl(rawContent) ? rawContent : ''));
if (src) {
return `<div class="content"><div>${rawContent ? String(rawContent) : ''}</div><img src="${src}" alt="图片消息" /></div>`;
}
}
// 视频 / 音频:简单通过扩展名或 MsgType 约定判断
if (msgType === 43 || msgType === 'video') {
const src = isUrl(rawContent) ? rawContent : '';
if (src) return `<div class="content"><video src="${src}" controls></video></div>`;
}
if (msgType === 34 || msgType === 'audio') {
const src = isUrl(rawContent) ? rawContent : '';
if (src) return `<div class="content"><audio src="${src}" controls></audio></div>`;
}
// 若内容是 URL则渲染为可点击链接
if (isUrl(rawContent)) {
const safe = String(rawContent);
return `<div class="content"><a href="${safe}" target="_blank" rel="noopener noreferrer">${safe}</a></div>`;
}
// 若内容看起来是图片 base64则按图片渲染
if (isBase64Like(rawContent) && (msgType === 3 || msgType === 0)) {
const src = 'data:image/png;base64,' + String(rawContent).replace(/\s+/g, '');
return `<div class="content"><img src="${src}" alt="图片消息" /></div>`;
}
// 兜底为纯文本(含 MsgType 提示)
const text = rawContent ? String(rawContent) : (msgType != null ? `MsgType=${msgType}` : '');
return `<div class="content">${text}</div>`;
}
async function loadMessages() {
const key = (function() {
try {
return localStorage.getItem(KEY_STORAGE) || '';
} catch (_) {
return '';
}
})();
if (!key) {
$('message-list').innerHTML = '<p class="small-label">请先在登录页扫码登录。</p>';
return;
}
try {
// 后端已按时间倒序(最新在前)返回
const data = await callApi('/api/messages?key=' + encodeURIComponent(key) + '&limit=80');
let list = data.items || [];
// 仅展示对话类消息:文本消息或我发出的消息,过滤掉系统通知、非会话类型
list = list.filter(m => {
const t = m.MsgType ?? m.msgType;
const dir = m.direction;
const content = (m.Content || m.content || '').toString().trim();
if (dir === 'out') return true;
if (t === 1 || t === '1') return !!content;
return false;
});
$('message-list').innerHTML = list.length ? list.map(m => {
const isOut = m.direction === 'out';
const fromLabel = isOut ? ('我 → ' + (m.ToUserName || '')) : (m.FromUserName || m.from || m.MsgId || '-').toString().slice(0, 32);
const time = m.CreateTime ? (typeof m.CreateTime === 'number'
? new Date(m.CreateTime * 1000).toLocaleTimeString('zh-CN', { hour12: false })
: m.CreateTime) : '';
const meta = '<div class="meta"><span class="from">' + fromLabel + '</span>' + (time ? '<span class="time">' + time + '</span>' : '') + '</div>';
const body = renderMessageContent(m);
return '<div class="msg-item' + (isOut ? ' out' : '') + '">' + meta + body + '</div>';
}).join('') : '<p class="small-label">暂无对话。请确保已登录且后端 WS 已连接 GetSyncMsg发送的消息含图片、音视频等也会在此展示。</p>';
} catch (e) {
$('message-list').innerHTML = '<p class="small-label">加载失败: ' + e.message + '</p>';
}
}
async function sendMessage() {
const key = (function() {
try { return localStorage.getItem(KEY_STORAGE) || ''; } catch (_) { return ''; }
})();
if (!key) { alert('请先在登录页扫码登录'); return; }
const to = $('send-to').value.trim();
const content = $('send-content').value.trim();
if (!to || !content) { alert('请填写对方用户名和内容'); return; }
try {
await callApi('/api/send-message', { method: 'POST', body: JSON.stringify({ key, to_user_name: to, content }) });
$('send-content').value = '';
loadMessages();
} catch (e) { alert('发送失败: ' + e.message); }
}
// 隐藏 key 行,仅内部使用 localStorage 中的 key
(function hideKeyRow() {
const row = document.querySelector('.key-row');
if (row) row.style.display = 'none';
})();
$('btn-refresh-msg').addEventListener('click', loadMessages);
$('btn-send-msg').addEventListener('click', sendMessage);
loadMessages();
// 定时轮询实时消息,配合回调可及时看到新消息
(function autoPollMessages() {
const INTERVAL_MS = 2000;
let timer = null;
function start() {
if (timer) return;
timer = setInterval(() => {
// 页面不可见时可跳过,减少无谓请求
if (document.hidden) return;
loadMessages();
}, INTERVAL_MS);
}
function stop() {
if (timer) {
clearInterval(timer);
timer = null;
}
}
document.addEventListener('visibilitychange', () => {
if (document.hidden) stop(); else start();
});
start();
})();
(function wsStatusCheck() {
let wasConnected = false;
const CHECK_MS = 8000;
const t = setInterval(async () => {
try {
const r = await fetch(API_BASE + '/api/ws-status', { cache: 'no-store' });
const d = await r.json().catch(() => ({}));
if (d && d.connected) wasConnected = true;
if (wasConnected && d && d.connected === false) {
clearInterval(t);
alert('连接已断开,请重新登录');
window.location.href = 'index.html';
}
} catch (_) {}
}, CHECK_MS);
})();
</script>
</body>
</html>