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

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.
Files changed (41) hide show
  1. package/README.md +49 -28
  2. package/docs/attachment-pipeline-v2.md +1 -1
  3. package/docs/feishu-onboarding.md +6 -5
  4. package/lib/client.js +1874 -1510
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +736 -353
  7. package/lib/index.js.map +1 -1
  8. package/package.json +2 -2
  9. package/src/client/api.ts +45 -2
  10. package/src/client/avatar.tsx +36 -0
  11. package/src/client/icon.tsx +25 -0
  12. package/src/client/index.ts +78 -23
  13. package/src/client/number-field.ts +97 -0
  14. package/src/client/origin-cache.ts +45 -0
  15. package/src/client/origin.tsx +201 -0
  16. package/src/client/panel/access-tab.tsx +90 -21
  17. package/src/client/panel/advanced-tab.tsx +59 -68
  18. package/src/client/panel/connection-tab.tsx +147 -164
  19. package/src/client/panel/credentials-card.tsx +124 -87
  20. package/src/client/panel/fields-tab.tsx +27 -0
  21. package/src/client/panel/index.tsx +77 -54
  22. package/src/client/panel/profiles.ts +73 -0
  23. package/src/client/panel/shared.tsx +358 -253
  24. package/src/client/panel/whitelist.tsx +203 -0
  25. package/src/client/panel/wiki-tab.tsx +33 -39
  26. package/src/client/styles.ts +132 -92
  27. package/src/domain.ts +42 -0
  28. package/src/feishu/scopes.ts +2 -1
  29. package/src/feishu-channel.ts +87 -0
  30. package/src/index.ts +1 -1
  31. package/src/routing/inbound.ts +13 -5
  32. package/src/service.ts +146 -3
  33. package/src/session/routes.ts +45 -4
  34. package/src/settings/contract.ts +265 -152
  35. package/src/settings/routes.ts +47 -0
  36. package/src/settings/surface.ts +201 -197
  37. package/src/client/panel/access-card.tsx +0 -329
  38. package/src/client/panel/analyze-token-card.tsx +0 -70
  39. package/src/client/panel/model-tab.tsx +0 -23
  40. package/src/client/panel/sessions-tab.tsx +0 -24
  41. package/src/client/panel/workspace-tab.tsx +0 -24
@@ -2,7 +2,7 @@
2
2
  * Web 设置面板的线上契约:Host 半侧的路由/面板数据与 Browser 半侧的渲染共用。
3
3
  *
4
4
  * 本模块**没有运行时依赖**:Host 与 Client bundle 都直接内联它。因此这里只放
5
- * 常量、字段清单与可序列化的 wire 类型,不 import zod / node / DSH 内部包。
5
+ * 常量、字段清单、可序列化的 wire 类型与无依赖的纯文案函数,不 import zod / node / DSH 内部包。
6
6
  *
7
7
  * @module dsh-channels/settings/contract
8
8
  */
@@ -38,6 +38,71 @@ export const ACCESS_REQUESTS_ROUTE = '/dsh-channels/access/requests'
38
38
  * [`EditableFieldWire.browse`],保留手写输入。
39
39
  */
40
40
  export const PICK_DIRECTORY_ROUTE = '/dsh-channels/pick-directory'
