@modesto_ma/dsh-channels 0.1.0 → 0.3.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.md +89 -55
- package/docs/access-control-design.md +5 -5
- package/docs/feasibility-and-design.md +54 -26
- package/docs/feishu-onboarding.md +26 -20
- package/docs/persistence.md +4 -4
- package/docs/refactor-plan.md +6 -5
- package/docs/workspace-isolation-design.md +78 -0
- package/lib/client.js +1715 -882
- package/lib/client.js.map +1 -1
- package/lib/index.js +1556 -680
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cards/index.ts +1 -0
- package/src/cards/onboarding.ts +23 -0
- package/src/cards/primitives.ts +20 -0
- package/src/cards/streaming.ts +310 -291
- package/src/cleanup.ts +256 -119
- package/src/client/api.ts +20 -0
- package/src/client/catalog.ts +1 -1
- package/src/client/index.ts +1 -1
- package/src/client/panel/access-card.tsx +294 -0
- package/src/client/panel/access-tab.tsx +46 -0
- package/src/client/panel/advanced-tab.tsx +80 -0
- package/src/client/panel/cleanup-card.tsx +106 -0
- package/src/client/panel/connection-tab.tsx +214 -0
- package/src/client/panel/credentials-card.tsx +104 -0
- package/src/client/panel/index.tsx +862 -0
- package/src/client/panel/model-tab.tsx +23 -0
- package/src/client/panel/sessions-tab.tsx +21 -0
- package/src/client/panel/shared.tsx +488 -0
- package/src/client/panel/workspace-tab.tsx +40 -0
- package/src/client/styles.ts +24 -6
- package/src/config.ts +44 -12
- package/src/domain.ts +34 -3
- package/src/feishu/agent-context.ts +9 -5
- package/src/index.ts +32 -14
- package/src/routing/inbound.ts +88 -43
- package/src/service.ts +259 -65
- package/src/session/isolation.ts +277 -0
- package/src/session/manager.ts +63 -20
- package/src/session/repair.ts +2 -3
- package/src/session/routes.ts +2 -0
- package/src/session-commands.ts +41 -14
- package/src/settings/contract.ts +223 -47
- package/src/settings/directory-picker.ts +121 -0
- package/src/settings/routes.ts +49 -2
- package/src/settings/surface.ts +171 -25
- package/src/streamer.ts +70 -30
- package/src/workspaces.ts +50 -28
- package/src/client/panel.tsx +0 -1475
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 「连接」tab:按 Host 派生的生命周期阶段渲染连接引导。
|
|
3
|
+
*
|
|
4
|
+
* - unconfigured:onboarding 三步 + 凭据卡(不渲染可用的「连接」按钮)
|
|
5
|
+
* - idle:连接信息卡 + 「连接飞书」
|
|
6
|
+
* - error:连接信息卡 + 最近一次连接失败 + 「重试」
|
|
7
|
+
* - connected:连接信息卡 + 「重启飞书连接」+ 凭据卡
|
|
8
|
+
* - connecting:禁用按钮显示「连接中…」
|
|
9
|
+
*
|
|
10
|
+
* @module dsh-channels/client/panel/connection-tab
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { PANEL_COPY } from '../../settings/contract.ts'
|
|
14
|
+
import type { ChannelStatusWire, PanelWire } from '../../settings/contract.ts'
|
|
15
|
+
import { CredentialsCard } from './credentials-card.tsx'
|
|
16
|
+
import { connectionStateLabel, fieldLabel, formatUptime, phaseBadge, TabHelp } from './shared.tsx'
|
|
17
|
+
import type { BusyAction, CredentialsView } from './shared.tsx'
|
|
18
|
+
|
|
19
|
+
export interface ConnectionTabProps {
|
|
20
|
+
panel: PanelWire
|
|
21
|
+
status: ChannelStatusWire
|
|
22
|
+
busy: BusyAction | null
|
|
23
|
+
credentials: CredentialsView
|
|
24
|
+
onReconnect: () => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function ConnectionTab(props: ConnectionTabProps): JSX.Element {
|
|
28
|
+
const { panel, status, busy, credentials, onReconnect } = props
|
|
29
|
+
return (
|
|
30
|
+
<div className="dsh-channels-cfg__tabBody">
|
|
31
|
+
<TabHelp text={PANEL_COPY.connectionTabHelp} />
|
|
32
|
+
{panel.config.restartPendingKeys.length === 0
|
|
33
|
+
? null
|
|
34
|
+
: <RestartBanner panel={panel} busy={busy} locked={status.busy} onReconnect={onReconnect} />}
|
|
35
|
+
{status.phase === 'unconfigured'
|
|
36
|
+
? <OnboardingCard status={status} />
|
|
37
|
+
: <ConnectionInfoCard status={status} busy={busy} onReconnect={onReconnect} />}
|
|
38
|
+
<CredentialsCard panel={panel} credentials={credentials} />
|
|
39
|
+
</div>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** First-run guide: create the app, fill the credentials, save and wait. */
|
|
44
|
+
function OnboardingCard(props: { status: ChannelStatusWire }): JSX.Element {
|
|
45
|
+
const { status } = props
|
|
46
|
+
const steps = [
|
|
47
|
+
{ title: PANEL_COPY.onboardingStep1Title, text: PANEL_COPY.onboardingStep1Text },
|
|
48
|
+
{ title: PANEL_COPY.onboardingStep2Title, text: PANEL_COPY.onboardingStep2Text },
|
|
49
|
+
{ title: PANEL_COPY.onboardingStep3Title, text: PANEL_COPY.onboardingStep3Text },
|
|
50
|
+
]
|
|
51
|
+
return (
|
|
52
|
+
<section className="dsh-channels-cfg__card">
|
|
53
|
+
<div className="dsh-channels-cfg__cardHead">
|
|
54
|
+
<span className="dsh-channels-cfg__cardTitle">{PANEL_COPY.onboardingTitle}</span>
|
|
55
|
+
<span className="dsh-channels-cfg__badge dsh-channels-cfg__badge--muted">
|
|
56
|
+
{PANEL_COPY.phaseUnconfigured}
|
|
57
|
+
</span>
|
|
58
|
+
</div>
|
|
59
|
+
<span className="dsh-channels-cfg__help">{PANEL_COPY.onboardingIntro}</span>
|
|
60
|
+
<ol className="dsh-channels-cfg__steps">
|
|
61
|
+
{steps.map((step, index) => (
|
|
62
|
+
<li className="dsh-channels-cfg__step" key={step.title}>
|
|
63
|
+
<span className="dsh-channels-cfg__stepIndex">{index + 1}</span>
|
|
64
|
+
<span className="dsh-channels-cfg__stepBody">
|
|
65
|
+
<span className="dsh-channels-cfg__stepTitle">{step.title}</span>
|
|
66
|
+
<span className="dsh-channels-cfg__help">{step.text}</span>
|
|
67
|
+
</span>
|
|
68
|
+
</li>
|
|
69
|
+
))}
|
|
70
|
+
</ol>
|
|
71
|
+
{status.missingConfig.length === 0
|
|
72
|
+
? null
|
|
73
|
+
: (
|
|
74
|
+
<div className="dsh-channels-cfg__banner">
|
|
75
|
+
<strong>{PANEL_COPY.missingConfig}:</strong>
|
|
76
|
+
<span>{status.missingConfig.join('、')}</span>
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
79
|
+
<span className="dsh-channels-cfg__help">{PANEL_COPY.statusHintUnconfigured}</span>
|
|
80
|
+
<span className="dsh-channels-cfg__help">{PANEL_COPY.onboardingDocsHint}</span>
|
|
81
|
+
</section>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Runtime facts plus the one lifecycle action available in the current phase. */
|
|
86
|
+
function ConnectionInfoCard(props: {
|
|
87
|
+
status: ChannelStatusWire
|
|
88
|
+
busy: BusyAction | null
|
|
89
|
+
onReconnect: () => void
|
|
90
|
+
}): JSX.Element {
|
|
91
|
+
const { status, busy, onReconnect } = props
|
|
92
|
+
const badge = phaseBadge(status.phase)
|
|
93
|
+
// `status.busy` covers a connection attempt started outside this panel; the
|
|
94
|
+
// local busy flag covers the panel's own action. Either way the button waits.
|
|
95
|
+
const actionBusy = busy !== null || status.busy
|
|
96
|
+
const connection = connectionStateLabel(status.connection?.state)
|
|
97
|
+
const facts = [
|
|
98
|
+
{ label: PANEL_COPY.statusCard, value: badge.text },
|
|
99
|
+
{ label: PANEL_COPY.connectionStateLabel, value: connection.text },
|
|
100
|
+
{ label: PANEL_COPY.botOpenId, value: status.botOpenIdTail ?? PANEL_COPY.unknownValue },
|
|
101
|
+
{ label: PANEL_COPY.uptime, value: formatUptime(status.uptimeMs) },
|
|
102
|
+
{ label: PANEL_COPY.routeCount, value: String(status.routeCount) },
|
|
103
|
+
{ label: PANEL_COPY.liveAgentCount, value: String(status.liveAgentCount) },
|
|
104
|
+
{ label: PANEL_COPY.inflightTurnCount, value: String(status.inflightTurnCount) },
|
|
105
|
+
]
|
|
106
|
+
return (
|
|
107
|
+
<section className="dsh-channels-cfg__card">
|
|
108
|
+
<div className="dsh-channels-cfg__cardHead">
|
|
109
|
+
<span className="dsh-channels-cfg__cardTitle">{PANEL_COPY.connectionInfo}</span>
|
|
110
|
+
<span className={`dsh-channels-cfg__badge dsh-channels-cfg__badge--${status.configured ? 'ok' : 'bad'}`}>
|
|
111
|
+
{status.configured ? PANEL_COPY.configuredBadge : PANEL_COPY.unconfiguredBadge}
|
|
112
|
+
</span>
|
|
113
|
+
</div>
|
|
114
|
+
{status.lastStartError === undefined
|
|
115
|
+
? null
|
|
116
|
+
: (
|
|
117
|
+
<div className="dsh-channels-cfg__notice dsh-channels-cfg__notice--error">
|
|
118
|
+
<strong>{PANEL_COPY.lastError}</strong>
|
|
119
|
+
<span className="dsh-channels-cfg__mono">{status.lastStartError}</span>
|
|
120
|
+
</div>
|
|
121
|
+
)}
|
|
122
|
+
<div className="dsh-channels-cfg__facts">
|
|
123
|
+
{facts.map(fact => (
|
|
124
|
+
<div className="dsh-channels-cfg__fact" key={fact.label}>
|
|
125
|
+
<span className="dsh-channels-cfg__factLabel">{fact.label}</span>
|
|
126
|
+
<span className="dsh-channels-cfg__factValue">{fact.value}</span>
|
|
127
|
+
</div>
|
|
128
|
+
))}
|
|
129
|
+
</div>
|
|
130
|
+
{status.reasons.length === 0
|
|
131
|
+
? null
|
|
132
|
+
: (
|
|
133
|
+
<span className={`dsh-channels-cfg__help${status.level === 'unhealthy' ? ' dsh-channels-cfg__reasonBad' : ''}`}>
|
|
134
|
+
{PANEL_COPY.healthReasons}:{status.reasons.join(';')}
|
|
135
|
+
</span>
|
|
136
|
+
)}
|
|
137
|
+
<div className="dsh-channels-cfg__actions">
|
|
138
|
+
{status.phase === 'connecting'
|
|
139
|
+
? (
|
|
140
|
+
<button type="button" className="dsh-channels-cfg__btn dsh-channels-cfg__btn--primary" disabled>
|
|
141
|
+
{PANEL_COPY.connecting}
|
|
142
|
+
</button>
|
|
143
|
+
)
|
|
144
|
+
: null}
|
|
145
|
+
{status.phase === 'idle'
|
|
146
|
+
? (
|
|
147
|
+
<button
|
|
148
|
+
type="button"
|
|
149
|
+
className="dsh-channels-cfg__btn dsh-channels-cfg__btn--primary"
|
|
150
|
+
onClick={onReconnect}
|
|
151
|
+
disabled={actionBusy}
|
|
152
|
+
>
|
|
153
|
+
{busy === 'reconnect' ? PANEL_COPY.connecting : PANEL_COPY.connect}
|
|
154
|
+
</button>
|
|
155
|
+
)
|
|
156
|
+
: null}
|
|
157
|
+
{status.phase === 'error'
|
|
158
|
+
? (
|
|
159
|
+
<button
|
|
160
|
+
type="button"
|
|
161
|
+
className="dsh-channels-cfg__btn dsh-channels-cfg__btn--primary"
|
|
162
|
+
onClick={onReconnect}
|
|
163
|
+
disabled={actionBusy}
|
|
164
|
+
>
|
|
165
|
+
{busy === 'reconnect' ? PANEL_COPY.connecting : PANEL_COPY.retry}
|
|
166
|
+
</button>
|
|
167
|
+
)
|
|
168
|
+
: null}
|
|
169
|
+
{status.phase === 'connected'
|
|
170
|
+
? (
|
|
171
|
+
<>
|
|
172
|
+
<button
|
|
173
|
+
type="button"
|
|
174
|
+
className="dsh-channels-cfg__btn dsh-channels-cfg__btn--danger"
|
|
175
|
+
onClick={onReconnect}
|
|
176
|
+
disabled={actionBusy}
|
|
177
|
+
>
|
|
178
|
+
{busy === 'reconnect' ? PANEL_COPY.reconnecting : PANEL_COPY.reconnect}
|
|
179
|
+
</button>
|
|
180
|
+
<span className="dsh-channels-cfg__help">{PANEL_COPY.restartWarning}</span>
|
|
181
|
+
</>
|
|
182
|
+
)
|
|
183
|
+
: null}
|
|
184
|
+
</div>
|
|
185
|
+
</section>
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Config saved but only applied on the next connect: offer the one-click restart. */
|
|
190
|
+
function RestartBanner(props: {
|
|
191
|
+
panel: PanelWire
|
|
192
|
+
busy: BusyAction | null
|
|
193
|
+
locked: boolean
|
|
194
|
+
onReconnect: () => void
|
|
195
|
+
}): JSX.Element {
|
|
196
|
+
const { panel, busy, locked, onReconnect } = props
|
|
197
|
+
const keys = panel.config.restartPendingKeys
|
|
198
|
+
.map(key => fieldLabel(panel.config.fields, key))
|
|
199
|
+
.join('、')
|
|
200
|
+
return (
|
|
201
|
+
<div className="dsh-channels-cfg__banner">
|
|
202
|
+
<span>{PANEL_COPY.restartBanner}{keys}</span>
|
|
203
|
+
<button
|
|
204
|
+
type="button"
|
|
205
|
+
className="dsh-channels-cfg__btn dsh-channels-cfg__btn--danger"
|
|
206
|
+
onClick={onReconnect}
|
|
207
|
+
disabled={busy !== null || locked}
|
|
208
|
+
>
|
|
209
|
+
{busy === 'reconnect' ? PANEL_COPY.reconnecting : PANEL_COPY.reconnect}
|
|
210
|
+
</button>
|
|
211
|
+
<span className="dsh-channels-cfg__help">{PANEL_COPY.restartWarning}</span>
|
|
212
|
+
</div>
|
|
213
|
+
)
|
|
214
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 飞书应用凭据编辑器:值经 Host 的 DSH 凭据服务写入本机凭据库,面板只显示
|
|
3
|
+
* 「已配置 / 未配置」,两个方向都不回显已存值。保存动作由页面外壳持有
|
|
4
|
+
* (凭据保存独立于配置 draft)。
|
|
5
|
+
*
|
|
6
|
+
* @module dsh-channels/client/panel/credentials-card
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { PANEL_COPY } from '../../settings/contract.ts'
|
|
10
|
+
import type { CredentialField, CredentialRefWire, PanelWire } from '../../settings/contract.ts'
|
|
11
|
+
import type { CredentialsView } from './shared.tsx'
|
|
12
|
+
|
|
13
|
+
export function CredentialsCard(props: {
|
|
14
|
+
panel: PanelWire
|
|
15
|
+
credentials: CredentialsView
|
|
16
|
+
}): JSX.Element {
|
|
17
|
+
const { panel, credentials } = props
|
|
18
|
+
const { appId, appSecret, issues, busy, locked, onAppIdChange, onAppSecretChange, onSave, onClear } = credentials
|
|
19
|
+
const appIdState = panel.config.credentials.appId
|
|
20
|
+
const appSecretState = panel.config.credentials.appSecret
|
|
21
|
+
const configured = appIdState.configured && appSecretState.configured
|
|
22
|
+
const writable = appIdState.writable || appSecretState.writable
|
|
23
|
+
const disabled = busy !== null || locked || !writable
|
|
24
|
+
const pending = appId.trim() !== '' || appSecret.trim() !== ''
|
|
25
|
+
|
|
26
|
+
const row = (
|
|
27
|
+
field: CredentialField,
|
|
28
|
+
label: string,
|
|
29
|
+
state: CredentialRefWire,
|
|
30
|
+
value: string,
|
|
31
|
+
type: 'text' | 'password',
|
|
32
|
+
placeholder: string,
|
|
33
|
+
onChange: (next: string) => void,
|
|
34
|
+
): JSX.Element => {
|
|
35
|
+
const issue = issues.find(item => item.key === field)
|
|
36
|
+
const inputClass = `dsh-channels-cfg__input${issue === undefined ? '' : ' dsh-channels-cfg__input--invalid'}`
|
|
37
|
+
return (
|
|
38
|
+
<div className="dsh-channels-cfg__field" key={field}>
|
|
39
|
+
<div className="dsh-channels-cfg__labelRow">
|
|
40
|
+
<span className="dsh-channels-cfg__label">{label}</span>
|
|
41
|
+
<span className={`dsh-channels-cfg__badge dsh-channels-cfg__badge--${state.configured ? 'ok' : 'bad'}`}>
|
|
42
|
+
{state.configured ? PANEL_COPY.credentialConfigured : PANEL_COPY.credentialMissing}
|
|
43
|
+
</span>
|
|
44
|
+
</div>
|
|
45
|
+
<div className="dsh-channels-cfg__cred">
|
|
46
|
+
<input
|
|
47
|
+
type={type}
|
|
48
|
+
className={inputClass}
|
|
49
|
+
value={value}
|
|
50
|
+
placeholder={placeholder}
|
|
51
|
+
autoComplete="new-password"
|
|
52
|
+
aria-label={label}
|
|
53
|
+
disabled={disabled}
|
|
54
|
+
onChange={event => onChange(event.target.value)}
|
|
55
|
+
/>
|
|
56
|
+
{state.configured
|
|
57
|
+
? (
|
|
58
|
+
<button
|
|
59
|
+
type="button"
|
|
60
|
+
className="dsh-channels-cfg__btn"
|
|
61
|
+
disabled={disabled}
|
|
62
|
+
onClick={() => onClear(field)}
|
|
63
|
+
>
|
|
64
|
+
{busy === 'credentials-clear' ? PANEL_COPY.removingCredential : PANEL_COPY.removeCredential}
|
|
65
|
+
</button>
|
|
66
|
+
)
|
|
67
|
+
: null}
|
|
68
|
+
</div>
|
|
69
|
+
<span className="dsh-channels-cfg__help">
|
|
70
|
+
{PANEL_COPY.credentialsRef}:<span className="dsh-channels-cfg__mono">{state.ref}</span>
|
|
71
|
+
{state.source === undefined ? '' : ` · ${state.source}`}
|
|
72
|
+
</span>
|
|
73
|
+
{issue === undefined ? null : <span className="dsh-channels-cfg__fieldError">{issue.message}</span>}
|
|
74
|
+
</div>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<section className="dsh-channels-cfg__card">
|
|
80
|
+
<div className="dsh-channels-cfg__cardHead">
|
|
81
|
+
<span className="dsh-channels-cfg__cardTitle">{PANEL_COPY.credentialsCard}</span>
|
|
82
|
+
<span className={`dsh-channels-cfg__badge dsh-channels-cfg__badge--${configured ? 'ok' : 'muted'}`}>
|
|
83
|
+
{configured ? PANEL_COPY.configuredBadge : PANEL_COPY.unconfiguredBadge}
|
|
84
|
+
</span>
|
|
85
|
+
</div>
|
|
86
|
+
<span className="dsh-channels-cfg__help">{PANEL_COPY.credentialsCardHelp}</span>
|
|
87
|
+
{row('appId', PANEL_COPY.appIdLabel, appIdState, appId, 'text', PANEL_COPY.appIdPlaceholder, onAppIdChange)}
|
|
88
|
+
{row('appSecret', PANEL_COPY.appSecretLabel, appSecretState, appSecret, 'password', PANEL_COPY.appSecretPlaceholder, onAppSecretChange)}
|
|
89
|
+
{writable ? null : <span className="dsh-channels-cfg__fieldError">{PANEL_COPY.credentialReadOnly}</span>}
|
|
90
|
+
<div className="dsh-channels-cfg__footer">
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
className="dsh-channels-cfg__btn dsh-channels-cfg__btn--primary"
|
|
94
|
+
onClick={onSave}
|
|
95
|
+
disabled={disabled || !pending}
|
|
96
|
+
>
|
|
97
|
+
{busy === 'credentials' ? PANEL_COPY.savingCredentials : PANEL_COPY.saveCredentials}
|
|
98
|
+
</button>
|
|
99
|
+
<span className="dsh-channels-cfg__spacer" />
|
|
100
|
+
<span className="dsh-channels-cfg__help">{PANEL_COPY.credentialsPendingHint}</span>
|
|
101
|
+
</div>
|
|
102
|
+
</section>
|
|
103
|
+
)
|
|
104
|
+
}
|