微信公众号文章批量下载工具

公众号这种私域实质上是阻碍信息交流的。

LightCone

沉迷MCP和Skills

其实我还没玩明白。

Pi

太多了,玩不明白。

在Emacs里连Open Science的OpenCode

内容是DeepSeek V4 Flash和Open Science共同生成。我唯一的修改是把备注里由DeepSeek生成挪前面了。你们能看出来是AI生成的么?别管质量了,快是真快,这些我不得写个半小时,调试这个不得调试半天。Agent大法好啊。

接上面沉迷MCP和Skills的话题,这几天玩了一个新东西:把Open Science Desktop启动的opencode服务,从WSL里的Emacs连过去用。

起因很简单:我整天泡在Emacs里,不想为了用opencode专门切到另一个窗口。Open Science Desktop本来就起了一个opencode服务,我的Emacs能不能直接连上去?

折腾了一圈,结论值得记下来。

发现opencode本来就是HTTP服务

Open Science Desktop启动opencode时,其实是跑了一个HTTP服务器,默认监听127.0.0.1,端口每次随机。它还带Basic Auth,密码是进程级环境变量OPENCODE_SERVER_PASSWORD,每次重启重新生成。

也就是说,它天生就是个可以被其他程序访问的服务,只是监听在本机回环地址上。

第一个坑:WSL连不到Windows的127.0.0.1

我的Emacs跑在WSL里。WSL2默认NAT模式下,连不到Windows宿主机上绑定127.0.0.1的服务——这是实测的,不是想当然。网关IP、localhost,都不通。

我的Windows是build 22000,不支持镜像网络,所以绕不开这个网络隔离。

解法:在Windows侧跑一个纯Python的TCP转发器,把某个端口转发到127.0.0.1:<端口>,WSL从网关IP连转发器即可。不需要管理员权限,这点很关键。

使用:python opencode-bridge.py <对外端口> <目标主机> <目标端口>
三个占位符替换成你自己的值:

  • <网关IP>:WSL 网关 IP,在 WSL 里执行 grep nameserver /etc/resolv.conf 查到(形如 172.x.x.1)。
  • <端口>:转发器对外端口(随意挑个空闲的),以及目标端口(opencode 服务端口,见上)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# opencode-bridge.py —— 简单端口转发,免管理员
import socket, threading, sys

LISTEN_HOST = "<网关IP>" # WSL网关IP,从/etc/resolv.conf的nameserver查
LISTEN_PORT = int(sys.argv[1])
TARGET_HOST = sys.argv[2]
TARGET_PORT = int(sys.argv[3])

def pipe(src, dst):
try:
while True:
data = src.recv(65536)
if not data: break
dst.sendall(data)
except OSError: pass
finally:
try: dst.shutdown(socket.SHUT_WR)
except OSError: pass
try: src.close()
except OSError: pass

def handle(client):
try:
up = socket.create_connection((TARGET_HOST, TARGET_PORT), timeout=10)
except OSError:
client.close(); return
threading.Thread(target=pipe, args=(client, up), daemon=True).start()
threading.Thread(target=pipe, args=(up, client), daemon=True).start()

def main():
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind((LISTEN_HOST, LISTEN_PORT)); srv.listen(128)
print("forwarding %s:%d -> %s:%d" % (LISTEN_HOST, LISTEN_PORT, TARGET_HOST, TARGET_PORT), flush=True)
while True:
c, _ = srv.accept()
threading.Thread(target=handle, args=(c,), daemon=True).start()

if __name__ == "__main__":
main()

密码怎么处理

密码是进程级的,用户环境变量里没有。它在 Open Science Desktop 进程的环境变量 OPENCODE_SERVER_PASSWORD 里,每次重启重新生成,所以没法一次性配好。解决方案:把它写进WSL的一个文件里,Emacs配置读取时从文件取,不硬编码;重启后重新执行一次写入即可。

把密码写进 WSL 文件(<base64> 替换为密码的 base64,实际取密码的方式见上文):

1
2
# 把当前密码写入WSL(不泄露明文)
wsl.exe -e bash -c "printf '%s' '<base64>' | base64 -d > ~/.config/osc-opencode/password && chmod 600 ~/.config/osc-opencode/password"

Emacs里这样读:

1
2
3
4
5
(defun opencode-desktop-read-password ()
(let ((f (expand-file-name "osc-opencode/password" "~/.config/")))
(if (file-readable-p f)
(string-trim (with-temp-buffer (insert-file-contents f) (buffer-string)))
(error "password file not found: %s" f))))

用现成的opencode.el

手写HTTP客户端太累了,GitHub上有现成的opencode.el,支持attach到已有的opencode服务。Emacs 29+内置的package-vc就能装:

1
2
M-x package-vc-install RET
https://github.com/karta0807913/opencode.el.git RET

配置基本三样:服务器地址、端口、密码。

折腾中踩的坑

  • C-c o键位时好时坏:因为opencode-mode是全局minor mode,要主动激活。
  • 会话列表空:opencode.el按当前目录过滤会话。要在对的工作目录里M-x cd
  • 界面卡busy:WSL下opencode.el把chat buffer的目录设成了Windows路径(如C:\home\kel\.emacs.d),导致SSE子进程起不来。加个钩子修正。
1
2
3
4
5
6
7
8
;; WSL路径修正:把被设成Windows路径的目录改回WSL路径
(defun opencode--wsl-valid-dir (dir)
(let ((exp (ignore-errors (expand-file-name dir))))
(if (and exp (file-directory-p exp)) exp (expand-file-name "~/"))))
(defun opencode--wsl-fix-chat-dir ()
(when (string-match "^[A-Za-z]:\\\\" default-directory)
(setq default-directory (opencode--wsl-valid-dir default-directory))))
(add-hook 'opencode-chat-mode-hook #'opencode--wsl-fix-chat-dir)

现在的工作流

  1. Windows侧起转发器
  2. Emacs里M-x cd到工作目录
  3. C-c o O attach
  4. C-c o c开会话

整体能用,但每次换目录要cd一下,还是有点糙。可能等opencode.el更成熟吧。

备注

中间折腾的细节和代码都是实测过的,但Emacs配置这东西,换环境可能就要调整,仅供参考。