// 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 && (
)}
{list.map(s => )}
带 人脉分享 的技能来自你通讯录里的人;我自制 的技能调用你在墨吞接入的引擎运行,也能像这样传给别人。
{showPlans &&
{ setPlan(id); setShowPlans(false); }} onClose={() => setShowPlans(false)} />}
);
}
// ========== 笔记技能足迹(详情页内 · 这一页是怎么写成的)==========
function SkillTrace({ note, onShareSkill }) {
const ids = noteSkillStack(note);
const [shared, setShared] = useStateSK(false);
if (!ids.length) return null;
return (
这一页是怎么写成的
{ids.map((id, i) => {
const s = skillById(id); if (!s) return null;
return (
{i > 0 && }
{s.name.split(' · ')[0]}
);
})}
{shared && (
朋友打开这篇时,可一键把这套配方装进自己的本子 —— 内容与技能,分开分享。
)}
);
}
// ========== 分享面板(内容 / 技能 两路)==========
const SHARE_CHANNELS = [
{ id: 'popor', label: 'Popor 好友', dot: '伴', cls: 'popor' },
{ id: 'wechat', label: '微信', dot: '微', cls: 'wechat' },
{ id: 'instagram', label: 'Instagram', dot: 'IG', cls: 'ins' },
{ id: 'twitter', label: 'Twitter', dot: 'X', cls: 'tw' },
{ id: 'link', label: '复制链接', dot: '链', cls: 'link' },
];
function ShareSheet({ note, onClose }) {
const [mode, setMode] = useStateSK('content'); // content | skill
const [done, setDone] = useStateSK(null);
const [picked, setPicked] = useStateSK({});
const stack = noteSkillStack(note);
function fire(ch) {
if (mode === 'content' && ch.id === 'popor') return; // 站内好友走选择
const what = mode === 'content' ? '这一页' : '这套技能配方';
setDone(ch.id === 'link' ? '链接已复制(演示)' : `已把${what}分享到${ch.label}(演示)`);
setTimeout(() => setDone(null), 1800);
}
return (
{ if (e.target.classList.contains('share-wrap')) onClose(); }}>
{mode === 'content' ? (
{note.title}
{note.preview}
{note.date} · {note.city}
) : (
这一页用到的技能配方
{stack.map((id, i) => {
const s = skillById(id); if (!s) return null;
return (
{i > 0 && }
{s.name.split(' · ')[0]}
);
})}
对方可一键把这套配方装进自己的本子
)}
{mode === 'content' && (
发给 Popor 好友
{CONTACTS.slice(0, 6).map(c => (
))}
{Object.values(picked).some(Boolean) && (
)}
)}
分享到
{SHARE_CHANNELS.filter(ch => !(mode === 'content' && ch.id === 'popor')).map(ch => (
))}
{done &&
{done}
}
);
}
Object.assign(window, { SkillStore, SkillTrace, ShareSheet, PlansPanel,
getInstalledSkills, skillInstallMap, setSkillInstall, skillPrompt, skillRunnable, SKILL_TOOLMAP });