BlackGlory / Gloria

🍂 A programmable notifier.

Home Page:https://chrome.google.com/webstore/detail/cnelmenogjgobndnoddckekbojgginbn

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

宣告Gloria项目的死亡

BlackGlory opened this issue · comments

失败的Manifest V3迁移之旅

Gloria是建立在Manifest V2之上的浏览器扩展程序, Manifest V2现已被Manifest V3替代, 最终会失去浏览器支持, 详见Chrome的Manifest V2支持时间表.
在尝试将Gloria从Manifest V2迁移至Manifest V3的过程中, 我们遇到了无法克服的障碍, 这导致迁移无法完成, 项目因此走向终结.

Offscreen Documents + Web Workers

Manifest V3的Service Worker限制了执行动态代码的能力, 因此我们需要通过offscreen document来绕过限制.

在offscreen document里, 存在一个奇妙的例外允许执行动态代码, 尚不确定这是否属于安全漏洞.

借助这一例外, 仍然不足以运行预期中的Gloria脚本, 因为Worker无法导入外部模块(原本使用内置模块gloria-utils的做法因为无法实现对依赖项的版本控制, 遭到废弃).

尝试1:

const script = `import { waitForTimeout } from 'https://esm.sh/@blackglory/wait-for@0.7.4'`
const blob = new Blob([script], { type: 'application/javascript' })
const url = URL.createObjectURL(blob)
new Worker(url, { type: 'module' })
Refused to create a worker from 'https://esm.sh/@blackglory/wait-for@0.7.4' because it violates the following Content Security Policy directive: "script-src 'self'". Note that 'worker-src' was not explicitly set, so 'script-src' is used as a fallback.

Refused to create a worker from 'https://esm.sh/@blackglory/wait-for@0.7.4' because it violates the following Content Security Policy directive: "script-src 'self' 'wasm-unsafe-eval' 'inline-speculation-rules' http://localhost:* http://127.0.0.1:*". Note that 'worker-src' was not explicitly set, so 'script-src' is used as a fallback.

尝试2:

const script = `import('https://esm.sh/@blackglory/wait-for@0.7.4')`
const blob = new Blob([script], { type: 'application/javascript' })
const url = URL.createObjectURL(blob)
new Worker(url, { type: 'module' })
Refused to create a worker from 'https://esm.sh/@blackglory/wait-for@0.7.4' because it violates the following Content Security Policy directive: "script-src 'self'". Note that 'worker-src' was not explicitly set, so 'script-src' is used as a fallback.

Refused to create a worker from 'https://esm.sh/@blackglory/wait-for@0.7.4' because it violates the following Content Security Policy directive: "script-src 'self' 'wasm-unsafe-eval' 'inline-speculation-rules' http://localhost:* http://127.0.0.1:*". Note that 'worker-src' was not explicitly set, so 'script-src' is used as a fallback.

尝试3:

import { javascript } from 'extra-tags'

const script = esm(`import { waitForTimeout } from 'https://esm.sh/@blackglory/wait-for@0.7.4'`)
const blob = new Blob([script], { type: 'application/javascript' })
const url = URL.createObjectURL(blob)
new Worker(url)

function esm(code) {
  return javascript`
    loadESMScript(${code})

    async function loadESMScript(script) {
      const blob = new Blob([script], { type: 'application/javascript' })
      const url = URL.createObjectURL(blob)
      await import(url)
      URL.revokeObjectURL(url)
    }
  `
}
Refused to load the script 'blob:chrome-extension://hjbedkekcmmaclhccpicpjbkbhjniblj/9fa8785b-5e3f-42fd-86df-7b845f443070' because it violates the following Content Security Policy directive: "script-src 'self'". Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback.

loadESMScript @ 321e052c-f1af-4bad-9d77-41d771f9e83e:6
321e052c-f1af-4bad-9d77-41d771f9e83e:6 Refused to load the script 'blob:chrome-extension://hjbedkekcmmaclhccpicpjbkbhjniblj/9fa8785b-5e3f-42fd-86df-7b845f443070' because it violates the following Content Security Policy directive: "script-src 'self' 'wasm-unsafe-eval' 'inline-speculation-rules' http://localhost:* http://127.0.0.1:*". Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback.

