# dsh-desktop-settings [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) [![Node.js](https://img.shields.io/badge/node-%5E22.19.0%20%7C%7C%20%3E%3D24-339933.svg)](./package.json) **English** | [简体中文](./README.md) Adds the sections missing from the official DeepSeek Harness (DSH) desktop settings page: README translation (DeepL) with a connectivity self-test, a GitHub acceleration proxy (self-hosted reverse proxy) with a connectivity self-test, plugin market cache cleanup, and an entry point for topping up the DeepSeek open platform account. It installs as a standalone bundle into a DSH profile and does not modify any official code. --- ## Contents - [Background](#background) - [Requirements](#requirements) - [Installation](#installation) - [Uninstallation](#uninstallation) - [How It Works](#how-it-works) - [Features and Settings](#features-and-settings) - [Configuration File](#configuration-file) - [HTTP Routes](#http-routes) - [Deliberately Unimplemented Capabilities](#deliberately-unimplemented-capabilities) - [Activation Criteria](#activation-criteria) - [Testing](#testing) - [Known Limitations](#known-limitations) - [License](#license) ## Background The official settings page already covers account / general / models / plugins / subagent / web-search / agent-loop / shell, but the following items have no entry point: | Missing item | Impact | | --- | --- | | README translation | Most repositories in the plugin market only ship an English README, which makes their purpose hard to judge for Chinese-speaking users | | GitHub acceleration proxy | Direct connections to GitHub are often slow or fail because of TLS handshake and time-to-first-byte latency; there is no shared place to configure an alternative route | | Cache cleanup | No entry point to clear the market list cache, download leftovers, or the translation cache | | Top-up entry | Users must find the top-up page themselves when the balance runs low | This plugin adds only those four items. The following three sections are deliberately **not** provided: "application name", "DSH data directory (DSH_HOME)", and "reuse configuration from another DSH directory". They all presuppose a shell that carves out a fully isolated data directory; the official desktop build does not isolate `~/.dsh` and has a single home directory, so those sections have no premise. ## Requirements | Item | Requirement | | --- | --- | | Node.js | `^22.19.0 \|\| >=24` | | DeepSeek Harness | Official desktop build (the host half relies on its `process.argv` layout and profile structure) | The host half only depends on `ctx.webServer`. The client half depends on the official client module loader and on the slots provided by `@deepseek-ai/dsh-client-ui-slots`. ## Installation Install into the target profile with the pnpm bundled with the official desktop build: ```bash # Install into a profile using the pnpm bundled with the official desktop build pnpm add "file:" # Or install directly from GitHub pnpm add "github:VCPr0j3k7/dsh-desktop-settings" ``` Example (replace the paths with your actual installation): ```bash PNPM="D:/DeepSeek Harness/resources/runtime/pnpm/bin/pnpm.cjs" NODE="D:/DeepSeek Harness/resources/runtime/primary-runtime/dependencies/node/bin/node.exe" PROFILE="C:/Users//.dsh/profiles/desktop" cd "$PROFILE" "$NODE" "$PNPM" add "file:C:/path/to/dsh-desktop-settings" ``` **After installing, you must also add the package name to the `dsh.profile.bundles` array in the profile's `package.json`**, otherwise the plugin will not load. This is the critical pitfall of this project: ```jsonc { "dsh": { "profile": { "bundles": [ // …existing entries… "dsh-desktop-settings" ] } } } ``` `pnpm add` only records the package in `dependencies` and installs it into `node_modules`. A plugin that declares `dsh.bundle.patch` must be registered in `dsh.profile.bundles` before it is mounted. When it is installed but not registered, the plugin loads without any error and nothing looks wrong in the UI — its settings section and sidebar entry simply never appear. The official `dsh plugin add` command performs this step automatically; when running pnpm manually you have to do it yourself. The target harness must be **restarted** after installation. The host assembles the plugin tree only at startup, and a running instance does not hot-reload plugins. To confirm it is active: 1. After restarting, the settings page should show a "桌面设置补充" section and the sidebar should show an "API 充值" entry. 2. Open the developer tools; the console should print: ```text [dsh-desktop-settings] client factory 已执行 [dsh-desktop-settings] 宿主基址 = (相对路径) ``` The first line means the client half was loaded; the second means it reached the host. If only the first line appears, the client failed to reach the host. ## Uninstallation ```bash cd "$PROFILE" "$NODE" "$PNPM" remove dsh-desktop-settings ``` Then remove the `dsh-desktop-settings` entry from `dsh.profile.bundles` and restart DeepSeek Harness. To disable it temporarily without uninstalling, just remove that entry from `dsh.profile.bundles` and restart. ## How It Works The plugin consists of two halves: ```text dsh-desktop-settings/ ├── package.json declares dsh.bundle.patch and dsh.client.platform ├── cordis.patch.yml host-side mount declaration ├── index.js host half: Cordis plugin registering routes under /dsh-desktop-settings/api ├── client.js client half: window.__ModuleLoader__.load({ id, factory }) └── host/ adapter layer and service implementations ├── env.mjs resolves process.argv into paths and the host context ├── router.mjs HTTP facade (prefix dispatch, {ok,data|error}, event queue) ├── config.mjs shared configuration (/dsh-extras.json) ├── system.mjs open browser / pick directory / open folder ├── logger.mjs output capture and log ring buffer ├── electron-shim.mjs stand-in for app.getPath('userData') ├── yaml-libs.mjs borrows the two YAML libraries the host itself uses (multiple candidate paths) └── services/ translation, GitHub proxy, and settings helpers ``` ### Host half 1. Registers a **single** `kind: 'prefix'` route on `ctx.webServer` with the prefix `/dsh-desktop-settings/api`; dispatch inside the prefix is handled by `host/router.mjs`. There is exactly one registration point, and the framework cleans it up on unload. 2. Every response is uniformly `{ ok: true, data }` or `{ ok: false, error }`, and business failures also return HTTP 200. Status codes are reserved for transport-level facts such as "route not found". 3. Progress events go into a ring buffer (400 entries max), and the client polls increments via `GET /events?since=`. SSE is not used because it cannot be confirmed whether the shell buffers response bodies when forwarding a custom scheme; polling has no such unknown. ### Client half 1. The page is served by the official shell, so its origin can be one of three things: the host's own HTTP address, the shell's custom scheme (`dsh-app://app/`), or an opaque origin (`location.origin === "null"`). The transport layer therefore tries the candidate base URLs in order on the first request (relative path → `http://dsh.internal`) and decides which one really works from the response of `/info`. 2. The criterion is `data.plugin` equalling this plugin's id — not "returned 200", because the SPA fallback route also answers unknown paths with 200 and a chunk of HTML. 3. Once chosen, the base URL is remembered and reused for subsequent requests. If none of the candidates works, it falls back to the relative path, prints a single console line, and does not retry repeatedly. ## Features and Settings ### README translation (DeepL) - Enabled by default. Translation is only invoked for text that is essentially non-Chinese: a request is sent only when the text contains fewer than 30 CJK characters and their share is below 8%, so a genuinely Chinese README never consumes quota. - Keyword extraction runs entirely locally, is unaffected by the translation toggle, and works offline. - Translations are cached on disk per repository, so reopening a README never bills twice. - Connectivity self-test: `POST /settings/test-translation` translates a fixed sentence and returns the detected source language, the target language, and the translated text. A 456 response means the quota is exhausted; 403 means the key is invalid or the Free / Pro endpoint is wrong. - DeepL Free keys end with `:fx` and must use the `api-free.deepl.com` host. The authorization header is `Authorization: DeepL-Auth-Key `, not Bearer. ### GitHub acceleration proxy - Disabled by default with an empty address. A proxy address belongs to whoever set it up, so the plugin preloads no value and never silently routes a user's requests to a machine. When disabled, behaviour is identical to not having the feature. - It is active only when the toggle is on **and** the address is non-empty. With the toggle on but no address, it silently falls back to the official URLs instead of raising an error. - Addresses are normalised: a missing scheme, a trailing slash, and a reverse proxy mounted under a sub-path are all tolerated. Only http / https are accepted; anything else is treated as invalid and falls back to a direct connection. - Path prefixes map one-to-one onto upstream hosts; tokens, Accept headers, and request methods are passed through unchanged: | Upstream host | Proxy path prefix | | --- | --- | | `api.github.com` | `/api` | | `raw.githubusercontent.com` | `/raw` | | `codeload.github.com` | `/codeload` | | `github.com` | `/gh` | | `objects.githubusercontent.com` | `/objects` | | `release-assets.githubusercontent.com` | `/release-assets` | - 302 responses are followed manually by this plugin (up to 6 hops): the `Location` value is rewritten to the proxy address before the next request. If fetch followed them automatically, it would connect straight to the redirect target, bypassing the proxy entirely. - Connectivity self-test: `POST /settings/test-github-proxy` fires two probes — the proxy root path and one real raw request (the README of `electron/electron`). Both must pass: a healthy root path only proves the reverse proxy is running, not that it can reach GitHub. - The optional `githubToken` is empty by default. When empty, requests are sent anonymously (60 per hour); with a personal access token, the authenticated allowance applies (5000 per hour). The token is only used for GitHub API access, is never written to logs, and is not distributed with the plugin. ### Cache cleanup `POST /market/clear` removes the following: | Target | Description | | --- | --- | | `/plugin-data/market-cache.json` | Plugin market list cache | | `/plugin-data/market-downloads` | Download leftovers directory | | `/plugin-data/translate-cache/` | Translation cache | If a single target cannot be removed it is skipped, without failing the whole operation. ### API top-up - `GET /recharge/status` returns the top-up URL, the usage URL, where the API key comes from, and the current data directory. - The API key source is only probed for existence; the key itself is **never read or returned**. The environment variable `DEEPSEEK_API_KEY` is checked first, then whether the key is present in `/.credentials.yaml`. - `POST /recharge/open` opens `https://platform.deepseek.com/top_up` in the system default browser. The URL is validated before opening: only http / https are allowed. - Clicking the "API 充值" sidebar icon opens the browser directly and switches the main panel to the top-up page. ## Configuration File All settings live in a single file, `/dsh-extras.json`, shared with the plugin market (`dsh-plugin-market`): this plugin writes it, and the market module also reads it because it needs translation and downloads. A file is used instead of inter-plugin HTTP calls because a file has no ordering problem of the form "the other plugin is not installed / has not started yet". | Field | Default | Description | | --- | --- | --- | | `appName` | `"DeepSeek Harness"` | Display name | | `githubProxy.enabled` | `false` | Whether the GitHub acceleration proxy is enabled | | `githubProxy.baseUrl` | `""` | Proxy address, e.g. `http://1.2.3.4:8080` | | `githubToken` | `""` | Optional GitHub API token | | `translate.enabled` | `true` | Whether README translation is enabled | | `translate.endpoint` | `https://api-free.deepl.com/v2/translate` | DeepL endpoint | | `translate.apiKey` | `""` | DeepL API key | | `translate.targetLang` | `"ZH"` | Target language code (uppercase for DeepL) | | `translate.characterCount` | `0` | Locally accumulated number of translated characters | | `translate.maxCharacters` | `4000` | Maximum number of characters translated per README | Configuration is never cached: the file is only a few hundred bytes, so re-reading it costs nothing and makes changes take effect immediately. Unknown fields are preserved as-is; missing fields fall back to their defaults. On first run, if `/dsh-extras.json` does not exist yet, the plugin attempts a one-time migration of `translate` and `githubProxy` from `%APPDATA%/DSH-Desktop/config.json` (or `DSH Desktop`, `dsh-desktop`), so previously entered keys and proxy addresses keep working. ## HTTP Routes The host half exposes the following routes under the `/dsh-desktop-settings/api` prefix. | Method | Path | Description | | --- | --- | --- | | GET | `/info` | Version, data directory, runtime directory, host state, and this plugin's id | | GET | `/config` | Reads the full configuration (internal path fields excluded) | | POST | `/config` | Deep-merges a patch into the configuration and returns the result | | POST | `/open-external` | Opens a URL with the system default handler (http / https only) | | POST | `/pick-directory` | Picks a directory: the official `directoryPicker` first, then a PowerShell folder dialog | | GET | `/restart-pending` | Whether profile files were modified after the host became ready | | POST | `/restart-host` | Deliberately refuses, see below | | GET | `/events` | Incremental fetch from the event queue, parameter `since` | | POST | `/settings/test-translation` | DeepL connectivity self-test | | POST | `/settings/test-github-proxy` | GitHub acceleration proxy connectivity self-test | | POST | `/market/clear` | Clears market cache, download leftovers, and translation cache | | GET | `/recharge/status` | Top-up page information and API key source | | POST | `/recharge/open` | Opens the top-up page | | GET | `/settings/inspect-homes` | Deliberately reports "not applicable", see below | | POST | `/settings/import-from` | Deliberately reports "not applicable", see below | The request body limit is 8MB; unknown routes return 404 with a JSON body. ## Deliberately Unimplemented Capabilities The three routes below exist but report a clear failure instead of being silently ignored or returning an empty result. ### Cross-DSH_HOME detection and import `GET /settings/inspect-homes` and `POST /settings/import-from` correspond to "reuse configuration from another DSH data directory". That capability presupposes a shell with a fully isolated data directory, whereas the official desktop build does not isolate `~/.dsh` and has a single home directory, so it does not apply. Both routes return a readable reason instead of 404, so that anyone constructing a request by hand gets an explanation. The full implementation of that capability is kept in `host/services/settings-ops.mjs` for environments that do need isolation semantics. ### Restarting the host `POST /restart-host` returns `插件无法重启宿主进程,请手动退出并重新打开 DeepSeek Harness`. In some shells DSH is a child process they spawned and can therefore be restarted. Here the plugin itself is part of what the host loaded, so it cannot make the host restart itself, and killing the process would also lose the user's sessions. The route therefore refuses honestly, and the UI shows "please restart manually". ## Activation Criteria | Half | Activation criterion | | --- | --- | | Host half | The package name appears in the profile's `dsh.profile.bundles` array; the host assembles the plugin tree at startup | | Client half | `dsh.client.platform === 'web'`, plus an environment that provides the official client module loader and the `@deepseek-ai/dsh-client-ui-slots` slots | The client half registers into the following slots: | Slot | Content | | --- | --- | | `settings.section` | The "桌面设置补充" section (`order: 30`) | | `sidebar.panellist` | The "API 充值" sidebar entry (`order: 44`); clicking it opens the top-up page | | `main` | The top-up page panel | The plugin library toggle in the official desktop build rewrites `dsh.profile.bundles` when saving (it calls `sanitizeProfile(...)` internally to strip non-official bundles). If the plugin stops working, check first whether that entry is still present. ## Testing ```bash npm test # equivalent to node test/check.mjs ``` `test/check.mjs` is an offline self-check that uses only Node built-ins and does not depend on the DSH runtime. It covers: - the host half can be imported and exports `name` / `inject` / `apply`; - the file referenced by `dsh.bundle.patch` in `package.json` really exists; - `dsh.client.platform === 'web'` and the file referenced by `exports['./client']` really exists; - **route alignment**: every route path called from `client.js` is extracted and checked one by one against the route table in `index.js`; - the defaults in `host/config.mjs` include the `githubToken` field. End-to-end verification requires a real machine: after installing and restarting the desktop build, the settings page should show the "桌面设置补充" section, the sidebar should show the "API 充值" entry, and both "测试连通性" buttons should return a result. ## Known Limitations - **You must register `dsh.profile.bundles` manually after installing.** A manual pnpm install does not do it, and the symptom of missing it is a completely silent plugin — no error, no log, and no visible change in the UI. - **Whether the client half can reach the host can only be confirmed in a real window.** The page origin has three possibilities, so the transport layer probes candidates in order; picking the wrong one leaves a "宿主基址 = …" line in the console. - **This repository does not include the server-side reverse proxy configuration.** The `deploy/ghproxy.conf` referenced in code comments is an nginx configuration example used when setting up the proxy and is not distributed with this package. The path prefixes and locations must match the table above; a mismatch raises no error and simply returns 404 quietly. - **Self-hosted reverse proxies are usually plain HTTP.** When `githubToken` is set, the Authorization header travels over it in clear text; confirm the link is trustworthy first, and prefer not to configure a token while the proxy is enabled. - **The plugin cannot restart the host.** After installing or changing configuration you must quit and reopen DeepSeek Harness manually. - **Translation quota is approximated by a local counter.** `translate.characterCount` only adds up the characters this plugin sent and may not match the actual DeepL account usage; it is only meant to show the rough scale on the settings page. - **If the official implementation changes the profile structure or the `process.argv` layout, the host half's path resolution breaks.** That resolution is centralised in `host/env.mjs`; the symptom is being unable to locate the profile or the bundled pnpm. ## License [MIT](./LICENSE)