分类: 技术技巧

远程获得APK版本的小脚本

一个很特殊的需求。 有一些上游的 APK 包,这些包都有固定的 url。上游升级 APK 时会覆盖原文件,我需要监控它们的版本号以更新到数据库。 有时候,一些 APK 包会特别大,例如游戏,动辄上 G。所以需要一些办法,不下载整个 APK 来提取信息。

脚本如下:

@echo off

goto :_Preparation
1. Install remotezip:
`pip install remotezip`
2. Download aapt2:
`remotezip https://dl.google.com/dl/android/maven2/com/android/tools/build/aapt2/8.3.1-10880808/aapt2-8.3.1-10880808-windows.jar aapt2.exe`
:_Preparation

set /p name="APK url: "
remotezip "%name%" AndroidManifest.xml 1>nul
zip -0 AM.zip AndroidManifest.xml 1>nul
aapt2 d badging AM.zip 2>nul|grep package| awk -F "'" "{print $6}{print $4}
del AM.zip AndroidManifest.xml

原理很简单,就是通过 remotezip 提取包内的 AndroidManifest.xml,将其打包进空的 zip,最后通过 aapt 解析。

【2024.01】目前好用的大语言模型以及部署情况

写于2024年1月。请注意内容是否过时。

之前写了一篇《一些中文LLM的试用体验》,内容有点过时。结合目前的情况更新一下。

依然以家用环境或免费的网络服务为主,也就是说不会出现超过 40B 的模型。
这里要特别提一下 MoE 模型,以 Mixtral-8x7B 为例,这个模型一次只调用 14B,但加载消耗的硬件与 ~40B 的模型相当。MoE 对个人自用来说是不划算的,只适合作为服务部署。

目前好用的中文模型:

这里尤其特指中文模型,因为中文模型比较少,更有不少是浪得虚名。中英文混杂输出是常见病,有些连句子都输出不通顺,这种东西只能自己试了才知道。

NSFW 篇:

非 NSFW 篇

  • DeepSeek 系列,主要是 deepseek-llm-7b-chat,当然 code 模型也很好用。体验 67B 版本可 前往官网。这个模型挺可爱的,有种早期 Sydeny 的感觉。
  • 书生系列的 internlm-chat-20b,这个模型懂很多俚语。也有 7B 版本,但没试过。

不确定的小模型

  • OpenBuddy-Zephyr-7b,印象中是 NSFW 的,能够流畅输出中文。
  • MiniChat-3B,超小的蒸馏模型,玩具,但也可以输出流畅的中文。

※ 部分模型一直在更新版本,具体可以在使用前翻阅作者的模型列表。

当前自用部署量化的情况:

大部分人用的是 text-generation-webui,但我很少用“懒人包”,因为我需要在服务器部署,然后通过兼容 OpenAI API 的接口给本地使用。

目前主流的量化格式包括 GGUF(llama.cpp)、GPTQ、AWQ、exl2(ExllamaV2)。RWKV 另说。
具体推理速度和硬件需求参考这篇 对比文章
总结:exl2 作为 GPTQ 的改良版非常快,GPTQ 和 AWQ 很接近,但 GPTQ 在处理 prompt 阶段明显更快,这对于超长文本比较重要,GGUF 是最慢的。

所以我推荐的是 GGUF。是的,GGUF 是最慢的,但是推荐。

先说 GGUF。家用硬件性能有限,使用模型都是奔着榨干性能上限去,而量化需要更多的资源,大量现成的 GGUF 就很实用。GGUF 还可以混合 CPU 和 GPU 部署,实在没显卡的话,慢总比不能用好。
GGUF 虽然慢,但也没有非常慢,相对 GPTQ 和 AWQ 并没有量级差。只要输出速度没有拖累阅读速度,就不会感觉到卡顿。
最最最重要的是独立二进制文件,不会存在依赖问题。

GGUF 如果部署为 server,有两种方案。

第一种最简单,用 llama.cpp-python,并且有现成的 cuBLAS wheel

