Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<div align="center">
<p><b>English</b> &nbsp;·&nbsp; <a href="./README.zh-CN.md">简体中文</a></p>

<p><img src="docs/media/hero.svg" alt="OpenCode Patch — session affinity, gateway headers and live Go quota" width="100%" /></p>

<img src="icon.svg" alt="OpenCode on DeepSeek Harness" width="112" />
<h1>dsh-opencode-patch</h1>
<p><b>OpenCode on DeepSeek Harness</b><br />Gateway origin headers · Session affinity · Free-tier tool fallback · Live dual-mode quota meter</p>
Expand Down
2 changes: 2 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<div align="center">
<p><a href="./README.md">English</a> &nbsp;·&nbsp; <b>简体中文</b></p>

<p><img src="docs/media/hero.svg" alt="OpenCode 补丁——会话保持、网关请求头与实时 Go 额度" width="100%" /></p>

<img src="icon.svg" alt="OpenCode on DeepSeek Harness" width="112" />
<h1>dsh-opencode-patch</h1>
<p><b>OpenCode on DeepSeek Harness</b><br />网关来源头 · 会话亲和 · 免费层工具回退 · 实时双模式额度计量</p>
Expand Down
18 changes: 18 additions & 0 deletions docs/detail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
![OpenCode Patch — session affinity, gateway headers and live Go quota](./media/hero.svg)

OpenCode's own CLI and this plugin make the same requests to the same gateway. The difference is what the gateway is told about them, and what the composer shows while they run: this patch restores the request identity the official CLI sends, keeps one session across turns, and puts the live Go quota beside the input box.

## What you get

| Surface | What it does |
| :-- | :-- |
| Gateway requests | Sends the official CLI's `User-Agent`, origin headers and workspace project tag, so Cloudflare's WAF checks pass |
| Sessions | One session id per conversation, derived and reused, so Zen and Go stop treating every turn as a stranger |
| Quota | The Go panel — 5-hour, weekly and monthly rings, the monthly allowance per model — and the Zen session spend |
| Routing | Models served on another protocol are routed to the endpoint that speaks it, instead of being refused |

## Where the rest lives

