分享 Alfred 思源搜索工作流(支持模糊搜索 / 标题搜索 / 全文搜索 / 多动作复制)

一直以来,我在 macOS 上使用思源写科研笔记、日志、阅读笔记时,都希望能像 Spotlight 或 Raycast 那样:

  • 输入几个字
  • 立刻从思源所有内容中搜索
  • 立即跳转到对应块
  • 或者快速复制内容 / 获取思源链接

官方搜索界面很好,但有时候手不在思源界面,切过去再搜就有点慢。
于是借助 AI,我创作了这个 Alfred 工作流,让思源真正进入系统级快捷搜索层面。

✨ 功能特点

✅ 1. 三级搜索模式(对应不同关键字)

关键字 范围 示例
sy 搜索全部内容(标题 + 正文块) sy ecology
syt 只搜索标题(Heading blocks) syt pine
syb 只搜索正文块 syb trait

✅ 2. 强力模糊搜索

  • 支持多关键字(AND 匹配):sy climate warming
  • 使用 difflib.SequenceMatcher 模糊评分
  • 自动排序最相关结果(越像越靠前)
  • 可匹配路径,结构化内容也能搜到

✅ 3. 强力去重(解决思源多条相似块的问题)

  • 很多笔记中有大量类似的引用、注释、摘要块。
  • 本工作流会自动对结果,按 snippet 去重,只展示最高匹配度的搜索结果 这样不会出现几十条一模一样的候选项。

✅ 4. 多动作支持

按键 动作
回车(Enter) 打开思源并跳转到块
⌘ + Enter 复制块链接(siyuan://blocks/...
⌥ + Enter 复制块的完整内容

✅ 5. 零依赖安装

  • 🌟 无需 pip、无需 requests、无需 jq
  • 纯 Python 标准库 + curl + Alfred 内置功能即可运行。
  • 这对 macOS 用户非常友好。

📸 效果预览

你可以在这里加几张图:

image.png

image.png

🔧 安装步骤(非常简单)

1. 确认思源 API 已启用

在思源设置:

设置 → 关于 → API Token

确认:

  • Token 已生成
  • 本地 API 服务已开启
  • 端口是 6806(你修改过的话,用自己的)

2. 创建 Alfred Workflow(空的就行)

添加 三个 Script Filter

Script Filter Keyword 模式
全部内容 sy MODE="all"
仅标题 syt MODE="title"
仅正文块 syb MODE="body"

注意:它们都使用同一份 Python 脚本,只需要改一行 MODE = ...

💻 具体脚本

🧠 Script Filter(Python 版本 以 sy 关键词为例)

  • Alfred 中添加 script filter,然后右键选 Configure Object
  • Keyword 中设置 sy
  • Language:/usr/bin/python3
  • Script 拷贝下面的代码,需要使用自己的 API token
  • 修改后记得保存
#!/usr/bin/python3
# -*- coding: utf-8 -*-

import sys
import json
import urllib.request
import urllib.error
from difflib import SequenceMatcher

HOST = "127.0.0.1"
PORT = 6806
TOKEN = "使用自己的token"


# 这里根据不同 Script Filter 修改:
#   sy  -> MODE = "all"    (所有内容)
#   syt -> MODE = "title"  (只标题)
#   syb -> MODE = "body"   (只正文块、非标题)
MODE = "all"   # <- 每个 Script Filter 自己改这一行


def build_alfred(items):
    return json.dumps({"items": items}, ensure_ascii=False)


def fuzzy_score(query, content, path):
    q = (query or "").lower()
    c = (content or "").lower()
    p = (path or "").lower()

    if not q:
        return 0.0

    s1 = SequenceMatcher(None, q, c).ratio() if c else 0.0
    s2 = SequenceMatcher(None, q, p).ratio() if p else 0.0
    return max(s1, s2)


def main():
    query = sys.argv[1] if len(sys.argv) > 1 else ""
    query = query.strip()

    if not query:
        mode_label = {
            "all": "全部内容",
            "title": "仅标题",
            "body": "仅正文块"
        }.get(MODE, "全部内容")

        print(build_alfred([{
            "title": f"输入关键字搜索思源笔记({mode_label})",
            "subtitle": "例如:sy oak / syt oak / syb oak",
            "arg": ""
        }]))
        return

    # 多词支持:按空白切分,构造 AND 条件
    words = [w for w in query.split() if w]
    if not words:
        words = [query]

    like_clauses = []
    for w in words:
        w_sql = w.replace("'", "''")
        like_clauses.append(f"content LIKE '%{w_sql}%'")

    where_sql = " AND ".join(like_clauses)

    # 根据 MODE 加上 type 条件
    type_condition = ""
    if MODE == "title":
        type_condition = " AND type = 'h'"
    elif MODE == "body":
        type_condition = " AND type <> 'h'"

    stmt = (
        "SELECT id, content, box, path, type "
        "FROM blocks "
        f"WHERE {where_sql}{type_condition} "
        "ORDER BY updated DESC "
        "LIMIT 200;"
    )

    body = json.dumps({"stmt": stmt}).encode("utf-8")
    url = f"http://{HOST}:{PORT}/api/query/sql"
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"token {TOKEN}",
    }

    req = urllib.request.Request(url, data=body, headers=headers, method="POST")

    try:
        with urllib.request.urlopen(req, timeout=3) as resp:
            raw = resp.read().decode("utf-8", errors="replace")
    except urllib.error.URLError as e:
        print(build_alfred([{
            "title": "无法连接思源 API",
            "subtitle": f"{e}",
            "arg": ""
        }]))
        return

    try:
        data = json.loads(raw)
    except json.JSONDecodeError as e:
        print(build_alfred([{
            "title": "思源返回了非法 JSON",
            "subtitle": f"{e}",
            "arg": ""
        }]))
        return

    if data.get("code") != 0:
        msg = data.get("msg") or "未知错误"
        print(build_alfred([{
            "title": "思源 API 返回错误",
            "subtitle": msg,
            "arg": ""
        }]))
        return

    rows = data.get("data") or []
    if not rows:
        mode_label = {
            "all": "全部内容",
            "title": "仅标题",
            "body": "仅正文块"
        }.get(MODE, "全部内容")

        print(build_alfred([{
            "title": "未找到相关内容",
            "subtitle": f"关键字:{query}({mode_label})",
            "arg": ""
        }]))
        return

    # ==== 强力去重:按 snippet 去重,只保留评分最高的那条 ====
    best_by_snippet = {}  # snippet -> {id, snippet, subtitle, score}

    for row in rows:
        block_id = row.get("id", "")
        if not block_id:
            continue

        content_full = (row.get("content") or "").replace("\n", " ")
        snippet = content_full[:80].strip()
        if not snippet:
            snippet = "(空内容块)"

        box = row.get("box") or ""
        path = row.get("path") or ""
        subtitle = f"{box} / {path}".strip(" /")

        score = fuzzy_score(query, content_full, path)

        prev = best_by_snippet.get(snippet)
        if (prev is None) or (score > prev["score"]):
            best_by_snippet[snippet] = {
                "id": block_id,
                "snippet": snippet,
                "subtitle": subtitle,
                "score": score,
            }

    if not best_by_snippet:
        print(build_alfred([{
            "title": "未找到非重复结果",
            "subtitle": f"关键字:{query}",
            "arg": ""
        }]))
        return

    scored = list(best_by_snippet.values())
    scored.sort(key=lambda x: x["score"], reverse=True)
    top = scored[:20]

    items = []
    for r in top:
        items.append({
            "title": r["snippet"],
            "subtitle": r["subtitle"],
            "arg": r["id"]
        })

    print(build_alfred(items))


