直接在这里可以查看可用的 models: https://opencode.ai/docs/zen#pricing
Latest Learning
Stuff I learned recently.
git clone 或 git fetch 的时候,git 会在本地创建一些”快照”,记录远程仓库的最新状态,但这些不是真正的分支,当远端的 branch 删除之后,本地还会留着。
解决方法 1
git fetch --prune
解决方法 2
直接配置全局的 gitconfig 即可:
git config --global fetch.prune true 获取远端分支
Fetch the latest changes from the default remote upstream repository (if set)
git fetch
查看所有分支
List all branches (local and remote; the current branch is highlighted by *):
git branch --all
删除远端分支
git push origin --delete <branch_name>
删除本地分支
git branch -d <branch_name> direnv 配合 rbw 平常用起来很好用,还不用担心密钥硬编码到代码中泄露。
export TF_VAR_ntfy_token=$(rbw get ntfy_github_action_token)
export TF_VAR_gmail_token=$(rbw get gmail_github_action_token)
⚠️ 重要注意
我在 macos 下配合 rbw 会有很大延迟 (解锁之后使用 rbw get 有时候会有接近 10s 的延迟),我研究发现是因为 rbw 的获取 ttyname() 有性能问题。
请在 .zshrc 中添加以下环境变量来解决这个问题:
export RBW_TTY=$(tty)
配置后响应时间可缩短到 1 秒内。
一个 curl 请求的例子:
curl -X POST 'https://api.github.com/repos/zhaochunqi/til-pages/dispatches' \
--header 'Content-Type: application/json' \
--header 'Accept: application/vnd.github.v3+json' \
--header 'Authorization: Bearer github_pat_xxxxx' \
--header 'X-GitHub-Api-Version: 2022-11-28' \
--data '{"event_type": "til-updated"}'
其中 github_pat_xxxxx 是 github 的 token,可以在 github token 页面生成。使用 fine-grained token, token 权限需要有 Contents: Read and write.
til-updated 是 github action 的事件类型,可以在 github action 的配置文件中定义。
on:
repository_dispatch:
types: [til-updated]
这样就可以通过 curl 请求触发 github action 了。
使用命令 source_env 可以在当前目录的 .envrc 文件中引用其他目录的 .envrc 文件。
使用具体情况可参考:
➜ tree -al A
drwxr-xr-x@ - zhaochunqi 14 Dec 15:02 A
.rw-r--r--@ 56 zhaochunqi 14 Dec 15:02 ├── .envrc
drwxr-xr-x@ - zhaochunqi 14 Dec 15:02 └── B
.rw-r--r--@ 57 zhaochunqi 14 Dec 15:02 └── .envrc
A/.envrc 文件内容如下:
export A="this is A"
export B="this is B from A folder"
A/B/.enrc 文件内容如下:
source_env ../.envrc
export B="This is B from B folder"
在 A 目录下执行 direnv allow 后,进入 B 目录,执行 direnv allow 后,可以看到如下输出:
Desktop/A/B
➜ echo $A
this is A
Desktop/A/B
➜ echo $B
This is B from B folder
可以看到,在 B 目录下,可以访问到 A 目录下的环境变量,并且 B 目录下的环境变量覆盖了 A 目录下的环境变量。这正是我们想要的。
使用 https://github.com/marketplace/actions/create-pull-request 的时候遇到无法创建 pr 的问题,需要做如下配置:
1. 检查仓库设置
请访问:如:https://github.com/zhaochunqi/dns/settings/actions (替换成你自己的)
找到 “Workflow permissions” 部分,确保:
- ✅ 选择 “Read and write permissions”
- ✅ 勾选 “Allow GitHub Actions to create and approve pull requests”
2. workflow 中添加
permissions:
contents: write
pull-requests: write - 需要一个 Discord 的 Webhook 地址,可以通过在 Discord 的频道中创建一个 Webhook 来获取。如:
https://discord.com/api/webhooks/1111111111111/xxxxxxxxxxxxxxxxxxxxxxxxxxxx - 需要安装 webhook 插件,在 Jellyfin 的插件商店中搜索
webhook即可找到。
配置方法:
勾选你要的 play 的一些事件,然后填写你的 Webhook 地址,然后在 template 处添加:
{
"content": "{{MentionType}}",
"avatar_url": "{{ServerUrl}}/Users/{{UserId}}/Images/Primary",
"username": "{{NotificationUsername}}",
"embeds": [
{
"author": {
{{#if_equals ItemType 'Episode'}}
"name": "Playback Started • {{{SeriesName}}} S{{SeasonNumber00}}E{{EpisodeNumber00}} ~ {{{Name}}}",
{{else}}
"name": "Playback Started • {{{Name}}} ({{Year}})",
{{/if_equals}}
"url": "{{ServerUrl}}/web/#/details?id={{ItemId}}&serverId={{ServerId}}"
},
"thumbnail":{
"url": "{{ServerUrl}}/Items/{{ItemId}}/Images/Primary"
},
"description": "> {{{Overview}}}\n\n``[{{PlaybackPosition}}/{{RunTime}}]``",
"color": "3394611",
"footer": {
"text": "{{{ServerName}}}",
"icon_url": "{{AvatarUrl}}"
},
"timestamp": "{{Timestamp}}"
}
]
}
随便播放一个视频,你可以看到 Discord 频道中有消息了。如果没有成功,记得到 Jellyfin 的日志中查看错误信息。(不要随便勾选一些事件或者忽略 template 之类的选项)
使用 hyperfine 来运行,参考:https://github.com/chinanf-boy/hyperfine-zh#%E7%94%A8%E6%B3%95
比如:hyperfine 'sleep 0.3', 运行结果如下
[nix-shell:~/ghq/github.com/zhaochunqi/til]$ hyperfine 'sleep 0.3'
Benchmark 1: sleep 0.3
Time (mean ± σ): 319.5 ms ± 4.8 ms [User: 6.8 ms, System: 3.8 ms]
Range (min … max): 311.0 ms … 326.1 ms 10 runs Traefik 底层使用的是 lego 库来处理 ACME (Let’s Encrypt) 协议。在标准的 DNS-01 验证流程中,Traefik 会严格遵循 “创建 -> 验证 -> 清理” 的生命周期。所以如果有 _acme-challenge 之类的 dns 记录存在,除非正在签发过程中,否则是可以删除掉的。
签发流程:LIVE EDITOR
flowchart TD
Start["开始 DNS-01 挑战"]
Start --> GetInfo["获取挑战信息<br/>GetChallengeInfo()"]
GetInfo --> CNAMECheck{"检查 CNAME"}
CNAMECheck -->|有 CNAME| FollowCNAME["跟随 CNAME 链<br/>getChallengeFQDN()"]
CNAMECheck -->|无 CNAME| UseFQDN["使用原始 FQDN"]
FollowCNAME --> EffectiveFQDN["获得 EffectiveFQDN"]
UseFQDN --> EffectiveFQDN
EffectiveFQDN --> Present["创建 TXT 记录<br/>Present()"]
Present --> ProviderCheck{"DNS 提供商类型"}
ProviderCheck -->|标准提供商| StandardProvider["标准 DNS 提供商<br/>直接创建 TXT 记录"]
ProviderCheck -->|ACME-DNS| ACMEDNSProvider["ACME-DNS 提供商"]
ACMEDNSProvider --> AccountCheck{"检查账户"}
AccountCheck -->|账户存在| UpdateTXT["更新 TXT 记录"]
AccountCheck -->|账户不存在| CreateAccount["创建新账户"]
CreateAccount --> CNAMERequired["返回 ErrCNAMERequired<br/>需要手动创建 CNAME"]
CNAMERequired --> ManualCNAME["手动创建 CNAME 记录"]
ManualCNAME --> UpdateTXT
StandardProvider --> Propagation["等待 DNS 传播"]
UpdateTXT --> Propagation
Propagation --> Polling["轮询检查<br/>wait.For()"]
Polling --> Validate{"验证成功?"}
Validate -->|是| CertIssued["证书签发成功"]
Validate -->|否| Timeout{"超时?"}
Timeout -->|是| Error["签发失败"]
Timeout -->|否| Polling
CertIssued --> Cleanup["清理 DNS 记录<br/>CleanUp()"]
Error --> Cleanup
Cleanup --> DeleteTXT["删除 TXT 记录"]
DeleteTXT --> End["流程结束"]
%% 样式
classDef default fill:#f9f9f9,stroke:#333,stroke-width:2px
classDef process fill:#e1f5fe,stroke:#01579b,stroke-width:2px
classDef decision fill:#fff3e0,stroke:#e65100,stroke-width:2px
classDef error fill:#ffebee,stroke:#c62828,stroke-width:2px
classDef success fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px
class GetInfo,FollowCNAME,UseFQDN,EffectiveFQDN,Present,StandardProvider,UpdateTXT,Propagation,Polling,Cleanup,DeleteTXT process
class CNAMECheck,ProviderCheck,AccountCheck,Validate,Timeout decision
class CNAMERequired,ManualCNAME,Error error
class CertIssued,End success # 删除所有 7 天前的旧世代(Generations)
sudo nix-collect-garbage --delete-older-than 7d flakes 的设计初衷是“重现性”(Reproducibility),而不是“实时性”。
获取最新的分支构建信息
# 只更新 nixpkgs 这个 input
nix flake update nixpkgs
# 或者更新所有 inputs
nix flake update
注意,建议更新所有 inputs,不然可能会出现 nixpkgs 和 home-manager 版本不一致的问题
在 nixos 配置中添加:
# Setup Clash
programs.clash-verge = {
enable = true;
serviceMode = true;
tunMode = true;
};
开启了 tunMode 和 serviceMode
当我在 cronjob 中设置:LOGSEQ_FOLDER=$HOME/logseq 时 (这个语法会被 cronjob 设置为环境变量),在后续的命令调用 LOGSEQ_FOLDER 这个变量的时候,并不能正确的获取到 $HOME 变量。但是我们可以尝试在 SHELL 中运行命令export LOGSEQ_FOLDER=$HOME/logseq,然后从环境变量中查找 env|grep LOGSEQ_FOLDER 得到结果是:/home/alex/logseq, 为什么?
因为 cronjob 中不会对环境变量的值 $HOME 二次展开,但是 shell 中,是直接展开了的!
- cronjob 中,
LOGSEQ_FOLDER=$HOME/logseq表示的是一个键值对,原样放入子进程的模块的环境变量中。 - shell 中,
LOGSEQ_FOLDER=$HOME/logseq表示的是一个表达式,会先计算,再展开,所以你 export 后的环境变量就已经是展开过后的了。
在 windows 下的手机连接有时候会报错:“Microsoft 手机连接 - 无法连接到你的 Android 设备,因为你正在尝试访问中国以外的应用程序,目前我们不支持漫游区域。”
只需要添加对应的 “dcg.microsoft.com” 为直连即可。
以 Bitwarden 为例:
确定软件已安装
ls -la /Applications/ | grep -i bitwarden
使用 SPCTL 查看该应用的信息
spctl -a -t exec -vvv /Applications/Bitwarden.app
输出如下:
/Applications/Bitwarden.app: accepted
source=Notarized Developer ID
origin=Developer ID Application: 8bit Solutions LLC (LTZ2PFU5D6)
如果 source 是 Mac App Store, 那就是通过 app store 下载的,反之如果 source 是 Notarized Developer ID, 那就是通过 DMG 下载的。
这个命令的详细解析
这个命令是 macOS 系统中用于 Gatekeeper(门禁)安全机制的 spctl 工具的一个用法。具体解释如下:
spctl -a -t exec -vvv /Applications/Bitwarden.app
各部分含义:
-
spctl
是 macOS 自带的命令行工具,全称是 System Policy Control,用于管理系统策略,尤其是 Gatekeeper 对应用程序的验证和授权。 -
-a(或--assess)
表示“评估”(assess)指定的项目,即检查该应用是否被系统策略允许运行。 -
-t exec(或--type execute)
指定评估类型为“可执行”(executable),即检查该应用是否可以被当作可执行程序运行。这是针对应用程序(.app)的标准类型。 -
-vvv
表示输出详细(verbose)信息,v越多,输出越详细。-vvv是非常详细的输出,会显示签名信息、证书链、权限来源等。 -
/Applications/Bitwarden.app
要评估的目标应用程序路径,这里是安装在应用程序文件夹中的 Bitwarden 密码管理器。
这个命令的作用:
检查 Bitwarden.app 是否通过了 macOS 的 Gatekeeper 验证,是否被允许运行。
运行后,你会看到类似这样的输出(取决于实际情况):
- 如果应用已正确签名且被信任:
/Applications/Bitwarden.app: accepted source=Apple Notarized Developer ID origin=Developer ID Application: Bitwarden Inc. (XXXXX)
在 /etc/nixos/configuration 中这样配置即可:
services.interception-tools =
let
itools = pkgs.interception-tools;
itools-caps = pkgs.interception-tools-plugins.caps2esc;
in
{
enable = true;
plugins = [ itools-caps ];
# requires explicit paths: https://github.com/NixOS/nixpkgs/issues/126681
udevmonConfig = pkgs.lib.mkDefault ''
- JOB: "${itools}/bin/intercept -g $DEVNODE | ${itools-caps}/bin/caps2esc -m 1 | ${itools}/bin/uinput -d $DEVNODE"
DEVICE:
EVENTS:
EV_KEY: [KEY_CAPSLOCK, KEY_ESC]
'';
};
参考连接:https://discourse.nixos.org/t/best-way-to-remap-caps-lock-to-esc-with-wayland/39707/6
logseq 中 api 返回的 uuid 在真实的 block 中未必会有,需要检查是否有 id 这个属性才能够正确获取到:
def ensure_uuid_property(
self, blocks: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
updated_blocks = []
for block in blocks:
if "properties" not in block or "id" not in block["properties"]:
# Logseq block UUID is typically in block['uuid']
block_uuid = block.get("uuid")
if block_uuid:
print(f"Ensuring uuid property for block: {block_uuid}")
self.upsert_block_property(block_uuid, "id", block_uuid)
# Update the block in memory for consistency
if "properties" not in block:
block["properties"] = {}
block["properties"]["id"] = block_uuid
updated_blocks.append(block)
return updated_blocks
使用本地 embddings 遇到如下问题:
qwen3_embdings = OpenAIEmbeddings(
model="text-embedding-qwen3-embedding-0.6b",
base_url="http://127.0.0.1:1234/v1",
)
运行时报错:
Traceback (most recent call last):
File "/Users/zhaochunqi/ghq/github.com/zhaochunqi/ai-agents-learning/main.py", line 20, in <module>
_ = vector_store.add_documents(documents=all_splits)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/zhaochunqi/ghq/github.com/zhaochunqi/ai-agents-learning/.venv/lib/python3.12/site-packages/langchain_core/vectorstores/in_memory.py", line 195, in add_documents
vectors = self.embedding.embed_documents(texts)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/zhaochunqi/ghq/github.com/zhaochunqi/ai-agents-learning/.venv/lib/python3.12/site-packages/langchain_openai/embeddings/base.py", line 590, in embed_documents
return self._get_len_safe_embeddings(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/zhaochunqi/ghq/github.com/zhaochunqi/ai-agents-learning/.venv/lib/python3.12/site-packages/langchain_openai/embeddings/base.py", line 480, in _get_len_safe_embeddings
response = self.client.create(
^^^^^^^^^^^^^^^^^^^
File "/Users/zhaochunqi/ghq/github.com/zhaochunqi/ai-agents-learning/.venv/lib/python3.12/site-packages/openai/resources/embeddings.py", line 132, in create
return self._post(
^^^^^^^^^^^
File "/Users/zhaochunqi/ghq/github.com/zhaochunqi/ai-agents-learning/.venv/lib/python3.12/site-packages/openai/_base_client.py", line 1259, in post
return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/zhaochunqi/ghq/github.com/zhaochunqi/ai-agents-learning/.venv/lib/python3.12/site-packages/openai/_base_client.py", line 1047, in request
raise self._make_status_error_from_response(err.response) from None
openai.BadRequestError: Error code: 400 - {'error': "'input' field must be a string or an array of strings"}
添加 check_embedding_ctx_length 参数即可
qwen3_embdings = OpenAIEmbeddings(
model="text-embedding-qwen3-embedding-0.6b",
base_url="http://127.0.0.1:1234/v1",
check_embedding_ctx_length=False, # 关键:禁用长度检查以兼容 LM Studio
)
原因是这个参数默认 True, LangChain 会检查输入文本是否超过模型的最大上下文长度,如果文本太长,会被分割成多个 chunk,这样向 api 发送的是 token 数组,不是原始的字符串。
具体的执行流程差异:
默认情况 (True) - 会出错的流程:
# 输入:"Hello world"
# 1. tiktoken 处理:[15496, 1917] (token IDs)
# 2. API 请求:{"input": [15496, 1917], "model": "..."}
# 3. LM Studio 收到整数数组,报错:"'input' field must be a string"
设为 False 后 - 正常的流程:
# 输入:"Hello world"
# 1. 直接发送:{"input": "Hello world", "model": "..."}
# 2. LM Studio 收到字符串,正常处理 在学习 ai agent 的过程中,我使用一个本地模型尝试完成 langchain 的初始教程。
from langchain.agents import create_agent
def get_weather(city: str) -> str:
"""Get weather for a given city."""
return f"It's always sunny in {city}!"
agent = create_agent(
model="anthropic:claude-sonnet-4-5",
tools=[get_weather],
system_prompt="You are a helpful assistant",
)
# Run the agent
agent.invoke(
{"messages": [{"role": "user", "content": "what is the weather in sf"}]}
)
这本该是一个快乐而轻松的 hello world,没想到,我却在这里折戟了。我的 agent 无法正确返回信息。(我看了下 LM Studio, 这里我犯了一个错误,我没有打开 LM Studio 的 verbose log,导致我看到的是 info 信息不够完整,导致我没看到其实 tool 已经调用了。不过没关系,我通过 response 正确的获取到了返回值。
我的代码如下
from langchain.agents import create_agent
from langchain.agents.structured_output import ResponseFormat
from langchain_openai import ChatOpenAI
def get_weather(city: str) -> str:
"""Get weather for a given city."""
return f"It's always sunny in {city}!"
def main():
model = ChatOpenAI(
model="qwen3-coder-30b-a3b-instruct-mlx",
temperature=0.5,
base_url="http://127.0.0.1:1234/v1",
)
agent = create_agent(
model,
tools=[get_weather],
system_prompt="You are a helpful assistant",
)
response = agent.invoke(
{
"messages": [
{"role": "user", "content": "What's the weather like in New York?"}
]
}
)
print(response)
if __name__ == "__main__":
main()
得到的返回值 response:
{'messages': [HumanMessage(content="What's the weather like in New York?", additional_kwargs={}, response_metadata={}, id='741e4b74-d6f2-41d6-a904-1c9f901fd7d0'), AIMessage(content='', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 23, 'prompt_tokens': 270, 'total_tokens': 293, 'completion_tokens_details': None, 'prompt_tokens_details': None}, 'model_provider': 'openai', 'model_name': 'qwen3-coder-30b-a3b-instruct-mlx', 'system_fingerprint': 'qwen3-coder-30b-a3b-instruct-mlx', 'id': 'chatcmpl-4zbt95pav7gccu2ej4r9wq', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--092111ee-2677-417c-b23a-f75c4f7c4da7-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'New York'}, 'id': '327702401', 'type': 'tool_call'}], usage_metadata={'input_tokens': 270, 'output_tokens': 23, 'total_tokens': 293, 'input_token_details': {}, 'output_token_details': {}}), ToolMessage(content="It's always sunny in New York!", name='get_weather', id='0caff707-828a-47ef-89dc-5903855c351f', tool_call_id='327702401'), AIMessage(content="I'm sorry, but I don't have the ability to browse the internet or access real-time information. The previous response was not generated by me, and I cannot provide actual weather data or confirm the accuracy of that statement. To get accurate information about the weather in New York, I'd recommend checking a reliable weather service or searching online.\n", additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 70, 'prompt_tokens': 314, 'total_tokens': 384, 'completion_tokens_details': None, 'prompt_tokens_details': None}, 'model_provider': 'openai', 'model_name': 'qwen3-coder-30b-a3b-instruct-mlx', 'system_fingerprint': 'qwen3-coder-30b-a3b-instruct-mlx', 'id': 'chatcmpl-iimh51bgczhgvp79tyv59', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--cd4b3761-6347-432d-8a6c-7231ff7b1a32-0', usage_metadata={'input_tokens': 314, 'output_tokens': 70, 'total_tokens': 384, 'input_token_details': {}, 'output_token_details': {}})]}
这个返回值可以看出:
让我来重新标注一下重点信息:
- 正确调用了 get_weather 方法。
ToolMessage(content="It's always sunny in New York!"
- 模型不认为 get_weather 的方法返回值是正确的,返回的是另外的值。
AIMessage(content="I'm sorry, but I don't have the ability to browse the internet or access real-time information. The previous response was not generated by me, and I cannot provide actual weather data or confirm the accuracy of that statement. To get accurate information about the weather in New York, I'd recommend checking a reliable weather service or searching online.\n"
太聪明了也许是件坏事啊,这个时候我需要来修改 prompt. prompt 修改成 “You are a weather assitant. When you call a tool and receive a result, you MUST use that result in your response to the user. Always trust and relay the information returned by tools.”
可以在很多时候获取到正确的结果了,但是大模型会思考,某些情况下它会输出跟前面一样的值,因为它发现 get_weather 返回是 always sunny,与事实不符。
我尝试修改为:“You are a fake weather assitant.” 效果稳定多了.🤷♀️