Blog
Top

Fragments 用于存放不适合单独成文的碎片内容。

内容随心所欲,没有方向限制。本页内容独立于归档文章,单独设置 RSS 全文订阅

10:00 · 23 Mar, 2023

NuxtGolang

用 Golang 和 Nuxt 重写了一个 PHP 软件,到插件系统时懵了,最后花了点时间折腾出来了,这里记录一下思路

  • Golang 同一个包的文件的命名对打包没太大影响(只要不是隐藏文件或者带不打包标记),但同一个包的所有文件会共用变量/函数名称。所以不能重复,要为每个插件准备自己的 namespace 作为变量的前缀或者后缀
    • 准备一个公共的列表变量,以及一个注册函数
      // /plugins/hooks.go
      type PluginInfo struct {
          Name string
          // ...other data or func
      }
      
      var PluginList = make(map[string]PluginInfo)
      func RegisterPlugin(name string, plugin PluginInfo) {
          PluginList[name] = plugin
      }
      
    • 插件文件也放置在 /plugins/,通过 init() 调用 RegisterPlugin() 自动将插件信息注入到 PluginList
      // /plugins/example.go
      func init() {
          RegisterPlugin(ExamplePlugin.Name, ExamplePlugin)
      }
      
      type ExamplePluginType struct {
          PluginInfo
      }
      
      var ExamplePlugin = ExamplePluginType{
          Name: "example1"
          //...
      }
      
    • 为什么还是要通过修改源码再编译打包实现?因为打包成动态库对系统有限制,还有可能遇到还不知道的坑,不如求稳 反正也只有我在写插件,一块分发更方便
  • Nuxt 做前端就没啥好说的,反正最终都会注入到 router ,Nuxt 有个 definePageMeta 可以用来放置页面信息
    <!--/pages/some-page.vue-->
    <script setup>
    definePageMeta({
      plugin_name: 'example1'
      // ...
    })
    </script>
    
    <!--/pages/another-page.vue-->
    <script setup>
    definePageMeta({
      plugin_name: 'example2'
      // ...
    })
    
    const router = useRouter()
    console.log(router.currentRoute.value.meta.plugin_name)// example2
    console.log(router.getRoutes().find(route => route.name === 'some-page')?.meta.plugin_name) // example1
    </script>
    

9:16 · 10 Oct, 2024

OnePlus 8t屏幕竖线

万恶起源是有一天发现屏幕冒了条绿线,手机是21年1月买的,上官网找了一下保外物料的价格:¥1150……还是凑合着继续用好了,然后就这么用了差不多一年。

直到昨晚吃过饭又有点受不了十几条绿线,去搜才发现一年前一加就已经针对这事提供以无修无摔无进水为前提的保外免费换屏,但官方从来都没有公开宣传过(印度那边的换屏政策也是通过媒体宣布的,一加官方对于这政策没有留下任何记录),于是今天爬起来做完备份就直接跑去本地的 OPPO 售后换屏,顺便自费换块电池。

一套流程(检测->拆盖->拆机->换屏/电池->组装->清胶打胶封盖->下载 H2OS 刷机->校准指纹传感器)下来大概耗时 2 小时,前 1 小时修机,后 1 小时刷机,我使用的 PixelExperience 没有一加的工程模式,要校准就只能刷回官方的系统,选择 ColorOS 是不可能的,我又不可能让中国售后给我刷 OxygenOS ……那只能折中一下刷大氢了……

下面是工单节选:

序号收费类型物料名称零售价数量优惠金额理赔金额
1维修电池组件¥119.001¥0.00¥0.00
2维修屏幕上盖组件¥1150.001¥1150.00¥0.00

物料原价合计:¥1269.00,单项优惠金额合计:¥1150.00,理赔金额合计:¥0.00

刷机期间有一台 9R 来送修,一看屏幕又是熟悉的绿线……

再后续是刷氧刷 PE 折腾到两三点:自从网页不再发布完整卡刷包以后,一加发了很多 OTA 完整包,我又没有这些包只能每次都下载安装一步一步更新上去,费了很多时间。恢复备份弄到四五点

