@dsh-ft/dsh-fft-settings 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -39,7 +39,7 @@ const apiKey = ctx.ftshareSettings.apiKey
39
39
  | `set(value)` | `Promise<void>`:只保存,不联网测试;去首尾空白,拒绝空值、超过 512 字符和控制字符 |
40
40
  | `clear()` | `Promise<void>`:只清除 DSH 保存值;可能露出 `.env` 回退值 |
41
41
  | `configureAndTest(value)` | `Promise<ConfigureResult>`:先保存,再检查远端服务和 Key;保存和验证分别返回,测试失败不回滚 |
42
- | `testConnection()` | `Promise<ValidationResult>`:先匿名检查远端,再验证调用时生效的 Key,不修改配置;无 Key 仍检查远端 |
42
+ | `testConnection()` | `Promise<ValidationResult>`:先检查远端(配置了 Key 时携带),再验证调用时生效的 Key,不修改配置;无 Key 仍检查远端 |
43
43
  | `getValidationStatus()` | `Promise<ValidationResult>`:读取最近验证结果,不联网 |
44
44
  | `subscribe(listener)` | 返回取消订阅函数;失效通知,无参数、无密钥;回调重新读取状态 |
45
45
  | `createEnvironment(base?)` | 返回新环境变量对象,用当前 Key 替换 `FTSHARE_API_KEY`;无 Key 删除该字段,不修改 base |
@@ -73,9 +73,11 @@ const result = await apiKey.configureAndTest(userInput)
73
73
 
74
74
  | 结果字段 | 检测目的 | 固定请求 | 是否携带 Key |
75
75
  | --- | --- | --- | --- |
76
- | `validation.remote` | 远端服务是否正常 | `GET https://market.ft.tech/gateway/api/v1/market/data/stock-candlesticks` | 不携带 |
76
+ | `validation.remote` | 远端服务是否正常 | `GET https://market.ft.tech/gateway/api/v1/market/data/stock-candlesticks` | 配置了 Key 时携带;未配置时匿名 |
77
77
  | `validation.apiKey` | 当前 Key 是否通过认证 | `GET https://market.ft.tech/gateway/api/v2/market/data/stock_minutes` | 使用 `FTSHARE_API_KEY` 请求头 |
78
78
 
79
+ 远端检查也携带 Key:网关的匿名额度按出口 IP 计(500 次/24 小时),用尽后未携带 Key 的请求一律 429。若远端检查仍走匿名,用户在已经配好可用 Key 的情况下会看到「请求受限、Key 未验证」,把 Key 问题和服务额度问题混在一起。携带 Key 后远端检查照旧只看 HTTP/业务码与 `data` 数组,认证与权限失败按下面规则归到 Key 上。
80
+
79
81
  日 K 请求使用 `600519.SH`、day、不复权、最近 30 天、`limit=2`;历史分钟请求使用同一股票、1 分钟、不复权、最近 24 小时、`limit=1`。服务端实际限制为包含首尾的三个北京时间自然日,而非滚动 72 小时;24 小时最多覆盖两个自然日。`adjust_kind` 使用实测接受的小写 `none`;文档中的 `None` 会被实际接口以 HTTP 400 拒绝。参数固定,不查交易日历、不回溯补数据、不验证 OHLC 字段。
80
82
 
81
83
  **成功条件是 HTTP 成功、业务 `code=200` 且 `data` 为数组;数组为空同样通过。** 本插件检查服务和认证结果,不检查行情数据完整性。HTTP 200 中的业务失败、错误响应结构不能当作成功。响应中的行情不公开返回、不持久化。
@@ -83,6 +85,7 @@ const result = await apiKey.configureAndTest(userInput)
83
85
  - 远端正常、Key 通过:`status=passed`;两项均为 `available`。
84
86
  - 远端正常、Key 无效/未配置/受限:`status=partial`;`remote` 仍为 `available`,检查 `apiKey.status` 获取原因。不能把 `partial` 当作 Key 验证通过。
85
87
  - 远端检查失败:`status=failed`;有 Key 时跳过验证,`apiKey.status=not_run`,不把服务故障归咎于 Key。
88
+ - 远端检查携带 Key 时,其 401/403 按认证、权限、额度原因分类;400/422 仍提示参数被拒,不代表 Key 无效。未配置 Key 的匿名远端检查遇到 401/403 记 `upstream_error`(服务拒绝匿名访问),不据此指控 Key。
86
89
  - 未配置 Key:仍执行匿名远端检查,Key 项显示 `auth_required` / “未配置 API Key”,不发送分钟请求。
87
90
  - 明确的 401 认证错误显示“API Key 未通过认证”;权限、额度、限流等保留独立原因,不能一律判为 Key 无效,也不能据此宣称认证成功。
88
91
 
