电脑端,使用tampermonkey插件,试试看把这个脚本拷贝进去,如果是你有修改权限的帖子会删除附件,如果你没修改权限的话会报404,至少我实验是有效的
// ==UserScript==
// @name SSTM 附件页删除按钮(区分主题/回复)
// @namespace https://example.com/
// @version 0.2.0
// @author sstm脚本组
// @description 在 https://sstm.moe/attachments/ 页面为每个附件块插入删除按钮,并根据来源链接自动判断编辑主题还是编辑回复
// @match https://sstm.moe/attachments/*
// @match https://sstm.moe/attachments/
// @grant none
// ==/UserScript==
(function () {
'use strict';
const CONFIG = {
itemSelector: 'div.ipsDataItem.ipsAttach',
attachmentLinkSelector: 'a[href*="/applications/core/interface/file/attachment.php?id="]',
sourceLinkSelector: '.ipsDataItem_generic.ipsDataItem_size9 a[href*="/topic/"]',
buttonClass: 'sstm-delete-attachment-btn',
confirmBeforeDelete: true,
debug: true
};
function log(...args) {
if (CONFIG.debug) {
console.log('[SSTM Attachment Delete]', ...args);
}
}
function absoluteUrl(url) {
return new URL(url, location.origin).toString();
}
function getCsrfKey() {
const fromMeta = document.querySelector('meta[name="csrfKey"]')?.content;
if (fromMeta) return fromMeta;
const fromWindow1 = window.ips?.getSetting?.('csrfKey');
if (fromWindow1) return fromWindow1;
const fromWindow2 = window.ipsSettings?.csrfKey;
if (fromWindow2) return fromWindow2;
const m = document.cookie.match(/(?:^|;\s*)csrfKey=([^;]+)/);
if (m) return decodeURIComponent(m[1]);
return null;
}
function extractAttachmentId(item) {
const link = item.querySelector(CONFIG.attachmentLinkSelector);
if (!link) return null;
try {
const url = new URL(link.href, location.origin);
return url.searchParams.get('id');
} catch (e) {
return null;
}
}
function extractFileName(item) {
const el = item.querySelector('.ipsAttach_title a, .ipsDataItem_title a');
return el ? el.textContent.trim() : '未知附件';
}
function extractSourceInfo(item) {
const sourceLink = item.querySelector(CONFIG.sourceLinkSelector);
if (!sourceLink) return null;
const href = absoluteUrl(sourceLink.href);
const url = new URL(href);
const commentId = url.searchParams.get('comment');
const doValue = url.searchParams.get('do');
const isReply = doValue === 'findComment' && !!commentId;
if (isReply) {
url.searchParams.delete('do');
url.searchParams.delete('comment');
return {
type: 'reply',
sourceUrl: href,
baseTopicUrl: url.toString(),
commentId
};
}
url.searchParams.delete('do');
url.searchParams.delete('comment');
return {
type: 'topic',
sourceUrl: href,
baseTopicUrl: url.toString(),
commentId: null
};
}
function buildEditUrl(sourceInfo, csrfKey) {
const url = new URL(sourceInfo.baseTopicUrl, location.origin);
if (sourceInfo.type === 'reply') {
url.searchParams.set('do', 'editComment');
url.searchParams.set('comment', sourceInfo.commentId);
} else {
url.searchParams.set('do', 'edit');
}
if (csrfKey) {
url.searchParams.set('csrfKey', csrfKey);
}
return url.toString();
}
async function fetchText(url, options = {}) {
const resp = await fetch(url, {
credentials: 'include',
cache: 'no-cache',
...options
});
if (!resp.ok) {
throw new Error(`请求失败 ${resp.status} ${resp.statusText}`);
}
return await resp.text();
}
function parseHTML(html) {
return new DOMParser().parseFromString(html, 'text/html');
}
function extractPostKeyFromEditHtml(html) {
const doc = new DOMParser().parseFromString(html, 'text/html');
// 1. 优先从 IPS 编辑器容器读取
const editorEl = doc.querySelector('[data-ipseditor-postkey]');
if (editorEl) {
const postKey = editorEl.getAttribute('data-ipseditor-postkey');
if (postKey) return postKey;
}
// 2. 兜底:兼容可能存在的 input
const input = doc.querySelector('input[name="postKey"]');
if (input?.value) return input.value;
// 3. 再兜底:正则扫原始 HTML
let m = html.match(/data-ipseditor-postkey=["']([^"']+)["']/i);
if (m) return m[1];
m = html.match(/name=["']postKey["'][^>]*value=["']([^"']+)["']/i);
if (m) return m[1];
m = html.match(/["']postKey["']\s*:\s*["']([^"']+)["']/i);
if (m) return m[1];
return null;
}
async function deleteAttachment(editUrl, attachmentId, postKey, csrfKey) {
const url = new URL(editUrl, location.origin);
url.searchParams.set('postKey', postKey);
url.searchParams.set('deleteFile', attachmentId);
if (csrfKey) {
url.searchParams.set('csrfKey', csrfKey);
}
log('删除请求 URL:', url.toString());
const resp = await fetch(url.toString(), {
method: 'GET',
credentials: 'include',
mode: 'cors',
headers: {
'accept': '*/*',
'cache-control': 'no-cache',
'pragma': 'no-cache',
'x-requested-with': 'XMLHttpRequest'
},
referrer: editUrl
});
if (!resp.ok) {
throw new Error(`删除请求失败 ${resp.status} ${resp.statusText}`);
}
return await resp.text();
}
function setBtnState(btn, state, text) {
btn.disabled = state === 'loading';
btn.textContent = text;
if (state === 'loading') {
btn.style.opacity = '0.7';
btn.style.cursor = 'wait';
} else {
btn.style.opacity = '1';
btn.style.cursor = 'pointer';
}
}
function removeItem(item) {
item.style.transition = 'opacity .25s ease, transform .25s ease';
item.style.opacity = '0';
item.style.transform = 'scale(0.98)';
setTimeout(() => item.remove(), 260);
}
async function handleDelete(item, btn) {
const attachmentId = extractAttachmentId(item);
const fileName = extractFileName(item);
const sourceInfo = extractSourceInfo(item);
const csrfKey = getCsrfKey();
if (!attachmentId) {
alert('未找到附件 ID');
return;
}
if (!sourceInfo) {
alert('未找到该附件对应的主题/回复链接');
return;
}
if (!csrfKey) {
alert('未找到 csrfKey');
return;
}
const targetText = sourceInfo.type === 'reply' ? `回复 #${sourceInfo.commentId}` : '主题首楼';
if (CONFIG.confirmBeforeDelete) {
const ok = confirm(
`确定删除附件吗?\n\n文件名:${fileName}\n附件ID:${attachmentId}\n挂载位置:${targetText}`
);
if (!ok) return;
}
try {
setBtnState(btn, 'loading', '读取编辑页...');
const editUrl = buildEditUrl(sourceInfo, csrfKey);
log('编辑页 URL:', editUrl);
log('来源类型:', sourceInfo.type, sourceInfo);
const editHtml = await fetchText(editUrl, {
headers: {
'accept': 'text/html,application/xhtml+xml'
}
});
const postKey = extractPostKeyFromEditHtml(editHtml);
if (!postKey) {
throw new Error('未能从编辑页中解析出 postKey');
}
setBtnState(btn, 'loading', '提交删除...');
const result = await deleteAttachment(editUrl, attachmentId, postKey, csrfKey);
log('删除响应:', result);
setBtnState(btn, 'success', '已删除');
removeItem(item);
} catch (err) {
console.error(err);
setBtnState(btn, 'error', '删除失败');
alert(`删除失败:${err.message}`);
setTimeout(() => {
setBtnState(btn, 'idle', '删除附件');
}, 1200);
}
}
function injectButton(item) {
if (item.querySelector(`.${CONFIG.buttonClass}`)) return;
const attachmentId = extractAttachmentId(item);
const sourceInfo = extractSourceInfo(item);
if (!attachmentId || !sourceInfo) return;
const btn = document.createElement('button');
btn.type = 'button';
btn.className = CONFIG.buttonClass;
btn.textContent = '删除附件';
btn.style.marginTop = '8px';
btn.style.padding = '6px 10px';
btn.style.border = '1px solid #cc3333';
btn.style.borderRadius = '4px';
btn.style.background = '#fff5f5';
btn.style.color = '#b30000';
btn.style.fontSize = '12px';
btn.style.lineHeight = '1.2';
btn.style.cursor = 'pointer';
btn.addEventListener('mouseenter', () => {
if (!btn.disabled) btn.style.background = '#ffeaea';
});
btn.addEventListener('mouseleave', () => {
if (!btn.disabled) btn.style.background = '#fff5f5';
});
btn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
handleDelete(item, btn);
});
const target = item.querySelector('.ipsDataItem_main') || item;
target.appendChild(btn);
}
function processAll() {
document.querySelectorAll(CONFIG.itemSelector).forEach(injectButton);
}
function observe() {
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (!(node instanceof Element)) continue;
if (node.matches?.(CONFIG.itemSelector)) {
injectButton(node);
}
node.querySelectorAll?.(CONFIG.itemSelector).forEach(injectButton);
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
processAll();
observe();
})();
然后之后如果想上传图片/视频的话,可以试试看这个https://sstm.moe/topic/378868-【油猴脚本】便捷文件上传自定义表情包/?do=findComment&comment=18887760