@modesto_ma/dsh-channels 0.5.0-beta.2 → 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 +72 -15
- package/docs/attachment-pipeline-v2.md +2 -2
- package/docs/feishu-onboarding.md +4 -3
- package/lib/client.js +369 -94
- package/lib/client.js.map +1 -1
- package/lib/index.js +7847 -5293
- 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 +52 -14
- package/src/domain.ts +55 -0
- package/src/feishu/scopes.ts +19 -1
- package/src/feishu/tools.ts +3 -1
- package/src/feishu/wiki/a1.ts +97 -0
- package/src/feishu/wiki/api.ts +21 -6
- package/src/feishu/wiki/base-api.ts +267 -0
- package/src/feishu/wiki/base-tools.ts +423 -0
- package/src/feishu/wiki/shared.ts +284 -0
- package/src/feishu/wiki/sheet-api.ts +250 -0
- package/src/feishu/wiki/sheet-tools.ts +493 -0
- package/src/feishu/wiki/tools.ts +102 -189
- package/src/feishu-channel.ts +14 -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 +69 -20
- 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
|
@@ -48,6 +48,9 @@ export const DEFAULT_ATTACHMENT_FETCH_MAX_BYTES = 100 * 1024 * 1024
|
|
|
48
48
|
/** Default whole-call timeout of `feishu_attachment_fetch` (120 s). */
|
|
49
49
|
export const DEFAULT_ATTACHMENT_FETCH_TIMEOUT_MS = 120_000
|
|
50
50
|
|
|
51
|
+
/** Largest delay Node's `setTimeout` honors (2^31 − 1 ms, about 24.8 days); longer delays fire at once. */
|
|
52
|
+
export const MAX_TIMER_DELAY_MS = 2_147_483_647
|
|
53
|
+
|
|
51
54
|
/** Default per-image byte cap for trigger-message prefetch (20 MiB). */
|
|
52
55
|
export const DEFAULT_PREFETCH_IMAGE_MAX_BYTES = 20 * 1024 * 1024
|
|
53
56
|
|
|
@@ -77,6 +80,12 @@ export const DEFAULT_TOPIC_MESSAGES_TIMEOUT_MS = 20_000
|
|
|
77
80
|
*/
|
|
78
81
|
export const DEFAULT_PROMPT_CONTEXT_TIMEOUT_MS = 3_000
|
|
79
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
|
+
|
|
80
89
|
/** A credential reference: `FEISHU_APP_ID`-style name resolved through ctx.credentials. */
|
|
81
90
|
const credentialRefSchema = z.string().regex(
|
|
82
91
|
CREDENTIAL_REF_PATTERN,
|
|
@@ -265,10 +274,12 @@ export const configSchema = z.object({
|
|
|
265
274
|
* fetch (1–50). A message with more attachments is refused with a readable
|
|
266
275
|
* reason so the model can retry per `file_key`.
|
|
267
276
|
* - `attachmentFetchMaxBytes`: total download budget of one tool call
|
|
268
|
-
* (default 100 MiB). Files stream to the native attachment
|
|
269
|
-
* limit is enforced while streaming rather than buffered
|
|
270
|
-
* -
|
|
271
|
-
*
|
|
277
|
+
* (default 100 MiB, no upper bound). Files stream to the native attachment
|
|
278
|
+
* library, so the limit is enforced while streaming rather than buffered
|
|
279
|
+
* first, and multi-GiB files need only disk space.
|
|
280
|
+
* - `attachmentFetchTimeoutMs`: whole-call timeout (default 120 s, at most
|
|
281
|
+
* {@link MAX_TIMER_DELAY_MS}). On expiry the in-flight download is
|
|
282
|
+
* cancelled and reported per attachment.
|
|
272
283
|
* - `topicMessages` (default on): register `feishu_topic_messages` in
|
|
273
284
|
* channel sessions so the model can read the quoted message / earlier
|
|
274
285
|
* topic messages on demand and authorize their attachments for fetch.
|
|
@@ -288,14 +299,23 @@ export const configSchema = z.object({
|
|
|
288
299
|
* `im:resource` (or `im:resource:upload`) scope; without it a call returns
|
|
289
300
|
* a readable failure and the turn continues.
|
|
290
301
|
*
|
|
291
|
-
* - `wikiRead` (default off): register `feishu_wiki_list
|
|
292
|
-
* `feishu_wiki_read`
|
|
293
|
-
* `
|
|
294
|
-
* `
|
|
295
|
-
*
|
|
296
|
-
* `
|
|
297
|
-
* `
|
|
298
|
-
* `
|
|
302
|
+
* - `wikiRead` (default off): register `feishu_wiki_list`,
|
|
303
|
+
* `feishu_wiki_read`, `feishu_wiki_sheet_read`, and
|
|
304
|
+
* `feishu_wiki_base_read` so the model can browse the wiki nodes under
|
|
305
|
+
* `wikiRoots` and read docx documents, spreadsheets, and Bases there.
|
|
306
|
+
* Needs `wiki:wiki` (or `wiki:wiki:readonly`), `docx:document` (or
|
|
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).
|
|
310
|
+
* - `wikiWrite` (default off): register `feishu_wiki_create`,
|
|
311
|
+
* `feishu_wiki_edit`, `feishu_wiki_sheet_edit`, and
|
|
312
|
+
* `feishu_wiki_base_edit` so the model can create docx documents,
|
|
313
|
+
* spreadsheets, and Bases under `wikiRoots`, edit documents and
|
|
314
|
+
* spreadsheets this app owns, and change records of Bases this app owns
|
|
315
|
+
* (other Bases accept new records only). Needs `wiki:wiki`,
|
|
316
|
+
* `docx:document`, `sheets:spreadsheet:write_only`, and
|
|
317
|
+
* `base:record:create` / `base:record:update` / `base:record:delete`,
|
|
318
|
+
* plus the read scopes above.
|
|
299
319
|
*
|
|
300
320
|
* The wiki tools act with the app identity and are mounted only while
|
|
301
321
|
* `wikiRoots` is non-empty. Each call is limited to topics whose owner is
|
|
@@ -309,9 +329,9 @@ export const configSchema = z.object({
|
|
|
309
329
|
attachmentFetch: z.boolean().default(true),
|
|
310
330
|
attachmentFetchMaxResources: z.number().int().min(1).max(50)
|
|
311
331
|
.default(DEFAULT_ATTACHMENT_FETCH_MAX_RESOURCES),
|
|
312
|
-
attachmentFetchMaxBytes: z.number().int().min(1)
|
|
332
|
+
attachmentFetchMaxBytes: z.number().int().min(1)
|
|
313
333
|
.default(DEFAULT_ATTACHMENT_FETCH_MAX_BYTES),
|
|
314
|
-
attachmentFetchTimeoutMs: z.number().int().min(1_000).max(
|
|
334
|
+
attachmentFetchTimeoutMs: z.number().int().min(1_000).max(MAX_TIMER_DELAY_MS)
|
|
315
335
|
.default(DEFAULT_ATTACHMENT_FETCH_TIMEOUT_MS),
|
|
316
336
|
topicMessages: z.boolean().default(true),
|
|
317
337
|
topicMessagesTimeoutMs: z.number().int().min(1_000).max(120_000)
|
|
@@ -397,6 +417,24 @@ export const configSchema = z.object({
|
|
|
397
417
|
* its own deadline and is left to the host signal instead.
|
|
398
418
|
*/
|
|
399
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),
|
|
400
438
|
})
|
|
401
439
|
|
|
402
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,16 +91,34 @@ 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
|
+
*/
|
|
99
|
+
const SHEET_BASE_READ_REQUIREMENTS: readonly ScopeRequirement[] = [
|
|
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)' },
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
/** Wiki read needs (`feishu_wiki_list` / `feishu_wiki_read` and the sheet/base read tools). */
|
|
95
107
|
const WIKI_READ_REQUIREMENTS: readonly ScopeRequirement[] = [
|
|
96
108
|
{ anyOf: ['wiki:wiki', 'wiki:wiki:readonly'], level: 'optional', purpose: '浏览知识库节点(feishu_wiki_list / feishu_wiki_read)' },
|
|
97
109
|
{ anyOf: ['docx:document', 'docx:document:readonly'], level: 'optional', purpose: '读取知识库中的文档内容(feishu_wiki_read)' },
|
|
110
|
+
...SHEET_BASE_READ_REQUIREMENTS,
|
|
98
111
|
]
|
|
99
112
|
|
|
100
113
|
/** Wiki write needs (`feishu_wiki_create` / `feishu_wiki_edit`). */
|
|
101
114
|
const WIKI_WRITE_REQUIREMENTS: readonly ScopeRequirement[] = [
|
|
102
115
|
{ anyOf: ['wiki:wiki'], level: 'optional', purpose: '在知识库中新建节点(feishu_wiki_create)' },
|
|
103
116
|
{ anyOf: ['docx:document'], level: 'optional', purpose: '写入与修改知识库文档内容(feishu_wiki_create / feishu_wiki_edit)' },
|
|
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)' },
|
|
121
|
+
...SHEET_BASE_READ_REQUIREMENTS,
|
|
104
122
|
]
|
|
105
123
|
|
|
106
124
|
/** Group receive needs while ordinary group messages require an @ mention. */
|
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
|
}
|