@klarkxy/dsh-zhihu 0.1.8 → 0.1.10

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/client.js CHANGED
@@ -167,10 +167,49 @@ window.__ModuleLoader__.load({
167
167
  var exports = module.exports;
168
168
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
169
169
  let react = require("react");
170
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
170
171
  let react_jsx_runtime = require("react/jsx-runtime");
172
+ //#region src/client-locale.ts
173
+ function zhihuLocale(value) {
174
+ return String(value ?? "zh").startsWith("zh") ? "zh" : "en";
175
+ }
176
+ function translator(locale = "zh") {
177
+ return (zh, en) => locale === "en" ? en : zh;
178
+ }
179
+ /** Components below the settings root read the locale here instead of threading a prop. */
180
+ const ZhihuLocaleContext = (0, react.createContext)("zh");
181
+ function useZhihuLocale() {
182
+ return (0, react.useContext)(ZhihuLocaleContext);
183
+ }
184
+ const TRANSLATORS = {
185
+ zh: translator("zh"),
186
+ en: translator("en")
187
+ };
188
+ function useT() {
189
+ return TRANSLATORS[useZhihuLocale()];
190
+ }
191
+ /** Follow the host locale when the service is present; standalone renders stay Chinese. */
192
+ function useHostLocale(service) {
193
+ const subscribe = (0, react.useCallback)((listener) => service ? service.subscribe(listener) : () => {}, [service]);
194
+ const snapshot = (0, react.useCallback)(() => service ? service.getSnapshot().active : "zh", [service]);
195
+ return zhihuLocale((0, react.useSyncExternalStore)(subscribe, snapshot, snapshot));
196
+ }
197
+ //#endregion
171
198
  //#region src/contracts.ts
172
199
  const ZHIHU_RPC_CHANNEL = "/zhihu";
173
200
  const ZHIHU_CREDENTIAL_REF = "ZHIHU_ACCESS_TOKEN";
201
+ /** Official quota IDs, not local execution counters or predicted balances. */
202
+ const ZHIHU_QUOTA_IDS = [
203
+ "global_search",
204
+ "zhihu_search",
205
+ "hot_list",
206
+ "question_answers",
207
+ "zhida_openai",
208
+ "tools",
209
+ "knowledge",
210
+ "user_data",
211
+ "creator"
212
+ ];
174
213
  //#endregion
175
214
  //#region src/client-state.ts
176
215
  /**
@@ -212,212 +251,504 @@ function createZhihuClientState() {
212
251
  }
213
252
  };
214
253
  }
215
- //#endregion
216
- //#region src/client-styles.ts
217
- const standaloneOutsideTheme = ":not(.radix-themes *)";
218
- const zhihuClientStyles = `
254
+ Object.values({
255
+ /** Surfaces, from the page up to the topmost nested card. */
256
+ surface: [
257
+ "--dsw-alias-bg-base",
258
+ "--dsw-alias-bg-layer-1",
259
+ "--dsw-alias-bg-layer-2",
260
+ "--dsw-alias-bg-layer-3",
261
+ "--dsw-alias-bg-layer-4",
262
+ "--dsw-alias-bg-module-platform",
263
+ "--dsw-alias-bg-overlay"
264
+ ],
265
+ /** Hairlines. l1 is near-invisible on a menu surface, so menus use l2+. */
266
+ border: [
267
+ "--dsw-alias-border-l1",
268
+ "--dsw-alias-border-l2",
269
+ "--dsw-alias-border-l3",
270
+ "--dsw-alias-border-l4"
271
+ ],
272
+ /** Text. `caption` is menu shortcut text, `dimmed` is placeholder text. */
273
+ label: [
274
+ "--dsw-alias-label-primary",
275
+ "--dsw-alias-label-secondary",
276
+ "--dsw-alias-label-tertiary",
277
+ "--dsw-alias-label-caption",
278
+ "--dsw-alias-label-dimmed",
279
+ "--dsw-alias-label-error",
280
+ "--dsw-alias-label-primary-foreground"
281
+ ],
282
+ /** Interactive fills. The `-danger` hover belongs to destructive rows. */
283
+ interactive: [
284
+ "--dsw-alias-interactive-bg-hover",
285
+ "--dsw-alias-interactive-bg-active",
286
+ "--dsw-alias-interactive-bg-hover-danger"
287
+ ],
288
+ /** Accent and status. `business` is the focus ring's default colour. */
289
+ accent: [
290
+ "--dsw-alias-brand-primary",
291
+ "--dsw-alias-button-primary-fill",
292
+ "--dsw-alias-button-primary-hover",
293
+ "--dsw-alias-button-tool-bar-fill",
294
+ "--dsw-alias-button-tool-bar-hover",
295
+ "--dsw-alias-button-ghost-active-fill",
296
+ "--dsw-alias-button-ghost-active-border"
297
+ ],
298
+ state: [
299
+ "--dsw-alias-state-business-primary",
300
+ "--dsw-alias-state-success-primary",
301
+ "--dsw-alias-state-warn-primary",
302
+ "--dsw-alias-state-error-primary",
303
+ "--dsw-alias-state-idle-primary"
304
+ ],
305
+ /** Corner scale: sm 8, md 12, lg the large card, panel the modal. */
306
+ radius: [
307
+ "--dsw-radius-sm",
308
+ "--dsw-radius-md",
309
+ "--dsw-radius-lg",
310
+ "--dsw-radius-panel"
311
+ ],
312
+ /** An elevated surface draws its edge in the shadow, so it keeps border: 0. */
313
+ elevation: [
314
+ "--dsw-elevation-soft",
315
+ "--dsw-elevation-stroke",
316
+ "--dsw-elevation-stroke-color",
317
+ "--dsw-elevation-panel",
318
+ "--dsw-elevation-prominent"
319
+ ],
320
+ /** Floats, masks and scrollbars. */
321
+ overlay: [
322
+ "--dsw-menu-surface-fill",
323
+ "--dsw-menu-backdrop-filter",
324
+ "--dsw-mask-blur",
325
+ "--dsw-alias-bg-mask-1",
326
+ "--dsw-alias-menu-icon",
327
+ "--dsw-alias-scrollbar-bg-l2",
328
+ "--dsw-alias-scrollbar-hover-l2",
329
+ "--dsw-shadow-lv3"
330
+ ],
331
+ /** Motion, focus and type axis, published by the host rather than by a component. */
332
+ system: [
333
+ "--dsw-focus-ring-width",
334
+ "--dsw-focus-ring-color",
335
+ "--dsw-font-markdown-code-font-family",
336
+ "--ds-transition-duration",
337
+ "--ds-ease-in-out",
338
+ "--ds-font-family-code",
339
+ "--dsh-scrollbar-thumb",
340
+ "--dsh-scrollbar-thumb-hover",
341
+ "--dsh-content-font-size-secondary",
342
+ "--dsh-content-font-delta",
343
+ "--dsh-frame-top-clearance"
344
+ ]
345
+ }).flat();
346
+ /**
347
+ * The host's focus ring shorthand, verbatim. Writing this per control is how
348
+ * rings drift apart; a stylesheet that draws its own focus style should use
349
+ * {@link officialFocus} instead. The colour fallback is the host's own default.
350
+ */
351
+ const OFFICIAL_FOCUS_RING = "var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary))";
352
+ /**
353
+ * A complete `outline` declaration at the official geometry. `offset` defaults
354
+ * to the 2px the native controls use; the host's inline field rules sit at 1px
355
+ * and a segmented tab pulls the ring inside itself at -2px.
356
+ */
357
+ function officialFocus(offset = "2px") {
358
+ return `outline: ${OFFICIAL_FOCUS_RING}; outline-offset: ${offset}`;
359
+ }
360
+ /**
361
+ * An elevated surface. The host draws a raised surface's edge inside its
362
+ * shadow rather than as a border, so a card sets `border: 0` and takes the edge
363
+ * from the elevation — which is also why its border stays off the alias-border
364
+ * set that a flat, non-elevated rule would use.
365
+ */
366
+ function officialElevation(level) {
367
+ return `border: 0; box-shadow: var(--dsw-elevation-${level})`;
368
+ }
369
+ /**
370
+ * The shared stylesheet for a plugin UI, scoped under that plugin's root class.
371
+ *
372
+ * Scoping is what keeps this safe to always-bundle into every client: the rules
373
+ * can only match inside the plugin's own subtree, so two plugins can both mount
374
+ * `dsh-ui-card` without either styling the other's markup.
375
+ *
376
+ * Each recipe matches the root element *and* its descendants, because the
377
+ * commonest arrangement is one element that is both — a surface that carries
378
+ * the root class and `dsh-ui-card`, or a portaled dialog that is the root and
379
+ * the `dsh-ui-surface`. A descendant-only scope would silently leave those
380
+ * unstyled, with no error anywhere.
381
+ *
382
+ * A caveat worth knowing: `Modal` and `Menu` portal to `document.body`, so their
383
+ * content leaves the plugin's subtree entirely. A portaled surface therefore
384
+ * needs its own root class on an element inside the portal.
385
+ *
386
+ * Only patterns that primitives do not provide live here. If a control appears
387
+ * below that `Button`/`Input`/`Switch`/`Tag`/`Menu` already covers, that is a
388
+ * bug in this file: use the primitive and delete the recipe.
389
+ */
390
+ function officialUiCss(roots) {
391
+ const scope = (typeof roots === "string" ? [roots] : roots).map((root) => `.${root}`).join(", ");
392
+ return `
393
+ /* Official DSH Web UI contract. Generated by @klarkxy/dsh-plugin-kit/official-ui.
394
+ * Do not hand-edit the copy inside a plugin: change this module so every plugin
395
+ * moves together. Scope: ${scope} */
396
+ /* The scope rule below stays open for the whole sheet: every rule after it
397
+ * nests with the parent selector, so one stylesheet can cover several unrelated
398
+ * roots without leaking into a neighbouring plugin. Its closing brace is last. */
399
+ ${scope} {
400
+ color: var(--dsw-alias-label-primary);
401
+ font-family: inherit;
402
+ font-size: 14px;
403
+ line-height: 22px;
404
+ min-width: 0;
405
+
406
+ & *, & *::before, & *::after { box-sizing: border-box; }
407
+ & h1, & h2, & h3, & h4, & p, & figure { margin: 0; }
408
+ /* Keyboard focus only: pointer input keeps the native resting appearance. The
409
+ * ring needs no radius of its own — the outline already follows the control's
410
+ * own border-radius, and overriding it here would flatten every control. */
411
+ & :focus-visible { ${officialFocus()}; }
412
+ & [hidden] { display: none !important; }
413
+
414
+ /* --- Type tiers. The same 14/22 and 13/20 steps the primitives use. --- */
415
+ & .dsh-ui-title { font-size: 16px; line-height: 24px; font-weight: 500; color: var(--dsw-alias-label-primary); }
416
+ & .dsh-ui-heading { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); }
417
+ & .dsh-ui-compact { font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-primary); }
418
+ & .dsh-ui-meta { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); }
419
+ & .dsh-ui-hint { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
420
+ & .dsh-ui-error { font-size: 12px; line-height: 18px; color: var(--dsw-alias-state-error-primary); }
421
+ & .dsh-ui-warn { color: var(--dsw-alias-state-warn-primary); }
422
+ & .dsh-ui-success { color: var(--dsw-alias-state-success-primary); }
423
+ & .dsh-ui-muted { color: var(--dsw-alias-label-tertiary); }
424
+ & .dsh-ui-truncate { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
425
+ & .dsh-ui-wrap { min-width: 0; overflow-wrap: anywhere; }
219
426
 
220
- .zhihu-settings-embed {
221
- font: 400 var(--font-size-2)/1.5 var(--default-font-family);
222
- color: var(--gray-12);
427
+ /* --- Layout scaffolding. --- */
428
+ & .dsh-ui-stack { display: flex; flex-direction: column; gap: 12px; }
429
+ & .dsh-ui-stack-lg { display: flex; flex-direction: column; gap: 20px; }
430
+ & .dsh-ui-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
431
+ & .dsh-ui-row-wrap { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
432
+ & .dsh-ui-spacer { flex: 1; min-width: 0; }
433
+ /* A 0.5px l2 rule is the host's section divider; reserve the same rhythm. */
434
+ & .dsh-ui-divider { height: 0.5px; background: var(--dsw-alias-border-l2); }
435
+ & .dsh-ui-section { display: flex; flex-direction: column; gap: 12px; padding-top: 16px; border-top: 0.5px solid var(--dsw-alias-border-l2); }
436
+
437
+ /* --- Card: a raised surface whose edge lives in the shadow. --- */
438
+ & .dsh-ui-card {
439
+ ${officialElevation("stroke")}
223
440
  display: flex;
224
441
  flex-direction: column;
442
+ gap: 12px;
443
+ padding: 16px;
444
+ border-radius: var(--dsw-radius-lg);
445
+ background: var(--dsw-alias-bg-layer-1);
446
+ color: var(--dsw-alias-label-primary);
225
447
  min-width: 0;
226
- width: 100%;
227
- max-width: 760px;
228
- min-height: 0;
229
- gap: 0;
230
448
  }
231
- .zhihu-settings-embed .zhihu-tabs {
232
- margin: 0 0 var(--space-2);
233
- }
234
- .zhihu-settings-embed .zhihu-panel-body {
235
- padding: var(--space-4) 0 0;
236
- overflow: visible;
237
- }
238
- .zhihu-panel button:focus-visible,
239
- .zhihu-panel input:focus-visible,
240
- .zhihu-panel select:focus-visible,
241
- .zhihu-panel a:focus-visible {
242
- box-shadow: 0 0 0 2px var(--accent-a8);
449
+ & .dsh-ui-card--flat { border: 0.5px solid var(--dsw-alias-border-l3); box-shadow: none; }
450
+ & .dsh-ui-card--nested { background: var(--dsw-alias-bg-layer-2); }
451
+ & .dsh-ui-card--plain { background: none; }
452
+
453
+ /* --- Field: label, control, help, error. The control itself is an
454
+ * official primitive, so this only owns the vertical rhythm. --- */
455
+ & .dsh-ui-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
456
+ & .dsh-ui-field + .dsh-ui-field { border-top: 0.5px solid var(--dsw-alias-border-l2); padding-top: 12px; }
457
+ & .dsh-ui-label { display: flex; align-items: center; gap: 6px; font-size: 13px; line-height: 20px; font-weight: 500; color: var(--dsw-alias-label-primary); }
458
+ & .dsh-ui-label-row { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
459
+ & .dsh-ui-help { font-size: 12px; line-height: 1.6; color: var(--dsw-alias-label-secondary); }
460
+ & .dsh-ui-help p { margin: 0; }
461
+ & .dsh-ui-help p + p { margin-top: 8px; }
462
+ & .dsh-ui-required { color: var(--dsw-alias-state-error-primary); }
463
+ & .dsh-ui-control { width: 100%; min-width: 0; }
464
+ /* An invalid control is marked on the control, not only in the message. */
465
+ & .dsh-ui-control[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }
466
+ & .dsh-ui-readonly { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
467
+ & .dsh-ui-readonly > dt { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
468
+ & .dsh-ui-readonly > dd { margin: 0; font-size: 13px; line-height: 20px; overflow-wrap: anywhere; }
469
+
470
+ /* --- Buttons row. A no-op container: it exists so actions line up on the
471
+ * trailing edge the way every native panel does. --- */
472
+ & .dsh-ui-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding-top: 4px; }
473
+ & .dsh-ui-actions-end { justify-content: flex-end; }
474
+
475
+ /* --- Banners. A banner is a left-rail notice, the shape used for warnings,
476
+ * deprecations and blocking instructions. --- */
477
+ & .dsh-ui-banner {
478
+ display: flex;
479
+ flex-direction: column;
480
+ gap: 8px;
481
+ padding: 10px 14px;
482
+ border: 0.5px solid var(--dsw-alias-border-l3);
483
+ border-left: 3px solid var(--dsw-alias-state-warn-primary);
484
+ border-radius: var(--dsw-radius-md);
485
+ background: var(--dsw-alias-bg-layer-1);
486
+ font-size: 13px;
487
+ line-height: 20px;
488
+ color: var(--dsw-alias-label-secondary);
243
489
  }
490
+ & .dsh-ui-banner-title { font-weight: 500; color: var(--dsw-alias-state-warn-primary); }
491
+ & .dsh-ui-banner--info { border-left-color: var(--dsw-alias-state-business-primary); }
492
+ & .dsh-ui-banner--danger { border-left-color: var(--dsw-alias-state-error-primary); }
493
+ & .dsh-ui-banner--success { border-left-color: var(--dsw-alias-state-success-primary); }
494
+ & .dsh-ui-banner p { margin: 0; }
244
495
 
245
- .zhihu-panel {
246
- font-family: var(--default-font-family);
247
- font-size: var(--font-size-2);
248
- color: var(--gray-12);
249
- background: var(--color-panel-solid);
496
+ /* --- Inline notice: a transient confirmation with no chrome of its own. --- */
497
+ & .dsh-ui-notice { display: flex; align-items: center; gap: 6px; font-size: 12px; line-height: 18px; color: var(--dsw-alias-state-success-primary); }
498
+ & .dsh-ui-notice--error { color: var(--dsw-alias-state-error-primary); }
499
+
500
+ /* --- Empty and loading states. Both sit in a dashed well so an empty panel
501
+ * reads as "nothing here yet" rather than as a broken layout. --- */
502
+ & .dsh-ui-empty {
503
+ display: flex;
504
+ flex-direction: column;
505
+ align-items: flex-start;
506
+ gap: 6px;
507
+ padding: 20px 16px;
508
+ border: 0.5px dashed var(--dsw-alias-border-l3);
509
+ border-radius: var(--dsw-radius-md);
510
+ font-size: 13px;
511
+ line-height: 20px;
512
+ color: var(--dsw-alias-label-secondary);
250
513
  }
251
- .zhihu-tabs {
252
- display: inline-flex;
253
- gap: 2px;
254
- padding: 2px;
255
- margin: var(--space-2) var(--space-3) 0;
256
- border-radius: var(--radius-3);
257
- background: var(--gray-3);
258
- box-shadow: 0 0 0 1px var(--gray-a5);
259
- width: max-content;
260
- }
261
- .zhihu-tab {
262
- font: inherit;
263
- font-size: var(--font-size-2);
264
- min-height: 28px;
265
- padding: 0 var(--space-3);
514
+ & .dsh-ui-loading { display: flex; align-items: center; gap: 8px; padding: 20px 0; font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-secondary); }
515
+
516
+ /* --- Selectable list. A row is a button-sized target with a hover fill and
517
+ * a selected fill; the name carries the weight, the description does not. --- */
518
+ & .dsh-ui-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
519
+ & .dsh-ui-list-scroll { max-height: min(66vh, 760px); overflow-y: auto; overflow-x: hidden; }
520
+ & .dsh-ui-list-row { border-radius: var(--dsw-radius-md); }
521
+ & .dsh-ui-list-row:hover { background: var(--dsw-alias-interactive-bg-hover); }
522
+ & .dsh-ui-list-row[aria-selected='true'], & .dsh-ui-list-row[data-selected] { background: var(--dsw-alias-interactive-bg-active); }
523
+ & .dsh-ui-list-item {
524
+ display: flex;
525
+ flex-direction: column;
526
+ align-items: flex-start;
527
+ gap: 4px;
528
+ width: 100%;
529
+ min-width: 0;
530
+ padding: 10px 12px;
266
531
  border: 0;
267
- border-radius: var(--radius-2);
268
- background: transparent;
269
- color: var(--gray-11);
532
+ border-radius: var(--dsw-radius-md);
533
+ background: none;
534
+ font: inherit;
535
+ color: inherit;
536
+ text-align: start;
270
537
  cursor: pointer;
271
538
  }
272
- .zhihu-tab:hover { color: var(--gray-12); background: var(--gray-a3); }
273
- .zhihu-tab[aria-selected="true"] {
274
- background: var(--accent-a3);
275
- color: var(--accent-12, var(--accent-11));
276
- box-shadow: 0 0 0 1px var(--gray-a5);
277
- font-weight: var(--font-weight-medium);
278
- transform: scale(1.04);
539
+ & .dsh-ui-list-item:disabled { cursor: default; }
540
+ & .dsh-ui-list-item:disabled:hover { background: none; }
541
+ & .dsh-ui-list-name { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); overflow-wrap: anywhere; }
542
+ & .dsh-ui-list-desc { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
543
+
544
+ /* --- Code and structured output. Wraps rather than scrolls, because a
545
+ * feature panel is read rather than scanned. --- */
546
+ & .dsh-ui-code {
547
+ padding: 10px 12px;
548
+ border: 0.5px solid var(--dsw-alias-border-l3);
549
+ border-radius: var(--dsw-radius-md);
550
+ background: var(--dsw-alias-bg-layer-1);
551
+ font-family: var(--ds-font-family-code, ui-monospace, 'Cascadia Mono', Consolas, monospace);
552
+ font-size: 12px;
553
+ line-height: 20px;
554
+ color: var(--dsw-alias-label-secondary);
555
+ overflow-wrap: anywhere;
556
+ white-space: pre-wrap;
279
557
  }
280
- .zhihu-panel-body {
281
- padding: var(--space-3);
558
+ & .dsh-ui-scroll { max-height: 60vh; overflow: auto; overscroll-behavior: contain; }
559
+
560
+ /* --- A toggle row: label on the left, switch on the trailing edge. This is
561
+ * layout only; the switch itself is the official primitive. --- */
562
+ & .dsh-ui-toggle-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-width: 0; padding: 12px 0; }
563
+ & .dsh-ui-toggle-row + .dsh-ui-toggle-row { border-top: 0.5px solid var(--dsw-alias-border-l2); }
564
+ & .dsh-ui-toggle-text { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
565
+ & .dsh-ui-toggle-label { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); }
566
+
567
+ /* --- Floating surface: a panel anchored to a trigger. This is the material
568
+ * a menu card is made of — the translucent fill plus its backdrop blur, with
569
+ * the edge riding inside the prominent elevation. A plugin that hand-rolls this
570
+ * is how tokens like --dsw-specific-menu get invented; use this instead. --- */
571
+ & .dsh-ui-surface {
572
+ --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
573
+ --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
574
+ --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
575
+ position: relative;
282
576
  display: flex;
283
577
  flex-direction: column;
284
- gap: var(--space-2);
285
- overflow-y: auto;
578
+ border: 0;
579
+ border-radius: var(--dsw-radius-lg);
580
+ background: var(--dsw-menu-surface-fill);
581
+ backdrop-filter: var(--dsw-menu-backdrop-filter);
582
+ box-shadow: var(--dsw-elevation-prominent);
583
+ color: var(--dsw-alias-label-primary);
584
+ font-size: 13px;
585
+ line-height: 20px;
586
+ min-width: 0;
286
587
  }
287
- .zhihu-field { display: flex; flex-direction: column; gap: 4px; }
288
- .zhihu-field-label { color: var(--gray-11); font-weight: var(--font-weight-medium); }
289
- .zhihu-input, .zhihu-select {
290
- width: 100%;
291
- min-height: var(--control-h, 34px);
588
+ & .dsh-ui-surface-body { display: flex; flex-direction: column; gap: 8px; padding: 12px 14px 14px; overflow-y: auto; }
589
+ & .dsh-ui-surface-head { display: flex; align-items: center; gap: 8px; min-width: 0; }
590
+ & .dsh-ui-surface-title { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); }
591
+
592
+ /* --- Native select. The primitives ship no select, so a plugin that offers a
593
+ * closed list of choices keeps the platform control and matches the official
594
+ * settings field geometry exactly (34px, 0.5px l4, radius-md, layer-3). A
595
+ * SegmentedControl is the primitive to reach for when the choices are few
596
+ * enough to show at once. --- */
597
+ & .dsh-ui-select {
292
598
  box-sizing: border-box;
599
+ width: 100%;
600
+ min-width: 0;
601
+ height: 34px;
602
+ padding: 0 12px;
603
+ border: 0.5px solid var(--dsw-alias-border-l4);
604
+ border-radius: var(--dsw-radius-md);
605
+ background: var(--dsw-alias-bg-layer-3);
293
606
  font: inherit;
294
- font-family: var(--default-font-family);
295
- font-size: var(--font-size-2);
296
- color: var(--gray-12);
297
- background: var(--color-surface);
298
- border: 1px solid var(--gray-6);
299
- border-radius: var(--radius-2);
300
- padding: 0 8px;
301
- }
302
- .zhihu-row { display: flex; align-items: center; gap: var(--space-2); }
303
- .zhihu-button {
304
- font: inherit; font-size: var(--font-size-2); min-height: var(--control-h, 34px); color: var(--gray-11); background: none;
305
- border: 1px solid var(--gray-6); border-radius: var(--radius-2);
306
- padding: 0 12px; cursor: pointer;
307
- }
308
- .zhihu-button:hover { background: var(--gray-a3); }
309
- .zhihu-button:disabled { opacity: 0.5; cursor: default; }
310
- .zhihu-button-primary {
311
- color: var(--accent-contrast);
312
- background: var(--accent-9);
313
- border-color: var(--accent-9);
314
- }
315
- .zhihu-button-primary:hover:not(:disabled) { background: var(--accent-10); }
316
- .zhihu-button-danger { color: var(--red-11); }
317
- .zhihu-hint { color: var(--gray-11); font-size: var(--font-size-2); margin: 0; }
318
- .zhihu-intro { margin: 0; color: var(--gray-11); line-height: 1.6; }
319
- .zhihu-field > .zhihu-intro { margin-bottom: var(--space-2); }
320
- .zhihu-settings, .zhihu-knowledge { display: flex; flex-direction: column; gap: var(--space-3); }
321
- .zhihu-knowledge > p { margin: 0; }
322
- .zhihu-status { color: var(--gray-11); margin: 0; padding: var(--space-2) 0; }
323
- .zhihu-error { color: var(--red-11); margin: 0; padding: var(--space-2) 0; }
324
- .zhihu-warning { color: var(--red-11); margin: 0; }
325
- .zhihu-saved { color: var(--green-11); margin: 0; }
326
- .zhihu-link { color: var(--accent-11); }
327
- /* Ordinary DSH paints a light fill on bare <a>. Keep plugin links
328
- transparent so computed contrast falls through to the panel surface. */
329
- .zhihu-panel .zhihu-link, .zhihu-panel .zhihu-result-title { background: transparent; }
330
- .zhihu-panel a.zhihu-link,
331
- .zhihu-panel a.zhihu-result-title {
332
- background-color: transparent;
333
- }
334
- .zhihu-stale {
335
- color: var(--gray-11);
336
- background: var(--gray-a3);
337
- border-radius: var(--radius-2);
338
- padding: var(--space-2);
339
- }
340
- .zhihu-results { display: flex; flex-direction: column; gap: var(--space-2); }
341
- .zhihu-results-summary { color: var(--gray-11); font-weight: var(--font-weight-medium); margin: 0; }
342
- .zhihu-result-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-2); }
343
- .zhihu-result-item {
344
- border: 1px solid var(--gray-6);
345
- border-radius: var(--radius-2);
346
- padding: var(--space-2);
607
+ font-size: 13px;
608
+ line-height: 1.5;
609
+ color: var(--dsw-alias-label-primary);
610
+ cursor: pointer;
611
+ }
612
+ & .dsh-ui-select:focus-visible { outline: none; border-color: var(--dsw-alias-state-business-primary); }
613
+ & .dsh-ui-select:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
614
+ & .dsh-ui-select[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }
615
+
616
+ /* Long panels scroll inside themselves instead of growing the host page. */
617
+ & .dsh-ui-panel { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
618
+ & .dsh-ui-panel-wide { max-width: 100%; }
619
+
620
+ @media (prefers-reduced-motion: reduce) {
621
+ & *, & *::before, & *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
622
+ }
623
+ }
624
+ `.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?![+>~])/gm, (_match, className) => `&${className}, & ${className}`);
625
+ }
626
+ //#endregion
627
+ //#region src/client-styles.ts
628
+ /**
629
+ * The shared contract paints this page: the type tiers, the card, the field,
630
+ * the banners, the empty state, the focus ring and every control's own recipe.
631
+ * What is left here is only the geometry the contract does not cover — the
632
+ * reading width of the plugin page, two inline SVG charts, the quota matrix and
633
+ * the day table.
634
+ *
635
+ * Nothing below declares a colour of its own beyond naming a host token for a
636
+ * feature the primitives have no equivalent for (a destructive hover, an inset
637
+ * snippet well, a legend chip), and nothing carries a literal fallback: the
638
+ * host publishes both themes and a literal would pin one of them.
639
+ *
640
+ * Every plugin rule nests under the plugin root, as the contract does, so a
641
+ * .zhihu-* class never paints a neighbouring plugin. Content portaled into a
642
+ * Modal carries the root class on its own element for the same reason.
643
+ */
644
+ const zhihuClientStyles = `${officialUiCss("zhihu-panel")}
645
+ .zhihu-panel {
646
+ /* --- Plugin page. The contract's rules nest under the scope selector, so the
647
+ * root's own column and reading width are the one part it cannot reach. --- */
648
+ &.zhihu-settings-embed {
347
649
  display: flex;
348
650
  flex-direction: column;
349
- gap: 4px;
350
- background: var(--color-panel-solid);
651
+ gap: 12px;
652
+ width: 100%;
653
+ max-width: 760px;
654
+ min-width: 0;
655
+ }
656
+ /* The tab strip is a primitive; this only places it in the page column. */
657
+ .zhihu-tabs { margin: 0; }
658
+
659
+ /* --- Fields. The official Input is an inline-flex wrapper, so a form-width
660
+ * field states that width once instead of per call site. --- */
661
+ .zhihu-input { width: 100%; }
662
+
663
+ /* --- Actions. A destructive button keeps the outlined resting appearance every
664
+ * other action uses and only shows its tone under the pointer or the keyboard. */
665
+ .zhihu-button-danger:hover:not(:disabled),
666
+ .zhihu-button-danger:focus-visible {
667
+ color: var(--dsw-alias-state-error-primary);
668
+ border-color: var(--dsw-alias-state-error-primary);
669
+ background: var(--dsw-alias-interactive-bg-hover-danger);
351
670
  }
352
- .zhihu-result-item:hover { background: var(--gray-3); }
353
- .zhihu-result-title { font-weight: var(--font-weight-medium); color: var(--gray-12); }
354
- .zhihu-result-meta { color: var(--gray-11); font-size: var(--font-size-2); }
355
- .zhihu-result-summary { color: var(--gray-11); }
671
+
672
+ /* --- Disclosures. The capability and fine-print blocks use the host
673
+ * DisclosureRow; only the reasoning block is still a native <summary>. --- */
674
+ .zhihu-ask-reasoning > summary { cursor: pointer; width: fit-content; }
675
+ .zhihu-guide-list { margin: 4px 0 0; padding-inline-start: 20px; display: grid; gap: 6px; }
676
+
677
+ /* --- Result rows. The card, the row and the name all come from the contract; a
678
+ * matched snippet is the one inset well the panel needs of its own. --- */
356
679
  .zhihu-result-snippet {
357
- color: var(--gray-11);
358
- background: var(--gray-3);
359
- border-radius: var(--radius-2);
360
- padding: 4px 8px;
361
- }
362
- .zhihu-ask-content { white-space: pre-wrap; line-height: 1.7; color: var(--gray-12); }
363
- .zhihu-ask-reasoning { color: var(--gray-11); font-size: var(--font-size-2); }
364
- .zhihu-status-grid { display: flex; gap: var(--space-2); margin: 0; align-items: baseline; }
365
- .zhihu-status-label { color: var(--gray-11); font-weight: var(--font-weight-medium); }
366
- .zhihu-status-value { margin: 0; display: flex; align-items: center; gap: 6px; }
367
- .zhihu-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
368
- .zhihu-dot-configured { background: var(--green-11); }
369
- .zhihu-dot-missing { background: var(--gray-9); }
370
- .zhihu-dot-locked { background: var(--red-9); }
371
- .zhihu-field-heading { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3); }
372
- .zhihu-settings .zhihu-guide { border: 0; padding: 0; color: var(--gray-11); font-size: var(--font-size-1); }
373
- .zhihu-settings .zhihu-guide summary { cursor: pointer; width: fit-content; }
374
- .zhihu-settings .zhihu-guide p { margin-top: var(--space-2); font-size: var(--font-size-1); }
375
- .zhihu-settings :is(.zhihu-field-heading a,.zhihu-status-grid) { font-size: var(--font-size-1); }
376
- .zhihu-settings :is(a,summary):focus-visible { outline: 2px solid var(--accent-8); outline-offset: 2px; }
377
- .zhihu-guide {
378
- border: 1px solid var(--gray-6);
379
- border-radius: var(--radius-2);
380
- padding: var(--space-2);
381
- }
382
- .zhihu-guide-title { margin: 0 0 4px; font-size: var(--font-size-2); font-weight: var(--font-weight-medium); font-family: var(--default-font-family); }
383
- .zhihu-guide-steps { margin: 0; padding-left: 18px; color: var(--gray-11); line-height: 1.7; }
384
- .zhihu-scopes { display: flex; gap: var(--space-3); flex-wrap: wrap; }
385
- .zhihu-scope {
386
- display: inline-flex; align-items: center; gap: 4px; color: var(--gray-11);
387
- font: inherit; background: none; border: 1px solid var(--gray-6); border-radius: var(--radius-2);
388
- padding: 0 10px; min-height: var(--control-h, 34px); cursor: pointer;
389
- }
390
- .zhihu-scope.is-on, .zhihu-scope[aria-pressed="true"] {
391
- color: var(--accent-11); background: var(--accent-a3); border-color: var(--accent-a6);
392
- }
393
- .zhihu-input.ui-input { width: 100%; }
394
- .zhihu-usage { display: flex; flex-direction: column; gap: var(--space-3); }
395
- .zhihu-usage-intro { margin: 0; color: var(--gray-11); line-height: 1.6; }
396
- .zhihu-usage-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr)); gap: var(--space-2); }
397
- .zhihu-usage-card { display: flex; flex-direction: column; gap: 4px; min-width: 0; padding: var(--space-2); border: 1px solid var(--gray-6); border-radius: var(--radius-2); background: var(--color-panel-solid); }
398
- .zhihu-usage-card:hover { background: var(--gray-a3); }
399
- .zhihu-usage-card-label { color: var(--gray-11); font-size: var(--font-size-2); }
400
- .zhihu-usage-card-value { font-weight: var(--font-weight-medium); font-variant-numeric: tabular-nums; color: var(--gray-12); }
401
- .zhihu-usage-card-unit { margin-left: 4px; color: var(--gray-11); font-size: var(--font-size-2); font-weight: 400; }
402
- .zhihu-usage-heading { margin: 0; font-size: var(--font-size-2); font-weight: var(--font-weight-medium); color: var(--gray-11); font-family: var(--default-font-family); }
403
- .zhihu-usage-legend { display: flex; flex-wrap: wrap; gap: 6px var(--space-3); margin: 0; padding: 0; list-style: none; color: var(--gray-11); font-size: var(--font-size-2); }
404
- .zhihu-usage-legend li { display: inline-flex; align-items: center; gap: 6px; }
405
- .zhihu-chart-chip { width: 8px; height: 8px; border-radius: 2px; flex: none; }
406
- .zhihu-chart-chip-ok { background: var(--accent-9); }
407
- .zhihu-chart-chip-fail { background: var(--red-9); }
408
- .zhihu-chart { width: 100%; height: auto; aspect-ratio: 600 / 176; display: block; }
409
- .zhihu-chart-bar-ok { fill: var(--accent-9); }
410
- .zhihu-chart-bar-fail { fill: var(--red-9); }
411
- .zhihu-chart-hit { fill: transparent; }
412
- .zhihu-chart-grid { stroke: var(--gray-6); stroke-width: 1; }
413
- .zhihu-chart-axis { fill: var(--gray-11); font-size: var(--font-size-1); font-family: var(--code-font-family); }
414
- .zhihu-chart-tick { fill: var(--gray-11); font-size: var(--font-size-1); font-family: var(--code-font-family); }
415
- .zhihu-chart-value { fill: var(--gray-11); font-size: var(--font-size-1); font-family: var(--code-font-family); }
680
+ display: block;
681
+ padding: 6px 10px;
682
+ border-radius: var(--dsw-radius-sm);
683
+ background: var(--dsw-alias-bg-layer-2);
684
+ }
685
+ .zhihu-ask-content { white-space: pre-wrap; }
686
+
687
+ /* --- Open platform. The section is the last block of its tab, so it keeps a
688
+ * trailing hairline; the raw response scrolls instead of growing the page. --- */
689
+ .zhihu-open-platform { padding-bottom: 16px; border-bottom: 0.5px solid var(--dsw-alias-border-l2); }
690
+ .zhihu-open-platform-quota > legend { padding: 0 8px 0 0; }
691
+ .zhihu-open-platform-data { max-height: 480px; overflow: auto; margin: 8px 0 0; }
692
+
693
+ /* --- Usage. The stat tiles are a grid and the day list is a table, neither of
694
+ * which the contract has a recipe for; figures align in the columns they sit
695
+ * in, so a column of counts scans as a column. --- */
696
+ .zhihu-usage-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr)); gap: 8px; }
697
+ .zhihu-usage-value { font-variant-numeric: tabular-nums; }
416
698
  .zhihu-usage-table-wrap { overflow-x: auto; }
417
699
  .zhihu-usage-table { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; }
418
- .zhihu-usage-table th, .zhihu-usage-table td { padding: 4px 6px; text-align: right; border-bottom: 1px solid var(--gray-6); color: var(--gray-11); }
419
- .zhihu-usage-table th:first-child, .zhihu-usage-table td:first-child { text-align: left; }
420
- .zhihu-usage-table th { color: var(--gray-11); font-weight: var(--font-weight-medium); }
700
+ .zhihu-usage-table th,
701
+ .zhihu-usage-table td { padding: 4px 6px; text-align: right; border-bottom: 0.5px solid var(--dsw-alias-border-l2); color: var(--dsw-alias-label-secondary); }
702
+ .zhihu-usage-table th:first-child,
703
+ .zhihu-usage-table td:first-child { text-align: start; }
704
+ .zhihu-usage-table th { color: var(--dsw-alias-label-tertiary); font-weight: 500; }
705
+
706
+ /* --- Usage chart. Inline SVG cannot read a CSS module, so the bars and their
707
+ * axis carry their own geometry and take every colour from a host token. --- */
708
+ .zhihu-chart-chip { width: 8px; height: 8px; border-radius: var(--dsw-radius-sm); flex: none; }
709
+ .zhihu-chart-chip-ok { background: var(--dsw-alias-state-business-primary); }
710
+ .zhihu-chart-chip-fail { background: var(--dsw-alias-state-error-primary); }
711
+ .zhihu-chart { width: 100%; height: auto; aspect-ratio: 600 / 176; display: block; }
712
+ .zhihu-chart-bar-ok { fill: var(--dsw-alias-state-business-primary); }
713
+ .zhihu-chart-bar-fail { fill: var(--dsw-alias-state-error-primary); }
714
+ .zhihu-chart-hit { fill: transparent; }
715
+ .zhihu-chart-grid { stroke: var(--dsw-alias-border-l2); stroke-width: 1; }
716
+ .zhihu-chart-axis,
717
+ .zhihu-chart-tick,
718
+ .zhihu-chart-value { fill: var(--dsw-alias-label-secondary); font-size: var(--dsh-content-font-size-secondary); font-family: var(--ds-font-family-code); }
719
+
720
+ /* --- Quota matrix. One row per API, one column per field: the header carries
721
+ * that field's own maximum, so a bar only ever states "this share of this
722
+ * column's maximum". The rule is a 3px underline under the figure, right-anchored
723
+ * to the digits it belongs to, so scanning one column is easy and the number
724
+ * stays the primary read. --- */
725
+ .zhihu-quota-scroll { overflow-x: auto; }
726
+ .zhihu-quota-table { width: 100%; border-collapse: collapse; }
727
+ .zhihu-quota-table th,
728
+ .zhihu-quota-table td { padding: 5px 8px; vertical-align: middle; }
729
+ .zhihu-quota-name { text-align: start; font-weight: 400; color: var(--dsw-alias-label-primary); }
730
+ .zhihu-quota-corner { text-align: start; font-weight: 500; color: var(--dsw-alias-label-secondary); }
731
+ .zhihu-quota-head { text-align: end; }
732
+ .zhihu-quota-head-label { display: block; font-weight: 500; color: var(--dsw-alias-label-secondary); }
733
+ .zhihu-quota-head-scale { display: block; font-weight: 400; color: var(--dsw-alias-label-tertiary); }
734
+ .zhihu-quota-cell { text-align: end; }
735
+ .zhihu-quota-value { display: block; font-variant-numeric: tabular-nums; color: var(--dsw-alias-label-primary); }
736
+ /* The ceiling is already in the fraction, so it steps back: same figures, less
737
+ * weight on the part that only supplies the denominator. */
738
+ .zhihu-quota-of { color: var(--dsw-alias-label-tertiary); }
739
+ .zhihu-quota-track { display: block; height: 3px; margin-top: 3px; border-radius: var(--dsw-radius-sm); background: var(--dsw-alias-bg-layer-3); overflow: hidden; }
740
+ .zhihu-quota-fill { display: block; height: 100%; margin-inline-start: auto; border-radius: inherit; background: var(--dsw-alias-state-business-primary); }
741
+ @media (max-width: 480px) {
742
+ /* The columns keep their room and the wrapper scrolls; crushing three scales
743
+ * into a phone width is what made the bars meaningless in the first place. */
744
+ .zhihu-quota-table { min-width: 24rem; }
745
+ .zhihu-quota-table th,
746
+ .zhihu-quota-table td { padding: 5px 6px; }
747
+ }
748
+
749
+ /* --- File field. The input is visually hidden but stays focusable and in the
750
+ * accessibility tree; its <label> is the visible trigger, so a click on the
751
+ * label opens the file dialog natively and keyboard focus rings the label. --- */
421
752
  .zhihu-file {
422
753
  position: absolute;
423
754
  width: 1px;
@@ -429,151 +760,71 @@ const zhihuClientStyles = `
429
760
  white-space: nowrap;
