@waterwx/dsh-novel-forge 2.0.1-alpha → 2.1.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.
Files changed (183) hide show
  1. package/README.md +368 -362
  2. package/lib/client.js +20340 -19622
  3. package/lib/index.js +11313 -7285
  4. package/lib/types/action-guard.d.ts +61 -0
  5. package/lib/types/ai-scan.d.ts +0 -6
  6. package/lib/types/assets.d.ts +20 -9
  7. package/lib/types/assistant.d.ts +29 -0
  8. package/lib/types/author-assets.d.ts +1 -3
  9. package/lib/types/bookshelf.d.ts +13 -3
  10. package/lib/types/client/api.d.ts +136 -27
  11. package/lib/types/client/chip-prefs.d.ts +36 -0
  12. package/lib/types/client/docx.d.ts +2 -2
  13. package/lib/types/client/llmLive.d.ts +31 -0
  14. package/lib/types/client/locales.d.ts +15 -70
  15. package/lib/types/client/mount.d.ts +0 -2
  16. package/lib/types/client/panel/AiTag.d.ts +1 -0
  17. package/lib/types/client/panel/AssistantTab.d.ts +4 -1
  18. package/lib/types/client/panel/AuthorHome.d.ts +13 -11
  19. package/lib/types/client/panel/BookAnalysisView.d.ts +4 -0
  20. package/lib/types/client/panel/CmdPalette.d.ts +12 -0
  21. package/lib/types/client/panel/CreateBookView.d.ts +5 -1
  22. package/lib/types/client/panel/DirectorView.d.ts +7 -0
  23. package/lib/types/client/panel/IdeaInspirationView.d.ts +6 -0
  24. package/lib/types/client/panel/KnowledgeBaseView.d.ts +4 -0
  25. package/lib/types/client/panel/LiveFeedLog.d.ts +13 -0
  26. package/lib/types/client/panel/MarketRadarView.d.ts +5 -0
  27. package/lib/types/client/panel/ModelManager.d.ts +2 -2
  28. package/lib/types/client/panel/NovelPanel.d.ts +6 -1
  29. package/lib/types/client/panel/PageHeader.d.ts +18 -0
  30. package/lib/types/client/panel/PromptSlotsView.d.ts +11 -0
  31. package/lib/types/client/panel/SettingsView.d.ts +12 -9
  32. package/lib/types/client/panel/ShelfView.d.ts +10 -1
  33. package/lib/types/client/panel/SubPage.d.ts +27 -0
  34. package/lib/types/client/panel/TensionView.d.ts +18 -0
  35. package/lib/types/client/panel/TimelineView.d.ts +18 -0
  36. package/lib/types/client/panel/helpers.d.ts +5 -3
  37. package/lib/types/client/panel/market-radar-state.d.ts +56 -0
  38. package/lib/types/client/panel/outline-markdown.d.ts +112 -0
  39. package/lib/types/client/panel/status-badge.d.ts +13 -0
  40. package/lib/types/client/panel/storage-parse.d.ts +46 -0
  41. package/lib/types/client/panel/toast.d.ts +2 -0
  42. package/lib/types/client/panel/usePersistentState.d.ts +17 -0
  43. package/lib/types/client/panel/views.d.ts +21 -35
  44. package/lib/types/client/run-status.d.ts +21 -0
  45. package/lib/types/client/sidebar-entry.d.ts +0 -2
  46. package/lib/types/digest-parse.d.ts +29 -0
  47. package/lib/types/docx-core.d.ts +16 -0
  48. package/lib/types/docx.d.ts +2 -3
  49. package/lib/types/engine.d.ts +150 -103
  50. package/lib/types/global-assets.d.ts +6 -0
  51. package/lib/types/home.d.ts +11 -0
  52. package/lib/types/index.d.ts +26 -12
  53. package/lib/types/llm-live.d.ts +70 -0
  54. package/lib/types/llm-retry.d.ts +38 -0
  55. package/lib/types/market-radar-scan.d.ts +18 -0
  56. package/lib/types/market-radar-sources.d.ts +31 -0
  57. package/lib/types/migrate-output.d.ts +25 -0
  58. package/lib/types/model-capability.d.ts +119 -0
  59. package/lib/types/model-request.d.ts +36 -0
  60. package/lib/types/output-budget.d.ts +86 -0
  61. package/lib/types/plotline-align.d.ts +35 -0
  62. package/lib/types/prompt-slots.d.ts +57 -0
  63. package/lib/types/prompting.d.ts +10 -0
  64. package/lib/types/protocol.d.ts +841 -601
  65. package/lib/types/revision.d.ts +76 -0
  66. package/lib/types/routes.d.ts +9 -3
  67. package/lib/types/run-selection.d.ts +51 -0
  68. package/lib/types/run.d.ts +32 -1
  69. package/lib/types/settings-persist.d.ts +59 -0
  70. package/lib/types/snapshots.d.ts +46 -0
  71. package/lib/types/stage-contract.d.ts +58 -0
  72. package/lib/types/stream-fallback.d.ts +60 -0
  73. package/lib/types/strip-headings.d.ts +33 -0
  74. package/lib/types/tension.d.ts +53 -0
  75. package/lib/types/timeline.d.ts +37 -0
  76. package/package.json +50 -27
  77. package/assets/styles/arcane-thick-3d.webp +0 -0
  78. package/assets/styles/chinese-ink.webp +0 -0
  79. package/assets/styles/clay-stopmotion-3d.webp +0 -0
  80. package/assets/styles/dark-ghibli.webp +0 -0
  81. package/assets/styles/dark-gothic-3d.webp +0 -0
  82. package/assets/styles/film-photo.webp +0 -0
  83. package/assets/styles/guofeng-heavy.webp +0 -0
  84. package/assets/styles/guoman-2d-gufeng.webp +0 -0
  85. package/assets/styles/guoman-3d-tianyuan.webp +0 -0
  86. package/assets/styles/guoman-3d-xuanhuan.webp +0 -0
  87. package/assets/styles/horror-game-3d.webp +0 -0
  88. package/assets/styles/hyperreal-3d.webp +0 -0
  89. package/assets/styles/japan-cel-dark.webp +0 -0
  90. package/assets/styles/korean-webtoon.webp +0 -0
  91. package/assets/styles/low-cyberpunk-3d.webp +0 -0
  92. package/assets/styles/nextgen-cg.webp +0 -0
  93. package/assets/styles/noir-film.webp +0 -0
  94. package/assets/styles/openworld-survival.webp +0 -0
  95. package/assets/styles/paper-cut-3d.webp +0 -0
  96. package/assets/styles/pixar-adult-3d.webp +0 -0
  97. package/assets/styles/pop-marvel-2d.webp +0 -0
  98. package/assets/styles/sandiao-qban.webp +0 -0
  99. package/assets/styles/shinkai-anime.webp +0 -0
  100. package/assets/styles/tactical-stealth.webp +0 -0
  101. package/assets/styles/urban-lightreal-3d.webp +0 -0
  102. package/assets/styles/vaporwave-3d.webp +0 -0
  103. package/assets/styles/vhs90.webp +0 -0
  104. package/lib/types/client/panel/BookshelfBar.d.ts +0 -11
  105. package/lib/types/client/panel/FlowGuide.d.ts +0 -22
  106. package/lib/types/client/panel/MangaRoleLibrary.d.ts +0 -20
  107. package/lib/types/client/panel/MangaWorkspace.d.ts +0 -12
  108. package/lib/types/client/panel/ModelPicker.d.ts +0 -11
  109. package/lib/types/client/panel/ProgressHomeView.d.ts +0 -5
  110. package/lib/types/client/panel/PropLibrary.d.ts +0 -8
  111. package/lib/types/client/panel/SceneLibrary.d.ts +0 -16
  112. package/lib/types/client/panel/StoryboardTab.d.ts +0 -19
  113. package/lib/types/client/panel/StyleCard.d.ts +0 -8
  114. package/lib/types/client/panel/StyleGallery.d.ts +0 -1
  115. package/lib/types/comic-presets.d.ts +0 -12
  116. package/lib/types/manga-genre-rules.d.ts +0 -22
  117. package/lib/types/prompt-utils.d.ts +0 -35
  118. package/lib/types/review-policy.d.ts +0 -97
  119. package/lib/types/shot-language.d.ts +0 -46
  120. package/lib/types/story-beat-language.d.ts +0 -35
  121. package/lib/types/style-library.d.ts +0 -33
  122. package/skills/chapter-batch/SKILL.md +0 -81
  123. package/src/ai-scan.ts +0 -179
  124. package/src/assets.ts +0 -879
  125. package/src/assistant.ts +0 -770
  126. package/src/author-assets.ts +0 -147
  127. package/src/bookshelf.ts +0 -167
  128. package/src/client/api.ts +0 -617
  129. package/src/client/css-modules.d.ts +0 -8
  130. package/src/client/docx.ts +0 -69
  131. package/src/client/index.ts +0 -34
  132. package/src/client/locales.ts +0 -418
  133. package/src/client/mount.tsx +0 -97
  134. package/src/client/mount.tsx._new +0 -97
  135. package/src/client/mount.tsx._nf +0 -97
  136. package/src/client/mount.tsx._tmp +0 -97
  137. package/src/client/panel/AdaptModeView.tsx +0 -520
  138. package/src/client/panel/AssetsTab.tsx +0 -458
  139. package/src/client/panel/AssistantTab.tsx +0 -301
  140. package/src/client/panel/AuthorAssetsView.tsx +0 -208
  141. package/src/client/panel/AuthorHome.tsx +0 -204
  142. package/src/client/panel/BookshelfBar.tsx +0 -170
  143. package/src/client/panel/CreateBookView.tsx +0 -352
  144. package/src/client/panel/FlowGuide.tsx +0 -84
  145. package/src/client/panel/GlobalAssetLibraryView.tsx +0 -172
  146. package/src/client/panel/ImportModal.tsx +0 -303
  147. package/src/client/panel/MangaRoleLibrary.tsx +0 -1002
  148. package/src/client/panel/MangaWorkspace.tsx +0 -715
  149. package/src/client/panel/ModelManager.tsx +0 -205
  150. package/src/client/panel/NovelPanel.tsx +0 -4620
  151. package/src/client/panel/ProgressConsole.tsx +0 -64
  152. package/src/client/panel/ProgressHomeView.tsx +0 -87
  153. package/src/client/panel/PropLibrary.tsx +0 -175
  154. package/src/client/panel/ReaderView.tsx +0 -306
  155. package/src/client/panel/ReasoningSection.tsx +0 -31
  156. package/src/client/panel/RunPanel.tsx +0 -204
  157. package/src/client/panel/SceneLibrary.tsx +0 -453
  158. package/src/client/panel/SettingsView.tsx +0 -286
  159. package/src/client/panel/ShelfView.tsx +0 -242
  160. package/src/client/panel/StoryboardTab.tsx +0 -472
  161. package/src/client/panel/StyleCard.tsx +0 -90
  162. package/src/client/panel/StyleGallery.tsx +0 -58
  163. package/src/client/panel/WorldTab.tsx +0 -202
  164. package/src/client/panel/controller.ts +0 -45
  165. package/src/client/panel/helpers.ts +0 -67
  166. package/src/client/panel/panel.module.css +0 -4360
  167. package/src/client/panel/views.tsx +0 -314
  168. package/src/client/sidebar-entry.ts +0 -122
  169. package/src/client/text.ts +0 -28
  170. package/src/comic-presets.ts +0 -67
  171. package/src/docx.ts +0 -83
  172. package/src/engine.ts +0 -5200
  173. package/src/index.ts +0 -329
  174. package/src/manga-genre-rules.ts +0 -87
  175. package/src/novel-context.ts +0 -287
  176. package/src/prompt-utils.ts +0 -73
  177. package/src/protocol.ts +0 -2233
  178. package/src/review-policy.ts +0 -161
  179. package/src/routes.ts +0 -3419
  180. package/src/run.ts +0 -331
  181. package/src/shot-language.ts +0 -181
  182. package/src/story-beat-language.ts +0 -130
  183. package/src/style-library.ts +0 -84
