@klarkxy/dsh-zhihu 0.1.9 → 0.2.0

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