@dsh-ft/dsh-fft-settings 0.0.0-stage → 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.
Files changed (132) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +395 -2
  3. package/cordis.patch.yml +18 -0
  4. package/docs/compatibility-dsh-0.1.6-alpha.1.md +58 -0
  5. package/docs/extension-service-design.md +351 -0
  6. package/docs/extension-service-sequences.html +545 -0
  7. package/docs/ftshare-settings-er.html +139 -0
  8. package/docs/ftshare-settings-er.svg +113 -0
  9. package/docs/ftshare-settings-results-er.html +148 -0
  10. package/docs/ftshare-settings-results-er.svg +122 -0
  11. package/docs/kline-integration.md +258 -0
  12. package/lib/agent.d.ts +24 -0
  13. package/lib/agent.js +26 -0
  14. package/lib/brand.d.ts +53 -0
  15. package/lib/brand.js +50 -0
  16. package/lib/client.d.ts +8 -0
  17. package/lib/client.js +806 -0
  18. package/lib/client.js.map +1 -0
  19. package/lib/contract.d.ts +14 -0
  20. package/lib/contract.js +14 -0
  21. package/lib/core/client-transport.d.ts +15 -0
  22. package/lib/core/client-transport.js +31 -0
  23. package/lib/core/contract.d.ts +62 -0
  24. package/lib/core/contract.js +28 -0
  25. package/lib/core/module-registry.d.ts +54 -0
  26. package/lib/core/module-registry.js +68 -0
  27. package/lib/core/rpc-core.d.ts +69 -0
  28. package/lib/core/rpc-core.js +88 -0
  29. package/lib/core/shell.d.ts +59 -0
  30. package/lib/core/shell.js +28 -0
  31. package/lib/core/styles.d.ts +31 -0
  32. package/lib/core/styles.js +43 -0
  33. package/lib/core/transport.d.ts +19 -0
  34. package/lib/core/transport.js +80 -0
  35. package/lib/default-preset.d.ts +52 -0
  36. package/lib/default-preset.js +121 -0
  37. package/lib/features/api-key/card.d.ts +24 -0
  38. package/lib/features/api-key/card.js +39 -0
  39. package/lib/features/api-key/client-api.d.ts +4 -0
  40. package/lib/features/api-key/client-api.js +11 -0
  41. package/lib/features/api-key/contract.d.ts +69 -0
  42. package/lib/features/api-key/contract.js +24 -0
  43. package/lib/features/api-key/host.d.ts +17 -0
  44. package/lib/features/api-key/host.js +32 -0
  45. package/lib/features/api-key/index.d.ts +13 -0
  46. package/lib/features/api-key/index.js +12 -0
  47. package/lib/features/api-key/logging.d.ts +15 -0
  48. package/lib/features/api-key/logging.js +1 -0
  49. package/lib/features/api-key/model.d.ts +24 -0
  50. package/lib/features/api-key/model.js +105 -0
  51. package/lib/features/api-key/probe.d.ts +17 -0
  52. package/lib/features/api-key/probe.js +148 -0
  53. package/lib/features/api-key/rpc.d.ts +4 -0
  54. package/lib/features/api-key/rpc.js +17 -0
  55. package/lib/features/api-key/service.d.ts +21 -0
  56. package/lib/features/api-key/service.js +181 -0
  57. package/lib/features/api-key/styles.d.ts +9 -0
  58. package/lib/features/api-key/styles.js +24 -0
  59. package/lib/features/logs/archive.d.ts +87 -0
  60. package/lib/features/logs/archive.js +156 -0
  61. package/lib/features/logs/card.d.ts +22 -0
  62. package/lib/features/logs/card.js +107 -0
  63. package/lib/features/logs/client-api.d.ts +4 -0
  64. package/lib/features/logs/client-api.js +11 -0
  65. package/lib/features/logs/collector.d.ts +91 -0
  66. package/lib/features/logs/collector.js +258 -0
  67. package/lib/features/logs/config.d.ts +70 -0
  68. package/lib/features/logs/config.js +97 -0
  69. package/lib/features/logs/contract.d.ts +130 -0
  70. package/lib/features/logs/contract.js +67 -0
  71. package/lib/features/logs/export.d.ts +92 -0
  72. package/lib/features/logs/export.js +212 -0
  73. package/lib/features/logs/format.d.ts +42 -0
  74. package/lib/features/logs/format.js +172 -0
  75. package/lib/features/logs/home.d.ts +18 -0
  76. package/lib/features/logs/home.js +39 -0
  77. package/lib/features/logs/host.d.ts +19 -0
  78. package/lib/features/logs/host.js +139 -0
  79. package/lib/features/logs/index.d.ts +12 -0
  80. package/lib/features/logs/index.js +12 -0
  81. package/lib/features/logs/rpc.d.ts +6 -0
  82. package/lib/features/logs/rpc.js +27 -0
  83. package/lib/features/logs/styles.d.ts +9 -0
  84. package/lib/features/logs/styles.js +13 -0
  85. package/lib/guidance.d.ts +41 -0
  86. package/lib/guidance.js +88 -0
  87. package/lib/index.d.ts +67 -0
  88. package/lib/index.js +50 -0
  89. package/lib/rpc.d.ts +10 -0
  90. package/lib/rpc.js +10 -0
  91. package/locale/en.json +6 -0
  92. package/locale/zh.json +6 -0
  93. package/package.json +108 -4
  94. package/src/agent.ts +31 -0
  95. package/src/brand.ts +79 -0
  96. package/src/client.tsx +21 -0
  97. package/src/contract.ts +14 -0
  98. package/src/core/client-transport.ts +37 -0
  99. package/src/core/contract.ts +57 -0
  100. package/src/core/module-registry.ts +102 -0
  101. package/src/core/rpc-core.ts +107 -0
  102. package/src/core/shell.tsx +81 -0
  103. package/src/core/styles.ts +43 -0
  104. package/src/core/transport.ts +82 -0
  105. package/src/default-preset.ts +151 -0
  106. package/src/features/api-key/card.tsx +77 -0
  107. package/src/features/api-key/client-api.ts +12 -0
  108. package/src/features/api-key/contract.ts +63 -0
  109. package/src/features/api-key/host.ts +36 -0
  110. package/src/features/api-key/index.ts +13 -0
  111. package/src/features/api-key/logging.ts +15 -0
  112. package/src/features/api-key/model.ts +63 -0
  113. package/src/features/api-key/probe.ts +125 -0
  114. package/src/features/api-key/rpc.ts +18 -0
  115. package/src/features/api-key/service.ts +157 -0
  116. package/src/features/api-key/styles.ts +24 -0
  117. package/src/features/logs/archive.ts +230 -0
  118. package/src/features/logs/card.tsx +169 -0
  119. package/src/features/logs/client-api.ts +20 -0
  120. package/src/features/logs/collector.ts +313 -0
  121. package/src/features/logs/config.ts +115 -0
  122. package/src/features/logs/contract.ts +149 -0
  123. package/src/features/logs/export.ts +274 -0
  124. package/src/features/logs/format.ts +188 -0
  125. package/src/features/logs/home.ts +43 -0
  126. package/src/features/logs/host.ts +153 -0
  127. package/src/features/logs/index.ts +12 -0
  128. package/src/features/logs/rpc.ts +29 -0
  129. package/src/features/logs/styles.ts +13 -0
  130. package/src/guidance.ts +114 -0
  131. package/src/index.ts +59 -0
  132. package/src/rpc.ts +10 -0
