有时会想在浏览器中使用.claude下配置的skill,但是来回copy比较复杂。
写了一个油猴脚本,用于将在网页端配置、使用skill。
最终效果是:
-
在网页端聊天框中按下
Tab键,会打开一个新的输入框- 如在ds中
按下tab打开:
- 如在ds中
-
在新的输入框输入"/"显示已经配置的skill,选择并使用
-
在模态框中按下回车,替换斜杠命令,并发送内容
油猴脚本
// ==UserScript==
// @name AI Input Commander (Modal Version)
// @namespace http://tampermonkey.net/
// @version 1.2.0
// @description 通过模态框高效调用并解析 Skill 快捷指令,完美支持上下键、Tab 键与多行输入
// @match https://gemini.google.com/*
// @match https://chatgpt.com/*
// @match https://www.kimi.com/*
// @match https://chat.deepseek.com/*
// @match https://chat.z.ai/*
// @match https://www.doubao.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// ==/UserScript==
(function() {
'use strict';
// --- 1. 数据初始化与清洗 ---
let rawSkills = GM_getValue("skills", [
{ name: "summary", description: "精简总结内容", content: "请精简总结以下内容:" },
{ name: "code", description: "代码架构审查", content: "请作为架构师审查这段代码的健壮性:" }
]);
let skills = sanitizeSkills(rawSkills);
function sanitizeSkills(arr) {
if (!Array.isArray(arr)) return [];
return arr.map(s => {
if (s.name) {
s.name = s.name.replace(/^\/+/, '').trim();
}
return s;
}).filter(s => s.name);
}
// --- 2. 状态变量 ---
let lastActiveElement = null;
let selectedIndex = -1;
let filteredSkills = [];
// 注入通用全局样式
const css = `
.cmd-item:hover, .cmd-item.active { background-color: #f0fdf4 !important; }
.cmd-btn { cursor: pointer; padding: 6px 14px; border: 1px solid #d1d5db; border-radius: 6px; background: #fff; font-size: 13px; transition: all 0.2s; user-select: none; }
.cmd-btn:hover { background: #f3f4f6; border-color: #9ca3af; }
.cmd-btn-primary { background: #2563eb !important; color: #fff !important; border-color: #2563eb !important; }
.cmd-btn-primary:hover { background: #1d4ed8 !important; }
.cmd-btn-danger { color: #dc2626 !important; border-color: #fca5a5 !important; }
.cmd-btn-danger:hover { background: #fef2f2 !important; }
.cmd-input { width: 100%; padding: 8px 12px; border: 1px solid #c8cbd0; border-radius: 6px; outline: none; font-size: 14px; box-sizing: border-box; }
.cmd-input:focus { border-color: #2563eb; box-shadow: 0 0 0 2px rgba(37,99,235,0.2); }
`;
const styleSheet = document.createElement("style");
styleSheet.innerText = css;
document.head.appendChild(styleSheet);
// --- 3. 功能:创建并显示【配置管理】独立模态框 ---
function showConfigModal() {
if (document.getElementById('cmd-config-overlay')) return;
const configOverlay = document.createElement('div');
configOverlay.id = 'cmd-config-overlay';
Object.assign(configOverlay.style, {
position: 'fixed', top: '0', left: '0', width: '100vw', height: '100vh',
backgroundColor: 'rgba(0,0,0,0.5)', zIndex: '100005', fontFamily: 'Segoe UI, system-ui, sans-serif',
overflow: "auto"
});
const configModal = document.createElement('div');
Object.assign(configModal.style, {
position: 'absolute', top: '8%', left: '50%', transform: 'translateX(-50%)',
width: '750px', padding: '24px', background: '#ffffff', borderRadius: '12px',
boxShadow: '0 25px 50px -12px rgba(0,0,0,0.25)', display: 'flex', flexDirection: 'column', gap: '16px', boxSizing: 'border-box'
});
// 3.1 头部栏
const header = document.createElement('div');
Object.assign(header.style, { display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: '1px solid #e5e7eb', paddingBottom: '12px' });
const title = document.createElement('div');
title.textContent = '📊 Skill 配置管理中心';
Object.assign(title.style, { fontWeight: 'bold', fontSize: '16px', color: '#1f2937' });
const btnCloseX = document.createElement('button');
btnCloseX.className = 'cmd-btn';
btnCloseX.textContent = '✕';
Object.assign(btnCloseX.style, { border: 'none', fontSize: '16px', padding: '0 4px' });
header.appendChild(title);
header.appendChild(btnCloseX);
// 3.2 快捷添加区域【🌟此处已完美修复样式语法错误】
const addSection = document.createElement('div');
Object.assign(addSection.style, {
display: 'flex',
flexDirection: 'column',
gap: '10px',
background: '#f9fafb',
padding: '14px',
borderRadius: '8px',
border: '1px solid #e5e7eb'
});
const addTitle = document.createElement('div');
addTitle.textContent = '➕ 快捷单条追加';
Object.assign(addTitle.style, { fontSize: '13px', fontWeight: '600', color: '#4b5563' });
const row1 = document.createElement('div');
Object.assign(row1.style, { display: 'flex', gap: '10px' });
const inputName = document.createElement('input');
inputName.type = 'text'; inputName.className = 'cmd-input'; inputName.placeholder = '命令名称 (如 summary)';
const inputDesc = document.createElement('input');
inputDesc.type = 'text'; inputDesc.className = 'cmd-input'; inputDesc.placeholder = '简短描述 (如 总结长文本)';
row1.appendChild(inputName); row1.appendChild(inputDesc);
const row2 = document.createElement('div');
const textContent = document.createElement('textarea');
textContent.className = 'cmd-input'; textContent.rows = 4; textContent.placeholder = '在此输入完整的 Skill 长 Prompt 内容...';
Object.assign(textContent.style, { resize: 'vertical', fontFamily: 'inherit' });
row2.appendChild(textContent);
const row3 = document.createElement('div');
Object.assign(row3.style, { display: 'flex', justifyContent: 'flex-end' });
const btnQuickAdd = document.createElement('button');
btnQuickAdd.className = 'cmd-btn cmd-btn-primary'; btnQuickAdd.textContent = '确认添加';
Object.assign(btnQuickAdd.style, { minWidth: '100px' });
row3.appendChild(btnQuickAdd);
addSection.appendChild(addTitle); addSection.appendChild(row1); addSection.appendChild(row2); addSection.appendChild(row3);
// 3.3 JSON 区域
const jsonSection = document.createElement('div');
Object.assign(jsonSection.style, { display: 'flex', flexDirection: 'column', gap: '6px' });
const jsonTitle = document.createElement('div');
jsonTitle.textContent = '📝 JSON 数据流';
Object.assign(jsonTitle.style, { fontSize: '13px', fontWeight: '600', color: '#4b5563' });
const jsonTextarea = document.createElement('textarea');
jsonTextarea.className = 'cmd-input'; jsonTextarea.rows = 6; jsonTextarea.placeholder = '在此粘贴 Skill 数组 JSON...';
Object.assign(jsonTextarea.style, { fontFamily: 'monospace', fontSize: '13px', lineHeight: '1.4', resize: 'vertical' });
jsonSection.appendChild(jsonTitle); jsonSection.appendChild(jsonTextarea);
// 3.4 底部操作栏
const footer = document.createElement('div');
Object.assign(footer.style, { display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderTop: '1px solid #e5e7eb', paddingTop: '14px' });
const tipsSpan = document.createElement('span');
Object.assign(tipsSpan.style, { fontSize: '13px', color: '#4b5563', fontWeight: '500' });
const btnGroup = document.createElement('div');
Object.assign(btnGroup.style, { display: 'flex', gap: '8px' });
const btnClear = document.createElement('button'); btnClear.className = 'cmd-btn cmd-btn-danger'; btnClear.textContent = '清空全部';
const btnMerge = document.createElement('button'); btnMerge.className = 'cmd-btn'; btnMerge.textContent = '📥 追加合并';
const btnCover = document.createElement('button'); btnCover.className = 'cmd-btn cmd-btn-primary'; btnCover.textContent = '💾 覆盖保存';
btnGroup.appendChild(btnClear); btnGroup.appendChild(btnMerge); btnGroup.appendChild(btnCover);
footer.appendChild(tipsSpan); footer.appendChild(btnGroup);
// 组装面板
configModal.appendChild(header); configModal.appendChild(addSection); configModal.appendChild(jsonSection); configModal.appendChild(footer);
configOverlay.appendChild(configModal); document.body.appendChild(configOverlay);
// 初始化填入数据
jsonTextarea.value = JSON.stringify(skills, null, 2);
tipsSpan.textContent = `当前共有 ${skills.length} 个 Skill`;
function syncAndSave() {
skills = sanitizeSkills(skills);
GM_setValue("skills", skills);
jsonTextarea.value = JSON.stringify(skills, null, 2);
tipsSpan.textContent = `当前共有 ${skills.length} 个 Skill`;
}
btnQuickAdd.onclick = () => {
let n = inputName.value.trim(); const d = inputDesc.value.trim(); const c = textContent.value.trim();
if (!n || !c) { alert("命令和内容不能为空!"); return; }
n = n.replace(/^\/+/, '').trim();
skills = skills.filter(s => s.name.toLowerCase() !== n.toLowerCase());
skills.push({ name: n, description: d, content: c });
syncAndSave();
inputName.value = ''; inputDesc.value = ''; textContent.value = '';
};
btnCover.onclick = () => {
try {
const parsed = JSON.parse(jsonTextarea.value);
if (Array.isArray(parsed)) { skills = sanitizeSkills(parsed); syncAndSave(); alert("💾 覆盖保存成功!"); } else { alert("格式错误:必须是一个数组 []"); }
} catch(err) { alert("JSON 语法解析错误,请检查格式!"); }
};
btnMerge.onclick = () => {
try {
const parsed = JSON.parse(jsonTextarea.value);
if (Array.isArray(parsed)) {
const cleanedNewSkills = sanitizeSkills(parsed);
cleanedNewSkills.forEach(newSkill => {
skills = skills.filter(s => s.name.toLowerCase() !== newSkill.name.toLowerCase());
skills.push(newSkill);
});
syncAndSave(); alert(`📥 合并成功!`);
} else { alert("格式错误:必须是一个数组 []"); }
} catch(err) { alert("JSON 语法解析错误,请检查格式!"); }
};
btnClear.onclick = () => { if (confirm("确定要清空本地所有快捷命令吗?")) { skills = []; syncAndSave(); } };
btnCloseX.onclick = () => document.body.removeChild(configOverlay);
configOverlay.onclick = (e) => { if(e.target === configOverlay) document.body.removeChild(configOverlay); };
}
// --- 4. 功能:创建并显示【主命令输入框】模态框 ---
function showCommander() {
if (document.getElementById('cmd-overlay')) return;
lastActiveElement = document.activeElement;
selectedIndex = -1;
filteredSkills = [];
const overlay = document.createElement('div');
overlay.id = 'cmd-overlay';
Object.assign(overlay.style, {
position: 'fixed', top: '0', left: '0', width: '100vw', height: '100vh',
backgroundColor: 'rgba(0,0,0,0.4)', zIndex: '100000', fontFamily: 'Segoe UI, system-ui, sans-serif'
});
const modal = document.createElement('div');
Object.assign(modal.style, {
position: 'absolute', top: '15%', left: '50%', transform: 'translateX(-50%)',
width: '650px', padding: '20px', background: '#ffffff', borderRadius: '12px',
boxShadow: '0 20px 25px -5px rgba(0,0,0,0.1), 0 10px 10px -5px rgba(0,0,0,0.04)',
display: 'flex', flexDirection: 'column', gap: '12px', boxSizing: 'border-box'
});
const textarea = document.createElement('textarea');
textarea.placeholder = "输入 / 唤出指令,上下键/Tab/鼠标选择补全,无高亮时 Enter 解析发送...";
textarea.rows = 4;
Object.assign(textarea.style, {
width: '100%', padding: '12px', fontSize: '15px', border: '1px solid #c8cbd0',
borderRadius: '6px', resize: 'vertical', outline: 'none', boxSizing: 'border-box',
lineHeight: '1.5', color: '#1f2937'
});
const listContainer = document.createElement('div');
Object.assign(listContainer.style, {
maxHeight: '180px', overflowY: 'auto', border: '1px solid #e5e7eb',
borderRadius: '6px', display: 'none', backgroundColor: '#ffffff'
});
const actionBar = document.createElement('div');
Object.assign(actionBar.style, { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '4px' });
const versionSpan = document.createElement('span');
versionSpan.textContent = 'AI Commander v2.1';
Object.assign(versionSpan.style, { fontSize: '12px', color: '#9ca3af' });
const mainBtnGroup = document.createElement('div');
Object.assign(mainBtnGroup.style, { display: 'flex', gap: '8px' });
const btnConfig = document.createElement('button'); btnConfig.className = 'cmd-btn'; btnConfig.textContent = '⚙️ 配置管理';
const btnClose = document.createElement('button'); btnClose.className = 'cmd-btn'; btnClose.textContent = '关闭 (Esc)';
mainBtnGroup.appendChild(btnConfig); mainBtnGroup.appendChild(btnClose);
actionBar.appendChild(versionSpan);
actionBar.appendChild(mainBtnGroup);
function renderList() {
const text = textarea.value.trim();
if (text.startsWith('/')) {
const hasSpaceAfterCmd = text.substring(1).includes(' ');
if (hasSpaceAfterCmd) {
filteredSkills = [];
} else {
const searchKey = text.replace(/^\/+/, '').toLowerCase();
filteredSkills = skills.filter(s => s.name.toLowerCase().startsWith(searchKey));
}
} else {
filteredSkills = [];
}
while (listContainer.firstChild) {
listContainer.removeChild(listContainer.firstChild);
}
if (filteredSkills.length === 0) {
listContainer.style.display = 'none';
selectedIndex = -1;
return;
}
listContainer.style.display = 'block';
filteredSkills.forEach((s, i) => {
const itemDiv = document.createElement('div');
itemDiv.id = `cmd-item-${i}`;
itemDiv.setAttribute('data-index', i);
itemDiv.className = `cmd-item ${i === selectedIndex ? 'active' : ''}`;
Object.assign(itemDiv.style, {
padding: '10px 14px', cursor: 'pointer', display: 'flex',
justifyContent: 'space-between', alignItems: 'center',
borderBottom: '1px solid #f3f4f6', background: '#fff'
});
const nameSpan = document.createElement('span');
nameSpan.textContent = `/${s.name}`;
Object.assign(nameSpan.style, { color: '#2563eb', fontWeight: '600', fontSize: '14px', display: 'inline', flexShrink: 0 });
const descSpan = document.createElement('span');
descSpan.textContent = s.description || '';
descSpan.title = s.description || '';
Object.assign(descSpan.style, { color: '#6b7280', fontSize: '13px', fontWeight: 'normal', display: 'inline', marginLeft: '20px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' });
itemDiv.appendChild(nameSpan); itemDiv.appendChild(descSpan);
listContainer.appendChild(itemDiv);
});
if (selectedIndex >= 0) {
const activeItem = document.getElementById(`cmd-item-${selectedIndex}`);
if (activeItem) activeItem.scrollIntoView({ block: 'nearest' });
}
}
textarea.oninput = () => { selectedIndex = -1; renderList(); };
textarea.onkeydown = (e) => {
if (e.key === 'ArrowDown' && filteredSkills.length > 0) {
e.preventDefault(); selectedIndex = (selectedIndex + 1) % filteredSkills.length; renderList(); return;
}
if (e.key === 'ArrowUp' && filteredSkills.length > 0) {
e.preventDefault(); selectedIndex = (selectedIndex - 1 + filteredSkills.length) % filteredSkills.length; renderList(); return;
}
if (e.key === 'Tab') {
e.preventDefault();
if (filteredSkills.length > 0) {
if (selectedIndex === -1) selectedIndex = 0;
textarea.value = "/" + filteredSkills[selectedIndex].name + " "; renderList();
}
return;
}
if (e.key === 'Enter') {
if (e.shiftKey) return;
e.preventDefault();
if (selectedIndex >= 0 && selectedIndex < filteredSkills.length) {
textarea.value = "/" + filteredSkills[selectedIndex].name + " "; selectedIndex = -1; renderList();
} else {
const finalContent = parseContent(textarea.value); closeAndFill(finalContent, true);
}
}
if (e.key === 'Escape') { e.preventDefault(); closeAndFill(textarea.value, false); }
};
listContainer.onclick = (e) => {
const item = e.target.closest('.cmd-item');
if (item) {
const idx = parseInt(item.getAttribute('data-index'));
textarea.value = "/" + filteredSkills[idx].name + " "; textarea.focus(); selectedIndex = -1; renderList();
}
};
btnClose.onclick = () => closeAndFill(textarea.value, false);
btnConfig.onclick = () => showConfigModal();
modal.appendChild(textarea); modal.appendChild(listContainer); modal.appendChild(actionBar);
overlay.appendChild(modal); document.body.appendChild(overlay);
textarea.focus();
function closeAndFill(content, shouldSend) {
document.body.removeChild(overlay); fillOriginalInput(content, shouldSend);
}
}
function parseContent(text) {
const targetText = text.trim();
if (!targetText.startsWith('/')) return text;
const matchCmd = targetText.match(/^\/+([^\s]+)/);
if (matchCmd) {
const parsedName = matchCmd[1].toLowerCase();
const foundSkill = skills.find(s => s.name.toLowerCase() === parsedName);
if (foundSkill) {
const userContent = targetText.substring(matchCmd[0].length).trim();
return (
`<skill name="${foundSkill.name}">${foundSkill.content}</skill>\n` +
`<command-name>/${foundSkill.name}</command-name>\n<command-args>${userContent}</command-args>`
);
}
}
return text;
}
function fillOriginalInput(content, shouldSend) {
if (!lastActiveElement) return;
lastActiveElement.focus();
document.execCommand('selectAll', false, null);
document.execCommand('insertText', false, content);
const inputEvent = new Event('input', { bubbles: true, cancelable: true });
const changeEvent = new Event('change', { bubbles: true, cancelable: true });
lastActiveElement.dispatchEvent(inputEvent);
lastActiveElement.dispatchEvent(changeEvent);
if (shouldSend) {
setTimeout(() => {
const selectors = [
'button[aria-label*="Send"]', 'button[aria-label*="发送"]',
'[data-testid="send-button"]', 'button[data-testid="fruitjuice-send-button"]',
'button[class*="send"]', 'button:has(svg path[d^="M2"])'
];
let sendBtn = null;
for (let selector of selectors) {
sendBtn = document.querySelector(selector);
if (sendBtn && !sendBtn.disabled) break;
}
if (sendBtn) {
sendBtn.click();
} else {
const enterEvent = new KeyboardEvent('keydown', {
key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true
});
lastActiveElement.dispatchEvent(enterEvent);
}
}, 150);
}
}
document.addEventListener('keydown', (e) => {
if (e.key === 'Tab' && !document.getElementById('cmd-overlay')) {
const active = document.activeElement;
if (active && (active.tagName === 'TEXTAREA' || active.isContentEditable)) {
e.preventDefault(); showCommander();
}
}
}, true);
})();
- 目前测试ok的网页:
- 由于不支持直接访问本地目录,附上一个导出.claude/skills中配置的skill的js脚本
- 使用示例:任意目录执行
node .\claude-skill-to-json.js > 要保存的位置.json - 然后将json通过配置管理导入到网页端
- 使用示例:任意目录执行
claude-skill-to-json.js文件内容
// 使用示例: node .\claude-skill-to-json.js > 要保存的位置.json
const fs = require(‘fs’);
const path = require(‘path’);
const os = require(‘os’); // 引入 os 模块
/**
- 修改后的配置:始终指向用户主目录下的 .claude/skills
- 例如:/Users/yourname/.claude/skills 或 C:\Users\yourname.claude\skills
*/
const SKILLS_DIR = path.join(os.homedir(), ‘.claude’, ‘skills’);
/**
-
简单的 YAML Frontmatter 解析器
/
function parseSkillFile(filePath) {
try {
const content = fs.readFileSync(filePath, ‘utf8’);
const regex = /^—\r?\n([\s\S]?)\r?\n—\r?\n([\s\S]*)$/;
const match = content.match(regex);if (!match) return null;
const yamlSection = match[1];
const bodyContent = match[2].trim();const metadata = {};
yamlSection.split(‘\n’).forEach(line => {
const [key, …valueParts] = line.split(‘:’);
if (key && valueParts.length) {
metadata[key.trim()] = valueParts.join(‘:’).trim();
}
});return {
name: metadata.name || ‘unknown’,
description: metadata.description || ‘’,
content: bodyContent
};
} catch (e) {
return null;
}
}
function main() {
if (!fs.existsSync(SKILLS_DIR)) {
console.error(Error: 技能目录不存在于用户目录: ${SKILLS_DIR});
process.exit(1);
}
const results = ;
const folders = fs.readdirSync(SKILLS_DIR);
folders.forEach(folder => {
const folderPath = path.join(SKILLS_DIR, folder);
if (fs.statSync(folderPath).isDirectory()) {
const skillFilePath = path.join(folderPath, 'SKILL.md');
if (fs.existsSync(skillFilePath)) {
const skillData = parseSkillFile(skillFilePath);
if (skillData) {
results.push(skillData);
}
}
}
});
// 输出最终 JSON
console.log(JSON.stringify(results, null, 2));
}
main();