希望这玩意能再战三年

  • 注1:免费保外换屏服务可能有手机型号限制
  • 注2:维修前打工单需要查看 IMEI,可能还需要检查购买凭证来确定购买日期
  • 注3:要校准指纹就最好用官方系统(ColorOS、H2OS、OxygenOS),不然可能会因为缺少工程模式而需要刷机,刷机不保资料。不过也可以选择不校准,拼完等胶水干了就可以拿走了
  • 注4:维修期间跟小哥聊天,听说 vivo 也有类似的换屏政策

8:29 · 5 Aug, 2024

树莓派Chromium

折腾某个 DePIN 项目(互联网是一个圈,早年的挂机网赚玩法换个皮又出来了)要用到 Chromium 插件挂机,拿我自己的笔记本挂怕是到最后连电费都交不起……最后翻箱倒柜找出当年的树莓派3B+

系统是 Ubuntu 22.04.4 LTS,装上 KDE 、 xrdp(我主要用 Windows 系统自带的 RDP 客户端,如果是其它系统可以用 VNC 代替) 和 Chromium 挂机就完事了

apt install kde-standard xrdp chromium-browser

这篇应该会在发币后公开(看看能有多少收益 :))

You’ve Successfully Claimed 55.04 GRASS

也太抠了

啊?怎么就四倍了???

3:00 · 3 Aug, 2024

MediaPipeLanguage Detector

虽然可能没什么用,不过既然花了点时间研究就顺便发出来吧

首先我要说的是这玩意没什么用,因为它并不开源,用户只能通过 npm 拿到混淆后的版本,其次文档也等于没有,参数给了也不一定会用到,不论是 官方demo 还是 用户自己实际体验 都跟 官网demo(注:官网需要登录 Google 帐号) 的体验不一致

官方似乎就没考虑过给 Node.js 等非浏览器运行时使用,硬是塞了一个完全没用上的 canvas API 进代码,最后导致其他运行时用不了

所以要用就得改改文件,由于 npm 包内的文件内容会被压缩到一行,所以要修改的部分基本都是在同一行的,需要单独搜索找到并修改

同样是因为被压缩到一行,所以我懒得放出特别大的 patch 包……尤其是在这个一次会加载全部内容的碎片栏目

# node_modules/@mediapipe/tasks-text/wasm/text_wasm_internal.js
## begin
#### note: esm only
+ import fs from "fs";
+ import path from "path";
+ import crypto from "crypto";
+ 
+ import { fileURLToPath } from "url";
+ import { dirname } from "path";
+ const __filename = fileURLToPath(import.meta.url);
+ const __dirname = dirname(__filename);
+ 
+ const nodePath = path;
+ const crypto_module = crypto;

#### ---
#### note: esm only
- var fs=require("fs");var nodePath=require("path");
- var crypto_module=require("crypto");

#### ---
### replace
- if(typeof custom_dbg==="undefined"){function custom_dbg(text){console.warn.apply(console,arguments)}}
+ function custom_dbg(text){console.warn.apply(console,arguments)}
#### ---

## end
### replace
#### note: esm only

- if (typeof exports === 'object' && typeof module === 'object')
-   module.exports = ModuleFactory;
- else if (typeof define === 'function' && define['amd'])
-   define([], () => ModuleFactory);
+ export default ModuleFactory

# node_modules/@mediapipe/tasks-text/text_bundle.mjs
### replace
- void 0!==e?this.h.canvas=e:Ui()?this.h.canvas=new OffscreenCanvas(1,1):(console.warn("OffscreenCanvas not supported and GraphRunner constructor glCanvas parameter is undefined. Creating backup canvas."),this.h.canvas=document.createElement("canvas"))
+ this.h.canvas=null

