MoreRSS

site iconSunZhongWei | 孙仲维 修改

博客名「大象笔记」,全干程序员一名,曾在金山,DNSPod,腾讯云,常驻烟台。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

SunZhongWei | 孙仲维 的 RSS 预览

Markdown 长文本分割为小段内容,发送给 Github Copilot SDK 逐个处理,规避 context deadline exceeded

2026-03-20 11:03:33

基于 Github Copilot SDK 的免费模型,做了一个中文翻译英文的本地服务(参考前文:博客新功能,中文版自动翻译为英文,基于 Github Copilot SDK 的免费 gpt 5 mini 模型)。但是在处理长文本时,总是不能返回完整的翻译结果,并且报错。

异常报错内容

在处理长文本时,Github Copilot SDK 可能会报错:

waiting for session.idle: context deadline exceeded

并不是不处理,而是返回一定长度的内容后,就没有继续返回了。这种情况通常出现在需要返回较长文本的场景,例如翻译长文、生成长文等。

我不确定是返回的 token 长度有限制,还是请求的总时长有限制,或者是其他原因导致的。在官方的 github issue 里也没有找到相关的讨论。

分割 Markdown 长文本

为了绕过这个问题,我尝试将 Markdown 长文本分割成小段内容,逐个发送给 Github Copilot SDK 处理。这样每次处理的文本长度就不会太长,应该能够避免报错。
至少目前在翻译短标题的场景下,从来没有遇到过这个问题。

因为需要翻译的文本都是我自己写的博客,没有太复杂的结构,就是正常的 markdown 格式。所以,我按照 markdown 的二级标题来进行分割。每当遇到一个二级标题,就认为是一个新的段落,进行一次翻译请求。最后再把翻译结果拼接起来,形成完整的翻译文本。测试了一下,效果不错:

Connected to server. Waiting for tasks...
Received task for Article ID: 1661
Translating Title...
Translating Content in segments...
Translating Content Fragment 1/3...
Translating Content Fragment 2/3...
Translating Content Fragment 3/3...
Result sent successfully.

切割 Markdown 的三方库

目前的基于二级标题的方式确实能解决我自己的问题,但是如果是一个结构更复杂,段落更长的 Markdown 文本,这种方式可能就不太适用了。比如说,如果一个段落下面还有三级标题,或者有很多的列表、代码块等,那么按照二级标题切割可能就不够细粒度了。

找到一个 golang 的 langchaingo 三方库,提供了文本切割的功能,可以按照 markdown 的结构进行切割

https://pkg.go.dev/github.com/tmc/langchaingo/textsplitter

langchaingo 是什么?

Building applications with LLMs through composability, with Go! This is the Go language implementation of LangChain.

为了我这么个简单的需求引入 langchaingo,感觉有点臃肿了,暂时先不考虑了。

人事管理系统填不完的坑,入职员工与 HR 同时编辑简历信息的实现方案

2026-03-19 19:39:55

本来以为开发一套人事管理系统两个月就足够了。没想要真正用起来,天天一堆的问题需要优化。改造起来也异常的费劲,主要是每次修改都需要兼容历史数据,心惊胆战的。好在今天遇到的问题,不是大规模修改组织架构这样的变态需求,而是一个相对独立的功能需求,改起来比较简单。

现存的问题

目前的人事管理系统,包含一个入职员工自己填写个人基本资料的功能,以及 HR 编辑员工信息的功能。
出现了一个严重的 BUG,复现流程:

  1. HR 进入员工信息编辑页面,开始编辑员工信息
  2. 然后,入职员工扫码进入员工端编辑页面,开始编辑个人基本资料
  3. 入职员工提交了个人基本资料,并保存成功
  4. HR 继续编辑员工信息,并提交保存

这时,HR 提交的内容会覆盖掉员工提交的内容,导致员工填写的个人基本资料丢失了 😓 估计入职的大兄弟已经泪流满面,还得重新填写一遍几十个字段的个人信息。。。

需求

其实最好是可以同时编辑,这样比较节省时间,员工在填写个人资料的时候,HR 可以同步编辑岗位信息。

方案

这个可以实现,但是得新增一个功能,就是增加一个独立的岗位信息编辑功能,或者叫非基本信息编辑功能(可能有更好的名字,再想想)。
点击这个按钮之后,可以编辑员工信息中除了基本信息的部分,即,跟员工端编辑的内容完全隔离开。这样就能同时操作了。
如果要保证用户体验,还是要加上一个状态提醒,即,员工在扫二维码编辑基本信息时,在 HR 端提示只能使用岗位信息编辑功能,不能使用原有的全量信息编辑功能。

我还写了一个演示 Demo 页面,来展示这个功能的实现细节,并且把 js 代码也放在了前端的页面里,方便前端同事了解如何实现。

