@modesto_ma/dsh-channels 0.4.0 → 0.5.0-beta.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/README.md +73 -55
- package/docs/access-control-design.md +7 -5
- package/docs/attachment-pipeline-v2.md +33 -41
- package/docs/feishu-onboarding.md +33 -33
- package/docs/persistence.md +6 -4
- package/docs/refactor-plan.md +1 -1
- package/docs/workspace-isolation-design.md +7 -8
- package/lib/client.js +173 -287
- package/lib/client.js.map +1 -1
- package/lib/index.js +3001 -2627
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/access/cards.ts +9 -9
- package/src/access/controller.ts +10 -10
- package/src/access/index.ts +1 -0
- package/src/access/policy.ts +10 -10
- package/src/access/requests.ts +32 -31
- package/src/access/store.ts +12 -12
- package/src/access/targets.ts +56 -0
- package/src/cards/approval.ts +96 -26
- package/src/cards/index.ts +1 -0
- package/src/cards/onboarding.ts +1 -0
- package/src/cards/picker.ts +115 -0
- package/src/cards/streaming.ts +56 -7
- package/src/client/api.ts +5 -25
- package/src/client/panel/access-card.tsx +24 -15
- package/src/client/panel/access-tab.tsx +3 -3
- package/src/client/panel/connection-tab.tsx +70 -1
- package/src/client/panel/index.tsx +11 -62
- package/src/client/panel/shared.tsx +0 -23
- package/src/client/panel/workspace-tab.tsx +2 -18
- package/src/client/styles.ts +2 -0
- package/src/commands/registry.ts +2 -2
- package/src/config.ts +25 -56
- package/src/domain.ts +28 -125
- package/src/feishu/inbound-attachments.ts +1 -1
- package/src/feishu/manifest.ts +8 -31
- package/src/feishu/message-read.ts +19 -22
- package/src/feishu/scopes.ts +171 -0
- package/src/feishu/send-file.ts +543 -0
- package/src/feishu/slash-commands.ts +134 -0
- package/src/feishu/tools.ts +72 -112
- package/src/feishu-channel.ts +270 -9
- package/src/index.ts +5 -4
- package/src/interactions/approvals.ts +187 -7
- package/src/interactions/pending.ts +3 -3
- package/src/routing/inbound.ts +73 -106
- package/src/service.ts +463 -438
- package/src/session/manager.ts +1 -1
- package/src/session/routes.ts +2 -3
- package/src/settings/contract.ts +68 -85
- package/src/settings/routes.ts +3 -17
- package/src/settings/surface.ts +37 -88
- package/src/streamer.ts +94 -5
- package/src/workspaces.ts +21 -29
- package/src/cleanup.ts +0 -546
- package/src/client/panel/cleanup-card.tsx +0 -106
- package/src/feishu/pending-attachments.ts +0 -562
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
* - error:连接信息卡 + 最近一次连接失败 + 「重试」
|
|
7
7
|
* - connected:连接信息卡 + 「重启飞书连接」+ 凭据卡
|
|
8
8
|
* - connecting:禁用按钮显示「连接中…」
|
|
9
|
+
* - 已连接且权限自检完成:权限自检卡(缺失权限红字 + 开发者后台深链)
|
|
9
10
|
*
|
|
10
11
|
* @module dsh-channels/client/panel/connection-tab
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
import { PANEL_COPY } from '../../settings/contract.ts'
|
|
14
|
-
import type { ChannelStatusWire, PanelWire } from '../../settings/contract.ts'
|
|
15
|
+
import type { ChannelStatusWire, PanelWire, ScopeCheckWire, SlashCommandSyncWire } from '../../settings/contract.ts'
|
|
15
16
|
import { CredentialsCard } from './credentials-card.tsx'
|
|
16
17
|
import { connectionStateLabel, fieldLabel, formatUptime, phaseBadge, TabHelp } from './shared.tsx'
|
|
17
18
|
import type { BusyAction, CredentialsView } from './shared.tsx'
|
|
@@ -35,6 +36,8 @@ export function ConnectionTab(props: ConnectionTabProps): JSX.Element {
|
|
|
35
36
|
{status.phase === 'unconfigured'
|
|
36
37
|
? <OnboardingCard status={status} />
|
|
37
38
|
: <ConnectionInfoCard status={status} busy={busy} onReconnect={onReconnect} />}
|
|
39
|
+
{status.scopeCheck === undefined ? null : <ScopeCheckCard check={status.scopeCheck} />}
|
|
40
|
+
{status.slashCommandSync === undefined ? null : <SlashCommandSyncCard sync={status.slashCommandSync} />}
|
|
38
41
|
<CredentialsCard panel={panel} credentials={credentials} />
|
|
39
42
|
</div>
|
|
40
43
|
)
|
|
@@ -186,6 +189,72 @@ function ConnectionInfoCard(props: {
|
|
|
186
189
|
)
|
|
187
190
|
}
|
|
188
191
|
|
|
192
|
+
/** Post-connect scope self-check: missing scopes in red, each with a console deep link. */
|
|
193
|
+
function ScopeCheckCard(props: { check: ScopeCheckWire }): JSX.Element {
|
|
194
|
+
const { check } = props
|
|
195
|
+
return (
|
|
196
|
+
<section className="dsh-channels-cfg__card">
|
|
197
|
+
<div className="dsh-channels-cfg__cardHead">
|
|
198
|
+
<span className="dsh-channels-cfg__cardTitle">{PANEL_COPY.scopeCheckTitle}</span>
|
|
199
|
+
</div>
|
|
200
|
+
{check.state === 'ok'
|
|
201
|
+
? <div className="dsh-channels-cfg__notice dsh-channels-cfg__notice--ok">{PANEL_COPY.scopeCheckOk}</div>
|
|
202
|
+
: null}
|
|
203
|
+
{check.state === 'error'
|
|
204
|
+
? (
|
|
205
|
+
<div className="dsh-channels-cfg__notice dsh-channels-cfg__notice--error">
|
|
206
|
+
<strong>{PANEL_COPY.scopeCheckError}</strong>
|
|
207
|
+
<span className="dsh-channels-cfg__mono">{check.error}</span>
|
|
208
|
+
</div>
|
|
209
|
+
)
|
|
210
|
+
: null}
|
|
211
|
+
{check.state === 'missing'
|
|
212
|
+
? (
|
|
213
|
+
<div className="dsh-channels-cfg__notice dsh-channels-cfg__notice--error">
|
|
214
|
+
<strong>{PANEL_COPY.scopeCheckMissing}</strong>
|
|
215
|
+
<ul className="dsh-channels-cfg__scopeList">
|
|
216
|
+
{check.missing.map(item => (
|
|
217
|
+
<li key={item.scopes.join(',')}>
|
|
218
|
+
<span>[{item.level === 'required' ? PANEL_COPY.scopeCheckRequired : PANEL_COPY.scopeCheckOptional}] </span>
|
|
219
|
+
<span className="dsh-channels-cfg__mono">{item.scopes.join(' / ')}</span>
|
|
220
|
+
<span>:{item.purpose} </span>
|
|
221
|
+
<a href={item.consoleUrl} target="_blank" rel="noreferrer noopener">{PANEL_COPY.scopeCheckOpenOne}</a>
|
|
222
|
+
</li>
|
|
223
|
+
))}
|
|
224
|
+
</ul>
|
|
225
|
+
<a href={check.consoleUrl} target="_blank" rel="noreferrer noopener">{PANEL_COPY.scopeCheckOpenAll}</a>
|
|
226
|
+
</div>
|
|
227
|
+
)
|
|
228
|
+
: null}
|
|
229
|
+
</section>
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Post-connect native slash-command panel sync: counts on success, the Feishu reason on failure. */
|
|
234
|
+
function SlashCommandSyncCard(props: { sync: SlashCommandSyncWire }): JSX.Element {
|
|
235
|
+
const { sync } = props
|
|
236
|
+
return (
|
|
237
|
+
<section className="dsh-channels-cfg__card">
|
|
238
|
+
<div className="dsh-channels-cfg__cardHead">
|
|
239
|
+
<span className="dsh-channels-cfg__cardTitle">{PANEL_COPY.slashCommandSyncTitle}</span>
|
|
240
|
+
</div>
|
|
241
|
+
{sync.state === 'ok'
|
|
242
|
+
? (
|
|
243
|
+
<div className="dsh-channels-cfg__notice dsh-channels-cfg__notice--ok">
|
|
244
|
+
<span>{PANEL_COPY.slashCommandSyncOk} </span>
|
|
245
|
+
<span>{PANEL_COPY.slashCommandSyncTotal} {sync.total} · {PANEL_COPY.slashCommandSyncCreated} {sync.created} · {PANEL_COPY.slashCommandSyncUpdated} {sync.updated} · {PANEL_COPY.slashCommandSyncRemoved} {sync.removed}</span>
|
|
246
|
+
</div>
|
|
247
|
+
)
|
|
248
|
+
: (
|
|
249
|
+
<div className="dsh-channels-cfg__notice dsh-channels-cfg__notice--error">
|
|
250
|
+
<strong>{PANEL_COPY.slashCommandSyncError}</strong>
|
|
251
|
+
<span className="dsh-channels-cfg__mono">{sync.error}</span>
|
|
252
|
+
</div>
|
|
253
|
+
)}
|
|
254
|
+
</section>
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
189
258
|
/** Config saved but only applied on the next connect: offer the one-click restart. */
|
|
190
259
|
function RestartBanner(props: {
|
|
191
260
|
panel: PanelWire
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* 设置 →「飞书渠道」页面:数据加载、5 秒状态轮询、全局 draft 与顶部状态头 /
|
|
3
3
|
* tab 导航的外壳。
|
|
4
4
|
*
|
|
5
|
-
* 所有 Host 交互(panel / status / catalog / config / credentials /
|
|
6
|
-
*
|
|
5
|
+
* 所有 Host 交互(panel / status / catalog / config / credentials / access /
|
|
6
|
+
* pick-directory)都集中在这里;tab 组件只接收渲染所需的数据与
|
|
7
7
|
* 回调。页面不 import 任何 DSH 客户端包,只依赖 react 与 `settings.section`
|
|
8
8
|
* 槽位。
|
|
9
9
|
*
|
|
@@ -17,7 +17,6 @@ import type {
|
|
|
17
17
|
AccessWire,
|
|
18
18
|
CatalogWire,
|
|
19
19
|
ChannelStatusWire,
|
|
20
|
-
CleanupRunWire,
|
|
21
20
|
ConfigValue,
|
|
22
21
|
CredentialField,
|
|
23
22
|
EditableKey,
|
|
@@ -39,7 +38,6 @@ import {
|
|
|
39
38
|
removeAccessGroup,
|
|
40
39
|
removeAccessUser,
|
|
41
40
|
resolveAccessRequest,
|
|
42
|
-
runCleanup,
|
|
43
41
|
saveConfig,
|
|
44
42
|
saveCredentials,
|
|
45
43
|
} from '../api.ts'
|
|
@@ -51,7 +49,6 @@ import { ModelTab } from './model-tab.tsx'
|
|
|
51
49
|
import { SessionsTab } from './sessions-tab.tsx'
|
|
52
50
|
import {
|
|
53
51
|
fieldLabel,
|
|
54
|
-
formatBytes,
|
|
55
52
|
messageOf,
|
|
56
53
|
NoticeBar,
|
|
57
54
|
phaseBadge,
|
|
@@ -85,8 +82,6 @@ export function FeishuChannelsSection(_props: FeishuChannelsSectionProps): JSX.E
|
|
|
85
82
|
const [appSecretInput, setAppSecretInput] = useState('')
|
|
86
83
|
const [catalog, setCatalog] = useState<CatalogWire | null>(null)
|
|
87
84
|
const [catalogBusy, setCatalogBusy] = useState(false)
|
|
88
|
-
const [cleanupResult, setCleanupResult] = useState<CleanupRunWire | null>(null)
|
|
89
|
-
const [cleanupError, setCleanupError] = useState<string | null>(null)
|
|
90
85
|
const [accessNotice, setAccessNotice] = useState<string | null>(null)
|
|
91
86
|
const [accessError, setAccessError] = useState<string | null>(null)
|
|
92
87
|
const catalogTicket = useRef(0)
|
|
@@ -102,8 +97,6 @@ export function FeishuChannelsSection(_props: FeishuChannelsSectionProps): JSX.E
|
|
|
102
97
|
const loadPanel = useCallback(async (): Promise<void> => {
|
|
103
98
|
setLoad({ phase: 'loading' })
|
|
104
99
|
setNotice(null)
|
|
105
|
-
setCleanupResult(null)
|
|
106
|
-
setCleanupError(null)
|
|
107
100
|
setAccessNotice(null)
|
|
108
101
|
setAccessError(null)
|
|
109
102
|
try {
|
|
@@ -145,7 +138,7 @@ export function FeishuChannelsSection(_props: FeishuChannelsSectionProps): JSX.E
|
|
|
145
138
|
setPollError(null)
|
|
146
139
|
// A phase transition changes what the panel payload can offer: the
|
|
147
140
|
// storage domain opens/closes with the connection, so the access
|
|
148
|
-
// whitelists
|
|
141
|
+
// whitelists go stale. Re-read the panel but
|
|
149
142
|
// never touch the dirty draft.
|
|
150
143
|
if (previousPhase !== undefined && previousPhase !== next.phase) {
|
|
151
144
|
try {
|
|
@@ -260,8 +253,8 @@ export function FeishuChannelsSection(_props: FeishuChannelsSectionProps): JSX.E
|
|
|
260
253
|
}, [])
|
|
261
254
|
|
|
262
255
|
/**
|
|
263
|
-
* Manual refresh: reload the whole panel payload so access lists
|
|
264
|
-
*
|
|
256
|
+
* Manual refresh: reload the whole panel payload so access lists and
|
|
257
|
+
* `restartPendingKeys` catch up with the Host. The dirty draft is
|
|
265
258
|
* deliberately preserved — refreshing never discards user input.
|
|
266
259
|
*/
|
|
267
260
|
const onRefresh = useCallback(async (): Promise<void> => {
|
|
@@ -301,8 +294,6 @@ export function FeishuChannelsSection(_props: FeishuChannelsSectionProps): JSX.E
|
|
|
301
294
|
}
|
|
302
295
|
setLoad({ phase: 'ready', panel: result.panel })
|
|
303
296
|
setStatus(result.panel.status)
|
|
304
|
-
setCleanupResult(null)
|
|
305
|
-
setCleanupError(null)
|
|
306
297
|
// Clear only the submitted keys: edits typed while the request was in
|
|
307
298
|
// flight stay in the draft instead of being silently discarded.
|
|
308
299
|
setDraft(previous => {
|
|
@@ -372,8 +363,6 @@ export function FeishuChannelsSection(_props: FeishuChannelsSectionProps): JSX.E
|
|
|
372
363
|
setLoad({ phase: 'ready', panel: result.panel })
|
|
373
364
|
setStatus(result.panel.status)
|
|
374
365
|
setDraft({})
|
|
375
|
-
setCleanupResult(null)
|
|
376
|
-
setCleanupError(null)
|
|
377
366
|
setNotice({ kind: 'ok', text: PANEL_COPY.cleared })
|
|
378
367
|
} catch (error) {
|
|
379
368
|
setNotice({ kind: 'error', text: messageOf(error) })
|
|
@@ -417,40 +406,6 @@ export function FeishuChannelsSection(_props: FeishuChannelsSectionProps): JSX.E
|
|
|
417
406
|
}
|
|
418
407
|
}, [refreshStatus, running])
|
|
419
408
|
|
|
420
|
-
/**
|
|
421
|
-
* Delete stale workspaces after an explicit confirmation. The Host refreshes
|
|
422
|
-
* the panel in the same response, so the preview counts update immediately;
|
|
423
|
-
* partial failures stay visible in the card instead of being swallowed.
|
|
424
|
-
*/
|
|
425
|
-
const onRunCleanup = useCallback(async (): Promise<void> => {
|
|
426
|
-
if (panel === null) return
|
|
427
|
-
const { cleanup } = panel
|
|
428
|
-
const confirmed = typeof window.confirm === 'function'
|
|
429
|
-
? window.confirm(
|
|
430
|
-
`${PANEL_COPY.cleanupConfirm}\n${PANEL_COPY.cleanupCandidates}:${cleanup.candidates} · ${PANEL_COPY.cleanupCandidateBytes}:${formatBytes(cleanup.candidateBytes)}`,
|
|
431
|
-
)
|
|
432
|
-
: true
|
|
433
|
-
if (!confirmed) return
|
|
434
|
-
setBusy('cleanup')
|
|
435
|
-
setNotice(null)
|
|
436
|
-
setCleanupResult(null)
|
|
437
|
-
setCleanupError(null)
|
|
438
|
-
try {
|
|
439
|
-
const response = await runCleanup()
|
|
440
|
-
if (!response.ok) {
|
|
441
|
-
setCleanupError(response.error)
|
|
442
|
-
return
|
|
443
|
-
}
|
|
444
|
-
setLoad({ phase: 'ready', panel: response.panel })
|
|
445
|
-
setStatus(response.panel.status)
|
|
446
|
-
setCleanupResult(response.result)
|
|
447
|
-
} catch (error) {
|
|
448
|
-
setCleanupError(messageOf(error))
|
|
449
|
-
} finally {
|
|
450
|
-
setBusy(null)
|
|
451
|
-
}
|
|
452
|
-
}, [panel])
|
|
453
|
-
|
|
454
409
|
/** Replace the embedded access snapshot after a whitelist mutation. */
|
|
455
410
|
const applyAccess = useCallback((access: AccessWire): void => {
|
|
456
411
|
setLoad(previous => previous.phase === 'ready'
|
|
@@ -498,12 +453,12 @@ export function FeishuChannelsSection(_props: FeishuChannelsSectionProps): JSX.E
|
|
|
498
453
|
}
|
|
499
454
|
}, [applyAccess])
|
|
500
455
|
|
|
501
|
-
const onAddAccessUser = useCallback(async (
|
|
456
|
+
const onAddAccessUser = useCallback(async (userId: string, name: string): Promise<boolean> => {
|
|
502
457
|
setBusy('access')
|
|
503
458
|
setAccessError(null)
|
|
504
459
|
setAccessNotice(null)
|
|
505
460
|
try {
|
|
506
|
-
const result = await addAccessUser(
|
|
461
|
+
const result = await addAccessUser(userId, name)
|
|
507
462
|
if (!result.ok) {
|
|
508
463
|
setAccessError(result.error)
|
|
509
464
|
return false
|
|
@@ -519,12 +474,12 @@ export function FeishuChannelsSection(_props: FeishuChannelsSectionProps): JSX.E
|
|
|
519
474
|
}
|
|
520
475
|
}, [applyAccess])
|
|
521
476
|
|
|
522
|
-
const onRemoveAccessUser = useCallback(async (
|
|
477
|
+
const onRemoveAccessUser = useCallback(async (userId: string): Promise<void> => {
|
|
523
478
|
setBusy('access')
|
|
524
479
|
setAccessError(null)
|
|
525
480
|
setAccessNotice(null)
|
|
526
481
|
try {
|
|
527
|
-
const result = await removeAccessUser(
|
|
482
|
+
const result = await removeAccessUser(userId)
|
|
528
483
|
if (!result.ok) {
|
|
529
484
|
setAccessError(result.error)
|
|
530
485
|
return
|
|
@@ -729,13 +684,7 @@ export function FeishuChannelsSection(_props: FeishuChannelsSectionProps): JSX.E
|
|
|
729
684
|
{activeTab === 'model' ? <ModelTab form={form} catalogBusy={catalogBusy} /> : null}
|
|
730
685
|
{activeTab === 'workspace'
|
|
731
686
|
? (
|
|
732
|
-
<WorkspaceTab
|
|
733
|
-
form={form}
|
|
734
|
-
cleanup={form.panel.cleanup}
|
|
735
|
-
cleanupResult={cleanupResult}
|
|
736
|
-
cleanupError={cleanupError}
|
|
737
|
-
onRunCleanup={() => { void onRunCleanup() }}
|
|
738
|
-
/>
|
|
687
|
+
<WorkspaceTab form={form} />
|
|
739
688
|
)
|
|
740
689
|
: null}
|
|
741
690
|
{activeTab === 'access'
|
|
@@ -747,7 +696,7 @@ export function FeishuChannelsSection(_props: FeishuChannelsSectionProps): JSX.E
|
|
|
747
696
|
onAddGroup={onAddAccessGroup}
|
|
748
697
|
onRemoveGroup={chatId => { void onRemoveAccessGroup(chatId) }}
|
|
749
698
|
onAddUser={onAddAccessUser}
|
|
750
|
-
onRemoveUser={
|
|
699
|
+
onRemoveUser={userId => { void onRemoveAccessUser(userId) }}
|
|
751
700
|
onResolveRequest={(token, decision) => { void onResolveAccessRequest(token, decision) }}
|
|
752
701
|
/>
|
|
753
702
|
)
|
|
@@ -13,7 +13,6 @@ import { FIELD_GROUP_LABELS, PANEL_COPY, TAB_FIELD_GROUPS } from '../../settings
|
|
|
13
13
|
import type {
|
|
14
14
|
AccessWire,
|
|
15
15
|
ChannelPhaseWire,
|
|
16
|
-
CleanupPreviewWire,
|
|
17
16
|
ConfigValue,
|
|
18
17
|
CredentialField,
|
|
19
18
|
EditableFieldWire,
|
|
@@ -46,7 +45,6 @@ export type BusyAction =
|
|
|
46
45
|
| 'reconnect'
|
|
47
46
|
| 'credentials'
|
|
48
47
|
| 'credentials-clear'
|
|
49
|
-
| 'cleanup'
|
|
50
48
|
| 'access'
|
|
51
49
|
|
|
52
50
|
/** Tone of a small badge; maps onto `dsh-channels-cfg__badge--*` classes. */
|
|
@@ -56,20 +54,6 @@ export function messageOf(error: unknown): string {
|
|
|
56
54
|
return error instanceof Error ? error.message : String(error)
|
|
57
55
|
}
|
|
58
56
|
|
|
59
|
-
/** Human-readable byte size for the cleanup preview/result (base-2 units). */
|
|
60
|
-
export function formatBytes(bytes: number): string {
|
|
61
|
-
if (!Number.isFinite(bytes) || bytes <= 0) return '0 B'
|
|
62
|
-
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
|
63
|
-
let value = bytes
|
|
64
|
-
let unit = 0
|
|
65
|
-
while (value >= 1024 && unit < units.length - 1) {
|
|
66
|
-
value /= 1024
|
|
67
|
-
unit += 1
|
|
68
|
-
}
|
|
69
|
-
const digits = unit === 0 || value >= 10 ? 0 : 1
|
|
70
|
-
return `${value.toFixed(digits)} ${units[unit] ?? 'B'}`
|
|
71
|
-
}
|
|
72
|
-
|
|
73
57
|
/** Compact duration for the connection card. */
|
|
74
58
|
export function formatUptime(ms: number | undefined): string {
|
|
75
59
|
if (ms === undefined || !Number.isFinite(ms)) return PANEL_COPY.unknownValue
|
|
@@ -479,10 +463,3 @@ export function connectionStateLabel(state: string | undefined): { text: string;
|
|
|
479
463
|
default: return { text: state ?? PANEL_COPY.unknownValue, tone: 'muted' }
|
|
480
464
|
}
|
|
481
465
|
}
|
|
482
|
-
|
|
483
|
-
/** Retention label, reusing the Host field options so the copy stays on the Host. */
|
|
484
|
-
export function retentionLabel(panel: PanelWire, cleanup: CleanupPreviewWire): string {
|
|
485
|
-
const field = panel.config.fields.find(item => item.key === 'workspaceRetention')
|
|
486
|
-
return field?.options?.find(option => option.value === cleanup.retention)?.label
|
|
487
|
-
?? (cleanup.retention === 'off' ? PANEL_COPY.cleanupRetentionOff : cleanup.retention)
|
|
488
|
-
}
|
|
@@ -1,40 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 「工作区」tab
|
|
3
|
-
* 表单保存栏在字段卡正下方,清理卡(独立动作,不走 draft)在其后。
|
|
2
|
+
* 「工作区」tab:工作区根目录字段与保存栏。
|
|
4
3
|
*
|
|
5
4
|
* @module dsh-channels/client/panel/workspace-tab
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
import { PANEL_COPY, TAB_FIELD_GROUPS } from '../../settings/contract.ts'
|
|
9
|
-
import type { CleanupPreviewWire, CleanupRunWire } from '../../settings/contract.ts'
|
|
10
|
-
import { CleanupCard } from './cleanup-card.tsx'
|
|
11
8
|
import { FieldsCard, SaveBar, TabHelp } from './shared.tsx'
|
|
12
9
|
import type { FormView } from './shared.tsx'
|
|
13
10
|
|
|
14
11
|
export interface WorkspaceTabProps {
|
|
15
12
|
form: FormView
|
|
16
|
-
cleanup: CleanupPreviewWire
|
|
17
|
-
cleanupResult: CleanupRunWire | null
|
|
18
|
-
cleanupError: string | null
|
|
19
|
-
onRunCleanup: () => void
|
|
20
13
|
}
|
|
21
14
|
|
|
22
15
|
export function WorkspaceTab(props: WorkspaceTabProps): JSX.Element {
|
|
23
|
-
const { form
|
|
16
|
+
const { form } = props
|
|
24
17
|
return (
|
|
25
18
|
<div className="dsh-channels-cfg__tabBody">
|
|
26
19
|
<TabHelp text={PANEL_COPY.workspaceTabHelp} />
|
|
27
20
|
<FieldsCard form={form} groups={TAB_FIELD_GROUPS.workspace} />
|
|
28
21
|
<SaveBar form={form} tab="workspace" />
|
|
29
|
-
<CleanupCard
|
|
30
|
-
panel={form.panel}
|
|
31
|
-
cleanup={cleanup}
|
|
32
|
-
busy={form.busy}
|
|
33
|
-
locked={form.locked}
|
|
34
|
-
result={cleanupResult}
|
|
35
|
-
error={cleanupError}
|
|
36
|
-
onRun={onRunCleanup}
|
|
37
|
-
/>
|
|
38
22
|
</div>
|
|
39
23
|
)
|
|
40
24
|
}
|
package/src/client/styles.ts
CHANGED
|
@@ -68,6 +68,8 @@ select.dsh-channels-cfg__input{appearance:auto;cursor:pointer}
|
|
|
68
68
|
.dsh-channels-cfg__notice{display:flex;flex-direction:column;gap:2px;padding:8px 10px;border-radius:8px;font-size:12px;line-height:18px;overflow-wrap:anywhere}
|
|
69
69
|
.dsh-channels-cfg__notice--ok{color:var(--dsw-alias-state-business-primary,#0a7d33);background:color-mix(in srgb,currentColor 8%,transparent)}
|
|
70
70
|
.dsh-channels-cfg__notice--error{color:var(--dsw-alias-state-danger-primary,#c62828);background:color-mix(in srgb,currentColor 8%,transparent)}
|
|
71
|
+
.dsh-channels-cfg__notice a{color:inherit;text-decoration:underline}
|
|
72
|
+
.dsh-channels-cfg__scopeList{margin:2px 0;padding-left:18px;display:flex;flex-direction:column;gap:2px}
|
|
71
73
|
.dsh-channels-cfg__banner{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:8px 10px;border-radius:8px;border:1px dashed var(--dsw-alias-state-warning-primary,#a15c00);color:var(--dsw-alias-state-warning-primary,#a15c00);font-size:12px;line-height:18px}
|
|
72
74
|
.dsh-channels-cfg__footer{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding-top:2px}
|
|
73
75
|
.dsh-channels-cfg__spacer{flex:1}
|
package/src/commands/registry.ts
CHANGED
|
@@ -72,8 +72,8 @@ export const CHANNEL_COMMANDS = [
|
|
|
72
72
|
usage: '[name]', channelPlane: true, agentScoped: false,
|
|
73
73
|
},
|
|
74
74
|
{
|
|
75
|
-
name: 'auth', group: '会话', description: '
|
|
76
|
-
usage: '[status|allow|revoke|list|user add
|
|
75
|
+
name: 'auth', group: '会话', description: '查看/管理访问授权,可 @ 成员授权(仅管理员)',
|
|
76
|
+
usage: '[status|allow|revoke|list|user add <@用户|user_id>|user remove <@用户|user_id>]',
|
|
77
77
|
routeFree: true, channelPlane: true, agentScoped: false,
|
|
78
78
|
},
|
|
79
79
|
{
|
package/src/config.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { z } from 'zod'
|
|
16
|
-
import {
|
|
16
|
+
import { FEISHU_USER_ID_PATTERN } from './domain.ts'
|
|
17
17
|
|
|
18
18
|
export const FEISHU_LOGGER_LEVELS = ['debug', 'info', 'warn', 'error'] as const
|
|
19
19
|
export type FeishuLoggerLevel = (typeof FEISHU_LOGGER_LEVELS)[number]
|
|
@@ -38,12 +38,6 @@ export const DEFAULT_QUESTION_TIMEOUT_MS = 5 * 60 * 1000
|
|
|
38
38
|
export const ATTACHMENT_STORE_MODES = ['native', 'archive'] as const
|
|
39
39
|
export type AttachmentStoreMode = (typeof ATTACHMENT_STORE_MODES)[number]
|
|
40
40
|
|
|
41
|
-
/** Default retention of silently registered attachment metadata: 7 days. */
|
|
42
|
-
export const DEFAULT_PENDING_ATTACHMENT_TTL_MS = 7 * 24 * 60 * 60 * 1000
|
|
43
|
-
|
|
44
|
-
/** Default per-`(chat, thread, sender)` cap of pending attachment rows. */
|
|
45
|
-
export const DEFAULT_PENDING_ATTACHMENT_LIMIT = 10
|
|
46
|
-
|
|
47
41
|
/** Default per-call attachment count of `feishu_attachment_fetch`. */
|
|
48
42
|
export const DEFAULT_ATTACHMENT_FETCH_MAX_RESOURCES = 10
|
|
49
43
|
|
|
@@ -103,13 +97,6 @@ export const configSchema = z.object({
|
|
|
103
97
|
* against the host process cwd.
|
|
104
98
|
*/
|
|
105
99
|
baseDir: z.string().min(1).default('./channels-data'),
|
|
106
|
-
/**
|
|
107
|
-
* Optional path of the Web settings overlay (default
|
|
108
|
-
* `$DSH_HOME/dsh-channels/settings.json`). The Web panel writes only
|
|
109
|
-
* editable fields there; this path is for embedded hosts, tests, or users
|
|
110
|
-
* who want multiple instances with different overlays.
|
|
111
|
-
*/
|
|
112
|
-
settingsFile: z.string().min(1).optional(),
|
|
113
100
|
/** LLM provider route for sessions this plugin creates (optional → deployment default). */
|
|
114
101
|
provider: z.string().optional(),
|
|
115
102
|
/** Model id for sessions this plugin creates (optional → deployment default). */
|
|
@@ -193,7 +180,7 @@ export const configSchema = z.object({
|
|
|
193
180
|
* behind a prompt manifest; with the archive backend the resources are saved
|
|
194
181
|
* below `<workspace>/attachments/` before the turn, exactly like the legacy
|
|
195
182
|
* pipeline. Disabled keeps attachment messages inert (no save, no turn, no
|
|
196
|
-
*
|
|
183
|
+
* manifest rows).
|
|
197
184
|
*/
|
|
198
185
|
acceptAttachments: z.boolean().default(true),
|
|
199
186
|
/**
|
|
@@ -263,24 +250,6 @@ export const configSchema = z.object({
|
|
|
263
250
|
*/
|
|
264
251
|
promptContextTimeoutMs: z.number().int().min(100).max(10_000)
|
|
265
252
|
.default(DEFAULT_PROMPT_CONTEXT_TIMEOUT_MS),
|
|
266
|
-
/**
|
|
267
|
-
* Retention of silently registered attachment metadata (the
|
|
268
|
-
* `pending_attachments` table). A message that does not trigger a model turn
|
|
269
|
-
* (attachment-only p2p message, un-mentioned group message, attachment-only
|
|
270
|
-
* message inside a bound topic) is recorded as metadata only; a later
|
|
271
|
-
* triggered message in the same `(chat, thread, sender)` scope may list and
|
|
272
|
-
* fetch it. Rows older than this window are removed by the periodic record
|
|
273
|
-
* GC. `0` disables age pruning (the per-scope cap still bounds each scope).
|
|
274
|
-
* Default 7 days.
|
|
275
|
-
*/
|
|
276
|
-
pendingAttachmentTtlMs: z.number().int().min(0).default(DEFAULT_PENDING_ATTACHMENT_TTL_MS),
|
|
277
|
-
/**
|
|
278
|
-
* Maximum silently registered attachment rows kept per `(chat, thread,
|
|
279
|
-
* sender)` scope. Registering beyond the cap evicts the oldest row of that
|
|
280
|
-
* scope; the prompt manifest only ever shows the most recent 5 unconsumed
|
|
281
|
-
* rows. Default 10.
|
|
282
|
-
*/
|
|
283
|
-
pendingAttachmentLimit: z.number().int().min(1).max(100).default(DEFAULT_PENDING_ATTACHMENT_LIMIT),
|
|
284
253
|
/**
|
|
285
254
|
* Model-facing Feishu tools.
|
|
286
255
|
*
|
|
@@ -312,6 +281,11 @@ export const configSchema = z.object({
|
|
|
312
281
|
* - `topicMessagesTimeoutMs`: whole-call timeout of one model-initiated
|
|
313
282
|
* topic read (default 20 s). Passive prompt excerpts use a separate short
|
|
314
283
|
* (~3 s) budget and never wait this long.
|
|
284
|
+
* - `sendFile` (default on): register `feishu_send_file` in channel sessions
|
|
285
|
+
* so the model can send an image or file from the topic's working
|
|
286
|
+
* directory back to the current conversation. Needs the Feishu
|
|
287
|
+
* `im:resource` (or `im:resource:upload`) scope; without it a call returns
|
|
288
|
+
* a readable failure and the turn continues.
|
|
315
289
|
*
|
|
316
290
|
* The Web panel/metrics ship with `panel_docs_release`.
|
|
317
291
|
*/
|
|
@@ -326,6 +300,7 @@ export const configSchema = z.object({
|
|
|
326
300
|
topicMessages: z.boolean().default(true),
|
|
327
301
|
topicMessagesTimeoutMs: z.number().int().min(1_000).max(120_000)
|
|
328
302
|
.default(DEFAULT_TOPIC_MESSAGES_TIMEOUT_MS),
|
|
303
|
+
sendFile: z.boolean().default(true),
|
|
329
304
|
}).default({
|
|
330
305
|
attachmentFetch: true,
|
|
331
306
|
attachmentFetchMaxResources: DEFAULT_ATTACHMENT_FETCH_MAX_RESOURCES,
|
|
@@ -333,28 +308,20 @@ export const configSchema = z.object({
|
|
|
333
308
|
attachmentFetchTimeoutMs: DEFAULT_ATTACHMENT_FETCH_TIMEOUT_MS,
|
|
334
309
|
topicMessages: true,
|
|
335
310
|
topicMessagesTimeoutMs: DEFAULT_TOPIC_MESSAGES_TIMEOUT_MS,
|
|
311
|
+
sendFile: true,
|
|
336
312
|
}),
|
|
337
|
-
/**
|
|
338
|
-
* Delete the topic directories of stale conversations, plus legacy
|
|
339
|
-
* one-workspace-per-topic directories written by older plugin versions.
|
|
340
|
-
* `off` (default) never deletes; `7d`/`30d`/`90d`/`180d` remove those
|
|
341
|
-
* directories when their last activity is older than the threshold. User
|
|
342
|
-
* workspaces themselves are never deleted. The managed Web panel exposes
|
|
343
|
-
* this as a select, previews the candidates, and can run one cleanup
|
|
344
|
-
* manually. DSH Session logs are never deleted.
|
|
345
|
-
*/
|
|
346
|
-
workspaceRetention: z.enum(WORKSPACE_RETENTION_VALUES).default('off'),
|
|
347
|
-
/**
|
|
348
|
-
* Run the retention cleanup at startup and once per day. Off by default:
|
|
349
|
-
* deletion is irreversible, so it stays an explicit opt-in even when a
|
|
350
|
-
* retention threshold is configured.
|
|
351
|
-
*/
|
|
352
|
-
workspaceRetentionAuto: z.boolean().default(false),
|
|
353
313
|
/**
|
|
354
314
|
* Send a one-time onboarding card on the first private message of a chat
|
|
355
315
|
* (facts + command cheat sheet). Persisted per chat; never repeats.
|
|
356
316
|
*/
|
|
357
317
|
welcomeOnboarding: z.boolean().default(true),
|
|
318
|
+
/**
|
|
319
|
+
* Sync the command registry to Feishu's native `/` panel on every connect
|
|
320
|
+
* (needs the `application:app_slash_command:read`/`write` scopes). Off by
|
|
321
|
+
* default: some tenants forbid apps from adding slash commands. When off,
|
|
322
|
+
* the plugin neither reads nor writes the panel.
|
|
323
|
+
*/
|
|
324
|
+
slashCommandPanel: z.boolean().default(false),
|
|
358
325
|
/**
|
|
359
326
|
* Allow `/permissions <name>` to switch this Session's sandbox + approval
|
|
360
327
|
* preset from Feishu. Read-only `/permissions` always works; switching is
|
|
@@ -373,15 +340,17 @@ export const configSchema = z.object({
|
|
|
373
340
|
*/
|
|
374
341
|
accessMode: z.enum(['open', 'guarded']).default('guarded'),
|
|
375
342
|
/**
|
|
376
|
-
* The unique administrator's Feishu
|
|
343
|
+
* The unique administrator's Feishu `user_id`. Set it in the Web
|
|
377
344
|
* settings panel (or the profile config). Get it by sending `/userid` to the
|
|
378
|
-
* bot in a private chat.
|
|
379
|
-
*
|
|
380
|
-
*
|
|
345
|
+
* bot in a private chat. When unset, the Feishu app owner (looked up via
|
|
346
|
+
* `application.v6.application.get` at connect; needs an app-info read scope)
|
|
347
|
+
* is the administrator. While `accessMode` is `guarded` and neither is
|
|
348
|
+
* available, every conversation is refused (fail closed); `/userid` and a
|
|
349
|
+
* fixed notice stay available so the deployment can be bootstrapped.
|
|
381
350
|
*/
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
'must be a Feishu
|
|
351
|
+
adminUserId: z.string().regex(
|
|
352
|
+
FEISHU_USER_ID_PATTERN,
|
|
353
|
+
'must be a Feishu user_id, not an open_id (send /userid to the bot in a private chat to see it)',
|
|
385
354
|
).optional(),
|
|
386
355
|
/**
|
|
387
356
|
* Lifetime of one pending authorization request and of the DM card that asks
|