@modesto_ma/dsh-channels 0.5.0-beta.4 → 0.5.0-beta.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modesto_ma/dsh-channels",
3
- "version": "0.5.0-beta.4",
3
+ "version": "0.5.0-beta.5",
4
4
  "description": "DeepSeek Harness (DSH) 飞书渠道插件:一个 dsh 实例对应一个飞书机器人,私聊/群聊/话题群 @ 触发会话,话题绑定 session,卡片 2.0 流式回复;Web 设置里提供「飞书渠道」状态与常用配置面板",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,97 @@
1
+ /**
2
+ * 浏览器半区的 `number` 字段辅助:按 Host 描述的 `scale` 在显示单位与存储
3
+ * 单位之间换算、保存前校验上下界,以及字节数的 MiB 读数。
4
+ *
5
+ * 不含 JSX/React,便于单测。Host 保存时按 config schema 复核。
6
+ *
7
+ * @module dsh-channels/client/number-field
8
+ */
9
+
10
+ import { fillCopy, PANEL_COPY } from '../settings/contract.ts'
11
+ import type { ConfigValue, EditableFieldWire } from '../settings/contract.ts'
12
+
13
+ /** Drop float noise from `stored / scale` (e.g. 0.30000000000000004). */
14
+ function tidy(value: number): number {
15
+ return Number(value.toPrecision(12))
16
+ }
17
+
18
+ /**
19
+ * Value the input shows for one field.
20
+ *
21
+ * @param field - The Host descriptor (`kind: 'number'`).
22
+ * @param value - Draft (the typed string) or effective value (stored units).
23
+ * @returns The input text.
24
+ */
25
+ export function numberInputValue(field: EditableFieldWire, value: ConfigValue | undefined): string {
26
+ if (typeof value === 'string') return value
27
+ if (typeof value !== 'number') return ''
28
+ return String(field.scale === undefined ? value : tidy(value / field.scale))
29
+ }
30
+
31
+ /**
32
+ * Input `step` in display units.
33
+ *
34
+ * @param field - The Host descriptor (`kind: 'number'`).
35
+ * @returns The step, or `undefined` when the Host gives none.
36
+ */
37
+ export function numberInputStep(field: EditableFieldWire): number | undefined {
38
+ if (field.step === undefined) return undefined
39
+ return field.scale === undefined ? field.step : tidy(field.step / field.scale)
40
+ }
41
+
42
+ /** One bound in display units with the field's unit appended. */
43
+ function boundLabel(field: EditableFieldWire, stored: number): string {
44
+ const shown = field.scale === undefined ? stored : tidy(stored / field.scale)
45
+ return field.unit === undefined ? String(shown) : `${shown} ${field.unit}`
46
+ }
47
+
48
+ /**
49
+ * Turn one number draft into its save payload, checking the Host bounds.
50
+ *
51
+ * @param field - The Host descriptor (`kind: 'number'`).
52
+ * @param raw - The draft: the typed string, or a stored-unit number.
53
+ * @returns The stored-unit value (`''` clears an optional field), or the reason it is rejected.
54
+ */
55
+ export function numberDraftValue(
56
+ field: EditableFieldWire,
57
+ raw: ConfigValue | undefined,
58
+ ): { ok: true; value: number | '' } | { ok: false; message: string } {
59
+ let stored: number
60
+ if (typeof raw === 'number') {
61
+ stored = raw
62
+ } else {
63
+ const text = typeof raw === 'string' ? raw.trim() : ''
64
+ if (text === '') {
65
+ return field.optional === true ? { ok: true, value: '' } : { ok: false, message: PANEL_COPY.numberInvalid }
66
+ }
67
+ const parsed = Number(text)
68
+ if (!Number.isFinite(parsed)) return { ok: false, message: PANEL_COPY.numberInvalid }
69
+ stored = field.scale === undefined ? parsed : Math.round(parsed * field.scale)
70
+ }
71
+ if (field.min !== undefined && stored < field.min) {
72
+ return { ok: false, message: fillCopy(PANEL_COPY.numberMin, { n: boundLabel(field, field.min) }) }
73
+ }
74
+ if (field.max !== undefined && stored > field.max) {
75
+ return { ok: false, message: fillCopy(PANEL_COPY.numberMax, { n: boundLabel(field, field.max) }) }
76
+ }
77
+ return { ok: true, value: stored }
78
+ }
79
+
80
+ /**
81
+ * Human reading of a byte count, e.g. `20 MiB`.
82
+ *
83
+ * @param text - The input text (bytes).
84
+ * @returns The reading, or `undefined` when the text is not a positive number.
85
+ */
86
+ export function bytesReading(text: string): string | undefined {
87
+ const bytes = Number(text)
88
+ if (text.trim() === '' || !Number.isFinite(bytes) || bytes <= 0) return undefined
89
+ const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB']
90
+ let value = bytes
91
+ let unit = 0
92
+ while (value >= 1024 && unit < units.length - 1) {
93
+ value /= 1024
94
+ unit += 1
95
+ }
96
+ return `${Number(value.toFixed(value >= 100 || unit === 0 ? 0 : 1))} ${units[unit]}`
97
+ }
@@ -1,15 +1,18 @@
1
1
  /**
2
- * 「访问授权」tab:准入与授权字段表单(管理员 user_id / 准入模式 /
3
- * 申请有效期)+ 白名单与待处理申请卡。表单保存走全局 draft,白名单
4
- * 增删是独立即时 mutation。
2
+ * 「访问授权」tab:待处理申请(有才显示,置顶)、准入字段(调试模式 /
3
+ * 管理员 / 申请有效期)与群、用户白名单。字段保存走全局 draft,白名单增删
4
+ * 与申请处理是独立即时 mutation。
5
+ *
6
+ * `access.loaded === false`(飞书未连接、插件 storage 域未打开)时白名单与
7
+ * 申请列表是「未知」而不是「空」,因此只渲染连接提示并禁用全部写操作。
5
8
  *
6
9
  * @module dsh-channels/client/panel/access-tab
7
10
  */
