@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.
Files changed (50) hide show
  1. package/README.md +89 -55
  2. package/docs/access-control-design.md +5 -5
  3. package/docs/feasibility-and-design.md +54 -26
  4. package/docs/feishu-onboarding.md +26 -20
  5. package/docs/persistence.md +4 -4
  6. package/docs/refactor-plan.md +6 -5
  7. package/docs/workspace-isolation-design.md +78 -0
  8. package/lib/client.js +1715 -882
  9. package/lib/client.js.map +1 -1
  10. package/lib/index.js +1556 -680
  11. package/lib/index.js.map +1 -1
  12. package/package.json +1 -1
  13. package/src/cards/index.ts +1 -0
  14. package/src/cards/onboarding.ts +23 -0
  15. package/src/cards/primitives.ts +20 -0
  16. package/src/cards/streaming.ts +310 -291
  17. package/src/cleanup.ts +256 -119
  18. package/src/client/api.ts +20 -0
  19. package/src/client/catalog.ts +1 -1
  20. package/src/client/index.ts +1 -1
  21. package/src/client/panel/access-card.tsx +294 -0
  22. package/src/client/panel/access-tab.tsx +46 -0
  23. package/src/client/panel/advanced-tab.tsx +80 -0
  24. package/src/client/panel/cleanup-card.tsx +106 -0
  25. package/src/client/panel/connection-tab.tsx +214 -0
  26. package/src/client/panel/credentials-card.tsx +104 -0
  27. package/src/client/panel/index.tsx +862 -0
  28. package/src/client/panel/model-tab.tsx +23 -0
  29. package/src/client/panel/sessions-tab.tsx +21 -0
  30. package/src/client/panel/shared.tsx +488 -0
  31. package/src/client/panel/workspace-tab.tsx +40 -0
  32. package/src/client/styles.ts +24 -6
  33. package/src/config.ts +44 -12
  34. package/src/domain.ts +34 -3
  35. package/src/feishu/agent-context.ts +9 -5
  36. package/src/index.ts +32 -14
  37. package/src/routing/inbound.ts +88 -43
  38. package/src/service.ts +259 -65
  39. package/src/session/isolation.ts +277 -0
  40. package/src/session/manager.ts +63 -20
  41. package/src/session/repair.ts +2 -3
  42. package/src/session/routes.ts +2 -0
  43. package/src/session-commands.ts +41 -14
  44. package/src/settings/contract.ts +223 -47
  45. package/src/settings/directory-picker.ts +121 -0
  46. package/src/settings/routes.ts +49 -2
  47. package/src/settings/surface.ts +171 -25
  48. package/src/streamer.ts +70 -30
  49. package/src/workspaces.ts +50 -28
  50. 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
+ }