41
+ /** 会话的飞书来源(GET/HEAD,`?sessionId=`):对话右侧栏「飞书来源」标签与会话头部入口使用。 */
42
+ export const SESSION_ORIGIN_ROUTE = '/dsh-channels/session-origin'
43
+
44
+ /** Where one Feishu-routed Session came from; names are resolved live and may be absent. */
45
+ export interface SessionOriginWire {
46
+ sessionId: string
47
+ /** `p2p` = 私聊,`group` = 群聊。 */
48
+ chatType: 'p2p' | 'group'
49
+ /** `thread` = 话题内会话,`chat` = 整个聊天一个会话(话题不可用时的降级)。 */
50
+ kind: 'thread' | 'chat'
51
+ chatId: string
52
+ /** 群名(群聊且能读取群信息时)。 */
53
+ chatName?: string
54
+ /** 群头像 URL(群聊且能读取群信息时)。 */
55
+ chatAvatarUrl?: string
56
+ /** 发起人 user_id(日志平台分析话题为请求人)。 */
57
+ senderUserId: string
58
+ /** 发起人姓名(通讯录或群成员列表能查到时)。 */
59
+ senderName?: string
60
+ /** 发起人头像 URL(通讯录能查到时)。 */
61
+ senderAvatarUrl?: string
62
+ /** 日志平台分析话题:群内成员都可以继续。 */
63
+ shared: boolean
64
+ /** Epoch ms when the Session was bound to its Feishu route. */
65
+ createdAt: number
66
+ /** Whether the Feishu route still points at this Session (false after `/renew`, `/new` or route expiry). */
67
+ bound: boolean
68
+ /** Epoch ms of the route's last activity, while bound. */
69
+ lastActivityAt?: number
70
+ /** 在飞书客户端打开该聊天的链接。 */
71
+ chatLink: string
72
+ }
73
+
74
+ /**
75
+ * `GET PROFILES_ROUTE?users=<user_id,...>&chats=<chat_id,...>`: live Feishu
76
+ * names and avatars for the users and chats the panel shows. At most
77
+ * {@link PROFILES_MAX_IDS} ids per list.
78
+ */
79
+ export const PROFILES_ROUTE = '/dsh-channels/profiles'
80
+
81
+ /** Most `user_id`s, and separately most `chat_id`s, one profiles request may name. */
82
+ export const PROFILES_MAX_IDS = 200
83
+
84
+ /** Display name and avatar of one Feishu user or chat. */
85
+ export interface FeishuProfileWire {
86
+ name?: string
87
+ /** `https:` image URL on the Feishu CDN. */
88
+ avatarUrl?: string
89
+ }
90
+
91
+ /**
92
+ * `GET PROFILES_ROUTE` response. Ids Feishu could not resolve, and ids that
93
+ * are not valid Feishu ids, are absent; `warning` carries the Feishu reason
94
+ * when a lookup failed (typically a missing scope).
95
+ */
96
+ export interface FeishuProfilesWire {
97
+ users: Record<string, FeishuProfileWire>
98
+ chats: Record<string, FeishuProfileWire>
99
+ warning?: string
100
+ }
101
+
102
+ /** `GET SESSION_ORIGIN_ROUTE` response: `origin` is null for a Session not started from Feishu. */
103
+ export interface SessionOriginResponse {
104
+ origin: SessionOriginWire | null
105
+ }
41
106
 
42
107
  /**
43
108
  * 可由 Web 面板覆盖的配置项。顶层键对应 `src/config.ts` 的字段;`feishuTools.*`
@@ -52,52 +117,62 @@ export const PICK_DIRECTORY_ROUTE = '/dsh-channels/pick-directory'
52
117
  * `baseDir`(工作区根目录 = 模型/工具产生文件的落点)可覆盖,但面板只接受
53
118
  * **绝对路径**(`~` 会展开);已有 Session / 目录不迁移,只影响之后新建的
54
119
  * 会话。校验与保存时建目录见 `src/workspaces.ts`。
120
+ *
121
+ * 数组顺序就是面板里同一分组内字段的显示顺序。
55
122
  */
56
123
  export const EDITABLE_KEYS = [
57
- // 工作区
58
- 'baseDir',
59
- // 行为
124
+ // 会话:触发
60
125
  'dmMode',
61
126
  'requireMention',
62
127
  'threadFallback',
128
+ // 会话:工作区
129
+ 'baseDir',
130
+ 'topicIsolation',
131
+ // 会话:话题与卡片
132
+ 'routeTtlMs',
133
+ 'approvalTimeoutMs',
134
+ 'questionTimeoutMs',
135
+ 'refreshMs',
136
+ 'statusRefreshMs',
137
+ // 会话:飞书侧功能
63
138
  'welcomeOnboarding',
64
139
  'slashCommandPanel',
65
140
  'allowPermissionSwitch',
66
- // 日志平台分析接口(访问令牌是凭据引用 analyzeToken,值写在 DSH 凭据里)
67
- 'analyzeListen',
68
- // 准入与授权(管理员/准入模式/申请有效期;白名单由「访问与授权」卡片管理)
69
- 'adminUserId',
70
- 'accessMode',
71
- 'authRequestTtlDays',
72
- // 内容与附件(v2 管线;`feishuTools.*` 是嵌套配置,覆盖层仍写成嵌套对象)
141
+ // 模型
142
+ 'provider',
143
+ 'model',
144
+ 'reasoningEffort',
145
+ 'maxTokens',
146
+ 'agentPreset',
147
+ // 附件(v2 管线)
73
148
  'acceptAttachments',
74
149
  'attachmentStore',
75
150
  'prefetchImages',
151
+ 'historyBootstrapLimit',
152
+ // 模型工具(`feishuTools.*` 是嵌套配置,覆盖层仍写成嵌套对象)
153
+ 'feishuTools.attachmentFetch',
154
+ 'feishuTools.topicMessages',
155
+ 'feishuTools.sendFile',
156
+ // 限额与超时
76
157
  'prefetchTimeoutMs',
77
158
  'prefetchImageMaxBytes',
78
159
  'prefetchTotalMaxBytes',
79
- 'historyBootstrapLimit',
80
- // 模型工具(feishuTools 子项)
81
- 'feishuTools.attachmentFetch',
82
160
  'feishuTools.attachmentFetchMaxResources',
83
161
  'feishuTools.attachmentFetchMaxBytes',
84
162
  'feishuTools.attachmentFetchTimeoutMs',
85
- 'feishuTools.topicMessages',
86
163
  'feishuTools.topicMessagesTimeoutMs',
87
- 'feishuTools.sendFile',
164
+ 'promptContextTimeoutMs',
165
+ // 准入与授权(白名单由「访问授权」tab 的列表管理)
166
+ 'accessMode',
167
+ 'adminUserId',
168
+ 'authRequestTtlDays',
169
+ // 知识库(顶层 `wikiRoots` 字符串列表;为空时不挂载任何知识库工具)
88
170
  'feishuTools.wikiRead',
89
171
  'feishuTools.wikiWrite',
90
- // 知识库范围(顶层 `wikiRoots` 字符串列表;为空时不挂载任何知识库工具)
91
172
  'wikiRoots',
92
- 'promptContextTimeoutMs',
93
- // 模型
94
- 'provider',
95
- 'model',
96
- 'reasoningEffort',
97
- 'agentPreset',
98
- // 流式卡片
99
- 'refreshMs',
100
- 'statusRefreshMs',
173
+ // 日志平台分析接口(访问令牌是凭据引用 analyzeToken,值写在 DSH 凭据里)
174
+ 'analyzeListen',
175
+ // 日志
101
176
  'loggerLevel',
102
177
  ] as const