尝试4:

import { javascript } from 'extra-tags'

const script = esm(`import('https://esm.sh/@blackglory/wait-for@0.7.4')`)
const blob = new Blob([script], { type: 'application/javascript' })
const url = URL.createObjectURL(blob)
new Worker(url)

function esm(code) {
  return javascript`
    loadESMScript(${code})

    async function loadESMScript(script) {
      const blob = new Blob([script], { type: 'application/javascript' })
      const url = URL.createObjectURL(blob)
      await import(url)
      URL.revokeObjectURL(url)
    }
  `
}
Refused to load the script 'blob:chrome-extension://hjbedkekcmmaclhccpicpjbkbhjniblj/e13f475e-4dbd-4dec-811e-cd417d0a37b5' because it violates the following Content Security Policy directive: "script-src 'self'". Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback.

loadESMScript @ 1b0b39d4-517b-42b2-af32-be06cf3be884:6
1b0b39d4-517b-42b2-af32-be06cf3be884:6 Refused to load the script 'blob:chrome-extension://hjbedkekcmmaclhccpicpjbkbhjniblj/e13f475e-4dbd-4dec-811e-cd417d0a37b5' because it violates the following Content Security Policy directive: "script-src 'self' 'wasm-unsafe-eval' 'inline-speculation-rules' http://localhost:* http://127.0.0.1:*". Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback.

实测表明针对性修改CSP也没用.

尽管我们确实可以在Worker里正常访问外部资源:

fetch('https://blackglory.me')
  .then(res => res.text())
  .then(console.log)

但这只能满足最低限度的Gloria脚本用例.
例如, 你可能会需要JSDOM, 因为你需要DOMParser来解析HTML或XML(原生Web Workers环境里并不存在DOMParser).

总之, 直接在offscreen document里执行动态代码的做法并不怎么靠谱:

  • 导入外部模块的能力受到限制, 无法创建具有外部依赖项的脚本.
  • 相关"特性"处于灰色地带, 随时有可能被"修复", 或者利用相关"特性"的扩展程序会被阻止上架Chrome Web Store.
    特别值得一提的是, Chrome官方认可的用户脚本现在要求扩展程序的用户手动启用开发者模式, 因此不授权就执行用户脚本的做法很可能是违规的.

Offscreen Documents + Iframe + Web Workers

Manifest V3实际上也有正规的执行不安全代码的方法, 即从Manifest V2就有的基于iframe的沙盒.
对于Gloria的用例, 需要在offscreen document里创建和使用基于iframe的沙盒.

最初, 我对此方案很有信心, 毕竟官方已经给出了执行不安全代码的方法, 还能出什么错呢?

尝试1:

fetch('https://blackglory.me')
  .then(res => res.text())
  .then(console.log)
Access to fetch at 'https://blackglory.me/' from origin 'null' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

原因在于iframe的origin是null, 因此不具有扩展程序的跨域能力, 在manifest.json里声明的host_permissions对iframe来说没有任何意义.
理论上, 可以通过为iframe启用allow-same-origin来使其获得与扩展程序相同的origin, 从而获得跨域能力.

尝试2:

const script = `import { waitForTimeout } from 'https://esm.sh/@blackglory/wait-for@0.7.4'`
const blob = new Blob([script], { type: 'application/javascript' })
const url = URL.createObjectURL(blob)
new Worker(url, { type: 'module' })
Refused to cross-origin redirects of the top-level worker script.

尝试3:

const script = `import('https://esm.sh/@blackglory/wait-for@0.7.4')`
const blob = new Blob([script], { type: 'application/javascript' })
const url = URL.createObjectURL(blob)
new Worker(url, { type: 'module' })
Refused to cross-origin redirects of the top-level worker script.

尝试4:

import { javascript } from 'extra-tags'

const script = esm(`import { waitForTimeout } from 'https://esm.sh/@blackglory/wait-for@0.7.4'`)
const blob = new Blob([script], { type: 'application/javascript' })
const url = URL.createObjectURL(blob)
new Worker(url)