@@ -0,0 +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":";;;;;;;;EAUA,MAAa,4BAA4B,CAAC,+BAA+B;;EAwBzE,SAAgB,gBAAgB,OAAuB;GACrD,MAAM,QAAQ,MAAM,KAAK;GACzB,IAAI,CAAC,OAAO,MAAM,IAAI,MAAM,aAAa;GACzC,IAAI,MAAM,SAAS,OAAO,kBAAkB,KAAK,KAAK,GAAG,MAAM,IAAI,MAAM,kCAAkC;GAC3G,OAAO;EACT;;;;;;;;;ECrBA,MAAa,2BAA2B;GAAE,IAAI;GAAW,OAAO;GAAK,OAAO;EAAO;;;;;;;;EAQnF,MAAa,6BAA6B;;;;;;;;EAQ1C,MAAa,yBAAyB;;EChCtC,IAAa,uBAAb,MAAkC;GAKZ;GAJpB,QAA+B;IAAE,MAAM;IAAO,SAAS;GAAG;GAC1D,4BAAoB,IAAI,IAAgB;GACxC,UAAkB;GAClB,WAAmB;GACnB,YAAY,KAA8B;IAAtB,KAAA,MAAA;GAAuB;GAC3C,oBAAmC,KAAK;GACxC,aAAa,aAAuC;IAAE,KAAK,UAAU,IAAI,QAAQ;IAAG,aAAa;KAAE,KAAK,UAAU,OAAO,QAAQ;IAAE;GAAE;GACrI,OAAe,OAAqC;IAAE,KAAK,QAAQ;KAAE,GAAG,KAAK;KAAO,GAAG;IAAM;IAAG,KAAK,MAAM,YAAY,KAAK,WAAW,SAAS;GAAE;GAClJ,QAAQ,WAA0C;IAChD,MAAM,UAAU,kBAAkB;KAAE,KAAU,QAAQ;IAAE,CAAC;IACzD,KAAU,QAAQ;IAClB,aAAa;KAAE,KAAK;KAAW,KAAK;KAAY,UAAU;IAAE;GAC9D;GACA,MAAM,UAAyB;IAC7B,MAAM,UAAU,EAAE,KAAK;IACvB,IAAI;KACF,MAAM,SAAS,MAAM,KAAK,IAAI,SAAS;KACvC,IAAI,CAAC,UAAU,OAAO,OAAO,eAAe,WAAW,MAAM,IAAI,MAAM,gBAAgB;KACvF,IAAI,YAAY,KAAK,SAAS,KAAK,OAAO;MAAE;MAAQ,aAAa,KAAA;KAAU,CAAC;IAC9E,QAAQ;KAAE,IAAI,YAAY,KAAK,SAAS,KAAK,OAAO;MAAE,QAAQ,KAAA;MAAW,aAAa;KAAiB,CAAC;IAAE;GAC5G;GACA,MAAM,iBAAiB,OAAiC;IACtD,IAAI,KAAK,MAAM,QAAQ,CAAC,KAAK,MAAM,QAAQ,UAAU,OAAO;IAC5D,IAAI;IACJ,IAAI;KAAE,QAAQ,gBAAgB,KAAK;IAAE,SAAS,OAAO;KAAE,KAAK,OAAO,EAAE,SAAU,MAAgB,QAAQ,CAAC;KAAG,OAAO;IAAM;IACxH,MAAM,WAAW,KAAK;IACtB,KAAK,OAAO;KAAE,MAAM;KAAM,MAAM,KAAA;KAAW,SAAS;IAAwB,CAAC;IAC7E,IAAI,QAAQ;IACZ,IAAI;KACF,MAAM,SAAS,MAAM,KAAK,IAAI,iBAAiB,KAAK;KACpD,QAAQ,OAAO,KAAK,WAAW;KAC/B,IAAI,aAAa,KAAK,UAAU,KAAK,OAAO;MAAE,MAAM,OAAO;MAAM,SAAS,QAAQ,kBAAkB,OAAO,WAAW,MAAM,IAAI,OAAO,KAAK;KAAQ,CAAC;IACvJ,QAAQ;KAAE,IAAI,aAAa,KAAK,UAAU,KAAK,OAAO,EAAE,SAAS,2BAA2B,CAAC;IAAE;IAC/F,IAAI,aAAa,KAAK,UAAU;KAAE,MAAM,KAAK,QAAQ;KAAG,KAAK,OAAO,EAAE,MAAM,MAAM,CAAC;IAAE;IACrF,OAAO;GACT;GACA,MAAM,iBAAgC;IACpC,IAAI,KAAK,MAAM,QAAQ,CAAC,KAAK,MAAM,QAAQ;IAC3C,MAAM,WAAW,KAAK;IACtB,KAAK,OAAO;KAAE,MAAM;KAAM,SAAS;IAAuB,CAAC;IAC3D,IAAI;KACF,MAAM,SAAS,MAAM,KAAK,IAAI,eAAe;KAC7C,IAAI,aAAa,KAAK,UAAU,KAAK,OAAO,EAAE,SAAS,kBAAkB,OAAO,MAAM,EAAE,CAAC;IAC3F,QAAQ;KAAE,IAAI,aAAa,KAAK,UAAU,KAAK,OAAO,EAAE,SAAS,aAAa,CAAC;IAAE;IACjF,IAAI,aAAa,KAAK,UAAU;KAAE,MAAM,KAAK,QAAQ;KAAG,KAAK,OAAO,EAAE,MAAM,MAAM,CAAC;IAAE;GACvF;GACA,MAAM,QAAuB;IAC3B,IAAI,KAAK,MAAM,QAAQ,CAAC,KAAK,MAAM,QAAQ,UAAU;IACrD,MAAM,WAAW,KAAK;IACtB,KAAK,OAAO;KAAE,MAAM;KAAM,MAAM,KAAA;KAAW,SAAS;IAAG,CAAC;IACxD,IAAI;KACF,MAAM,KAAK,IAAI,MAAM;KACrB,IAAI,aAAa,KAAK,UAAU,KAAK,OAAO,EAAE,SAAS,qCAAqC,CAAC;IAC/F,QAAQ;KAAE,IAAI,aAAa,KAAK,UAAU,KAAK,OAAO,EAAE,SAAS,gBAAgB,CAAC;IAAE;IACpF,IAAI,aAAa,KAAK,UAAU;KAAE,MAAM,KAAK,QAAQ;KAAG,KAAK,OAAO,EAAE,MAAM,MAAM,CAAC;IAAE;GACvF;EACF;EACA,SAAS,kBAAkB,QAAwB;GACjD,OAAQ;IAAE,QAAQ;IAAuB,SAAS;IAA2B,QAAQ;IAAiB,SAAS;IAAkB,YAAY;IAAe,YAAY;GAAO,EAA6B,WAAW;EACzN;;;;;;;;;;;ECmCA,SAAS,wBAAwB,UAAkB,OAA4C;GAC7F,IAAI,OAAO,SAAS,aAAa,OAAO,gBAAgB,SAAS;GACjE,OAAO,kBAAkB,OAAO,QAAQ,UAAU,IAAI,OAAO,WAAW;EAC1E;EACA,eAAsB,gBAAmB,WAA4B,WAAmB,QAAgB,UAAmB,CAAC,GAAe;GACzI,MAAM,WAAW,GAAG,UAAU,GAAG;GACjC,MAAM,SAAS,MAAM,UAAU,KAAK,UAAU,OAAO;GACrD,IAAI,CAAC,OAAO,IAAI,MAAM,IAAI,MAAM,wBAAwB,UAAU,OAAO,KAAK,CAAC;GAC/E,OAAO,OAAO;EAChB;;;ECvGA,SAAgB,mBAAmB,WAA6C;GAC9E,eAAe,KAAQ,QAAsB,UAAmB,CAAC,GAAe;IAC9E,OAAO,gBAAmB,WAAW,UAAU,QAAQ,OAAO;GAChE;GACA,OAAO;IACL,gBAAgB,KAAK,UAAU;IAAG,MAAK,UAAS,KAAK,OAAO,EAAE,MAAM,CAAC;IAAG,aAAa,KAAK,OAAO;IACjG,mBAAkB,UAAS,KAAK,oBAAoB,EAAE,MAAM,CAAC;IAAG,sBAAsB,KAAK,gBAAgB;IAAG,2BAA2B,KAAK,qBAAqB;GACrK;EACF;;;;;;;;;;;;;ECOA,SAAgB,wBAAwB,SAA0C;GAChF,MAAM,OAAsB,aAAa,OAAO,SAAS,WAAW,MAAM,OAAO,IAAI;GACrF,OAAO,EACL,MAAM,KAAK,UAAU,UAAU,CAAC,GAAG,QAAQ;IACzC,MAAM,WAAW,MAAM,KAAK,wBAAwB;KAClD,QAAQ;KACR,SAAS,EAAE,gBAAgB,mBAAmB;KAC9C,MAAM,KAAK,UAAU;MAAE;MAAU;KAAQ,CAAC;KAC1C,GAAG,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO;IAC1C,CAAC;IACD,IAAI,CAAC,SAAS,IAGZ,MAAM,IAAI,MAAM,yBAAyB,SAAS,SAAS,SAAS,QAAQ;IAE9E,OAAO,MAAM,SAAS,KAAK;GAC7B,EACF;EACF;;;;;;;;;;EC5BA,MAAa,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;ECgBhC,SAAgB,mBAAmB,KAAoC;GACrE,OAAO,IAAI,MAAM,OAAO,kCAAkC,IAAI,MAAM,SAClE;IAAE,MAAM;IAA4B,IAAI;IAAU,OAAO;GAAG,GAC5D,iBAAiB,GAAG,CACtB,CAAC;EACH;;EAGA,SAAS,iBAAiB,KAAyC;GACjE,MAAM,MAAM,mBAAmB,wBAAwB,CAAC;GACxD,MAAM,oBAAqC,aAAY;IACrD,MAAM,YAAY,0BAA0B,KAAI,UAAS,IAAI,OAAO,IAAI,QAAO,QAAO;KAAE,IAAI,QAAA,mBAA6B,SAAS;IAAE,CAAC,CAAC;IACtI,UAAU,KAAK,IAAI,GAAG,oBAAoB,QAAQ,CAAC;IACnD,aAAa;KAAE,KAAK,MAAM,WAAW,WAAW,QAAQ;IAAE;GAC5D;GACA,aAAa,iBAAA,GAAA,kBAAA,IAAA,CAAC,YAAD;IAAiB;IAAuB;GAAmB,CAAA;EAC1E;EAEA,SAAgB,WAAW,EAAE,KAAK,oBAAgH;GAChJ,MAAM,SAAA,GAAQA,MAAAA,QAAAA,OAAc,IAAI,qBAAqB,GAAG,GAAG,CAAC,GAAG,CAAC;GAChE,MAAM,EAAE,QAAQ,MAAM,SAAS,aAAa,UAAA,GAASC,MAAAA,qBAAAA,CAAqB,MAAM,WAAW,MAAM,aAAa,MAAM,WAAW;GAC/H,MAAM,CAAC,OAAO,aAAA,GAAYC,MAAAA,SAAAA,CAAS,EAAE;GACrC,CAAA,GAAA,MAAA,UAAA,OAAgB,MAAM,QAAQ,gBAAgB,GAAG,CAAC,OAAO,gBAAgB,CAAC;GAC1E,MAAM,WAAW,QAAQ,CAAC,QAAQ;GAClC,MAAM,YAAY,YAAY;IAAE,IAAI,MAAM,MAAM,iBAAiB,KAAK,GAAG,SAAS,EAAE;GAAE;GACtF,OAAO,iBAAA,GAAA,kBAAA,KAAA,CAAA,kBAAA,UAAA,EAAA,UAAA,CACL,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD,EAAA,UAAQ,iBAAwB,CAAA,GAChC,iBAAA,GAAA,kBAAA,KAAA,CAAC,WAAD;IAAS,WAAU;IAAmB,cAAW;IAAa,aAAW;IAAzE,UAAA;KACA,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD,EAAA,UAAQ,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD,EAAA,UAAA,CAAK,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;MAAM,WAAU;MAAkB,UAAA;KAAuB,CAAA,GAAC,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD,EAAA,UAAI,OAAQ,CAAA,CAAM,EAAA,CAAA,EAAS,CAAA;KAClG,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD,EAAA,UAAG,oBAAoB,CAAA;KACtB,UAAU,CAAC,OAAO,YAAY,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD,EAAA,UAAG,8BAA8B,CAAA;KAChE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;MAAK,WAAU;MAAf,UAAA;OAA8B,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;QAAO,SAAQ;QAAkB,UAAA;OAAsB,CAAA;OACnF,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;QAAO,IAAG;QAAkB,MAAK;QAAW,OAAO;QAAO,aAAa,QAAQ,aAAa,OAAO,eAAe,aAAa;QAAe,WAAU,MAAK,SAAS,EAAE,OAAO,KAAK;QAAa;QAAU,cAAa;QAAe,WAAW;OAAM,CAAA;OACxP,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;QAAK,WAAU;QAAf,UAAA;SACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;UAAQ,WAAU;UAAU,MAAK;UAAS,eAAe,KAAK,UAAU;UAAG,UAAU,YAAY,CAAC,MAAM,KAAK;UAAG,UAAA;SAAa,CAAA;SAC7H,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;UAAQ,MAAK;UAAS,eAAe,KAAK,MAAM,eAAe;UAAG,UAAU,QAAQ,CAAC;UAAQ,UAAA;SAAc,CAAA;SAC3G,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;UAAQ,MAAK;UAAS,eAAe,KAAK,MAAM,MAAM;UAAG,UAAU,YAAY,CAAC,QAAQ;UAAY,UAAA;SAAe,CAAA;SACnH,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;UAAQ,MAAK;UAAS,eAAe,KAAK,MAAM,QAAQ;UAAG,UAAU;UAAM,OAAM;UAA6B,UAAA;SAAc,CAAA;SAC5H,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD;UAAG,MAAK;UAA2C,QAAO;UAAS,KAAI;UAAa,UAAA;SAAe,CAAA;QAChG;;OACJ,SAAS,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD,EAAA,UAAO,mCAAuC,CAAA;OACvD,QAAQ,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD;QAAG,MAAK;QAAS,UAAA;OAAY,CAAA;MACnC;;KACL,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;MAAK,WAAU;MAAe,aAAU;MAAxC,UAAA;OAAiD,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD,EAAA,UAAI,OAAQ,CAAA;OAAC,iBAAA,GAAA,kBAAA,KAAA,CAAC,MAAD,EAAA,UAAA;QAC3D,QAAQ,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;SAAK,WAAU;SAAf,UAAA,CAAgC,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD,EAAA,UAAI,OAAQ,CAAA,GAAC,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD,EAAA,UAAK,KAAK,QAAY,CAAA,CAAM;;QAClF,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;SAAK,WAAU;SAAf,UAAA,CAAgC,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD,EAAA,UAAI,OAAQ,CAAA,GAAC,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD,EAAA,UAAK,QAAQ,WAAW,OAAO,WAAW,QAAY,CAAA,CAAM;;QACzG,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;SAAK,WAAU;SAAf,UAAA,CAAgC,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD,EAAA,UAAI,UAAW,CAAA,GAAC,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD,EAAA,UAAK,QAAQ,WAAW,OAAO,WAAW,QAAY,CAAA,CAAM;;QAC3G,QAAQ,WAAW,YAAY,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;SAAK,WAAU;SAAf,UAAA,CAAgC,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD,EAAA,UAAI,OAAQ,CAAA,GAAC,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD,EAAA,UAAK,IAAI,KAAK,OAAO,WAAW,QAAQ,CAAC,CAAC,eAAe,EAAM,CAAA,CAAM;;OAChJ,EAAA,CAAA;OAAC,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD,EAAA,UAAO,oBAAwB,CAAA;MAAM;;KACzC,WAAW,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD;MAAG,MAAK;MAAU,UAAA;KAAW,CAAA;KAAG,eAAe,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD;MAAG,WAAU;MAAgB,MAAK;MAAS,UAAA;KAAe,CAAA;IAC5G;GACT,CAAA,CAAA,EAAA,CAAA;EACJ;;;;;;;;;;;;;EChEA,MAAa,eAAe;;;;;;;;EA6D5B,MAAa,qBAAqB;;;EC9DlC,SAAgB,iBAAiB,WAA2C;GAC1E,MAAM,QAAW,QAAoB,UAAmB,CAAC,MACvD,gBAAmB,WAAW,QAAQ,QAAQ,OAAO;GACvD,OAAO;IACL,aAAa,KAAgB,OAAO;IACpC,UAAU,QAAA,OAAuC,KAAkB,WAAW,EAAE,MAAM,CAAC;IACvF,cAAc,KAAiB,QAAQ;GACzC;EACF;;;;;;;;;;ECXA,MAAa,gBAAgB;;;;;;;ECU7B,MAAM,kBAAkB;GAAC;;;EAAsD;;;;;;;;;EAU/E,SAAgB,iBAAiB,QAAyB;GACxD,OAAO,SAAS,SAAS;EAC3B;EAEA,SAAS,aAAa,OAAuB;GAC3C,IAAI,QAAQ,MAAM,OAAO,GAAG,MAAM;GAClC,IAAI,QAAQ,SAAa,OAAO,IAAI,QAAQ,KAAA,CAAM,QAAQ,CAAC,EAAE;GAC7D,OAAO,IAAI,QAAQ,OAAO,KAAA,CAAM,QAAQ,CAAC,EAAE;EAC7C;;EAGA,SAAS,aAAa,UAAkB,MAAoB;GAC1D,MAAM,MAAM,IAAI,gBAAgB,IAAI,KAAK,CAAC,IAAI,GAAG,EAAE,MAAM,2BAA2B,CAAC,CAAC;GACtF,MAAM,SAAS,SAAS,cAAc,GAAG;GACzC,OAAO,OAAO;GACd,OAAO,WAAW;GAClB,OAAO,MAAM;GACb,SAAS,KAAK,YAAY,MAAM;GAChC,OAAO,MAAM;GACb,OAAO,OAAO;GACd,iBAAiB,IAAI,gBAAgB,GAAG,GAAG,CAAC;EAC9C;;;;;;;EAQA,SAAgB,iBAAiB,KAA8C;GAC7E,OAAO,IAAI,MAAM,OAAO,kCAAkC,IAAI,MAAM,SAClE;IAAE,MAAM;IAA4B,IAAI;IAAc,OAAA;GAAuB,GAC7E,eAAe,CACjB,CAAC;EACH;;EAGA,SAAS,iBAAkC;GACzC,MAAM,MAAM,iBAAiB,wBAAwB,CAAC;GACtD,aAAa,iBAAA,GAAA,kBAAA,IAAA,CAAC,aAAD,EAAkB,IAAM,CAAA;EACvC;EAEA,SAAgB,YAAY,EAAE,OAAmD;GAC/E,MAAM,CAAC,OAAO,aAAA,GAAYC,MAAAA,SAAAA,CAAoB;GAC9C,MAAM,CAAC,SAAS,eAAA,GAAcA,MAAAA,SAAAA,CAAsB;GACpD,MAAM,CAAC,OAAO,aAAA,GAAYA,MAAAA,SAAAA,CAAAA,EAAmC;GAC7D,MAAM,CAAC,MAAM,YAAA,GAAWA,MAAAA,SAAAA,CAAS,KAAK;GACtC,MAAM,CAAC,SAAS,eAAA,GAAcA,MAAAA,SAAAA,CAAS,EAAE;GACzC,MAAM,CAAC,OAAO,aAAA,GAAYA,MAAAA,SAAAA,CAAS,EAAE;;GAErC,MAAM,cAAc,iBAAiB,UAAU,EAAE;GAEjD,MAAM,WAAA,GAAUC,MAAAA,YAAAA,CAAY,YAAY;IACtC,QAAQ,IAAI;IACZ,SAAS,EAAE;IACX,IAAI;KACF,SAAS,MAAM,IAAI,MAAM,CAAC;KAC1B,WAAW,MAAM,IAAI,QAAQ,KAAK,CAAC;IACrC,SAAS,OAAO;KACd,SAAS,iBAAiB,QAAQ,MAAM,UAAU,UAAU;IAC9D,UAAU;KACR,QAAQ,KAAK;IACf;GACF,GAAG,CAAC,KAAK,KAAK,CAAC;GAEf,CAAA,GAAA,MAAA,UAAA,OAAgB;IAAE,QAAa;GAAE,GAAG,CAAC,OAAO,CAAC;GAE7C,MAAM,WAAW,YAAY;IAC3B,QAAQ,IAAI;IACZ,SAAS,EAAE;IACX,WAAW,EAAE;IACb,IAAI;KACF,MAAM,SAAS,MAAM,IAAI,OAAO;KAChC,aAAa,OAAO,UAAU,OAAO,IAAI;KACzC,WAAW,OAAO,OAAO,SAAS,GAAG,aAAa,OAAO,KAAK,IAAI,OAAO,YAAY,eAAe,GAAG,EAAE;IAC3G,SAAS,OAAO;KACd,SAAS,iBAAiB,QAAQ,MAAM,UAAU,QAAQ;IAC5D,UAAU;KACR,QAAQ,KAAK;IACf;GACF;GAEA,OAAO,iBAAA,GAAA,kBAAA,KAAA,CAAA,kBAAA,UAAA,EAAA,UAAA,CACL,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD,EAAA,UAAQ,cAAqB,CAAA,GAC7B,iBAAA,GAAA,kBAAA,KAAA,CAAC,WAAD;IAAS,WAAU;IAAmB,cAAW;IAAU,aAAW;IAAtE,UAAA;KACA,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD,EAAA,UAAQ,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD,EAAA,UAAA,CAAK,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;MAAM,WAAU;MAAkB,UAAA;KAAoB,CAAA,GAAC,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD,EAAA,UAAI,OAAQ,CAAA,CAAM,EAAA,CAAA,EAAS,CAAA;KAC/F,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;MAAK,WAAU;MAAf,UAAA;OACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD,EAAA,UAAI,OAAQ,CAAA;OACZ,iBAAA,GAAA,kBAAA,KAAA,CAAC,MAAD,EAAA,UAAA;QACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;SAAK,WAAU;SAAf,UAAA,CAAgC,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD,EAAA,UAAI,OAAQ,CAAA,GAAC,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD,EAAA,UAAK,OAAO,QAAQ,YAAgB,CAAA,CAAM;;QACvF,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;SAAK,WAAU;SAAf,UAAA,CAAgC,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD,EAAA,UAAI,YAAa,CAAA,GAAC,iBAAA,GAAA,kBAAA,KAAA,CAAC,MAAD,EAAA,UAAA,CAC/C,UAAU,KAAA,IAAY,cAAc,GAAG,MAAM,QAAQ,KACrD,OAAO,YAAY,KAAA,KAAa,IAAI,MAAM,QAAQ,KAAK,MAAM,OAAO,EACnE,EAAA,CAAA,CAAM;;QACV,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;SAAK,WAAU;SAAf,UAAA,CAAgC,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD,EAAA,UAAI,YAAa,CAAA,GAAC,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD,EAAA,UAC/C,UAAU,KAAA,IAAY,cAAc,GAAG,aAAa,MAAM,SAAS,EAAE,KAAK,aAAa,MAAM,QAAQ,IACpG,CAAA,CAAM;;QACV,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;SAAK,WAAU;SAAf,UAAA,CAAgC,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD,EAAA,UAAI,gBAAiB,CAAA,GAAC,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD,EAAA,UACnD,UAAU,KAAA,IAAY,cAAc,GAAG,MAAM,QAAQ,KAAK,MAAM,UAAU,KAAK,MAAM,gBACpF,CAAA,CAAM;;QACV,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;SAAK,WAAU;SAAf,UAAA,CAAgC,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD,EAAA,UAAI,OAAQ,CAAA,GAAC,iBAAA,GAAA,kBAAA,KAAA,CAAC,MAAD,EAAA,UAAA,CAAK,OAAO,SAAS,aAAY,GAAK,EAAA,CAAA,CAAM;;QACzF,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;SAAK,WAAU;SAAf,UAAA,CAAgC,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD,EAAA,UAAI,KAAM,CAAA,GAAC,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD,EAAA,UACxC,UAAU,KAAA,IACP,cACA,OAAO,KAAK,MAAM,MAAM,CAAC,CAAC,WAAW,IACnC,OACA,OAAO,QAAQ,MAAM,MAAM,CAAC,CAAC,KAAK,CAAC,MAAM,WAAW,GAAG,KAAK,GAAG,OAAO,CAAC,CAAC,KAAK,GAAG,EACpF,CAAA,CAAM;;QACV,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;SAAK,WAAU;SAAf,UAAA,CAAgC,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD,EAAA,UAAI,OAAQ,CAAA,GAAC,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD,EAAA,UAC1C,UAAU,KAAA,IACP,cACA,MAAM,QAAQ,WAAW,IACvB,OACA,MAAM,QAAQ,KAAI,WAAU,GAAG,OAAO,KAAK,GAAG,OAAO,OAAO,OAAO,UAAU,KAAA,IAAY,KAAK,IAAI,aAAa,OAAO,KAAK,IAAI,EAAE,CAAC,CAAC,KAAK,GAAG,EAC/I,CAAA,CAAM;;OACR,EAAA,CAAA;OACJ,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;QAAK,WAAU;QAAf,UAAA;SACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;UAAQ,WAAU;UAAU,MAAK;UAAS,eAAe,KAAK,SAAS;UAAG,UAAU;UAAM,UAAA;SAAY,CAAA;SAEtG,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD;UAAG,WAAU;UAAmB,MAAM;UAAoB,UAAS;UAAG,UAAA;SAAQ,CAAA;SAC9E,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;UAAQ,MAAK;UAAS,eAAe,KAAK,QAAQ;UAAG,UAAU;UAAM,UAAA;SAAU,CAAA;SAC/E,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;UAAO,SAAQ;UAAoB,UAAA;SAAW,CAAA;SAC9C,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;UAAQ,IAAG;UAAoB,OAAO;UAAO,WAAU,MAAK,SAAS,OAAO,EAAE,OAAO,KAAK,CAAC;UAAG,UAAU;UACrG,UAAA,gBAAgB,KAAI,WAAU,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;WAAqB,OAAO;WAAS,UAAA;UAAe,GAAvC,MAAuC,CAAC;SAC9E,CAAA;QACL;;OACJ,QAAQ,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD;QAAG,MAAK;QAAS,UAAA;OAAY,CAAA;OACrC,WAAW,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD;QAAG,MAAK;QAAU,UAAA;OAAW,CAAA;OACxC,SAAS,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD;QAAG,WAAU;QAAgB,MAAK;QAAS,UAAA;OAAS,CAAA;MAC3D;;KACL,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;MAAK,WAAU;MAAf,UAAA,CACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,MAAD,EAAA,UAAA,CAAI,QAAK,SAAS,cAAc,OAAO,UAAU,EAAO,EAAA,CAAA,GACvD,YAAY,KAAA,IACT,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD,EAAA,UAAI,UAAU,KAAK,UAAU,kBAAqB,CAAA,IAClD,QAAQ,MAAM,WAAW,IACvB,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD,EAAA,UAAG,IAAI,CAAA,IACP,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;OAAK,WAAU;OAAuB,UAAA,QAAQ,MAAM,KAAK,IAAI;MAAO,CAAA,CACvE;;IACI;GACT,CAAA,CAAA,EAAA,CAAA;EACJ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EC1IA,MAAa,eAAe;;;;;;;;;;;;;;;ECtB5B,MAAM,gCAAgC;;;;;EA+CtC,SAAgB,uBAAuB,EAAE,cAAsD;GAC7F,OAAO,iBAAA,GAAA,kBAAA,KAAA,CAAA,kBAAA,UAAA,EAAA,UAAA,CACL,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD,EAAA,UAAQ,aAAoB,CAAA,GAC3B,WAAW,4BAA4B,CAAC,CAAC,CAC1C,EAAA,CAAA;EACJ;;;;;;;;;;EAWA,SAAgB,wBAAwB,KAA8C;GACpF,OAAO,IAAI,MAAM,OAAO,qCAAqC,IAAI,MAAM,SACrE;IACE,MAAM;IACN,GAAG;IACH,UAAU,GAAG,6BAA6B;KAAE,MAAM;KAAQ,OAAO;IAAO,EAAE;GAC5E,GACA,sBACF,CAAC;EACH;;;EC1DA,MAAM,aAAa,WAAoC;GAAE,OAAO;GAAO,MAAM;EAAM;;EAGnF,MAAa,kBAAkB;;;;;;;;;;EAW/B,MAAa,kBAA6D,OAAO,OAAO;GACtF,eAAe,UAAU,SAAS;GAClC,mBAAmB,UAAU,SAAS;GACtC,oBAAoB;IAAE,OAAO;IAAW,MAAM;GAAU;GACxD,0BAA0B;IAAE,OAAO;IAAW,MAAM;GAAU;GAC9D,wBAAwB;IAAE,OAAO;IAAW,MAAM;GAAU;GAC5D,yBAAyB;IAAE,OAAO;IAAW,MAAM;GAAU;GAC7D,eAAe,UAAU,SAAS;GAClC,mBAAmB,UAAU,SAAS;EACxC,CAAC;;;;;;;;;EA0BD,SAAgB,iBAAiB,KAA8B;GAC7D,IAAI,OAAO,CAAC,OAAO,IAAI,UAAU;IAC/B,MAAM,aACE,MAAM,MAAM,eAAe,iBAAiB,eAAe,GACjE,8BACF;GACF,CAAC;EACH;;ECvEA,MAAa,SAAS,CAAC,SAAS,QAAQ;EACxC,SAAgB,MAAM,KAAyC;GAC7D,wBAAwB,GAAG;GAI3B,iBAAiB,GAAmC;EACtD;;EAEA,SAAgB,wBAAwB,KAAyC;GAC/E,wBAAwB,GAAG;GAC3B,mBAAmB,GAAG;GACtB,iBAAiB,GAAG;EACtB"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * `@dsh-ft/dsh-fft-settings/contract` — the aggregate contract entry.
3
+ *
4
+ * Re-exports the generic seam contract plus every built-in feature contract,
5
+ * so `FtshareSettingsService`, the route constants and the API Key / logs
6
+ * vocabulary keep arriving from one import. It has no Node runtime dependency, so it is
7
+ * safe in a browser bundle and in a host plugin alike. Two type sources feed
8
+ * it: the Host contract (`./core/contract.js` plus each feature contract) and,
9
+ * for UI composition, `@dsh-ft/dsh-fft-settings/client`, which merges the card
10
+ * slot into the renderer's SlotMap.
11
+ */
12
+ export * from './core/contract.js';
13
+ export * from './features/api-key/contract.js';
14
+ export * from './features/logs/contract.js';
@@ -0,0 +1,14 @@
1
+ /**
2
+ * `@dsh-ft/dsh-fft-settings/contract` — the aggregate contract entry.
3
+ *
4
+ * Re-exports the generic seam contract plus every built-in feature contract,
5
+ * so `FtshareSettingsService`, the route constants and the API Key / logs
6
+ * vocabulary keep arriving from one import. It has no Node runtime dependency, so it is
7
+ * safe in a browser bundle and in a host plugin alike. Two type sources feed
8
+ * it: the Host contract (`./core/contract.js` plus each feature contract) and,
9
+ * for UI composition, `@dsh-ft/dsh-fft-settings/client`, which merges the card
10
+ * slot into the renderer's SlotMap.
11
+ */
12
+ export * from './core/contract.js';
13
+ export * from './features/api-key/contract.js';
14
+ export * from './features/logs/contract.js';
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Browser half of the settings transport.
3
+ *
4
+ * Calls are same-origin fetches to the plugin route, so the browser session
5
+ * cookie carries authentication and this plugin needs no Connection RPC client.
6
+ */
7
+ import type { ClientTransport } from './rpc-core.js';
8
+ /** Transport this caller posts through; same signature as the global `fetch`. */
9
+ export type SettingsFetch = (input: string, init: RequestInit) => Promise<Response>;
10
+ /**
11
+ * Create the browser-backed caller used by the settings cards.
12
+ * @param doFetch - transport override; defaults to the page's global fetch.
13
+ * @returns caller that owns request encoding and envelope decoding.
14
+ */
15
+ export declare function createSettingsTransport(doFetch?: SettingsFetch): ClientTransport;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Browser half of the settings transport.
3
+ *
4
+ * Calls are same-origin fetches to the plugin route, so the browser session
5
+ * cookie carries authentication and this plugin needs no Connection RPC client.
6
+ */
7
+ import { FTSHARE_SETTINGS_ROUTE } from './contract.js';
8
+ /**
9
+ * Create the browser-backed caller used by the settings cards.
10
+ * @param doFetch - transport override; defaults to the page's global fetch.
11
+ * @returns caller that owns request encoding and envelope decoding.
12
+ */
13
+ export function createSettingsTransport(doFetch) {
14
+ const send = doFetch ?? ((input, init) => globalThis.fetch(input, init));
15
+ return {
16
+ async call(endpoint, payload = {}, signal) {
17
+ const response = await send(FTSHARE_SETTINGS_ROUTE, {
18
+ method: 'POST',
19
+ headers: { 'content-type': 'application/json' },
20
+ body: JSON.stringify({ endpoint, payload }),
21
+ ...signal === undefined ? {} : { signal },
22
+ });
23
+ if (!response.ok) {
24
+ // The carrier answers 401/403 before dispatch; anything else is a route
25
+ // or plugin-level failure. Both are transport failures for the caller.
26
+ throw new Error(`transport failure for ${endpoint}: HTTP ${response.status}`);
27
+ }
28
+ return await response.json();
29
+ },
30
+ };
31
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Generic half of the settings contract: the seam's identity, its transport
3
+ * constants, and the service shape. It carries no feature vocabulary — a
4
+ * feature's own contract lives beside that feature and merges into
5
+ * {@link FtshareSettingsNamespaces}.
6
+ */
7
+ import type { FtshareSettingsModuleRegistry } from './module-registry.js';
8
+ export type { FtshareSettingsModuleRegistry, SettingsModule, SettingsRegistration } from './module-registry.js';
9
+ export type { SettingsRpcMethod, SettingsRpcMethods } from './rpc-core.js';
10
+ /** Protocol version of the settings route and envelope. */
11
+ export declare const FTSHARE_SETTINGS_API_VERSION: 1;
12
+ /**
13
+ * Identity of the settings section that carries this plugin's cards.
14
+ *
15
+ * The id replaces the old `ftshare`. The label is a neutral product name, not
16
+ * the internal brand: the page ships publicly, and the data source it
17
+ * integrates is named inside the cards instead.
18
+ */
19
+ export declare const FTSHARE_SETTINGS_SECTION: {
20
+ readonly id: "dsh-fft";
21
+ readonly order: 120;
22
+ readonly label: "金融终端";
23
+ };
24
+ /**
25
+ * Child slot of {@link FTSHARE_SETTINGS_SECTION} that carries its cards.
26
+ *
27
+ * The section declares the slot when it registers; every card — ours and a
28
+ * third party's — contributes through it with `id`/`order`/`label`, and
29
+ * `order` (ascending, default 100) decides the card's position.
30
+ */
31
+ export declare const FTSHARE_SETTINGS_CARD_SLOT: "ftshare.settings.card";
32
+ /**
33
+ * Browser-facing route for this plugin's settings calls.
34
+ *
35
+ * It sits below `/api` so the shared connection carrier applies the Host/Origin
36
+ * fence and browser authentication before dispatch; this plugin only handles
37
+ * the call itself.
38
+ */
39
+ export declare const FTSHARE_SETTINGS_ROUTE: "/api/ftshare-settings";
40
+ /** Largest request body accepted on {@link FTSHARE_SETTINGS_ROUTE}. */
41
+ export declare const FTSHARE_SETTINGS_MAX_BODY_BYTES: number;
42
+ /**
43
+ * Merge point for namespaces a package reads but does not produce. A producer
44
+ * gets its type from `register` inference; a consumer that reads someone
45
+ * else's namespace declares one line, e.g. `cockpit: CockpitSettings`. Each
46
+ * built-in feature merges its own line from its own feature directory.
47
+ */
48
+ export interface FtshareSettingsNamespaces {
49
+ }
50
+ /**
51
+ * Host service published as `ctx.ftshareSettings`.
52
+ *
53
+ * `apiVersion` and `modules` are permanent; every other member appears only
54
+ * while its namespace is registered, so consumers must read properties at call
55
+ * time instead of caching them during their own apply.
56
+ */
57
+ export type FtshareSettingsService = {
58
+ readonly apiVersion: typeof FTSHARE_SETTINGS_API_VERSION;
59
+ readonly modules: FtshareSettingsModuleRegistry;
60
+ } & {
61
+ readonly [N in keyof FtshareSettingsNamespaces & string]: FtshareSettingsNamespaces[N];
62
+ };
@@ -0,0 +1,28 @@
1
+ /** Protocol version of the settings route and envelope. */
2
+ export const FTSHARE_SETTINGS_API_VERSION = 1;
3
+ /**
4
+ * Identity of the settings section that carries this plugin's cards.
5
+ *
6
+ * The id replaces the old `ftshare`. The label is a neutral product name, not
7
+ * the internal brand: the page ships publicly, and the data source it
8
+ * integrates is named inside the cards instead.
9
+ */
10
+ export const FTSHARE_SETTINGS_SECTION = { id: 'dsh-fft', order: 120, label: '金融终端' };
11
+ /**
12
+ * Child slot of {@link FTSHARE_SETTINGS_SECTION} that carries its cards.
13
+ *
14
+ * The section declares the slot when it registers; every card — ours and a
15
+ * third party's — contributes through it with `id`/`order`/`label`, and
16
+ * `order` (ascending, default 100) decides the card's position.
17
+ */
18
+ export const FTSHARE_SETTINGS_CARD_SLOT = 'ftshare.settings.card';
19
+ /**
20
+ * Browser-facing route for this plugin's settings calls.
21
+ *
22
+ * It sits below `/api` so the shared connection carrier applies the Host/Origin
23
+ * fence and browser authentication before dispatch; this plugin only handles
24
+ * the call itself.
25
+ */
26
+ export const FTSHARE_SETTINGS_ROUTE = '/api/ftshare-settings';
27
+ /** Largest request body accepted on {@link FTSHARE_SETTINGS_ROUTE}. */
28
+ export const FTSHARE_SETTINGS_MAX_BODY_BYTES = 64 * 1024;
@@ -0,0 +1,54 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import { type FtshareSettingsService } from './contract.js';
3
+ import { type SettingsRpcHandler, type SettingsRpcMethods } from './rpc-core.js';
4
+ /**
5
+ * One contribution to the settings service: a namespace, the host services it
6
+ * needs, and a synchronous factory for its service instance and RPC methods.
7
+ * Third parties may write this shape structurally; `defineSettingsModule` only
8
+ * buys type inference.
9
+ */
10
+ export interface SettingsModule<N extends string = string, S = unknown> {
11
+ /** Namespace: the RPC endpoint prefix and the service property name. */
12
+ readonly namespace: N;
13
+ /** Host services this module resolves on its owner context at registration. */
14
+ readonly inject: readonly string[];
15
+ /** Register lifecycle cleanup through ctx.effect/on; never return browser secrets as RPC methods. */
16
+ create(ctx: Context): {
17
+ service: S;
18
+ rpcMethods: SettingsRpcMethods;
19
+ };
20
+ }
21
+ export declare function defineSettingsModule<const N extends string, S>(module: SettingsModule<N, S>): SettingsModule<N, S>;
22
+ /** Handle returned by {@link FtshareSettingsModuleRegistry.register}. */
23
+ export interface SettingsRegistration<N extends string = string, S = unknown> {
24
+ readonly namespace: N;
25
+ readonly service: S;
26
+ /** Idempotent; also runs when the owner fiber releases. */
27
+ dispose(): void;
28
+ }
29
+ /** Live namespace registry behind `ctx.ftshareSettings.modules`. */
30
+ export interface FtshareSettingsModuleRegistry {
31
+ register<const N extends string, S>(module: SettingsModule<N, S>, owner: Context): SettingsRegistration<N, S>;
32
+ /** Namespaces currently published on the service object, in registration order. */
33
+ namespaces(): readonly string[];
34
+ }
35
+ /** The host half assembled by {@link createSettingsHost}. */
36
+ export interface FtshareSettingsHost {
37
+ /** Service object published as `ctx.ftshareSettings`; namespaces appear on it at runtime. */
38
+ readonly service: FtshareSettingsService;
39
+ /** Live dispatch table; mount it once and let namespaces come and go. */
40
+ readonly handler: SettingsRpcHandler;
41
+ }
42
+ /**
43
+ * Assemble the host half of the seam: one service object plus one live route
44
+ * table that namespaces register into.
45
+ *
46
+ * Registration is validate-then-write: a bad namespace, a reserved name, a
47
+ * duplicate, or an unresolvable `module.inject` entry throws before `create`
48
+ * runs and leaves no service property or route behind. A successful
49
+ * registration installs a release effect on the owner fiber, so unloading the
50
+ * owner removes the service property and every route of that namespace
51
+ * together; the returned `dispose()` is idempotent and equivalent.
52
+ * @returns the service object and its route table.
53
+ */
54
+ export declare function createSettingsHost(): FtshareSettingsHost;
@@ -0,0 +1,68 @@
1
+ import { FTSHARE_SETTINGS_API_VERSION } from './contract.js';
2
+ import { assertSettingsName, assertSettingsRpcModule, createSettingsRpcHandler } from './rpc-core.js';
3
+ export function defineSettingsModule(module) { return module; }
4
+ /**
5
+ * Assemble the host half of the seam: one service object plus one live route
6
+ * table that namespaces register into.
7
+ *
8
+ * Registration is validate-then-write: a bad namespace, a reserved name, a
9
+ * duplicate, or an unresolvable `module.inject` entry throws before `create`
10
+ * runs and leaves no service property or route behind. A successful
11
+ * registration installs a release effect on the owner fiber, so unloading the
12
+ * owner removes the service property and every route of that namespace
13
+ * together; the returned `dispose()` is idempotent and equivalent.
14
+ * @returns the service object and its route table.
15
+ */
16
+ export function createSettingsHost() {
17
+ const handler = createSettingsRpcHandler();
18
+ /** Namespaces published by this host, in registration order. */
19
+ const published = new Map();
20
+ const target = { apiVersion: FTSHARE_SETTINGS_API_VERSION };
21
+ function register(module, owner) {
22
+ // Validate everything knowable before the first write. `rpcMethods` exists
23
+ // only after `create`, so its endpoint names are checked immediately after,
24
+ // still before this registration publishes anything.
25
+ assertSettingsName(module.namespace);
26
+ if (published.has(module.namespace))
27
+ throw new Error(`Duplicate settings namespace: ${module.namespace}`);
28
+ for (const name of module.inject) {
29
+ if (owner.get(name) === undefined)
30
+ throw new Error(`Missing settings module dependency for ${module.namespace}: ${name}`);
31
+ }
32
+ let released = false;
33
+ let committed = false;
34
+ let instance;
35
+ const release = () => {
36
+ if (released)
37
+ return;
38
+ released = true;
39
+ if (!committed)
40
+ return;
41
+ if (target[module.namespace] === instance)
42
+ delete target[module.namespace];
43
+ if (published.get(module.namespace) === registration) {
44
+ published.delete(module.namespace);
45
+ handler.unregister(module.namespace);
46
+ }
47
+ };
48
+ const registration = {
49
+ namespace: module.namespace,
50
+ get service() { return instance; },
51
+ dispose() { release(); },
52
+ };
53
+ // Install the release effect first: an inactive owner then fails before
54
+ // this module allocates anything of its own.
55
+ owner.effect(() => release, `dsh-fft-settings: namespace ${module.namespace}`);
56
+ const created = module.create(owner);
57
+ instance = created.service;
58
+ assertSettingsRpcModule({ namespace: module.namespace, rpcMethods: created.rpcMethods });
59
+ handler.register({ namespace: module.namespace, rpcMethods: created.rpcMethods });
60
+ Object.defineProperty(target, module.namespace, { value: created.service, enumerable: true, writable: true, configurable: true });
61
+ published.set(module.namespace, registration);
62
+ committed = true;
63
+ return registration;
64
+ }
65
+ const registry = { register, namespaces: () => [...published.keys()] };
66
+ target.modules = registry;
67
+ return { service: target, handler };
68
+ }
@@ -0,0 +1,69 @@
1
+ import type { FTSHARE_SETTINGS_ROUTE } from './contract.js';
2
+ export interface RpcResult<T> {
3
+ ok: boolean;
4
+ value?: T;
5
+ error?: {
6
+ code: string;
7
+ message: string;
8
+ details: Record<string, unknown>;
9
+ };
10
+ }
11
+ /** Browser caller for this plugin's route. */
12
+ export interface ClientTransport {
13
+ call(endpoint: string, payload?: unknown, signal?: AbortSignal): Promise<RpcResult<unknown>>;
14
+ }
15
+ export type RpcHandler = (endpoint: string, payload: unknown, signal: AbortSignal) => Promise<RpcResult<unknown>>;
16
+ /** Structural shape of one exact Fetch route; `connection.fetch.register` accepts it. */
17
+ export interface SettingsRoute {
18
+ readonly path: typeof FTSHARE_SETTINGS_ROUTE;
19
+ readonly methods: readonly string[];
20
+ readonly requestBody: 'buffered' | 'streaming';
21
+ readonly fetch: (request: Request) => Promise<Response>;
22
+ }
23
+ /** Host registry for exact Fetch routes on the shared `/api` carrier. */
24
+ export interface HostFetch {
25
+ register(route: SettingsRoute): () => Promise<void>;
26
+ }
27
+ export interface SettingsRpcMethod {
28
+ validate(payload: unknown): boolean;
29
+ invoke(payload: unknown, signal: AbortSignal): unknown | Promise<unknown>;
30
+ }
31
+ export type SettingsRpcMethods = Readonly<Record<string, SettingsRpcMethod>>;
32
+ export interface SettingsRpcModule {
33
+ namespace: string;
34
+ rpcMethods: SettingsRpcMethods;
35
+ }
36
+ /**
37
+ * Live dispatch table behind {@link createSettingsRpcHandler}: routes appear
38
+ * when a namespace is registered and disappear when it is released, so the one
39
+ * route mounted at boot keeps serving namespaces that arrive later.
40
+ */
41
+ export interface SettingsRpcTable {
42
+ /** Validate one whole module, then publish its routes; a failure writes nothing. */
43
+ register(module: SettingsRpcModule): void;
44
+ /** Remove one namespace and every route under it; unknown namespaces are a no-op. */
45
+ unregister(namespace: string): void;
46
+ /** Namespaces currently owning at least one route, in registration order. */
47
+ namespaces(): readonly string[];
48
+ }
49
+ /** The dispatcher callable plus the increment/decrement surface of its route table. */
50
+ export type SettingsRpcHandler = RpcHandler & SettingsRpcTable;
51
+ /** Names the service object and the route table reserve for themselves. */
52
+ export declare const SETTINGS_RESERVED_NAMES: readonly ["apiVersion", "modules", "constructor", "prototype", "__proto__"];
53
+ export declare function assertSettingsName(name: string): void;
54
+ /** Validate every endpoint part of one module without writing to any table. */
55
+ export declare function assertSettingsRpcModule(module: SettingsRpcModule): void;
56
+ /**
57
+ * Build the live dispatcher for this plugin's route.
58
+ *
59
+ * Only explicitly registered methods are callable; never reflects over a
60
+ * service. The returned function carries its own route table, so the caller
61
+ * mounts the route once and mutates through `register` / `unregister`
62
+ * afterwards. Each `register` validates the whole module first — namespace
63
+ * format, reserved names, duplicates, and every method name — and writes
64
+ * nothing when any check fails.
65
+ * @param modules - optional initial namespaces, registered in order.
66
+ * @returns dispatcher with the live route table attached.
67
+ */
68
+ export declare function createSettingsRpcHandler(modules?: readonly SettingsRpcModule[]): SettingsRpcHandler;
69
+ export declare function callSettingsRpc<T>(transport: ClientTransport, namespace: string, method: string, payload?: unknown): Promise<T>;
@@ -0,0 +1,88 @@
1
+ const fail = (code, message) => ({ ok: false, error: { code, message, details: {} } });
2
+ /** Names the service object and the route table reserve for themselves. */
3
+ export const SETTINGS_RESERVED_NAMES = ['apiVersion', 'modules', 'constructor', 'prototype', '__proto__'];
4
+ export function assertSettingsName(name) {
5
+ if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(name) || SETTINGS_RESERVED_NAMES.includes(name))
6
+ throw new Error(`Invalid settings name: ${name}`);
7
+ }
8
+ /** Validate every endpoint part of one module without writing to any table. */
9
+ export function assertSettingsRpcModule(module) {
10
+ assertSettingsName(module.namespace);
11
+ for (const name of Object.keys(module.rpcMethods))
12
+ assertSettingsName(name);
13
+ }
14
+ /**
15
+ * Build the live dispatcher for this plugin's route.
16
+ *
17
+ * Only explicitly registered methods are callable; never reflects over a
18
+ * service. The returned function carries its own route table, so the caller
19
+ * mounts the route once and mutates through `register` / `unregister`
20
+ * afterwards. Each `register` validates the whole module first — namespace
21
+ * format, reserved names, duplicates, and every method name — and writes
22
+ * nothing when any check fails.
23
+ * @param modules - optional initial namespaces, registered in order.
24
+ * @returns dispatcher with the live route table attached.
25
+ */
26
+ export function createSettingsRpcHandler(modules = []) {
27
+ const routes = new Map();
28
+ const namespaces = new Set();
29
+ const register = (module) => {
30
+ assertSettingsRpcModule(module);
31
+ if (namespaces.has(module.namespace))
32
+ throw new Error(`Duplicate settings namespace: ${module.namespace}`);
33
+ // Committed: every check above ran before the first write.
34
+ namespaces.add(module.namespace);
35
+ for (const [name, method] of Object.entries(module.rpcMethods))
36
+ routes.set(`${module.namespace}/${name}`, method);
37
+ };
38
+ const unregister = (namespace) => {
39
+ if (!namespaces.delete(namespace))
40
+ return;
41
+ const prefix = `${namespace}/`;
42
+ for (const endpoint of [...routes.keys()])
43
+ if (endpoint.startsWith(prefix))
44
+ routes.delete(endpoint);
45
+ };
46
+ const handler = (async (endpoint, payload, signal) => {
47
+ const method = routes.get(endpoint);
48
+ if (!method)
49
+ return fail('not_found', '接口不存在');
50
+ if (signal.aborted)
51
+ return fail('cancelled', '请求已取消');
52
+ try {
53
+ if (!method.validate(payload))
54
+ return fail('bad_request', '请求参数不正确');
55
+ return { ok: true, value: (await method.invoke(payload, signal)) ?? null };
56
+ }
57
+ catch {
58
+ return fail('operation_failed', '操作失败,请刷新状态后重试');
59
+ }
60
+ });
61
+ handler.register = register;
62
+ handler.unregister = unregister;
63
+ handler.namespaces = () => [...namespaces];
64
+ for (const module of modules)
65
+ register(module);
66
+ return handler;
67
+ }
68
+ /**
69
+ * 把一次失败的信封翻译成调用方能展示的原因。
70
+ *
71
+ * `not_found` 是「浏览器半边比宿主半边新」的可观测症状:升级插件后客户端 bundle 由磁盘重新
72
+ * 加载,而宿主进程仍持有旧包,它没有新命名空间。此时重试永远不会成功,必须点名重启宿主。
73
+ * @param endpoint - 这次调用的 `<namespace>/<method>`。
74
+ * @param error - 宿主回传的失败原因,畸形信封下可能缺省。
75
+ * @returns 面向用户的失败描述。
76
+ */
77
+ function describeSettingsFailure(endpoint, error) {
78
+ if (error?.code === 'not_found')
79
+ return `FTShare 设置接口 ${endpoint} 不存在:宿主插件还是升级前的版本,重启 DSH 后再试`;
80
+ return `FTShare 设置操作失败(${error?.code ?? 'unknown'}):${error?.message ?? '宿主未说明原因'}`;
81
+ }
82
+ export async function callSettingsRpc(transport, namespace, method, payload = {}) {
83
+ const endpoint = `${namespace}/${method}`;
84
+ const result = await transport.call(endpoint, payload);
85
+ if (!result.ok)
86
+ throw new Error(describeSettingsFailure(endpoint, result.error));
87
+ return result.value;
88
+ }