430
761
  border: 0;
431
762
  }
432
- .zhihu-upload-confirm {
433
- border: 1px solid var(--gray-6);
434
- border-radius: var(--radius-2);
435
- padding: var(--space-2);
436
- color: var(--gray-11);
763
+ .zhihu-file-trigger {
764
+ display: inline-flex;
765
+ align-items: center;
766
+ padding: 4px 12px;
767
+ border: 0.5px solid var(--dsw-alias-border-l2);
768
+ border-radius: var(--dsw-radius-md);
769
+ color: var(--dsw-alias-label-primary);
770
+ cursor: pointer;
771
+ flex: none;
437
772
  }
773
+ .zhihu-file-trigger:hover { background: var(--dsw-alias-interactive-bg-hover); }
774
+ .zhihu-file-trigger[aria-disabled="true"] { opacity: .5; pointer-events: none; }
775
+ .zhihu-file:focus-visible + .dsh-ui-row .zhihu-file-trigger { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 2px; }
776
+
438
777
  /* 活动反馈:三点呼吸(pulse-dots),参数改写自 Amicro(MIT License,
439
778
  Copyright (c) 2026 Syed Subhan Uddin);装饰元素 aria-hidden。 */
440
- @keyframes zhihu-activity-pulse { 0%, 100% { opacity: .2; } 50% { opacity: 1; } }
441
- .zhihu-dots { display: inline-flex; align-items: center; gap: 3px; margin-inline-end: .4em; vertical-align: middle; color: var(--accent-9); }
779
+ .zhihu-dots { display: inline-flex; align-items: center; gap: 3px; margin-inline-end: .4em; vertical-align: middle; color: var(--dsw-alias-state-business-primary); }
442
780
  .zhihu-dots i { width: .32em; height: .32em; min-width: 3px; min-height: 3px; border-radius: 50%; background: currentColor; animation: zhihu-activity-pulse 1.4s ease infinite; }
443
781
  .zhihu-dots i:nth-child(2) { animation-delay: .2s; }
444
782
  .zhihu-dots i:nth-child(3) { animation-delay: .4s; }
445
783
  @media (prefers-reduced-motion: reduce) {
446
- .zhihu-panel, .zhihu-panel * {
447
- animation: none !important; transition: none !important;
448
- transform: none !important; filter: none !important;
449
- }
784
+ /* Stop the loop outright so the three dots stay lit instead of flickering. */
450
785
  .zhihu-dots i { animation: none; }
451
786
  }
452
- /* Local tokens follow the host theme when the settings page is outside Radix. */
453
- .zhihu-panel${standaloneOutsideTheme} {
454
-
455
- --default-font-family: "Noto Sans SC", "PingFang SC", "Microsoft YaHei UI", system-ui, sans-serif;
456
- --code-font-family: ui-monospace, "SF Mono", "JetBrains Mono", Consolas, monospace;
457
- --font-weight-medium: 500;
458
- --font-size-1: 13px;
459
- --font-size-2: 14px;
460
- --font-size-3: 16px;
461
- --font-size-4: 18px;
462
- --font-size-5: 20px;
463
- --font-size-6: 24px;
464
- --font-size-7: 28px;
465
- --font-size-8: 35px;
466
- --font-size-9: 60px;
467
- --space-1: 4px;
468
- --space-2: 8px;
469
- --space-3: 12px;
470
- --space-4: 16px;
471
- --space-5: 24px;
472
- --radius-2: 4px;
473
- --radius-3: 6px;
474
- --control-h: 34px;
475
- --gray-1: #fcfcfc;
476
- --gray-2: #f9f9f9;
477
- --gray-3: #f0f0f0;
478
- --gray-4: #e8e8e8;
479
- --gray-5: #e0e0e0;
480
- --gray-6: #d9d9d9;
481
- --gray-7: #cecece;
482
- --gray-8: #bbbbbb;
483
- --gray-9: #8d8d8d;
484
- --gray-10: #838383;
485
- --gray-11: #646464;
486
- --gray-12: #202020;
487
- --gray-a2: #00000006;
488
- --gray-a3: #0000000f;
489
- --gray-a4: #00000017;
490
- --gray-a5: #0000001f;
491
- --gray-a6: #00000026;
492
- --accent-8: #8da4ef;
493
- --accent-9: #3e63dd;
494
- --accent-10: #3358d4;
495
- --accent-11: #3a5bc7;
496
- --accent-a3: #0047f112;
497
- --accent-a4: #0044ff1e;
498
- --accent-a8: #0034dc72;
499
- --accent-contrast: #ffffff;
500
- --red-9: #e5484d;
501
- --red-11: #ce2c31;
502
- --green-9: #30a46c;
503
- --green-11: #218358;
504
- --amber-a4: #ffd40063;
505
- --amber-a6: #eab5008c;
506
- --color-background: #ffffff;
507
- --color-panel-solid: #ffffff;
508
- --color-surface: rgba(255, 255, 255, 0.85);
509
- --shadow-2: 0 0 0 1px #0000000f, 0 0 0 0.5px rgba(0, 0, 0, 0.05), 0 1px 1px 0 #00000006, 0 2px 1px -1px rgba(0, 0, 0, 0.05), 0 1px 3px 0 rgba(0, 0, 0, 0.05);
510
- --shadow-3: 0 0 0 1px #0000000f, 0 2px 3px -2px #0000000f, 0 3px 12px -4px rgba(0, 0, 0, 0.1), 0 4px 16px -8px rgba(0, 0, 0, 0.1);
511
- --shadow-4: 0 0 0 1px #0000000f, 0 8px 40px rgba(0, 0, 0, 0.05), 0 12px 32px -16px #0000000f;
512
- color-scheme: light;
513
- }
514
- html:not([data-theme]) body[data-ds-dark-theme] .zhihu-panel${standaloneOutsideTheme},
515
- :root[data-theme="dark"] .zhihu-panel${standaloneOutsideTheme} {
787
+ }
516
788
 
517
- --default-font-family: "Noto Sans SC", "PingFang SC", "Microsoft YaHei UI", system-ui, sans-serif;
518
- --code-font-family: ui-monospace, "SF Mono", "JetBrains Mono", Consolas, monospace;
519
- --font-weight-medium: 500;
520
- --font-size-1: 13px;
521
- --font-size-2: 14px;
522
- --font-size-3: 16px;
523
- --font-size-4: 18px;
524
- --font-size-5: 20px;
525
- --font-size-6: 24px;
526
- --font-size-7: 28px;
527
- --font-size-8: 35px;
528
- --font-size-9: 60px;
529
- --space-1: 4px;
530
- --space-2: 8px;
531
- --space-3: 12px;
532
- --space-4: 16px;
533
- --space-5: 24px;
534
- --radius-2: 4px;
535
- --radius-3: 6px;
536
- --control-h: 34px;
537
- --gray-1: #111111;
538
- --gray-2: #191919;
539
- --gray-3: #222222;
540
- --gray-4: #2a2a2a;
541
- --gray-5: #313131;
542
- --gray-6: #3a3a3a;
543
- --gray-7: #484848;
544
- --gray-8: #606060;
545
- --gray-9: #6e6e6e;
546
- --gray-10: #7b7b7b;
547
- --gray-11: #b4b4b4;
548
- --gray-12: #eeeeee;
549
- --gray-a2: #ffffff09;
550
- --gray-a3: #ffffff12;
551
- --gray-a4: #ffffff1b;
552
- --gray-a5: #ffffff22;
553
- --gray-a6: #ffffff2c;
554
- --accent-8: #435db1;
555
- --accent-9: #3e63dd;
556
- --accent-10: #5472e4;
557
- --accent-11: #9eb1ff;
558
- --accent-a3: #2f62ff3c;
559
- --accent-a4: #3566ff57;
560
- --accent-a8: #5b81feac;
561
- --accent-contrast: #ffffff;
562
- --red-9: #e5484d;
563
- --red-11: #ff9592;
564
- --green-9: #30a46c;
565
- --green-11: #3dd68c;
566
- --amber-a4: #fc820032;
567
- --amber-a6: #fd9b0051;
568
- --color-background: #111111;
569
- --color-panel-solid: #191919;
570
- --color-surface: rgba(0, 0, 0, 0.25);
571
- --shadow-2: 0 0 0 1px #ffffff2c, 0 0 0 0.5px rgba(0, 0, 0, 0.15), 0 1px 1px 0 rgba(0, 0, 0, 0.4), 0 2px 1px -1px rgba(0, 0, 0, 0.4), 0 1px 3px 0 rgba(0, 0, 0, 0.3);
572
- --shadow-3: 0 0 0 1px #ffffff2c, 0 2px 3px -2px rgba(0, 0, 0, 0.15), 0 3px 8px -2px rgba(0, 0, 0, 0.4), 0 4px 12px -4px rgba(0, 0, 0, 0.5);
573
- --shadow-4: 0 0 0 1px #ffffff2c, 0 8px 40px rgba(0, 0, 0, 0.15), 0 12px 32px -16px rgba(0, 0, 0, 0.3);
574
- color-scheme: dark;
789
+ /* --- Links. Ordinary DSH paints a light fill on a bare <a>, so plugin links
790
+ * stay transparent and the computed contrast falls through to the card behind
791
+ * them. Anchoring the selector here also outranks the contract's own row-name
792
+ * colour, so a result title reads as a link rather than as body text. It sits
793
+ * outside the nested block because it already names the root. */
794
+ .zhihu-panel a.zhihu-link {
795
+ color: var(--dsw-alias-brand-primary);
796
+ background: none;
575
797
  }
798
+
799
+ /* Keyframes cannot nest inside a style rule; the name is plugin-prefixed. */
800
+ @keyframes zhihu-activity-pulse { 0%, 100% { opacity: .2; } 50% { opacity: 1; } }
576
801
  `;
802
+ //#endregion
803
+ //#region src/client-host-ui.tsx
804
+ /**
805
+ * Folded fine print: one concise sentence stays in the flow, the detail list
806
+ * sits behind the host disclosure row. Children are `<li>` items.
807
+ */
808
+ function ZhihuDetails(props) {
809
+ const [open, setOpen] = (0, react.useState)(props.defaultOpen === true);
810
+ const toggle = (0, react.useCallback)(() => setOpen((value) => !value), []);
811
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
812
+ className: "zhihu-guide",
813
+ "data-testid": props.testId,
814
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.DisclosureRow, {
815
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChecklistOutlineRegular, {}),
816
+ title: props.title,
817
+ open,
818
+ expandable: true,
819
+ expandOnRowClick: true,
820
+ onToggle: toggle,
821
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
822
+ className: "zhihu-guide-list dsh-ui-help",
823
+ children: props.children
824
+ })
825
+ })
826
+ });
827
+ }
577
828
  /** React KeyboardEvent: isComposing/keyCode live on nativeEvent, not the synthetic event. */
578
829
  function nativeKeyFlags(event) {
579
830
  const native = event.nativeEvent;
@@ -620,8 +871,8 @@ function hostComponentsFromRenderProps(props) {
620
871
  }
621
872
  function renderInput(Input, props) {
622
873
  if (Input) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, { ...props });
623
- const { onChange, ...rest } = props;
624
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
874
+ const { onChange, ref, ...rest } = props;
875
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
625
876
  ...rest,
626
877
  onChange: (event) => onChange(event.target.value)
627
878
  });
@@ -629,6 +880,7 @@ function renderInput(Input, props) {
629
880
  function renderSelect(Select, props, className) {
630
881
  if (Select) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Select, { ...props });
631
882
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
883
+ id: props.id,
632
884
  className,
633
885
  value: props.value,
634
886
  disabled: props.disabled,
@@ -640,7 +892,21 @@ function renderSelect(Select, props, className) {
640
892
  }, option.value === "" ? "__empty" : option.value))
641
893
  });
642
894
  }
643
- /** Prefer a structurally supplied host control; standalone Web needs no private UI package. */
895
+ /**
896
+ * The host's own Button still wins whenever the slot supplies one, so a page
897
+ * rendered inside DSH keeps the surrounding chrome. Standalone there is no host
898
+ * to defer to, and the plugin renders the official primitive instead of a
899
+ * hand-rolled `<button>`: the two legacy variant names the call sites use are
900
+ * mapped onto it here. `danger` has no primitive variant, so it keeps the
901
+ * outlined appearance and takes its tone from a class in the plugin stylesheet
902
+ * — the way every other destructive action in the host reads.
903
+ */
904
+ const BUTTON_VARIANT = {
905
+ default: "outline",
906
+ primary: "primary",
907
+ danger: "outline",
908
+ icon: "ghost"
909
+ };
644
910
  const ZhihuButton = (0, react.forwardRef)(function ZhihuButton({ host, variant, className, ...rest }, ref) {
645
911
  if (host) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(host, {
646
912
  ref,
@@ -648,21 +914,861 @@ const ZhihuButton = (0, react.forwardRef)(function ZhihuButton({ host, variant,
648
914
  className,
649
915
  ...rest
650
916
  });
651
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
917
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
652
918
  ref,
653
- type: "button",
654
- className: [variant === "primary" ? "primary-action" : variant === "danger" ? "danger-action" : variant === "icon" ? "icon-button" : "", className].filter(Boolean).join(" "),
919
+ variant: BUTTON_VARIANT[variant ?? "default"],
920
+ className,
655
921
  ...rest
656
922
  });
657
923
  });
658
924
  //#endregion
925
+ //#region src/client-open-platform.tsx
926
+ const OPERATION_COPY = [
927
+ {
928
+ value: "question.recommendations",
929
+ zh: "适合回答的问题",
930
+ en: "Questions to answer"
931
+ },
932
+ {
933
+ value: "question.answers",
934
+ zh: "问题回答摘要(非全文)",
935
+ en: "Answer summaries (not full text)"
936
+ },
937
+ {
938
+ value: "content.detail",
939
+ zh: "本人创作全文",
940
+ en: "My content (full text)"
941
+ },
942
+ {
943
+ value: "content.comments",
944
+ zh: "本人创作评论",
945
+ en: "Comments on my content"
946
+ },
947
+ {
948
+ value: "creator.account.stats",
949
+ zh: "账号创作数据",
950
+ en: "Account creator stats"
951
+ },
952
+ {
953
+ value: "creator.content.stats",
954
+ zh: "单篇创作数据",
955
+ en: "Per-post creator stats"
956
+ },
957
+ {
958
+ value: "quota",
959
+ zh: "官方每日额度",
960
+ en: "Official daily quota"
961
+ }
962
+ ];
963
+ function openPlatformOptions(t = translator()) {
964
+ return OPERATION_COPY.map((option) => ({
965
+ value: option.value,
966
+ label: t(option.zh, option.en)
967
+ }));
968
+ }
969
+ openPlatformOptions();
970
+ const createOpenPlatformForm = () => ({
971
+ query: "",
972
+ count: "5",
973
+ questionUrl: "",
974
+ contentUrl: "",
975
+ offset: "0",
976
+ limit: "20",
977
+ order: "score",
978
+ contentType: "all",
979
+ startDate: "",
980
+ endDate: "",
981
+ apiIds: []
982
+ });
983
+ const isRecord = (value) => !!value && typeof value === "object" && !Array.isArray(value);
984
+ /** Int64 stays decimal text throughout the browser; never round-trip through Number. */
985
+ function openPlatformOffset(value) {
986
+ if (typeof value !== "string" || !/^\d+$/.test(value)) return null;
987
+ const decimal = value.replace(/^0+(?=\d)/, "");
988
+ return decimal.length < 19 || decimal.length === 19 && decimal <= "9223372036854775807" ? decimal : null;
989
+ }
990
+ /** `field` is the user-facing name; `param` stays in the message so a log still maps to the API. */
991
+ function integer(value, maximum, field, param, t) {
992
+ const message = t(`${field}(${param})必须是 1–${maximum} 的整数。`, `${field} (${param}) must be a whole number from 1 to ${maximum}.`);
993
+ if (!/^\d+$/.test(value.trim())) throw new Error(message);
994
+ const n = Number(value);
995
+ if (!Number.isInteger(n) || n < 1 || n > maximum) throw new Error(message);
996
+ return n;
997
+ }
998
+ function contentUrl(value, question, t) {
999
+ const clean = value.trim();
1000
+ const invalid = t("须为有效的 HTTPS 知乎链接。", "Must be a valid HTTPS Zhihu link.");
1001
+ if (!clean || /[\u0000-\u0020\u007f\\]/.test(clean)) throw new Error(invalid);
1002
+ let url;
1003
+ try {
1004
+ url = new URL(clean);
1005
+ } catch {
1006
+ throw new Error(invalid);
1007
+ }
1008
+ const pathOk = question ? url.hostname === "www.zhihu.com" && /^\/question\/\d+\/?$/.test(url.pathname) : url.hostname === "www.zhihu.com" && /^\/(?:answer\/\d+|question\/\d+\/answer\/\d+|pin\/\d+|zvideo\/\d+)\/?$/.test(url.pathname) || url.hostname === "zhuanlan.zhihu.com" && /^\/p\/\d+\/?$/.test(url.pathname);
1009
+ if (url.protocol !== "https:" || url.username || url.password || url.port || !pathOk) throw new Error(question ? t("问题链接须为 https://www.zhihu.com/question/{id}。", "The question link must be https://www.zhihu.com/question/{id}.") : t("内容链接须为知乎回答、文章、想法或视频。", "The content link must be a Zhihu answer, article, pin or video."));
1010
+ return clean;
1011
+ }
1012
+ function validDate(value) {
1013
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value) || value.startsWith("0000")) return false;
1014
+ const date = /* @__PURE__ */ new Date(`${value}T00:00:00Z`);
1015
+ return Number.isFinite(date.getTime()) && date.toISOString().slice(0, 10) === value;
1016
+ }
1017
+ /** Browser-only validation; authoritative validation remains with the existing RPC. */
1018
+ function buildOpenPlatformParams(operation, form, t = translator()) {
1019
+ if (!OPERATION_COPY.some((option) => option.value === operation)) throw new Error(t("未知操作。", "Unknown operation."));
1020
+ const params = {};
1021
+ if (operation === "question.recommendations") {
1022
+ if (form.query.trim()) params.query = form.query.trim();
1023
+ params.count = integer(form.count, 20, t("数量", "Count"), "count", t);
1024
+ }
1025
+ if (operation === "question.answers") params.questionUrl = contentUrl(form.questionUrl, true, t);
1026
+ if ([
1027
+ "content.detail",
1028
+ "content.comments",
1029
+ "creator.content.stats"
1030
+ ].includes(operation)) params.contentUrl = contentUrl(form.contentUrl, false, t);
1031
+ if (operation === "question.answers" || operation === "content.comments") {
1032
+ const offset = openPlatformOffset(form.offset.trim());
1033
+ if (offset === null) throw new Error(t("起始位置(offset)必须是非负整数。", "Start position (offset) must be a non-negative whole number."));
1034
+ params.offset = offset;
1035
+ params.limit = integer(form.limit, 50, t("每页数量", "Page size"), "limit", t);
1036
+ }
1037
+ if (operation === "content.comments") {
1038
+ if (![
1039
+ "score",
1040
+ "reverse",
1041
+ "ascending"
1042
+ ].includes(form.order)) throw new Error(t("评论排序无效。", "Invalid comment order."));
1043
+ params.order = form.order;
1044
+ }
1045
+ if (operation === "creator.account.stats") {
1046
+ if (![
1047
+ "all",
1048
+ "answer",
1049
+ "article",
1050
+ "pin",
1051
+ "zvideo"
1052
+ ].includes(form.contentType)) throw new Error(t("内容类型无效。", "Invalid content type."));
1053
+ params.contentType = form.contentType;
1054
+ }
1055
+ if (operation === "creator.account.stats" || operation === "creator.content.stats") {
1056
+ const start = form.startDate.trim(), end = form.endDate.trim();
1057
+ if (start || end) {
1058
+ if (!validDate(start) || !validDate(end)) throw new Error(t("开始与结束日期须同时填写有效的 YYYY-MM-DD,或同时留空。", "Fill both start and end dates as YYYY-MM-DD, or leave both empty."));
1059
+ if (end < start) throw new Error(t("结束日期不得早于开始日期。", "The end date cannot be before the start date."));
1060
+ params.startDate = start;
1061
+ params.endDate = end;
1062
+ }
1063
+ }
1064
+ if (operation === "quota") {
1065
+ if (!Array.isArray(form.apiIds) || form.apiIds.some((id) => !ZHIHU_QUOTA_IDS.includes(id))) throw new Error(t("包含未知的额度项。", "Contains an unknown quota item."));
1066
+ if (form.apiIds.length) params.apiIds = [...new Set(form.apiIds)];
1067
+ }
1068
+ return params;
1069
+ }
1070
+ function jsonValue(value, depth = 0) {
1071
+ if (depth > 64) return false;
1072
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
1073
+ if (typeof value === "number") return Number.isFinite(value);
1074
+ if (Array.isArray(value)) return value.every((item) => jsonValue(item, depth + 1));
1075
+ return isRecord(value) && Object.values(value).every((item) => jsonValue(item, depth + 1));
1076
+ }
1077
+ function parseOpenPlatformResponse(operation, raw) {
1078
+ if (!isRecord(raw) || raw.ok !== true) {
1079
+ if (isRecord(raw) && raw.ok === false && isRecord(raw.error) && typeof raw.error.message === "string") throw new Error(`${typeof raw.error.code === "string" ? raw.error.code + ":" : ""}${raw.error.message}`);
1080
+ throw new Error("RPC 返回格式无效。");
1081
+ }
1082
+ const value = raw.value;
1083
+ if (!isRecord(value) || value.version !== 1 || value.operation !== operation || !isRecord(value.data) || !jsonValue(value.data)) throw new Error("开放平台响应格式无效。");
1084
+ if (value.quotaId !== void 0 && !ZHIHU_QUOTA_IDS.includes(value.quotaId)) throw new Error("响应额度项无效。");
1085
+ if (value.paging !== void 0 && (!isRecord(value.paging) || typeof value.paging.canContinue !== "boolean" || value.paging.isEnd !== void 0 && typeof value.paging.isEnd !== "boolean" || value.paging.warning !== void 0 && typeof value.paging.warning !== "string")) throw new Error("分页响应格式无效。");
1086
+ return value;
1087
+ }
1088
+ function openPlatformNextOffset(result, currentOffset) {
1089
+ if (!result || !["question.answers", "content.comments"].includes(result.operation) || result.paging?.canContinue !== true || result.paging.isEnd === true) return null;
1090
+ const next = openPlatformOffset(result.paging.nextOffset), current = openPlatformOffset(currentOffset);
1091
+ if (next === null || current === null) return null;
1092
+ return next.length > current.length || next.length === current.length && next > current ? next : null;
1093
+ }
1094
+ /** Shared lineage executor, also testable without a DOM. Cancelled/superseded replies never publish. */
1095
+ async function requestOpenPlatform(rpc, state, operation, params, publish) {
1096
+ const { ticket, signal } = state.begin();
1097
+ try {
1098
+ const raw = await rpc.call(ZHIHU_RPC_CHANNEL, operation, params, signal);
1099
+ if (!state.isCurrent(ticket)) return;
1100
+ publish({ value: parseOpenPlatformResponse(operation, raw) });
1101
+ } catch (error) {
1102
+ if (state.isCurrent(ticket)) publish({ error: error instanceof Error ? error.message : String(error) });
1103
+ }
1104
+ }
1105
+ function ZhihuOpenPlatformSection({ rpc, Button, Input, Select, quotaOnly = false, selectedOperation }) {
1106
+ const t = useT();
1107
+ const [operation, setOperation] = (0, react.useState)(quotaOnly ? "quota" : "question.recommendations");
1108
+ const [form, setForm] = (0, react.useState)(createOpenPlatformForm);
1109
+ const [result, setResult] = (0, react.useState)(null);
1110
+ const [error, setError] = (0, react.useState)("");
1111
+ const [loading, setLoading] = (0, react.useState)(false);
1112
+ const [stale, setStale] = (0, react.useState)(false);
1113
+ const state = (0, react.useRef)(createZhihuClientState()).current;
1114
+ const activeOperation = quotaOnly ? "quota" : selectedOperation ?? operation;
1115
+ /** Switching operation or transport: the old result no longer belongs here. */
1116
+ const reset = () => {
1117
+ state.noteInput();
1118
+ setLoading(false);
1119
+ setResult(null);
1120
+ setStale(false);
1121
+ setError("");
1122
+ };
1123
+ /** Editing a field: abort in flight, keep the last result, flag it stale. */
1124
+ const edited = () => {
1125
+ state.noteInput();
1126
+ setLoading(false);
1127
+ setError("");
1128
+ setStale(true);
1129
+ };
1130
+ (0, react.useEffect)(() => {
1131
+ reset();
1132
+ return () => state.cancel();
1133
+ }, [
1134
+ rpc,
1135
+ quotaOnly,
1136
+ selectedOperation,
1137
+ state
1138
+ ]);
1139
+ const change = (key, value) => {
1140
+ edited();
1141
+ setForm((previous) => ({
1142
+ ...previous,
1143
+ [key]: value
1144
+ }));
1145
+ };
1146
+ /** Label is user copy; the API parameter name rides in the hint for people cross-checking the docs. */
1147
+ const field = (key, label, hint, placeholder) => {
1148
+ const hintId = `zhihu-op-${key}-hint`;
1149
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1150
+ className: "dsh-ui-field",
1151
+ children: [
1152
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1153
+ className: "dsh-ui-label",
1154
+ children: label
1155
+ }),
1156
+ renderInput(Input, {
1157
+ value: form[key],
1158
+ onChange: (value) => change(key, value),
1159
+ "aria-label": label,
1160
+ "aria-describedby": hintId,
1161
+ placeholder,
1162
+ className: "zhihu-input"
1163
+ }),
1164
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1165
+ className: "dsh-ui-hint",
1166
+ id: hintId,
1167
+ children: hint
1168
+ })
1169
+ ]
1170
+ }, key);
1171
+ };
1172
+ const select = (key, label, hint, options) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1173
+ className: "dsh-ui-field",
1174
+ children: [
1175
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1176
+ className: "dsh-ui-label",
1177
+ children: label
1178
+ }),
1179
+ renderSelect(Select, {
1180
+ value: form[key],
1181
+ onChange: (value) => change(key, value),
1182
+ "aria-label": label,
1183
+ options
1184
+ }, "dsh-ui-select"),
1185
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1186
+ className: "dsh-ui-hint",
1187
+ children: hint
1188
+ })
1189
+ ]
1190
+ });
1191
+ const run = (offset) => {
1192
+ let params;
1193
+ const nextForm = offset === void 0 ? form : {
1194
+ ...form,
1195
+ offset
1196
+ };
1197
+ try {
1198
+ params = buildOpenPlatformParams(activeOperation, nextForm, t);
1199
+ } catch (cause) {
1200
+ setError(cause instanceof Error ? cause.message : String(cause));
1201
+ return;
1202
+ }
1203
+ if (offset !== void 0) setForm(nextForm);
1204
+ setLoading(true);
1205
+ setError("");
1206
+ requestOpenPlatform(rpc, state, activeOperation, params, (outcome) => {
1207
+ setLoading(false);
1208
+ if ("value" in outcome) {
1209
+ setResult(outcome.value);
1210
+ setStale(false);
1211
+ } else setError(outcome.error);
1212
+ });
1213
+ };
1214
+ const nextOffset = stale ? null : openPlatformNextOffset(result, form.offset.trim());
1215
+ const paged = activeOperation === "question.answers" || activeOperation === "content.comments";
1216
+ const dated = activeOperation === "creator.account.stats" || activeOperation === "creator.content.stats";
1217
+ const options = openPlatformOptions(t);
1218
+ const title = quotaOnly ? t("官方每日额度", "Official daily quota") : t("问题与创作", "Questions and creator");
1219
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1220
+ className: "zhihu-open-platform dsh-ui-stack",
1221
+ "aria-label": title,
1222
+ children: [
1223
+ selectedOperation === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1224
+ className: "dsh-ui-heading",
1225
+ children: title
1226
+ }),
1227
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1228
+ className: "dsh-ui-help",
1229
+ children: quotaOnly ? t("查询知乎返回的官方额度,不推算余额。", "Shows the quota Zhihu reports; no balance is inferred.") : t("手动触发,不自动翻页或重试。", "Manual trigger only; no automatic paging or retry.")
1230
+ }),
1231
+ !quotaOnly && selectedOperation === void 0 && renderSelect(Select, {
1232
+ value: activeOperation,
1233
+ options,
1234
+ "aria-label": t("开放平台操作", "Open Platform operation"),
1235
+ onChange: (value) => {
1236
+ if (options.some((option) => option.value === value)) {
1237
+ reset();
1238
+ setOperation(value);
1239
+ }
1240
+ }
1241
+ }, "dsh-ui-select"),
1242
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OpenPlatformDetails, { quotaOnly }),
1243
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1244
+ className: "dsh-ui-stack",
1245
+ children: [
1246
+ activeOperation === "question.recommendations" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [field("query", t("主题关键词", "Topic keywords"), t("query · 留空按账号画像推荐", "query · leave empty to recommend from your profile")), field("count", t("推荐数量", "Number of questions"), t("count · 1–20", "count · 1–20"))] }),
1247
+ activeOperation === "question.answers" && field("questionUrl", t("知乎问题链接", "Zhihu question link"), "questionUrl", "https://www.zhihu.com/question/123"),
1248
+ [
1249
+ "content.detail",
1250
+ "content.comments",
1251
+ "creator.content.stats"
1252
+ ].includes(activeOperation) && field("contentUrl", t("本人内容链接", "Link to your content"), t("contentUrl · 回答、文章、想法或视频", "contentUrl · answer, article, pin or video"), "https://www.zhihu.com/answer/123"),
1253
+ paged && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [field("offset", t("起始位置", "Start position"), t("offset · 非负整数,从 0 开始", "offset · non-negative whole number, starting at 0")), field("limit", t("每页数量", "Page size"), t("limit · 1–50", "limit · 1–50"))] }),
1254
+ activeOperation === "content.comments" && select("order", t("评论排序", "Comment order"), "order", [
1255
+ {
1256
+ value: "score",
1257
+ label: t("热度", "Top")
1258
+ },
1259
+ {
1260
+ value: "reverse",
1261
+ label: t("最新", "Newest")
1262
+ },
1263
+ {
1264
+ value: "ascending",
1265
+ label: t("最早", "Oldest")
1266
+ }
1267
+ ]),
1268
+ activeOperation === "creator.account.stats" && select("contentType", t("内容类型", "Content type"), "contentType", [
1269
+ {
1270
+ value: "all",
1271
+ label: t("全部", "All")
1272
+ },
1273
+ {
1274
+ value: "answer",
1275
+ label: t("回答", "Answers")
1276
+ },
1277
+ {
1278
+ value: "article",
1279
+ label: t("文章", "Articles")
1280
+ },
1281
+ {
1282
+ value: "pin",
1283
+ label: t("想法", "Pins")
1284
+ },
1285
+ {
1286
+ value: "zvideo",
1287
+ label: t("视频", "Videos")
1288
+ }
1289
+ ]),
1290
+ dated && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [field("startDate", t("开始日期", "Start date"), t("startDate · YYYY-MM-DD,与结束日期成对填写", "startDate · YYYY-MM-DD, set together with the end date")), field("endDate", t("结束日期", "End date"), t("endDate · YYYY-MM-DD,不早于开始日期", "endDate · YYYY-MM-DD, not before the start date"))] }),
1291
+ activeOperation === "quota" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
1292
+ className: "zhihu-open-platform-quota dsh-ui-card dsh-ui-card--flat",
1293
+ children: [
1294
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("legend", {
1295
+ className: "dsh-ui-label",
1296
+ children: t("额度项", "Quota items")
1297
+ }),
1298
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1299
+ className: "dsh-ui-hint",
1300
+ children: t("apiIds · 不选表示全部", "apiIds · none selected means all")
1301
+ }),
1302
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Checkbox, {
1303
+ checked: form.apiIds.length === 0,
1304
+ onChange: () => change("apiIds", []),
1305
+ label: t("全部额度项", "All quota items")
1306
+ }),
1307
+ ZHIHU_QUOTA_IDS.map((id) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Checkbox, {
1308
+ checked: form.apiIds.includes(id),
1309
+ onChange: (next) => change("apiIds", next ? [...form.apiIds, id] : form.apiIds.filter((value) => value !== id)),
1310
+ label: id
1311
+ }, id))
1312
+ ]
1313
+ })
1314
+ ]
1315
+ }),
1316
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1317
+ className: "dsh-ui-actions",
1318
+ children: [
1319
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZhihuButton, {
1320
+ host: Button,
1321
+ variant: "primary",
1322
+ disabled: loading,
1323
+ onClick: () => run(),
1324
+ children: activeOperation === "quota" ? t("查询官方额度", "Check quota") : t("执行请求", "Run request")
1325
+ }),
1326
+ loading && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZhihuButton, {
1327
+ host: Button,
1328
+ onClick: () => {
1329
+ state.cancel();
1330
+ setLoading(false);
1331
+ setError(t("已取消;晚到的结果将被丢弃。", "Cancelled; a late response will be discarded."));
1332
+ },
1333
+ children: t("取消请求", "Cancel")
1334
+ }),
1335
+ nextOffset !== null && !loading && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZhihuButton, {
1336
+ host: Button,
1337
+ onClick: () => run(nextOffset),
1338
+ children: t("下一页", "Next page")
1339
+ })
1340
+ ]
1341
+ }),
1342
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1343
+ className: loading ? "dsh-ui-loading" : "dsh-ui-hint",
1344
+ role: "status",
1345
+ children: loading ? t("等待响应…", "Waiting for response…") : ""
1346
+ }),
1347
+ error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1348
+ className: "dsh-ui-error",
1349
+ role: "alert",
1350
+ children: error
1351
+ }),
1352
+ result && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1353
+ className: "dsh-ui-stack",
1354
+ children: [
1355
+ stale && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1356
+ className: "dsh-ui-banner",
1357
+ children: t("参数已变化,结果对应旧参数,请重新请求。", "Parameters changed; results are from the previous request. Run it again.")
1358
+ }),
1359
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
1360
+ className: "dsh-ui-heading",
1361
+ children: activeOperation === "quota" ? t("官方原始额度字段", "Raw quota fields") : t("返回数据", "Response data")
1362
+ }),
1363
+ result.paging?.warning && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1364
+ className: "dsh-ui-banner",
1365
+ children: result.paging.warning
1366
+ }),
1367
+ !stale && paged && result.paging?.canContinue && nextOffset === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1368
+ className: "dsh-ui-banner",
1369
+ children: t("下一页位置缺失或未前进,已停止翻页。", "The next page position is missing or did not advance; paging stopped.")
1370
+ }),
1371
+ activeOperation === "content.detail" && (typeof result.data.Body !== "string" || !result.data.Body.trim()) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1372
+ className: "dsh-ui-banner dsh-ui-banner--danger",
1373
+ children: t("响应缺少正文(Body),未取得全文。", "The response has no body (Body); full text was not retrieved.")
1374
+ }),
1375
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
1376
+ className: "zhihu-open-platform-data dsh-ui-code",
1377
+ children: JSON.stringify(result.data, null, 2)
1378
+ })
1379
+ ]
1380
+ })
1381
+ ]
1382
+ });
1383
+ }
1384
+ /** The fine print, folded: one line stays in the flow, the rest sits here. */
1385
+ function OpenPlatformDetails({ quotaOnly }) {
1386
+ const t = useT();
1387
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ZhihuDetails, {
1388
+ title: t("说明", "Details"),
1389
+ children: [
1390
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: t("只使用当前 Access Secret,不使用 OAuth,也不切换身份。", "Uses the current Access Secret only; no OAuth or identity switching.") }),
1391
+ !quotaOnly && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: t("问题回答仅提供摘要,非全文;本人创作全文、评论和数据仅限当前凭据对应的本人内容。", "Answer results are summaries, not full text. Your content, comments and stats are limited to the account behind this key.") }),
1392
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: t("业务请求可能消耗官方额度或产生费用,以知乎控制台为准;本地调用计数不是账单。", "Requests may consume official quota or incur cost; the Zhihu console is authoritative. Local counts are not billing.") }),
1393
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: t("额度查询不计入本地调用次数,不推算余额,不为缺失字段补零。", "Quota checks are not counted locally; no balance is inferred and missing fields are not zero-filled.") })
1394
+ ]
1395
+ });
1396
+ }
1397
+ //#endregion
1398
+ //#region src/client-quota.tsx
1399
+ const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
1400
+ const identityFields = /* @__PURE__ */ new Set([
1401
+ "APIID",
1402
+ "ApiId",
1403
+ "apiId",
1404
+ "api_id",
1405
+ "id",
1406
+ "Name",
1407
+ "name",
1408
+ "APIName",
1409
+ "ApiName",
1410
+ "Description",
1411
+ "description",
1412
+ "Date",
1413
+ "date",
1414
+ "Period",
1415
+ "period",
1416
+ "Unit",
1417
+ "unit",
1418
+ "Timestamp",
1419
+ "timestamp",
1420
+ "version"
1421
+ ]);
1422
+ /** Friendly name first, official field in parentheses so it still maps to the Zhihu docs. */
1423
+ function fieldLabel(field, t) {
1424
+ switch (field) {
1425
+ case "RemainingQuota": return t("剩余额度(RemainingQuota)", "Remaining (RemainingQuota)");
1426
+ case "TotalQuota": return t("总额度(TotalQuota)", "Total (TotalQuota)");
1427
+ case "UsedQuota": return t("已用额度(UsedQuota)", "Used (UsedQuota)");
1428
+ case "TotalUsed": return t("已用额度(TotalUsed)", "Used (TotalUsed)");
1429
+ default: return field;
1430
+ }
1431
+ }
1432
+ function quotaFieldRole(field) {
1433
+ if (field === "RemainingQuota") return "remaining";
1434
+ if (field === "TotalQuota") return "total";
1435
+ if (field === "UsedQuota" || field === "TotalUsed") return "used";
1436
+ return "other";
1437
+ }
1438
+ /** Only nonnegative finite, safe magnitudes are chartable; exact decimal text is retained. */
1439
+ function quotaNumericValue(value) {
1440
+ if (typeof value !== "number" && typeof value !== "string") return null;
1441
+ if (typeof value === "string") {
1442
+ const decimal = value.trim();
1443
+ if (!/^\+?\d+(?:\.\d+)?$/.test(decimal)) return null;
1444
+ const [whole, fraction = ""] = decimal.replace(/^\+/, "").split(".");
1445
+ const integer = whole.replace(/^0+(?=\d)/, "");
1446
+ const limit = String(Number.MAX_SAFE_INTEGER);
1447
+ if (integer.length > limit.length || integer.length === limit.length && (integer > limit || integer === limit && /[1-9]/.test(fraction))) return null;
1448
+ }
1449
+ const n = typeof value === "number" ? value : Number(value.trim());
1450
+ if (!Number.isFinite(n) || n < 0 || n > Number.MAX_SAFE_INTEGER) return null;
1451
+ if (typeof value === "string" && n === 0 && /[1-9]/.test(value)) return null;
1452
+ return n;
1453
+ }
1454
+ function quotaMetric(field, raw, t = translator()) {
1455
+ const value = quotaNumericValue(raw);
1456
+ let text = t("不可用", "Unavailable");
1457
+ if (value !== null) text = typeof raw === "string" ? raw.trim() : String(raw);
1458
+ else if (typeof raw === "number" && Number.isFinite(raw) || typeof raw === "string" && (/^[+-]?\d+(?:\.\d+)?$/.test(raw.trim()) || /^(?:unlimited|infinite|infinity|不限|无限|无限制)$/i.test(raw.trim()))) text = String(raw).trim() + t("(原始标记,不绘图)", " (raw marker, not charted)");
1459
+ return {
1460
+ field,
1461
+ label: fieldLabel(field, t),
1462
+ text,
1463
+ value
1464
+ };
1465
+ }
1466
+ const chartableRole = (category, role) => {
1467
+ const metric = category.metrics.find((value) => quotaFieldRole(value.field) === role);
1468
+ return metric !== void 0 && metric.value !== null;
1469
+ };
1470
+ /** The one meter worth drawing: what is left of this API's own ceiling. Both
1471
+ * figures are the server's, for this row; without both the cell stays a number
1472
+ * and never borrows a scale from a neighbouring row or field. */
1473
+ function quotaMeter(metrics) {
1474
+ const remaining = metrics.find((value) => quotaFieldRole(value.field) === "remaining" && value.value !== null);
1475
+ const total = metrics.find((value) => quotaFieldRole(value.field) === "total" && value.value !== null);
1476
+ if (!remaining || !total || !total.value) return null;
1477
+ return {
1478
+ remaining,
1479
+ total,
1480
+ share: remaining.value / total.value
1481
+ };
1482
+ }
1483
+ function quotaColumns(categories, fields, t = translator()) {
1484
+ const holders = categories.filter((category) => category.metrics.some((value) => quotaFieldRole(value.field) === "used"));
1485
+ const derivable = holders.length > 0 && holders.every((category) => chartableRole(category, "total") && chartableRole(category, "remaining"));
1486
+ const rest = fields.filter((field) => {
1487
+ const role = quotaFieldRole(field);
1488
+ if (role === "total" || role === "remaining") return false;
1489
+ if (role === "used") return !derivable;
1490
+ return true;
1491
+ }).map((field) => ({
1492
+ kind: "field",
1493
+ field,
1494
+ label: fieldLabel(field, t),
1495
+ role: quotaFieldRole(field)
1496
+ }));
1497
+ return fields.some((field) => {
1498
+ const role = quotaFieldRole(field);
1499
+ return role === "total" || role === "remaining";
1500
+ }) ? [{
1501
+ kind: "meter",
1502
+ field: "",
1503
+ label: t("剩余 / 上限", "Remaining / ceiling"),
1504
+ role: "meter"
1505
+ }, ...rest] : rest;
1506
+ }
1507
+ /** The one cell every row is read through: the fraction when the row has both
1508
+ * halves, otherwise whichever single figure the server sent, named so a lone
1509
+ * number is never read as the other half. */
1510
+ function quotaFractionCell(category, t) {
1511
+ const total = category.metrics.find((value) => quotaFieldRole(value.field) === "total");
1512
+ const remaining = category.metrics.find((value) => quotaFieldRole(value.field) === "remaining");
1513
+ const lone = (metric, role) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1514
+ className: "zhihu-quota-value",
1515
+ children: [metric.text, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1516
+ className: "zhihu-quota-of",
1517
+ children: role === "total" ? t(" 上限", " ceiling") : t(" 剩余", " remaining")
1518
+ })]
1519
+ });
1520
+ const meter = quotaMeter(category.metrics);
1521
+ if (meter) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1522
+ className: "zhihu-quota-value",
1523
+ children: [
1524
+ meter.remaining.text,
1525
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1526
+ className: "zhihu-quota-of",
1527
+ children: [" / ", meter.total.text]
1528
+ }),
1529
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1530
+ className: "zhihu-quota-track",
1531
+ "aria-hidden": "true",
1532
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1533
+ className: "zhihu-quota-fill",
1534
+ style: { width: `${Number((meter.share * 100).toFixed(2))}%` }
1535
+ })
1536
+ })
1537
+ ]
1538
+ });
1539
+ if (remaining) return lone(remaining, "remaining");
1540
+ if (total) return lone(total, "total");
1541
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1542
+ className: "zhihu-quota-value",
1543
+ children: t("不可用", "Unavailable")
1544
+ });
1545
+ }
1546
+ /** Supports direct per-API rows only. No recursive guessing, aggregation, or balance derivation. */
1547
+ function projectQuota(data, t = translator()) {
1548
+ if (!record(data)) return {
1549
+ categories: [],
1550
+ fields: []
1551
+ };
1552
+ const source = Object.hasOwn(data, "Items") ? data.Items : data;
1553
+ const rows = [];
1554
+ if (Array.isArray(source)) {
1555
+ for (const raw of source) if (record(raw)) rows.push({ raw });
1556
+ } else if (record(source)) {
1557
+ if ([
1558
+ "APIID",
1559
+ "ApiId",
1560
+ "apiId",
1561
+ "api_id",
1562
+ "id"
1563
+ ].some((key) => typeof source[key] === "string" && !!source[key].trim() || typeof source[key] === "number" && Number.isSafeInteger(source[key]))) rows.push({ raw: source });
1564
+ else for (const [key, raw] of Object.entries(source)) if (record(raw)) rows.push({
1565
+ raw,
1566
+ key
1567
+ });
1568
+ }
1569
+ const fields = /* @__PURE__ */ new Set();
1570
+ return {
1571
+ categories: rows.map(({ raw, key }, index) => {
1572
+ const identifier = [
1573
+ raw.APIID,
1574
+ raw.ApiId,
1575
+ raw.apiId,
1576
+ raw.api_id,
1577
+ raw.id,
1578
+ key
1579
+ ].find((value) => typeof value === "string" && value.trim() || typeof value === "number" && Number.isSafeInteger(value));
1580
+ const id = identifier === void 0 ? t(`类别 ${index + 1}`, `Category ${index + 1}`) : String(identifier);
1581
+ const name = [
1582
+ raw.APIName,
1583
+ raw.ApiName,
1584
+ raw.Name,
1585
+ raw.name
1586
+ ].find((value) => typeof value === "string" && value.trim());
1587
+ const metrics = Object.entries(raw).filter(([field, value]) => !identityFields.has(field) && !record(value) && !Array.isArray(value)).map(([field, value]) => {
1588
+ fields.add(field);
1589
+ return quotaMetric(field, value, t);
1590
+ });
1591
+ return {
1592
+ id,
1593
+ label: name ? `${String(name)}(${id})` : id,
1594
+ metrics
1595
+ };
1596
+ }),
1597
+ fields: [...fields]
1598
+ };
1599
+ }
1600
+ /**
1601
+ * One row per API, read as `remaining / ceiling` with the bar under it. The
1602
+ * total earns no column of its own — it is the denominator the fraction already
1603
+ * prints — and the consumed figure is the gap between the two numbers, so it
1604
+ * earns none either. An earlier version gave all three their own panel, which
1605
+ * repeated the API list three times and let a full bar mean "exhausted" in one
1606
+ * place and "untouched" in another, in the same colour and the same length.
1607
+ */
1608
+ function QuotaCharts({ data }) {
1609
+ const t = useT();
1610
+ const { categories, fields } = projectQuota(data, t);
1611
+ if (!categories.length || !fields.length) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1612
+ className: "dsh-ui-empty",
1613
+ children: t("官方额度数据不可用:响应中没有可识别的额度字段。", "Official quota data is unavailable: the response has no recognisable quota fields.")
1614
+ });
1615
+ const columns = quotaColumns(categories, fields, t);
1616
+ const metered = columns[0]?.kind === "meter";
1617
+ const api = t("接口", "API");
1618
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1619
+ className: "dsh-ui-stack",
1620
+ children: [
1621
+ metered && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1622
+ className: "dsh-ui-help",
1623
+ children: t("蓝条=该接口还剩自己上限的多少。各接口上限不同,条长不跨行比较。", "A bar is what an API has left of its own ceiling. Limits differ per API, so lengths are not comparable across rows.")
1624
+ }),
1625
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ZhihuDetails, {
1626
+ title: t("如何阅读", "How to read this"),
1627
+ children: [
1628
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: t("「剩余 / 上限」中的上限是知乎给该接口的总额度,蓝条是其中未消耗的部分。", "The ceiling in “remaining / ceiling” is the total Zhihu reports for that API, and the bar is the unconsumed part of it.") }),
1629
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: t("已消耗是上限减剩余,因此不单列;官方未返回上限时只显示剩余数字,不画条。", "Consumed is the ceiling minus the remaining, so it gets no column. With no total returned, only the remaining shows and no bar is drawn.") }),
1630
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: t("不合计不同 API,不推算已用、总额或余额。", "Different APIs are not summed; used, total and balance are never inferred.") }),
1631
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: t("统计周期以官方定义为准,不把累计数据当作今日用量。", "Periods follow the official definition; cumulative figures are not treated as today’s usage.") })
1632
+ ]
1633
+ }),
1634
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1635
+ className: "zhihu-quota-scroll",
1636
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
1637
+ className: "zhihu-quota-table",
1638
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
1639
+ scope: "col",
1640
+ className: "zhihu-quota-corner",
1641
+ children: api
1642
+ }), columns.map((column) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("th", {
1643
+ scope: "col",
1644
+ className: "zhihu-quota-head",
1645
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1646
+ className: "zhihu-quota-head-label",
1647
+ children: column.label
1648
+ }), column.kind === "meter" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1649
+ className: "zhihu-quota-head-scale",
1650
+ children: "RemainingQuota / TotalQuota"
1651
+ })]
1652
+ }, column.field || "meter"))] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: categories.map((category, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
1653
+ scope: "row",
1654
+ className: "zhihu-quota-name",
1655
+ children: category.label
1656
+ }), columns.map((column) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
1657
+ className: "zhihu-quota-cell",
1658
+ children: column.kind === "meter" ? quotaFractionCell(category, t) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1659
+ className: "zhihu-quota-value",
1660
+ children: (category.metrics.find((value) => value.field === column.field) ?? quotaMetric(column.field, void 0, t)).text
1661
+ })
1662
+ }, column.field || "meter"))] }, `${category.id}-${index}`)) })]
1663
+ })
1664
+ }),
1665
+ categories.filter((category) => !category.metrics.length).map((category, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1666
+ className: "dsh-ui-hint",
1667
+ children: t(`${category.label}:额度字段不可用。`, `${category.label}: quota fields unavailable.`)
1668
+ }, `${category.id}-${index}`))
1669
+ ]
1670
+ });
1671
+ }
1672
+ function ZhihuQuotaSection({ rpc, Button }) {
1673
+ const state = (0, react.useRef)(createZhihuClientState()).current;
1674
+ const [snapshot, setSnapshot] = (0, react.useState)(null);
1675
+ const [loading, setLoading] = (0, react.useState)(false);
1676
+ const [error, setError] = (0, react.useState)("");
1677
+ const [stale, setStale] = (0, react.useState)(false);
1678
+ const t = useT();
1679
+ const locale = useZhihuLocale();
1680
+ const refresh = (0, react.useCallback)(() => {
1681
+ setLoading(true);
1682
+ setError("");
1683
+ requestOpenPlatform(rpc, state, "quota", {}, (outcome) => {
1684
+ setLoading(false);
1685
+ if ("value" in outcome) {
1686
+ setSnapshot({
1687
+ data: outcome.value.data,
1688
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1689
+ });
1690
+ setStale(false);
1691
+ } else {
1692
+ setError(outcome.error);
1693
+ setStale(true);
1694
+ }
1695
+ });
1696
+ }, [rpc, state]);
1697
+ (0, react.useEffect)(() => {
1698
+ refresh();
1699
+ return () => state.cancel();
1700
+ }, [refresh, state]);
1701
+ const title = t("官方额度用量", "Official quota");
1702
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1703
+ className: "dsh-ui-stack",
1704
+ "aria-label": title,
1705
+ children: [
1706
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1707
+ className: "dsh-ui-heading",
1708
+ children: title
1709
+ }),
1710
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1711
+ className: "dsh-ui-help",
1712
+ children: t("打开此页时自动查询,不自动重试。", "Loads when this tab opens; never retried automatically.")
1713
+ }),
1714
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ZhihuDetails, {
1715
+ title: t("说明", "Details"),
1716
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: t("仅显示官方返回字段,不补零,不推算费用。", "Shows only the fields Zhihu returns; nothing is zero-filled or priced.") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: t("官方额度与本地每日调用计数分开,额度查询不计入本地调用次数。", "Official quota is separate from local daily counts; quota checks are not counted locally.") })]
1717
+ }),
1718
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1719
+ className: "dsh-ui-actions",
1720
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZhihuButton, {
1721
+ host: Button,
1722
+ variant: "primary",
1723
+ disabled: loading,
1724
+ onClick: refresh,
1725
+ children: t("刷新官方额度", "Refresh quota")
1726
+ }), loading && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZhihuButton, {
1727
+ host: Button,
1728
+ onClick: () => {
1729
+ state.cancel();
1730
+ setLoading(false);
1731
+ setError(t("已取消;晚到的结果将被丢弃。", "Cancelled; a late response will be discarded."));
1732
+ },
1733
+ children: t("取消请求", "Cancel")
1734
+ })]
1735
+ }),
1736
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1737
+ className: loading ? "dsh-ui-loading" : "dsh-ui-hint",
1738
+ role: "status",
1739
+ children: loading ? t("正在查询官方额度…", "Checking official quota…") : !snapshot && !error ? t("等待查询官方额度。", "Waiting to check official quota.") : ""
1740
+ }),
1741
+ error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1742
+ className: "dsh-ui-error",
1743
+ role: "alert",
1744
+ children: error
1745
+ }),
1746
+ snapshot && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1747
+ className: "dsh-ui-stack",
1748
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1749
+ className: stale ? "dsh-ui-warn" : "dsh-ui-hint",
1750
+ children: [stale ? t("旧数据(刷新未成功) · ", "Old data (refresh failed) · ") : t("最近成功查询 · ", "Last checked · "), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("time", {
1751
+ dateTime: snapshot.timestamp,
1752
+ children: new Date(snapshot.timestamp).toLocaleString(locale === "en" ? "en" : "zh-CN")
1753
+ })]
1754
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuotaCharts, { data: snapshot.data })]
1755
+ }),
1756
+ !snapshot && !loading && error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1757
+ className: "dsh-ui-empty",
1758
+ children: t("官方额度暂不可用,请稍后手动刷新。", "Official quota is unavailable. Refresh again later.")
1759
+ })
1760
+ ]
1761
+ });
1762
+ }
1763
+ //#endregion
659
1764
  //#region src/client.tsx
660
1765
  const name = "dsh-zhihu-client";
661
1766
  const inject = [
662
1767
  "slots",
663
1768
  "connection",
664
1769
  "remote",
665
- "remote.credentials"
1770
+ "remote.credentials",
1771
+ "locale"
666
1772
  ];
667
1773
  const PACKAGE_NAME = "@klarkxy/dsh-zhihu";
668
1774
  const SLOT_ORDER = 120;
@@ -706,29 +1812,85 @@ function safeUrl(url) {
706
1812
  return /^https?:\/\//i.test(url) ? url : null;
707
1813
  }
708
1814
  /** Credential absence and transport failure read differently from a plain bad request. */
709
- function failureOf(code, message) {
1815
+ function failureOf(code, message, t = translator()) {
710
1816
  if (code === "token-missing") return {
711
1817
  kind: "credential",
712
- text: "未配置知乎 Access Secret 或凭证不可用,请到「设置」页完成配置。"
1818
+ text: t("未配置知乎 Access Secret 或凭证不可用,请到「设置」页配置。", "Zhihu Access Secret is missing or unavailable. Set it on the Settings tab.")
713
1819
  };
714
1820
  return {
715
1821
  kind: "request",
716
- text: `请求失败:${message}`
1822
+ text: t(`请求失败:${message}`, `Request failed: ${message}`)
717
1823
  };
718
1824
  }
719
- function networkFailure(cause) {
1825
+ function networkFailure(cause, t = translator()) {
1826
+ const detail = cause instanceof Error ? cause.message : String(cause);
720
1827
  return {
721
1828
  kind: "network",
722
- text: `网络或连接失败:${cause instanceof Error ? cause.message : String(cause)}`
1829
+ text: t(`网络或连接失败:${detail}`, `Network or connection failed: ${detail}`)
723
1830
  };
724
1831
  }
725
- const MODE_LABEL = {
726
- search: "站内搜索",
727
- global: "全网搜索",
728
- hot: "知乎热榜",
729
- knowledge: "知识库检索",
730
- ask: "直答"
731
- };
1832
+ /**
1833
+ * One external-link behaviour for the whole page: an http(s) anchor that the
1834
+ * desktop bridge opens in the system browser when present, and a plain new tab
1835
+ * otherwise. Anything that is not http(s) degrades to text.
1836
+ */
1837
+ function ExternalLink(props) {
1838
+ const url = safeUrl(props.url);
1839
+ if (!url) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1840
+ className: props.className,
1841
+ children: props.children
1842
+ });
1843
+ const open = (event) => {
1844
+ const bridge = globalThis.dshWindow;
1845
+ if (bridge?.openExternal) {
1846
+ event.preventDefault();
1847
+ bridge.openExternal(url);
1848
+ }
1849
+ };
1850
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
1851
+ className: props.className ?? "zhihu-link",
1852
+ href: url,
1853
+ target: "_blank",
1854
+ rel: "noreferrer noopener",
1855
+ onClick: open,
1856
+ children: props.children
1857
+ });
1858
+ }
1859
+ const MODES = [
1860
+ "search",
1861
+ "global",
1862
+ "hot",
1863
+ "knowledge",
1864
+ "ask"
1865
+ ];
1866
+ function modeLabel(mode, t) {
1867
+ switch (mode) {
1868
+ case "search": return t("站内搜索", "Zhihu search");
1869
+ case "global": return t("全网搜索", "Web search");
1870
+ case "hot": return t("知乎热榜", "Trending");
1871
+ case "knowledge": return t("知识库检索", "Knowledge base search");
1872
+ case "ask": return t("直答", "Direct answer");
1873
+ }
1874
+ }
1875
+ function testOptions(t) {
1876
+ return [...MODES.map((value) => ({
1877
+ value,
1878
+ label: modeLabel(value, t)
1879
+ })), ...openPlatformOptions(t).filter((option) => option.value !== "quota")];
1880
+ }
1881
+ const TEST_OPTIONS = testOptions(translator("zh"));
1882
+ function ZhihuTestSelector({ value, onChange, Select }) {
1883
+ const t = useT();
1884
+ const options = testOptions(t);
1885
+ return renderSelect(Select, {
1886
+ "aria-label": t("测试功能", "Feature to test"),
1887
+ value,
1888
+ options,
1889
+ onChange: (next) => {
1890
+ if (options.some((option) => option.value === next)) onChange(next);
1891
+ }
1892
+ }, "dsh-ui-select");
1893
+ }
732
1894
  const MODE_ENDPOINT = {
733
1895
  search: "search",
734
1896
  global: "global.search",
@@ -739,29 +1901,35 @@ const MODE_ENDPOINT = {
739
1901
  const ASK_MODELS = [
740
1902
  {
741
1903
  value: "zhida-thinking-1p5",
742
- label: "思考"
1904
+ zh: "思考",
1905
+ en: "Thinking"
743
1906
  },
744
1907
  {
745
1908
  value: "zhida-fast-1p5",
746
- label: "快速"
1909
+ zh: "快速",
1910
+ en: "Fast"
747
1911
  },
748
1912
  {
749
1913
  value: "zhida-agent",
750
- label: "智能体"
1914
+ zh: "智能体",
1915
+ en: "Agent"
751
1916
  }
752
1917
  ];
753
1918
  const SCOPE_OPTIONS = [
754
1919
  {
755
1920
  value: "public",
756
- label: "公开库"
1921
+ zh: "公开库",
1922
+ en: "Public"
757
1923
  },
758
1924
  {
759
1925
  value: "personal",
760
- label: "个人库"
1926
+ zh: "个人库",
1927
+ en: "Personal"
761
1928
  },
762
1929
  {
763
1930
  value: "subscription",
764
- label: "订阅库"
1931
+ zh: "订阅库",
1932
+ en: "Subscribed"
765
1933
  }
766
1934
  ];
767
1935
  function objectItems(value) {
@@ -793,7 +1961,7 @@ function parseOutcome(mode, value) {
793
1961
  if (mode === "hot") return {
794
1962
  mode: "hot",
795
1963
  items: items.map((item) => ({
796
- title: text(item.title, "(无标题)"),
1964
+ title: text(item.title, "—"),
797
1965
  url: text(item.url),
798
1966
  summary: text(item.summary)
799
1967
  }))
@@ -801,7 +1969,7 @@ function parseOutcome(mode, value) {
801
1969
  if (mode === "knowledge") return {
802
1970
  mode: "knowledge",
803
1971
  items: items.map((item) => ({
804
- docName: text(item.docName, "(未命名文档)"),
1972
+ docName: text(item.docName, "—"),
805
1973
  originUrl: text(item.originUrl),
806
1974
  snippets: Array.isArray(item.snippets) ? item.snippets.filter((s) => typeof s === "string") : []
807
1975
  }))
@@ -810,67 +1978,68 @@ function parseOutcome(mode, value) {
810
1978
  mode,
811
1979
  emptyReason: typeof value.emptyReason === "string" ? value.emptyReason : void 0,
812
1980
  items: items.map((item) => ({
813
- title: text(item.title, "(无标题)"),
814
- type: text(item.type, "内容"),
1981
+ title: text(item.title, "—"),
1982
+ type: text(item.type),
815
1983
  url: text(item.url),
816
1984
  summary: text(item.summary),
817
1985
  votes: typeof item.votes === "number" ? item.votes : 0,
818
1986
  comments: typeof item.comments === "number" ? item.comments : 0,
819
- author: text(item.author, "匿名"),
1987
+ author: text(item.author),
820
1988
  editTime: text(item.editTime)
821
1989
  }))
822
1990
  };
823
1991
  }
824
1992
  function LinkOrText(props) {
825
- const url = safeUrl(props.url);
826
- if (!url) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1993
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExternalLink, {
1994
+ url: props.url,
827
1995
  className: props.className,
828
1996
  children: props.label
829
1997
  });
830
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
831
- className: props.className ?? "zhihu-link",
832
- href: url,
833
- target: "_blank",
834
- rel: "noreferrer",
835
- children: props.label
836
- });
837
1998
  }
838
1999
  function OutcomeView(props) {
839
2000
  const { outcome, stale } = props;
2001
+ const t = useT();
2002
+ const staleBanner = stale ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2003
+ className: "dsh-ui-banner",
2004
+ role: "status",
2005
+ children: t("查询已变化,结果对应旧查询,请重新搜索。", "The query changed; results are from the previous query. Search again.")
2006
+ }) : null;
840
2007
  let body = null;
841
2008
  let summary = "";
842
2009
  if (outcome.mode === "ask") {
843
- summary = `直答(${outcome.answer.model || "默认模型"})`;
2010
+ summary = t(`直答(${outcome.answer.model || "默认模型"})`, `Direct answer (${outcome.answer.model || "default model"})`);
844
2011
  body = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [outcome.answer.reasoning ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
845
- className: "zhihu-ask-reasoning",
846
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: "思考过程" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: outcome.answer.reasoning })]
2012
+ className: "zhihu-ask-reasoning dsh-ui-card dsh-ui-card--flat",
2013
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", {
2014
+ className: "dsh-ui-heading",
2015
+ children: t("思考过程", "Reasoning")
2016
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2017
+ className: "dsh-ui-help",
2018
+ children: outcome.answer.reasoning
2019
+ })]
847
2020
  }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
848
- className: "zhihu-ask-content",
2021
+ className: "zhihu-ask-content dsh-ui-wrap",
849
2022
  children: outcome.answer.content
850
2023
  })] });
851
2024
  } else if (outcome.items.length === 0) {
852
2025
  const reason = outcome.mode === "search" || outcome.mode === "global" ? outcome.emptyReason : void 0;
853
2026
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
854
2027
  "data-testid": "zhihu-results",
855
- className: "zhihu-results",
856
- children: [stale ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
857
- className: "zhihu-stale",
858
- role: "status",
859
- children: "查询已变化,以下内容对应旧查询,请重新搜索。"
860
- }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
861
- className: "zhihu-results-summary",
862
- children: `未找到相关结果${reason ? `(${reason})` : ""}。`
2028
+ className: "dsh-ui-stack",
2029
+ children: [staleBanner, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2030
+ className: "dsh-ui-empty",
2031
+ children: t(`未找到相关结果${reason ? `(${reason})` : ""}。`, `No results${reason ? ` (${reason})` : ""}.`)
863
2032
  })]
864
2033
  });
865
2034
  } else if (outcome.mode === "knowledge") {
866
- summary = `知识库检索共 ${outcome.items.length} 条`;
2035
+ summary = t(`知识库检索共 ${outcome.items.length} 条`, `${outcome.items.length} knowledge base results`);
867
2036
  body = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
868
- className: "zhihu-result-list",
2037
+ className: "dsh-ui-list",
869
2038
  children: outcome.items.map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
870
- className: "zhihu-result-item",
2039
+ className: "dsh-ui-card dsh-ui-card--flat dsh-ui-list-row",
871
2040
  children: [
872
2041
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
873
- className: "zhihu-result-title",
2042
+ className: "dsh-ui-list-name",
874
2043
  children: item.docName
875
2044
  }),
876
2045
  item.originUrl && safeUrl(item.originUrl) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LinkOrText, {
@@ -878,46 +2047,46 @@ function OutcomeView(props) {
878
2047
  label: item.originUrl
879
2048
  }) : null,
880
2049
  item.snippets.map((snippet, snippetIndex) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
881
- className: "zhihu-result-snippet",
2050
+ className: "zhihu-result-snippet dsh-ui-help",
882
2051
  children: snippet
883
2052
  }, snippetIndex))
884
2053
  ]
885
2054
  }, `${item.docName}:${index}`))
886
2055
  });
887
2056
  } else if (outcome.mode === "hot") {
888
- summary = `知乎热榜共 ${outcome.items.length} 条`;
2057
+ summary = t(`知乎热榜共 ${outcome.items.length} 条`, `${outcome.items.length} trending items`);
889
2058
  body = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
890
- className: "zhihu-result-list",
2059
+ className: "dsh-ui-list",
891
2060
  children: outcome.items.map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
892
- className: "zhihu-result-item",
2061
+ className: "dsh-ui-card dsh-ui-card--flat dsh-ui-list-row",
893
2062
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LinkOrText, {
894
2063
  url: item.url,
895
2064
  label: item.title,
896
- className: "zhihu-result-title zhihu-link"
2065
+ className: "dsh-ui-list-name zhihu-link"
897
2066
  }), item.summary ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
898
- className: "zhihu-result-summary",
2067
+ className: "dsh-ui-meta",
899
2068
  children: item.summary
900
2069
  }) : null]
901
2070
  }, `${index}`))
902
2071
  });
903
2072
  } else {
904
- summary = `${MODE_LABEL[outcome.mode]}共 ${outcome.items.length} 条`;
2073
+ summary = t(`${modeLabel(outcome.mode, t)}共 ${outcome.items.length} 条`, `${modeLabel(outcome.mode, t)}: ${outcome.items.length} results`);
905
2074
  body = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
906
- className: "zhihu-result-list",
2075
+ className: "dsh-ui-list",
907
2076
  children: outcome.items.map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
908
- className: "zhihu-result-item",
2077
+ className: "dsh-ui-card dsh-ui-card--flat dsh-ui-list-row",
909
2078
  children: [
910
2079
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LinkOrText, {
911
2080
  url: item.url,
912
2081
  label: item.title,
913
- className: "zhihu-result-title zhihu-link"
2082
+ className: "dsh-ui-list-name zhihu-link"
914
2083
  }),
915
2084
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
916
- className: "zhihu-result-meta",
917
- children: `类型:${item.type} 作者:${item.author} 赞同 ${item.votes} 评论 ${item.comments}${item.editTime ? ` 时间:${item.editTime}` : ""}`
2085
+ className: "dsh-ui-list-desc",
2086
+ children: t(`${item.type || "内容"} · ${item.author || "匿名"} · 赞同 ${item.votes} · 评论 ${item.comments}${item.editTime ? ` · ${item.editTime}` : ""}`, `${item.type || "Content"} · ${item.author || "Anonymous"} · ${item.votes} upvotes · ${item.comments} comments${item.editTime ? ` · ${item.editTime}` : ""}`)
918
2087
  }),
919
2088
  item.summary ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
920
- className: "zhihu-result-summary",
2089
+ className: "dsh-ui-meta",
921
2090
  children: item.summary
922
2091
  }) : null
923
2092
  ]
@@ -926,16 +2095,11 @@ function OutcomeView(props) {
926
2095
  }
927
2096
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
928
2097
  "data-testid": "zhihu-results",
929
- className: "zhihu-results",
2098
+ className: "dsh-ui-stack",
930
2099
  children: [
931
- stale ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
932
- className: "zhihu-stale",
933
- role: "status",
934
- children: "查询已变化,以下内容对应旧查询,请重新搜索。"
935
- }) : null,
2100
+ staleBanner,
936
2101
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
937
- className: "zhihu-results-summary",
938
- "aria-live": "polite",
2102
+ className: "dsh-ui-heading",
939
2103
  children: summary
940
2104
  }),
941
2105
  body
@@ -967,32 +2131,56 @@ function useAlive() {
967
2131
  }, []);
968
2132
  return alive;
969
2133
  }
970
- function CapabilitiesNote() {
971
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
972
- className: "zhihu-guide",
973
- "data-testid": "zhihu-capabilities",
974
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: "可用能力" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
975
- className: "zhihu-hint",
976
- children: "站内搜索、全网搜索、热榜、直答和知识库检索。全网搜索可在「网络搜索」中启用。"
977
- })]
2134
+ /**
2135
+ * Collapsible detail block. The primitive owns the row chrome; the body keeps
2136
+ * the contract's help tier so a long explanation never sits in the page flow.
2137
+ */
2138
+ function CapabilitiesNote(props) {
2139
+ const t = useT();
2140
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ZhihuDetails, {
2141
+ title: t("可用能力", "Capabilities"),
2142
+ defaultOpen: props.defaultOpen,
2143
+ testId: "zhihu-capabilities",
2144
+ children: [
2145
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: t("测试:站内搜索、全网搜索、热榜、直答、知识库检索与问题创作查询。", "Test: Zhihu search, web search, trending, direct answers, knowledge base search, question and creator queries.") }),
2146
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: t("问题与创作:问题推荐、回答摘要、本人已发布内容及评论、账号与单篇创作数据。", "Questions and creator: question recommendations, answer summaries, your published content and comments, account and per-post stats.") }),
2147
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: t("统计:本地记录的每日调用、失败与结果条数。", "Stats: daily calls, failures and result counts recorded locally.") }),
2148
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: t("官方用量:知乎返回的官方剩余额度。", "Official quota: the remaining quota Zhihu reports.") }),
2149
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: t("知识库:读取列表与上传参考资料,文件进入知乎云端。", "Knowledge base: list bases and upload references; files go to Zhihu cloud.") }),
2150
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: t("全部能力共用此 Access Secret,不支持 OAuth 身份切换。", "Every feature shares this Access Secret; OAuth identity switching is not supported.") }),
2151
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
2152
+ t("模型工具需在 Agent 中加载 ", "For model tools, load "),
2153
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: "@klarkxy/dsh-zhihu/tools" }),
2154
+ t(",仅配置密钥不会启用工具。", " in the Agent; a key alone does not enable them.")
2155
+ ] }),
2156
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: t("请求手动触发(官方用量在打开页签时加载),不会自动重试。", "Requests run only when you trigger them (official quota loads when its tab opens) and are never retried automatically.") })
2157
+ ]
978
2158
  });
979
2159
  }
980
2160
  function SettingsShell(props) {
2161
+ const t = useT();
981
2162
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
982
- className: "zhihu-settings",
2163
+ className: "dsh-ui-stack",
983
2164
  "data-testid": "zhihu-settings",
984
- "aria-label": "知乎凭证设置",
985
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CapabilitiesNote, {}), props.children]
2165
+ "aria-label": t("知乎凭证设置", "Zhihu credential settings"),
2166
+ children: [props.children, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CapabilitiesNote, {})]
986
2167
  });
987
2168
  }
988
2169
  function SettingsSection(props) {
989
2170
  const { credentials } = props;
2171
+ const t = useT();
990
2172
  const alive = useAlive();
991
2173
  const [state, setState] = (0, react.useState)({ status: "loading" });
992
2174
  const [keyDraft, setKeyDraft] = (0, react.useState)("");
993
- const [busy, setBusy] = (0, react.useState)(false);
2175
+ const [pending, setPending] = (0, react.useState)(null);
2176
+ const [confirmClear, setConfirmClear] = (0, react.useState)(false);
994
2177
  const [failure, setFailure] = (0, react.useState)(void 0);
995
2178
  const [note, setNote] = (0, react.useState)(void 0);
2179
+ const busy = pending !== null;
2180
+ const editDraft = (value) => {
2181
+ setKeyDraft(value);
2182
+ setNote(void 0);
2183
+ };
996
2184
  const load = (0, react.useCallback)(async () => {
997
2185
  setState((current) => current.status === "ready" ? current : { status: "loading" });
998
2186
  try {
@@ -1021,32 +2209,35 @@ function SettingsSection(props) {
1021
2209
  load();
1022
2210
  }, [load]);
1023
2211
  if (state.status === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsShell, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1024
- className: "zhihu-status",
2212
+ className: "dsh-ui-loading",
1025
2213
  role: "status",
1026
- children: [activityDots(), "正在读取凭证状态…"]
2214
+ children: [activityDots(), t("正在读取凭证状态…", "Loading credential status…")]
1027
2215
  }) });
1028
- if (state.status === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsShell, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1029
- className: "zhihu-error",
2216
+ if (state.status === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SettingsShell, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2217
+ className: "dsh-ui-error",
1030
2218
  role: "alert",
1031
- children: [`读取失败:${state.error} `, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZhihuButton, {
2219
+ children: t(`读取失败:${state.error}`, `Could not load: ${state.error}`)
2220
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2221
+ className: "dsh-ui-actions",
2222
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZhihuButton, {
1032
2223
  host: props.Button,
1033
- className: "zhihu-button",
1034
2224
  onClick: () => void load(),
1035
- children: "重试"
1036
- })]
1037
- }) });
2225
+ children: t("重试", "Retry")
2226
+ })
2227
+ })] });
1038
2228
  const credential = state.credential;
1039
2229
  const keyLocked = credential?.writable === false;
1040
2230
  const configured = credential?.configured === true;
1041
2231
  const draftFailure = keyFailure(keyDraft);
1042
2232
  const keyValue = keyDraft.trim();
1043
- const statusText = keyLocked ? "由环境变量提供(只读)" : configured ? `已保存${credential?.source ? `(来源:${credential.source})` : ""}` : "未配置";
1044
- const dotClass = keyLocked ? "zhihu-dot zhihu-dot-locked" : configured ? "zhihu-dot zhihu-dot-configured" : "zhihu-dot zhihu-dot-missing";
1045
- const placeholder = keyLocked ? "由环境变量提供,无法在界面修改" : configured ? "已保存,输入新密钥可覆盖" : "粘贴知乎开放平台 Access Secret";
2233
+ const statusText = keyLocked ? t("由环境变量提供(只读)", "Provided by an environment variable (read-only)") : configured ? t(`已保存${credential?.source ? `(来源:${credential.source})` : ""}`, `Saved${credential?.source ? ` (source: ${credential.source})` : ""}`) : t("未配置", "Not set");
2234
+ const dotState = keyLocked ? "warning" : configured ? "done" : "idle";
2235
+ const placeholder = keyLocked ? t("由环境变量提供,无法在界面修改", "Set by an environment variable; cannot be edited here") : configured ? t("已保存,输入新密钥可覆盖", "Saved. Enter a new key to replace it") : t("粘贴知乎开放平台 Access Secret", "Paste your Zhihu Open Platform Access Secret");
1046
2236
  const save = async () => {
1047
2237
  if (keyLocked || draftFailure !== void 0 || keyValue.length === 0 || busy) return;
1048
- setBusy(true);
2238
+ setPending("save");
1049
2239
  setFailure(void 0);
2240
+ setNote(void 0);
1050
2241
  try {
1051
2242
  const result = await credentials.set({
1052
2243
  ref: ZHIHU_CREDENTIAL_REF,
@@ -1058,19 +2249,21 @@ function SettingsSection(props) {
1058
2249
  return;
1059
2250
  }
1060
2251
  setKeyDraft("");
1061
- setNote("已保存。");
2252
+ setNote(t("已保存。", "Saved."));
1062
2253
  await load();
1063
2254
  } catch (cause) {
1064
2255
  if (!alive.current) return;
1065
2256
  setFailure(cause instanceof Error ? cause.message : String(cause));
1066
2257
  } finally {
1067
- if (alive.current) setBusy(false);
2258
+ if (alive.current) setPending(null);
1068
2259
  }
1069
2260
  };
1070
2261
  const clear = async () => {
2262
+ setConfirmClear(false);
1071
2263
  if (keyLocked || !configured || busy) return;
1072
- setBusy(true);
2264
+ setPending("clear");
1073
2265
  setFailure(void 0);
2266
+ setNote(void 0);
1074
2267
  try {
1075
2268
  const result = await credentials.unset({ ref: ZHIHU_CREDENTIAL_REF });
1076
2269
  if (!alive.current) return;
@@ -1079,51 +2272,35 @@ function SettingsSection(props) {
1079
2272
  return;
1080
2273
  }
1081
2274
  setKeyDraft("");
1082
- setNote("已清除。");
2275
+ setNote(t("已清除。", "Cleared."));
1083
2276
  await load();
1084
2277
  } catch (cause) {
1085
2278
  if (!alive.current) return;
1086
2279
  setFailure(cause instanceof Error ? cause.message : String(cause));
1087
2280
  } finally {
1088
- if (alive.current) setBusy(false);
2281
+ if (alive.current) setPending(null);
1089
2282
  }
1090
2283
  };
1091
2284
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SettingsShell, { children: [
1092
2285
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dl", {
1093
- className: "zhihu-status-grid",
1094
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", {
1095
- className: "zhihu-status-label",
1096
- children: "状态"
1097
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dd", {
1098
- className: "zhihu-status-value",
1099
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1100
- className: dotClass,
1101
- "aria-hidden": true
1102
- }), statusText]
2286
+ className: "dsh-ui-readonly",
2287
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("状态", "Status") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dd", {
2288
+ className: "dsh-ui-row",
2289
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: dotState }), statusText]
1103
2290
  })]
1104
2291
  }),
1105
2292
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1106
- className: "zhihu-field",
2293
+ className: "dsh-ui-field",
1107
2294
  children: [
1108
2295
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1109
- className: "zhihu-field-heading",
2296
+ className: "dsh-ui-label-row",
1110
2297
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1111
- className: "zhihu-field-label",
2298
+ className: "dsh-ui-label",
1112
2299
  id: "zhihu-access-secret-label",
1113
2300
  children: "Access Secret"
1114
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
1115
- className: "zhihu-link",
1116
- href: ZHIHU_CONSOLE_URL,
1117
- target: "_blank",
1118
- rel: "noreferrer noopener",
1119
- onClick: (event) => {
1120
- const bridge = globalThis.dshWindow;
1121
- if (bridge?.openExternal) {
1122
- event.preventDefault();
1123
- bridge.openExternal(ZHIHU_CONSOLE_URL);
1124
- }
1125
- },
1126
- children: "获取密钥"
2301
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExternalLink, {
2302
+ url: ZHIHU_CONSOLE_URL,
2303
+ children: t("获取密钥", "Get a key")
1127
2304
  })]
1128
2305
  }),
1129
2306
  renderInput(props.Input, {
@@ -1133,42 +2310,66 @@ function SettingsSection(props) {
1133
2310
  placeholder,
1134
2311
  disabled: busy || keyLocked,
1135
2312
  "aria-label": "Access Secret",
1136
- onChange: setKeyDraft
2313
+ onChange: editDraft
1137
2314
  }),
1138
2315
  draftFailure === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1139
- className: "zhihu-warning",
2316
+ className: "dsh-ui-error",
1140
2317
  role: "alert",
1141
- children: draftFailure === "blank" ? "密钥不能只包含空白字符。" : "密钥含有非法字符(应为可打印 ASCII,且不是 ENV 赋值行)。"
2318
+ children: draftFailure === "blank" ? t("密钥不能只包含空白字符。", "The key cannot be only whitespace.") : t("密钥含非法字符(应为可打印 ASCII,且不是 ENV 赋值行)。", "The key has invalid characters (use printable ASCII, not an ENV assignment line).")
1142
2319
  })
1143
2320
  ]
1144
2321
  }),
1145
2322
  failure !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1146
- className: "zhihu-warning",
2323
+ className: "dsh-ui-error",
1147
2324
  role: "alert",
1148
2325
  children: failure
1149
2326
  }) : null,
1150
2327
  note !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1151
- className: "zhihu-saved",
2328
+ className: "dsh-ui-notice",
1152
2329
  role: "status",
1153
2330
  children: note
1154
2331
  }) : null,
1155
2332
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1156
- className: "zhihu-row",
2333
+ className: "dsh-ui-actions",
1157
2334
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZhihuButton, {
1158
2335
  host: props.Button,
1159
2336
  variant: "danger",
1160
- className: "zhihu-button zhihu-button-danger",
2337
+ className: "zhihu-button-danger",
1161
2338
  disabled: busy || keyLocked || !configured,
1162
- onClick: () => void clear(),
1163
- children: busy ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [activityDots(), "处理中…"] }) : "清除"
2339
+ onClick: () => setConfirmClear(true),
2340
+ children: pending === "clear" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [activityDots(), t("清除中…", "Clearing…")] }) : t("清除", "Clear")
1164
2341
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZhihuButton, {
1165
2342
  host: props.Button,
1166
2343
  variant: "primary",
1167
- className: "zhihu-button zhihu-button-primary",
1168
2344
  disabled: busy || keyLocked || keyValue.length === 0 || draftFailure !== void 0,
1169
2345
  onClick: () => void save(),
1170
- children: busy ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [activityDots(), "保存中…"] }) : "保存"
2346
+ children: pending === "save" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [activityDots(), t("保存中…", "Saving…")] }) : t("保存", "Save")
1171
2347
  })]
2348
+ }),
2349
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
2350
+ open: confirmClear,
2351
+ onClose: () => setConfirmClear(false),
2352
+ title: t("清除 Access Secret?", "Clear Access Secret?"),
2353
+ closeLabel: t("关闭", "Close"),
2354
+ className: "zhihu-panel",
2355
+ footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2356
+ className: "dsh-ui-actions",
2357
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZhihuButton, {
2358
+ host: props.Button,
2359
+ onClick: () => setConfirmClear(false),
2360
+ children: t("取消", "Cancel")
2361
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZhihuButton, {
2362
+ host: props.Button,
2363
+ variant: "danger",
2364
+ className: "zhihu-button-danger",
2365
+ onClick: () => void clear(),
2366
+ children: t("清除", "Clear")
2367
+ })]
2368
+ }),
2369
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2370
+ className: "dsh-ui-help",
2371
+ children: t("清除后,搜索、工具和知识库请求会失败,直到重新保存密钥。", "Search, tools and knowledge base requests will fail until you save a key again.")
2372
+ })
1172
2373
  })
1173
2374
  ] });
1174
2375
  }
@@ -1259,7 +2460,26 @@ const CHART_PAD = {
1259
2460
  bottom: 24,
1260
2461
  left: 36
1261
2462
  };
2463
+ /** Indexes whose bar prints its count: the first maximum and the latest active day. */
2464
+ function usageValueLabels(days) {
2465
+ const chosen = /* @__PURE__ */ new Set();
2466
+ let max = -1;
2467
+ let maxIndex = -1;
2468
+ let latest = -1;
2469
+ days.forEach((day, index) => {
2470
+ if (day.calls <= 0) return;
2471
+ if (day.calls > max) {
2472
+ max = day.calls;
2473
+ maxIndex = index;
2474
+ }
2475
+ latest = index;
2476
+ });
2477
+ if (maxIndex >= 0) chosen.add(maxIndex);
2478
+ if (latest >= 0) chosen.add(latest);
2479
+ return chosen;
2480
+ }
1262
2481
  function UsageChart(props) {
2482
+ const t = useT();
1263
2483
  const days = props.days;
1264
2484
  const maxCalls = Math.max(1, ...days.map((day) => day.calls));
1265
2485
  const plotLeft = CHART_PAD.left;
@@ -1287,12 +2507,13 @@ function UsageChart(props) {
1287
2507
  children: String(value)
1288
2508
  })] }, `y-${value}`);
1289
2509
  });
2510
+ const labelled = usageValueLabels(days);
1290
2511
  const bars = days.map((day, index) => {
1291
2512
  const x = plotLeft + index * slot + (slot - barWidth) / 2;
1292
2513
  const okCalls = Math.max(0, day.calls - day.failures);
1293
2514
  const failHeight = day.failures / maxCalls * plotHeight;
1294
2515
  const okHeight = okCalls / maxCalls * plotHeight;
1295
- const label = `${day.date}:调用 ${day.calls} 次,成功 ${okCalls} 次,失败 ${day.failures} 次,结果 ${day.results} 条`;
2516
+ const label = t(`${day.date}:调用 ${day.calls} 次,成功 ${okCalls} 次,失败 ${day.failures} 次,结果 ${day.results} 条`, `${day.date}: ${day.calls} calls, ${okCalls} succeeded, ${day.failures} failed, ${day.results} results`);
1296
2517
  const parts = [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
1297
2518
  className: "zhihu-chart-hit",
1298
2519
  x: plotLeft + index * slot,
@@ -1314,7 +2535,7 @@ function UsageChart(props) {
1314
2535
  width: barWidth,
1315
2536
  height: failHeight
1316
2537
  }, "fail"));
1317
- if (day.calls > 0) parts.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
2538
+ if (day.calls > 0 && labelled.has(index)) parts.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
1318
2539
  className: "zhihu-chart-value",
1319
2540
  x: x + barWidth / 2,
1320
2541
  y: plotBottom - okHeight - failHeight - 4,
@@ -1341,7 +2562,7 @@ function UsageChart(props) {
1341
2562
  className: "zhihu-chart",
1342
2563
  viewBox: `0 0 ${CHART_WIDTH} ${CHART_HEIGHT}`,
1343
2564
  role: "img",
1344
- "aria-label": `近 ${USAGE_DAYS} 天知乎工具调用 ${props.totals.calls} 次,失败 ${props.totals.failures} 次,结果 ${props.totals.results} 条`,
2565
+ "aria-label": t(`近 ${USAGE_DAYS} 天知乎工具调用 ${props.totals.calls} 次,失败 ${props.totals.failures} 次,结果 ${props.totals.results} 条`, `Last ${USAGE_DAYS} days: ${props.totals.calls} Zhihu calls, ${props.totals.failures} failed, ${props.totals.results} results`),
1345
2566
  children: [
1346
2567
  grid,
1347
2568
  bars,
@@ -1351,6 +2572,7 @@ function UsageChart(props) {
1351
2572
  }
1352
2573
  function UsageSection(props) {
1353
2574
  const { rpc } = props;
2575
+ const t = useT();
1354
2576
  const gateRef = (0, react.useRef)(null);
1355
2577
  if (!gateRef.current) gateRef.current = createZhihuClientState();
1356
2578
  const gate = gateRef.current;
@@ -1373,7 +2595,7 @@ function UsageSection(props) {
1373
2595
  if (!isUsageSummary(result.value)) {
1374
2596
  setState({
1375
2597
  status: "error",
1376
- error: "响应格式与契约不符。"
2598
+ error: "invalid response shape"
1377
2599
  });
1378
2600
  return;
1379
2601
  }
@@ -1392,87 +2614,106 @@ function UsageSection(props) {
1392
2614
  (0, react.useEffect)(() => {
1393
2615
  load();
1394
2616
  }, [load]);
2617
+ const sectionLabel = t("知乎调用用量", "Zhihu call usage");
1395
2618
  if (state.status === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
1396
2619
  "data-testid": "zhihu-usage",
1397
- "aria-label": "知乎调用用量",
2620
+ "aria-label": sectionLabel,
1398
2621
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1399
- className: "zhihu-status",
2622
+ className: "dsh-ui-loading",
1400
2623
  role: "status",
1401
- children: [activityDots(), "正在读取用量…"]
2624
+ children: [activityDots(), t("正在读取用量…", "Loading usage…")]
1402
2625
  })
1403
2626
  });
1404
- if (state.status === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
2627
+ if (state.status === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2628
+ className: "dsh-ui-stack",
1405
2629
  "data-testid": "zhihu-usage",
1406
- "aria-label": "知乎调用用量",
1407
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1408
- className: "zhihu-error",
2630
+ "aria-label": sectionLabel,
2631
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2632
+ className: "dsh-ui-error",
1409
2633
  role: "alert",
1410
- children: [`读取失败:${state.error} `, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZhihuButton, {
2634
+ children: t(`读取失败:${state.error}`, `Could not load: ${state.error}`)
2635
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2636
+ className: "dsh-ui-actions",
2637
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZhihuButton, {
1411
2638
  host: props.Button,
1412
- className: "zhihu-button",
1413
2639
  onClick: () => void load(),
1414
- children: "重试"
1415
- })]
1416
- })
2640
+ children: t("重试", "Retry")
2641
+ })
2642
+ })]
1417
2643
  });
1418
2644
  const totals = summarizeUsage(state.days, localDayKey());
1419
2645
  const hasAny = totals.calls > 0;
2646
+ const times = t("次", "");
1420
2647
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1421
- className: "zhihu-usage",
2648
+ className: "dsh-ui-stack",
1422
2649
  "data-testid": "zhihu-usage",
1423
- "aria-label": "知乎调用用量",
2650
+ "aria-label": sectionLabel,
1424
2651
  children: [
1425
2652
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1426
- className: "zhihu-usage-intro",
1427
- children: "本机搜索、问答、热榜和知识库的调用次数,不是知乎官方配额或费用。"
2653
+ className: "dsh-ui-help",
2654
+ children: t("本地调用计数,非知乎官方额度或费用。", "Calls recorded locally; not Zhihu official quota or billing.")
2655
+ }),
2656
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2657
+ className: "dsh-ui-actions",
2658
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZhihuButton, {
2659
+ host: props.Button,
2660
+ onClick: () => void load(),
2661
+ children: t("刷新统计", "Refresh")
2662
+ })
1428
2663
  }),
1429
2664
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1430
2665
  className: "zhihu-usage-cards",
1431
2666
  children: [
1432
2667
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageCard, {
1433
- label: "今日调用",
2668
+ label: t("今日调用", "Today"),
1434
2669
  value: totals.todayCalls,
1435
- unit: "次"
2670
+ unit: times
1436
2671
  }),
1437
2672
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageCard, {
1438
- label: `近 ${USAGE_DAYS} 天`,
2673
+ label: t(`近 ${USAGE_DAYS} 天`, `Last ${USAGE_DAYS} days`),
1439
2674
  value: totals.calls,
1440
- unit: "次"
2675
+ unit: times
1441
2676
  }),
1442
2677
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageCard, {
1443
- label: "失败",
2678
+ label: t("失败", "Failed"),
1444
2679
  value: totals.failures,
1445
- unit: "次"
2680
+ unit: times
1446
2681
  }),
1447
2682
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageCard, {
1448
- label: "结果条数",
2683
+ label: t("结果条数", "Results"),
1449
2684
  value: totals.results,
1450
- unit: "条"
2685
+ unit: t("条", "")
1451
2686
  })
1452
2687
  ]
1453
2688
  }),
1454
2689
  hasAny ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [
1455
2690
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1456
- className: "zhihu-usage-heading",
1457
- children: "每日调用次数"
2691
+ className: "dsh-ui-heading",
2692
+ children: t("每日调用次数", "Daily calls")
1458
2693
  }),
1459
2694
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageChart, {
1460
2695
  days: state.days,
1461
2696
  totals
1462
2697
  }),
1463
2698
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ul", {
1464
- className: "zhihu-usage-legend",
1465
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1466
- className: "zhihu-chart-chip zhihu-chart-chip-ok",
1467
- "aria-hidden": "true"
1468
- }), "成功"] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1469
- className: "zhihu-chart-chip zhihu-chart-chip-fail",
1470
- "aria-hidden": "true"
1471
- }), "失败"] })]
2699
+ className: "dsh-ui-row-wrap dsh-ui-meta",
2700
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
2701
+ className: "dsh-ui-row",
2702
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2703
+ className: "zhihu-chart-chip zhihu-chart-chip-ok",
2704
+ "aria-hidden": "true"
2705
+ }), t("成功", "Succeeded")]
2706
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
2707
+ className: "dsh-ui-row",
2708
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2709
+ className: "zhihu-chart-chip zhihu-chart-chip-fail",
2710
+ "aria-hidden": "true"
2711
+ }), t("失败", "Failed")]
2712
+ })]
1472
2713
  }),
1473
2714
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1474
- className: "zhihu-usage-heading",
1475
- children: "有记录的日期"
2715
+ className: "dsh-ui-heading",
2716
+ children: t("有记录的日期", "Days with calls")
1476
2717
  }),
1477
2718
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1478
2719
  className: "zhihu-usage-table-wrap",
@@ -1481,23 +2722,23 @@ function UsageSection(props) {
1481
2722
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
1482
2723
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
1483
2724
  scope: "col",
1484
- children: "日期"
2725
+ children: t("日期", "Date")
1485
2726
  }),
1486
2727
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
1487
2728
  scope: "col",
1488
- children: "调用"
2729
+ children: t("调用", "Calls")
1489
2730
  }),
1490
2731
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
1491
2732
  scope: "col",
1492
- children: "成功"
2733
+ children: t("成功", "Succeeded")
1493
2734
  }),
1494
2735
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
1495
2736
  scope: "col",
1496
- children: "失败"
2737
+ children: t("失败", "Failed")
1497
2738
  }),
1498
2739
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
1499
2740
  scope: "col",
1500
- children: "结果条数"
2741
+ children: t("结果条数", "Results")
1501
2742
  })
1502
2743
  ] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: totals.active.map((day) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
1503
2744
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
@@ -1512,22 +2753,22 @@ function UsageSection(props) {
1512
2753
  })
1513
2754
  })
1514
2755
  ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1515
- className: "zhihu-status",
1516
- children: "近 30 天暂无调用记录。"
2756
+ className: "dsh-ui-empty",
2757
+ children: t(`近 ${USAGE_DAYS} 天暂无调用记录。`, `No calls in the last ${USAGE_DAYS} days.`)
1517
2758
  })
1518
2759
  ]
1519
2760
  });
1520
2761
  }
1521
2762
  function UsageCard(props) {
1522
2763
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1523
- className: "zhihu-usage-card",
2764
+ className: "dsh-ui-card dsh-ui-card--flat dsh-ui-list-row",
1524
2765
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1525
- className: "zhihu-usage-card-label",
2766
+ className: "dsh-ui-meta",
1526
2767
  children: props.label
1527
2768
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1528
- className: "zhihu-usage-card-value",
2769
+ className: "dsh-ui-heading zhihu-usage-value",
1529
2770
  children: [String(props.value), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1530
- className: "zhihu-usage-card-unit",
2771
+ className: "dsh-ui-meta",
1531
2772
  children: props.unit
1532
2773
  })]
1533
2774
  })]
@@ -1554,6 +2795,7 @@ function formatSize(size) {
1554
2795
  }
1555
2796
  function KnowledgeSection(props) {
1556
2797
  const { rpc } = props;
2798
+ const t = useT();
1557
2799
  const gateRef = (0, react.useRef)(null);
1558
2800
  if (!gateRef.current) gateRef.current = createZhihuClientState();
1559
2801
  const gate = gateRef.current;
@@ -1561,7 +2803,6 @@ function KnowledgeSection(props) {
1561
2803
  const [baseId, setBaseId] = (0, react.useState)("");
1562
2804
  const [file, setFile] = (0, react.useState)(void 0);
1563
2805
  const [fileKey, setFileKey] = (0, react.useState)(0);
1564
- const fileRef = (0, react.useRef)(null);
1565
2806
  const [busy, setBusy] = (0, react.useState)(false);
1566
2807
  const [note, setNote] = (0, react.useState)(void 0);
1567
2808
  const [failure, setFailure] = (0, react.useState)(void 0);
@@ -1576,7 +2817,7 @@ function KnowledgeSection(props) {
1576
2817
  if (!result.ok) {
1577
2818
  setList({
1578
2819
  status: "error",
1579
- failure: failureOf(result.error.code, result.error.message)
2820
+ failure: failureOf(result.error.code, result.error.message, t)
1580
2821
  });
1581
2822
  return;
1582
2823
  }
@@ -1585,7 +2826,7 @@ function KnowledgeSection(props) {
1585
2826
  status: "error",
1586
2827
  failure: {
1587
2828
  kind: "request",
1588
- text: "响应格式与契约不符。"
2829
+ text: t("响应格式与契约不符。", "The response does not match the expected shape.")
1589
2830
  }
1590
2831
  });
1591
2832
  return;
@@ -1598,10 +2839,14 @@ function KnowledgeSection(props) {
1598
2839
  if (!gate.isCurrent(ticket)) return;
1599
2840
  setList({
1600
2841
  status: "error",
1601
- failure: networkFailure(cause)
2842
+ failure: networkFailure(cause, t)
1602
2843
  });
1603
2844
  }
1604
- }, [gate, rpc]);
2845
+ }, [
2846
+ gate,
2847
+ rpc,
2848
+ t
2849
+ ]);
1605
2850
  (0, react.useEffect)(() => {
1606
2851
  load();
1607
2852
  }, [load]);
@@ -1610,7 +2855,7 @@ function KnowledgeSection(props) {
1610
2855
  setFailure(void 0);
1611
2856
  setNote(void 0);
1612
2857
  if (file.size > KB_MAX_BYTES) {
1613
- setFailure("文件超过 20 MB 上限。");
2858
+ setFailure(t("文件超过 20 MB 上限。", "The file exceeds the 20 MB limit."));
1614
2859
  return;
1615
2860
  }
1616
2861
  const { ticket, signal } = gate.begin();
@@ -1626,161 +2871,183 @@ function KnowledgeSection(props) {
1626
2871
  if (!gate.isCurrent(ticket)) return;
1627
2872
  const result = raw;
1628
2873
  if (!result.ok) {
1629
- setFailure(`上传失败:${result.error.message}`);
2874
+ setFailure(t(`上传失败:${result.error.message}`, `Upload failed: ${result.error.message}`));
1630
2875
  return;
1631
2876
  }
1632
- setNote("已上传到知乎知识库。");
2877
+ setNote(t("已上传到知乎知识库。", "Uploaded to the Zhihu knowledge base."));
1633
2878
  setFile(void 0);
1634
2879
  setFileKey((key) => key + 1);
1635
2880
  await load();
1636
2881
  } catch (cause) {
1637
2882
  if (!gate.isCurrent(ticket)) return;
1638
- setFailure(`上传失败:${cause instanceof Error ? cause.message : String(cause)}`);
2883
+ const detail = cause instanceof Error ? cause.message : String(cause);
2884
+ setFailure(t(`上传失败:${detail}`, `Upload failed: ${detail}`));
1639
2885
  } finally {
1640
2886
  setBusy(false);
1641
2887
  }
1642
2888
  };
1643
2889
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1644
- className: "zhihu-knowledge",
2890
+ className: "dsh-ui-stack",
1645
2891
  "data-testid": "zhihu-knowledge",
1646
- "aria-label": "知乎知识库",
2892
+ "aria-label": t("知乎知识库", "Zhihu knowledge base"),
1647
2893
  children: [
1648
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1649
- className: "zhihu-intro",
1650
- children: "把参考资料上传到知乎知识库,供搭档检索。文件会进入知乎云端,请勿上传未发表手稿。"
2894
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
2895
+ className: "dsh-ui-help",
2896
+ children: [
2897
+ t("上传参考资料供检索;文件进入知乎云端,勿上传未发表手稿。", "Upload references for retrieval. Files go to Zhihu cloud, so avoid unpublished drafts."),
2898
+ " ",
2899
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExternalLink, {
2900
+ url: KB_MANAGE_URL,
2901
+ children: t("管理知识库", "Manage knowledge bases")
2902
+ })
2903
+ ]
1651
2904
  }),
1652
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
1653
- className: "zhihu-link",
1654
- href: KB_MANAGE_URL,
1655
- target: "_blank",
1656
- rel: "noreferrer",
1657
- children: "管理知识库"
1658
- }) }),
1659
2905
  list.status === "loading" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1660
- className: "zhihu-status",
2906
+ className: "dsh-ui-loading",
1661
2907
  role: "status",
1662
- children: [activityDots(), "正在读取知识库列表…"]
2908
+ children: [activityDots(), t("正在读取知识库列表…", "Loading knowledge bases…")]
1663
2909
  }) : null,
1664
- list.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1665
- className: "zhihu-error",
2910
+ list.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2911
+ className: "dsh-ui-error",
1666
2912
  role: "alert",
1667
- children: [`${list.failure.text} `, list.failure.kind !== "credential" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZhihuButton, {
2913
+ children: list.failure.text
2914
+ }), list.failure.kind !== "credential" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2915
+ className: "dsh-ui-actions",
2916
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZhihuButton, {
1668
2917
  host: props.Button,
1669
- className: "zhihu-button",
1670
2918
  onClick: () => void load(),
1671
- children: "重试"
1672
- }) : null]
1673
- }) : null,
2919
+ children: t("重试", "Retry")
2920
+ })
2921
+ }) : null] }) : null,
1674
2922
  list.status === "ready" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1675
- className: "zhihu-field",
2923
+ className: "dsh-ui-field",
1676
2924
  children: [
1677
2925
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1678
- className: "zhihu-field-label",
2926
+ className: "dsh-ui-label",
1679
2927
  htmlFor: "zhihu-kb-base",
1680
- children: "目标知识库"
2928
+ children: t("目标知识库", "Target knowledge base")
1681
2929
  }),
1682
2930
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1683
- className: "zhihu-row",
2931
+ className: "dsh-ui-row",
1684
2932
  children: [renderSelect(props.Select, {
2933
+ id: "zhihu-kb-base",
1685
2934
  value: baseId,
1686
2935
  disabled: busy,
1687
- "aria-label": "目标知识库",
2936
+ "aria-label": t("目标知识库", "Target knowledge base"),
1688
2937
  onChange: setBaseId,
1689
2938
  options: [{
1690
2939
  value: "",
1691
- label: "默认知识库"
2940
+ label: t("默认知识库", "Default knowledge base")
1692
2941
  }, ...list.bases.map((base) => ({
1693
2942
  value: base.id,
1694
- label: `${base.name}${base.isDefault ? "(默认)" : ""} · ${base.contentCount} 篇`
2943
+ label: t(`${base.name}${base.isDefault ? "(默认)" : ""} · ${base.contentCount} 篇`, `${base.name}${base.isDefault ? " (default)" : ""} · ${base.contentCount} docs`)
1695
2944
  }))]
1696
- }, "zhihu-select"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZhihuButton, {
2945
+ }, "dsh-ui-select"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZhihuButton, {
1697
2946
  host: props.Button,
1698
- className: "zhihu-button",
1699
2947
  disabled: busy,
1700
2948
  onClick: () => void load(),
1701
- children: "刷新"
2949
+ children: t("刷新", "Refresh")
1702
2950
  })]
1703
2951
  }),
1704
2952
  list.bases.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1705
- className: "zhihu-hint",
1706
- children: "暂无可用知识库。"
2953
+ className: "dsh-ui-hint",
2954
+ children: t("暂无可用知识库,将上传到默认知识库。", "No knowledge bases yet; uploads go to the default one.")
1707
2955
  }) : null
1708
2956
  ]
1709
2957
  }) : null,
1710
2958
  list.status === "ready" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1711
- className: "zhihu-field",
2959
+ className: "dsh-ui-field",
1712
2960
  children: [
1713
2961
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1714
- className: "zhihu-field-label",
1715
- children: "选择文件"
2962
+ className: "dsh-ui-label",
2963
+ id: "zhihu-kb-file-label",
2964
+ children: t("参考资料", "Reference file")
1716
2965
  }),
1717
2966
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1718
- ref: fileRef,
1719
2967
  id: "zhihu-kb-file",
1720
2968
  type: "file",
1721
2969
  accept: KB_ACCEPT,
1722
- hidden: true,
1723
2970
  className: "zhihu-file",
2971
+ "aria-labelledby": "zhihu-kb-file-label",
1724
2972
  disabled: busy,
1725
2973
  onChange: (event) => setFile(event.target.files?.[0])
1726
2974
  }, fileKey),
1727
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZhihuButton, {
1728
- host: props.Button,
1729
- className: "zhihu-button",
1730
- disabled: busy,
1731
- onClick: () => fileRef.current?.click(),
1732
- children: file ? file.name : "选择文件"
2975
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2976
+ className: "dsh-ui-row",
2977
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
2978
+ htmlFor: "zhihu-kb-file",
2979
+ className: "zhihu-file-trigger",
2980
+ "aria-disabled": busy || void 0,
2981
+ children: t("选择文件", "Choose file")
2982
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2983
+ className: "dsh-ui-meta dsh-ui-wrap",
2984
+ children: file ? `${file.name} · ${formatSize(file.size)}` : t("未选择文件(最大 20 MB)", "No file chosen (max 20 MB)")
2985
+ })]
1733
2986
  })
1734
2987
  ]
1735
2988
  }) : null,
1736
2989
  list.status === "ready" && file ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1737
- className: "zhihu-upload-confirm",
1738
- children: `确认将「${file.name}」(${formatSize(file.size)})上传到${baseId ? "所选知识库" : "默认知识库"}?文件会进入知乎云端。`
2990
+ className: "dsh-ui-banner",
2991
+ children: t(`将「${file.name}」上传到${baseId ? "所选知识库" : "默认知识库"}?文件进入知乎云端。`, `Upload "${file.name}" to the ${baseId ? "selected" : "default"} knowledge base? It goes to Zhihu cloud.`)
1739
2992
  }) : null,
1740
2993
  list.status === "ready" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1741
- className: "zhihu-row",
2994
+ className: "dsh-ui-actions",
1742
2995
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZhihuButton, {
1743
2996
  host: props.Button,
1744
2997
  variant: "primary",
1745
- className: "zhihu-button zhihu-button-primary",
1746
2998
  disabled: busy || !file,
1747
2999
  onClick: () => void upload(),
1748
- children: busy ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [activityDots(), "上传中…"] }) : "确认上传"
3000
+ children: busy ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [activityDots(), t("上传中…", "Uploading…")] }) : t("确认上传", "Upload")
1749
3001
  })
1750
3002
  }) : null,
1751
3003
  note ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1752
- className: "zhihu-saved",
3004
+ className: "dsh-ui-notice",
1753
3005
  role: "status",
1754
3006
  children: note
1755
3007
  }) : null,
1756
3008
  failure ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1757
- className: "zhihu-warning",
3009
+ className: "dsh-ui-error",
1758
3010
  role: "alert",
1759
3011
  children: failure
1760
3012
  }) : null
1761
3013
  ]
1762
3014
  });
1763
3015
  }
1764
- const TAB_LABEL = {
1765
- search: "搜索",
1766
- settings: "设置",
1767
- usage: "用量",
1768
- knowledge: "知识库"
1769
- };
3016
+ function tabLabel(tab, t) {
3017
+ switch (tab) {
3018
+ case "settings": return t("设置", "Settings");
3019
+ case "usage": return t("统计", "Stats");
3020
+ case "quota": return t("官方用量", "Official quota");
3021
+ case "knowledge": return t("知识库", "Knowledge base");
3022
+ case "test": return t("测试", "Test");
3023
+ }
3024
+ }
1770
3025
  const SETTINGS_TABS = [
1771
3026
  "settings",
1772
3027
  "usage",
3028
+ "quota",
1773
3029
  "knowledge",
1774
- "search"
3030
+ "test"
1775
3031
  ];
3032
+ /**
3033
+ * Tabs that have been opened stay mounted (hidden when inactive), so their
3034
+ * loaded data is cached here instead of being refetched on every switch.
3035
+ */
3036
+ function nextVisitedTabs(visited, next) {
3037
+ return visited.includes(next) ? [...visited] : [...visited, next];
3038
+ }
1776
3039
  function ZhihuSettings(props) {
1777
3040
  const { rpc, credentials, Select, Button, Input } = props;
3041
+ const t = useT();
1778
3042
  const gateRef = (0, react.useRef)(null);
1779
3043
  if (!gateRef.current) gateRef.current = createZhihuClientState();
1780
3044
  const gate = gateRef.current;
1781
3045
  const [tab, setTab] = (0, react.useState)("settings");
3046
+ const [visited, setVisited] = (0, react.useState)(["settings"]);
1782
3047
  const [query, setQuery] = (0, react.useState)("");
1783
- const [mode, setMode] = (0, react.useState)("search");
3048
+ const [testOperation, setTestOperation] = (0, react.useState)("search");
3049
+ const isSearchOperation = MODES.includes(testOperation);
3050
+ const mode = isSearchOperation ? testOperation : "search";
1784
3051
  const [askModel, setAskModel] = (0, react.useState)("zhida-thinking-1p5");
1785
3052
  const [scopes, setScopes] = (0, react.useState)(["public"]);
1786
3053
  const [phase, setPhase] = (0, react.useState)("idle");
@@ -1788,7 +3055,6 @@ function ZhihuSettings(props) {
1788
3055
  const [outcome, setOutcome] = (0, react.useState)(null);
1789
3056
  const [revision, setRevision] = (0, react.useState)(0);
1790
3057
  const [resultRevision, setResultRevision] = (0, react.useState)(0);
1791
- const queryRef = (0, react.useRef)(null);
1792
3058
  (0, react.useEffect)(() => () => gate.cancel(), [gate]);
1793
3059
  const runSearch = (0, react.useCallback)(async () => {
1794
3060
  if (mode !== "hot" && !query.trim()) return;
@@ -1817,7 +3083,7 @@ function ZhihuSettings(props) {
1817
3083
  if (!parsed) {
1818
3084
  setFailure({
1819
3085
  kind: "request",
1820
- text: "响应格式与契约不符。"
3086
+ text: t("响应格式与契约不符。", "The response does not match the expected shape.")
1821
3087
  });
1822
3088
  setPhase("error");
1823
3089
  return;
@@ -1827,12 +3093,12 @@ function ZhihuSettings(props) {
1827
3093
  setPhase("done");
1828
3094
  } else if (response.error.code === "cancelled") setPhase("idle");
1829
3095
  else {
1830
- setFailure(failureOf(response.error.code, response.error.message));
3096
+ setFailure(failureOf(response.error.code, response.error.message, t));
1831
3097
  setPhase("error");
1832
3098
  }
1833
3099
  } catch (cause) {
1834
3100
  if (!gate.isCurrent(ticket)) return;
1835
- setFailure(networkFailure(cause));
3101
+ setFailure(networkFailure(cause, t));
1836
3102
  setPhase("error");
1837
3103
  }
1838
3104
  }, [
@@ -1841,7 +3107,8 @@ function ZhihuSettings(props) {
1841
3107
  mode,
1842
3108
  query,
1843
3109
  askModel,
1844
- scopes
3110
+ scopes,
3111
+ t
1845
3112
  ]);
1846
3113
  const resetToIdle = () => {
1847
3114
  setPhase((current) => current === "loading" || current === "error" ? "idle" : current);
@@ -1852,25 +3119,26 @@ function ZhihuSettings(props) {
1852
3119
  setRevision(gate.noteInput());
1853
3120
  resetToIdle();
1854
3121
  };
1855
- const onModeChange = (next) => {
1856
- if (next === mode) return;
3122
+ const onTestOperationChange = (next) => {
3123
+ if (next === testOperation) return;
1857
3124
  setRevision(gate.noteInput());
1858
- setMode(next);
1859
- resetToIdle();
3125
+ setTestOperation(next);
3126
+ setOutcome(null);
3127
+ setPhase("idle");
3128
+ setFailure(null);
1860
3129
  };
1861
3130
  const onTabChange = (next) => {
1862
3131
  if (next === tab) return;
1863
3132
  gate.cancel();
1864
3133
  setPhase((current) => current === "loading" ? "idle" : current);
1865
3134
  setTab(next);
3135
+ setVisited((current) => nextVisitedTabs(current, next));
1866
3136
  };
1867
3137
  const toggleScope = (scope) => {
3138
+ if (scopes.length === 1 && scopes[0] === scope) return;
1868
3139
  setRevision(gate.noteInput());
1869
3140
  resetToIdle();
1870
- setScopes((current) => {
1871
- const next = current.includes(scope) ? current.filter((item) => item !== scope) : [...current, scope];
1872
- return next.length > 0 ? next : current;
1873
- });
3141
+ setScopes((current) => current.includes(scope) ? current.filter((item) => item !== scope) : [...current, scope]);
1874
3142
  };
1875
3143
  const onAskModelChange = (next) => {
1876
3144
  if (next === askModel) return;
@@ -1889,150 +3157,154 @@ function ZhihuSettings(props) {
1889
3157
  const isSearchDisabled = () => phase === "loading" || mode !== "hot" && !query.trim();
1890
3158
  const stale = outcome !== null && resultRevision !== revision;
1891
3159
  const searchDisabled = isSearchDisabled();
1892
- const tablist = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3160
+ const [firstTab, ...restTabs] = SETTINGS_TABS.map((key) => ({
3161
+ value: key,
3162
+ label: tabLabel(key, t),
3163
+ id: `zhihu-tab-${key}`,
3164
+ panelId: `zhihu-tabpanel-${key}`
3165
+ }));
3166
+ const tablist = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SegmentedTabs, {
3167
+ value: tab,
3168
+ onChange: onTabChange,
3169
+ label: t("知乎分区", "Zhihu sections"),
1893
3170
  className: "zhihu-tabs",
1894
- role: "tablist",
1895
- "aria-label": "知乎资料分区",
1896
- onKeyDown: (event) => {
1897
- if (![
1898
- "ArrowLeft",
1899
- "ArrowRight",
1900
- "Home",
1901
- "End"
1902
- ].includes(event.key)) return;
1903
- const buttons = [...event.currentTarget.querySelectorAll("[role=\"tab\"]")];
1904
- const index = buttons.indexOf(event.target);
1905
- if (index < 0) return;
1906
- event.preventDefault();
1907
- const next = event.key === "Home" ? 0 : event.key === "End" ? buttons.length - 1 : (index + (event.key === "ArrowRight" ? 1 : -1) + buttons.length) % buttons.length;
1908
- buttons[next]?.focus();
1909
- buttons[next]?.click();
1910
- },
1911
- children: SETTINGS_TABS.map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZhihuButton, {
1912
- host: Button,
1913
- type: "button",
1914
- role: "tab",
1915
- "aria-selected": tab === key,
1916
- tabIndex: tab === key ? 0 : -1,
1917
- className: "zhihu-tab",
1918
- onClick: () => onTabChange(key),
1919
- children: TAB_LABEL[key]
1920
- }, key))
1921
- }, "tabs");
1922
- const body = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1923
- className: "zhihu-panel-body",
1924
- children: [
1925
- tab === "search" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1926
- role: "tabpanel",
1927
- className: "zhihu-field",
1928
- children: [
1929
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1930
- className: "zhihu-row",
1931
- children: [renderSelect(Select, {
1932
- "aria-label": "搜索方式",
1933
- value: mode,
1934
- onChange: (next) => onModeChange(next),
1935
- options: Object.keys(MODE_LABEL).map((key) => ({
1936
- value: key,
1937
- label: MODE_LABEL[key]
1938
- }))
1939
- }, "zhihu-select"), mode === "ask" ? renderSelect(Select, {
1940
- "aria-label": "直答模型",
1941
- value: askModel,
1942
- onChange: (next) => onAskModelChange(next),
1943
- options: ASK_MODELS.map((model) => ({
1944
- value: model.value,
1945
- label: model.label
1946
- }))
1947
- }, "zhihu-select") : null]
1948
- }),
1949
- mode === "knowledge" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1950
- className: "zhihu-scopes",
1951
- role: "group",
1952
- "aria-label": "检索范围",
1953
- children: SCOPE_OPTIONS.map((scope) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZhihuButton, {
1954
- host: Button,
1955
- role: "checkbox",
1956
- "aria-checked": scopes.includes(scope.value),
1957
- className: `zhihu-scope${scopes.includes(scope.value) ? " is-on" : ""}`,
1958
- onClick: () => toggleScope(scope.value),
1959
- children: scope.label
1960
- }, scope.value))
1961
- }) : null,
1962
- renderInput(Input, {
1963
- ref: queryRef,
1964
- type: "search",
1965
- className: "zhihu-input",
1966
- "data-testid": "zhihu-query",
1967
- "aria-label": "搜索关键词",
1968
- placeholder: mode === "hot" ? "热榜无需关键词" : "输入关键词…",
1969
- value: query,
1970
- disabled: mode === "hot",
1971
- onChange: onQueryChange,
1972
- onKeyDown: onQueryKeyDown
1973
- }),
1974
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1975
- className: "zhihu-row",
1976
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZhihuButton, {
1977
- host: Button,
1978
- variant: "primary",
1979
- className: "zhihu-button zhihu-button-primary",
1980
- "data-testid": "zhihu-search",
1981
- disabled: searchDisabled,
1982
- onClick: () => void runSearch(),
1983
- children: phase === "loading" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [activityDots(), "请求中…"] }) : mode === "hot" ? "获取热榜" : "搜索"
1984
- }), phase === "loading" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZhihuButton, {
1985
- host: Button,
1986
- className: "zhihu-button",
1987
- onClick: () => {
1988
- gate.cancel();
1989
- setPhase("idle");
1990
- },
1991
- children: "取消"
1992
- }) : null]
1993
- }),
1994
- phase === "idle" && mode !== "hot" && !query.trim() ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1995
- className: "zhihu-status",
1996
- role: "status",
1997
- children: "输入关键词后搜索。"
1998
- }) : null,
1999
- phase === "loading" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2000
- className: "zhihu-status",
2001
- role: "status",
2002
- children: [activityDots(), "正在请求知乎…"]
2003
- }) : null,
2004
- phase === "error" && failure ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2005
- className: "zhihu-error",
2006
- role: "alert",
2007
- children: failure.text
2008
- }) : null,
2009
- phase === "done" && outcome ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OutcomeView, {
2010
- outcome,
2011
- stale
2012
- }) : null
2013
- ]
3171
+ items: [firstTab, ...restTabs]
3172
+ });
3173
+ const panel = (key, content) => visited.includes(key) || key === tab ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3174
+ id: `zhihu-tabpanel-${key}`,
3175
+ role: "tabpanel",
3176
+ "aria-labelledby": `zhihu-tab-${key}`,
3177
+ hidden: key !== tab,
3178
+ className: "dsh-ui-panel",
3179
+ children: content
3180
+ }, key) : null;
3181
+ const testPanel = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3182
+ className: "dsh-ui-field",
3183
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZhihuTestSelector, {
3184
+ value: testOperation,
3185
+ onChange: onTestOperationChange,
3186
+ Select
3187
+ }), isSearchOperation ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [
3188
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3189
+ className: "dsh-ui-row",
3190
+ children: mode === "ask" ? renderSelect(Select, {
3191
+ "aria-label": t("直答模型", "Answer model"),
3192
+ value: askModel,
3193
+ onChange: (next) => onAskModelChange(next),
3194
+ options: ASK_MODELS.map((model) => ({
3195
+ value: model.value,
3196
+ label: t(model.zh, model.en)
3197
+ }))
3198
+ }, "dsh-ui-select") : null
3199
+ }),
3200
+ mode === "knowledge" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3201
+ className: "dsh-ui-row-wrap",
3202
+ role: "group",
3203
+ "aria-label": t("检索范围", "Search scope"),
3204
+ "aria-describedby": "zhihu-scope-hint",
3205
+ children: SCOPE_OPTIONS.map((scope) => {
3206
+ const checked = scopes.includes(scope.value);
3207
+ const locked = checked && scopes.length === 1;
3208
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Pill, {
3209
+ active: checked,
3210
+ role: "checkbox",
3211
+ "aria-checked": checked,
3212
+ "aria-disabled": locked || void 0,
3213
+ onClick: () => toggleScope(scope.value),
3214
+ children: t(scope.zh, scope.en)
3215
+ }, scope.value);
3216
+ })
3217
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3218
+ className: "dsh-ui-hint",
3219
+ id: "zhihu-scope-hint",
3220
+ children: t("至少保留一个检索范围。", "Keep at least one scope selected.")
3221
+ })] }) : null,
3222
+ renderInput(Input, {
3223
+ type: "search",
3224
+ className: "zhihu-input",
3225
+ "data-testid": "zhihu-query",
3226
+ "aria-label": t("搜索关键词", "Search keywords"),
3227
+ placeholder: mode === "hot" ? t("热榜无需关键词", "Trending needs no keywords") : t("输入关键词…", "Enter keywords…"),
3228
+ value: query,
3229
+ disabled: mode === "hot",
3230
+ onChange: onQueryChange,
3231
+ onKeyDown: onQueryKeyDown
3232
+ }),
3233
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3234
+ className: "dsh-ui-actions",
3235
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZhihuButton, {
3236
+ host: Button,
3237
+ variant: "primary",
3238
+ "data-testid": "zhihu-search",
3239
+ disabled: searchDisabled,
3240
+ onClick: () => void runSearch(),
3241
+ children: mode === "hot" ? t("获取热榜", "Get trending") : t("搜索", "Search")
3242
+ }), phase === "loading" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZhihuButton, {
3243
+ host: Button,
3244
+ onClick: () => {
3245
+ gate.cancel();
3246
+ setPhase("idle");
3247
+ },
3248
+ children: t("取消", "Cancel")
3249
+ }) : null]
3250
+ }),
3251
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3252
+ className: phase === "loading" ? "dsh-ui-loading" : "dsh-ui-hint",
3253
+ role: "status",
3254
+ children: phase === "loading" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [activityDots(), t("正在请求知乎…", "Requesting Zhihu…")] }) : phase === "idle" && mode !== "hot" && !query.trim() ? t("输入关键词后搜索。", "Enter keywords to search.") : null
3255
+ }),
3256
+ phase === "error" && failure ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3257
+ className: "dsh-ui-error",
3258
+ role: "alert",
3259
+ children: failure.text
2014
3260
  }) : null,
2015
- tab === "settings" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsSection, {
3261
+ phase === "done" && outcome ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OutcomeView, {
3262
+ outcome,
3263
+ stale
3264
+ }) : null
3265
+ ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZhihuOpenPlatformSection, {
3266
+ selectedOperation: testOperation,
3267
+ rpc,
3268
+ Button,
3269
+ Input,
3270
+ Select
3271
+ }, testOperation)]
3272
+ });
3273
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3274
+ className: "zhihu-panel zhihu-settings-embed",
3275
+ "data-testid": "zhihu-settings-embed",
3276
+ "aria-label": t("知乎", "Zhihu"),
3277
+ children: [
3278
+ tablist,
3279
+ panel("settings", /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsSection, {
2016
3280
  credentials,
2017
3281
  Button,
2018
3282
  Input
2019
- }) : null,
2020
- tab === "usage" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageSection, {
3283
+ })),
3284
+ panel("usage", /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageSection, {
2021
3285
  rpc,
2022
3286
  Button
2023
- }) : null,
2024
- tab === "knowledge" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KnowledgeSection, {
3287
+ })),
3288
+ panel("quota", /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZhihuQuotaSection, {
3289
+ rpc,
3290
+ Button
3291
+ })),
3292
+ panel("knowledge", /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KnowledgeSection, {
2025
3293
  rpc,
2026
3294
  Select,
2027
3295
  Button
2028
- }) : null
3296
+ })),
3297
+ panel("test", testPanel)
2029
3298
  ]
2030
- }, "body");
2031
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2032
- className: "zhihu-panel zhihu-settings-embed",
2033
- "data-testid": "zhihu-settings-embed",
2034
- "aria-label": SLOT_LABEL,
2035
- children: [tablist, body]
3299
+ });
3300
+ }
3301
+ /** Settings root: resolves the host locale once and hands it to every section. */
3302
+ function ZhihuSettingsRoot(props) {
3303
+ const { locale, ...rest } = props;
3304
+ const active = useHostLocale(locale);
3305
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZhihuLocaleContext.Provider, {
3306
+ value: active,
3307
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZhihuSettings, { ...rest })
2036
3308
  });
2037
3309
  }
2038
3310
  function apply(ctx) {
@@ -2041,9 +3313,16 @@ function apply(ctx) {
2041
3313
  return () => style?.remove();
2042
3314
  }, "zhihu.styles");
2043
3315
  const client = ctx;
2044
- const settingsRender = (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZhihuSettings, {
3316
+ let locale;
3317
+ try {
3318
+ locale = client.locale;
3319
+ } catch {
3320
+ locale = void 0;
3321
+ }
3322
+ const settingsRender = (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZhihuSettingsRoot, {
2045
3323
  rpc: client.connection.rpc,
2046
3324
  credentials: wrapCredentials(client.remote.credentials),
3325
+ locale,
2047
3326
  ...hostComponentsFromRenderProps(props)
2048
3327
  });
2049
3328
  ctx.effect(() => client.slots.inject("plugins.bundle.config", () => client.slots.register({
@@ -2054,12 +3333,19 @@ function apply(ctx) {
2054
3333
  }, settingsRender)), "zhihu.settings");
2055
3334
  }
2056
3335
  //#endregion
3336
+ exports.CapabilitiesNote = CapabilitiesNote;
3337
+ exports.ExternalLink = ExternalLink;
3338
+ exports.SETTINGS_TABS = SETTINGS_TABS;
3339
+ exports.TEST_OPTIONS = TEST_OPTIONS;
3340
+ exports.ZhihuTestSelector = ZhihuTestSelector;
2057
3341
  exports.apply = apply;
2058
3342
  exports.formatUsageDate = formatUsageDate;
2059
3343
  exports.inject = inject;
2060
3344
  exports.name = name;
3345
+ exports.nextVisitedTabs = nextVisitedTabs;
2061
3346
  exports.summarizeUsage = summarizeUsage;
2062
3347
  exports.usageChartTicks = usageChartTicks;
3348
+ exports.usageValueLabels = usageValueLabels;
2063
3349
 
2064
3350
  //# sourceMappingURL=client.inner.cjs.map
2065
3351
  return module.exports;