function esm(code) {
  return javascript`
    loadESMScript(${code})

    async function loadESMScript(script) {
      const blob = new Blob([script], { type: 'application/javascript' })
      const url = URL.createObjectURL(blob)
      await import(url)
      URL.revokeObjectURL(url)
    }
  `
}

正常运行, 至少我们有一种方式可以导入带有CORS header的外部模块.

尝试在manifest.json里添加allow-same-origin来解决跨域问题:

"content_security_policy": {
  "sandbox": "sandbox allow-scripts allow-same-origin;"
}
Invalid value for 'content_security_policy.sandbox'.

在HTML的iframe的sandbox属性上添加allow-same-origin则会静默失败.

显然, Chrome有意阻止为Sandbox启用allow-same-origin选项.
其中一个原因可能是同时启用allow-scriptsallow-same-origin能让沙盒内的代码逃逸.

至此我们陷入一个奇怪的局面:

  • 在offscreen document里不能导入外部模块, 但能访问任意外部资源.
  • 在iframe里不能访问任意外部资源, 但能导入外部模块(尽管是CORS限定, 但也够用了).

一种解决方案是在offscreen document里向iframe暴露一个API, 使其能够访问任意外部资源.
这意味着对fetch, EventSource, WebSocket这样的Web API进行包装.
此方案的实施难度大, 兼容性差, 其中一些数据类型很可能无法在上下文之间复制或转移, 不可行.

另一种解决方案是通过Manifest V3臭名昭著的DNR为响应添加CORS header, 从而绕过跨域限制.
然而, DNR的过滤条件无法匹配到由扩展程序沙盒发出的来自opaque origin的请求.

理想状态下, 这应该适用于沙盒, 可惜它没有:

chrome.declarativeNetRequest.updateDynamicRules({
  removeRuleIds: [1]
, addRules: [
    {
      id: 1
    , condition: {
        initiatorDomains: [chrome.runtime.id]
      }
    , action: {
        type: chrome.declarativeNetRequest.RuleActionType.MODIFY_HEADERS
      , responseHeaders: [
          {
            operation: chrome.declarativeNetRequest.HeaderOperation.SET
          , header: 'Access-Control-Allow-Origin'
          , value: '*'
          }
        ]
      }
    }
  ]
})

这适用于沙盒, 但影响了浏览器内的所有请求, 引入巨大的安全问题:

chrome.declarativeNetRequest.updateDynamicRules({
  removeRuleIds: [1]
, addRules: [
    {
      id: 1
    , condition: {}
    , action: {
        type: chrome.declarativeNetRequest.RuleActionType.MODIFY_HEADERS
      , responseHeaders: [
          {
            operation: chrome.declarativeNetRequest.HeaderOperation.SET
          , header: 'Access-Control-Allow-Origin'
          , value: '*'
          }
        ]
      }
    }
  ]
})

另一方面, 很难用DNR维持Gloria现有的Cookie, Referer, Origin动态注入能力, 这破坏了Gloria订阅私人消息的用例.

对Gloria的事后验尸

我在Gloria上的大多数技术决策都受到开发Gloria时的时代局限, 在这方面我不认为有做错什么.
在开发Gloria时, JavaScript被CoffeeScript替代, 因此我选择用CoffeeScript的超集LiveScript来开发, ESM支持和ESM CDN不存在, 流行模块标准至少有三个, 大多数MVVM都被Angular带上了双向数据绑定的弯路, TypeScript则根本没几个人使用.
如今JavaScript已经发展到ES2023, 我们有了原生的ESM支持, 有像https://esm.sh这样的ESM CDN,
有React这样成熟的MVVM框架, 并且大多数仍被使用的npm模块要么用TypeScript重写, 要么具有TypeScript类型定义.

现有的Web技术是当年难以想象的, Gloria项目最失败的部分是没有跟上Web技术的步伐,
这都是因为我在开发Gloria时没有采用一个易于维护的架构.
当Gloria的代码逐渐变得陈旧, 任何大的改变都需要以重写的形式来实现时, 项目的发展理所当然地停滞了.
最终, 重写没有到来, 到来的是Manifest V3替代Manifest V2的历史车轮.