if __name__ == "__main__":
    main()


image.png

🔧 Run Script(默认动作:跳转到块)

使用 /bin/zsh

#!/bin/zsh

BLOCK_ID="$1"

if [[ -z "$BLOCK_ID" ]]; then
  exit 0
fi

open "siyuan://blocks/${BLOCK_ID}"

image.png

🔧 Run Script(⌘ + Enter:复制块链接)

#!/bin/zsh

BLOCK_ID="$1"

if [[ -z "$BLOCK_ID" ]]; then
  exit 0
fi

echo "siyuan://blocks/${BLOCK_ID}" | pbcopy

🔧 Run Script(⌥ + Enter:复制块内容)

使用 /usr/bin/python3

##!/usr/bin/python3
# -*- coding: utf-8 -*-

import sys
import json
import urllib.request
import urllib.error
import subprocess

HOST = "127.0.0.1"
PORT = 6806
TOKEN = "使用你自己的API token"


def get_block_content(block_id: str) -> str:
    # 防止 SQL 注入,简单转义单引号
    bid = block_id.replace("'", "''")
    stmt = f"SELECT content FROM blocks WHERE id = '{bid}' LIMIT 1;"

    body = json.dumps({"stmt": stmt}).encode("utf-8")
    url = f"http://{HOST}:{PORT}/api/query/sql"
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"token {TOKEN}",
    }
    req = urllib.request.Request(url, data=body, headers=headers, method="POST")
    try:
        with urllib.request.urlopen(req, timeout=3) as resp:
            raw = resp.read().decode("utf-8", errors="replace")
    except urllib.error.URLError:
        return ""

    try:
        data = json.loads(raw)
    except json.JSONDecodeError:
        return ""

    if data.get("code") != 0:
        return ""

    rows = data.get("data") or []
    if not rows:
        return ""

    return rows[0].get("content") or ""


def main():
    if len(sys.argv) < 2:
        return
    block_id = sys.argv[1].strip()
    if not block_id:
        return

    content = get_block_content(block_id)

    # 复制到剪贴板(macOS)
    try:
        subprocess.run(["pbcopy"], input=content, text=True)
    except Exception:
        pass


if __name__ == "__main__":
    main()

🎯 使用方式

使用关键字 + 搜索词进行搜索(下面是几个例子)

  • sy trait → 搜索所有内容
  • syt ecology → 只搜索标题
  • syb moult → 只搜索正文块

候选出来后:

  • Enter → 跳转到思源对应块
  • ⌘ + Enter → 复制思源块链接
  • ⌥ + Enter → 复制块内容文本

📦 下载链接

也可以下载我制作的工作流包,安装之后需要修改每个脚本的 token

SiYuanSearchId246.alfredworkflow.zip

🙌 结语

使用这个插件可以一键跳到思源对应的位置,大幅加快笔记检索与利用效率。

欢迎大家在此基础上继续改进和分享(如按目录搜索、拼音匹配、标签过滤、自动生成 Anki 卡片等)。由于个人精力有限,本帖主要分享思路,不喜勿喷,不提供“伸手式”定制服务,望理解。

  • 思源笔记

    思源笔记是一款隐私优先的个人知识管理系统,支持完全离线使用,同时也支持端到端加密同步。

    融合块、大纲和双向链接,重构你的思维。

    28444 引用 • 119764 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...