- The full reference — configuration, every control, the model tables — is in [README.md](https://github.com/viztor/dsh-opencode-patch#readme).
- The meter's numbers, and what each row means, are in [docs/quota-meter.md](https://github.com/viztor/dsh-opencode-patch/blob/main/docs/quota-meter.md).
- The endpoints, the headers and the measurements behind them are in [docs/redirect-planes.md](https://github.com/viztor/dsh-opencode-patch/blob/main/docs/redirect-planes.md).
18 changes: 18 additions & 0 deletions docs/detail.zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
![OpenCode 补丁——会话保持、网关请求头与实时 Go 额度](./media/hero.svg)

OpenCode 自己的 CLI 和这个插件向同一个网关发的是同一批请求,差别在于网关被告知了什么、以及运行的时候输入框旁边显示了什么:本补丁恢复官方 CLI 的请求身份、让同一段对话共用一个会话、并把实时 Go 额度放在输入框旁。

## 你能得到什么

| 界面 | 作用 |
| :-- | :-- |
| 网关请求 | 发送官方 CLI 的 `User-Agent`、来源请求头和工作区项目标识,使 Cloudflare WAF 校验通过 |
| 会话 | 每段对话一个会话 id,推导并复用,Zen 与 Go 不再把每一轮都当成陌生人 |
| 额度 | Go 面板——5 小时、每周、每月三个环,以及按模型的月度额度——和 Zen 的会话消耗 |
| 路由 | 走另一种协议的模型会被送到说那种协议的端点,而不是被拒绝 |

## 其余内容在哪儿

- 完整参考——配置、每个控件、模型表——在 [README.zh-CN.md](https://github.com/viztor/dsh-opencode-patch/blob/main/README.zh-CN.md)。
- 计量表上每个数字、每一行在说什么,在 [docs/quota-meter.zh-CN.md](https://github.com/viztor/dsh-opencode-patch/blob/main/docs/quota-meter.zh-CN.md)。
- 端点、请求头以及它们背后的实测,在 [docs/redirect-planes.md](https://github.com/viztor/dsh-opencode-patch/blob/main/docs/redirect-planes.md)。
55 changes: 55 additions & 0 deletions docs/media/hero.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion locale/en.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"meta": {
"title": "OpenCode Patch",
"description": "Restore OpenCode Zen/Go gateway headers and session affinity, and track live Go quota beside the composer."
"description": "Restore OpenCode Zen/Go gateway headers and session affinity, and track live Go quota beside the composer.",
"detail": "docs/detail.md"
}
}
3 changes: 2 additions & 1 deletion locale/zh.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"meta": {
"title": "OpenCode 补丁",
"description": "恢复 OpenCode Zen/Go 网关请求头与会话保持,并在输入框旁实时显示 Go 额度。"
"description": "恢复 OpenCode Zen/Go 网关请求头与会话保持,并在输入框旁实时显示 Go 额度。",
"detail": "docs/detail.zh-CN.md"
}
}
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@
"README.zh-CN.md",
"CONTRIBUTING.md",
"CHANGELOG.md",
"LICENSE"
"LICENSE",
"docs/detail.md",
"docs/detail.zh-CN.md",
"docs/media/hero.svg"
],
"type": "module",
"main": "lib/index.mjs",
Expand Down
10 changes: 7 additions & 3 deletions test/docs-pairing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { join } from "node:path";
import { describe, expect, it } from "vitest";

/** Every document published in two languages, by its base name. */
const PAIRS = ["README", "docs/quota-meter"] as const;
const PAIRS = ["README", "docs/quota-meter", "docs/detail"] as const;

/** Read one document of the repository. */
const read = (base: string, suffix: string): string =>
Expand Down Expand Up @@ -96,9 +96,10 @@ describe("the documents published in two languages", () => {
it(`${base}: the two languages carry the same code blocks`, () => {
// A command that exists in one language only is worse than a typo: the
// reader following that language cannot run it.
// No minimum: a page with no commands is a legitimate page, and the
// pairing is what this asserts — not that every document has a fence.
const en = fences(read(base, ""));
const zh = fences(read(base, ".zh-CN"));
expect(en).toBeGreaterThan(0);
expect(zh, `${base}.zh-CN`).toBe(en);
});

Expand Down Expand Up @@ -126,7 +127,10 @@ describe("the documents published in two languages", () => {
for (const base of PAIRS) {
for (const suffix of ["", ".zh-CN"]) {
const text = read(base, suffix);
expect(text.length, `${base}${suffix}.md`).toBeGreaterThan(1000);
// A floor that proves the file was READ, not a length policy: the
// featured-image page is three short sections and 808 characters, and a
// threshold tuned to it would fail the next short document for no reason.
expect(text.length, `${base}${suffix}.md`).toBeGreaterThan(200);
expect(headings(text).length, `${base}${suffix}.md`).toBeGreaterThan(1);
expect(tableRows(text).length, `${base}${suffix}.md`).toBeGreaterThan(
0
Expand Down
133 changes: 133 additions & 0 deletions test/plugin-detail.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/**
* What the plugin page shows about this package.
*
* The page renders ONE document inline: the Markdown that `meta.detail` in
* `locale/<language>.json` names, or — when nothing declares one — the whole
* `README.md`. This package shipped no `meta.detail`, so its page rendered 461
* lines of engineering notes above the settings card, and the featured image it
* could have had was nothing more than an icon.
*
* The host's rules, from `@deepseek-ai/dsh-plugin-detail`:
*
* - `meta.detail` is a `/`-separated Markdown path relative to the package root,
* and a path that does not exist is an `invalid` failure — a blank page, not a
* fallback;
* - an image the document writes is served from the package, so a path it names
* has to be shipped;
* - the document is rendered INLINE, which is why its length is part of the
* contract rather than a taste question.
*
* @module test/plugin-detail.test
*/

import { readFileSync, statSync } from "node:fs";
import { join } from "node:path";

import { describe, expect, it } from "vitest";

const ROOT = process.cwd();

/** Read a text file of the package, failing loudly when it is absent. */
const read = (relative: string): string =>
readFileSync(join(ROOT, relative), "utf8");

interface LocaleMeta {
detail?: string;
title?: string;
}

/** The `meta` block of one locale file. */
const metaOf = (locale: string): LocaleMeta => {
const parsed = JSON.parse(read(`locale/${locale}.json`)) as {
meta?: LocaleMeta;
};
return parsed.meta ?? {};
};

/** The paths `package.json` tells npm to ship. */
const shippedFiles = (): string[] =>
(JSON.parse(read("package.json")) as { files?: string[] }).files ?? [];

/** The locales this package ships, by file name. */
const LOCALES = ["en", "zh"] as const;

describe("the document the plugin page renders", () => {
it("is declared by every locale, so no language falls back to the README", () => {
// The fallback is the whole README. A language that declares nothing gets
// that, which is the state this package was in: every page, every language.
for (const locale of LOCALES) {
const { detail } = metaOf(locale);
expect(detail, `locale/${locale}.json meta.detail`).toBeTruthy();
expect(detail).toMatch(/\.(?:md|markdown)$/u);
// Relative, `/`-separated, and inside the package: the host refuses
// anything else as `invalid`, which renders nothing rather than the README.
expect(detail?.startsWith("/")).toBe(false);
expect(detail).not.toContain("\\");
expect(detail?.split("/")).not.toContain("..");
}
});

it("names a file that really exists, and that npm really ships", () => {
// Two independent ways to be broken: the file is absent (the host answers
// `invalid`), or the file is present in the repo and missing from the
// tarball (the page works here and not for a user).
const shipped = new Set(shippedFiles());
for (const locale of LOCALES) {
const detail = metaOf(locale).detail ?? "";
expect(statSync(join(ROOT, detail)).size, detail).toBeGreaterThan(0);
expect(
shipped.has(detail),
`${detail} must be in package.json files`
).toBe(true);
}
});

it("opens with the featured image, and ships it", () => {
// The page's whole header is this document, so the image has to be the first
// thing in it — and the detail-image route serves package-relative paths, so
// the file has to be there.
const shipped = new Set(shippedFiles());
for (const locale of LOCALES) {
const detail = metaOf(locale).detail ?? "";
const [first] = read(detail).split("\n");
const image = /^!\[[^\]]*\]\(([^)]+)\)$/u.exec(first ?? "")?.[1];
expect(image, `${detail} must open with an image`).toBeTruthy();
const relative = image?.replace(/^\.\//u, "") ?? "";
const resolved = join(detail, "..", relative);
expect(
statSync(resolved).size,
`${resolved} must exist beside the document`
).toBeGreaterThan(0);
expect(
shipped.has(join(detail, "..", relative)),
`${resolved} must be in package.json files`
).toBe(true);
}
});

it("stays short enough to read above the settings card", () => {
// Not a style rule: the document is rendered inline, and the card is below
// it. The state this replaced was a 461-line README, and the report was
// "why do I have such a big readme here".
for (const locale of LOCALES) {
const detail = metaOf(locale).detail ?? "";
const lines = read(detail).split("\n").length;
expect(lines, `${detail} is ${lines} lines`).toBeLessThanOrEqual(40);
}
});

it("names the pages a reader needs next", () => {
// A short page has to hand off, or it is just a shorter dead end: the full
// reference, the meter's numbers, and the endpoints behind them.
const en = read(metaOf("en").detail ?? "");
for (const target of [
"github.com/viztor/dsh-opencode-patch",
"docs/quota-meter.md",
"docs/redirect-planes.md",
]) {
expect(en, `the English detail document must link ${target}`).toContain(
target
);
}
});
});
Loading