feat: add initial skill authoring workspace

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
木炎
2026-04-02 18:34:56 +08:00
parent a461b0734e
commit 51913555ad
30 changed files with 7114 additions and 0 deletions

View File

@@ -0,0 +1,179 @@
function cleanText(value) {
return String(value || '')
.replace(/\s+/g, ' ')
.replace(/\u200b/g, '')
.trim();
}
function pageText() {
const body = document.body;
return cleanText(body && (body.innerText || body.textContent || ''));
}
function isLoginBlocked(url, text) {
return /\/signin\b|\/signup\b/.test(url) ||
/登录|注册|验证码|安全验证|验证后继续|请先登录/.test(text);
}
function isVisible(node) {
if (!node) {
return false;
}
const rect = typeof node.getBoundingClientRect === 'function' ? node.getBoundingClientRect() : null;
return !rect || rect.width > 0 || rect.height > 0;
}
function attrText(node, name) {
if (!node || typeof node.getAttribute !== 'function') {
return '';
}
return cleanText(node.getAttribute(name) || '');
}
function looksLikeTitleInput(node) {
const signals = [
attrText(node, 'placeholder'),
attrText(node, 'data-placeholder'),
attrText(node, 'aria-label'),
].filter(Boolean);
return signals.some((value) => value.includes('标题'));
}
function dispatchTextInput(node) {
node.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
node.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
}
function fillInput(node, value) {
node.focus();
if ('value' in node) {
node.value = '';
dispatchTextInput(node);
node.value = value;
dispatchTextInput(node);
return;
}
node.textContent = value;
dispatchTextInput(node);
}
function fillEditable(node, value) {
node.focus();
node.innerHTML = '';
const lines = String(value || '').split(/\n/);
lines.forEach((line, index) => {
if (index > 0) {
node.appendChild(document.createElement('br'));
}
node.appendChild(document.createTextNode(line));
});
dispatchTextInput(node);
}
function findVisible(selectors) {
for (const selector of selectors) {
const nodes = Array.from(document.querySelectorAll(selector));
const match = nodes.find((node) => {
return isVisible(node);
});
if (match) {
return match;
}
}
return null;
}
function findBodyEditor(titleInput) {
for (const selector of [
"div[contenteditable='true'][role='textbox']",
"div.public-DraftEditor-content[contenteditable='true']",
"[role='textbox'][contenteditable='true']",
"[contenteditable='true'][data-placeholder]",
"div[contenteditable='true']",
]) {
const nodes = Array.from(document.querySelectorAll(selector));
const match = nodes.find((node) => isVisible(node) && node !== titleInput && !looksLikeTitleInput(node));
if (match) {
return match;
}
}
return null;
}
function findButtonByText(fragment) {
const candidates = Array.from(document.querySelectorAll("button, [role='button'], a"));
const wanted = cleanText(fragment);
return candidates.find((node) => {
const text = cleanText(node.textContent);
if (!text || !text.includes(wanted)) {
return false;
}
return isVisible(node);
}) || null;
}
const currentUrl = location.href;
const text = pageText();
if (isLoginBlocked(currentUrl, text)) {
return {
status: 'login_required',
current_url: currentUrl,
};
}
const titleInput = findVisible([
"textarea[placeholder*='标题']",
"input[placeholder*='标题']",
"textarea[data-placeholder*='标题']",
"input[data-placeholder*='标题']",
"[role='textbox'][aria-label*='标题']",
"[contenteditable='true'][aria-label*='标题']",
"[contenteditable='true'][data-placeholder*='标题']",
]);
const bodyEditor = findBodyEditor(titleInput);
if (!titleInput || !bodyEditor) {
return {
status: 'editor_not_ready',
current_url: currentUrl,
};
}
fillInput(titleInput, String(args.title || ''));
fillEditable(bodyEditor, String(args.body || ''));
const publishMode = String(args.publish_mode || '').toLowerCase() === 'true';
if (!publishMode) {
return {
status: 'draft_ready',
current_url: currentUrl,
title: cleanText(args.title),
};
}
const publishButton = findButtonByText('发布');
if (!publishButton) {
return {
status: 'publish_button_missing',
current_url: currentUrl,
title: cleanText(args.title),
};
}
publishButton.click();
const confirmButton = findButtonByText('确认发布');
if (!confirmButton) {
return {
status: 'publish_clicked',
current_url: currentUrl,
title: cleanText(args.title),
};
}
confirmButton.click();
return {
status: 'publish_submitted',
current_url: currentUrl,
title: cleanText(args.title),
};