参考两位大神的插件,让claude做了些修改: f14XuanLv/Claude-Powerest-Manager_Enhancer: An all-in-one Tampermonkey script to supercharge your Claude.ai experience with a powerful conversation manager, advanced export, and unique chat enhancements. Claude Floating Control Panel
1、在原本悬浮插件的基础上,增加了收起状态下也能显示用量和手动刷新
2、对面板UI做了汉化
3、参考大神整体插件,提取了中文重命名逻辑,增加成独立按钮
// ==UserScript==
// @name Claude Floating Control Panel
// @name:en Claude Floating Control Panel
// @namespace usage-and-quick-settings-of-claude
// @author Yalums, f14xuanlv
// @version 2.2
// @description 浮动面板显示Claude的Usage和功能开关,支持智能中文重命名
// @description:en Floating panel to display Claude usage and feature toggles with conversation rename
// @match https://claude.ai/*
// @grant none
// @run-at document-idle
// @license GNU General Public License v3.0
// ==/UserScript==
(function() {
'use strict';
const FEATURES = [
{ key: 'enabled_monkeys_in_a_barrel', name: '代码执行', desc: '虚拟代码环境(支持压缩包)', exclusive: 'enabled_artifacts_attachments' },
{ key: 'enabled_artifacts_attachments', name: 'Analysis tool', desc: 'Artifacts的附加功能(不建议)', exclusive: 'enabled_monkeys_in_a_barrel' },
{ key: 'enabled_saffron', name: '记忆功能', desc: '跨窗口记忆' },
{ key: 'enabled_saffron_search', name: '搜索对话', desc: '对话搜索功能' },
{ key: 'enabled_sourdough', name: '项目功能', desc: '项目记忆' },
];
let panelState = {
isExpanded: localStorage.getItem('claudePanel_expanded') !== 'false',
position: JSON.parse(localStorage.getItem('claudePanel_position') || '{"right":"20px","bottom":"20px"}'),
autoRename: localStorage.getItem('claudePanel_autoRename') === 'true'
};
async function getUserSettings() {
try {
const response = await fetch('/api/account', {
credentials: 'include'
});
const data = await response.json();
return data.settings;
} catch (err) {
console.error('[Claude Panel] Failed to fetch settings:', err);
return null;
}
}
async function toggleFeature(key, currentValue, exclusiveKey = null) {
try {
const body = { [key]: !currentValue };
if (exclusiveKey && !currentValue) {
body[exclusiveKey] = false;
}
const response = await fetch('/api/account/settings', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify(body)
});
if (!response.ok) {
console.error('[Claude Panel] HTTP error:', response.status);
return { success: false };
}
const result = await response.json();
return { success: true, data: result };
} catch (err) {
console.error('[Claude Panel] Toggle failed:', err);
return { success: false };
}
}
async function getUsageData() {
try {
const orgsResponse = await fetch('/api/organizations', {
credentials: 'include'
});
const orgs = await orgsResponse.json();
const orgId = orgs[0]?.uuid;
if (!orgId) return null;
const usageResponse = await fetch(`/api/organizations/${orgId}/usage`, {
credentials: 'include'
});
return await usageResponse.json();
} catch (err) {
console.error('[Claude Panel] Failed to fetch usage:', err);
return null;
}
}
// ========== 重命名功能相关函数 ==========
async function getOrgId() {
try {
const r = await fetch('/api/organizations');
const orgs = await r.json();
return orgs[0]?.uuid || null;
} catch (err) {
console.error('[Claude Panel] 获取组织ID失败:', err);
return null;
}
}
function getCurrentConversationUuid() {
const match = window.location.pathname.match(/\/chat\/([a-f0-9-]+)/);
return match ? match[1] : null;
}
async function getConversation(orgId, uuid) {
try {
const r = await fetch(`/api/organizations/${orgId}/chat_conversations/${uuid}`);
return await r.json();
} catch (err) {
console.error('[Claude Panel] 获取对话失败:', err);
return null;
}
}
async function createTempConversation(orgId) {
try {
const r = await fetch(`/api/organizations/${orgId}/chat_conversations`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ uuid: crypto.randomUUID(), name: '' })
});
return await r.json();
} catch (err) {
console.error('[Claude Panel] 创建临时对话失败:', err);
return null;
}
}
async function generateTitle(orgId, tempUuid, messageContent) {
try {
const r = await fetch(`/api/organizations/${orgId}/chat_conversations/${tempUuid}/title`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message_content: messageContent, recent_titles: [] })
});
if (!r.ok) {
console.error('[Claude Panel] 生成标题失败, HTTP状态:', r.status);
return null;
}
const data = await r.json();
const title = data?.title?.trim();
if (!title || title.toLowerCase().includes('untitled')) {
console.error('[Claude Panel] 生成的标题无效:', title);
return null;
}
return title;
} catch (err) {
console.error('[Claude Panel] 生成标题失败:', err);
return null;
}
}
async function updateConversationName(orgId, uuid, newName) {
try {
const r = await fetch(`/api/organizations/${orgId}/chat_conversations/${uuid}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: newName })
});
return r.ok;
} catch (err) {
console.error('[Claude Panel] 更新对话名称失败:', err);
return false;
}
}
async function deleteTempConversation(orgId, uuid) {
try {
await fetch(`/api/organizations/${orgId}/chat_conversations/${uuid}`, {
method: 'DELETE'
});
} catch (err) {
console.error('[Claude Panel] 删除临时对话失败:', err);
}
}
function chineseRatio(s) {
const onlyText = s.replace(/\s+/g, '');
if (!onlyText) return 0;
const cn = (onlyText.match(/[\u4E00-\u9FFF]/g) || []).length;
return cn / onlyText.length;
}
function isAsciiOrUntitled(name) {
if (!name || name.trim() === '') return true;
const trimmed = name.trim();
if (/^New Chat( \d+)?$/i.test(trimmed)) return true;
return /^[\x00-\x7F]+$/.test(trimmed);
}
// 检测标题是否为英文(包含大量英文字母)
function isEnglishTitle(name) {
if (!name || name.trim() === '') return true;
const trimmed = name.trim();
// 如果是默认标题,视为英文
if (/^New Chat( \d+)?$/i.test(trimmed)) return true;
// 如果包含中文字符,不是英文标题
if (/[\u4E00-\u9FFF]/.test(trimmed)) return false;
// 统计英文字母比例
const letters = trimmed.match(/[a-zA-Z]/g) || [];
const totalChars = trimmed.replace(/\s+/g, '').length;
// 如果英文字母占比超过50%,视为英文标题
return totalChars > 0 && (letters.length / totalChars) > 0.5;
}
function hasBeenRenamed(uuid) {
const key = `claude_renamed_${uuid}`;
return localStorage.getItem(key) === 'true';
}
function markAsRenamed(uuid) {
const key = `claude_renamed_${uuid}`;
localStorage.setItem(key, 'true');
}
async function renameCurrentConversation() {
const uuid = getCurrentConversationUuid();
if (!uuid) {
console.log('[Claude Panel] 未找到当前对话UUID');
return { success: false, message: '未找到当前对话' };
}
if (hasBeenRenamed(uuid)) {
console.log('[Claude Panel] 对话已重命名过,跳过');
return { success: false, message: '该对话已重命名过' };
}
const orgId = await getOrgId();
if (!orgId) {
return { success: false, message: '获取组织ID失败' };
}
const conv = await getConversation(orgId, uuid);
if (!conv) {
return { success: false, message: '获取对话信息失败' };
}
const currentName = conv.name || '';
// 如果标题不是英文(已经是中文或其他语言),跳过重命名
if (!isEnglishTitle(currentName)) {
console.log('[Claude Panel] 对话名称已非英文,跳过');
markAsRenamed(uuid);
return { success: false, message: '对话名称已非英文' };
}
const messages = conv.chat_messages || [];
if (messages.length < 2) {
console.log('[Claude Panel] 对话消息不足2条,跳过');
return { success: false, message: '对话消息不足,无法生成标题' };
}
// 检查前2轮对话的中文比例
let totalText = '';
for (let i = 0; i < Math.min(4, messages.length); i++) {
const msg = messages[i];
if (msg.text) totalText += msg.text;
}
if (chineseRatio(totalText) < 0.25) {
console.log('[Claude Panel] 中文比例不足25%,跳过');
return { success: false, message: '对话中文内容不足' };
}
// 构建用于生成标题的消息内容(前2轮对话)
const maxRounds = 2;
const roundsToUse = Math.min(Math.floor(messages.length / 2), maxRounds);
const messagesToProcess = messages.slice(0, roundsToUse * 2);
let messageParts = [];
messagesToProcess.forEach((msg, index) => {
const senderLabel = 'Message ' + (index + 1) + ' (' + (msg.sender === 'human' ? 'User' : 'Assistant') + ')';
let textContent = '';
// 提取文本内容
if (Array.isArray(msg.content)) {
textContent = msg.content
.filter(b => b.type === 'text' && b.text)
.map(b => b.text)
.join('\\n');
}
if (!textContent && msg.text) {
textContent = msg.text;
}
if (textContent.trim()) {
messageParts.push(senderLabel + ':\\n\\n' + textContent.trim());
}
});
if (messageParts.length === 0) {
return { success: false, message: '没有有效的文本内容' };
}
// 组装完整的消息内容,添加中文标题生成指令
const langPrompt = '中文';
const startInstruction = 'TASK: Generate a title for the following conversation.\\nRULE: The title language must be strictly ' + langPrompt + '.\\n\\n--- Conversation Start ---';
const endInstruction = '\\n--- Conversation End ---\\nREMINDER: Generate the title in ' + langPrompt + ' now.';
const finalMessageContent = startInstruction + '\\n\\n' + messageParts.join('\\n\\n') + '\\n' + endInstruction;
// 创建临时对话用于生成标题
const tempConv = await createTempConversation(orgId);
if (!tempConv || !tempConv.uuid) {
return { success: false, message: '创建临时对话失败' };
}
const tempUuid = tempConv.uuid;
const newTitle = await generateTitle(orgId, tempUuid, finalMessageContent);
// 清理临时对话
await deleteTempConversation(orgId, tempUuid);
if (!newTitle) {
return { success: false, message: '生成标题失败' };
}
const success = await updateConversationName(orgId, uuid, newTitle);
if (success) {
markAsRenamed(uuid);
console.log('[Claude Panel] 重命名成功:', newTitle);
// 延迟刷新页面,让用户看到成功提示
setTimeout(() => {
console.log('[Claude Panel] 正在刷新页面以更新标题...');
window.location.reload();
}, 1000);
return { success: true, message: `已重命名为: ${newTitle},即将刷新页面...` };
} else {
return { success: false, message: '更新对话名称失败' };
}
}
function formatResetTime(isoTime) {
if (!isoTime) return '未知';
const date = new Date(isoTime);
const now = new Date();
const diff = date - now;
const minutes = Math.floor(diff / 60000);
const hours = Math.floor(diff / 3600000);
const days = Math.floor(diff / 86400000);
if (minutes < 1) return '即将重置';
if (minutes < 60) return `${minutes}分钟后`;
if (hours < 24) return `${hours}小时后`;
return `${days}天后`;
}
function injectStyles() {
const style = document.createElement('style');
style.textContent = `
#claude-control-panel {
position: fixed;
z-index: 9999;
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
border: 1px solid #404040;
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
color: #ffffff;
max-width: 400px;
max-height: 90vh;
overflow: hidden;
transition: all 0.3s ease;
will-change: transform;
}
#claude-control-panel.dragging {
transition: none;
}
#claude-control-panel.collapsed {
width: 200px !important;
height: 44px !important;
opacity: 0.7;
}
#claude-control-panel.collapsed:hover {
opacity: 1;
}
.collapsed-usage {
display: none;
}
.collapsed .collapsed-usage {
display: flex;
align-items: center;
flex: 1;
min-width: 0;
}
.collapsed-usage-value {
font-size: 14px;
font-weight: 700;
color: #4ade80;
line-height: 1;
white-space: nowrap;
min-width: 35px;
}
.collapsed-usage-value.warning {
color: #fbbf24;
}
.collapsed-usage-value.danger {
color: #f87171;
}
.collapsed-refresh {
display: none;
}
.collapsed .collapsed-refresh {
display: flex;
align-items: center;
}
.collapsed-refresh-btn {
background: rgba(255, 255, 255, 0.1);
border: none;
border-radius: 6px;
width: 28px;
height: 28px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
font-size: 13px;
padding: 0;
flex-shrink: 0;
}
.collapsed-refresh-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
.collapsed-refresh-btn:active {
transform: scale(0.95);
}
.collapsed-refresh-btn.refreshing {
animation: pulse 0.8s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.15);
opacity: 0.7;
}
}
.header-rename-btn {
background: rgba(255, 255, 255, 0.1);
border: none;
border-radius: 6px;
padding: 4px 10px;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
transition: all 0.2s;
font-size: 12px;
color: #e0e0e0;
white-space: nowrap;
}
.header-rename-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
.header-rename-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.panel-header {
padding: 16px;
background: rgba(255, 255, 255, 0.05);
border-bottom: 1px solid #404040;
display: flex;
align-items: center;
justify-content: space-between;
cursor: move;
user-select: none;
}
.collapsed .panel-header {
padding: 8px 12px;
border-bottom: none;
justify-content: space-between;
gap: 10px;
}
.panel-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
.panel-controls {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.collapsed .panel-controls {
gap: 4px;
}
.panel-btn {
background: rgba(255, 255, 255, 0.1);
border: none;
border-radius: 6px;
width: 28px;
height: 28px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
flex-shrink: 0;
}
.panel-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
.panel-content {
max-height: calc(90vh - 60px);
overflow-y: auto;
padding: 16px;
}
.collapsed .panel-content {
display: none;
}
.collapsed .panel-header h3 {
display: none;
}
.collapsed .header-rename-btn {
font-size: 11px;
padding: 4px 8px;
}
.collapsed .header-rename-btn {
padding: 4px 8px;
font-size: 11px;
}
.usage-section {
background: rgba(255, 255, 255, 0.05);
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
}
.usage-item {
margin-bottom: 16px;
}
.usage-item:last-child {
margin-bottom: 0;
}
.usage-label {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 13px;
}
.usage-label-text {
font-weight: 500;
color: #e0e0e0;
}
.usage-label-time {
color: #909090;
font-size: 12px;
}
.usage-bar {
height: 8px;
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
overflow: hidden;
margin-bottom: 4px;
}
.usage-bar-fill {
height: 100%;
background: linear-gradient(90deg, #4ade80 0%, #22c55e 100%);
transition: width 0.3s ease;
}
.usage-bar-fill.warning {
background: linear-gradient(90deg, #fbbf24 0%, #f59e0b 100%);
}
.usage-bar-fill.danger {
background: linear-gradient(90deg, #f87171 0%, #ef4444 100%);
}
.usage-percent {
text-align: right;
font-size: 12px;
color: #b0b0b0;
}
.features-section {
background: rgba(255, 255, 255, 0.05);
border-radius: 12px;
padding: 16px;
}
.section-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 12px;
color: #e0e0e0;
}
.feature-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
gap: 24px;
}
.feature-item:last-child {
border-bottom: none;
}
.feature-info {
flex: 1;
min-width: 0;
}
.feature-name {
font-size: 13px;
font-weight: 500;
color: #e0e0e0;
margin-bottom: 4px;
}
.feature-desc {
font-size: 11px;
color: #909090;
line-height: 1.3;
}
.feature-toggle {
padding: 6px 12px;
border-radius: 6px;
border: none;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
min-width: 50px;
flex-shrink: 0;
}
.feature-toggle.on {
background: #22c55e;
color: white;
}
.feature-toggle.off {
background: rgba(255, 255, 255, 0.1);
color: #909090;
}
.feature-toggle:hover:not(:disabled) {
transform: scale(1.05);
}
.feature-toggle:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.refresh-btn {
width: 100%;
padding: 10px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
color: #e0e0e0;
font-size: 13px;
cursor: pointer;
margin-top: 16px;
transition: all 0.2s;
}
.refresh-btn:hover {
background: rgba(255, 255, 255, 0.15);
}
.loading {
text-align: center;
padding: 20px;
color: #909090;
font-size: 13px;
}
.panel-content::-webkit-scrollbar {
width: 6px;
}
.panel-content::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
}
.panel-content::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
}
.panel-content::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
`;
document.head.appendChild(style);
}
function createPanel() {
const panel = document.createElement('div');
panel.id = 'claude-control-panel';
panel.style.right = panelState.position.right || '20px';
panel.style.bottom = panelState.position.bottom || '20px';
if (!panelState.isExpanded) {
panel.classList.add('collapsed');
}
panel.innerHTML = `
<div class="panel-header">
<h3>
<span>⚡</span>
<span>Claude 控制面板</span>
</h3>
<div class="collapsed-usage" id="collapsed-usage"></div>
<div class="panel-controls">
<button class="header-rename-btn" id="header-rename-btn" title="重命名当前对话">
✏️ 重命名
</button>
<div class="collapsed-refresh">
<button class="collapsed-refresh-btn" id="collapsed-refresh-btn" title="刷新数据">🔄</button>
</div>
<button class="panel-btn" id="toggle-panel" title="收起/展开">
${panelState.isExpanded ? '−' : '+'}
</button>
</div>
</div>
<div class="panel-content">
<div class="loading">加载中...</div>
</div>
`;
document.body.appendChild(panel);
makeDraggable(panel);
panel.querySelector('#toggle-panel').addEventListener('click', () => {
panelState.isExpanded = !panelState.isExpanded;
localStorage.setItem('claudePanel_expanded', panelState.isExpanded);
panel.classList.toggle('collapsed');
panel.querySelector('#toggle-panel').textContent = panelState.isExpanded ? '−' : '+';
});
// 收起状态的刷新按钮
panel.querySelector('#collapsed-refresh-btn').addEventListener('click', async (e) => {
const btn = e.target;
if (btn.classList.contains('refreshing')) return;
btn.classList.add('refreshing');
await updatePanelContent(panel);
setTimeout(() => {
btn.classList.remove('refreshing');
}, 1000);
});
// 标题栏重命名按钮
panel.querySelector('#header-rename-btn').addEventListener('click', async (e) => {
const btn = e.target;
if (btn.disabled) return;
btn.disabled = true;
btn.innerHTML = '⏳ 重命名中...';
const result = await renameCurrentConversation();
if (result.success) {
btn.innerHTML = '✅ 成功';
btn.style.background = 'rgba(34, 197, 94, 0.3)';
} else {
btn.innerHTML = '❌ ' + result.message;
btn.style.background = 'rgba(239, 68, 68, 0.3)';
}
setTimeout(() => {
btn.disabled = false;
btn.innerHTML = '✏️ 重命名';
btn.style.background = '';
}, 3000);
});
return panel;
}
async function updatePanelContent(panel) {
const content = panel.querySelector('.panel-content');
const [usageData, settings] = await Promise.all([
getUsageData(),
getUserSettings()
]);
if (!usageData && !settings) {
content.innerHTML = '<div class="loading">❌ 加载失败</div>';
return;
}
// 更新收起状态的用量显示
const collapsedUsage = panel.querySelector('#collapsed-usage');
if (usageData && usageData.five_hour) {
const percent = usageData.five_hour.utilization || 0;
const colorClass = percent > 80 ? 'danger' : percent > 60 ? 'warning' : '';
collapsedUsage.innerHTML = `<span class="collapsed-usage-value ${colorClass}">${percent}%</span>`;
}
let html = '';
if (usageData) {
html += '<div class="usage-section"><div class="section-title">📊 使用情况</div>';
if (usageData.five_hour) {
const percent = usageData.five_hour.utilization || 0;
const barClass = percent > 80 ? 'danger' : percent > 60 ? 'warning' : '';
html += `
<div class="usage-item">
<div class="usage-label">
<span class="usage-label-text">当前会话</span>
<span class="usage-label-time">${formatResetTime(usageData.five_hour.resets_at)}</span>
</div>
<div class="usage-bar">
<div class="usage-bar-fill ${barClass}" style="width: ${percent}%"></div>
</div>
<div class="usage-percent">已使用 ${percent}%</div>
</div>
`;
}
if (usageData.seven_day) {
const percent = usageData.seven_day.utilization || 0;
const barClass = percent > 80 ? 'danger' : percent > 60 ? 'warning' : '';
html += `
<div class="usage-item">
<div class="usage-label">
<span class="usage-label-text">每周限额(所有模型)</span>
<span class="usage-label-time">${formatResetTime(usageData.seven_day.resets_at)}</span>
</div>
<div class="usage-bar">
<div class="usage-bar-fill ${barClass}" style="width: ${percent}%"></div>
</div>
<div class="usage-percent">已使用 ${percent}%</div>
</div>
`;
}
if (usageData.seven_day_opus) {
const percent = usageData.seven_day_opus.utilization || 0;
const barClass = percent > 80 ? 'danger' : percent > 60 ? 'warning' : '';
html += `
<div class="usage-item">
<div class="usage-label">
<span class="usage-label-text">每周限额(仅Opus)</span>
<span class="usage-label-time">${formatResetTime(usageData.seven_day_opus.resets_at)}</span>
</div>
<div class="usage-bar">
<div class="usage-bar-fill ${barClass}" style="width: ${percent}%"></div>
</div>
<div class="usage-percent">已使用 ${percent}%</div>
</div>
`;
}
html += '</div>';
}
if (settings) {
html += '<div class="features-section"><div class="section-title">🔧 功能开关</div>';
FEATURES.forEach(feature => {
const isEnabled = settings[feature.key] === true;
html += `
<div class="feature-item">
<div class="feature-info">
<div class="feature-name">${feature.name}</div>
<div class="feature-desc">${feature.desc}</div>
</div>
<button class="feature-toggle ${isEnabled ? 'on' : 'off'}"
data-key="${feature.key}"
data-value="${isEnabled}"
data-exclusive="${feature.exclusive || ''}">
${isEnabled ? '✓ 开启' : '关闭'}
</button>
</div>
`;
});
// 添加自动重命名开关
html += `
<div class="feature-item">
<div class="feature-info">
<div class="feature-name">自动重命名</div>
<div class="feature-desc">打开对话时自动重命名为中文</div>
</div>
<button class="feature-toggle ${panelState.autoRename ? 'on' : 'off'}"
id="auto-rename-toggle">
${panelState.autoRename ? '✓ 开启' : '关闭'}
</button>
</div>
`;
html += '</div>';
}
html += '<button class="refresh-btn" id="refresh-panel">🔄 刷新数据</button>';
content.innerHTML = html;
content.querySelectorAll('.feature-toggle').forEach(btn => {
btn.addEventListener('click', async (e) => {
const button = e.target;
// 处理自动重命名开关
if (button.id === 'auto-rename-toggle') {
panelState.autoRename = !panelState.autoRename;
localStorage.setItem('claudePanel_autoRename', panelState.autoRename);
button.className = `feature-toggle ${panelState.autoRename ? 'on' : 'off'}`;
button.textContent = panelState.autoRename ? '✓ 开启' : '关闭';
return;
}
const key = button.dataset.key;
const currentValue = button.dataset.value === 'true';
const exclusiveKey = button.dataset.exclusive || null;
button.disabled = true;
button.textContent = '...';
const result = await toggleFeature(key, currentValue, exclusiveKey);
setTimeout(() => updatePanelContent(panel), 300);
});
});
content.querySelector('#refresh-panel')?.addEventListener('click', () => {
content.querySelector('.loading')?.remove();
const loading = document.createElement('div');
loading.className = 'loading';
loading.textContent = '刷新中...';
content.insertBefore(loading, content.firstChild);
setTimeout(() => {
loading.remove();
updatePanelContent(panel);
}, 300);
});
}
function makeDraggable(element) {
const header = element.querySelector('.panel-header');
let isDragging = false;
let currentX, currentY, initialX, initialY;
let rafId = null;
header.addEventListener('mousedown', (e) => {
if (e.target.closest('.panel-btn')) return;
isDragging = true;
element.classList.add('dragging');
const rect = element.getBoundingClientRect();
initialX = e.clientX - rect.left;
initialY = e.clientY - rect.top;
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
e.preventDefault();
if (rafId) {
cancelAnimationFrame(rafId);
}
rafId = requestAnimationFrame(() => {
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
element.style.left = currentX + 'px';
element.style.top = currentY + 'px';
element.style.right = 'auto';
element.style.bottom = 'auto';
});
});
document.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
element.classList.remove('dragging');
if (rafId) {
cancelAnimationFrame(rafId);
rafId = null;
}
panelState.position = {
left: element.style.left,
top: element.style.top
};
localStorage.setItem('claudePanel_position', JSON.stringify(panelState.position));
}
});
}
// 监听URL变化,自动重命名
let lastUrl = location.href;
let renameTimeout = null;
function checkUrlChange() {
const currentUrl = location.href;
if (currentUrl !== lastUrl) {
lastUrl = currentUrl;
// 检查是否进入了对话页面
const match = currentUrl.match(/\/chat\/([a-f0-9-]+)/);
if (match && panelState.autoRename) {
// 延迟2秒后执行重命名,确保页面加载完成
if (renameTimeout) clearTimeout(renameTimeout);
renameTimeout = setTimeout(async () => {
console.log('[Claude Panel] 检测到新对话,准备自动重命名...');
const result = await renameCurrentConversation();
if (result.success) {
console.log('[Claude Panel] 自动重命名成功:', result.message);
} else {
console.log('[Claude Panel] 自动重命名跳过:', result.message);
}
}, 2000);
}
}
}
function init() {
injectStyles();
const panel = createPanel();
updatePanelContent(panel);
// 自动刷新:每30秒更新一次
setInterval(() => updatePanelContent(panel), 30000);
// 监听URL变化
setInterval(checkUrlChange, 500);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

