LinkCat

LinkCat是我在尝试了大部分自建导航服务后都不怎么满意的情况下萌生出的一个项目。LInkCat主要目的就是组织和管理个人网络资源,包括连接、账户、消息、学习记录等。通过一个统一的自建服务、网站来管理自己拥有的所有网络资源。

ps:有人推荐好点的wordpress github插件吗

架构狂草图

LinkCat最核心的概念是注解和拓展。

注解Annotation

resource.origin=blog.oyxf.top;app=wordpress;snapshot=2022/02/27

reource.origin=f:/code/proj/linkcat;folder=true;git.repository=true;

上面两行分别定义了两个资源的简单注解,前者为我的博客网站的网站类型注解,同时表明这是个wordpress app,如果安装了依赖该注解的插件,这个插件就会执行。

下面定义了本地的一个文件夹资源,同时表明了它是的git repository。

这两个例子只是LinkCat要实现的功能中非常简单的两种用途,其他注解还可以考虑后续与浏览器插件结合提供更加多样化的注解能力。

定义

所有的网络资源都被看做一个容纳标签的容器,例如一个网站可以拥有 协议、域名、标题、摘要、上次访问时间等等标签。这些标签由LInkCat注解系统中的插件提供。同时用户可以通过向网络资源添加注解例如:app=github app=portainer app=rancher tech=’WebAssembly’ 来为网络资源添加额外的功能。

功能

LinkCat通过插件来提供注解能力,同时内置一些必要的注解,例如协议类型,资源来源等root注解。

LinkCat将为标签提供必要的搜索和过滤功能,为前端页面更加多样化的显示提供后端基础。

LinkCat所有注解将以Plugin的形式提供,例如现在内置的的ResourceOriginFillerPlugin和MetaRetrieverPlugin插件。这两个插件前者提供资源来源标签,后者提供基础的网页信息标签(通过内置的PuppeteerService从网页提取标题、摘要等信息)。

注解来源

注解由DataProvider类型的插件提供,有关对于资源注解方面的插件部分我命名为DataProviderPlugin(数据提供插件),下文将以数据提供插件或DataProviderPlugin来称呼。

DataProviderPlugin实际上是通过注解构建器的api生成并构建一个注解解析器Annotation.Resolver。

DataProviderPlugin中需要首先确定注解的Scope,即在何种情况下该注解会执行。而后是注解类型(文本、URL等),匹配器类型(默认为全文匹配),注解函数。

资源来源插件 ResourceOriginFillerPlugin

这个插件唯一的用途是将服务器接收的新建资源事件对象的负载中将来源提取出来,并为该负载添加一个 resource.origin=资源来源 标签,同时为 resource.origin 标签添加一个特化的匹配器(PathMatcher),匹配器比较复杂后面有时间再写文章记录。

下面是LinkCat中ResourceOriginFillerPlugin插件源码:

import { Annotation } from "../annotation";
import { Context } from "../context";
import { PathMatcherFactory } from "../matcher";

export class ResourceOriginFillerPlugin {
  constructor(ctx: Context) {
    //将该插件上下文的作用域定义为 * ,即所有资源
    const wild = ctx.on("*");//[^:]+:\/\/[^\/]+
    //协议提取正则
    const protocol = /^(?<protocol>[^:]+):\/\/(?<domain>[^\/]+)(?<path>\/.+)/;
    //注解的builder直接从作用域下生成
    //wild.build() 就生成了一个注解构建器
    //bulder.define()定义了该注解
    //builder.define(注解名称,注解中文,注解类型,注解函数)
    const builder = wild.build();
    builder.define("resource.origin", "来源", Annotation.BaseTypeDefinition.RawText, (ctx, payload) => {
      //payload中包含当前资源创建Session下的所有插件产生的资源
      //该函数为注解函数,在匹配到对应上下文时,LinkCat会自动执行该函数
      let res;
      if ((res = protocol.exec(payload.origin)) != null) {
        const node = res[2].split(".");
        if (node.length == 2)
          node.reverse().push("www");
        //设置payload中对应的注解
        payload.set("resource.origin",`${res[1]}/${node.join("/")}${res[3]}`);
        console.log(payload.annotations["resource.origin"]);
      }
      else {
        //TODO throow Error and Stop
      }
      //设置该注解类型的Matcher
      //此处为resource.origin优化的pathmatcher
    }).matcher(new PathMatcherFactory(0, { resolver: -1, goal: "*", priority: 0, name: "Resource.Origin.Matcher" }))
    //注解的别名,后续考虑增加不同语言适配
    .alias("资源链接");

    //注册注解到系统
    builder.register();
  }
}

这个插件只有一个目的,为资源提供一个 resource.origin 注解。

网页元信息提取插件 MetaRetrieverPlugin

