dsh-qoder-connect 0.1.0 → 0.1.1
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.en.md +147 -147
- package/README.md +2 -2
- package/cordis.patch.yml +6 -6
- package/lib/bin.js +1 -1
- package/lib/client.js +6 -0
- package/lib/index.d.ts +7 -0
- package/lib/index.js +83 -7
- package/lib/{variants-jbFhMKO8.js → variants-CQg-9veJ.js} +128 -8
- package/package.json +116 -116
package/README.en.md
CHANGED
|
@@ -1,147 +1,147 @@
|
|
|
1
|
-
# DSH Qoder Connect
|
|
2
|
-
|
|
3
|
-
**English** | [简体中文](./README.md)
|
|
4
|
-
|
|
5
|
-
A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH) plugin that brings your **Qoder subscription** models into DSH through a Personal Access Token (PAT). One plugin serves both Qoder product lines — each with its own settings card, provider id, credential file and saved catalog, never mixed:
|
|
6
|
-
|
|
7
|
-
| Variant | Provider id | Card title | Where the PAT is minted |
|
|
8
|
-
|---|---|---|---|
|
|
9
|
-
| China (国内版) | `qoder` | Qoder | qoder.com.cn |
|
|
10
|
-
| International (国际版) | `qoder-global` | Qoder Global | qoder.com |
|
|
11
|
-
|
|
12
|
-
Configure only what you use: a variant with no saved PAT shows no model group and never interferes with the other one. Streaming, reasoning content and tool calls ride the plugin's built-in Qoder transport; the conversation loop, compaction and permissions stay Harness-owned.
|
|
13
|
-
|
|
14
|
-
## Features
|
|
15
|
-
|
|
16
|
-
- **PAT: save it and it works** — paste a PAT into the card and press *Save*; the token is validated live against that variant's region before anything is written, and the model group appears immediately — no DSH restart. Once signed in, *Replace PAT* (the new token overwrites only after it validates, so a mistyped paste can never strand a working credential) and *Clear PAT* are one click away.
|
|
17
|
-
- **Two independent variants** — `qoder` (china) and `qoder-global` (global) keep separate credential files, routes and saved catalogs: a token minted on qoder.com does not work against the China deployment and vice versa.
|
|
18
|
-
- **Three-tier model catalog (live → saved → fallback)** — the card states where the list on screen came from: *“Model list updated …”* (fetched now), *“Showing the saved model list from …”* (this account's last successful fetch, restored after a restart or a failed fetch), or *“Showing the built-in model list (not yet updated from Qoder)”* (the roster compiled into the plugin). The reason of the most recent failed fetch is shown too, and a *Refresh model list* button sits right there.
|
|
19
|
-
- **One-toggle context window** — each variant card's *Context window* tab lists every model's capacity (the default and the largest declared window) and carries its own *“Use the largest declared context window”* preference (on by default): on, requests declare the maximum (e.g. Qwen3.8-Max at 1M); off, the default (200K). The two variants keep independent toggle state.
|
|
20
|
-
- **Sidebar quota card + credit details** — the *“Qoder sidebar display”* card toggles a per-variant sidebar quota widget (off by default; each toggle needs its variant's PAT saved) with one shared refresh interval (default 5 min, minimum 1 min). Clicking the sidebar widget opens the *Qoder quota* panel: one row per credit package — *“Remaining / Total + bar | Expires”* — plus the cycle share and reset time. Click the same widget again to close; click the other to switch variants.
|
|
21
|
-
- **Rate display `x<priceFactor>`** — each model name is suffixed with the upstream-reported price multiplier (e.g. `Some Model · x0.79`, free is `x0`), spelled `x<n>` from the catalog's `price_factor`. Display only — it never changes the request; models whose rate the upstream did not report simply show no suffix.
|
|
22
|
-
|
|
23
|
-
## Screenshots
|
|
24
|
-
|
|
25
|
-
### Sidebar quota widgets & credit details
|
|
26
|
-
|
|
27
|
-
Once enabled on the *“Qoder sidebar display”* card, a per-variant quota widget sits at the sidebar's foot; clicking it opens the *Qoder quota* panel with one row per credit package, switchable between the two variants at any time.
|
|
28
|
-
|
|
29
|
-
**China (after clicking the sidebar widget):**
|
|
30
|
-
|
|
31
|
-

|
|
32
|
-
|
|
33
|
-
**International (after clicking the sidebar widget):**
|
|
34
|
-
|
|
35
|
-

|
|
36
|
-
|
|
37
|
-
### Sidebar toggles
|
|
38
|
-
|
|
39
|
-
The *“Qoder sidebar display”* card controls both widgets' visibility and the shared refresh interval:
|
|
40
|
-
|
|
41
|
-

|
|
42
|
-
|
|
43
|
-
### Picking a model in a session
|
|
44
|
-
|
|
45
|
-
Once a PAT is saved, the model picker gains a Qoder group with per-model rate multipliers (`x0.5`, `x0`, …):
|
|
46
|
-
|
|
47
|
-

|
|
48
|
-
|
|
49
|
-
### Context-window settings
|
|
50
|
-
|
|
51
|
-
Each variant card's *Context window* tab lists capacities per model under the *“Use the largest declared context window”* preference:
|
|
52
|
-
|
|
53
|
-
**China:**
|
|
54
|
-
|
|
55
|
-

|
|
56
|
-
|
|
57
|
-
**International:**
|
|
58
|
-
|
|
59
|
-

|
|
60
|
-
|
|
61
|
-
## Install
|
|
62
|
-
|
|
63
|
-
Prerequisites:
|
|
64
|
-
|
|
65
|
-
- DSH core `0.1.5-rc.1` or newer (this plugin peers on the `@deepseek-ai/*` `^0.1.5-rc.1` line);
|
|
66
|
-
- Node.js `^22.19.0 || >=24.0.0` (per `package.json` engines);
|
|
67
|
-
- your own Qoder account and at least one Personal Access Token.
|
|
68
|
-
|
|
69
|
-
```sh
|
|
70
|
-
# From GitHub (recommended)
|
|
71
|
-
dsh plugin --profile web add github:masknull/dsh-qoder-connect
|
|
72
|
-
# Or from npm, once published
|
|
73
|
-
dsh plugin --profile web add dsh-qoder-connect
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
Swap the `--profile` value for the profile you use (`web` / `desktop` / `tui`, e.g. `dsh plugin --profile desktop add dsh-qoder-connect`) — data stays inside that profile, so web / desktop / tui never collide. On a terminal-only profile (TUI) there is no browser card: manage the PAT and check status through the bundled CLI instead; the settings card and sidebar widgets require Web or Desktop.
|
|
77
|
-
|
|
78
|
-
## Configuration
|
|
79
|
-
|
|
80
|
-
### Mint a PAT
|
|
81
|
-
|
|
82
|
-
- **Qoder (China)**: sign in at qoder.com.cn → account settings → Personal Access Token.
|
|
83
|
-
- **Qoder Global**: sign in at qoder.com → account settings → Personal Access Token.
|
|
84
|
-
|
|
85
|
-
(The card repeats the same guidance: *“Generate a PAT in your qoder.com account settings (Account → Personal Access Token), then paste it here.”*)
|
|
86
|
-
|
|
87
|
-
### Save it on the card
|
|
88
|
-
|
|
89
|
-
DSH → Settings → Plugins → the variant's card: paste the PAT into the password field and press *Save* (*“Validating and saving…”* while it runs). The token is validated against that variant's region first — a refusal saves nothing and the card says *“That PAT was rejected — generate a new one in your account settings and try again.”*. Do this on both cards to run both groups side by side.
|
|
90
|
-
|
|
91
|
-
### Environment-variable fallback
|
|
92
|
-
|
|
93
|
-
For headless setups, the plugin reads an environment fallback **only when no credential file exists**:
|
|
94
|
-
|
|
95
|
-
| Variant | Environment variable |
|
|
96
|
-
|---|---|
|
|
97
|
-
| `qoder` (china) | `QODER_CN_PERSONAL_ACCESS_TOKEN` |
|
|
98
|
-
| `qoder-global` | `QODER_PERSONAL_ACCESS_TOKEN` |
|
|
99
|
-
|
|
100
|
-
The precedence is **file > env**: as soon as a valid credential file is saved, a stray environment token stops mattering (the saved token always wins). Env-sourced credentials show as *source: environment variable* on the card, with no saved-at time.
|
|
101
|
-
|
|
102
|
-
## Data & privacy
|
|
103
|
-
|
|
104
|
-
### File layout
|
|
105
|
-
|
|
106
|
-
Everything the plugin owns lives in one data directory: `<profile>/.dsh-qoder-connect/` — credentials at the root, rebuildable caches under `state/` (so “clear the cache” can never touch a credential):
|
|
107
|
-
|
|
108
|
-
```text
|
|
109
|
-
<profile>/.dsh-qoder-connect/
|
|
110
|
-
├── .qoder-auth.json # China PAT ({version:2, pat, region, savedAt})
|
|
111
|
-
├── .qoder-global-auth.json # Global PAT
|
|
112
|
-
└── state/
|
|
113
|
-
├── .qoder-catalog.json # China per-account saved catalog
|
|
114
|
-
├── .qoder-global-catalog.json
|
|
115
|
-
├── .qoder-probe.json # China reasoning-effort probe records
|
|
116
|
-
├── .qoder-global-probe.json
|
|
117
|
-
├── .qoder-host-heartbeat.json # host heartbeat (doctor uses it)
|
|
118
|
-
└── .qoder-machine-id # transport machine-id seed (never written to ~/.qoder)
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
The directory follows the profile that declares this plugin (found under `$DSH_HOME/profiles/`), falling back to the Harness home when none can be determined; `DSH_QODER_DATA_DIR` overrides it explicitly.
|
|
122
|
-
|
|
123
|
-
## Development
|
|
124
|
-
|
|
125
|
-
```sh
|
|
126
|
-
pnpm install
|
|
127
|
-
pnpm run build # tsdown → lib/ (host bundle + client bundle)
|
|
128
|
-
pnpm test # vitest run (tests/**/*.spec.ts)
|
|
129
|
-
pnpm run test:qoder # node --test (tests/qoder/*.test.ts, the Qoder transport suite)
|
|
130
|
-
pnpm run typecheck # tsc across the host and client tsconfigs
|
|
131
|
-
pnpm run check # typecheck + test + test:qoder + build in one gate
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
The upstream `node:test` suite for the Qoder transport layer now lives in `tests/qoder/` (run it with `pnpm run test:qoder`; the pristine `reference-qoder-tests/` copy has been removed) — `package.json` scripts are the source of truth.
|
|
135
|
-
|
|
136
|
-
## Provenance & license
|
|
137
|
-
|
|
138
|
-
- Derived from [masknull/dsh-workbuddy-connect](https://github.com/masknull/dsh-workbuddy-connect) (MIT) — the connect-plugin skeleton this project was refactored from.
|
|
139
|
-
- Qoder transport layer ported from [mo-n/dsh-provider-qoder](https://github.com/mo-n/dsh-provider-qoder) (MIT).
|
|
140
|
-
|
|
141
|
-
Released under the [MIT](./LICENSE) license; see [NOTICE](./NOTICE) for the upstream attributions. This is a community adapter — not affiliated with, authorized by, or endorsed by Qoder or DeepSeek, and not an official implementation.
|
|
142
|
-
|
|
143
|
-
## Disclaimer
|
|
144
|
-
|
|
145
|
-
- Qoder's endpoints come from the upstream repositories: upstream may change, rate-limit or block them at any time, breaking part or all of this plugin with no compatibility window to promise.
|
|
146
|
-
- For personal learning and research only; this tool drives **your own** Qoder account. Do not use it commercially or beyond reasonable personal use. You are responsible for complying with Qoder's terms of service and for any consequence — account restriction, quota loss, service interruption — of using this project.
|
|
147
|
-
- The authors are not liable for any direct or indirect loss arising from use or misuse of this project. The names Qoder, DeepSeek and related marks belong to their respective owners and appear here only to describe compatibility.
|
|
1
|
+
# DSH Qoder Connect
|
|
2
|
+
|
|
3
|
+
**English** | [简体中文](./README.md)
|
|
4
|
+
|
|
5
|
+
A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH) plugin that brings your **Qoder subscription** models into DSH through a Personal Access Token (PAT). One plugin serves both Qoder product lines — each with its own settings card, provider id, credential file and saved catalog, never mixed:
|
|
6
|
+
|
|
7
|
+
| Variant | Provider id | Card title | Where the PAT is minted |
|
|
8
|
+
|---|---|---|---|
|
|
9
|
+
| China (国内版) | `qoder` | Qoder | qoder.com.cn |
|
|
10
|
+
| International (国际版) | `qoder-global` | Qoder Global | qoder.com |
|
|
11
|
+
|
|
12
|
+
Configure only what you use: a variant with no saved PAT shows no model group and never interferes with the other one. Streaming, reasoning content and tool calls ride the plugin's built-in Qoder transport; the conversation loop, compaction and permissions stay Harness-owned.
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- **PAT: save it and it works** — paste a PAT into the card and press *Save*; the token is validated live against that variant's region before anything is written, and the model group appears immediately — no DSH restart. Once signed in, *Replace PAT* (the new token overwrites only after it validates, so a mistyped paste can never strand a working credential) and *Clear PAT* are one click away.
|
|
17
|
+
- **Two independent variants** — `qoder` (china) and `qoder-global` (global) keep separate credential files, routes and saved catalogs: a token minted on qoder.com does not work against the China deployment and vice versa.
|
|
18
|
+
- **Three-tier model catalog (live → saved → fallback)** — the card states where the list on screen came from: *“Model list updated …”* (fetched now), *“Showing the saved model list from …”* (this account's last successful fetch, restored after a restart or a failed fetch), or *“Showing the built-in model list (not yet updated from Qoder)”* (the roster compiled into the plugin). The reason of the most recent failed fetch is shown too, and a *Refresh model list* button sits right there.
|
|
19
|
+
- **One-toggle context window** — each variant card's *Context window* tab lists every model's capacity (the default and the largest declared window) and carries its own *“Use the largest declared context window”* preference (on by default): on, requests declare the maximum (e.g. Qwen3.8-Max at 1M); off, the default (200K). The two variants keep independent toggle state.
|
|
20
|
+
- **Sidebar quota card + credit details** — the *“Qoder sidebar display”* card toggles a per-variant sidebar quota widget (off by default; each toggle needs its variant's PAT saved) with one shared refresh interval (default 5 min, minimum 1 min). Clicking the sidebar widget opens the *Qoder quota* panel: one row per credit package — *“Remaining / Total + bar | Expires”* — plus the cycle share and reset time. Click the same widget again to close; click the other to switch variants.
|
|
21
|
+
- **Rate display `x<priceFactor>`** — each model name is suffixed with the upstream-reported price multiplier (e.g. `Some Model · x0.79`, free is `x0`), spelled `x<n>` from the catalog's `price_factor`. Display only — it never changes the request; models whose rate the upstream did not report simply show no suffix.
|
|
22
|
+
|
|
23
|
+
## Screenshots
|
|
24
|
+
|
|
25
|
+
### Sidebar quota widgets & credit details
|
|
26
|
+
|
|
27
|
+
Once enabled on the *“Qoder sidebar display”* card, a per-variant quota widget sits at the sidebar's foot; clicking it opens the *Qoder quota* panel with one row per credit package, switchable between the two variants at any time.
|
|
28
|
+
|
|
29
|
+
**China (after clicking the sidebar widget):**
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
33
|
+
**International (after clicking the sidebar widget):**
|
|
34
|
+
|
|
35
|
+

|
|
36
|
+
|
|
37
|
+
### Sidebar toggles
|
|
38
|
+
|
|
39
|
+
The *“Qoder sidebar display”* card controls both widgets' visibility and the shared refresh interval:
|
|
40
|
+
|
|
41
|
+

|
|
42
|
+
|
|
43
|
+
### Picking a model in a session
|
|
44
|
+
|
|
45
|
+
Once a PAT is saved, the model picker gains a Qoder group with per-model rate multipliers (`x0.5`, `x0`, …):
|
|
46
|
+
|
|
47
|
+

|
|
48
|
+
|
|
49
|
+
### Context-window settings
|
|
50
|
+
|
|
51
|
+
Each variant card's *Context window* tab lists capacities per model under the *“Use the largest declared context window”* preference:
|
|
52
|
+
|
|
53
|
+
**China:**
|
|
54
|
+
|
|
55
|
+

|
|
56
|
+
|
|
57
|
+
**International:**
|
|
58
|
+
|
|
59
|
+

|
|
60
|
+
|
|
61
|
+
## Install
|
|
62
|
+
|
|
63
|
+
Prerequisites:
|
|
64
|
+
|
|
65
|
+
- DSH core `0.1.5-rc.1` or newer (this plugin peers on the `@deepseek-ai/*` `^0.1.5-rc.1` line);
|
|
66
|
+
- Node.js `^22.19.0 || >=24.0.0` (per `package.json` engines);
|
|
67
|
+
- your own Qoder account and at least one Personal Access Token.
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
# From GitHub (recommended)
|
|
71
|
+
dsh plugin --profile web add github:masknull/dsh-qoder-connect
|
|
72
|
+
# Or from npm, once published
|
|
73
|
+
dsh plugin --profile web add dsh-qoder-connect
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Swap the `--profile` value for the profile you use (`web` / `desktop` / `tui`, e.g. `dsh plugin --profile desktop add dsh-qoder-connect`) — data stays inside that profile, so web / desktop / tui never collide. On a terminal-only profile (TUI) there is no browser card: manage the PAT and check status through the bundled CLI instead; the settings card and sidebar widgets require Web or Desktop.
|
|
77
|
+
|
|
78
|
+
## Configuration
|
|
79
|
+
|
|
80
|
+
### Mint a PAT
|
|
81
|
+
|
|
82
|
+
- **Qoder (China)**: sign in at qoder.com.cn → account settings → Personal Access Token, then copy it. [Open the page](https://qoder.cn/account/integrations)
|
|
83
|
+
- **Qoder Global**: sign in at qoder.com → account settings → Personal Access Token, then copy it. [Open the page](https://qoder.com/account/integrations)
|
|
84
|
+
|
|
85
|
+
(The card repeats the same guidance: *“Generate a PAT in your qoder.com account settings (Account → Personal Access Token), then paste it here.”*)
|
|
86
|
+
|
|
87
|
+
### Save it on the card
|
|
88
|
+
|
|
89
|
+
DSH → Settings → Plugins → the variant's card: paste the PAT into the password field and press *Save* (*“Validating and saving…”* while it runs). The token is validated against that variant's region first — a refusal saves nothing and the card says *“That PAT was rejected — generate a new one in your account settings and try again.”*. Do this on both cards to run both groups side by side.
|
|
90
|
+
|
|
91
|
+
### Environment-variable fallback
|
|
92
|
+
|
|
93
|
+
For headless setups, the plugin reads an environment fallback **only when no credential file exists**:
|
|
94
|
+
|
|
95
|
+
| Variant | Environment variable |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `qoder` (china) | `QODER_CN_PERSONAL_ACCESS_TOKEN` |
|
|
98
|
+
| `qoder-global` | `QODER_PERSONAL_ACCESS_TOKEN` |
|
|
99
|
+
|
|
100
|
+
The precedence is **file > env**: as soon as a valid credential file is saved, a stray environment token stops mattering (the saved token always wins). Env-sourced credentials show as *source: environment variable* on the card, with no saved-at time.
|
|
101
|
+
|
|
102
|
+
## Data & privacy
|
|
103
|
+
|
|
104
|
+
### File layout
|
|
105
|
+
|
|
106
|
+
Everything the plugin owns lives in one data directory: `<profile>/.dsh-qoder-connect/` — credentials at the root, rebuildable caches under `state/` (so “clear the cache” can never touch a credential):
|
|
107
|
+
|
|
108
|
+
```text
|
|
109
|
+
<profile>/.dsh-qoder-connect/
|
|
110
|
+
├── .qoder-auth.json # China PAT ({version:2, pat, region, savedAt})
|
|
111
|
+
├── .qoder-global-auth.json # Global PAT
|
|
112
|
+
└── state/
|
|
113
|
+
├── .qoder-catalog.json # China per-account saved catalog
|
|
114
|
+
├── .qoder-global-catalog.json
|
|
115
|
+
├── .qoder-probe.json # China reasoning-effort probe records
|
|
116
|
+
├── .qoder-global-probe.json
|
|
117
|
+
├── .qoder-host-heartbeat.json # host heartbeat (doctor uses it)
|
|
118
|
+
└── .qoder-machine-id # transport machine-id seed (never written to ~/.qoder)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The directory follows the profile that declares this plugin (found under `$DSH_HOME/profiles/`), falling back to the Harness home when none can be determined; `DSH_QODER_DATA_DIR` overrides it explicitly.
|
|
122
|
+
|
|
123
|
+
## Development
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
pnpm install
|
|
127
|
+
pnpm run build # tsdown → lib/ (host bundle + client bundle)
|
|
128
|
+
pnpm test # vitest run (tests/**/*.spec.ts)
|
|
129
|
+
pnpm run test:qoder # node --test (tests/qoder/*.test.ts, the Qoder transport suite)
|
|
130
|
+
pnpm run typecheck # tsc across the host and client tsconfigs
|
|
131
|
+
pnpm run check # typecheck + test + test:qoder + build in one gate
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The upstream `node:test` suite for the Qoder transport layer now lives in `tests/qoder/` (run it with `pnpm run test:qoder`; the pristine `reference-qoder-tests/` copy has been removed) — `package.json` scripts are the source of truth.
|
|
135
|
+
|
|
136
|
+
## Provenance & license
|
|
137
|
+
|
|
138
|
+
- Derived from [masknull/dsh-workbuddy-connect](https://github.com/masknull/dsh-workbuddy-connect) (MIT) — the connect-plugin skeleton this project was refactored from.
|
|
139
|
+
- Qoder transport layer ported from [mo-n/dsh-provider-qoder](https://github.com/mo-n/dsh-provider-qoder) (MIT).
|
|
140
|
+
|
|
141
|
+
Released under the [MIT](./LICENSE) license; see [NOTICE](./NOTICE) for the upstream attributions. This is a community adapter — not affiliated with, authorized by, or endorsed by Qoder or DeepSeek, and not an official implementation.
|
|
142
|
+
|
|
143
|
+
## Disclaimer
|
|
144
|
+
|
|
145
|
+
- Qoder's endpoints come from the upstream repositories: upstream may change, rate-limit or block them at any time, breaking part or all of this plugin with no compatibility window to promise.
|
|
146
|
+
- For personal learning and research only; this tool drives **your own** Qoder account. Do not use it commercially or beyond reasonable personal use. You are responsible for complying with Qoder's terms of service and for any consequence — account restriction, quota loss, service interruption — of using this project.
|
|
147
|
+
- The authors are not liable for any direct or indirect loss arising from use or misuse of this project. The names Qoder, DeepSeek and related marks belong to their respective owners and appear here only to describe compatibility.
|
package/README.md
CHANGED
|
@@ -79,8 +79,8 @@ dsh plugin --profile web add dsh-qoder-connect
|
|
|
79
79
|
|
|
80
80
|
### 生成 PAT
|
|
81
81
|
|
|
82
|
-
- **Qoder(中国版)**:登录 qoder.com.cn → 账号设置 → 个人访问令牌,生成后复制。
|
|
83
|
-
- **Qoder Global(国际版)**:登录 qoder.com → 账号设置 → 个人访问令牌,生成后复制。
|
|
82
|
+
- **Qoder(中国版)**:登录 qoder.com.cn → 账号设置 → 个人访问令牌,生成后复制。[直达获取](https://qoder.cn/account/integrations)
|
|
83
|
+
- **Qoder Global(国际版)**:登录 qoder.com → 账号设置 → 个人访问令牌,生成后复制。[直达获取](https://qoder.com/account/integrations)
|
|
84
84
|
|
|
85
85
|
(卡片内也有同样指引:「请先在 qoder.com.cn 的账号设置(账号 → 个人访问令牌)中生成 PAT,再粘贴到下方输入框。」)
|
|
86
86
|
|
package/cordis.patch.yml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Register the Qoder provider without changing the profile's current
|
|
2
|
-
# default model. Credentials are Personal Access Tokens (PATs) the user
|
|
3
|
-
# pastes into the plugin's card; no OAuth or device flow is started.
|
|
4
|
-
- insert:
|
|
5
|
-
- id: llm-qoder
|
|
6
|
-
name: dsh-qoder-connect
|
|
1
|
+
# Register the Qoder provider without changing the profile's current
|
|
2
|
+
# default model. Credentials are Personal Access Tokens (PATs) the user
|
|
3
|
+
# pastes into the plugin's card; no OAuth or device flow is started.
|
|
4
|
+
- insert:
|
|
5
|
+
- id: llm-qoder
|
|
6
|
+
name: dsh-qoder-connect
|
package/lib/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { B as qoderCredentialIdentity, D as FALLBACK_QODER_MODELS, E as qoderCatalogPath, R as QoderCredentialStore, T as QoderCatalogStore, b as createQoderTransport, f as QODER_CONNECT_VERSION, i as variantFor, l as qoderHostHeartbeatPath, m as QoderUpstreamClient, r as QODER_VARIANTS, s as isHeartbeatProcessAlive, t as CHINA_VARIANT, u as readHostHeartbeat, y as validateApiKey } from "./variants-
|
|
2
|
+
import { B as qoderCredentialIdentity, D as FALLBACK_QODER_MODELS, E as qoderCatalogPath, R as QoderCredentialStore, T as QoderCatalogStore, b as createQoderTransport, f as QODER_CONNECT_VERSION, i as variantFor, l as qoderHostHeartbeatPath, m as QoderUpstreamClient, r as QODER_VARIANTS, s as isHeartbeatProcessAlive, t as CHINA_VARIANT, u as readHostHeartbeat, y as validateApiKey } from "./variants-CQg-9veJ.js";
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import { realpathSync } from "node:fs";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
package/lib/client.js
CHANGED
|
@@ -1107,6 +1107,10 @@ window.__ModuleLoader__.load({
|
|
|
1107
1107
|
status?.status === "signed-in" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1108
1108
|
status.pat === void 0 ? null : patSummaryLine(status.pat),
|
|
1109
1109
|
replacing ? patEntry() : null,
|
|
1110
|
+
status.jobTokenRefreshedAt === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1111
|
+
style: bodyStyle,
|
|
1112
|
+
children: t("jobTokenRefreshed", { time: formatTime(status.jobTokenRefreshedAt) })
|
|
1113
|
+
}),
|
|
1110
1114
|
status.catalog === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1111
1115
|
style: rowStyle$1,
|
|
1112
1116
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -3022,6 +3026,7 @@ window.__ModuleLoader__.load({
|
|
|
3022
3026
|
refreshing: "Refreshing…",
|
|
3023
3027
|
refreshModels: "Refresh model list",
|
|
3024
3028
|
refreshingModels: "Refreshing models…",
|
|
3029
|
+
jobTokenRefreshed: "Job token was auto-refreshed at {time} (upstream rejected the old one; the message recovered).",
|
|
3025
3030
|
catalogLive: "Model list updated {time}",
|
|
3026
3031
|
catalogSaved: "Showing the saved model list from {time}",
|
|
3027
3032
|
catalogFallback: "Showing the built-in model list (not yet updated from Qoder)",
|
|
@@ -3150,6 +3155,7 @@ window.__ModuleLoader__.load({
|
|
|
3150
3155
|
refresh: "刷新",
|
|
3151
3156
|
refreshing: "正在刷新…",
|
|
3152
3157
|
refreshModels: "刷新模型列表",
|
|
3158
|
+
jobTokenRefreshed: "jobToken 已于 {time} 自动刷新(上游拒绝了旧令牌,消息已自动恢复)。",
|
|
3153
3159
|
refreshingModels: "正在刷新模型…",
|
|
3154
3160
|
catalogLive: "模型列表更新于 {time}",
|
|
3155
3161
|
catalogSaved: "当前显示已保存的模型列表,更新于 {time}",
|
package/lib/index.d.ts
CHANGED
|
@@ -246,6 +246,13 @@ type QoderWebStatus = {
|
|
|
246
246
|
probe?: QoderWebProbeSection;
|
|
247
247
|
/** Card preference selecting larger declared context windows. */
|
|
248
248
|
useMaximumContextWindow?: boolean;
|
|
249
|
+
/**
|
|
250
|
+
* The last automatic job-token refresh the self-heal performed, epoch ms.
|
|
251
|
+
* Absent when no refresh has happened in this process. The card renders
|
|
252
|
+
* this as a visible "token auto-refreshed" notice, so an otherwise
|
|
253
|
+
* invisible recovery is observable.
|
|
254
|
+
*/
|
|
255
|
+
jobTokenRefreshedAt?: number;
|
|
249
256
|
/**
|
|
250
257
|
* In-process key authorizing probe control writes. Handed to the card with
|
|
251
258
|
* the status document (the card is same-origin and already had to pass the
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as QODER_GLOBAL_AUTH_PATH, B as qoderCredentialIdentity, C as probeModel, D as FALLBACK_QODER_MODELS, E as qoderCatalogPath, F as QODER_AUTH_FILENAME, G as qoderPluginDataDir, H as QODER_DATA_DIR_ENV, I as QODER_PAT_ENV_CN, K as qoderStateDir, L as QODER_PAT_ENV_GLOBAL, M as QODER_GLOBAL_STATUS_PATH, N as QODER_PROBE_PATH, O as QoderCatalog, P as QODER_STATUS_PATH, R as QoderCredentialStore, S as PROBE_EFFORT_CANDIDATES, T as QoderCatalogStore, U as QODER_DATA_DIR_NAME, V as qoderOwnAuthPath, W as qoderMachineIdPath, _ as modelInfoOf, a as QODER_HOST_HEARTBEAT_FILENAME, b as createQoderTransport, c as processStartTimeMs, d as writeHostHeartbeat, f as QODER_CONNECT_VERSION, g as kindFromQoderFailure, h as classifyUpstreamError, i as variantFor, j as QODER_GLOBAL_PROBE_PATH, k as QODER_AUTH_PATH, l as qoderHostHeartbeatPath, m as QoderUpstreamClient, n as GLOBAL_VARIANT, o as clearHostHeartbeat, p as KIND_STATUS, r as QODER_VARIANTS, s as isHeartbeatProcessAlive, t as CHINA_VARIANT, u as readHostHeartbeat, v as normalizeCredits, w as randomSentinel, x as getMachineId, y as validateApiKey, z as patTail } from "./variants-
|
|
1
|
+
import { A as QODER_GLOBAL_AUTH_PATH, B as qoderCredentialIdentity, C as probeModel, D as FALLBACK_QODER_MODELS, E as qoderCatalogPath, F as QODER_AUTH_FILENAME, G as qoderPluginDataDir, H as QODER_DATA_DIR_ENV, I as QODER_PAT_ENV_CN, K as qoderStateDir, L as QODER_PAT_ENV_GLOBAL, M as QODER_GLOBAL_STATUS_PATH, N as QODER_PROBE_PATH, O as QoderCatalog, P as QODER_STATUS_PATH, R as QoderCredentialStore, S as PROBE_EFFORT_CANDIDATES, T as QoderCatalogStore, U as QODER_DATA_DIR_NAME, V as qoderOwnAuthPath, W as qoderMachineIdPath, _ as modelInfoOf, a as QODER_HOST_HEARTBEAT_FILENAME, b as createQoderTransport, c as processStartTimeMs, d as writeHostHeartbeat, f as QODER_CONNECT_VERSION, g as kindFromQoderFailure, h as classifyUpstreamError, i as variantFor, j as QODER_GLOBAL_PROBE_PATH, k as QODER_AUTH_PATH, l as qoderHostHeartbeatPath, m as QoderUpstreamClient, n as GLOBAL_VARIANT, o as clearHostHeartbeat, p as KIND_STATUS, r as QODER_VARIANTS, s as isHeartbeatProcessAlive, t as CHINA_VARIANT, u as readHostHeartbeat, v as normalizeCredits, w as randomSentinel, x as getMachineId, y as validateApiKey, z as patTail } from "./variants-CQg-9veJ.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -1045,15 +1045,20 @@ async function qoderWebStatus(deps) {
|
|
|
1045
1045
|
...deps.probeKey === void 0 ? {} : { probeKey: deps.probeKey },
|
|
1046
1046
|
...deps.useMaximumContextWindow === void 0 ? {} : { useMaximumContextWindow: deps.useMaximumContextWindow() }
|
|
1047
1047
|
};
|
|
1048
|
+
const refreshedAt = deps.jobTokenRefreshedAt?.();
|
|
1049
|
+
const withRefreshNotice = refreshedAt === void 0 ? probed : {
|
|
1050
|
+
...probed,
|
|
1051
|
+
jobTokenRefreshedAt: refreshedAt
|
|
1052
|
+
};
|
|
1048
1053
|
try {
|
|
1049
1054
|
const credits = await deps.client.fetchCredits();
|
|
1050
1055
|
return {
|
|
1051
|
-
...
|
|
1056
|
+
...withRefreshNotice,
|
|
1052
1057
|
credits
|
|
1053
1058
|
};
|
|
1054
1059
|
} catch (error) {
|
|
1055
1060
|
return {
|
|
1056
|
-
...
|
|
1061
|
+
...withRefreshNotice,
|
|
1057
1062
|
creditsError: safeMessage(error)
|
|
1058
1063
|
};
|
|
1059
1064
|
}
|
|
@@ -1243,6 +1248,57 @@ function registerQoderProbeRoute(ctx, deps, key) {
|
|
|
1243
1248
|
}, "dsh-qoder-connect: probe control route");
|
|
1244
1249
|
}
|
|
1245
1250
|
//#endregion
|
|
1251
|
+
//#region src/job-token-hint.ts
|
|
1252
|
+
/** The name recorded on the hint row (and therefore its displayed title). */
|
|
1253
|
+
const JOB_TOKEN_HINT_NAME = "qoder";
|
|
1254
|
+
/** Refresh time per agent id, so a later read can name when it happened. */
|
|
1255
|
+
const lastRefreshAt = /* @__PURE__ */ new Map();
|
|
1256
|
+
/** The most recent agent observed entering a running turn. */
|
|
1257
|
+
let lastRunningAgent;
|
|
1258
|
+
/** Monotonic suffix keeping appended ids unique within this process. */
|
|
1259
|
+
let hintSeq = 0;
|
|
1260
|
+
/**
|
|
1261
|
+
* Start tracking which agent is running.
|
|
1262
|
+
*
|
|
1263
|
+
* `agent/status` is a plain `emit` event: a listener that returns nothing
|
|
1264
|
+
* cannot disturb the emitting path (unlike a waterfall event, where a listener
|
|
1265
|
+
* owes the chain its `next()` call). The self-heal fires inside that agent's
|
|
1266
|
+
* request, so the most recently running agent is the conversation to print
|
|
1267
|
+
* into.
|
|
1268
|
+
*/
|
|
1269
|
+
function installJobTokenHint(ctx) {
|
|
1270
|
+
ctx.on("agent/status", ((payload) => {
|
|
1271
|
+
if (payload.status === "running" && payload.agent !== void 0) lastRunningAgent = payload.agent;
|
|
1272
|
+
}));
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Print the refresh row into the conversation whose request triggered it.
|
|
1276
|
+
*
|
|
1277
|
+
* Best effort by design: a missing agent, an absent session, or a projection
|
|
1278
|
+
* that rejects the append must never affect the chat that just recovered.
|
|
1279
|
+
*
|
|
1280
|
+
* @param at - When the job token was rotated.
|
|
1281
|
+
* @param text - The row's summary line.
|
|
1282
|
+
*/
|
|
1283
|
+
function emitJobTokenHint(at, text) {
|
|
1284
|
+
const agent = lastRunningAgent;
|
|
1285
|
+
if (agent?.session === void 0) return;
|
|
1286
|
+
lastRefreshAt.set(String(agent.id), at);
|
|
1287
|
+
const commandId = `cmd-qoder-hint-${Date.now().toString(36)}-${++hintSeq}`;
|
|
1288
|
+
try {
|
|
1289
|
+
agent.session.append("command/run", {
|
|
1290
|
+
commandId,
|
|
1291
|
+
name: JOB_TOKEN_HINT_NAME,
|
|
1292
|
+
source: { kind: "user" }
|
|
1293
|
+
});
|
|
1294
|
+
agent.session.append("command/done", {
|
|
1295
|
+
commandId,
|
|
1296
|
+
kind: "success",
|
|
1297
|
+
text
|
|
1298
|
+
});
|
|
1299
|
+
} catch {}
|
|
1300
|
+
}
|
|
1301
|
+
//#endregion
|
|
1246
1302
|
//#region src/index.ts
|
|
1247
1303
|
/** Stable Cordis plugin name. */
|
|
1248
1304
|
const name = "llm-qoder";
|
|
@@ -1403,16 +1459,32 @@ function settingsNamespaceFor(variant) {
|
|
|
1403
1459
|
function fallbackFor(_variant) {
|
|
1404
1460
|
return FALLBACK_QODER_MODELS;
|
|
1405
1461
|
}
|
|
1462
|
+
/**
|
|
1463
|
+
* The hint row's summary line: what happened, and when.
|
|
1464
|
+
*
|
|
1465
|
+
* Named here rather than in the hint module because the text is this plugin's
|
|
1466
|
+
* user-facing wording, and the transport callback that produces it runs before
|
|
1467
|
+
* the plugin's own scope exists.
|
|
1468
|
+
*/
|
|
1469
|
+
function jobTokenHintText(at) {
|
|
1470
|
+
return `jobToken 已自动刷新(${new Date(at).toLocaleTimeString("zh-CN", { hour12: false })})— 旧令牌被上游拒绝,已自动重换并恢复`;
|
|
1471
|
+
}
|
|
1406
1472
|
/** Build one variant's stores, transport, and probe state. */
|
|
1407
1473
|
function createVariantRuntime(ctx, config, variant, current, identityOf) {
|
|
1408
1474
|
const store = new QoderCredentialStore({
|
|
1409
1475
|
variant,
|
|
1410
1476
|
logger: ctx.logger
|
|
1411
1477
|
});
|
|
1478
|
+
let jobTokenRefreshedAt;
|
|
1412
1479
|
const transport = createQoderTransport({
|
|
1413
1480
|
region: variant.region,
|
|
1414
1481
|
resolvePat: () => store.patPromise(),
|
|
1415
|
-
resolveMachineId: () => getMachineId([qoderMachineIdPath()])
|
|
1482
|
+
resolveMachineId: () => getMachineId([qoderMachineIdPath()]),
|
|
1483
|
+
onJobTokenRefreshed: (info) => {
|
|
1484
|
+
jobTokenRefreshedAt = info.at;
|
|
1485
|
+
ctx.logger.warn(`dsh-qoder-connect: ${variant.displayName} job token was auto-refreshed after an upstream rejection`);
|
|
1486
|
+
emitJobTokenHint(info.at, jobTokenHintText(info.at));
|
|
1487
|
+
}
|
|
1416
1488
|
});
|
|
1417
1489
|
const client = new QoderUpstreamClient({
|
|
1418
1490
|
region: variant.region,
|
|
@@ -1442,6 +1514,7 @@ function createVariantRuntime(ctx, config, variant, current, identityOf) {
|
|
|
1442
1514
|
variant,
|
|
1443
1515
|
store,
|
|
1444
1516
|
client,
|
|
1517
|
+
transport,
|
|
1445
1518
|
catalog,
|
|
1446
1519
|
probeStore,
|
|
1447
1520
|
probeService: new QoderProbeService({
|
|
@@ -1461,7 +1534,8 @@ function createVariantRuntime(ctx, config, variant, current, identityOf) {
|
|
|
1461
1534
|
catalogGeneration: 0,
|
|
1462
1535
|
inflightFetch: void 0,
|
|
1463
1536
|
invalidate: () => {},
|
|
1464
|
-
registered: false
|
|
1537
|
+
registered: false,
|
|
1538
|
+
jobTokenRefreshedAt: () => jobTokenRefreshedAt
|
|
1465
1539
|
};
|
|
1466
1540
|
}
|
|
1467
1541
|
/** The catalog provenance the card displays. */
|
|
@@ -1604,6 +1678,7 @@ function apply(ctx, config) {
|
|
|
1604
1678
|
*/
|
|
1605
1679
|
const lastIdentities = /* @__PURE__ */ new Map();
|
|
1606
1680
|
const runtimes = QODER_VARIANTS.map((variant) => createVariantRuntime(ctx, config, variant, () => current(), (id) => lastIdentities.get(id)));
|
|
1681
|
+
installJobTokenHint(ctx);
|
|
1607
1682
|
const probeKey = createProbeKey();
|
|
1608
1683
|
/**
|
|
1609
1684
|
* The in-process key authorizing PAT writes, minted separately from the
|
|
@@ -1685,7 +1760,8 @@ function apply(ctx, config) {
|
|
|
1685
1760
|
probe: () => probeSection(runtime, current().probeConsent === true),
|
|
1686
1761
|
probeKey,
|
|
1687
1762
|
authKey,
|
|
1688
|
-
...runtime.variant.id === CHINA_VARIANT.id ? { useMaximumContextWindow: () => current().useMaximumContextWindowCN === true } : { useMaximumContextWindow: () => current().useMaximumContextWindow === true }
|
|
1763
|
+
...runtime.variant.id === CHINA_VARIANT.id ? { useMaximumContextWindow: () => current().useMaximumContextWindowCN === true } : { useMaximumContextWindow: () => current().useMaximumContextWindow === true },
|
|
1764
|
+
jobTokenRefreshedAt: runtime.jobTokenRefreshedAt
|
|
1689
1765
|
});
|
|
1690
1766
|
registerQoderAuthRoute(webCtx, {
|
|
1691
1767
|
path: runtime.variant.authPath,
|
|
@@ -1697,7 +1773,7 @@ function apply(ctx, config) {
|
|
|
1697
1773
|
const credential = await runtime.store.save(pat);
|
|
1698
1774
|
const identity = credentialIdentity(credential);
|
|
1699
1775
|
adoptIdentity(runtime, identity);
|
|
1700
|
-
fetchCatalog(runtime, identity);
|
|
1776
|
+
await fetchCatalog(runtime, identity);
|
|
1701
1777
|
return {
|
|
1702
1778
|
ok: true,
|
|
1703
1779
|
status: await runtime.store.status()
|
|
@@ -903,6 +903,20 @@ function qoderRequestId(headers) {
|
|
|
903
903
|
const normalized = (headers?.get("x-request-id") ?? headers?.get("request-id") ?? headers?.get("x-amzn-requestid"))?.trim();
|
|
904
904
|
return normalized ? ProviderRequestId(normalized) : void 0;
|
|
905
905
|
}
|
|
906
|
+
/**
|
|
907
|
+
* Whether this failure is an upstream authorization rejection worth one
|
|
908
|
+
* re-auth retry.
|
|
909
|
+
*
|
|
910
|
+
* The job token the transport signs requests with is cached in memory; an
|
|
911
|
+
* upstream that invalidates it mid-lifetime (gateway rotation or a fault
|
|
912
|
+
* window) answers HTTP 401/403 before any payload is produced. That state is
|
|
913
|
+
* distinguishable from a genuinely revoked PAT only by trying a fresh
|
|
914
|
+
* exchange, so callers clear their credential cache and retry once before
|
|
915
|
+
* reporting `AUTH` to the user.
|
|
916
|
+
*/
|
|
917
|
+
function isQoderAuthRejection(error) {
|
|
918
|
+
return error instanceof LlmError && (error.code === "AUTH" || error.failure.status === 401 || error.failure.status === 403);
|
|
919
|
+
}
|
|
906
920
|
function retryAfterMs(value, nowMs = Date.now()) {
|
|
907
921
|
const normalized = value?.trim();
|
|
908
922
|
if (!normalized) return void 0;
|
|
@@ -1296,6 +1310,17 @@ var QoderAuthService = class {
|
|
|
1296
1310
|
if (pat) this.cache.delete(`${this.region}:${opaqueCredentialKey(pat)}`);
|
|
1297
1311
|
else this.cache.clear();
|
|
1298
1312
|
}
|
|
1313
|
+
/**
|
|
1314
|
+
* Exchange a fresh job token OUTSIDE the single-flight, for the self-heal
|
|
1315
|
+
* retry. The shared flight can be aborted by a departing concurrent waiter
|
|
1316
|
+
* (the quota poll, the catalog sweep), and a retry that joins it would then
|
|
1317
|
+
* be cancelled by a path that has nothing to do with the chat. The result
|
|
1318
|
+
* replaces the cache entry.
|
|
1319
|
+
*/
|
|
1320
|
+
async exchangeFresh(pat, signal) {
|
|
1321
|
+
this.clear(pat);
|
|
1322
|
+
return await this.exchangeAndResolve(pat, signal ?? AbortSignal.timeout(this.timeoutMs));
|
|
1323
|
+
}
|
|
1299
1324
|
async getCredentials(pat, signal) {
|
|
1300
1325
|
if (!pat || typeof pat !== "string") throw new QoderLlmError("Qoder Personal Access Token is missing or invalid. Configure Qoder in the Qoder settings page.", "MISSING_CREDENTIAL");
|
|
1301
1326
|
if (signal?.aborted) throw abortedError();
|
|
@@ -2283,6 +2308,16 @@ var QoderUsageReader = class {
|
|
|
2283
2308
|
return this.flights.run(cacheKey, options?.signal, (sharedSignal) => this.loadAccount(pat, sharedSignal, cacheKey), () => new QoderLlmError("Qoder account request was aborted.", "ABORTED"));
|
|
2284
2309
|
}
|
|
2285
2310
|
async loadAccount(pat, signal, cacheKey) {
|
|
2311
|
+
try {
|
|
2312
|
+
return await this.loadAccountWith(pat, signal, cacheKey);
|
|
2313
|
+
} catch (error) {
|
|
2314
|
+
if (!isQoderAuthRejection(error) || signal.aborted) throw error;
|
|
2315
|
+
this.logger?.warn?.("[Qoder Account] Usage read rejected as unauthorized; exchanging a fresh job token and retrying once");
|
|
2316
|
+
}
|
|
2317
|
+
this.authService.clear(pat);
|
|
2318
|
+
return this.loadAccountWith(pat, signal, cacheKey);
|
|
2319
|
+
}
|
|
2320
|
+
async loadAccountWith(pat, signal, cacheKey) {
|
|
2286
2321
|
const creds = await this.authService.getCredentials(pat, signal);
|
|
2287
2322
|
const profile = {
|
|
2288
2323
|
id: creds.userID,
|
|
@@ -3001,6 +3036,15 @@ var DefaultQoderTransport = class {
|
|
|
3001
3036
|
attachments;
|
|
3002
3037
|
imageUploader;
|
|
3003
3038
|
preserveThinking;
|
|
3039
|
+
onJobTokenRefreshed;
|
|
3040
|
+
/**
|
|
3041
|
+
* Set when the self-heal exchanged a fresh job token, cleared when a chat
|
|
3042
|
+
* afterwards succeeds. The notice reports "the token was rotated because the
|
|
3043
|
+
* old one was rejected", which is true from that exchange on — so it must
|
|
3044
|
+
* not be tied to the heal's OWN retry, which can still lose a race with a
|
|
3045
|
+
* transient upstream timeout and be rescued by a later attempt.
|
|
3046
|
+
*/
|
|
3047
|
+
pendingRefreshAt;
|
|
3004
3048
|
constructor(options) {
|
|
3005
3049
|
this.region = options.region;
|
|
3006
3050
|
this.resolvePat = options.resolvePat;
|
|
@@ -3011,6 +3055,7 @@ var DefaultQoderTransport = class {
|
|
|
3011
3055
|
this.metadataTimeoutMs = options.metadataTimeoutMs;
|
|
3012
3056
|
this.attachments = options.attachments;
|
|
3013
3057
|
this.preserveThinking = options.preserveThinking;
|
|
3058
|
+
this.onJobTokenRefreshed = options.onJobTokenRefreshed;
|
|
3014
3059
|
this.auth = new QoderAuthService({
|
|
3015
3060
|
fetch: this.fetchImpl,
|
|
3016
3061
|
logger: this.logger,
|
|
@@ -3040,6 +3085,23 @@ var DefaultQoderTransport = class {
|
|
|
3040
3085
|
stream(options, model) {
|
|
3041
3086
|
return this.generate(options, model);
|
|
3042
3087
|
}
|
|
3088
|
+
/**
|
|
3089
|
+
* Report the pending self-heal once a chat is accepted, then clear it.
|
|
3090
|
+
*
|
|
3091
|
+
* The notice means "the stale token was rejected, so it was rotated, and the
|
|
3092
|
+
* chat works again" — all three are true by the time a chat is accepted
|
|
3093
|
+
* after the refresh, no matter which attempt delivered it.
|
|
3094
|
+
*/
|
|
3095
|
+
flushPendingRefreshNotice() {
|
|
3096
|
+
if (this.pendingRefreshAt === void 0) return;
|
|
3097
|
+
const at = this.pendingRefreshAt;
|
|
3098
|
+
this.pendingRefreshAt = void 0;
|
|
3099
|
+
this.logger?.warn?.("[Qoder Stream] Job token was auto-refreshed after an upstream rejection; the chat has recovered");
|
|
3100
|
+
this.onJobTokenRefreshed?.({
|
|
3101
|
+
region: this.region,
|
|
3102
|
+
at
|
|
3103
|
+
});
|
|
3104
|
+
}
|
|
3043
3105
|
async discoverModels(signal) {
|
|
3044
3106
|
const pat = await this.requirePat(signal);
|
|
3045
3107
|
const key = opaqueCredentialKey(pat);
|
|
@@ -3068,6 +3130,30 @@ var DefaultQoderTransport = class {
|
|
|
3068
3130
|
if (signal?.aborted) throw aborted("Qoder request was aborted.");
|
|
3069
3131
|
return pat;
|
|
3070
3132
|
}
|
|
3133
|
+
/**
|
|
3134
|
+
* Stream one chat, reporting acceptance as soon as the FIRST chunk arrives.
|
|
3135
|
+
*
|
|
3136
|
+
* `streamQoderChat` throws before yielding anything when the upstream rejects
|
|
3137
|
+
* the request, so a first chunk means the credential was accepted. Reporting
|
|
3138
|
+
* at that moment — rather than after the whole stream drains — matters
|
|
3139
|
+
* because the consumer may close the stream early, and code after a
|
|
3140
|
+
* completed `yield*` would then never run.
|
|
3141
|
+
*
|
|
3142
|
+
* @param onAccepted - Called once, before the first chunk is forwarded.
|
|
3143
|
+
*/
|
|
3144
|
+
async *streamChat(options, model, credentials, messages, onAccepted) {
|
|
3145
|
+
const stream = streamQoderChat(options, model, credentials, messages, {
|
|
3146
|
+
fetch: this.fetchImpl,
|
|
3147
|
+
logger: this.logger,
|
|
3148
|
+
region: this.region,
|
|
3149
|
+
responseHeaderTimeoutMs: this.responseHeaderTimeoutMs,
|
|
3150
|
+
streamIdleTimeoutMs: this.streamIdleTimeoutMs
|
|
3151
|
+
});
|
|
3152
|
+
const first = await stream.next();
|
|
3153
|
+
onAccepted();
|
|
3154
|
+
if (!first.done) yield first.value;
|
|
3155
|
+
yield* stream;
|
|
3156
|
+
}
|
|
3071
3157
|
async *generate(options, model) {
|
|
3072
3158
|
if (options.signal?.aborted) throw aborted("Request was aborted prior to generation.");
|
|
3073
3159
|
validateQoderRequestShape(options, model);
|
|
@@ -3078,13 +3164,44 @@ var DefaultQoderTransport = class {
|
|
|
3078
3164
|
credentials,
|
|
3079
3165
|
preserveThinking: this.preserveThinking
|
|
3080
3166
|
});
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
})
|
|
3167
|
+
const chatCredentials = await this.auth.getCredentials(pat, options.signal);
|
|
3168
|
+
try {
|
|
3169
|
+
yield* this.streamChat(options, model, chatCredentials, messages, () => {
|
|
3170
|
+
this.flushPendingRefreshNotice();
|
|
3171
|
+
});
|
|
3172
|
+
return;
|
|
3173
|
+
} catch (error) {
|
|
3174
|
+
if (!isQoderAuthRejection(error) || options.signal?.aborted) throw error;
|
|
3175
|
+
this.logger?.warn?.("[Qoder Stream] Chat rejected as unauthorized; exchanging a fresh job token and retrying once", { status: error.failure.status });
|
|
3176
|
+
}
|
|
3177
|
+
let lastRejection;
|
|
3178
|
+
for (let round = 0; round < 2; round++) {
|
|
3179
|
+
const refreshed = await this.auth.exchangeFresh(pat, options.signal);
|
|
3180
|
+
this.pendingRefreshAt = Date.now();
|
|
3181
|
+
try {
|
|
3182
|
+
yield* this.streamChat(options, model, refreshed, messages, () => {
|
|
3183
|
+
this.flushPendingRefreshNotice();
|
|
3184
|
+
});
|
|
3185
|
+
return;
|
|
3186
|
+
} catch (error) {
|
|
3187
|
+
if (!isQoderAuthRejection(error) || options.signal?.aborted) throw error;
|
|
3188
|
+
lastRejection = error;
|
|
3189
|
+
if (round + 1 < 2) {
|
|
3190
|
+
this.logger?.warn?.("[Qoder Stream] Fresh job token also rejected; waiting 2s and retrying once more", {
|
|
3191
|
+
status: error.failure.status,
|
|
3192
|
+
round: round + 1
|
|
3193
|
+
});
|
|
3194
|
+
await new Promise((resolve, reject) => {
|
|
3195
|
+
const timer = setTimeout(resolve, 2e3);
|
|
3196
|
+
options.signal?.addEventListener("abort", () => {
|
|
3197
|
+
clearTimeout(timer);
|
|
3198
|
+
reject(aborted("Request was aborted during the re-auth backoff."));
|
|
3199
|
+
}, { once: true });
|
|
3200
|
+
});
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
throw lastRejection;
|
|
3088
3205
|
}
|
|
3089
3206
|
};
|
|
3090
3207
|
//#endregion
|
|
@@ -3167,6 +3284,9 @@ function classifyUpstreamError(status, body) {
|
|
|
3167
3284
|
for (const marker of [
|
|
3168
3285
|
"invalid token",
|
|
3169
3286
|
"invalid pat",
|
|
3287
|
+
"invalid job token",
|
|
3288
|
+
"token expired",
|
|
3289
|
+
"token has expired",
|
|
3170
3290
|
"unauthorized",
|
|
3171
3291
|
"forbidden",
|
|
3172
3292
|
"not authenticated"
|
|
@@ -3882,7 +4002,7 @@ const DEFAULT_MAX_TOKENS = 32768;
|
|
|
3882
4002
|
const DEFAULT_CONTEXT_WINDOW = 18e4;
|
|
3883
4003
|
//#endregion
|
|
3884
4004
|
//#region src/version.ts
|
|
3885
|
-
const QODER_CONNECT_VERSION = "0.1.
|
|
4005
|
+
const QODER_CONNECT_VERSION = "0.1.1";
|
|
3886
4006
|
//#endregion
|
|
3887
4007
|
//#region src/host-heartbeat.ts
|
|
3888
4008
|
/**
|
package/package.json
CHANGED
|
@@ -1,116 +1,116 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "dsh-qoder-connect",
|
|
3
|
-
"displayName": "DSH Qoder Connect",
|
|
4
|
-
"description": "将 Qoder
|
|
5
|
-
"version": "0.1.
|
|
6
|
-
"author": "corrinehu",
|
|
7
|
-
"keywords": [
|
|
8
|
-
"dsh-plugin",
|
|
9
|
-
"deepseek-harness",
|
|
10
|
-
"deepseek",
|
|
11
|
-
"qoder",
|
|
12
|
-
"model-provider"
|
|
13
|
-
],
|
|
14
|
-
"type": "module",
|
|
15
|
-
"engines": {
|
|
16
|
-
"node": "^22.19.0 || >=24.0.0"
|
|
17
|
-
},
|
|
18
|
-
"main": "lib/index.js",
|
|
19
|
-
"types": "lib/index.d.ts",
|
|
20
|
-
"bin": {
|
|
21
|
-
"dsh-qoder-connect": "lib/bin.js"
|
|
22
|
-
},
|
|
23
|
-
"scripts": {
|
|
24
|
-
"prepack": "npm run build",
|
|
25
|
-
"build": "tsdown",
|
|
26
|
-
"test": "vitest run",
|
|
27
|
-
"test:qoder": "node --test --test-isolation=none --experimental-strip-types tests/qoder/*.test.ts",
|
|
28
|
-
"typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.client.json",
|
|
29
|
-
"check": "pnpm run typecheck && pnpm run test && pnpm run test:qoder && pnpm run build"
|
|
30
|
-
},
|
|
31
|
-
"exports": {
|
|
32
|
-
".": {
|
|
33
|
-
"types": "./lib/index.d.ts",
|
|
34
|
-
"default": "./lib/index.js"
|
|
35
|
-
},
|
|
36
|
-
"./client": "./lib/client.js",
|
|
37
|
-
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
38
|
-
"./package.json": "./package.json"
|
|
39
|
-
},
|
|
40
|
-
"files": [
|
|
41
|
-
"lib",
|
|
42
|
-
"assets",
|
|
43
|
-
"cordis.patch.yml",
|
|
44
|
-
"README.md",
|
|
45
|
-
"README.en.md",
|
|
46
|
-
"LICENSE"
|
|
47
|
-
],
|
|
48
|
-
"license": "MIT",
|
|
49
|
-
"repository": {
|
|
50
|
-
"type": "git",
|
|
51
|
-
"url": "git+https://github.com/masknull/dsh-qoder-connect.git"
|
|
52
|
-
},
|
|
53
|
-
"homepage": "https://github.com/masknull/dsh-qoder-connect#readme",
|
|
54
|
-
"bugs": "https://github.com/masknull/dsh-qoder-connect/issues",
|
|
55
|
-
"dsh": {
|
|
56
|
-
"bundle": {
|
|
57
|
-
"patch": "./cordis.patch.yml"
|
|
58
|
-
},
|
|
59
|
-
"client": {
|
|
60
|
-
"inject": [
|
|
61
|
-
"@deepseek-ai/dsh-api-remotes",
|
|
62
|
-
"@deepseek-ai/dsh-api-session-controller",
|
|
63
|
-
"@deepseek-ai/dsh-client-ui-renderer",
|
|
64
|
-
"@deepseek-ai/dsh-client-ui-settings-plugins",
|
|
65
|
-
"@deepseek-ai/dsh-client-ui-settings",
|
|
66
|
-
"@deepseek-ai/dsh-client-ui-conversation",
|
|
67
|
-
"@deepseek-ai/dsh-client-ui-model-selection",
|
|
68
|
-
"@deepseek-ai/dsh-client-ui-sidebar",
|
|
69
|
-
"@deepseek-ai/dsh-client-locale"
|
|
70
|
-
],
|
|
71
|
-
"platform": "web"
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
"peerDependencies": {
|
|
75
|
-
"@deepseek-ai/cordis": "^4.0.2",
|
|
76
|
-
"@deepseek-ai/dsh-atomic-write": "^0.1.5-rc.1",
|
|
77
|
-
"@deepseek-ai/dsh-attachment": "^0.1.5-rc.1",
|
|
78
|
-
"@deepseek-ai/dsh-home-paths": "^0.1.5-rc.1",
|
|
79
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.1",
|
|
80
|
-
"@deepseek-ai/dsh-llm": "^0.1.5-rc.1",
|
|
81
|
-
"@deepseek-ai/dsh-llm-pi-ai": "^0.1.5-rc.1",
|
|
82
|
-
"@deepseek-ai/dsh-settings": "^0.1.5-rc.1",
|
|
83
|
-
"@deepseek-ai/schemastery": "^3.18.2",
|
|
84
|
-
"@earendil-works/pi-ai": "^0.85.1",
|
|
85
|
-
"react": "^18.2.0"
|
|
86
|
-
},
|
|
87
|
-
"devDependencies": {
|
|
88
|
-
"@deepseek-ai/cordis": "^4.0.2",
|
|
89
|
-
"@deepseek-ai/dsh-atomic-write": "^0.1.5-rc.1",
|
|
90
|
-
"@deepseek-ai/dsh-attachment": "^0.1.5-rc.1",
|
|
91
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.5-rc.1",
|
|
92
|
-
"@deepseek-ai/dsh-client-ui-conversation": "0.1.5-rc.1",
|
|
93
|
-
"@deepseek-ai/dsh-client-ui-model-selection": "0.1.5-rc.1",
|
|
94
|
-
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.5-rc.1",
|
|
95
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.1.5-rc.1",
|
|
96
|
-
"@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.5-rc.1",
|
|
97
|
-
"@deepseek-ai/dsh-client-ui-sidebar": "^0.1.5-rc.1",
|
|
98
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.1",
|
|
99
|
-
"@deepseek-ai/dsh-credentials": "^0.1.5-rc.1",
|
|
100
|
-
"@deepseek-ai/dsh-home-paths": "^0.1.5-rc.1",
|
|
101
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.1",
|
|
102
|
-
"@deepseek-ai/dsh-llm": "^0.1.5-rc.1",
|
|
103
|
-
"@deepseek-ai/dsh-llm-pi-ai": "^0.1.5-rc.1",
|
|
104
|
-
"@deepseek-ai/dsh-settings": "^0.1.5-rc.1",
|
|
105
|
-
"@deepseek-ai/schemastery": "^3.18.2",
|
|
106
|
-
"@earendil-works/pi-ai": "^0.85.1",
|
|
107
|
-
"@types/node": "^22.20.0",
|
|
108
|
-
"@types/react": "~18.3.1",
|
|
109
|
-
"@types/react-test-renderer": "^18.3.1",
|
|
110
|
-
"react": "^18.2.0",
|
|
111
|
-
"react-test-renderer": "18.3.1",
|
|
112
|
-
"tsdown": "^0.22.2",
|
|
113
|
-
"typescript": "^5.9.3",
|
|
114
|
-
"vitest": "^4.1.8"
|
|
115
|
-
}
|
|
116
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-qoder-connect",
|
|
3
|
+
"displayName": "DSH Qoder Connect",
|
|
4
|
+
"description": "将 Qoder(国内版 / 国际版)的模型接入 DeepSeek Harness — bring Qoder (CN & international) models into DeepSeek Harness.",
|
|
5
|
+
"version": "0.1.1",
|
|
6
|
+
"author": "corrinehu",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"dsh-plugin",
|
|
9
|
+
"deepseek-harness",
|
|
10
|
+
"deepseek",
|
|
11
|
+
"qoder",
|
|
12
|
+
"model-provider"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
17
|
+
},
|
|
18
|
+
"main": "lib/index.js",
|
|
19
|
+
"types": "lib/index.d.ts",
|
|
20
|
+
"bin": {
|
|
21
|
+
"dsh-qoder-connect": "lib/bin.js"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"prepack": "npm run build",
|
|
25
|
+
"build": "tsdown",
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"test:qoder": "node --test --test-isolation=none --experimental-strip-types tests/qoder/*.test.ts",
|
|
28
|
+
"typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.client.json",
|
|
29
|
+
"check": "pnpm run typecheck && pnpm run test && pnpm run test:qoder && pnpm run build"
|
|
30
|
+
},
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./lib/index.d.ts",
|
|
34
|
+
"default": "./lib/index.js"
|
|
35
|
+
},
|
|
36
|
+
"./client": "./lib/client.js",
|
|
37
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
38
|
+
"./package.json": "./package.json"
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"lib",
|
|
42
|
+
"assets",
|
|
43
|
+
"cordis.patch.yml",
|
|
44
|
+
"README.md",
|
|
45
|
+
"README.en.md",
|
|
46
|
+
"LICENSE"
|
|
47
|
+
],
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/masknull/dsh-qoder-connect.git"
|
|
52
|
+
},
|
|
53
|
+
"homepage": "https://github.com/masknull/dsh-qoder-connect#readme",
|
|
54
|
+
"bugs": "https://github.com/masknull/dsh-qoder-connect/issues",
|
|
55
|
+
"dsh": {
|
|
56
|
+
"bundle": {
|
|
57
|
+
"patch": "./cordis.patch.yml"
|
|
58
|
+
},
|
|
59
|
+
"client": {
|
|
60
|
+
"inject": [
|
|
61
|
+
"@deepseek-ai/dsh-api-remotes",
|
|
62
|
+
"@deepseek-ai/dsh-api-session-controller",
|
|
63
|
+
"@deepseek-ai/dsh-client-ui-renderer",
|
|
64
|
+
"@deepseek-ai/dsh-client-ui-settings-plugins",
|
|
65
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
66
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
67
|
+
"@deepseek-ai/dsh-client-ui-model-selection",
|
|
68
|
+
"@deepseek-ai/dsh-client-ui-sidebar",
|
|
69
|
+
"@deepseek-ai/dsh-client-locale"
|
|
70
|
+
],
|
|
71
|
+
"platform": "web"
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"peerDependencies": {
|
|
75
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
76
|
+
"@deepseek-ai/dsh-atomic-write": "^0.1.5-rc.1",
|
|
77
|
+
"@deepseek-ai/dsh-attachment": "^0.1.5-rc.1",
|
|
78
|
+
"@deepseek-ai/dsh-home-paths": "^0.1.5-rc.1",
|
|
79
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.1",
|
|
80
|
+
"@deepseek-ai/dsh-llm": "^0.1.5-rc.1",
|
|
81
|
+
"@deepseek-ai/dsh-llm-pi-ai": "^0.1.5-rc.1",
|
|
82
|
+
"@deepseek-ai/dsh-settings": "^0.1.5-rc.1",
|
|
83
|
+
"@deepseek-ai/schemastery": "^3.18.2",
|
|
84
|
+
"@earendil-works/pi-ai": "^0.85.1",
|
|
85
|
+
"react": "^18.2.0"
|
|
86
|
+
},
|
|
87
|
+
"devDependencies": {
|
|
88
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
89
|
+
"@deepseek-ai/dsh-atomic-write": "^0.1.5-rc.1",
|
|
90
|
+
"@deepseek-ai/dsh-attachment": "^0.1.5-rc.1",
|
|
91
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.5-rc.1",
|
|
92
|
+
"@deepseek-ai/dsh-client-ui-conversation": "0.1.5-rc.1",
|
|
93
|
+
"@deepseek-ai/dsh-client-ui-model-selection": "0.1.5-rc.1",
|
|
94
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.5-rc.1",
|
|
95
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.5-rc.1",
|
|
96
|
+
"@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.5-rc.1",
|
|
97
|
+
"@deepseek-ai/dsh-client-ui-sidebar": "^0.1.5-rc.1",
|
|
98
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.1",
|
|
99
|
+
"@deepseek-ai/dsh-credentials": "^0.1.5-rc.1",
|
|
100
|
+
"@deepseek-ai/dsh-home-paths": "^0.1.5-rc.1",
|
|
101
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.1",
|
|
102
|
+
"@deepseek-ai/dsh-llm": "^0.1.5-rc.1",
|
|
103
|
+
"@deepseek-ai/dsh-llm-pi-ai": "^0.1.5-rc.1",
|
|
104
|
+
"@deepseek-ai/dsh-settings": "^0.1.5-rc.1",
|
|
105
|
+
"@deepseek-ai/schemastery": "^3.18.2",
|
|
106
|
+
"@earendil-works/pi-ai": "^0.85.1",
|
|
107
|
+
"@types/node": "^22.20.0",
|
|
108
|
+
"@types/react": "~18.3.1",
|
|
109
|
+
"@types/react-test-renderer": "^18.3.1",
|
|
110
|
+
"react": "^18.2.0",
|
|
111
|
+
"react-test-renderer": "18.3.1",
|
|
112
|
+
"tsdown": "^0.22.2",
|
|
113
|
+
"typescript": "^5.9.3",
|
|
114
|
+
"vitest": "^4.1.8"
|
|
115
|
+
}
|
|
116
|
+
}
|