人事系统协同编辑 DEMO

员工端

在进入员工端编辑页面后,浏览器端的 js 每 30 秒钟向服务器发送一次 POST 请求,表示正在编辑基本信息。

后端会提供一个接口,接收员工的编辑状态,并把这个状态存储在内存中(或者 Redis 中)。这个状态可以设置一个过期时间,例如 60 秒,如果超过 60 秒没有收到更新,就认为员工已经不在编辑了。前端调用接口时,提供员工共享编辑接口使用的那个 token,后端会从 token 中解析出员工 ID,来标识哪个员工正在编辑。

HR 端

通过 SSE(Server-Sent Events)接收服务器端推送来的员工编辑状态。
浏览器对同一个域名下的 SSE 连接数有限制(通常是 6 个),所以采用全局的方式来管理 SSE 连接,避免每个员工都建立一个 SSE 连接导致资源浪费。
服务器端会返回一个正在编辑个人资料的员工 ID 列表。
HR 端在收到这个列表后,更新界面上对应员工的编辑状态提示。包括:

  • 员工列表页,现在正在编辑的提示图标,或文字
  • 员工详情页,如果员工正在编辑,提示只能编辑岗位信息,不能编辑基本信息。或者把全部信息保存的按钮自动禁用掉,只保留保存岗位信息的按钮。

SSE 的优点:

  • 自动重连:如果后端服务重启或网络波动,浏览器会自动发起重连,你不需要写任何心跳重连逻辑(WebSocket 必须手动写)。
  • 轻量级:SSE 基于普通的 HTTP 协议,不需要像 WebSocket 那样处理协议升级(Upgrade)。
  • 单向通信:需求只是“员工操作 -> 告知 HR”,这是典型的单向推送。

服务器端的实现

用 golang 来实现这个需求的服务端异常简单,既可以通过共享内存来存储员工的编辑状态,也可以方便的调用 Gin 框架的 SSE 功能来推送消息给前端。
如果用 Python 来搞,估计服务器部署就得折腾半天。

这里不使用 Redis,而直接使用 golang 的 go-cache 来存储员工的编辑状态。因为这个状态不需要持久化,也不需要跨服务器共享,所以 go-cache 就足够了。

import (
	"time"
	"github.com/patrickmn/go-cache"
)

// 创建一个缓存,默认 5 分钟过期,每 10 分钟清理一次过期项
var EditLock = cache.New(5*time.Minute, 10*time.Minute)

// 设置编辑锁
func LockUser(userID string) {
	// 设置该用户正在编辑,有效期 60 秒
	EditLock.Set(userID, true, 60*time.Second)
}

// 检查是否在编辑
func IsUserLocked(userID string) bool {
	_, found := EditLock.Get(userID)
	return found
}

// 返回正在编辑的用户列表
func GetLockedUsers() []string {
	items := EditLock.Items()
	lockedUsers := make([]string, 0, len(items))
	for userID := range items {
		lockedUsers = append(lockedUsers, userID)
	}
	return lockedUsers
}

golang Gin 端 SSE 的实现示例:

func StreamHandler(c *gin.Context) {
    c.Writer.Header().Set("Content-Type", "text/event-stream")
    c.Writer.Header().Set("Cache-Control", "no-cache")
    c.Writer.Header().Set("Connection", "keep-alive")

    // 模拟持续推送,每 2 秒钟检查一次编辑状态并推送给前端
    ticker := time.NewTicker(2 * time.Second)
    defer ticker.Stop()

    for {
        select {
        case <-ticker.C:
            // 检查内存/Redis 中的编辑状态
            isEditing := CheckStatus(c.Query("id"))
            c.SSEvent("message", map[string]interface{}{
                "is_editing": isEditing,
            })
            c.Writer.Flush() // 必须手动 Flush 才能实时发送
        case <-c.Request.Context().Done():
            // HR 关闭了页面,后端停止推送
            return
        }
    }
}

Nginx 配置

如果 Golang Gin 后端部署在 Nginx 反向代理后面,需要确保 Nginx 配置支持 SSE 的长连接。可以在 Nginx 配置中添加以下内容, 否则会出现线上接口收不到任何推送消息的情况:

# 专门为 SSE 接口做的特殊配置
location /api/v1/employee/status-stream {
	proxy_pass http://golang_backend;

	# 核心:必须关闭缓冲,否则消息会被攒在 Nginx 缓存里不发给前端
	proxy_buffering off;
	proxy_cache off;

	# 核心:设置长连接超时(根据业务需求,比如 1 小时或 24 小时)
	proxy_read_timeout 3600s;
	proxy_send_timeout 3600s;

	# 核心:处理 HTTP/1.1 协议及 Connection 头部
	# Nginx 默认会把 Connection 改为 close,这会导致 SSE 无法持续
	proxy_http_version 1.1;
	proxy_set_header Connection "";

	# 修正:告诉浏览器不要缓存这个流
	proxy_set_header Cache-Control "no-cache";

	# 实时转发,不缓冲
	chunked_transfer_encoding off;
}