# 以colab环境为例
!pip install llama-cpp-python[server] --prefer-binary --extra-index-url=https://jllllll.github.io/llama-cpp-python-cuBLAS-wheels/AVX2/cu118
!python -m llama_cpp.server --model ./model.gguf --n_gpu_layers 1000 --chat_format chatml --host 0.0.0.0 --port 8000

不过这里有个问题,chat_format 是写死在代码里的,要改需要改源代码,自己重新编译。如果模板里正好有对应的格式,那这个最方便。

第二种是直接从 llama.cpp 编译 server。这里我以 Kaggle 环境为例,因为它的 libcuda.so 位置比较刁钻,需要修改一下源码才能编译。

%cd /kaggle
!git clone https://github.com/ggerganov/llama.cpp
%cd /kaggle/llama.cpp
!sed -i 's| -lcuda | -L/usr/local/nvidia/lib64 -lcuda |g' /kaggle/llama.cpp/Makefile
!make -j server LLAMA_CUBLAS=1

编译完成后的二进制文件可以保存下来,下次就可以直接拿来用了。

server 提供的接口不是 OpenAI 格式的,可以启动项目中的 api_like_OAI.py 兼容。这里可以参考 llama-cpp-oai-like-server。我做了一个不太优雅的兼容,重点是很容易定义聊天格式,直接替代原始的 api_like_OAI.py 使用。

然后说 GPTQ 和 AWQ。比较常见的是用 auto-gptq 和 auto-awq 加载。但是这两个模块经常会受到上游包更新的影响,例如 transformer。一旦出现冲突,就需要等作者更新。
如果经常在 Colab 这种环境跑,每次都是新安装,部署就容易出问题。
此外,GPTQ 要求所有模块使用 GPU 版本,不然只能关闭 Exallma,这会很影响推理速度。

如果一定要用的话,推荐 AWQ+fastllm(不要用 vllm,问题实在太多)
Colab 版本大致如下:

!pip install "fschat[model_worker,webui]"
!pip install auto-awq
!python -m fastchat.serve.controller --host 0.0.0.0 &\
python -m fastchat.serve.model_worker --model-path Author/M-AWQ --host 0.0.0.0 &\
python -m fastchat.serve.openai_api_server --host 0.0.0.0 --port 8000

接着说 exl2。很快,但资源很少,量化是最最最耗时的,一个次量化几小时不算长。除非是决定日常要一直用某个模型,或者要部署成在线服务,否则不太推荐。

最后说 RWKV。RWKV 属于比较奇怪的存在,有两种方法在服务器上部署。

第一种是用 cgisky1980/ai00_rwkv_server。比较有意思的是这个项目用的是 webGPU 而不是 CUDA。
Colab 环境的重点是要安装 libnvidia-gl-XXX 这个包。其他跟随项目介绍就行了。
并不太推荐。一是不能多卡部署,二是报错不太友好有问题难定位,三是 KV Cache 有点问题。

第二种是用 josStorer/RWKV-Runner。要分两步执行,大致如下:

# 启动一个后端
import os
os.environ['RWKV_CUDA_ON'] = '1'
!python ./RWKV-Runner/backend-python/main.py --port 8000
# 通过 API 加载模型
curl http://your.site/switch-model -X POST -H "Content-Type: application/json"\
-d '{"model":"./RWKV-Runner/models/the_model.pth","strategy":"cuda:0 fp16 *20 -> cuda:1 fp16"}'

加载策略(strategy)参考 这个指南。怎么量化就只能怎么加载,想要改只能重新量化。
支持多卡,但多卡推理时会遇到一点问题,输出结果有折扣,暂不清楚原因。

结论就是:RWKV 在多卡方面都会遇到挫折。

总结:

当前非常推荐 GGUF,尽管相比其他量化慢一些,但最不容易随升级出问题。而且在线临时环境部署非常轻松,可以直接使用以前生成的 binary,而不需要每次都安装很多依赖。
未来可能会推荐 exl2,视其生态发展而定。

30秒通过OpenAI格式接口与Gemini Pro对话

① 获得接口链接

新建 Colab 笔记

把以下内容粘贴进去:

!wget -q -c https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -O c
!chmod a+x c
!pip install udocker
!udocker --allow-root pull zhu327/gemini-openai-proxy:latest
!udocker --allow-root create --name=gemini zhu327/gemini-openai-proxy:latest
!./c tunnel --url http://localhost:8443 & udocker --allow-root run -p 8443:8080 gemini

启动,等待 30 秒,获得到接口地址。

示意图:

② 获得 Gemini Pro API Key

从Google Makersuite 申请 Key:点此直达

③Demo


④ 一点补充

  • gemini-openai-proxy 项目提供了兼容接口
  • Colab 不支持安装 Docker,所以用 udocker 作为替代
  • Colab 的 8080 端口默认被占用,所以使用 8443
  • 为了兼容 Kaggle,cloudflared 需要重命名,否则会引起强制重启
  • 随机 url 过于繁琐,最好在 CF 绑定域名

⑤ One more thing

是否支持 NSFW?有限支持。我没有特别写过越狱 prompt,Gemini 并没有拒绝回答。
有其他用户提到模型会拒绝回答,对此,我的经验是伪造 AI 一轮或多轮的回复(当然这需要客户端支持)。

在vscode中使用deepseek coder

运行 server 端,以 colab 为例:

首先用 CF-Tunnel、Ngrok 之类的工具穿透,然后加载模型:

!mkdir -p /root/.tabby
!wget https://github.com/TabbyML/tabby/releases/download/v0.6.0/tabby_x86_64-manylinux2014-cuda117
!chmod +x tabby_x86_64-manylinux2014-cuda117
!TABBY_DISABLE_USAGE_COLLECTION=1 ./tabby_x86_64-manylinux2014-cuda117 serve --model TabbyML/DeepseekCoder-6.7B --device cuda --port 8443
# change the default port due to 8080 is occupied by colab

在 vscode 中安装 Tabby 插件,填入远程地址,即可:

问题:经常会超时,多次后就会拒绝连接。修改 config.toml 似乎无效,不知道什么原因。

与Colab交互的一些姿势

Colab 使用 cell 控制有诸多不便,为了方便交互和监控资源,研究了一下通过终端交互的方式。

管理 Colab

【弃用】方案 1:通过 webshell… 对,就是木马当管理器用。

!apt install php
!nohup php -S localhost:8000 > /dev/null &
!echo '<?php @eval($_POST["shell"]);?>' > connect.php
# 然后用 ngrok(我一开始就是这样修改文件的)

结论:不会被制裁。体验一言难尽,蛋疼。

【弃用】方案 2:使用 tmate

!apt install tmate
!tmate
# 自动分配外网地址

结论:一般不会被制裁。比没有强,像 tmux 那样操作。

【在用】方案 3:使用正经 ssh

!echo $($(echo "pip install colab""_ssh --upgrade"))
from colab_ssh import launch_ssh_cloudflared, init_git_cloudflared
launch_ssh_cloudflared(password="X")

结论:直接安装 colab_ssh 会有警告,虽然能绕过但制不制裁看官方心情。体验很爽。

方案 3 解释

这个方案需要本地下载一个 cloudflared,具体的 vscode 配置可以参考>这篇教程

作者给了一个修改本地 .ssh/config 的方案,实际上也可以在本地端口监听,像这样:
cloudflared.exe access tcp --listener localhost:2222 --hostname sub.trycloudflare.com

这样各种 ssh 客户端,如 Xshell,也可以通过连接 localhost:2222 来访问 colab

更多用法

  1. 快速得到一个 webui 的外部地址,但仅限自己浏览器访问

    from google.colab.output import eval_js
    print(eval_js("google.colab.kernel.proxyPort(8000)"))
  2. 通过 cloudflare tunnel 将任意端口暴露到外网,所有人可访问

    !wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -O cloudflared
    !chmod a+x cloudflared
    import nest_asyncio
    nest_asyncio.apply()
    import subprocess
    f = open("stdout", "w")
    p = subprocess.Popen(['./cloudflared', '--url', 'http://localhost:8000'], bufsize=0, stdout=f, stderr=subprocess.STDOUT)
    import time
    time.sleep(3)
    !grep -F trycloudflare stdout
  3. 防止 colab 断连,console 自动点击 connect。

    var startClickConnect = function startClickConnect(){
    var clickConnect = function clickConnect(){
        console.log("Connnect Clicked - Start");
        document.querySelector("#top-toolbar > colab-connect-button").shadowRoot.querySelector("#connect").click();
        console.log("Connnect Clicked - End"); 
    };
    var intervalId = setInterval(clickConnect, 60000);
    var stopClickConnectHandler = function stopClickConnect() {
        console.log("Connnect Clicked Stopped - Start");
        clearInterval(intervalId);
        console.log("Connnect Clicked Stopped - End");
    };
    return stopClickConnectHandler;
    };
    var stopClickConnect = startClickConnect();
    // In order to stop, call:
    // stopClickConnect();

