dsh-sub2api-sync 0.2.8 → 0.3.0

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/README.md CHANGED
@@ -2,61 +2,47 @@
2
2
 
3
3
  Sync one [sub2api](https://github.com/Wei-Shaw/sub2api) instance's groups, keys, and models into [`dsh-llm-pi-ai`](https://github.com/deepseek-ai/deepseek-harness) provider routes: log in once, integrate automatically, keep syncing.
4
4
 
5
- Design Agent Note: see [the design notes](../../../docs/design/2026-09-04-sub2api-provider-sync.md) and [Chinese version](../../../docs/design/2026-09-04-sub2api-provider-sync.zh.md).
5
+ Design notes: [Phase 1 sync engine](../../../docs/design/2026-09-04-sub2api-provider-sync.md) and the [client UI revamp](../../../docs/design/2026-09-30-sub2api-sync-client-ui.zh.md) (Chinese; the current implementation baseline).
6
6
 
7
- ## Status: Phase 1 (key-first, Mode B)
7
+ ## Status: Mode A (panel-driven) + client UI
8
8
 
9
- Phase 1 ships the key-first path: declare groups by hand in the settings card, paste each group's relay key, and sync. The panel-driven path (Mode A: email/password login, 2FA, automatic key creation, group discovery, models.dev metadata enrichment, scheduled resync, the `sub2api_sync` tool) lands in Phase 2.
9
+ - **Sign-in channels**: panel password + 2FA (C3) is shipped; web authorization (C2, OAuth authorization code + PKCE) lands once sub2api deploys its OAuth endpoints. PAT (C1) was removed with this plugin — sub2api upstream never shipped the matching endpoint, so the channel has no token source;
10
+ - **Keys-driven sync**: the Keys card lists the panel's existing relay keys (none ticked by default) → **tick one or many** (cross-group) → "Sync selected" → **one key becomes one provider** (route `sub2api-<10-hex>`, no sequence id) → models are selectable on the next request; create new keys with the "Create key" dialog (defaulting to the group's name);
11
+ - **Provider naming**: rendered from `providerNameTemplate`, default `{siteName}({keyName})` — `<site name>(<key name>)`, the site name comes from instance probing;
12
+ - **UI**: the top-level settings card (`settings.section`), the plugin detail page's connect card and sync report (`plugins.detail.section`), and the plugin-page tuning card (`plugins.bundle.config`), styled after `ui-settings-account`, fully bilingual (zh/en);
13
+ - **Data seam**: configuration reads/writes go through the platform `configForms` (field-level, revision-fenced, schema-validated); host routes only carry actions, secrets, and status projections;
14
+ - The fingerprint routes (old Mode B) and group-driven sync (rev.5) are both retired — sync anchors real panel key ids.
10
15
 
11
16
  ## Install
12
17
 
13
18
  ### User install (release channels)
14
19
 
15
- The harness loads this package through its `main`/`exports` entry, which is the
16
- **built** `lib/index.js`. Tarballs from any channel below ship `lib/` already —
17
- no local build needed:
20
+ The harness loads the plugin through the package's `main`/`exports` entry — the **built** `lib/index.js`. Tarballs from any channel below ship `lib/` already; no local build needed:
18
21
 
19
- **npm** — published under two names simultaneously: same code, same version.
20
- The scoped name is canonical; the unscoped short name is an alias. Either
21
- installs as a first-class plugin:
22
+ **npm** — two names published in lockstep: same code, same version, the scoped name is canonical and the unscoped short name is an alias. Either installs:
22
23
 
23
24
  ```sh
24
25
  dsh plugin --profile web add @mangoc/dsh-sub2api-sync # canonical (scoped)
25
26
  dsh plugin --profile web add dsh-sub2api-sync # alias (unscoped short name)
26
27
  ```
27
28
 
28
- **GitHub Releases tarball** (no npm involved; the URL always points to the
29
- latest release):
29
+ **GitHub Releases tarball** (npm not involved; the URL always points to the latest release):
30
30
 
31
31
  ```sh
32
32
  dsh plugin --profile web add https://github.com/Shuai-S/dsh-sub2api-sync/releases/latest/download/dsh-sub2api-sync.tgz
33
33
  ```
34
34
 
