@dsh-plus/llm-pi 0.1.11 → 0.1.12

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": "@dsh-plus/llm-pi",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "dsh-plus service+ui plugin: 基于 PiAiAdapter 的自定义 LLM 路由(全量 compat、模型继承、models.dev 目录兜底)",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -32,7 +32,8 @@
32
32
  }
33
33
  },
34
34
  "dependencies": {
35
- "https-proxy-agent": "^7.0.6"
35
+ "https-proxy-agent": "^7.0.6",
36
+ "@dsh-plus/shared": "0.1.3"
36
37
  },
37
38
  "peerDependencies": {
38
39
  "@deepseek-ai/cordis": "^4.0.1",
@@ -3,12 +3,20 @@
3
3
  * 顶部:enabled / catalogUrl / catalogRefreshHours / 只读状态行(kitSource、
4
4
  * modelsDevStatus,来自模型目录端点)+ 保存(settings.replace 全量)与
5
5
  * 错误/成功提示;下方为 providers 路由列表(新增/删除/字段编辑/compat/模型
6
- * 目录,见 views/)。配置读写走官方 settingsScope 传输(scope.ts)。
7
- * 交互对齐官方卡片与 notify-email:折叠/展开、staged draft、未保存标记。
6
+ * 目录,见 views/)。外壳与基础控件走 @dsh-plus/shared/client 套件。
8
7
  * @module llm-pi/client/card
9
8
  */
10
- import { type ReactElement, useEffect, useMemo, useState, useSyncExternalStore } from 'react'
11
9
 
10
+ import {
11
+ CardChrome,
12
+ type CardStatusState,
13
+ CheckRow,
14
+ IDLE_STATUS,
15
+ type Scope,
16
+ type SettingsApi,
17
+ TextField,
18
+ } from '@dsh-plus/shared/client'
19
+ import { type ReactElement, useEffect, useMemo, useState, useSyncExternalStore } from 'react'
12
20
  import { SETTINGS_NS } from '../ns.ts'
13
21
  import { type ConfigValue, fetchCatalog, refreshCatalog, type WireModelsDevStatus } from './api.ts'
