@wengine-ai/ccr-provider-qoder 0.2.0 → 0.3.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.md CHANGED
@@ -18,7 +18,7 @@ Everything below exists once per site; the two sites are independent (own accoun
18
18
  - **Transformer** (`qoder` / `qoder-cn`): unified → chat-completions request conversion (leading system message, thinking → `reasoning_content`, stable session id → `prompt_cache_key`, reasoning effort passthrough) and strict SSE/JSON response validation (error-frame classification, `[DONE]` integrity, usage mapping). The conversion is site-independent; each managed provider locks its own transformer name.
19
19
  - **Managed runtime** (`qoder` / `qoder-cn`): pins that site's fixed chat upstream, routes through the host's subscription dispatcher, owns abort/deadline wiring and the error taxonomy (`qoder_reauth_required`, `qoder_rate_limited` with `retry_after`-derived cooldown, `qoder_upstream_error`). A runtime claims only its own managed provider.
20
20
  - **Auth service**: PAT sign-in with the job-token engine, per-site credential file (0600, atomic, size-capped), model sync/visibility, usage reads with a 60s cache, cooling circuit breaker and reconnect.
21
- - **Admin routes** (loopback-only, per site): status, connect, sync, usage, model visibility, disconnect.
21
+ - **Admin routes** (loopback-only, per site): status, connect, sync, usage, checkin, model visibility, disconnect.
22
22
  - **One config protector**: re-locks BOTH managed providers in a single pass and re-adds any of them that vanish from the config.
23
23
  - **UI card + locales** (en/zh): one card with two independent sections (Qoder / Qoder CN) — PAT connect form, status, quota windows, model management.
24
24
 
@@ -38,9 +38,29 @@ Then, in the CCR settings page, open the Qoder card and use the matching section
38
38
 
39
39
  Both sections can be connected at the same time; disconnecting one never touches the other.
40
40
 
41
+ ## Manual daily check-in
42
+
43
+ The Qoder CN card has a **Check in** button. It lists campaigns, claims the first
44
+ `CLAIM_BENEFIT` marked `CLAIMABLE`, and shows the awarded credits. Already claimed
45
+ and unavailable campaigns are ordinary results, with no automatic retry or timer.
46
+
47
+ Loopback-only `POST /api/qoder/cn/checkin` returns
48
+ `{ outcome, credit?, message? }`, where outcome is `claimed`, `done`, `inactive`,
49
+ `ineligible`, or `failed`. Credits are a positive finite number when supplied.
50
+ `POST /api/qoder/checkin` returns `ineligible`: international campaigns require
51
+ Qoder's signed desktop identity, which this PAT-only plugin does not read. The
52
+ international card directs users to claim in Qoder desktop.
53
+
54
+ CN PAT requests use the exchanged job token and the upstream's supported empty
55
+ native-identity fallback. No machine token or machine type is fabricated.
56
+ Requests use the subscription dispatcher (direct fallback), identity encoding,
57
+ and reject redirects. Upstream response bodies and transport errors are never
58
+ returned in failure messages. Admin authorization failures retain HTTP 403;
59
+ check-in outcomes return HTTP 200.
60
+
41
61
  ## Scope limits
42
62
 
43
- - Remote origins are fixed per site: `openapi.qoder.sh` / `api3.qoder.sh` for qoder.com and `openapi.qoder.com.cn` / `gateway.qoder.com.cn` for qoder.cn. No device flow, no campaigns/check-in, no VPC, no QwenWork.
63
+ - Remote origins are fixed per site: `openapi.qoder.sh` / `api3.qoder.sh` for qoder.com and `openapi.qoder.com.cn` / `gateway.qoder.com.cn` for qoder.cn. No device flow, no VPC, no QwenWork.
44
64
  - Sign-in is PAT-only; the job token is refreshed from its refresh token when present, and a lapsed/absent refresh falls back to PAT re-exchange (a refused PAT marks that site's account expired until you reconnect). The PAT is kept in the plugin's private 0600 credential file.
45
65
 
46
66
  ## Development
package/dist/README.md CHANGED
@@ -18,7 +18,7 @@ Everything below exists once per site; the two sites are independent (own accoun
18
18
  - **Transformer** (`qoder` / `qoder-cn`): unified → chat-completions request conversion (leading system message, thinking → `reasoning_content`, stable session id → `prompt_cache_key`, reasoning effort passthrough) and strict SSE/JSON response validation (error-frame classification, `[DONE]` integrity, usage mapping). The conversion is site-independent; each managed provider locks its own transformer name.
19
19
  - **Managed runtime** (`qoder` / `qoder-cn`): pins that site's fixed chat upstream, routes through the host's subscription dispatcher, owns abort/deadline wiring and the error taxonomy (`qoder_reauth_required`, `qoder_rate_limited` with `retry_after`-derived cooldown, `qoder_upstream_error`). A runtime claims only its own managed provider.
20
20
  - **Auth service**: PAT sign-in with the job-token engine, per-site credential file (0600, atomic, size-capped), model sync/visibility, usage reads with a 60s cache, cooling circuit breaker and reconnect.
21
- - **Admin routes** (loopback-only, per site): status, connect, sync, usage, model visibility, disconnect.
21
+ - **Admin routes** (loopback-only, per site): status, connect, sync, usage, checkin, model visibility, disconnect.
22
22
  - **One config protector**: re-locks BOTH managed providers in a single pass and re-adds any of them that vanish from the config.
23
23
  - **UI card + locales** (en/zh): one card with two independent sections (Qoder / Qoder CN) — PAT connect form, status, quota windows, model management.
24
24
 
@@ -38,9 +38,29 @@ Then, in the CCR settings page, open the Qoder card and use the matching section
38
38
 
39
39
  Both sections can be connected at the same time; disconnecting one never touches the other.
40
40
 
41
+ ## Manual daily check-in
42
+
43
+ The Qoder CN card has a **Check in** button. It lists campaigns, claims the first
44
+ `CLAIM_BENEFIT` marked `CLAIMABLE`, and shows the awarded credits. Already claimed
45
+ and unavailable campaigns are ordinary results, with no automatic retry or timer.
46
+
47
+ Loopback-only `POST /api/qoder/cn/checkin` returns
48
+ `{ outcome, credit?, message? }`, where outcome is `claimed`, `done`, `inactive`,
49
+ `ineligible`, or `failed`. Credits are a positive finite number when supplied.
50
+ `POST /api/qoder/checkin` returns `ineligible`: international campaigns require
51
+ Qoder's signed desktop identity, which this PAT-only plugin does not read. The
52
+ international card directs users to claim in Qoder desktop.
53
+
54
+ CN PAT requests use the exchanged job token and the upstream's supported empty
55
+ native-identity fallback. No machine token or machine type is fabricated.
56
+ Requests use the subscription dispatcher (direct fallback), identity encoding,
57
+ and reject redirects. Upstream response bodies and transport errors are never
58
+ returned in failure messages. Admin authorization failures retain HTTP 403;
59
+ check-in outcomes return HTTP 200.
60
+
41
61
  ## Scope limits
42
62
 
43
- - Remote origins are fixed per site: `openapi.qoder.sh` / `api3.qoder.sh` for qoder.com and `openapi.qoder.com.cn` / `gateway.qoder.com.cn` for qoder.cn. No device flow, no campaigns/check-in, no VPC, no QwenWork.
63
+ - Remote origins are fixed per site: `openapi.qoder.sh` / `api3.qoder.sh` for qoder.com and `openapi.qoder.com.cn` / `gateway.qoder.com.cn` for qoder.cn. No device flow, no VPC, no QwenWork.
44
64
  - Sign-in is PAT-only; the job token is refreshed from its refresh token when present, and a lapsed/absent refresh falls back to PAT re-exchange (a refused PAT marks that site's account expired until you reconnect). The PAT is kept in the plugin's private 0600 credential file.
45
65
 
46
66
  ## Development
package/dist/card.js CHANGED
@@ -12,23 +12,26 @@ var qoderLocales = {
12
12
  error: "Management request failed",
13
13
  account: "Account",
14
14
  models: "Models",
15
- models_count: "{{count}} models",
16
15
  pat: "Personal access token",
17
16
  patPlaceholder: "pt-\u2026",
18
17
  connect: "Connect",
18
+ checkin: "Check in",
19
+ checkin_action: "Check in",
20
+ checkin_pending: "Checking in\u2026",
21
+ checkin_claimed: "Credits claimed",
22
+ checkin_done: "Already claimed today",
23
+ checkin_inactive: "No campaign today",
24
+ checkin_ineligible: "Unavailable",
25
+ checkin_failed: "Check-in failed",
26
+ checkinDesktop: "International check-in requires Qoder desktop device identity; claim in Qoder desktop.",
19
27
  sync: "Refresh models",
20
28
  usage: "Refresh quota",
21
29
  unknown: "Quota unknown",
22
30
  stale: "Last known quota (stale)",
23
31
  used: "Allowance",
24
- restore_partial: "Partially completed: restored {{count}} of {{total}} models. Management request failed.",
25
- hide_partial: "Partially completed: hid {{count}} of {{total}} models. Management request failed.",
26
- restore_model: "Restore {{model}}",
27
32
  disconnect: "Disconnect",
28
- hide_model: "Hide {{model}}",
29
- hidden_models: "Hidden models ({{count}})",
33
+ hide: "Hide",
30
34
  restore: "Restore",
31
- restore_all: "Restore all",
32
35
  cooling: "Rate limited until",
33
36
  noModels: "Refresh models after connecting",
34
37
  free: "free",
@@ -46,23 +49,26 @@ var qoderLocales = {
46
49
  error: "\u7BA1\u7406\u64CD\u4F5C\u5931\u8D25",
47
50
  account: "\u8D26\u53F7",
48
51
  models: "\u6A21\u578B",
49
- models_count: "{{count}} \u4E2A\u6A21\u578B",
50
52
  pat: "\u4E2A\u4EBA\u8BBF\u95EE\u4EE4\u724C",
51
53
  patPlaceholder: "pt-\u2026",
52
54
  connect: "\u8FDE\u63A5",
55
+ checkin: "\u7B7E\u5230",
56
+ checkin_action: "\u7B7E\u5230",
57
+ checkin_pending: "\u7B7E\u5230\u4E2D\u2026",
58
+ checkin_claimed: "\u9886\u53D6\u6210\u529F",
59
+ checkin_done: "\u4ECA\u65E5\u5DF2\u9886",
60
+ checkin_inactive: "\u4ECA\u65E5\u65E0\u6D3B\u52A8",
61
+ checkin_ineligible: "\u4E0D\u53EF\u7528",
62
+ checkin_failed: "\u7B7E\u5230\u5931\u8D25",
63
+ checkinDesktop: "\u56FD\u9645\u7AD9\u9700\u8981\u5728 Qoder \u684C\u9762\u7AEF\u5185\u7B7E\u5230\u3002",
53
64
  sync: "\u5237\u65B0\u6A21\u578B",
54
65
  usage: "\u5237\u65B0\u989D\u5EA6",
55
66
  unknown: "\u989D\u5EA6\u672A\u77E5",
56
67
  stale: "\u4E0A\u6B21\u989D\u5EA6\uFF08\u5DF2\u8FC7\u671F\uFF09",
57
68
  used: "\u5DF2\u7528\u989D\u5EA6",
58
- restore_partial: "\u90E8\u5206\u5B8C\u6210\uFF1A\u5DF2\u6062\u590D {{count}} \u4E2A\u6A21\u578B\uFF0C\u5171 {{total}} \u4E2A\u3002\u7BA1\u7406\u64CD\u4F5C\u5931\u8D25\u3002",
59
- hide_partial: "\u90E8\u5206\u5B8C\u6210\uFF1A\u5DF2\u9690\u85CF {{count}} \u4E2A\u6A21\u578B\uFF0C\u5171 {{total}} \u4E2A\u3002\u7BA1\u7406\u64CD\u4F5C\u5931\u8D25\u3002",
60
- restore_model: "\u6062\u590D {{model}}",
61
69
  disconnect: "\u65AD\u5F00\u8FDE\u63A5",
62
- hide_model: "\u9690\u85CF {{model}}",
63
- hidden_models: "\u5DF2\u9690\u85CF\u6A21\u578B\uFF08{{count}}\uFF09",
70
+ hide: "\u9690\u85CF",
64
71
  restore: "\u6062\u590D",
65
- restore_all: "\u6062\u590D\u5168\u90E8",
66
72
  cooling: "\u9650\u6D41\u81F3",
67
73
  noModels: "\u8FDE\u63A5\u540E\u8BF7\u5237\u65B0\u6A21\u578B",
68
74
  free: "\u514D\u8D39",
@@ -156,36 +162,9 @@ function mount(el, props) {
156
162
  n.className = cls;
157
163
  return n;
158
164
  };
159
- const CHIP = "inline-flex max-w-full items-center gap-1 break-all whitespace-normal rounded-lg border border-white/5 bg-white/5 px-2 py-0.5 text-[10px] font-bold text-muted-foreground";
160
- const CHIP_HIDE = "rounded p-0.5 text-muted-foreground/60 transition-colors hover:bg-white/10 hover:text-foreground disabled:opacity-40";
161
- const BUTTON_BASE = "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xl text-sm font-semibold ring-offset-background transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 active:scale-95";
162
- const BUTTON_OUTLINE = "border border-input bg-input/50 backdrop-blur-md shadow-sm hover:bg-input/80 hover:border-primary/50 hover:shadow-md hover:text-accent-foreground";
163
- const xIcon = (className) => {
164
- const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
165
- svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
166
- svg.setAttribute("width", "24");
167
- svg.setAttribute("height", "24");
168
- svg.setAttribute("viewBox", "0 0 24 24");
169
- svg.setAttribute("fill", "none");
170
- svg.setAttribute("stroke", "currentColor");
171
- svg.setAttribute("stroke-width", "2");
172
- svg.setAttribute("stroke-linecap", "round");
173
- svg.setAttribute("stroke-linejoin", "round");
174
- svg.setAttribute("aria-hidden", "true");
175
- svg.setAttribute("class", className);
176
- for (const d of ["M18 6 6 18", "m6 6 12 12"]) {
177
- const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
178
- path.setAttribute("d", d);
179
- svg.appendChild(path);
180
- }
181
- return svg;
182
- };
183
- const format = (key, vars = {}) => String(words[key] ?? key).replace(/\{\{(\w+)\}\}/g, (_, name) => String(vars[name] ?? ""));
184
- const sectionBusy = () => disposed || props.disabled || sections.some((s) => s.busy);
185
165
  const sectionWords = (section) => section.site.id === "qoder" ? { title: words.title, description: words.description } : { title: words.cnTitle, description: words.cnDescription };
186
166
  const finish = (section) => {
187
167
  section.busy = false;
188
- section.pendingModel = null;
189
168
  section.release?.();
190
169
  section.release = void 0;
191
170
  props.onBusy?.(false);
@@ -203,7 +182,6 @@ function mount(el, props) {
203
182
  section.release = lease;
204
183
  section.busy = true;
205
184
  section.error = false;
206
- section.errorMessage = void 0;
207
185
  props.onBusy?.(true);
208
186
  render();
209
187
  try {
@@ -230,35 +208,33 @@ function mount(el, props) {
230
208
  });
231
209
  const action = (section, label, fn) => {
232
210
  const button = node("button", words[label], "px-3 py-2 rounded-md bg-secondary text-secondary-foreground hover:bg-secondary/80 disabled:opacity-50");
233
- button.disabled = sectionBusy();
211
+ button.disabled = disposed || props.disabled || sections.some((s) => s.busy);
234
212
  button.onclick = fn;
235
213
  return button;
236
214
  };
237
- const smallButton = (label, extra, fn) => {
238
- const button = node("button", label, `${BUTTON_BASE} ${BUTTON_OUTLINE} ${extra}`);
215
+ const checkinButton = (section) => {
216
+ const pending = !!section.checkinPending;
217
+ const button = node("button", pending ? words.checkin_pending : words.checkin_action, "inline-flex h-8 w-full items-center justify-center gap-2 whitespace-nowrap rounded-xl border border-input bg-input/50 text-xs font-semibold shadow-sm hover:bg-input/80 disabled:pointer-events-none disabled:opacity-50");
239
218
  button.type = "button";
240
- button.disabled = sectionBusy();
241
- button.onclick = fn;
219
+ button.disabled = disposed || props.disabled || sections.some((s) => s.busy) || section.status?.status !== "connected";
220
+ button.setAttribute("aria-busy", String(pending));
221
+ button.onclick = () => {
222
+ section.checkinPending = true;
223
+ void run(section, async () => {
224
+ try {
225
+ section.checkin = void 0;
226
+ try {
227
+ section.checkin = await call(section, "/checkin", {});
228
+ } catch {
229
+ section.checkin = { outcome: "failed" };
230
+ }
231
+ } finally {
232
+ section.checkinPending = false;
233
+ }
234
+ });
235
+ };
242
236
  return button;
243
237
  };
244
- const setModelVisibility = (section, ids, hidden) => void run(section, async () => {
245
- let completed = 0;
246
- try {
247
- for (const id of ids) {
248
- await call(section, "/models/visibility", { model: id, hidden });
249
- completed += 1;
250
- }
251
- } catch (error) {
252
- if (completed > 0) section.errorMessage = format(hidden ? "hide_partial" : "restore_partial", { count: completed, total: ids.length });
253
- throw error;
254
- } finally {
255
- try {
256
- await reconcile(section);
257
- } finally {
258
- await refresh(section);
259
- }
260
- }
261
- });
262
238
  const renderSection = (section) => {
263
239
  const panel = node("div", "", "space-y-3 border-t border-border pt-4 first:border-t-0 first:pt-0");
264
240
  panel.append(
@@ -268,7 +244,7 @@ function mount(el, props) {
268
244
  panel.append(node("p", section.status ? words[section.status.status] ?? words.error : words.loading));
269
245
  if (section.status?.expired) panel.append(node("p", words.expired, "text-destructive"));
270
246
  if (section.status?.account) panel.append(node("p", `${words.account}: ${section.status.account}`));
271
- if (section.error) panel.append(node("p", section.errorMessage ?? words.error, "text-destructive"));
247
+ if (section.error) panel.append(node("p", words.error, "text-destructive"));
272
248
  const buttons = node("div", "", "flex flex-wrap gap-2");
273
249
  if (section.status?.status === "not_connected") {
274
250
  const input = node("input", "", "w-full rounded-md border border-input bg-background px-3 py-2 text-sm");
@@ -292,18 +268,37 @@ function mount(el, props) {
292
268
  await reconcile(section);
293
269
  await refresh(section);
294
270
  })));
271
+ if (section.site.id === "qoder-cn") {
272
+ panel.append(checkinButton(section));
273
+ if (section.checkin) {
274
+ const result = section.checkin;
275
+ const amount = result.credit;
276
+ const detail = result.outcome === "claimed" && typeof amount === "number" && Number.isFinite(amount) && amount > 0 ? lang === "zh" ? `+${amount} \u79EF\u5206` : `+${amount} credits` : words[`checkin_${result.outcome}`] ?? words.checkin_failed;
277
+ const note = node("p", detail + (result.outcome === "failed" && result.message ? `: ${result.message}` : ""), result.outcome === "failed" ? "text-destructive" : "text-xs text-muted-foreground");
278
+ note.setAttribute("role", "status");
279
+ panel.append(note);
280
+ }
281
+ }
295
282
  buttons.append(action(section, "usage", () => void run(section, async () => {
296
283
  section.usage = await call(section, "/usage", {});
297
284
  })));
298
285
  buttons.append(action(section, "disconnect", () => void run(section, async () => {
299
286
  await call(section, "/disconnect", {});
300
287
  section.usage = void 0;
288
+ section.checkin = void 0;
301
289
  await reconcile(section);
302
290
  await refresh(section);
303
291
  })));
304
292
  }
305
293
  buttons.append(action(section, "refresh", () => void run(section, () => refresh(section))));
306
294
  panel.append(buttons);
295
+ if (section.status?.status === "connected" && section.site.id === "qoder") panel.append(node("p", words.checkinDesktop, "text-sm text-muted-foreground"));
296
+ if (section.checkin) {
297
+ const result = section.checkin;
298
+ const amount = result.credit;
299
+ const detail = result.outcome === "claimed" && typeof amount === "number" && Number.isFinite(amount) && amount > 0 ? lang === "zh" ? `+${amount} \u79EF\u5206` : `+${amount} credits` : words[`checkin_${result.outcome}`] ?? words.checkin_failed;
300
+ panel.append(node("p", detail + (result.outcome === "failed" && result.message ? `: ${result.message}` : ""), result.outcome === "failed" ? "text-destructive" : "text-sm"));
301
+ }
307
302
  const usageLine = node("p", "", "text-sm text-muted-foreground");
308
303
  if (section.usage?.known && Array.isArray(section.usage.windows) && section.usage.windows.length) {
309
304
  usageLine.textContent = `${section.usage.stale ? words.stale : words.used}: ` + section.usage.windows.map((w) => {
@@ -314,67 +309,18 @@ function mount(el, props) {
314
309
  } else usageLine.textContent = words.unknown;
315
310
  panel.append(usageLine);
316
311
  if (section.status?.coolingUntil > Date.now()) panel.append(node("p", `${words.cooling}: ${new Date(section.status.coolingUntil).toLocaleString()}`, "text-sm text-muted-foreground"));
317
- const models = Array.isArray(section.status?.models) ? section.status.models : [];
318
- const visible = models.filter((model) => !model.hidden);
319
- const hiddenModels = models.filter((model) => model.hidden);
320
- if (section.status?.status === "connected" || models.length) {
321
- const count = node("dl", "", "grid max-w-md grid-cols-[auto_minmax(0,1fr)] gap-x-3 gap-y-1 text-xs");
322
- count.append(
323
- node("dt", words.models, "text-muted-foreground"),
324
- node("dd", format("models_count", { count: section.status?.modelCount ?? models.length }), "break-all text-foreground")
325
- );
326
- panel.append(count);
327
- }
328
- if (!models.length) panel.append(node("p", words.noModels, "text-sm text-muted-foreground"));
329
- if (visible.length) {
330
- const chips = node("div", "", "flex flex-wrap gap-2 pt-1");
331
- for (const model of visible) {
332
- const id = String(model.id);
333
- const chip = node("span", id, CHIP);
334
- if (model.context) chip.append(node("span", String(model.context), "font-medium text-muted-foreground/60"));
335
- if (model.free) chip.append(node("span", words.free, "font-medium text-muted-foreground/60"));
336
- const hide = node("button", "", CHIP_HIDE);
337
- hide.type = "button";
338
- hide.disabled = sectionBusy();
339
- hide.setAttribute("aria-label", format("hide_model", { model: id }));
340
- hide.setAttribute("aria-busy", String(section.busy && section.pendingModel === id));
341
- hide.append(xIcon("h-3 w-3"));
342
- hide.onclick = () => {
343
- section.pendingModel = id;
344
- setModelVisibility(section, [id], true);
345
- };
346
- chip.append(hide);
347
- chips.append(chip);
348
- }
349
- panel.append(chips);
350
- }
351
- if (hiddenModels.length) {
352
- const details = node("details", "", "max-w-md text-xs text-muted-foreground");
353
- details.append(node("summary", format("hidden_models", { count: hiddenModels.length }), "cursor-pointer select-none font-medium"));
354
- const list = node("ul", "", "mt-2 space-y-1");
355
- for (const model of hiddenModels) {
356
- const id = String(model.id);
357
- const item = node("li", "", "flex items-center justify-between gap-2");
358
- item.append(node("span", id, "break-all font-mono"));
359
- const restore = smallButton(
360
- words.restore,
361
- "h-6 shrink-0 px-2 text-[11px]",
362
- () => {
363
- section.pendingModel = id;
364
- setModelVisibility(section, [id], false);
365
- }
366
- );
367
- restore.setAttribute("aria-label", format("restore_model", { model: id }));
368
- item.append(restore);
369
- list.append(item);
370
- }
371
- details.append(list);
372
- details.append(smallButton(
373
- words.restore_all,
374
- "mt-2 h-7 px-2 text-[11px]",
375
- () => setModelVisibility(section, hiddenModels.map((model) => String(model.id)), false)
376
- ));
377
- panel.append(details);
312
+ panel.append(node("h4", words.models, "font-medium"));
313
+ if (!section.status?.models?.length) panel.append(node("p", words.noModels, "text-sm text-muted-foreground"));
314
+ for (const model of section.status?.models ?? []) {
315
+ const row = node("div", "", "flex items-center justify-between gap-2");
316
+ const label = `${model.id}${model.context ? ` \xB7 ${model.context}` : ""}${model.free ? ` \xB7 ${words.free}` : ""}`;
317
+ row.append(node("span", label, "text-sm"));
318
+ row.append(action(section, model.hidden ? "restore" : "hide", () => void run(section, async () => {
319
+ await call(section, "/models/visibility", { model: model.id, hidden: !model.hidden });
320
+ await reconcile(section);
321
+ await refresh(section);
322
+ })));
323
+ panel.append(row);
378
324
  }
379
325
  return panel;
380
326
  };
package/dist/plugin.js CHANGED
@@ -1,3 +1,211 @@
1
+ // src/protocol.ts
2
+ var CHAT_PATH = "/algo/api/v2/service/pro/sse/agent_chat_generation?FetchKeys=llm_model_result&AgentId=agent_common&Encode=1";
3
+ var QODER_SITES = Object.freeze([
4
+ {
5
+ id: "qoder",
6
+ label: "Qoder",
7
+ openapi: "https://openapi.qoder.sh",
8
+ api: "https://api3.qoder.sh",
9
+ web: "https://qoder.com",
10
+ chatUrl: `https://api3.qoder.sh${CHAT_PATH}`,
11
+ provider: "qoder-subscription",
12
+ marker: "ccr-managed-qoder",
13
+ credentialFile: "qoder-credentials.json",
14
+ routeBase: "/api/qoder"
15
+ },
16
+ {
17
+ id: "qoder-cn",
18
+ label: "Qoder CN",
19
+ openapi: "https://openapi.qoder.com.cn",
20
+ api: "https://gateway.qoder.com.cn",
21
+ web: "https://qoder.cn",
22
+ chatUrl: `https://gateway.qoder.com.cn${CHAT_PATH}`,
23
+ provider: "qoder-cn-subscription",
24
+ marker: "ccr-managed-qoder-cn",
25
+ credentialFile: "qoder-cn-credentials.json",
26
+ routeBase: "/api/qoder/cn"
27
+ }
28
+ ]);
29
+ var QODER_CHAT = QODER_SITES[0].chatUrl;
30
+ var QODER_PROVIDER = QODER_SITES[0].provider;
31
+ var QODER_MARKER = QODER_SITES[0].marker;
32
+ function qoderSite(site) {
33
+ return QODER_SITES.find((s) => s.id === site) ?? QODER_SITES[0];
34
+ }
35
+ function qoderError(code, status = 422) {
36
+ return Object.assign(new Error(code), { code, statusCode: status, nonRetryable: true });
37
+ }
38
+ function isQoderManaged(p, site = "qoder") {
39
+ const managedBy = qoderSite(site);
40
+ const name = p?.name;
41
+ const owner = typeof name === "string" ? QODER_SITES.find((s) => s.provider === name) : void 0;
42
+ if (owner) {
43
+ return owner.id === managedBy.id && (p?.qoder_managed === true || p?.qoderManaged === true || (p?.api_key ?? p?.apiKey) === managedBy.marker);
44
+ }
45
+ if (managedBy.id !== QODER_SITES[0].id) return false;
46
+ return p?.qoder_managed === true || p?.qoderManaged === true;
47
+ }
48
+ function lockQoderProvider(p, site = "qoder") {
49
+ const managedBy = qoderSite(site);
50
+ return {
51
+ ...p,
52
+ name: managedBy.provider,
53
+ qoder_managed: true,
54
+ qoderManaged: true,
55
+ api_base_url: managedBy.chatUrl,
56
+ api_key: managedBy.marker,
57
+ transformer: { use: [managedBy.id] }
58
+ };
59
+ }
60
+ function protectQoderConfig(next, previous) {
61
+ if (!next || typeof next !== "object") return;
62
+ const key = Array.isArray(next?.providers) && !Array.isArray(next?.Providers) ? "providers" : "Providers";
63
+ const old = previous?.Providers ?? previous?.providers ?? [];
64
+ const list = Array.isArray(next[key]) ? next[key] : [];
65
+ next[key] = list.map((p) => {
66
+ for (const managedBy of QODER_SITES) {
67
+ const previousEntry = old.find((o) => isQoderManaged(o, managedBy.id));
68
+ if (isQoderManaged(p, managedBy.id) || previousEntry && p?.name === managedBy.provider) {
69
+ return lockQoderProvider(p, managedBy.id);
70
+ }
71
+ }
72
+ return p;
73
+ });
74
+ for (const managedBy of QODER_SITES) {
75
+ const previousEntry = old.find((o) => isQoderManaged(o, managedBy.id));
76
+ if (previousEntry && !next[key].some((p) => isQoderManaged(p, managedBy.id))) {
77
+ next[key].push(lockQoderProvider(previousEntry, managedBy.id));
78
+ }
79
+ }
80
+ }
81
+ var configWriteQueues = /* @__PURE__ */ new Map();
82
+ function serializeQoderConfigWrite(configDir, fn) {
83
+ const previous = configWriteQueues.get(configDir) ?? Promise.resolve();
84
+ const job = previous.then(fn, fn);
85
+ configWriteQueues.set(configDir, job.catch(() => {
86
+ }));
87
+ return job;
88
+ }
89
+ var RETRY_AFTER_MIN_MS = 3e4;
90
+ var RETRY_AFTER_MAX_MS = 30 * 6e4;
91
+ var DEFAULT_429_COOLDOWN_MS = 3e5;
92
+ function classifyQoderError(status, body) {
93
+ if (status === 0) return { code: "qoder_upstream_error", message: "qoder_upstream_error", httpStatus: 502, retryable: true };
94
+ let hint = body;
95
+ if (typeof body === "string") {
96
+ try {
97
+ hint = JSON.parse(body);
98
+ } catch {
99
+ hint = void 0;
100
+ }
101
+ }
102
+ if (status === 401) return { code: "qoder_reauth_required", message: "qoder_reauth_required", httpStatus: 401, retryable: false };
103
+ if (status === 429) {
104
+ const seconds = Number(hint?.retry_after);
105
+ const cooldownMs = Number.isFinite(seconds) && seconds > 0 ? Math.min(Math.max(seconds * 1e3, RETRY_AFTER_MIN_MS), RETRY_AFTER_MAX_MS) : DEFAULT_429_COOLDOWN_MS;
106
+ return { code: "qoder_rate_limited", message: "qoder_rate_limited", httpStatus: 429, cooldownMs, retryable: false };
107
+ }
108
+ return {
109
+ code: "qoder_upstream_error",
110
+ message: "qoder_upstream_error",
111
+ httpStatus: status,
112
+ retryable: status >= 500
113
+ };
114
+ }
115
+ function isQoderErrorEnvelope(body) {
116
+ return body?.error?.type === "qoder_error";
117
+ }
118
+ function validPAT(value) {
119
+ return typeof value === "string" && value.startsWith("pt-") && value.length <= 4096 && !/[\x00-\x1f\x7f]/.test(value);
120
+ }
121
+
122
+ // src/checkin.ts
123
+ var CAMPAIGNS = "/sash/api/v1/me/campaigns";
124
+ var failed = (message) => ({ outcome: "failed", message });
125
+ var record = (v) => v !== null && typeof v === "object" && !Array.isArray(v) ? v : {};
126
+ var creditOf = (benefit) => {
127
+ const amount = record(benefit).amount;
128
+ const n = typeof amount === "number" || typeof amount === "string" ? Number(amount) : NaN;
129
+ return Number.isFinite(n) && n > 0 ? { credit: n } : {};
130
+ };
131
+ var RFC3339 = /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?(Z|[+-]\d\d:\d\d)$/i;
132
+ var at = (v) => {
133
+ if (typeof v === "string" && RFC3339.test(v)) {
134
+ const date = Date.parse(v);
135
+ return Number.isFinite(date) ? date : void 0;
136
+ }
137
+ const n = typeof v === "number" ? v : typeof v === "string" && v.trim() ? Number(v) : NaN;
138
+ return Number.isFinite(n) && n > 0 ? n < 1e12 ? n * 1e3 : n : void 0;
139
+ };
140
+ async function checkinQoder(credential, options) {
141
+ const machine = options.machine ?? {};
142
+ if (options.site === "qoder" && (!machine["Cosy-MachineToken"] || !machine["Cosy-MachineType"])) {
143
+ return { outcome: "ineligible", message: "International check-in requires Qoder desktop device identity" };
144
+ }
145
+ const origin = qoderSite(options.site).openapi;
146
+ let token = credential.deviceToken || credential.access;
147
+ if (!token) return failed("Check-in requires a connected account");
148
+ let rotated = false;
149
+ const network = options.fetch ?? fetch;
150
+ const call = async (url, method) => {
151
+ const request = () => network(url, {
152
+ method,
153
+ redirect: "error",
154
+ signal: AbortSignal.timeout(2e4),
155
+ dispatcher: options.host.getSubscriptionDispatcher?.() ?? options.host.getDirectDispatcher(),
156
+ headers: { ...machine, Accept: "application/json", "Accept-Encoding": "identity", "Content-Type": "application/json", Authorization: `Bearer ${token}`, "Cosy-ClientType": "10", "User-Agent": "Qoder" },
157
+ ...method === "POST" ? { body: "{}" } : {}
158
+ });
159
+ let response = await request();
160
+ if ((response.status === 401 || response.status === 403) && credential.deviceToken && options.refreshDeviceToken && !rotated) {
161
+ rotated = true;
162
+ await response.body?.cancel();
163
+ token = await options.refreshDeviceToken();
164
+ if (!token) throw new Error("missing rotated token");
165
+ response = await request();
166
+ }
167
+ return response;
168
+ };
169
+ try {
170
+ const listResponse = await call(origin + CAMPAIGNS, "GET");
171
+ if (listResponse.status !== 200) {
172
+ await listResponse.body?.cancel();
173
+ return failed(`Campaigns: HTTP ${listResponse.status}`);
174
+ }
175
+ let list;
176
+ try {
177
+ list = record(await listResponse.json());
178
+ } catch {
179
+ return failed("Campaigns: unreadable reply");
180
+ }
181
+ if (!Array.isArray(list.campaigns)) return failed("Campaigns: invalid reply");
182
+ const campaigns = list.campaigns.map(record).filter((c) => c.actionType === "CLAIM_BENEFIT");
183
+ const campaign = campaigns.find((c) => c.claimStatus === "CLAIMABLE");
184
+ if (!campaign) {
185
+ const now = options.now?.() ?? Date.now();
186
+ const done = campaigns.find((c) => c.claimStatus === "CLAIMED" && (at(c.startAt) ?? -Infinity) <= now && now < (at(c.endAt) ?? Infinity));
187
+ return done ? { outcome: "done", ...creditOf(done.benefit) } : { outcome: "inactive" };
188
+ }
189
+ if (typeof campaign.campaignId !== "string" || !campaign.campaignId || campaign.campaignId.length > 1024) return failed("Campaigns: missing campaign id");
190
+ const claimResponse = await call(`${origin}${CAMPAIGNS}/${encodeURIComponent(campaign.campaignId)}/claim`, "POST");
191
+ if (claimResponse.status !== 200) {
192
+ await claimResponse.body?.cancel();
193
+ return failed(`Claim: HTTP ${claimResponse.status}`);
194
+ }
195
+ let claim;
196
+ try {
197
+ claim = record(await claimResponse.json());
198
+ } catch {
199
+ return failed("Claim: unreadable reply");
200
+ }
201
+ const data = record(claim.data);
202
+ if ((claim.status ?? data.status) !== "CLAIMED") return failed("Claim: unexpected reply");
203
+ return { outcome: "claimed", ...creditOf(claim.benefit ?? data.benefit ?? campaign.benefit) };
204
+ } catch {
205
+ return failed("Check-in request failed");
206
+ }
207
+ }
208
+
1
209
  // src/auth-service.ts
2
210
  import { constants as constants2 } from "node:fs";
3
211
  import { open, mkdir, rename, rm, unlink } from "node:fs/promises";
@@ -161,8 +369,8 @@ ${path}`).digest("hex");
161
369
  const expiresAt = (jt) => now() + (jt.expires_in > 0 ? jt.expires_in : DAY);
162
370
  function exchangeExpiry(jt) {
163
371
  for (const k of ["expire_time", "expires_at"]) {
164
- const at = Date.parse(String(jt?.[k] ?? "").trim());
165
- if (!isNaN(at)) return at;
372
+ const at2 = Date.parse(String(jt?.[k] ?? "").trim());
373
+ if (!isNaN(at2)) return at2;
166
374
  }
167
375
  if (jt?.expires_in > 0) return now() + jt.expires_in;
168
376
  return now() + DAY;
@@ -343,15 +551,15 @@ ${path}`).digest("hex");
343
551
  function fitEffort(want, levels) {
344
552
  if (want === "ultra" && !levels.includes(want)) want = "max";
345
553
  if (!levels.length || levels.includes(want)) return want;
346
- const at = EFFORT_RANK.indexOf(want);
347
- if (at < 0) return want;
554
+ const at2 = EFFORT_RANK.indexOf(want);
555
+ if (at2 < 0) return want;
348
556
  let best = want;
349
557
  let dist = EFFORT_RANK.length;
350
558
  for (const l of levels) {
351
559
  const i = EFFORT_RANK.indexOf(l);
352
560
  if (i < 0 || l === "none") continue;
353
- const d = Math.abs(i - at);
354
- if (d < dist || d === dist && i > at) [best, dist] = [l, d];
561
+ const d = Math.abs(i - at2);
562
+ if (d < dist || d === dist && i > at2) [best, dist] = [l, d];
355
563
  }
356
564
  return best;
357
565
  }
@@ -381,10 +589,10 @@ ${path}`).digest("hex");
381
589
  return out;
382
590
  }
383
591
  const textOf = (c) => typeof c === "string" ? c : blocksOf(c).filter((b) => b.type === "text").map((b) => b.text).join("");
384
- function callID(at, k, c) {
592
+ function callID(at2, k, c) {
385
593
  const args = c.function?.arguments;
386
594
  const said = typeof args === "string" ? args : JSON.stringify(args ?? {});
387
- return "call_" + createHash("sha256").update(`${at}\0${k}\0${c.function?.name ?? ""}\0${said}`).digest("hex").slice(0, 32);
595
+ return "call_" + createHash("sha256").update(`${at2}\0${k}\0${c.function?.name ?? ""}\0${said}`).digest("hex").slice(0, 32);
388
596
  }
389
597
  function qoderMessages(msgs) {
390
598
  const out = [];
@@ -410,8 +618,8 @@ ${path}`).digest("hex");
410
618
  let txt = textOf(m.content);
411
619
  const ims = Array.isArray(m.content) ? m.content.filter((p) => p?.type === "image_url").map(imageBlock).filter(Boolean) : [];
412
620
  const named = String(m.tool_call_id ?? "").trim();
413
- const at = named ? turn?.open.findIndex((id) => id === named) ?? -1 : -1;
414
- const pick = at >= 0 ? at : turn ? turn.open.findIndex((id) => !turn.kept.has(id)) : -1;
621
+ const at2 = named ? turn?.open.findIndex((id) => id === named) ?? -1 : -1;
622
+ const pick = at2 >= 0 ? at2 : turn ? turn.open.findIndex((id) => !turn.kept.has(id)) : -1;
415
623
  if (pick < 0) {
416
624
  asText(txt, ims);
417
625
  continue;
@@ -467,10 +675,10 @@ ${path}`).digest("hex");
467
675
  const out = msgs.map((m) => m.role === "user" && Array.isArray(m.content) ? { ...m, contents: m.content } : m);
468
676
  const last = out.at(-1);
469
677
  if (last?.role !== "user" || !Array.isArray(last.content)) return out;
470
- const at = last.content.findLastIndex((b) => b?.type && !UNCACHED.has(b.type));
471
- if (at < 0) return out;
678
+ const at2 = last.content.findLastIndex((b) => b?.type && !UNCACHED.has(b.type));
679
+ if (at2 < 0) return out;
472
680
  const blocks = last.content.slice();
473
- blocks[at] = { ...blocks[at], cache_control: { type: "ephemeral" } };
681
+ blocks[at2] = { ...blocks[at2], cache_control: { type: "ephemeral" } };
474
682
  out[out.length - 1] = { ...last, content: blocks, contents: blocks };
475
683
  return out;
476
684
  }
@@ -491,8 +699,8 @@ ${path}`).digest("hex");
491
699
  const taskName = (prompt) => prompt.length > 10 ? prompt.slice(0, 10) : prompt;
492
700
  function taskOf(sid, chat2, atTime = now()) {
493
701
  const msgs = chat2.messages ?? [];
494
- const at = msgs.findLastIndex((x) => x?.role === "user");
495
- const key = createHash("sha256").update(JSON.stringify([sid, at, at < 0 ? null : msgs[at].content ?? null])).digest("hex");
702
+ const at2 = msgs.findLastIndex((x) => x?.role === "user");
703
+ const key = createHash("sha256").update(JSON.stringify([sid, at2, at2 < 0 ? null : msgs[at2].content ?? null])).digest("hex");
496
704
  for (const [k, t2] of TASKS) {
497
705
  if (atTime - t2.used <= TASK_IDLE) break;
498
706
  TASKS.delete(k);
@@ -500,7 +708,7 @@ ${path}`).digest("hex");
500
708
  let t = TASKS.get(key);
501
709
  if (t && atTime - t.used > TASK_IDLE) t = void 0;
502
710
  TASKS.delete(key);
503
- t ??= { id: randomUUID(), begin: atTime, answered: false, name: taskName(at < 0 ? "" : textOf(msgs[at].content)) };
711
+ t ??= { id: randomUUID(), begin: atTime, answered: false, name: taskName(at2 < 0 ? "" : textOf(msgs[at2].content)) };
504
712
  t.used = atTime;
505
713
  TASKS.set(key, t);
506
714
  while (TASKS.size > TASK_MAX) TASKS.delete(TASKS.keys().next().value);
@@ -515,9 +723,9 @@ ${path}`).digest("hex");
515
723
  const isObj = (v) => v && typeof v === "object" && !Array.isArray(v);
516
724
  const target = (ref) => {
517
725
  if (!ref.startsWith("#/")) return void 0;
518
- let at = root;
519
- for (const k of ref.slice(2).split("/")) at = at?.[k.replaceAll("~1", "/").replaceAll("~0", "~")];
520
- return isObj(at) ? at : void 0;
726
+ let at2 = root;
727
+ for (const k of ref.slice(2).split("/")) at2 = at2?.[k.replaceAll("~1", "/").replaceAll("~0", "~")];
728
+ return isObj(at2) ? at2 : void 0;
521
729
  };
522
730
  const object = (b, seen = []) => {
523
731
  if (!isObj(b)) return false;
@@ -718,7 +926,7 @@ ${path}`).digest("hex");
718
926
  async function queueWait(api, cred, ask, first, signal, deadline, site2) {
719
927
  let q = first ?? {};
720
928
  let asked = false;
721
- let failed = 0;
929
+ let failed2 = 0;
722
930
  for (; ; ) {
723
931
  checkAbort(signal);
724
932
  if (QUEUE.now() >= deadline) return false;
@@ -731,11 +939,11 @@ ${path}`).digest("hex");
731
939
  } catch (e) {
732
940
  checkAbort(signal);
733
941
  if (e instanceof QueueEnd) return false;
734
- if (++failed >= QUEUE.failures) return false;
942
+ if (++failed2 >= QUEUE.failures) return false;
735
943
  q = { ...q, retryAfterSeconds: void 0 };
736
944
  continue;
737
945
  }
738
- failed = 0;
946
+ failed2 = 0;
739
947
  q = { ...st, modelKey: st.modelKey ?? q.modelKey, queueType: st.queueType ?? q.queueType };
740
948
  if (st.isQueued === false) {
741
949
  if (st.retryAfterSeconds > 0) await QUEUE.pause(Math.min(queuePause(st.retryAfterSeconds), Math.max(0, deadline - QUEUE.now())), signal);
@@ -863,21 +1071,21 @@ ${path}`).digest("hex");
863
1071
  // when the turn ended in an error
864
1072
  bare(out, comb, x, end) {
865
1073
  const head = comb.slice(0, x);
866
- const at = comb.slice(x);
867
- const opener = at.match(/^<[^<>]*>?/)[0];
1074
+ const at2 = comb.slice(x);
1075
+ const opener = at2.match(/^<[^<>]*>?/)[0];
868
1076
  if (quoted(this.sent + head)) {
869
1077
  this.text(out, head + opener);
870
- return at.slice(opener.length);
1078
+ return at2.slice(opener.length);
871
1079
  }
872
- const b = xmlCall(at, end, this.tools);
1080
+ const b = xmlCall(at2, end, this.tools);
873
1081
  if (b.wait) {
874
1082
  if (head) this.text(out, head);
875
- this.textBuf = at;
1083
+ this.textBuf = at2;
876
1084
  return null;
877
1085
  }
878
1086
  if (b.text) {
879
1087
  this.text(out, head + opener);
880
- return at.slice(opener.length);
1088
+ return at2.slice(opener.length);
881
1089
  }
882
1090
  if (head) this.text(out, head);
883
1091
  if (b.error) {
@@ -1220,9 +1428,9 @@ ${path}`).digest("hex");
1220
1428
  if (x < -4 || x >= 6) return `${m}e${x < 0 ? "-" : "+"}${String(Math.abs(x)).padStart(2, "0")}`;
1221
1429
  return String(n);
1222
1430
  }
1223
- const RFC3339 = /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?(Z|[+-]\d\d:\d\d)$/i;
1431
+ const RFC33392 = /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?(Z|[+-]\d\d:\d\d)$/i;
1224
1432
  function when(v) {
1225
- if (typeof v === "string" && RFC3339.test(v) && !isNaN(Date.parse(v))) return new Date(v).toISOString();
1433
+ if (typeof v === "string" && RFC33392.test(v) && !isNaN(Date.parse(v))) return new Date(v).toISOString();
1226
1434
  const n = typeof v === "number" ? v : typeof v === "string" && v.trim() !== "" ? Number(v.trim()) : NaN;
1227
1435
  if (!isFinite(n) || n <= 0) return void 0;
1228
1436
  return new Date(Math.trunc(n < 1e12 ? n * 1e3 : n)).toISOString();
@@ -1471,127 +1679,6 @@ ${path}`).digest("hex");
1471
1679
  };
1472
1680
  }
1473
1681
 
1474
- // src/protocol.ts
1475
- var CHAT_PATH = "/algo/api/v2/service/pro/sse/agent_chat_generation?FetchKeys=llm_model_result&AgentId=agent_common&Encode=1";
1476
- var QODER_SITES = Object.freeze([
1477
- {
1478
- id: "qoder",
1479
- label: "Qoder",
1480
- openapi: "https://openapi.qoder.sh",
1481
- api: "https://api3.qoder.sh",
1482
- web: "https://qoder.com",
1483
- chatUrl: `https://api3.qoder.sh${CHAT_PATH}`,
1484
- provider: "qoder-subscription",
1485
- marker: "ccr-managed-qoder",
1486
- credentialFile: "qoder-credentials.json",
1487
- routeBase: "/api/qoder"
1488
- },
1489
- {
1490
- id: "qoder-cn",
1491
- label: "Qoder CN",
1492
- openapi: "https://openapi.qoder.com.cn",
1493
- api: "https://gateway.qoder.com.cn",
1494
- web: "https://qoder.cn",
1495
- chatUrl: `https://gateway.qoder.com.cn${CHAT_PATH}`,
1496
- provider: "qoder-cn-subscription",
1497
- marker: "ccr-managed-qoder-cn",
1498
- credentialFile: "qoder-cn-credentials.json",
1499
- routeBase: "/api/qoder/cn"
1500
- }
1501
- ]);
1502
- var QODER_CHAT = QODER_SITES[0].chatUrl;
1503
- var QODER_PROVIDER = QODER_SITES[0].provider;
1504
- var QODER_MARKER = QODER_SITES[0].marker;
1505
- function qoderSite(site) {
1506
- return QODER_SITES.find((s) => s.id === site) ?? QODER_SITES[0];
1507
- }
1508
- function qoderError(code, status = 422) {
1509
- return Object.assign(new Error(code), { code, statusCode: status, nonRetryable: true });
1510
- }
1511
- function isQoderManaged(p, site = "qoder") {
1512
- const managedBy = qoderSite(site);
1513
- const name = p?.name;
1514
- const owner = typeof name === "string" ? QODER_SITES.find((s) => s.provider === name) : void 0;
1515
- if (owner) {
1516
- return owner.id === managedBy.id && (p?.qoder_managed === true || p?.qoderManaged === true || (p?.api_key ?? p?.apiKey) === managedBy.marker);
1517
- }
1518
- if (managedBy.id !== QODER_SITES[0].id) return false;
1519
- return p?.qoder_managed === true || p?.qoderManaged === true;
1520
- }
1521
- function lockQoderProvider(p, site = "qoder") {
1522
- const managedBy = qoderSite(site);
1523
- return {
1524
- ...p,
1525
- name: managedBy.provider,
1526
- qoder_managed: true,
1527
- qoderManaged: true,
1528
- api_base_url: managedBy.chatUrl,
1529
- api_key: managedBy.marker,
1530
- transformer: { use: [managedBy.id] }
1531
- };
1532
- }
1533
- function protectQoderConfig(next, previous) {
1534
- if (!next || typeof next !== "object") return;
1535
- const key = Array.isArray(next?.providers) && !Array.isArray(next?.Providers) ? "providers" : "Providers";
1536
- const old = previous?.Providers ?? previous?.providers ?? [];
1537
- const list = Array.isArray(next[key]) ? next[key] : [];
1538
- next[key] = list.map((p) => {
1539
- for (const managedBy of QODER_SITES) {
1540
- const previousEntry = old.find((o) => isQoderManaged(o, managedBy.id));
1541
- if (isQoderManaged(p, managedBy.id) || previousEntry && p?.name === managedBy.provider) {
1542
- return lockQoderProvider(p, managedBy.id);
1543
- }
1544
- }
1545
- return p;
1546
- });
1547
- for (const managedBy of QODER_SITES) {
1548
- const previousEntry = old.find((o) => isQoderManaged(o, managedBy.id));
1549
- if (previousEntry && !next[key].some((p) => isQoderManaged(p, managedBy.id))) {
1550
- next[key].push(lockQoderProvider(previousEntry, managedBy.id));
1551
- }
1552
- }
1553
- }
1554
- var configWriteQueues = /* @__PURE__ */ new Map();
1555
- function serializeQoderConfigWrite(configDir, fn) {
1556
- const previous = configWriteQueues.get(configDir) ?? Promise.resolve();
1557
- const job = previous.then(fn, fn);
1558
- configWriteQueues.set(configDir, job.catch(() => {
1559
- }));
1560
- return job;
1561
- }
1562
- var RETRY_AFTER_MIN_MS = 3e4;
1563
- var RETRY_AFTER_MAX_MS = 30 * 6e4;
1564
- var DEFAULT_429_COOLDOWN_MS = 3e5;
1565
- function classifyQoderError(status, body) {
1566
- if (status === 0) return { code: "qoder_upstream_error", message: "qoder_upstream_error", httpStatus: 502, retryable: true };
1567
- let hint = body;
1568
- if (typeof body === "string") {
1569
- try {
1570
- hint = JSON.parse(body);
1571
- } catch {
1572
- hint = void 0;
1573
- }
1574
- }
1575
- if (status === 401) return { code: "qoder_reauth_required", message: "qoder_reauth_required", httpStatus: 401, retryable: false };
1576
- if (status === 429) {
1577
- const seconds = Number(hint?.retry_after);
1578
- const cooldownMs = Number.isFinite(seconds) && seconds > 0 ? Math.min(Math.max(seconds * 1e3, RETRY_AFTER_MIN_MS), RETRY_AFTER_MAX_MS) : DEFAULT_429_COOLDOWN_MS;
1579
- return { code: "qoder_rate_limited", message: "qoder_rate_limited", httpStatus: 429, cooldownMs, retryable: false };
1580
- }
1581
- return {
1582
- code: "qoder_upstream_error",
1583
- message: "qoder_upstream_error",
1584
- httpStatus: status,
1585
- retryable: status >= 500
1586
- };
1587
- }
1588
- function isQoderErrorEnvelope(body) {
1589
- return body?.error?.type === "qoder_error";
1590
- }
1591
- function validPAT(value) {
1592
- return typeof value === "string" && value.startsWith("pt-") && value.length <= 4096 && !/[\x00-\x1f\x7f]/.test(value);
1593
- }
1594
-
1595
1682
  // src/auth-service.ts
1596
1683
  var REFRESH_LEAD_MS = 5 * 6e4;
1597
1684
  var USAGE_CACHE_MS = 6e4;
@@ -1624,9 +1711,9 @@ function validate(value) {
1624
1711
  }
1625
1712
  function maskEmail(email) {
1626
1713
  if (!email) return null;
1627
- const at = email.indexOf("@");
1628
- if (at <= 0) return null;
1629
- return `${email[0]}***${email.slice(at)}`;
1714
+ const at2 = email.indexOf("@");
1715
+ if (at2 <= 0) return null;
1716
+ return `${email[0]}***${email.slice(at2)}`;
1630
1717
  }
1631
1718
  function toRecord(m) {
1632
1719
  return { key: String(m.key), name: String(m.name ?? m.key), context: Number(m.context) || 0, free: m.free === true };
@@ -1780,17 +1867,17 @@ var QoderAuthService = class {
1780
1867
  const before = structuredClone(cfg);
1781
1868
  const key = Array.isArray(cfg.providers) && !Array.isArray(cfg.Providers) ? "providers" : "Providers";
1782
1869
  const list = cfg[key] ?? [];
1783
- const at = list.findIndex((p) => isQoderManaged(p, this.site.id));
1870
+ const at2 = list.findIndex((p) => isQoderManaged(p, this.site.id));
1784
1871
  if (saved) {
1785
- if (at < 0 && list.some((p) => p?.name === this.site.provider)) throw qoderError("qoder_provider_name_conflict");
1872
+ if (at2 < 0 && list.some((p) => p?.name === this.site.provider)) throw qoderError("qoder_provider_name_conflict");
1786
1873
  const provider = lockQoderProvider({
1787
- ...at >= 0 ? list[at] : {},
1874
+ ...at2 >= 0 ? list[at2] : {},
1788
1875
  models: saved.models.filter((m) => !saved.hiddenModels.includes(m.key)).map((m) => m.key),
1789
- enabled: at >= 0 ? list[at].enabled !== false : true
1876
+ enabled: at2 >= 0 ? list[at2].enabled !== false : true
1790
1877
  }, this.site.id);
1791
- if (at >= 0) list[at] = provider;
1878
+ if (at2 >= 0) list[at2] = provider;
1792
1879
  else list.push(provider);
1793
- } else if (at >= 0) list.splice(at, 1);
1880
+ } else if (at2 >= 0) list.splice(at2, 1);
1794
1881
  cfg[key] = list;
1795
1882
  try {
1796
1883
  await this.options.host.writeConfig(cfg);
@@ -1946,6 +2033,22 @@ var QoderAuthService = class {
1946
2033
  return { success: true };
1947
2034
  });
1948
2035
  }
2036
+ /** Manual check-in is independent of chat cooldown and model selection.
2037
+ * CN transactions join the exclusive lane so concurrent requests cannot
2038
+ * double-claim (the second observes the upstream's post-claim state).
2039
+ * The intl gate stays outside the lane: it does no network work and must
2040
+ * return immediately even while a CN transaction is queued. */
2041
+ async checkin() {
2042
+ if (this.site.id === "qoder") return { outcome: "ineligible", message: "International check-in requires Qoder desktop device identity" };
2043
+ return this.exclusive(async () => {
2044
+ try {
2045
+ const credential = await this.freshCredential();
2046
+ return await checkinQoder(credential, { site: this.site.id, host: this.options.host, fetch: this.options.fetch });
2047
+ } catch {
2048
+ return { outcome: "failed", message: "Check-in requires a usable connected account" };
2049
+ }
2050
+ });
2051
+ }
1949
2052
  async getUsage() {
1950
2053
  await this.load();
1951
2054
  if (!this.saved || this.disconnecting) throw qoderError("qoder_not_connected", 409);
@@ -2409,6 +2512,7 @@ function registerQoderAdminRoutes(app, service, site = "qoder") {
2409
2512
  }],
2410
2513
  ["POST", "sync", () => service.syncModels()],
2411
2514
  ["POST", "usage", () => service.getUsage()],
2515
+ ["POST", "checkin", () => service.checkin()],
2412
2516
  ["POST", "models/visibility", (body) => {
2413
2517
  if (typeof body?.model !== "string" || typeof body.hidden !== "boolean") throw Object.assign(new Error(), { code: "qoder_model_unknown", statusCode: 422 });
2414
2518
  return service.setModelHidden(body.model, body.hidden);
@@ -2444,23 +2548,26 @@ var qoderLocales = {
2444
2548
  error: "Management request failed",
2445
2549
  account: "Account",
2446
2550
  models: "Models",
2447
- models_count: "{{count}} models",
2448
2551
  pat: "Personal access token",
2449
2552
  patPlaceholder: "pt-\u2026",
2450
2553
  connect: "Connect",
2554
+ checkin: "Check in",
2555
+ checkin_action: "Check in",
2556
+ checkin_pending: "Checking in\u2026",
2557
+ checkin_claimed: "Credits claimed",
2558
+ checkin_done: "Already claimed today",
2559
+ checkin_inactive: "No campaign today",
2560
+ checkin_ineligible: "Unavailable",
2561
+ checkin_failed: "Check-in failed",
2562
+ checkinDesktop: "International check-in requires Qoder desktop device identity; claim in Qoder desktop.",
2451
2563
  sync: "Refresh models",
2452
2564
  usage: "Refresh quota",
2453
2565
  unknown: "Quota unknown",
2454
2566
  stale: "Last known quota (stale)",
2455
2567
  used: "Allowance",
2456
- restore_partial: "Partially completed: restored {{count}} of {{total}} models. Management request failed.",
2457
- hide_partial: "Partially completed: hid {{count}} of {{total}} models. Management request failed.",
2458
- restore_model: "Restore {{model}}",
2459
2568
  disconnect: "Disconnect",
2460
- hide_model: "Hide {{model}}",
2461
- hidden_models: "Hidden models ({{count}})",
2569
+ hide: "Hide",
2462
2570
  restore: "Restore",
2463
- restore_all: "Restore all",
2464
2571
  cooling: "Rate limited until",
2465
2572
  noModels: "Refresh models after connecting",
2466
2573
  free: "free",
@@ -2478,23 +2585,26 @@ var qoderLocales = {
2478
2585
  error: "\u7BA1\u7406\u64CD\u4F5C\u5931\u8D25",
2479
2586
  account: "\u8D26\u53F7",
2480
2587
  models: "\u6A21\u578B",
2481
- models_count: "{{count}} \u4E2A\u6A21\u578B",
2482
2588
  pat: "\u4E2A\u4EBA\u8BBF\u95EE\u4EE4\u724C",
2483
2589
  patPlaceholder: "pt-\u2026",
2484
2590
  connect: "\u8FDE\u63A5",
2591
+ checkin: "\u7B7E\u5230",
2592
+ checkin_action: "\u7B7E\u5230",
2593
+ checkin_pending: "\u7B7E\u5230\u4E2D\u2026",
2594
+ checkin_claimed: "\u9886\u53D6\u6210\u529F",
2595
+ checkin_done: "\u4ECA\u65E5\u5DF2\u9886",
2596
+ checkin_inactive: "\u4ECA\u65E5\u65E0\u6D3B\u52A8",
2597
+ checkin_ineligible: "\u4E0D\u53EF\u7528",
2598
+ checkin_failed: "\u7B7E\u5230\u5931\u8D25",
2599
+ checkinDesktop: "\u56FD\u9645\u7AD9\u9700\u8981\u5728 Qoder \u684C\u9762\u7AEF\u5185\u7B7E\u5230\u3002",
2485
2600
  sync: "\u5237\u65B0\u6A21\u578B",
2486
2601
  usage: "\u5237\u65B0\u989D\u5EA6",
2487
2602
  unknown: "\u989D\u5EA6\u672A\u77E5",
2488
2603
  stale: "\u4E0A\u6B21\u989D\u5EA6\uFF08\u5DF2\u8FC7\u671F\uFF09",
2489
2604
  used: "\u5DF2\u7528\u989D\u5EA6",
2490
- restore_partial: "\u90E8\u5206\u5B8C\u6210\uFF1A\u5DF2\u6062\u590D {{count}} \u4E2A\u6A21\u578B\uFF0C\u5171 {{total}} \u4E2A\u3002\u7BA1\u7406\u64CD\u4F5C\u5931\u8D25\u3002",
2491
- hide_partial: "\u90E8\u5206\u5B8C\u6210\uFF1A\u5DF2\u9690\u85CF {{count}} \u4E2A\u6A21\u578B\uFF0C\u5171 {{total}} \u4E2A\u3002\u7BA1\u7406\u64CD\u4F5C\u5931\u8D25\u3002",
2492
- restore_model: "\u6062\u590D {{model}}",
2493
2605
  disconnect: "\u65AD\u5F00\u8FDE\u63A5",
2494
- hide_model: "\u9690\u85CF {{model}}",
2495
- hidden_models: "\u5DF2\u9690\u85CF\u6A21\u578B\uFF08{{count}}\uFF09",
2606
+ hide: "\u9690\u85CF",
2496
2607
  restore: "\u6062\u590D",
2497
- restore_all: "\u6062\u590D\u5168\u90E8",
2498
2608
  cooling: "\u9650\u6D41\u81F3",
2499
2609
  noModels: "\u8FDE\u63A5\u540E\u8BF7\u5237\u65B0\u6A21\u578B",
2500
2610
  free: "\u514D\u8D39",
@@ -2503,7 +2613,7 @@ var qoderLocales = {
2503
2613
  };
2504
2614
 
2505
2615
  // src/plugin.ts
2506
- var manifest = { name: "qoder", version: "0.2.0", coreApi: "1", description: "Qoder (qoder.com / qoder.cn) subscription provider with PAT sign-in, model management and usage." };
2616
+ var manifest = { name: "qoder", version: "0.3.1", coreApi: "1", description: "Qoder (qoder.com / qoder.cn) subscription provider with PAT sign-in, model management and usage." };
2507
2617
  function activatedTransformer(site, service) {
2508
2618
  return class ActivatedQoderTransformer extends QoderTransformer {
2509
2619
  static TransformerName = site;
package/dist/plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qoder",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "coreApi": "1",
5
5
  "description": "Qoder subscription provider (qoder.com and qoder.cn): Personal Access Token sign-in, fixed chat upstream, model management and quota."
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wengine-ai/ccr-provider-qoder",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Qoder subscription provider (qoder.com and qoder.cn): Personal Access Token sign-in, fixed agent_chat_generation upstream, model management and usage.",
5
5
  "type": "module",
6
6
  "main": "dist/plugin.js",
@@ -29,4 +29,4 @@
29
29
  "dist/*.json",
30
30
  "dist/*.md"
31
31
  ]
32
- }
32
+ }