### replace
- if(e&&await Oi(e),!self.ModuleFactory)throw Error("ModuleFactory not set.");if(n&&(await Oi(n),!self.ModuleFactory))throw Error("ModuleFactory not set.");return self.Module&&r&&((e=self.Module).locateFile=r.locateFile,r.mainScriptUrlOrBlob&&(e.mainScriptUrlOrBlob=r.mainScriptUrlOrBlob)),
#### note: next line is ending with a space " "
+ return

然后就是很不优雅的使用,需要提前将模型下载到本地

import { LanguageDetector,FilesetResolver } from "@mediapipe/tasks-text";

import ModuleFactory from "./node_modules/@mediapipe/tasks-text/wasm/text_wasm_internal.js";

import { readFileSync } from "fs";
globalThis.ModuleFactory = ModuleFactory;
global.self = globalThis;

const buf = readFileSync(`./language_detector.tflite`);

const wasmFileSet = await FilesetResolver.forTextTasks('./node_modules/@mediapipe/tasks-text/wasm')

const languageDetector = await LanguageDetector.createFromOptions(wasmFileSet, {
  baseOptions: {
    modelAssetBuffer: new Uint8Array(buf),
    maxResults: -1
  },
});
const textData = "hello";
const result = languageDetector.detect(textData);

console.log(result);

// Graph successfully started running.
// INFO: Created TensorFlow Lite XNNPACK delegate for CPU.
// WARNING: Attempting to use a delegate that only supports static-sized tensors with a graph that has dynamic-sized tensors (tensor#15 is a dynamic-sized tensor).
// W0714 11:12:40.749000  678624 inference_feedback_manager.cc:121] Feedback manager requires a model with a single signature inference. Disabling support for feedback tensors.
// {
//   languages: [ { languageCode: 'en', probability: 0.5111953616142273 } ]
// }

我也不知道为什么只能返回一种语言,鉴于 官方demo 其实也只会返回一种语言,我也无法可说了

10:27 · 14 Jul, 2024

随想

我一直在用中文写作的原因其实很简单……外语没学好,与其写出非中文和中文使用者都看不懂的奇怪表达,不如用中文写明白写清楚,写到即使机翻也能轻松转换

不过,我还是进行了新的尝试,前两天我上传了一篇拿 GPT-4o 翻译的库存文章,未来可能还会上传这种机翻润色的文章来帮助到非中文的读者。

现在归档页面是全语言的文章混合的,未来可能会按照语言来拆分不同的栏目,并且做好其他仅中文的组件的翻译工作


The following part is translated using GPT-4o.

The reason I've been writing in Chinese all along is quite simple: I haven't mastered foreign languages. Instead of producing strange expressions that neither Chinese nor non-Chinese speakers can understand, it's better to write clearly and intelligibly in Chinese, so that even machine translation can easily convert it.

However, I've tried something new recently. A few days ago, I uploaded a previously written article translated by GPT-4o. In the future, I might upload more machine-translated and polished articles to help non-Chinese readers.

Currently, the archive page mixes articles in all languages. In the future, I might split it into different sections based on language and ensure proper translation for other Chinese-only components.

16:30 · 10 Jul, 2024

sqlitejson

7:20 · 13 Feb, 2024

护照

年初我就想着去更新我那个早在 2018 年就过期的护照,结果一路拖到年末才去,前后10分钟就搞定了,之前在网上看的一大堆“教程”似乎也没什么用

在移民局小程序预约最大的作用是提早填好一大堆琐碎的东西,不必到现场再填,现场并没有多少人,所以不看预约,直接去也是可以的

拍照可以现场用自助机拍照,至于要穿有衣领的衣服什么的古早规定早就不用在意了……

然后就是经典的提交相片回执和身份证,签名,交钱,然后就没了

邮寄 18CNY 每本

旧护照/通行证也不管,不回收也不剪角,所以可以不带

最后战绩:

  • 护照
  • 港澳通行证
  • 台湾通行证(说是没开,那之前看的推友们是怎么办成的?)

后续补充:

移民局小程序查到待取证状态就可以带身份证直接去自助机取了,那张写着要带的回执并没有用上

另外自助机真的很慢很慢……

