@modesto_ma/dsh-channels 0.5.0-beta.3 → 0.5.0-beta.4
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 +69 -12
- package/docs/feishu-onboarding.md +1 -1
- package/lib/client.js +369 -94
- package/lib/client.js.map +1 -1
- package/lib/index.js +7549 -6353
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/access/controller.ts +32 -1
- package/src/analyze/contract.ts +121 -0
- package/src/analyze/launcher.ts +353 -0
- package/src/analyze/server.ts +201 -0
- package/src/cards/analyze.ts +116 -0
- package/src/cards/index.ts +1 -0
- package/src/cards/streaming.ts +58 -0
- package/src/client/api.ts +15 -0
- package/src/client/panel/access-card.tsx +96 -70
- package/src/client/panel/analyze-token-card.tsx +70 -0
- package/src/client/panel/index.tsx +72 -10
- package/src/client/panel/sessions-tab.tsx +7 -4
- package/src/client/panel/shared.tsx +12 -0
- package/src/client/panel/wiki-tab.tsx +62 -0
- package/src/config.ts +30 -4
- package/src/domain.ts +55 -0
- package/src/feishu/scopes.ts +13 -7
- package/src/feishu/tools.ts +3 -1
- package/src/health.ts +12 -0
- package/src/host/agent.ts +5 -1
- package/src/index.ts +1 -1
- package/src/interactions/pending.ts +11 -2
- package/src/routing/inbound.ts +57 -14
- package/src/service.ts +161 -28
- package/src/settings/contract.ts +37 -6
- package/src/settings/routes.ts +66 -14
- package/src/settings/surface.ts +65 -16
- package/src/streamer.ts +156 -7
- package/src/workspaces.ts +31 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 日志分析接口令牌编辑器:值经 Host 的 DSH 凭据服务写入本机凭据库,面板只显示
|
|
3
|
+
* 「已配置 / 未配置」,不回显已存值;保存或移除后下一次接口请求即生效,不触发重连。
|
|
4
|
+
*
|
|
5
|
+
* @module dsh-channels/client/panel/analyze-token-card
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { PANEL_COPY } from '../../settings/contract.ts'
|
|
9
|
+
import type { PanelWire } from '../../settings/contract.ts'
|
|
10
|
+
import type { AnalyzeTokenView } from './shared.tsx'
|
|
11
|
+
|
|
12
|
+
export function AnalyzeTokenCard(props: {
|
|
13
|
+
panel: PanelWire
|
|
14
|
+
token: AnalyzeTokenView
|
|
15
|
+
}): JSX.Element {
|
|
16
|
+
const { panel, token } = props
|
|
17
|
+
const state = panel.config.credentials.analyzeToken
|
|
18
|
+
const disabled = token.busy !== null || token.locked || !state.writable
|
|
19
|
+
const issue = token.issues.find(item => item.key === 'analyzeToken')
|
|
20
|
+
const inputClass = `dsh-channels-cfg__input${issue === undefined ? '' : ' dsh-channels-cfg__input--invalid'}`
|
|
21
|
+
return (
|
|
22
|
+
<section className="dsh-channels-cfg__card">
|
|
23
|
+
<div className="dsh-channels-cfg__cardHead">
|
|
24
|
+
<span className="dsh-channels-cfg__cardTitle">{PANEL_COPY.analyzeTokenCard}</span>
|
|
25
|
+
<span className={`dsh-channels-cfg__badge dsh-channels-cfg__badge--${state.configured ? 'ok' : 'muted'}`}>
|
|
26
|
+
{state.configured ? PANEL_COPY.credentialConfigured : PANEL_COPY.credentialMissing}
|
|
27
|
+
</span>
|
|
28
|
+
</div>
|
|
29
|
+
<span className="dsh-channels-cfg__help">{PANEL_COPY.analyzeTokenCardHelp}</span>
|
|
30
|
+
<div className="dsh-channels-cfg__field">
|
|
31
|
+
<span className="dsh-channels-cfg__label">{PANEL_COPY.analyzeTokenLabel}</span>
|
|
32
|
+
<div className="dsh-channels-cfg__cred">
|
|
33
|
+
<input
|
|
34
|
+
type="password"
|
|
35
|
+
className={inputClass}
|
|
36
|
+
value={token.value}
|
|
37
|
+
placeholder={PANEL_COPY.analyzeTokenPlaceholder}
|
|
38
|
+
autoComplete="new-password"
|
|
39
|
+
aria-label={PANEL_COPY.analyzeTokenLabel}
|
|
40
|
+
disabled={disabled}
|
|
41
|
+
onChange={event => token.onChange(event.target.value)}
|
|
42
|
+
/>
|
|
43
|
+
{state.configured
|
|
44
|
+
? (
|
|
45
|
+
<button type="button" className="dsh-channels-cfg__btn" disabled={disabled} onClick={token.onClear}>
|
|
46
|
+
{token.busy === 'credentials-clear' ? PANEL_COPY.removingCredential : PANEL_COPY.removeCredential}
|
|
47
|
+
</button>
|
|
48
|
+
)
|
|
49
|
+
: null}
|
|
50
|
+
</div>
|
|
51
|
+
<span className="dsh-channels-cfg__help">
|
|
52
|
+
{PANEL_COPY.credentialsRef}:<span className="dsh-channels-cfg__mono">{state.ref}</span>
|
|
53
|
+
{state.source === undefined ? '' : ` · ${state.source}`}
|
|
54
|
+
</span>
|
|
55
|
+
{issue === undefined ? null : <span className="dsh-channels-cfg__fieldError">{issue.message}</span>}
|
|
56
|
+
</div>
|
|
57
|
+
{state.writable ? null : <span className="dsh-channels-cfg__fieldError">{PANEL_COPY.credentialReadOnly}</span>}
|
|
58
|
+
<div className="dsh-channels-cfg__footer">
|
|
59
|
+
<button
|
|
60
|
+
type="button"
|
|
61
|
+
className="dsh-channels-cfg__btn dsh-channels-cfg__btn--primary"
|
|
62
|
+
onClick={token.onSave}
|
|
63
|
+
disabled={disabled || token.value.trim() === ''}
|
|
64
|
+
>
|
|
65
|
+
{token.busy === 'credentials' ? PANEL_COPY.savingCredentials : PANEL_COPY.saveAnalyzeToken}
|
|
66
|
+
</button>
|
|
67
|
+
</div>
|
|
68
|
+
</section>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
@@ -14,6 +14,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
|
14
14
|
import type { KeyboardEvent } from 'react'
|
|
15
15
|
import { PANEL_COPY, PANEL_TABS, TAB_FIELD_GROUPS } from '../../settings/contract.ts'
|
|
16
16
|
import type {
|
|
17
|
+
AccessMutationResponse,
|
|
17
18
|
AccessWire,
|
|
18
19
|
CatalogWire,
|
|
19
20
|
ChannelStatusWire,
|
|
@@ -28,6 +29,7 @@ import type {
|
|
|
28
29
|
import {
|
|
29
30
|
addAccessGroup,
|
|
30
31
|
addAccessUser,
|
|
32
|
+
addWikiUser,
|
|
31
33
|
clearConfig,
|
|
32
34
|
clearCredential,
|
|
33
35
|
fetchCatalog,
|
|
@@ -37,6 +39,7 @@ import {
|
|
|
37
39
|
reconnect,
|
|
38
40
|
removeAccessGroup,
|
|
39
41
|
removeAccessUser,
|
|
42
|
+
removeWikiUser,
|
|
40
43
|
resolveAccessRequest,
|
|
41
44
|
saveConfig,
|
|
42
45
|
saveCredentials,
|
|
@@ -56,12 +59,14 @@ import {
|
|
|
56
59
|
validateDraft,
|
|
57
60
|
} from './shared.tsx'
|
|
58
61
|
import type {
|
|
62
|
+
AnalyzeTokenView,
|
|
59
63
|
BusyAction,
|
|
60
64
|
CredentialsView,
|
|
61
65
|
FormView,
|
|
62
66
|
LoadState,
|
|
63
67
|
Notice,
|
|
64
68
|
} from './shared.tsx'
|
|
69
|
+
import { WikiTab } from './wiki-tab.tsx'
|
|
65
70
|
import { WorkspaceTab } from './workspace-tab.tsx'
|
|
66
71
|
|
|
67
72
|
/** Owner props handed to a `settings.section` contribution (unused beyond parity). */
|
|
@@ -80,6 +85,7 @@ export function FeishuChannelsSection(_props: FeishuChannelsSectionProps): JSX.E
|
|
|
80
85
|
const [pollError, setPollError] = useState<string | null>(null)
|
|
81
86
|
const [appIdInput, setAppIdInput] = useState('')
|
|
82
87
|
const [appSecretInput, setAppSecretInput] = useState('')
|
|
88
|
+
const [analyzeTokenInput, setAnalyzeTokenInput] = useState('')
|
|
83
89
|
const [catalog, setCatalog] = useState<CatalogWire | null>(null)
|
|
84
90
|
const [catalogBusy, setCatalogBusy] = useState(false)
|
|
85
91
|
const [accessNotice, setAccessNotice] = useState<string | null>(null)
|
|
@@ -106,6 +112,7 @@ export function FeishuChannelsSection(_props: FeishuChannelsSectionProps): JSX.E
|
|
|
106
112
|
setDraft({})
|
|
107
113
|
setAppIdInput('')
|
|
108
114
|
setAppSecretInput('')
|
|
115
|
+
setAnalyzeTokenInput('')
|
|
109
116
|
} catch (error) {
|
|
110
117
|
setLoad({ phase: 'error', message: messageOf(error) })
|
|
111
118
|
}
|
|
@@ -453,18 +460,24 @@ export function FeishuChannelsSection(_props: FeishuChannelsSectionProps): JSX.E
|
|
|
453
460
|
}
|
|
454
461
|
}, [applyAccess])
|
|
455
462
|
|
|
456
|
-
|
|
463
|
+
/** Add one user to a whitelist; resolves true when the input should clear. */
|
|
464
|
+
const addUser = useCallback(async (
|
|
465
|
+
add: (userId: string, name: string) => Promise<AccessMutationResponse>,
|
|
466
|
+
userId: string,
|
|
467
|
+
name: string,
|
|
468
|
+
notice: string,
|
|
469
|
+
): Promise<boolean> => {
|
|
457
470
|
setBusy('access')
|
|
458
471
|
setAccessError(null)
|
|
459
472
|
setAccessNotice(null)
|
|
460
473
|
try {
|
|
461
|
-
const result = await
|
|
474
|
+
const result = await add(userId, name)
|
|
462
475
|
if (!result.ok) {
|
|
463
476
|
setAccessError(result.error)
|
|
464
477
|
return false
|
|
465
478
|
}
|
|
466
479
|
applyAccess(result.access)
|
|
467
|
-
setAccessNotice(
|
|
480
|
+
setAccessNotice(notice)
|
|
468
481
|
return true
|
|
469
482
|
} catch (error) {
|
|
470
483
|
setAccessError(messageOf(error))
|
|
@@ -474,18 +487,22 @@ export function FeishuChannelsSection(_props: FeishuChannelsSectionProps): JSX.E
|
|
|
474
487
|
}
|
|
475
488
|
}, [applyAccess])
|
|
476
489
|
|
|
477
|
-
const
|
|
490
|
+
const removeUser = useCallback(async (
|
|
491
|
+
remove: (userId: string) => Promise<AccessMutationResponse>,
|
|
492
|
+
userId: string,
|
|
493
|
+
notice: string,
|
|
494
|
+
): Promise<void> => {
|
|
478
495
|
setBusy('access')
|
|
479
496
|
setAccessError(null)
|
|
480
497
|
setAccessNotice(null)
|
|
481
498
|
try {
|
|
482
|
-
const result = await
|
|
499
|
+
const result = await remove(userId)
|
|
483
500
|
if (!result.ok) {
|
|
484
501
|
setAccessError(result.error)
|
|
485
502
|
return
|
|
486
503
|
}
|
|
487
504
|
applyAccess(result.access)
|
|
488
|
-
setAccessNotice(
|
|
505
|
+
setAccessNotice(notice)
|
|
489
506
|
} catch (error) {
|
|
490
507
|
setAccessError(messageOf(error))
|
|
491
508
|
} finally {
|
|
@@ -552,6 +569,28 @@ export function FeishuChannelsSection(_props: FeishuChannelsSectionProps): JSX.E
|
|
|
552
569
|
}
|
|
553
570
|
}, [appIdInput, appSecretInput])
|
|
554
571
|
|
|
572
|
+
/** Save the analyze API token; it applies to the next request, no reconnect. */
|
|
573
|
+
const onSaveAnalyzeToken = useCallback(async (): Promise<void> => {
|
|
574
|
+
if (analyzeTokenInput.trim() === '') return
|
|
575
|
+
setBusy('credentials')
|
|
576
|
+
setNotice(null)
|
|
577
|
+
try {
|
|
578
|
+
const result = await saveCredentials({ analyzeToken: analyzeTokenInput })
|
|
579
|
+
if (!result.ok) {
|
|
580
|
+
setNotice({ kind: 'error', text: result.error, issues: result.issues })
|
|
581
|
+
return
|
|
582
|
+
}
|
|
583
|
+
setLoad({ phase: 'ready', panel: result.panel })
|
|
584
|
+
setStatus(result.panel.status)
|
|
585
|
+
setAnalyzeTokenInput('')
|
|
586
|
+
setNotice({ kind: 'ok', text: PANEL_COPY.analyzeTokenSaved })
|
|
587
|
+
} catch (error) {
|
|
588
|
+
setNotice({ kind: 'error', text: messageOf(error) })
|
|
589
|
+
} finally {
|
|
590
|
+
setBusy(null)
|
|
591
|
+
}
|
|
592
|
+
}, [analyzeTokenInput])
|
|
593
|
+
|
|
555
594
|
const onClearCredential = useCallback(async (field: CredentialField): Promise<void> => {
|
|
556
595
|
const confirmed = typeof window.confirm === 'function'
|
|
557
596
|
? window.confirm(PANEL_COPY.credentialRemoveConfirm)
|
|
@@ -569,7 +608,9 @@ export function FeishuChannelsSection(_props: FeishuChannelsSectionProps): JSX.E
|
|
|
569
608
|
setStatus(result.panel.status)
|
|
570
609
|
setNotice({
|
|
571
610
|
kind: 'ok',
|
|
572
|
-
text:
|
|
611
|
+
text: field === 'analyzeToken'
|
|
612
|
+
? PANEL_COPY.analyzeTokenRemoved
|
|
613
|
+
: result.reconnect === 'pending' ? PANEL_COPY.credentialRemovedPending : PANEL_COPY.credentialRemoved,
|
|
573
614
|
})
|
|
574
615
|
} catch (error) {
|
|
575
616
|
setNotice({ kind: 'error', text: messageOf(error) })
|
|
@@ -628,6 +669,16 @@ export function FeishuChannelsSection(_props: FeishuChannelsSectionProps): JSX.E
|
|
|
628
669
|
onClear: (field: CredentialField) => { void onClearCredential(field) },
|
|
629
670
|
}), [appIdInput, appSecretInput, busy, hostBusy, issues, onClearCredential, onSaveCredentials])
|
|
630
671
|
|
|
672
|
+
const analyzeToken = useMemo<AnalyzeTokenView>(() => ({
|
|
673
|
+
value: analyzeTokenInput,
|
|
674
|
+
issues,
|
|
675
|
+
busy,
|
|
676
|
+
locked: hostBusy,
|
|
677
|
+
onChange: setAnalyzeTokenInput,
|
|
678
|
+
onSave: () => { void onSaveAnalyzeToken() },
|
|
679
|
+
onClear: () => { void onClearCredential('analyzeToken') },
|
|
680
|
+
}), [analyzeTokenInput, busy, hostBusy, issues, onClearCredential, onSaveAnalyzeToken])
|
|
681
|
+
|
|
631
682
|
if (load.phase === 'loading') {
|
|
632
683
|
return (
|
|
633
684
|
<div className="dsh-channels-cfg">
|
|
@@ -680,7 +731,7 @@ export function FeishuChannelsSection(_props: FeishuChannelsSectionProps): JSX.E
|
|
|
680
731
|
/>
|
|
681
732
|
)
|
|
682
733
|
: null}
|
|
683
|
-
{activeTab === 'sessions' ? <SessionsTab form={form} /> : null}
|
|
734
|
+
{activeTab === 'sessions' ? <SessionsTab form={form} analyzeToken={analyzeToken} /> : null}
|
|
684
735
|
{activeTab === 'model' ? <ModelTab form={form} catalogBusy={catalogBusy} /> : null}
|
|
685
736
|
{activeTab === 'workspace'
|
|
686
737
|
? (
|
|
@@ -695,12 +746,23 @@ export function FeishuChannelsSection(_props: FeishuChannelsSectionProps): JSX.E
|
|
|
695
746
|
accessError={accessError}
|
|
696
747
|
onAddGroup={onAddAccessGroup}
|
|
697
748
|
onRemoveGroup={chatId => { void onRemoveAccessGroup(chatId) }}
|
|
698
|
-
onAddUser={
|
|
699
|
-
onRemoveUser={userId => { void
|
|
749
|
+
onAddUser={(userId, name) => addUser(addAccessUser, userId, name, PANEL_COPY.accessUserAdded)}
|
|
750
|
+
onRemoveUser={userId => { void removeUser(removeAccessUser, userId, PANEL_COPY.accessUserRemoved) }}
|
|
700
751
|
onResolveRequest={(token, decision) => { void onResolveAccessRequest(token, decision) }}
|
|
701
752
|
/>
|
|
702
753
|
)
|
|
703
754
|
: null}
|
|
755
|
+
{activeTab === 'wiki'
|
|
756
|
+
? (
|
|
757
|
+
<WikiTab
|
|
758
|
+
form={form}
|
|
759
|
+
accessNotice={accessNotice}
|
|
760
|
+
accessError={accessError}
|
|
761
|
+
onAddUser={(userId, name) => addUser(addWikiUser, userId, name, PANEL_COPY.wikiUserAdded)}
|
|
762
|
+
onRemoveUser={userId => { void removeUser(removeWikiUser, userId, PANEL_COPY.wikiUserRemoved) }}
|
|
763
|
+
/>
|
|
764
|
+
)
|
|
765
|
+
: null}
|
|
704
766
|
{activeTab === 'advanced'
|
|
705
767
|
? <AdvancedTab form={form} onClear={() => { void onClear() }} />
|
|
706
768
|
: null}
|
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 「会话与行为」tab:行为 + 内容与附件两组字段(私聊/群聊触发方式、
|
|
3
|
-
*
|
|
3
|
+
* 首次引导、权限切换、附件接收、日志分析接口)。表单字段完全由 Host 描述驱动;
|
|
4
|
+
* 日志分析接口令牌是凭据,单独一张卡片。
|
|
4
5
|
*
|
|
5
6
|
* @module dsh-channels/client/panel/sessions-tab
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import { PANEL_COPY, TAB_FIELD_GROUPS } from '../../settings/contract.ts'
|
|
10
|
+
import { AnalyzeTokenCard } from './analyze-token-card.tsx'
|
|
9
11
|
import { FieldsCard, SaveBar, TabHelp } from './shared.tsx'
|
|
10
|
-
import type { FormView } from './shared.tsx'
|
|
12
|
+
import type { AnalyzeTokenView, FormView } from './shared.tsx'
|
|
11
13
|
|
|
12
|
-
export function SessionsTab(props: { form: FormView }): JSX.Element {
|
|
13
|
-
const { form } = props
|
|
14
|
+
export function SessionsTab(props: { form: FormView; analyzeToken: AnalyzeTokenView }): JSX.Element {
|
|
15
|
+
const { form, analyzeToken } = props
|
|
14
16
|
return (
|
|
15
17
|
<div className="dsh-channels-cfg__tabBody">
|
|
16
18
|
<TabHelp text={PANEL_COPY.sessionsTabHelp} />
|
|
17
19
|
<FieldsCard form={form} groups={TAB_FIELD_GROUPS.sessions} />
|
|
18
20
|
<SaveBar form={form} tab="sessions" />
|
|
21
|
+
<AnalyzeTokenCard panel={form.panel} token={analyzeToken} />
|
|
19
22
|
</div>
|
|
20
23
|
)
|
|
21
24
|
}
|
|
@@ -182,6 +182,18 @@ export interface CredentialsView {
|
|
|
182
182
|
onClear: (field: CredentialField) => void
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
/** Analyze API token editor data owned by the page shell, rendered by the sessions tab. */
|
|
186
|
+
export interface AnalyzeTokenView {
|
|
187
|
+
value: string
|
|
188
|
+
issues: FieldIssueWire[]
|
|
189
|
+
busy: BusyAction | null
|
|
190
|
+
/** Host-side lifecycle busy (connecting/restarting): block writes. */
|
|
191
|
+
locked: boolean
|
|
192
|
+
onChange: (value: string) => void
|
|
193
|
+
onSave: () => void
|
|
194
|
+
onClear: () => void
|
|
195
|
+
}
|
|
196
|
+
|
|
185
197
|
/** Everything a field-bearing tab needs from the page shell's single draft. */
|
|
186
198
|
export interface FormView {
|
|
187
199
|
panel: PanelWire
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 「知识库」tab:知识库工具字段表单(读取/写入开关、知识库范围)+ 知识库
|
|
3
|
+
* 白名单卡。表单保存走全局 draft,白名单增删是独立即时 mutation。
|
|
4
|
+
*
|
|
5
|
+
* @module dsh-channels/client/panel/wiki-tab
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { PANEL_COPY, TAB_FIELD_GROUPS } from '../../settings/contract.ts'
|
|
9
|
+
import { UserWhitelist } from './access-card.tsx'
|
|
10
|
+
import { EmptyState, FieldsCard, SaveBar, TabHelp } from './shared.tsx'
|
|
11
|
+
import type { FormView } from './shared.tsx'
|
|
12
|
+
|
|
13
|
+
export interface WikiTabProps {
|
|
14
|
+
form: FormView
|
|
15
|
+
accessNotice: string | null
|
|
16
|
+
accessError: string | null
|
|
17
|
+
onAddUser: (userId: string, name: string) => Promise<boolean>
|
|
18
|
+
onRemoveUser: (userId: string) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function WikiTab(props: WikiTabProps): JSX.Element {
|
|
22
|
+
const { form, accessNotice, accessError, onAddUser, onRemoveUser } = props
|
|
23
|
+
const { access } = form.panel
|
|
24
|
+
const disabled = form.busy !== null || form.locked || !access.loaded
|
|
25
|
+
return (
|
|
26
|
+
<div className="dsh-channels-cfg__tabBody">
|
|
27
|
+
<TabHelp text={PANEL_COPY.wikiTabHelp} />
|
|
28
|
+
<FieldsCard form={form} groups={TAB_FIELD_GROUPS.wiki} />
|
|
29
|
+
<SaveBar form={form} tab="wiki" />
|
|
30
|
+
<section className="dsh-channels-cfg__card">
|
|
31
|
+
<div className="dsh-channels-cfg__cardHead">
|
|
32
|
+
<span className="dsh-channels-cfg__cardTitle">{PANEL_COPY.wikiUsersCard}</span>
|
|
33
|
+
</div>
|
|
34
|
+
<span className="dsh-channels-cfg__help">{PANEL_COPY.wikiUsersHelp}</span>
|
|
35
|
+
{!access.loaded
|
|
36
|
+
? <EmptyState text={PANEL_COPY.accessNotLoaded} />
|
|
37
|
+
: (
|
|
38
|
+
<UserWhitelist
|
|
39
|
+
title={PANEL_COPY.wikiUsersCard}
|
|
40
|
+
empty={PANEL_COPY.wikiUsersEmpty}
|
|
41
|
+
addLabel={PANEL_COPY.accessAddUser}
|
|
42
|
+
users={access.wikiUsers}
|
|
43
|
+
disabled={disabled}
|
|
44
|
+
working={form.busy === 'access'}
|
|
45
|
+
onAdd={onAddUser}
|
|
46
|
+
onRemove={onRemoveUser}
|
|
47
|
+
/>
|
|
48
|
+
)}
|
|
49
|
+
{accessNotice === null
|
|
50
|
+
? null
|
|
51
|
+
: (
|
|
52
|
+
<div className="dsh-channels-cfg__notice dsh-channels-cfg__notice--ok" role="status" aria-live="polite">
|
|
53
|
+
<span>{accessNotice}</span>
|
|
54
|
+
</div>
|
|
55
|
+
)}
|
|
56
|
+
{accessError === null
|
|
57
|
+
? null
|
|
58
|
+
: <span className="dsh-channels-cfg__fieldError" role="alert">{accessError}</span>}
|
|
59
|
+
</section>
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -80,6 +80,12 @@ export const DEFAULT_TOPIC_MESSAGES_TIMEOUT_MS = 20_000
|
|
|
80
80
|
*/
|
|
81
81
|
export const DEFAULT_PROMPT_CONTEXT_TIMEOUT_MS = 3_000
|
|
82
82
|
|
|
83
|
+
/** Conventional credential reference holding the log-platform analyze API bearer token. */
|
|
84
|
+
export const DEFAULT_ANALYZE_TOKEN_REF = 'DSH_CHANNELS_ANALYZE_TOKEN'
|
|
85
|
+
|
|
86
|
+
/** `host:port` listen address of the analyze API (`[v6]:port` for IPv6 literals). */
|
|
87
|
+
export const ANALYZE_LISTEN_PATTERN = /^(?:\[[0-9A-Fa-f:.]+\]|[A-Za-z0-9.-]+):(?:[1-9][0-9]{0,4})$/u
|
|
88
|
+
|
|
83
89
|
/** A credential reference: `FEISHU_APP_ID`-style name resolved through ctx.credentials. */
|
|
84
90
|
const credentialRefSchema = z.string().regex(
|
|
85
91
|
CREDENTIAL_REF_PATTERN,
|
|
@@ -298,16 +304,18 @@ export const configSchema = z.object({
|
|
|
298
304
|
* `feishu_wiki_base_read` so the model can browse the wiki nodes under
|
|
299
305
|
* `wikiRoots` and read docx documents, spreadsheets, and Bases there.
|
|
300
306
|
* Needs `wiki:wiki` (or `wiki:wiki:readonly`), `docx:document` (or
|
|
301
|
-
* `docx:document:readonly`), `sheets:spreadsheet
|
|
302
|
-
*
|
|
307
|
+
* `docx:document:readonly`), `sheets:spreadsheet:read`, and
|
|
308
|
+
* `base:table:read` + `base:field:read` + `base:record:read` (the
|
|
309
|
+
* classic `sheets:spreadsheet` / `bitable:app` grants do not apply).
|
|
303
310
|
* - `wikiWrite` (default off): register `feishu_wiki_create`,
|
|
304
311
|
* `feishu_wiki_edit`, `feishu_wiki_sheet_edit`, and
|
|
305
312
|
* `feishu_wiki_base_edit` so the model can create docx documents,
|
|
306
313
|
* spreadsheets, and Bases under `wikiRoots`, edit documents and
|
|
307
314
|
* spreadsheets this app owns, and change records of Bases this app owns
|
|
308
315
|
* (other Bases accept new records only). Needs `wiki:wiki`,
|
|
309
|
-
* `docx:document`, `sheets:spreadsheet`, and
|
|
310
|
-
*
|
|
316
|
+
* `docx:document`, `sheets:spreadsheet:write_only`, and
|
|
317
|
+
* `base:record:create` / `base:record:update` / `base:record:delete`,
|
|
318
|
+
* plus the read scopes above.
|
|
311
319
|
*
|
|
312
320
|
* The wiki tools act with the app identity and are mounted only while
|
|
313
321
|
* `wikiRoots` is non-empty. Each call is limited to topics whose owner is
|
|
@@ -409,6 +417,24 @@ export const configSchema = z.object({
|
|
|
409
417
|
* its own deadline and is left to the host signal instead.
|
|
410
418
|
*/
|
|
411
419
|
questionTimeoutMs: z.number().int().min(10_000).max(3_600_000).default(DEFAULT_QUESTION_TIMEOUT_MS),
|
|
420
|
+
/**
|
|
421
|
+
* Listen address (`host:port`, e.g. `0.0.0.0:3090`) of the log-platform
|
|
422
|
+
* analyze API: a dedicated HTTP listener serving only
|
|
423
|
+
* `POST /dsh-channels/analyze`, separate from the DSH Web port. Unset
|
|
424
|
+
* (default) keeps the API off. The listener runs while the Feishu connection
|
|
425
|
+
* is up and is rebound on reconnect.
|
|
426
|
+
*/
|
|
427
|
+
analyzeListen: z.string().trim().regex(
|
|
428
|
+
ANALYZE_LISTEN_PATTERN,
|
|
429
|
+
'must be host:port, e.g. 0.0.0.0:3090 or 127.0.0.1:3090',
|
|
430
|
+
).refine(value => Number(value.slice(value.lastIndexOf(':') + 1)) <= 65_535, 'port must be 1-65535')
|
|
431
|
+
.optional(),
|
|
432
|
+
/**
|
|
433
|
+
* Credential reference holding the analyze API bearer token (defaults to
|
|
434
|
+
* `DSH_CHANNELS_ANALYZE_TOKEN`). Resolved per request, so rotating the value
|
|
435
|
+
* applies to the next request; an unresolvable token refuses every request.
|
|
436
|
+
*/
|
|
437
|
+
analyzeToken: credentialRefSchema.default(DEFAULT_ANALYZE_TOKEN_REF),
|
|
412
438
|
})
|
|
413
439
|
|
|
414
440
|
export type FeishuConfig = z.infer<typeof configSchema>
|
package/src/domain.ts
CHANGED
|
@@ -61,6 +61,12 @@ export const routeRecordSchema = z.object({
|
|
|
61
61
|
lastActivityAt: z.number().int().nonnegative(),
|
|
62
62
|
/** Sessions this topic has pointed at, oldest first. Optional for v1 compatibility. */
|
|
63
63
|
sessionHistory: z.array(sessionHistoryEntrySchema).optional(),
|
|
64
|
+
/**
|
|
65
|
+
* Group-shared route (log-platform analysis topics): every member admitted
|
|
66
|
+
* in `chatId` may continue the Session and decide its cards, not only
|
|
67
|
+
* `senderUserId` (who then names the requester). Absent = owner-only.
|
|
68
|
+
*/
|
|
69
|
+
shared: z.literal(true).optional(),
|
|
64
70
|
})
|
|
65
71
|
|
|
66
72
|
export type RouteRecord = z.infer<typeof routeRecordSchema>
|
|
@@ -281,6 +287,40 @@ export const processedRecordSchema = z.object({
|
|
|
281
287
|
|
|
282
288
|
export type ProcessedRecord = z.infer<typeof processedRecordSchema>
|
|
283
289
|
|
|
290
|
+
/**
|
|
291
|
+
* One accepted analyze API request, keyed by its `Idempotency-Key`. Kept
|
|
292
|
+
* forever: a retried key returns the stored response instead of posting a
|
|
293
|
+
* second analysis, and a reused key with a different body is refused.
|
|
294
|
+
*/
|
|
295
|
+
export const analyzeRequestRecordSchema = z.object({
|
|
296
|
+
/** The request's `Idempotency-Key` (also the table key). */
|
|
297
|
+
key: z.string().min(1),
|
|
298
|
+
/** SHA-256 (hex) of the canonical request body. */
|
|
299
|
+
bodyHash: z.string().min(1),
|
|
300
|
+
/** Target group chat. */
|
|
301
|
+
chatId: z.string().min(1),
|
|
302
|
+
/** Root card message id (the analysis topic's root). */
|
|
303
|
+
messageId: z.string().min(1),
|
|
304
|
+
/** Epoch ms when the request was accepted. */
|
|
305
|
+
acceptedAt: z.number().int().nonnegative(),
|
|
306
|
+
/** What the root card shows, kept so the card can be redrawn after a restart. */
|
|
307
|
+
subject: z.object({
|
|
308
|
+
requesterUserId: z.string(),
|
|
309
|
+
triggeredAt: z.string(),
|
|
310
|
+
app: z.string(),
|
|
311
|
+
errorCode: z.string(),
|
|
312
|
+
logDetails: z.array(z.object({ name: z.string(), url: z.string() })),
|
|
313
|
+
}),
|
|
314
|
+
/**
|
|
315
|
+
* Root card status. `running` until the first turn settles; a record still
|
|
316
|
+
* `running` when the channel starts belongs to an analysis the previous
|
|
317
|
+
* instance never finished, and becomes `interrupted`.
|
|
318
|
+
*/
|
|
319
|
+
status: z.enum(['running', 'done', 'stopped', 'error', 'failed', 'interrupted']),
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
export type AnalyzeRequestRecord = z.infer<typeof analyzeRequestRecordSchema>
|
|
323
|
+
|
|
284
324
|
export interface FeishuDomainSpecShape {
|
|
285
325
|
name: 'channels_routes_v2'
|
|
286
326
|
version: 1
|
|
@@ -321,6 +361,10 @@ export interface FeishuDomainSpecShape {
|
|
|
321
361
|
valueSchema: typeof accessUserRecordSchema
|
|
322
362
|
__key?: string
|
|
323
363
|
}
|
|
364
|
+
analyze_requests: {
|
|
365
|
+
valueSchema: typeof analyzeRequestRecordSchema
|
|
366
|
+
__key?: string
|
|
367
|
+
}
|
|
324
368
|
}
|
|
325
369
|
}
|
|
326
370
|
|
|
@@ -361,6 +405,8 @@ export const feishuDomain: FeishuDomainSpecShape = {
|
|
|
361
405
|
// independent grant. Added without a version bump like the other access
|
|
362
406
|
// tables: a missing table loads empty.
|
|
363
407
|
access_wiki_users: { valueSchema: accessUserRecordSchema },
|
|
408
|
+
// Analyze API idempotency records; a missing table loads empty.
|
|
409
|
+
analyze_requests: { valueSchema: analyzeRequestRecordSchema },
|
|
364
410
|
},
|
|
365
411
|
}
|
|
366
412
|
|
|
@@ -427,6 +473,13 @@ export interface AccessRequestsTable {
|
|
|
427
473
|
entries(): IterableIterator<[string, AccessRequestRecord]>
|
|
428
474
|
}
|
|
429
475
|
|
|
476
|
+
/** Normalized table handle for analyze API requests (key = `Idempotency-Key`). */
|
|
477
|
+
export interface AnalyzeRequestsTable {
|
|
478
|
+
get(key: string): AnalyzeRequestRecord | undefined
|
|
479
|
+
put(key: string, value: AnalyzeRequestRecord): Promise<unknown> | void
|
|
480
|
+
entries(): IterableIterator<[string, AnalyzeRequestRecord]>
|
|
481
|
+
}
|
|
482
|
+
|
|
430
483
|
/** Normalized domain handle. */
|
|
431
484
|
export interface FeishuDomainHandle {
|
|
432
485
|
routes: RoutesTable
|
|
@@ -438,6 +491,8 @@ export interface FeishuDomainHandle {
|
|
|
438
491
|
accessRequests: AccessRequestsTable
|
|
439
492
|
/** Wiki whitelist (key = `user_id`); same records as {@link AccessUsersTable}. */
|
|
440
493
|
accessWikiUsers: AccessUsersTable
|
|
494
|
+
/** Analyze API idempotency records. */
|
|
495
|
+
analyzeRequests: AnalyzeRequestsTable
|
|
441
496
|
getGlobal(): FeishuDomainGlobal
|
|
442
497
|
setGlobal(value: FeishuDomainGlobal): Promise<unknown> | void
|
|
443
498
|
close(): Promise<void>
|
package/src/feishu/scopes.ts
CHANGED
|
@@ -91,10 +91,16 @@ const SEND_FILE_REQUIREMENT: ScopeRequirement = {
|
|
|
91
91
|
anyOf: ['im:resource', 'im:resource:upload'], level: 'optional', purpose: '上传图片/文件,让模型用 feishu_send_file 把生成的文件发给用户',
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* Spreadsheet and Base read needs, shared by the wiki read and write sets. The
|
|
96
|
+
* `sheet_ai/v2` and `base/v3` APIs accept only their fine-grained scopes; the
|
|
97
|
+
* classic `sheets:spreadsheet` / `bitable:app` grants do not satisfy them.
|
|
98
|
+
*/
|
|
95
99
|
const SHEET_BASE_READ_REQUIREMENTS: readonly ScopeRequirement[] = [
|
|
96
|
-
{ anyOf: ['sheets:spreadsheet
|
|
97
|
-
{ anyOf: ['
|
|
100
|
+
{ anyOf: ['sheets:spreadsheet:read'], level: 'optional', purpose: '读取知识库电子表格(feishu_wiki_sheet_read)' },
|
|
101
|
+
{ anyOf: ['base:table:read'], level: 'optional', purpose: '列出知识库多维表格的数据表(feishu_wiki_base_read)' },
|
|
102
|
+
{ anyOf: ['base:field:read'], level: 'optional', purpose: '读取知识库多维表格的字段(feishu_wiki_base_read)' },
|
|
103
|
+
{ anyOf: ['base:record:read'], level: 'optional', purpose: '读取知识库多维表格的记录(feishu_wiki_base_read)' },
|
|
98
104
|
]
|
|
99
105
|
|
|
100
106
|
/** Wiki read needs (`feishu_wiki_list` / `feishu_wiki_read` and the sheet/base read tools). */
|
|
@@ -108,10 +114,10 @@ const WIKI_READ_REQUIREMENTS: readonly ScopeRequirement[] = [
|
|
|
108
114
|
const WIKI_WRITE_REQUIREMENTS: readonly ScopeRequirement[] = [
|
|
109
115
|
{ anyOf: ['wiki:wiki'], level: 'optional', purpose: '在知识库中新建节点(feishu_wiki_create)' },
|
|
110
116
|
{ anyOf: ['docx:document'], level: 'optional', purpose: '写入与修改知识库文档内容(feishu_wiki_create / feishu_wiki_edit)' },
|
|
111
|
-
{ anyOf: ['sheets:spreadsheet
|
|
112
|
-
{ anyOf: ['
|
|
113
|
-
{ anyOf: ['
|
|
114
|
-
{ anyOf: ['
|
|
117
|
+
{ anyOf: ['sheets:spreadsheet:write_only'], level: 'optional', purpose: '写入知识库电子表格(feishu_wiki_sheet_edit)' },
|
|
118
|
+
{ anyOf: ['base:record:create'], level: 'optional', purpose: '新增知识库多维表格记录(feishu_wiki_base_edit)' },
|
|
119
|
+
{ anyOf: ['base:record:update'], level: 'optional', purpose: '修改本应用创建的多维表格中的记录(feishu_wiki_base_edit)' },
|
|
120
|
+
{ anyOf: ['base:record:delete'], level: 'optional', purpose: '删除本应用创建的多维表格中的记录(feishu_wiki_base_edit)' },
|
|
115
121
|
...SHEET_BASE_READ_REQUIREMENTS,
|
|
116
122
|
]
|
|
117
123
|
|
package/src/feishu/tools.ts
CHANGED
|
@@ -105,6 +105,8 @@ export interface AttachmentFetchRoute {
|
|
|
105
105
|
readonly chatId: string
|
|
106
106
|
readonly chatType: 'p2p' | 'group'
|
|
107
107
|
readonly senderUserId: string
|
|
108
|
+
/** Group-shared route: messages of any sender in the route's topic count as its own. */
|
|
109
|
+
readonly shared?: boolean
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
/** Why a message became known to one route. */
|
|
@@ -811,7 +813,7 @@ export class AttachmentFetchToolRuntime {
|
|
|
811
813
|
if (!read.ok) return undefined
|
|
812
814
|
const container = quoteItemsOf(read.value)
|
|
813
815
|
if (container === undefined || container.deleted) return undefined
|
|
814
|
-
if (container.senderUserId !== call.route.senderUserId) return undefined
|
|
816
|
+
if (call.route.shared !== true && container.senderUserId !== call.route.senderUserId) return undefined
|
|
815
817
|
if (!messageBelongsToRoute(call.route, container)) return undefined
|
|
816
818
|
return knownMessageOfItems(read.value)
|
|
817
819
|
}
|
package/src/health.ts
CHANGED
|
@@ -55,6 +55,8 @@ export interface HealthSnapshotInput {
|
|
|
55
55
|
routeCount: number
|
|
56
56
|
liveAgentCount: number
|
|
57
57
|
inflightTurnCount: number
|
|
58
|
+
/** Why the configured analyze listener could not bind; absent when it is bound or not configured. */
|
|
59
|
+
analyzeListenError?: string
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
export interface HealthSnapshot extends Omit<HealthSnapshotInput, 'fibers' | 'botOpenId'> {
|
|
@@ -148,6 +150,7 @@ export function foldHealthSnapshot(input: HealthSnapshotInput): HealthSnapshot {
|
|
|
148
150
|
|| fibers.pending > 0
|
|
149
151
|
|| fibers.loading > 0
|
|
150
152
|
|| fibers.unloading > 0
|
|
153
|
+
|| input.analyzeListenError !== undefined
|
|
151
154
|
) {
|
|
152
155
|
level = 'degraded'
|
|
153
156
|
} else {
|
|
@@ -176,6 +179,10 @@ export function foldHealthSnapshot(input: HealthSnapshotInput): HealthSnapshot {
|
|
|
176
179
|
)
|
|
177
180
|
}
|
|
178
181
|
|
|
182
|
+
if (input.analyzeListenError !== undefined) {
|
|
183
|
+
reasons.push(`日志分析接口监听失败:${input.analyzeListenError}`)
|
|
184
|
+
}
|
|
185
|
+
|
|
179
186
|
return {
|
|
180
187
|
level,
|
|
181
188
|
fibers,
|
|
@@ -186,6 +193,7 @@ export function foldHealthSnapshot(input: HealthSnapshotInput): HealthSnapshot {
|
|
|
186
193
|
routeCount: input.routeCount,
|
|
187
194
|
liveAgentCount: input.liveAgentCount,
|
|
188
195
|
inflightTurnCount: input.inflightTurnCount,
|
|
196
|
+
...(input.analyzeListenError === undefined ? {} : { analyzeListenError: input.analyzeListenError }),
|
|
189
197
|
inactiveFibers: inactive,
|
|
190
198
|
inactiveTotal,
|
|
191
199
|
reasons,
|
|
@@ -229,5 +237,9 @@ export function buildHealthCard(snapshot: HealthSnapshot): object {
|
|
|
229
237
|
tag: 'markdown',
|
|
230
238
|
content: `**dsh-channels**\n${snapshot.started ? 'started' : 'stopped'} · 持久化路由 ${snapshot.routeCount} · live Agent ${snapshot.liveAgentCount} · in-flight turn ${snapshot.inflightTurnCount}`,
|
|
231
239
|
},
|
|
240
|
+
...(snapshot.analyzeListenError === undefined ? [] : [{
|
|
241
|
+
tag: 'markdown',
|
|
242
|
+
content: `**日志分析接口**\n❌ 监听失败:${snapshot.analyzeListenError}`,
|
|
243
|
+
}]),
|
|
232
244
|
])
|
|
233
245
|
}
|
package/src/host/agent.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { AttachmentStore } from './attachments.ts'
|
|
9
9
|
import type { CommandRuntime } from './commands.ts'
|
|
10
|
-
import type { Session, SessionId, UserMessage } from './core.ts'
|
|
10
|
+
import type { MessageId, Session, SessionId, UserMessage } from './core.ts'
|
|
11
11
|
|
|
12
12
|
// ---- Agent surface ------------------------------------------------------
|
|
13
13
|
|
|
@@ -138,9 +138,13 @@ export interface Agent {
|
|
|
138
138
|
readonly inbox: {
|
|
139
139
|
readonly nextTurn: readonly unknown[]
|
|
140
140
|
readonly nextStep: readonly unknown[]
|
|
141
|
+
/** Remove one still-pending message; false once the driver claimed it. */
|
|
142
|
+
remove(messageId: MessageId): boolean
|
|
141
143
|
}
|
|
142
144
|
readonly ctx: AgentContext
|
|
143
145
|
followup(message: UserMessage): void
|
|
146
|
+
/** Deliver input at the running turn's next step boundary (an idle driver starts a turn). */
|
|
147
|
+
steer(message: UserMessage): void
|
|
144
148
|
/** Abort the active turn (and pending input unless `keepInbox`). */
|
|
145
149
|
cancel?(cause: AgentCancelCause, options?: { keepInbox?: boolean }): void
|
|
146
150
|
}
|