这是Gloria原本预定实装的新脚本格式, 对想要开发类似项目的开发者也许会有参考价值:

// -- 此脚本的各种元数据, 语法类似于油猴脚本 --
// @name 脚本显示的名称
// @update-url 脚本的更新地址

// -- 导入外部ESM模块 --

// -- 其他只在创建Worker时运行一次的代码 --

// -- 作为ESM模块的默认项返回, 执行器将会根据返回值类型决定是否采用轮询方式 --
export default function (signal: AbortSignal):
| INotification[]
| PromiseLike<INotification[]>
| Observable<INotification>
| AsyncIterable<INotification>

接下来会发生什么?

  • 随着Manifest V2的生命周期走向终结, 你无法在Chromium浏览器上继续下载、安装、使用Gloria.
    你可以在旧版本的Chromium里继续使用Gloria, 但这注定无法长期维持下去.
    作为用户, 你可以尝试转去使用Gloria的开源替代项目Gloria-X,
    Gloria-X很可能最终会面临与本项目类似的问题, 但也许相关功能可以在Firefox上延续下去.
  • 该项目的源代码存储库会转入归档状态, 仅提供代码下载功能, 直到未来某一天我决定删除它.
    作为开发者, 你可以转去为Gloria的开源替代项目Gloria-X做贡献.
  • https://gloria.pub网站将会下线, 服务器源代码将被删除, 数据库将被删除, 域名将停止续费.
  • 该项目依赖项的源代码存储库, 例如worker-sandboxgloria-sandbox将被删除, 你仍然可以在npm里下载这些依赖项.
    如果你需要在Web Workers里动态定制沙盒环境, 我相信delight-rpc/browser是更好的解决方案.
  • 我会转去尝试开发在浏览器环境以外运行的替代解决方案.
    脱离浏览器环境的解决方案注定不会有Gloria这样的集成度, 我相信它不会适用于绝大多数现有的Gloria用户.

事情还会有转机吗?

一旦我开始开发替代解决方案, 就不再可能会有转机, 因为我不能同时维护复数服务于相似目的的项目.

commented

真是个悲伤的故事,好在我一直在用绿色版的Edge,也通过 https://gloria.pub/ 网站,学到了一些基础编写小任务的知识,就算以后脱离了网站自己也可以编写一些小任务,即使在这个项目的生命周期结束之前,后来的人也能通过 Wayback Machine 项目来查看网页的快照

最后感谢项目的开发者,向你致敬😘

我在替代方案方面已经有了一些想法.
新的方案是一个命名为Hallu的项目, 它本身是一个Git存储库, 用户需要将存储库clone到本地, 针对自己的需要修改配置文件, 然后通过Deno运行时启动它.

config.ts

使用者需要手动编辑存储库根目录下的config.ts文件以实现Cookies获取和通知弹出/转发.
这是暂定的默认实现的样子:

import { config } from '@src/types.ts'
import { Notification } from 'https://deno.land/x/deno_notify@1.4.3/ts/mod.ts'

export default config({
  getCookies() {
    return null
  }
, notify(notifications) {
    notifications.forEach(notification => {
      const instance = new Notification()

      if (notification.title) {
        instance.title(notification.title)
      }

      if (notification.message) {
        instance.body(notification.message)
      }

      instance.show()
    })
  }
})

main.ts

使用者需要手动编辑存储库根目录下的main.ts文件以决定启动哪些脚本, 你可以通过参数来复用用户脚本, 包含一定的配置项.
事实上你可以在这里将被运行的用户脚本包装进Worker, 但这对于大多数用户脚本来说没有必要.

import { start } from '@src/start.ts'
import startup from '@scripts/startup.ts'
import subscribeRSS from '@scripts/subscribe-rss.ts'
import watchPageChanges from '@scripts/watch-page-changes.ts'

start(startup(), {
  once: true
, ignoreInitialCommit: false
, ignoreStartupCommit: false
})

