dsh-qoder-connect 0.1.8 → 0.1.9

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/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-DE76gF0Y.js";
2
+ import { D as FALLBACK_QODER_MODELS, E as qoderCatalogPath, T as QoderCatalogStore, V as qoderCredentialIdentity, 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, z as QoderCredentialStore } from "./variants-C8ezztjV.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
@@ -213,9 +213,9 @@ window.__ModuleLoader__.load({
213
213
  * The shared quota-settings card: one card above the two variant cards that
214
214
  * configures both sidebar quota widgets.
215
215
  *
216
- * Like the built-in plugin cards, it registers into `settings.plugin.item`
217
- * keyed by its own settings namespace (`qoder-quota`), binds that
218
- * namespace through the client settings scope, and writes through the scope's
216
+ * Like the built-in plugin cards, it registers into the Plugins tab's card
217
+ * list — `settings.plugin.item` on DSH 0.1.5, the shared 《插件设置》 block on
218
+ * 0.1.7, where that list is gone — and writes through the settings face's
219
219
  * revision-fenced `set` — the same durable-write path every preference row
220
220
  * uses. A toggle commits on click: each click is one explicit user choice,
221
221
  * and the scope's ordering makes the last one win, so no staged-draft form is
@@ -2793,6 +2793,115 @@ window.__ModuleLoader__.load({
2793
2793
  return t("probeNoteUnknown");
2794
2794
  }
2795
2795
  //#endregion
2796
+ //#region src/client/http-settings-scope.ts
2797
+ /** Base path of the host half's settings face. */
2798
+ const ROUTE_BASE = "/plugins/dsh-qoder-connect";
2799
+ /**
2800
+ * Fetch one settings face document.
2801
+ * @param init - the request init (method, headers, body).
2802
+ * @returns the parsed document.
2803
+ * @throws when the transport or the host refuses the request.
2804
+ */
2805
+ async function request(init) {
2806
+ const response = await fetch(`${ROUTE_BASE}/settings`, {
2807
+ credentials: "same-origin",
2808
+ ...init
2809
+ });
2810
+ const value = await response.json().catch(() => void 0);
2811
+ if (!response.ok) {
2812
+ const error = typeof value === "object" && value !== null && "error" in value ? String(value["error"]) : `HTTP ${response.status}`;
2813
+ throw new Error(error);
2814
+ }
2815
+ if (typeof value !== "object" || value === null || !("value" in value)) throw new Error("qoder: settings face answered an invalid document");
2816
+ return value;
2817
+ }
2818
+ /**
2819
+ * The scope the card was written against, backed by this plugin's own file.
2820
+ *
2821
+ * A write optimistically adopts the value it just sent, then confirms with the
2822
+ * host's answer (which re-reads the file it wrote), so the card never renders a
2823
+ * value the host does not hold.
2824
+ */
2825
+ var OwnQuotaSettingsScope = class {
2826
+ document;
2827
+ listeners = /* @__PURE__ */ new Set();
2828
+ snapshot = {
2829
+ status: "loading",
2830
+ value: void 0,
2831
+ writable: true
2832
+ };
2833
+ /**
2834
+ * Load the current document. Call once before the card binds; repeated calls
2835
+ * are harmless and re-read the host.
2836
+ * @returns the effective values.
2837
+ */
2838
+ async load() {
2839
+ try {
2840
+ this.document = await request({ headers: { accept: "application/json" } });
2841
+ } catch {
2842
+ return;
2843
+ }
2844
+ this.publish();
2845
+ return this.document.value;
2846
+ }
2847
+ /** @returns the stable snapshot the card reads (ready once loaded). */
2848
+ getSnapshot() {
2849
+ return this.snapshot;
2850
+ }
2851
+ /** @param listener - invoked after every snapshot change. @returns the disposer. */
2852
+ subscribe(listener) {
2853
+ this.listeners.add(listener);
2854
+ return () => {
2855
+ this.listeners.delete(listener);
2856
+ };
2857
+ }
2858
+ /**
2859
+ * Write one field.
2860
+ * @param field - field name inside the settings document.
2861
+ * @param value - the JSON-shaped value to store.
2862
+ * @returns whether the host accepted the write.
2863
+ */
2864
+ async set(field, value) {
2865
+ return this.patch({ [field]: value });
2866
+ }
2867
+ /**
2868
+ * Clear one field: the value falls back to the schema default.
2869
+ * @param field - field name inside the settings document.
2870
+ * @returns whether the host accepted the clear.
2871
+ */
2872
+ async unset(field) {
2873
+ return this.patch({ [field]: null });
2874
+ }
2875
+ /** Send one patch and adopt the host's answer. */
2876
+ async patch(patch) {
2877
+ const key = this.document?.key;
2878
+ if (key === void 0) return false;
2879
+ try {
2880
+ this.document = await request({
2881
+ method: "POST",
2882
+ headers: {
2883
+ "Content-Type": "application/json",
2884
+ "X-Qoder-Settings-Key": key
2885
+ },
2886
+ body: JSON.stringify(patch)
2887
+ });
2888
+ this.publish();
2889
+ return true;
2890
+ } catch {
2891
+ return false;
2892
+ }
2893
+ }
2894
+ /** Rebuild the cached snapshot and notify the subscribers. */
2895
+ publish() {
2896
+ this.snapshot = {
2897
+ status: this.document === void 0 ? "loading" : "ready",
2898
+ value: this.document?.value,
2899
+ writable: true
2900
+ };
2901
+ for (const listener of this.listeners) listener();
2902
+ }
2903
+ };
2904
+ //#endregion
2796
2905
  //#region src/client/quota-merge.ts
2797
2906
  /**
2798
2907
  * Group credit accounts by (packageName, packageEndTime) and sum each group.
@@ -3928,23 +4037,29 @@ window.__ModuleLoader__.load({
3928
4037
  /** Stable browser-plugin name. */
3929
4038
  const name = "dsh-qoder-connect-client";
3930
4039
  /**
3931
- * Client services required by the Plugin configuration contribution.
4040
+ * Client services this bundle requires BEFORE it activates.
3932
4041
  *
3933
4042
  * DSH 0.1.2 removed `@deepseek-ai/dsh-client-runtime` (the package that used to
3934
4043
  * hold the browser `ClientContext` alias and the `slots` service). The services
3935
4044
  * this card relies on now come from narrower packages: the `slots` registry
3936
4045
  * moved to `@deepseek-ai/dsh-client-ui-renderer`, `locale` stayed in
3937
- * `@deepseek-ai/dsh-client-locale`, and the `settings.plugin.item` slot is
3938
- * declared by `@deepseek-ai/dsh-client-ui-settings-plugins`. All three are
3939
- * named in the package's `dsh.client.inject` list, so cordis has activated
3940
- * them before this plugin's fiber starts.
4046
+ * `@deepseek-ai/dsh-client-locale`, and the Plugins tab's card list
4047
+ * (`settings.plugin.item`) is declared by
4048
+ * `@deepseek-ai/dsh-client-ui-settings-plugins`. Those packages are named in
4049
+ * the package's `dsh.client.inject` list.
4050
+ *
4051
+ * NOT declared here: the settings services. `settingsScope` (0.1.5) does not
4052
+ * exist at all on 0.1.7 — a static service dependency on it is what left this
4053
+ * plugin's client activation pending forever — and `configForms` (0.1.7) does
4054
+ * not exist on 0.1.5. Both are waited for with `ctx.inject([...], callback)`
4055
+ * below, where a missing service simply never calls back instead of holding
4056
+ * activation.
3941
4057
  */
3942
4058
  const inject = [
3943
4059
  "slots",
3944
4060
  "locale",
3945
4061
  "remote",
3946
- "remote.session",
3947
- "settingsScope"
4062
+ "remote.session"
3948
4063
  ];
3949
4064
  /** The status routes per variant id (mirrors the host's locked route table). */
3950
4065
  const VARIANT_STATUS = {
@@ -3952,6 +4067,47 @@ window.__ModuleLoader__.load({
3952
4067
  "qoder-global": QODER_GLOBAL_STATUS_PATH
3953
4068
  };
3954
4069
  /**
4070
+ * The shared 《插件设置》 block's slot and entry ids.
4071
+ *
4072
+ * The three connect plugins must agree on these exactly: `settings.section` is
4073
+ * a `list` slot, so many blocks may coexist, but a child slot name may be
4074
+ * declared once and a list entry id may be registered once. The first plugin to
4075
+ * arrive registers the container (declaring the child slot); the others detect
4076
+ * it and attach their own entry, which is why the ids are fixed constants
4077
+ * rather than per-plugin names.
4078
+ */
4079
+ const SHARED_SECTION_SLOT = "settings.section";
4080
+ const SHARED_SECTION_ID = "plugin-settings";
4081
+ const SHARED_SECTION_ORDER = 900;
4082
+ const SHARED_ITEM_SLOT = "plugin-settings.item";
4083
+ /** This plugin's entry id inside the shared block: its package name (unique). */
4084
+ const SHARED_ITEM_ID = "dsh-qoder-connect";
4085
+ /** The shared block's title, fixed by the contract so all three plugins agree. */
4086
+ const SHARED_SECTION_LABEL = "插件设置";
4087
+ /**
4088
+ * The shared 《插件设置》 section the 0.1.7 card list lives in.
4089
+ *
4090
+ * It renders nothing but its child slot: every connect plugin contributes its
4091
+ * own card as one `plugin-settings.item` entry. The list wrapper mirrors the
4092
+ * list the removed Plugins tab used for the same cards (`gap: 10`, no markers),
4093
+ * so the cards keep the spacing they had on 0.1.5.
4094
+ *
4095
+ * @param props - the section's slot props (owner share plus `renderSlot`).
4096
+ * @returns the ordered card list.
4097
+ */
4098
+ function PluginSettingsSection(props) {
4099
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
4100
+ style: {
4101
+ display: "flex",
4102
+ flexDirection: "column",
4103
+ gap: 6,
4104
+ padding: 0,
4105
+ listStyle: "none"
4106
+ },
4107
+ children: props.renderSlot(SHARED_ITEM_SLOT, {})
4108
+ });
4109
+ }
4110
+ /**
3955
4111
  * Register card copy, the shared quota-settings card, the two variant cards,
3956
4112
  * and the sidebar quota cards.
3957
4113
  *
@@ -3983,9 +4139,22 @@ window.__ModuleLoader__.load({
3983
4139
  en
3984
4140
  }), "dsh-qoder-connect: settings copy");
3985
4141
  const t = ctx.locale.bind(namespace);
4142
+ /**
4143
+ * The bound quota settings face, filled in as soon as a host configuration
4144
+ * service becomes available (see the two `ctx.inject` callbacks below).
4145
+ *
4146
+ * It is bound as a SERVICE ARRIVES rather than when the settings card's
4147
+ * inject factory first runs: the factory only executes while the settings
4148
+ * page renders, so a fresh page load read no toggles and rendered no sidebar
4149
+ * card until the user opened settings — the exact regression the
4150
+ * commandcode card avoids by reading its STORED fact independently of the
4151
+ * settings page. The face's subscription mirrors every accepted snapshot
4152
+ * (toggles + interval) into the shared store the sidebar cards and the
4153
+ * dashboard read; a deployment with no configuration service never binds,
4154
+ * and the sidebar cards stay hidden.
4155
+ */
3986
4156
  let quotaScope;
3987
- try {
3988
- const scope = ctx.settingsScope.bind({ namespace: "qoder-quota" });
4157
+ const adoptQuotaScope = (scope) => {
3989
4158
  quotaScope = scope;
3990
4159
  const applySnapshot = () => {
3991
4160
  const value = scope.getSnapshot().value;
@@ -3994,20 +4163,76 @@ window.__ModuleLoader__.load({
3994
4163
  };
3995
4164
  applySnapshot();
3996
4165
  scope.subscribe(applySnapshot);
3997
- } catch (error) {
3998
- console.error("[dsh-qoder-connect] quota settings scope unavailable (sidebar cards stay hidden):", error);
3999
- }
4000
- ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
4001
- name: "settings.plugin.item",
4002
- key: "qoder",
4003
- priority: 50,
4004
- inject: () => ({
4166
+ };
4167
+ const registerUnifiedCard = (slot) => {
4168
+ const inject = () => ({
4005
4169
  t,
4006
4170
  scope: quotaScope,
4007
4171
  signedIn: () => quotaSignInState(),
4008
4172
  unified: true
4009
- })
4010
- }, QoderPluginCard));
4173
+ });
4174
+ if (slot === "settings.plugin.item") return ctx.slots.inject(slot, () => ctx.slots.register({
4175
+ name: slot,
4176
+ key: "qoder",
4177
+ priority: 50,
4178
+ inject
4179
+ }, QoderPluginCard));
4180
+ return ctx.slots.inject(slot, () => ctx.slots.register({
4181
+ name: slot,
4182
+ id: SHARED_ITEM_ID,
4183
+ order: 30,
4184
+ inject
4185
+ }, QoderPluginCard));
4186
+ };
4187
+ /**
4188
+ * Attach this plugin's card to the shared 《插件设置》 block, building the
4189
+ * block first if nobody has.
4190
+ *
4191
+ * Backoff protocol (identical in all three connect plugins, on purpose: a
4192
+ * `list` slot takes many entries but only ONE registration per id, and a
4193
+ * child slot name may be declared once, so the container is a rendezvous
4194
+ * rather than a race to be won):
4195
+ * 1. wait for the host to declare `settings.section`;
4196
+ * 2. if an entry with the shared id is already there, only attach;
4197
+ * 3. otherwise register the container — and if a sibling won the race and
4198
+ * the registration throws, fall back to attaching to theirs.
4199
+ */
4200
+ const joinSharedSettingsBlock = () => {
4201
+ ctx.slots.inject(SHARED_SECTION_SLOT, () => {
4202
+ let disposeContainer;
4203
+ if (!ctx.slots.entries(SHARED_SECTION_SLOT).some((entry) => entry.options?.id === SHARED_SECTION_ID)) try {
4204
+ disposeContainer = ctx.slots.register({
4205
+ name: SHARED_SECTION_SLOT,
4206
+ id: SHARED_SECTION_ID,
4207
+ order: SHARED_SECTION_ORDER,
4208
+ label: () => SHARED_SECTION_LABEL,
4209
+ children: { [SHARED_ITEM_SLOT]: {
4210
+ kind: "list",
4211
+ scope: "root"
4212
+ } }
4213
+ }, PluginSettingsSection);
4214
+ } catch (error) {
4215
+ console.error("[dsh-qoder-connect] shared plugin-settings block lost the race; attaching to the winner:", error);
4216
+ }
4217
+ const disposeItem = registerUnifiedCard(SHARED_ITEM_SLOT);
4218
+ return () => {
4219
+ disposeItem();
4220
+ disposeContainer?.();
4221
+ };
4222
+ });
4223
+ };
4224
+ /**
4225
+ * 插件自有配置(`<profile>/.dsh-qoder-connect/settings.json`):两条宿主线的
4226
+ * 读写都走宿主半的 settings face,不再经过 settingsScope / configForms。
4227
+ *
4228
+ * 0.1.7 的 configForms 写入会整树 reconcile + fiber 热重载(每次约
4229
+ * 1~1.5 秒,且每次保存都刷新所有客户端镜像);自有文件写入是本地毫秒级
4230
+ * 原子写。卡片注册无条件进行:scope 在启动时载入,迟到也不会漏掉 UI。
4231
+ */
4232
+ const ownQuotaScope = new OwnQuotaSettingsScope();
4233
+ ownQuotaScope.load();
4234
+ adoptQuotaScope(ownQuotaScope);
4235
+ joinSharedSettingsBlock();
4011
4236
  const QUOTA_PANEL_ID = "qoder-quota-panel";
4012
4237
  const CONVERSATION_PANEL_ID = "conversation";
4013
4238
  const dashboardDocuments = {