Skip to content

Instantly share code, notes, and snippets.

@Axoloteera
Created July 22, 2025 09:03
Show Gist options
  • Save Axoloteera/afb005aea6be2572cd5161448f6f67ce to your computer and use it in GitHub Desktop.
Save Axoloteera/afb005aea6be2572cd5161448f6f67ce to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name CCW Comment Annotator + Export
// @namespace https://ccw.site/
// @version 0.2
// @description 在评论旁标注“正常/坏”,并支持一键导出 JSONL
// @author ChatGPT
// @match https://*.ccw.site/*
// @grant none
// ==/UserScript==
(() => {
'use strict';
/* ---------- 配置 ---------- */
const LABELS = [
{ key: 'normal', text: '✅ 正常' },
{ key: 'juvenile', text: '🚸 坏' }
];
const STORAGE_KEY = 'ccw-comment-labels-v1';
const store = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
/* ---------- 辅助函数 ---------- */
function saveStore() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(store));
}
function createButtons(cid, text, time) {
const box = document.createElement('span');
box.style.marginLeft = '8px';
box.style.fontSize = '12px';
LABELS.forEach(l => {
const btn = document.createElement('button');
btn.textContent = l.text;
btn.style.margin = '0 2px';
btn.style.padding = '1px 4px';
btn.style.border = '1px solid #666';
btn.style.borderRadius = '4px';
const picked = store[cid]?.label === l.key;
if (picked) {
btn.style.background = '#2d8cff';
btn.style.color = '#fff';
}
btn.addEventListener('click', () => {
store[cid] = { label: l.key, text, time };
saveStore();
[...box.querySelectorAll('button')].forEach(b => {
b.style.background = '';
b.style.color = '';
});
btn.style.background = '#2d8cff';
btn.style.color = '#fff';
});
box.appendChild(btn);
});
return box;
}
function annotate(root) {
root.querySelectorAll('.c-comment-item').forEach(item => {
if (item.dataset.annotated) return;
const content = item.querySelector('.c-comment-content');
if (!content) return;
const timeText = item.querySelector('.c-reply-control-time')
?.textContent.trim() || 'unknown';
const rawText = content.textContent.trim().replace(/\s+/g, ' ');
const cid = `${timeText}|${rawText.slice(0,30)}`;
content.appendChild(createButtons(cid, rawText, timeText));
item.dataset.annotated = '1';
});
}
/* ---------- 导出功能 ---------- */
function exportLabels() {
const out = Object.entries(store)
.map(([cid, v]) => JSON.stringify({ id: cid, ...v }))
.join('\n');
const blob = new Blob([out], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download =
`ccw_labels_${new Date().toISOString().slice(0,10)}.jsonl`;
a.click();
URL.revokeObjectURL(url);
}
function injectExportBtn() {
const btn = document.createElement('button');
btn.textContent = '📤 导出标注';
Object.assign(btn.style, {
position: 'fixed', right: '20px', bottom: '20px',
padding: '6px 10px', fontSize: '14px',
border: '1px solid #666', borderRadius: '6px',
background: '#fff', cursor: 'pointer', zIndex: 9999
});
btn.onclick = exportLabels;
document.body.appendChild(btn);
window.addEventListener('keydown', e => {
if (e.altKey && e.code === 'KeyE') { e.preventDefault(); exportLabels(); }
});
}
/* ---------- 启动 ---------- */
annotate(document);
injectExportBtn();
new MutationObserver(muts => {
muts.forEach(m => m.addedNodes.forEach(node => {
if (node.nodeType === 1) annotate(node);
}));
}).observe(document.body, { childList: true, subtree: true });
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment