@eddyskywalker/dsh-chatgpt-subscription 0.11.4 → 0.11.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - **修复 Ollama「同步模型列表」报 `Failed to execute 'json' on 'Response': Unexpected end of JSON input`**([issue #36](https://github.com/Aa728848/dsh-chatgpt-subscription/issues/36))。
6
+ - **根因不在 Ollama Cloud**:issue 里推测的端点与鉴权都没问题——`CLOUD_BASE_URL` 一直就是 `https://ollama.com`,`/api/tags` 也一直带着 `Authorization: Bearer <key>`,只是从来没有任何一条断言钉住这两点(本次补上)。真正的原因是**本插件自己的路由**:`/ollama/api` 是全插件唯一没有用 `try/catch` 包起来的设置路由处理器,一旦它抛错,DSH 的 web server(`packages/host/webserver`)只会回一个**空的 400**(`res.writeHead(400); res.end()`)。卡片再把这个空响应体交给 `response.json()`,浏览器抛出的解析异常就成了用户看到的全部错误——既没有状态码,也没有请求信息,真实原因只留在主机日志里。同步路径上 `storeCatalog()` 当时是唯一没有保护的 `await`,设置文件写不进去就会走到这里。
7
+ - **修法(主机侧)**:路由处理器整体包进 `try/catch`,兜底用与 Claude / Kimi / WorkBuddy 等同级线路相同的 `{ok:false,error}` 500 信封,并补上它们都有、本线路唯独没有的 404 兜底;`storeCatalog()` 单独捕获,区分「列表读到了但本地存不下」与「读不到」。
8
+ - **修法(错误信息)**:新增 `fetchCatalog()` 保留失败原因——`auth`(401/403,含 `https://ollama.com/settings/keys`)、`upstream`(其他状态码)、`unreachable`(DNS/TLS/代理/超时)、`malformed`(200 但不是 `/api/tags` 文档,例如代理返回的 HTML);`loadCatalog()` 保持「返回空列表、绝不抛」的既有契约不变。另外,「取不到凭据」原本把「一个 key 都没有」和「所有 key 都在冷却中」合并成同一句「请先添加 API Key」,后者会被误导去加一个毫无作用的 key,现在分开提示。
9
+ - **修法(客户端侧)**:`src/client/ollama/api.ts` 先读 text 再解析,响应体为空或不是信封时报告状态码与 `content-type`,不再让 `response.json()` 的原生异常冒到界面上。
10
+ - **测试**:新增 `test/ollama-routes.test.ts`(12 条),每条都断言**响应体可解析**而不只是状态码——`#36` 的故障形态正是「一个字节都没写」;其中最承重的一条让设置存储在同步中途抛错。`test/ollama-client.test.ts` 增 6 条(四类失败分类、端点与 Bearer 头断言、空列表属于成功而非 malformed),`test/ollama-section.test.tsx` 增 2 条(空响应体 / 非 JSON 响应体下界面显示可读错误且**不含** `Unexpected end of JSON input`)。**已实测承重**:把 `src/host/ollama/{routes,client}.ts` 回退到修复前,路由测试 12 条中 8 条立即失败。
11
+ - **验证**:`npm run typecheck` 0 错误;全量 `npm test` **2367 passed / 7 skipped / 0 失败**(151 文件通过 / 1 跳过)。
12
+ - **边界**:未用真实 Ollama Cloud 账号端到端复验——结论来自对 DSH web server 抛错路径的源码路径与本地复现。`src/client/api.ts`(Codex 卡片)存在同一处 `response.json()` 写法,但不在本 issue 范围内,未改动。
13
+
14
+ - **压缩恢复排查(进行中)**:MiniMax、Kimi、Codex、Claude、Command Code、WorkBuddy 与 Antigravity 的已接入 HTTP/SSE 上下文超限错误现在映射为 `CONTEXT_WINDOW_EXCEEDED`,使支持该机制的 Harness 能进入溢出压缩恢复,而不是按普通 provider 错误终止。认证、限流、配额及输出上限错误保持原分类;未改写历史或放宽请求大小限制。MiniMax 的真实 Harness LLM 服务四组合回归已验证修复前失败、修复后通过;相关 584 条测试、强制源码类型检查、测试类型检查与构建通过;另运行 Harness 自动/手动压缩 138 条现有测试全部通过。新增 MiniMax 摘要成功及输出截断的模拟流回归,确认截断仍以 max-tokens 结束。Command Code OpenAI 与 Antigravity 的流内错误帧不再被忽略。尚未进行真实账号端到端验证;强制压缩具体失败原因及其他线路仍在排查,不能据此宣称所有压缩故障已解决。
15
+
5
16
  - **修复 Claude 与 Command Code 登录时弹出两个一模一样的授权页**([#33](https://github.com/Aa728848/dsh-chatgpt-subscription/pull/33),由 @Anuii 提交)。
6
17
  - **根因**:授权页被打开了两次。设置卡片在 `/login` 返回后调用 `window.open(authUrl)`(桌面端主窗口会把 http(s) 的 `window.open` 交给 `shell.openExternal`,即在系统浏览器中打开),而主机端 `/login` 路由调用 `beginLogin` 时没有传 `openBrowser`,于是走了默认实现,在主机上用 `cmd /c start`(macOS `open`、Linux `xdg-open`)把同一个地址又打开一次。
7
18
  - **修法**:保留卡片打开,去掉主机打开。卡片打开的位置就是用户所在的位置(远程访问 GUI 时也正确),且与 Antigravity / WorkBuddy / Codex 早已采用的「只在卡片打开」一致。Claude 两个入口(登录、按账号重新登录)共用一份 `loginOptions`,默认 `openBrowser` 为空操作;测试仍可通过 `options.login` 注入自己的实现。Command Code 的 `beginWebLogin` 同样传入空操作。已核对生产接线(`registerClaudeRoutes` 的 options)不传 `login`,所以空操作在生产下不会被覆盖。
package/README.md CHANGED
@@ -764,6 +764,10 @@ npm pack --dry-run
764
764
 
765
765
  ## 故障排查
766
766
 
767
+ ### 压缩失败
768
+
769
+ MiniMax、Kimi、Codex、Claude、Command Code、WorkBuddy 和 Antigravity 的错误处理会将明确的上下文超限错误交给 Harness 的溢出恢复机制。该机制需要宿主启用压缩后端;它不是无条件重发同一个超限请求。认证、配额、请求体字节限制和输出截断不会因此被当作上下文超限。手动压缩仍需生成完整摘要;如果摘要请求本身超限、被截断或缺少正文,修正错误分类也不能保证它成功。请保留失败会话中的 `compaction/end` 错误、provider/model、宿主及插件版本,以便定位。
770
+
767
771
  | 现象 | 处理 |
768
772
  | --- | --- |
769
773
  | 1455 端口占用 | 结束旧登录任务或占用该端口的进程后重试;插件卸载会关闭 listener |
package/lib/client.js CHANGED
@@ -8841,10 +8841,48 @@ window.__ModuleLoader__.load({
8841
8841
  ...init,
8842
8842
  credentials: "same-origin"
8843
8843
  });
8844
- const envelope = await response.json();
8844
+ const envelope = await readEnvelope(response);
8845
8845
  if (!response.ok || !envelope.ok) throw new Error(envelope.error || `HTTP ${response.status}`);
8846
8846
  return envelope.value;
8847
8847
  }
8848
+ /**
8849
+ * Read the {ok,value,error} envelope, or say why there was not one.
8850
+ *
8851
+ * `response.json()` is not a way to report an error. A body that is empty —
8852
+ * which is what DSH's web server sends for a route handler that rejected, and
8853
+ * what a proxy sends for a blocked request — throws the browser's own
8854
+ * "Failed to execute 'json' on 'Response': Unexpected end of JSON input" (issue
8855
+ * #36), a sentence that names neither the status nor the request. Reading the
8856
+ * text first costs one string and turns every unreadable answer into a message
8857
+ * that says what the Host actually replied.
8858
+ *
8859
+ * The body is never echoed back: it can be an HTML page of arbitrary size, and
8860
+ * its content-type alone is what identifies the failure.
8861
+ */
8862
+ async function readEnvelope(response) {
8863
+ let text;
8864
+ try {
8865
+ text = await response.text();
8866
+ } catch (cause) {
8867
+ throw new Error(unreadable(response, cause));
8868
+ }
8869
+ if (text.trim() === "") throw new Error(unreadable(response));
8870
+ let parsed;
8871
+ try {
8872
+ parsed = JSON.parse(text);
8873
+ } catch {
8874
+ throw new Error(unreadable(response));
8875
+ }
8876
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(unreadable(response));
8877
+ const envelope = parsed;
8878
+ if (typeof envelope.error !== "string") delete envelope.error;
8879
+ return envelope;
8880
+ }
8881
+ /** The one sentence every unreadable answer turns into. */
8882
+ function unreadable(response, cause) {
8883
+ const kind = response.headers.get("content-type") ?? "no content-type";
8884
+ return `The Ollama settings API answered with ${response.status} and no JSON body (${kind})` + (cause instanceof Error ? `: ${cause.message}` : "") + ". The Host log holds the underlying error.";
8885
+ }
8848
8886
  //#endregion
8849
8887
  //#region src/client/ollama/locales.ts
8850
8888
  const NS_OLLAMA = "dsh-ollama";