@mhfire/dsh-im-bridge 0.1.7 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +16 -3
- package/README.md +16 -3
- package/cordis.patch.yml +4 -0
- package/lib/client.js +789 -153
- package/lib/client.js.map +1 -0
- package/lib/index.js +570 -0
- package/package.json +34 -3
- package/src/client/PluginCard.module.css +153 -0
- package/src/client/PluginCard.tsx +83 -0
- package/src/client/WecomCard.tsx +135 -0
- package/src/client/card-controller.ts +146 -0
- package/src/client/card-form.ts +293 -0
- package/src/client/css-modules.d.ts +4 -0
- package/src/client/fields.module.css +124 -0
- package/src/client/fields.tsx +112 -0
- package/src/client/index.ts +37 -0
- package/src/client/locales.ts +112 -0
- package/src/index.ts +542 -0
- package/src/{wecom.js → wecom.ts} +98 -56
- package/tsconfig.json +14 -0
- package/tsdown.client.ts +109 -0
- package/tsdown.host.ts +36 -0
- package/src/index.js +0 -401
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/* Plugin card chrome: matches the Host Plugins section cards. */
|
|
2
|
+
|
|
3
|
+
.card {
|
|
4
|
+
list-style: none;
|
|
5
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
6
|
+
border-radius: 12px;
|
|
7
|
+
background: var(--dsw-alias-bg-layer-3);
|
|
8
|
+
transition: border-color .16s, background .16s;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.card:hover {
|
|
12
|
+
border-color: var(--dsw-alias-label-dimmed);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.cardOpen {
|
|
16
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
17
|
+
border-color: var(--dsw-alias-label-dimmed);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.header {
|
|
21
|
+
width: 100%;
|
|
22
|
+
appearance: none;
|
|
23
|
+
border: 0;
|
|
24
|
+
background: none;
|
|
25
|
+
font: inherit;
|
|
26
|
+
color: inherit;
|
|
27
|
+
text-align: left;
|
|
28
|
+
cursor: pointer;
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
gap: 12px;
|
|
32
|
+
padding: 14px 16px;
|
|
33
|
+
border-radius: 12px;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.header:focus-visible {
|
|
37
|
+
outline: 2px solid var(--dsw-alias-brand-primary);
|
|
38
|
+
outline-offset: -2px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.headText {
|
|
42
|
+
flex: 1;
|
|
43
|
+
min-width: 0;
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
gap: 4px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.name {
|
|
50
|
+
font-size: 15px;
|
|
51
|
+
font-weight: 600;
|
|
52
|
+
line-height: 1.4;
|
|
53
|
+
color: var(--dsw-alias-label-primary);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.description {
|
|
57
|
+
font-size: 13px;
|
|
58
|
+
line-height: 1.5;
|
|
59
|
+
color: var(--dsw-alias-label-tertiary);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.chevron {
|
|
63
|
+
flex: none;
|
|
64
|
+
color: var(--dsw-alias-label-tertiary);
|
|
65
|
+
transition: transform .16s;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.chevronOpen {
|
|
69
|
+
transform: rotate(180deg);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.body {
|
|
73
|
+
border-top: 1px solid var(--dsw-alias-border-l2);
|
|
74
|
+
margin: 0 16px;
|
|
75
|
+
padding-bottom: 8px;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.readOnly {
|
|
79
|
+
margin: 12px 0 0;
|
|
80
|
+
font-size: 12px;
|
|
81
|
+
line-height: 1.5;
|
|
82
|
+
color: var(--dsw-alias-label-tertiary);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.pending {
|
|
86
|
+
flex: none;
|
|
87
|
+
border-radius: 999px;
|
|
88
|
+
padding: 1px 8px;
|
|
89
|
+
font-size: 11px;
|
|
90
|
+
line-height: 17px;
|
|
91
|
+
font-weight: 500;
|
|
92
|
+
white-space: nowrap;
|
|
93
|
+
background: var(--dsw-alias-bg-module-platform);
|
|
94
|
+
color: var(--dsw-alias-label-secondary);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.footer {
|
|
98
|
+
display: flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
justify-content: flex-end;
|
|
101
|
+
gap: 8px;
|
|
102
|
+
padding: 12px 0 4px;
|
|
103
|
+
border-top: 1px solid var(--dsw-alias-border-l2);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.failed {
|
|
107
|
+
flex: 1;
|
|
108
|
+
min-width: 0;
|
|
109
|
+
margin: 0;
|
|
110
|
+
font-size: 12px;
|
|
111
|
+
line-height: 1.5;
|
|
112
|
+
color: var(--dsw-alias-label-error);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.discard,
|
|
116
|
+
.save {
|
|
117
|
+
appearance: none;
|
|
118
|
+
border: 1px solid transparent;
|
|
119
|
+
border-radius: 8px;
|
|
120
|
+
padding: 5px 14px;
|
|
121
|
+
font: inherit;
|
|
122
|
+
font-size: 13px;
|
|
123
|
+
line-height: 1.5;
|
|
124
|
+
cursor: pointer;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.discard {
|
|
128
|
+
border-color: var(--dsw-alias-border-l2);
|
|
129
|
+
background: none;
|
|
130
|
+
color: var(--dsw-alias-label-secondary);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.discard:hover:not(:disabled) {
|
|
134
|
+
color: var(--dsw-alias-label-primary);
|
|
135
|
+
border-color: var(--dsw-alias-label-dimmed);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.save {
|
|
139
|
+
background: var(--dsw-alias-label-primary);
|
|
140
|
+
color: var(--dsw-alias-bg-layer-3);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.discard:disabled,
|
|
144
|
+
.save:disabled {
|
|
145
|
+
opacity: 0.4;
|
|
146
|
+
cursor: default;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.discard:focus-visible,
|
|
150
|
+
.save:focus-visible {
|
|
151
|
+
outline: 2px solid var(--dsw-alias-brand-primary);
|
|
152
|
+
outline-offset: 1px;
|
|
153
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/** Expandable plugin card chrome matching the Host Plugins section. */
|
|
2
|
+
|
|
3
|
+
import { useState, type ReactElement, type ReactNode } from 'react'
|
|
4
|
+
import { IconChevronDownOutline14 } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
5
|
+
import type { ImBridgeLocaleKey } from './locales.ts'
|
|
6
|
+
import type { CardShell } from './card-form.ts'
|
|
7
|
+
import css from './PluginCard.module.css'
|
|
8
|
+
|
|
9
|
+
/** Card chrome shared by this plugin's settings card. */
|
|
10
|
+
export interface PluginCardProps {
|
|
11
|
+
/** Locale reader for this card's copy. */
|
|
12
|
+
t: (key: ImBridgeLocaleKey) => string
|
|
13
|
+
/** Locale key of the plugin's name. */
|
|
14
|
+
titleKey: ImBridgeLocaleKey
|
|
15
|
+
/** Locale key of the line describing what these settings govern. */
|
|
16
|
+
descriptionKey: ImBridgeLocaleKey
|
|
17
|
+
/** The card's form state. */
|
|
18
|
+
state: CardShell
|
|
19
|
+
/** Write every staged edit. */
|
|
20
|
+
onSave: () => void
|
|
21
|
+
/** Drop every staged edit. */
|
|
22
|
+
onDiscard: () => void
|
|
23
|
+
/** The plugin's controls. */
|
|
24
|
+
children: ReactNode
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Render one plugin card.
|
|
29
|
+
* @param props - locale copy, form state, and controls.
|
|
30
|
+
* @returns the card, or nothing when the namespace is unavailable.
|
|
31
|
+
*/
|
|
32
|
+
export function PluginCard(props: PluginCardProps): ReactElement | null {
|
|
33
|
+
const [open, setOpen] = useState(false)
|
|
34
|
+
const { state } = props
|
|
35
|
+
if (!state.available) return null
|
|
36
|
+
const title = props.t(props.titleKey)
|
|
37
|
+
const blocked = !state.dirty || state.invalid || state.saving
|
|
38
|
+
return (
|
|
39
|
+
<li className={open ? `${css.card} ${css.cardOpen}` : css.card}>
|
|
40
|
+
<button
|
|
41
|
+
type="button"
|
|
42
|
+
className={css.header}
|
|
43
|
+
aria-expanded={open}
|
|
44
|
+
aria-label={`${props.t(open ? 'collapse' : 'expand')}: ${title}`}
|
|
45
|
+
onClick={() => { setOpen(current => !current) }}
|
|
46
|
+
>
|
|
47
|
+
<span className={css.headText}>
|
|
48
|
+
<span className={css.name}>{title}</span>
|
|
49
|
+
<span className={css.description}>{props.t(props.descriptionKey)}</span>
|
|
50
|
+
</span>
|
|
51
|
+
{state.dirty ? <span className={css.pending}>{props.t('unsaved')}</span> : null}
|
|
52
|
+
<IconChevronDownOutline14 className={open ? `${css.chevron} ${css.chevronOpen}` : css.chevron} />
|
|
53
|
+
</button>
|
|
54
|
+
{open
|
|
55
|
+
? (
|
|
56
|
+
<div className={css.body}>
|
|
57
|
+
{!state.writable ? <p className={css.readOnly} role="status">{props.t('readOnly')}</p> : null}
|
|
58
|
+
{props.children}
|
|
59
|
+
<div className={css.footer}>
|
|
60
|
+
{state.failed ? <p className={css.failed} role="status">{props.t('saveFailed')}</p> : null}
|
|
61
|
+
<button
|
|
62
|
+
type="button"
|
|
63
|
+
className={css.discard}
|
|
64
|
+
disabled={!state.dirty || state.saving}
|
|
65
|
+
onClick={props.onDiscard}
|
|
66
|
+
>
|
|
67
|
+
{props.t('discard')}
|
|
68
|
+
</button>
|
|
69
|
+
<button
|
|
70
|
+
type="button"
|
|
71
|
+
className={css.save}
|
|
72
|
+
disabled={blocked}
|
|
73
|
+
onClick={props.onSave}
|
|
74
|
+
>
|
|
75
|
+
{props.t(state.saving ? 'saving' : 'save')}
|
|
76
|
+
</button>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
)
|
|
80
|
+
: null}
|
|
81
|
+
</li>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/** WeCom settings card: owned chrome, staged fields, save/discard. */
|
|
2
|
+
|
|
3
|
+
import type { ReactElement } from 'react'
|
|
4
|
+
import type { ImBridgeLocaleKey } from './locales.ts'
|
|
5
|
+
import type { WecomCardFace, WecomCardState } from './card-controller.ts'
|
|
6
|
+
import { PluginCard } from './PluginCard.tsx'
|
|
7
|
+
import { SecretField, ValueField } from './fields.tsx'
|
|
8
|
+
|
|
9
|
+
/** Props the slot renderer binds for this card. */
|
|
10
|
+
export interface WecomCardProps {
|
|
11
|
+
/** Translate a dictionary key of the `im-bridge` namespace. */
|
|
12
|
+
t: (key: ImBridgeLocaleKey) => string
|
|
13
|
+
/** Card snapshot bound from inject.hooks.wecomCard. */
|
|
14
|
+
useWecomCard: <S>(select: (state: WecomCardState) => S) => S
|
|
15
|
+
/** Stage draft text for one field. */
|
|
16
|
+
edit: WecomCardFace['edit']
|
|
17
|
+
/** Stage a clear back to the composition layer. */
|
|
18
|
+
resetField: WecomCardFace['resetField']
|
|
19
|
+
/** Write every staged edit. */
|
|
20
|
+
save: WecomCardFace['save']
|
|
21
|
+
/** Drop every staged edit. */
|
|
22
|
+
discard: WecomCardFace['discard']
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Render the WeCom settings card.
|
|
27
|
+
* @param props - locale copy, snapshot hook, and form actions.
|
|
28
|
+
* @returns the card, or nothing when the namespace is unavailable.
|
|
29
|
+
*/
|
|
30
|
+
export function WecomCard(props: WecomCardProps): ReactElement | null {
|
|
31
|
+
const { t } = props
|
|
32
|
+
const state = props.useWecomCard(snapshot => snapshot)
|
|
33
|
+
const disabled = !state.writable
|
|
34
|
+
const field = {
|
|
35
|
+
overriddenLabel: t('overridden'),
|
|
36
|
+
resetLabel: t('reset'),
|
|
37
|
+
invalidLabel: t('invalidNumber'),
|
|
38
|
+
disabled,
|
|
39
|
+
}
|
|
40
|
+
return (
|
|
41
|
+
<PluginCard
|
|
42
|
+
t={t}
|
|
43
|
+
titleKey="title"
|
|
44
|
+
descriptionKey="description"
|
|
45
|
+
state={state}
|
|
46
|
+
onSave={props.save}
|
|
47
|
+
onDiscard={props.discard}
|
|
48
|
+
>
|
|
49
|
+
<SecretField
|
|
50
|
+
id="im-bridge-botId"
|
|
51
|
+
label={t('botId')}
|
|
52
|
+
hint={t('secretHint')}
|
|
53
|
+
disabled={disabled}
|
|
54
|
+
text={state.botId.text}
|
|
55
|
+
configured={state.botIdConfigured}
|
|
56
|
+
stateLabel={state.botIdConfigured ? t('secretConfigured') : t('secretUnset')}
|
|
57
|
+
onEdit={text => { props.edit('botId', text) }}
|
|
58
|
+
/>
|
|
59
|
+
<SecretField
|
|
60
|
+
id="im-bridge-secret"
|
|
61
|
+
label={t('secret')}
|
|
62
|
+
hint={t('secretHint')}
|
|
63
|
+
disabled={disabled}
|
|
64
|
+
text={state.secret.text}
|
|
65
|
+
configured={state.secretConfigured}
|
|
66
|
+
stateLabel={state.secretConfigured ? t('secretConfigured') : t('secretUnset')}
|
|
67
|
+
onEdit={text => { props.edit('secret', text) }}
|
|
68
|
+
/>
|
|
69
|
+
<ValueField
|
|
70
|
+
id="im-bridge-allowFrom"
|
|
71
|
+
label={t('allowFrom')}
|
|
72
|
+
hint={t('allowFromHint')}
|
|
73
|
+
{...field}
|
|
74
|
+
{...state.allowFrom}
|
|
75
|
+
onEdit={text => { props.edit('allowFrom', text) }}
|
|
76
|
+
onReset={() => { props.resetField('allowFrom') }}
|
|
77
|
+
/>
|
|
78
|
+
<ValueField
|
|
79
|
+
id="im-bridge-agentTimeoutSec"
|
|
80
|
+
label={t('agentTimeoutSec')}
|
|
81
|
+
hint={t('agentTimeoutSecHint')}
|
|
82
|
+
numeric
|
|
83
|
+
{...field}
|
|
84
|
+
{...state.agentTimeoutSec}
|
|
85
|
+
onEdit={text => { props.edit('agentTimeoutSec', text) }}
|
|
86
|
+
onReset={() => { props.resetField('agentTimeoutSec') }}
|
|
87
|
+
/>
|
|
88
|
+
<ValueField
|
|
89
|
+
id="im-bridge-startHint"
|
|
90
|
+
label={t('startHint')}
|
|
91
|
+
hint={t('startHintHint')}
|
|
92
|
+
{...field}
|
|
93
|
+
{...state.startHint}
|
|
94
|
+
onEdit={text => { props.edit('startHint', text) }}
|
|
95
|
+
onReset={() => { props.resetField('startHint') }}
|
|
96
|
+
/>
|
|
97
|
+
<ValueField
|
|
98
|
+
id="im-bridge-deniedMessage"
|
|
99
|
+
label={t('deniedMessage')}
|
|
100
|
+
hint={t('deniedMessageHint')}
|
|
101
|
+
{...field}
|
|
102
|
+
{...state.deniedMessage}
|
|
103
|
+
onEdit={text => { props.edit('deniedMessage', text) }}
|
|
104
|
+
onReset={() => { props.resetField('deniedMessage') }}
|
|
105
|
+
/>
|
|
106
|
+
<ValueField
|
|
107
|
+
id="im-bridge-welcomeMessage"
|
|
108
|
+
label={t('welcomeMessage')}
|
|
109
|
+
hint={t('welcomeMessageHint')}
|
|
110
|
+
{...field}
|
|
111
|
+
{...state.welcomeMessage}
|
|
112
|
+
onEdit={text => { props.edit('welcomeMessage', text) }}
|
|
113
|
+
onReset={() => { props.resetField('welcomeMessage') }}
|
|
114
|
+
/>
|
|
115
|
+
<ValueField
|
|
116
|
+
id="im-bridge-provider"
|
|
117
|
+
label={t('provider')}
|
|
118
|
+
hint={t('providerHint')}
|
|
119
|
+
{...field}
|
|
120
|
+
{...state.provider}
|
|
121
|
+
onEdit={text => { props.edit('provider', text) }}
|
|
122
|
+
onReset={() => { props.resetField('provider') }}
|
|
123
|
+
/>
|
|
124
|
+
<ValueField
|
|
125
|
+
id="im-bridge-model"
|
|
126
|
+
label={t('model')}
|
|
127
|
+
hint={t('modelHint')}
|
|
128
|
+
{...field}
|
|
129
|
+
{...state.model}
|
|
130
|
+
onEdit={text => { props.edit('model', text) }}
|
|
131
|
+
onReset={() => { props.resetField('model') }}
|
|
132
|
+
/>
|
|
133
|
+
</PluginCard>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/** Staged form over the `im-bridge` settings namespace. */
|
|
2
|
+
|
|
3
|
+
import type { SettingsScope, SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
|
4
|
+
import {
|
|
5
|
+
CardForm,
|
|
6
|
+
csvField,
|
|
7
|
+
numberField,
|
|
8
|
+
textField,
|
|
9
|
+
type CardActions,
|
|
10
|
+
type CardFieldState,
|
|
11
|
+
type CardShell,
|
|
12
|
+
} from './card-form.ts'
|
|
13
|
+
|
|
14
|
+
/** One redacted secret slot from `settings.describe`. */
|
|
15
|
+
interface SecretSlotView {
|
|
16
|
+
path: readonly string[]
|
|
17
|
+
set: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Cross-namespace describe face. Typed locally so this out-of-tree package
|
|
22
|
+
* does not value- or type-import `@deepseek-ai/dsh-client-ui-settings`.
|
|
23
|
+
*/
|
|
24
|
+
interface SettingsDescribeFace {
|
|
25
|
+
getSnapshot(): {
|
|
26
|
+
view?: {
|
|
27
|
+
namespaces: ReadonlyArray<{
|
|
28
|
+
ns: string
|
|
29
|
+
secrets: readonly SecretSlotView[]
|
|
30
|
+
}>
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Settings namespace paired with this card. */
|
|
36
|
+
const NS = 'im-bridge'
|
|
37
|
+
|
|
38
|
+
/** User-editable slice of the im-bridge settings namespace. */
|
|
39
|
+
export interface WecomCardSettings {
|
|
40
|
+
allowFrom?: string[]
|
|
41
|
+
agentTimeoutSec?: number
|
|
42
|
+
startHint?: string
|
|
43
|
+
deniedMessage?: string
|
|
44
|
+
welcomeMessage?: string
|
|
45
|
+
provider?: string
|
|
46
|
+
model?: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Snapshot the WeCom card renders. */
|
|
50
|
+
export interface WecomCardState extends CardShell {
|
|
51
|
+
botId: CardFieldState
|
|
52
|
+
secret: CardFieldState
|
|
53
|
+
botIdConfigured: boolean
|
|
54
|
+
secretConfigured: boolean
|
|
55
|
+
allowFrom: CardFieldState
|
|
56
|
+
agentTimeoutSec: CardFieldState
|
|
57
|
+
startHint: CardFieldState
|
|
58
|
+
deniedMessage: CardFieldState
|
|
59
|
+
welcomeMessage: CardFieldState
|
|
60
|
+
provider: CardFieldState
|
|
61
|
+
model: CardFieldState
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Face the card's slot registration injects. */
|
|
65
|
+
export interface WecomCardFace extends CardActions {
|
|
66
|
+
hooks: {
|
|
67
|
+
/** Card snapshot bound by the renderer as useWecomCard. */
|
|
68
|
+
wecomCard: SnapshotStore<WecomCardState>
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Bridges the `im-bridge` scope onto the staged card form. */
|
|
73
|
+
export class WecomCardController {
|
|
74
|
+
private readonly form: CardForm<WecomCardSettings>
|
|
75
|
+
private readonly store: SnapshotStore<WecomCardState>
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param scope - bound settings scope for the `im-bridge` namespace.
|
|
79
|
+
* @param describe - Host describe face; secret literals never ride it.
|
|
80
|
+
*/
|
|
81
|
+
constructor(
|
|
82
|
+
private readonly scope: SettingsScope<WecomCardSettings>,
|
|
83
|
+
private readonly describe: SettingsDescribeFace,
|
|
84
|
+
) {
|
|
85
|
+
this.form = new CardForm(
|
|
86
|
+
scope,
|
|
87
|
+
[
|
|
88
|
+
csvField('allowFrom'),
|
|
89
|
+
numberField('agentTimeoutSec'),
|
|
90
|
+
textField('startHint'),
|
|
91
|
+
textField('deniedMessage'),
|
|
92
|
+
textField('welcomeMessage'),
|
|
93
|
+
textField('provider'),
|
|
94
|
+
textField('model'),
|
|
95
|
+
],
|
|
96
|
+
[
|
|
97
|
+
{ field: 'botId', write: text => this.writeSecret('botId', text) },
|
|
98
|
+
{ field: 'secret', write: text => this.writeSecret('secret', text) },
|
|
99
|
+
],
|
|
100
|
+
)
|
|
101
|
+
this.store = this.form.bind(() => this.projection())
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private projection(): WecomCardState {
|
|
105
|
+
return {
|
|
106
|
+
...this.form.shell(),
|
|
107
|
+
botId: this.form.field('botId'),
|
|
108
|
+
secret: this.form.field('secret'),
|
|
109
|
+
botIdConfigured: this.secretConfigured('botId'),
|
|
110
|
+
secretConfigured: this.secretConfigured('secret'),
|
|
111
|
+
allowFrom: this.form.field('allowFrom'),
|
|
112
|
+
agentTimeoutSec: this.form.field('agentTimeoutSec'),
|
|
113
|
+
startHint: this.form.field('startHint'),
|
|
114
|
+
deniedMessage: this.form.field('deniedMessage'),
|
|
115
|
+
welcomeMessage: this.form.field('welcomeMessage'),
|
|
116
|
+
provider: this.form.field('provider'),
|
|
117
|
+
model: this.form.field('model'),
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Whether the Host reports a configured value for one secret slot.
|
|
123
|
+
* @param field - `botId` or `secret`.
|
|
124
|
+
* @returns true when describe lists that slot as set.
|
|
125
|
+
*/
|
|
126
|
+
private secretConfigured(field: string): boolean {
|
|
127
|
+
const row = this.describe.getSnapshot().view?.namespaces.find(candidate => candidate.ns === NS)
|
|
128
|
+
return row?.secrets.some(slot => slot.path[0] === field && slot.set) === true
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Write one credential into the user layer, then read configured state back.
|
|
133
|
+
* @param field - `botId` or `secret`.
|
|
134
|
+
* @param text - the staged literal.
|
|
135
|
+
* @returns whether describe reports the slot set afterwards.
|
|
136
|
+
*/
|
|
137
|
+
private async writeSecret(field: string, text: string): Promise<boolean> {
|
|
138
|
+
await this.scope.set(field, text)
|
|
139
|
+
return this.secretConfigured(field)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Face the slot registration injects. */
|
|
143
|
+
inject(): WecomCardFace {
|
|
144
|
+
return { hooks: { wecomCard: this.store }, ...this.form.actions() }
|
|
145
|
+
}
|
|
146
|
+
}
|