@mhfire/dsh-im-bridge 0.3.0 → 0.4.3

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,77 +1,94 @@
1
- {
2
- "name": "@mhfire/dsh-im-bridge",
3
- "version": "0.3.0",
4
- "description": "企业微信智能机器人 ⇄ DeepSeek Harness Agent 桥接插件:进程内按企微窗口创建 Agent(单聊一人一条、同一群共用一条),会话在 GUI 实时可见;含 Settings 插件配置卡片",
5
- "license": "MIT",
6
- "repository": {
7
- "type": "git",
8
- "url": "git+https://github.com/MHfire/dsh-im-bridge.git"
9
- },
10
- "type": "module",
11
- "main": "lib/index.js",
12
- "exports": {
13
- ".": "./lib/index.js",
14
- "./client": "./lib/client.js",
15
- "./cordis.patch.yml": "./cordis.patch.yml",
16
- "./package.json": "./package.json"
17
- },
18
- "files": [
19
- "src",
20
- "lib",
21
- "tsdown.host.ts",
22
- "tsdown.client.ts",
23
- "tsconfig.json",
24
- "cordis.patch.yml",
25
- "persona.default.md",
26
- "persona.default.en.md",
27
- "persona.example.md",
28
- "README.md",
29
- "README.en.md"
30
- ],
31
- "scripts": {
32
- "build": "tsdown --config tsdown.host.ts && tsdown --config tsdown.client.ts",
33
- "prepare": "tsdown --config tsdown.host.ts && tsdown --config tsdown.client.ts",
34
- "test": "node --experimental-strip-types --test tests/reply-images.test.ts tests/session-key.test.ts"
35
- },
36
- "dsh": {
37
- "bundle": {
38
- "patch": "./cordis.patch.yml"
39
- },
40
- "client": {
41
- "inject": [
42
- "@deepseek-ai/dsh-client-connection",
43
- "@deepseek-ai/dsh-client-locale",
44
- "@deepseek-ai/dsh-client-runtime",
45
- "@deepseek-ai/dsh-client-ui-settings",
46
- "@deepseek-ai/dsh-client-ui-settings-plugins",
47
- "@deepseek-ai/dsh-api-remotes"
48
- ],
49
- "platform": "web"
50
- }
51
- },
52
- "peerDependencies": {
53
- "@deepseek-ai/cordis": "*",
54
- "@deepseek-ai/dsh-agent": "*",
55
- "@deepseek-ai/dsh-llm": "*",
56
- "@deepseek-ai/dsh-session": "*",
57
- "@deepseek-ai/dsh-settings": "*",
58
- "@deepseek-ai/schemastery": "*"
59
- },
60
- "peerDependenciesMeta": {
61
- "@deepseek-ai/cordis": { "optional": true },
62
- "@deepseek-ai/dsh-agent": { "optional": true },
63
- "@deepseek-ai/dsh-llm": { "optional": true },
64
- "@deepseek-ai/dsh-session": { "optional": true },
65
- "@deepseek-ai/dsh-settings": { "optional": true },
66
- "@deepseek-ai/schemastery": { "optional": true }
67
- },
68
- "dependencies": {
69
- "@wecom/aibot-node-sdk": "^1.0.7"
70
- },
71
- "devDependencies": {
72
- "@types/react": "~18.3.1",
73
- "lightningcss": "^1.32.0",
74
- "react": "^18.2.0",
75
- "tsdown": "^0.22.2"
76
- }
77
- }
1
+ {
2
+ "name": "@mhfire/dsh-im-bridge",
3
+ "version": "0.4.3",
4
+ "description": "企业微信智能机器人 ⇄ DeepSeek Harness Agent 桥接插件:进程内按企微窗口创建 Agent(单聊一人一条、同一群共用一条),会话在 GUI 实时可见;含 Settings 插件配置卡片",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/MHfire/dsh-im-bridge.git"
9
+ },
10
+ "type": "module",
11
+ "main": "lib/index.js",
12
+ "exports": {
13
+ ".": "./lib/index.js",
14
+ "./client": "./lib/client.js",
15
+ "./cordis.patch.yml": "./cordis.patch.yml",
16
+ "./package.json": "./package.json"
17
+ },
18
+ "files": [
19
+ "src",
20
+ "lib",
21
+ "tsdown.host.ts",
22
+ "tsdown.client.ts",
23
+ "tsconfig.json",
24
+ "cordis.patch.yml",
25
+ "persona.default.md",
26
+ "persona.default.en.md",
27
+ "persona.example.md",
28
+ "README.md",
29
+ "README.en.md"
30
+ ],
31
+ "scripts": {
32
+ "build": "tsdown --config tsdown.host.ts && tsdown --config tsdown.client.ts",
33
+ "prepare": "tsdown --config tsdown.host.ts && tsdown --config tsdown.client.ts",
34
+ "typecheck": "tsc -p tsconfig.typecheck.json",
35
+ "test": "npm run typecheck && node --experimental-strip-types --test tests/reply-images.test.ts tests/session-key.test.ts tests/wecom-cli.test.ts"
36
+ },
37
+ "dsh": {
38
+ "bundle": {
39
+ "patch": "./cordis.patch.yml"
40
+ },
41
+ "client": {
42
+ "inject": [
43
+ "@deepseek-ai/dsh-client-connection",
44
+ "@deepseek-ai/dsh-client-locale",
45
+ "@deepseek-ai/dsh-client-ui-settings",
46
+ "@deepseek-ai/dsh-client-ui-settings-plugins",
47
+ "@deepseek-ai/dsh-api-remotes"
48
+ ],
49
+ "platform": "web"
50
+ }
51
+ },
52
+ "peerDependencies": {
53
+ "@deepseek-ai/cordis": "*",
54
+ "@deepseek-ai/dsh-agent": "*",
55
+ "@deepseek-ai/dsh-llm": "*",
56
+ "@deepseek-ai/dsh-session": "*",
57
+ "@deepseek-ai/dsh-settings": "*",
58
+ "@deepseek-ai/schemastery": "*"
59
+ },
60
+ "peerDependenciesMeta": {
61
+ "@deepseek-ai/cordis": {
62
+ "optional": true
63
+ },
64
+ "@deepseek-ai/dsh-agent": {
65
+ "optional": true
66
+ },
67
+ "@deepseek-ai/dsh-llm": {
68
+ "optional": true
69
+ },
70
+ "@deepseek-ai/dsh-session": {
71
+ "optional": true
72
+ },
73
+ "@deepseek-ai/dsh-settings": {
74
+ "optional": true
75
+ },
76
+ "@deepseek-ai/schemastery": {
77
+ "optional": true
78
+ }
79
+ },
80
+ "dependencies": {
81
+ "@wecom/aibot-node-sdk": "^1.0.7",
82
+ "@wecom/cli": "^1.2.0",
83
+ "fflate": "^0.8.3",
84
+ "yaml": "^2.8.1"
85
+ },
86
+ "devDependencies": {
87
+ "@types/node": "^22.20.0",
88
+ "@types/react": "~18.3.1",
89
+ "lightningcss": "^1.32.0",
90
+ "react": "^18.2.0",
91
+ "tsdown": "^0.22.2",
92
+ "typescript": "^6.0.3"
93
+ }
94
+ }
@@ -5,6 +5,7 @@ import type { ImBridgeLocaleKey } from './locales.ts'
5
5
  import type { WecomCardFace, WecomCardState } from './card-controller.ts'