35
- **From source** (pnpm runs the `prepare` build on git dependencies; needs a
36
- local toolchain and is slower on first install):
35
+ **From source** (pnpm runs the `prepare` build on git dependencies; needs a local toolchain, slower on first install):
37
36
 
38
37
  ```sh
39
38
  dsh plugin --profile web add github:Shuai-S/dsh-sub2api-sync#path:packages/llm/sub2api-sync
40
39
  ```
41
40
 
42
- After installing, restart the app or toggle the plugin off/on so the entry is
43
- imported again; a failed entry is not retried on its own.
41
+ After installing, restart the app or toggle the plugin off/on so the entry is imported again; a failed entry is not retried on its own.
44
42
 
45
43
  ### Release (maintainers)
46
44
 
47
- Every release publishes **both names in lockstep**
48
- ([scripts/pack-all.mjs](scripts/pack-all.mjs)): one tarball for the scoped
49
- canonical name and one for the unscoped alias, sharing the same version. Each
50
- variant embeds its own plugin name at pack time (client-bundle `PKG_NAME`,
51
- ModuleLoader wrapper id, `cordis.patch.yml`), so both install independently.
52
- CI ([.github/workflows/release.yml](../../../.github/workflows/release.yml))
53
- runs the whole flow: bump `version` in
54
- `packages/llm/sub2api-sync/package.json` → commit →
55
- `git tag vX.Y.Z && git push origin vX.Y.Z` → `pnpm check` → two tarballs →
56
- GitHub Release (1 stable-name tarball asset) → npm dual
57
- publish (requires the repo secret `NPM_TOKEN`; `publishConfig.registry` is
58
- pinned to the official npmjs registry, immune to mirror configs). Manual dual
59
- publish from a local checkout:
45
+ Every release publishes **both names in lockstep** ([scripts/pack-all.mjs](scripts/pack-all.mjs)): one tarball for the scoped canonical name and one for the unscoped alias, sharing the same version. The plugin name embedded in the client bundle (mount key, detail-page matching, ModuleLoader id) and `cordis.patch.yml` are injected per package name at pack time, so both variants install independently. CI ([.github/workflows/release.yml](../../../.github/workflows/release.yml)) runs the whole flow: bump `version` in `packages/llm/sub2api-sync/package.json` → commit → `git tag vX.Y.Z && git push origin vX.Y.Z` → `pnpm check` → two tarballs → npm dual publish (requires the repo secret `NPM_TOKEN`; `publishConfig.registry` is pinned to the official npmjs registry, immune to mirror configs); the GitHub Release carries the short-name tarball asset. Manual dual publish from a local checkout:
60
46
 
61
47
  ```sh
62
48
  pnpm pack:all # emits both tarballs into dist/; repo files restored afterwards
@@ -64,14 +50,11 @@ npm publish dist/mangoc-dsh-sub2api-sync-*.tgz --registry=https://registry.npmjs
64
50
  npm publish dist/dsh-sub2api-sync-*.tgz --registry=https://registry.npmjs.org --access public
65
51
  ```
66
52
 
67
- Publishing only the canonical package also works:
68
- `pnpm --filter @mangoc/dsh-sub2api-sync publish`.
53
+ Publishing only the canonical package also works: `pnpm --filter @mangoc/dsh-sub2api-sync publish`.
69
54
 
70
55
  ### Local development (linked checkout)
71
56
 
72
- `dsh plugin add` only symlinks a local directory (`link:`); it does **not**
73
- install or build the target, so building first is a hard precondition — not
74
- just a development convenience:
57
+ `dsh plugin add` only symlinks a local directory (`link:`); it does **not** install dependencies or build the target, so building first is a hard precondition — not a development-time optional step:
75
58
 
76
59
  ```sh
77
60
  pnpm install # installs deps (runs the `prepare` build)
@@ -79,65 +62,64 @@ pnpm build # tsdown + client wrapper; emits lib/index.js and lib/clie
79
62
  dsh plugin --profile web add ./packages/llm/sub2api-sync
80
63
  ```
81
64
 
