#884 SSH 转义序列

2023-05-19

Closing a stale SSH connection(关闭过时的 SSH 连接)中介绍了利用 SSH 转义序列来关闭失去响应的 SSH 连接。

也就是说在 SSH 终端输入 ~. 会直接中断 SSH 连接。
经过试验,确实有效(使用 SSH 代理建立的连接就没效)。

所有 SSH 转义序列:

[root@dell ~]# ~?
Supported escape sequences:
 ~.   - terminate connection (and any multiplexed sessions)
 ~B   - send a BREAK to the remote system
 ~C   - open a command line
 ~R   - request rekey
 ~V/v - decrease/increase verbosity (LogLevel)
 ~^Z  - suspend ssh
 ~#   - list forwarded connections
 ~&   - background ssh (when waiting for connections to terminate)
 ~?   - this message
 ~~   - send the escape character by typing it twice
(Note that escapes are only recognized immediately after newline.)

#883 ChatGPT 插件开发

2023-05-05

体验

获得 ChatGPT 4 的资格(购买 Pro)之后,就可以看到左边页面多了一个 Model 选项,选中了 GPT 4
如果 Model 选择 Plugin 那一项,右边又会多出来一个 Plugins 选项


右边的 Plugins 选项一直往下拖,最下面一栏是 Plugin store,点击进入。

上面的插件可以选中体验体验。

安装

可以看到下方有 Install an unverified pluginDevelop your own plugin 两项。

我们开发的插件就是服务器端 API + 相关声明文件,如果就只是放在自己的服务器上,那就算 unverified plugin。
第一选项就是用来安装这样未经验证的插件,可以在 https://www.gptplugins.app/ 找一个试一下。
输入域名,ChatGPT 自动去获取声明文件 https://域名/.well-known/ai-plugin.json

第二项是用来注册插件到 ChatGPT,也可以用来调试本地插件。
如果是注册插件就填域名好了,如果是调试就输入 localhost:3000 这样的地址。
我用局域网 IP,似乎是不行的,可能只支持 localhost 这个主机名。

使用

现阶段最多能够同时勾选三个插件。
聊天过程中,ChatGPT 自动判断是否需要触发插件的使用。

开发

经过我的体验,开发非常简单,除了原本的服务之外,需要的额外工作就两项:清单文件,OpenAPI(如果原本没有的话)。

清单文件:

{
  "schema_version": "v1",
  "name_for_model": "todo",
  "name_for_human": "Todo Plugin",
  "description_for_model": "Simple task management, task description, task date, task completion. Supports adding, deleting, and querying.",
  "description_for_human": "Simple task management.",
  "auth": {
    // 本地测试 Auth Type 必须是 none
    "type": "none"
  },
  "api": {
    "url": "http://localhost:8080/.well-known/openapi.yaml",
    "has_user_authentication": true,
    "type": "openapi"
  },
  "logo_url": "http://localhost:8080/.well-known/logo.png",
  "contact_email": "hello@contact.com",
  "legal_info_url": "hello@legal.com"
}

#882 通过 Redis 实现事件广播

2023-04-30

我一两年前设计的一个通过 Redis ZSet 做事件广播的方案,刚用 Python 写了一个示例代码贴出来。

  1. 这是一个 Push / Pull 方式的广播机制。
  2. 推送方将消息推送到一个 zset key 中,score 为毫秒时间戳。
  3. key 名为 xxx:timestamp//10,也就是精确到 10 秒的时间戳。
    也就是说每 10 秒一个 key,通过 TTL(5 分钟)实现历史数据自动清除,也避免 event 太多导致大 key 的问题。
  4. 拉取方用上一次拉取时间和当前时间做 score range,从最近的三个 zset 中读到这个时间段内的事件。
import logging
import threading
import time

import redis

logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(name)s %(message)s')

redis_host = '127.0.0.1'
redis_port = 6379
redis_db = 1
redis_password = None
redis_prefix = 'broadcast:'
redis_conn = redis.StrictRedis(host=redis_host, port=redis_port, db=redis_db, password=redis_password)


def handle_broadcast(data):
    # 这里是处理收到的广播请求数据的函数
    # 你需要根据具体需求来实现这个函数
    logging.info(f'处理广播请求数据:{data} ===== ===== ===== =====')


def event_broadcast(data):
    now = time.time()
    now_ms = int(now * 1000)
    now_10s = int(now) // 10

    key = redis_prefix + str(now_10s)
    score = now_ms

    pipeline = redis_conn.pipeline()
    pipeline.zadd(key, {data: score})
    pipeline.expire(key, 300)
    pipeline.execute()


