// Skills.jsx — 技能工具箱 + 笔记技能足迹 + 分享面板 const { useState: useStateSK } = React; function fmtUses(n) { return n >= 10000 ? (n / 10000).toFixed(1).replace(/\.0$/, '') + '万' : (n >= 1000 ? (n / 1000).toFixed(1).replace(/\.0$/, '') + 'k' : String(n)); } function loadCustomSkills() { try { return JSON.parse(localStorage.getItem('popor.skills.custom') || '[]'); } catch (_) { return []; } } function saveCustomSkills(list) { localStorage.setItem('popor.skills.custom', JSON.stringify(list)); } // —— 已安装技能 · 持久化(工具箱与书写台共享)—— function loadSkillInstall() { try { return JSON.parse(localStorage.getItem('popor.skills.install') || 'null'); } catch (_) { return null; } } function saveSkillInstall(map) { localStorage.setItem('popor.skills.install', JSON.stringify(map)); } function skillInstallMap() { let m = loadSkillInstall(); if (!m) { m = {}; SKILLS.forEach(s => { if (s.installed) m[s.id] = true; }); } loadCustomSkills().forEach(s => { if (m[s.id] === undefined) m[s.id] = true; }); return m; } function setSkillInstall(id, on) { const m = skillInstallMap(); m[id] = on; saveSkillInstall(m); } function getInstalledSkills() { const m = skillInstallMap(); return SKILLS.concat(loadCustomSkills()).filter(s => m[s.id]); } // 官方技能的提示词(自制技能用自己的 .prompt)。没有提示词的是影像类,走生成工具。 const SKILL_PROMPTS = { 'text-gen': '你是作者本人的续写助手。顺着下面这页的语气与思路,自然地接着往下写一到两段,只补充语境、不重复已有内容,不解释、不加标题。', 'translate': '把下面这页文字翻成地道、克制的英文,保留原有分段。逐段输出中英对照:每段先原文中文,另起一行英文。', 'summarize': '把下面这页内容提炼成 2–3 条可引用的摘录卡片,每条一句话,保留原意,用「· 」开头分行。', 'layout': '为下面这页拟一个清晰的结构:先一句主旨,再用小标题加要点把零散内容排成有节奏的大纲。', }; const SKILL_TOOLMAP = { 'img-gen': 'image', 'compose': 'audio', 'video-edit': 'video', 'voice': 'tts' }; function skillPrompt(s) { return s.prompt || SKILL_PROMPTS[s.id] || null; } function skillRunnable(s) { return !!skillPrompt(s) || !!SKILL_TOOLMAP[s.id]; } // ========== 技能卡片 ========== function SkillCard({ s, installed, onToggle, onDelete }) { const official = s.by === '官方'; return (
{s.name}
{official ? 官方 : s.custom ? 我自制 : {s.byName || s.by}} {s.shared && !official && !s.custom && 人脉分享}
{s.custom && }

{s.desc}

{s.custom ? 走你接入的引擎 : {s.credits} 积分/次} {!s.custom && {fmtUses(s.uses)} 人用过}
); } // ========== 订阅档位面板 ========== function PlansPanel({ current, onPick, onClose }) { return (
{ if (e.target.classList.contains('plans-wrap')) onClose(); }}>
订阅档位
订阅获得每月积分,深度模型与技能不再受限
{PLANS.map(p => (
{p.name}
{p.price === 0 ? '免费' : ¥{p.price}{p.unit}}
每月 {p.monthly} 积分
{p.blurb}
{p.tiers.map(t => (MODEL_TIERS.find(m => m.id === t) || {}).name).join(' · ')} 模型
))}
); } // ========== 技能工具箱(全屏)========== function SkillStore({ onClose }) { const [cat, setCat] = useStateSK('all'); const [custom, setCustom] = useStateSK(() => loadCustomSkills()); const [installed, setInstalled] = useStateSK(() => skillInstallMap()); const [plan, setPlan] = useStateSK(USER_WALLET.plan); const [showPlans, setShowPlans] = useStateSK(false); const [adding, setAdding] = useStateSK(false); const [form, setForm] = useStateSK({ name: '', desc: '', prompt: '', cat: 'create' }); const connected = (typeof hasActiveKey === 'function') && hasActiveKey(); const engineName = (typeof activeProviderName === 'function') ? activeProviderName() : '墨吞'; const credits = (PLANS.find(p => p.id === plan) || PLANS[0]).monthly; const all = SKILLS.concat(custom); const list = all.filter(s => cat === 'all' ? true : (cat === 'mine' ? installed[s.id] : s.cat === cat)); function toggle(id) { const on = !installed[id]; setSkillInstall(id, on); setInstalled(m => Object.assign({}, m, { [id]: on })); } function createSkill() { if (!form.name.trim() || !form.prompt.trim()) return; const id = 'my-' + Date.now(); const sk = { id, name: form.name.trim(), desc: form.desc.trim() || '我自制的技能', prompt: form.prompt.trim(), icon: 'spark', cat: form.cat, by: '我自制', custom: true, credits: 0, uses: 0, installed: true }; const next = custom.concat([sk]); setCustom(next); saveCustomSkills(next); setSkillInstall(id, true); setInstalled(m => Object.assign({}, m, { [id]: true })); setForm({ name: '', desc: '', prompt: '', cat: 'create' }); setAdding(false); setCat('mine'); } function deleteSkill(id) { const next = custom.filter(s => s.id !== id); setCustom(next); saveCustomSkills(next); setSkillInstall(id, false); setInstalled(m => { const c = Object.assign({}, m); delete c[id]; return c; }); } return (
{ if (e.target.classList.contains('sheet')) onClose(); }}>
技能工具箱像插件一样调用 · 装进本子就能在书写台用
{/* 引擎横幅:技能跑在墨吞接入的引擎上(任意一家都行)*/}
{connected ? ( 技能运行在你接入的引擎上 · 当前 {engineName} DeepSeek / Gemini / Grok / ChatGPT / 千问 都行,不限 Claude ) : ( 技能要用 AI —— 先在顶栏「墨吞」接入任意一家引擎(DeepSeek / Gemini / Grok / ChatGPT / 千问)。调用时若没接入,会提醒你设置。 )}
{SKILL_CATS.map(c => ( ))}
{adding && (
写一个你自己的技能 · 调用墨吞的引擎运行
setForm({ ...form, name: e.target.value })} />
setForm({ ...form, desc: e.target.value })} />