网页元信息提取插件由于我的懒惰还没有完全写好,但是基本逻辑已经建立起来。这个插件将从网页资源上提取标题、图标、摘要,后续考虑加入第三方插件式的网页信息描述库服务(AdBlock?网站评价?)。

//TODO move to @linkcat/utils
type Pair = {[key:string]:string};
export class MetaRetrieverPlugin {
  constructor(ctx: Context, _config: Plugin.Config) {
    //定义该插件的作用域
    //这里使用onPage而不是on,因为onPage是依赖Puppeteer服务模块的一个语法糖,其中实际是在作用域中添加了:$page=true,表明该注解只有在页面服务成功建立时才执行。
    //如果资源是http、https资源才执行,这里的路径是 协议/顶级域名/主域名/次级域名.../剩余路径 构成,这种方式有很多好处,也是域名DNS定位的实际逻辑
    //这里的language()还没有实现,只有个空方法
    //prepare()函数为所有后续注解解析函数前的数据预处理函数,整个注解构建器下的注解解析函数执行前会先将prepare中函数执行。
    //注解解析器中函数执行顺序为 prepare[] => [processor,processor,processor] => finalizer[] 其中define中定义的是processor,finalizer由finalize函数定义
    const scoped = ctx.onPage("resource.origin=:protocol(http|https)/:rest(.*)").language("zh-cn").build().prepare((_ctx, _payload) => {
      console.log("multi-children resolver");
    });
    scoped.define("linkcat.buildin.title", "标题", Annotation.BaseTypeDefinition.RawText, async (ctx, payload) => {
      //由于使用ctx.onPage,所以只有$page服务有效时才会执行该注解解析函数
      //payload.global 包含各种服务为该资源解析Session下提供的各种工具,这里的 $page是页面服务(默认使用Puppeteer页面服务模块)提供的网页内容获取器。
      //$Content为通过document.querySelector()获取特定标签内容,如果无法获取或超时则返回空串。
        payload.set("linkcat.buildin.title",await payload.global.$page.$Content("title"));
    });
    scoped.define("linkcat.buildin.favicon", "图标", Annotation.BaseTypeDefinition.RawText, (ctx, payload) => {
      payload.set("linkcat.buildin.favicon","fakeData");
    });
    scoped.define("linkcat.buildin.excerpt", "摘要", Annotation.BaseTypeDefinition.RawText, (ctx, payload) => {
      payload.set("linkcat.buildin.excerpt","fakeData");
    });
    scoped.define("linkcat.buildin.description", "描述", Annotation.BaseTypeDefinition.RawText, (ctx, payload) => {
      payload.set("linkcat.buildin.description","fakeData");
    });
    //协议正则
    const protocolRegex = /([^/]+):(.+)/;
    const protocol:Pair={
      "http":"linkcat.buildin.protocol.http",
      "https":"linkcat.buildin.protocol.https",
      "file":"linkcat.buildin.protocol.file",
      "ftp":"linkcat.buildin.protocol.ftp",
    }
    //定义协议注解,同时定义该注解值为 linkcat.buildin.protocol.ftp等注解
    scoped.define("linkcat.buildin.protocol", "协议", "linkcat.buildin.protocol.ftp;linkcat.buildin.protocol.http;linkcat.buildin.protocol.https;linkcat.buildin.protocol.file;linkcat.buildin.protocol.unknown", (ctx, payload) => {
      const res = protocolRegex.exec(payload.origin);
      //这里直接填入注解的meta name,LinkCat将自动填入对应注解
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      payload.set("linkcat.buildin.protocol",(res)?(protocol[res[1]] as any)??"linkcat.buildin.protocol.unknown":"linkcat.buildin.protocol.unknown");
    });
    //定义protocol值类型注解,不传入注解解析函数在创建注解是会给true作为值
    scoped.define("linkcat.buildin.protocol.ftp", "文件传输协议", Annotation.BaseTypeDefinition.Boolean);
    scoped.define("linkcat.buildin.protocol.http", "超文本传输协议", Annotation.BaseTypeDefinition.Boolean);
    scoped.define("linkcat.buildin.protocol.https", "安全超文本传输协议", Annotation.BaseTypeDefinition.Boolean);
    scoped.define("linkcat.buildin.protocol.file", "本地文件", Annotation.BaseTypeDefinition.Boolean);
    scoped.define("linkcat.buildin.protocol.unknown","未知协议",Annotation.BaseTypeDefinition.Boolean);
    //注册注解解析器
    scoped.register();
  }
  //还没有实现的Service IoC能力
  deps: string[] = ["annotate", "puppeteer"];
}

拓展Flexible & Extensible


v0.1 wep 第一波更新,介绍基础内容

发表评论

您的电子邮箱地址不会被公开。 必填项已用 * 标注