@klarkxy/dsh-zhihu 0.1.9 → 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/README.md +24 -16
- package/docs/README.zh-CN.md +24 -16
- package/lib/client.js +2011 -725
- package/lib/index.js +7 -2
- package/lib/index.js.map +1 -1
- package/lib/tools.js +11 -11
- package/lib/tools.js.map +1 -1
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +12 -6
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
.
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
.
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
.
|
|
239
|
-
.
|
|
240
|
-
.
|
|
241
|
-
.
|
|
242
|
-
|
|
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
|
-
.
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
.
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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-
|
|
268
|
-
background:
|
|
269
|
-
|
|
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
|
-
.
|
|
273
|
-
.
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
.
|
|
281
|
-
|
|
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
|
-
|
|
285
|
-
|
|
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
|
-
.
|
|
288
|
-
.
|
|
289
|
-
.
|
|
290
|
-
|
|
291
|
-
|
|
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-
|
|
295
|
-
|
|
296
|
-
color: var(--
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
.
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
.
|
|
335
|
-
|
|
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:
|
|
350
|
-
|
|
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
|
-
|
|
353
|
-
.
|
|
354
|
-
|
|
355
|
-
.zhihu-
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
border-radius: var(--radius-
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
.zhihu-ask-content { white-space: pre-wrap;
|
|
363
|
-
|
|
364
|
-
.
|
|
365
|
-
|
|
366
|
-
.zhihu-
|
|
367
|
-
.zhihu-
|
|
368
|
-
.zhihu-
|
|
369
|
-
|
|
370
|
-
.
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
.zhihu-
|
|
374
|
-
.zhihu-
|
|
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,
|
|
419
|
-
.zhihu-usage-table
|
|
420
|
-
.zhihu-usage-table th
|
|
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-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
padding:
|
|
436
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
--
|
|
524
|
-
|
|
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)(
|
|
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
|
-
/**
|
|
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)(
|
|
917
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
652
918
|
ref,
|
|
653
|
-
|
|
654
|
-
className
|
|
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: `网络或连接失败:${
|
|
1829
|
+
text: t(`网络或连接失败:${detail}`, `Network or connection failed: ${detail}`)
|
|
723
1830
|
};
|
|
724
1831
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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
|
-
|
|
1904
|
+
zh: "思考",
|
|
1905
|
+
en: "Thinking"
|
|
743
1906
|
},
|
|
744
1907
|
{
|
|
745
1908
|
value: "zhida-fast-1p5",
|
|
746
|
-
|
|
1909
|
+
zh: "快速",
|
|
1910
|
+
en: "Fast"
|
|
747
1911
|
},
|
|
748
1912
|
{
|
|
749
1913
|
value: "zhida-agent",
|
|
750
|
-
|
|
1914
|
+
zh: "智能体",
|
|
1915
|
+
en: "Agent"
|
|
751
1916
|
}
|
|
752
1917
|
];
|
|
753
1918
|
const SCOPE_OPTIONS = [
|
|
754
1919
|
{
|
|
755
1920
|
value: "public",
|
|
756
|
-
|
|
1921
|
+
zh: "公开库",
|
|
1922
|
+
en: "Public"
|
|
757
1923
|
},
|
|
758
1924
|
{
|
|
759
1925
|
value: "personal",
|
|
760
|
-
|
|
1926
|
+
zh: "个人库",
|
|
1927
|
+
en: "Personal"
|
|
761
1928
|
},
|
|
762
1929
|
{
|
|
763
1930
|
value: "subscription",
|
|
764
|
-
|
|
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
|
-
|
|
826
|
-
|
|
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", {
|
|
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: "
|
|
856
|
-
children: [
|
|
857
|
-
className: "
|
|
858
|
-
|
|
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: "
|
|
2037
|
+
className: "dsh-ui-list",
|
|
869
2038
|
children: outcome.items.map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
870
|
-
className: "
|
|
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: "
|
|
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: "
|
|
2059
|
+
className: "dsh-ui-list",
|
|
891
2060
|
children: outcome.items.map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
892
|
-
className: "
|
|
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: "
|
|
2065
|
+
className: "dsh-ui-list-name zhihu-link"
|
|
897
2066
|
}), item.summary ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
898
|
-
className: "
|
|
2067
|
+
className: "dsh-ui-meta",
|
|
899
2068
|
children: item.summary
|
|
900
2069
|
}) : null]
|
|
901
2070
|
}, `${index}`))
|
|
902
2071
|
});
|
|
903
2072
|
} else {
|
|
904
|
-
summary = `${
|
|
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: "
|
|
2075
|
+
className: "dsh-ui-list",
|
|
907
2076
|
children: outcome.items.map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
908
|
-
className: "
|
|
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: "
|
|
2082
|
+
className: "dsh-ui-list-name zhihu-link"
|
|
914
2083
|
}),
|
|
915
2084
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
916
|
-
className: "
|
|
917
|
-
children:
|
|
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: "
|
|
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: "
|
|
2098
|
+
className: "dsh-ui-stack",
|
|
930
2099
|
children: [
|
|
931
|
-
|
|
932
|
-
className: "zhihu-stale",
|
|
933
|
-
role: "status",
|
|
934
|
-
children: "查询已变化,以下内容对应旧查询,请重新搜索。"
|
|
935
|
-
}) : null,
|
|
2100
|
+
staleBanner,
|
|
936
2101
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
937
|
-
className: "
|
|
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
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
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: "
|
|
2163
|
+
className: "dsh-ui-stack",
|
|
983
2164
|
"data-testid": "zhihu-settings",
|
|
984
|
-
"aria-label": "知乎凭证设置",
|
|
985
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CapabilitiesNote, {})
|
|
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 [
|
|
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: "
|
|
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.
|
|
1029
|
-
className: "
|
|
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:
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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)
|
|
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: "
|
|
1094
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", {
|
|
1095
|
-
className: "
|
|
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: "
|
|
2293
|
+
className: "dsh-ui-field",
|
|
1107
2294
|
children: [
|
|
1108
2295
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1109
|
-
className: "
|
|
2296
|
+
className: "dsh-ui-label-row",
|
|
1110
2297
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1111
|
-
className: "
|
|
2298
|
+
className: "dsh-ui-label",
|
|
1112
2299
|
id: "zhihu-access-secret-label",
|
|
1113
2300
|
children: "Access Secret"
|
|
1114
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
1115
|
-
|
|
1116
|
-
|
|
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:
|
|
2313
|
+
onChange: editDraft
|
|
1137
2314
|
}),
|
|
1138
2315
|
draftFailure === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1139
|
-
className: "
|
|
2316
|
+
className: "dsh-ui-error",
|
|
1140
2317
|
role: "alert",
|
|
1141
|
-
children: draftFailure === "blank" ? "密钥不能只包含空白字符。" : "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
|
2337
|
+
className: "zhihu-button-danger",
|
|
1161
2338
|
disabled: busy || keyLocked || !configured,
|
|
1162
|
-
onClick: () =>
|
|
1163
|
-
children:
|
|
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:
|
|
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: "
|
|
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.
|
|
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.
|
|
1408
|
-
className: "
|
|
2630
|
+
"aria-label": sectionLabel,
|
|
2631
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2632
|
+
className: "dsh-ui-error",
|
|
1409
2633
|
role: "alert",
|
|
1410
|
-
children:
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
1465
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
1466
|
-
className: "
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
"
|
|
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: "
|
|
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: "
|
|
1516
|
-
children:
|
|
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: "
|
|
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: "
|
|
2766
|
+
className: "dsh-ui-meta",
|
|
1526
2767
|
children: props.label
|
|
1527
2768
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1528
|
-
className: "zhihu-usage-
|
|
2769
|
+
className: "dsh-ui-heading zhihu-usage-value",
|
|
1529
2770
|
children: [String(props.value), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1530
|
-
className: "
|
|
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
|
-
}, [
|
|
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
|
-
|
|
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: "
|
|
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.
|
|
1649
|
-
className: "
|
|
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: "
|
|
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: "
|
|
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:
|
|
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
|
-
})
|
|
1673
|
-
}) : null,
|
|
2919
|
+
children: t("重试", "Retry")
|
|
2920
|
+
})
|
|
2921
|
+
}) : null] }) : null,
|
|
1674
2922
|
list.status === "ready" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1675
|
-
className: "
|
|
2923
|
+
className: "dsh-ui-field",
|
|
1676
2924
|
children: [
|
|
1677
2925
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
1678
|
-
className: "
|
|
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: "
|
|
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
|
-
}, "
|
|
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: "
|
|
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: "
|
|
2959
|
+
className: "dsh-ui-field",
|
|
1712
2960
|
children: [
|
|
1713
2961
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1714
|
-
className: "
|
|
1715
|
-
|
|
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.
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
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: "
|
|
1738
|
-
children:
|
|
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: "
|
|
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: "
|
|
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: "
|
|
3009
|
+
className: "dsh-ui-error",
|
|
1758
3010
|
role: "alert",
|
|
1759
3011
|
children: failure
|
|
1760
3012
|
}) : null
|
|
1761
3013
|
]
|
|
1762
3014
|
});
|
|
1763
3015
|
}
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
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
|
-
"
|
|
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 [
|
|
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
|
|
1856
|
-
if (next ===
|
|
3122
|
+
const onTestOperationChange = (next) => {
|
|
3123
|
+
if (next === testOperation) return;
|
|
1857
3124
|
setRevision(gate.noteInput());
|
|
1858
|
-
|
|
1859
|
-
|
|
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
|
|
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
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
},
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
children:
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
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
|
-
|
|
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
|
-
})
|
|
2020
|
-
|
|
3283
|
+
})),
|
|
3284
|
+
panel("usage", /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageSection, {
|
|
2021
3285
|
rpc,
|
|
2022
3286
|
Button
|
|
2023
|
-
})
|
|
2024
|
-
|
|
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
|
-
})
|
|
3296
|
+
})),
|
|
3297
|
+
panel("test", testPanel)
|
|
2029
3298
|
]
|
|
2030
|
-
}
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
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
|
-
|
|
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;
|