🧠 第二章 模型部署:在本地使用DeepSeek
📄 一句话概括

不设 OLLAMA_ORIGINS 时,请求其实已经被服务端处理完了, 只是响应里少了 Access-Control-Allow-Origin 这个头,浏览器就不让页面读。 服务端日志一切正常,页面却报网络错误——这是最容易误判的一类故障。

📝 本节和前面几节的关系

上一节扩展装好了、模型也读到了。这一节讲它唯一一个真正的坑:跨域。 这一步卡住的人比前面所有步骤加起来都多。

一、跨域是什么,为什么本机也算跨域

浏览器的同源策略规定:协议、域名、端口三者全同,才算同源。只要有一项不同,就是跨域, 就要经过额外许可。 扩展页面的地址不是 http://,是 chrome-extension://abcdef...。协议不同、域名不同、端口也不同。 所以在 Ollama 眼里,这个扩展是一个彻底的外站。 这就解释了一个反直觉的现象:明明两边都在 127.0.0.1 上,明明没有走网络,照样被拦。 跨域判断的是「页面出身」,不是「距离远近」。 顺带一个对比:Cherry Studio 是桌面程序,不是网页,它不受同源策略约束, 所以上一节完全没遇到这个问题。这就是「桌面端能连、扩展连不上」的根本原因。

二、预检请求:浏览器先问一句

对于带 JSON 的 POST,浏览器会先发一个 OPTIONS 请求,相当于问一句「我能发这个请求吗」。 这叫预检。 服务端如果回了 Access-Control-Allow-Origin 这个头(简称 ACAO),而且值匹配, 浏览器才放行真正的 POST。没这个头,浏览器在 JS 拿到响应之前就把请求掐掉。 这里有个关键细节,也是最多人误解的地方:服务端其实已经把请求处理完了。它跑了模型、生成了回答、 写了响应,只是在响应头里少了一行。 所以现象是:Ollama 的日志里一切正常,页面控制台报 CORS 错误。两边的信息完全矛盾, 人被夹在中间不知道该信谁。信控制台。 Demo 里把两种情况都跑了一遍:不设 CORS 时,响应里没有 ACAO 头; 设了之后,多出 Access-Control-Allow-Origin: *。 两行输出只差一个头,但决定的是一次请求能不能完成。

三、怎么设:一个环境变量加一次重启

Windows:set OLLAMA_ORIGINS=*(当前终端窗口有效),或者在系统环境变量里加一条。 macOS/Linux:export OLLAMA_ORIGINS=*,或者写进启动脚本。 然后重启 Ollama 服务。 环境变量只在启动时读一次,改完不重启等于没改——这一步漏掉的人非常多。 生产环境别用 *,写具体来源(比如 chrome-extension://你的扩展ID)。 * 的意思是「任何网页都能调你的模型」,本机自用无所谓,但如果你在局域网里暴露了 11434, 这等于把模型开放给所有人。 还有一个隐性的点:如果 Ollama 装在别的机器上,要连它的地址也得配。跨域是浏览器端的限制, 跟服务端在哪儿无关——你在 A 机器开浏览器、连 B 机器的 Ollama,照样要 B 那边设 origins。

四、为什么这个错误看起来像「连不上」

浏览器把 CORS 失败报成网络错误。 控制台里可能写 Failed to fetch 或者 net::ERR_FAILED,看起来就像服务没起。 于是人就去做端口探测——结果端口通、/api/tags 也通、curl 发 POST 也正常。 因为这三件事都不经过浏览器的同源检查。 这个误导非常彻底:用命令行测,永远测不出跨域问题。命令行没有浏览器,没有同源策略。 所以判断方法要换一条:如果「命令行能通、页面不能」, 那就是跨域,不用再查别的。 这个判据简单而且几乎不会错。

五、动手 Demo:没设 origins 时响应少了 ACAO 头,服务端却已经处理完了

用同一个假服务,分别在「不带 CORS 头」和「带 CORS 头」两种配置下接收同一个请求, 打印状态码和 Access-Control-Allow-Origin 头的有无。再顺带列一遍三个配置项按出错概率的排序。

origin = u'chrome-extension://abcdefghijklmnop'

Mock.cors = False                       # 默认:不加 CORS 头
st, _b, hd, _d = post(url + u'/v1/chat/completions',
                      {u'model': u'x'}, headers={u'Origin': origin})
print(st, hd.get(u'Access-Control-Allow-Origin'))

Mock.cors = True                        # 设了 OLLAMA_ORIGINS
st2, _b2, hd2, _d2 = post(url + u'/v1/chat/completions',
                          {u'model': u'x'}, headers={u'Origin': origin})
print(st2, hd2.get(u'Access-Control-Allow-Origin'))

实跑输出:

--------------------------------------------------------------------
配置 Page Assist:卡住九成人的那一步是跨域
--------------------------------------------------------------------
  扩展页面的出身不是 http,是 chrome-extension://,所以在 Ollama 眼里它是「外站」。
  扩展的 Origin            chrome-extension://abcdefghijklmnop

  第一步,浏览器的预检请求(OPTIONS):
  默认配置                 状态 200,ACAO 头 没有
  **没有这个头,浏览器会在 JS 拿到响应之前就把请求掐掉**,控制台报 CORS 错误。
  注意:服务端其实已经处理完了,是浏览器不让页面读。

  设了 OLLAMA_ORIGINS      状态 200,ACAO 头 *
  有了这个头,请求才真正完成一个来回。

  对应的动作只有一个:启动 Ollama 之前设一个环境变量。
  Windows                  set OLLAMA_ORIGINS=*(当前窗口有效)
  macOS/Linux             export OLLAMA_ORIGINS=*
  然后**重启 Ollama 服务**——环境变量只在启动时读一次,改完不重启等于没改。

  三个配置项,按出错概率排:
  ① 跨域                   扩展白屏、控制台报 CORS,就是这一条
  ② 地址端口               填 11434 还是别的端口,要和 Ollama 实际监听一致
  ③ 上下文长度             默认 2048,长文档要手动调,不然内容被截断还说不出原因

第一行:状态 200,ACAO 头没有。 注意状态码是 200——服务端从头到尾都正常工作了, 它只是没在响应头里加那一行。 第二行:状态 200,ACAO 头 *。 加了这一行,浏览器才允许页面读响应。 两行输出的差别只有一个头,但一次请求能不能完成就取决于它。 这段输出正好复刻了现实里最迷惑的场景:服务端一切正常、状态码 200、内容也对,可页面就是拿不到。 因为「拿不到」是浏览器干的,不是服务端干的。 后面那三个配置项按出错概率排了序。 跨域排第一不是因为它难, 是因为它的报错信息把人往错的方向引——报「连不上」,实际是「连上了但读不到」。 最后一条「上下文长度」排在第三,但它的隐蔽性最强:不报错、不白屏,只是模型「忘事」。 前两条会闹,第三条不闹,所以更难发现。

30 秒实验

  1. 把 Mock.cors 在两个分支之间来回切,各跑一次,对比两次的完整响应头。 除了 ACAO,其他头一模一样。
  2. 把 Origin 改成 http://localhost:3000(一个普通网页),再跑一次。 结论不变——只要是跨源,就要这个头,跟是不是扩展无关。
  3. 把 ACAO 的值从 * 改成 chrome-extension://abc...(只允许那一个来源), 再用另一个 Origin 请求,看浏览器会不会拒。这就是生产环境的做法。

常见误区

  1. 「两边都在 127.0.0.1 上,不可能跨域」。跨域看的是页面出身(协议+域名+端口),不是物理距离。 chrome-extension:// 对 http:// 就是跨源。
  2. 「用 curl 测通了就没问题」。 curl 没有浏览器,没有同源策略,永远测不出跨域问题。 命令行通、页面不通,就是跨域。
  3. 「改完环境变量就生效」。环境变量只在服务启动时读一次,必须重启 Ollama。 改完不重启,现象一模一样。
自测三问(点开对答案) 1. 为什么两边都在本机,扩展访问 Ollama 还是跨域? —— 同源策略看的是协议、域名、端口三者是否全同。 扩展页面是 `chrome-extension://`,Ollama 是 `http://127.0.0.1:11434`,协议和域名都不同, 所以是跨源。 2. 为什么「服务端日志正常、页面报网络错误」这种情况会出现? —— 因为服务端确实处理完了请求并返回了 200,只是响应头缺 ACAO。 浏览器在 JS 拿到响应之前把它拦掉了。**错在浏览器,不在服务端。** 3. 怎么快速判断一个「连不上」是不是跨域问题? —— 用命令行发同一个请求。命令行能通、页面不能,就是跨域。因为命令行不经过浏览器的同源检查。

跨域过了,最后一步是验收。下一节用三项指标判断这套东西到底能不能用。

相关笔记