@topmindspace/topmind-writing-skills 0.8.50 → 0.8.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.en.md +10 -0
- package/README.md +10 -0
- package/bin/topmind-writing-skills.js +16 -2
- package/lib/host-pack.cjs +35 -0
- package/lib/zip.cjs +18 -0
- package/package.json +6 -2
- package/runtime/article-renderer.d.mts +9 -0
- package/runtime/article-renderer.mjs +130 -0
- package/runtime/host-guide.md +7 -0
- package/topmind-wechat-post/SKILL.md +1 -1
- package/topmind-wechat-post/package.json +1 -1
- package/topmind-wechat-post/references/workspace.md +8 -0
- package/topmind-x-article/SKILL.md +1 -1
- package/topmind-x-article/package.json +1 -1
- package/topmind-x-article/references/workspace.md +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## [0.8.51] - 2026-10-26
|
|
2
|
+
|
|
3
|
+
- 新增独立宿主分发协议、可复现 ZIP、SHA256SUMS 和 latest.json;保留各技能独立版本与原 npx 安装方式。
|
|
4
|
+
- 新增无 IO 的 HTML/文本排版核心、八个写作动作、宿主能力说明、图片清单及格式诊断,由 Desktop/Obsidian 以固定快照消费。
|
|
5
|
+
- CLI 先完整复制到临时目录,再替换旧技能,复制或切换失败恢复旧版;不再先删除旧安装。
|
|
6
|
+
- 补充公众号/X 长文的宿主适配与原位兼容说明,加入包构建、排版、安全和安装回归。
|
|
7
|
+
|
|
1
8
|
## [0.8.50] - 2026-10-26
|
|
2
9
|
|
|
3
10
|
### 创作方式与参考
|
package/README.en.md
CHANGED
|
@@ -18,6 +18,16 @@ npx @topmindspace/topmind-writing-skills install topmind-viral-posts # install
|
|
|
18
18
|
npx @topmindspace/topmind-writing-skills list # see all
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
## Desktop and Obsidian integration
|
|
22
|
+
|
|
23
|
+
A reviewed release snapshot of [topmind-writing-skills](https://github.com/topmindspace/topmind-writing-skills) is bundled by default: revision, formatting, briefs, social posts, WeChat, X Articles, covers and posters. The writing repository keeps its own release cadence and version.
|
|
24
|
+
|
|
25
|
+
Open a Markdown document and choose **Writing & delivery** from Apps/status/commands in Desktop, or note menus/commands in Obsidian. Edit the source in the main editor, choose a delivery format and theme, then preview, copy or export beside it in `out/`. Existing article folders and WeChat drafts work in place.
|
|
26
|
+
|
|
27
|
+
Built-in HTML/text typography requires no Python. Preview and copy use the same output; changed sources or images require regeneration. Diagnostics identify missing images, diagram source, extended containers and table paste limitations. AI images, PNG rendering and publishing require their respective tools. Copying does not publish; wording scores do not decide editorial quality.
|
|
28
|
+
|
|
29
|
+
About & updates tracks the application, core skills, independent writing pack and integrations separately. Writing resources can update independently; compiled renderer code follows the host release. Managed packs are isolated, local modifications block replacement and failed installs preserve the previous version. Supported Desktop installs can download an update and explicitly restart; manual downloads remain available when signing or the installation channel prevents automatic updates.
|
|
30
|
+
|
|
21
31
|
## More choices, concrete references
|
|
22
32
|
|
|
23
33
|
- Read actual references and visible specimens, then adapt their methods to the current material.
|
package/README.md
CHANGED
|
@@ -18,6 +18,16 @@ npx @topmindspace/topmind-writing-skills install topmind-viral-posts # 装一
|
|
|
18
18
|
npx @topmindspace/topmind-writing-skills list # 看全部
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
## 在 Desktop 与 Obsidian 中使用
|
|
22
|
+
|
|
23
|
+
默认内置 [topmind-writing-skills](https://github.com/topmindspace/topmind-writing-skills) 的版本化快照:改稿、只排版、短文、互动短帖、公众号、X 长文、封面与长图。写作仓独立迭代和发布,宿主内置经过验证的版本。
|
|
24
|
+
|
|
25
|
+
打开 Markdown,Desktop 从应用、状态栏或命令面板进入“写作与交付”;Obsidian 从命令面板或笔记菜单进入。源稿继续在主编辑器修改;选择格式和主题后生成预览,可复制正文或导出到源稿旁的 `out/`。旧 `公众号稿.md` 和文章目录可直接使用,不会被批量改名。
|
|
26
|
+
|
|
27
|
+
内置 HTML/文本排版无需 Python,预览与复制同源;源稿或图片改变后需要重新生成。扩展容器、图表源码、缺图和平台表格粘贴限制会显示诊断。封面生图、PNG 和外部发布依赖实际工具,复制不等于已发布。审校不使用“AI 味分数”作为定稿门槛。
|
|
28
|
+
|
|
29
|
+
“关于与更新”分别展示 Desktop、核心技能、独立写作包和集成组件。写作资源可独立更新;编译进宿主的排版代码随宿主升级。受管包按身份隔离,覆盖本地定制会被阻止,失败保留旧版。Desktop 支持标准更新器下载后显式重启;签名、平台或渠道不支持时保留手动下载。
|
|
30
|
+
|
|
21
31
|
## 加选项,让经验帮助创作
|
|
22
32
|
|
|
23
33
|
- **看过再借鉴**:实际阅读参考作品和可见样张,迁移角度、组织、节奏与设计关系。
|
|
@@ -242,9 +242,23 @@ function install(skillId, targetRoot, { force = false, defaultTarget = false } =
|
|
|
242
242
|
);
|
|
243
243
|
}
|
|
244
244
|
upgradedFrom = cmp > 0 ? installedVer || 'unknown' : null;
|
|
245
|
-
fs.rmSync(dest, { recursive: true, force: true });
|
|
246
245
|
}
|
|
247
|
-
|
|
246
|
+
const stage = fs.mkdtempSync(path.join(targetRoot, `.${skillId}-stage-`));
|
|
247
|
+
let previous = null;
|
|
248
|
+
try {
|
|
249
|
+
copyDir(src, stage);
|
|
250
|
+
if (fs.existsSync(dest)) {
|
|
251
|
+
previous = fs.mkdtempSync(path.join(targetRoot, `.${skillId}-previous-`));
|
|
252
|
+
fs.rmdirSync(previous);
|
|
253
|
+
fs.renameSync(dest, previous);
|
|
254
|
+
}
|
|
255
|
+
try { fs.renameSync(stage, dest); }
|
|
256
|
+
catch (error) { if (previous) { fs.renameSync(previous, dest); previous = null; } throw error; }
|
|
257
|
+
if (previous) { try { fs.rmSync(previous, { recursive: true, force: true }); } catch { console.warn(`Previous copy retained at: ${previous}`); } }
|
|
258
|
+
} catch (error) {
|
|
259
|
+
fs.rmSync(stage, { recursive: true, force: true });
|
|
260
|
+
die(`Install failed; the previous installed skill was retained. ${error.message}`);
|
|
261
|
+
} finally { fs.rmSync(stage, { recursive: true, force: true }); }
|
|
248
262
|
// 安装摘要三行:装到哪里 / 装了什么版本 / 下一步。
|
|
249
263
|
const toNote = defaultTarget ? ' (default, auto-detected — no --to given)' : '';
|
|
250
264
|
const skillVer = readSkillVersion(dest) || 'unknown';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Official portable host distribution. Independent from any host checkout. */
|
|
2
|
+
'use strict';
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { createHash } = require('node:crypto');
|
|
6
|
+
const hash = value => createHash('sha256').update(value).digest('hex');
|
|
7
|
+
function buildHostPack(root) {
|
|
8
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
9
|
+
const ids = fs.readdirSync(root).filter(id => /^[a-z0-9-]+$/.test(id) && fs.existsSync(path.join(root, id, 'SKILL.md'))).sort();
|
|
10
|
+
const resources = {};
|
|
11
|
+
const files = {};
|
|
12
|
+
function walk(dir, relative) {
|
|
13
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
|
|
14
|
+
if (entry.name.startsWith('.') || ['node_modules', 'dist', '__pycache__'].includes(entry.name)) continue;
|
|
15
|
+
const rel = `${relative}/${entry.name}`;
|
|
16
|
+
if (entry.isSymbolicLink()) throw new Error(`Symlink in host pack: ${rel}`);
|
|
17
|
+
if (entry.isDirectory()) walk(path.join(dir, entry.name), rel);
|
|
18
|
+
else if (entry.isFile() && /\.(md|py|json|yaml|yml|css|html|txt|js|mjs|mts)$/i.test(entry.name)) {
|
|
19
|
+
const content = fs.readFileSync(path.join(dir, entry.name), 'utf8');
|
|
20
|
+
resources[rel] = content; files[rel] = hash(content);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
for (const id of [...ids, 'runtime']) walk(path.join(root, id), id);
|
|
25
|
+
const skills = ids.map(id => {
|
|
26
|
+
const raw = resources[`${id}/SKILL.md`];
|
|
27
|
+
const meta = JSON.parse(fs.readFileSync(path.join(root, id, 'package.json'), 'utf8'));
|
|
28
|
+
if (!/^---\r?\n/u.test(raw) || !/^name:\s*/mu.test(raw)) throw new Error(`Invalid skill: ${id}`);
|
|
29
|
+
return { id, name: id, description: meta.description || id, version: meta.version, packId: 'topmind-writing-skills' };
|
|
30
|
+
});
|
|
31
|
+
const manifest = { schema_version: 1, name: 'topmind-writing-skills', version: pkg.version, license: pkg.license, repository: pkg.repository, entry_files: ids.map(id => `${id}/SKILL.md`), files, runtime: { api: 1, entry: 'runtime/article-renderer.mjs', update: 'host-release' } };
|
|
32
|
+
const digest = hash(JSON.stringify(manifest));
|
|
33
|
+
return { manifest: { ...manifest, digest }, snapshot: { id: manifest.name, version: pkg.version, source: 'https://github.com/topmindspace/topmind-writing-skills', license: pkg.license, digest, skills, resources } };
|
|
34
|
+
}
|
|
35
|
+
module.exports = { buildHostPack };
|
package/lib/zip.cjs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Deterministic portable ZIP writer. Fixed DOS timestamp; no host permissions or paths. */
|
|
2
|
+
const { deflateRawSync } = require('node:zlib');
|
|
3
|
+
const table = Array.from({ length: 256 }, (_, n) => { let c = n; for (let i = 0; i < 8; i++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; return c >>> 0; });
|
|
4
|
+
const crc32 = data => { let crc = 0xffffffff; for (const byte of data) crc = table[(crc ^ byte) & 255] ^ (crc >>> 8); return (crc ^ 0xffffffff) >>> 0; };
|
|
5
|
+
function createZip(files) {
|
|
6
|
+
const locals = [], directory = []; let offset = 0;
|
|
7
|
+
for (const [name, value] of Object.entries(files).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
|
|
8
|
+
const filename = Buffer.from(name); const data = Buffer.from(value); const compressed = deflateRawSync(data); const crc = crc32(data);
|
|
9
|
+
const local = Buffer.alloc(30); local.writeUInt32LE(0x04034b50); local.writeUInt16LE(20, 4); local.writeUInt16LE(0x800, 6); local.writeUInt16LE(8, 8); local.writeUInt16LE(33, 12); local.writeUInt32LE(crc, 14); local.writeUInt32LE(compressed.length, 18); local.writeUInt32LE(data.length, 22); local.writeUInt16LE(filename.length, 26);
|
|
10
|
+
locals.push(local, filename, compressed);
|
|
11
|
+
const central = Buffer.alloc(46); central.writeUInt32LE(0x02014b50); central.writeUInt16LE(20, 4); central.writeUInt16LE(20, 6); central.writeUInt16LE(0x800, 8); central.writeUInt16LE(8, 10); central.writeUInt16LE(33, 14); central.writeUInt32LE(crc, 16); central.writeUInt32LE(compressed.length, 20); central.writeUInt32LE(data.length, 24); central.writeUInt16LE(filename.length, 28); central.writeUInt32LE(offset, 42);
|
|
12
|
+
directory.push(central, filename); offset += local.length + filename.length + compressed.length;
|
|
13
|
+
}
|
|
14
|
+
const bytes = Buffer.concat(directory), end = Buffer.alloc(22); const count = Object.keys(files).length;
|
|
15
|
+
end.writeUInt32LE(0x06054b50); end.writeUInt16LE(count, 8); end.writeUInt16LE(count, 10); end.writeUInt32LE(bytes.length, 12); end.writeUInt32LE(offset, 16);
|
|
16
|
+
return Buffer.concat([...locals, bytes, end]);
|
|
17
|
+
}
|
|
18
|
+
module.exports = { createZip };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@topmindspace/topmind-writing-skills",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.51",
|
|
4
4
|
"description": "TopMindSpace 写作技能合集 — 通过 npm / npx 安装技能(公众号长文、X 长文、干货短文、X 引流短帖、改稿与质检、封面图、长图海报)。只出稿与发布包,不代发。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "TopMindspace",
|
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
},
|
|
31
31
|
"files": [
|
|
32
32
|
"bin",
|
|
33
|
+
"lib",
|
|
34
|
+
"runtime",
|
|
33
35
|
"README.md",
|
|
34
36
|
"README.en.md",
|
|
35
37
|
"LICENSE",
|
|
@@ -78,7 +80,9 @@
|
|
|
78
80
|
"check": "node scripts/run_skill_gates.js check",
|
|
79
81
|
"privacy": "python3 scripts/ci_privacy_scan.py",
|
|
80
82
|
"audit": "node scripts/run_skill_gates.js audit",
|
|
81
|
-
"package:skill": "node scripts/run_skill_gates.js package"
|
|
83
|
+
"package:skill": "node scripts/run_skill_gates.js package",
|
|
84
|
+
"test": "node --test tests/*.test.mjs",
|
|
85
|
+
"host:pack": "node scripts/build-host-pack.cjs"
|
|
82
86
|
},
|
|
83
87
|
"publishConfig": {
|
|
84
88
|
"access": "public"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type WritingPlatform = 'wechat' | 'x' | 'text';
|
|
2
|
+
export type WritingTheme = 'minimal-ink' | 'tech-blue' | 'newsprint' | 'graphite';
|
|
3
|
+
export const WRITING_THEMES: Record<WritingTheme, { ink: string; accent: string; paper: string; muted: string }>;
|
|
4
|
+
export const WRITING_ACTIONS: ReadonlyArray<{ id: string; zh: string; en: string; prompt: string; promptEn: string }>;
|
|
5
|
+
export function stripFrontmatter(value: string): string;
|
|
6
|
+
export interface ArticleDelivery { platform: WritingPlatform; theme: WritingTheme; html: string; body: string; text: string; checklist: string; images: Array<{ source: string; alt: string; status: string }>; diagnostics: Array<{ code: string; source?: string }> }
|
|
7
|
+
export function renderArticle(input: { markdown: string; platform?: WritingPlatform; theme?: WritingTheme; title?: string; images?: Record<string, string> }): ArticleDelivery;
|
|
8
|
+
export function resolveArticleImage(sourcePath: string, reference: string): string;
|
|
9
|
+
export function prepareArticle(input: { sourcePath: string; markdown: string; platform?: WritingPlatform; theme?: WritingTheme; title?: string }, readImage: (relative: string) => Promise<string>): Promise<ArticleDelivery>;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/** Portable host renderer. Canonical source: topmind-writing-skills. No IO or executable HTML. */
|
|
2
|
+
export const WRITING_THEMES = Object.freeze({
|
|
3
|
+
'minimal-ink': { ink: '#252525', accent: '#955247', paper: '#ffffff', muted: '#666666' },
|
|
4
|
+
'tech-blue': { ink: '#172033', accent: '#0369a1', paper: '#ffffff', muted: '#536174' },
|
|
5
|
+
newsprint: { ink: '#24211c', accent: '#80502a', paper: '#fffdf8', muted: '#655f55' },
|
|
6
|
+
graphite: { ink: '#222222', accent: '#444444', paper: '#ffffff', muted: '#666666' },
|
|
7
|
+
});
|
|
8
|
+
export const WRITING_ACTIONS = Object.freeze([
|
|
9
|
+
{ id: 'topmind-revise', zh: '改稿', en: 'Revise', prompt: '按 topmind-revise 审校当前内容,保留事实、数字、归因和作者原意。' },
|
|
10
|
+
{ id: 'topmind-format', zh: '只排版', en: 'Format', prompt: '按 topmind-format 只调整当前内容的排版,不改写文字。' },
|
|
11
|
+
{ id: 'topmind-briefs', zh: '短文', en: 'Brief', prompt: '按 topmind-briefs,基于当前材料写一篇信息清楚的短文。' },
|
|
12
|
+
{ id: 'topmind-viral-posts', zh: '互动短帖', en: 'Social post', prompt: '按 topmind-viral-posts,基于当前材料写一条互动短帖。' },
|
|
13
|
+
{ id: 'topmind-wechat-post', zh: '公众号', en: 'WeChat', prompt: '按 topmind-wechat-post,基于当前材料完成公众号稿,只出稿不代发。' },
|
|
14
|
+
{ id: 'topmind-x-article', zh: 'X 长文', en: 'X Article', prompt: '按 topmind-x-article,基于当前材料完成 X 长文,只出稿不代发。' },
|
|
15
|
+
{ id: 'topmind-cover', zh: '封面', en: 'Cover', prompt: '按 topmind-cover,为当前文章制作封面;先核对实际生图能力,缺少工具时说明。' },
|
|
16
|
+
{ id: 'topmind-poster', zh: '长图', en: 'Poster', prompt: '按 topmind-poster,为当前内容制作信息长图;先核对实际渲染能力,缺少工具时保留 HTML 并说明。' },
|
|
17
|
+
].map(action => ({ ...action, promptEn: `Use ${action.id} for the current document. Preserve source language, facts and author intent. Use only available tools; explain missing image or rendering capabilities. Draft and deliver only; do not publish.` })));
|
|
18
|
+
const esc = value => String(value).replace(/[&<>"']/gu, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
19
|
+
export function stripFrontmatter(value) {
|
|
20
|
+
return String(value || '').replace(/^\uFEFF/u, '').replace(/\r\n?/gu, '\n').replace(/^---\n[\s\S]*?\n---(?:\n|$)/u, '');
|
|
21
|
+
}
|
|
22
|
+
export function resolveArticleImage(sourcePath, reference) {
|
|
23
|
+
if (!sourcePath || /[\\:]|^\//u.test(sourcePath) || /[\\:]|^\//u.test(reference)) throw new Error('Invalid workspace image path');
|
|
24
|
+
const parts = sourcePath.split('/').slice(0, -1);
|
|
25
|
+
for (const segment of reference.split('/')) {
|
|
26
|
+
if (!segment || segment === '.') continue;
|
|
27
|
+
if (segment === '..') { if (!parts.length) throw new Error('Image escapes workspace'); parts.pop(); }
|
|
28
|
+
else parts.push(segment);
|
|
29
|
+
}
|
|
30
|
+
return parts.join('/');
|
|
31
|
+
}
|
|
32
|
+
export async function prepareArticle(input, readImage) {
|
|
33
|
+
const images = Object.create(null);
|
|
34
|
+
for (const match of String(input.markdown).matchAll(/!\[[^\]]*\]\(([^\s)]+)(?:\s+"[^"]*")?\)/gu)) {
|
|
35
|
+
const source = match[1];
|
|
36
|
+
if (/^(https?:|data:)/iu.test(source) || Object.hasOwn(images, source)) continue;
|
|
37
|
+
try { images[source] = await readImage(resolveArticleImage(input.sourcePath, source)); }
|
|
38
|
+
catch { images[source] = ''; }
|
|
39
|
+
}
|
|
40
|
+
return renderArticle({ ...input, images });
|
|
41
|
+
}
|
|
42
|
+
function safeUrl(value, image = false) {
|
|
43
|
+
const url = String(value || '').trim();
|
|
44
|
+
if (/[\u0000-\u0020\u007f<>"']/u.test(url)) return null;
|
|
45
|
+
if (/^https?:\/\//iu.test(url)) return url;
|
|
46
|
+
if (!image && /^(mailto:|#)/iu.test(url)) return url;
|
|
47
|
+
if (image && /^data:image\/(png|jpe?g|gif|webp);base64,[a-z0-9+/=]+$/iu.test(url)) return url;
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
/** Render Markdown without interpreting raw HTML. Local image bytes must be supplied by the host. */
|
|
51
|
+
export function renderArticle({ markdown = '', platform = 'wechat', theme = 'minimal-ink', title = '', images = {} } = {}) {
|
|
52
|
+
if (!['wechat', 'x', 'text'].includes(platform)) throw new Error('Unsupported delivery format');
|
|
53
|
+
if (!Object.hasOwn(WRITING_THEMES, theme)) throw new Error('Unknown writing theme');
|
|
54
|
+
const palette = WRITING_THEMES[theme];
|
|
55
|
+
const source = stripFrontmatter(markdown);
|
|
56
|
+
const diagnostics = [];
|
|
57
|
+
const imageList = [];
|
|
58
|
+
const footnotes = [];
|
|
59
|
+
const inline = raw => {
|
|
60
|
+
const stash = [];
|
|
61
|
+
const token = html => { stash.push(html); return `\u0001${stash.length - 1}\u0002`; };
|
|
62
|
+
// Remove control characters so a source cannot impersonate a stash token.
|
|
63
|
+
let value = String(raw).replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/gu, '');
|
|
64
|
+
value = value.replace(/`([^`]+)`/gu, (_, code) => token(`<code style="font-family:monospace;background:#f1f1f1;padding:2px 4px;">${esc(code)}</code>`));
|
|
65
|
+
value = value.replace(/!\[([^\]]*)\]\(([^\s)]+)(?:\s+"[^"]*")?\)/gu, (_, alt, src) => {
|
|
66
|
+
const supplied = Object.hasOwn(images, src) ? images[src] : src;
|
|
67
|
+
const url = safeUrl(supplied, true);
|
|
68
|
+
const status = !url ? 'missing' : /^data:/iu.test(url) ? 'embedded' : 'remote';
|
|
69
|
+
imageList.push({ source: src, alt, status });
|
|
70
|
+
if (!url) { diagnostics.push({ code: 'image-unavailable', source: src }); return token(`<span style="color:${palette.muted};">[${esc(alt || src)}]</span>`); }
|
|
71
|
+
return token(`<img src="${esc(url)}" alt="${esc(alt)}" style="max-width:100%;height:auto;display:block;margin:18px auto;" />`);
|
|
72
|
+
});
|
|
73
|
+
value = value.replace(/\[([^\]]+)\]\(([^\s)]+)(?:\s+"[^"]*")?\)/gu, (_, label, rawUrl) => {
|
|
74
|
+
const url = safeUrl(rawUrl);
|
|
75
|
+
if (!url) { diagnostics.push({ code: 'unsafe-link' }); return token(esc(label)); }
|
|
76
|
+
if (platform === 'wechat' && /^https?:/iu.test(url)) {
|
|
77
|
+
footnotes.push(url);
|
|
78
|
+
return token(`${esc(label)}<sup>[${footnotes.length}]</sup>`);
|
|
79
|
+
}
|
|
80
|
+
return token(`<a href="${esc(url)}" style="color:${palette.accent};">${esc(label)}</a>`);
|
|
81
|
+
});
|
|
82
|
+
value = esc(value).replace(/\*\*([^*]+)\*\*/gu, '<strong>$1</strong>').replace(/__([^_]+)__/gu, '<strong>$1</strong>').replace(/(?<!\*)\*([^*]+)\*(?!\*)/gu, '<em>$1</em>').replace(/~~([^~]+)~~/gu, '<s>$1</s>');
|
|
83
|
+
return value.replace(/\u0001(\d+)\u0002/gu, (_, n) => stash[Number(n)] || '');
|
|
84
|
+
};
|
|
85
|
+
const lines = source.split('\n');
|
|
86
|
+
const out = [];
|
|
87
|
+
let paragraph = [];
|
|
88
|
+
const flush = () => { if (paragraph.length) out.push(`<p style="margin:0 0 18px;line-height:1.85;">${inline(paragraph.join('\n')).replace(/\n/gu, '<br />')}</p>`); paragraph = []; };
|
|
89
|
+
for (let i = 0; i < lines.length; i++) {
|
|
90
|
+
const line = lines[i];
|
|
91
|
+
if (/^\s*```/u.test(line)) {
|
|
92
|
+
flush(); const language = line.trim().slice(3); const code = [];
|
|
93
|
+
while (++i < lines.length && !/^\s*```/u.test(lines[i])) code.push(lines[i]);
|
|
94
|
+
if (i === lines.length) diagnostics.push({ code: 'unclosed-code-fence' });
|
|
95
|
+
out.push(`<section style="font-family:monospace;white-space:pre-wrap;overflow-wrap:anywhere;background:#f4f4f4;padding:14px;margin:18px 0;">${esc(code.join('\n'))}</section>`);
|
|
96
|
+
if (language === 'mermaid') diagnostics.push({ code: 'diagram-source-only' });
|
|
97
|
+
} else if (/^#{1,6}\s/u.test(line)) {
|
|
98
|
+
flush(); const [, marks, text] = line.match(/^(#{1,6})\s+(.*)$/u);
|
|
99
|
+
out.push(`<h${Math.max(2, marks.length)} style="font-size:${marks.length < 3 ? 22 : 18}px;line-height:1.5;margin:28px 0 14px;color:${palette.accent};">${inline(text)}</h${Math.max(2, marks.length)}>`);
|
|
100
|
+
} else if (/^\s*([-*_])(?:\s*\1){2,}\s*$/u.test(line)) {
|
|
101
|
+
flush(); out.push('<hr style="border:0;border-top:1px solid #dddddd;margin:26px 0;" />');
|
|
102
|
+
} else if (/^\s*>/u.test(line)) {
|
|
103
|
+
flush(); const quote = [line.replace(/^\s*>\s?/u, '')];
|
|
104
|
+
while (i + 1 < lines.length && /^\s*>/u.test(lines[i + 1])) quote.push(lines[++i].replace(/^\s*>\s?/u, ''));
|
|
105
|
+
out.push(`<blockquote style="margin:18px 0;padding:10px 16px;border-left:3px solid ${palette.accent};color:${palette.muted};">${inline(quote.join('\n')).replace(/\n/gu, '<br />')}</blockquote>`);
|
|
106
|
+
} else if (/^\s*(?:[-+*]|\d+[.)])\s+/u.test(line)) {
|
|
107
|
+
flush(); const ordered = /^\s*\d/u.test(line); const tag = ordered ? 'ol' : 'ul'; const items = [line];
|
|
108
|
+
const re = ordered ? /^\s*\d+[.)]\s+/u : /^\s*[-+*]\s+/u;
|
|
109
|
+
while (i + 1 < lines.length && re.test(lines[i + 1])) items.push(lines[++i]);
|
|
110
|
+
out.push(`<${tag} style="padding-left:24px;margin:16px 0;">${items.map(item => `<li style="margin:8px 0;line-height:1.8;">${inline(item.replace(re, ''))}</li>`).join('')}</${tag}>`);
|
|
111
|
+
} else if (line.includes('|') && i + 1 < lines.length && /^\s*\|?\s*:?-{3,}/u.test(lines[i + 1])) {
|
|
112
|
+
flush(); const cells = row => row.trim().replace(/^\||\|$/gu, '').split('|').map(c => c.trim());
|
|
113
|
+
const head = cells(line); i++; const rows = [];
|
|
114
|
+
while (i + 1 < lines.length && lines[i + 1].trim() && lines[i + 1].includes('|')) rows.push(cells(lines[++i]));
|
|
115
|
+
const row = (values, tag) => `<tr>${values.map(cell => `<${tag} style="border:1px solid #dddddd;padding:8px;text-align:left;">${inline(cell)}</${tag}>`).join('')}</tr>`;
|
|
116
|
+
out.push(`<table style="border-collapse:collapse;width:100%;font-size:14px;margin:20px 0;"><thead>${row(head, 'th')}</thead><tbody>${rows.map(r => row(r, 'td')).join('')}</tbody></table>`);
|
|
117
|
+
if (platform === 'x') diagnostics.push({ code: 'table-paste-check' });
|
|
118
|
+
} else if (/^:::/u.test(line)) {
|
|
119
|
+
flush(); diagnostics.push({ code: 'container-as-text' }); paragraph.push(line);
|
|
120
|
+
} else if (!line.trim()) flush();
|
|
121
|
+
else paragraph.push(line);
|
|
122
|
+
}
|
|
123
|
+
flush();
|
|
124
|
+
if (footnotes.length) out.push(`<section style="font-size:13px;color:${palette.muted};margin-top:28px;">${footnotes.map((url, i) => `<p>[${i + 1}] ${esc(url)}</p>`).join('')}</section>`);
|
|
125
|
+
const text = source.trim();
|
|
126
|
+
const body = `<section style="color:${palette.ink};background:${palette.paper};font-family:-apple-system,BlinkMacSystemFont,'PingFang SC',sans-serif;font-size:16px;line-height:1.85;overflow-wrap:anywhere;">${out.join('\n')}</section>`;
|
|
127
|
+
const html = `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src data: https:; style-src 'unsafe-inline';"><title>${esc(title || '文章预览')}</title></head><body style="max-width:720px;margin:0 auto;padding:24px;">${body}</body></html>`;
|
|
128
|
+
const checklist = ['# 图片交付清单', '', '按正文出现顺序核对;内嵌不代表目标平台已托管或发布。', '', ...imageList.map((item, i) => `${i + 1}. ${item.status} · ${item.source.replace(/[\r\n]/gu, ' ')}`)].join('\n');
|
|
129
|
+
return { platform, theme, html, body, text, images: imageList, diagnostics, checklist };
|
|
130
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Topmind 内置写作能力
|
|
2
|
+
|
|
3
|
+
这是独立写作包的宿主快照。正文和参考按技能来源读取,不混用其他包的同名资源。
|
|
4
|
+
|
|
5
|
+
宿主提供 `render_article`:从工作区源稿生成公众号、X 长文或文本的确定性排版结果;需要持久交付时再使用宿主文件写入工具,遵守原有写闸。也可在“写作与交付”界面预览、复制和导出。不要求用户照抄 Python 命令,不把脚本文本可读当成可执行权限。
|
|
6
|
+
|
|
7
|
+
排版不改原稿;主题、表格粘贴、缺图、扩展容器和图表限制按工具真实诊断说明。封面、海报、PNG、外部检索和发布依实际工具能力执行,不虚构成功。旧文章直接使用,不为兼容批量改名。审校基于事实与作者意图,不以措辞分数定稿。
|
|
@@ -3,7 +3,7 @@ name: topmind-wechat-post
|
|
|
3
3
|
description: "创作、整理和排版公众号文章;从选题或底稿到 Markdown,按需导出内联 HTML、预览与上传清单。支持已有稿件直接排版;公众号短文用 topmind-briefs,独立改稿用 topmind-revise。"
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
|
-
version: "0.3.
|
|
6
|
+
version: "0.3.22"
|
|
7
7
|
action_category: "write"
|
|
8
8
|
triggers: "公众号, 写公众号, 微信排版, 公众号排版, 公众号稿, 公众号长文, 排版这篇文章, 定稿, 发公众号, 公众号交付, wechat, mp format, 写公众号文章, 公众号排版定稿, 公众号发布清单"
|
|
9
9
|
author: "TopMindSpace"
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
本包默认一篇长文一份源稿,用户已有目录与流程优先;纯聊天写稿无需生成全套文件夹。
|
|
4
4
|
|
|
5
|
+
## Topmind 宿主适配
|
|
6
|
+
|
|
7
|
+
写作包保持独立仓库、版本和发布。Topmind Desktop 与 Obsidian 插件内置经验证的快照,日常从当前笔记、选区或自然语言进入,不要求先安装或选择技能。外部 Agent 继续使用本包 CLI 和脚本。
|
|
8
|
+
|
|
9
|
+
宿主公布的真实能力优先于脚本示例:内置 HTML/文本排版使用宿主交付入口,不要求用户安装 Python;脚本可读不代表允许执行 shell。封面、生图、外部检索和 PNG 必须核对实际工具,缺少能力时说明具体未完成项。高级脚本与宿主排版支持范围不同,遇到扩展容器、图表或表格粘贴限制须保留内容并说明。
|
|
10
|
+
|
|
11
|
+
预览、复制和导出使用同一份源稿及主题。源稿或图片改变后重新生成;预览不改写原稿,复制不代表发布成功。旧文章目录和 `公众号稿.md` 可原位使用,不为工具兼容批量改名。
|
|
12
|
+
|
|
5
13
|
常用结构是 `<文章名>/<文章名>.md` 配 `images/`、`cover/`、`素材来源.md`、`raw/`、`out/`,复杂稿可用 `骨架.md`,外来参考可放 `参考/`。从源稿导出需要的平台版本到 `out/`,修改后重新派生,减少多稿漂移。
|
|
6
14
|
|
|
7
15
|
路径从 CLI 参数、已配置环境和实际目录解析,不猜个人路径。`article_scaffold.py --base` 可明确位置;公众号 `new-article.py` 的 `--base`、`TOPMIND_WECHAT_BASE`、`TOPMIND_WORKSPACE` 解析见平台操作参考。工具采用的文件名、frontmatter 和图片顺序协议在采用该工具时核对,不当作所有写作任务的先决条件。
|
|
@@ -3,7 +3,7 @@ name: topmind-x-article
|
|
|
3
3
|
description: "创作或整理 X Article 长文,按需导出可复制 HTML、纯文本、封面与发布清单。保留单源稿与图文顺序,由用户在编辑器发布。普通短帖用 topmind-briefs 或 topmind-viral-posts,纯格式化用 topmind-format。"
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
|
-
version: "0.4.
|
|
6
|
+
version: "0.4.27"
|
|
7
7
|
action_category: "write"
|
|
8
8
|
triggers: "X 长文, 发 X 文章, 写 X 文章, X article, 长文发 X, X 发长文, X 文章排版, 转纯文本, X 纯文本, X发布稿, 发 article, 一键复制, 写 X 长文, X 长文发布, 推特长文, twitter 长文, Twitter Article"
|
|
9
9
|
author: "TopMindSpace"
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
本包默认一篇长文一份源稿,用户已有目录与流程优先;纯聊天写稿无需生成全套文件夹。
|
|
4
4
|
|
|
5
|
+
## Topmind 宿主适配
|
|
6
|
+
|
|
7
|
+
写作包保持独立仓库、版本和发布。Topmind Desktop 与 Obsidian 插件内置经验证的快照,日常从当前笔记、选区或自然语言进入,不要求先安装或选择技能。外部 Agent 继续使用本包 CLI 和脚本。
|
|
8
|
+
|
|
9
|
+
宿主公布的真实能力优先于脚本示例:内置 HTML/文本排版使用宿主交付入口,不要求用户安装 Python;脚本可读不代表允许执行 shell。封面、生图、外部检索和 PNG 必须核对实际工具,缺少能力时说明具体未完成项。高级脚本与宿主排版支持范围不同,遇到扩展容器、图表或表格粘贴限制须保留内容并说明。
|
|
10
|
+
|
|
11
|
+
预览、复制和导出使用同一份源稿及主题。源稿或图片改变后重新生成;预览不改写原稿,复制不代表发布成功。旧文章目录和 `公众号稿.md` 可原位使用,不为工具兼容批量改名。
|
|
12
|
+
|
|
5
13
|
常用结构是 `<文章名>/<文章名>.md` 配 `images/`、`cover/`、`素材来源.md`、`raw/`、`out/`,复杂稿可用 `骨架.md`,外来参考可放 `参考/`。从源稿导出需要的平台版本到 `out/`,修改后重新派生,减少多稿漂移。
|
|
6
14
|
|
|
7
15
|
路径从 CLI 参数、已配置环境和实际目录解析,不猜个人路径。`article_scaffold.py --base` 可明确位置;公众号 `new-article.py` 的 `--base`、`TOPMIND_WECHAT_BASE`、`TOPMIND_WORKSPACE` 解析见平台操作参考。工具采用的文件名、frontmatter 和图片顺序协议在采用该工具时核对,不当作所有写作任务的先决条件。
|