下面是已修改的完整 Taio Action 脚本:优先自动读取剪切板内容作为 Markdown 发文;若剪切板为空,再回退到动作输入。其余逻辑保持不变。
// Taio Action: Publish Markdown to Blog via /api/publish (blog only) - smart title - clipboard first
const BASE_URL = "https://yourdomain"; // 修改为你的后端地址
const ENDPOINT = `${BASE_URL}/api/publish`;
const TARGETS = ["blog"];
// 标题最长长度(中文/英文字符都按 1 计)
const TITLE_MAX_LEN = 40;
function defaultTags() {
const d = new Date();
return `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, "0")}`;
}
// 优先从剪切板读取 Markdown,若为空再使用动作输入
const rawInput = ($clipboard && $clipboard.text) ? $clipboard.text : $actions.inputValue;
const md = (rawInput || "").trim();
if (!md) {
$actions.reject("没有内容:请把要发布的 Markdown 放到剪贴板,或作为动作输入。");
}
// 智能截断为标题:优先按句号/问号/感叹号/换行截断,再按长度截断
function smartCut(str, maxLen) {
const s = str.replace(/\s+/g, " ").trim();
const punctIdx = (() => {
const idx = [
s.indexOf("。"),
s.indexOf("!"),
s.indexOf("?"),
s.indexOf(". "),
s.indexOf("! "),
s.indexOf("? "),
s.indexOf("\n")
].filter(i => i >= 0);
return idx.length ? Math.min(...idx) : -1;
})();
let cut = punctIdx >= 0 ? s.slice(0, punctIdx) : s;
if (cut.length > maxLen) cut = cut.slice(0, maxLen);
return cut || "Untitled";
}
// 解析 YAML Front Matter 与标题
function parseMeta(markdown) {
const meta = {};
let body = markdown;
// 兼容 \n 和 \r\n 的 front matter
const fm = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
if (fm) {
body = markdown.slice(fm[0].length);
fm[1].split(/\r?\n/).forEach(line => {
const m = line.match(/^([^:#\s][^:]*):\s*(.*)$/);
if (m) {
const k = m[1].trim().toLowerCase();
let v = m[2].trim();
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
v = v.slice(1, -1);
}
if (k === "tags") {
try {
meta.tags = v.startsWith("[")
? JSON.parse(v.replace(/'/g, '"'))
: v.split(",").map(s => s.trim()).filter(Boolean);
} catch {
meta.tags = v.split(",").map(s => s.trim()).filter(Boolean);
}
} else {
meta[k] = v;
}
}
});
}
// 标题:front matter -> 第一个 H1 -> 正文智能截断
let title = meta.title;
if (!title) {
const h1 = body.match(/^\s{0,3}#\s+(.+?)\s*$/m);
if (h1) title = h1[1].trim();
}
if (!title) {
const firstNonEmptyLine = body.split(/\r?\n/).find(l => l.trim().length > 0) || "";
title = smartCut(firstNonEmptyLine, TITLE_MAX_LEN);
} else if (title.length > TITLE_MAX_LEN) {
title = smartCut(title, TITLE_MAX_LEN);
}
return {
title: title || "Untitled",
tags: Array.isArray(meta.tags) ? meta.tags : [],
body,
};
}
// Markdown 转纯文本(供 content 字段)
function mdToPlain(markdown) {
return markdown
.replace(/```[\s\S]*?```/g, " ")
.replace(/`+/g, "")
.replace(/^\s{0,3}#{1,6}\s+/gm, "")
.replace(/!\[([^\]]*)\]\((?:[^()]|\([^()]*\))*\)/g, "$1")
.replace(/\[([^\]]+)\]\((?:[^()]|\([^()]*\))*\)/g, "$1")
.replace(/(\*\*|__|\*|_)/g, "")
.replace(/<[^>]+>/g, "")
.replace(/[ \t]+\n/g, "\n")
.replace(/\n{3,}/g, "\n\n")
.trim();
}
function httpPostJSON(url, json) {
return new Promise((resolve, reject) => {
$http.request({
method: "POST",
url,
header: { "Content-Type": "application/json" },
body: json,
handler: resp => {
try {
const status = resp.response ? resp.response.statusCode : resp.statusCode;
const data = resp.data;
if (status >= 200 && status < 300) resolve(data);
else reject(new Error(`HTTP ${status}: ${typeof data === "string" ? data : JSON.stringify(data)}`));
} catch (e) {
reject(e);
}
}
});
});
}
(async () => {
try {
const { title, tags, body } = parseMeta(md);
const content_md = body;
const content_plain = mdToPlain(content_md);
const tagStr = (Array.isArray(tags) && tags.length) ? tags.join(",") : defaultTags();
const payload = {
title, // 改进后的智能标题
content: content_plain,
content_md: content_md,
tags: tagStr,
targets: TARGETS
};
const result = await httpPostJSON(ENDPOINT, payload);
const blog = result && result.blog;
if (blog && blog.status === "success") {
const url = blog.redirect_url || "(无跳转链接)";
$actions.resolve(`博客发布成功:${url}`);
} else {
const msg = blog ? (blog.message || JSON.stringify(blog)) : JSON.stringify(result);
$actions.reject(`博客发布失败:${msg}`);
}
} catch (err) {
$actions.reject(`发布失败:${err.message || err}`);
} finally {
$actions.finish();
}
})();