SSE 对比 WebSocket

我这是第一次使用 SSE 来给前端推送消息,之前都是用 WebSocket 的。
我感觉 SSE 用起来非常方便,省去了很多 WebSocket 需要处理的细节,尤其是自动重连和协议升级方面。对于这种单向推送的场景,SSE 真的是非常合适的选择。
怪不得现在大模型的网页端,都是用 SSE 来实时推送 AI 生成的内容的,完美契合了单向通信的需求。

博客新功能,中文版自动翻译为英文,基于 Github Copilot SDK 的免费 gpt 5 mini 模型

2026-03-18 22:09:46

我感觉中文博客已经没啥希望了,看的人寥寥无几,来自搜索引擎的流量也日渐衰落。而一些编程相关的内容,来自海外的反而多一些,即便是中文版。于是,我就想能否自动把中文的博客翻译为英文。这样也许受众能更大一些。

之前尝试过 免费 AI 大模型 API 接口,Gemini 3 Flash 预览版的 Golang 代理实现 。里面即便是小模型,效果也不错,但是国内服务器访问 Google 是个大麻烦,虽然我通过德国的代理是可以正常访问的,但是,犹豫再三我还是放弃了这个方案。一是因为,Gemini 相关的模型月配额太少,二是不想维护另一套代理服务。

Github Copilot SDK 做翻译服务

参考前文:无限免费大模型 token, Github Copilot CLI SDK 安装及测试。既然 Github Copilot SDK 提供了免费的大模型 gpt 5 mini 和 gpt 4.1,这两个做英文翻译绰绰有余。

我不确定在服务器上部署一套 Copilot CLI 会不会导致封号,因为如果同时多台不同 IP 的机器上使用同一个 copilot pro 账号,总感觉是有风险的。于是,我采用了一个类似 OpenClaw 的方案:

  1. 基于 Github Copilot Golang SDK 的翻译服务,运行在我本地电脑上,即个人笔记本上
  2. 这个笔记本上的翻译服务,通过 websocket 连接我服务器上的博客服务
  3. 当博客里设置了一个中文内容需要翻译时,通过 websocket 自动下发翻译任务
  4. 本地的翻译服务处理完成后,把结果返回服务器。服务器发布新内容即可

效果很不错。效果参考这篇翻译:Setting up OpenClaw and Integrating GitHub Copilot for Unlimited Free Model Tokens

模型的选择

使用 gpt 5 mini 和 gpt 4.1 都可以。但是千万不要使用 gpt 4o,因为实际测试发现 gpt 4o 是占用 Premium requests 额度的。而且每次消耗 1x 。。。比 gemini 3 flash 都贵,服气了。但是,VSCode 里的 copilot 模型列表显示,这个 gpt 4o 是免费的。我说今天消耗了 N 多配额,吓哭。

我在 github issue 里找到一个类似的问题:

https://github.com/github/copilot-sdk/issues/334

Some models are missing from CopilotClient.list_models. The issue indicates that several models (Gemini 3 Flash, Groke Code Fast 1, GPT-4o, and Raptor mini) are not appearing in CopilotClient.list_models()

虽然没说占用 Premium requests 的问题,但是感觉 copilot SDK 有点混乱。

GEO 学习:Bing Webmaster 的 AI Performance 指标 Total Citations 和 Avg. Cited Pages 分别是什么意思

2026-03-18 19:03:23

我发现 Bing Webmaster Tool 中新增了一个 AI Performance 功能。可以显示你的网站内容被 AI 大模型推荐了多少次。但是这个页面没有中文翻译,我有点看不太懂。不了解指标 Total Citations 和 Avg. Cited Pages 分别是什么意思😅。。。

Bing Webmaster 的 AI Performance 指标

我对这两个名词感兴趣是因为,我发现 Clarity 最近也要出类似 AI 统计功能。现在 GEO (Generative Engine Optimization,生成式引擎优化)这么火,还是有必要了解一下。也方便做量化的统计,毕竟现在烟台本地也出现了专门做 GEO 的公司,我们公司就购买了这个服务,还挺贵,且没有量化指标。如果能记录 Bing Webmaster 这个数据监控来判断效果,就不至于被骗。

数据依据

Microsoft Copilots 及其合作服务中的统计数据。虽然国内没法用 Windows 系统的 copilot,但是 bing 搜索很多时候在搜索结果的顶部会给出 AI 建议。估计也涵盖了这部分的展示数据。