start(subscribeRSS('https://news.ycombinator.com/rss'))

start(watchPageChanges({
  name: 'Hacker News'
, url: 'https://news.ycombinator.com/'
, selector: 'body'
}))

用户脚本示例

启动提醒

// @name Startup notification
import { script, Mode } from '@src/script.ts'

export default script(
  () => ({
    id: Date.now().toString()
  , title: 'Hallu started'
  })
, { mode: Mode.Passthrough }
)

RSS订阅

// @name Subscribe to RSS Feed
import { parseFeed } from 'https://deno.land/x/rss@1.0.0/mod.ts'
import { unescape } from 'https://deno.land/std@0.207.0/html/mod.ts'
import { firstNotNullishOf } from 'https://deno.land/std@0.207.0/collections/mod.ts'
import { script, Mode } from '@src/script.ts'

export default script(
  async (url: string) => {
    const xml = await fetch(url).then(res => res.text())

    const feed = await parseFeed(xml)

    return feed.entries.map(entry => ({
      id: entry.id
    , title: entry.title?.value
        ? unescape(entry.title?.value)
        : undefined
    , message: entry.content?.value
        ? unescape(entry.content.value)
        : undefined
    , url: firstNotNullishOf(entry.links, link => link.href)
    }))
  }
, { mode: Mode.KeepDiff }
)

监视网页变化

// @name Watch page changes
import { DOMParser } from 'https://deno.land/x/deno_dom@v0.1.43/deno-dom-wasm.ts'
import { assert } from 'https://deno.land/std@0.207.0/assert/mod.ts'
import { script, Mode } from '@src/script.ts'

const parser = new DOMParser()

export default script(
  async ({ name, url, selector }: {
    name: string
    url: string
    selector: string
  }) => {
    const html = await fetch(url).then(res => res.text())

    const document = parser.parseFromString(html, 'text/html')
    assert(document)

    const element = document.querySelector(selector)
    assert(element)

    return {
      id: element.outerHTML
    , title: `${name} Changed`
    , url
    }
  }
, { mode: Mode.KeepLatestDiff }
)

使用

# 以开发模式启动项目
deno task dev

# 将项目编译为可执行文件
deno task build

# 启动可执行文件
deno task start

# 试运行用户脚本
deno task test <script-relative-filename>

# 更新用户脚本(基于用户脚本的元数据`@update-url`)
deno task update <script-relative-filename>

# 更新所有用户脚本(基于用户脚本的元数据`@update-url`)
deno task update-all

# 清空存储数据
deno task clean <id>

# 清空所有存储数据
deno task clean-all

image

Hallu的基础功能做完了, 目前的主要障碍是Deno的开发环境太烂, 没有动力写测试, 以及还没有想好该怎么处理Cookies.


Gloria过去的功能请求和未实现功能, 在Hallu里已经被实现的:

  • id字段现在可以是number类型, 主要区别在于可以用Date.now()替代Date.now().toString().
  • 为通知添加了新的字段expires?: number(以毫秒为单位的Unix时间戳)以满足让通知过期的需求.
    具体如何处理过期通知需要由通知器决定.
  • 类似于我的另一个项目eternity, 用户脚本具有元数据@update-url, 从而允许通过网络更新脚本.
  • 通知生成器与通知器现已完全分离, 从而允许高级用户定制通知的表现形式.
  • 可以对每个用户脚本的实例进行单独配置:
    • ignoreInitialCommit: boolean: 是否忽略掉生成的首个commit.
    • ignoreStartupCommit: boolean: 是否忽略掉此次启动后生成的首个commit.
    • interval: number: 重新运行的时间间隔(毫秒).
    • once: boolean: 是否只需要此次启动后运行一次.
    • id: string: 标识名, 用于生成存储路径.
      该属性可以省略, 省略的情况下, 会生成一个临时的标识名, 存储数据会在Hallu退出时被删除.
  • 建立在ESM模块之上的用户脚本格式:
    • 细粒度的依赖项版本控制.
    • commit函数被移除, 以返回值类型Awaitable | Observable | Iterable | AsyncIterable替代.
    • 用户脚本不再根据返回值分为不同类型, 而是需要主动声明自身的模式.
      模式决定了从用户脚本生成的通知提交到notify函数的中间环节:~
      • Passthrough: 保留返回的所有通知, 保留的通知不会被记录.
      • KeepAll: 保留返回的所有通知, 保留的通知会被记录.
      • KeepDiff: 保留返回的通知里不同于已记录通知的通知, 保留的通知会被记录.
      • KeepLatestDiff: 仅当返回的通知跟上一个已记录通知不同时保留, 保留的通知会被记录.
    • TypeScript支持, 尽管你仍然可以写JavaScript, 但非常不建议这么做.
    • 可以通过函数参数实现用户脚本的复用.