82
- After rebuilding, restart the app or toggle the plugin off/on so the entry is
83
- imported again; a failed entry is not retried on its own.
84
-
85
- ## Configuration
86
-
87
- Open **Settings → Sub2API 同步** (or edit `$DSH_HOME/settings.yaml` under the `sub2api-sync:` section; the cordis.yml entry is the base layer):
88
-
89
- ```yaml
90
- sub2api-sync:
91
- baseURL: https://sub2api.example.com
92
- manualGroups:
93
- "12":
94
- name: Claude 拼车
95
- platform: anthropic
96
- "15":
97
- platform: openai
98
- # Optional:
99
- enabledGroups: [] # empty = all declared groups
100
- protocolOverrides:
101
- "15": openai-completions # force a wire protocol per group
102
- modelListOverrides:
103
- "15": [gpt-x, gpt-y] # fallback when /v1/models is unreachable
104
- compatTemplate:
105
- compat:
106
- supportsDeveloperRole: false # inherited by every synced model entry
107
- ```
65
+ After rebuilding, restart the app or toggle the plugin off/on so the entry is imported again; a failed entry is not retried on its own.
66
+
67
+ ## Usage
68
+
69
+ 1. Click **Connect panel** (from the settings card or the detail page's connect card), fill the **instance URL** (e.g. `https://cn.api.nexarelay.com`, optionally with `/v1`) plus credentials (panel email/password; TOTP accounts enter the one-time code when prompted; the password is stored in the credential store, so later sign-ins need no retyping);
70
+ 2. The **Keys card** lists the instance's relay keys (newest first, 5 per page, "Load more" at the bottom). The checkbox in front of each row selects it for sync; "**Create key**" in the corner opens a dialog: pick a group → the name defaults to the group's name → the new key is ticked automatically;
71
+ 3. Tick the keys to use (multi-select, cross-group) and hit "**Sync selected (N)**" — the engine builds **one provider per ticked key**, discovers models, maps the wire protocol from the owning group's platform (anthropic → anthropic-messages, the OpenAI family → openai-responses, the rest → openai-completions, overridable per group), and writes them into `llm-pi-ai.providers`;
72
+ 4. Models are selectable on the next request. The "Sync" block at the bottom of the main card also triggers a full sync and shows the complete report; every report entry carries a "**Remove**" button — local routes and credentials only (**no panel calls**, works even with an empty balance) — and unticks the key so the next sync cannot resurrect it; the optional tuning fields live on the **Plugins page → Sub2API Sync** config card.
73
+
74
+ ## dsh-Go (Android) adaptation
75
+
76
+ The plugin adapts to [DSH Go](https://github.com/ccch1mneyyy/dsh-Go)'s native settings capabilities (native protocol v1):
77
+
78
+ - **Native settings page** (Settings › Plugins › Sub2API Sync): every field applies instantly (volatile) with bilingual descriptions; the engine-maintained `routeKeyIds` map is hidden natively and advanced groups (retry policy and the like) start collapsed; the **panel password** is a credential slot (`DSH_SUB2API_PANEL_PASSWORD`) — set it from the phone and both the web card and the native panel sign in without retyping (the per-instance ref wins; this slot is the fallback);
79
+ - **Native surfaces**: a status strip above the input (routes · last sync) and a **Sync status** panel (chat details › Plugins tab): sign-in state, instance, route list, and **Sync now / Sign in / Sign out** actions; long actions run in the background and refresh on settle, failures show in the panel's callout. Requires the dsh-go bridge on the host (bundled with dsh-Go's local engine; remote hosts usually lack it, and nothing shows over DSH Remote);
80
+ - **Web island**: the full settings card works in dsh-Go's web mode. Over DSH Remote the plugin's action routes are not relayed — the card shows an "action routes unreachable" banner; sign in and sync from the desktop DSH Web instead;
81
+ - Two-factor (TOTP) accounts finish only the first leg natively; complete the second leg in the web settings card.
82
+
83
+ ## Security
108
84
 
109
- Relay keys never live in settings: paste each group's key once in the card; it is stored through the harness credential seam under the deterministic reference `DSH_SUB2API_KEY_<group-id>`.
85
+ - All secrets (panel tokens, passwords, relay keys) live only in the harness credential store (deterministic refs `DSH_SUB2API_TOKEN_<instance hash>`, `DSH_SUB2API_KEY_<key-id>`, `DSH_SUB2API_PASSWORD_<instance hash>`, and the dsh-Go native slot `DSH_SUB2API_PANEL_PASSWORD`); never in settings, logs, or sessions;
86
+ - `GET /keys` returns only key metadata (id/name/group/status), **never plaintext**; plaintext is used by the engine to resolve that key and write it into the credential store immediately;
87
+ - Host action routes accept loopback same-origin requests only; over cross-device web access the configuration is read-only (platform `memory` mode) and actions require `allowRemoteActions` in `cordis.yml` (off by default).
110
88
 
111
89
  ## What a sync does
112
90
 
113
- For every declared group with a stored key, the engine:
91
+ For every ticked key, the engine (idempotent diff + single-flight + optimistic concurrency):
114
92
 
115
- 1. calls `GET {instance}/v1/models` with that group's key (falling back to `modelListOverrides` on failure);
116
- 2. maps the group's platform onto a wire protocol — `anthropic` → `anthropic-messages`, the OpenAI family (`openai`/`grok`/`kimi`/`zhipu`/`deepseek`) → `openai-responses`, the rest (`gemini`/`antigravity`/`composite`) → `openai-completions` — overridable per group;
117
- 3. derives the profile `baseURL` per protocol (OpenAI-family SDKs get `{instance}/v1`; the Anthropic SDK gets the bare host, since it appends `/v1/messages` itself);
118
- 4. writes a hand-declared `llm-pi-ai` provider profile at `providers.sub2api-<10-hex>` through a scoped `settings.mutate` carrying `expectedRevision` (three conflict-retry attempts), with a lenient retry policy (5 retries / 120 s backoff cap) tuned for proxied upstream 429 windows.
93
+ 1. resolves the key's plaintext (from the panel list when it returns it, otherwise the credential store: the new-style `DSH_SUB2API_KEY_<10-hex>` ref first, then the legacy `DSH_SUB2API_KEY_<key-id>` migration fallback, finally the OAuth reveal endpoint), keeping the credential consistent with the panel;
94
+ 2. calls `GET {instance}/v1/models` with that key (falling back to `modelListOverrides` on failure);
95
+ 3. maps the **owning group's** `platform` onto a wire protocol and derives the entry `baseURL` (the Anthropic SDK gets the bare host, OpenAI-family SDKs get the `/v1` root);
96
+ 4. writes `providers.sub2api-<10-hex>` through a targeted `settings.mutate` carrying `expectedRevision`, with a lenient retry policy (5 retries / 120 s backoff cap).
119
97
 
120
- Routes take effect on the next request. The route suffix is a SHA-256 truncation over (instance baseURL, panel key id) — stable, collision-free across instances, and it deliberately carries no trace of the panel's sequential key numbering (a shared config screenshot reveals nothing about how many keys the site has issued). Entries only ever cover the `sub2api-<10-hex>` (current) and `sub2api-<digits>` (legacy, migrated on the next sync) shapes — the ownership boundary. Anything else in `llm-pi-ai.providers` (your hand-entered routes, other plugins' routes) is never touched. Keys that disappear from the selection lose their entry and their credential; the remote key itself is only revoked when `deleteOrphanKeys` is enabled. Each **registered-route row** in the sync card carries a **Remove** button — it deletes the local route and credential only (**no panel calls**, so it works even with an empty balance) and unchecks the key so the next sync cannot resurrect it; the report below stays a read-only hint of the last run. Logout, definitive auth loss, and instance switches purge the plugin's sync state site-wide — owned routes, relay-key credentials, and the last sync report go together (a report describing state that no longer exists must not keep rendering).
98
+ The route suffix is a **10-hex-digit truncation of SHA-256(instance URL, key id)** — stable, collision-free across instances, and **deliberately free of the panel's sequential key id**: the route name leaks nothing about how many keys the site has issued, and a shared config screenshot reveals nothing either. Entries only ever cover the `sub2api-<10-hex>` (current) and `sub2api-<digits>` (legacy, migrated away on the next sync) shapes (**one key, one provider**; the display name is the key name rendered from `providerNameTemplate`). Keys that disappear from the selection lose their entry and credential; the remote key is only revoked when `deleteOrphanKeys` is enabled (the key id is reverse-mapped from the panel key list; legacy numeric routes carry their own). Sign-out, definitive auth loss, and instance switches **purge the plugin's sync state site-wide** — owned routes, relay-key credentials, and the **last sync report** go together (a report describing state that no longer exists must not keep rendering).
121
99
 
