[作者]:楚北
一、背景
当我们收藏或者浏览过的语雀文章非常多时,我们可能无法从这些自己曾经关注过的文章里面「迅速搜索」到自己想要的文章。下面将开始介绍下针对语雀收藏、浏览过的文档进行聚焦搜索定制一个Alfred插件的详细教程。
二、环境要求
- Mac OS(Alfred 并不支持 Windows系统)
- Alfred 4 + Powerpack(呼吁大家支持正版,付费购买软件的Powerpack使用许可,RMB约¥260)
- Node.js >= 8
- Visual Studio Code(建议安装)
三、自定义插件步骤
- 打开 Alfred Preferences -> Workflows -> 点击 "+" -> 选择 "Blank Workflow"
- 参考下图填写好新增插件(下文以“yuque插件”简称)的基本信息
- 此时会在Alfred的插件目录下生成一个类似 user.workflow.53C71033-D345-4BF4-8F61-4C328FC5DE22 的目录,后缀是随机生成的(下文以该路径举例)。
该目录的完整路径将会是:
/Users/${YOUR_USER_NAME}/Library/Application Support/Alfred/Alfred.alfredpreferences/workflows/user.workflow.53C71033-D345-4BF4-8F61-4C328FC5DE22
- 全局安装
yeoman
sudo npm install -g yo
- 全局安装
generator-alfred
sudo npm install -g generator-alfred
- 进到该“yuque插件”工程根目录,建议采用右键点击“yuque插件” -> Open in Terminal 方便快速
- 运行如下命令初始化符合Alfred Node.js规范的插件工程
yo alfred
按步骤填写一下必要的参数,注意确保如下项目填写你期望唤起该yuque插件的快捷指令(比如填“yq”)
- 继续在该目录下运行如下命令打开Visual Studio Code实现插件编码逻辑
code .
- 安装
node-fetch、fuse.js依赖
npm install node-fetch fuse.js --save
- 编辑
index.js
"use strict";
import alfy from "alfy";
import fetch from "node-fetch";
import Fuse from "fuse.js";
(async () => {
const DOMAIN = process.env.DOMAIN || "www.yuque.com";
const CSRF_TOKEN = process.env.CSRF_TOKEN;
const COOKIE = process.env.COOKIE;
const options = {
headers: {
accept: "application/json",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
"content-type": "application/json",
"sec-ch-ua": '" Not;A Brand";v="99", "Google Chrome";v="91", "Chromium";v="91"',
"sec-ch-ua-mobile": "?0",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"x-requested-with": "XMLHttpRequest",
"x-csrf-token": CSRF_TOKEN,
cookie: COOKIE,
},
referrer: `https://${DOMAIN}/dashboard/collections`,
referrerPolicy: "strict-origin-when-cross-origin",
body: null,
method: "GET",
mode: "cors",
};
const fetchJSON = async (...args) => {
let resp = await fetch(...args);
return await resp.json();
};
const uniqBy = (list, prop) => {
if (!list || !list.length) return [];
return list.filter((obj, pos, arr) => {
return arr.map((mapObj) => mapObj[prop]).indexOf(obj[prop]) === pos;
});
};
const [dataFavorite, dataHistory] = await Promise.all([
// 收藏接口
fetchJSON(`https://${DOMAIN}/api/mine/marks?limit=1000&offset=0&type=all&q=${encodeURIComponent(alfy.input)}`, options),
// 历史记录接口
fetchJSON(`https://${DOMAIN}/api/mine/docs?limit=1000&offset=0&type=recent_read`, options),
]);
const getFavoriteUrl = (element) => {
let url = element?._url || "";
let title = element?.title || "";
if (element?._url) {
let suffixUrl = element._url;
suffixUrl = suffixUrl.indexOf("/") === 0 ? suffixUrl : `/${suffixUrl}`;
return `https://${DOMAIN}${suffixUrl}`;
}
if (title) {
return `https://${DOMAIN}/search?scope=&p=1&q=${encodeURIComponent(title)}&type=content&advanced=&related=`;
}
return `https://${DOMAIN}/`;
};
const getHistoryUrl = (element) => {
let firstSlug = element?.book?.user?.login;
let secondSlug = element?.book?.slug;
let thirdSlug = element?.slug;
if (element?.share?.type === "share" && element?.share?.token) {
firstSlug = "docs";
secondSlug = "share";
thirdSlug = element.share.token;
}
let suffixUrl = [firstSlug, secondSlug, thirdSlug].filter(Boolean).join("/");
return `https://${DOMAIN}/${suffixUrl}`;
};
const getFuzzyMatchResults = (list, input) => {
const fuse = new Fuse(list || [], {
includeScore: true,
useExtendedSearch: true,
keys: [
{
name: "title",
weight: 1,
},
// {
// name: "subtitle",
// weight: 0.9,
// },
{
name: "description",
weight: 0.6,
},
{
name: "username",
weight: 1,
},
{
name: "userlogin",
weight: 1,
},
{
name: "workid",
weight: 1,
},
],
});
return fuse.search(input).map((res) => {
const title = res?.item?.title || "暂无标题";
const percentage = `${((1 - res.score) * 100).toFixed(5)}%`;
const isShare = res?.item?.element?.action_option === "share_Doc_doc" || res?.item?.element?.share?.type === "share";
const defaultSubtitle = isShare ? "分享" : "文档";
const subtitle = `${res?.item?.subtitle || defaultSubtitle} | ${percentage}`;
const arg = res?.item?.isFavorite ? getFavoriteUrl(res?.item?.element) : getHistoryUrl(res?.item?.element);
return {
title,
subtitle,
arg,
};
});
};
const itemsFavorite = (dataFavorite?.data?.actions || []).map((element) => {
const title = element?.title;
const description = element?.target?.description;
const username = element?.target?.user?.name;
const userlogin = element?.target?.user?.login;
const workid = element?.target?.user?.work_id;
const subtitle = username || userlogin || workid;
const id = element?.target_id;
return {
title,
subtitle,
description,
username,
userlogin,
workid,
element,
isFavorite: true,
_id: id + "#" + title,
};
});
const itemsHistory = (dataHistory?.data || []).map((element) => {
const title = element?.title;
const username = element?.book?.user?.name;
const userlogin = element?.book?.user?.login;
const subtitle = username || userlogin;
const id = element?.id;
return {
title,
subtitle,
description: "",
username,
userlogin,
workid: "",
element,
isFavorite: false,
_id: id + "#" + title,
};
});
const itemsUniq = uniqBy([...itemsFavorite, ...itemsHistory], "_id");
const items = getFuzzyMatchResults(itemsUniq, alfy.input);
alfy.output(items);
})();
- 编辑完需要“yuque插件” -> 双击 Script Filter
Script处使用如下代码完整粘贴覆盖:
DOMAIN="$domain" CSRF_TOKEN="$csrfToken" COOKIE="$cookie" ./node_modules/.bin/run-node index.js "$1"
- 最后可以通过右键点击“yuque插件” -> Edit Details 来完善图标和其他信息
- 快捷键 “
Option+ 空格” 可以快速运行检索,选择结果后回车即可跳转到对应的文档地址
注意:
- “
yq” 和 “关键词” 中间是有空格的 - 关键词目前支持收藏过、浏览过的文档标题、作者、内容进行模糊匹配
- 由于使用了
fuse.js扩展搜索功能,可以使用下述规则进行高级功能检索
四、福利环节
Alfred Workflows包含非常多好用的插件,这里推荐几个好用的开发者插件,谁用谁知道😂
- Chrome Bookmarks(Chrome书签搜索插件)
- DevDocs(开发者文档搜索插件)
- Github Repos(Github仓库搜索插件)
- Google Chrome History(Chrome历史记录搜索插件)
- Homebrew & Cask for Alfred(Homebrew 和 Cask安装辅助插件)
- MDN Search(MDN文档搜索插件)
- Pocket for Alfred(Pocket文档搜索插件)
- QR-Code(二维码生成插件)