11:07 · 14 Dec, 2023

前端Material Design

翻 md3 官网时看到一个 figma 插件,于是就改造了一下,现在支持主题色了,分享一下主题色是 #1a6356

9:46 · 28 Nov, 2023

Twitter

Some twitter api endpoints location

North America

  • Atlanta, GA
  • Portland, OR

* I made a mistake, NOT ALL verified accounts send tweets to Atlanta

South America

  • Sao Paulo, BR

Europe

  • London, UK

Asia

  • Tokyo, JP
  • Singapore, SG

Oceania

  • Sydney, SYD

3:16 · 21 Oct, 2023

爬虫golang

最近学习了一番怎么写高并发的爬虫

首先丢掉的是用了很久的 Node.js,类型转过来倒过去时间就被浪费掉了,想了半天我还记得又便于部署的语言,就剩下 golang 了

然后到了各种被各种“教程”写到烂的优化办法,我就不细讲了

  • 尽量不用 https,用也不验证证书(所以要我自己保证连接安全?)
  • 调大 MaxIdleConnsPerHost 和 MaxIdleConns……具体用处好像是 Transport 的连接池保留多少连接 超时肯定要设置的,不然等着卡死吧
  • goroutine + channel,都用 go 了,这个没什么好说的
  • 什么代理和帐号池?常规操作不值一提
  • json.NewDecoder(..).Decode(...) / json.NewEncoder(..).Encode(...) 替代更慢的 json.Marshal() / json.Unmarshal(),同时关掉 json.NewEncoder(..) 的转义 html 选项
  • 少点输出,输出也占时间,而且还不少,在我羸弱的树莓派上面能用几百毫秒

然而还是有个一个奇奇怪怪的接口,它的反爬手法是限制单个 tcp 连接的总请求数…… stackoverflow 的回答说 http.Client 没有这个方法,得自己手操 tcp 连接池(net.Dial()),GPT 只会跟我说要 response.Body.Close(),手操连接池什么的还是饶了我吧……经过几天的现场学习,我暂且算是想到了一个解决办法,丢在这里算是抛砖引玉吧

package main

import (
    "io"
    "log"
    "net/http"
    "time"
)

func initClient() *http.Client {
    var transport = http.DefaultTransport.(*http.Transport).Clone()
    return &http.Client{
        Timeout:   time.Second,
        Transport: transport,
    }
}

var sequence = 0
var client *http.Client
var maxConnection = 100

func main() {
    client = initClient()
    var requestTicker = time.NewTicker(time.Millisecond * 100)
    for {
        select {
        case <-requestTicker.C:
            if sequence%maxConnection == 0 {
                client = initClient()
            }
            go func() {
                req, _ := http.NewRequest("GET", "http://example.com", nil)
                response, err := client.Do(req)
                if err != nil {
                    log.Fatal(err)
                    return
                }
                defer response.Body.Close()
                str, _ := io.ReadAll(response.Body)
                log.Println(string(str[:]))
            }()
            sequence++
        }
    }
}

大概思路就是次数够了就创建一个新的 http.Client(),再用指针 client 指向它,原来的 http.Client() 会继续执行到结束,然后被 GC 回收 <- 不知道 GPT 有没有坑我

8:15 · 15 Oct, 2023

随想Twitter

最近 Twitter 费尽心思在阻止用户和开发者匿名访问推文内容,尽管我在 nitter 的 issue 提出取得访客帐号并签名请求的方案,但在本篇我也提到过要运行这玩意需要大量的访客帐号,所以单打独斗可能不是一个好的办法。

压力大时我就会胡思乱想,想着能不能构建一个分布式的帐号池,由于 Twitter 客户端的请求统一采用 OAuth 签名,这一套方案甚至能在访客帐号彻底失效后继续由人工录入 userbot 的各种信息的方式延续下去。