今天才发现不知道哪个版本的Chrome又把这个flag加回来了, 光在我记忆里这个flag已经被删除过两次.

image

image

由于我非常嫌弃Windows系统的通知系统, 我还用Electron重新实现过Chrome风格的通知.
如果Chrome能继续把这个flag保留下去, 那么开发专门用于弹出Chrome风格通知的客户端就不必要了, 只要做一个PWA就可以通杀所有支持Chrome的平台.

甚至有两个现成的项目可以改:

chrome插件别下架,我还在用

扩展不会主动下架的.
如果未来发现扩展的商店页打不开了, 那也是CWS把它判定为已淘汰的扩展类型, 将其隐藏掉了.
直到这个项目彻底死亡我估计还有1年的时间.

今天才发现不知道哪个版本的Chrome又把这个flag加回来了, 光在我记忆里这个flag已经被删除过两次.

😝 Chrome 里的 flag 确实经常回旋镖。

替代解决方案Hallu的设计更新

弃用透明fetch的支持

Hallu原计划实现一个透明的fetch供用户脚本使用, 这意味着Hallu有一个自动处理Cookies的内置fetch.
这一计划最终被放弃, 其原因有两个:

  • 只是实现透明的fetch是不够的, 还有像WebSocketEventSource这样的API需要凭据, 凭据也未必会保存成Cookies, 需要实现的东西似乎无穷无尽.
  • 使透明fetch的行为与浏览器的行为保持一致会对项目维护带来隐患.

同时被弃用的还有Hallu里与Cookies相关的配置项, 因为它们现在并不被使用.
如果用户脚本需要获取Cookies, 只需要将getCookies函数声明为参数, 并没有实现一个统一接口的必要.
同理, 如果用户脚本需要设置Cookies, 只需要声明一个setCookies参数.
当用户能够提供getCookiessetCookies时, 通过开源库来实现一个透明fetch包装并不困难,
因此也可以将包装过的fetch作为参数传给用户脚本.

这一决定会削弱用户脚本的统一性, 因为不同来源的用户脚本会要求不同的参数, 但同时也带来了更大的灵活性.

附: 透明fetch包装的实现

要实现一个透明的fetch, 需要准备一个CookieJar, 然后根据CookieJar包装fetch, 在请求时注入Cookie头, 在响应时劫持Set-Cookie头.
JavaScript生态环境中已经有tough-cookie这样被大量使用的CookieJar实现, 所以不需要自行实现CookieJar.

麻烦的部分在于, 若要保持和原生fetch的行为一致, 你需要手动处理重定向等与浏览器同源策略相关的边缘情况, 这往往需要额外编写和持续维护上百行代码.

好在, 总是有一些开源项目可以直接使用:

如何利用浏览器Cookies

当用户脚本需要Cookies时, 用户希望能直接利用浏览器的Cookies.

Hallu的原始设计在利用浏览器Cookies方面非常繁琐, 它考虑额外增加这些项目:

  • 一个用于同步Cookies的浏览器扩展程序.
  • 一个Cookies存储服务, 提供基于HTTP/WebSocket的API供浏览器扩展和Hallu读写.
    这大概率还会导致一个JavaScript库用来封装API, 以及需要将服务设置为开机启动项.