122
100
  ## Model Experience
123
101
 
124
- - **Context blocks (short form)**: one provider per sub2api group, one relay key per provider, model catalog discovered from the instance; per-model `compat` inherits the user template. Reasoning efforts (`reasoningEfforts`) follow an explicit template when set (including `false`), and otherwise auto-fill per model from the models.dev enrichment.
125
- - **Reasoning-effort auto-fill**: the models.dev `reasoning_options` effort enumeration maps onto pi-ai's `reasoningEfforts` (`none` → `off: null`, same-name passthrough otherwise). Merging is first-party first: a vendor-official entry (openai/anthropic/google/zhipuai/zai/deepseek/moonshotai/xai/alibaba, vendor-governed cloud hosting) wins outright — including when it carries no effort data (budget-only older Claude/Gemini entries), which silences aggregator-invented vocabularies; with no first-party entry, aggregator sources intersect. Toggle/budget-only reasoning (older Claude, Gemini, deepseek-v3.2, kimi-k2.x) has no effort-string wire shape and stays template/`compat`-owned.
126
- - **Native Claude reasoning efforts**: for confirmed adaptive-thinking Claude models with catalog effort metadata (such as Opus 4.6/4.7/4.8 and Sonnet 4.6), syncing over `anthropic-messages` also writes per-model `compat.forceAdaptiveThinking: true`. This SDK switch sends Claude's native `thinking: { type: "adaptive" }` and `output_config: { effort: "max" }` when Max is selected: no OpenAI protocol bridge and no Max-to-token-budget downgrade. Explicit template compat values (including `false`) win; disabled reasoning, legacy/unknown models, missing catalog effort metadata, and other protocols do not auto-enable it. Update the plugin and sync again to repair existing managed routes; subsequent syncs remain idempotent. The usage log records the declared effort, not a measurement of actual thinking.
127
- - **Display-name enrichment**: the models.dev `name` lands on each pi-ai model entry's `name` field — the display name the model selector and the model-catalog editor (显示名称 column) show. The models.dev " (latest)" family marker is stripped, a name repeating the id is dropped, merging is first-party first (official name overrides, aggregator names fill absences), and a name can be an id's only enriched fact.
128
- - Model ids come verbatim from the instance; the provider display name is the `providerNameTemplate` rendering of the key name (fallback `Group <id>`).
102
+ - One provider per group, one relay key each; the model catalog comes from instance discovery; per-model `compat` inherits the user template; reasoning-effort templates win when explicitly set, otherwise they auto-fill per model (see below);
103
+ - **Model metadata enrichment (C4)**: at sync time, per-model facts are filled from the models.dev directory — **display name** (`name`, the model-catalog editor's display-name column), `contextWindow`, `maxTokens`, image input, reasoning efforts. With `enrichModelMeta` on (default) it fetches over the network (24 h in-process cache), falling back to the **bundled snapshot** (`data/models-dev.snapshot.json`, regenerate with `pnpm --filter @mangoc/dsh-sub2api-sync models:refresh`) on failure; off, only the bundled snapshot is used (offline-deterministic). Models hit by neither keep the route-level defaults 128000/8192/text-only;
104
+ - **Reasoning-effort auto-fill**: the models.dev entry's `reasoning_options` effort enumeration maps onto pi-ai's `reasoningEfforts` (`none` → `off: null`, same-name passthrough otherwise). Merging is **first-party first**: a vendor-official entry (openai/anthropic/google/zhipuai/zai/deepseek/moonshotai/xai/alibaba and vendor-governed cloud hosting) wins outright — official entries without effort data (budget-only older Claude/Gemini) likewise silence aggregator-invented vocabularies; with no first-party entry, aggregator sources intersect. Priority: **explicit template setting (including `false`) > per-model auto-fill > field left unset**; the display name is the key name rendered from `providerNameTemplate`.
105
+ - **Native Claude reasoning efforts**: for confirmed adaptive-thinking Claude models with catalog effort metadata (Opus 4.6/4.7/4.8, Sonnet 4.6 and the like), syncing over `anthropic-messages` also writes per-model `compat.forceAdaptiveThinking: true`. The switch makes the SDK send Claude's native `thinking: { type: "adaptive" }` and `output_config: { effort: "max" }` (Max selected as the example) — no OpenAI protocol bridge, no Max-to-token-budget downgrade. User-template compat values of the same name (including explicit `false`) win; disabled reasoning, legacy models, unknown aliases/new versions, missing catalog effort metadata, and other protocols never auto-enable it. Update the plugin and sync again to repair existing managed routes; repeated syncs write nothing twice; no sub2api record values change. Note: the usage log's effort is the request's declared effort, not a measurement of actual thinking.
106
+ - **Display-name enrichment**: the models.dev `name` lands on the pi-ai model entry's `name` field (what the model selector and the "customized model catalog" editor display); the models.dev " (latest)" family marker is stripped, names identical to the model id (no information) are not written, merging is first-party first (official names override, aggregator names fill absences), and name-only entries join the enrichment too.
129
107
 
130
108
  ## Known Limitations and Deferred Work
131
109
 
132
- - **No invariant surface**: this package publishes no `./invariant`. It has no owned relation whose independent observations can diverge — the sync is a settings write with the result readable back from the same service that accepted it.
133
- - Mode A (panel login, 2FA, automatic key creation, group discovery) is Phase 2; until then groups are declared by hand (paste a key — that's all).
134
- - The relay key cannot be reverse-mapped to its group: `/v1/sub2api/billing` and `/v1/usage` carry no group id/name/platform. The card never asks for a group id — the route name derives from the key's FNV-1a fingerprint (`sub2api-<fingerprint>`, idempotent re-add per key); the platform is guessed from the model list (manual override available); a subscription group's name is backfilled from `/v1/usage` `planName`.
135
- - Capacity metadata (context window, max output, image input) follows the models.dev enrichment, with route-level defaults for ids the catalog does not know — expect generous defaults rather than exact figures for unmatched models.
136
- - Usage lookup (`GET /v1/usage`) is a decided non-goal — consult the sub2api console.
137
- - The panel API is a private frontend contract without a stability promise; responses are validated for required fields and fail loudly with context.
138
- - Coexistence with the community plugin `GodD6366/dsh-sub2api`: route names do not collide (ours are `sub2api-<10-hex>` slugs, plus legacy `sub2api-<digits>` cleanup), but each plugin manages its own entries and credential refs — do not expect shared state.
139
- - Requires a dsh release where `dsh-llm-pi-ai` attaches zero usage to replayed assistant messages (multi-turn pi-ai crash fixed on current master / the 0.1.2-rc.1 line).
140
- - REAL composition, disposal, and snapshot tests against a live harness composition are pending; Phase 1 is verified by unit tests plus a live smoke test.
110
+ - **Masked panel plaintext**: if your panel account cannot see key plaintext (non-admin), sync fails and asks you to sign in with an admin account — a deliberate non-silent degradation.
111
+ - **No invariant surface**: this package publishes no `./invariant` (the sync is a settings write, verifiable by reading back from the same service).
112
+ - **Web authorization (C2) awaits sub2api's OAuth endpoint deployment** (design note §12); until then the UI hides the channel automatically.
113
+ - **dsh version floor: 0.2.0-rc.2** (the client half depends on the `configForms`/locale services; 0.1.x-era apps fail loudly).
114
+ - **The client half allows baseline external modules only** (React / Cordis / ui-primitives / ui-slots / client-store / ui-dockkit) — client code must not value-import `config.ts` (it would drag schemastery into the browser bundle; the dynamic module table has no such row and loading fails silently); `test/client-bundle.test.ts` guards the artifact.
115
+ - Behind Cloudflare, the zone must allow the host-side calls to `/api/v1/*` (a WAF skip rule).
116
+ - Metadata enrichment sends one public catalog request to models.dev (no credentials, no conversation content); `enrichModelMeta: false` goes fully offline (bundled snapshot only).
117
+ - Reasoning-effort auto-fill covers only models whose models.dev entry provides an `effort` enumeration (OpenAI gpt-5 line/o3, GLM 5.x, Qwen3.8, DeepSeek V4 line, and the like); toggle/`budget_tokens`-style reasoning (older Claude, Gemini, deepseek-v3.2, kimi-k2.x) has no effort-string wire shape, `reasoningEfforts` cannot express it, and the template/`compat` stays in charge.
118
+ - Usage lookup (`GET /v1/usage`) is a decided non-goal.
119
+ - Coexistence with the community plugin `GodD6366/dsh-sub2api`: route names do not collide (this plugin's `sub2api-<10-hex>` digest shape), and each plugin manages its own entries and credential refs.
120
+ - **The panel API is a private frontend contract** with no stability promise; responses are validated for required fields and fail loudly with context.
121
+ - **Requires a dsh release where `dsh-llm-pi-ai` attaches zero usage to replayed assistant messages** (the multi-turn pi-ai crash is fixed on current master / the 0.1.2-rc.1 line).
122
+ - **REAL composition, disposal, and snapshot tests against a live harness composition are pending**; current verification is unit tests plus a live smoke test.
141
123
 
142
124
  ## Development
143
125
 
package/README.zh.md CHANGED
@@ -9,7 +9,7 @@
9
9
  - **登录通道**:面板密码+2FA(C3)已交付;网页授权(C2,OAuth 授权码 + PKCE)待 sub2api 侧部署后接入。PAT(C1)已随本插件移除——sub2api 上游从未部署对应端点,该通道无令牌来源;
10
10
  - **Keys 驱动同步**:Keys 卡列出面板已有 relay key(缺省全不选)→ **勾选多把**(可跨分组)→「同步所选」→ **一把 key 一个供应商**(路由 `sub2api-<10-hex>`,不含顺序 id)→ 模型下一请求即可用;需要新 key 用「创建 key」弹窗(默认与分组同名);
11
11
  - **供应商命名**:`providerNameTemplate` 渲染,缺省 `{siteName}({keyName})`——即 `<站点名称>(<key 名>)`,站点名称来自实例探测;
12
- - **UI**:设置页顶级卡片(`settings.section`)+ 插件页调参卡(`plugins.bundle.config`),样式对齐 `ui-settings-account`,文案全双语(zh/en);
12
+ - **UI**:设置页顶级卡片(`settings.section`)+ 插件详情页连接卡与同步报告(`plugins.detail.section`)+ 插件页调参卡(`plugins.bundle.config`),样式对齐 `ui-settings-account`,文案全双语(zh/en);
13
13
  - **数据缝**:配置读写走平台 `configForms`(字段级、revision 围栏、schema 校验);宿主路由只承载动作/秘密/状态投影;
14
14
  - 指纹路由(旧 Mode B)与分组驱动同步(rev.5)均已退役——同步锚定真实面板 key id。
15
15
 
@@ -66,14 +66,23 @@ dsh plugin --profile web add ./packages/llm/sub2api-sync
66
66
 
67
67
  ## 使用
68
68
 
69
- 1. 点「登录」,在弹窗里填**实例地址**(如 `https://cn.api.nexarelay.com`,可带 `/v1`)+ 凭据(面板邮箱/密码,TOTP 账号按提示输入一次性验证码;密码会存入凭据存储,之后免输入);
69
+ 1. 点「连接面板」(设置卡或插件详情页的连接卡均可进入),在弹窗里填**实例地址**(如 `https://cn.api.nexarelay.com`,可带 `/v1`)+ 凭据(面板邮箱/密码,TOTP 账号按提示输入一次性验证码;密码会存入凭据存储,之后免输入);
70
70
  2. **Keys 卡**列出该实例已有的 relay key(最新创建在前,首屏 5 条,底部「加载更多」翻页)。每一行前面的勾选框 = 选中用于同步;右上角「**创建 key**」弹窗可新建:选分组 → 名称默认与分组同名 → 创建即自动勾选;
71
71
  3. 勾选好要用的 key(可多选、可跨分组),点「**同步所选(N)**」——引擎为**每一把勾选的 key 建一个供应商**,各自发现模型、按所属分组平台映射协议(anthropic → anthropic-messages,OpenAI 家族 → openai-responses,其余 → openai-completions,可按分组覆盖),写入 `llm-pi-ai.providers`;
72
72
  4. 模型选择器下一请求即可用。主卡下方的「同步」块也能触发一次全量同步并查看完整报告;报告的每个条目带「**移除**」按钮——只删本地路由与凭据(**不调面板**,余额不足也可用),并自动取消该 key 的勾选,下次同步不会复活;可选高级参数在 **插件页 → Sub2API Sync** 调参卡。
73
73
 
74
+ ## dsh-Go(Android)适配
75
+
76
+ 插件适配 [DSH Go](https://github.com/ccch1mneyyy/dsh-Go)(社区 Android 客户端)的原生设置能力(原生协议 v1):
77
+
78
+ - **原生设置页**(设置 › 插件 › Sub2API Sync):全部字段即改即生效(volatile),附中英文说明;引擎内部字段(`routeKeyIds`)在原生页隐藏,重试策略等高级组默认折叠;**面板密码**是凭据槽字段(`DSH_SUB2API_PANEL_PASSWORD`)——手机上即可写入凭据库,之后网页卡与原生面板登录都免输密码(实例派生引用优先,本槽兜底);
79
+ - **原生界面**:输入框上方状态条(路由数 · 上次同步)与「同步状态」面板(对话详情 › 插件页签):登录状态、实例、路由列表、**立即同步 / 登录 / 退出**动作;长动作后台执行、完成即刷新,失败原因显示在面板顶部提示条。需要主机带 dsh-go 桥接(dsh-Go 本机引擎自带;远程主机一般没有,经 DSH Remote 不显示);
80
+ - **网页岛**:完整设置卡在 dsh-Go 的网页版可用;经 DSH Remote 遥控电脑时动作路由不随中继转发——卡片顶部会出现「动作路由不可达」警示条,登录与同步请回到电脑上的 DSH Web;
81
+ - 两步验证(TOTP)账号的原生登录只完成第一段,第二段请到网页版设置卡完成。
82
+
74
83
  ## 安全
75
84
 
76
- - 一切秘密(面板 token、密码、各 relay key)只进 harness 凭据存储(确定性引用 `DSH_SUB2API_TOKEN_<实例哈希>`、`DSH_SUB2API_KEY_<key-id>`、`DSH_SUB2API_PASSWORD_<实例哈希>`),不进 settings、不进日志、不进会话;
85
+ - 一切秘密(面板 token、密码、各 relay key)只进 harness 凭据存储(确定性引用 `DSH_SUB2API_TOKEN_<实例哈希>`、`DSH_SUB2API_KEY_<key-id>`、`DSH_SUB2API_PASSWORD_<实例哈希>`、dsh-Go 原生设置槽 `DSH_SUB2API_PANEL_PASSWORD`),不进 settings、不进日志、不进会话;
77
86
  - `GET /keys` 只回 key 元数据(id/名称/分组/状态),**不回明文**;明文只用于引擎解析该 key 并即时写入凭据存储;
78
87
  - 宿主动作路由只接受回环同源请求;跨设备访问 web UI 时配置只读(平台 `memory` 模式),动作需在 `cordis.yml` 显式开启 `allowRemoteActions`(默认关闭)。
79
88
 
@@ -108,6 +117,9 @@ dsh plugin --profile web add ./packages/llm/sub2api-sync
108
117
  - 推理档位自动填充只覆盖 models.dev 提供 `effort` 枚举的模型(OpenAI gpt-5 系/o3、GLM 5.x、Qwen3.8、DeepSeek V4 系等);`toggle`/`budget_tokens` 型推理(旧版 Claude、Gemini、deepseek-v3.2、kimi-k2.x 等)的线上形态不是 effort 字符串,`reasoningEfforts` 无法表达,仍由模板/`compat` 管辖。
109
118
  - 用量查询(`GET /v1/usage`)是已决策的非目标。
110
119
  - 与社区插件 `GodD6366/dsh-sub2api` 同装:路由名不冲突(本插件 `sub2api-<10-hex>` 摘要形态),各自管理条目与凭据引用。
120
+ - **面板 API 是私有前端契约**:无稳定性承诺;响应按必需字段校验,缺失即带上下文响亮失败。
121
+ - **需要 `dsh-llm-pi-ai` 对重放的 assistant 消息计零用量**的 dsh 版本(多轮 pi-ai 崩溃已在当前 master / 0.1.2-rc.1 线修复)。
122
+ - **针对真实 harness 组合的 REAL 组合/销毁/快照测试待补**:当前以单元测试 + 真机冒烟验证。
111
123
 
112
124
  ## 开发
113
125
 
package/docs/DETAIL.md ADDED
@@ -0,0 +1 @@
1
+ ![Two relay keys checked on the sub2api panel sync into two dsh provider routes, each with a model-count badge](preview.svg)
@@ -0,0 +1 @@
1
+ ![sub2api 面板勾选两把 relay key,经同步生成 dsh 的两条供应商路由,各带模型数量徽章](preview.zh.svg)