# function event_broadcast(data) {
#   const now = Date.now();
#   const now_ms = now;
#   const now_10s = Math.floor(now / 10000);
#
#   const key = redis_prefix + now_10s;
#   const score = now_ms;
#
#   const pipeline = redis_conn.pipeline();
#   pipeline.zadd(key, score, data);
#   pipeline.expire(key, 300);
#   pipeline.exec();
# }

last_score = 0


def event_fetch():
    global last_score

    now = time.time()
    now_ms = int(now * 1000)
    now_10s = int(now) // 10

    keys = [
        redis_prefix + str(now_10s - 2),
        redis_prefix + str(now_10s - 1),
        redis_prefix + str(now_10s),
    ]

    pipeline = redis_conn.pipeline()
    for key in keys:
        logging.info('%s %20s %20s', key, last_score, now_ms)
        pipeline.zrangebyscore(key, last_score, now_ms, withscores=True)

    results = pipeline.execute()
    for data_list in results:
        for data, _ in data_list:
            handle_broadcast(data.decode('utf-8'))

    last_score = now_ms


def broadcast_loop():
    i = 0
    while True:
        i += 1
        data = f'广播请求数据 {i}'
        event_broadcast(data)
        logging.info(f'广播请求:{data}')
        time.sleep(0.5)


def main():
    broadcast_thread = threading.Thread(target=broadcast_loop, daemon=True)
    broadcast_thread.start()
    while True:
        event_fetch()
        time.sleep(5)


main()

#881 Golang 实现简单反向代理

2023-04-27

看到微信公众号 Go语言教程 的文章《golang 实现简单网关》才知道还有 httputil.ReverseProxy 这么个东西。
PS:这玩意儿有什么必要放在标准库?

还挺有趣,在作者示例的基础之上完善了一下,实现多服务,多后端节点的一个负载均衡(还可以再补上权重)。

package main

import (
    "bytes"
    "fmt"
    "io/ioutil"
    "log"
    "math/rand"
    "net/http"
    "net/http/httputil"
    "net/url"
    "strings"
    "time"
)

func main() {
    addr := "127.0.0.1:2002"
    backends := map[string][]string{
        "service1": {"http://127.0.0.1:2003", "http://127.0.0.1:2004"},
    }
    reversePorxy := NewReverseProxy(backends)
    log.Println("Starting httpserver at " + addr)
    log.Fatal(http.ListenAndServe(addr, reversePorxy))
}

func reqConvert(req *http.Request, target *url.URL) {
    req.URL.Scheme = target.Scheme
    req.URL.Host = target.Host
    req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
    if target.RawQuery == "" || req.URL.RawQuery == "" {
        req.URL.RawQuery = target.RawQuery + req.URL.RawQuery
    } else {
        req.URL.RawQuery = target.RawQuery + "&" + req.URL.RawQuery
    }
    if _, ok := req.Header["User-Agent"]; !ok {
        req.Header.Set("User-Agent", "")
    }
    req.Header.Set("X-Real-Ip", strings.Split(req.RemoteAddr, ":")[0])
}

func NewReverseProxy(backends map[string][]string) *httputil.ReverseProxy {
    var targets = make(map[string][]*url.URL)
    for srv, nodes := range backends {
        for _, nodeUrl := range nodes {
            target, _ := url.Parse(nodeUrl)
            targets[srv] = append(targets[srv], target)
        }
    }

    director := func(req *http.Request) {
        segments := strings.SplitN(req.URL.Path, "/", 3)
        if len(segments) != 3 {
            return
        }
        srv := segments[1]
        req.URL.Path = segments[2]
        if _, ok := targets[srv]; !ok {
            log.Printf("unknown path: %s", req.URL.Path)
            return
        }
        rand.Seed(time.Now().UnixNano())
        randomIndex := rand.Intn(len(targets[srv]))
        target := targets[srv][randomIndex]
        reqConvert(req, target)
    }

    modifyFunc := func(res *http.Response) error {
        if res.StatusCode != http.StatusOK {
            oldPayLoad, err := ioutil.ReadAll(res.Body)
            if err != nil {
                return err
            }
            newPayLoad := []byte("hello " + string(oldPayLoad))
            res.Body = ioutil.NopCloser(bytes.NewBuffer(newPayLoad))
            res.ContentLength = int64(len(newPayLoad))
            res.Header.Set("Content-Length", fmt.Sprint(len(newPayLoad)))
        }
        return nil
    }
    return &httputil.ReverseProxy{Director: director, ModifyResponse: modifyFunc}
}

func singleJoiningSlash(a, b string) string {
    aslash := strings.HasSuffix(a, "/")
    bslash := strings.HasPrefix(b, "/")
    switch {
    case aslash && bslash:
        return a + b[1:]
    case !aslash && !bslash:
        return a + "/" + b
    }
    return a + b
}

#880 Git LFS

2023-04-26