package/lib/client.js CHANGED
@@ -14,8 +14,7 @@ window.__ModuleLoader__.load({
14
14
  if (value.length > 512 || /[\x00-\x1f\x7f]/.test(value)) throw new Error("API Key 长度不能超过 512 个字符,且不能包含控制字符");
15
15
  return value;
16
16
  }
17
- //#endregion
18
- //#region src/core/contract.ts
17
+ // src/core/contract.ts
19
18
  /**
20
19
  * Identity of the settings section that carries this plugin's cards.
21
20
  *
@@ -44,8 +43,7 @@ window.__ModuleLoader__.load({
44
43
  * the call itself.
45
44
  */
46
45
  const FTSHARE_SETTINGS_ROUTE = "/api/ftshare-settings";
47
- //#endregion
48
- //#region src/features/api-key/model.ts
46
+ // src/features/api-key/model.ts
49
47
  var FtshareSettingsModel = class {
50
48
  api;
51
49
  state = {
@@ -179,8 +177,7 @@ window.__ModuleLoader__.load({
179
177
  not_tested: "尚未测试"
180
178
  }[status] ?? "请查看分项结果";
181
179
  }
182
- //#endregion
183
- //#region src/core/rpc-core.ts
180
+ // src/core/rpc-core.ts
184
181
  /**
185
182
  * 把一次失败的信封翻译成调用方能展示的原因。
186
183
  *
@@ -200,8 +197,7 @@ window.__ModuleLoader__.load({
200
197
  if (!result.ok) throw new Error(describeSettingsFailure(endpoint, result.error));
201
198
  return result.value;
202
199
  }
203
- //#endregion
204
- //#region src/features/api-key/client-api.ts
200
+ // src/features/api-key/client-api.ts
205
201
  /** Browser adapter deliberately has no resolve/createEnvironment method. */
206
202
  function createApiKeyClient(transport) {
207
203
  async function call(method, payload = {}) {
@@ -216,8 +212,7 @@ window.__ModuleLoader__.load({
216
212
  getValidationStatus: () => call("getValidationStatus")
217
213
  };
218
214
  }
219
- //#endregion
220
- //#region src/core/client-transport.ts
215
+ // src/core/client-transport.ts
221
216
  /**
222
217
  * Browser half of the settings transport.
223
218
  *
@@ -245,8 +240,7 @@ window.__ModuleLoader__.load({
245
240
  return await response.json();
246
241
  } };
247
242
  }
248
- //#endregion
249
- //#region src/features/api-key/styles.ts
243
+ // src/features/api-key/styles.ts
250
244
  /**
251
245
  * The API Key card's own styles, rendered by the card component's `<style>`.
252
246
  *
@@ -271,8 +265,7 @@ window.__ModuleLoader__.load({
271
265
  .ftshare-settings .ftshare-error { color:var(--ftshare-error); }
272
266
  @media(max-width:540px) { .ftshare-settings .ftshare-card { padding:16px; } .ftshare-settings .ftshare-result { grid-template-columns:1fr; gap:4px; } }
273
267
  `;
274
- //#endregion
275
- //#region src/features/api-key/card.tsx
268
+ // src/features/api-key/card.tsx
276
269
  /**
277
270
  * Contribute the built-in API Key card through the same slot a third-party
278
271
  * card uses. Nothing about it is privileged: same declaration, same options.
@@ -422,8 +415,7 @@ window.__ModuleLoader__.load({
422
415
  ]
423
416
  })] });
424
417
  }
425
- //#endregion
426
- //#region src/features/logs/contract.ts
418
+ // src/features/logs/contract.ts
427
419
  /**
428
420
  * `logs` 命名空间的共享契约:只有常量与类型,浏览器半边可以安全导入。
429
421
  *
@@ -444,8 +436,7 @@ window.__ModuleLoader__.load({
444
436
  * 它随 owner fiber 释放(`ctx.effect` + `register` 返回的 disposer)。
445
437
  */
446
438
  const LOGS_ARCHIVE_ROUTE = "/api/ftshare-settings/logs.zip";
447
- //#endregion
448
- //#region src/features/logs/client-api.ts
439
+ // src/features/logs/client-api.ts
449
440
  /** 浏览器适配器:只读,且不暴露本地路径以外的任何 Host 能力(刷盘与释放句柄)。 */
450
441
  function createLogsClient(transport) {
451
442
  const call = (method, payload = {}) => callSettingsRpc(transport, "logs", method, payload);
@@ -455,8 +446,7 @@ window.__ModuleLoader__.load({
455
446
  export: () => call("export")
456
447
  };
457
448
  }
458
- //#endregion
459
- //#region src/features/logs/styles.ts
449
+ // src/features/logs/styles.ts
460
450
  /**
461
451
  * The `logs` card's own styles, rendered by the card component's `<style>`.
462
452
  *
@@ -470,8 +460,7 @@ window.__ModuleLoader__.load({
470
460
  .ftshare-settings .ftshare-download { display:inline-flex; align-items:center; min-height:32px; padding:0 14px; border:0.5px solid var(--ftshare-control); border-radius:var(--dsw-radius-md); color:inherit; text-decoration:none; cursor:pointer; }
471
461
  .ftshare-settings .ftshare-log-preview { max-height:320px; overflow:auto; margin:0 0 12px; padding:12px; background:var(--ftshare-input); border:0.5px solid var(--ftshare-border); border-radius:var(--dsw-radius-lg); font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:12px; line-height:1.5; white-space:pre-wrap; overflow-wrap:anywhere; }
472
462
  `;
473
- //#endregion
474
- //#region src/features/logs/card.tsx
463
+ // src/features/logs/card.tsx
475
464
  /** 预览行数候选:上限由 Host 侧校验兜底。 */
476
465
  const PREVIEW_CHOICES = [
477
466
  50,
@@ -668,8 +657,7 @@ window.__ModuleLoader__.load({
668
657
  ]
669
658
  })] });
670
659
  }
671
- //#endregion
672
- //#region src/core/styles.ts
660
+ // src/core/styles.ts
673
661
  /**
674
662
  * The section shell's styles plus the theme tokens every card reads.
675
663
  *
@@ -713,8 +701,7 @@ window.__ModuleLoader__.load({
713
701
  .ftshare-settings a { color:var(--ftshare-link); }
714
702
  @media(max-width:540px) { .ftshare-settings header { flex-direction:column; gap:10px; } }
715
703
  `;
716
- //#endregion
717
- //#region src/core/shell.tsx
704
+ // src/core/shell.tsx
718
705
  /** Section slot this seam contributes into; declared by the settings shell. */
719
706
  const FTSHARE_SETTINGS_SECTION_NAME = "settings.section";
720
707
  /**
@@ -743,8 +730,7 @@ window.__ModuleLoader__.load({
743
730
  } }
744
731
  }, FtshareSettingsSection));
745
732
  }
746
- //#endregion
747
- //#region src/brand.ts
733
+ // src/brand.ts
748
734
  /** One value for both palettes: brand fills are scheme-invariant. */
749
735
  const invariant = (value) => ({
750
736
  light: value,
@@ -796,8 +782,7 @@ window.__ModuleLoader__.load({
796
782
  scope.effect(() => scope.theme.overrideTokens(FT_BRAND_SOURCE, FT_BRAND_TOKENS), "ft-settings: FT brand tokens");
797
783
  });
798
784
  }
799
- //#endregion
800
- //#region src/client.tsx
785
+ // src/client.tsx
801
786
  const inject = ["slots", "remote"];
802
787
  function apply(ctx) {
803
788
  registerFtshareSettings(ctx);
@@ -809,7 +794,6 @@ window.__ModuleLoader__.load({
809
794
  registerApiKeyCard(ctx);
810
795
  registerLogsCard(ctx);
811
796
  }
812
- //#endregion
813
797
  exports.FtshareSettingsSection = FtshareSettingsSection;
814
798
  exports.apply = apply;
815
799
  exports.inject = inject;
package/lib/client.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":["useMemo","useSyncExternalStore","useState","useState","useCallback"],"sources":["../src/features/api-key/contract.ts","../src/core/contract.ts","../src/features/api-key/model.ts","../src/core/rpc-core.ts","../src/features/api-key/client-api.ts","../src/core/client-transport.ts","../src/features/api-key/styles.ts","../src/features/api-key/card.tsx","../src/features/logs/contract.ts","../src/features/logs/client-api.ts","../src/features/logs/styles.ts","../src/features/logs/card.tsx","../src/core/styles.ts","../src/core/shell.tsx","../src/brand.ts","../src/client.tsx"],"sourcesContent":["/**\n * API Key namespace contract: the credential reference and lifecycle events,\n * the status/validation vocabulary, and the two faces the namespace exposes\n * (the browser-safe subset and the host-only superset).\n *\n * It merges its own line into the generic {@link FtshareSettingsNamespaces}\n * merge point, so `ctx.ftshareSettings.apiKey` keeps its existing type for\n * every consumer.\n */\nexport const FTSHARE_API_KEY_REF = 'FTSHARE_API_KEY' as const\nexport const FTSHARE_CREDENTIAL_EVENTS = ['credentials/reference-updated'] as const\nexport const API_KEY_METHODS = ['describe', 'set', 'clear', 'configureAndTest', 'testConnection', 'getValidationStatus'] as const\nexport type ApiKeyMethod = typeof API_KEY_METHODS[number]\nexport type SubscribeChanges = (listener: () => void) => () => void\nexport interface CredentialStatus { configured: boolean; writable: boolean; source?: string }\nexport type ProbeState = 'available' | 'auth_required' | 'auth_failed' | 'permission_denied' | 'quota_exceeded' | 'rate_limited' | 'network_error' | 'timeout' | 'upstream_error' | 'invalid_response' | 'not_run'\nexport interface ProbeResult { status: ProbeState; message: string }\nexport interface ValidationResult {\n status: 'not_tested' | 'not_run' | 'passed' | 'partial' | 'failed' | 'superseded'\n remote: ProbeResult\n apiKey: ProbeResult\n testedAt?: string\n}\nexport interface ApiKeyStatus extends CredentialStatus {\n /** Display only. Fixed mask plus last four characters; never submit as a Key. */\n maskedValue?: string\n revision: number\n validation: ValidationResult\n}\nexport interface ConfigureResult {\n save: { status: 'saved' | 'failed'; code?: 'invalid_input' | 'read_only' | 'save_failed'; message: string }\n validation: ValidationResult\n}\n/** Local syntax validation, never an assertion of upstream validity. */\nexport function normalizeApiKey(input: string): string {\n const value = input.trim()\n if (!value) throw new Error('请输入 API Key')\n if (value.length > 512 || /[\\x00-\\x1f\\x7f]/.test(value)) throw new Error('API Key 长度不能超过 512 个字符,且不能包含控制字符')\n return value\n}\nexport function untested(status: 'not_tested' | 'not_run' | 'superseded' = 'not_tested', message = '尚未测试'): ValidationResult {\n return { status, remote: { status: 'not_run', message }, apiKey: { status: 'not_run', message } }\n}\n/** Everything the browser adapter may call; `resolve`/`createEnvironment` are not here. */\nexport interface ApiKeyPublicApi {\n describe(): Promise<ApiKeyStatus>\n set(value: string): Promise<void>\n clear(): Promise<void>\n configureAndTest(value: string): Promise<ConfigureResult>\n testConnection(): Promise<ValidationResult>\n getValidationStatus(): Promise<ValidationResult>\n}\n/** The host namespace object: the public API plus the secret-bearing host methods. */\nexport interface ApiKeySettings extends ApiKeyPublicApi {\n readonly credentialRef: typeof FTSHARE_API_KEY_REF\n /** Host only. Re-resolve per operation; never send this value to browsers. */\n resolve(): Promise<string | undefined>\n createEnvironment(base?: Readonly<Record<string, string | undefined>>): Promise<Record<string, string | undefined>>\n subscribe: SubscribeChanges\n}\ndeclare module '../../core/contract.js' {\n interface FtshareSettingsNamespaces { apiKey: ApiKeySettings }\n}\n","/**\n * Generic half of the settings contract: the seam's identity, its transport\n * constants, and the service shape. It carries no feature vocabulary — a\n * feature's own contract lives beside that feature and merges into\n * {@link FtshareSettingsNamespaces}.\n */\nimport type { FtshareSettingsModuleRegistry } from './module-registry.js'\nexport type { FtshareSettingsModuleRegistry, SettingsModule, SettingsRegistration } from './module-registry.js'\nexport type { SettingsRpcMethod, SettingsRpcMethods } from './rpc-core.js'\n/** Protocol version of the settings route and envelope. */\nexport const FTSHARE_SETTINGS_API_VERSION = 1 as const\n/**\n * Identity of the settings section that carries this plugin's cards.\n *\n * The id replaces the old `ftshare`. The label is a neutral product name, not\n * the internal brand: the page ships publicly, and the data source it\n * integrates is named inside the cards instead.\n */\nexport const FTSHARE_SETTINGS_SECTION = { id: 'dsh-fft', order: 120, label: '金融终端' } as const\n/**\n * Child slot of {@link FTSHARE_SETTINGS_SECTION} that carries its cards.\n *\n * The section declares the slot when it registers; every card — ours and a\n * third party's — contributes through it with `id`/`order`/`label`, and\n * `order` (ascending, default 100) decides the card's position.\n */\nexport const FTSHARE_SETTINGS_CARD_SLOT = 'ftshare.settings.card' as const\n/**\n * Browser-facing route for this plugin's settings calls.\n *\n * It sits below `/api` so the shared connection carrier applies the Host/Origin\n * fence and browser authentication before dispatch; this plugin only handles\n * the call itself.\n */\nexport const FTSHARE_SETTINGS_ROUTE = '/api/ftshare-settings' as const\n/** Largest request body accepted on {@link FTSHARE_SETTINGS_ROUTE}. */\nexport const FTSHARE_SETTINGS_MAX_BODY_BYTES = 64 * 1024\n/**\n * Merge point for namespaces a package reads but does not produce. A producer\n * gets its type from `register` inference; a consumer that reads someone\n * else's namespace declares one line, e.g. `cockpit: CockpitSettings`. Each\n * built-in feature merges its own line from its own feature directory.\n */\nexport interface FtshareSettingsNamespaces {}\n/**\n * Host service published as `ctx.ftshareSettings`.\n *\n * `apiVersion` and `modules` are permanent; every other member appears only\n * while its namespace is registered, so consumers must read properties at call\n * time instead of caching them during their own apply.\n */\nexport type FtshareSettingsService =\n & {\n readonly apiVersion: typeof FTSHARE_SETTINGS_API_VERSION\n readonly modules: FtshareSettingsModuleRegistry\n }\n & { readonly [N in keyof FtshareSettingsNamespaces & string]: FtshareSettingsNamespaces[N] }\n","import { normalizeApiKey, type ApiKeyPublicApi, type ApiKeyStatus, type ConfigureResult, type SubscribeChanges } from './contract.js'\nexport interface SettingsState { status?: ApiKeyStatus; busy: boolean; message: string; statusError?: string; save?: ConfigureResult['save'] }\nexport class FtshareSettingsModel {\n private state: SettingsState = { busy: false, message: '' }\n private listeners = new Set<() => void>()\n private request = 0\n private lifetime = 0\n constructor(private api: ApiKeyPublicApi) {}\n getSnapshot = (): SettingsState => this.state\n subscribe = (listener: () => void): (() => void) => { this.listeners.add(listener); return () => { this.listeners.delete(listener) } }\n private update(patch: Partial<SettingsState>): void { this.state = { ...this.state, ...patch }; for (const listener of this.listeners) listener() }\n connect(subscribe?: SubscribeChanges): () => void {\n const dispose = subscribe?.(() => { void this.refresh() })\n void this.refresh()\n return () => { this.request++; this.lifetime++; dispose?.() }\n }\n async refresh(): Promise<void> {\n const request = ++this.request\n try {\n const status = await this.api.describe()\n if (!status || typeof status.configured !== 'boolean') throw new Error('missing_status')\n if (request === this.request) this.update({ status, statusError: undefined })\n } catch { if (request === this.request) this.update({ status: undefined, statusError: '无法读取设置状态,请刷新重试' }) }\n }\n async configureAndTest(input: string): Promise<boolean> {\n if (this.state.busy || !this.state.status?.writable) return false\n let value: string\n try { value = normalizeApiKey(input) } catch (error) { this.update({ message: (error as Error).message }); return false }\n const lifetime = this.lifetime\n this.update({ busy: true, save: undefined, message: '正在保存并检查远端服务和 API Key…' })\n let saved = false\n try {\n const result = await this.api.configureAndTest(value)\n saved = result.save.status === 'saved'\n if (lifetime === this.lifetime) this.update({ save: result.save, message: saved ? validationMessage(result.validation.status) : result.save.message })\n } catch { if (lifetime === this.lifetime) this.update({ message: '请求未完成,保存状态待确认;请刷新后测试当前连接' }) }\n if (lifetime === this.lifetime) { await this.refresh(); this.update({ busy: false }) }\n return saved\n }\n async testConnection(): Promise<void> {\n if (this.state.busy || !this.state.status) return\n const lifetime = this.lifetime\n this.update({ busy: true, message: '正在检查远端服务和当前 API Key…' })\n try {\n const result = await this.api.testConnection()\n if (lifetime === this.lifetime) this.update({ message: validationMessage(result.status) })\n } catch { if (lifetime === this.lifetime) this.update({ message: '测试请求失败,请重试' }) }\n if (lifetime === this.lifetime) { await this.refresh(); this.update({ busy: false }) }\n }\n async clear(): Promise<void> {\n if (this.state.busy || !this.state.status?.writable) return\n const lifetime = this.lifetime\n this.update({ busy: true, save: undefined, message: '' })\n try {\n await this.api.clear()\n if (lifetime === this.lifetime) this.update({ message: '已清除 DSH 保存的密钥;若有 .env 配置,下方显示其有效状态' })\n } catch { if (lifetime === this.lifetime) this.update({ message: '清除失败,请刷新状态后重试' }) }\n if (lifetime === this.lifetime) { await this.refresh(); this.update({ busy: false }) }\n }\n}\nfunction validationMessage(status: string): string {\n return ({ passed: '远端服务正常,API Key 验证通过', partial: '远端服务正常,请查看 API Key 检查结果', failed: '测试未通过,请查看分项结果', not_run: '未执行测试,请先确认凭据配置', superseded: '凭据已变化,请重新测试', not_tested: '尚未测试' } as Record<string, string>)[status] ?? '请查看分项结果'\n}\n","import type { FTSHARE_SETTINGS_ROUTE } from './contract.js'\nexport interface RpcResult<T> { ok: boolean; value?: T; error?: { code: string; message: string; details: Record<string, unknown> } }\n/** Browser caller for this plugin's route. */\nexport interface ClientTransport { call(endpoint: string, payload?: unknown, signal?: AbortSignal): Promise<RpcResult<unknown>> }\nexport type RpcHandler = (endpoint: string, payload: unknown, signal: AbortSignal) => Promise<RpcResult<unknown>>\n/** Structural shape of one exact Fetch route; `connection.fetch.register` accepts it. */\nexport interface SettingsRoute {\n readonly path: typeof FTSHARE_SETTINGS_ROUTE\n readonly methods: readonly string[]\n readonly requestBody: 'buffered' | 'streaming'\n readonly fetch: (request: Request) => Promise<Response>\n}\n/** Host registry for exact Fetch routes on the shared `/api` carrier. */\nexport interface HostFetch { register(route: SettingsRoute): () => Promise<void> }\nexport interface SettingsRpcMethod {\n validate(payload: unknown): boolean\n invoke(payload: unknown, signal: AbortSignal): unknown | Promise<unknown>\n}\nexport type SettingsRpcMethods = Readonly<Record<string, SettingsRpcMethod>>\nexport interface SettingsRpcModule { namespace: string; rpcMethods: SettingsRpcMethods }\n/**\n * Live dispatch table behind {@link createSettingsRpcHandler}: routes appear\n * when a namespace is registered and disappear when it is released, so the one\n * route mounted at boot keeps serving namespaces that arrive later.\n */\nexport interface SettingsRpcTable {\n /** Validate one whole module, then publish its routes; a failure writes nothing. */\n register(module: SettingsRpcModule): void\n /** Remove one namespace and every route under it; unknown namespaces are a no-op. */\n unregister(namespace: string): void\n /** Namespaces currently owning at least one route, in registration order. */\n namespaces(): readonly string[]\n}\n/** The dispatcher callable plus the increment/decrement surface of its route table. */\nexport type SettingsRpcHandler = RpcHandler & SettingsRpcTable\nconst fail = (code: string, message: string): RpcResult<unknown> => ({ ok: false, error: { code, message, details: {} } })\n/** Names the service object and the route table reserve for themselves. */\nexport const SETTINGS_RESERVED_NAMES = ['apiVersion', 'modules', 'constructor', 'prototype', '__proto__'] as const\nexport function assertSettingsName(name: string): void {\n if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(name) || (SETTINGS_RESERVED_NAMES as readonly string[]).includes(name)) throw new Error(`Invalid settings name: ${name}`)\n}\n/** Validate every endpoint part of one module without writing to any table. */\nexport function assertSettingsRpcModule(module: SettingsRpcModule): void {\n assertSettingsName(module.namespace)\n for (const name of Object.keys(module.rpcMethods)) assertSettingsName(name)\n}\n/**\n * Build the live dispatcher for this plugin's route.\n *\n * Only explicitly registered methods are callable; never reflects over a\n * service. The returned function carries its own route table, so the caller\n * mounts the route once and mutates through `register` / `unregister`\n * afterwards. Each `register` validates the whole module first — namespace\n * format, reserved names, duplicates, and every method name — and writes\n * nothing when any check fails.\n * @param modules - optional initial namespaces, registered in order.\n * @returns dispatcher with the live route table attached.\n */\nexport function createSettingsRpcHandler(modules: readonly SettingsRpcModule[] = []): SettingsRpcHandler {\n const routes = new Map<string, SettingsRpcMethod>()\n const namespaces = new Set<string>()\n const register = (module: SettingsRpcModule): void => {\n assertSettingsRpcModule(module)\n if (namespaces.has(module.namespace)) throw new Error(`Duplicate settings namespace: ${module.namespace}`)\n // Committed: every check above ran before the first write.\n namespaces.add(module.namespace)\n for (const [name, method] of Object.entries(module.rpcMethods)) routes.set(`${module.namespace}/${name}`, method)\n }\n const unregister = (namespace: string): void => {\n if (!namespaces.delete(namespace)) return\n const prefix = `${namespace}/`\n for (const endpoint of [...routes.keys()]) if (endpoint.startsWith(prefix)) routes.delete(endpoint)\n }\n const handler = (async (endpoint: string, payload: unknown, signal: AbortSignal): Promise<RpcResult<unknown>> => {\n const method = routes.get(endpoint)\n if (!method) return fail('not_found', '接口不存在')\n if (signal.aborted) return fail('cancelled', '请求已取消')\n try {\n if (!method.validate(payload)) return fail('bad_request', '请求参数不正确')\n return { ok: true, value: (await method.invoke(payload, signal)) ?? null }\n } catch { return fail('operation_failed', '操作失败,请刷新状态后重试') }\n }) as SettingsRpcHandler\n handler.register = register\n handler.unregister = unregister\n handler.namespaces = () => [...namespaces]\n for (const module of modules) register(module)\n return handler\n}\n/**\n * 把一次失败的信封翻译成调用方能展示的原因。\n *\n * `not_found` 是「浏览器半边比宿主半边新」的可观测症状:升级插件后客户端 bundle 由磁盘重新\n * 加载,而宿主进程仍持有旧包,它没有新命名空间。此时重试永远不会成功,必须点名重启宿主。\n * @param endpoint - 这次调用的 `<namespace>/<method>`。\n * @param error - 宿主回传的失败原因,畸形信封下可能缺省。\n * @returns 面向用户的失败描述。\n */\nfunction describeSettingsFailure(endpoint: string, error: RpcResult<unknown>['error']): string {\n if (error?.code === 'not_found') return `FTShare 设置接口 ${endpoint} 不存在:宿主插件还是升级前的版本,重启 DSH 后再试`\n return `FTShare 设置操作失败(${error?.code ?? 'unknown'}):${error?.message ?? '宿主未说明原因'}`\n}\nexport async function callSettingsRpc<T>(transport: ClientTransport, namespace: string, method: string, payload: unknown = {}): Promise<T> {\n const endpoint = `${namespace}/${method}`\n const result = await transport.call(endpoint, payload)\n if (!result.ok) throw new Error(describeSettingsFailure(endpoint, result.error))\n return result.value as T\n}\n","import type { ApiKeyPublicApi, ApiKeyMethod } from './contract.js'\nimport { callSettingsRpc, type ClientTransport } from '../../core/rpc-core.js'\n/** Browser adapter deliberately has no resolve/createEnvironment method. */\nexport function createApiKeyClient(transport: ClientTransport): ApiKeyPublicApi {\n async function call<T>(method: ApiKeyMethod, payload: unknown = {}): Promise<T> {\n return callSettingsRpc<T>(transport, 'apiKey', method, payload)\n }\n return {\n describe: () => call('describe'), set: value => call('set', { value }), clear: () => call('clear'),\n configureAndTest: value => call('configureAndTest', { value }), testConnection: () => call('testConnection'), getValidationStatus: () => call('getValidationStatus'),\n }\n}\n","/**\n * Browser half of the settings transport.\n *\n * Calls are same-origin fetches to the plugin route, so the browser session\n * cookie carries authentication and this plugin needs no Connection RPC client.\n */\n\nimport { FTSHARE_SETTINGS_ROUTE } from './contract.js'\nimport type { ClientTransport, RpcResult } from './rpc-core.js'\n\n/** Transport this caller posts through; same signature as the global `fetch`. */\nexport type SettingsFetch = (input: string, init: RequestInit) => Promise<Response>\n\n/**\n * Create the browser-backed caller used by the settings cards.\n * @param doFetch - transport override; defaults to the page's global fetch.\n * @returns caller that owns request encoding and envelope decoding.\n */\nexport function createSettingsTransport(doFetch?: SettingsFetch): ClientTransport {\n const send: SettingsFetch = doFetch ?? ((input, init) => globalThis.fetch(input, init))\n return {\n async call(endpoint, payload = {}, signal) {\n const response = await send(FTSHARE_SETTINGS_ROUTE, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ endpoint, payload }),\n ...signal === undefined ? {} : { signal },\n })\n if (!response.ok) {\n // The carrier answers 401/403 before dispatch; anything else is a route\n // or plugin-level failure. Both are transport failures for the caller.\n throw new Error(`transport failure for ${endpoint}: HTTP ${response.status}`)\n }\n return await response.json() as RpcResult<unknown>\n },\n }\n}\n","/**\n * The API Key card's own styles, rendered by the card component's `<style>`.\n *\n * A card owns everything past the section shell: the core declares the\n * `--ftshare-*` tokens and the section container, never card internals.\n * Every selector stays scoped under `.ftshare-settings`, so a card never\n * styles another card's markup.\n */\nexport const API_KEY_CARD_CSS = `\n.ftshare-settings .ftshare-eyebrow { color:var(--ftshare-accent-on-surface); font-size:11px; letter-spacing:.12em; }\n.ftshare-settings .ftshare-card { padding:22px; background:var(--ftshare-panel); border:0.5px solid var(--ftshare-border); border-radius:var(--dsw-radius-xl); margin-top:16px; }\n.ftshare-settings label { display:block; font-weight:500; }\n.ftshare-settings input { width:100%; height:32px; border:0.5px solid var(--ftshare-control); border-radius:var(--dsw-radius-md); padding:0 12px; background:var(--ftshare-input); color:inherit; font:inherit; }\n.ftshare-settings input::placeholder { color:var(--ftshare-muted); opacity:1; }\n.ftshare-settings .ftshare-actions { display:flex; flex-wrap:wrap; gap:10px; align-items:center; margin-top:16px; }\n.ftshare-settings button { min-height:36px; border:0.5px solid var(--ftshare-control); border-radius:var(--dsw-radius-md); padding:0 14px; font:inherit; background:transparent; color:inherit; cursor:pointer; }\n.ftshare-settings button.primary { background:var(--ftshare-accent); color:var(--ftshare-accent-ink); border-color:var(--ftshare-accent); font-weight:500; }\n.ftshare-settings button:disabled { opacity:.5; cursor:not-allowed; }\n.ftshare-settings dl { margin:0; }\n.ftshare-settings .ftshare-result { display:grid; grid-template-columns:130px 1fr; gap:12px; padding:10px 0; border-top:0.5px solid var(--ftshare-border); }\n.ftshare-settings dd { margin:0; overflow-wrap:anywhere; }\n.ftshare-settings .ftshare-error { color:var(--ftshare-error); }\n@media(max-width:540px) { .ftshare-settings .ftshare-card { padding:16px; } .ftshare-settings .ftshare-result { grid-template-columns:1fr; gap:4px; } }\n`\n","import { useEffect, useMemo, useState, useSyncExternalStore, type ReactNode } from 'react'\nimport { FTSHARE_API_KEY_REF, FTSHARE_CREDENTIAL_EVENTS, type SubscribeChanges, type ApiKeyPublicApi } from './contract.js'\nimport { FTSHARE_SETTINGS_CARD_SLOT } from '../../core/contract.js'\nimport { FtshareSettingsModel } from './model.js'\nimport { createApiKeyClient } from './client-api.js'\nimport { createSettingsTransport } from '../../core/client-transport.js'\nimport type { FtshareSettingsShellContext } from '../../core/shell.js'\nimport { API_KEY_CARD_CSS } from './styles.js'\n\n/**\n * Browser context this card requires: the shell's slots service plus the\n * invalidations that make a cached credential status stale.\n */\nexport interface ApiKeyCardContext extends FtshareSettingsShellContext {\n remote: { $on(event: string, listener: (ref: string) => void): () => void }\n on(event: 'connection/reset', listener: () => void): () => void\n}\n\n/**\n * Contribute the built-in API Key card through the same slot a third-party\n * card uses. Nothing about it is privileged: same declaration, same options.\n * @param ctx - client context carrying the renderer's slots service.\n * @returns idempotent disposer for the card registration.\n */\nexport function registerApiKeyCard(ctx: ApiKeyCardContext): () => void {\n return ctx.slots.inject(FTSHARE_SETTINGS_CARD_SLOT, () => ctx.slots.register(\n { name: FTSHARE_SETTINGS_CARD_SLOT, id: 'apiKey', order: 10 },\n createApiKeyCard(ctx),\n ))\n}\n\n/** Bind the card's adapters; subscriptions are acquired in the component's effect. */\nfunction createApiKeyCard(ctx: ApiKeyCardContext): () => ReactNode {\n const api = createApiKeyClient(createSettingsTransport())\n const subscribeChanges: SubscribeChanges = listener => {\n const disposers = FTSHARE_CREDENTIAL_EVENTS.map(event => ctx.remote.$on(event, ref => { if (ref === FTSHARE_API_KEY_REF) listener() }))\n disposers.push(ctx.on('connection/reset', listener))\n return () => { for (const dispose of disposers) dispose() }\n }\n return () => <ApiKeyCard api={api} subscribeChanges={subscribeChanges} />\n}\n\nexport function ApiKeyCard({ api, subscribeChanges }: { readonly api: ApiKeyPublicApi; readonly subscribeChanges?: SubscribeChanges }): ReactNode {\n const model = useMemo(() => new FtshareSettingsModel(api), [api])\n const { status, busy, message, statusError, save } = useSyncExternalStore(model.subscribe, model.getSnapshot, model.getSnapshot)\n const [draft, setDraft] = useState('')\n useEffect(() => model.connect(subscribeChanges), [model, subscribeChanges])\n const disabled = busy || !status?.writable\n const configure = async () => { if (await model.configureAndTest(draft)) setDraft('') }\n return <>\n <style>{API_KEY_CARD_CSS}</style>\n <section className=\"ftshare-settings\" aria-label=\"FTShare 设置\" aria-busy={busy}>\n <header><div><span className=\"ftshare-eyebrow\">FTSHARE / API KEY</span><h2>访问凭据</h2></div></header>\n <p>目前集成 FTShare 数据源。</p>\n {status && !status.writable && <p>当前来源为只读,可测试连接;修改密钥请到对应来源操作。</p>}\n <div className=\"ftshare-card\"><label htmlFor=\"ftshare-api-key\">FTShare API Key</label>\n <input id=\"ftshare-api-key\" type=\"password\" value={draft} placeholder={status?.configured ? status.maskedValue ?? '••••••••' : '请输入 API Key'} onChange={e => setDraft(e.target.value)} disabled={disabled} autoComplete=\"new-password\" maxLength={512} />\n <div className=\"ftshare-actions\">\n <button className=\"primary\" type=\"button\" onClick={() => void configure()} disabled={disabled || !draft.trim()}>配置并测试</button>\n <button type=\"button\" onClick={() => void model.testConnection()} disabled={busy || !status}>测试当前连接</button>\n <button type=\"button\" onClick={() => void model.clear()} disabled={disabled || !status?.configured}>清除已保存密钥</button>\n <button type=\"button\" onClick={() => void model.refresh()} disabled={busy} title=\"重新读取已保存的配置和上次检测结果,不会发起联网检测\">重新读取状态</button>\n <a href=\"https://ftai.chat/me/profile?tab=api-key\" target=\"_blank\" rel=\"noreferrer\">获取 API Key ↗</a>\n </div>\n {draft && <small>“测试当前连接”使用已生效的密钥,不测试输入框内尚未保存的内容。</small>}\n {busy && <p role=\"status\">正在处理,请稍候…</p>}\n </div>\n <div className=\"ftshare-card\" aria-live=\"polite\"><h3>验证结果</h3><dl>\n {save && <div className=\"ftshare-result\"><dt>本次保存</dt><dd>{save.message}</dd></div>}\n <div className=\"ftshare-result\"><dt>远端服务</dt><dd>{status?.validation.remote.message ?? '状态待确认'}</dd></div>\n <div className=\"ftshare-result\"><dt>API Key</dt><dd>{status?.validation.apiKey.message ?? '状态待确认'}</dd></div>\n {status?.validation.testedAt && <div className=\"ftshare-result\"><dt>测试时间</dt><dd>{new Date(status.validation.testedAt).toLocaleString()}</dd></div>}\n </dl><small>节假日或收盘后的空数据不影响验证。</small></div>\n {message && <p role=\"status\">{message}</p>}{statusError && <p className=\"ftshare-error\" role=\"alert\">{statusError}</p>}\n </section>\n </>\n}\n","/**\n * `logs` 命名空间的共享契约:只有常量与类型,浏览器半边可以安全导入。\n *\n * 采集器(Host 常驻)与导出器(设置页)通过文件解耦;本文件是两者唯一的共同语言。\n */\n\n/**\n * 卡片在 `ftshare.settings.card` 槽位里的 id 与 order。\n *\n * 归 feature 所有(与业务契约同一份来源):卡片组件与浏览器侧的守卫测试都读它,\n * order 20 排在 API Key 卡(10)之后。\n */\nexport const LOGS_CARD_ID = 'logs'\nexport const LOGS_CARD_ORDER = 20\n\n/** 显式登记的 RPC 方法;未登记的方法一律 `not_found`。 */\nexport const LOGS_METHODS = ['stats', 'preview', 'export'] as const\nexport type LogsMethod = typeof LOGS_METHODS[number]\n\nexport type LogLevelName = 'error' | 'info' | 'warn' | 'debug'\n\n/**\n * 配置级别 → cordis `levels.default` 数值。\n *\n * cordis 的 `levels` 是**接收上限**而不是下限:`targetLevel < level` 才丢弃。\n * 因此 `warn`(2) 收 error/info/warn,`debug`(3) 收全部,`info`(1) 会把 `warn` 丢掉。\n */\nexport const LOG_LEVEL_THRESHOLD: Record<LogLevelName, number> = { error: 0, info: 1, warn: 2, debug: 3 }\nexport const LOG_LEVEL_NAMES = ['warn', 'info', 'debug', 'error'] as const\n\n/**\n * 可配置的级别(规格 §10)。\n *\n * 不提供 `info` 档:`levels` 是接收上限,`info`(1) 会把 `warn`(2) 一起丢掉,与规格 §6\n * 「失败日志不低于 warn」冲突;`error`(0) 同样不成立(等于只剩 error)。允许值收敛为\n * `warn | debug`,档位越界由 Schemastery 直接拒绝(插件不挂载),不做静默夹紧。\n */\nexport const LOGS_CONFIG_LEVELS = ['warn', 'debug'] as const\nexport type LogsConfigLevel = typeof LOGS_CONFIG_LEVELS[number]\n\n/** 落盘位置:`$DSH_HOME/logs/ft/`。 */\nexport const LOGS_DIR_SEGMENTS = ['logs', 'ft'] as const\nexport const LOGS_CURRENT_FILE = 'current.log'\nexport const LOGS_ROTATED_PREFIX = 'ft-'\nexport const LOGS_FILE_SUFFIX = '.log'\n\nexport const LOGS_LINE_MAX_BYTES = 8 * 1024\nexport const LOGS_TRUNCATED_MARK = '...[已截断]'\nexport const LOGS_REPEAT_FIELD = '重复次数'\nexport const LOGS_THROTTLE_WINDOW_MS = 10_000\nexport const LOGS_MAX_PENDING_KEYS = 2_000\nexport const LOGS_MAX_FIELD_LENGTH = 2_000\nexport const LOGS_MAX_FIELD_DEPTH = 3\n\nexport const LOGS_DEFAULT_PREVIEW_LINES = 80\nexport const LOGS_MAX_PREVIEW_LINES = 200\nexport const LOGS_PREVIEW_MAX_BYTES = 256 * 1024\n\n/** 导出体积上限(规格 §9):单来源 2 MiB、合计 8 MiB、目录最多 20 个 `*.log`。 */\nexport const LOGS_SOURCE_MAX_BYTES = 2 * 1024 * 1024\nexport const LOGS_EXPORT_MAX_BYTES = 8 * 1024 * 1024\nexport const LOGS_MAX_SOURCE_FILES = 20\n\nexport const LOGS_EXPORT_FILE_PREFIX = 'ft-runtime-'\nexport const LOGS_SECRET_TTL_MS = 5_000\n\n/**\n * 诊断包路由(规格 §9 / §12 决策 3):`/api` 载体上的第二条精确路由,归 `logs` feature 所有。\n *\n * 卡片用 `<a download>` 直接指向它:浏览器同源 GET 自带凭据,不需要 fetch/Blob,\n * `/api` 前缀的鉴权由载体负责,本 feature 不重复鉴权、也不读任何请求参数。\n * 它随 owner fiber 释放(`ctx.effect` + `register` 返回的 disposer)。\n */\nexport const LOGS_ARCHIVE_ROUTE = '/api/ftshare-settings/logs.zip' as const\n/** 归档内固定条目名(`manifest.json` 永远排第一)。 */\nexport const LOGS_ARCHIVE_MANIFEST = 'manifest.json'\nexport const LOGS_ARCHIVE_LOG = 'ft-runtime.log'\n/** 额外来源原文的目录前缀:`sources/<n>-<basename>`。 */\nexport const LOGS_ARCHIVE_SOURCE_DIR = 'sources/'\n\n/** 导出 header 里的来源段落分隔标记。 */\nexport const LOGS_SECTION_RULE = '====='\n\n/** 一条来源(文件或目录)在 Host 侧的存在性快照。 */\nexport interface LogsSourceState {\n readonly path: string\n readonly kind: 'file' | 'directory' | 'missing'\n readonly bytes?: number\n readonly files?: number\n}\n\n/** 导出物 header 里每个来源的统计。 */\nexport interface LogsSourceSummary {\n readonly label: string\n readonly path: string\n readonly lines: number\n readonly bytes: number\n readonly firstAt?: string\n readonly lastAt?: string\n readonly truncated: boolean\n readonly missing?: boolean\n}\n\n/** 采集器运行时统计;时间一律是北京时间 ISO 字符串。 */\nexport interface LogsStats {\n readonly file: string\n readonly fileBytes: number\n readonly level: LogLevelName\n readonly maxBytes: number\n readonly keep: number\n readonly records: number\n readonly dropped: number\n readonly rotations: number\n readonly writeFailures: number\n readonly firstAt?: string\n readonly lastAt?: string\n readonly byName: Readonly<Record<string, number>>\n readonly sources: readonly LogsSourceState[]\n}\n\nexport interface LogsPreview {\n readonly lines: readonly string[]\n readonly bytes: number\n readonly truncated: boolean\n readonly fileBytes: number\n}\n\nexport interface LogsExport {\n readonly fileName: string\n readonly text: string\n readonly bytes: number\n readonly truncated: boolean\n readonly sources: readonly LogsSourceSummary[]\n}\n\n/** 浏览器可用的只读日志接口。 */\nexport interface LogsPublicApi {\n stats(): Promise<LogsStats>\n preview(lines?: number): Promise<LogsPreview>\n /** 先刷盘,再把主日志与配置的 sources 拼成单文件文本。 */\n export(): Promise<LogsExport>\n}\n\n/** Host 服务:比浏览器多出本地路径与刷盘/释放句柄的能力。 */\nexport interface LogsSettings extends LogsPublicApi {\n readonly filePath: string\n flush(): Promise<void>\n dispose(): Promise<void>\n}\n","import {\n LOGS_DEFAULT_PREVIEW_LINES,\n type LogsExport,\n type LogsMethod,\n type LogsPreview,\n type LogsPublicApi,\n type LogsStats,\n} from './contract.js'\nimport { callSettingsRpc, type ClientTransport } from '../../core/rpc-core.js'\n\n/** 浏览器适配器:只读,且不暴露本地路径以外的任何 Host 能力(刷盘与释放句柄)。 */\nexport function createLogsClient(transport: ClientTransport): LogsPublicApi {\n const call = <T>(method: LogsMethod, payload: unknown = {}): Promise<T> =>\n callSettingsRpc<T>(transport, 'logs', method, payload)\n return {\n stats: () => call<LogsStats>('stats'),\n preview: (lines = LOGS_DEFAULT_PREVIEW_LINES) => call<LogsPreview>('preview', { lines }),\n export: () => call<LogsExport>('export'),\n }\n}\n","/**\n * The `logs` card's own styles, rendered by the card component's `<style>`.\n *\n * These two rules used to live in the core section stylesheet; a card owns\n * everything past the section shell, so they moved here with the card. Every\n * selector stays scoped under `.ftshare-settings`, so a card never styles\n * another card's markup.\n */\nexport const LOGS_CARD_CSS = `\n.ftshare-settings select { height:32px; border:0.5px solid var(--ftshare-control); border-radius:var(--dsw-radius-md); padding:0 10px; background:var(--ftshare-input); color:inherit; font:inherit; }\n.ftshare-settings .ftshare-download { display:inline-flex; align-items:center; min-height:32px; padding:0 14px; border:0.5px solid var(--ftshare-control); border-radius:var(--dsw-radius-md); color:inherit; text-decoration:none; cursor:pointer; }\n.ftshare-settings .ftshare-log-preview { max-height:320px; overflow:auto; margin:0 0 12px; padding:12px; background:var(--ftshare-input); border:0.5px solid var(--ftshare-border); border-radius:var(--dsw-radius-lg); font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:12px; line-height:1.5; white-space:pre-wrap; overflow-wrap:anywhere; }\n`\n","import { useCallback, useEffect, useState, type ReactNode } from 'react'\nimport {\n LOGS_ARCHIVE_ROUTE,\n LOGS_CARD_ID,\n LOGS_CARD_ORDER,\n LOGS_DEFAULT_PREVIEW_LINES,\n LOGS_MAX_PREVIEW_LINES,\n type LogsPreview,\n type LogsPublicApi,\n type LogsStats,\n} from './contract.js'\nimport { createLogsClient } from './client-api.js'\nimport { createSettingsTransport } from '../../core/client-transport.js'\nimport { FTSHARE_SETTINGS_CARD_SLOT } from '../../core/contract.js'\nimport type { FtshareSettingsShellContext } from '../../core/shell.js'\nimport { LOGS_CARD_CSS } from './styles.js'\n\n/** 预览行数候选:上限由 Host 侧校验兜底。 */\nconst PREVIEW_CHOICES = [50, LOGS_DEFAULT_PREVIEW_LINES, LOGS_MAX_PREVIEW_LINES]\n\n/**\n * 快照缺失时的占位文案。\n *\n * 失败后不能继续写「读取中…」:那会把一次已经结束的失败说成正在进行的读取,掩盖真正的\n * 原因(例如宿主插件没有这个接口,重试永远不会成功)。\n * @param failed - 本次读取是否已经失败。\n * @returns 失败提示,或仍在读取的提示。\n */\nexport function statsPlaceholder(failed: boolean): string {\n return failed ? '读取失败' : '读取中…'\n}\n\nfunction displayBytes(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`\n return `${(bytes / 1024 / 1024).toFixed(2)} MiB`\n}\n\n/** 把文本包成 Blob,用 a[download] 触发下载(Web 与桌面同一套)。 */\nfunction downloadText(fileName: string, text: string): void {\n const url = URL.createObjectURL(new Blob([text], { type: 'text/plain;charset=utf-8' }))\n const anchor = document.createElement('a')\n anchor.href = url\n anchor.download = fileName\n anchor.rel = 'noopener'\n document.body.appendChild(anchor)\n anchor.click()\n anchor.remove()\n setTimeout(() => URL.revokeObjectURL(url), 0)\n}\n\n/**\n * Contribute the built-in runtime-log card through the same slot a third-party\n * card uses. Nothing about it is privileged: same declaration, same options.\n * @param ctx - client context carrying the renderer's slots service.\n * @returns idempotent disposer for the card registration.\n */\nexport function registerLogsCard(ctx: FtshareSettingsShellContext): () => void {\n return ctx.slots.inject(FTSHARE_SETTINGS_CARD_SLOT, () => ctx.slots.register(\n { name: FTSHARE_SETTINGS_CARD_SLOT, id: LOGS_CARD_ID, order: LOGS_CARD_ORDER },\n createLogsCard(),\n ))\n}\n\n/** Bind the card's adapter; the component only reads through it. */\nfunction createLogsCard(): () => ReactNode {\n const api = createLogsClient(createSettingsTransport())\n return () => <LogsSection api={api} />\n}\n\nexport function LogsSection({ api }: { readonly api: LogsPublicApi }): ReactNode {\n const [stats, setStats] = useState<LogsStats>()\n const [preview, setPreview] = useState<LogsPreview>()\n const [lines, setLines] = useState(LOGS_DEFAULT_PREVIEW_LINES)\n const [busy, setBusy] = useState(false)\n const [message, setMessage] = useState('')\n const [error, setError] = useState('')\n /** 读取已失败且还没有任何快照时,每个字段都必须说出来,而不是假装还在读。 */\n const placeholder = statsPlaceholder(error !== '')\n\n const refresh = useCallback(async () => {\n setBusy(true)\n setError('')\n try {\n setStats(await api.stats())\n setPreview(await api.preview(lines))\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : '读取日志状态失败')\n } finally {\n setBusy(false)\n }\n }, [api, lines])\n\n useEffect(() => { void refresh() }, [refresh])\n\n const doExport = async () => {\n setBusy(true)\n setError('')\n setMessage('')\n try {\n const result = await api.export()\n downloadText(result.fileName, result.text)\n setMessage(`已导出 ${result.fileName}(${displayBytes(result.bytes)}${result.truncated ? ',已达体积上限并截断' : ''})`)\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : '导出日志失败')\n } finally {\n setBusy(false)\n }\n }\n\n return <>\n <style>{LOGS_CARD_CSS}</style>\n <section className=\"ftshare-settings\" aria-label=\"FT 运行日志\" aria-busy={busy}>\n <header><div><span className=\"ftshare-eyebrow\">FTSHARE / LOGS</span><h2>运行日志</h2></div></header>\n <div className=\"ftshare-card\">\n <h3>采集状态</h3>\n <dl>\n <div className=\"ftshare-result\"><dt>日志文件</dt><dd>{stats?.file ?? placeholder}</dd></div>\n <div className=\"ftshare-result\"><dt>记录 / 时间范围</dt><dd>\n {stats === undefined ? placeholder : `${stats.records} 条`}\n {stats?.firstAt !== undefined && `(${stats.firstAt} ~ ${stats.lastAt})`}\n </dd></div>\n <div className=\"ftshare-result\"><dt>文件大小 / 上限</dt><dd>\n {stats === undefined ? placeholder : `${displayBytes(stats.fileBytes)} / ${displayBytes(stats.maxBytes)}`}\n </dd></div>\n <div className=\"ftshare-result\"><dt>丢弃 / 轮转 / 写失败</dt><dd>\n {stats === undefined ? placeholder : `${stats.dropped} / ${stats.rotations} / ${stats.writeFailures}`}\n </dd></div>\n <div className=\"ftshare-result\"><dt>日志级别</dt><dd>{stats?.level ?? placeholder} </dd></div>\n <div className=\"ftshare-result\"><dt>统计</dt><dd>\n {stats === undefined\n ? placeholder\n : Object.keys(stats.byName).length === 0\n ? '暂无'\n : Object.entries(stats.byName).map(([name, count]) => `${name}=${count}`).join('、')}\n </dd></div>\n <div className=\"ftshare-result\"><dt>额外来源</dt><dd>\n {stats === undefined\n ? placeholder\n : stats.sources.length === 0\n ? '暂无'\n : stats.sources.map(source => `${source.path}(${source.kind}${source.bytes === undefined ? '' : ` ${displayBytes(source.bytes)}`})`).join(';')}\n </dd></div>\n </dl>\n <div className=\"ftshare-actions\">\n <button className=\"primary\" type=\"button\" onClick={() => void doExport()} disabled={busy}>导出日志</button>\n {/* 同源 GET,浏览器自带凭据:直接指向路由,不需要 fetch/Blob。 */}\n <a className=\"ftshare-download\" href={LOGS_ARCHIVE_ROUTE} download=\"\">导出诊断包</a>\n <button type=\"button\" onClick={() => void refresh()} disabled={busy}>刷新</button>\n <label htmlFor=\"ftshare-log-lines\">预览行数</label>\n <select id=\"ftshare-log-lines\" value={lines} onChange={e => setLines(Number(e.target.value))} disabled={busy}>\n {PREVIEW_CHOICES.map(choice => <option key={choice} value={choice}>{choice}</option>)}\n </select>\n </div>\n {busy && <p role=\"status\">正在处理,请稍候…</p>}\n {message && <p role=\"status\">{message}</p>}\n {error && <p className=\"ftshare-error\" role=\"alert\">{error}</p>}\n </div>\n <div className=\"ftshare-card\">\n <h3>最近日志{preview?.truncated === true ? '(已截断)' : ''}</h3>\n {preview === undefined\n ? <p>{error === '' ? '正在读取…' : '日志预览读取失败,见上方提示。'}</p>\n : preview.lines.length === 0\n ? <p>无</p>\n : <pre className=\"ftshare-log-preview\">{preview.lines.join('\\n')}</pre>}\n </div>\n </section>\n </>\n}\n","/**\n * The section shell's styles plus the theme tokens every card reads.\n *\n * The shell owns the container, typography, focus ring and link rules that\n * every card sits inside; a card owns its own CSS and renders it from its own\n * component, and core never styles card internals.\n *\n * The theme layer declares the plugin's `--ftshare-*` tokens once, on\n * `.ftshare-settings` — the root element every card renders. Surfaces and text\n * take their value from the host palette: `--dsw-alias-*` is what DSH\n * publishes for the light, the dark and every registered theme, so one\n * stylesheet follows the live palette with no script, no `prefers-color-scheme`\n * copy and no second palette to keep in sync. The literal inside each `var()`\n * is the fallback for a host that publishes no palette.\n *\n * The declarations sit on the card root rather than on `:root` because a\n * custom property resolves where it is *declared*: `--dsw-alias-*` exists from\n * `body` downwards, so a value derived from it above `body` would resolve to\n * nothing and fall back. Consequence: the place to pin a look is the host\n * palette itself (`ctx.theme.register` / `ctx.theme.overrideTokens`), not these\n * names.\n *\n * `--ftshare-accent*` is the plugin's own brand and has no host equivalent. The\n * values come from the FT brand layer's `--ft-*` tokens (`src/brand.ts`), with\n * the same literal as the fallback so the card keeps its colors when that layer\n * is absent. The fill and its ink are scheme-invariant;\n * `--ftshare-accent-on-surface` mixes the same yellow towards the host's text\n * colour, so the brand stays legible as the 11px eyebrow and as the focus ring\n * on either surface.\n */\nexport const SETTINGS_CSS = `\n.ftshare-settings { --ftshare-text:var(--dsw-alias-label-primary,#f4f4f5); --ftshare-muted:var(--dsw-alias-label-secondary,#aeb2bd); --ftshare-panel:var(--dsw-alias-settings-card-fill,#292a30); --ftshare-border:var(--dsw-alias-settings-card-stroke,#44464f); --ftshare-control:var(--dsw-alias-border-l4,#696c77); --ftshare-input:var(--dsw-alias-bg-layer-1,#1d1e23); --ftshare-link:var(--dsw-alias-link,#d8c4fc); --ftshare-error:var(--dsw-alias-state-error-primary,#ffb5ad); --ftshare-accent:var(--ft-accent,#f0c433); --ftshare-accent-ink:var(--ft-accent-ink,#171717); --ftshare-accent-on-surface:color-mix(in srgb,var(--ftshare-accent) 50%,var(--dsw-alias-label-primary,#f4f4f5)); }\n.ftshare-settings { color:var(--ftshare-text); max-width:760px; font-size:14px; line-height:1.6; }\n.ftshare-settings * { box-sizing:border-box; }\n.ftshare-settings h2 { font-size:26px; margin:4px 0 8px; }\n.ftshare-settings h3 { font-size:15px; margin:0 0 12px; }\n.ftshare-settings p { margin:8px 0; color:var(--ftshare-muted); }\n.ftshare-settings header { display:flex; justify-content:space-between; align-items:flex-start; gap:20px; margin-bottom:20px; }\n.ftshare-settings small { display:block; color:var(--ftshare-muted); font-size:12px; margin:5px 0 12px; }\n.ftshare-settings :is(button,input,a):focus-visible { outline:2px solid var(--ftshare-accent-on-surface); outline-offset:3px; }\n.ftshare-settings a { color:var(--ftshare-link); }\n@media(max-width:540px) { .ftshare-settings header { flex-direction:column; gap:10px; } }\n`\n","import type { ReactNode } from 'react'\nimport type { PropsRenderSlots, PropsRuntime, SlotCore, SlotMap } from '@deepseek-ai/dsh-client-ui-slots'\n// Type-only: pulls the `settings.section` SlotMap entry into this program.\nimport type {} from '@deepseek-ai/dsh-client-ui-settings/client'\nimport { FTSHARE_SETTINGS_CARD_SLOT, FTSHARE_SETTINGS_SECTION } from './contract.js'\nimport { SETTINGS_CSS } from './styles.js'\n\n/** Section slot this seam contributes into; declared by the settings shell. */\nconst FTSHARE_SETTINGS_SECTION_NAME = 'settings.section' as const\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface SlotMap {\n /** One card inside the FTShare settings section; `order` sets its position. */\n 'ftshare.settings.card': {\n kind: 'list'\n scope: 'root'\n owner: FtshareSettingsCardOwnerProps\n }\n }\n}\n/** Owner share of a card: the section supplies no card-specific props. */\nexport interface FtshareSettingsCardOwnerProps {\n /** Marker field: card owner props are intentionally empty. */\n children?: never\n}\n/**\n * The slice of the renderer-owned slots service this seam uses.\n *\n * `register` IS the ui-slots registration signature (the runtime registry\n * reuses it verbatim, adding disposal through the caller's fiber); `inject`\n * is the runtime wrapper's declaration-lifetime hook, which is why it is\n * mirrored structurally here.\n */\nexport interface FtshareSettingsSlotsService {\n readonly register: SlotCore['register']\n /** Run `callback` for each declaration lifetime of `key`. */\n inject(key: keyof SlotMap & string, callback: () => () => void): () => void\n}\n/**\n * Browser context the section shell requires: the renderer's slots service.\n * A card that needs more (its own services, invalidation events) declares the\n * extra members on its own context type.\n */\nexport interface FtshareSettingsShellContext {\n slots: FtshareSettingsSlotsService\n}\n/** Composed props of the section entry: the shell's owner share plus its declared child render. */\nexport type FtshareSettingsSectionProps =\n & PropsRuntime<typeof FTSHARE_SETTINGS_SECTION_NAME>\n & PropsRenderSlots<typeof FTSHARE_SETTINGS_CARD_SLOT>\n\n/**\n * The settings section shell: core CSS variables, the section chrome, and the\n * single render site every card appears at. Cards own everything inside it.\n */\nexport function FtshareSettingsSection({ renderSlot }: FtshareSettingsSectionProps): ReactNode {\n return <>\n <style>{SETTINGS_CSS}</style>\n {renderSlot(FTSHARE_SETTINGS_CARD_SLOT, {})}\n </>\n}\n\n/**\n * Declare the card slot and contribute the section that renders it.\n *\n * Declaring the child slot is what authorizes the render site; a card\n * registering through {@link FTSHARE_SETTINGS_CARD_SLOT} before this runs waits\n * for the declaration instead of failing.\n * @param ctx - client context carrying the renderer's slots service.\n * @returns idempotent disposer for the section and its declaration.\n */\nexport function registerSettingsSection(ctx: FtshareSettingsShellContext): () => void {\n return ctx.slots.inject(FTSHARE_SETTINGS_SECTION_NAME, () => ctx.slots.register(\n {\n name: FTSHARE_SETTINGS_SECTION_NAME,\n ...FTSHARE_SETTINGS_SECTION,\n children: { [FTSHARE_SETTINGS_CARD_SLOT]: { kind: 'list', scope: 'root' } },\n },\n FtshareSettingsSection,\n ))\n}\n","/**\n * FT brand tokens published as a host theme override layer.\n *\n * The layer only *adds* brand variables on `body`; it never overrides a\n * `--dsw-alias-*` value, so host chrome keeps its own palette and any user\n * theme keeps working underneath. Consumers read `var(--ft-*)` with the same\n * value as their fallback: the layer is a convenience, not a dependency, so\n * every sub-plugin keeps its brand colors when this plugin is absent (or when\n * the host has no `theme` service at all). Scheme-invariant tokens repeat one\n * value; palette-aware tokens spell both with `light-dark()`.\n *\n * Keep those fallbacks equal to {@link FT_BRAND_TOKENS}; `tests/brand.test.mjs`\n * scans every package for the pairing and fails on a drift.\n */\n\n/** One token value per palette mode; the host rejects a bare string. */\nexport interface BrandTokenModes {\n readonly light: string\n readonly dark: string\n}\n\n/** One value for both palettes: brand fills are scheme-invariant. */\nconst invariant = (value: string): BrandTokenModes => ({ light: value, dark: value })\n\n/** Layer identity reported to `ctx.theme.overrideTokens` (one layer per source). */\nexport const FT_BRAND_SOURCE = '@dsh-ft/dsh-fft-settings'\n\n/**\n * The published brand colors.\n *\n * `accent` is the FT yellow used by this plugin's primary action and eyebrow;\n * `accent-warm` is the orange trading accent shared by market and trade\n * surfaces (it is the only palette-aware group); `wechat` is the enterprise-\n * WeChat green the SSO card keeps as its identity. Inks are the text colors on\n * those fills, and `-soft` is the tinted surface behind the accent.\n */\nexport const FT_BRAND_TOKENS: Readonly<Record<string, BrandTokenModes>> = Object.freeze({\n '--ft-accent': invariant('#f0c433'),\n '--ft-accent-ink': invariant('#171717'),\n '--ft-accent-warm': { light: '#cf5623', dark: '#ff8352' },\n '--ft-accent-warm-hover': { light: '#b8481b', dark: '#ff9a72' },\n '--ft-accent-warm-ink': { light: '#ffffff', dark: '#1a120e' },\n '--ft-accent-warm-soft': { light: '#fbeee8', dark: '#2b1d16' },\n '--ft-wechat': invariant('#2ebd68'),\n '--ft-wechat-ink': invariant('#102419'),\n})\n\n/** The slice of the host theme service this layer uses. */\nexport interface BrandThemeService {\n overrideTokens(source: string, tokens: Readonly<Record<string, BrandTokenModes>>): () => void\n}\n\n/** Scoped context delivered once the `theme` service is available. */\nexport interface BrandThemeScope {\n readonly theme: BrandThemeService\n effect(callback: () => () => void, label: string): void\n}\n\n/** The slice of the client context this layer needs: a deferred service inject. */\nexport interface BrandThemeContext {\n inject(deps: readonly string[], callback: (scope: BrandThemeScope) => void): void\n}\n\n/**\n * Stack the brand layer once the host theme service exists.\n *\n * The deferred `inject` is deliberate: this plugin's declared client context\n * names only the services its cards read, and a host without `theme` must\n * still load the settings page — the CSS fallbacks then carry the colors.\n * @param ctx - client context carrying the host theme service.\n */\nexport function applyBrandTokens(ctx: BrandThemeContext): void {\n ctx.inject(['theme'], (scope) => {\n scope.effect(\n () => scope.theme.overrideTokens(FT_BRAND_SOURCE, FT_BRAND_TOKENS),\n 'ft-settings: FT brand tokens',\n )\n })\n}\n","import { registerApiKeyCard, type ApiKeyCardContext } from './features/api-key/card.js'\nimport { registerLogsCard } from './features/logs/card.js'\nimport { registerSettingsSection } from './core/shell.js'\nimport { applyBrandTokens, type BrandThemeContext } from './brand.js'\nexport * from './core/shell.js'\n/** Browser context this plugin's own client half requires. */\nexport type FtshareSettingsClientContext = ApiKeyCardContext\nexport const inject = ['slots', 'remote']\nexport function apply(ctx: FtshareSettingsClientContext): void {\n registerFtshareSettings(ctx)\n // The declared context above names only the services the cards read; the\n // brand layer defers on `theme` through the full Cordis context, so this\n // plugin still loads on a host without the theme service.\n applyBrandTokens(ctx as unknown as BrandThemeContext)\n}\n/** Contribute the section shell, then the built-in cards through the slot it declares. */\nexport function registerFtshareSettings(ctx: FtshareSettingsClientContext): void {\n registerSettingsSection(ctx)\n registerApiKeyCard(ctx)\n registerLogsCard(ctx)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":["useMemo","useSyncExternalStore","useState","useState","useCallback"],"sources":["../src/features/api-key/contract.ts","../src/core/contract.ts","../src/features/api-key/model.ts","../src/core/rpc-core.ts","../src/features/api-key/client-api.ts","../src/core/client-transport.ts","../src/features/api-key/styles.ts","../src/features/api-key/card.tsx","../src/features/logs/contract.ts","../src/features/logs/client-api.ts","../src/features/logs/styles.ts","../src/features/logs/card.tsx","../src/core/styles.ts","../src/core/shell.tsx","../src/brand.ts","../src/client.tsx"],"sourcesContent":["/**\n * API Key namespace contract: the credential reference and lifecycle events,\n * the status/validation vocabulary, and the two faces the namespace exposes\n * (the browser-safe subset and the host-only superset).\n *\n * It merges its own line into the generic {@link FtshareSettingsNamespaces}\n * merge point, so `ctx.ftshareSettings.apiKey` keeps its existing type for\n * every consumer.\n */\nexport const FTSHARE_API_KEY_REF = 'FTSHARE_API_KEY' as const\nexport const FTSHARE_CREDENTIAL_EVENTS = ['credentials/reference-updated'] as const\nexport const API_KEY_METHODS = ['describe', 'set', 'clear', 'configureAndTest', 'testConnection', 'getValidationStatus'] as const\nexport type ApiKeyMethod = typeof API_KEY_METHODS[number]\nexport type SubscribeChanges = (listener: () => void) => () => void\nexport interface CredentialStatus { configured: boolean; writable: boolean; source?: string }\nexport type ProbeState = 'available' | 'auth_required' | 'auth_failed' | 'permission_denied' | 'quota_exceeded' | 'rate_limited' | 'network_error' | 'timeout' | 'upstream_error' | 'invalid_response' | 'not_run'\nexport interface ProbeResult { status: ProbeState; message: string }\nexport interface ValidationResult {\n status: 'not_tested' | 'not_run' | 'passed' | 'partial' | 'failed' | 'superseded'\n remote: ProbeResult\n apiKey: ProbeResult\n testedAt?: string\n}\nexport interface ApiKeyStatus extends CredentialStatus {\n /** Display only. Fixed mask plus last four characters; never submit as a Key. */\n maskedValue?: string\n revision: number\n validation: ValidationResult\n}\nexport interface ConfigureResult {\n save: { status: 'saved' | 'failed'; code?: 'invalid_input' | 'read_only' | 'save_failed'; message: string }\n validation: ValidationResult\n}\n/** Local syntax validation, never an assertion of upstream validity. */\nexport function normalizeApiKey(input: string): string {\n const value = input.trim()\n if (!value) throw new Error('请输入 API Key')\n if (value.length > 512 || /[\\x00-\\x1f\\x7f]/.test(value)) throw new Error('API Key 长度不能超过 512 个字符,且不能包含控制字符')\n return value\n}\nexport function untested(status: 'not_tested' | 'not_run' | 'superseded' = 'not_tested', message = '尚未测试'): ValidationResult {\n return { status, remote: { status: 'not_run', message }, apiKey: { status: 'not_run', message } }\n}\n/** Everything the browser adapter may call; `resolve`/`createEnvironment` are not here. */\nexport interface ApiKeyPublicApi {\n describe(): Promise<ApiKeyStatus>\n set(value: string): Promise<void>\n clear(): Promise<void>\n configureAndTest(value: string): Promise<ConfigureResult>\n testConnection(): Promise<ValidationResult>\n getValidationStatus(): Promise<ValidationResult>\n}\n/** The host namespace object: the public API plus the secret-bearing host methods. */\nexport interface ApiKeySettings extends ApiKeyPublicApi {\n readonly credentialRef: typeof FTSHARE_API_KEY_REF\n /** Host only. Re-resolve per operation; never send this value to browsers. */\n resolve(): Promise<string | undefined>\n createEnvironment(base?: Readonly<Record<string, string | undefined>>): Promise<Record<string, string | undefined>>\n subscribe: SubscribeChanges\n}\ndeclare module '../../core/contract.js' {\n interface FtshareSettingsNamespaces { apiKey: ApiKeySettings }\n}\n","/**\n * Generic half of the settings contract: the seam's identity, its transport\n * constants, and the service shape. It carries no feature vocabulary — a\n * feature's own contract lives beside that feature and merges into\n * {@link FtshareSettingsNamespaces}.\n */\nimport type { FtshareSettingsModuleRegistry } from './module-registry.js'\nexport type { FtshareSettingsModuleRegistry, SettingsModule, SettingsRegistration } from './module-registry.js'\nexport type { SettingsRpcMethod, SettingsRpcMethods } from './rpc-core.js'\n/** Protocol version of the settings route and envelope. */\nexport const FTSHARE_SETTINGS_API_VERSION = 1 as const\n/**\n * Identity of the settings section that carries this plugin's cards.\n *\n * The id replaces the old `ftshare`. The label is a neutral product name, not\n * the internal brand: the page ships publicly, and the data source it\n * integrates is named inside the cards instead.\n */\nexport const FTSHARE_SETTINGS_SECTION = { id: 'dsh-fft', order: 120, label: '金融终端' } as const\n/**\n * Child slot of {@link FTSHARE_SETTINGS_SECTION} that carries its cards.\n *\n * The section declares the slot when it registers; every card — ours and a\n * third party's — contributes through it with `id`/`order`/`label`, and\n * `order` (ascending, default 100) decides the card's position.\n */\nexport const FTSHARE_SETTINGS_CARD_SLOT = 'ftshare.settings.card' as const\n/**\n * Browser-facing route for this plugin's settings calls.\n *\n * It sits below `/api` so the shared connection carrier applies the Host/Origin\n * fence and browser authentication before dispatch; this plugin only handles\n * the call itself.\n */\nexport const FTSHARE_SETTINGS_ROUTE = '/api/ftshare-settings' as const\n/** Largest request body accepted on {@link FTSHARE_SETTINGS_ROUTE}. */\nexport const FTSHARE_SETTINGS_MAX_BODY_BYTES = 64 * 1024\n/**\n * Merge point for namespaces a package reads but does not produce. A producer\n * gets its type from `register` inference; a consumer that reads someone\n * else's namespace declares one line, e.g. `cockpit: CockpitSettings`. Each\n * built-in feature merges its own line from its own feature directory.\n */\nexport interface FtshareSettingsNamespaces {}\n/**\n * Host service published as `ctx.ftshareSettings`.\n *\n * `apiVersion` and `modules` are permanent; every other member appears only\n * while its namespace is registered, so consumers must read properties at call\n * time instead of caching them during their own apply.\n */\nexport type FtshareSettingsService =\n & {\n readonly apiVersion: typeof FTSHARE_SETTINGS_API_VERSION\n readonly modules: FtshareSettingsModuleRegistry\n }\n & { readonly [N in keyof FtshareSettingsNamespaces & string]: FtshareSettingsNamespaces[N] }\n","import { normalizeApiKey, type ApiKeyPublicApi, type ApiKeyStatus, type ConfigureResult, type SubscribeChanges } from './contract.js'\nexport interface SettingsState { status?: ApiKeyStatus; busy: boolean; message: string; statusError?: string; save?: ConfigureResult['save'] }\nexport class FtshareSettingsModel {\n private state: SettingsState = { busy: false, message: '' }\n private listeners = new Set<() => void>()\n private request = 0\n private lifetime = 0\n constructor(private api: ApiKeyPublicApi) {}\n getSnapshot = (): SettingsState => this.state\n subscribe = (listener: () => void): (() => void) => { this.listeners.add(listener); return () => { this.listeners.delete(listener) } }\n private update(patch: Partial<SettingsState>): void { this.state = { ...this.state, ...patch }; for (const listener of this.listeners) listener() }\n connect(subscribe?: SubscribeChanges): () => void {\n const dispose = subscribe?.(() => { void this.refresh() })\n void this.refresh()\n return () => { this.request++; this.lifetime++; dispose?.() }\n }\n async refresh(): Promise<void> {\n const request = ++this.request\n try {\n const status = await this.api.describe()\n if (!status || typeof status.configured !== 'boolean') throw new Error('missing_status')\n if (request === this.request) this.update({ status, statusError: undefined })\n } catch { if (request === this.request) this.update({ status: undefined, statusError: '无法读取设置状态,请刷新重试' }) }\n }\n async configureAndTest(input: string): Promise<boolean> {\n if (this.state.busy || !this.state.status?.writable) return false\n let value: string\n try { value = normalizeApiKey(input) } catch (error) { this.update({ message: (error as Error).message }); return false }\n const lifetime = this.lifetime\n this.update({ busy: true, save: undefined, message: '正在保存并检查远端服务和 API Key…' })\n let saved = false\n try {\n const result = await this.api.configureAndTest(value)\n saved = result.save.status === 'saved'\n if (lifetime === this.lifetime) this.update({ save: result.save, message: saved ? validationMessage(result.validation.status) : result.save.message })\n } catch { if (lifetime === this.lifetime) this.update({ message: '请求未完成,保存状态待确认;请刷新后测试当前连接' }) }\n if (lifetime === this.lifetime) { await this.refresh(); this.update({ busy: false }) }\n return saved\n }\n async testConnection(): Promise<void> {\n if (this.state.busy || !this.state.status) return\n const lifetime = this.lifetime\n this.update({ busy: true, message: '正在检查远端服务和当前 API Key…' })\n try {\n const result = await this.api.testConnection()\n if (lifetime === this.lifetime) this.update({ message: validationMessage(result.status) })\n } catch { if (lifetime === this.lifetime) this.update({ message: '测试请求失败,请重试' }) }\n if (lifetime === this.lifetime) { await this.refresh(); this.update({ busy: false }) }\n }\n async clear(): Promise<void> {\n if (this.state.busy || !this.state.status?.writable) return\n const lifetime = this.lifetime\n this.update({ busy: true, save: undefined, message: '' })\n try {\n await this.api.clear()\n if (lifetime === this.lifetime) this.update({ message: '已清除 DSH 保存的密钥;若有 .env 配置,下方显示其有效状态' })\n } catch { if (lifetime === this.lifetime) this.update({ message: '清除失败,请刷新状态后重试' }) }\n if (lifetime === this.lifetime) { await this.refresh(); this.update({ busy: false }) }\n }\n}\nfunction validationMessage(status: string): string {\n return ({ passed: '远端服务正常,API Key 验证通过', partial: '远端服务正常,请查看 API Key 检查结果', failed: '测试未通过,请查看分项结果', not_run: '未执行测试,请先确认凭据配置', superseded: '凭据已变化,请重新测试', not_tested: '尚未测试' } as Record<string, string>)[status] ?? '请查看分项结果'\n}\n","import type { FTSHARE_SETTINGS_ROUTE } from './contract.js'\nexport interface RpcResult<T> { ok: boolean; value?: T; error?: { code: string; message: string; details: Record<string, unknown> } }\n/** Browser caller for this plugin's route. */\nexport interface ClientTransport { call(endpoint: string, payload?: unknown, signal?: AbortSignal): Promise<RpcResult<unknown>> }\nexport type RpcHandler = (endpoint: string, payload: unknown, signal: AbortSignal) => Promise<RpcResult<unknown>>\n/** Structural shape of one exact Fetch route; `connection.fetch.register` accepts it. */\nexport interface SettingsRoute {\n readonly path: typeof FTSHARE_SETTINGS_ROUTE\n readonly methods: readonly string[]\n readonly requestBody: 'buffered' | 'streaming'\n readonly fetch: (request: Request) => Promise<Response>\n}\n/** Host registry for exact Fetch routes on the shared `/api` carrier. */\nexport interface HostFetch { register(route: SettingsRoute): () => Promise<void> }\nexport interface SettingsRpcMethod {\n validate(payload: unknown): boolean\n invoke(payload: unknown, signal: AbortSignal): unknown | Promise<unknown>\n}\nexport type SettingsRpcMethods = Readonly<Record<string, SettingsRpcMethod>>\nexport interface SettingsRpcModule { namespace: string; rpcMethods: SettingsRpcMethods }\n/**\n * Live dispatch table behind {@link createSettingsRpcHandler}: routes appear\n * when a namespace is registered and disappear when it is released, so the one\n * route mounted at boot keeps serving namespaces that arrive later.\n */\nexport interface SettingsRpcTable {\n /** Validate one whole module, then publish its routes; a failure writes nothing. */\n register(module: SettingsRpcModule): void\n /** Remove one namespace and every route under it; unknown namespaces are a no-op. */\n unregister(namespace: string): void\n /** Namespaces currently owning at least one route, in registration order. */\n namespaces(): readonly string[]\n}\n/** The dispatcher callable plus the increment/decrement surface of its route table. */\nexport type SettingsRpcHandler = RpcHandler & SettingsRpcTable\nconst fail = (code: string, message: string): RpcResult<unknown> => ({ ok: false, error: { code, message, details: {} } })\n/** Names the service object and the route table reserve for themselves. */\nexport const SETTINGS_RESERVED_NAMES = ['apiVersion', 'modules', 'constructor', 'prototype', '__proto__'] as const\nexport function assertSettingsName(name: string): void {\n if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(name) || (SETTINGS_RESERVED_NAMES as readonly string[]).includes(name)) throw new Error(`Invalid settings name: ${name}`)\n}\n/** Validate every endpoint part of one module without writing to any table. */\nexport function assertSettingsRpcModule(module: SettingsRpcModule): void {\n assertSettingsName(module.namespace)\n for (const name of Object.keys(module.rpcMethods)) assertSettingsName(name)\n}\n/**\n * Build the live dispatcher for this plugin's route.\n *\n * Only explicitly registered methods are callable; never reflects over a\n * service. The returned function carries its own route table, so the caller\n * mounts the route once and mutates through `register` / `unregister`\n * afterwards. Each `register` validates the whole module first — namespace\n * format, reserved names, duplicates, and every method name — and writes\n * nothing when any check fails.\n * @param modules - optional initial namespaces, registered in order.\n * @returns dispatcher with the live route table attached.\n */\nexport function createSettingsRpcHandler(modules: readonly SettingsRpcModule[] = []): SettingsRpcHandler {\n const routes = new Map<string, SettingsRpcMethod>()\n const namespaces = new Set<string>()\n const register = (module: SettingsRpcModule): void => {\n assertSettingsRpcModule(module)\n if (namespaces.has(module.namespace)) throw new Error(`Duplicate settings namespace: ${module.namespace}`)\n // Committed: every check above ran before the first write.\n namespaces.add(module.namespace)\n for (const [name, method] of Object.entries(module.rpcMethods)) routes.set(`${module.namespace}/${name}`, method)\n }\n const unregister = (namespace: string): void => {\n if (!namespaces.delete(namespace)) return\n const prefix = `${namespace}/`\n for (const endpoint of [...routes.keys()]) if (endpoint.startsWith(prefix)) routes.delete(endpoint)\n }\n const handler = (async (endpoint: string, payload: unknown, signal: AbortSignal): Promise<RpcResult<unknown>> => {\n const method = routes.get(endpoint)\n if (!method) return fail('not_found', '接口不存在')\n if (signal.aborted) return fail('cancelled', '请求已取消')\n try {\n if (!method.validate(payload)) return fail('bad_request', '请求参数不正确')\n return { ok: true, value: (await method.invoke(payload, signal)) ?? null }\n } catch { return fail('operation_failed', '操作失败,请刷新状态后重试') }\n }) as SettingsRpcHandler\n handler.register = register\n handler.unregister = unregister\n handler.namespaces = () => [...namespaces]\n for (const module of modules) register(module)\n return handler\n}\n/**\n * 把一次失败的信封翻译成调用方能展示的原因。\n *\n * `not_found` 是「浏览器半边比宿主半边新」的可观测症状:升级插件后客户端 bundle 由磁盘重新\n * 加载,而宿主进程仍持有旧包,它没有新命名空间。此时重试永远不会成功,必须点名重启宿主。\n * @param endpoint - 这次调用的 `<namespace>/<method>`。\n * @param error - 宿主回传的失败原因,畸形信封下可能缺省。\n * @returns 面向用户的失败描述。\n */\nfunction describeSettingsFailure(endpoint: string, error: RpcResult<unknown>['error']): string {\n if (error?.code === 'not_found') return `FTShare 设置接口 ${endpoint} 不存在:宿主插件还是升级前的版本,重启 DSH 后再试`\n return `FTShare 设置操作失败(${error?.code ?? 'unknown'}):${error?.message ?? '宿主未说明原因'}`\n}\nexport async function callSettingsRpc<T>(transport: ClientTransport, namespace: string, method: string, payload: unknown = {}): Promise<T> {\n const endpoint = `${namespace}/${method}`\n const result = await transport.call(endpoint, payload)\n if (!result.ok) throw new Error(describeSettingsFailure(endpoint, result.error))\n return result.value as T\n}\n","import type { ApiKeyPublicApi, ApiKeyMethod } from './contract.js'\nimport { callSettingsRpc, type ClientTransport } from '../../core/rpc-core.js'\n/** Browser adapter deliberately has no resolve/createEnvironment method. */\nexport function createApiKeyClient(transport: ClientTransport): ApiKeyPublicApi {\n async function call<T>(method: ApiKeyMethod, payload: unknown = {}): Promise<T> {\n return callSettingsRpc<T>(transport, 'apiKey', method, payload)\n }\n return {\n describe: () => call('describe'), set: value => call('set', { value }), clear: () => call('clear'),\n configureAndTest: value => call('configureAndTest', { value }), testConnection: () => call('testConnection'), getValidationStatus: () => call('getValidationStatus'),\n }\n}\n","/**\n * Browser half of the settings transport.\n *\n * Calls are same-origin fetches to the plugin route, so the browser session\n * cookie carries authentication and this plugin needs no Connection RPC client.\n */\n\nimport { FTSHARE_SETTINGS_ROUTE } from './contract.js'\nimport type { ClientTransport, RpcResult } from './rpc-core.js'\n\n/** Transport this caller posts through; same signature as the global `fetch`. */\nexport type SettingsFetch = (input: string, init: RequestInit) => Promise<Response>\n\n/**\n * Create the browser-backed caller used by the settings cards.\n * @param doFetch - transport override; defaults to the page's global fetch.\n * @returns caller that owns request encoding and envelope decoding.\n */\nexport function createSettingsTransport(doFetch?: SettingsFetch): ClientTransport {\n const send: SettingsFetch = doFetch ?? ((input, init) => globalThis.fetch(input, init))\n return {\n async call(endpoint, payload = {}, signal) {\n const response = await send(FTSHARE_SETTINGS_ROUTE, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ endpoint, payload }),\n ...signal === undefined ? {} : { signal },\n })\n if (!response.ok) {\n // The carrier answers 401/403 before dispatch; anything else is a route\n // or plugin-level failure. Both are transport failures for the caller.\n throw new Error(`transport failure for ${endpoint}: HTTP ${response.status}`)\n }\n return await response.json() as RpcResult<unknown>\n },\n }\n}\n","/**\n * The API Key card's own styles, rendered by the card component's `<style>`.\n *\n * A card owns everything past the section shell: the core declares the\n * `--ftshare-*` tokens and the section container, never card internals.\n * Every selector stays scoped under `.ftshare-settings`, so a card never\n * styles another card's markup.\n */\nexport const API_KEY_CARD_CSS = `\n.ftshare-settings .ftshare-eyebrow { color:var(--ftshare-accent-on-surface); font-size:11px; letter-spacing:.12em; }\n.ftshare-settings .ftshare-card { padding:22px; background:var(--ftshare-panel); border:0.5px solid var(--ftshare-border); border-radius:var(--dsw-radius-xl); margin-top:16px; }\n.ftshare-settings label { display:block; font-weight:500; }\n.ftshare-settings input { width:100%; height:32px; border:0.5px solid var(--ftshare-control); border-radius:var(--dsw-radius-md); padding:0 12px; background:var(--ftshare-input); color:inherit; font:inherit; }\n.ftshare-settings input::placeholder { color:var(--ftshare-muted); opacity:1; }\n.ftshare-settings .ftshare-actions { display:flex; flex-wrap:wrap; gap:10px; align-items:center; margin-top:16px; }\n.ftshare-settings button { min-height:36px; border:0.5px solid var(--ftshare-control); border-radius:var(--dsw-radius-md); padding:0 14px; font:inherit; background:transparent; color:inherit; cursor:pointer; }\n.ftshare-settings button.primary { background:var(--ftshare-accent); color:var(--ftshare-accent-ink); border-color:var(--ftshare-accent); font-weight:500; }\n.ftshare-settings button:disabled { opacity:.5; cursor:not-allowed; }\n.ftshare-settings dl { margin:0; }\n.ftshare-settings .ftshare-result { display:grid; grid-template-columns:130px 1fr; gap:12px; padding:10px 0; border-top:0.5px solid var(--ftshare-border); }\n.ftshare-settings dd { margin:0; overflow-wrap:anywhere; }\n.ftshare-settings .ftshare-error { color:var(--ftshare-error); }\n@media(max-width:540px) { .ftshare-settings .ftshare-card { padding:16px; } .ftshare-settings .ftshare-result { grid-template-columns:1fr; gap:4px; } }\n`\n","import { useEffect, useMemo, useState, useSyncExternalStore, type ReactNode } from 'react'\nimport { FTSHARE_API_KEY_REF, FTSHARE_CREDENTIAL_EVENTS, type SubscribeChanges, type ApiKeyPublicApi } from './contract.js'\nimport { FTSHARE_SETTINGS_CARD_SLOT } from '../../core/contract.js'\nimport { FtshareSettingsModel } from './model.js'\nimport { createApiKeyClient } from './client-api.js'\nimport { createSettingsTransport } from '../../core/client-transport.js'\nimport type { FtshareSettingsShellContext } from '../../core/shell.js'\nimport { API_KEY_CARD_CSS } from './styles.js'\n\n/**\n * Browser context this card requires: the shell's slots service plus the\n * invalidations that make a cached credential status stale.\n */\nexport interface ApiKeyCardContext extends FtshareSettingsShellContext {\n remote: { $on(event: string, listener: (ref: string) => void): () => void }\n on(event: 'connection/reset', listener: () => void): () => void\n}\n\n/**\n * Contribute the built-in API Key card through the same slot a third-party\n * card uses. Nothing about it is privileged: same declaration, same options.\n * @param ctx - client context carrying the renderer's slots service.\n * @returns idempotent disposer for the card registration.\n */\nexport function registerApiKeyCard(ctx: ApiKeyCardContext): () => void {\n return ctx.slots.inject(FTSHARE_SETTINGS_CARD_SLOT, () => ctx.slots.register(\n { name: FTSHARE_SETTINGS_CARD_SLOT, id: 'apiKey', order: 10 },\n createApiKeyCard(ctx),\n ))\n}\n\n/** Bind the card's adapters; subscriptions are acquired in the component's effect. */\nfunction createApiKeyCard(ctx: ApiKeyCardContext): () => ReactNode {\n const api = createApiKeyClient(createSettingsTransport())\n const subscribeChanges: SubscribeChanges = listener => {\n const disposers = FTSHARE_CREDENTIAL_EVENTS.map(event => ctx.remote.$on(event, ref => { if (ref === FTSHARE_API_KEY_REF) listener() }))\n disposers.push(ctx.on('connection/reset', listener))\n return () => { for (const dispose of disposers) dispose() }\n }\n return () => <ApiKeyCard api={api} subscribeChanges={subscribeChanges} />\n}\n\nexport function ApiKeyCard({ api, subscribeChanges }: { readonly api: ApiKeyPublicApi; readonly subscribeChanges?: SubscribeChanges }): ReactNode {\n const model = useMemo(() => new FtshareSettingsModel(api), [api])\n const { status, busy, message, statusError, save } = useSyncExternalStore(model.subscribe, model.getSnapshot, model.getSnapshot)\n const [draft, setDraft] = useState('')\n useEffect(() => model.connect(subscribeChanges), [model, subscribeChanges])\n const disabled = busy || !status?.writable\n const configure = async () => { if (await model.configureAndTest(draft)) setDraft('') }\n return <>\n <style>{API_KEY_CARD_CSS}</style>\n <section className=\"ftshare-settings\" aria-label=\"FTShare 设置\" aria-busy={busy}>\n <header><div><span className=\"ftshare-eyebrow\">FTSHARE / API KEY</span><h2>访问凭据</h2></div></header>\n <p>目前集成 FTShare 数据源。</p>\n {status && !status.writable && <p>当前来源为只读,可测试连接;修改密钥请到对应来源操作。</p>}\n <div className=\"ftshare-card\"><label htmlFor=\"ftshare-api-key\">FTShare API Key</label>\n <input id=\"ftshare-api-key\" type=\"password\" value={draft} placeholder={status?.configured ? status.maskedValue ?? '••••••••' : '请输入 API Key'} onChange={e => setDraft(e.target.value)} disabled={disabled} autoComplete=\"new-password\" maxLength={512} />\n <div className=\"ftshare-actions\">\n <button className=\"primary\" type=\"button\" onClick={() => void configure()} disabled={disabled || !draft.trim()}>配置并测试</button>\n <button type=\"button\" onClick={() => void model.testConnection()} disabled={busy || !status}>测试当前连接</button>\n <button type=\"button\" onClick={() => void model.clear()} disabled={disabled || !status?.configured}>清除已保存密钥</button>\n <button type=\"button\" onClick={() => void model.refresh()} disabled={busy} title=\"重新读取已保存的配置和上次检测结果,不会发起联网检测\">重新读取状态</button>\n <a href=\"https://ftai.chat/me/profile?tab=api-key\" target=\"_blank\" rel=\"noreferrer\">获取 API Key ↗</a>\n </div>\n {draft && <small>“测试当前连接”使用已生效的密钥,不测试输入框内尚未保存的内容。</small>}\n {busy && <p role=\"status\">正在处理,请稍候…</p>}\n </div>\n <div className=\"ftshare-card\" aria-live=\"polite\"><h3>验证结果</h3><dl>\n {save && <div className=\"ftshare-result\"><dt>本次保存</dt><dd>{save.message}</dd></div>}\n <div className=\"ftshare-result\"><dt>远端服务</dt><dd>{status?.validation.remote.message ?? '状态待确认'}</dd></div>\n <div className=\"ftshare-result\"><dt>API Key</dt><dd>{status?.validation.apiKey.message ?? '状态待确认'}</dd></div>\n {status?.validation.testedAt && <div className=\"ftshare-result\"><dt>测试时间</dt><dd>{new Date(status.validation.testedAt).toLocaleString()}</dd></div>}\n </dl><small>节假日或收盘后的空数据不影响验证。</small></div>\n {message && <p role=\"status\">{message}</p>}{statusError && <p className=\"ftshare-error\" role=\"alert\">{statusError}</p>}\n </section>\n </>\n}\n","/**\n * `logs` 命名空间的共享契约:只有常量与类型,浏览器半边可以安全导入。\n *\n * 采集器(Host 常驻)与导出器(设置页)通过文件解耦;本文件是两者唯一的共同语言。\n */\n\n/**\n * 卡片在 `ftshare.settings.card` 槽位里的 id 与 order。\n *\n * 归 feature 所有(与业务契约同一份来源):卡片组件与浏览器侧的守卫测试都读它,\n * order 20 排在 API Key 卡(10)之后。\n */\nexport const LOGS_CARD_ID = 'logs'\nexport const LOGS_CARD_ORDER = 20\n\n/** 显式登记的 RPC 方法;未登记的方法一律 `not_found`。 */\nexport const LOGS_METHODS = ['stats', 'preview', 'export'] as const\nexport type LogsMethod = typeof LOGS_METHODS[number]\n\nexport type LogLevelName = 'error' | 'info' | 'warn' | 'debug'\n\n/**\n * 配置级别 → cordis `levels.default` 数值。\n *\n * cordis 的 `levels` 是**接收上限**而不是下限:`targetLevel < level` 才丢弃。\n * 因此 `warn`(2) 收 error/info/warn,`debug`(3) 收全部,`info`(1) 会把 `warn` 丢掉。\n */\nexport const LOG_LEVEL_THRESHOLD: Record<LogLevelName, number> = { error: 0, info: 1, warn: 2, debug: 3 }\nexport const LOG_LEVEL_NAMES = ['warn', 'info', 'debug', 'error'] as const\n\n/**\n * 可配置的级别(规格 §10)。\n *\n * 不提供 `info` 档:`levels` 是接收上限,`info`(1) 会把 `warn`(2) 一起丢掉,与规格 §6\n * 「失败日志不低于 warn」冲突;`error`(0) 同样不成立(等于只剩 error)。允许值收敛为\n * `warn | debug`,档位越界由 Schemastery 直接拒绝(插件不挂载),不做静默夹紧。\n */\nexport const LOGS_CONFIG_LEVELS = ['warn', 'debug'] as const\nexport type LogsConfigLevel = typeof LOGS_CONFIG_LEVELS[number]\n\n/** 落盘位置:`$DSH_HOME/logs/ft/`。 */\nexport const LOGS_DIR_SEGMENTS = ['logs', 'ft'] as const\nexport const LOGS_CURRENT_FILE = 'current.log'\nexport const LOGS_ROTATED_PREFIX = 'ft-'\nexport const LOGS_FILE_SUFFIX = '.log'\n\nexport const LOGS_LINE_MAX_BYTES = 8 * 1024\nexport const LOGS_TRUNCATED_MARK = '...[已截断]'\nexport const LOGS_REPEAT_FIELD = '重复次数'\nexport const LOGS_THROTTLE_WINDOW_MS = 10_000\nexport const LOGS_MAX_PENDING_KEYS = 2_000\nexport const LOGS_MAX_FIELD_LENGTH = 2_000\nexport const LOGS_MAX_FIELD_DEPTH = 3\n\nexport const LOGS_DEFAULT_PREVIEW_LINES = 80\nexport const LOGS_MAX_PREVIEW_LINES = 200\nexport const LOGS_PREVIEW_MAX_BYTES = 256 * 1024\n\n/** 导出体积上限(规格 §9):单来源 2 MiB、合计 8 MiB、目录最多 20 个 `*.log`。 */\nexport const LOGS_SOURCE_MAX_BYTES = 2 * 1024 * 1024\nexport const LOGS_EXPORT_MAX_BYTES = 8 * 1024 * 1024\nexport const LOGS_MAX_SOURCE_FILES = 20\n\nexport const LOGS_EXPORT_FILE_PREFIX = 'ft-runtime-'\nexport const LOGS_SECRET_TTL_MS = 5_000\n\n/**\n * 诊断包路由(规格 §9 / §12 决策 3):`/api` 载体上的第二条精确路由,归 `logs` feature 所有。\n *\n * 卡片用 `<a download>` 直接指向它:浏览器同源 GET 自带凭据,不需要 fetch/Blob,\n * `/api` 前缀的鉴权由载体负责,本 feature 不重复鉴权、也不读任何请求参数。\n * 它随 owner fiber 释放(`ctx.effect` + `register` 返回的 disposer)。\n */\nexport const LOGS_ARCHIVE_ROUTE = '/api/ftshare-settings/logs.zip' as const\n/** 归档内固定条目名(`manifest.json` 永远排第一)。 */\nexport const LOGS_ARCHIVE_MANIFEST = 'manifest.json'\nexport const LOGS_ARCHIVE_LOG = 'ft-runtime.log'\n/** 额外来源原文的目录前缀:`sources/<n>-<basename>`。 */\nexport const LOGS_ARCHIVE_SOURCE_DIR = 'sources/'\n\n/** 导出 header 里的来源段落分隔标记。 */\nexport const LOGS_SECTION_RULE = '====='\n\n/** 一条来源(文件或目录)在 Host 侧的存在性快照。 */\nexport interface LogsSourceState {\n readonly path: string\n readonly kind: 'file' | 'directory' | 'missing'\n readonly bytes?: number\n readonly files?: number\n}\n\n/** 导出物 header 里每个来源的统计。 */\nexport interface LogsSourceSummary {\n readonly label: string\n readonly path: string\n readonly lines: number\n readonly bytes: number\n readonly firstAt?: string\n readonly lastAt?: string\n readonly truncated: boolean\n readonly missing?: boolean\n}\n\n/** 采集器运行时统计;时间一律是北京时间 ISO 字符串。 */\nexport interface LogsStats {\n readonly file: string\n readonly fileBytes: number\n readonly level: LogLevelName\n readonly maxBytes: number\n readonly keep: number\n readonly records: number\n readonly dropped: number\n readonly rotations: number\n readonly writeFailures: number\n readonly firstAt?: string\n readonly lastAt?: string\n readonly byName: Readonly<Record<string, number>>\n readonly sources: readonly LogsSourceState[]\n}\n\nexport interface LogsPreview {\n readonly lines: readonly string[]\n readonly bytes: number\n readonly truncated: boolean\n readonly fileBytes: number\n}\n\nexport interface LogsExport {\n readonly fileName: string\n readonly text: string\n readonly bytes: number\n readonly truncated: boolean\n readonly sources: readonly LogsSourceSummary[]\n}\n\n/** 浏览器可用的只读日志接口。 */\nexport interface LogsPublicApi {\n stats(): Promise<LogsStats>\n preview(lines?: number): Promise<LogsPreview>\n /** 先刷盘,再把主日志与配置的 sources 拼成单文件文本。 */\n export(): Promise<LogsExport>\n}\n\n/** Host 服务:比浏览器多出本地路径与刷盘/释放句柄的能力。 */\nexport interface LogsSettings extends LogsPublicApi {\n readonly filePath: string\n flush(): Promise<void>\n dispose(): Promise<void>\n}\n","import {\n LOGS_DEFAULT_PREVIEW_LINES,\n type LogsExport,\n type LogsMethod,\n type LogsPreview,\n type LogsPublicApi,\n type LogsStats,\n} from './contract.js'\nimport { callSettingsRpc, type ClientTransport } from '../../core/rpc-core.js'\n\n/** 浏览器适配器:只读,且不暴露本地路径以外的任何 Host 能力(刷盘与释放句柄)。 */\nexport function createLogsClient(transport: ClientTransport): LogsPublicApi {\n const call = <T>(method: LogsMethod, payload: unknown = {}): Promise<T> =>\n callSettingsRpc<T>(transport, 'logs', method, payload)\n return {\n stats: () => call<LogsStats>('stats'),\n preview: (lines = LOGS_DEFAULT_PREVIEW_LINES) => call<LogsPreview>('preview', { lines }),\n export: () => call<LogsExport>('export'),\n }\n}\n","/**\n * The `logs` card's own styles, rendered by the card component's `<style>`.\n *\n * These two rules used to live in the core section stylesheet; a card owns\n * everything past the section shell, so they moved here with the card. Every\n * selector stays scoped under `.ftshare-settings`, so a card never styles\n * another card's markup.\n */\nexport const LOGS_CARD_CSS = `\n.ftshare-settings select { height:32px; border:0.5px solid var(--ftshare-control); border-radius:var(--dsw-radius-md); padding:0 10px; background:var(--ftshare-input); color:inherit; font:inherit; }\n.ftshare-settings .ftshare-download { display:inline-flex; align-items:center; min-height:32px; padding:0 14px; border:0.5px solid var(--ftshare-control); border-radius:var(--dsw-radius-md); color:inherit; text-decoration:none; cursor:pointer; }\n.ftshare-settings .ftshare-log-preview { max-height:320px; overflow:auto; margin:0 0 12px; padding:12px; background:var(--ftshare-input); border:0.5px solid var(--ftshare-border); border-radius:var(--dsw-radius-lg); font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:12px; line-height:1.5; white-space:pre-wrap; overflow-wrap:anywhere; }\n`\n","import { useCallback, useEffect, useState, type ReactNode } from 'react'\nimport {\n LOGS_ARCHIVE_ROUTE,\n LOGS_CARD_ID,\n LOGS_CARD_ORDER,\n LOGS_DEFAULT_PREVIEW_LINES,\n LOGS_MAX_PREVIEW_LINES,\n type LogsPreview,\n type LogsPublicApi,\n type LogsStats,\n} from './contract.js'\nimport { createLogsClient } from './client-api.js'\nimport { createSettingsTransport } from '../../core/client-transport.js'\nimport { FTSHARE_SETTINGS_CARD_SLOT } from '../../core/contract.js'\nimport type { FtshareSettingsShellContext } from '../../core/shell.js'\nimport { LOGS_CARD_CSS } from './styles.js'\n\n/** 预览行数候选:上限由 Host 侧校验兜底。 */\nconst PREVIEW_CHOICES = [50, LOGS_DEFAULT_PREVIEW_LINES, LOGS_MAX_PREVIEW_LINES]\n\n/**\n * 快照缺失时的占位文案。\n *\n * 失败后不能继续写「读取中…」:那会把一次已经结束的失败说成正在进行的读取,掩盖真正的\n * 原因(例如宿主插件没有这个接口,重试永远不会成功)。\n * @param failed - 本次读取是否已经失败。\n * @returns 失败提示,或仍在读取的提示。\n */\nexport function statsPlaceholder(failed: boolean): string {\n return failed ? '读取失败' : '读取中…'\n}\n\nfunction displayBytes(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`\n return `${(bytes / 1024 / 1024).toFixed(2)} MiB`\n}\n\n/** 把文本包成 Blob,用 a[download] 触发下载(Web 与桌面同一套)。 */\nfunction downloadText(fileName: string, text: string): void {\n const url = URL.createObjectURL(new Blob([text], { type: 'text/plain;charset=utf-8' }))\n const anchor = document.createElement('a')\n anchor.href = url\n anchor.download = fileName\n anchor.rel = 'noopener'\n document.body.appendChild(anchor)\n anchor.click()\n anchor.remove()\n setTimeout(() => URL.revokeObjectURL(url), 0)\n}\n\n/**\n * Contribute the built-in runtime-log card through the same slot a third-party\n * card uses. Nothing about it is privileged: same declaration, same options.\n * @param ctx - client context carrying the renderer's slots service.\n * @returns idempotent disposer for the card registration.\n */\nexport function registerLogsCard(ctx: FtshareSettingsShellContext): () => void {\n return ctx.slots.inject(FTSHARE_SETTINGS_CARD_SLOT, () => ctx.slots.register(\n { name: FTSHARE_SETTINGS_CARD_SLOT, id: LOGS_CARD_ID, order: LOGS_CARD_ORDER },\n createLogsCard(),\n ))\n}\n\n/** Bind the card's adapter; the component only reads through it. */\nfunction createLogsCard(): () => ReactNode {\n const api = createLogsClient(createSettingsTransport())\n return () => <LogsSection api={api} />\n}\n\nexport function LogsSection({ api }: { readonly api: LogsPublicApi }): ReactNode {\n const [stats, setStats] = useState<LogsStats>()\n const [preview, setPreview] = useState<LogsPreview>()\n const [lines, setLines] = useState(LOGS_DEFAULT_PREVIEW_LINES)\n const [busy, setBusy] = useState(false)\n const [message, setMessage] = useState('')\n const [error, setError] = useState('')\n /** 读取已失败且还没有任何快照时,每个字段都必须说出来,而不是假装还在读。 */\n const placeholder = statsPlaceholder(error !== '')\n\n const refresh = useCallback(async () => {\n setBusy(true)\n setError('')\n try {\n setStats(await api.stats())\n setPreview(await api.preview(lines))\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : '读取日志状态失败')\n } finally {\n setBusy(false)\n }\n }, [api, lines])\n\n useEffect(() => { void refresh() }, [refresh])\n\n const doExport = async () => {\n setBusy(true)\n setError('')\n setMessage('')\n try {\n const result = await api.export()\n downloadText(result.fileName, result.text)\n setMessage(`已导出 ${result.fileName}(${displayBytes(result.bytes)}${result.truncated ? ',已达体积上限并截断' : ''})`)\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : '导出日志失败')\n } finally {\n setBusy(false)\n }\n }\n\n return <>\n <style>{LOGS_CARD_CSS}</style>\n <section className=\"ftshare-settings\" aria-label=\"FT 运行日志\" aria-busy={busy}>\n <header><div><span className=\"ftshare-eyebrow\">FTSHARE / LOGS</span><h2>运行日志</h2></div></header>\n <div className=\"ftshare-card\">\n <h3>采集状态</h3>\n <dl>\n <div className=\"ftshare-result\"><dt>日志文件</dt><dd>{stats?.file ?? placeholder}</dd></div>\n <div className=\"ftshare-result\"><dt>记录 / 时间范围</dt><dd>\n {stats === undefined ? placeholder : `${stats.records} 条`}\n {stats?.firstAt !== undefined && `(${stats.firstAt} ~ ${stats.lastAt})`}\n </dd></div>\n <div className=\"ftshare-result\"><dt>文件大小 / 上限</dt><dd>\n {stats === undefined ? placeholder : `${displayBytes(stats.fileBytes)} / ${displayBytes(stats.maxBytes)}`}\n </dd></div>\n <div className=\"ftshare-result\"><dt>丢弃 / 轮转 / 写失败</dt><dd>\n {stats === undefined ? placeholder : `${stats.dropped} / ${stats.rotations} / ${stats.writeFailures}`}\n </dd></div>\n <div className=\"ftshare-result\"><dt>日志级别</dt><dd>{stats?.level ?? placeholder} </dd></div>\n <div className=\"ftshare-result\"><dt>统计</dt><dd>\n {stats === undefined\n ? placeholder\n : Object.keys(stats.byName).length === 0\n ? '暂无'\n : Object.entries(stats.byName).map(([name, count]) => `${name}=${count}`).join('、')}\n </dd></div>\n <div className=\"ftshare-result\"><dt>额外来源</dt><dd>\n {stats === undefined\n ? placeholder\n : stats.sources.length === 0\n ? '暂无'\n : stats.sources.map(source => `${source.path}(${source.kind}${source.bytes === undefined ? '' : ` ${displayBytes(source.bytes)}`})`).join(';')}\n </dd></div>\n </dl>\n <div className=\"ftshare-actions\">\n <button className=\"primary\" type=\"button\" onClick={() => void doExport()} disabled={busy}>导出日志</button>\n {/* 同源 GET,浏览器自带凭据:直接指向路由,不需要 fetch/Blob。 */}\n <a className=\"ftshare-download\" href={LOGS_ARCHIVE_ROUTE} download=\"\">导出诊断包</a>\n <button type=\"button\" onClick={() => void refresh()} disabled={busy}>刷新</button>\n <label htmlFor=\"ftshare-log-lines\">预览行数</label>\n <select id=\"ftshare-log-lines\" value={lines} onChange={e => setLines(Number(e.target.value))} disabled={busy}>\n {PREVIEW_CHOICES.map(choice => <option key={choice} value={choice}>{choice}</option>)}\n </select>\n </div>\n {busy && <p role=\"status\">正在处理,请稍候…</p>}\n {message && <p role=\"status\">{message}</p>}\n {error && <p className=\"ftshare-error\" role=\"alert\">{error}</p>}\n </div>\n <div className=\"ftshare-card\">\n <h3>最近日志{preview?.truncated === true ? '(已截断)' : ''}</h3>\n {preview === undefined\n ? <p>{error === '' ? '正在读取…' : '日志预览读取失败,见上方提示。'}</p>\n : preview.lines.length === 0\n ? <p>无</p>\n : <pre className=\"ftshare-log-preview\">{preview.lines.join('\\n')}</pre>}\n </div>\n </section>\n </>\n}\n","/**\n * The section shell's styles plus the theme tokens every card reads.\n *\n * The shell owns the container, typography, focus ring and link rules that\n * every card sits inside; a card owns its own CSS and renders it from its own\n * component, and core never styles card internals.\n *\n * The theme layer declares the plugin's `--ftshare-*` tokens once, on\n * `.ftshare-settings` — the root element every card renders. Surfaces and text\n * take their value from the host palette: `--dsw-alias-*` is what DSH\n * publishes for the light, the dark and every registered theme, so one\n * stylesheet follows the live palette with no script, no `prefers-color-scheme`\n * copy and no second palette to keep in sync. The literal inside each `var()`\n * is the fallback for a host that publishes no palette.\n *\n * The declarations sit on the card root rather than on `:root` because a\n * custom property resolves where it is *declared*: `--dsw-alias-*` exists from\n * `body` downwards, so a value derived from it above `body` would resolve to\n * nothing and fall back. Consequence: the place to pin a look is the host\n * palette itself (`ctx.theme.register` / `ctx.theme.overrideTokens`), not these\n * names.\n *\n * `--ftshare-accent*` is the plugin's own brand and has no host equivalent. The\n * values come from the FT brand layer's `--ft-*` tokens (`src/brand.ts`), with\n * the same literal as the fallback so the card keeps its colors when that layer\n * is absent. The fill and its ink are scheme-invariant;\n * `--ftshare-accent-on-surface` mixes the same yellow towards the host's text\n * colour, so the brand stays legible as the 11px eyebrow and as the focus ring\n * on either surface.\n */\nexport const SETTINGS_CSS = `\n.ftshare-settings { --ftshare-text:var(--dsw-alias-label-primary,#f4f4f5); --ftshare-muted:var(--dsw-alias-label-secondary,#aeb2bd); --ftshare-panel:var(--dsw-alias-settings-card-fill,#292a30); --ftshare-border:var(--dsw-alias-settings-card-stroke,#44464f); --ftshare-control:var(--dsw-alias-border-l4,#696c77); --ftshare-input:var(--dsw-alias-bg-layer-1,#1d1e23); --ftshare-link:var(--dsw-alias-link,#d8c4fc); --ftshare-error:var(--dsw-alias-state-error-primary,#ffb5ad); --ftshare-accent:var(--ft-accent,#f0c433); --ftshare-accent-ink:var(--ft-accent-ink,#171717); --ftshare-accent-on-surface:color-mix(in srgb,var(--ftshare-accent) 50%,var(--dsw-alias-label-primary,#f4f4f5)); }\n.ftshare-settings { color:var(--ftshare-text); max-width:760px; font-size:14px; line-height:1.6; }\n.ftshare-settings * { box-sizing:border-box; }\n.ftshare-settings h2 { font-size:26px; margin:4px 0 8px; }\n.ftshare-settings h3 { font-size:15px; margin:0 0 12px; }\n.ftshare-settings p { margin:8px 0; color:var(--ftshare-muted); }\n.ftshare-settings header { display:flex; justify-content:space-between; align-items:flex-start; gap:20px; margin-bottom:20px; }\n.ftshare-settings small { display:block; color:var(--ftshare-muted); font-size:12px; margin:5px 0 12px; }\n.ftshare-settings :is(button,input,a):focus-visible { outline:2px solid var(--ftshare-accent-on-surface); outline-offset:3px; }\n.ftshare-settings a { color:var(--ftshare-link); }\n@media(max-width:540px) { .ftshare-settings header { flex-direction:column; gap:10px; } }\n`\n","import type { ReactNode } from 'react'\nimport type { PropsRenderSlots, PropsRuntime, SlotCore, SlotMap } from '@deepseek-ai/dsh-client-ui-slots'\n// Type-only: pulls the `settings.section` SlotMap entry into this program.\nimport type {} from '@deepseek-ai/dsh-client-ui-settings/client'\nimport { FTSHARE_SETTINGS_CARD_SLOT, FTSHARE_SETTINGS_SECTION } from './contract.js'\nimport { SETTINGS_CSS } from './styles.js'\n\n/** Section slot this seam contributes into; declared by the settings shell. */\nconst FTSHARE_SETTINGS_SECTION_NAME = 'settings.section' as const\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface SlotMap {\n /** One card inside the FTShare settings section; `order` sets its position. */\n 'ftshare.settings.card': {\n kind: 'list'\n scope: 'root'\n owner: FtshareSettingsCardOwnerProps\n }\n }\n}\n/** Owner share of a card: the section supplies no card-specific props. */\nexport interface FtshareSettingsCardOwnerProps {\n /** Marker field: card owner props are intentionally empty. */\n children?: never\n}\n/**\n * The slice of the renderer-owned slots service this seam uses.\n *\n * `register` IS the ui-slots registration signature (the runtime registry\n * reuses it verbatim, adding disposal through the caller's fiber); `inject`\n * is the runtime wrapper's declaration-lifetime hook, which is why it is\n * mirrored structurally here.\n */\nexport interface FtshareSettingsSlotsService {\n readonly register: SlotCore['register']\n /** Run `callback` for each declaration lifetime of `key`. */\n inject(key: keyof SlotMap & string, callback: () => () => void): () => void\n}\n/**\n * Browser context the section shell requires: the renderer's slots service.\n * A card that needs more (its own services, invalidation events) declares the\n * extra members on its own context type.\n */\nexport interface FtshareSettingsShellContext {\n slots: FtshareSettingsSlotsService\n}\n/** Composed props of the section entry: the shell's owner share plus its declared child render. */\nexport type FtshareSettingsSectionProps =\n & PropsRuntime<typeof FTSHARE_SETTINGS_SECTION_NAME>\n & PropsRenderSlots<typeof FTSHARE_SETTINGS_CARD_SLOT>\n\n/**\n * The settings section shell: core CSS variables, the section chrome, and the\n * single render site every card appears at. Cards own everything inside it.\n */\nexport function FtshareSettingsSection({ renderSlot }: FtshareSettingsSectionProps): ReactNode {\n return <>\n <style>{SETTINGS_CSS}</style>\n {renderSlot(FTSHARE_SETTINGS_CARD_SLOT, {})}\n </>\n}\n\n/**\n * Declare the card slot and contribute the section that renders it.\n *\n * Declaring the child slot is what authorizes the render site; a card\n * registering through {@link FTSHARE_SETTINGS_CARD_SLOT} before this runs waits\n * for the declaration instead of failing.\n * @param ctx - client context carrying the renderer's slots service.\n * @returns idempotent disposer for the section and its declaration.\n */\nexport function registerSettingsSection(ctx: FtshareSettingsShellContext): () => void {\n return ctx.slots.inject(FTSHARE_SETTINGS_SECTION_NAME, () => ctx.slots.register(\n {\n name: FTSHARE_SETTINGS_SECTION_NAME,\n ...FTSHARE_SETTINGS_SECTION,\n children: { [FTSHARE_SETTINGS_CARD_SLOT]: { kind: 'list', scope: 'root' } },\n },\n FtshareSettingsSection,\n ))\n}\n","/**\n * FT brand tokens published as a host theme override layer.\n *\n * The layer only *adds* brand variables on `body`; it never overrides a\n * `--dsw-alias-*` value, so host chrome keeps its own palette and any user\n * theme keeps working underneath. Consumers read `var(--ft-*)` with the same\n * value as their fallback: the layer is a convenience, not a dependency, so\n * every sub-plugin keeps its brand colors when this plugin is absent (or when\n * the host has no `theme` service at all). Scheme-invariant tokens repeat one\n * value; palette-aware tokens spell both with `light-dark()`.\n *\n * Keep those fallbacks equal to {@link FT_BRAND_TOKENS}; `tests/brand.test.mjs`\n * scans every package for the pairing and fails on a drift.\n */\n\n/** One token value per palette mode; the host rejects a bare string. */\nexport interface BrandTokenModes {\n readonly light: string\n readonly dark: string\n}\n\n/** One value for both palettes: brand fills are scheme-invariant. */\nconst invariant = (value: string): BrandTokenModes => ({ light: value, dark: value })\n\n/** Layer identity reported to `ctx.theme.overrideTokens` (one layer per source). */\nexport const FT_BRAND_SOURCE = '@dsh-ft/dsh-fft-settings'\n\n/**\n * The published brand colors.\n *\n * `accent` is the FT yellow used by this plugin's primary action and eyebrow;\n * `accent-warm` is the orange trading accent shared by market and trade\n * surfaces (it is the only palette-aware group); `wechat` is the enterprise-\n * WeChat green the SSO card keeps as its identity. Inks are the text colors on\n * those fills, and `-soft` is the tinted surface behind the accent.\n */\nexport const FT_BRAND_TOKENS: Readonly<Record<string, BrandTokenModes>> = Object.freeze({\n '--ft-accent': invariant('#f0c433'),\n '--ft-accent-ink': invariant('#171717'),\n '--ft-accent-warm': { light: '#cf5623', dark: '#ff8352' },\n '--ft-accent-warm-hover': { light: '#b8481b', dark: '#ff9a72' },\n '--ft-accent-warm-ink': { light: '#ffffff', dark: '#1a120e' },\n '--ft-accent-warm-soft': { light: '#fbeee8', dark: '#2b1d16' },\n '--ft-wechat': invariant('#2ebd68'),\n '--ft-wechat-ink': invariant('#102419'),\n})\n\n/** The slice of the host theme service this layer uses. */\nexport interface BrandThemeService {\n overrideTokens(source: string, tokens: Readonly<Record<string, BrandTokenModes>>): () => void\n}\n\n/** Scoped context delivered once the `theme` service is available. */\nexport interface BrandThemeScope {\n readonly theme: BrandThemeService\n effect(callback: () => () => void, label: string): void\n}\n\n/** The slice of the client context this layer needs: a deferred service inject. */\nexport interface BrandThemeContext {\n inject(deps: readonly string[], callback: (scope: BrandThemeScope) => void): void\n}\n\n/**\n * Stack the brand layer once the host theme service exists.\n *\n * The deferred `inject` is deliberate: this plugin's declared client context\n * names only the services its cards read, and a host without `theme` must\n * still load the settings page — the CSS fallbacks then carry the colors.\n * @param ctx - client context carrying the host theme service.\n */\nexport function applyBrandTokens(ctx: BrandThemeContext): void {\n ctx.inject(['theme'], (scope) => {\n scope.effect(\n () => scope.theme.overrideTokens(FT_BRAND_SOURCE, FT_BRAND_TOKENS),\n 'ft-settings: FT brand tokens',\n )\n })\n}\n","import { registerApiKeyCard, type ApiKeyCardContext } from './features/api-key/card.js'\nimport { registerLogsCard } from './features/logs/card.js'\nimport { registerSettingsSection } from './core/shell.js'\nimport { applyBrandTokens, type BrandThemeContext } from './brand.js'\nexport * from './core/shell.js'\n/** Browser context this plugin's own client half requires. */\nexport type FtshareSettingsClientContext = ApiKeyCardContext\nexport const inject = ['slots', 'remote']\nexport function apply(ctx: FtshareSettingsClientContext): void {\n registerFtshareSettings(ctx)\n // The declared context above names only the services the cards read; the\n // brand layer defers on `theme` through the full Cordis context, so this\n // plugin still loads on a host without the theme service.\n applyBrandTokens(ctx as unknown as BrandThemeContext)\n}\n/** Contribute the section shell, then the built-in cards through the slot it declares. */\nexport function registerFtshareSettings(ctx: FtshareSettingsClientContext): void {\n registerSettingsSection(ctx)\n registerApiKeyCard(ctx)\n registerLogsCard(ctx)\n}\n"],"mappings":"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"}
@@ -7,5 +7,11 @@ export declare const PROBE_CONTRACTS: {
7
7
  };
8
8
  /** Error text is classified internally and never returned verbatim. */
9
9
  export declare function classifyFailure(code: number, text: string): ProbeResult;
10
- /** Two fixed, bounded requests. No SDK, arbitrary URL, retries or stored rows. */
10
+ /**
11
+ * Two fixed, bounded requests. No SDK, arbitrary URL, retries or stored rows.
12
+ *
13
+ * 配置了 Key 时两项检查都携带它:匿名额度按出口 IP 计,用尽后未携带 Key 的请求会被
14
+ * 网关以 429 拒绝,若远端检查仍走匿名就会把「Key 已配置且可用」误报成受限。未配置
15
+ * Key 时退回匿名远端检查(401/403 不能用来指控 Key)。
16
+ */
11
17
  export declare function probeApiKey(key: string | undefined, signal: AbortSignal, fetcher?: typeof fetch, now?: number, logger?: FeatureLogger): Promise<ValidationResult>;
@@ -61,10 +61,10 @@ async function readBoundedJson(response) {
61
61
  }
62
62
  /**
63
63
  * 一次固定探测。失败(发出过请求且没有通过)写一条 `warn`:
64
- * 字段固定 `{ target, status, durationMs }` —— `target` 由是否携带 Key 决定;
64
+ * 字段固定 `{ target, status, durationMs }`,`target` 由调用点显式给出;
65
65
  * 未发请求的 `not_run` 不是探测失败,成功也不记(规格 §6/§7)。
66
66
  */
67
- async function probe(path, params, key, parent, fetcher, now, logger) {
67
+ async function probe(target, path, params, key, parent, fetcher, now, logger) {
68
68
  const url = new URL(path, ORIGIN);
69
69
  url.search = new URLSearchParams(params).toString();
70
70
  const timeout = AbortSignal.timeout(PROBE_TIMEOUT_MS);
@@ -72,7 +72,7 @@ async function probe(path, params, key, parent, fetcher, now, logger) {
72
72
  const started = Date.now();
73
73
  const finish = (outcome) => {
74
74
  if (outcome.status !== 'available' && outcome.status !== 'not_run') {
75
- logger?.warn('FTShare 探测未通过', { target: key === undefined ? 'remote' : 'apiKey', status: outcome.status, durationMs: Date.now() - started });
75
+ logger?.warn('FTShare 探测未通过', { target, status: outcome.status, durationMs: Date.now() - started });
76
76
  }
77
77
  return outcome;
78
78
  };
@@ -106,7 +106,7 @@ async function probe(path, params, key, parent, fetcher, now, logger) {
106
106
  return finish(result('invalid_response', '接口未返回 K 线数组'));
107
107
  // This checks service/authentication success, not market-data availability.
108
108
  // Empty holiday or post-close windows are successful responses too.
109
- return finish(result('available', key === undefined ? '远端服务正常' : 'API Key 验证通过'));
109
+ return finish(result('available', target === 'remote' ? '远端服务正常' : 'API Key 验证通过'));
110
110
  }
111
111
  catch {
112
112
  if (parent.aborted)
@@ -116,12 +116,18 @@ async function probe(path, params, key, parent, fetcher, now, logger) {
116
116
  return finish(result('network_error', '连接失败,请检查网络后重试'));
117
117
  }
118
118
  }
119
- /** Two fixed, bounded requests. No SDK, arbitrary URL, retries or stored rows. */
119
+ /**
120
+ * Two fixed, bounded requests. No SDK, arbitrary URL, retries or stored rows.
121
+ *
122
+ * 配置了 Key 时两项检查都携带它:匿名额度按出口 IP 计,用尽后未携带 Key 的请求会被
123
+ * 网关以 429 拒绝,若远端检查仍走匿名就会把「Key 已配置且可用」误报成受限。未配置
124
+ * Key 时退回匿名远端检查(401/403 不能用来指控 Key)。
125
+ */
120
126
  export async function probeApiKey(key, signal, fetcher = fetch, now = Date.now(), logger) {
121
- const remote = await probe(PROBE_CONTRACTS.daily, {
127
+ const remote = await probe('remote', PROBE_CONTRACTS.daily, {
122
128
  symbol: '600519.SH', interval_unit: 'day', adjust_kind: 'none',
123
129
  since_ts_millis: String(now - 30 * DAY), until_ts_millis: String(now), limit: '2',
124
- }, undefined, signal, fetcher, now, logger);
130
+ }, key, signal, fetcher, now, logger);
125
131
  let apiKey;
126
132
  if (!key) {
127
133
  apiKey = result('auth_required', '未配置 API Key');
@@ -132,7 +138,7 @@ export async function probeApiKey(key, signal, fetcher = fetch, now = Date.now()
132
138
  else {
133
139
  // The server counts inclusive Beijing calendar dates, not elapsed days.
134
140
  // A 24-hour window covers at most two dates; empty data still verifies auth.
135
- apiKey = await probe(PROBE_CONTRACTS.historicalMinute, {
141
+ apiKey = await probe('apiKey', PROBE_CONTRACTS.historicalMinute, {
136
142
  symbol: '600519.SH', interval_value: '1', adjust_kind: 'none',
137
143
  since_ts_millis: String(now - DAY), until_ts_millis: String(now), limit: '1',
138
144
  }, key, signal, fetcher, now, logger);
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "FT Settings",
4
+ "description": "Shared plugin pages for the financial workbench."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "FT 设置",
4
+ "description": "金融工作台共享插件页"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,19 +1,16 @@
1
1
  {
2
2
  "name": "@dsh-ft/dsh-fft-settings",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "FT 设置与凭据底座:FTShare API Key、设置页卡片、运行日志与品牌主题层,供 FT 系列插件共享。",
5
- "type": "module",
6
- "main": "lib/index.js",
7
- "types": "lib/index.d.ts",
8
5
  "license": "MIT",
9
- "homepage": "https://github.com/gigaflw/dsh-fft-bundle/tree/dev/packages/dsh-fft-settings#readme",
6
+ "homepage": "https://github.com/gigaflw/dsh-fft-base/tree/dev/packages/dsh-fft-settings#readme",
10
7
  "repository": {
11
8
  "type": "git",
12
- "url": "git+https://github.com/gigaflw/dsh-fft-bundle.git",
9
+ "url": "git+https://github.com/gigaflw/dsh-fft-base.git",
13
10
  "directory": "packages/dsh-fft-settings"
14
11
  },
15
12
  "bugs": {
16
- "url": "https://github.com/gigaflw/dsh-fft-bundle/issues"
13
+ "url": "https://github.com/gigaflw/dsh-fft-base/issues"
17
14
  },
18
15
  "keywords": [
19
16
  "dsh-plugin",
@@ -21,6 +18,44 @@
21
18
  "ftshare",
22
19
  "settings"
23
20
  ],
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "type": "module",
25
+ "main": "lib/index.js",
26
+ "types": "lib/index.d.ts",
27
+ "scripts": {
28
+ "build": "tsc -p tsconfig.json && tsdown --config tsdown.config.ts",
29
+ "prepare": "pnpm run build",
30
+ "typecheck": "tsc -p tsconfig.json --noEmit",
31
+ "test": "pnpm run build && node --test tests/*.test.mjs",
32
+ "theme:preview": "pnpm run build && node tools/theme-preview.mjs"
33
+ },
34
+ "files": [
35
+ "lib",
36
+ "src",
37
+ "README.md",
38
+ "LICENSE",
39
+ "docs",
40
+ "cordis.patch.yml",
41
+ "locale/*.json",
42
+ "package.json"
43
+ ],
44
+ "dsh": {
45
+ "bundle": {
46
+ "patch": "./cordis.patch.yml"
47
+ },
48
+ "client": {
49
+ "platform": "web",
50
+ "inject": [
51
+ "@deepseek-ai/dsh-client-ui-slots",
52
+ "@deepseek-ai/dsh-api-remotes"
53
+ ]
54
+ },
55
+ "engines": {
56
+ "dsh": ">=0.2.0-rc.2"
57
+ }
58
+ },
24
59
  "exports": {
25
60
  ".": {
26
61
  "types": "./lib/index.d.ts",
@@ -41,23 +76,11 @@
41
76
  "./client-api": {
42
77
  "types": "./lib/rpc.d.ts",
43
78
  "default": "./lib/rpc.js"
44
- }
45
- },
46
- "files": [
47
- "lib",
48
- "src",
49
- "README.md",
50
- "LICENSE",
51
- "docs",
52
- "cordis.patch.yml",
53
- "package.json"
54
- ],
55
- "publishConfig": {
56
- "access": "public"
79
+ },
80
+ "./package.json": "./package.json",
81
+ "./locale/*.json": "./locale/*.json"
57
82
  },
58
83
  "dependencies": {
59
- "@deepseek-ai/dsh-credentials": ">=0.2.0-rc.2",
60
- "@deepseek-ai/schemastery": ">=3.18.4",
61
84
  "fflate": ">=0.8.3"
62
85
  },
63
86
  "peerDependencies": {
@@ -65,24 +88,23 @@
65
88
  "@deepseek-ai/dsh-api-remotes": ">=0.2.0-rc.2",
66
89
  "@deepseek-ai/dsh-client-ui-settings": ">=0.2.0-rc.2",
67
90
  "@deepseek-ai/dsh-client-ui-slots": ">=0.2.0-rc.2",
68
- "react": "^18.2.0"
91
+ "react": "^18.2.0",
92
+ "@deepseek-ai/dsh-credentials": ">=0.2.0-rc.2",
93
+ "@deepseek-ai/schemastery": ">=3.18.4"
94
+ },
95
+ "devDependencies": {
96
+ "@types/node": "^22.15.0",
97
+ "@types/react": "^18.3.24",
98
+ "react": "^18.3.1",
99
+ "tsdown": "^0.22.2",
100
+ "typescript": "^5.8.3",
101
+ "@deepseek-ai/cordis": "catalog:",
102
+ "@deepseek-ai/dsh-client-ui-settings": "catalog:",
103
+ "@deepseek-ai/dsh-client-ui-slots": "catalog:",
104
+ "@deepseek-ai/dsh-credentials": "catalog:",
105
+ "@deepseek-ai/schemastery": "catalog:"
69
106
  },
70
107
  "engines": {
71
108
  "node": ">=22.19.0"
72
- },
73
- "dsh": {
74
- "bundle": {
75
- "patch": "./cordis.patch.yml"
76
- },
77
- "client": {
78
- "platform": "web",
79
- "inject": [
80
- "@deepseek-ai/dsh-client-ui-slots",
81
- "@deepseek-ai/dsh-api-remotes"
82
- ]
83
- },
84
- "engines": {
85
- "dsh": ">=0.2.0-rc.2"
86
- }
87
109
  }
88
110
  }
@@ -47,12 +47,15 @@ async function readBoundedJson(response: Response): Promise<unknown> {
47
47
  return JSON.parse(new TextDecoder().decode(bytes))
48
48
  }
49
49
 
50
+ /** 日志里的分项:远端服务检查与 Key 验证分开记,不因是否携带 Key 而错位。 */
51
+ type ProbeTarget = 'remote' | 'apiKey'
52
+
50
53
  /**
51
54
  * 一次固定探测。失败(发出过请求且没有通过)写一条 `warn`:
52
- * 字段固定 `{ target, status, durationMs }` —— `target` 由是否携带 Key 决定;
55
+ * 字段固定 `{ target, status, durationMs }`,`target` 由调用点显式给出;
53
56
  * 未发请求的 `not_run` 不是探测失败,成功也不记(规格 §6/§7)。
54
57
  */
55
- async function probe(path: string, params: Record<string, string>, key: string | undefined, parent: AbortSignal, fetcher: typeof fetch, now: number, logger?: FeatureLogger): Promise<ProbeResult> {
58
+ async function probe(target: ProbeTarget, path: string, params: Record<string, string>, key: string | undefined, parent: AbortSignal, fetcher: typeof fetch, now: number, logger?: FeatureLogger): Promise<ProbeResult> {
56
59
  const url = new URL(path, ORIGIN)
57
60
  url.search = new URLSearchParams(params).toString()
58
61
  const timeout = AbortSignal.timeout(PROBE_TIMEOUT_MS)
@@ -60,7 +63,7 @@ async function probe(path: string, params: Record<string, string>, key: string |
60
63
  const started = Date.now()
61
64
  const finish = (outcome: ProbeResult): ProbeResult => {
62
65
  if (outcome.status !== 'available' && outcome.status !== 'not_run') {
63
- logger?.warn('FTShare 探测未通过', { target: key === undefined ? 'remote' : 'apiKey', status: outcome.status, durationMs: Date.now() - started })
66
+ logger?.warn('FTShare 探测未通过', { target, status: outcome.status, durationMs: Date.now() - started })
64
67
  }
65
68
  return outcome
66
69
  }
@@ -84,7 +87,7 @@ async function probe(path: string, params: Record<string, string>, key: string |
84
87
  if (!Array.isArray(payload.data)) return finish(result('invalid_response', '接口未返回 K 线数组'))
85
88
  // This checks service/authentication success, not market-data availability.
86
89
  // Empty holiday or post-close windows are successful responses too.
87
- return finish(result('available', key === undefined ? '远端服务正常' : 'API Key 验证通过'))
90
+ return finish(result('available', target === 'remote' ? '远端服务正常' : 'API Key 验证通过'))
88
91
  } catch {
89
92
  if (parent.aborted) return finish(result('not_run', '测试已取消或凭据已变化'))
90
93
  if (timeout.aborted) return finish(result('timeout', '连接超时,请稍后重试'))
@@ -92,12 +95,18 @@ async function probe(path: string, params: Record<string, string>, key: string |
92
95
  }
93
96
  }
94
97
 
95
- /** Two fixed, bounded requests. No SDK, arbitrary URL, retries or stored rows. */
98
+ /**
99
+ * Two fixed, bounded requests. No SDK, arbitrary URL, retries or stored rows.
100
+ *
101
+ * 配置了 Key 时两项检查都携带它:匿名额度按出口 IP 计,用尽后未携带 Key 的请求会被
102
+ * 网关以 429 拒绝,若远端检查仍走匿名就会把「Key 已配置且可用」误报成受限。未配置
103
+ * Key 时退回匿名远端检查(401/403 不能用来指控 Key)。
104
+ */
96
105
  export async function probeApiKey(key: string | undefined, signal: AbortSignal, fetcher: typeof fetch = fetch, now: number = Date.now(), logger?: FeatureLogger): Promise<ValidationResult> {
97
- const remote = await probe(PROBE_CONTRACTS.daily, {
106
+ const remote = await probe('remote', PROBE_CONTRACTS.daily, {
98
107
  symbol: '600519.SH', interval_unit: 'day', adjust_kind: 'none',
99
108
  since_ts_millis: String(now - 30 * DAY), until_ts_millis: String(now), limit: '2',
100
- }, undefined, signal, fetcher, now, logger)
109
+ }, key, signal, fetcher, now, logger)
101
110
  let apiKey: ProbeResult
102
111
  if (!key) {
103
112
  apiKey = result('auth_required', '未配置 API Key')
@@ -106,7 +115,7 @@ export async function probeApiKey(key: string | undefined, signal: AbortSignal,
106
115
  } else {
107
116
  // The server counts inclusive Beijing calendar dates, not elapsed days.
108
117
  // A 24-hour window covers at most two dates; empty data still verifies auth.
109
- apiKey = await probe(PROBE_CONTRACTS.historicalMinute, {
118
+ apiKey = await probe('apiKey', PROBE_CONTRACTS.historicalMinute, {
110
119
  symbol: '600519.SH', interval_value: '1', adjust_kind: 'none',
111
120
  since_ts_millis: String(now - DAY), until_ts_millis: String(now), limit: '1',
112
121
  }, key, signal, fetcher, now, logger)