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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":["useMemo","useSyncExternalStore","useState","useState","useCallback"],"sources":["../src/features/api-key/contract.ts","../src/core/contract.ts","../src/features/api-key/model.ts","../src/core/rpc-core.ts","../src/features/api-key/client-api.ts","../src/core/client-transport.ts","../src/features/api-key/styles.ts","../src/features/api-key/card.tsx","../src/features/logs/contract.ts","../src/features/logs/client-api.ts","../src/features/logs/styles.ts","../src/features/logs/card.tsx","../src/core/styles.ts","../src/core/shell.tsx","../src/brand.ts","../src/client.tsx"],"sourcesContent":["/**\n * API Key namespace contract: the credential reference and lifecycle events,\n * the status/validation vocabulary, and the two faces the namespace exposes\n * (the browser-safe subset and the host-only superset).\n *\n * It merges its own line into the generic {@link FtshareSettingsNamespaces}\n * merge point, so `ctx.ftshareSettings.apiKey` keeps its existing type for\n * every consumer.\n */\nexport const FTSHARE_API_KEY_REF = 'FTSHARE_API_KEY' as const\nexport const FTSHARE_CREDENTIAL_EVENTS = ['credentials/reference-updated'] as const\nexport const API_KEY_METHODS = ['describe', 'set', 'clear', 'configureAndTest', 'testConnection', 'getValidationStatus'] as const\nexport type ApiKeyMethod = typeof API_KEY_METHODS[number]\nexport type SubscribeChanges = (listener: () => void) => () => void\nexport interface CredentialStatus { configured: boolean; writable: boolean; source?: string }\nexport type ProbeState = 'available' | 'auth_required' | 'auth_failed' | 'permission_denied' | 'quota_exceeded' | 'rate_limited' | 'network_error' | 'timeout' | 'upstream_error' | 'invalid_response' | 'not_run'\nexport interface ProbeResult { status: ProbeState; message: string }\nexport interface ValidationResult {\n status: 'not_tested' | 'not_run' | 'passed' | 'partial' | 'failed' | 'superseded'\n remote: ProbeResult\n apiKey: ProbeResult\n testedAt?: string\n}\nexport interface ApiKeyStatus extends CredentialStatus {\n /** Display only. Fixed mask plus last four characters; never submit as a Key. */\n maskedValue?: string\n revision: number\n validation: ValidationResult\n}\nexport interface ConfigureResult {\n save: { status: 'saved' | 'failed'; code?: 'invalid_input' | 'read_only' | 'save_failed'; message: string }\n validation: ValidationResult\n}\n/** Local syntax validation, never an assertion of upstream validity. */\nexport function normalizeApiKey(input: string): string {\n const value = input.trim()\n if (!value) throw new Error('请输入 API Key')\n if (value.length > 512 || /[\\x00-\\x1f\\x7f]/.test(value)) throw new Error('API Key 长度不能超过 512 个字符,且不能包含控制字符')\n return value\n}\nexport function untested(status: 'not_tested' | 'not_run' | 'superseded' = 'not_tested', message = '尚未测试'): ValidationResult {\n return { status, remote: { status: 'not_run', message }, apiKey: { status: 'not_run', message } }\n}\n/** Everything the browser adapter may call; `resolve`/`createEnvironment` are not here. */\nexport interface ApiKeyPublicApi {\n describe(): Promise<ApiKeyStatus>\n set(value: string): Promise<void>\n clear(): Promise<void>\n configureAndTest(value: string): Promise<ConfigureResult>\n testConnection(): Promise<ValidationResult>\n getValidationStatus(): Promise<ValidationResult>\n}\n/** The host namespace object: the public API plus the secret-bearing host methods. */\nexport interface ApiKeySettings extends ApiKeyPublicApi {\n readonly credentialRef: typeof FTSHARE_API_KEY_REF\n /** Host only. Re-resolve per operation; never send this value to browsers. */\n resolve(): Promise<string | undefined>\n createEnvironment(base?: Readonly<Record<string, string | undefined>>): Promise<Record<string, string | undefined>>\n subscribe: SubscribeChanges\n}\ndeclare module '../../core/contract.js' {\n interface FtshareSettingsNamespaces { apiKey: ApiKeySettings }\n}\n","/**\n * Generic half of the settings contract: the seam's identity, its transport\n * constants, and the service shape. It carries no feature vocabulary — a\n * feature's own contract lives beside that feature and merges into\n * {@link FtshareSettingsNamespaces}.\n */\nimport type { FtshareSettingsModuleRegistry } from './module-registry.js'\nexport type { FtshareSettingsModuleRegistry, SettingsModule, SettingsRegistration } from './module-registry.js'\nexport type { SettingsRpcMethod, SettingsRpcMethods } from './rpc-core.js'\n/** Protocol version of the settings route and envelope. */\nexport const FTSHARE_SETTINGS_API_VERSION = 1 as const\n/**\n * Identity of the settings section that carries this plugin's cards.\n *\n * The id replaces the old `ftshare`. The label is a neutral product name, not\n * the internal brand: the page ships publicly, and the data source it\n * integrates is named inside the cards instead.\n */\nexport const FTSHARE_SETTINGS_SECTION = { id: 'dsh-fft', order: 120, label: '金融终端' } as const\n/**\n * Child slot of {@link FTSHARE_SETTINGS_SECTION} that carries its cards.\n *\n * The section declares the slot when it registers; every card — ours and a\n * third party's — contributes through it with `id`/`order`/`label`, and\n * `order` (ascending, default 100) decides the card's position.\n */\nexport const FTSHARE_SETTINGS_CARD_SLOT = 'ftshare.settings.card' as const\n/**\n * Browser-facing route for this plugin's settings calls.\n *\n * It sits below `/api` so the shared connection carrier applies the Host/Origin\n * fence and browser authentication before dispatch; this plugin only handles\n * the call itself.\n */\nexport const FTSHARE_SETTINGS_ROUTE = '/api/ftshare-settings' as const\n/** Largest request body accepted on {@link FTSHARE_SETTINGS_ROUTE}. */\nexport const FTSHARE_SETTINGS_MAX_BODY_BYTES = 64 * 1024\n/**\n * Merge point for namespaces a package reads but does not produce. A producer\n * gets its type from `register` inference; a consumer that reads someone\n * else's namespace declares one line, e.g. `cockpit: CockpitSettings`. Each\n * built-in feature merges its own line from its own feature directory.\n */\nexport interface FtshareSettingsNamespaces {}\n/**\n * Host service published as `ctx.ftshareSettings`.\n *\n * `apiVersion` and `modules` are permanent; every other member appears only\n * while its namespace is registered, so consumers must read properties at call\n * time instead of caching them during their own apply.\n */\nexport type FtshareSettingsService =\n & {\n readonly apiVersion: typeof FTSHARE_SETTINGS_API_VERSION\n readonly modules: FtshareSettingsModuleRegistry\n }\n & { readonly [N in keyof FtshareSettingsNamespaces & string]: FtshareSettingsNamespaces[N] }\n","import { normalizeApiKey, type ApiKeyPublicApi, type ApiKeyStatus, type ConfigureResult, type SubscribeChanges } from './contract.js'\nexport interface SettingsState { status?: ApiKeyStatus; busy: boolean; message: string; statusError?: string; save?: ConfigureResult['save'] }\nexport class FtshareSettingsModel {\n private state: SettingsState = { busy: false, message: '' }\n private listeners = new Set<() => void>()\n private request = 0\n private lifetime = 0\n constructor(private api: ApiKeyPublicApi) {}\n getSnapshot = (): SettingsState => this.state\n subscribe = (listener: () => void): (() => void) => { this.listeners.add(listener); return () => { this.listeners.delete(listener) } }\n private update(patch: Partial<SettingsState>): void { this.state = { ...this.state, ...patch }; for (const listener of this.listeners) listener() }\n connect(subscribe?: SubscribeChanges): () => void {\n const dispose = subscribe?.(() => { void this.refresh() })\n void this.refresh()\n return () => { this.request++; this.lifetime++; dispose?.() }\n }\n async refresh(): Promise<void> {\n const request = ++this.request\n try {\n const status = await this.api.describe()\n if (!status || typeof status.configured !== 'boolean') throw new Error('missing_status')\n if (request === this.request) this.update({ status, statusError: undefined })\n } catch { if (request === this.request) this.update({ status: undefined, statusError: '无法读取设置状态,请刷新重试' }) }\n }\n async configureAndTest(input: string): Promise<boolean> {\n if (this.state.busy || !this.state.status?.writable) return false\n let value: string\n try { value = normalizeApiKey(input) } catch (error) { this.update({ message: (error as Error).message }); return false }\n const lifetime = this.lifetime\n this.update({ busy: true, save: undefined, message: '正在保存并检查远端服务和 API Key…' })\n let saved = false\n try {\n const result = await this.api.configureAndTest(value)\n saved = result.save.status === 'saved'\n if (lifetime === this.lifetime) this.update({ save: result.save, message: saved ? validationMessage(result.validation.status) : result.save.message })\n } catch { if (lifetime === this.lifetime) this.update({ message: '请求未完成,保存状态待确认;请刷新后测试当前连接' }) }\n if (lifetime === this.lifetime) { await this.refresh(); this.update({ busy: false }) }\n return saved\n }\n async testConnection(): Promise<void> {\n if (this.state.busy || !this.state.status) return\n const lifetime = this.lifetime\n this.update({ busy: true, message: '正在检查远端服务和当前 API Key…' })\n try {\n const result = await this.api.testConnection()\n if (lifetime === this.lifetime) this.update({ message: validationMessage(result.status) })\n } catch { if (lifetime === this.lifetime) this.update({ message: '测试请求失败,请重试' }) }\n if (lifetime === this.lifetime) { await this.refresh(); this.update({ busy: false }) }\n }\n async clear(): Promise<void> {\n if (this.state.busy || !this.state.status?.writable) return\n const lifetime = this.lifetime\n this.update({ busy: true, save: undefined, message: '' })\n try {\n await this.api.clear()\n if (lifetime === this.lifetime) this.update({ message: '已清除 DSH 保存的密钥;若有 .env 配置,下方显示其有效状态' })\n } catch { if (lifetime === this.lifetime) this.update({ message: '清除失败,请刷新状态后重试' }) }\n if (lifetime === this.lifetime) { await this.refresh(); this.update({ busy: false }) }\n }\n}\nfunction validationMessage(status: string): string {\n return ({ passed: '远端服务正常,API Key 验证通过', partial: '远端服务正常,请查看 API Key 检查结果', failed: '测试未通过,请查看分项结果', not_run: '未执行测试,请先确认凭据配置', superseded: '凭据已变化,请重新测试', not_tested: '尚未测试' } as Record<string, string>)[status] ?? '请查看分项结果'\n}\n","import type { FTSHARE_SETTINGS_ROUTE } from './contract.js'\nexport interface RpcResult<T> { ok: boolean; value?: T; error?: { code: string; message: string; details: Record<string, unknown> } }\n/** Browser caller for this plugin's route. */\nexport interface ClientTransport { call(endpoint: string, payload?: unknown, signal?: AbortSignal): Promise<RpcResult<unknown>> }\nexport type RpcHandler = (endpoint: string, payload: unknown, signal: AbortSignal) => Promise<RpcResult<unknown>>\n/** Structural shape of one exact Fetch route; `connection.fetch.register` accepts it. */\nexport interface SettingsRoute {\n readonly path: typeof FTSHARE_SETTINGS_ROUTE\n readonly methods: readonly string[]\n readonly requestBody: 'buffered' | 'streaming'\n readonly fetch: (request: Request) => Promise<Response>\n}\n/** Host registry for exact Fetch routes on the shared `/api` carrier. */\nexport interface HostFetch { register(route: SettingsRoute): () => Promise<void> }\nexport interface SettingsRpcMethod {\n validate(payload: unknown): boolean\n invoke(payload: unknown, signal: AbortSignal): unknown | Promise<unknown>\n}\nexport type SettingsRpcMethods = Readonly<Record<string, SettingsRpcMethod>>\nexport interface SettingsRpcModule { namespace: string; rpcMethods: SettingsRpcMethods }\n/**\n * Live dispatch table behind {@link createSettingsRpcHandler}: routes appear\n * when a namespace is registered and disappear when it is released, so the one\n * route mounted at boot keeps serving namespaces that arrive later.\n */\nexport interface SettingsRpcTable {\n /** Validate one whole module, then publish its routes; a failure writes nothing. */\n register(module: SettingsRpcModule): void\n /** Remove one namespace and every route under it; unknown namespaces are a no-op. */\n unregister(namespace: string): void\n /** Namespaces currently owning at least one route, in registration order. */\n namespaces(): readonly string[]\n}\n/** The dispatcher callable plus the increment/decrement surface of its route table. */\nexport type SettingsRpcHandler = RpcHandler & SettingsRpcTable\nconst fail = (code: string, message: string): RpcResult<unknown> => ({ ok: false, error: { code, message, details: {} } })\n/** Names the service object and the route table reserve for themselves. */\nexport const SETTINGS_RESERVED_NAMES = ['apiVersion', 'modules', 'constructor', 'prototype', '__proto__'] as const\nexport function assertSettingsName(name: string): void {\n if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(name) || (SETTINGS_RESERVED_NAMES as readonly string[]).includes(name)) throw new Error(`Invalid settings name: ${name}`)\n}\n/** Validate every endpoint part of one module without writing to any table. */\nexport function assertSettingsRpcModule(module: SettingsRpcModule): void {\n assertSettingsName(module.namespace)\n for (const name of Object.keys(module.rpcMethods)) assertSettingsName(name)\n}\n/**\n * Build the live dispatcher for this plugin's route.\n *\n * Only explicitly registered methods are callable; never reflects over a\n * service. The returned function carries its own route table, so the caller\n * mounts the route once and mutates through `register` / `unregister`\n * afterwards. Each `register` validates the whole module first — namespace\n * format, reserved names, duplicates, and every method name — and writes\n * nothing when any check fails.\n * @param modules - optional initial namespaces, registered in order.\n * @returns dispatcher with the live route table attached.\n */\nexport function createSettingsRpcHandler(modules: readonly SettingsRpcModule[] = []): SettingsRpcHandler {\n const routes = new Map<string, SettingsRpcMethod>()\n const namespaces = new Set<string>()\n const register = (module: SettingsRpcModule): void => {\n assertSettingsRpcModule(module)\n if (namespaces.has(module.namespace)) throw new Error(`Duplicate settings namespace: ${module.namespace}`)\n // Committed: every check above ran before the first write.\n namespaces.add(module.namespace)\n for (const [name, method] of Object.entries(module.rpcMethods)) routes.set(`${module.namespace}/${name}`, method)\n }\n const unregister = (namespace: string): void => {\n if (!namespaces.delete(namespace)) return\n const prefix = `${namespace}/`\n for (const endpoint of [...routes.keys()]) if (endpoint.startsWith(prefix)) routes.delete(endpoint)\n }\n const handler = (async (endpoint: string, payload: unknown, signal: AbortSignal): Promise<RpcResult<unknown>> => {\n const method = routes.get(endpoint)\n if (!method) return fail('not_found', '接口不存在')\n if (signal.aborted) return fail('cancelled', '请求已取消')\n try {\n if (!method.validate(payload)) return fail('bad_request', '请求参数不正确')\n return { ok: true, value: (await method.invoke(payload, signal)) ?? null }\n } catch { return fail('operation_failed', '操作失败,请刷新状态后重试') }\n }) as SettingsRpcHandler\n handler.register = register\n handler.unregister = unregister\n handler.namespaces = () => [...namespaces]\n for (const module of modules) register(module)\n return handler\n}\n/**\n * 把一次失败的信封翻译成调用方能展示的原因。\n *\n * `not_found` 是「浏览器半边比宿主半边新」的可观测症状:升级插件后客户端 bundle 由磁盘重新\n * 加载,而宿主进程仍持有旧包,它没有新命名空间。此时重试永远不会成功,必须点名重启宿主。\n * @param endpoint - 这次调用的 `<namespace>/<method>`。\n * @param error - 宿主回传的失败原因,畸形信封下可能缺省。\n * @returns 面向用户的失败描述。\n */\nfunction describeSettingsFailure(endpoint: string, error: RpcResult<unknown>['error']): string {\n if (error?.code === 'not_found') return `FTShare 设置接口 ${endpoint} 不存在:宿主插件还是升级前的版本,重启 DSH 后再试`\n return `FTShare 设置操作失败(${error?.code ?? 'unknown'}):${error?.message ?? '宿主未说明原因'}`\n}\nexport async function callSettingsRpc<T>(transport: ClientTransport, namespace: string, method: string, payload: unknown = {}): Promise<T> {\n const endpoint = `${namespace}/${method}`\n const result = await transport.call(endpoint, payload)\n if (!result.ok) throw new Error(describeSettingsFailure(endpoint, result.error))\n return result.value as T\n}\n","import type { ApiKeyPublicApi, ApiKeyMethod } from './contract.js'\nimport { callSettingsRpc, type ClientTransport } from '../../core/rpc-core.js'\n/** Browser adapter deliberately has no resolve/createEnvironment method. */\nexport function createApiKeyClient(transport: ClientTransport): ApiKeyPublicApi {\n async function call<T>(method: ApiKeyMethod, payload: unknown = {}): Promise<T> {\n return callSettingsRpc<T>(transport, 'apiKey', method, payload)\n }\n return {\n describe: () => call('describe'), set: value => call('set', { value }), clear: () => call('clear'),\n configureAndTest: value => call('configureAndTest', { value }), testConnection: () => call('testConnection'), getValidationStatus: () => call('getValidationStatus'),\n }\n}\n","/**\n * Browser half of the settings transport.\n *\n * Calls are same-origin fetches to the plugin route, so the browser session\n * cookie carries authentication and this plugin needs no Connection RPC client.\n */\n\nimport { FTSHARE_SETTINGS_ROUTE } from './contract.js'\nimport type { ClientTransport, RpcResult } from './rpc-core.js'\n\n/** Transport this caller posts through; same signature as the global `fetch`. */\nexport type SettingsFetch = (input: string, init: RequestInit) => Promise<Response>\n\n/**\n * Create the browser-backed caller used by the settings cards.\n * @param doFetch - transport override; defaults to the page's global fetch.\n * @returns caller that owns request encoding and envelope decoding.\n */\nexport function createSettingsTransport(doFetch?: SettingsFetch): ClientTransport {\n const send: SettingsFetch = doFetch ?? ((input, init) => globalThis.fetch(input, init))\n return {\n async call(endpoint, payload = {}, signal) {\n const response = await send(FTSHARE_SETTINGS_ROUTE, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ endpoint, payload }),\n ...signal === undefined ? {} : { signal },\n })\n if (!response.ok) {\n // The carrier answers 401/403 before dispatch; anything else is a route\n // or plugin-level failure. Both are transport failures for the caller.\n throw new Error(`transport failure for ${endpoint}: HTTP ${response.status}`)\n }\n return await response.json() as RpcResult<unknown>\n },\n }\n}\n","/**\n * The API Key card's own styles, rendered by the card component's `<style>`.\n *\n * A card owns everything past the section shell: the core declares the\n * `--ftshare-*` tokens and the section container, never card internals.\n * Every selector stays scoped under `.ftshare-settings`, so a card never\n * styles another card's markup.\n */\nexport const API_KEY_CARD_CSS = `\n.ftshare-settings .ftshare-eyebrow { color:var(--ftshare-accent-on-surface); font-size:11px; letter-spacing:.12em; }\n.ftshare-settings .ftshare-card { padding:22px; background:var(--ftshare-panel); border:0.5px solid var(--ftshare-border); border-radius:var(--dsw-radius-xl); margin-top:16px; }\n.ftshare-settings label { display:block; font-weight:500; }\n.ftshare-settings input { width:100%; height:32px; border:0.5px solid var(--ftshare-control); border-radius:var(--dsw-radius-md); padding:0 12px; background:var(--ftshare-input); color:inherit; font:inherit; }\n.ftshare-settings input::placeholder { color:var(--ftshare-muted); opacity:1; }\n.ftshare-settings .ftshare-actions { display:flex; flex-wrap:wrap; gap:10px; align-items:center; margin-top:16px; }\n.ftshare-settings button { min-height:36px; border:0.5px solid var(--ftshare-control); border-radius:var(--dsw-radius-md); padding:0 14px; font:inherit; background:transparent; color:inherit; cursor:pointer; }\n.ftshare-settings button.primary { background:var(--ftshare-accent); color:var(--ftshare-accent-ink); border-color:var(--ftshare-accent); font-weight:500; }\n.ftshare-settings button:disabled { opacity:.5; cursor:not-allowed; }\n.ftshare-settings dl { margin:0; }\n.ftshare-settings .ftshare-result { display:grid; grid-template-columns:130px 1fr; gap:12px; padding:10px 0; border-top:0.5px solid var(--ftshare-border); }\n.ftshare-settings dd { margin:0; overflow-wrap:anywhere; }\n.ftshare-settings .ftshare-error { color:var(--ftshare-error); }\n@media(max-width:540px) { .ftshare-settings .ftshare-card { padding:16px; } .ftshare-settings .ftshare-result { grid-template-columns:1fr; gap:4px; } }\n`\n","import { useEffect, useMemo, useState, useSyncExternalStore, type ReactNode } from 'react'\nimport { FTSHARE_API_KEY_REF, FTSHARE_CREDENTIAL_EVENTS, type SubscribeChanges, type ApiKeyPublicApi } from './contract.js'\nimport { FTSHARE_SETTINGS_CARD_SLOT } from '../../core/contract.js'\nimport { FtshareSettingsModel } from './model.js'\nimport { createApiKeyClient } from './client-api.js'\nimport { createSettingsTransport } from '../../core/client-transport.js'\nimport type { FtshareSettingsShellContext } from '../../core/shell.js'\nimport { API_KEY_CARD_CSS } from './styles.js'\n\n/**\n * Browser context this card requires: the shell's slots service plus the\n * invalidations that make a cached credential status stale.\n */\nexport interface ApiKeyCardContext extends FtshareSettingsShellContext {\n remote: { $on(event: string, listener: (ref: string) => void): () => void }\n on(event: 'connection/reset', listener: () => void): () => void\n}\n\n/**\n * Contribute the built-in API Key card through the same slot a third-party\n * card uses. Nothing about it is privileged: same declaration, same options.\n * @param ctx - client context carrying the renderer's slots service.\n * @returns idempotent disposer for the card registration.\n */\nexport function registerApiKeyCard(ctx: ApiKeyCardContext): () => void {\n return ctx.slots.inject(FTSHARE_SETTINGS_CARD_SLOT, () => ctx.slots.register(\n { name: FTSHARE_SETTINGS_CARD_SLOT, id: 'apiKey', order: 10 },\n createApiKeyCard(ctx),\n ))\n}\n\n/** Bind the card's adapters; subscriptions are acquired in the component's effect. */\nfunction createApiKeyCard(ctx: ApiKeyCardContext): () => ReactNode {\n const api = createApiKeyClient(createSettingsTransport())\n const subscribeChanges: SubscribeChanges = listener => {\n const disposers = FTSHARE_CREDENTIAL_EVENTS.map(event => ctx.remote.$on(event, ref => { if (ref === FTSHARE_API_KEY_REF) listener() }))\n disposers.push(ctx.on('connection/reset', listener))\n return () => { for (const dispose of disposers) dispose() }\n }\n return () => <ApiKeyCard api={api} subscribeChanges={subscribeChanges} />\n}\n\nexport function ApiKeyCard({ api, subscribeChanges }: { readonly api: ApiKeyPublicApi; readonly subscribeChanges?: SubscribeChanges }): ReactNode {\n const model = useMemo(() => new FtshareSettingsModel(api), [api])\n const { status, busy, message, statusError, save } = useSyncExternalStore(model.subscribe, model.getSnapshot, model.getSnapshot)\n const [draft, setDraft] = useState('')\n useEffect(() => model.connect(subscribeChanges), [model, subscribeChanges])\n const disabled = busy || !status?.writable\n const configure = async () => { if (await model.configureAndTest(draft)) setDraft('') }\n return <>\n <style>{API_KEY_CARD_CSS}</style>\n <section className=\"ftshare-settings\" aria-label=\"FTShare 设置\" aria-busy={busy}>\n <header><div><span className=\"ftshare-eyebrow\">FTSHARE / API KEY</span><h2>访问凭据</h2></div></header>\n <p>目前集成 FTShare 数据源。</p>\n {status && !status.writable && <p>当前来源为只读,可测试连接;修改密钥请到对应来源操作。</p>}\n <div className=\"ftshare-card\"><label htmlFor=\"ftshare-api-key\">FTShare API Key</label>\n <input id=\"ftshare-api-key\" type=\"password\" value={draft} placeholder={status?.configured ? status.maskedValue ?? '••••••••' : '请输入 API Key'} onChange={e => setDraft(e.target.value)} disabled={disabled} autoComplete=\"new-password\" maxLength={512} />\n <div className=\"ftshare-actions\">\n <button className=\"primary\" type=\"button\" onClick={() => void configure()} disabled={disabled || !draft.trim()}>配置并测试</button>\n <button type=\"button\" onClick={() => void model.testConnection()} disabled={busy || !status}>测试当前连接</button>\n <button type=\"button\" onClick={() => void model.clear()} disabled={disabled || !status?.configured}>清除已保存密钥</button>\n <button type=\"button\" onClick={() => void model.refresh()} disabled={busy} title=\"重新读取已保存的配置和上次检测结果,不会发起联网检测\">重新读取状态</button>\n <a href=\"https://ftai.chat/me/profile?tab=api-key\" target=\"_blank\" rel=\"noreferrer\">获取 API Key ↗</a>\n </div>\n {draft && <small>“测试当前连接”使用已生效的密钥,不测试输入框内尚未保存的内容。</small>}\n {busy && <p role=\"status\">正在处理,请稍候…</p>}\n </div>\n <div className=\"ftshare-card\" aria-live=\"polite\"><h3>验证结果</h3><dl>\n {save && <div className=\"ftshare-result\"><dt>本次保存</dt><dd>{save.message}</dd></div>}\n <div className=\"ftshare-result\"><dt>远端服务</dt><dd>{status?.validation.remote.message ?? '状态待确认'}</dd></div>\n <div className=\"ftshare-result\"><dt>API Key</dt><dd>{status?.validation.apiKey.message ?? '状态待确认'}</dd></div>\n {status?.validation.testedAt && <div className=\"ftshare-result\"><dt>测试时间</dt><dd>{new Date(status.validation.testedAt).toLocaleString()}</dd></div>}\n </dl><small>节假日或收盘后的空数据不影响验证。</small></div>\n {message && <p role=\"status\">{message}</p>}{statusError && <p className=\"ftshare-error\" role=\"alert\">{statusError}</p>}\n </section>\n </>\n}\n","/**\n * `logs` 命名空间的共享契约:只有常量与类型,浏览器半边可以安全导入。\n *\n * 采集器(Host 常驻)与导出器(设置页)通过文件解耦;本文件是两者唯一的共同语言。\n */\n\n/**\n * 卡片在 `ftshare.settings.card` 槽位里的 id 与 order。\n *\n * 归 feature 所有(与业务契约同一份来源):卡片组件与浏览器侧的守卫测试都读它,\n * order 20 排在 API Key 卡(10)之后。\n */\nexport const LOGS_CARD_ID = 'logs'\nexport const LOGS_CARD_ORDER = 20\n\n/** 显式登记的 RPC 方法;未登记的方法一律 `not_found`。 */\nexport const LOGS_METHODS = ['stats', 'preview', 'export'] as const\nexport type LogsMethod = typeof LOGS_METHODS[number]\n\nexport type LogLevelName = 'error' | 'info' | 'warn' | 'debug'\n\n/**\n * 配置级别 → cordis `levels.default` 数值。\n *\n * cordis 的 `levels` 是**接收上限**而不是下限:`targetLevel < level` 才丢弃。\n * 因此 `warn`(2) 收 error/info/warn,`debug`(3) 收全部,`info`(1) 会把 `warn` 丢掉。\n */\nexport const LOG_LEVEL_THRESHOLD: Record<LogLevelName, number> = { error: 0, info: 1, warn: 2, debug: 3 }\nexport const LOG_LEVEL_NAMES = ['warn', 'info', 'debug', 'error'] as const\n\n/**\n * 可配置的级别(规格 §10)。\n *\n * 不提供 `info` 档:`levels` 是接收上限,`info`(1) 会把 `warn`(2) 一起丢掉,与规格 §6\n * 「失败日志不低于 warn」冲突;`error`(0) 同样不成立(等于只剩 error)。允许值收敛为\n * `warn | debug`,档位越界由 Schemastery 直接拒绝(插件不挂载),不做静默夹紧。\n */\nexport const LOGS_CONFIG_LEVELS = ['warn', 'debug'] as const\nexport type LogsConfigLevel = typeof LOGS_CONFIG_LEVELS[number]\n\n/** 落盘位置:`$DSH_HOME/logs/ft/`。 */\nexport const LOGS_DIR_SEGMENTS = ['logs', 'ft'] as const\nexport const LOGS_CURRENT_FILE = 'current.log'\nexport const LOGS_ROTATED_PREFIX = 'ft-'\nexport const LOGS_FILE_SUFFIX = '.log'\n\nexport const LOGS_LINE_MAX_BYTES = 8 * 1024\nexport const LOGS_TRUNCATED_MARK = '...[已截断]'\nexport const LOGS_REPEAT_FIELD = '重复次数'\nexport const LOGS_THROTTLE_WINDOW_MS = 10_000\nexport const LOGS_MAX_PENDING_KEYS = 2_000\nexport const LOGS_MAX_FIELD_LENGTH = 2_000\nexport const LOGS_MAX_FIELD_DEPTH = 3\n\nexport const LOGS_DEFAULT_PREVIEW_LINES = 80\nexport const LOGS_MAX_PREVIEW_LINES = 200\nexport const LOGS_PREVIEW_MAX_BYTES = 256 * 1024\n\n/** 导出体积上限(规格 §9):单来源 2 MiB、合计 8 MiB、目录最多 20 个 `*.log`。 */\nexport const LOGS_SOURCE_MAX_BYTES = 2 * 1024 * 1024\nexport const LOGS_EXPORT_MAX_BYTES = 8 * 1024 * 1024\nexport const LOGS_MAX_SOURCE_FILES = 20\n\nexport const LOGS_EXPORT_FILE_PREFIX = 'ft-runtime-'\nexport const LOGS_SECRET_TTL_MS = 5_000\n\n/**\n * 诊断包路由(规格 §9 / §12 决策 3):`/api` 载体上的第二条精确路由,归 `logs` feature 所有。\n *\n * 卡片用 `<a download>` 直接指向它:浏览器同源 GET 自带凭据,不需要 fetch/Blob,\n * `/api` 前缀的鉴权由载体负责,本 feature 不重复鉴权、也不读任何请求参数。\n * 它随 owner fiber 释放(`ctx.effect` + `register` 返回的 disposer)。\n */\nexport const LOGS_ARCHIVE_ROUTE = '/api/ftshare-settings/logs.zip' as const\n/** 归档内固定条目名(`manifest.json` 永远排第一)。 */\nexport const LOGS_ARCHIVE_MANIFEST = 'manifest.json'\nexport const LOGS_ARCHIVE_LOG = 'ft-runtime.log'\n/** 额外来源原文的目录前缀:`sources/<n>-<basename>`。 */\nexport const LOGS_ARCHIVE_SOURCE_DIR = 'sources/'\n\n/** 导出 header 里的来源段落分隔标记。 */\nexport const LOGS_SECTION_RULE = '====='\n\n/** 一条来源(文件或目录)在 Host 侧的存在性快照。 */\nexport interface LogsSourceState {\n readonly path: string\n readonly kind: 'file' | 'directory' | 'missing'\n readonly bytes?: number\n readonly files?: number\n}\n\n/** 导出物 header 里每个来源的统计。 */\nexport interface LogsSourceSummary {\n readonly label: string\n readonly path: string\n readonly lines: number\n readonly bytes: number\n readonly firstAt?: string\n readonly lastAt?: string\n readonly truncated: boolean\n readonly missing?: boolean\n}\n\n/** 采集器运行时统计;时间一律是北京时间 ISO 字符串。 */\nexport interface LogsStats {\n readonly file: string\n readonly fileBytes: number\n readonly level: LogLevelName\n readonly maxBytes: number\n readonly keep: number\n readonly records: number\n readonly dropped: number\n readonly rotations: number\n readonly writeFailures: number\n readonly firstAt?: string\n readonly lastAt?: string\n readonly byName: Readonly<Record<string, number>>\n readonly sources: readonly LogsSourceState[]\n}\n\nexport interface LogsPreview {\n readonly lines: readonly string[]\n readonly bytes: number\n readonly truncated: boolean\n readonly fileBytes: number\n}\n\nexport interface LogsExport {\n readonly fileName: string\n readonly text: string\n readonly bytes: number\n readonly truncated: boolean\n readonly sources: readonly LogsSourceSummary[]\n}\n\n/** 浏览器可用的只读日志接口。 */\nexport interface LogsPublicApi {\n stats(): Promise<LogsStats>\n preview(lines?: number): Promise<LogsPreview>\n /** 先刷盘,再把主日志与配置的 sources 拼成单文件文本。 */\n export(): Promise<LogsExport>\n}\n\n/** Host 服务:比浏览器多出本地路径与刷盘/释放句柄的能力。 */\nexport interface LogsSettings extends LogsPublicApi {\n readonly filePath: string\n flush(): Promise<void>\n dispose(): Promise<void>\n}\n","import {\n LOGS_DEFAULT_PREVIEW_LINES,\n type LogsExport,\n type LogsMethod,\n type LogsPreview,\n type LogsPublicApi,\n type LogsStats,\n} from './contract.js'\nimport { callSettingsRpc, type ClientTransport } from '../../core/rpc-core.js'\n\n/** 浏览器适配器:只读,且不暴露本地路径以外的任何 Host 能力(刷盘与释放句柄)。 */\nexport function createLogsClient(transport: ClientTransport): LogsPublicApi {\n const call = <T>(method: LogsMethod, payload: unknown = {}): Promise<T> =>\n callSettingsRpc<T>(transport, 'logs', method, payload)\n return {\n stats: () => call<LogsStats>('stats'),\n preview: (lines = LOGS_DEFAULT_PREVIEW_LINES) => call<LogsPreview>('preview', { lines }),\n export: () => call<LogsExport>('export'),\n }\n}\n","/**\n * The `logs` card's own styles, rendered by the card component's `<style>`.\n *\n * These two rules used to live in the core section stylesheet; a card owns\n * everything past the section shell, so they moved here with the card. Every\n * selector stays scoped under `.ftshare-settings`, so a card never styles\n * another card's markup.\n */\nexport const LOGS_CARD_CSS = `\n.ftshare-settings select { height:32px; border:0.5px solid var(--ftshare-control); border-radius:var(--dsw-radius-md); padding:0 10px; background:var(--ftshare-input); color:inherit; font:inherit; }\n.ftshare-settings .ftshare-download { display:inline-flex; align-items:center; min-height:32px; padding:0 14px; border:0.5px solid var(--ftshare-control); border-radius:var(--dsw-radius-md); color:inherit; text-decoration:none; cursor:pointer; }\n.ftshare-settings .ftshare-log-preview { max-height:320px; overflow:auto; margin:0 0 12px; padding:12px; background:var(--ftshare-input); border:0.5px solid var(--ftshare-border); border-radius:var(--dsw-radius-lg); font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:12px; line-height:1.5; white-space:pre-wrap; overflow-wrap:anywhere; }\n`\n","import { useCallback, useEffect, useState, type ReactNode } from 'react'\nimport {\n LOGS_ARCHIVE_ROUTE,\n LOGS_CARD_ID,\n LOGS_CARD_ORDER,\n LOGS_DEFAULT_PREVIEW_LINES,\n LOGS_MAX_PREVIEW_LINES,\n type LogsPreview,\n type LogsPublicApi,\n type LogsStats,\n} from './contract.js'\nimport { createLogsClient } from './client-api.js'\nimport { createSettingsTransport } from '../../core/client-transport.js'\nimport { FTSHARE_SETTINGS_CARD_SLOT } from '../../core/contract.js'\nimport type { FtshareSettingsShellContext } from '../../core/shell.js'\nimport { LOGS_CARD_CSS } from './styles.js'\n\n/** 预览行数候选:上限由 Host 侧校验兜底。 */\nconst PREVIEW_CHOICES = [50, LOGS_DEFAULT_PREVIEW_LINES, LOGS_MAX_PREVIEW_LINES]\n\n/**\n * 快照缺失时的占位文案。\n *\n * 失败后不能继续写「读取中…」:那会把一次已经结束的失败说成正在进行的读取,掩盖真正的\n * 原因(例如宿主插件没有这个接口,重试永远不会成功)。\n * @param failed - 本次读取是否已经失败。\n * @returns 失败提示,或仍在读取的提示。\n */\nexport function statsPlaceholder(failed: boolean): string {\n return failed ? '读取失败' : '读取中…'\n}\n\nfunction displayBytes(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`\n return `${(bytes / 1024 / 1024).toFixed(2)} MiB`\n}\n\n/** 把文本包成 Blob,用 a[download] 触发下载(Web 与桌面同一套)。 */\nfunction downloadText(fileName: string, text: string): void {\n const url = URL.createObjectURL(new Blob([text], { type: 'text/plain;charset=utf-8' }))\n const anchor = document.createElement('a')\n anchor.href = url\n anchor.download = fileName\n anchor.rel = 'noopener'\n document.body.appendChild(anchor)\n anchor.click()\n anchor.remove()\n setTimeout(() => URL.revokeObjectURL(url), 0)\n}\n\n/**\n * Contribute the built-in runtime-log card through the same slot a third-party\n * card uses. Nothing about it is privileged: same declaration, same options.\n * @param ctx - client context carrying the renderer's slots service.\n * @returns idempotent disposer for the card registration.\n */\nexport function registerLogsCard(ctx: FtshareSettingsShellContext): () => void {\n return ctx.slots.inject(FTSHARE_SETTINGS_CARD_SLOT, () => ctx.slots.register(\n { name: FTSHARE_SETTINGS_CARD_SLOT, id: LOGS_CARD_ID, order: LOGS_CARD_ORDER },\n createLogsCard(),\n ))\n}\n\n/** Bind the card's adapter; the component only reads through it. */\nfunction createLogsCard(): () => ReactNode {\n const api = createLogsClient(createSettingsTransport())\n return () => <LogsSection api={api} />\n}\n\nexport function LogsSection({ api }: { readonly api: LogsPublicApi }): ReactNode {\n const [stats, setStats] = useState<LogsStats>()\n const [preview, setPreview] = useState<LogsPreview>()\n const [lines, setLines] = useState(LOGS_DEFAULT_PREVIEW_LINES)\n const [busy, setBusy] = useState(false)\n const [message, setMessage] = useState('')\n const [error, setError] = useState('')\n /** 读取已失败且还没有任何快照时,每个字段都必须说出来,而不是假装还在读。 */\n const placeholder = statsPlaceholder(error !== '')\n\n const refresh = useCallback(async () => {\n setBusy(true)\n setError('')\n try {\n setStats(await api.stats())\n setPreview(await api.preview(lines))\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : '读取日志状态失败')\n } finally {\n setBusy(false)\n }\n }, [api, lines])\n\n useEffect(() => { void refresh() }, [refresh])\n\n const doExport = async () => {\n setBusy(true)\n setError('')\n setMessage('')\n try {\n const result = await api.export()\n downloadText(result.fileName, result.text)\n setMessage(`已导出 ${result.fileName}(${displayBytes(result.bytes)}${result.truncated ? ',已达体积上限并截断' : ''})`)\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : '导出日志失败')\n } finally {\n setBusy(false)\n }\n }\n\n return <>\n <style>{LOGS_CARD_CSS}</style>\n <section className=\"ftshare-settings\" aria-label=\"FT 运行日志\" aria-busy={busy}>\n <header><div><span className=\"ftshare-eyebrow\">FTSHARE / LOGS</span><h2>运行日志</h2></div></header>\n <div className=\"ftshare-card\">\n <h3>采集状态</h3>\n <dl>\n <div className=\"ftshare-result\"><dt>日志文件</dt><dd>{stats?.file ?? placeholder}</dd></div>\n <div className=\"ftshare-result\"><dt>记录 / 时间范围</dt><dd>\n {stats === undefined ? placeholder : `${stats.records} 条`}\n {stats?.firstAt !== undefined && `(${stats.firstAt} ~ ${stats.lastAt})`}\n </dd></div>\n <div className=\"ftshare-result\"><dt>文件大小 / 上限</dt><dd>\n {stats === undefined ? placeholder : `${displayBytes(stats.fileBytes)} / ${displayBytes(stats.maxBytes)}`}\n </dd></div>\n <div className=\"ftshare-result\"><dt>丢弃 / 轮转 / 写失败</dt><dd>\n {stats === undefined ? placeholder : `${stats.dropped} / ${stats.rotations} / ${stats.writeFailures}`}\n </dd></div>\n <div className=\"ftshare-result\"><dt>日志级别</dt><dd>{stats?.level ?? placeholder} </dd></div>\n <div className=\"ftshare-result\"><dt>统计</dt><dd>\n {stats === undefined\n ? placeholder\n : Object.keys(stats.byName).length === 0\n ? '暂无'\n : Object.entries(stats.byName).map(([name, count]) => `${name}=${count}`).join('、')}\n </dd></div>\n <div className=\"ftshare-result\"><dt>额外来源</dt><dd>\n {stats === undefined\n ? placeholder\n : stats.sources.length === 0\n ? '暂无'\n : stats.sources.map(source => `${source.path}(${source.kind}${source.bytes === undefined ? '' : ` ${displayBytes(source.bytes)}`})`).join(';')}\n </dd></div>\n </dl>\n <div className=\"ftshare-actions\">\n <button className=\"primary\" type=\"button\" onClick={() => void doExport()} disabled={busy}>导出日志</button>\n {/* 同源 GET,浏览器自带凭据:直接指向路由,不需要 fetch/Blob。 */}\n <a className=\"ftshare-download\" href={LOGS_ARCHIVE_ROUTE} download=\"\">导出诊断包</a>\n <button type=\"button\" onClick={() => void refresh()} disabled={busy}>刷新</button>\n <label htmlFor=\"ftshare-log-lines\">预览行数</label>\n <select id=\"ftshare-log-lines\" value={lines} onChange={e => setLines(Number(e.target.value))} disabled={busy}>\n {PREVIEW_CHOICES.map(choice => <option key={choice} value={choice}>{choice}</option>)}\n </select>\n </div>\n {busy && <p role=\"status\">正在处理,请稍候…</p>}\n {message && <p role=\"status\">{message}</p>}\n {error && <p className=\"ftshare-error\" role=\"alert\">{error}</p>}\n </div>\n <div className=\"ftshare-card\">\n <h3>最近日志{preview?.truncated === true ? '(已截断)' : ''}</h3>\n {preview === undefined\n ? <p>{error === '' ? '正在读取…' : '日志预览读取失败,见上方提示。'}</p>\n : preview.lines.length === 0\n ? <p>无</p>\n : <pre className=\"ftshare-log-preview\">{preview.lines.join('\\n')}</pre>}\n </div>\n </section>\n </>\n}\n","/**\n * The section shell's styles plus the theme tokens every card reads.\n *\n * The shell owns the container, typography, focus ring and link rules that\n * every card sits inside; a card owns its own CSS and renders it from its own\n * component, and core never styles card internals.\n *\n * The theme layer declares the plugin's `--ftshare-*` tokens once, on\n * `.ftshare-settings` — the root element every card renders. Surfaces and text\n * take their value from the host palette: `--dsw-alias-*` is what DSH\n * publishes for the light, the dark and every registered theme, so one\n * stylesheet follows the live palette with no script, no `prefers-color-scheme`\n * copy and no second palette to keep in sync. The literal inside each `var()`\n * is the fallback for a host that publishes no palette.\n *\n * The declarations sit on the card root rather than on `:root` because a\n * custom property resolves where it is *declared*: `--dsw-alias-*` exists from\n * `body` downwards, so a value derived from it above `body` would resolve to\n * nothing and fall back. Consequence: the place to pin a look is the host\n * palette itself (`ctx.theme.register` / `ctx.theme.overrideTokens`), not these\n * names.\n *\n * `--ftshare-accent*` is the plugin's own brand and has no host equivalent. The\n * values come from the FT brand layer's `--ft-*` tokens (`src/brand.ts`), with\n * the same literal as the fallback so the card keeps its colors when that layer\n * is absent. The fill and its ink are scheme-invariant;\n * `--ftshare-accent-on-surface` mixes the same yellow towards the host's text\n * colour, so the brand stays legible as the 11px eyebrow and as the focus ring\n * on either surface.\n */\nexport const SETTINGS_CSS = `\n.ftshare-settings { --ftshare-text:var(--dsw-alias-label-primary,#f4f4f5); --ftshare-muted:var(--dsw-alias-label-secondary,#aeb2bd); --ftshare-panel:var(--dsw-alias-settings-card-fill,#292a30); --ftshare-border:var(--dsw-alias-settings-card-stroke,#44464f); --ftshare-control:var(--dsw-alias-border-l4,#696c77); --ftshare-input:var(--dsw-alias-bg-layer-1,#1d1e23); --ftshare-link:var(--dsw-alias-link,#d8c4fc); --ftshare-error:var(--dsw-alias-state-error-primary,#ffb5ad); --ftshare-accent:var(--ft-accent,#f0c433); --ftshare-accent-ink:var(--ft-accent-ink,#171717); --ftshare-accent-on-surface:color-mix(in srgb,var(--ftshare-accent) 50%,var(--dsw-alias-label-primary,#f4f4f5)); }\n.ftshare-settings { color:var(--ftshare-text); max-width:760px; font-size:14px; line-height:1.6; }\n.ftshare-settings * { box-sizing:border-box; }\n.ftshare-settings h2 { font-size:26px; margin:4px 0 8px; }\n.ftshare-settings h3 { font-size:15px; margin:0 0 12px; }\n.ftshare-settings p { margin:8px 0; color:var(--ftshare-muted); }\n.ftshare-settings header { display:flex; justify-content:space-between; align-items:flex-start; gap:20px; margin-bottom:20px; }\n.ftshare-settings small { display:block; color:var(--ftshare-muted); font-size:12px; margin:5px 0 12px; }\n.ftshare-settings :is(button,input,a):focus-visible { outline:2px solid var(--ftshare-accent-on-surface); outline-offset:3px; }\n.ftshare-settings a { color:var(--ftshare-link); }\n@media(max-width:540px) { .ftshare-settings header { flex-direction:column; gap:10px; } }\n`\n","import type { ReactNode } from 'react'\nimport type { PropsRenderSlots, PropsRuntime, SlotCore, SlotMap } from '@deepseek-ai/dsh-client-ui-slots'\n// Type-only: pulls the `settings.section` SlotMap entry into this program.\nimport type {} from '@deepseek-ai/dsh-client-ui-settings/client'\nimport { FTSHARE_SETTINGS_CARD_SLOT, FTSHARE_SETTINGS_SECTION } from './contract.js'\nimport { SETTINGS_CSS } from './styles.js'\n\n/** Section slot this seam contributes into; declared by the settings shell. */\nconst FTSHARE_SETTINGS_SECTION_NAME = 'settings.section' as const\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface SlotMap {\n /** One card inside the FTShare settings section; `order` sets its position. */\n 'ftshare.settings.card': {\n kind: 'list'\n scope: 'root'\n owner: FtshareSettingsCardOwnerProps\n }\n }\n}\n/** Owner share of a card: the section supplies no card-specific props. */\nexport interface FtshareSettingsCardOwnerProps {\n /** Marker field: card owner props are intentionally empty. */\n children?: never\n}\n/**\n * The slice of the renderer-owned slots service this seam uses.\n *\n * `register` IS the ui-slots registration signature (the runtime registry\n * reuses it verbatim, adding disposal through the caller's fiber); `inject`\n * is the runtime wrapper's declaration-lifetime hook, which is why it is\n * mirrored structurally here.\n */\nexport interface FtshareSettingsSlotsService {\n readonly register: SlotCore['register']\n /** Run `callback` for each declaration lifetime of `key`. */\n inject(key: keyof SlotMap & string, callback: () => () => void): () => void\n}\n/**\n * Browser context the section shell requires: the renderer's slots service.\n * A card that needs more (its own services, invalidation events) declares the\n * extra members on its own context type.\n */\nexport interface FtshareSettingsShellContext {\n slots: FtshareSettingsSlotsService\n}\n/** Composed props of the section entry: the shell's owner share plus its declared child render. */\nexport type FtshareSettingsSectionProps =\n & PropsRuntime<typeof FTSHARE_SETTINGS_SECTION_NAME>\n & PropsRenderSlots<typeof FTSHARE_SETTINGS_CARD_SLOT>\n\n/**\n * The settings section shell: core CSS variables, the section chrome, and the\n * single render site every card appears at. Cards own everything inside it.\n */\nexport function FtshareSettingsSection({ renderSlot }: FtshareSettingsSectionProps): ReactNode {\n return <>\n <style>{SETTINGS_CSS}</style>\n {renderSlot(FTSHARE_SETTINGS_CARD_SLOT, {})}\n </>\n}\n\n/**\n * Declare the card slot and contribute the section that renders it.\n *\n * Declaring the child slot is what authorizes the render site; a card\n * registering through {@link FTSHARE_SETTINGS_CARD_SLOT} before this runs waits\n * for the declaration instead of failing.\n * @param ctx - client context carrying the renderer's slots service.\n * @returns idempotent disposer for the section and its declaration.\n */\nexport function registerSettingsSection(ctx: FtshareSettingsShellContext): () => void {\n return ctx.slots.inject(FTSHARE_SETTINGS_SECTION_NAME, () => ctx.slots.register(\n {\n name: FTSHARE_SETTINGS_SECTION_NAME,\n ...FTSHARE_SETTINGS_SECTION,\n children: { [FTSHARE_SETTINGS_CARD_SLOT]: { kind: 'list', scope: 'root' } },\n },\n FtshareSettingsSection,\n ))\n}\n","/**\n * FT brand tokens published as a host theme override layer.\n *\n * The layer only *adds* brand variables on `body`; it never overrides a\n * `--dsw-alias-*` value, so host chrome keeps its own palette and any user\n * theme keeps working underneath. Consumers read `var(--ft-*)` with the same\n * value as their fallback: the layer is a convenience, not a dependency, so\n * every sub-plugin keeps its brand colors when this plugin is absent (or when\n * the host has no `theme` service at all). Scheme-invariant tokens repeat one\n * value; palette-aware tokens spell both with `light-dark()`.\n *\n * Keep those fallbacks equal to {@link FT_BRAND_TOKENS}; `tests/brand.test.mjs`\n * scans every package for the pairing and fails on a drift.\n */\n\n/** One token value per palette mode; the host rejects a bare string. */\nexport interface BrandTokenModes {\n readonly light: string\n readonly dark: string\n}\n\n/** One value for both palettes: brand fills are scheme-invariant. */\nconst invariant = (value: string): BrandTokenModes => ({ light: value, dark: value })\n\n/** Layer identity reported to `ctx.theme.overrideTokens` (one layer per source). */\nexport const FT_BRAND_SOURCE = '@dsh-ft/dsh-fft-settings'\n\n/**\n * The published brand colors.\n *\n * `accent` is the FT yellow used by this plugin's primary action and eyebrow;\n * `accent-warm` is the orange trading accent shared by market and trade\n * surfaces (it is the only palette-aware group); `wechat` is the enterprise-\n * WeChat green the SSO card keeps as its identity. Inks are the text colors on\n * those fills, and `-soft` is the tinted surface behind the accent.\n */\nexport const FT_BRAND_TOKENS: Readonly<Record<string, BrandTokenModes>> = Object.freeze({\n '--ft-accent': invariant('#f0c433'),\n '--ft-accent-ink': invariant('#171717'),\n '--ft-accent-warm': { light: '#cf5623', dark: '#ff8352' },\n '--ft-accent-warm-hover': { light: '#b8481b', dark: '#ff9a72' },\n '--ft-accent-warm-ink': { light: '#ffffff', dark: '#1a120e' },\n '--ft-accent-warm-soft': { light: '#fbeee8', dark: '#2b1d16' },\n '--ft-wechat': invariant('#2ebd68'),\n '--ft-wechat-ink': invariant('#102419'),\n})\n\n/** The slice of the host theme service this layer uses. */\nexport interface BrandThemeService {\n overrideTokens(source: string, tokens: Readonly<Record<string, BrandTokenModes>>): () => void\n}\n\n/** Scoped context delivered once the `theme` service is available. */\nexport interface BrandThemeScope {\n readonly theme: BrandThemeService\n effect(callback: () => () => void, label: string): void\n}\n\n/** The slice of the client context this layer needs: a deferred service inject. */\nexport interface BrandThemeContext {\n inject(deps: readonly string[], callback: (scope: BrandThemeScope) => void): void\n}\n\n/**\n * Stack the brand layer once the host theme service exists.\n *\n * The deferred `inject` is deliberate: this plugin's declared client context\n * names only the services its cards read, and a host without `theme` must\n * still load the settings page — the CSS fallbacks then carry the colors.\n * @param ctx - client context carrying the host theme service.\n */\nexport function applyBrandTokens(ctx: BrandThemeContext): void {\n ctx.inject(['theme'], (scope) => {\n scope.effect(\n () => scope.theme.overrideTokens(FT_BRAND_SOURCE, FT_BRAND_TOKENS),\n 'ft-settings: FT brand tokens',\n )\n })\n}\n","import { registerApiKeyCard, type ApiKeyCardContext } from './features/api-key/card.js'\nimport { registerLogsCard } from './features/logs/card.js'\nimport { registerSettingsSection } from './core/shell.js'\nimport { applyBrandTokens, type BrandThemeContext } from './brand.js'\nexport * from './core/shell.js'\n/** Browser context this plugin's own client half requires. */\nexport type FtshareSettingsClientContext = ApiKeyCardContext\nexport const inject = ['slots', 'remote']\nexport function apply(ctx: FtshareSettingsClientContext): void {\n registerFtshareSettings(ctx)\n // The declared context above names only the services the cards read; the\n // brand layer defers on `theme` through the full Cordis context, so this\n // plugin still loads on a host without the theme service.\n applyBrandTokens(ctx as unknown as BrandThemeContext)\n}\n/** Contribute the section shell, then the built-in cards through the slot it declares. */\nexport function registerFtshareSettings(ctx: FtshareSettingsClientContext): void {\n registerSettingsSection(ctx)\n registerApiKeyCard(ctx)\n registerLogsCard(ctx)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":["useMemo","useSyncExternalStore","useState","useMemo","useSyncExternalStore","useState","useRef","useCallback","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/free-key-notice/storage.ts","../src/features/free-key-notice/decision.ts","../src/features/free-key-notice/styles.ts","../src/features/free-key-notice/card.tsx","../src/features/free-key-notice/index.ts","../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\n/**\n * 申请/购买 API Key 的页面。`utm_source=dsh-fft` 标记来源为本插件,站点据此归因;\n * 卡片与一次性提示共用这一条地址,不要在别处另写一份。\n */\nexport const FTSHARE_API_KEY_APPLY_URL = 'https://ftai.chat/me/profile?tab=api-key&utm_source=dsh-fft' as const\n/**\n * 设备级「已看过免费 Key 提示」标记的 localStorage 键。\n *\n * 带版本号:改一次文案就把版本加一,让所有用户重新看一次,不需要让用户清缓存。\n * 设备级语义(重启、升级都保留,跨 workspace/窗口共享)由桌面版主窗口的\n * `dsh-app://app/` origin 私有存储提供,不写宿主的配置文件。\n */\nexport const FREE_KEY_NOTICE_STORAGE_KEY = 'dsh-fft.free-key-notice.v1' 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_APPLY_URL, 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={FTSHARE_API_KEY_APPLY_URL} 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 * 「看过免费 Key 提示」的设备级标记。\n *\n * 只有一个事实(看过/没看过),所以不建服务、不建 store:读一次、写一次,\n * 两个纯函数加一个最小的存储接口。存储接口只声明用到的那两个方法,测试可以传\n * 一个内存实现,浏览器里传 `window.localStorage`。\n *\n * 读失败一律按「没看过」处理:隐私模式或存储被禁用时 `localStorage` 会抛异常,\n * 这时提示最多多弹一次,比整个设置页崩掉可接受。\n */\n\n/** 这一层用到的 `localStorage` 子集;隔离出来是为了让测试不碰真实存储。 */\nexport interface NoticeFlagStorage {\n getItem(key: string): string | null\n setItem(key: string, value: string): void\n}\n\n/**\n * 取浏览器存储;拿不到(没有 window、被禁用)时返回 undefined。\n * @returns 可用存储,或 undefined。\n */\nexport function browserNoticeStorage(): NoticeFlagStorage | undefined {\n try {\n const storage = globalThis.localStorage\n return storage === undefined ? undefined : { getItem: key => storage.getItem(key), setItem: (key, value) => { storage.setItem(key, value) } }\n } catch (_blocked) {\n // 访问 `localStorage` 本身在受限环境会抛 SecurityError。\n return undefined\n }\n}\n\n/**\n * 读标记。任何异常与空值都算「没看过」。\n * @param storage - 目标存储;缺失视为没看过。\n * @param key - 标记键。\n * @returns 是否已看过。\n */\nexport function readNoticeSeen(storage: NoticeFlagStorage | undefined, key: string): boolean {\n if (storage === undefined) return false\n try {\n return storage.getItem(key) === '1'\n } catch (_blocked) {\n return false\n }\n}\n\n/**\n * 写标记。写失败不抛:提示已经显示过了,这次没记下来只是下次会再弹一次。\n * @param storage - 目标存储;缺失时什么都不做。\n * @param key - 标记键。\n * @returns 是否写入成功。\n */\nexport function writeNoticeSeen(storage: NoticeFlagStorage | undefined, key: string): boolean {\n if (storage === undefined) return false\n try {\n storage.setItem(key, '1')\n return true\n } catch (_blocked) {\n // 配额满或被禁用:不阻断用户,下次再弹。\n return false\n }\n}\n","/**\n * 「这条一次性提示该不该弹」的判定。\n *\n * 与渲染分开:判定是纯函数、零依赖,能在 Node 测试里直接钉住;渲染层只管把\n * 模态框摆出去。三条输入里任何一条没准备好都按「不弹」处理——这是刻意的,\n * 一次没弹只是少一次提醒,弹错一次就是打扰。\n */\n\n/** 宿主 `shell.overlay` 槽名;这个 feature 只往这一个座位贡献。 */\nexport const FREE_KEY_NOTICE_SLOT = 'shell.overlay' as const\n/** 条目 id:必须自己的,占用宿主的会顶掉别人的浮层。 */\nexport const FREE_KEY_NOTICE_ID = 'ftshare.free-key-notice' as const\n\n/** 提示出现前需要的会话事实,只取用得到的字段。 */\nexport interface FreeKeyNoticeSessionState {\n phase: string\n byId: Record<string, { blank: boolean; retainedBy?: { mainView?: number } }>\n}\n\n/**\n * 主视图是否停在一个空会话上(没有内容会被弹窗挡住)。\n * @param state - 会话列表快照。\n * @returns 是否可以安全地弹出模态框。\n */\nexport function isViewIdle(state: FreeKeyNoticeSessionState): boolean {\n if (state.phase !== 'ready') return false\n const main = Object.values(state.byId).find(session => (session.retainedBy?.mainView ?? 0) > 0)\n return main === undefined || main.blank\n}\n\n/**\n * 是否应当显示提示。\n * - 已看过:不弹(设备级一次);\n * - Key 已配置、或状态还读不到:不弹(读不到时宁可少弹);\n * - 主视图被有内容的会话占着:不弹,等它空下来。\n * @param facts - 三个输入。\n * @returns 是否显示。\n */\nexport function shouldShowNotice(facts: {\n seen: boolean\n configured: boolean | undefined\n viewIdle: boolean\n}): boolean {\n if (facts.seen) return false\n if (facts.configured !== false) return false\n return facts.viewIdle\n}\n","/**\n * 一次性提示的样式:固定在左下角的非阻塞卡片。\n *\n * 不盖遮罩、不抢焦点——这一层(宿主的 `shell.overlay`)本身可穿透点击,\n * 条目只有在这张卡片范围内才接收指针事件,所以卡片以外的界面照常可用。\n * 位置用 `fixed` 而不是依赖外层布局:浮层是 `inset:0` 的绝对定位层,\n * 条目自己决定停在哪里。\n *\n * 颜色全走 `--dsw-alias-*` 与 `--ft-*` 变量,深色/浅色两套调色板共用一份。\n */\nexport const FREE_KEY_NOTICE_CSS = `\n.ftshare-free-key-notice { position:fixed; left:24px; bottom:24px; z-index:20; box-sizing:border-box; width:min(320px, calc(100vw - 48px)); padding:16px; border:0.5px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1)); border-radius:var(--dsw-radius-xl,20px); background:var(--dsw-alias-bg-layer-2,#fff); color:var(--dsw-alias-label-primary,#0f1115); box-shadow:var(--dsw-elevation-prominent,0 3px 8px rgba(0,0,0,.04), 0 0 20px rgba(0,0,0,.05)); animation:ftshareNoticeIn .2s ease; }\n.ftshare-free-key-notice * { box-sizing:border-box; }\n.ftshare-free-key-notice .ftshare-free-key-head { display:flex; align-items:flex-start; justify-content:space-between; gap:8px; }\n.ftshare-free-key-notice strong { font-size:14px; font-weight:500; line-height:22px; }\n.ftshare-free-key-notice .ftshare-free-key-close { flex:none; width:22px; height:22px; display:inline-flex; align-items:center; justify-content:center; border:none; border-radius:var(--dsw-radius-sm,8px); background:transparent; color:var(--dsw-alias-label-secondary,#61666b); font:inherit; font-size:12px; line-height:1; cursor:pointer; }\n.ftshare-free-key-notice .ftshare-free-key-close:hover { background:var(--dsw-alias-bg-layer-3,#f9fafb); }\n.ftshare-free-key-notice p { margin:6px 0 14px; font-size:13px; line-height:1.6; color:var(--dsw-alias-label-secondary,#61666b); }\n.ftshare-free-key-notice .ftshare-free-key-actions { display:flex; gap:8px; }\n.ftshare-free-key-notice .ftshare-free-key-actions :is(button,a) { min-height:30px; display:inline-flex; align-items:center; justify-content:center; border-radius:var(--dsw-radius-md,12px); border:0.5px solid var(--dsw-alias-border-l4,rgba(0,0,0,.16)); padding:0 12px; font:inherit; font-size:13px; background:transparent; color:inherit; text-decoration:none; cursor:pointer; }\n/* 主入口用品牌黄的半透明底:保留 FT 的色,又不至于在左下角变成一块醒目的亮黄。\n 文字仍取正文主色(不是压在上面的深黑字),70% 在深浅两套调色板下都能读。 */\n.ftshare-free-key-notice .ftshare-free-key-actions .primary { flex:1; background:color-mix(in srgb,var(--ft-accent,#f0c433) 70%,transparent); border-color:var(--ft-accent,#f0c433); color:var(--dsw-alias-label-primary,#0f1115); font-weight:500; }\n.ftshare-free-key-notice .ftshare-free-key-actions .primary:hover { background:color-mix(in srgb,var(--ft-accent,#f0c433) 82%,transparent); }\n@keyframes ftshareNoticeIn { from { opacity:0; transform:translateY(6px); } to { opacity:1; transform:none; } }\n@media (prefers-reduced-motion: reduce) { .ftshare-free-key-notice { animation:none; } }\n`\n","/**\n * 一次性提示:还没配置 API Key 的用户,去获取免费的 FTShare Key。\n *\n * 三个设计决定:\n *\n * 1. **左下角、不阻塞**。它是提示,不是必须处理的阻断:不盖遮罩、不抢焦点、\n * 不打断正在发生的事,12 秒后自己淡出(鼠标悬停时暂停计时)。用 `Modal`\n * 会遮住整个界面,代价太大。\n * 2. **只在确认「没配 Key」时出现**。凭据状态异步读来,没读到时一律不出现,\n * 否则配了 Key 的用户每次启动都会被闪一下。\n * 3. **设备级只出现一次**。首次出现就写 `localStorage` 标记(见 `./storage.ts`),\n * 不等用户点按钮——否则自动消失后标记不落盘,下次启动又会打扰。\n *\n * 挂载在宿主框架级的 `shell.overlay` 槽上(`ui-layout` 的 AppFrame 常驻渲染),\n * 不在设置页里:要触达的恰恰是从不打开设置的用户。该图层本身可穿透点击,\n * 条目只在自己的卡片范围内接收指针事件。\n */\nimport { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, type ReactNode } from 'react'\nimport { FTSHARE_API_KEY_APPLY_URL, FTSHARE_API_KEY_REF, FTSHARE_CREDENTIAL_EVENTS, FREE_KEY_NOTICE_STORAGE_KEY } from '../api-key/contract.js'\nimport type { ApiKeyCardContext } from '../api-key/card.js'\nimport { createApiKeyClient } from '../api-key/client-api.js'\nimport { createSettingsTransport } from '../../core/client-transport.js'\nimport { browserNoticeStorage, readNoticeSeen, writeNoticeSeen, type NoticeFlagStorage } from './storage.js'\nimport { FREE_KEY_NOTICE_ID, FREE_KEY_NOTICE_SLOT, isViewIdle, shouldShowNotice, type FreeKeyNoticeSessionState } from './decision.js'\nimport { FREE_KEY_NOTICE_CSS } from './styles.js'\n\n/** 停留时长:足够读完一句话并决定要不要去获取,又不至于一直挂在界面上。 */\nexport const FREE_KEY_NOTICE_AUTO_DISMISS_MS = 12_000\n\nexport { FREE_KEY_NOTICE_ID, FREE_KEY_NOTICE_SLOT }\n\n/** 提示组件依赖的外部输入;全部可注入,测试不需要真实网络与浏览器存储。 */\nexport interface FreeKeyNoticeDeps {\n /** 读当前 Key 是否已配置;reject 表示读不到。 */\n readConfigured: () => Promise<boolean>\n /** 订阅「Key 可能变了」,配置好后立即收起。 */\n subscribeConfigured: (listener: () => void) => () => void\n storage: NoticeFlagStorage | undefined\n applyUrl: string\n}\n\n/**\n * 把异步的 `configured` 摊成 uSES 快照。只在有订阅者时读一次,订阅清理时停止。\n * @param deps - 读函数与订阅函数。\n * @returns uSES 快照源。\n */\nfunction useConfigured(deps: FreeKeyNoticeDeps): boolean | undefined {\n const source = useMemo(() => {\n let value: boolean | undefined\n let stop: (() => void) | undefined\n const listeners = new Set<() => void>()\n const publish = () => { for (const listener of [...listeners]) listener() }\n const load = () => {\n deps.readConfigured().then(next => { value = next; publish() }, () => { /* 读失败保持 undefined */ })\n }\n return {\n getSnapshot: () => value,\n subscribe: (listener: () => void) => {\n listeners.add(listener)\n if (stop === undefined) {\n load()\n const off = deps.subscribeConfigured(load)\n stop = () => { off() }\n }\n return () => {\n listeners.delete(listener)\n if (listeners.size === 0) {\n stop?.()\n stop = undefined\n }\n }\n },\n }\n }, [deps])\n return useSyncExternalStore(source.subscribe, source.getSnapshot, source.getSnapshot)\n}\n\n/** 槽位提供的标准份额里,这里只用得到的这一个(由 ui-session 合并进所有槽位)。 */\ntype UseSessionsShare = (selector: (state: FreeKeyNoticeSessionState) => boolean) => boolean\n\n/**\n * 提示本体。\n * @param props - 依赖注入,以及宿主通过槽位交给每个条目的标准份额。\n * @returns 左下角卡片,或 null。\n */\nexport function FreeKeyNotice({ deps, useSessions }: {\n deps: FreeKeyNoticeDeps\n useSessions: UseSessionsShare\n}): ReactNode {\n const [seen, setSeen] = useState(() => readNoticeSeen(deps.storage, FREE_KEY_NOTICE_STORAGE_KEY))\n const [visible, setVisible] = useState(true)\n const configured = useConfigured(deps)\n const viewIdle = useSessions(isViewIdle)\n const show = visible && shouldShowNotice({ seen, configured, viewIdle })\n\n // 出现即记:自动消失或用户没点时也要落盘,不然下次启动又弹。\n useEffect(() => {\n if (!show || seen) return\n writeNoticeSeen(deps.storage, FREE_KEY_NOTICE_STORAGE_KEY)\n setSeen(true)\n }, [show, seen, deps.storage])\n\n // 自动淡出。悬停时暂停,移开后按剩余时间继续,避免用户正要看就被收走。\n const deadline = useRef<number | undefined>(undefined)\n const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)\n const dismiss = useCallback(() => { setVisible(false) }, [])\n const hold = useCallback(() => {\n if (timer.current !== undefined) clearTimeout(timer.current)\n timer.current = undefined\n deadline.current = undefined\n }, [])\n const resume = useCallback(() => {\n if (deadline.current === undefined) return\n const remaining = Math.max(0, deadline.current - Date.now())\n timer.current = setTimeout(dismiss, remaining)\n }, [dismiss])\n useEffect(() => {\n if (!show) return\n deadline.current = Date.now() + FREE_KEY_NOTICE_AUTO_DISMISS_MS\n timer.current = setTimeout(dismiss, FREE_KEY_NOTICE_AUTO_DISMISS_MS)\n return () => {\n if (timer.current !== undefined) clearTimeout(timer.current)\n timer.current = undefined\n deadline.current = undefined\n }\n }, [show, dismiss])\n\n if (!show) return null\n\n return <>\n <style>{FREE_KEY_NOTICE_CSS}</style>\n <aside\n className=\"ftshare-free-key-notice\"\n role=\"status\"\n aria-live=\"polite\"\n onMouseEnter={hold}\n onMouseLeave={resume}\n >\n <div className=\"ftshare-free-key-head\">\n <strong>免费获取 Key</strong>\n <button type=\"button\" className=\"ftshare-free-key-close\" aria-label=\"关闭\" onClick={dismiss}>✕</button>\n </div>\n <p>海量金融数据,一次配置长期使用</p>\n <div className=\"ftshare-free-key-actions\">\n <a className=\"primary\" href={deps.applyUrl} target=\"_blank\" rel=\"noreferrer\" onClick={dismiss}>去获取</a>\n <button type=\"button\" onClick={dismiss}>知道了</button>\n </div>\n </aside>\n </>\n}\n\n/**\n * 浏览器侧装配:把凭据通道、真实 localStorage 接到组件上。\n * `useSessions` 是宿主交给 `shell.overlay` 条目的标准份额,从 props 透传而不是自造。\n * @param ctx - 卡片那套上下文(slots + 凭据失效事件)。\n * @returns 槽位组件工厂。\n */\nexport function createFreeKeyNotice(ctx: ApiKeyCardContext): (props: { useSessions: UseSessionsShare }) => ReactNode {\n const api = createApiKeyClient(createSettingsTransport())\n const subscribeConfigured = (listener: () => void) => {\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 const deps: FreeKeyNoticeDeps = {\n readConfigured: async () => (await api.describe()).configured,\n subscribeConfigured,\n storage: browserNoticeStorage(),\n applyUrl: FTSHARE_API_KEY_APPLY_URL,\n }\n return ({ useSessions }) => <FreeKeyNotice deps={deps} useSessions={useSessions} />\n}\n","/**\n * 一次性提示的注册入口。\n *\n * 它和设置卡片走同一个 `ctx.slots`,但目标是框架级的 `shell.overlay`——\n * 宿主给「徽标、toast、状态条、对话框」这类全局浮层留的加法槽(`kind: list`,\n * `scope: root`),`ui-layout` 的 AppFrame 常驻渲染它,不依赖设置面板是否打开。\n *\n * 两条注册纪律来自宿主的槽位契约:\n * 1. `ctx.slots.inject(key, ...)` 等到槽位被声明再注册——启动早期 `shell.overlay`\n * 可能还不存在,直接 `register` 会抛。\n * 2. `id` 必须自己的。槽位按 id 去重,复用宿主的 id 会顶掉别人的浮层。\n */\nimport type { ReactNode } from 'react'\n// Type-only: pulls `shell.overlay` 的 SlotMap 声明(含 `useSessions` 这类标准份额)进本程序。\nimport type {} from '@deepseek-ai/dsh-client-ui-layout/client'\nimport type { ApiKeyCardContext } from '../api-key/card.js'\nimport { createFreeKeyNotice, FREE_KEY_NOTICE_ID, FREE_KEY_NOTICE_SLOT } from './card.js'\n\n/**\n * 注册一次性提示。\n * @param ctx - 客户端上下文,需带 `slots` 服务。\n * @returns 幂等 disposer。\n */\nexport function registerFreeKeyNotice(ctx: ApiKeyCardContext): () => void {\n const component = createFreeKeyNotice(ctx)\n return ctx.slots.inject(FREE_KEY_NOTICE_SLOT, () => ctx.slots.register(\n { name: FREE_KEY_NOTICE_SLOT, id: FREE_KEY_NOTICE_ID, order: 100 },\n component,\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 { registerFreeKeyNotice } from './features/free-key-notice/index.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'\nexport { FREE_KEY_NOTICE_ID, FREE_KEY_NOTICE_SLOT, isViewIdle, shouldShowNotice, type FreeKeyNoticeSessionState } from './features/free-key-notice/decision.js'\nexport { readNoticeSeen, writeNoticeSeen, type NoticeFlagStorage } from './features/free-key-notice/storage.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/**\n * Contribute the settings section, its cards, and the frame-wide notice.\n *\n * The notice deliberately does not live in the settings page: it has to reach\n * users who never open settings, so it takes a seat in `shell.overlay`.\n */\nexport function registerFtshareSettings(ctx: FtshareSettingsClientContext): void {\n registerSettingsSection(ctx)\n registerApiKeyCard(ctx)\n registerLogsCard(ctx)\n registerFreeKeyNotice(ctx)\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEffect, useMemo, useState, useSyncExternalStore } from 'react';
3
- import { FTSHARE_API_KEY_REF, FTSHARE_CREDENTIAL_EVENTS } from './contract.js';
3
+ import { FTSHARE_API_KEY_APPLY_URL, FTSHARE_API_KEY_REF, FTSHARE_CREDENTIAL_EVENTS } from './contract.js';
4
4
  import { FTSHARE_SETTINGS_CARD_SLOT } from '../../core/contract.js';
5
5
  import { FtshareSettingsModel } from './model.js';
6
6
  import { createApiKeyClient } from './client-api.js';
@@ -35,5 +35,5 @@ export function ApiKeyCard({ api, subscribeChanges }) {
35
35
  const disabled = busy || !status?.writable;
36
36
  const configure = async () => { if (await model.configureAndTest(draft))
37
37
  setDraft(''); };
38
- return _jsxs(_Fragment, { children: [_jsx("style", { children: API_KEY_CARD_CSS }), _jsxs("section", { className: "ftshare-settings", "aria-label": "FTShare \u8BBE\u7F6E", "aria-busy": busy, children: [_jsx("header", { children: _jsxs("div", { children: [_jsx("span", { className: "ftshare-eyebrow", children: "FTSHARE / API KEY" }), _jsx("h2", { children: "\u8BBF\u95EE\u51ED\u636E" })] }) }), _jsx("p", { children: "\u76EE\u524D\u96C6\u6210 FTShare \u6570\u636E\u6E90\u3002" }), status && !status.writable && _jsx("p", { children: "\u5F53\u524D\u6765\u6E90\u4E3A\u53EA\u8BFB\uFF0C\u53EF\u6D4B\u8BD5\u8FDE\u63A5\uFF1B\u4FEE\u6539\u5BC6\u94A5\u8BF7\u5230\u5BF9\u5E94\u6765\u6E90\u64CD\u4F5C\u3002" }), _jsxs("div", { className: "ftshare-card", children: [_jsx("label", { htmlFor: "ftshare-api-key", children: "FTShare API Key" }), _jsx("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 }), _jsxs("div", { className: "ftshare-actions", children: [_jsx("button", { className: "primary", type: "button", onClick: () => void configure(), disabled: disabled || !draft.trim(), children: "\u914D\u7F6E\u5E76\u6D4B\u8BD5" }), _jsx("button", { type: "button", onClick: () => void model.testConnection(), disabled: busy || !status, children: "\u6D4B\u8BD5\u5F53\u524D\u8FDE\u63A5" }), _jsx("button", { type: "button", onClick: () => void model.clear(), disabled: disabled || !status?.configured, children: "\u6E05\u9664\u5DF2\u4FDD\u5B58\u5BC6\u94A5" }), _jsx("button", { type: "button", onClick: () => void model.refresh(), disabled: busy, title: "\u91CD\u65B0\u8BFB\u53D6\u5DF2\u4FDD\u5B58\u7684\u914D\u7F6E\u548C\u4E0A\u6B21\u68C0\u6D4B\u7ED3\u679C\uFF0C\u4E0D\u4F1A\u53D1\u8D77\u8054\u7F51\u68C0\u6D4B", children: "\u91CD\u65B0\u8BFB\u53D6\u72B6\u6001" }), _jsx("a", { href: "https://ftai.chat/me/profile?tab=api-key", target: "_blank", rel: "noreferrer", children: "\u83B7\u53D6 API Key \u2197" })] }), draft && _jsx("small", { children: "\u201C\u6D4B\u8BD5\u5F53\u524D\u8FDE\u63A5\u201D\u4F7F\u7528\u5DF2\u751F\u6548\u7684\u5BC6\u94A5\uFF0C\u4E0D\u6D4B\u8BD5\u8F93\u5165\u6846\u5185\u5C1A\u672A\u4FDD\u5B58\u7684\u5185\u5BB9\u3002" }), busy && _jsx("p", { role: "status", children: "\u6B63\u5728\u5904\u7406\uFF0C\u8BF7\u7A0D\u5019\u2026" })] }), _jsxs("div", { className: "ftshare-card", "aria-live": "polite", children: [_jsx("h3", { children: "\u9A8C\u8BC1\u7ED3\u679C" }), _jsxs("dl", { children: [save && _jsxs("div", { className: "ftshare-result", children: [_jsx("dt", { children: "\u672C\u6B21\u4FDD\u5B58" }), _jsx("dd", { children: save.message })] }), _jsxs("div", { className: "ftshare-result", children: [_jsx("dt", { children: "\u8FDC\u7AEF\u670D\u52A1" }), _jsx("dd", { children: status?.validation.remote.message ?? '状态待确认' })] }), _jsxs("div", { className: "ftshare-result", children: [_jsx("dt", { children: "API Key" }), _jsx("dd", { children: status?.validation.apiKey.message ?? '状态待确认' })] }), status?.validation.testedAt && _jsxs("div", { className: "ftshare-result", children: [_jsx("dt", { children: "\u6D4B\u8BD5\u65F6\u95F4" }), _jsx("dd", { children: new Date(status.validation.testedAt).toLocaleString() })] })] }), _jsx("small", { children: "\u8282\u5047\u65E5\u6216\u6536\u76D8\u540E\u7684\u7A7A\u6570\u636E\u4E0D\u5F71\u54CD\u9A8C\u8BC1\u3002" })] }), message && _jsx("p", { role: "status", children: message }), statusError && _jsx("p", { className: "ftshare-error", role: "alert", children: statusError })] })] });
38
+ return _jsxs(_Fragment, { children: [_jsx("style", { children: API_KEY_CARD_CSS }), _jsxs("section", { className: "ftshare-settings", "aria-label": "FTShare \u8BBE\u7F6E", "aria-busy": busy, children: [_jsx("header", { children: _jsxs("div", { children: [_jsx("span", { className: "ftshare-eyebrow", children: "FTSHARE / API KEY" }), _jsx("h2", { children: "\u8BBF\u95EE\u51ED\u636E" })] }) }), _jsx("p", { children: "\u76EE\u524D\u96C6\u6210 FTShare \u6570\u636E\u6E90\u3002" }), status && !status.writable && _jsx("p", { children: "\u5F53\u524D\u6765\u6E90\u4E3A\u53EA\u8BFB\uFF0C\u53EF\u6D4B\u8BD5\u8FDE\u63A5\uFF1B\u4FEE\u6539\u5BC6\u94A5\u8BF7\u5230\u5BF9\u5E94\u6765\u6E90\u64CD\u4F5C\u3002" }), _jsxs("div", { className: "ftshare-card", children: [_jsx("label", { htmlFor: "ftshare-api-key", children: "FTShare API Key" }), _jsx("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 }), _jsxs("div", { className: "ftshare-actions", children: [_jsx("button", { className: "primary", type: "button", onClick: () => void configure(), disabled: disabled || !draft.trim(), children: "\u914D\u7F6E\u5E76\u6D4B\u8BD5" }), _jsx("button", { type: "button", onClick: () => void model.testConnection(), disabled: busy || !status, children: "\u6D4B\u8BD5\u5F53\u524D\u8FDE\u63A5" }), _jsx("button", { type: "button", onClick: () => void model.clear(), disabled: disabled || !status?.configured, children: "\u6E05\u9664\u5DF2\u4FDD\u5B58\u5BC6\u94A5" }), _jsx("button", { type: "button", onClick: () => void model.refresh(), disabled: busy, title: "\u91CD\u65B0\u8BFB\u53D6\u5DF2\u4FDD\u5B58\u7684\u914D\u7F6E\u548C\u4E0A\u6B21\u68C0\u6D4B\u7ED3\u679C\uFF0C\u4E0D\u4F1A\u53D1\u8D77\u8054\u7F51\u68C0\u6D4B", children: "\u91CD\u65B0\u8BFB\u53D6\u72B6\u6001" }), _jsx("a", { href: FTSHARE_API_KEY_APPLY_URL, target: "_blank", rel: "noreferrer", children: "\u83B7\u53D6 API Key \u2197" })] }), draft && _jsx("small", { children: "\u201C\u6D4B\u8BD5\u5F53\u524D\u8FDE\u63A5\u201D\u4F7F\u7528\u5DF2\u751F\u6548\u7684\u5BC6\u94A5\uFF0C\u4E0D\u6D4B\u8BD5\u8F93\u5165\u6846\u5185\u5C1A\u672A\u4FDD\u5B58\u7684\u5185\u5BB9\u3002" }), busy && _jsx("p", { role: "status", children: "\u6B63\u5728\u5904\u7406\uFF0C\u8BF7\u7A0D\u5019\u2026" })] }), _jsxs("div", { className: "ftshare-card", "aria-live": "polite", children: [_jsx("h3", { children: "\u9A8C\u8BC1\u7ED3\u679C" }), _jsxs("dl", { children: [save && _jsxs("div", { className: "ftshare-result", children: [_jsx("dt", { children: "\u672C\u6B21\u4FDD\u5B58" }), _jsx("dd", { children: save.message })] }), _jsxs("div", { className: "ftshare-result", children: [_jsx("dt", { children: "\u8FDC\u7AEF\u670D\u52A1" }), _jsx("dd", { children: status?.validation.remote.message ?? '状态待确认' })] }), _jsxs("div", { className: "ftshare-result", children: [_jsx("dt", { children: "API Key" }), _jsx("dd", { children: status?.validation.apiKey.message ?? '状态待确认' })] }), status?.validation.testedAt && _jsxs("div", { className: "ftshare-result", children: [_jsx("dt", { children: "\u6D4B\u8BD5\u65F6\u95F4" }), _jsx("dd", { children: new Date(status.validation.testedAt).toLocaleString() })] })] }), _jsx("small", { children: "\u8282\u5047\u65E5\u6216\u6536\u76D8\u540E\u7684\u7A7A\u6570\u636E\u4E0D\u5F71\u54CD\u9A8C\u8BC1\u3002" })] }), message && _jsx("p", { role: "status", children: message }), statusError && _jsx("p", { className: "ftshare-error", role: "alert", children: statusError })] })] });
39
39
  }
@@ -8,6 +8,19 @@
8
8
  * every consumer.
9
9
  */
10
10
  export declare const FTSHARE_API_KEY_REF: "FTSHARE_API_KEY";
11
+ /**
12
+ * 申请/购买 API Key 的页面。`utm_source=dsh-fft` 标记来源为本插件,站点据此归因;
13
+ * 卡片与一次性提示共用这一条地址,不要在别处另写一份。
14
+ */
15
+ export declare const FTSHARE_API_KEY_APPLY_URL: "https://ftai.chat/me/profile?tab=api-key&utm_source=dsh-fft";
16
+ /**
17
+ * 设备级「已看过免费 Key 提示」标记的 localStorage 键。
18
+ *
19
+ * 带版本号:改一次文案就把版本加一,让所有用户重新看一次,不需要让用户清缓存。
20
+ * 设备级语义(重启、升级都保留,跨 workspace/窗口共享)由桌面版主窗口的
21
+ * `dsh-app://app/` origin 私有存储提供,不写宿主的配置文件。
22
+ */
23
+ export declare const FREE_KEY_NOTICE_STORAGE_KEY: "dsh-fft.free-key-notice.v1";
11
24
  export declare const FTSHARE_CREDENTIAL_EVENTS: readonly ["credentials/reference-updated"];
12
25
  export declare const API_KEY_METHODS: readonly ["describe", "set", "clear", "configureAndTest", "testConnection", "getValidationStatus"];
13
26
  export type ApiKeyMethod = typeof API_KEY_METHODS[number];
@@ -8,6 +8,19 @@
8
8
  * every consumer.
9
9
  */
10
10
  export const FTSHARE_API_KEY_REF = 'FTSHARE_API_KEY';
11
+ /**
12
+ * 申请/购买 API Key 的页面。`utm_source=dsh-fft` 标记来源为本插件,站点据此归因;
13
+ * 卡片与一次性提示共用这一条地址,不要在别处另写一份。
14
+ */
15
+ export const FTSHARE_API_KEY_APPLY_URL = 'https://ftai.chat/me/profile?tab=api-key&utm_source=dsh-fft';
16
+ /**
17
+ * 设备级「已看过免费 Key 提示」标记的 localStorage 键。
18
+ *
19
+ * 带版本号:改一次文案就把版本加一,让所有用户重新看一次,不需要让用户清缓存。
20
+ * 设备级语义(重启、升级都保留,跨 workspace/窗口共享)由桌面版主窗口的
21
+ * `dsh-app://app/` origin 私有存储提供,不写宿主的配置文件。
22
+ */
23
+ export const FREE_KEY_NOTICE_STORAGE_KEY = 'dsh-fft.free-key-notice.v1';
11
24
  export const FTSHARE_CREDENTIAL_EVENTS = ['credentials/reference-updated'];
12
25
  export const API_KEY_METHODS = ['describe', 'set', 'clear', 'configureAndTest', 'testConnection', 'getValidationStatus'];
13
26
  /** Local syntax validation, never an assertion of upstream validity. */
@@ -0,0 +1,53 @@
1
+ /**
2
+ * 一次性提示:还没配置 API Key 的用户,去获取免费的 FTShare Key。
3
+ *
4
+ * 三个设计决定:
5
+ *
6
+ * 1. **左下角、不阻塞**。它是提示,不是必须处理的阻断:不盖遮罩、不抢焦点、
7
+ * 不打断正在发生的事,12 秒后自己淡出(鼠标悬停时暂停计时)。用 `Modal`
8
+ * 会遮住整个界面,代价太大。
9
+ * 2. **只在确认「没配 Key」时出现**。凭据状态异步读来,没读到时一律不出现,
10
+ * 否则配了 Key 的用户每次启动都会被闪一下。
11
+ * 3. **设备级只出现一次**。首次出现就写 `localStorage` 标记(见 `./storage.ts`),
12
+ * 不等用户点按钮——否则自动消失后标记不落盘,下次启动又会打扰。
13
+ *
14
+ * 挂载在宿主框架级的 `shell.overlay` 槽上(`ui-layout` 的 AppFrame 常驻渲染),
15
+ * 不在设置页里:要触达的恰恰是从不打开设置的用户。该图层本身可穿透点击,
16
+ * 条目只在自己的卡片范围内接收指针事件。
17
+ */
18
+ import { type ReactNode } from 'react';
19
+ import type { ApiKeyCardContext } from '../api-key/card.js';
20
+ import { type NoticeFlagStorage } from './storage.js';
21
+ import { FREE_KEY_NOTICE_ID, FREE_KEY_NOTICE_SLOT, type FreeKeyNoticeSessionState } from './decision.js';
22
+ /** 停留时长:足够读完一句话并决定要不要去获取,又不至于一直挂在界面上。 */
23
+ export declare const FREE_KEY_NOTICE_AUTO_DISMISS_MS = 12000;
24
+ export { FREE_KEY_NOTICE_ID, FREE_KEY_NOTICE_SLOT };
25
+ /** 提示组件依赖的外部输入;全部可注入,测试不需要真实网络与浏览器存储。 */
26
+ export interface FreeKeyNoticeDeps {
27
+ /** 读当前 Key 是否已配置;reject 表示读不到。 */
28
+ readConfigured: () => Promise<boolean>;
29
+ /** 订阅「Key 可能变了」,配置好后立即收起。 */
30
+ subscribeConfigured: (listener: () => void) => () => void;
31
+ storage: NoticeFlagStorage | undefined;
32
+ applyUrl: string;
33
+ }
34
+ /** 槽位提供的标准份额里,这里只用得到的这一个(由 ui-session 合并进所有槽位)。 */
35
+ type UseSessionsShare = (selector: (state: FreeKeyNoticeSessionState) => boolean) => boolean;
36
+ /**
37
+ * 提示本体。
38
+ * @param props - 依赖注入,以及宿主通过槽位交给每个条目的标准份额。
39
+ * @returns 左下角卡片,或 null。
40
+ */
41
+ export declare function FreeKeyNotice({ deps, useSessions }: {
42
+ deps: FreeKeyNoticeDeps;
43
+ useSessions: UseSessionsShare;
44
+ }): ReactNode;
45
+ /**
46
+ * 浏览器侧装配:把凭据通道、真实 localStorage 接到组件上。
47
+ * `useSessions` 是宿主交给 `shell.overlay` 条目的标准份额,从 props 透传而不是自造。
48
+ * @param ctx - 卡片那套上下文(slots + 凭据失效事件)。
49
+ * @returns 槽位组件工厂。
50
+ */
51
+ export declare function createFreeKeyNotice(ctx: ApiKeyCardContext): (props: {
52
+ useSessions: UseSessionsShare;
53
+ }) => ReactNode;