103
178
 
@@ -140,7 +215,19 @@ export type StringListItemFormat = 'wiki-root-link'
140
215
  * panel tab (see {@link TAB_FIELD_GROUPS}); the Host is the source of truth for
141
216
  * which key belongs to which group.
142
217
  */
143
- export type FieldGroup = 'workspace' | 'behavior' | 'content' | 'tools' | 'model' | 'streaming' | 'access' | 'wiki'
218
+ export type FieldGroup =
219
+ | 'trigger'
220
+ | 'workspace'
221
+ | 'cards'
222
+ | 'features'
223
+ | 'model'
224
+ | 'attachments'
225
+ | 'tools'
226
+ | 'limits'
227
+ | 'access'
228
+ | 'wiki'
229
+ | 'analyze'
230
+ | 'logging'
144
231
 
145
232
  /** One selectable value rendered by the generic form. */
146
233
  export interface FieldOptionWire {
@@ -148,14 +235,38 @@ export interface FieldOptionWire {
148
235
  label: string
149
236
  }
150
237
 
238
+ /**
239
+ * A two-value `select` rendered as a switch: `on` is the value stored while
240
+ * the switch is on, `off` while it is off. Both are members of `options`.
241
+ */
242
+ export interface FieldToggleWire {
243
+ on: string
244
+ off: string
245
+ }
246
+
151
247
  /** One editable row, fully described by the Host so the client stays schema-free. */
152
248
  export interface EditableFieldWire {
153
249
  key: EditableKey
154
250
  label: string
155
251
  kind: FieldKind
156
252
  group: FieldGroup
253
+ /** One-line description always shown under the label. */
254
+ summary?: string
255
+ /** Longer explanation, collapsed behind the row's「详情」link. */
157
256
  help?: string
158
257
  options?: readonly FieldOptionWire[]
258
+ /** `select` only: render the two options as a switch. */
259
+ toggle?: FieldToggleWire
260
+ /**
261
+ * `number` only: display unit next to the input. With `scale`, the input
262
+ * shows `stored / scale` and the panel submits `round(input * scale)`;
263
+ * `min` / `max` / `step` stay in stored units.
264
+ */
265
+ unit?: string
266
+ /** `number` only: stored units per displayed unit (e.g. 1000 for 秒 over ms). */
267
+ scale?: number
268
+ /** `number` only: an empty input clears the override instead of failing. */
269
+ optional?: boolean
159
270
  /** Selector is not editable yet (e.g. pick the provider before the model). */
160
271
  disabled?: boolean
161
272
  min?: number
@@ -167,7 +278,7 @@ export interface EditableFieldWire {
167
278
  maxItems?: number
168
279
  /** `string-list` only: format every non-empty entry must satisfy. */
169
280
  itemFormat?: StringListItemFormat
170
- /** Render across the whole form width (long paths). */
281
+ /** Render the control below the label across the row width (paths, lists). */
171
282
  wide?: boolean
172
283
  /**
173
284
  * `directory`:给这个 text 字段加一个「选择目录…」按钮,点击后由 Host
@@ -218,14 +329,6 @@ export interface CatalogWire {
218
329
  export type ConfigValue = string | number | boolean | readonly string[]
219
330
  export type EditableValues = Partial<Record<EditableKey, ConfigValue>>
220
331
 
221
- /** Read-only facts shown in the advanced card. */
222
- export interface ReadOnlyFieldWire {
223
- key: string
224
- label: string
225
- value: string
226
- help?: string
227
- }
228
-
229
332
  /** One credential reference plus its Host-side state (values never cross the wire). */
230
333
  export interface CredentialRefWire {
231
334
  ref: string
@@ -344,7 +447,6 @@ export interface PanelConfigWire {
344
447
  /** 与当前运行中连接不一致、需重连才生效的字段。 */
345
448
  restartPendingKeys: EditableKey[]
346
449
  fields: EditableFieldWire[]
347
- readOnly: ReadOnlyFieldWire[]
348
450
  credentials: {
349
451
  appId: CredentialRefWire
350
452
  appSecret: CredentialRefWire
@@ -530,9 +632,9 @@ export type ClearCredentialResponse =
530
632
  */
531
633
  export const PANEL_TABS = [
532
634
  { id: 'connection', label: '连接' },
533
- { id: 'sessions', label: '会话与行为' },
635
+ { id: 'sessions', label: '会话' },
534
636
  { id: 'model', label: '模型' },
535
- { id: 'workspace', label: '工作区' },
637
+ { id: 'attachments', label: '附件与工具' },
536
638
  { id: 'access', label: '访问授权' },
537
639
  { id: 'wiki', label: '知识库' },
538
640
  { id: 'advanced', label: '高级' },
@@ -540,92 +642,91 @@ export const PANEL_TABS = [
540
642
 
541
643
  export type PanelTabId = (typeof PANEL_TABS)[number]['id']
542
644
 
543
- /** Which editable field groups each tab renders (the tabs own one or two). */
645
+ /** Which editable field groups each tab renders, in section order. */
544
646
  export const TAB_FIELD_GROUPS: Readonly<Record<PanelTabId, readonly FieldGroup[]>> = {
545
647
  connection: [],
546
- sessions: ['behavior', 'content', 'tools'],
648
+ sessions: ['trigger', 'workspace', 'cards', 'features'],
547
649
  model: ['model'],
548
- workspace: ['workspace'],
650
+ attachments: ['attachments', 'tools', 'limits'],
549
651
  access: ['access'],
550
652
  wiki: ['wiki'],
551
- advanced: ['streaming'],
653
+ advanced: ['analyze', 'logging'],
552
654
  }
553
655
 
554
656
  /** Section heading for one field group inside its tab. */
555
657
  export const FIELD_GROUP_LABELS: Readonly<Record<FieldGroup, string>> = {
556
- workspace: '工作区与文件',
557
- behavior: '行为',
558
- content: '内容与附件',
559
- tools: '模型工具',
560
- model: '模型与推理',
561
- streaming: '流式卡片与日志',
562
- access: '准入与授权',
658
+ trigger: '触发',
659
+ workspace: '工作区',
660
+ cards: '话题与卡片',
661
+ features: '飞书侧功能',
662
+ model: '模型',
663
+ attachments: '入站附件',
664
+ tools: '模型可用的飞书工具',
665
+ limits: '限额与超时',
666
+ access: '准入',
563
667
  wiki: '知识库工具',
668
+ analyze: '日志分析接口',
669
+ logging: '日志',
564
670
  }
565
671
 
672
+ /** Groups rendered collapsed behind one summary row (rarely changed values). */
673
+ export const COLLAPSED_FIELD_GROUPS: readonly FieldGroup[] = ['limits']
674
+
675
+ /** `unit` of byte-count fields; the panel adds a MiB reading next to them. */
676
+ export const BYTES_UNIT = '字节'
677
+
566
678
  /** Display labels for the panel header/cards (kept on the wire so the client owns no copy). */
567
679
  export const PANEL_COPY = {
568
680
  title: '飞书渠道',
569
- description: '飞书长连接、应用凭据与渠道配置。凭据只写入 DSH 凭据库且不回显;标注「需重连」的项在连接建立时读取。',
570
681
  // ---- lifecycle / status -------------------------------------------------
571
- statusCard: '连接状态',
572
682
  phaseUnconfigured: '待配置',
573
683
  phaseIdle: '未连接',
574
684
  phaseConnecting: '连接中',
575
685
  phaseConnected: '已连接',
576
686
  phaseError: '连接失败',
577
- configuredBadge: '凭据已配置',
578
- unconfiguredBadge: '凭据未配置',
579
- statusSummaryConnected: '飞书长连接已建立,机器人可以收发消息。',
580
- statusSummaryUnconfigured: '尚未配置飞书应用凭据:插件已加载,但不会连接飞书。填写凭据并保存后会自动连接。',
581
- statusSummaryIdle: '飞书应用凭据已就绪,但当前没有建立长连接。',
582
- statusSummaryConnecting: '正在连接飞书长连接…',
583
- statusSummaryError: '最近一次连接飞书失败,请检查凭据与网络后重试。',
687
+ statusSummaryConnected: '机器人在线,可以收发消息',
688
+ statusSummaryUnconfigured: '还没有填写飞书应用凭据,插件不会连接飞书',
689
+ statusSummaryIdle: '凭据已就绪,还没有建立连接',
690
+ statusSummaryConnecting: '正在连接飞书…',
691
+ statusSummaryError: '连接飞书失败,请检查凭据与网络后重试',
692
+ uptimePrefix: '运行',
584
693
  lastError: '最近一次连接失败',
585
- scopeCheckTitle: '权限自检',
586
- scopeCheckOk: '已开通插件需要的全部飞书权限。',
587
- scopeCheckMissing: '以下飞书权限未开通,开通后需在开发者后台发布新版本:',
588
- scopeCheckOpenAll: '在开发者后台开通全部缺失权限',
694
+ checksTitle: '自检',
695
+ scopeCheckOk: '插件需要的飞书权限都已开通',
696
+ scopeCheckMissing: '{n} 项权限未开通',
697
+ scopeCheckMissingRequired: '{n} 项必需权限未开通,相关功能无法使用',
698
+ scopeCheckMissingOptional: '不影响收发消息,只有相关功能不可用。开通后需在开发者后台发布新版本。',
699
+ scopeCheckPublish: '开通后需在开发者后台发布新版本。',
700
+ scopeCheckOpenAll: '全部开通',
589
701
  scopeCheckOpenOne: '去开通',
590
- scopeCheckOptional: '可选',
591
702
  scopeCheckRequired: '必需',
592
- scopeCheckError: '无法读取应用的权限授予状态:',
703
+ scopeCheckError: '无法读取权限开通情况',
593
704
  slashCommandSyncTitle: '斜杠指令面板',
594
- slashCommandSyncOk: '插件命令已同步到飞书原生斜杠指令面板,在聊天框输入 / 即可选择(飞书客户端约 5 分钟后刷新)。',
595
- slashCommandSyncTotal: '命令数',
596
- slashCommandSyncCreated: '本次新增',
597
- slashCommandSyncUpdated: '本次更新',
598
- slashCommandSyncRemoved: '本次删除',
599
- slashCommandSyncError: '同步飞书原生斜杠指令面板失败(不影响直接输入命令):',
705
+ slashCommandSyncOk: '已同步 {total} 条命令,飞书客户端约 5 分钟后刷新',
706
+ slashCommandSyncError: '同步失败,直接输入命令不受影响',
600
707
  missingConfig: '缺少配置',
601
- statusHintUnconfigured: '配置完成后插件会自动连接;也可以稍后在本页手动连接。',
602
- connectionInfo: '连接信息',
603
- connectionStateLabel: '飞书长连接',
604
- botOpenId: 'Bot open_id',
605
- uptime: '运行时长',
606
708
  routeCount: '话题路由',
607
709
  liveAgentCount: '活动会话',
608
710
  inflightTurnCount: '在途回复',
711
+ botOpenId: 'Bot open_id',
609
712
  healthReasons: '健康判定',
610
713
  unknownValue: '—',
611
714
  connect: '连接飞书',
612
715
  connecting: '连接中…',
613
716
  // ---- onboarding ---------------------------------------------------------
614
717
  onboardingTitle: '开始使用飞书渠道',
615
- onboardingIntro: '只需三步:在飞书开放平台创建自建应用,把 App ID / App Secret 填入本页,保存后自动连接。',
616
718
  onboardingStep1Title: '创建飞书自建应用',
617
- onboardingStep1Text: '在飞书开放平台创建企业自建应用,开启机器人能力;事件订阅选择「长连接」,订阅 im.message.receive_v1(接收消息)。',
719
+ onboardingStep1Text: '在 open.feishu.cn/app 创建企业自建应用,开启机器人能力;事件订阅选「长连接」,订阅 im.message.receive_v1。',
618
720
  onboardingStep2Title: '填写应用凭据',
619
- onboardingStep2Text: '把应用的 App ID(cli_ 开头)与 App Secret 填入下方「飞书应用凭据」,值会写入 DSH 凭据库。',
620
- onboardingStep3Title: '保存并等待连接',
621
- onboardingStep3Text: '两项凭据都保存后插件会自动连接;状态变为「已连接」即可在飞书里私聊机器人。',
622
- onboardingDocsHint: '开放平台地址:open.feishu.cn/app;应用需要开通 im:message、im:message:send_as_bot 等消息权限。',
721
+ onboardingStep2Text: '把 App ID(cli_ 开头)和 App Secret 填到下方「应用凭据」。',
722
+ onboardingStep3Title: '保存后自动连接',
723
+ onboardingStep3Text: '两项都保存后插件会自动连接,状态变成「已连接」就能在飞书里私聊机器人。',
623
724
  // ---- credentials --------------------------------------------------------
624
- credentialsCard: '飞书应用凭据',
625
- credentialsCardHelp: '凭据值经 DSH 凭据服务写入本机凭据库;面板只显示「已配置 / 未配置」,永不回显已存值。两项都配置后才会连接飞书。',
626
- appIdLabel: '飞书 App ID',
627
- appSecretLabel: '飞书 App Secret',
628
- credentialsRef: '凭据引用',
725
+ credentialsTitle: '应用凭据',
726
+ credentialsHint: '值只存在本机凭据库,面板不回显。',
727
+ appIdLabel: 'App ID',
728
+ appSecretLabel: 'App Secret',
729
+ credentialStore: '凭据库',
629
730
  saveCredentials: '保存凭据',
630
731
  savingCredentials: '保存中…',
631
732
  credentialSaved: '凭据已保存',
@@ -635,28 +736,30 @@ export const PANEL_COPY = {
635
736
  credentialAutoConnectFailed: '凭据已保存,但自动连接失败',
636
737
  credentialRemoved: '已移除存储的凭据',
637
738
  credentialRemovedPending: '已移除存储的凭据,重启飞书连接后生效',
638
- removeCredential: '移除已存值',
739
+ replaceCredential: '更换',
740
+ setCredential: '设置',
741
+ cancel: '取消',
742
+ removeCredential: '移除',
639
743
  removingCredential: '移除中…',
640
744
  credentialConfigured: '已配置',
641
745
  credentialMissing: '未配置',
642
746
  credentialReadOnly: '当前凭据存储不可写',
643
747
  appIdPlaceholder: '输入 App ID(cli_…)',
644
748
  appSecretPlaceholder: '输入 App Secret',
645
- credentialsPendingHint: '填写后点「保存凭据」;两项齐全时插件会自动连接飞书。',
646
- // ---- tabs / forms -------------------------------------------------------
647
- connectionTabHelp: '连接状态与飞书应用凭据。凭据不完整时不会连接飞书。',
648
- sessionsTabHelp: '私聊、群聊触发方式与入站内容:附件存储/预取、模型工具开关与读取预算;「需重连」项在下次连接时读取。',
649
- modelTabHelp: '飞书会话使用的模型服务商、模型、推理档位与 Agent 预设;下拉选项来自宿主机实时目录,保存后对新会话生效。',
650
- workspaceTabHelp: '每个用户一个工作区(模型/工具产生文件的落点),每个话题在其中拥有独立工作目录;只影响之后新建的话题。入站附件默认存于 DSH 原生附件库,不占用话题目录。插件不会自动删除工作区或话题目录;过期目录可手动删除,话题下次对话时会重建空目录。',
651
- accessTabHelp: '准入采用 fail-closed:管理员不受白名单限制,非管理员私聊会被拒绝,群聊需要「群已授权 + 用户在白名单」两层同时命中。',
652
- wikiTabHelp: '知识库工具以应用身份访问「知识库范围」内的文档、电子表格和多维表格。只有管理员或知识库白名单里的用户发起的话题能用;每次打开、新建、修改仍需话题发起人在授权卡上确认。',
653
- advancedTabHelp: '流式刷新等低层参数、Web 覆盖文件与只读信息。',
654
- save: '保存更改',
749
+ credentialRemoveConfirm: '确定移除已存储的凭据值?移除后不会回显,需要时须重新填写。',
750
+ // ---- forms --------------------------------------------------------------
751
+ save: '保存',
655
752
  saving: '保存中…',
656
- saveHintDirty: '只保存改动过的字段;标注「需重连」的项在下次连接时生效。',
657
- discardTab: '放弃本页更改',
658
- saveHintClean: '没有未保存的更改。',
659
- dirtyCount: '未保存项',
753
+ discard: '放弃',
754
+ dirtyCount: '{n} 项未保存',
755
+ dirtyRestart: ',其中 {n} 项需要重连后生效',
756
+ details: '详情',
757
+ expand: '展开',
758
+ collapse: '收起',
759
+ collapsedCount: '{n} 项',
760
+ collapsedClean: '一般不用改',
761
+ collapsedChanged: '{n} 项已在面板修改',
762
+ restartTag: '需重连',
660
763
  listAdd: '添加',
661
764
  listRemove: '移除',
662
765
  listEntry: '第 {n} 项',
@@ -668,64 +771,61 @@ export const PANEL_COPY = {
668
771
  directoryPicked: '已选择目录',
669
772
  directoryPickFailed: '无法打开系统目录选择器',
670
773
  saved: '已保存',
671
- cleared: '已清除 Web 覆盖,恢复为 profile 配置',
672
- clear: '清除 Web 覆盖',
673
- clearing: '清除中…',
674
- noOverrides: '当前没有 Web 覆盖',
675
- overridden: 'Web 覆盖',
676
- inherited: '继承 profile',
677
- restartTag: '需重连',
678
- restartBanner: '以下配置需重启飞书连接后生效:',
679
- restartWarning: '重启会断开当前飞书长连接并中断进行中的回复,请确认空闲时操作。',
680
- reconnect: '重启飞书连接',
774
+ saveInvalid: '请先修正标红的配置项',
775
+ numberInvalid: '请输入数字',
776
+ numberMin: '不得小于 {n}',
777
+ numberMax: '不得大于 {n}',
778
+ modelFootnote: '对之后新建的会话生效。',
779
+ catalogFailed: '模型目录加载失败,已退回手填。',
780
+ catalogLoading: '正在读取宿主机选项…',
781
+ catalogUnavailable: '宿主机未提供可选项,请手填。',
782
+ // ---- restart ------------------------------------------------------------
783
+ restartBanner: '以下配置要重启飞书连接后生效:',
784
+ restartWarning: '重启会断开当前飞书连接并中断进行中的回复,确定现在重启?',
785
+ reconnect: '重启连接',
681
786
  reconnecting: '重连中…',
682
- refresh: '刷新状态',
787
+ refresh: '刷新',
683
788
  retry: '重试',
684
789
  pollFailed: '状态刷新失败',
685
790
  connectDone: '已连接飞书',
686
791
  reconnectDone: '已重启飞书连接',
687
- saveInvalid: '请先修正标红的配置项',
688
- enabled: '已启用',
689
- disabled: '已关闭',
690
- credentialRemoveConfirm: '确定移除已存储的凭据值?移除后不会回显,需要时须重新填写。',
691
- analyzeTokenCard: '日志分析接口令牌',
692
- analyzeTokenCardHelp: '日志平台调用分析接口时 Authorization: Bearer 后面的值。经 DSH 凭据服务写入本机凭据库,面板永不回显;保存或移除后下一次请求即生效,无需重连。未配置时接口对所有请求返回 503。',
792
+ loading: '加载中…',
793
+ loadFailed: '设置面板加载失败',
794
+ // ---- advanced -----------------------------------------------------------
693
795
  analyzeTokenLabel: '访问令牌',
796
+ analyzeTokenSummary: '请求头 Authorization: Bearer 的值,保存后立即生效;未配置时接口拒绝所有请求',
694
797
  analyzeTokenPlaceholder: '填写新令牌(建议 32 位以上随机串)',
695
- saveAnalyzeToken: '保存令牌',
696
798
  analyzeTokenSaved: '令牌已保存,下一次请求生效',
697
799
  analyzeTokenRemoved: '令牌已移除,接口将拒绝所有请求',
698
- accessNotLoaded: '飞书连接建立后加载授权列表',
699
- loading: '加载中…',
700
- loadFailed: '设置面板加载失败',
701
- catalogFailed: '模型目录加载失败,已退回手填。',
702
- catalogLoading: '正在读取宿主机选项…',
703
- catalogUnavailable: '宿主机未提供可选项,请手填。',
704
- version: '插件版本',
800
+ overlayTitle: 'Web 覆盖',
705
801
  overlayPath: '覆盖文件',
706
- overlayPathHelp: '可以直接编辑该 JSON 文件;面板保存会整体重写它,「清除 Web 覆盖」会删除它并回落到 profile 配置。',
707
- advancedCard: '只读与高级信息',
708
- advancedHelp: '这些值来自 profile 配置,不能在面板里编辑。',
709
- readOnlyCard: '部署级参数',
710
- // ---- model --------------------------------------------------------------
802
+ overlayCount: '{n} 项配置由面板覆盖',
803
+ noOverrides: '当前没有面板覆盖的配置',
804
+ overlayPathHelp: '面板保存的配置写在这个 JSON 文件里,优先级高于 profile。可以直接编辑;面板保存会整体重写它。',
805
+ clear: '恢复 profile 配置',
806
+ clearing: '恢复中…',
807
+ clearConfirm: '确定删除所有面板覆盖,恢复为 profile 配置?',
808
+ cleared: '已恢复为 profile 配置',
711
809
  // ---- access -------------------------------------------------------------
712
- accessCard: '访问与授权',
713
- accessCardHelp: '准入采用 fail-closed:管理员不受白名单限制(私聊、任意群聊都可直接对话);非管理员私聊会被拒绝,群聊需要「群已授权 + 用户在白名单」两层同时命中。管理员与准入模式在上方「准入与授权」中编辑;私聊机器人发送 /userid 获取自己的 user_id。',
714
- accessModeOpenWarning: '当前准入模式为开放(open):应用可用范围内的任何人都能开启对话。该模式不安全,仅建议调试时使用。',
715
- accessAdminMissingWarning: '当前准入=guarded 但未配置管理员,也无法自动使用应用所有者,所有对话都会被拒绝。请在上方「准入与授权」填写管理员 user_id(私聊机器人发送 /userid 获取;私聊模式关闭时 /userid 不可用,可在飞书管理后台的成员详情里查看用户 ID)。',
716
- accessAdminOwnerNotice: '未配置管理员,已自动使用应用所有者作为管理员:',
810
+ debugModeWarning: '调试模式已打开:应用可用范围内的任何人都能和机器人对话。用完请在「访问授权」关闭。',
811
+ accessAdminMissingWarning: '没有可用的管理员,所有对话都会被拒绝。请在下方填写管理员 user_id(私聊机器人发送 /userid 获取)。',
812
+ accessAdminCurrent: '当前管理员',
813
+ accessAdminSourceConfig: '已配置',
814
+ accessAdminSourceOwner: '应用所有者',
815
+ profilesWarning: '部分飞书名字和头像未能读取:{reason}',
816
+ accessNotLoaded: '飞书连接建立后加载授权列表',
817
+ accessRequestsTitle: '待处理申请',
717
818
  accessGroupsTitle: '已授权群',
718
- accessGroupsEmpty: '尚未授权任何群。机器人被拉进群后,管理员会收到带 chat_id 的群信息卡;也可在群内发送 /groupid 获取。',
719
819
  accessUsersTitle: '已授权用户',
720
- accessUsersEmpty: '尚未授权任何用户。已授权用户可在所有已授权群里与机器人对话(私聊仍只开给管理员)。',
721
- accessRequestsTitle: '待处理申请',
722
- accessRequestsEmpty: '没有待处理的授权申请。',
723
- accessGroupChatIdPlaceholder: '群 chat_id(oc_…)',
820
+ accessGroupsEmpty: '还没有授权任何群',
821
+ accessUsersEmpty: '还没有授权任何用户',
822
+ accessGroupChatIdPlaceholder: 'chat_id(oc_…,群里发 /groupid 获取)',
724
823
  accessGroupNamePlaceholder: '群名(可选)',
725
- accessUserIdPlaceholder: '用户 user_id',
824
+ accessUserIdPlaceholder: 'user_id(私聊机器人发 /userid 获取)',
726
825
  accessUserNamePlaceholder: '昵称(可选)',
727
826
  accessAddGroup: '添加群',
728
827
  accessAddUser: '添加用户',
828
+ accessAdd: '添加',
729
829
  accessRemove: '移除',
730
830
  accessAllow: '授权',
731
831
  accessDeny: '忽略',
@@ -733,22 +833,35 @@ export const PANEL_COPY = {
733
833
  accessSourceCommand: '对话命令',
734
834
  accessSourceCard: '授权卡',
735
835
  accessSourcePanel: '设置面板',
736
- accessRequestGroup: '群申请',
737
- accessRequestUser: '用户申请',
738
- accessRequestRequester: '申请人',
739
- accessRequestExpires: '有效期至',
836
+ accessRequestGroup: '申请授权群',
837
+ accessRequestUser: '申请使用',
838
+ accessRequestIn: '在「{group}」',
839
+ accessRequestExpires: '{date} 前有效',
740
840
  accessGroupAdded: '已添加群授权',
741
841
  accessGroupRemoved: '已移除群授权',
742
842
  accessUserAdded: '已添加用户授权',
743
843
  accessUserRemoved: '已移除用户授权',
744
844
  accessRequestAllowed: '已授权',
745
845
  accessRequestDenied: '已忽略',
746
- accessHints: '三种授权入口:群内 /auth 命令、入群与用户授权卡、本面板。撤销立即生效;白名单只约束非管理员(管理员在任何私聊/群聊都直接放行),非管理员需要群和用户两层同时命中。',
747
846
  // ---- wiki ---------------------------------------------------------------
748
- wikiUsersCard: '知识库白名单',
749
- wikiUsersHelp: '与「访问授权」里的用户白名单相互独立:这里的用户(以及管理员)发起的话题可以使用知识库工具。也可以在飞书里发送 /auth wiki add @用户 或 /auth wiki remove @用户;移除立即生效。',
750
- wikiUsersEmpty: '知识库白名单为空:目前只有管理员发起的话题能使用知识库工具。',
847
+ wikiUsersTitle: '可使用的人',
848
+ wikiUsersAdminPlus: '管理员 + {n}',
849
+ wikiUsersEmpty: '目前只有管理员发起的话题能用知识库工具',
850
+ wikiUsersHint: '也可以在飞书发 /auth wiki add @用户',
751
851
  wikiUserAdded: '已加入知识库白名单',
752
852
  wikiUserRemoved: '已移出知识库白名单',
753
- accessChatIdHint: 'chat_id 可从入群群信息卡或群内 /groupid 获取;user_id 可私聊机器人发送 /userid 获取。',
754
853
  } as const
854
+
855
+ /**
856
+ * Fill `{name}` placeholders of one `PANEL_COPY` template.
857
+ *
858
+ * @param template - Copy containing `{name}` placeholders.
859
+ * @param values - Replacement per placeholder name.
860
+ * @returns The filled string; unknown placeholders stay as written.
861
+ */
862
+ export function fillCopy(template: string, values: Readonly<Record<string, string | number>>): string {
863
+ return template.replace(/\{(\w+)\}/g, (match, name: string) => {
864
+ const value = values[name]
865
+ return value === undefined ? match : String(value)
866
+ })
867
+ }