实际上, Firefox用户有一个不错的替代方案, 因为Firefox的Cookies是毫无保护的SQLite数据库,
你可以像处理一般SQLite一样直接读写它.

对于Chromium用户, 可以使用DevTools协议Network.getCookiesNetwork.setCookies方法来获取Cookies和设置Cookies.
尽管这也有一些限制, 但比原始设计还是方便多了.
事实上, 用户脚本也可以更进一步, 直接通过DevTools协议发出请求或打开网页.

config.ts被合并到main.ts

在移除Cookies相关的配置项后, config.ts里只剩下了notify函数这一个配置项.
此时单独保留一个config.ts文件变得不再那么有意义.

于是, 原本config.ts文件里的配置项现在被移动到main.ts里, 并且直接作为start函数的选项存在.
与单独的config.ts相比, 新的设计允许用户为不同的脚本启用不同的配置项.

替代解决方案Hallu的设计更新

Hallu现在是一个库

在上周的设计更新里, 我对Hallu的功能进行了简化.
今天回顾Hallu的设计时, 我发现上周的简化很有启发性.
现在Hallu变得更适合作为一个库, 而不是作为一个Git存储库存在.
既然如此, 那么就让它作为一个库存在, 你不需要学会Git.

删除用户脚本的概念

在原本的设计里, 存在用户脚本的概念, 而这个概念里的"用户脚本"实际上只是一个满足特定接口的函数.
让用户脚本成为用户脚本的, 是它的元数据.

用户脚本的元数据目前为止只有两个字段:

  • @name: 脚本的名称
  • @update-url: 脚本的更新链接

和油猴脚本不同, Hallu的用户脚本并没有权限相关的字段.
这是因为用户脚本直接在本机上运行, 天生具有很高的权限, 在这之上设计任何权限控制相关的字段都显得多余.

脚本的名称并不是真的很有意义, 因为我们实际上没有需要显示脚本名称的地方.
一个关键事实是, 由于用户脚本是可以重用的, 即使需要显示用户脚本的名称, 用户也会想要能够动态生成脚本名称, 而不是硬编码的名称.

唯一有用的元数据是脚本的更新链接, Hallu可以凭此检查用户脚本是否需要更新, 并在需要时完成更新.
然而, 这也是一个可以被放弃的设计, 因为低频率地手动更新少量脚本是很容易的.

如果用户有大量脚本需要更新, 或者需要高频率的更新, 乃至每次运行脚本之前都检查脚本是否需要更新, 更适合通过一个独立的脚本更新CLI程序来实现.
只要约定好格式, 这样的CLI程序不仅可以适用于Hallu用户脚本, 也可以适用于任何形式的Deno脚本, 而包括这样的CLI程序并不是Hallu需要完成的目标.

至此, 每一个需要用户脚本继续存在的理由都已经被消除, 那么用户脚本这一概念也可以被删除了:
只需要写函数就好, 函数是不是写在一个单独的脚本文件里并不重要.

通知的id被重命名为salt

历史上, Gloria通知的id的意义经过修改, 这使得id这个名称变得不符合它实际上代表的东西.
现在, id被重命名为salt, 即:
掺入盐后, 内容相同的通知在去重时会被视作两个不同的通知.

Deduplication Is All You Need

由于Hallu现在并不负责弹出通知, 继续使用通知结构这一约定是没有必要的.
你所需要的只是数据去重——在获取数据后滤出新数据(附带相关的持久化机制)——仅此而已.

原定的Gloria继任项目Hallu现已被Deno包extra-deduplicator替代, 展开上方被标记为过时的评论可以找到这么做的原因.

extra-deduplicator项目是直接在Hallu项目的基础上重写的,
ce96a1a21471fcd703c74b17dc829240364852c4为Hallu的最后一次提交,
之后的提交为extra-deduplicator的代码.

提供类Chrome通知的跨平台桌面应用程序notifier现在也已基本可用, 它基本上使用与Gloria相同的通知结构.

gloria.pub网站现已下线, 域名将于1个月后到期, 期间访问网站将跳转至本页.

这是网站所有任务脚本的数据库dump: gloria.pub scripts.json.