dsh-any-deepseek 0.0.1 → 0.0.3

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/adapter.d.ts CHANGED
@@ -103,11 +103,21 @@ export declare class AnyDeepSeekAdapter extends LlmAdapter {
103
103
  constructor(deps: AdapterDeps);
104
104
  providerInfo(provider: string): LlmProviderInfo;
105
105
  /**
106
- * Advertise exactly one model.
106
+ * Advertise exactly one model — **unconditionally**.
107
107
  *
108
- * A relay that is missing a key still gets no catalog entry — the picker then hides the whole
109
- * group, which is the harness's own convention for a dormant provider — but the settings page
110
- * keeps its entry, so the user can go fix it.
108
+ * 🔴 **This used to require a resolvable key, and that was wrong.**
109
+ *
110
+ * The old rule was "a relay with no key gets no catalog entry; the picker then hides the whole
111
+ * group, which is the harness's convention for a dormant provider." That reasoning leaks the
112
+ * provider's *credentials* into its *identity*: the provider disappeared from the model picker
113
+ * exactly when the user most needed to see it — during first-run setup, before any key exists.
114
+ * Measured on a real host (owner report, 2026-10-19): with the only channel disabled, the
115
+ * catalog came back `[]`, so `any-deepseek` was registered but *invisible*, which reads as
116
+ * "the plugin did not install" no matter what the schema or the routes say.
117
+ *
118
+ * ⇒ The catalog answers "what does this provider serve", which is a property of the plugin.
119
+ * Key presence is answered per request (`MISSING_CREDENTIAL`) and per row in the panel
120
+ * (`keyConfigured`), where the user can actually act on it.
111
121
  */
112
122
  listModels(_provider: string): Promise<LlmModelInfo[]>;
113
123
  resolveModel(provider: string, model: string, _signal?: AbortSignal): Promise<LlmResolvedModelInfo>;
package/lib/client.js CHANGED
@@ -199,15 +199,21 @@ window.__ModuleLoader__.load({
199
199
  * Run one settings write and surface a refusal.
200
200
  *
201
201
  * A write can be refused for reasons the user can act on (read-only deployment, the settings
202
- * service not composed, a revision conflict) and for reasons they cannot (transport). Both are
203
- * facts. Silently discarding them is what makes a panel look alive while nothing is saved —
204
- * which is exactly how this plugin's writes were failing without a trace.
202
+ * service not composed, a revision conflict, a field the host considers non-volatile) and for
203
+ * reasons they cannot (transport). Both are facts, and silently discarding them is what makes
204
+ * a panel look alive while nothing is saved.
205
+ *
206
+ * 🔴 **The refusal message must NOT name causes we have not checked.** This used to read
207
+ * "只读部署,或设置服务未启用" — and the one time it actually fired, the real cause was
208
+ * neither: `current` was missing `.volatile()` in the schema, three layers away. Naming two
209
+ * unchecked causes sent the investigation in the wrong direction. A refusal is a refusal:
210
+ * say that, and let the host's own error speak for itself.
205
211
  * @param run - the write.
206
212
  * @param context - what the user was trying to do.
207
213
  */
208
214
  const write = (run, context = "保存失败") => {
209
215
  run().then((accepted) => {
210
- if (!accepted) setError(`${context}:宿主拒绝了这个修改(只读部署,或设置服务未启用)`);
216
+ if (!accepted) setError(`${context}:宿主拒绝了这个修改`);
211
217
  else setError(null);
212
218
  }, (cause) => {
213
219
  setError(`${context}:${cause instanceof Error ? cause.message : String(cause)}`);
package/lib/config.d.ts CHANGED
@@ -101,20 +101,39 @@ export interface PluginConfigInput {
101
101
  /**
102
102
  * Plugin configuration as the plugin reads it (schema OUTPUT).
103
103
  *
104
- * `channels` parses to a reference wrapper — created even when the user set nothing, so the
105
- * field is always present. Read it through {@link unwrapChannels}.
104
+ * `channels` and `current` parse to reference wrappers — created even when the user set
105
+ * nothing, so the fields are always present. Read them through {@link unwrap}.
106
+ *
107
+ * 🔴 **`current` is volatile too, and that is not cosmetic** — see the `.volatile()` note on
108
+ * `PluginConfigSchema`.
106
109
  */
107
110
  export interface PluginConfigValue {
108
111
  channels: Volatile<Record<string, ChannelConfigValue>>;
109
- current: string;
112
+ current: Volatile<string>;
110
113
  usage: UsageConfigValue;
111
114
  fallback: string[];
112
115
  }
113
116
  /**
114
117
  * Plugin configuration schema.
115
118
  *
116
- * `channels` is volatile: the panel can only render and edit a field the harness sees as a
117
- * reference, and the runtime value arrives as a wrapper whose `.get()` returns the document.
119
+ * ## Why EVERY panel-writable field must be `.volatile()`
120
+ *
121
+ * The host projects an entry's Config into a settings form only for volatile fields, and its
122
+ * write path **refuses** any path outside one:
123
+ *
124
+ * ```js
125
+ * for (const path of paths) if (path.length && !isVolatilePath(schema, path))
126
+ * throw new Error(`Config field "${path.join('.')}" is not volatile`)
127
+ * ```
128
+ *
129
+ * `channels` was volatile from the start, so channel edits worked. `current` was **not**, so
130
+ * every click on "设为当前" was rejected by the host — and the host's client-side form turns
131
+ * that refusal into a bare `false`, which the panel reported as "read-only deployment or the
132
+ * settings service is not enabled". **That message named two causes it had never checked.**
133
+ * The real cause was a schema annotation three layers away.
134
+ *
135
+ * ⇒ Rule: a field the panel writes must be `.volatile()`, and the runtime side must
136
+ * `unwrap()` it. Adding a writable field without both halves reproduces this bug silently.
118
137
  */
119
138
  export declare const PluginConfigSchema: SchemaOf<PluginConfigInput, PluginConfigValue>;
120
139
  /**
package/lib/index.js CHANGED
@@ -72,12 +72,28 @@ const UsageConfigSchema = z.object({
72
72
  /**
73
73
  * Plugin configuration schema.
74
74
  *
75
- * `channels` is volatile: the panel can only render and edit a field the harness sees as a
76
- * reference, and the runtime value arrives as a wrapper whose `.get()` returns the document.
75
+ * ## Why EVERY panel-writable field must be `.volatile()`
76
+ *
77
+ * The host projects an entry's Config into a settings form only for volatile fields, and its
78
+ * write path **refuses** any path outside one:
79
+ *
80
+ * ```js
81
+ * for (const path of paths) if (path.length && !isVolatilePath(schema, path))
82
+ * throw new Error(`Config field "${path.join('.')}" is not volatile`)
83
+ * ```
84
+ *
85
+ * `channels` was volatile from the start, so channel edits worked. `current` was **not**, so
86
+ * every click on "设为当前" was rejected by the host — and the host's client-side form turns
87
+ * that refusal into a bare `false`, which the panel reported as "read-only deployment or the
88
+ * settings service is not enabled". **That message named two causes it had never checked.**
89
+ * The real cause was a schema annotation three layers away.
90
+ *
91
+ * ⇒ Rule: a field the panel writes must be `.volatile()`, and the runtime side must
92
+ * `unwrap()` it. Adding a writable field without both halves reproduces this bug silently.
77
93
  */
78
94
  const PluginConfigSchema = z.object({
79
95
  channels: z.dict(ChannelConfigSchema).default({}).volatile().description("渠道表:每行一个中转渠道"),
80
- current: z.string().default("").description("当前使用的渠道 id"),
96
+ current: z.string().default("").volatile().description("当前使用的渠道 id"),
81
97
  usage: UsageConfigSchema.default({ enabled: true }),
82
98
  fallback: z.array(z.string()).default([]).description("预留:故障转移顺序(v1 未实现)")
83
99
  });
@@ -881,28 +897,32 @@ var AnyDeepSeekAdapter = class extends LlmAdapter {
881
897
  };
882
898
  }
883
899
  /**
884
- * Advertise exactly one model.
900
+ * Advertise exactly one model — **unconditionally**.
901
+ *
902
+ * 🔴 **This used to require a resolvable key, and that was wrong.**
903
+ *
904
+ * The old rule was "a relay with no key gets no catalog entry; the picker then hides the whole
905
+ * group, which is the harness's convention for a dormant provider." That reasoning leaks the
906
+ * provider's *credentials* into its *identity*: the provider disappeared from the model picker
907
+ * exactly when the user most needed to see it — during first-run setup, before any key exists.
908
+ * Measured on a real host (owner report, 2026-10-19): with the only channel disabled, the
909
+ * catalog came back `[]`, so `any-deepseek` was registered but *invisible*, which reads as
910
+ * "the plugin did not install" no matter what the schema or the routes say.
885
911
  *
886
- * A relay that is missing a key still gets no catalog entry — the picker then hides the whole
887
- * group, which is the harness's own convention for a dormant provider — but the settings page
888
- * keeps its entry, so the user can go fix it.
912
+ * ⇒ The catalog answers "what does this provider serve", which is a property of the plugin.
913
+ * Key presence is answered per request (`MISSING_CREDENTIAL`) and per row in the panel
914
+ * (`keyConfigured`), where the user can actually act on it.
889
915
  */
890
916
  async listModels(_provider) {
891
- const channels = this.deps.channels();
892
- const withKey = [];
893
- for (const channel of channels) {
894
- const key = await this.deps.resolveKey(channel.apiKeyEnv);
895
- if (key !== void 0 && key !== "") {
896
- withKey.push({
897
- provider: PROVIDER_ID,
898
- id: ADVERTISED_MODEL_ID,
899
- name: ADVERTISED_MODEL_NAME,
900
- description: `via ${channel.label}`
901
- });
902
- break;
903
- }
904
- }
905
- return withKey;
917
+ const selected = this.selected();
918
+ const first = this.deps.channels()[0];
919
+ const label = selected?.label ?? first?.label;
920
+ return [{
921
+ provider: PROVIDER_ID,
922
+ id: ADVERTISED_MODEL_ID,
923
+ name: ADVERTISED_MODEL_NAME,
924
+ description: label === void 0 ? "no channel configured yet" : `via ${label}`
925
+ }];
906
926
  }
907
927
  async resolveModel(provider, model, _signal) {
908
928
  const channel = this.selected();
@@ -1592,8 +1612,8 @@ function apply(ctx, config) {
1592
1612
  return enabledChannels(resolveChannels(map), map);
1593
1613
  };
1594
1614
  const current = () => {
1595
- const live = ctx.fiber?.config ?? config;
1596
- return typeof live.current === "string" ? live.current : void 0;
1615
+ const value = unwrap((ctx.fiber?.config ?? config).current);
1616
+ return typeof value === "string" ? value : void 0;
1597
1617
  };
1598
1618
  const adapter = new AnyDeepSeekAdapter({
1599
1619
  channels,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-any-deepseek",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "One DeepSeek V4.1 Flash provider for DeepSeek Harness: paste a key per channel, switch channels manually, token usage booked per real upstream.",
5
5
  "license": "MIT",
6
6
  "repository": {