@mhfire/dsh-im-bridge 0.1.7 → 0.2.0

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.
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Staged settings form owned by this plugin.
3
+ * Mirrors the Host Plugins section model without importing its chrome.
4
+ */
5
+
6
+ import {
7
+ createSnapshotStore,
8
+ type SettingsScope,
9
+ type SettingsScopeSnapshot,
10
+ type SnapshotStore,
11
+ } from '@deepseek-ai/dsh-client-runtime/client'
12
+
13
+ /** Write one staged field performs on save. */
14
+ export type FieldWrite =
15
+ | { kind: 'set'; value: unknown }
16
+ | { kind: 'clear' }
17
+
18
+ /** Convert one section field between stored value and draft text. */
19
+ export interface CardFieldSpec {
20
+ /** Field name inside the namespace section. */
21
+ field: string
22
+ /** Render a stored value as draft text. */
23
+ format: (value: unknown) => string
24
+ /** The write this draft stages, or undefined when the text is invalid. */
25
+ parse: (text: string) => FieldWrite | undefined
26
+ }
27
+
28
+ /**
29
+ * A control whose literal never rides a response. The draft starts blank;
30
+ * a blank draft writes nothing so a save cannot clear a stored secret.
31
+ */
32
+ export interface CardSecretSpec {
33
+ /** Field name addressing this control inside the card's form. */
34
+ field: string
35
+ /** Write the staged text; resolves to whether the Host accepted it. */
36
+ write: (text: string) => Promise<boolean>
37
+ }
38
+
39
+ /** One field as a control renders it. */
40
+ export interface CardFieldState {
41
+ /** Draft text the control renders. */
42
+ text: string
43
+ /** Whether saving would leave a user-layer entry. */
44
+ overridden: boolean
45
+ /** Whether the draft is not a value this field accepts. */
46
+ invalid: boolean
47
+ }
48
+
49
+ /** Card-level form state. */
50
+ export interface CardShell {
51
+ /** False while the namespace is not served; the card renders nothing. */
52
+ available: boolean
53
+ /** Whether the Host document accepts writes. */
54
+ writable: boolean
55
+ /** Whether the form holds edits a save would write. */
56
+ dirty: boolean
57
+ /** Whether any staged draft is invalid. */
58
+ invalid: boolean
59
+ /** Whether a save is crossing the wire. */
60
+ saving: boolean
61
+ /** Whether the last save did not land. */
62
+ failed: boolean
63
+ }
64
+
65
+ /** Write actions a card's slot entry injects. */
66
+ export interface CardActions {
67
+ /** Stage draft text for one field. */
68
+ edit: (field: string, text: string) => void
69
+ /** Stage a clear so the field re-inherits the composition layer. */
70
+ resetField: (field: string) => void
71
+ /** Write every staged edit, then re-seed from what the Host accepted. */
72
+ save: () => void
73
+ /** Drop every staged edit. */
74
+ discard: () => void
75
+ }
76
+
77
+ interface StagedEdit {
78
+ text: string
79
+ clear: boolean
80
+ }
81
+
82
+ interface PlannedWrite {
83
+ field: string
84
+ run: (() => Promise<boolean>) | undefined
85
+ }
86
+
87
+ /** Whole-number field. Empty draft clears. */
88
+ export function numberField(field: string): CardFieldSpec {
89
+ return {
90
+ field,
91
+ format: value => typeof value === 'number' ? String(value) : '',
92
+ parse: (text) => {
93
+ const trimmed = text.trim()
94
+ if (trimmed === '') return { kind: 'clear' }
95
+ const parsed = Number(trimmed)
96
+ return Number.isFinite(parsed) ? { kind: 'set', value: parsed } : undefined
97
+ },
98
+ }
99
+ }
100
+
101
+ /** Free-text field. Empty draft clears. */
102
+ export function textField(field: string): CardFieldSpec {
103
+ return {
104
+ field,
105
+ format: value => typeof value === 'string' ? value : '',
106
+ parse: (text) => {
107
+ const trimmed = text.trim()
108
+ return trimmed === '' ? { kind: 'clear' } : { kind: 'set', value: trimmed }
109
+ },
110
+ }
111
+ }
112
+
113
+ /** Comma-separated string list. Empty draft stores []. */
114
+ export function csvField(field: string): CardFieldSpec {
115
+ return {
116
+ field,
117
+ format: value => Array.isArray(value) ? value.join(',') : '',
118
+ parse: (text) => {
119
+ const items = text.split(',').map(item => item.trim()).filter(Boolean)
120
+ return { kind: 'set', value: items }
121
+ },
122
+ }
123
+ }
124
+
125
+ /** Stages edits over one settings namespace and writes them on save. */
126
+ export class CardForm<T> {
127
+ private readonly specs: Map<string, CardFieldSpec>
128
+ private readonly secretSpecs: Map<string, CardSecretSpec>
129
+ private readonly staged = new Map<string, StagedEdit>()
130
+ private readonly listeners = new Set<() => void>()
131
+ private saving = false
132
+ private failed = false
133
+
134
+ /**
135
+ * @param scope - bound settings scope for this card's namespace.
136
+ * @param specs - section fields this card edits.
137
+ * @param secrets - write-only controls; a blank draft is a no-op.
138
+ */
139
+ constructor(
140
+ private readonly scope: SettingsScope<T>,
141
+ specs: CardFieldSpec[],
142
+ secrets: CardSecretSpec[] = [],
143
+ ) {
144
+ this.specs = new Map(specs.map(spec => [spec.field, spec]))
145
+ this.secretSpecs = new Map(secrets.map(spec => [spec.field, spec]))
146
+ scope.subscribe(() => { this.publish() })
147
+ }
148
+
149
+ /** Publish a projection rebuilt whenever the scope or a draft changes. */
150
+ bind<S>(project: () => S): SnapshotStore<S> {
151
+ const store = createSnapshotStore(project())
152
+ this.listeners.add(() => { store.set(project()) })
153
+ return store
154
+ }
155
+
156
+ /** Card-level state: what the Host serves and what a save would do. */
157
+ shell(): CardShell {
158
+ const snapshot = this.scope.getSnapshot()
159
+ const plan = this.plan()
160
+ return {
161
+ available: snapshot.status === 'ready',
162
+ writable: snapshot.writable,
163
+ dirty: plan.length > 0,
164
+ invalid: plan.some(item => item.run === undefined),
165
+ saving: this.saving,
166
+ failed: this.failed,
167
+ }
168
+ }
169
+
170
+ /** One control's staged text, override badge, and validity. */
171
+ field(field: string): CardFieldState {
172
+ const staged = this.staged.get(field)
173
+ if (this.secretSpecs.has(field)) {
174
+ return { text: staged?.text ?? '', overridden: false, invalid: false }
175
+ }
176
+ const spec = this.spec(field)
177
+ if (staged === undefined) {
178
+ return { text: spec.format(this.sectionValue(field)), overridden: this.stored(field), invalid: false }
179
+ }
180
+ const write = staged.clear ? { kind: 'clear' as const } : spec.parse(staged.text)
181
+ return {
182
+ text: staged.text,
183
+ overridden: write?.kind === 'set',
184
+ invalid: write === undefined,
185
+ }
186
+ }
187
+
188
+ /** Edit, reset, save, and discard actions bound to this form. */
189
+ actions(): CardActions {
190
+ return {
191
+ edit: (field, text) => { this.stage(field, { text, clear: false }) },
192
+ resetField: (field) => {
193
+ this.stage(field, { text: this.spec(field).format(this.baseValue(field)), clear: true })
194
+ },
195
+ save: () => { void this.save() },
196
+ discard: () => {
197
+ if (this.staged.size === 0 && !this.failed) return
198
+ this.staged.clear()
199
+ this.failed = false
200
+ this.publish()
201
+ },
202
+ }
203
+ }
204
+
205
+ /** Write every staged edit, then re-seed from what the Host accepted. */
206
+ async save(): Promise<void> {
207
+ const plan = this.plan()
208
+ const writes = plan.flatMap(item => item.run === undefined ? [] : [item.run])
209
+ if (plan.length === 0 || this.saving || writes.length !== plan.length) return
210
+ this.saving = true
211
+ this.failed = false
212
+ this.publish()
213
+ let landed = true
214
+ for (const write of writes) {
215
+ landed = await write() && landed
216
+ }
217
+ if (landed) this.staged.clear()
218
+ this.saving = false
219
+ this.failed = !landed
220
+ this.publish()
221
+ }
222
+
223
+ private plan(): PlannedWrite[] {
224
+ const plan: PlannedWrite[] = []
225
+ for (const [field, staged] of this.staged) {
226
+ const secret = this.secretSpecs.get(field)
227
+ if (secret !== undefined) {
228
+ const value = staged.text.trim()
229
+ if (value !== '') plan.push({ field, run: () => secret.write(value) })
230
+ continue
231
+ }
232
+ const spec = this.spec(field)
233
+ if (staged.clear) {
234
+ if (this.stored(field)) plan.push({ field, run: () => this.clear(field) })
235
+ continue
236
+ }
237
+ if (staged.text === spec.format(this.sectionValue(field))) continue
238
+ const write = spec.parse(staged.text)
239
+ if (write === undefined) plan.push({ field, run: undefined })
240
+ else if (write.kind === 'clear') plan.push({ field, run: () => this.clear(field) })
241
+ else plan.push({ field, run: () => this.store(field, write.value) })
242
+ }
243
+ return plan
244
+ }
245
+
246
+ private async clear(field: string): Promise<boolean> {
247
+ await this.scope.unset(field)
248
+ return !this.stored(field)
249
+ }
250
+
251
+ private async store(field: string, value: unknown): Promise<boolean> {
252
+ await this.scope.set(field, value)
253
+ return this.userLayer()?.[field] === value
254
+ || (Array.isArray(value) && JSON.stringify(this.userLayer()?.[field]) === JSON.stringify(value))
255
+ }
256
+
257
+ private stage(field: string, edit: StagedEdit): void {
258
+ this.staged.set(field, edit)
259
+ this.failed = false
260
+ this.publish()
261
+ }
262
+
263
+ private spec(field: string): CardFieldSpec {
264
+ const spec = this.specs.get(field)
265
+ if (spec === undefined) throw new Error(`im-bridge card has no field ${field}`)
266
+ return spec
267
+ }
268
+
269
+ private snapshotOf(): SettingsScopeSnapshot<T> {
270
+ return this.scope.getSnapshot()
271
+ }
272
+
273
+ private sectionValue(field: string): unknown {
274
+ return (this.snapshotOf().value as Record<string, unknown> | undefined)?.[field]
275
+ }
276
+
277
+ private baseValue(field: string): unknown {
278
+ return (this.snapshotOf().base as Record<string, unknown> | undefined)?.[field]
279
+ }
280
+
281
+ private userLayer(): Record<string, unknown> | undefined {
282
+ return this.snapshotOf().user as Record<string, unknown> | undefined
283
+ }
284
+
285
+ private stored(field: string): boolean {
286
+ const user = this.userLayer()
287
+ return user !== undefined && Object.hasOwn(user, field)
288
+ }
289
+
290
+ private publish(): void {
291
+ for (const listener of this.listeners) listener()
292
+ }
293
+ }
@@ -0,0 +1,4 @@
1
+ declare module '*.module.css' {
2
+ const classes: { readonly [key: string]: string }
3
+ export default classes
4
+ }
@@ -0,0 +1,124 @@
1
+ /* Plugin configuration fields: label, control, override badge, and hint. */
2
+
3
+ .field {
4
+ display: flex;
5
+ flex-direction: column;
6
+ gap: 6px;
7
+ padding: 12px 0;
8
+ }
9
+
10
+ .field + .field {
11
+ border-top: 1px solid var(--dsw-alias-border-l2);
12
+ }
13
+
14
+ .head {
15
+ display: flex;
16
+ align-items: center;
17
+ gap: 8px;
18
+ }
19
+
20
+ .label {
21
+ flex: 1;
22
+ min-width: 0;
23
+ font-size: 13px;
24
+ font-weight: 500;
25
+ line-height: 1.5;
26
+ color: var(--dsw-alias-label-primary);
27
+ }
28
+
29
+ .badges {
30
+ display: inline-flex;
31
+ align-items: center;
32
+ gap: 8px;
33
+ }
34
+
35
+ .badge {
36
+ border-radius: 999px;
37
+ padding: 1px 8px;
38
+ font-size: 11px;
39
+ line-height: 17px;
40
+ white-space: nowrap;
41
+ font-weight: 500;
42
+ background: var(--dsw-alias-bg-module-platform);
43
+ color: var(--dsw-alias-label-secondary);
44
+ }
45
+
46
+ .badgeMuted {
47
+ border-radius: 999px;
48
+ padding: 1px 8px;
49
+ font-size: 11px;
50
+ line-height: 17px;
51
+ white-space: nowrap;
52
+ color: var(--dsw-alias-label-tertiary);
53
+ }
54
+
55
+ .reset {
56
+ border: none;
57
+ background: none;
58
+ padding: 0;
59
+ font: inherit;
60
+ font-size: 12px;
61
+ line-height: 1.5;
62
+ color: var(--dsw-alias-label-secondary);
63
+ cursor: pointer;
64
+ }
65
+
66
+ .reset:hover:not(:disabled) {
67
+ color: var(--dsw-alias-label-primary);
68
+ }
69
+
70
+ .reset:disabled {
71
+ cursor: default;
72
+ }
73
+
74
+ .input {
75
+ height: 34px;
76
+ padding: 0 12px;
77
+ border: 1px solid var(--dsw-alias-border-l2);
78
+ border-radius: 8px;
79
+ background: var(--dsw-alias-bg-layer-3);
80
+ font: inherit;
81
+ font-size: 13px;
82
+ line-height: 1.5;
83
+ color: var(--dsw-alias-label-primary);
84
+ }
85
+
86
+ .input:focus-visible {
87
+ outline: none;
88
+ border-color: var(--dsw-alias-brand-primary);
89
+ }
90
+
91
+ .input:disabled {
92
+ color: var(--dsw-alias-label-tertiary);
93
+ cursor: default;
94
+ }
95
+
96
+ .inputInvalid {
97
+ height: 34px;
98
+ padding: 0 12px;
99
+ border: 1px solid var(--dsw-alias-label-error);
100
+ border-radius: 8px;
101
+ background: var(--dsw-alias-bg-layer-3);
102
+ font: inherit;
103
+ font-size: 13px;
104
+ line-height: 1.5;
105
+ color: var(--dsw-alias-label-primary);
106
+ }
107
+
108
+ .inputInvalid:focus-visible {
109
+ outline: none;
110
+ }
111
+
112
+ .invalid {
113
+ margin: 0;
114
+ font-size: 12px;
115
+ line-height: 1.5;
116
+ color: var(--dsw-alias-label-error);
117
+ }
118
+
119
+ .hint {
120
+ margin: 0;
121
+ font-size: 12px;
122
+ line-height: 1.5;
123
+ color: var(--dsw-alias-label-tertiary);
124
+ }
@@ -0,0 +1,112 @@
1
+ /** Staged value and write-only secret controls. */
2
+
3
+ import type { ReactElement } from 'react'
4
+ import css from './fields.module.css'
5
+
6
+ /** Shared props for a labelled field control. */
7
+ export interface FieldProps {
8
+ /** Stable id associating the label with its control. */
9
+ id: string
10
+ /** Visible label. */
11
+ label: string
12
+ /** One-line explanation under the control. */
13
+ hint: string
14
+ /** Draft text the control renders. */
15
+ text: string
16
+ /** Whether saving would leave a user-layer entry. */
17
+ overridden: boolean
18
+ /** Whether the draft is not a value this field accepts. */
19
+ invalid: boolean
20
+ /** Copy for the overridden badge. */
21
+ overriddenLabel: string
22
+ /** Copy for the reset control. */
23
+ resetLabel: string
24
+ /** Copy shown in place of the hint while the draft is invalid. */
25
+ invalidLabel: string
26
+ /** Disables every control. */
27
+ disabled: boolean
28
+ /** Stage draft text. */
29
+ onEdit: (text: string) => void
30
+ /** Stage a clear so the field re-inherits the composition layer. */
31
+ onReset: () => void
32
+ }
33
+
34
+ /**
35
+ * A staged value field.
36
+ * @param props - the field's copy, staged text, and edit actions.
37
+ * @returns the labelled control.
38
+ */
39
+ export function ValueField(props: FieldProps & {
40
+ /** Hints a numeric keypad without narrowing accepted drafts. */
41
+ numeric?: boolean
42
+ }): ReactElement {
43
+ return (
44
+ <div className={css.field}>
45
+ <div className={css.head}>
46
+ <label className={css.label} htmlFor={props.id}>{props.label}</label>
47
+ {props.overridden
48
+ ? (
49
+ <span className={css.badges}>
50
+ <span className={css.badge}>{props.overriddenLabel}</span>
51
+ <button
52
+ type="button"
53
+ className={css.reset}
54
+ disabled={props.disabled}
55
+ onClick={props.onReset}
56
+ >
57
+ {props.resetLabel}
58
+ </button>
59
+ </span>
60
+ )
61
+ : null}
62
+ </div>
63
+ <input
64
+ id={props.id}
65
+ className={props.invalid ? css.inputInvalid : css.input}
66
+ type="text"
67
+ {...props.numeric === true ? { inputMode: 'numeric' as const } : {}}
68
+ {...props.invalid ? { 'aria-invalid': true } : {}}
69
+ value={props.text}
70
+ disabled={props.disabled}
71
+ onChange={(event) => { props.onEdit(event.target.value) }}
72
+ />
73
+ <p className={props.invalid ? css.invalid : css.hint}>
74
+ {props.invalid ? props.invalidLabel : props.hint}
75
+ </p>
76
+ </div>
77
+ )
78
+ }
79
+
80
+ /**
81
+ * Write-only credential control. The literal never rides a response, so the
82
+ * control starts blank and reports only whether one is configured.
83
+ * @param props - the field's copy, staged text, and configured state.
84
+ * @returns the labelled control.
85
+ */
86
+ export function SecretField(props: Pick<FieldProps, 'id' | 'label' | 'hint' | 'text' | 'disabled' | 'onEdit'> & {
87
+ /** Whether the Host reports a configured value for this secret slot. */
88
+ configured: boolean
89
+ /** Copy describing the configured state. */
90
+ stateLabel: string
91
+ }): ReactElement {
92
+ return (
93
+ <div className={css.field}>
94
+ <div className={css.head}>
95
+ <label className={css.label} htmlFor={props.id}>{props.label}</label>
96
+ <span className={css.badges}>
97
+ <span className={props.configured ? css.badge : css.badgeMuted}>{props.stateLabel}</span>
98
+ </span>
99
+ </div>
100
+ <input
101
+ id={props.id}
102
+ className={css.input}
103
+ type="password"
104
+ autoComplete="off"
105
+ value={props.text}
106
+ disabled={props.disabled}
107
+ onChange={(event) => { props.onEdit(event.target.value) }}
108
+ />
109
+ <p className={css.hint}>{props.hint}</p>
110
+ </div>
111
+ )
112
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Browser half: Settings card keyed by the `im-bridge` namespace.
3
+ */
4
+
5
+ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
6
+ // Type-only: ctx.settingsScope.describe and the keyed slot declaration.
7
+ import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
8
+ import type {} from '@deepseek-ai/dsh-client-ui-settings-plugins/client'
9
+ import { WecomCard } from './WecomCard.tsx'
10
+ import { WecomCardController } from './card-controller.ts'
11
+ import { en, zh } from './locales.ts'
12
+
13
+ /** Settings namespace shared with the Host half. */
14
+ const NS = 'im-bridge'
15
+
16
+ /** Required browser services. */
17
+ export const inject = ['slots', 'locale', 'connection', 'remote', 'settingsScope']
18
+
19
+ /**
20
+ * Register locale copy and the Plugins-tab card.
21
+ * @param ctx - browser plugin context.
22
+ */
23
+ export function apply(ctx: ClientContext): void {
24
+ const card = new WecomCardController(
25
+ ctx.settingsScope.bind({ namespace: NS }),
26
+ ctx.settingsScope.describe(),
27
+ )
28
+
29
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'im-bridge: locale dicts')
30
+
31
+ ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({
32
+ name: 'settings.plugin.item',
33
+ key: NS,
34
+ locale: NS,
35
+ inject: () => card.inject(),
36
+ }, WecomCard))
37
+ }
@@ -0,0 +1,112 @@
1
+ /** Dictionary keys owned by the im-bridge settings card. */
2
+ export type ImBridgeLocaleKey =
3
+ | 'title'
4
+ | 'description'
5
+ | 'unsaved'
6
+ | 'readOnly'
7
+ | 'saveFailed'
8
+ | 'save'
9
+ | 'saving'
10
+ | 'discard'
11
+ | 'expand'
12
+ | 'collapse'
13
+ | 'overridden'
14
+ | 'reset'
15
+ | 'invalidNumber'
16
+ | 'botId'
17
+ | 'secret'
18
+ | 'secretHint'
19
+ | 'secretConfigured'
20
+ | 'secretUnset'
21
+ | 'allowFrom'
22
+ | 'allowFromHint'
23
+ | 'agentTimeoutSec'
24
+ | 'agentTimeoutSecHint'
25
+ | 'startHint'
26
+ | 'startHintHint'
27
+ | 'deniedMessage'
28
+ | 'deniedMessageHint'
29
+ | 'welcomeMessage'
30
+ | 'welcomeMessageHint'
31
+ | 'provider'
32
+ | 'providerHint'
33
+ | 'model'
34
+ | 'modelHint'
35
+
36
+ /** English copy for the im-bridge card. */
37
+ export const en: Record<ImBridgeLocaleKey, string> = {
38
+ title: 'WeCom Bridge',
39
+ description: 'Credentials, allow-list, timeouts, and WeCom-only model overrides.',
40
+ unsaved: 'Unsaved',
41
+ readOnly: 'This document is read-only.',
42
+ saveFailed: 'Save did not land. Correct the fields and try again.',
43
+ save: 'Save',
44
+ saving: 'Saving…',
45
+ discard: 'Discard',
46
+ expand: 'Show settings',
47
+ collapse: 'Hide settings',
48
+ overridden: 'Overridden',
49
+ reset: 'Reset',
50
+ invalidNumber: 'Enter a finite number.',
51
+ botId: 'Bot ID',
52
+ secret: 'Secret',
53
+ secretHint: 'Leave blank to keep the stored value. Save, then restart the process to open the WebSocket.',
54
+ secretConfigured: 'Configured',
55
+ secretUnset: 'Not configured',
56
+ allowFrom: 'Allowed sender userids',
57
+ allowFromHint: 'Comma-separated. Empty allows everyone.',
58
+ agentTimeoutSec: 'Task timeout (seconds)',
59
+ agentTimeoutSecHint: 'Progress bar and remaining-time estimate.',
60
+ startHint: 'Placeholder while thinking',
61
+ startHintHint: 'First stream line after a message arrives.',
62
+ deniedMessage: 'Denied-sender reply',
63
+ deniedMessageHint: 'Sent when the userid is outside the allow-list.',
64
+ welcomeMessage: 'Welcome message',
65
+ welcomeMessageHint: 'Sent when a user opens the WeCom chat.',
66
+ provider: 'WeCom-only provider',
67
+ providerHint: 'Empty follows the GUI default model. Both provider and model must be set to override.',
68
+ model: 'WeCom-only model',
69
+ modelHint: 'Takes effect only together with provider.',
70
+ }
71
+
72
+ /** Chinese copy for the im-bridge card. */
73
+ export const zh: Record<ImBridgeLocaleKey, string> = {
74
+ title: '企业微信桥接',
75
+ description: '凭证、白名单、超时和企微专用模型覆盖。',
76
+ unsaved: '未保存',
77
+ readOnly: '当前文档不可写。',
78
+ saveFailed: '保存未生效,请修正后重试。',
79
+ save: '保存',
80
+ saving: '保存中…',
81
+ discard: '放弃',
82
+ expand: '展开设置',
83
+ collapse: '收起设置',
84
+ overridden: '已覆盖',
85
+ reset: '重置',
86
+ invalidNumber: '请输入有效数字。',
87
+ botId: 'Bot ID',
88
+ secret: 'Secret',
89
+ secretHint: '留空保留已存值。保存后需重启进程才会连 WebSocket。',
90
+ secretConfigured: '已配置',
91
+ secretUnset: '未配置',
92
+ allowFrom: '允许的发送者 userid',
93
+ allowFromHint: '逗号分隔;空 = 允许所有人。',
94
+ agentTimeoutSec: '单任务超时(秒)',
95
+ agentTimeoutSecHint: '动画进度条和剩余估算的基准。',
96
+ startHint: '开始处理时的占位提示',
97
+ startHintHint: '收到消息后推送的第一条流式文案。',
98
+ deniedMessage: '非白名单拒绝文案',
99
+ deniedMessageHint: '发送者不在白名单时回复。',
100
+ welcomeMessage: '进入会话欢迎语',
101
+ welcomeMessageHint: '用户打开企微会话时发送。',
102
+ provider: '企微专用 provider',
103
+ providerHint: '空 = 跟随 GUI 默认模型。须与 model 同时填写才覆盖。',
104
+ model: '企微专用 model',
105
+ modelHint: '仅在同时填写 provider 时生效。',
106
+ }
107
+
108
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
109
+ interface LocaleNamespaceMap {
110
+ 'im-bridge': ImBridgeLocaleKey
111
+ }
112
+ }