14
22
  import {
@@ -19,8 +27,6 @@ import {
19
27
  type ProviderDraft,
20
28
  toPatch,
21
29
  } from './draft.ts'
22
- import { CheckRow, TextField } from './fields.tsx'
23
- import type { Scope, SettingsApi } from './scope.ts'
24
30
  import { ProvidersSection } from './views/providers.tsx'
25
31
 
26
32
  export interface CardProps {
@@ -29,13 +35,6 @@ export interface CardProps {
29
35
  api: SettingsApi
30
36
  }
31
37
 
32
- interface Status {
33
- kind: 'idle' | 'ok' | 'error'
34
- text: string
35
- }
36
-
37
- const IDLE_STATUS: Status = { kind: 'idle', text: '' }
38
-
39
38
  function modelsDevText(status: WireModelsDevStatus | null, t: (key: string) => string): string {
40
39
  if (status === null) return t('modelsDevEmpty')
41
40
  if (status.error !== null) return `${t('modelsDevError')}${status.error}`
@@ -57,7 +56,7 @@ export function LlmPiCard(props: CardProps): ReactElement | null {
57
56
  const [modelsDevStatus, setModelsDevStatus] = useState<WireModelsDevStatus | null>(null)
58
57
  const [saving, setSaving] = useState(false)
59
58
  const [refreshing, setRefreshing] = useState(false)
60
- const [status, setStatus] = useState<Status>(IDLE_STATUS)
59
+ const [status, setStatus] = useState<CardStatusState>(IDLE_STATUS)
61
60
 
62
61
  // 首次拿到解析值后播种草稿;后续 Host 更新不覆盖在途编辑(与官方 staged 表单一致)。
63
62
  useEffect(() => {
@@ -172,127 +171,106 @@ export function LlmPiCard(props: CardProps): ReactElement | null {
172
171
 
173
172
  const disabled = !snapshot.writable
174
173
  return (
175
- <li className={`lpc-card${open ? ' lpc-cardOpen' : ''}`}>
176
- <button
177
- type="button"
178
- className="lpc-header"
179
- aria-expanded={open}
180
- aria-label={`${t(open ? 'collapse' : 'expand')}: ${t('title')}`}
181
- onClick={() => setOpen(!open)}
182
- >
183
- <span className="lpc-headText">
184
- <span className="lpc-name">{t('title')}</span>
185
- <span className="lpc-description">{t('description')}</span>
174
+ <CardChrome
175
+ prefix="lpc"
176
+ title={t('title')}
177
+ description={t('description')}
178
+ open={open}
179
+ onToggle={setOpen}
180
+ statusBadge={{ text: t(draft.enabled ? 'enabledOn' : 'enabledOff'), on: draft.enabled }}
181
+ dirty={dirty}
182
+ dirtyLabel={t('unsaved')}
183
+ readOnlyNotice={disabled ? t('readOnly') : undefined}
184
+ status={status}
185
+ actions={[
186
+ {
187
+ key: 'refresh',
188
+ label: t(refreshing ? 'refreshingCatalog' : 'refreshCatalog'),
189
+ disabled: disabled || refreshing,
190
+ onClick: onRefreshCatalog,
191
+ },
192
+ {
193
+ key: 'discard',
194
+ label: t('discard'),
195
+ disabled: !dirty || saving,
196
+ onClick: onDiscard,
197
+ },
198
+ {
199
+ key: 'save',
200
+ label: t(saving ? 'saving' : 'save'),
201
+ variant: 'primary',
202
+ disabled: !dirty || invalid || saving || disabled,
203
+ onClick: onSave,
204
+ },
205
+ ]}
206
+ >
207
+ <CheckRow
208
+ prefix="lpc"
209
+ id="lpc-enabled"
210
+ label={t('enabled')}
211
+ checked={draft.enabled}
212
+ disabled={disabled}
213
+ onEdit={(value) => {
214
+ setDraft({ ...draft, enabled: value })
215
+ setStatus(IDLE_STATUS)
216
+ }}
217
+ />
218
+ <TextField
219
+ prefix="lpc"
220
+ id="lpc-catalogUrl"
221
+ label={t('catalogUrl')}
222
+ hint={t('catalogUrlHint')}
223
+ value={draft.catalogUrl}
224
+ disabled={disabled}
225
+ onEdit={(value) => {
226
+ setDraft({ ...draft, catalogUrl: value })
227
+ setStatus(IDLE_STATUS)
228
+ }}
229
+ />
230
+ <TextField
231
+ prefix="lpc"
232
+ id="lpc-catalogRefresh"
233
+ label={t('catalogRefreshHours')}
234
+ hint={t('catalogRefreshHoursHint')}
235
+ value={draft.catalogRefreshHours}
236
+ numeric
237
+ disabled={disabled}
238
+ invalid={!numTextOk(draft.catalogRefreshHours)}
239
+ invalidLabel={t('invalidNumber')}
240
+ onEdit={(value) => {
241
+ setDraft({ ...draft, catalogRefreshHours: value })
242
+ setStatus(IDLE_STATUS)
243
+ }}
244
+ />
245
+ <TextField
246
+ prefix="lpc"
247
+ id="lpc-catalogProxy"
248
+ label={t('catalogProxy')}
249
+ hint={t('catalogProxyHint')}
250
+ value={draft.catalogProxy}
251
+ disabled={disabled}
252
+ onEdit={(value) => {
253
+ setDraft({ ...draft, catalogProxy: value })
254
+ setStatus(IDLE_STATUS)
255
+ }}
256
+ />
257
+ <p className="lpc-statusRow">
258
+ {t('kitSource')}:{kitSource ?? ''}
259
+ </p>
260
+ <div className="lpc-statusRow">
261
+ <span>
262
+ {t('modelsDevStatus')}:{modelsDevText(modelsDevStatus, t)}
186
263
  </span>
187
- {dirty ? <span className="lpc-pending">{t('unsaved')}</span> : null}
188
- <span className={`lpc-chevron${open ? ' lpc-chevronOpen' : ''}`}>▾</span>
189
- </button>
190
- {open ? (
191
- <div className="lpc-body">
192
- {disabled ? (
193
- <p className="lpc-readOnly" role="status">
194
- {t('readOnly')}
195
- </p>
196
- ) : null}
197
- <CheckRow
198
- id="lpc-enabled"
199
- label={t('enabled')}
200
- checked={draft.enabled}
201
- disabled={disabled}
202
- onEdit={(value) => {
203
- setDraft({ ...draft, enabled: value })
204
- setStatus(IDLE_STATUS)
205
- }}
206
- />
207
- <TextField
208
- id="lpc-catalogUrl"
209
- label={t('catalogUrl')}
210
- hint={t('catalogUrlHint')}
211
- value={draft.catalogUrl}
212
- disabled={disabled}
213
- onEdit={(value) => {
214
- setDraft({ ...draft, catalogUrl: value })
215
- setStatus(IDLE_STATUS)
216
- }}
217
- />
218
- <TextField
219
- id="lpc-catalogRefresh"
220
- label={t('catalogRefreshHours')}
221
- hint={t('catalogRefreshHoursHint')}
222
- value={draft.catalogRefreshHours}
223
- numeric
224
- disabled={disabled}
225
- invalid={!numTextOk(draft.catalogRefreshHours)}
226
- invalidLabel={t('invalidNumber')}
227
- onEdit={(value) => {
228
- setDraft({ ...draft, catalogRefreshHours: value })
229
- setStatus(IDLE_STATUS)
230
- }}
231
- />
232
- <TextField
233
- id="lpc-catalogProxy"
234
- label={t('catalogProxy')}
235
- hint={t('catalogProxyHint')}
236
- value={draft.catalogProxy}
237
- disabled={disabled}
238
- onEdit={(value) => {
239
- setDraft({ ...draft, catalogProxy: value })
240
- setStatus(IDLE_STATUS)
241
- }}
242
- />
243
- <p className="lpc-statusRow">
244
- {t('kitSource')}:{kitSource ?? ''}
245
- </p>
246
- <div className="lpc-statusRow">
247
- <span>
248
- {t('modelsDevStatus')}:{modelsDevText(modelsDevStatus, t)}
249
- </span>
250
- <button
251
- type="button"
252
- className="lpc-btn lpc-btnGhost lpc-btnSmall lpc-refreshBtn"
253
- disabled={disabled || refreshing}
254
- onClick={onRefreshCatalog}
255
- >
256
- {t(refreshing ? 'refreshingCatalog' : 'refreshCatalog')}
257
- </button>
258
- </div>
259
- <ProvidersSection
260
- providers={draft.providers}
261
- epoch={epoch}
262
- disabled={disabled}
263
- t={t}
264
- onAddRoute={onAddRoute}
265
- onRemoveRoute={onRemoveRoute}
266
- onPatchProvider={setProvider}
267
- />
268
- <div className="lpc-footer">
269
- {status.kind !== 'idle' ? (
270
- <p
271
- className={`lpc-status${status.kind === 'error' ? ' lpc-statusError' : ''}`}
272
- role="status"
273
- >
274
- {status.text}
275
- </p>
276
- ) : null}
277
- <button
278
- type="button"
279
- className="lpc-btn lpc-btnGhost"
280
- disabled={!dirty || saving}
281
- onClick={onDiscard}
282
- >
283
- {t('discard')}
284
- </button>
285
- <button
286
- type="button"
287
- className="lpc-btn lpc-btnPrimary"
288
- disabled={!dirty || invalid || saving || disabled}
289
- onClick={onSave}
290
- >
291
- {t(saving ? 'saving' : 'save')}
292
- </button>
293
- </div>
294
- </div>
295
- ) : null}
296
- </li>
264
+ </div>
265
+ <ProvidersSection
266
+ providers={draft.providers}
267
+ epoch={epoch}
268
+ disabled={disabled}
269
+ t={t}
270
+ onAddRoute={onAddRoute}
271
+ onRemoveRoute={onRemoveRoute}
272
+ onPatchProvider={setProvider}
273
+ />
274
+ </CardChrome>
297
275
  )
298
276
  }
@@ -4,8 +4,8 @@
4
4
  * (包装见 tsdown.config.ts);样式沿用官方 data-plugin-css 约定,HMR 据此卸载。
5
5
  *
6
6
  * 类型说明:浏览器半只用到 slots/locale/connection/remote 的很窄一面,
7
- * 此处以最小本地接口声明(见 ./scope.ts),避免为构建期类型引入整条官方
8
- * client 依赖树;运行时契约以官方 dsh-client-ui-settings-plugins 的
7
+ * scope 与基础控件走 @dsh-plus/shared/client 套件(收编自本插件原实现);
8
+ * 运行时契约以官方 dsh-client-ui-settings-plugins 的
9
9
  * settings.plugin.item 插槽与 dsh-client-connection 的 settings RPC 面为准。
10
10
  * rc7 起该插槽为 keyed 槽位:key 必须是卡片编辑的 settings 命名空间
11
11
  * (即服务端 settingsNamespace() 注册的同一字面量,见 ../ns.ts),
@@ -17,11 +17,10 @@
17
17
  * @module @dsh-plus/llm-pi/client
18
18
  */
19
19
  import type { Context } from '@deepseek-ai/cordis'
20
-
20
+ import { createApiScope } from '@dsh-plus/shared/client'
21
21
  import { SETTINGS_NS } from '../ns.ts'
22
22
  import { LlmPiCard } from './card.tsx'
23
23
  import { en, NS, zh } from './i18n.ts'
24
- import type { Scope, ScopeSnapshot, SettingsApi } from './scope.ts'
25
24
  import { injectStyle } from './styles.ts'
26
25
 
27
26
  export const name = 'dsh-plus-llm-pi'
@@ -44,7 +43,7 @@ interface RemoteLike {
44
43
  }
45
44
 
46
45
  interface ConnectionLike {
47
- api: { settings: SettingsApi }
46
+ api: { settings: Parameters<typeof createApiScope>[0] }
48
47
  }
49
48
 
50
49
  interface ClientContext {
@@ -56,82 +55,6 @@ interface ClientContext {
56
55
  effect(execute: () => () => void, label?: string): unknown
57
56
  }
58
57
 
59
- /**
60
- * api 直连实现的命名空间 scope(官方配置页 tab 同款模式)。
61
- * 读:describe 取命名空间视图(脱敏 value/revision + 顶层 writable);
62
- * 刷新:remote `settings/document-updated`(按命名空间过滤)与
63
- * `connection/reset`;generation 防旧读覆盖新发布。任何页面 origin
64
- * (loopback 或 tailnet 信任域名)下行为一致。
65
- */
66
- function createApiScope(api: SettingsApi, ns: string, c: ClientContext): Scope {
67
- let state: ScopeSnapshot = {
68
- status: 'loading',
69
- value: undefined,
70
- revision: undefined,
71
- writable: false,
72
- }
73
- const listeners = new Set<() => void>()
74
- let generation = 0
75
- const publish = (next: ScopeSnapshot): void => {
76
- state = next
77
- for (const listener of [...listeners]) listener()
78
- }
79
- const load = async (): Promise<void> => {
80
- const gen = ++generation
81
- let response: Awaited<ReturnType<SettingsApi['describe']>>
82
- try {
83
- response = await api.describe({})
84
- } catch {
85
- return
86
- }
87
- if (gen !== generation || !response.result.ok) return
88
- const writable = response.result.value?.writable ?? false
89
- const view = response.result.value?.namespaces.find((candidate) => candidate.ns === ns)
90
- if (view === undefined) {
91
- publish({
92
- status: 'unavailable',
93
- value: undefined,
94
- revision: undefined,
95
- writable,
96
- })
97
- return
98
- }
99
- publish({
100
- status: 'ready',
101
- value: view.value,
102
- revision: view.revision,
103
- writable,
104
- })
105
- }
106
- const refresh = (namespace?: unknown): void => {
107
- if (namespace !== undefined && namespace !== ns) return
108
- void load()
109
- }
110
- const disposers = [
111
- c.get('remote').$on('settings/document-updated', refresh),
112
- c.on('connection/reset', () => {
113
- void load()
114
- }),
115
- ]
116
- c.effect(
117
- () => () => {
118
- for (const dispose of disposers) dispose()
119
- },
120
- 'llm-pi: settings scope',
121
- )
122
- void load()
123
- return {
124
- getSnapshot: () => state,
125
- subscribe: (listener: () => void) => {
126
- listeners.add(listener)
127
- return () => {
128
- listeners.delete(listener)
129
- }
130
- },
131
- load,
132
- }
133
- }
134
-
135
58
  export function apply(ctx: Context): void {
136
59
  const c = ctx as unknown as ClientContext
137
60
  const tag = injectStyle()
@@ -143,7 +66,7 @@ export function apply(ctx: Context): void {
143
66
  )
144
67
  c.effect(() => c.locale.register(NS, { zh, en }), 'llm-pi: locale')
145
68
  const api = c.get('connection').api.settings
146
- const scope = createApiScope(api, SETTINGS_NS, c)
69
+ const scope = createApiScope(api, SETTINGS_NS, c, 'llm-pi: settings scope')
147
70
  c.slots.inject('settings.plugin.item', () =>
148
71
  c.slots.register(
149
72
  {
@@ -1,6 +1,7 @@
1
1
  /**
2
- * 配置卡片基础控件:文本/数字输入、勾选行、下拉、键值对编辑、JSON 文本框。
3
- * 视觉对齐官方卡片字段(label + hint + 控件纵列),样式类前缀 lpc-。
2
+ * llm-pi 特有卡片控件:可折叠小节、键值对编辑、JSON 文本框、带 datalist 的
3
+ * 文本字段。通用控件(TextField/CheckRow)走 @dsh-plus/shared/client;
4
+ * 本文件的 SelectField 是 llm-pi 的窄变体(readonly string[] 选项 + 未设置项)。
4
5
  * @module llm-pi/client/fields
5
6
  */
6
7
  import { type ReactElement, useEffect, useState } from 'react'
@@ -81,29 +82,6 @@ export function TextField(props: TextFieldProps): ReactElement {
81
82
  )
82
83
  }
83
84
 
84
- export interface CheckRowProps {
85
- id: string
86
- label: string
87
- checked: boolean
88
- disabled?: boolean
89
- onEdit(checked: boolean): void
90
- }
91
-
92
- export function CheckRow(props: CheckRowProps): ReactElement {
93
- return (
94
- <div className="lpc-checkRow">
95
- <input
96
- id={props.id}
97
- type="checkbox"
98
- checked={props.checked}
99
- disabled={props.disabled === true}
100
- onChange={(event) => props.onEdit(event.target.checked)}
101
- />
102
- <label htmlFor={props.id}>{props.label}</label>
103
- </div>
104
- )
105
- }
106
-
107
85
  export interface SelectFieldProps {
108
86
  id: string
109
87
  label: string
@@ -156,14 +134,8 @@ export interface KeyValueEditorProps {
156
134
  }
157
135
 
158
136
  export function KeyValueEditor(props: KeyValueEditorProps): ReactElement {
159
- const update = (index: number, patch: Partial<HeaderPair>): void => {
160
- props.onEdit(props.pairs.map((pair, i) => (i === index ? { ...pair, ...patch } : pair)))
161
- }
162
- const remove = (index: number): void => {
163
- props.onEdit(props.pairs.filter((_, i) => i !== index))
164
- }
165
137
  return (
166
- <div className="lpc-field lpc-wide">
138
+ <div className="lpc-field">
167
139
  <div className="lpc-head">
168
140
  <label className="lpc-label" htmlFor={props.id}>
169
141
  {props.label}
@@ -171,29 +143,39 @@ export function KeyValueEditor(props: KeyValueEditorProps): ReactElement {
171
143
  </div>
172
144
  {props.pairs.map((pair, index) => (
173
145
  // biome-ignore lint/suspicious/noArrayIndexKey: 可编辑 KV 行无稳定 id,行序即身份(增删行经 onEdit 整体回写)
174
- <div className="lpc-kvRow" key={index}>
146
+ <div className="lpc-kvRow" key={`${props.id}-${index}`}>
175
147
  <input
176
- id={index === 0 ? props.id : undefined}
177
- className="lpc-input"
178
- value={pair.key}
179
148
  placeholder={props.keyPlaceholder}
149
+ className="lpc-input lpc-kvKey"
150
+ type="text"
151
+ value={pair.key}
180
152
  disabled={props.disabled === true}
181
- onChange={(event) => update(index, { key: event.target.value })}
153
+ onChange={(event) => {
154
+ const next = [...props.pairs]
155
+ next[index] = { ...pair, key: event.target.value }
156
+ props.onEdit(next)
157
+ }}
182
158
  />
183
159
  <input
184
- className="lpc-input"
185
- value={pair.value}
186
160
  placeholder={props.valuePlaceholder}
161
+ className="lpc-input lpc-kvValue"
162
+ type="text"
163
+ value={pair.value}
187
164
  disabled={props.disabled === true}
188
- onChange={(event) => update(index, { value: event.target.value })}
165
+ onChange={(event) => {
166
+ const next = [...props.pairs]
167
+ next[index] = { ...pair, value: event.target.value }
168
+ props.onEdit(next)
169
+ }}
189
170
  />
190
171
  <button
191
172
  type="button"
192
173
  className="lpc-btn lpc-btnGhost lpc-btnSmall"
193
174
  disabled={props.disabled === true}
194
- onClick={() => remove(index)}
175
+ aria-label={props.removeLabel}
176
+ onClick={() => props.onEdit(props.pairs.filter((_, i) => i !== index))}
195
177
  >
196
- {props.removeLabel}
178
+
197
179
  </button>
198
180
  </div>
199
181
  ))}
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * 配置卡片文案(zh/en)。经 ctx.locale.register 注册、bind 取用,与官方卡片同机制。
3
+ * 公共键(save/discard/unsaved 等)来自 shared 的 common 字典,本文件只维护业务键。
3
4
  * @module llm-pi/client/i18n
4
5
  */
6
+ import { commonEn, commonZh, mergeDict } from '@dsh-plus/shared/client'
5
7
 
6
8
  export const NS = 'dsh-plus-llm-pi'
7
9
 
8
- export const zh: Record<string, string> = {
10
+ const ownZh: Record<string, string> = {
9
11
  title: 'LLM 路由(llm-pi)',
10
12
  description: '自定义 LLM 路由:协议、compat、模型目录与 models.dev 目录兜底。',
11
13
  enabled: '启用插件',
@@ -82,20 +84,11 @@ export const zh: Record<string, string> = {
82
84
  catalogProvider: '候选 provider',
83
85
  catalogLoading: '目录加载中…',
84
86
  catalogFailed: '目录加载失败。',
85
- save: '保存',
86
- saving: '保存中…',
87
- discard: '放弃',
88
- unsaved: '未保存',
89
87
  saveOk: '已保存。',
90
88
  saveFailed: '保存失败:',
91
- loading: '加载中…',
92
- readOnly: '当前部署无 settings provider,配置为只读;请编辑 settings.yaml。',
93
- expand: '展开',
94
- collapse: '收起',
95
- invalidNumber: '请输入有效数字',
96
89
  }
97
90
 
98
- export const en: Record<string, string> = {
91
+ const ownEn: Record<string, string> = {
99
92
  title: 'LLM routes (llm-pi)',
100
93
  description: 'Custom LLM routes: protocol, compat, model catalog and models.dev fallback.',
101
94
  enabled: 'Enable plugin',
@@ -174,15 +167,9 @@ export const en: Record<string, string> = {
174
167
  catalogProvider: 'Candidate provider',
175
168
  catalogLoading: 'Loading catalog…',
176
169
  catalogFailed: 'Failed to load catalog.',
177
- save: 'Save',
178
- saving: 'Saving…',
179
- discard: 'Discard',
180
- unsaved: 'Unsaved',
181
170
  saveOk: 'Saved.',
182
171
  saveFailed: 'Save failed: ',
183
- loading: 'Loading…',
184
- readOnly: 'No settings provider in this deployment; edit settings.yaml instead.',
185
- expand: 'Expand',
186
- collapse: 'Collapse',
187
- invalidNumber: 'Enter a valid number',
188
172
  }
173
+
174
+ export const zh: Record<string, string> = mergeDict(commonZh, ownZh)
175
+ export const en: Record<string, string> = mergeDict(commonEn, ownEn)
@@ -1,52 +1,22 @@
1
1
  /**
2
- * 配置卡片样式:沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载),
3
- * 视觉对齐官方卡片(--dsw-alias-* 变量),不覆盖上游任何选择器。
2
+ * 配置卡片样式:基础规则走 @dsh-plus/shared/client cardCss('lpc'),
3
+ * 本文件补插件特有规则(route/model 折叠块、键值对行、网格、JSON 文本框)
4
+ * 及窄屏响应式:views 的双列网格在窄屏转单列、route 头部按钮换行、
5
+ * 键值对行纵向堆叠、文本框 16px 防 iOS 缩放。
6
+ * 沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载)。
4
7
  * @module llm-pi/client/styles
5
8
  */
9
+ import { cardCss, injectCardStyle } from '@dsh-plus/shared/client'
6
10
 
7
11
  export const PLUGIN_ID = '@dsh-plus/llm-pi'
8
- export const STYLE_TAG_ID = `${PLUGIN_ID}/card.css`
9
12
 
10
- export const cardCss = `
11
- .lpc-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}
12
- .lpc-card:hover{border-color:var(--dsw-alias-label-dimmed)}
13
- .lpc-cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}
14
- .lpc-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}
15
- .lpc-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}
16
- .lpc-headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}
17
- .lpc-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}
18
- .lpc-description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}
19
- .lpc-chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s;font-size:12px}
20
- .lpc-chevronOpen{transform:rotate(180deg)}
21
- .lpc-pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
22
- .lpc-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}
23
- .lpc-field{flex-direction:column;gap:6px;padding:12px 0;display:flex;min-width:0}
24
- .lpc-field+.lpc-field{border-top:1px solid var(--dsw-alias-border-l2)}
25
- .lpc-head{align-items:center;gap:8px;display:flex}
26
- .lpc-label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}
27
- .lpc-input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;width:100%;box-sizing:border-box;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px}
28
- .lpc-input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}
29
- .lpc-input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}
30
- .lpc-inputInvalid{border-color:var(--dsw-alias-label-error)}
13
+ const extraCss = `
14
+ .lpc-input{width:100%;box-sizing:border-box}
31
15
  .lpc-select{appearance:none}
32
16
  .lpc-textarea{height:auto;min-height:72px;padding:8px 12px;line-height:1.5;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;resize:vertical}
33
- .lpc-hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}
34
- .lpc-invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}
35
- .lpc-checkRow{align-items:center;gap:8px;display:flex;padding:3px 0}
36
- .lpc-checkRow input{accent-color:var(--dsw-alias-brand-primary)}
37
- .lpc-checkRow label{color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5;cursor:pointer}
38
- .lpc-groupLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;padding:14px 0 2px;margin:0}
39
- .lpc-readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}
17
+ .lpc-checkRow{padding:3px 0}
40
18
  .lpc-statusRow{color:var(--dsw-alias-label-tertiary);margin:6px 0 0;font-size:12px;line-height:1.6;word-break:break-all}
41
- .lpc-footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex;flex-wrap:wrap}
42
- .lpc-status{min-width:0;color:var(--dsw-alias-label-secondary);flex:1;margin:0;font-size:12px;line-height:1.5}
43
- .lpc-statusError{color:var(--dsw-alias-label-error)}
44
- .lpc-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5;flex:none}
45
- .lpc-btnGhost{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}
46
- .lpc-btnGhost:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}
47
- .lpc-btnPrimary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}
48
- .lpc-btn:disabled{opacity:.4;cursor:default}
49
- .lpc-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
19
+ .lpc-btn{flex:none}
50
20
  .lpc-btnSmall{padding:2px 10px;font-size:12px}
51
21
  .lpc-grid{display:grid;grid-template-columns:1fr 1fr;gap:0 16px}
52
22
  .lpc-gridNested{margin-top:2px}
@@ -75,18 +45,24 @@ export const cardCss = `
75
45
  .lpc-collapseTitle{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600}
76
46
  .lpc-collapseBody{border-top:1px dashed var(--dsw-alias-border-l2);padding-bottom:6px}
77
47
  .lpc-refreshBtn{margin-left:8px;vertical-align:middle}
48
+ @media (max-width:767px){
49
+ .lpc-grid{grid-template-columns:1fr}
50
+ .lpc-routeHead{flex-wrap:wrap}
51
+ .lpc-routeKey{white-space:normal;word-break:break-all}
52
+ .lpc-routeBody{margin:0 8px}
53
+ .lpc-modelRow{padding:0 8px}
54
+ .lpc-modelHead{flex-wrap:wrap}
55
+ .lpc-kvRow{flex-direction:column;align-items:stretch}
56
+ .lpc-kvRow .lpc-btn{align-self:flex-end}
57
+ .lpc-textarea{font-size:16px}
58
+ .lpc-statusRow{display:flex;flex-direction:column;gap:4px}
59
+ .lpc-catalogBar .lpc-input{flex:1;min-width:140px}
60
+ }
78
61
  `
79
62
 
63
+ export const cardCssAll = cardCss('lpc', extraCss)
64
+
80
65
  /** 幂等注入样式标签;返回标签(已存在或环境无 document 时为 null)。 */
81
66
  export function injectStyle(): HTMLStyleElement | null {
82
- if (typeof document === 'undefined') return null
83
- if (document.querySelector(`style[data-plugin-css=${JSON.stringify(STYLE_TAG_ID)}]`) !== null) {
84
- return null
85
- }
86
- const tag = document.createElement('style')
87
- tag.dataset.plugin = PLUGIN_ID
88
- tag.dataset.pluginCss = STYLE_TAG_ID
89
- tag.textContent = cardCss
90
- document.head.appendChild(tag)
91
- return tag
67
+ return injectCardStyle(PLUGIN_ID, cardCssAll)
92
68
  }