结论

Colab 虽然限制 ssh,但并不是特别严格,还是可以变相使用。不过,Colab 似乎不支持 websocket,所以诸如 ttydcode-server 等基于网页的终端无法使用。
Kaggle 严格限制 ssh,用了直接封号。不过,Kaggle 支持 websocket,所以可以使用 code-server
以上就是在两者直接使用终端交互的方式。

最后,连接时最好检查一下本地防火墙。如果火绒处于免打扰模式,很可能会自动拦截 ssh 或者 cloudflared

参考

vscode连接Google colab
cloudflare-tunnel-to-colab.ipynb
Quick Tunnels · Cloudflare Zero Trust docs
Arbitrary TCP · Cloudflare Zero Trust docs
How can I prevent Google Colab from disconnecting?

(更新)一个简单的多线程消费者示例

更新:修改了一些容易卡锁的细节

一条简单的笔记。

需求:我有一批获取的 IP,要一一验证可用性。

一个个验证太慢了,需要使用多线程。
ChatGPT 给出的方法是,先用//分割文本,然后每个线程处理相等数量的 IP。但是,这个方法预分配了所有条目,总有线程特别慢,迟迟难以收尾。

之前写了一个简单的多消费者模型。先把文本全部读进列表,增加一个行计数器。每个线程一次只分配一条数据,在取任何数据前,先将计数器+1,代表对应行已被分配。处理完了再分配下一条数据。
不过,代码很奇怪,我决定用 queue 再重写一下。

以下是一个修改后的通用实现,附带一个简单的进度条。

from os import _exit
from time import sleep
from queue import Queue
from threading import Thread
from signal import signal, SIGINT
from dataclasses import dataclass
from time import time, strftime, gmtime

def consume(queue):
    while not queue.empty():
        item = queue.get()

        ''' do something here '''
        sleep(0.1)
        ''' do something here '''

        queue.task_done()

@dataclass
class QProgress:
    ''' a wget-like progress bar '''
    queue: Queue
    qsize: int = 0 # queue.qsize() by default
    width: int = 50 # characters of progress bar

    alive = True

    def interrupt(self):
        self.alive = False

    def show(self):
        data_size = max(self.queue.qsize(), self.qsize)
        start_time, showing_count= time(), 0
        while self.alive and showing_count < data_size:
            showing_count = data_size-self.queue.qsize()
            per = showing_count/data_size
            print(''.join([f"\r{('%.2f'%(per*100)).rjust(6,' ')}%[",
                f"{'='*int(per*self.width-1)}>".ljust(self.width,' '),
                f"] {showing_count}/{data_size} ",
                strftime('%H:%M:%S', gmtime(time()-start_time))]),
                end='', flush=True)

        if self.alive:
            print('', 'Done', sep='\n')

if __name__ == "__main__":

    queue = Queue()

    # 假定一些已生产的数据
    data_size = 6661
    [queue.put(i) for i in range(data_size)]

    # 进度条,启动!
    qprogress= QProgress(queue)
    Thread(target=qprogress.show, daemon=True).start()

    # 消费者们,启动!
    num_consumers = 661

    threads = [Thread(target=consume, args=(queue,), daemon=True)
        for _ in range(num_consumers)]
    [t.start() for t in threads]

    # 配合 daemon 响应 Ctrl+C
    def signal_handler(signal, frame):
        qprogress.interrupt()
        print('', 'Interrupted', sep='\n')
        _exit(0)

    signal(SIGINT,signal_handler)

    while any(t.is_alive() for t in threads):
        sleep(0)