6
6
  import { PluginCard } from './PluginCard.tsx'
7
7
  import { SecretField, ValueField } from './fields.tsx'
8
+ import css from './fields.module.css'
8
9
 
9
10
  /** Props the slot renderer binds for this card. */
10
11
  export interface WecomCardProps {
@@ -20,6 +21,8 @@ export interface WecomCardProps {
20
21
  save: WecomCardFace['save']
21
22
  /** Drop every staged edit. */
22
23
  discard: WecomCardFace['discard']
24
+ /** Download official wecomcli-* into the Host-resolved directory. */
25
+ installSkills: WecomCardFace['installSkills']
23
26
  }
24
27
 
25
28
  /**
@@ -66,6 +69,16 @@ export function WecomCard(props: WecomCardProps): ReactElement | null {
66
69
  stateLabel={state.secretConfigured ? t('secretConfigured') : t('secretUnset')}
67
70
  onEdit={text => { props.edit('secret', text) }}
68
71
  />
72
+ <SkillsInstall
73
+ t={t}
74
+ disabled={disabled}
75
+ available={state.skillsInstallAvailable}
76
+ status={state.skillsInstallStatus}
77
+ dest={state.skillsDest}
78
+ count={state.skillsCount}
79
+ error={state.skillsError}
80
+ onInstall={props.installSkills}
81
+ />
69
82
  <ValueField
70
83
  id="im-bridge-allowFrom"
71
84
  label={t('allowFrom')}
@@ -133,3 +146,65 @@ export function WecomCard(props: WecomCardProps): ReactElement | null {
133
146
  </PluginCard>
134
147
  )
135
148
  }
149
+
150
+ function skillsStatusText(
151
+ t: (key: ImBridgeLocaleKey) => string,
152
+ status: WecomCardState['skillsInstallStatus'],
153
+ dest: string,
154
+ count: number,
155
+ error: string,
156
+ available: boolean,
157
+ ): string {
158
+ if (!available) return t('skillsUnavailable')
159
+ if (status === 'ok') {
160
+ return t('skillsInstalled').replace('{count}', String(count)).replace('{dest}', dest)
161
+ }
162
+ if (status === 'error') {
163
+ return error === '' ? t('skillsFailed') : `${t('skillsFailed')} ${error}`
164
+ }
165
+ return t('skillsHint')
166
+ }
167
+
168
+ /**
169
+ * Host-side install of official wecomcli-* (browser never chooses the path).
170
+ * @param props - copy, status, and the install action.
171
+ * @returns the labelled control.
172
+ */
173
+ function SkillsInstall(props: {
174
+ t: (key: ImBridgeLocaleKey) => string
175
+ disabled: boolean
176
+ available: boolean
177
+ status: WecomCardState['skillsInstallStatus']
178
+ dest: string
179
+ count: number
180
+ error: string
181
+ onInstall: () => void
182
+ }): ReactElement {
183
+ const installing = props.status === 'installing'
184
+ const status = skillsStatusText(
185
+ props.t,
186
+ props.status,
187
+ props.dest,
188
+ props.count,
189
+ props.error,
190
+ props.available,
191
+ )
192
+ return (
193
+ <div className={css.field}>
194
+ <div className={css.head}>
195
+ <span className={css.label}>{props.t('skillsTitle')}</span>
196
+ </div>
197
+ <button
198
+ type="button"
199
+ className={css.install}
200
+ disabled={props.disabled || !props.available || installing}
201
+ onClick={props.onInstall}
202
+ >
203
+ {props.t(installing ? 'skillsInstalling' : 'skillsInstall')}
204
+ </button>
205
+ <p className={props.status === 'error' || !props.available ? css.invalid : css.hint}>
206
+ {status}
207
+ </p>
208
+ </div>
209
+ )
210
+ }
@@ -1,6 +1,7 @@
1
1
  /** Staged form over the `im-bridge` settings namespace. */
2
2
 
3
- import type { SettingsScope, SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
3
+ import type { SnapshotStore } from '@deepseek-ai/dsh-client-store'
4
+ import type { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client'
4
5
  import {
5
6
  CardForm,
6
7
  csvField,
@@ -59,28 +60,59 @@ export interface WecomCardState extends CardShell {
59
60
  welcomeMessage: CardFieldState
60
61
  provider: CardFieldState
61
62
  model: CardFieldState
63
+ /** Whether the Host Connection RPC is available. */
64
+ skillsInstallAvailable: boolean
65
+ /** idle / installing / ok / error. */
66
+ skillsInstallStatus: 'idle' | 'installing' | 'ok' | 'error'
67
+ /** Destination after a successful install. */
68
+ skillsDest: string
69
+ /** Loaded wecomcli-* count after a successful install. */
70
+ skillsCount: number
71
+ /** Host error message after a failed install. */
72
+ skillsError: string
62
73
  }
63
74
 
64
75
  /** Face the card's slot registration injects. */
65
76
  export interface WecomCardFace extends CardActions {
77
+ /** Download official wecomcli-* into the Host-resolved directory. */
78
+ installSkills: () => void
66
79
  hooks: {
67
80
  /** Card snapshot bound by the renderer as useWecomCard. */
68
81
  wecomCard: SnapshotStore<WecomCardState>
69
82
  }
70
83
  }
71
84
 
85
+ /** Unary RPC caller used by the install button. */
86
+ export interface SkillsInstallRpc {
87
+ call(
88
+ channel: string,
89
+ endpoint: string,
90
+ payload: unknown,
91
+ signal?: AbortSignal,
92
+ ): Promise<
93
+ | { ok: true; value: unknown }
94
+ | { ok: false; error: { message: string } }
95
+ >
96
+ }
97
+
72
98
  /** Bridges the `im-bridge` scope onto the staged card form. */
73
99
  export class WecomCardController {
74
100
  private readonly form: CardForm<WecomCardSettings>
75
101
  private readonly store: SnapshotStore<WecomCardState>
102
+ private skillsInstallStatus: WecomCardState['skillsInstallStatus'] = 'idle'
103
+ private skillsDest = ''
104
+ private skillsCount = 0
105
+ private skillsError = ''
76
106
 
77
107
  /**
78
108
  * @param scope - bound settings scope for the `im-bridge` namespace.
79
109
  * @param describe - Host describe face; secret literals never ride it.
110
+ * @param rpc - optional Connection RPC for the skills install button.
80
111
  */
81
112
  constructor(
82
113
  private readonly scope: SettingsScope<WecomCardSettings>,
83
114
  private readonly describe: SettingsDescribeFace,
115
+ private readonly rpc: SkillsInstallRpc | undefined,
84
116
  ) {
85
117
  this.form = new CardForm(
86
118
  scope,
@@ -115,6 +147,11 @@ export class WecomCardController {
115
147
  welcomeMessage: this.form.field('welcomeMessage'),
116
148
  provider: this.form.field('provider'),
117
149
  model: this.form.field('model'),
150
+ skillsInstallAvailable: this.rpc !== undefined,
151
+ skillsInstallStatus: this.skillsInstallStatus,
152
+ skillsDest: this.skillsDest,
153
+ skillsCount: this.skillsCount,
154
+ skillsError: this.skillsError,
118
155
  }
119
156
  }
120
157
 
@@ -139,8 +176,45 @@ export class WecomCardController {
139
176
  return this.secretConfigured(field)
140
177
  }
141
178
 
179
+ /**
180
+ * Download official wecomcli-* into the Host-resolved directory.
181
+ * The browser does not choose the path.
182
+ */
183
+ installSkills(): void {
184
+ void this.runInstall()
185
+ }
186
+
187
+ private async runInstall(): Promise<void> {
188
+ if (this.rpc === undefined || this.skillsInstallStatus === 'installing') return
189
+ this.skillsInstallStatus = 'installing'
190
+ this.skillsError = ''
191
+ this.form.notify()
192
+ try {
193
+ const result = await this.rpc.call('/im-bridge', 'wecomcli.installSkills', {})
194
+ if (!result.ok) {
195
+ this.skillsInstallStatus = 'error'
196
+ this.skillsError = result.error.message
197
+ this.form.notify()
198
+ return
199
+ }
200
+ const value = result.value as { dest?: unknown; count?: unknown }
201
+ this.skillsInstallStatus = 'ok'
202
+ this.skillsDest = typeof value.dest === 'string' ? value.dest : ''
203
+ this.skillsCount = typeof value.count === 'number' ? value.count : 0
204
+ this.skillsError = ''
205
+ } catch (error) {
206
+ this.skillsInstallStatus = 'error'
207
+ this.skillsError = error instanceof Error ? error.message : String(error)
208
+ }
209
+ this.form.notify()
210
+ }
211
+
142
212
  /** Face the slot registration injects. */
143
213
  inject(): WecomCardFace {
144
- return { hooks: { wecomCard: this.store }, ...this.form.actions() }
214
+ return {
215
+ hooks: { wecomCard: this.store },
216
+ ...this.form.actions(),
217
+ installSkills: () => { this.installSkills() },
218
+ }
145
219
  }
146
220
  }
@@ -3,12 +3,8 @@
3
3
  * Mirrors the Host Plugins section model without importing its chrome.
4
4
  */
5
5
 
6
- import {
7
- createSnapshotStore,
8
- type SettingsScope,
9
- type SettingsScopeSnapshot,
10
- type SnapshotStore,
11
- } from '@deepseek-ai/dsh-client-runtime/client'
6
+ import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-store'
7
+ import type { SettingsScope, SettingsScopeSnapshot } from '@deepseek-ai/dsh-client-ui-settings/client'
12
8
 
13
9
  /** Write one staged field performs on save. */
14
10
  export type FieldWrite =
@@ -260,6 +256,11 @@ export class CardForm<T> {
260
256
  this.publish()
261
257
  }
262
258
 
259
+ /** Rebuild bound projections for card-owned state outside the settings draft. */
260
+ notify(): void {
261
+ this.publish()
262
+ }
263
+
263
264
  private spec(field: string): CardFieldSpec {
264
265
  const spec = this.specs.get(field)
265
266
  if (spec === undefined) throw new Error(`im-bridge card has no field ${field}`)
@@ -93,6 +93,10 @@
93
93
  cursor: default;
94
94
  }
95
95
 
96
+ .input::placeholder {
97
+ color: var(--dsw-alias-label-tertiary);
98
+ }
99
+
96
100
  .inputInvalid {
97
101
  height: 34px;
98
102
  padding: 0 12px;
@@ -122,3 +126,31 @@
122
126
  line-height: 1.5;
123
127
  color: var(--dsw-alias-label-tertiary);
124
128
  }
129
+
130
+ .install {
131
+ appearance: none;
132
+ align-self: flex-start;
133
+ border: 1px solid transparent;
134
+ border-radius: 8px;
135
+ padding: 5px 14px;
136
+ font: inherit;
137
+ font-size: 13px;
138
+ line-height: 1.5;
139
+ cursor: pointer;
140
+ background: var(--dsw-alias-label-primary);
141
+ color: var(--dsw-alias-bg-layer-3);
142
+ }
143
+
144
+ .install:hover:not(:disabled) {
145
+ opacity: 0.92;
146
+ }
147
+
148
+ .install:disabled {
149
+ opacity: 0.4;
150
+ cursor: default;
151
+ }
152
+
153
+ .install:focus-visible {
154
+ outline: 2px solid var(--dsw-alias-brand-primary);
155
+ outline-offset: 1px;
156
+ }
@@ -79,7 +79,8 @@ export function ValueField(props: FieldProps & {
79
79
 
80
80
  /**
81
81
  * Write-only credential control. The literal never rides a response, so the
82
- * control starts blank and reports only whether one is configured.
82
+ * control starts blank and reports only whether one is configured. A configured
83
+ * empty draft shows a dots placeholder so the box does not look unset.
83
84
  * @param props - the field's copy, staged text, and configured state.
84
85
  * @returns the labelled control.
85
86
  */
@@ -103,6 +104,7 @@ export function SecretField(props: Pick<FieldProps, 'id' | 'label' | 'hint' | 't
103
104
  type="password"
104
105
  autoComplete="off"
105
106
  value={props.text}
107
+ placeholder={props.configured && props.text === '' ? '••••••••' : undefined}
106
108
  disabled={props.disabled}
107
109
  onChange={(event) => { props.onEdit(event.target.value) }}
108
110
  />
@@ -2,12 +2,16 @@
2
2
  * Browser half: Settings card keyed by the `im-bridge` namespace.
3
3
  */
4
4
 
5
- import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
6
- // Type-only: ctx.settingsScope.describe and the keyed slot declaration.
5
+ import type { Context as ClientContext } from '@deepseek-ai/cordis'
6
+ // Type-only: ctx.slots, ctx.locale, ctx.settingsScope.describe, and the keyed
7
+ // slot declaration. Behavior crosses packages through services and slots only.
8
+ import type {} from '@deepseek-ai/dsh-client-ui-slots'
9
+ import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
10
+ import type {} from '@deepseek-ai/dsh-client-locale/client'
7
11
  import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
8
12
  import type {} from '@deepseek-ai/dsh-client-ui-settings-plugins/client'
9
13
  import { WecomCard } from './WecomCard.tsx'
10
- import { WecomCardController } from './card-controller.ts'
14
+ import { WecomCardController, type SkillsInstallRpc } from './card-controller.ts'
11
15
  import { en, zh } from './locales.ts'
12
16
 
13
17
  /** Settings namespace shared with the Host half. */
@@ -21,9 +25,11 @@ export const inject = ['slots', 'locale', 'connection', 'remote', 'settingsScope
21
25
  * @param ctx - browser plugin context.
22
26
  */
23
27
  export function apply(ctx: ClientContext): void {
28
+ const connection = ctx.get('connection') as { rpc?: SkillsInstallRpc } | undefined
24
29
  const card = new WecomCardController(
25
30
  ctx.settingsScope.bind({ namespace: NS }),
26
31
  ctx.settingsScope.describe(),
32
+ connection?.rpc,
27
33
  )
28
34
 
29
35
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'im-bridge: locale dicts')
@@ -32,6 +32,13 @@ export type ImBridgeLocaleKey =
32
32
  | 'providerHint'
33
33
  | 'model'
34
34
  | 'modelHint'
35
+ | 'skillsTitle'
36
+ | 'skillsHint'
37
+ | 'skillsInstall'
38
+ | 'skillsInstalling'
39
+ | 'skillsInstalled'
40
+ | 'skillsFailed'
41
+ | 'skillsUnavailable'
35
42
 
36
43
  /** English copy for the im-bridge card. */
37
44
  export const en: Record<ImBridgeLocaleKey, string> = {
@@ -50,7 +57,7 @@ export const en: Record<ImBridgeLocaleKey, string> = {
50
57
  invalidNumber: 'Enter a finite number.',
51
58
  botId: 'Bot ID',
52
59
  secret: 'Secret',
53
- secretHint: 'Leave blank to keep the stored value. Save, then restart the process to open the WebSocket.',
60
+ secretHint: 'The box stays empty on purpose (the stored value never rides the wire). A Configured badge means it is saved; type a new value to replace it. Save, then restart the process to open the WebSocket.',
54
61
  secretConfigured: 'Configured',
55
62
  secretUnset: 'Not configured',
56
63
  allowFrom: 'Allowed sender userids',
@@ -67,6 +74,13 @@ export const en: Record<ImBridgeLocaleKey, string> = {
67
74
  providerHint: 'Empty follows the GUI default model. Both provider and model must be set to override.',
68
75
  model: 'WeCom-only model',
69
76
  modelHint: 'Takes effect only together with provider.',
77
+ skillsTitle: 'WeCom office skills',
78
+ skillsHint: 'Installs wecomcli-* into the plugin directory ($DSH_HOME/wecom-cli-skills). Do not use npx skills add -g; that CLI has no --dir. An empty Bot ID / Secret box is normal.',
79
+ skillsInstall: 'Install official skills',
80
+ skillsInstalling: 'Installing…',
81
+ skillsInstalled: 'Installed {count} skills into {dest}',
82
+ skillsFailed: 'Install failed.',
83
+ skillsUnavailable: 'Install needs the Web host Connection.',
70
84
  }
71
85
 
72
86
  /** Chinese copy for the im-bridge card. */
@@ -86,7 +100,7 @@ export const zh: Record<ImBridgeLocaleKey, string> = {
86
100
  invalidNumber: '请输入有效数字。',
87
101
  botId: 'Bot ID',
88
102
  secret: 'Secret',
89
- secretHint: '留空保留已存值。保存后需重启进程才会连 WebSocket。',
103
+ secretHint: '框空是正常的(已存值不会传到浏览器)。徽章「已配置」即已保存;重新输入可覆盖。保存后需重启进程才会连 WebSocket。',
90
104
  secretConfigured: '已配置',
91
105
  secretUnset: '未配置',
92
106
  allowFrom: '允许的发送者 userid',
@@ -103,6 +117,13 @@ export const zh: Record<ImBridgeLocaleKey, string> = {
103
117
  providerHint: '空 = 跟随 GUI 默认模型。须与 model 同时填写才覆盖。',
104
118
  model: '企微专用 model',
105
119
  modelHint: '仅在同时填写 provider 时生效。',
120
+ skillsTitle: '企微办公 skills',
121
+ skillsHint: '装到程序目录 $DSH_HOME/wecom-cli-skills。不要用 npx skills add -g;CLI 没有 --dir。Bot ID / Secret 框空是正常的。',
122
+ skillsInstall: '安装官方 skills',
123
+ skillsInstalling: '正在安装…',
124
+ skillsInstalled: '已装 {count} 个到 {dest}',
125
+ skillsFailed: '安装失败。',
126
+ skillsUnavailable: '安装需要 Web Host 的 Connection。',
106
127
  }
107
128
 
108
129
  declare module '@deepseek-ai/dsh-client-ui-slots' {