此外,要了解这两个指标是什么,首先要知道 citation 这个单词的含义。citation 这个单词我还是第一次见,citation [saɪ’teɪʃnz] 的中文意思是“引用”。需要注意,引文(citation)和参考文献(Reference)的区别:引文(在正文中)对应参考文献(通常放在正文末尾,比如,在参考书目)。

Total Citations 指标

即,总引用次数。指在选定的时间范围内,你的网站内容在 AI 生成的回答中被作为“来源(Source)”或“参考文献”显示的累计总次数。

  • 这是一个曝光量指标。每当 Copilot 或 Bing AI 在回答用户问题时,引用了你网页里的某段信息并在回复中打上链接(脚标或来源列表),就会计入一次。
  • 注意,这不代表点击量(Clicks),而是代表 AI 对你内容的“信任度”和“使用频率”。引用次数越高,说明你的内容越容易被 AI 抓取作为答案的支撑。

Avg. Cited Pages 指标

即,平均被引用页面数。指在选定周期内,平均每天有多少个不重复的网页(Unique URLs)被 AI 引用。

这是一个覆盖面指标。它能帮你判断 AI 是只盯着你家的一两个“爆款”页面薅羊毛,还是觉得你整个网站的很多内容都很有价值。

  • 如果该数值很低: 说明你的 AI 流量来源非常集中,只有少数几个权威页在撑场面。
  • 如果该数值很高: 说明你的网站具有广泛的“主题权威性”,AI 认为你全站很多页面都值得参考。

概况一下

总引用次数关注的是量级;平均被引用页面数关注的是多样性。

Grounding Queries

即,基准查询/支撑查询。在曲线图的底部,还会看到一个 Grounding Queries 的数据列表。里面是一堆关键词及对应的引用次数。

这些关键词并不是用户输入的原始问题,而是 AI 为了生成答案,去搜索引擎里进行“背景调查”时使用的关键词。

Github Copilot CLI 升级到最新版本

2026-03-18 18:34:15

总感觉我使用的 Github Copilot CLI 体验不太好,远不如 OpenCode 舒服。今天在测试使用 CLI 翻译一个文本文件的内容时,处理不但很慢,而且结果也非常不理想(翻译格式有问题,调用本地 PowerShell 命令也要想半天),甚至远远不如在线版本的 DeepSeek。我怀疑是我本地的 Copilot CLI 版本过低的原因。所以想升级一下到最新版本,看看有没有什么改善。

查看当前版本

> copilot.exe version
GitHub Copilot CLI 0.0.418

Update available: 1.0.7
Run 'copilot update' to update, or download from: https://github.com/github/copilot-cli/releases/tag/v1.0.7

可以看到,我当前安装的版本是:0.0.418,而最新版本是: 1.0.7。感觉我的版本过旧了。

升级命令

> copilot.exe update
Checking for updates...

这个提示有误导性,虽然显示是 “Checking for updates…”。但实际上已经开始在后台下载了,从 Windows 系统资源管理器就能看到 copilot 进程已经开始从 github cdn 服务器下载更新了。但是国内下载速度很慢,只有 10k 左右的下载速度。。。耐心等待吧。

升级完成会看到版本号就变成最新的了:

> copilot.exe update
Checking for updates...
Copilot CLI version 1.0.7 downloaded.

> copilot.exe version
GitHub Copilot CLI 1.0.7
You are running the latest version.

注销了中信银行的信用卡

2026-03-15 11:14:52

手里一堆信用卡,招行的/中信的/农行的,现在除了农行有点活动,其他的啥优惠也没有。尤其是招行,要不是服务器续费和 github 续费需要用美金支付,招行我也想注销了。手机 APP 一个比一个臃肿,还占手机空间。

这些银行的 app 客户端越做越花哨,但是一点优惠也没,积分少,也没啥用,还不如直接用支付宝和微信。

注销的主要原因是,卡太多,每个月还款的时间也不一样,非常分散精力。虽然可以调整还款日期,我还是觉得麻烦。干脆不用了。

注销流程

  1. 还清中信信用卡欠款
  2. 确保满足一年 20 次的刷卡次数,防止被扣年费。具体数字忘了,要不一月份就注销了。最近尽量用中信消费,就是为了凑足使用次数。
  3. 各种购物客户端解绑中信卡。除了微信解绑比较简单直接,其他都是各种套路,特别是 pdd。把解绑按钮藏在屏幕外,需要下拉才能看到,一贯的无耻。
  4. 拨打官方电话 40088-95558 按操作提示。参考: https://creditcard.ecitic.com/h5/baike/160112_2063.html

人工也各种套路,非得说 5 天后有人要联系我确认,我说不要确认了,立即注销。果然可以,真无语。都 2026 年了,注销还不能在手机上自助操作,甚至官方客户端里搜索“销卡”没有任何搜索结果,渣渣。

浪费 20 分钟。以后还是控制手里卡的数量。