一种基于终端输出的数据传输方式

哦,就是 base64 嘛。


最近 scaleway 服务很不稳定,终有一天它失联了。
根据以前的经验,一般是机器重启,需要去网页 TTY 重置下网络。

登录后,scaleway 问我,新的网络上线,更快更好,要不要一键 auto migrant?
一手贱,然后就双向失联了。具体表现为什么都 ping 不通,没有任何公网地址,连 apt update 也无法运行,通过 scw 分配 IP 也救不活。
此时 sacleway 后台弹出马后炮提示:您的套餐迁移时如果分配了静态 IP,需要先升级 blablabla,否则就会丢失网络连接。怎么救,没说。

这台服务器主要运行了这个博客,尽管文章在本地都有备份,但还是想着先抢救一下数据库。
我使用的是 sqlite 数据库,就一个/wp-content/database/.ht.sqlite文件。

抢救一下

网络彻底失联的情况下,我和这台机器唯一的交互手段就只有后台提供的网页版的 TTY。于是,只能靠屏幕输出了。

具体而言的话:

f=".ht.sqlite";
zip -9 -q f "$f";
base64 -w0 f.zip;echo "";

将输出保存到文件,从另一台机器

base64 -d save.txt > f.zip

进一步压缩

之前是针对空白系统的抢救办法。由于屏幕的输出空间很有限,结合一些工具的话,可以进一步缩减体积。

apt-get install sqlite3 p7zip-full python3-pip
pip install base2048
f=".ht.sqlite";
sqlite3 $f 'VACUUM;';
:<<eof
- or dump to sql -
sqlite3 $f .dump > dump.sql; f="dump.sql"; 
eof
7z a -bso0 -mx -myx -mmt=off -ms=on -mtm=off -mtc=off -mta=off -m0=LZMA:d=384m:fb=273:lc=4 -mmc=1000000000 f $f;
python3 -c "print(__import__('base2048').encode(open('f.7z','rb').read()))";

等待 TTY 狂暴输出。

这块截图大约是 10KB 的数据

其实还有更壮观的 base65536,输出的都是形似汉字的字符,但复制时可能会造成数据错乱。另一方面,base2048 屏幕输出占一个 ASCII 码的宽度,能容纳 2 个 ASCII 码的数据,而 base65536 占 2 个 ASCII 码的宽度,能容纳 3 个 ASCII 码的数据,压缩比提升不大且异常卡顿,实测还是 base2048 好。

恢复数据:将 TTY 打印的内容放入 2048.txt,然后恢复到 2048.7z。
其中明文数据大约是解码数据的 1.8 倍。

import base2048

with open('2048.txt', 'r',encoding='utf-8') as file:
    encoded_data = file.read()

decoded_data = base2048.decode(encoded_data)

with open('2048.7z', 'wb') as output_file:
    output_file.write(decoded_data)

更多可能

终端还可以展示带颜色的文字,用以进一步扩充符号数。如果能够显示图片,则可以结合一些隐写方法,将数据压缩到 RGB 值内。又或者结合屏幕刷新 + 录像,以类似二维码的原理定位和容错,并提供自动化的可能。

这样即使不需要数据线和网络,也能把电脑文件复制到手机上(当然首先要能在宿主上执行程序,听上去像是 007 会干的事)。
这些想法没什么太多应用场景,就只是想想。

给网站加上“原生”的文件直链(踩坑)

最近存在一些文件分享需求,我希望它符合以下要求:

  1. 链接长期有效,即使文件失效再补也能保持原链接
  2. 不要出现过度限速问题
  3. 不要存在我的服务器上,我的磁盘很有限
  4. 不要暴露任何与文件本身无关的信息

一些尝试

首先我尝试把文件存在 Github 的 release 上,因为 release 不限制带宽,也是官方推荐的分发方式。不过,release 的文件链接会包括上传者和 repo 名,我不希望它出现。