开源中国文章 紧跟 AI 步伐, Gitee 已支持 AI 模型托管 中介绍了 Git LFS 的使用。

结合 git lfs help,记录如下:

  1. 需要先 install 一下,启用 LFS 功能
$ git lfs install
Updated Git hooks.
Git LFS initialized.
  1. 追踪大文件
git lfs trace path/to/largefile
# 会记录在 .gitattributes 文件中
# path/to/largefile filter=lfs diff=lfs merge=lfs -text
  1. 推送大文件
# 正常提交
git add .gitattributes path/to/largefile
git commit -m "..."

# 推送
git lfs push --all
git lfs push --all origin
  1. 拉取大文件
git lfs pull --all

# 指定文件
git lfs pull -I <filepath>
  1. git lfs ls-files 列出大文件

#879 国际化与本地化

2023-04-24

做开发有时会需要系统支持在多个地区提供支持,主要是语言,其实还应该包括时区,货币单位等其他信息。

这一套动作叫做国际化和本地化。
国际化是指不要把一些东西写死,是指能够简单灵活改造适应不同地区的需求。
而本地化是指,将这套国际化的产品,针对不同区域做一个适配。
也就是说国际化和本地化是相辅相成的,是一套组合拳。

国际化的英语单词是 internationalization,缩写成 i18n(i 和 n 之间有 18 个字母)。
本地话的英语单词是 localization,缩写成 l10n(l 和 n 之间有 10 个字母)。

明明是一件事,有些地方叫国际化,有些地方写本地化,混乱!我更喜欢全球化 Globalization 这个词。

在如微软及IBM等企业中,则会使用全球化(globalization)来表示此两者的合称。
在英文中,也会使用g11n做为简称。
也有使用缩写 GILT (globalization、internationalization、localization和translation),即“全球化、国际化、本地化和翻译”。

相关工作内容(继续参考维基百科):

  • 语言
  • 电子文件
    • 字母。目前大部分的系统都采用Unicode为标准来解决字符编码。
    • 不同的数字命名系统。
    • 书写方向。譬如德语、英语是从左到右,而波斯语、希伯来语和阿拉伯语是由右到左。
    • 相同语言在不同地区的拼法差异,如美国英语、加拿大英语使用localization,而英国英语和澳洲英语使用localisation。
    • 文件处理上的差异,如某些文字存在大小写,其它则否。字母顺序。
  • 文字的图像表示(打印物、内含在线图片)。
  • 读法(音频)
  • 视频的字幕
  • 文化
  • 图片和颜色:这牵涉到理解和文化适宜的议题。
  • 名字和称谓
  • 政府给定的编码(如美国的社会安全码,英国的National Insurance number,爱沙尼亚的Isikukood及其它各国的身份证号码)和护照
  • 电话号码、地址和国际邮递区号
  • 货币(符号、货币标志的位置)
  • 度量衡
  • 纸张大小
  • 书写习惯
  • 日期跟时间的格式,包含各式日历。
  • 时区(在国际场合会使用世界标准时间)
  • 数字格式(小数点、分隔点的位置、分隔所用的字符)
  • 产品和服务所要面向的法规

只属于本地化的主题有:

  • 翻译
  • 针对特定语言(如东亚语言)的特别支持
  • 符合当地习惯
  • 符合当地的道德观念
  • 针对当地撰写内容
  • 符号
  • 排序方法
  • 美学
  • 当地的文化价值和社会环境

我们绝大多数场景应该只能关注到上面标粗的三个点:翻译,货币,合规。

参考资料与拓展阅读

#877 Web 分词 API

2023-04-21

看了 How to split JavaScript strings into sentences, words or graphemes with "Intl.Segmenter" 了解到,现在 Web 已经支持分词了:

const text = `我爱北京天安门,天安门上太阳升。伟大领袖毛主席,指引我们向前进。`;
const granularities = ["sentence", "word", "grapheme"];
granularities.forEach(function (granularity) {
  // console.log([granularity, index, self])
  let segmenter = new Intl.Segmenter("zh", { granularity: granularity });
  let seg = segmenter.segment(text);
  // console.log(seg) // Segments{}
  let result = Array.from(seg, (s) => s.segment);
  console.log(result);
});
// ['我爱北京天安门,天安门上太阳升。', '伟大领袖毛主席,指引我们向前进。']
// ['我', '爱', '北京', '天安门', ',', '天安门', '上', '太阳', '升', '。', '伟大', '领袖', '毛主席', ',', '指引', '我们', '向', '前进', '。']
// ['我', '爱', '北', '京', '天', '安', '门', ',', '天', '安', '门', '上', '太', '阳', '升', '。', '伟', '大', '领', '袖', '毛', '主', '席', ',', '指', '引', '我', '们', '向', '前', '进', '。']