feat: improve web console and Windows startup
This commit is contained in:
@@ -17,7 +17,14 @@
|
||||
WxAgent.Host.exe serve --config C:\Users\USERNAME\wx-agent\service.json
|
||||
```
|
||||
|
||||
默认只监听 `127.0.0.1:5088`。外部监听必须同时设置 `AllowExternal=true`、明确 IP/端口和精确 `AllowedHosts`/`AllowedOrigins`;自行配置防火墙,服务不会自动开放端口。HTTP 不加密 Token、Cookie、消息或附件,不直接暴露公网。
|
||||
需要安装为当前用户登录后自动启动、创建桌面快捷方式并打开控制台时,使用发布目录中的脚本:
|
||||
|
||||
```powershell
|
||||
Set-ExecutionPolicy -Scope Process Bypass
|
||||
.\scripts\windows\Install-WxAgent.ps1 -InstallRoot C:\Users\USERNAME\wx-agent -Start -OpenBrowser -PreventAutoLock
|
||||
```
|
||||
|
||||
脚本不会覆盖已有 `credentials.json` 或 `service.json`;首次安装且没有凭据时生成一次本地管理 Token,仅在终端显示、不写入明文文件。`-PreventAutoLock` 会关闭当前电源计划的自动息屏/睡眠/休眠和屏保,并设置 Windows 锁屏策略;不阻止用户手动锁定。默认只监听 `127.0.0.1:5088`。外部监听必须同时设置 `AllowExternal=true`、明确 IP/端口和精确 `AllowedHosts`/`AllowedOrigins`;自行配置防火墙,服务不会自动开放端口。HTTP 不加密 Token、Cookie、消息或附件,不直接暴露公网。
|
||||
|
||||
浏览器访问 `/`,输入 Token 登录。HTTP/MCP 客户端使用:
|
||||
|
||||
|
||||
Binary file not shown.
@@ -16,5 +16,6 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\WxAgent.Windows\WxAgent.Windows.csproj" />
|
||||
<ProjectReference Include="..\WxAgent.Service\WxAgent.Service.csproj" />
|
||||
<None Include="..\..\scripts\windows\Install-WxAgent.ps1" Link="Install-WxAgent.ps1" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="Always" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,16 +1,253 @@
|
||||
const $=id=>document.getElementById(id);let csrf='';
|
||||
function showError(e){$('error').textContent=e.message||'请求失败'}
|
||||
async function api(path,init={}){const r=await fetch(path,{...init,headers:{'Accept':'application/json',...(init.headers||{})}});if(!r.ok){let x={};try{x=await r.json()}catch{}if(r.status===401){$('app').hidden=true;$('login').hidden=false}throw new Error(x.error?.message||`HTTP ${r.status}`)}return r.status===204?null:r.json()}
|
||||
async function mutate(path,body){return api(path,{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf,'Origin':location.origin},body:JSON.stringify(body)})}
|
||||
function rows(id,items,format){$(id).replaceChildren(...items.map(x=>{const d=document.createElement('div');d.className='row';d.textContent=format(x);return d}))}
|
||||
function renderBindings(accounts,targets){
|
||||
const accountById=new Map(accounts.map(x=>[x.accountId,x]));
|
||||
const accountRows=accounts.map(account=>{const d=document.createElement('div');d.className='row';d.textContent=`${account.accountId} ${account.bindingStatus|| (account.binding?'Bound':'Unbound')} ${account.binding?`进程 ${account.binding.processId} 窗口 ${account.binding.windowHandle}`:''}`;if(account.binding){const b=document.createElement('button');b.textContent='解绑';b.onclick=async()=>{try{await mutate('/api/v1/accounts/unbind',{accountId:account.accountId});await refresh()}catch(e){showError(e)}};d.append(' ',b)}return d});
|
||||
$('accounts').replaceChildren(...accountRows);
|
||||
const targetRows=targets.map(target=>{const d=document.createElement('div');d.className='row';d.textContent=`${target.targetId} ${target.wechatId||target.nickname||'[身份未读取]'}(PID ${target.processId} / HWND ${target.windowHandle})`;if(target.isBound){d.append(' 已绑定')}else{const select=document.createElement('select');select.append(new Option('选择账号',''),...accounts.filter(x=>!x.binding).map(x=>new Option(x.accountId,x.accountId)));const b=document.createElement('button');b.textContent='绑定';b.onclick=async()=>{if(!select.value)return;try{await mutate('/api/v1/accounts/bind',{accountId:select.value,targetId:target.targetId});await refresh()}catch(e){showError(e)}};d.append(' ',select,' ',b)}return d});
|
||||
$('uiTargets').replaceChildren(...targetRows);
|
||||
if(!accountById.size&&!targets.length){$('uiTargets').textContent='未发现当前交互式会话中的微信主窗口。'}
|
||||
const $ = id => document.getElementById(id);
|
||||
let csrf = '';
|
||||
let refreshTimer = 0;
|
||||
let busy = false;
|
||||
|
||||
function showError(error) {
|
||||
$('error').textContent = error?.message || '请求失败';
|
||||
}
|
||||
async function refresh(){try{const [s,c,a,t]=await Promise.all([api('/api/v1/status'),api('/api/v1/capabilities'),api('/api/v1/accounts?limit=50'),api('/api/v1/ui-targets')]);$('status').textContent=JSON.stringify(s,null,2);rows('capabilities',c,x=>`${x.operation} — ${x.enabled?'可用':'禁用'}${x.disabledReason?`:${x.disabledReason}`:''}`);const select=$('accountSelect');const old=select.value;select.replaceChildren(new Option('选择已绑定账号',''),...a.items.filter(x=>x.binding).map(x=>new Option(x.accountId,x.accountId)));select.value=a.items.some(x=>x.accountId===old&&x.binding)?old:'';renderBindings(a.items,t);if(!select.value){rows('sessions',[] ,()=> '');rows('contacts',[] ,()=> '');rows('messages',[] ,()=> '');return}const id=`&accountId=${encodeURIComponent(select.value)}`;const [se,co,m]=await Promise.all([api(`/api/v1/sessions?limit=50${id}`),api(`/api/v1/contacts?limit=50${id}`),api(`/api/v1/messages?limit=50&includeContent=${$('content').checked}${id}`)]);rows('sessions',se.items,x=>`${x.name}${x.isCurrent?'(当前)':''}`);rows('contacts',co.items,x=>`${x.displayName||'[未知]'} — ${x.id}`);rows('messages',m.items,x=>`${x.type} ${x.sender||''}: ${x.content??x.summary??'[正文未授权]'}`)}catch(e){showError(e)}}
|
||||
$('loginForm').addEventListener('submit',async e=>{e.preventDefault();try{const x=await api('/api/v1/login',{method:'POST',headers:{'Content-Type':'application/json','Origin':location.origin},body:JSON.stringify({token:$('token').value})});csrf=x.csrfToken;$('token').value='';$('login').hidden=true;$('app').hidden=false;await refresh()}catch(e){showError(e)}});
|
||||
$('refresh').onclick=refresh;$('accountSelect').onchange=refresh;$('content').onchange=refresh;$('lookup').onclick=async()=>{try{$('operation').textContent=JSON.stringify(await api('/api/v1/operations/'+encodeURIComponent($('operationId').value)),null,2)}catch(e){showError(e)}};$('logout').onclick=async()=>{try{await api('/api/v1/logout',{method:'POST',headers:{'X-CSRF-Token':csrf,'Origin':location.origin}})}finally{$('app').hidden=true;$('login').hidden=false}};
|
||||
|
||||
function clearError() {
|
||||
$('error').textContent = '';
|
||||
}
|
||||
|
||||
async function api(path, init = {}) {
|
||||
const response = await fetch(path, {
|
||||
...init,
|
||||
headers: { Accept: 'application/json', ...(init.headers || {}) }
|
||||
});
|
||||
if (!response.ok) {
|
||||
let body = {};
|
||||
try { body = await response.json(); } catch { /* response may not be JSON */ }
|
||||
if (response.status === 401) {
|
||||
$('app').hidden = true;
|
||||
$('login').hidden = false;
|
||||
$('connectionBadge').textContent = '未登录';
|
||||
$('connectionBadge').className = 'badge neutral';
|
||||
}
|
||||
throw new Error(body.error?.message || `HTTP ${response.status}`);
|
||||
}
|
||||
return response.status === 204 ? null : response.json();
|
||||
}
|
||||
|
||||
async function get(path) {
|
||||
try { return { ok: true, value: await api(path) }; }
|
||||
catch (error) { return { ok: false, error }; }
|
||||
}
|
||||
|
||||
async function mutate(path, body) {
|
||||
return api(path, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf, Origin: location.origin },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
}
|
||||
|
||||
function setList(id, items, format) {
|
||||
const target = $(id);
|
||||
target.replaceChildren(...items.map(item => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'row';
|
||||
row.textContent = format(item);
|
||||
return row;
|
||||
}));
|
||||
}
|
||||
|
||||
function setListMessage(id, message, error = false) {
|
||||
const target = $(id);
|
||||
const row = document.createElement('div');
|
||||
row.className = error ? 'row error-row' : 'row';
|
||||
row.textContent = message;
|
||||
target.replaceChildren(row);
|
||||
}
|
||||
|
||||
function value(result, fallback) {
|
||||
return result.ok ? result.value : fallback;
|
||||
}
|
||||
|
||||
function friendlyAccount(account) {
|
||||
const binding = account.binding;
|
||||
const identity = binding?.nickname || binding?.wechatId || account.displayName;
|
||||
return identity ? `${identity} · ${account.bindingStatus || 'Unknown'}` : `数据库账号 ${account.accountId.slice(0, 10)}… · ${account.bindingStatus || 'Unknown'}`;
|
||||
}
|
||||
|
||||
function renderStatus(result) {
|
||||
if (!result.ok) {
|
||||
$('connectionBadge').textContent = '状态读取失败';
|
||||
$('connectionBadge').className = 'badge error';
|
||||
return;
|
||||
}
|
||||
const status = result.value;
|
||||
$('status').textContent = JSON.stringify(status, null, 2);
|
||||
$('serviceState').textContent = status.serviceOnline ? '在线' : '离线';
|
||||
$('wechatState').textContent = status.wechatAvailable && status.sessionAvailable ? '可用' : '不可用';
|
||||
$('bindingState').textContent = status.activeAccountBound ? '已绑定' : '未绑定';
|
||||
$('modeState').textContent = status.defaultReadOnly ? '只读' : '可写';
|
||||
const healthy = status.serviceOnline && status.wechatAvailable && status.sessionAvailable;
|
||||
$('connectionBadge').textContent = healthy ? '已连接' : '需检查';
|
||||
$('connectionBadge').className = `badge ${healthy ? 'ok' : 'warn'}`;
|
||||
}
|
||||
|
||||
function renderCapabilities(result) {
|
||||
if (!result.ok) return setListMessage('capabilities', result.error.message, true);
|
||||
setList('capabilities', result.value, item => `${item.operation} — ${item.enabled ? '可用' : '禁用'}${item.disabledReason ? `:${item.disabledReason}` : ''}`);
|
||||
}
|
||||
|
||||
function renderBindings(accounts, targets) {
|
||||
const accountRows = accounts.map(account => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'row';
|
||||
const main = document.createElement('div');
|
||||
main.className = 'row-main';
|
||||
const title = document.createElement('div');
|
||||
title.className = 'row-title';
|
||||
title.textContent = friendlyAccount(account);
|
||||
const meta = document.createElement('div');
|
||||
meta.className = 'row-meta';
|
||||
meta.textContent = account.binding
|
||||
? `${account.binding.wechatId || '微信号未读取'} · PID ${account.binding.processId} · HWND ${account.binding.windowHandle}`
|
||||
: `Fingerprint: ${account.accountId}`;
|
||||
main.append(title, meta);
|
||||
row.append(main);
|
||||
if (account.binding) {
|
||||
const unbind = document.createElement('button');
|
||||
unbind.className = 'secondary';
|
||||
unbind.textContent = '解绑';
|
||||
unbind.onclick = async () => {
|
||||
unbind.disabled = true;
|
||||
try { await mutate('/api/v1/accounts/unbind', { accountId: account.accountId }); await refresh(); }
|
||||
catch (error) { showError(error); unbind.disabled = false; }
|
||||
};
|
||||
row.append(unbind);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
$('accounts').replaceChildren(...accountRows);
|
||||
if (!accountRows.length) setListMessage('accounts', '暂无数据库账号。');
|
||||
|
||||
const targetRows = targets.map(target => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'row';
|
||||
const main = document.createElement('div');
|
||||
main.className = 'row-main';
|
||||
const title = document.createElement('div');
|
||||
title.className = 'row-title';
|
||||
title.textContent = target.wechatId || target.nickname || '身份未读取';
|
||||
const meta = document.createElement('div');
|
||||
meta.className = 'row-meta';
|
||||
meta.textContent = `${target.targetId} · PID ${target.processId} · HWND ${target.windowHandle}${target.isBound ? ' · 已绑定' : ''}`;
|
||||
main.append(title, meta);
|
||||
row.append(main);
|
||||
if (!target.isBound) {
|
||||
const select = document.createElement('select');
|
||||
select.setAttribute('aria-label', `为 ${target.targetId} 选择数据库账号`);
|
||||
select.append(new Option('选择账号', ''), ...accounts.filter(account => !account.binding).map(account => new Option(friendlyAccount(account), account.accountId)));
|
||||
const bind = document.createElement('button');
|
||||
bind.textContent = '绑定';
|
||||
bind.onclick = async () => {
|
||||
if (!select.value) { showError(new Error('请先选择数据库账号')); return; }
|
||||
bind.disabled = true;
|
||||
try { await mutate('/api/v1/accounts/bind', { accountId: select.value, targetId: target.targetId }); await refresh(); }
|
||||
catch (error) { showError(error); bind.disabled = false; }
|
||||
};
|
||||
row.append(select, bind);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
$('uiTargets').replaceChildren(...targetRows);
|
||||
if (!targetRows.length) setListMessage('uiTargets', '未发现当前交互式会话中的微信主窗口。');
|
||||
}
|
||||
|
||||
function populateAccounts(result) {
|
||||
if (!result.ok) return setListMessage('accounts', result.error.message, true);
|
||||
const accounts = result.value.items || [];
|
||||
const select = $('accountSelect');
|
||||
const previous = select.value;
|
||||
select.replaceChildren(new Option('选择已绑定账号', ''), ...accounts.filter(account => account.binding).map(account => new Option(friendlyAccount(account), account.accountId)));
|
||||
select.value = accounts.some(account => account.accountId === previous && account.binding) ? previous : '';
|
||||
return accounts;
|
||||
}
|
||||
|
||||
function renderScoped(result, id, format, emptyMessage) {
|
||||
if (!result.ok) return setListMessage(id, result.error.message, true);
|
||||
const items = result.value.items || [];
|
||||
setList(id, items, format);
|
||||
if (!items.length) setListMessage(id, emptyMessage);
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
if (busy) return;
|
||||
busy = true;
|
||||
clearError();
|
||||
$('refresh').disabled = true;
|
||||
$('refresh').textContent = '刷新中…';
|
||||
try {
|
||||
const [status, capabilities, accounts, targets] = await Promise.all([
|
||||
get('/api/v1/status'), get('/api/v1/capabilities'), get('/api/v1/accounts?limit=50'), get('/api/v1/ui-targets')
|
||||
]);
|
||||
renderStatus(status);
|
||||
renderCapabilities(capabilities);
|
||||
const accountItems = populateAccounts(accounts) || [];
|
||||
renderBindings(accountItems, value(targets, []));
|
||||
|
||||
const accountId = $('accountSelect').value;
|
||||
if (!accountId) {
|
||||
const message = '选择已绑定账号后加载数据。';
|
||||
setListMessage('sessions', message); setListMessage('contacts', message); setListMessage('messages', message);
|
||||
return;
|
||||
}
|
||||
const query = `&accountId=${encodeURIComponent(accountId)}`;
|
||||
const [sessions, contacts, messages] = await Promise.all([
|
||||
get(`/api/v1/sessions?limit=50${query}`),
|
||||
get(`/api/v1/contacts?limit=50${query}`),
|
||||
get(`/api/v1/messages?limit=50&includeContent=${$('content').checked}${query}`)
|
||||
]);
|
||||
renderScoped(sessions, 'sessions', item => `${item.name}${item.isCurrent ? '(当前)' : ''}`, '暂无可见会话。');
|
||||
renderScoped(contacts, 'contacts', item => `${item.displayName || '[未知]'} — ${item.id}`, '暂无联系人数据或当前凭据未授权。');
|
||||
renderScoped(messages, 'messages', item => `${item.type} ${item.sender || ''}: ${item.content ?? item.summary ?? '[正文未授权]'}`, '暂无可见消息。');
|
||||
} finally {
|
||||
$('refresh').disabled = false;
|
||||
$('refresh').textContent = '刷新';
|
||||
$('lastUpdated').textContent = `更新于 ${new Date().toLocaleTimeString()}`;
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
function setAutoRefresh(enabled) {
|
||||
if (refreshTimer) window.clearInterval(refreshTimer);
|
||||
refreshTimer = enabled ? window.setInterval(() => { if (!document.hidden) refresh(); }, 15000) : 0;
|
||||
localStorage.setItem('wxagent-auto-refresh', enabled ? '1' : '0');
|
||||
}
|
||||
|
||||
$('loginForm').addEventListener('submit', async event => {
|
||||
event.preventDefault();
|
||||
const button = $('loginButton');
|
||||
button.disabled = true;
|
||||
button.textContent = '登录中…';
|
||||
clearError();
|
||||
try {
|
||||
const response = await api('/api/v1/login', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: location.origin }, body: JSON.stringify({ token: $('token').value }) });
|
||||
csrf = response.csrfToken;
|
||||
$('token').value = '';
|
||||
$('login').hidden = true;
|
||||
$('app').hidden = false;
|
||||
await refresh();
|
||||
} catch (error) { showError(error); }
|
||||
finally { button.disabled = false; button.textContent = '登录'; }
|
||||
});
|
||||
|
||||
$('refresh').onclick = refresh;
|
||||
$('accountSelect').onchange = refresh;
|
||||
$('content').onchange = refresh;
|
||||
$('autoRefresh').checked = localStorage.getItem('wxagent-auto-refresh') === '1';
|
||||
$('autoRefresh').onchange = event => setAutoRefresh(event.target.checked);
|
||||
if ($('autoRefresh').checked) setAutoRefresh(true);
|
||||
$('lookup').onclick = async () => {
|
||||
if (!$('operationId').value.trim()) { showError(new Error('请输入 Operation ID')); return; }
|
||||
try { $('operation').textContent = JSON.stringify(await api(`/api/v1/operations/${encodeURIComponent($('operationId').value.trim())}`), null, 2); }
|
||||
catch (error) { showError(error); }
|
||||
};
|
||||
$('logout').onclick = async () => {
|
||||
try { await api('/api/v1/logout', { method: 'POST', headers: { 'X-CSRF-Token': csrf, Origin: location.origin } }); }
|
||||
finally { if (refreshTimer) window.clearInterval(refreshTimer); $('app').hidden = true; $('login').hidden = false; $('connectionBadge').textContent = '未连接'; $('connectionBadge').className = 'badge neutral'; }
|
||||
};
|
||||
|
||||
@@ -1,6 +1,57 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>WxAgent</title><link rel="stylesheet" href="styles.css"></head>
|
||||
<body><main><h1>WxAgent 控制台</h1><p class="warning">HTTP 明文不会保护 Token、消息或附件;仅在可信隔离网络使用。</p>
|
||||
<section id="login"><h2>登录</h2><form id="loginForm"><label>Token <input id="token" type="password" autocomplete="off" required></label><button>登录</button></form></section>
|
||||
<section id="app" hidden><div class="toolbar"><button id="refresh">刷新状态</button><button id="logout">退出</button><label>当前账号 <select id="accountSelect"><option value="">选择账号</option></select></label></div><pre id="status" role="status"></pre>
|
||||
<h2>能力</h2><div id="capabilities"></div><h2>账号</h2><div id="accounts"></div><h2>微信窗口 / 显式绑定</h2><div id="uiTargets"></div><h2>会话</h2><div id="sessions"></div><h2>联系人/群</h2><div id="contacts"></div><h2>任务</h2><label>Operation ID <input id="operationId" autocomplete="off"><button id="lookup">查询</button></label><pre id="operation"></pre><h2>可见消息</h2><label><input id="content" type="checkbox"> 显示正文(需要 content 权限)</label><div id="messages"></div><p id="error" class="error" role="alert"></p></section></main><script type="module" src="app.js"></script></body></html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>WxAgent 控制台</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<header class="hero">
|
||||
<div><p class="eyebrow">DESKTOP AGENT</p><h1>WxAgent 控制台</h1><p class="subtitle">安全查看微信状态、账号绑定和只读数据。</p></div>
|
||||
<span id="connectionBadge" class="badge neutral">未连接</span>
|
||||
</header>
|
||||
<p class="warning">HTTP 明文不会保护 Token、消息或附件;仅在可信隔离网络使用。</p>
|
||||
|
||||
<section id="login" class="panel login-panel">
|
||||
<h2>登录控制台</h2>
|
||||
<p class="muted">Token 只在当前浏览器会话使用,不会保存到页面或 URL。</p>
|
||||
<form id="loginForm" class="login-form">
|
||||
<label for="token">访问 Token</label>
|
||||
<div class="input-row"><input id="token" type="password" autocomplete="off" required placeholder="粘贴 Token"><button id="loginButton">登录</button></div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section id="app" class="app-shell" hidden>
|
||||
<div class="toolbar panel">
|
||||
<div class="toolbar-group"><button id="refresh">刷新</button><label class="toggle"><input id="autoRefresh" type="checkbox"> 自动刷新 <span class="muted">15 秒</span></label><span id="lastUpdated" class="muted">尚未刷新</span></div>
|
||||
<div class="toolbar-group account-picker"><label for="accountSelect">当前账号</label><select id="accountSelect"><option value="">选择已绑定账号</option></select></div>
|
||||
<button id="logout" class="secondary">退出</button>
|
||||
</div>
|
||||
|
||||
<div class="status-grid" aria-live="polite">
|
||||
<div class="metric panel"><span>服务</span><strong id="serviceState">—</strong></div>
|
||||
<div class="metric panel"><span>微信 UI</span><strong id="wechatState">—</strong></div>
|
||||
<div class="metric panel"><span>当前绑定</span><strong id="bindingState">—</strong></div>
|
||||
<div class="metric panel"><span>默认模式</span><strong id="modeState">只读</strong></div>
|
||||
</div>
|
||||
|
||||
<p id="error" class="error" role="alert" aria-live="assertive"></p>
|
||||
<details class="panel details"><summary>诊断详情</summary><pre id="status" role="status"></pre></details>
|
||||
<section class="panel"><div class="section-heading"><h2>能力</h2><span class="muted">按当前凭据显示</span></div><div id="capabilities" class="list"></div></section>
|
||||
|
||||
<div class="dashboard-grid">
|
||||
<section class="panel"><div class="section-heading"><h2>账号绑定</h2><span class="muted">数据库账号 ↔ 微信窗口</span></div><div id="accounts" class="list"></div></section>
|
||||
<section class="panel"><div class="section-heading"><h2>微信窗口</h2><span class="muted">显式选择后绑定</span></div><div id="uiTargets" class="list"></div></section>
|
||||
</div>
|
||||
|
||||
<section class="panel"><div class="section-heading"><h2>会话</h2><span class="muted">需要选择已绑定账号</span></div><div id="sessions" class="list"></div></section>
|
||||
<section class="panel"><div class="section-heading"><h2>联系人 / 群</h2><span class="muted">只读数据库数据</span></div><div id="contacts" class="list"></div></section>
|
||||
<section class="panel"><div class="section-heading"><h2>可见消息</h2><label class="toggle"><input id="content" type="checkbox"> 显示正文 <span class="muted">需要 content 权限</span></label></div><div id="messages" class="list"></div></section>
|
||||
<section class="panel"><div class="section-heading"><h2>任务查询</h2><span class="muted">粘贴 Operation ID</span></div><div class="input-row"><input id="operationId" autocomplete="off" placeholder="Operation ID"><button id="lookup">查询</button></div><pre id="operation"></pre></section>
|
||||
</section>
|
||||
</main>
|
||||
<script type="module" src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1 +1 @@
|
||||
*{box-sizing:border-box}body{margin:0;background:#f6f7f9;color:#18202a;font:16px system-ui,sans-serif}main{max-width:1100px;margin:auto;padding:24px}section{background:white;border:1px solid #d9dee7;border-radius:8px;padding:18px;margin:16px 0}label{display:block;margin:10px 0}input{max-width:100%;padding:9px;border:1px solid #9aa6b2;border-radius:4px}button{padding:9px 14px;margin:5px;border:0;border-radius:4px;background:#1459c7;color:white;cursor:pointer}button:focus,input:focus{outline:3px solid #ffbf47;outline-offset:2px}.warning{padding:12px;background:#fff3cd;border:1px solid #e0b400}.error{color:#a00;min-height:1.4em}pre{white-space:pre-wrap;overflow:auto;background:#111827;color:#e5e7eb;padding:12px;border-radius:4px}.row{padding:8px;border-bottom:1px solid #eee}.disabled{color:#777}h1{margin-top:0}@media(max-width:600px){main{padding:12px}}
|
||||
:root{color-scheme:light;--bg:#f4f7fb;--panel:#fff;--ink:#182230;--muted:#64748b;--line:#dbe3ee;--blue:#155eef;--blue-dark:#0d47b5;--danger:#b42318;--success:#067647;--warning:#8a5b00}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif}main{max-width:1240px;margin:auto;padding:28px 20px 60px}.hero{display:flex;align-items:flex-start;justify-content:space-between;gap:20px;margin-bottom:18px}.eyebrow{margin:0;color:var(--blue);font-size:12px;font-weight:800;letter-spacing:.12em}.hero h1{margin:2px 0 4px;font-size:30px;letter-spacing:-.02em}.subtitle{margin:0;color:var(--muted)}.badge{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border-radius:999px;font-size:13px;font-weight:700;white-space:nowrap}.badge:before{content:"";width:7px;height:7px;border-radius:50%;background:currentColor}.badge.ok{color:var(--success);background:#ecfdf3}.badge.warn{color:var(--warning);background:#fffaeb}.badge.error{color:var(--danger);background:#fef3f2}.badge.neutral{color:var(--muted);background:#eef2f6}.warning{margin:0 0 18px;padding:11px 14px;color:var(--warning);background:#fffaeb;border:1px solid #f5d78e;border-radius:10px}.panel{background:var(--panel);border:1px solid var(--line);border-radius:14px;box-shadow:0 3px 12px #172b4d0b}.login-panel{max-width:620px;padding:24px}.app-shell{display:grid;gap:16px}.toolbar{display:flex;align-items:center;justify-content:space-between;gap:14px;padding:12px 14px;position:sticky;top:10px;z-index:2}.toolbar-group{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.account-picker{min-width:310px}.account-picker label{margin:0;white-space:nowrap;font-weight:700}.input-row{display:flex;align-items:center;gap:10px}.input-row input{flex:1;min-width:0}.login-form label{display:block;margin:0 0 6px;font-weight:700}input,select{width:100%;min-height:40px;padding:9px 11px;border:1px solid #a8b5c5;border-radius:8px;background:#fff;color:var(--ink);font:inherit}button{min-height:40px;padding:8px 15px;border:1px solid var(--blue);border-radius:8px;background:var(--blue);color:#fff;font:inherit;font-weight:700;cursor:pointer;transition:background .15s,transform .15s}button:hover{background:var(--blue-dark)}button:active{transform:translateY(1px)}button:disabled{opacity:.55;cursor:wait}.secondary{border-color:var(--line);background:#fff;color:var(--ink)}.secondary:hover{background:#f8fafc}.toggle{display:inline-flex;align-items:center;gap:7px;margin:0;color:var(--ink);white-space:nowrap}.toggle input{width:16px;min-height:16px;margin:0}.muted{color:var(--muted);font-size:13px}.status-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.metric{padding:16px 18px}.metric span{display:block;color:var(--muted);font-size:13px}.metric strong{display:block;margin-top:3px;font-size:21px}.dashboard-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}.panel:not(.toolbar){padding:18px}.section-heading{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:10px}.section-heading h2{margin:0;font-size:18px}.list{min-height:40px}.row{display:flex;align-items:center;gap:8px;padding:10px 0;border-bottom:1px solid #edf1f5;overflow-wrap:anywhere}.row:last-child{border-bottom:0}.row-main{flex:1;min-width:0}.row-title{font-weight:700}.row-meta{color:var(--muted);font-size:12px}.row button{min-height:34px;padding:5px 10px;font-size:13px}.row select{width:auto;min-width:170px;min-height:34px;padding:5px 8px;font-size:13px}.error{min-height:0;margin:0;color:var(--danger);font-weight:600}.error:not(:empty){padding:10px 12px;background:#fef3f2;border:1px solid #fecdca;border-radius:8px}.error-row{color:var(--danger);background:#fff8f7;border-radius:7px;padding:10px}.details summary{cursor:pointer;font-weight:700;color:#334155}.details pre{margin:12px 0 0}pre{white-space:pre-wrap;overflow:auto;background:#111827;color:#e5e7eb;padding:12px;border-radius:8px;font:12px/1.5 ui-monospace,SFMono-Regular,Consolas,monospace}#operation:empty{display:none}@media(max-width:850px){main{padding:18px 12px 40px}.toolbar{position:static;align-items:flex-start;flex-direction:column}.account-picker{width:100%;min-width:0}.status-grid{grid-template-columns:repeat(2,1fr)}.dashboard-grid{grid-template-columns:1fr}}@media(max-width:520px){.hero{display:block}.hero .badge{margin-top:12px}.status-grid{grid-template-columns:1fr 1fr}.input-row{align-items:stretch}.input-row button{flex:none}.section-heading{align-items:flex-start;flex-direction:column}.row{align-items:flex-start;flex-wrap:wrap}.row select{flex:1;min-width:150px}}
|
||||
|
||||
Reference in New Issue
Block a user