@dsh-ft/dsh-fft-settings 0.0.0-stage → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +395 -2
- package/cordis.patch.yml +18 -0
- package/docs/compatibility-dsh-0.1.6-alpha.1.md +58 -0
- package/docs/extension-service-design.md +351 -0
- package/docs/extension-service-sequences.html +545 -0
- package/docs/ftshare-settings-er.html +139 -0
- package/docs/ftshare-settings-er.svg +113 -0
- package/docs/ftshare-settings-results-er.html +148 -0
- package/docs/ftshare-settings-results-er.svg +122 -0
- package/docs/kline-integration.md +258 -0
- package/lib/agent.d.ts +24 -0
- package/lib/agent.js +26 -0
- package/lib/brand.d.ts +53 -0
- package/lib/brand.js +50 -0
- package/lib/client.d.ts +8 -0
- package/lib/client.js +806 -0
- package/lib/client.js.map +1 -0
- package/lib/contract.d.ts +14 -0
- package/lib/contract.js +14 -0
- package/lib/core/client-transport.d.ts +15 -0
- package/lib/core/client-transport.js +31 -0
- package/lib/core/contract.d.ts +62 -0
- package/lib/core/contract.js +28 -0
- package/lib/core/module-registry.d.ts +54 -0
- package/lib/core/module-registry.js +68 -0
- package/lib/core/rpc-core.d.ts +69 -0
- package/lib/core/rpc-core.js +88 -0
- package/lib/core/shell.d.ts +59 -0
- package/lib/core/shell.js +28 -0
- package/lib/core/styles.d.ts +31 -0
- package/lib/core/styles.js +43 -0
- package/lib/core/transport.d.ts +19 -0
- package/lib/core/transport.js +80 -0
- package/lib/default-preset.d.ts +52 -0
- package/lib/default-preset.js +121 -0
- package/lib/features/api-key/card.d.ts +24 -0
- package/lib/features/api-key/card.js +39 -0
- package/lib/features/api-key/client-api.d.ts +4 -0
- package/lib/features/api-key/client-api.js +11 -0
- package/lib/features/api-key/contract.d.ts +69 -0
- package/lib/features/api-key/contract.js +24 -0
- package/lib/features/api-key/host.d.ts +17 -0
- package/lib/features/api-key/host.js +32 -0
- package/lib/features/api-key/index.d.ts +13 -0
- package/lib/features/api-key/index.js +12 -0
- package/lib/features/api-key/logging.d.ts +15 -0
- package/lib/features/api-key/logging.js +1 -0
- package/lib/features/api-key/model.d.ts +24 -0
- package/lib/features/api-key/model.js +105 -0
- package/lib/features/api-key/probe.d.ts +17 -0
- package/lib/features/api-key/probe.js +148 -0
- package/lib/features/api-key/rpc.d.ts +4 -0
- package/lib/features/api-key/rpc.js +17 -0
- package/lib/features/api-key/service.d.ts +21 -0
- package/lib/features/api-key/service.js +181 -0
- package/lib/features/api-key/styles.d.ts +9 -0
- package/lib/features/api-key/styles.js +24 -0
- package/lib/features/logs/archive.d.ts +87 -0
- package/lib/features/logs/archive.js +156 -0
- package/lib/features/logs/card.d.ts +22 -0
- package/lib/features/logs/card.js +107 -0
- package/lib/features/logs/client-api.d.ts +4 -0
- package/lib/features/logs/client-api.js +11 -0
- package/lib/features/logs/collector.d.ts +91 -0
- package/lib/features/logs/collector.js +258 -0
- package/lib/features/logs/config.d.ts +70 -0
- package/lib/features/logs/config.js +97 -0
- package/lib/features/logs/contract.d.ts +130 -0
- package/lib/features/logs/contract.js +67 -0
- package/lib/features/logs/export.d.ts +92 -0
- package/lib/features/logs/export.js +212 -0
- package/lib/features/logs/format.d.ts +42 -0
- package/lib/features/logs/format.js +172 -0
- package/lib/features/logs/home.d.ts +18 -0
- package/lib/features/logs/home.js +39 -0
- package/lib/features/logs/host.d.ts +19 -0
- package/lib/features/logs/host.js +139 -0
- package/lib/features/logs/index.d.ts +12 -0
- package/lib/features/logs/index.js +12 -0
- package/lib/features/logs/rpc.d.ts +6 -0
- package/lib/features/logs/rpc.js +27 -0
- package/lib/features/logs/styles.d.ts +9 -0
- package/lib/features/logs/styles.js +13 -0
- package/lib/guidance.d.ts +41 -0
- package/lib/guidance.js +88 -0
- package/lib/index.d.ts +67 -0
- package/lib/index.js +50 -0
- package/lib/rpc.d.ts +10 -0
- package/lib/rpc.js +10 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +108 -4
- package/src/agent.ts +31 -0
- package/src/brand.ts +79 -0
- package/src/client.tsx +21 -0
- package/src/contract.ts +14 -0
- package/src/core/client-transport.ts +37 -0
- package/src/core/contract.ts +57 -0
- package/src/core/module-registry.ts +102 -0
- package/src/core/rpc-core.ts +107 -0
- package/src/core/shell.tsx +81 -0
- package/src/core/styles.ts +43 -0
- package/src/core/transport.ts +82 -0
- package/src/default-preset.ts +151 -0
- package/src/features/api-key/card.tsx +77 -0
- package/src/features/api-key/client-api.ts +12 -0
- package/src/features/api-key/contract.ts +63 -0
- package/src/features/api-key/host.ts +36 -0
- package/src/features/api-key/index.ts +13 -0
- package/src/features/api-key/logging.ts +15 -0
- package/src/features/api-key/model.ts +63 -0
- package/src/features/api-key/probe.ts +125 -0
- package/src/features/api-key/rpc.ts +18 -0
- package/src/features/api-key/service.ts +157 -0
- package/src/features/api-key/styles.ts +24 -0
- package/src/features/logs/archive.ts +230 -0
- package/src/features/logs/card.tsx +169 -0
- package/src/features/logs/client-api.ts +20 -0
- package/src/features/logs/collector.ts +313 -0
- package/src/features/logs/config.ts +115 -0
- package/src/features/logs/contract.ts +149 -0
- package/src/features/logs/export.ts +274 -0
- package/src/features/logs/format.ts +188 -0
- package/src/features/logs/home.ts +43 -0
- package/src/features/logs/host.ts +153 -0
- package/src/features/logs/index.ts +12 -0
- package/src/features/logs/rpc.ts +29 -0
- package/src/features/logs/styles.ts +13 -0
- package/src/guidance.ts +114 -0
- package/src/index.ts +59 -0
- package/src/rpc.ts +10 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { PropsRenderSlots, PropsRuntime, SlotCore, SlotMap } from '@deepseek-ai/dsh-client-ui-slots';
|
|
3
|
+
import { FTSHARE_SETTINGS_CARD_SLOT } from './contract.js';
|
|
4
|
+
/** Section slot this seam contributes into; declared by the settings shell. */
|
|
5
|
+
declare const FTSHARE_SETTINGS_SECTION_NAME: "settings.section";
|
|
6
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
7
|
+
interface SlotMap {
|
|
8
|
+
/** One card inside the FTShare settings section; `order` sets its position. */
|
|
9
|
+
'ftshare.settings.card': {
|
|
10
|
+
kind: 'list';
|
|
11
|
+
scope: 'root';
|
|
12
|
+
owner: FtshareSettingsCardOwnerProps;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Owner share of a card: the section supplies no card-specific props. */
|
|
17
|
+
export interface FtshareSettingsCardOwnerProps {
|
|
18
|
+
/** Marker field: card owner props are intentionally empty. */
|
|
19
|
+
children?: never;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The slice of the renderer-owned slots service this seam uses.
|
|
23
|
+
*
|
|
24
|
+
* `register` IS the ui-slots registration signature (the runtime registry
|
|
25
|
+
* reuses it verbatim, adding disposal through the caller's fiber); `inject`
|
|
26
|
+
* is the runtime wrapper's declaration-lifetime hook, which is why it is
|
|
27
|
+
* mirrored structurally here.
|
|
28
|
+
*/
|
|
29
|
+
export interface FtshareSettingsSlotsService {
|
|
30
|
+
readonly register: SlotCore['register'];
|
|
31
|
+
/** Run `callback` for each declaration lifetime of `key`. */
|
|
32
|
+
inject(key: keyof SlotMap & string, callback: () => () => void): () => void;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Browser context the section shell requires: the renderer's slots service.
|
|
36
|
+
* A card that needs more (its own services, invalidation events) declares the
|
|
37
|
+
* extra members on its own context type.
|
|
38
|
+
*/
|
|
39
|
+
export interface FtshareSettingsShellContext {
|
|
40
|
+
slots: FtshareSettingsSlotsService;
|
|
41
|
+
}
|
|
42
|
+
/** Composed props of the section entry: the shell's owner share plus its declared child render. */
|
|
43
|
+
export type FtshareSettingsSectionProps = PropsRuntime<typeof FTSHARE_SETTINGS_SECTION_NAME> & PropsRenderSlots<typeof FTSHARE_SETTINGS_CARD_SLOT>;
|
|
44
|
+
/**
|
|
45
|
+
* The settings section shell: core CSS variables, the section chrome, and the
|
|
46
|
+
* single render site every card appears at. Cards own everything inside it.
|
|
47
|
+
*/
|
|
48
|
+
export declare function FtshareSettingsSection({ renderSlot }: FtshareSettingsSectionProps): ReactNode;
|
|
49
|
+
/**
|
|
50
|
+
* Declare the card slot and contribute the section that renders it.
|
|
51
|
+
*
|
|
52
|
+
* Declaring the child slot is what authorizes the render site; a card
|
|
53
|
+
* registering through {@link FTSHARE_SETTINGS_CARD_SLOT} before this runs waits
|
|
54
|
+
* for the declaration instead of failing.
|
|
55
|
+
* @param ctx - client context carrying the renderer's slots service.
|
|
56
|
+
* @returns idempotent disposer for the section and its declaration.
|
|
57
|
+
*/
|
|
58
|
+
export declare function registerSettingsSection(ctx: FtshareSettingsShellContext): () => void;
|
|
59
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { FTSHARE_SETTINGS_CARD_SLOT, FTSHARE_SETTINGS_SECTION } from './contract.js';
|
|
3
|
+
import { SETTINGS_CSS } from './styles.js';
|
|
4
|
+
/** Section slot this seam contributes into; declared by the settings shell. */
|
|
5
|
+
const FTSHARE_SETTINGS_SECTION_NAME = 'settings.section';
|
|
6
|
+
/**
|
|
7
|
+
* The settings section shell: core CSS variables, the section chrome, and the
|
|
8
|
+
* single render site every card appears at. Cards own everything inside it.
|
|
9
|
+
*/
|
|
10
|
+
export function FtshareSettingsSection({ renderSlot }) {
|
|
11
|
+
return _jsxs(_Fragment, { children: [_jsx("style", { children: SETTINGS_CSS }), renderSlot(FTSHARE_SETTINGS_CARD_SLOT, {})] });
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Declare the card slot and contribute the section that renders it.
|
|
15
|
+
*
|
|
16
|
+
* Declaring the child slot is what authorizes the render site; a card
|
|
17
|
+
* registering through {@link FTSHARE_SETTINGS_CARD_SLOT} before this runs waits
|
|
18
|
+
* for the declaration instead of failing.
|
|
19
|
+
* @param ctx - client context carrying the renderer's slots service.
|
|
20
|
+
* @returns idempotent disposer for the section and its declaration.
|
|
21
|
+
*/
|
|
22
|
+
export function registerSettingsSection(ctx) {
|
|
23
|
+
return ctx.slots.inject(FTSHARE_SETTINGS_SECTION_NAME, () => ctx.slots.register({
|
|
24
|
+
name: FTSHARE_SETTINGS_SECTION_NAME,
|
|
25
|
+
...FTSHARE_SETTINGS_SECTION,
|
|
26
|
+
children: { [FTSHARE_SETTINGS_CARD_SLOT]: { kind: 'list', scope: 'root' } },
|
|
27
|
+
}, FtshareSettingsSection));
|
|
28
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The section shell's styles plus the theme tokens every card reads.
|
|
3
|
+
*
|
|
4
|
+
* The shell owns the container, typography, focus ring and link rules that
|
|
5
|
+
* every card sits inside; a card owns its own CSS and renders it from its own
|
|
6
|
+
* component, and core never styles card internals.
|
|
7
|
+
*
|
|
8
|
+
* The theme layer declares the plugin's `--ftshare-*` tokens once, on
|
|
9
|
+
* `.ftshare-settings` — the root element every card renders. Surfaces and text
|
|
10
|
+
* take their value from the host palette: `--dsw-alias-*` is what DSH
|
|
11
|
+
* publishes for the light, the dark and every registered theme, so one
|
|
12
|
+
* stylesheet follows the live palette with no script, no `prefers-color-scheme`
|
|
13
|
+
* copy and no second palette to keep in sync. The literal inside each `var()`
|
|
14
|
+
* is the fallback for a host that publishes no palette.
|
|
15
|
+
*
|
|
16
|
+
* The declarations sit on the card root rather than on `:root` because a
|
|
17
|
+
* custom property resolves where it is *declared*: `--dsw-alias-*` exists from
|
|
18
|
+
* `body` downwards, so a value derived from it above `body` would resolve to
|
|
19
|
+
* nothing and fall back. Consequence: the place to pin a look is the host
|
|
20
|
+
* palette itself (`ctx.theme.register` / `ctx.theme.overrideTokens`), not these
|
|
21
|
+
* names.
|
|
22
|
+
*
|
|
23
|
+
* `--ftshare-accent*` is the plugin's own brand and has no host equivalent. The
|
|
24
|
+
* values come from the FT brand layer's `--ft-*` tokens (`src/brand.ts`), with
|
|
25
|
+
* the same literal as the fallback so the card keeps its colors when that layer
|
|
26
|
+
* is absent. The fill and its ink are scheme-invariant;
|
|
27
|
+
* `--ftshare-accent-on-surface` mixes the same yellow towards the host's text
|
|
28
|
+
* colour, so the brand stays legible as the 11px eyebrow and as the focus ring
|
|
29
|
+
* on either surface.
|
|
30
|
+
*/
|
|
31
|
+
export declare 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";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The section shell's styles plus the theme tokens every card reads.
|
|
3
|
+
*
|
|
4
|
+
* The shell owns the container, typography, focus ring and link rules that
|
|
5
|
+
* every card sits inside; a card owns its own CSS and renders it from its own
|
|
6
|
+
* component, and core never styles card internals.
|
|
7
|
+
*
|
|
8
|
+
* The theme layer declares the plugin's `--ftshare-*` tokens once, on
|
|
9
|
+
* `.ftshare-settings` — the root element every card renders. Surfaces and text
|
|
10
|
+
* take their value from the host palette: `--dsw-alias-*` is what DSH
|
|
11
|
+
* publishes for the light, the dark and every registered theme, so one
|
|
12
|
+
* stylesheet follows the live palette with no script, no `prefers-color-scheme`
|
|
13
|
+
* copy and no second palette to keep in sync. The literal inside each `var()`
|
|
14
|
+
* is the fallback for a host that publishes no palette.
|
|
15
|
+
*
|
|
16
|
+
* The declarations sit on the card root rather than on `:root` because a
|
|
17
|
+
* custom property resolves where it is *declared*: `--dsw-alias-*` exists from
|
|
18
|
+
* `body` downwards, so a value derived from it above `body` would resolve to
|
|
19
|
+
* nothing and fall back. Consequence: the place to pin a look is the host
|
|
20
|
+
* palette itself (`ctx.theme.register` / `ctx.theme.overrideTokens`), not these
|
|
21
|
+
* names.
|
|
22
|
+
*
|
|
23
|
+
* `--ftshare-accent*` is the plugin's own brand and has no host equivalent. The
|
|
24
|
+
* values come from the FT brand layer's `--ft-*` tokens (`src/brand.ts`), with
|
|
25
|
+
* the same literal as the fallback so the card keeps its colors when that layer
|
|
26
|
+
* is absent. The fill and its ink are scheme-invariant;
|
|
27
|
+
* `--ftshare-accent-on-surface` mixes the same yellow towards the host's text
|
|
28
|
+
* colour, so the brand stays legible as the 11px eyebrow and as the focus ring
|
|
29
|
+
* on either surface.
|
|
30
|
+
*/
|
|
31
|
+
export const SETTINGS_CSS = `
|
|
32
|
+
.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)); }
|
|
33
|
+
.ftshare-settings { color:var(--ftshare-text); max-width:760px; font-size:14px; line-height:1.6; }
|
|
34
|
+
.ftshare-settings * { box-sizing:border-box; }
|
|
35
|
+
.ftshare-settings h2 { font-size:26px; margin:4px 0 8px; }
|
|
36
|
+
.ftshare-settings h3 { font-size:15px; margin:0 0 12px; }
|
|
37
|
+
.ftshare-settings p { margin:8px 0; color:var(--ftshare-muted); }
|
|
38
|
+
.ftshare-settings header { display:flex; justify-content:space-between; align-items:flex-start; gap:20px; margin-bottom:20px; }
|
|
39
|
+
.ftshare-settings small { display:block; color:var(--ftshare-muted); font-size:12px; margin:5px 0 12px; }
|
|
40
|
+
.ftshare-settings :is(button,input,a):focus-visible { outline:2px solid var(--ftshare-accent-on-surface); outline-offset:3px; }
|
|
41
|
+
.ftshare-settings a { color:var(--ftshare-link); }
|
|
42
|
+
@media(max-width:540px) { .ftshare-settings header { flex-direction:column; gap:10px; } }
|
|
43
|
+
`;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host half of the settings transport.
|
|
3
|
+
*
|
|
4
|
+
* One exact route on the shared `/api` carrier: the carrier owns the Host/Origin
|
|
5
|
+
* fence and browser authentication, so this layer only decodes the call, bounds
|
|
6
|
+
* the body and returns the result envelope. Registering through
|
|
7
|
+
* `connection.rpc.handle` instead would read `webServer` through the context
|
|
8
|
+
* property proxy, whose resolution is ancestor-only and therefore fails for a
|
|
9
|
+
* plugin mounted beside the Web server.
|
|
10
|
+
*/
|
|
11
|
+
import type { RpcHandler, SettingsRoute } from './rpc-core.js';
|
|
12
|
+
/**
|
|
13
|
+
* Build the exact Fetch route served by the connection carrier.
|
|
14
|
+
* @param options - the settings call handler.
|
|
15
|
+
* @returns route definition ready for `connection.fetch.register`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function createSettingsRoute({ handler }: {
|
|
18
|
+
readonly handler: RpcHandler;
|
|
19
|
+
}): SettingsRoute;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host half of the settings transport.
|
|
3
|
+
*
|
|
4
|
+
* One exact route on the shared `/api` carrier: the carrier owns the Host/Origin
|
|
5
|
+
* fence and browser authentication, so this layer only decodes the call, bounds
|
|
6
|
+
* the body and returns the result envelope. Registering through
|
|
7
|
+
* `connection.rpc.handle` instead would read `webServer` through the context
|
|
8
|
+
* property proxy, whose resolution is ancestor-only and therefore fails for a
|
|
9
|
+
* plugin mounted beside the Web server.
|
|
10
|
+
*/
|
|
11
|
+
import { FTSHARE_SETTINGS_MAX_BODY_BYTES, FTSHARE_SETTINGS_ROUTE } from './contract.js';
|
|
12
|
+
const invalidRequest = () => ({
|
|
13
|
+
ok: false,
|
|
14
|
+
error: { code: 'bad_request', message: '请求参数不正确', details: {} },
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* Build the exact Fetch route served by the connection carrier.
|
|
18
|
+
* @param options - the settings call handler.
|
|
19
|
+
* @returns route definition ready for `connection.fetch.register`.
|
|
20
|
+
*/
|
|
21
|
+
export function createSettingsRoute({ handler }) {
|
|
22
|
+
return {
|
|
23
|
+
path: FTSHARE_SETTINGS_ROUTE,
|
|
24
|
+
methods: ['POST'],
|
|
25
|
+
requestBody: 'buffered',
|
|
26
|
+
fetch: async (request) => {
|
|
27
|
+
const body = await readJsonBody(request);
|
|
28
|
+
if (body === null || typeof body !== 'object' || Array.isArray(body)) {
|
|
29
|
+
return json(400, invalidRequest());
|
|
30
|
+
}
|
|
31
|
+
const call = body;
|
|
32
|
+
const endpoint = typeof call.endpoint === 'string' ? call.endpoint : '';
|
|
33
|
+
return json(200, await handler(endpoint, call.payload, request.signal));
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** Read and parse the JSON body; oversized, malformed or interrupted reads return undefined. */
|
|
38
|
+
async function readJsonBody(request) {
|
|
39
|
+
const reader = request.body?.getReader();
|
|
40
|
+
if (reader === undefined)
|
|
41
|
+
return undefined;
|
|
42
|
+
const chunks = [];
|
|
43
|
+
let size = 0;
|
|
44
|
+
try {
|
|
45
|
+
for (;;) {
|
|
46
|
+
const { done, value } = await reader.read();
|
|
47
|
+
if (done)
|
|
48
|
+
break;
|
|
49
|
+
size += value.byteLength;
|
|
50
|
+
if (size > FTSHARE_SETTINGS_MAX_BODY_BYTES) {
|
|
51
|
+
await reader.cancel();
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
chunks.push(value);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// A client-side abort only means this request lost its body; nothing else to read.
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
const bytes = new Uint8Array(size);
|
|
62
|
+
let offset = 0;
|
|
63
|
+
for (const chunk of chunks) {
|
|
64
|
+
bytes.set(chunk, offset);
|
|
65
|
+
offset += chunk.byteLength;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// A non-JSON body has the same single outcome: invalid request parameters.
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function json(status, payload) {
|
|
76
|
+
return new Response(JSON.stringify(payload), {
|
|
77
|
+
status,
|
|
78
|
+
headers: { 'content-type': 'application/json' },
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claim the finance preset as the default once per installation.
|
|
3
|
+
*
|
|
4
|
+
* The bundle patch already carries `default: ft-finance` on the preset
|
|
5
|
+
* registry row, which decides every session on a machine that never chose a
|
|
6
|
+
* default. A machine whose user *did* choose one keeps that choice, because
|
|
7
|
+
* `selectedDefault` outranks `default` — this module overrides it exactly once,
|
|
8
|
+
* then records the claim so a later user choice is never touched again.
|
|
9
|
+
*
|
|
10
|
+
* The write goes through the settings service, the same path the settings page
|
|
11
|
+
* uses: only volatile fields are writable there, so the patch this module hands
|
|
12
|
+
* over carries `selectedDefault` and nothing else. The row it lands in is wider
|
|
13
|
+
* than that patch — the service writes the effective config's non-volatile keys
|
|
14
|
+
* alongside the volatile change, so `default: ft-finance` is copied into the
|
|
15
|
+
* profile row too. Because it is still the one change this plugin ever makes to
|
|
16
|
+
* the user's own profile patch, the file is copied into this plugin's data
|
|
17
|
+
* directory first, and a claim without that copy never happens.
|
|
18
|
+
* @module @dsh-ft/dsh-fft-settings/default-preset
|
|
19
|
+
*/
|
|
20
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
21
|
+
/** The preset this bundle wants as the default. */
|
|
22
|
+
export declare const DEFAULT_PRESET_ID = "ft-finance";
|
|
23
|
+
/** Id of the profile entry that owns the preset registry's config. */
|
|
24
|
+
export declare const REGISTRY_ENTRY_ID = "agent-preset-registry";
|
|
25
|
+
/** Data directory this plugin owns under the DSH home. */
|
|
26
|
+
export declare const DATA_DIR_NAME = ".dsh-fft-settings";
|
|
27
|
+
/** File whose presence means the claim already happened on this machine. */
|
|
28
|
+
export declare const CLAIM_FILE_NAME = "default-preset.json";
|
|
29
|
+
/** Snapshot of the profile patch, taken before this module's single write. */
|
|
30
|
+
export declare const BACKUP_FILE_NAME = "cordis.patch.yml.bak";
|
|
31
|
+
/**
|
|
32
|
+
* Record that this installation claimed the default, once.
|
|
33
|
+
* @param home - the DSH home the plugin writes its own data into.
|
|
34
|
+
* @returns the claim file path.
|
|
35
|
+
*/
|
|
36
|
+
export declare function claimFilePath(home?: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Snapshot path for the profile patch.
|
|
39
|
+
* @param home - the DSH home the plugin writes its own data into.
|
|
40
|
+
* @returns the snapshot file path.
|
|
41
|
+
*/
|
|
42
|
+
export declare function backupFilePath(home?: string): string;
|
|
43
|
+
/**
|
|
44
|
+
* Make `ft-finance` the default unless this machine already made a claim.
|
|
45
|
+
*
|
|
46
|
+
* Every step that cannot complete skips silently and retries on the next start:
|
|
47
|
+
* a missing registry or settings service, a preset that is not registered yet,
|
|
48
|
+
* and a failed write all leave no claim file behind. Nothing here may fail the
|
|
49
|
+
* plugin's activation.
|
|
50
|
+
* @param ctx - the plugin's host context.
|
|
51
|
+
*/
|
|
52
|
+
export declare function installDefaultPresetOnce(ctx: Context): void;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claim the finance preset as the default once per installation.
|
|
3
|
+
*
|
|
4
|
+
* The bundle patch already carries `default: ft-finance` on the preset
|
|
5
|
+
* registry row, which decides every session on a machine that never chose a
|
|
6
|
+
* default. A machine whose user *did* choose one keeps that choice, because
|
|
7
|
+
* `selectedDefault` outranks `default` — this module overrides it exactly once,
|
|
8
|
+
* then records the claim so a later user choice is never touched again.
|
|
9
|
+
*
|
|
10
|
+
* The write goes through the settings service, the same path the settings page
|
|
11
|
+
* uses: only volatile fields are writable there, so the patch this module hands
|
|
12
|
+
* over carries `selectedDefault` and nothing else. The row it lands in is wider
|
|
13
|
+
* than that patch — the service writes the effective config's non-volatile keys
|
|
14
|
+
* alongside the volatile change, so `default: ft-finance` is copied into the
|
|
15
|
+
* profile row too. Because it is still the one change this plugin ever makes to
|
|
16
|
+
* the user's own profile patch, the file is copied into this plugin's data
|
|
17
|
+
* directory first, and a claim without that copy never happens.
|
|
18
|
+
* @module @dsh-ft/dsh-fft-settings/default-preset
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync } from 'node:fs';
|
|
21
|
+
import { copyFile, mkdir, writeFile } from 'node:fs/promises';
|
|
22
|
+
import { dirname, join } from 'node:path';
|
|
23
|
+
import { resolveDshHome } from './features/logs/home.js';
|
|
24
|
+
/** The preset this bundle wants as the default. */
|
|
25
|
+
export const DEFAULT_PRESET_ID = 'ft-finance';
|
|
26
|
+
/** Id of the profile entry that owns the preset registry's config. */
|
|
27
|
+
export const REGISTRY_ENTRY_ID = 'agent-preset-registry';
|
|
28
|
+
/** Data directory this plugin owns under the DSH home. */
|
|
29
|
+
export const DATA_DIR_NAME = '.dsh-fft-settings';
|
|
30
|
+
/** File whose presence means the claim already happened on this machine. */
|
|
31
|
+
export const CLAIM_FILE_NAME = 'default-preset.json';
|
|
32
|
+
/** Snapshot of the profile patch, taken before this module's single write. */
|
|
33
|
+
export const BACKUP_FILE_NAME = 'cordis.patch.yml.bak';
|
|
34
|
+
/**
|
|
35
|
+
* Record that this installation claimed the default, once.
|
|
36
|
+
* @param home - the DSH home the plugin writes its own data into.
|
|
37
|
+
* @returns the claim file path.
|
|
38
|
+
*/
|
|
39
|
+
export function claimFilePath(home = resolveDshHome()) {
|
|
40
|
+
return join(home, DATA_DIR_NAME, CLAIM_FILE_NAME);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Snapshot path for the profile patch.
|
|
44
|
+
* @param home - the DSH home the plugin writes its own data into.
|
|
45
|
+
* @returns the snapshot file path.
|
|
46
|
+
*/
|
|
47
|
+
export function backupFilePath(home = resolveDshHome()) {
|
|
48
|
+
return join(home, DATA_DIR_NAME, BACKUP_FILE_NAME);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Make `ft-finance` the default unless this machine already made a claim.
|
|
52
|
+
*
|
|
53
|
+
* Every step that cannot complete skips silently and retries on the next start:
|
|
54
|
+
* a missing registry or settings service, a preset that is not registered yet,
|
|
55
|
+
* and a failed write all leave no claim file behind. Nothing here may fail the
|
|
56
|
+
* plugin's activation.
|
|
57
|
+
* @param ctx - the plugin's host context.
|
|
58
|
+
*/
|
|
59
|
+
export function installDefaultPresetOnce(ctx) {
|
|
60
|
+
ctx.inject(['settings', 'agentPresets'], (scope) => {
|
|
61
|
+
const registry = scope.get('agentPresets');
|
|
62
|
+
const settings = scope.get('settings');
|
|
63
|
+
if (registry === undefined || settings === undefined)
|
|
64
|
+
return;
|
|
65
|
+
const profile = scope.get('profileContext');
|
|
66
|
+
void claimDefaultPreset(registry, settings, profile, ctx)
|
|
67
|
+
.catch((error) => {
|
|
68
|
+
ctx.logger.warn('dsh-fft-settings: could not claim the default preset', error);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Perform the one-time claim, snapshotting the file it is about to change.
|
|
74
|
+
* @param registry - the live preset registry.
|
|
75
|
+
* @param settings - the settings writer.
|
|
76
|
+
* @param profile - the profile context, absent in a context that has no patch file.
|
|
77
|
+
* @param ctx - the plugin context, used for the claim log line.
|
|
78
|
+
*/
|
|
79
|
+
async function claimDefaultPreset(registry, settings, profile, ctx) {
|
|
80
|
+
const file = claimFilePath();
|
|
81
|
+
if (existsSync(file))
|
|
82
|
+
return;
|
|
83
|
+
// An unregistered preset would pin an id that resolves to nothing, so the
|
|
84
|
+
// claim waits for the composition that defines it.
|
|
85
|
+
const presets = await registry.list();
|
|
86
|
+
if (!presets.some(preset => preset.id === DEFAULT_PRESET_ID))
|
|
87
|
+
return;
|
|
88
|
+
let backup;
|
|
89
|
+
if (registry.defaultId !== DEFAULT_PRESET_ID) {
|
|
90
|
+
// Insurance first: the editor writes atomically and rolls back a failed
|
|
91
|
+
// reconcile, but this write still lands in the user's own profile patch, so
|
|
92
|
+
// a claim is only recorded once the snapshot exists.
|
|
93
|
+
if (profile !== undefined)
|
|
94
|
+
backup = await snapshotProfilePatch(profile.patchPath);
|
|
95
|
+
await settings.update(REGISTRY_ENTRY_ID, { selectedDefault: DEFAULT_PRESET_ID });
|
|
96
|
+
ctx.logger.info(`dsh-fft-settings: ${DEFAULT_PRESET_ID} is now the default preset`);
|
|
97
|
+
}
|
|
98
|
+
await mkdir(dirname(file), { recursive: true });
|
|
99
|
+
const claim = { preset: DEFAULT_PRESET_ID, at: new Date().toISOString(), ...(backup === undefined ? {} : { backup }) };
|
|
100
|
+
await writeFile(file, `${JSON.stringify(claim, null, 2)}\n`);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Copy the profile patch into this plugin's data directory, once.
|
|
104
|
+
*
|
|
105
|
+
* A failed copy throws, and the caller aborts the claim: the write must not
|
|
106
|
+
* happen without its snapshot. The first copy wins, so a retry after a failed
|
|
107
|
+
* write still holds the state from before the very first attempt. A missing
|
|
108
|
+
* patch file — a profile that never wrote anything — needs no copy.
|
|
109
|
+
* @param patchPath - the profile's own `cordis.patch.yml`.
|
|
110
|
+
* @returns the snapshot path, or `undefined` when there was nothing to copy.
|
|
111
|
+
*/
|
|
112
|
+
async function snapshotProfilePatch(patchPath) {
|
|
113
|
+
if (!existsSync(patchPath))
|
|
114
|
+
return undefined;
|
|
115
|
+
const snapshot = backupFilePath();
|
|
116
|
+
if (existsSync(snapshot))
|
|
117
|
+
return snapshot;
|
|
118
|
+
await mkdir(dirname(snapshot), { recursive: true });
|
|
119
|
+
await copyFile(patchPath, snapshot);
|
|
120
|
+
return snapshot;
|
|
121
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type SubscribeChanges, type ApiKeyPublicApi } from './contract.js';
|
|
3
|
+
import type { FtshareSettingsShellContext } from '../../core/shell.js';
|
|
4
|
+
/**
|
|
5
|
+
* Browser context this card requires: the shell's slots service plus the
|
|
6
|
+
* invalidations that make a cached credential status stale.
|
|
7
|
+
*/
|
|
8
|
+
export interface ApiKeyCardContext extends FtshareSettingsShellContext {
|
|
9
|
+
remote: {
|
|
10
|
+
$on(event: string, listener: (ref: string) => void): () => void;
|
|
11
|
+
};
|
|
12
|
+
on(event: 'connection/reset', listener: () => void): () => void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Contribute the built-in API Key card through the same slot a third-party
|
|
16
|
+
* card uses. Nothing about it is privileged: same declaration, same options.
|
|
17
|
+
* @param ctx - client context carrying the renderer's slots service.
|
|
18
|
+
* @returns idempotent disposer for the card registration.
|
|
19
|
+
*/
|
|
20
|
+
export declare function registerApiKeyCard(ctx: ApiKeyCardContext): () => void;
|
|
21
|
+
export declare function ApiKeyCard({ api, subscribeChanges }: {
|
|
22
|
+
readonly api: ApiKeyPublicApi;
|
|
23
|
+
readonly subscribeChanges?: SubscribeChanges;
|
|
24
|
+
}): ReactNode;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useState, useSyncExternalStore } from 'react';
|
|
3
|
+
import { FTSHARE_API_KEY_REF, FTSHARE_CREDENTIAL_EVENTS } from './contract.js';
|
|
4
|
+
import { FTSHARE_SETTINGS_CARD_SLOT } from '../../core/contract.js';
|
|
5
|
+
import { FtshareSettingsModel } from './model.js';
|
|
6
|
+
import { createApiKeyClient } from './client-api.js';
|
|
7
|
+
import { createSettingsTransport } from '../../core/client-transport.js';
|
|
8
|
+
import { API_KEY_CARD_CSS } from './styles.js';
|
|
9
|
+
/**
|
|
10
|
+
* Contribute the built-in API Key card through the same slot a third-party
|
|
11
|
+
* card uses. Nothing about it is privileged: same declaration, same options.
|
|
12
|
+
* @param ctx - client context carrying the renderer's slots service.
|
|
13
|
+
* @returns idempotent disposer for the card registration.
|
|
14
|
+
*/
|
|
15
|
+
export function registerApiKeyCard(ctx) {
|
|
16
|
+
return ctx.slots.inject(FTSHARE_SETTINGS_CARD_SLOT, () => ctx.slots.register({ name: FTSHARE_SETTINGS_CARD_SLOT, id: 'apiKey', order: 10 }, createApiKeyCard(ctx)));
|
|
17
|
+
}
|
|
18
|
+
/** Bind the card's adapters; subscriptions are acquired in the component's effect. */
|
|
19
|
+
function createApiKeyCard(ctx) {
|
|
20
|
+
const api = createApiKeyClient(createSettingsTransport());
|
|
21
|
+
const subscribeChanges = listener => {
|
|
22
|
+
const disposers = FTSHARE_CREDENTIAL_EVENTS.map(event => ctx.remote.$on(event, ref => { if (ref === FTSHARE_API_KEY_REF)
|
|
23
|
+
listener(); }));
|
|
24
|
+
disposers.push(ctx.on('connection/reset', listener));
|
|
25
|
+
return () => { for (const dispose of disposers)
|
|
26
|
+
dispose(); };
|
|
27
|
+
};
|
|
28
|
+
return () => _jsx(ApiKeyCard, { api: api, subscribeChanges: subscribeChanges });
|
|
29
|
+
}
|
|
30
|
+
export function ApiKeyCard({ api, subscribeChanges }) {
|
|
31
|
+
const model = useMemo(() => new FtshareSettingsModel(api), [api]);
|
|
32
|
+
const { status, busy, message, statusError, save } = useSyncExternalStore(model.subscribe, model.getSnapshot, model.getSnapshot);
|
|
33
|
+
const [draft, setDraft] = useState('');
|
|
34
|
+
useEffect(() => model.connect(subscribeChanges), [model, subscribeChanges]);
|
|
35
|
+
const disabled = busy || !status?.writable;
|
|
36
|
+
const configure = async () => { if (await model.configureAndTest(draft))
|
|
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 })] })] });
|
|
39
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ApiKeyPublicApi } from './contract.js';
|
|
2
|
+
import { type ClientTransport } from '../../core/rpc-core.js';
|
|
3
|
+
/** Browser adapter deliberately has no resolve/createEnvironment method. */
|
|
4
|
+
export declare function createApiKeyClient(transport: ClientTransport): ApiKeyPublicApi;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { callSettingsRpc } from '../../core/rpc-core.js';
|
|
2
|
+
/** Browser adapter deliberately has no resolve/createEnvironment method. */
|
|
3
|
+
export function createApiKeyClient(transport) {
|
|
4
|
+
async function call(method, payload = {}) {
|
|
5
|
+
return callSettingsRpc(transport, 'apiKey', method, payload);
|
|
6
|
+
}
|
|
7
|
+
return {
|
|
8
|
+
describe: () => call('describe'), set: value => call('set', { value }), clear: () => call('clear'),
|
|
9
|
+
configureAndTest: value => call('configureAndTest', { value }), testConnection: () => call('testConnection'), getValidationStatus: () => call('getValidationStatus'),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Key namespace contract: the credential reference and lifecycle events,
|
|
3
|
+
* the status/validation vocabulary, and the two faces the namespace exposes
|
|
4
|
+
* (the browser-safe subset and the host-only superset).
|
|
5
|
+
*
|
|
6
|
+
* It merges its own line into the generic {@link FtshareSettingsNamespaces}
|
|
7
|
+
* merge point, so `ctx.ftshareSettings.apiKey` keeps its existing type for
|
|
8
|
+
* every consumer.
|
|
9
|
+
*/
|
|
10
|
+
export declare const FTSHARE_API_KEY_REF: "FTSHARE_API_KEY";
|
|
11
|
+
export declare const FTSHARE_CREDENTIAL_EVENTS: readonly ["credentials/reference-updated"];
|
|
12
|
+
export declare const API_KEY_METHODS: readonly ["describe", "set", "clear", "configureAndTest", "testConnection", "getValidationStatus"];
|
|
13
|
+
export type ApiKeyMethod = typeof API_KEY_METHODS[number];
|
|
14
|
+
export type SubscribeChanges = (listener: () => void) => () => void;
|
|
15
|
+
export interface CredentialStatus {
|
|
16
|
+
configured: boolean;
|
|
17
|
+
writable: boolean;
|
|
18
|
+
source?: string;
|
|
19
|
+
}
|
|
20
|
+
export type ProbeState = 'available' | 'auth_required' | 'auth_failed' | 'permission_denied' | 'quota_exceeded' | 'rate_limited' | 'network_error' | 'timeout' | 'upstream_error' | 'invalid_response' | 'not_run';
|
|
21
|
+
export interface ProbeResult {
|
|
22
|
+
status: ProbeState;
|
|
23
|
+
message: string;
|
|
24
|
+
}
|
|
25
|
+
export interface ValidationResult {
|
|
26
|
+
status: 'not_tested' | 'not_run' | 'passed' | 'partial' | 'failed' | 'superseded';
|
|
27
|
+
remote: ProbeResult;
|
|
28
|
+
apiKey: ProbeResult;
|
|
29
|
+
testedAt?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface ApiKeyStatus extends CredentialStatus {
|
|
32
|
+
/** Display only. Fixed mask plus last four characters; never submit as a Key. */
|
|
33
|
+
maskedValue?: string;
|
|
34
|
+
revision: number;
|
|
35
|
+
validation: ValidationResult;
|
|
36
|
+
}
|
|
37
|
+
export interface ConfigureResult {
|
|
38
|
+
save: {
|
|
39
|
+
status: 'saved' | 'failed';
|
|
40
|
+
code?: 'invalid_input' | 'read_only' | 'save_failed';
|
|
41
|
+
message: string;
|
|
42
|
+
};
|
|
43
|
+
validation: ValidationResult;
|
|
44
|
+
}
|
|
45
|
+
/** Local syntax validation, never an assertion of upstream validity. */
|
|
46
|
+
export declare function normalizeApiKey(input: string): string;
|
|
47
|
+
export declare function untested(status?: 'not_tested' | 'not_run' | 'superseded', message?: string): ValidationResult;
|
|
48
|
+
/** Everything the browser adapter may call; `resolve`/`createEnvironment` are not here. */
|
|
49
|
+
export interface ApiKeyPublicApi {
|
|
50
|
+
describe(): Promise<ApiKeyStatus>;
|
|
51
|
+
set(value: string): Promise<void>;
|
|
52
|
+
clear(): Promise<void>;
|
|
53
|
+
configureAndTest(value: string): Promise<ConfigureResult>;
|
|
54
|
+
testConnection(): Promise<ValidationResult>;
|
|
55
|
+
getValidationStatus(): Promise<ValidationResult>;
|
|
56
|
+
}
|
|
57
|
+
/** The host namespace object: the public API plus the secret-bearing host methods. */
|
|
58
|
+
export interface ApiKeySettings extends ApiKeyPublicApi {
|
|
59
|
+
readonly credentialRef: typeof FTSHARE_API_KEY_REF;
|
|
60
|
+
/** Host only. Re-resolve per operation; never send this value to browsers. */
|
|
61
|
+
resolve(): Promise<string | undefined>;
|
|
62
|
+
createEnvironment(base?: Readonly<Record<string, string | undefined>>): Promise<Record<string, string | undefined>>;
|
|
63
|
+
subscribe: SubscribeChanges;
|
|
64
|
+
}
|
|
65
|
+
declare module '../../core/contract.js' {
|
|
66
|
+
interface FtshareSettingsNamespaces {
|
|
67
|
+
apiKey: ApiKeySettings;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Key namespace contract: the credential reference and lifecycle events,
|
|
3
|
+
* the status/validation vocabulary, and the two faces the namespace exposes
|
|
4
|
+
* (the browser-safe subset and the host-only superset).
|
|
5
|
+
*
|
|
6
|
+
* It merges its own line into the generic {@link FtshareSettingsNamespaces}
|
|
7
|
+
* merge point, so `ctx.ftshareSettings.apiKey` keeps its existing type for
|
|
8
|
+
* every consumer.
|
|
9
|
+
*/
|
|
10
|
+
export const FTSHARE_API_KEY_REF = 'FTSHARE_API_KEY';
|
|
11
|
+
export const FTSHARE_CREDENTIAL_EVENTS = ['credentials/reference-updated'];
|
|
12
|
+
export const API_KEY_METHODS = ['describe', 'set', 'clear', 'configureAndTest', 'testConnection', 'getValidationStatus'];
|
|
13
|
+
/** Local syntax validation, never an assertion of upstream validity. */
|
|
14
|
+
export function normalizeApiKey(input) {
|
|
15
|
+
const value = input.trim();
|
|
16
|
+
if (!value)
|
|
17
|
+
throw new Error('请输入 API Key');
|
|
18
|
+
if (value.length > 512 || /[\x00-\x1f\x7f]/.test(value))
|
|
19
|
+
throw new Error('API Key 长度不能超过 512 个字符,且不能包含控制字符');
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
export function untested(status = 'not_tested', message = '尚未测试') {
|
|
23
|
+
return { status, remote: { status: 'not_run', message }, apiKey: { status: 'not_run', message } };
|
|
24
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type CredentialRef } from '@deepseek-ai/dsh-credentials';
|
|
2
|
+
import { type ApiKeySettings } from './contract.js';
|
|
3
|
+
import type { FeatureLogger } from './logging.js';
|
|
4
|
+
declare module '@deepseek-ai/cordis' {
|
|
5
|
+
interface Events {
|
|
6
|
+
'credentials/reference-updated'(ref: CredentialRef): void;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Build the `apiKey` module, closing over the plugin's named logger.
|
|
11
|
+
*
|
|
12
|
+
* 工厂化与 `createLogsModule` 一致:插件入口在 `apply` 里取一次 `ctx.logger()`
|
|
13
|
+
* (无参 = 插件名)再传进来,因此 feature 不依赖模块级单例,也不认识 cordis。
|
|
14
|
+
* @param logger - 可选具名 logger;省略时本模块不写任何日志(既有调用方不变)。
|
|
15
|
+
* @returns 可交给 `ctx.ftshareSettings.modules.register` 的模块。
|
|
16
|
+
*/
|
|
17
|
+
export declare function createApiKeyModule(logger?: FeatureLogger): import("../../core/module-registry.js").SettingsModule<"apiKey", ApiKeySettings>;
|