Github release 的链接和 raw 不同,会 302 到 objects 子域,无法直接 rewrite。我试图用 CF workers 代理这部分流量,发现在 workers.dev 下可以工作,但绑定自定义域名后就会出现 52x 错误。后来我在 CF workers 社区中找到了相关讨论,这是个历史遗留问题,答案就是 CF 的限制(或 Bug?)
如果再试图用 worker A 通过域名访问 worker B 来绕过,会显示两者无法连通。
回头看目前基于 CF 的 Github 代理,就能发现大家都是通过加一层外部 proxy 绕过这个限制。

也就是说想要自定义域名代理 Github release 文件的话,无论如何都必须引入 CF 外的服务。

换个方案

在服务器上 fetch() 并传递给 CF 自然是可行的,不过事已至此,我决定换 AList 挂载网盘。

最终我选了 mega 网盘。挂载这个网盘只需要用户名和密码,不涉及 token、私有 API、Cookie,不需考虑过期更新的问题,也没有限速问题,非常符合开头提到的要求。

具体而言,我将 down 子域 指向我的 AList 服务器并反代,然后将直链中的 /[d|p]/files/<挂载目录> 通过 Transform Rules 去掉。

根据 CF 的文档,缓存单文件最大 100M,Page Rule 中设置最长可缓存 14 天,除了耗费一点回源流量,基本没什么消耗。

最终效果:https://down.liedown.win/Tinify-mod.zip

一次简单的 webpack 验证探索

前阵子在通过 Fofa 收集信息,发现了 Fofa-hack 项目。

一开始这个项目是登录抓取,但后来 Fofa 改了规则,限制了免费用户的访问总数。
于是,项目改为抓取网页上的链接,一次只能抓 10 条,通过 after / before 语法抓取更多之后的页面。当每页提升到 20 条时,访问里会出现一个 sign 验证参数,一段时间内无人解决。
再后来,项目引入了 webdriver,还有 fuzz 来提升抓取量。

我不太了解前端,在研究了 Fofa 的网页以及查了一些资料后,得知是 webpack。然后稍微逆了一下。与其说是逆向,不如说网站本身也没做复杂的混淆,防君子不防小人吧。

首先,验证参数是 sign,所以搜索 sign:

可以看到,这里跳到了一个 sortFun(),继续追踪这个方法。

可以看到,sortFun() 实际是对 GET 参数的 key 进行 sort,然后拼接一下,最后为字符串 createSign()。

现在,Fofa-hack 已经支持了 sign 计算,并删掉了其他抓取方法。
python 版可以在项目内看到:简单的实现

这个接口一次可以抓取 100 条数据,配合 after / before 语法几乎可以抓全数据。


其他:

  1. 直到完成 PoC,才发现 app_id 是静态的,其实搜索 app_id 也能轻松定位到验证入口。
  2. 全网搜索 app_id 的值,发现已经有人研究过 sign 的算法,不过是用 Go 写的。
  3. 从前人的研究看,似乎还有 page 访问超过 100 页无法抓取的问题,不过我没有触碰到这个上限,具体情况不清楚。
  4. 这个接口除了可以突破网页版单页条目上限,也许还有其他网页不支持的特性,概率比较低,有空可以再看看。

Backblaze CDN 图床

原本是用 Github 作为图床,感觉不太好。第一 Github 本身就不是干这个的,第二 Repo 是全公开的。研究了一下决定切换到 Backblaze。

图片预览:@vreemdear

3 Cuties

以下内容并不是教程,只是一些注意事项和补充。

为了保证足够的缓存,需要设置以下内容:

  1. Backblaze bucket settings 中将 Bucket Info 设为 60 天 {"cache-control":"max-age=5184000"}
  2. 在 Cloudflare Page rules 中将 Cache Level 设为 Cache Everything
  3. 在 Cloudflare Page rules 中将 Edge Cache TTL 设为 7 days

Rewrite:
在 Cloudflare Transform rules 中,
将 Custom filter expression 设为 (not starts_with(http.request.uri.path, "/file/<bucketname>") and http.host eq "<your_custom_domain>")
rewrite to Dynamic 设为concat("/file/<bucketname>",http.request.uri.path)
// 似乎调换顺序也会报错,不清楚原因,可能是因为没来得及缓存。