@viztor/dsh-tinyfish 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of @viztor/dsh-tinyfish might be problematic. Click here for more details.

package/lib/client.js ADDED
@@ -0,0 +1,400 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-tinyfish",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
8
+ let react_jsx_runtime = require("react/jsx-runtime");
9
+ //#region src/settings-page.tsx
10
+ /**
11
+ * `dsh-tinyfish` settings page — the client half of the bundle.
12
+ *
13
+ * The harness renders a plugin's settings form only when a client bundle
14
+ * contributes a slot to the Plugins page. It never reads the plugin's `Config`
15
+ * schema, so a package with no client half has no form however well its schema
16
+ * is declared, and the harness degrades quietly rather than saying so.
17
+ *
18
+ * This is that half. It is bundled separately from `src/index.ts` — the host
19
+ * half — into `lib/client.cjs`, wrapped in the `window.__ModuleLoader__.load`
20
+ * call the web client expects, and the manifest declares both `dsh.bundle` and
21
+ * `dsh.client`. Those two keys are read by different subsystems and never
22
+ * consult each other, so one package can carry both and the user installs once.
23
+ *
24
+ * Nothing here writes as the user types. The form stages drafts and writes them
25
+ * on save, so what is on screen is exactly what a save would store.
26
+ *
27
+ * @module dsh-tinyfish/settings-page
28
+ */
29
+ /**
30
+ * The settings namespace, spelled rather than imported.
31
+ *
32
+ * A client package must not depend on a Host package, so this string is
33
+ * duplicated on purpose. It must equal the exported `name` of `src/index.ts`:
34
+ * that is what the Plugins page keys the row by.
35
+ */
36
+ const NS = "web-tinyfish";
37
+ /**
38
+ * Client-side services this page needs.
39
+ *
40
+ * `configForms` supplies the form scope, `slots` is how the page is
41
+ * contributed, `locale` carries the dictionaries, and the two `remote` entries
42
+ * are the credential reference and its invalidation events.
43
+ */
44
+ const inject = [
45
+ "slots",
46
+ "locale",
47
+ "remote",
48
+ "remote.credentials",
49
+ "configForms"
50
+ ];
51
+ /** English copy. */
52
+ const en = {
53
+ title: "TinyFish",
54
+ description: "TinyFish-backed web search and fetch, at $0.",
55
+ channel: "Channel",
56
+ channelHint: "direct calls TinyFish with its own key; monid routes through a Monid key.",
57
+ apiKey: "API key",
58
+ apiKeyHint: "Stored outside the settings file. Leave blank to keep the current key.",
59
+ monidApiKey: "Monid platform key",
60
+ monidApiKeyHint: "Stored separately from the TinyFish key, so switching channels keeps both. `monid keys add` also works and takes precedence over this.",
61
+ apiKeySet: "A key is configured.",
62
+ apiKeyUnset: "No key is configured, so searches fail until one is set.",
63
+ purpose: "Purpose",
64
+ purposeHint: "Optional goal statement; TinyFish ranks results against it.",
65
+ attempts: "Attempts",
66
+ attemptsHint: "Retries for a transient failure or an empty result, 1 to 5.",
67
+ search: "Offer search",
68
+ searchHint: "When off, web_search falls through to another provider.",
69
+ fetch: "Offer fetch",
70
+ fetchHint: "When off, web_fetch falls through to another provider.",
71
+ channelDirect: "Direct",
72
+ channelMonid: "Monid",
73
+ bothOff: "Both providers are off, so this plugin is registered but answers nothing. Turn one back on to use it.",
74
+ overridden: "Overridden",
75
+ reset: "Reset to default",
76
+ invalidNumber: "Enter a whole number, or leave blank to use the default.",
77
+ invalidText: "This value was not accepted; leave blank to use the default.",
78
+ readOnly: "This deployment stores settings read-only.",
79
+ unavailable: "This plugin is not loaded, so it cannot be configured right now.",
80
+ save: "Save",
81
+ saving: "Saving…",
82
+ saveFailed: "The deployment did not accept these values; they were left for you to correct."
83
+ };
84
+ /** Simplified Chinese copy, for the profile locale this bundle was written in. */
85
+ const zh = {
86
+ title: "TinyFish",
87
+ description: "基于 TinyFish 的网页搜索与抓取,零成本。",
88
+ channel: "通道",
89
+ channelHint: "direct 使用 TinyFish 自己的密钥;monid 通过 Monid 密钥转发。",
90
+ apiKey: "API Key",
91
+ apiKeyHint: "不写入设置文件。留空表示保持当前密钥。",
92
+ monidApiKey: "Monid 平台密钥",
93
+ monidApiKeyHint: "与 TinyFish 密钥分开保存,切换通道时两者都会保留。也可运行 `monid keys add`,其优先级高于此项。",
94
+ apiKeySet: "已配置密钥。",
95
+ apiKeyUnset: "未配置密钥,搜索会失败,直到设置为止。",
96
+ purpose: "目标说明",
97
+ purposeHint: "可选的目标描述;TinyFish 会据此排序结果。",
98
+ attempts: "尝试次数",
99
+ attemptsHint: "瞬时失败或结果为空时的重试次数,1 到 5。",
100
+ search: "提供搜索",
101
+ searchHint: "关闭后,web_search 会转由其他提供方处理。",
102
+ fetch: "提供抓取",
103
+ fetchHint: "关闭后,web_fetch 会转由其他提供方处理。",
104
+ channelDirect: "直连",
105
+ channelMonid: "Monid",
106
+ bothOff: "搜索与抓取均已关闭,此插件已注册但不再响应任何请求。重新开启其中一个即可恢复使用。",
107
+ overridden: "已覆盖",
108
+ reset: "恢复默认",
109
+ invalidNumber: "请填整数;留空表示使用默认值。",
110
+ invalidText: "该值未被接受;留空表示使用默认值。",
111
+ readOnly: "本部署的设置为只读。",
112
+ unavailable: "该插件当前未加载,暂时无法配置。",
113
+ save: "保存",
114
+ saving: "保存中…",
115
+ saveFailed: "本部署没有接受这些值,已保留供你修改。"
116
+ };
117
+ /** Field names in one place, so the form and its specs cannot drift apart. */
118
+ const FIELD = {
119
+ channel: "channel",
120
+ apiKey: "apiKey",
121
+ apiKeyEnv: "apiKeyEnv",
122
+ monidApiKey: "monidApiKey",
123
+ monidKeyEnv: "monidKeyEnv",
124
+ purpose: "purpose",
125
+ attempts: "attempts",
126
+ search: "search",
127
+ fetch: "fetch"
128
+ };
129
+ /** Credential references the provider falls back to when the section names none. */
130
+ const DEFAULT_API_KEY_REF = "TINYFISH_API_KEY";
131
+ const DEFAULT_MONID_KEY_REF = "MONID_API_KEY";
132
+ /**
133
+ * A boolean field.
134
+ *
135
+ * The primitives ship a text and a number spec but no boolean one, and the Host
136
+ * validates the stored value regardless — this only decides what a draft means.
137
+ * `parse` returning `undefined` is what blocks the save on a typo instead of
138
+ * silently discarding the edit, which is the difference between a visible
139
+ * mistake and a setting that quietly did not apply.
140
+ *
141
+ * @param field - field name inside the namespace section.
142
+ * @returns the field's conversion spec.
143
+ */
144
+ /** What each accepted boolean draft stages. Anything absent blocks the save. */
145
+ const BOOLEAN_DRAFTS = {
146
+ "": { kind: "clear" },
147
+ true: {
148
+ kind: "set",
149
+ value: true
150
+ },
151
+ false: {
152
+ kind: "set",
153
+ value: false
154
+ }
155
+ };
156
+ function settingsBooleanField(field) {
157
+ return {
158
+ field,
159
+ format: (value) => typeof value === "boolean" || typeof value === "number" ? String(value) : "",
160
+ parse: (text) => BOOLEAN_DRAFTS[text.trim().toLowerCase()]
161
+ };
162
+ }
163
+ /** The section fields this card edits, in render order. */
164
+ const SPECS = [
165
+ (0, _deepseek_ai_dsh_client_ui_primitives.settingsTextField)(FIELD.channel),
166
+ (0, _deepseek_ai_dsh_client_ui_primitives.settingsTextField)(FIELD.purpose),
167
+ (0, _deepseek_ai_dsh_client_ui_primitives.settingsNumberField)(FIELD.attempts),
168
+ settingsBooleanField(FIELD.search),
169
+ settingsBooleanField(FIELD.fetch)
170
+ ];
171
+ /**
172
+ * The credential reference the section currently names.
173
+ *
174
+ * Read from the accepted section rather than from a draft, because the reference
175
+ * is what the *provider* will resolve — a key typed against a reference that is
176
+ * not yet saved would be stored where nothing looks for it.
177
+ *
178
+ * @param snapshot - the form scope's current snapshot.
179
+ * @param which - which channel's reference to read.
180
+ * @returns the reference name, or that channel's default.
181
+ */
182
+ function refOf(snapshot, which) {
183
+ const section = snapshot?.value;
184
+ const field = which === "monid" ? FIELD.monidKeyEnv : FIELD.apiKeyEnv;
185
+ const named = section?.[field];
186
+ const fallback = which === "monid" ? DEFAULT_MONID_KEY_REF : DEFAULT_API_KEY_REF;
187
+ return typeof named === "string" && named.trim() !== "" ? named.trim() : fallback;
188
+ }
189
+ /** The labels the shared form frame renders. */
190
+ const formLabels = (t) => ({
191
+ unavailable: t("unavailable"),
192
+ readOnly: t("readOnly"),
193
+ saveFailed: t("saveFailed"),
194
+ save: t("save"),
195
+ saving: t("saving")
196
+ });
197
+ /**
198
+ * Render the Plugins list's one-line summary, or the settings form.
199
+ *
200
+ * @param props - the view asked for, locale copy, the form snapshot, its
201
+ * actions, and the credential's configured state.
202
+ * @returns the summary, or the form.
203
+ */
204
+ /**
205
+ * Resolve a boolean switch to its effective value.
206
+ *
207
+ * The draft text is authoritative when present; a blank draft means untouched,
208
+ * so it falls back to the schema default (on for both switches). The Switch
209
+ * only ever writes "true" or "false", so any other text cannot occur through
210
+ * the UI — but the fallback keeps the control honest if the section arrives
211
+ * in an unexpected shape.
212
+ */
213
+ function switchValue(text) {
214
+ if (text === "false") return false;
215
+ return true;
216
+ }
217
+ function TinyfishCard(props) {
218
+ const { t } = props;
219
+ if (props.view === "summary") return t("description");
220
+ const state = props.useTinyfishCard((snapshot) => snapshot);
221
+ const disabled = !state.shell.writable;
222
+ const field = (name) => ({
223
+ id: `plugin-config-tinyfish-${name}`,
224
+ disabled,
225
+ overriddenLabel: t("overridden"),
226
+ resetLabel: t("reset"),
227
+ ...state.fields[name],
228
+ onEdit: (text) => {
229
+ props.edit(name, text);
230
+ },
231
+ onReset: () => {
232
+ props.resetField(name);
233
+ }
234
+ });
235
+ const channel = (state.fields[FIELD.channel]?.text ?? "") === "monid" ? "monid" : "direct";
236
+ const key = state.keys[channel];
237
+ const keyChannel = channel === "monid" ? FIELD.monidApiKey : FIELD.apiKey;
238
+ const searchOn = switchValue(state.fields[FIELD.search]?.text ?? "");
239
+ const fetchOn = switchValue(state.fields[FIELD.fetch]?.text ?? "");
240
+ const resetButton = (name, overridden) => overridden && !disabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
241
+ type: "button",
242
+ onClick: () => {
243
+ props.resetField(name);
244
+ },
245
+ children: t("reset")
246
+ }) : null;
247
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.SettingsForm, {
248
+ labels: formLabels(t),
249
+ state: state.shell,
250
+ onSave: props.save,
251
+ onDiscard: props.discard,
252
+ children: [
253
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
254
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SegmentedControl, {
255
+ id: `plugin-config-tinyfish-${FIELD.channel}`,
256
+ label: t("channel"),
257
+ value: channel,
258
+ options: [{
259
+ value: "direct",
260
+ label: t("channelDirect")
261
+ }, {
262
+ value: "monid",
263
+ label: t("channelMonid")
264
+ }],
265
+ onChange: (next) => {
266
+ props.edit(FIELD.channel, next);
267
+ },
268
+ disabled
269
+ }),
270
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("channelHint") }),
271
+ resetButton(FIELD.channel, state.fields[FIELD.channel]?.overridden ?? false)
272
+ ] }),
273
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SettingsSecretField, {
274
+ id: `plugin-config-tinyfish-${keyChannel}`,
275
+ label: channel === "monid" ? t("monidApiKey") : t("apiKey"),
276
+ hint: channel === "monid" ? t("monidApiKeyHint") : t("apiKeyHint"),
277
+ text: key.text,
278
+ disabled,
279
+ configured: key.named,
280
+ stateLabel: key.named ? t("apiKeySet") : t("apiKeyUnset"),
281
+ onEdit: (text) => {
282
+ props.edit(keyChannel, text);
283
+ }
284
+ }),
285
+ searchOn && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SettingsValueField, {
286
+ ...field(FIELD.purpose),
287
+ label: t("purpose"),
288
+ hint: t("purposeHint"),
289
+ invalidLabel: t("invalidText")
290
+ }),
291
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SettingsValueField, {
292
+ ...field(FIELD.attempts),
293
+ label: t("attempts"),
294
+ hint: t("attemptsHint"),
295
+ invalidLabel: t("invalidNumber"),
296
+ numeric: true
297
+ }),
298
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
299
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
300
+ label: t("search"),
301
+ checked: searchOn,
302
+ onChange: (next) => {
303
+ props.edit(FIELD.search, String(next));
304
+ },
305
+ disabled
306
+ }),
307
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("searchHint") }),
308
+ resetButton(FIELD.search, state.fields[FIELD.search]?.overridden ?? false)
309
+ ] }),
310
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
311
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
312
+ label: t("fetch"),
313
+ checked: fetchOn,
314
+ onChange: (next) => {
315
+ props.edit(FIELD.fetch, next.toString());
316
+ },
317
+ disabled
318
+ }),
319
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("fetchHint") }),
320
+ resetButton(FIELD.fetch, state.fields[FIELD.fetch]?.overridden ?? false)
321
+ ] }),
322
+ !searchOn && !fetchOn && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("bothOff") })
323
+ ]
324
+ });
325
+ }
326
+ /**
327
+ * Mount the TinyFish settings page while the Host serves its namespace.
328
+ *
329
+ * @param ctx - the browser plugin context.
330
+ */
331
+ function apply(ctx) {
332
+ ctx.effect(() => {
333
+ ctx.locale.register(NS, {
334
+ zh,
335
+ en
336
+ });
337
+ }, "dsh-tinyfish: dictionaries");
338
+ const scope = ctx.configForms.get(NS);
339
+ const model = new _deepseek_ai_dsh_client_ui_primitives.SettingsFormModel(scope, SPECS, [{
340
+ field: FIELD.apiKey,
341
+ write: async (text) => {
342
+ try {
343
+ await ctx.remote.credentials.set(refOf(scope.getSnapshot(), "direct"), text);
344
+ return true;
345
+ } catch {
346
+ return false;
347
+ }
348
+ }
349
+ }, {
350
+ field: FIELD.monidApiKey,
351
+ write: async (text) => {
352
+ try {
353
+ await ctx.remote.credentials.set(refOf(scope.getSnapshot(), "monid"), text);
354
+ return true;
355
+ } catch {
356
+ return false;
357
+ }
358
+ }
359
+ }]);
360
+ const store = model.bind(() => ({
361
+ shell: model.shell(),
362
+ fields: Object.fromEntries(SPECS.map((spec) => [spec.field, model.field(spec.field)])),
363
+ keys: {
364
+ direct: {
365
+ text: model.field(FIELD.apiKey).text,
366
+ named: refOf(scope.getSnapshot(), "direct") !== DEFAULT_API_KEY_REF
367
+ },
368
+ monid: {
369
+ text: model.field(FIELD.monidApiKey).text,
370
+ named: refOf(scope.getSnapshot(), "monid") !== DEFAULT_MONID_KEY_REF
371
+ }
372
+ }
373
+ }));
374
+ ctx.effect(() => () => {
375
+ model.dispose();
376
+ }, "dsh-tinyfish: form subscription");
377
+ ctx.effect(() => {
378
+ ctx.configForms.whileServed([NS], () => {
379
+ ctx.slots.inject("plugins.bundle.config", () => {
380
+ ctx.slots.register({
381
+ name: "plugins.bundle.config",
382
+ key: "dsh-tinyfish",
383
+ locale: NS,
384
+ inject: () => ({
385
+ hooks: { tinyfishCard: store },
386
+ ...model.actions()
387
+ })
388
+ }, TinyfishCard);
389
+ });
390
+ });
391
+ }, "dsh-tinyfish: page");
392
+ }
393
+ //#endregion
394
+ exports.NS = NS;
395
+ exports.apply = apply;
396
+ exports.inject = inject;
397
+
398
+ return module.exports;
399
+ },
400
+ });