架构我就参考 ZeroTier,总共分成三级:

  • Planet,这个是中心节点,会保存属于自己的帐号池,通过录入添加的 userbot 在允许共享前也只会存在这里,下一层的节点会跟此节点共享帐号池;Planet 可以自由地允许或者移除下一级别的节点的连接;基于上一点,Planet 需要公网可访问。
  • Node,Node 可以自由地选择连接到一个或多个 Planet,同一 Planet 下的 Node 会共享帐号池,Node 可以理解为一种基础设施,是所有人都能用的
  • Leaf,叶节点专注于获得访客帐号,只能单向将获取到的访客帐号传输给 Node,一般用于没有公网ip的设备,如果设备能在公网访问,其实可以跟 Node 节点合并。

Planet 可以开放 API 供外部调用,发号顺序应该是 本机的访客帐号 > 同步来的访客帐号 > 本机的 userbot 帐号(如果开放)

这只是一个想法,还有很多不完善的地方,暂时想到的:

  • 如何防止”串号“的情况发生?方案似乎没有解决这个问题。
  • 存储方面我在考虑 SQLite,也许会有更好的方案?
  • 通信用什么方式,长连接?WebSocket?还是别的,我不知道。如果一个 Planet 连接着大量的下一级节点,会不会因为这些连接拖慢整个系统的运行效率?
  • 怎么防止恶意请求?如果每位 Planet 控制者都限制他人的连接这还叫不叫“分布式”?
  • 如果将整个架构改成类似 bt 的架构,Planet 改成类似 Tracker,Node 之间共享访客帐号,这时 Twitter 偷偷也建立一个 Node 节点加进来,将所有的帐号都一网打尽时又该如何应对?
  • 访客帐号有效期只有一个月,一个月后怎么同步删除?

当然啦,这只是我的一点想法,目前我还没有时间和精力去完善那些问题,甚至还没新建文件夹。

也许只是又一个在我的胡思乱想下的无用产物。

19:38 · 21 Aug, 2023

前端滚动emoji

在两个月前的某一天,我就想到了我这个 blog 的标题栏的那个 很适合拿来替换成各种各样的元素,于是这个组件的雏形就出来了:

  • 单字。因为 也是单字,这就意味着中间的元素需要尽量方正,否则切换时会让标题反复横跳,于是 emoji 就成了我的首选
  • 扁平化。当时我看了 Android 和 iOS 自带的 emoji,感觉都不够平,于是选了微软的 fluent-emoji-flat,由于微软一向不自绘各种国旗,所以想要国旗的只能自己另找图标了
  • 动画。直接切换太生硬了,加点动画是很有必要的
  • 可筛选时间。目前我写了一套简单的方法来决定什么时候放什么 emoji,以后独立成组件时应该会用相关轮子来提高可玩性

至于怎么引入 svg,我试了好几种,目前的方案是 @iconify-json + @unocss,这并不是最优解,我还需要自己在 uno.config.tssafelist

动画用了点奇技淫巧,本来我想像轮播图那样滚动,js 控制当前要显示的元素,css 控制动画,于是不同步的计时器就会导致动画跑完了图还没切最后一闪而过的问题,最后选择用 position: absolute + display: none 来让所有元素都堆在同一位置,再把进出的动画拆分,当下一个元素显示时当前元素会淡出,最后不透明度变 0,从视觉上消失

19:45 · 15 Aug, 2023

前端生日气球

前段时间心血来潮写 chāo 了个类似 Twitter 生日气球的组件,因为是 vue 组件所以这边也能玩,点卡片右下角的气球标就会放气球,鼠标移上去就可以戳爆了,气球跑完以后再点再放

跟原版有小改动以适应博客环境,未来会加入好玩的组件大包中。

  • 调教水平有限,直接用原版左右飘关键帧得到的效果过于诡异,所以我去掉了
  • 击破统计挡字的可以点一下以清零隐藏

17:27 · 5 Aug, 2023

GitHub Actions

最近用 GitHub Actions 来更新静态资产,由于涉及到爬虫,需要跟第三方网站交互,我感觉用官方实例来跑有可能会像那些跑签到脚本的仓库那样被封,终究还是自托管一个实例。