@@ -1,205 +0,0 @@
1
- /**
2
- * 模型管理(DSH 风格):两块分区——
3
- * ① 模型(提供方管理):提供方卡片(编辑/删除)+ 添加提供方 / 添加自定义提供方;
4
- * ② 当前写作模型:提供商/模型下拉 + 测试连通 + 推理强度。
5
- */
6
- import { useCallback, useEffect, useState } from 'react'
7
- import { PlugZap, Plus, Trash2, RefreshCw, X } from 'lucide-react'
8
- import type { NovelApi } from '../api.ts'
9
- import type { SavedModel, AddModelRequest, LlmModelOption, LlmVendorOption } from '../../protocol.ts'
10
- import { LLM_VENDORS } from '../../protocol.ts'
11
- import { tt } from './helpers.ts'
12
- import css from './panel.module.css'
13
-
14
- /** 目录不可用时回退的内置预设(历史行为)。 */
15
- export const FALLBACK_MODEL_PRESETS = ['deepseek-v4-flash', 'deepseek-v4-pro', 'deepseek-chat', 'deepseek-reasoner'] as const
16
-
17
- const CUSTOM = '__custom__'
18
- interface TestState { testing: boolean; ok?: boolean; ms?: number; message?: string }
19
- interface ProviderInfo { id: string; name: string }
20
- type AddPanel = 'none' | 'standard' | 'custom'
21
-
22
- export interface ModelManagerProps {
23
- api: NovelApi
24
- provider: string
25
- model: string
26
- savedModels: SavedModel[]
27
- onProvider: (provider: string) => void
28
- onModel: (model: string) => void
29
- onSavedModels: (models: SavedModel[]) => void
30
- }
31
-
32
- function fallbackVendors(): LlmVendorOption[] {
33
- return LLM_VENDORS.map(v => ({ id: v.route, name: v.name, models: v.models, apiKeyEnv: v.apiKeyEnv, builtin: v.builtin }))
34
- }
35
-
36
- export function ModelManager({ api, provider, model, onProvider, onModel }: ModelManagerProps): JSX.Element {
37
- // ---- 当前写作模型
38
- const [configuredProviders, setConfiguredProviders] = useState<ProviderInfo[]>([])
39
- const [providerModels, setProviderModels] = useState<LlmModelOption[]>([])
40
- const [modelsLoading, setModelsLoading] = useState(false)
41
- const [providerCustom, setProviderCustom] = useState(false)
42
- const [modelCustom, setModelCustom] = useState(false)
43
- const [test, setTest] = useState<TestState>({ testing: false })
44
-
45
- // ---- 提供方管理
46
- const [vendors, setVendors] = useState<LlmVendorOption[]>(fallbackVendors())
47
- const [addPanel, setAddPanel] = useState<AddPanel>('none')
48
- const [editTarget, setEditTarget] = useState('')
49
- const [vendorId, setVendorId] = useState('zai-coding-cn')
50
- const [addModel, setAddModel] = useState('')
51
- const [addApiKey, setAddApiKey] = useState('')
52
- const [addName, setAddName] = useState('')
53
- const [custProvider, setCustProvider] = useState('')
54
- const [custName, setCustName] = useState('')
55
- const [custBaseURL, setCustBaseURL] = useState('')
56
- const [custProtocol, setCustProtocol] = useState('openai-completions')
57
- const [custModel, setCustModel] = useState('')
58
- const [custApiKey, setCustApiKey] = useState('')
59
- const [savingAdd, setSavingAdd] = useState(false)
60
- const [addMsg, setAddMsg] = useState('')
61
- const [addErr, setAddErr] = useState('')
62
-
63
-
64
- const loadProviders = useCallback(async (): Promise<void> => {
65
- try { const r = await api.llmProviders(); if (r.providers.length > 0) setConfiguredProviders(r.providers) } catch { /* 兜底 */ }
66
- }, [api])
67
- const loadVendors = useCallback(async (): Promise<void> => {
68
- try { const r = await api.llmVendors(); if (r.vendors.length > 0) setVendors(r.vendors) } catch { /* 静态兜底 */ }
69
- }, [api])
70
- const loadProviderModels = useCallback(async (p: string): Promise<void> => {
71
- setModelsLoading(true)
72
- try { const r = await api.llmModels(p); setProviderModels(r.models) } catch { setProviderModels([]) } finally { setModelsLoading(false) }
73
- }, [api])
74
-
75
- useEffect(() => { void loadProviders(); void loadVendors() }, [api, loadProviders, loadVendors])
76
- useEffect(() => { if (provider !== '') void loadProviderModels(provider) }, [provider, loadProviderModels])
77
-
78
- const currentVendor = vendors.find(v => v.id === vendorId)
79
-
80
- const runTest = async (): Promise<void> => {
81
- const p = provider.trim(); const m = model.trim()
82
- if (p === '' || m === '' || test.testing) return
83
- setTest({ testing: true })
84
- try { const r = await api.llmTest(p, m); setTest({ testing: false, ok: r.ok, ms: r.ms, message: r.message }) }
85
- catch (err) { setTest({ testing: false, ok: false, message: (err as Error).message }) }
86
- }
87
-
88
- const openPanel = (panel: AddPanel, target = ''): void => {
89
- setAddPanel(panel); setEditTarget(target); setAddMsg(''); setAddErr('')
90
- if (panel === 'standard') { setVendorId(target !== '' ? target : 'zai-coding-cn'); setAddModel(''); setAddApiKey(''); setAddName('') }
91
- else if (panel === 'custom') { setCustProvider(target); setCustName(''); setCustBaseURL(''); setCustModel(''); setCustApiKey('') }
92
- }
93
-
94
- const saveStandard = async (): Promise<void> => {
95
- if (vendorId.trim() === '') { setAddErr('请选择提供方'); return }
96
- if (addApiKey.trim() === '') { setAddErr('请填写 API 密钥(或留空使用环境认证)'); return }
97
- const modelId = addModel.trim()
98
- setSavingAdd(true); setAddMsg(''); setAddErr('')
99
- try {
100
- const res = await api.addModel({ mode: 'vendor', vendor: vendorId, model: modelId || vendorId, apiKey: addApiKey.trim(), apiKeyEnv: currentVendor?.apiKeyEnv, name: addName.trim() || undefined })
101
- setAddMsg(res.message ?? '已保存'); setAddPanel('none'); setAddApiKey(''); setAddModel(''); setAddName(''); setEditTarget('')
102
- await loadProviders()
103
- } catch (err) { setAddErr((err as Error).message) } finally { setSavingAdd(false) }
104
- }
105
-
106
- const saveCustom = async (): Promise<void> => {
107
- if (custProvider.trim() === '') { setAddErr('请填 Provider ID'); return }
108
- if (custBaseURL.trim() === '') { setAddErr('请填 API 地址'); return }
109
- if (custApiKey.trim() === '') { setAddErr('请填 API 密钥(或留空使用环境认证)'); return }
110
- if (custModel.trim() === '') { setAddErr('请填模型 id(或点“获取可用模型”)'); return }
111
- setSavingAdd(true); setAddMsg(''); setAddErr('')
112
- try {
113
- const res = await api.addModel({ mode: 'custom', provider: custProvider.trim(), model: custModel.trim(), apiKey: custApiKey.trim(), baseURL: custBaseURL.trim(), name: custName.trim() || undefined })
114
- setAddMsg(res.message ?? '已创建提供方'); setAddPanel('none'); setCustProvider(''); setCustName(''); setCustBaseURL(''); setCustModel(''); setCustApiKey('')
115
- await loadProviders()
116
- } catch (err) { setAddErr((err as Error).message) } finally { setSavingAdd(false) }
117
- }
118
-
119
- const removeProvider = async (p: ProviderInfo): Promise<void> => {
120
- if (!window.confirm('删除提供方「' + (p.name !== '' ? p.name : p.id) + '」?将移除 DSH 配置与 API 密钥。')) return
121
- try { const vendor = vendors.find(v => v.id === p.id); await api.removeProvider({ provider: p.id, apiKeyEnv: vendor?.apiKeyEnv }); await loadProviders() }
122
- catch (err) { setAddErr((err as Error).message) }
123
- }
124
-
125
-
126
- const providerOptions: ProviderInfo[] = configuredProviders.some(p => p.id === provider) ? configuredProviders : (provider !== '' && provider !== CUSTOM ? [{ id: provider, name: provider }, ...configuredProviders] : configuredProviders)
127
-
128
- const modelOptions: LlmModelOption[] = modelCustom ? [] : (provider === 'deepseek-official'
129
- ? [...FALLBACK_MODEL_PRESETS.map(id => ({ id, name: id })), ...providerModels.filter(m => !(FALLBACK_MODEL_PRESETS as readonly string[]).includes(m.id))]
130
- : providerModels)
131
- const modelInList = modelOptions.some(m => m.id === model)
132
- const modelValue = modelCustom || (!modelInList && model !== '' && modelOptions.length > 0) ? CUSTOM : model
133
- const providerValue = providerCustom ? CUSTOM : provider
134
-
135
-
136
- return (
137
- <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-24)', minWidth: 0, width: '100%' }}>
138
- {/* ① 模型(提供方管理) */}
139
- <div className={css.card + ' ' + css.settingsCard} style={{ gap: 'var(--nf-space-16)' }}>
140
- <span className={css.cardTitle}>模型</span>
141
- <span className={css.meta}>填入各提供方的 API 密钥即可使用其模型。</span>
142
- {configuredProviders.map(p => {
143
- const isBuiltin = p.id === 'deepseek-official'
144
- const isKnownVendor = vendors.some(v => v.id === p.id)
145
- return (
146
- <div key={p.id} style={{ border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-12)', padding: 'var(--nf-space-12)', display: 'flex', alignItems: 'center', gap: 'var(--nf-space-8)', flexWrap: 'wrap', justifyContent: 'space-between' }}>
147
- <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--nf-space-8)' }}><span style={{ fontWeight: 600 }}>{p.name !== '' && p.name !== p.id ? p.name : p.id}</span><span style={{ width: 8, height: 8, borderRadius: '50%', background: 'var(--nf-success)' }} /></div>
148
- <div style={{ display: 'flex', gap: 'var(--nf-space-6)', alignItems: 'center' }}>
149
- {!isBuiltin && <button type="button" className={css.button + ' ' + css.buttonSmall} onClick={() => openPanel(isKnownVendor ? 'standard' : 'custom', p.id)}>{tt('settings.edit')}</button>}
150
- {!isBuiltin && <button type="button" className={css.button + ' ' + css.buttonSmall} style={{ color: 'var(--nf-error)' }} onClick={() => { void removeProvider(p) }}>{tt('settings.delete')}</button>}
151
- </div>
152
- </div>
153
- )
154
- })}
155
- <div className={css.row} style={{ gap: 'var(--nf-space-10)', flexWrap: 'wrap' }}>
156
- <button type="button" className={css.button + ' ' + css.buttonSmall} onClick={() => openPanel('standard')}><Plus size={13} style={{ verticalAlign: -2 }} /> 添加提供方</button>
157
- <button type="button" className={css.button + ' ' + css.buttonSmall} onClick={() => openPanel('custom')}><Plus size={13} style={{ verticalAlign: -2 }} /> 添加自定义提供方</button>
158
- </div>
159
-
160
- {addPanel !== 'none' && (
161
- <div style={{ border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-12)', padding: 'var(--nf-space-12)', display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-12)' }}>
162
- <div className={css.row} style={{ justifyContent: 'space-between', alignItems: 'center' }}><span className={css.fieldLabel}>{addPanel === 'standard' ? (editTarget !== '' ? '编辑提供方' : '添加提供方') : '自定义提供方'}</span><button type="button" className={css.button + ' ' + css.buttonSmall} onClick={() => openPanel('none')}><X size={13} style={{ verticalAlign: -2 }} /> 关闭</button></div>
163
- {addPanel === 'standard' ? (
164
- <>
165
- <div className={css.field}><label className={css.fieldLabel}>提供方</label><select className={css.input} value={vendorId} onChange={e => setVendorId(e.target.value)}>{vendors.map(v => <option key={v.id} value={v.id}>{v.name}</option>)}</select></div>
166
- <div className={css.field}><label className={css.fieldLabel}>API 密钥</label><input className={css.input} type="password" value={addApiKey} onChange={e => setAddApiKey(e.target.value)} placeholder="输入 API 密钥,或留空使用环境认证" /></div>
167
- <div className={css.field}><label className={css.fieldLabel}>模型 id(可选)</label><input className={css.input} value={addModel} onChange={e => setAddModel(e.target.value)} placeholder="例如 glm-5.3-flash" /></div>
168
- <div className={css.row} style={{ justifyContent: 'flex-end', gap: 'var(--nf-space-8)' }}><button type="button" className={css.button + ' ' + css.buttonSmall} onClick={() => openPanel('none')}>取消</button><button type="button" className={css.button + ' ' + css.buttonSmall + ' ' + css.buttonPrimary} disabled={savingAdd} onClick={() => { void saveStandard() }}>{savingAdd ? '保存中…' : '保存'}</button></div>
169
- </>
170
- ) : (
171
- <>
172
- <div className={css.field}><label className={css.fieldLabel}>Provider ID</label><input className={css.input} value={custProvider} onChange={e => setCustProvider(e.target.value)} placeholder="acme-gateway" /></div>
173
- <div className={css.field}><label className={css.fieldLabel}>显示名称</label><input className={css.input} value={custName} onChange={e => setCustName(e.target.value)} placeholder="显示名称" /></div>
174
- <div className={css.field}><label className={css.fieldLabel}>API 地址</label><input className={css.input} value={custBaseURL} onChange={e => setCustBaseURL(e.target.value)} placeholder="https://gateway.example/v1" /></div>
175
- <div className={css.field}><label className={css.fieldLabel}>API 协议</label><select className={css.input} value={custProtocol} onChange={e => setCustProtocol(e.target.value)}><option value="openai-completions">openai-completions</option></select></div>
176
- <div className={css.field}><label className={css.fieldLabel}>API 密钥</label><input className={css.input} type="password" value={custApiKey} onChange={e => setCustApiKey(e.target.value)} placeholder="输入 API 密钥" /></div>
177
- <div className={css.field}><label className={css.fieldLabel}>模型目录 / 模型 id</label><input className={css.input} value={custModel} onChange={e => setCustModel(e.target.value)} placeholder="例如 glm-5.3-flash" /></div>
178
- <div className={css.row} style={{ justifyContent: 'flex-end', gap: 'var(--nf-space-8)' }}><button type="button" className={css.button + ' ' + css.buttonSmall} onClick={() => openPanel('none')}>取消</button><button type="button" className={css.button + ' ' + css.buttonSmall + ' ' + css.buttonPrimary} disabled={savingAdd} onClick={() => { void saveCustom() }}>{savingAdd ? '创建中…' : '创建提供方'}</button></div>
179
- </>
180
- )}
181
- </div>
182
- )}
183
- {addMsg !== '' && <span className={css.meta} style={{ color: 'var(--nf-success)' }}>✓ {addMsg}</span>}
184
- {addErr !== '' && <span className={css.meta} style={{ color: 'var(--nf-error)' }}>✗ {addErr}</span>}
185
- </div>
186
-
187
- {/* ② 当前写作模型 */}
188
- <div className={css.card + ' ' + css.settingsCard} style={{ gap: 'var(--nf-space-16)' }}>
189
- <span className={css.cardTitle}>当前写作模型</span>
190
- <div className={css.field}><label className={css.fieldLabel}>{tt('settings.provider')}</label><select className={css.input} value={providerValue} onChange={e => { const v = e.target.value; if (v === CUSTOM) setProviderCustom(true); else { setProviderCustom(false); onProvider(v) } }}>{providerOptions.map(p => <option key={p.id} value={p.id}>{p.name !== '' && p.name !== p.id ? p.name + '(' + p.id + ')' : p.id}</option>)}<option value={CUSTOM}>{tt('settings.customProvider')}</option></select>{providerCustom && <input className={css.input} style={{ marginTop: 'var(--nf-space-6)' }} value={provider} placeholder="provider 路由 id" onChange={e => onProvider(e.target.value)} />}</div>
191
- <div className={css.field}>
192
- <label className={css.fieldLabel}>{tt('settings.model')}</label>
193
- <div className={css.row} style={{ gap: 'var(--nf-space-8)', flexWrap: 'wrap' }}>
194
- <select className={css.input} style={{ flex: 1, minWidth: 200 }} value={modelValue} onChange={e => { const v = e.target.value; if (v === CUSTOM) setModelCustom(true); else { setModelCustom(false); onModel(v) } }}>{modelOptions.map(m => <option key={m.id} value={m.id}>{m.name !== '' && m.name !== m.id ? m.name + '(' + m.id + ')' : m.id}</option>)}<option value={CUSTOM}>{tt('settings.modelCustom')}</option></select>
195
- <button type="button" className={css.button + ' ' + css.buttonSmall} disabled={modelsLoading || provider === ''} title="刷新模型" onClick={() => { void loadProviderModels(provider) }}><RefreshCw size={13} style={{ verticalAlign: -2 }} /> {modelsLoading ? '…' : tt('settings.refreshModels')}</button>
196
- <button type="button" className={css.button + ' ' + css.buttonSmall} disabled={test.testing || provider.trim() === '' || model.trim() === ''} onClick={() => { void runTest() }} title={tt('settings.testHint')}><PlugZap size={13} style={{ verticalAlign: -2 }} /> {test.testing ? tt('settings.testRunning') : tt('settings.testConnection')}</button>
197
- </div>
198
- {modelCustom && <input className={css.input} style={{ marginTop: 'var(--nf-space-6)' }} value={model} placeholder={tt('settings.modelCustomPlaceholder')} onChange={e => onModel(e.target.value)} />}
199
- <span className={css.meta} style={{ color: test.ok === true ? 'var(--nf-success)' : test.ok === false ? 'var(--nf-error)' : undefined }}>{test.testing ? tt('settings.testRunning') + '…' : test.ok === true ? '✓ ' + tt('settings.testOk') + ' · ' + (test.ms ?? '?') + 'ms' : test.ok === false ? '✗ ' + tt('settings.testFail') + (test.message !== undefined ? ':' + test.message : '') : tt('settings.testHint')}</span>
200
- </div>
201
- </div>
202
- </div>
203
- )
204
- }
205
-