@modesto_ma/dsh-channels 0.3.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 +123 -53
- package/docs/access-control-design.md +7 -5
- package/docs/attachment-pipeline-v2.md +310 -0
- package/docs/feasibility-and-design.md +1 -1
- package/docs/feishu-onboarding.md +127 -21
- package/docs/persistence.md +6 -4
- package/docs/refactor-plan.md +2 -2
- package/docs/workspace-isolation-design.md +13 -11
- package/lib/client.js +179 -288
- package/lib/client.js.map +1 -1
- package/lib/index.js +12125 -6382
- 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 +190 -32
- package/src/domain.ts +28 -18
- package/src/feishu/agent-context.ts +65 -19
- package/src/feishu/inbound-attachments.ts +440 -3
- package/src/feishu/manifest.ts +247 -0
- package/src/feishu/message-read.ts +1330 -0
- package/src/feishu/native-attachments.ts +998 -0
- 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 +2305 -0
- package/src/feishu-channel.ts +322 -6
- package/src/host/agent.ts +55 -0
- package/src/host/attachments.ts +210 -0
- package/src/host/index.ts +1 -0
- 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 +620 -49
- package/src/service.ts +808 -375
- package/src/session/manager.ts +67 -13
- package/src/session/routes.ts +2 -3
- package/src/settings/contract.ts +90 -89
- package/src/settings/overlay.ts +124 -13
- package/src/settings/routes.ts +3 -17
- package/src/settings/surface.ts +156 -82
- package/src/streamer.ts +94 -5
- package/src/workspaces.ts +39 -26
- package/src/cleanup.ts +0 -535
- package/src/client/panel/cleanup-card.tsx +0 -106
|
@@ -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]
|
|
@@ -34,6 +34,48 @@ export const DEFAULT_APPROVAL_TIMEOUT_MS = 5 * 60 * 1000
|
|
|
34
34
|
/** Default structured-question window: 5 minutes per `ask_user_question` call. */
|
|
35
35
|
export const DEFAULT_QUESTION_TIMEOUT_MS = 5 * 60 * 1000
|
|
36
36
|
|
|
37
|
+
/** Backends for inbound attachment storage, selected by `attachmentStore`. */
|
|
38
|
+
export const ATTACHMENT_STORE_MODES = ['native', 'archive'] as const
|
|
39
|
+
export type AttachmentStoreMode = (typeof ATTACHMENT_STORE_MODES)[number]
|
|
40
|
+
|
|
41
|
+
/** Default per-call attachment count of `feishu_attachment_fetch`. */
|
|
42
|
+
export const DEFAULT_ATTACHMENT_FETCH_MAX_RESOURCES = 10
|
|
43
|
+
|
|
44
|
+
/** Default per-call total download budget of `feishu_attachment_fetch` (100 MiB). */
|
|
45
|
+
export const DEFAULT_ATTACHMENT_FETCH_MAX_BYTES = 100 * 1024 * 1024
|
|
46
|
+
|
|
47
|
+
/** Default whole-call timeout of `feishu_attachment_fetch` (120 s). */
|
|
48
|
+
export const DEFAULT_ATTACHMENT_FETCH_TIMEOUT_MS = 120_000
|
|
49
|
+
|
|
50
|
+
/** Default per-image byte cap for trigger-message prefetch (20 MiB). */
|
|
51
|
+
export const DEFAULT_PREFETCH_IMAGE_MAX_BYTES = 20 * 1024 * 1024
|
|
52
|
+
|
|
53
|
+
/** Default total byte budget for one trigger-message image prefetch (50 MiB). */
|
|
54
|
+
export const DEFAULT_PREFETCH_TOTAL_MAX_BYTES = 50 * 1024 * 1024
|
|
55
|
+
|
|
56
|
+
/** Default short timeout for one trigger-message image prefetch (10 s). */
|
|
57
|
+
export const DEFAULT_PREFETCH_TIMEOUT_MS = 10_000
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Default number of recent topic messages attached to a new Session's first
|
|
61
|
+
* turn (0 disables the history bootstrap). Mirrors
|
|
62
|
+
* `HISTORY_BOOTSTRAP_DEFAULT` in `feishu/message-read.ts`.
|
|
63
|
+
*/
|
|
64
|
+
export const DEFAULT_HISTORY_BOOTSTRAP_LIMIT = 10
|
|
65
|
+
|
|
66
|
+
/** Hard cap of the first-turn history bootstrap. */
|
|
67
|
+
export const MAX_HISTORY_BOOTSTRAP_LIMIT = 20
|
|
68
|
+
|
|
69
|
+
/** Default whole-call timeout of `feishu_topic_messages` (20 s). */
|
|
70
|
+
export const DEFAULT_TOPIC_MESSAGES_TIMEOUT_MS = 20_000
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Default budget of one passive prompt-enrichment message read (quoted excerpt
|
|
74
|
+
* / first-turn topic history). Kept short: the turn must not wait long for an
|
|
75
|
+
* optional context block.
|
|
76
|
+
*/
|
|
77
|
+
export const DEFAULT_PROMPT_CONTEXT_TIMEOUT_MS = 3_000
|
|
78
|
+
|
|
37
79
|
/** A credential reference: `FEISHU_APP_ID`-style name resolved through ctx.credentials. */
|
|
38
80
|
const credentialRefSchema = z.string().regex(
|
|
39
81
|
CREDENTIAL_REF_PATTERN,
|
|
@@ -55,13 +97,6 @@ export const configSchema = z.object({
|
|
|
55
97
|
* against the host process cwd.
|
|
56
98
|
*/
|
|
57
99
|
baseDir: z.string().min(1).default('./channels-data'),
|
|
58
|
-
/**
|
|
59
|
-
* Optional path of the Web settings overlay (default
|
|
60
|
-
* `$DSH_HOME/dsh-channels/settings.json`). The Web panel writes only
|
|
61
|
-
* editable fields there; this path is for embedded hosts, tests, or users
|
|
62
|
-
* who want multiple instances with different overlays.
|
|
63
|
-
*/
|
|
64
|
-
settingsFile: z.string().min(1).optional(),
|
|
65
100
|
/** LLM provider route for sessions this plugin creates (optional → deployment default). */
|
|
66
101
|
provider: z.string().optional(),
|
|
67
102
|
/** Model id for sessions this plugin creates (optional → deployment default). */
|
|
@@ -138,34 +173,155 @@ export const configSchema = z.object({
|
|
|
138
173
|
*/
|
|
139
174
|
approvalTimeoutMs: z.number().int().min(10_000).max(3_600_000).default(DEFAULT_APPROVAL_TIMEOUT_MS),
|
|
140
175
|
/**
|
|
141
|
-
* Accept inbound Feishu images, files, and merged-forward chat records
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
176
|
+
* Accept inbound Feishu images, files, and merged-forward chat records.
|
|
177
|
+
*
|
|
178
|
+
* With the native backend (default) images of the triggering message are
|
|
179
|
+
* prefetched into the DSH native attachment library and files stay lazy
|
|
180
|
+
* behind a prompt manifest; with the archive backend the resources are saved
|
|
181
|
+
* below `<workspace>/attachments/` before the turn, exactly like the legacy
|
|
182
|
+
* pipeline. Disabled keeps attachment messages inert (no save, no turn, no
|
|
183
|
+
* manifest rows).
|
|
146
184
|
*/
|
|
147
185
|
acceptAttachments: z.boolean().default(true),
|
|
148
186
|
/**
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* `
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
187
|
+
* Backend for inbound Feishu images and files.
|
|
188
|
+
*
|
|
189
|
+
* - `native` (default): store into the DSH native attachment library
|
|
190
|
+
* (`ctx.attachments`) as durable `ImageBlock`/`FileBlock` references; if
|
|
191
|
+
* the host does not mount that service, the storage layer falls back to
|
|
192
|
+
* `archive` for that deployment.
|
|
193
|
+
* - `archive`: legacy behavior — write the resources into the topic
|
|
194
|
+
* workspace `attachments/` directory through the existing intake.
|
|
195
|
+
*
|
|
196
|
+
* Admission consumes this switch through the backend-selecting inbound
|
|
197
|
+
* store: an explicit `archive` always uses the legacy download path, and a
|
|
198
|
+
* `native` deployment without the mounted host service degrades to the same
|
|
199
|
+
* legacy path (see `effectiveAttachmentMode`).
|
|
200
|
+
*/
|
|
201
|
+
attachmentStore: z.enum(ATTACHMENT_STORE_MODES).default('native'),
|
|
202
|
+
/**
|
|
203
|
+
* Prefetch images attached to the triggering message into the first model
|
|
204
|
+
* turn (native attachment backend only). The session, topic, and streaming
|
|
205
|
+
* card are already visible before the prefetch starts; the turn only waits
|
|
206
|
+
* for the (short, bounded) image download, never for file bytes. Failed,
|
|
207
|
+
* oversized, or timed-out images degrade to a manifest row that the model
|
|
208
|
+
* can fetch with `feishu_attachment_fetch`. Set `false` to keep every
|
|
209
|
+
* attachment lazy (images then need the tool as well).
|
|
210
|
+
*/
|
|
211
|
+
prefetchImages: z.boolean().default(true),
|
|
212
|
+
/**
|
|
213
|
+
* Largest single image the trigger-message prefetch buffers in memory before
|
|
214
|
+
* refusing it (default 20 MiB). A refused image stays in the manifest and
|
|
215
|
+
* is fetched on demand.
|
|
216
|
+
*/
|
|
217
|
+
prefetchImageMaxBytes: z.number().int().min(1).max(200 * 1024 * 1024)
|
|
218
|
+
.default(DEFAULT_PREFETCH_IMAGE_MAX_BYTES),
|
|
219
|
+
/**
|
|
220
|
+
* Total byte budget of one trigger-message image prefetch (default 50 MiB).
|
|
221
|
+
* Once exhausted, the remaining images degrade to the manifest.
|
|
222
|
+
*/
|
|
223
|
+
prefetchTotalMaxBytes: z.number().int().min(1).max(1024 * 1024 * 1024)
|
|
224
|
+
.default(DEFAULT_PREFETCH_TOTAL_MAX_BYTES),
|
|
225
|
+
/**
|
|
226
|
+
* Short whole-prefetch timeout (default 10 s). On expiry the already-loaded
|
|
227
|
+
* images stay in the prompt and the rest degrade to the manifest, so a slow
|
|
228
|
+
* image download never blocks the turn for long.
|
|
229
|
+
*/
|
|
230
|
+
prefetchTimeoutMs: z.number().int().min(100).max(120_000).default(DEFAULT_PREFETCH_TIMEOUT_MS),
|
|
231
|
+
/**
|
|
232
|
+
* Number of recent messages of the current topic automatically attached to
|
|
233
|
+
* the FIRST turn of a new Session (decision D1: the model should see the
|
|
234
|
+
* discussion before @ without asking). The `<feishu_history>` block is
|
|
235
|
+
* bounded and truncated with an explicit marker and each line carries the
|
|
236
|
+
* message_id needed to fetch a named attachment; older messages remain
|
|
237
|
+
* reachable through the `feishu_topic_messages` tool. `0` disables the
|
|
238
|
+
* bootstrap entirely. Reads use a short (~3 s) prompt-enrichment budget and a
|
|
239
|
+
* failure (missing scope, p2p history unavailable, channel down) degrades
|
|
240
|
+
* silently: the turn still runs.
|
|
156
241
|
*/
|
|
157
|
-
|
|
242
|
+
historyBootstrapLimit: z.number().int().min(0).max(MAX_HISTORY_BOOTSTRAP_LIMIT)
|
|
243
|
+
.default(DEFAULT_HISTORY_BOOTSTRAP_LIMIT),
|
|
158
244
|
/**
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
245
|
+
* Whole-read budget (milliseconds) of the passive prompt enrichment: the
|
|
246
|
+
* quoted-message excerpt and the first-turn topic history. Deliberately short
|
|
247
|
+
* (default 3 s) and independent from `feishuTools.topicMessagesTimeoutMs`:
|
|
248
|
+
* on expiry the block is omitted and the turn runs. Model-initiated reads
|
|
249
|
+
* through `feishu_topic_messages` use the tool timeout instead.
|
|
162
250
|
*/
|
|
163
|
-
|
|
251
|
+
promptContextTimeoutMs: z.number().int().min(100).max(10_000)
|
|
252
|
+
.default(DEFAULT_PROMPT_CONTEXT_TIMEOUT_MS),
|
|
253
|
+
/**
|
|
254
|
+
* Model-facing Feishu tools.
|
|
255
|
+
*
|
|
256
|
+
* - `attachmentFetch` (default on): register `feishu_attachment_fetch` in
|
|
257
|
+
* channel sessions so the model can fetch images/files of a known message
|
|
258
|
+
* on demand. Registration is skipped (with one warning) on deployments
|
|
259
|
+
* without the host `tools` / `attachments` services. Turning it off also
|
|
260
|
+
* keeps inbound attachments on the archive backend: lazy native files
|
|
261
|
+
* would be unreachable without the tool, so admission downloads them to
|
|
262
|
+
* the workspace instead (see `effectiveAttachmentMode`).
|
|
263
|
+
* - `attachmentFetchMaxResources`: maximum attachments one tool call may
|
|
264
|
+
* fetch (1–50). A message with more attachments is refused with a readable
|
|
265
|
+
* reason so the model can retry per `file_key`.
|
|
266
|
+
* - `attachmentFetchMaxBytes`: total download budget of one tool call
|
|
267
|
+
* (default 100 MiB). Files stream to the native attachment library, so the
|
|
268
|
+
* limit is enforced while streaming rather than buffered first.
|
|
269
|
+
* - `attachmentFetchTimeoutMs`: whole-call timeout (default 120 s). On expiry
|
|
270
|
+
* the in-flight download is cancelled and reported per attachment.
|
|
271
|
+
* - `topicMessages` (default on): register `feishu_topic_messages` in
|
|
272
|
+
* channel sessions so the model can read the quoted message / earlier
|
|
273
|
+
* topic messages on demand and authorize their attachments for fetch.
|
|
274
|
+
* Reads require the Feishu message-read scopes; a missing scope degrades to
|
|
275
|
+
* a readable tool failure and never fails the turn. This switch only
|
|
276
|
+
* controls the model-facing tool: the passive `<feishu_quote>` /
|
|
277
|
+
* `<feishu_history>` prompt excerpts stay enabled (they are bounded and
|
|
278
|
+
* degrade silently) and are configured by `historyBootstrapLimit`. On the
|
|
279
|
+
* archive backend the tool is still mounted for history text, while its
|
|
280
|
+
* description states that `feishu_attachment_fetch` is unavailable.
|
|
281
|
+
* - `topicMessagesTimeoutMs`: whole-call timeout of one model-initiated
|
|
282
|
+
* topic read (default 20 s). Passive prompt excerpts use a separate short
|
|
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.
|
|
289
|
+
*
|
|
290
|
+
* The Web panel/metrics ship with `panel_docs_release`.
|
|
291
|
+
*/
|
|
292
|
+
feishuTools: z.object({
|
|
293
|
+
attachmentFetch: z.boolean().default(true),
|
|
294
|
+
attachmentFetchMaxResources: z.number().int().min(1).max(50)
|
|
295
|
+
.default(DEFAULT_ATTACHMENT_FETCH_MAX_RESOURCES),
|
|
296
|
+
attachmentFetchMaxBytes: z.number().int().min(1).max(1024 * 1024 * 1024)
|
|
297
|
+
.default(DEFAULT_ATTACHMENT_FETCH_MAX_BYTES),
|
|
298
|
+
attachmentFetchTimeoutMs: z.number().int().min(1_000).max(600_000)
|
|
299
|
+
.default(DEFAULT_ATTACHMENT_FETCH_TIMEOUT_MS),
|
|
300
|
+
topicMessages: z.boolean().default(true),
|
|
301
|
+
topicMessagesTimeoutMs: z.number().int().min(1_000).max(120_000)
|
|
302
|
+
.default(DEFAULT_TOPIC_MESSAGES_TIMEOUT_MS),
|
|
303
|
+
sendFile: z.boolean().default(true),
|
|
304
|
+
}).default({
|
|
305
|
+
attachmentFetch: true,
|
|
306
|
+
attachmentFetchMaxResources: DEFAULT_ATTACHMENT_FETCH_MAX_RESOURCES,
|
|
307
|
+
attachmentFetchMaxBytes: DEFAULT_ATTACHMENT_FETCH_MAX_BYTES,
|
|
308
|
+
attachmentFetchTimeoutMs: DEFAULT_ATTACHMENT_FETCH_TIMEOUT_MS,
|
|
309
|
+
topicMessages: true,
|
|
310
|
+
topicMessagesTimeoutMs: DEFAULT_TOPIC_MESSAGES_TIMEOUT_MS,
|
|
311
|
+
sendFile: true,
|
|
312
|
+
}),
|
|
164
313
|
/**
|
|
165
314
|
* Send a one-time onboarding card on the first private message of a chat
|
|
166
315
|
* (facts + command cheat sheet). Persisted per chat; never repeats.
|
|
167
316
|
*/
|
|
168
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),
|
|
169
325
|
/**
|
|
170
326
|
* Allow `/permissions <name>` to switch this Session's sandbox + approval
|
|
171
327
|
* preset from Feishu. Read-only `/permissions` always works; switching is
|
|
@@ -184,15 +340,17 @@ export const configSchema = z.object({
|
|
|
184
340
|
*/
|
|
185
341
|
accessMode: z.enum(['open', 'guarded']).default('guarded'),
|
|
186
342
|
/**
|
|
187
|
-
* The unique administrator's Feishu
|
|
343
|
+
* The unique administrator's Feishu `user_id`. Set it in the Web
|
|
188
344
|
* settings panel (or the profile config). Get it by sending `/userid` to the
|
|
189
|
-
* bot in a private chat.
|
|
190
|
-
*
|
|
191
|
-
*
|
|
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.
|
|
192
350
|
*/
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
'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)',
|
|
196
354
|
).optional(),
|
|
197
355
|
/**
|
|
198
356
|
* Lifetime of one pending authorization request and of the DM card that asks
|