8
11
 
9
- import { PANEL_COPY, TAB_FIELD_GROUPS } from '../../settings/contract.ts'
10
- import { AccessCard } from './access-card.tsx'
11
- import { FieldsCard, SaveBar, TabHelp } from './shared.tsx'
12
+ import { fillCopy, PANEL_COPY, TAB_FIELD_GROUPS } from '../../settings/contract.ts'
13
+ import { FieldSections, NoticeBar } from './shared.tsx'
12
14
  import type { FormView } from './shared.tsx'
15
+ import { RequestList, Whitelist } from './whitelist.tsx'
13
16
 
14
17
  export interface AccessTabProps {
15
18
  form: FormView
@@ -24,22 +27,60 @@ export interface AccessTabProps {
24
27
 
25
28
  export function AccessTab(props: AccessTabProps): JSX.Element {
26
29
  const { form, accessNotice, accessError, onAddGroup, onRemoveGroup, onAddUser, onRemoveUser, onResolveRequest } = props
30
+ const { access } = form.panel
31
+ const working = form.busy === 'access'
32
+ const disabled = form.busy !== null || form.locked || !access.loaded
33
+ // The administrator row says who is in charge right now when the owner fallback applies.
34
+ const ownerAdmin = access.mode === 'guarded' && access.admin.source === 'owner' ? access.admin.userId : undefined
35
+ const fields = ownerAdmin === undefined
36
+ ? form.fields
37
+ : form.fields.map(field => field.key === 'adminUserId'
38
+ ? { ...field, summary: fillCopy(PANEL_COPY.accessAdminOwnerSummary, { id: ownerAdmin }) }
39
+ : field)
27
40
  return (
28
41
  <div className="dsh-channels-cfg__tabBody">
29
- <TabHelp text={PANEL_COPY.accessTabHelp} />
30
- <FieldsCard form={form} groups={TAB_FIELD_GROUPS.access} />
31
- <SaveBar form={form} tab="access" />
32
- <AccessCard
33
- panel={form.panel}
34
- busy={form.busy}
35
- locked={form.locked}
36
- notice={accessNotice}
37
- error={accessError}
38
- onAddGroup={onAddGroup}
39
- onRemoveGroup={onRemoveGroup}
40
- onAddUser={onAddUser}
41
- onRemoveUser={onRemoveUser}
42
- onResolveRequest={onResolveRequest}
42
+ {accessNotice === null ? null : <NoticeBar kind="ok" text={accessNotice} />}
43
+ {accessError === null ? null : <NoticeBar kind="error" text={accessError} />}
44
+ {access.loaded && access.mode === 'guarded' && !access.admin.configured
45
+ ? (
46
+ <NoticeBar
47
+ kind="error"
48
+ text={access.admin.ownerLookupError === undefined
49
+ ? PANEL_COPY.accessAdminMissingWarning
50
+ : `${PANEL_COPY.accessAdminMissingWarning}(${access.admin.ownerLookupError})`}
51
+ />
52
+ )
53
+ : null}
54
+ {access.loaded && access.requests.length > 0
55
+ ? <RequestList requests={access.requests} disabled={disabled} working={working} onResolve={onResolveRequest} />
56
+ : null}
57
+ <FieldSections form={{ ...form, fields }} groups={TAB_FIELD_GROUPS.access} />
58
+ <Whitelist
59
+ title={PANEL_COPY.accessGroupsTitle}
60
+ entries={access.groups.map(group => ({ id: group.chatId, name: group.name, source: group.source }))}
61
+ avatar="群"
62
+ empty={PANEL_COPY.accessGroupsEmpty}
63
+ loaded={access.loaded}
64
+ addLabel={PANEL_COPY.accessAddGroup}
65
+ idPlaceholder={PANEL_COPY.accessGroupChatIdPlaceholder}
66
+ namePlaceholder={PANEL_COPY.accessGroupNamePlaceholder}
67
+ disabled={disabled}
68
+ working={working}
69
+ onAdd={onAddGroup}
70
+ onRemove={onRemoveGroup}
71
+ />
72
+ <Whitelist
73
+ title={PANEL_COPY.accessUsersTitle}
74
+ entries={access.users.map(user => ({ id: user.userId, name: user.name, source: user.source }))}
75
+ empty={PANEL_COPY.accessUsersEmpty}
76
+ loaded={access.loaded}
77
+ addLabel={PANEL_COPY.accessAddUser}
78
+ idPlaceholder={PANEL_COPY.accessUserIdPlaceholder}
79
+ namePlaceholder={PANEL_COPY.accessUserNamePlaceholder}
80
+ disabled={disabled}
81
+ working={working}
82
+ onAdd={onAddUser}
83
+ onRemove={onRemoveUser}
43
84
  />
44
85
  </div>
45
86
  )
@@ -1,80 +1,71 @@
1
1
  /**
2
- * 「高级」tab:流式刷新等低层字段 + 只读部署信息(overlayPath / version /
3
- * 部署级参数)+ 「清除 Web 覆盖」。清除覆盖是整体动作,不走 draft。
2
+ * 「高级」tab:日志分析接口(监听地址字段 + 访问令牌凭据行)、SDK 日志级别
3
+ * 与 Web 覆盖文件(「恢复 profile 配置」删除整个覆盖层,不走 draft)。
4
4
  *
5
5
  * @module dsh-channels/client/panel/advanced-tab
6
6
  */
7
7
 
8
- import { PANEL_COPY, TAB_FIELD_GROUPS } from '../../settings/contract.ts'
9
- import type { PanelWire } from '../../settings/contract.ts'
10
- import { FieldsCard, SaveBar, TabHelp } from './shared.tsx'
11
- import type { BusyAction, FormView } from './shared.tsx'
8
+ import { fillCopy, PANEL_COPY, TAB_FIELD_GROUPS } from '../../settings/contract.ts'
9
+ import { CredentialRow } from './credentials-card.tsx'
10
+ import { FieldSections, Row, Section } from './shared.tsx'
11
+ import type { AnalyzeTokenView, FormView } from './shared.tsx'
12
12
 
13
- export function AdvancedTab(props: { form: FormView; onClear: () => void }): JSX.Element {
14
- const { form, onClear } = props
15
- return (
16
- <div className="dsh-channels-cfg__tabBody">
17
- <TabHelp text={PANEL_COPY.advancedTabHelp} />
18
- <FieldsCard form={form} groups={TAB_FIELD_GROUPS.advanced} />
19
- <SaveBar form={form} tab="advanced" />
20
- <AdvancedCard panel={form.panel} busy={form.busy} locked={form.locked} onClear={onClear} />
21
- </div>
22
- )
23
- }
24
-
25
- /** Read-only deployment facts; the only write action here drops the overlay file. */
26
- function AdvancedCard(props: {
27
- panel: PanelWire
28
- busy: BusyAction | null
29
- locked: boolean
13
+ export function AdvancedTab(props: {
14
+ form: FormView
15
+ analyzeToken: AnalyzeTokenView
30
16
  onClear: () => void
31
17
  }): JSX.Element {
32
- const { panel, busy, locked, onClear } = props
33
- const { readOnly } = panel.config
18
+ const { form, analyzeToken, onClear } = props
19
+ const { panel, busy, locked } = form
20
+ const tokenState = panel.config.credentials.analyzeToken
34
21
  const overridden = panel.config.overriddenKeys.length
35
22
  return (
36
- <section className="dsh-channels-cfg__card">
37
- <div className="dsh-channels-cfg__cardHead">
38
- <span className="dsh-channels-cfg__cardTitle">{PANEL_COPY.advancedCard}</span>
39
- <span className="dsh-channels-cfg__cardMeta">
40
- {overridden === 0 ? PANEL_COPY.inherited : `${PANEL_COPY.overridden} ${overridden} 项`}
41
- </span>
42
- </div>
43
- <span className="dsh-channels-cfg__help">{PANEL_COPY.advancedHelp}</span>
44
- <div className="dsh-channels-cfg__labelRow">
45
- <span className="dsh-channels-cfg__cardTitle">{PANEL_COPY.readOnlyCard}</span>
46
- </div>
47
- <div className="dsh-channels-cfg__readOnly">
48
- {readOnly.map(field => (
49
- <div className="dsh-channels-cfg__kv" key={field.key}>
50
- <span className="dsh-channels-cfg__kvKey">{field.label}</span>
51
- <span className="dsh-channels-cfg__kvValue">{field.value}</span>
52
- {field.help === undefined ? null : <span className="dsh-channels-cfg__help">{field.help}</span>}
53
- </div>
54
- ))}
55
- <div className="dsh-channels-cfg__kv">
56
- <span className="dsh-channels-cfg__kvKey">{PANEL_COPY.overlayPath}</span>
57
- <span className="dsh-channels-cfg__kvValue dsh-channels-cfg__mono">{panel.status.overlayPath}</span>
58
- <span className="dsh-channels-cfg__help">{PANEL_COPY.overlayPathHelp}</span>
59
- </div>
60
- <div className="dsh-channels-cfg__kv">
61
- <span className="dsh-channels-cfg__kvKey">{PANEL_COPY.version}</span>
62
- <span className="dsh-channels-cfg__kvValue dsh-channels-cfg__mono">{panel.status.version}</span>
63
- </div>
64
- </div>
65
- <div className="dsh-channels-cfg__actions">
66
- <button
67
- type="button"
68
- className="dsh-channels-cfg__btn"
69
- onClick={onClear}
70
- disabled={overridden === 0 || busy !== null || locked}
71
- >
72
- {busy === 'clear' ? PANEL_COPY.clearing : PANEL_COPY.clear}
73
- </button>
74
- <span className="dsh-channels-cfg__help">
75
- {overridden === 0 ? PANEL_COPY.noOverrides : `${PANEL_COPY.overridden} ${overridden} 项`}
76
- </span>
77
- </div>
78
- </section>
23
+ <div className="dsh-channels-cfg__tabBody">
24
+ <FieldSections
25
+ form={form}
26
+ groups={TAB_FIELD_GROUPS.advanced}
27
+ extra={{
28
+ analyze: (
29
+ <CredentialRow
30
+ label={PANEL_COPY.analyzeTokenLabel}
31
+ summary={PANEL_COPY.analyzeTokenSummary}
32
+ state={tokenState}
33
+ value={analyzeToken.value}
34
+ secret
35
+ placeholder={PANEL_COPY.analyzeTokenPlaceholder}
36
+ issue={analyzeToken.issues.find(item => item.key === 'analyzeToken')}
37
+ busy={analyzeToken.busy}
38
+ disabled={analyzeToken.busy !== null || analyzeToken.locked || !tokenState.writable}
39
+ onChange={analyzeToken.onChange}
40
+ onSave={analyzeToken.onSave}
41
+ onClear={analyzeToken.onClear}
42
+ />
43
+ ),
44
+ }}
45
+ />
46
+ <Section title={PANEL_COPY.overlayTitle}>
47
+ <Row
48
+ label={PANEL_COPY.overlayPath}
49
+ summary={<span className="dsh-channels-cfg__mono">{panel.status.overlayPath}</span>}
50
+ details={(
51
+ <>
52
+ {PANEL_COPY.overlayPathHelp}
53
+ {' '}
54
+ {overridden === 0 ? PANEL_COPY.noOverrides : fillCopy(PANEL_COPY.overlayCount, { n: overridden })}
55
+ </>
56
+ )}
57
+ control={(
58
+ <button
59
+ type="button"
60
+ className="dsh-channels-cfg__btn dsh-channels-cfg__btn--danger"
61
+ onClick={onClear}
62
+ disabled={overridden === 0 || busy !== null || locked}
63
+ >
64
+ {busy === 'clear' ? PANEL_COPY.clearing : PANEL_COPY.clear}
65
+ </button>
66
+ )}
67
+ />
68
+ </Section>
69
+ </div>
79
70
  )
80
71
  }