吐槽几句:

  • 每个实例只能用于一个仓库,不能重复添加……想加个仓库?那就再新建一个文件夹吧……
  • 没有 yarn,首次使用时需要自己装,另外我这台甲骨文的小鸡跑 npm i 直接跑挂了,不知什么问题
  • 上半年 GitHub 泄露了私钥因此最近更换了公钥,我用的其中一个组件没更新 known_hosts ,最后自己换上去就好了,没有用 KNOWN_HOSTS_FILE 是因为文档也没细讲干脆一改了之
  • 后来翻阅 GitHub 附加产品和功能条款,发现 此外,无论操作是否使用自托管运行器,Actions 都不应用于...,使用自托管也还是有风险,今后可能还是会换成 ci/cd + cron 的方案(主要是简单了解一下市面上的那些 ci/cd 占内存都是以 GB 为单位的,而我的小鸡的内存 1GB 都不到

16:12 · 5 Aug, 2023

SequelizeSQLite吐槽

怎么说呢……体验就是很难受:

  • 由于 SQLite 的库不像 MySQL 的那样支持自动将 BIGINT 转成 TEXT,只能自己想办法,找了半天找到一个 CAST(<AA> AS text) AS <AA>,然后就是很苦逼地挨个去拷贝粘贴
      attributes: [
          [Sequelize.options.dialect === 'sqlite' ? Sequelize.literal('CAST(value AS text)') : 'value', 'value'],
      ]
    
  • 没有 MATCH...AGAINSTMATCH 还在研究怎么搞,REGEXP 那个耗时看着就头疼,最后只能回归 LIKE%% 大法
  • 全局大锁直接锁死了,不知为啥 WAL 模式也救不了,写都写不进去;高 I/O 的查询直接歇菜,动不动就是几十秒的耗时

吐槽归吐槽,最后还要继续折腾……

18:50 · 24 Jul, 2023

CloudflareBackblaze B2

一直都有听说 B2 桶,但注册以后用不上只能摆在一边吃灰,最近准备将跑了三年半的 bing 每日图片丢上云,于是这玩意就成了省钱利器。

什么注册账号配置代理这些烂大街的内容就不说了,看 如有乐享的文章 就好。

这里就补充一个,应该是前人写文章时还没出的功能:Redirect Rules。

  • If...
    • All incoming requests
    • Custom filter expression
  • When incoming requests match…
    (http.host eq "b2.example.com" and http.request.uri.query ne "")
  • Then...
    TypeExpressionStatus code
    Dynamicconcat("https://b2.example.com", http.request.uri.path)301
    • Preserve query string

其中那个 b2.example.com 改成自己的 host

这样做主要是防止被有心人无限改查询字符串刷调用次数,毕竟 2500 次动动手指就刷完了。

17:20 · 20 Jul, 2023

AndroidFiddler抓包

签发证书

先把证书导出来得到一个 FiddlerRoot.cer

openssl x509 -inform der -in FiddlerRoot.cer -out fiddler.pem
file1=$(openssl x509 -inform PEM -subject_hash_old -in fiddler.pem | head -1)
cat fiddler.pem > "$file1.0"
openssl x509 -inform PEM -text -in fiddler.pem -out /dev/null >>"$file1.0"
file2=$(openssl x509 -inform PEM -subject_hash -in fiddler.pem | head -1)
cat fiddler.pem > "$file2.0"
openssl x509 -inform PEM -text -in fiddler.pem -out /dev/null >>"$file2.0"

然后将生成的这两个文件拷贝到 /system/etc/security/cacerts/ 目录下,重启设备。

SSL Pinning Bypass

LSPosed + JustTrustMe

4:20 · 7 Jul, 2023

尝试Svelte

拿 Svelte 来重构了一下blog,虽然最后没完成,不过还是挺有意思的,处理 Markdown 的生态一般,有种 戴着镣铐跳舞的感觉

留下了一个本来用来替换掉右下角 NuxtJS 标的玩意。

svelte-logo

9:14 · 30 May, 2023


评论区