Commit d60ecae9 authored by edy's avatar edy

feat(settings): edit model endpoint configs

parent c591100b
......@@ -223,19 +223,19 @@ function mergeExpertModelConfig(
): ExpertModelConfig {
return {
image: {
baseUrl: FIXED_EXPERT_MODEL_ENDPOINTS.image.baseUrl,
baseUrl: typeof input?.image?.baseUrl === "string" ? input.image.baseUrl.trim() : current.image.baseUrl,
apiKeyConfigured: typeof input?.image?.apiKey === "string" ? Boolean(input.image.apiKey.trim()) : current.image.apiKeyConfigured,
modelId: FIXED_EXPERT_MODEL_ENDPOINTS.image.modelId
modelId: typeof input?.image?.modelId === "string" ? input.image.modelId.trim() : current.image.modelId
},
video: {
baseUrl: FIXED_EXPERT_MODEL_ENDPOINTS.video.baseUrl,
baseUrl: typeof input?.video?.baseUrl === "string" ? input.video.baseUrl.trim() : current.video.baseUrl,
apiKeyConfigured: typeof input?.video?.apiKey === "string" ? Boolean(input.video.apiKey.trim()) : current.video.apiKeyConfigured,
modelId: FIXED_EXPERT_MODEL_ENDPOINTS.video.modelId
modelId: typeof input?.video?.modelId === "string" ? input.video.modelId.trim() : current.video.modelId
},
copywriting: {
baseUrl: FIXED_EXPERT_MODEL_ENDPOINTS.copywriting.baseUrl,
baseUrl: typeof input?.copywriting?.baseUrl === "string" ? input.copywriting.baseUrl.trim() : current.copywriting.baseUrl,
apiKeyConfigured: typeof input?.copywriting?.apiKey === "string" ? Boolean(input.copywriting.apiKey.trim()) : current.copywriting.apiKeyConfigured,
modelId: FIXED_EXPERT_MODEL_ENDPOINTS.copywriting.modelId
modelId: typeof input?.copywriting?.modelId === "string" ? input.copywriting.modelId.trim() : current.copywriting.modelId
},
digitalHuman: {
...FIXED_DIGITAL_HUMAN_CONFIG,
......@@ -395,19 +395,25 @@ export class AppConfigService {
runtimeMode: normalizeRuntimeMode(config.runtimeMode ?? process.env.QJCLAW_RUNTIME_MODE),
expertModelConfig: {
image: {
baseUrl: defaultExpertModelConfig.image.baseUrl,
baseUrl: typeof config.expertModelConfig?.image?.baseUrl === "string"
? config.expertModelConfig.image.baseUrl
: defaultExpertModelConfig.image.baseUrl,
apiKeyConfigured: Boolean(config.expertModelConfig?.image?.apiKeyConfigured),
modelId: defaultExpertModelConfig.image.modelId
modelId: config.expertModelConfig?.image?.modelId ?? defaultExpertModelConfig.image.modelId
},
video: {
baseUrl: defaultExpertModelConfig.video.baseUrl,
baseUrl: typeof config.expertModelConfig?.video?.baseUrl === "string"
? config.expertModelConfig.video.baseUrl
: defaultExpertModelConfig.video.baseUrl,
apiKeyConfigured: Boolean(config.expertModelConfig?.video?.apiKeyConfigured),
modelId: defaultExpertModelConfig.video.modelId
modelId: config.expertModelConfig?.video?.modelId ?? defaultExpertModelConfig.video.modelId
},
copywriting: {
baseUrl: defaultExpertModelConfig.copywriting.baseUrl,
baseUrl: typeof config.expertModelConfig?.copywriting?.baseUrl === "string"
? config.expertModelConfig.copywriting.baseUrl
: defaultExpertModelConfig.copywriting.baseUrl,
apiKeyConfigured: Boolean(config.expertModelConfig?.copywriting?.apiKeyConfigured),
modelId: defaultExpertModelConfig.copywriting.modelId
modelId: config.expertModelConfig?.copywriting?.modelId ?? defaultExpertModelConfig.copywriting.modelId
},
digitalHuman: {
...FIXED_DIGITAL_HUMAN_CONFIG,
......
......@@ -366,10 +366,22 @@ export default function App() {
setWorkspacePathDraft,
imageModelApiKeyDraft,
setImageModelApiKeyDraft,
imageModelBaseUrlDraft,
setImageModelBaseUrlDraft,
imageModelModelIdDraft,
setImageModelModelIdDraft,
videoModelApiKeyDraft,
setVideoModelApiKeyDraft,
videoModelBaseUrlDraft,
setVideoModelBaseUrlDraft,
videoModelModelIdDraft,
setVideoModelModelIdDraft,
copywritingModelApiKeyDraft,
setCopywritingModelApiKeyDraft,
copywritingModelBaseUrlDraft,
setCopywritingModelBaseUrlDraft,
copywritingModelModelIdDraft,
setCopywritingModelModelIdDraft,
digitalHumanVolcAccessKeyDraft,
setDigitalHumanVolcAccessKeyDraft,
digitalHumanVolcSecretKeyDraft,
......@@ -436,8 +448,14 @@ export default function App() {
lobsterKey: lobsterKeyDraft,
workspacePath: workspacePathDraft,
imageModelApiKey: imageModelApiKeyDraft,
imageModelBaseUrl: imageModelBaseUrlDraft,
imageModelModelId: imageModelModelIdDraft,
videoModelApiKey: videoModelApiKeyDraft,
videoModelBaseUrl: videoModelBaseUrlDraft,
videoModelModelId: videoModelModelIdDraft,
copywritingModelApiKey: copywritingModelApiKeyDraft,
copywritingModelBaseUrl: copywritingModelBaseUrlDraft,
copywritingModelModelId: copywritingModelModelIdDraft,
digitalHumanVolcAccessKey: digitalHumanVolcAccessKeyDraft,
digitalHumanVolcSecretKey: digitalHumanVolcSecretKeyDraft,
digitalHumanQiniuAccessKey: digitalHumanQiniuAccessKeyDraft,
......@@ -464,14 +482,26 @@ export default function App() {
setWorkspacePathDraft,
setLobsterKeyDraft,
setImageModelApiKeyDraft,
setImageModelBaseUrlDraft,
setImageModelModelIdDraft,
setVideoModelApiKeyDraft,
setVideoModelBaseUrlDraft,
setVideoModelModelIdDraft,
setCopywritingModelApiKeyDraft,
setCopywritingModelBaseUrlDraft,
setCopywritingModelModelIdDraft,
setDigitalHumanVolcAccessKeyDraft,
setDigitalHumanVolcSecretKeyDraft,
setDigitalHumanQiniuAccessKeyDraft,
setDigitalHumanQiniuSecretKeyDraft,
setVideoAnalyzerBaseUrlDraft,
setVideoAnalyzerModelIdDraft,
setVideoAnalyzerApiKeyDraft,
setReplicationBriefBaseUrlDraft,
setReplicationBriefModelIdDraft,
setReplicationBriefApiKeyDraft,
setVectcutBaseUrlDraft,
setVectcutFileBaseUrlDraft,
setVectcutApiKeyDraft,
setXhsFeishuAppIdDraft,
setXhsFeishuAppSecretDraft,
......@@ -506,9 +536,21 @@ export default function App() {
const displayedWorkspacePath = workspacePathDraft.trim() || savedWorkspacePath || ui.none;
const hasPendingWorkspacePathChange = Boolean(config && workspacePathDraft.trim() && workspacePathDraft.trim() !== savedWorkspacePath);
const hasPendingBasicConfig = hasPendingLobsterKey || hasPendingWorkspacePathChange;
const hasPendingCopywritingConfig = Boolean(copywritingModelApiKeyDraft.trim());
const hasPendingImageConfig = Boolean(imageModelApiKeyDraft.trim());
const hasPendingVideoConfig = Boolean(videoModelApiKeyDraft.trim());
const hasPendingCopywritingConfig = Boolean(
copywritingModelBaseUrlDraft.trim() !== (config?.expertModelConfig.copywriting.baseUrl ?? "").trim()
|| copywritingModelModelIdDraft.trim() !== (config?.expertModelConfig.copywriting.modelId ?? "").trim()
|| copywritingModelApiKeyDraft.trim()
);
const hasPendingImageConfig = Boolean(
imageModelBaseUrlDraft.trim() !== (config?.expertModelConfig.image.baseUrl ?? "").trim()
|| imageModelModelIdDraft.trim() !== (config?.expertModelConfig.image.modelId ?? "").trim()
|| imageModelApiKeyDraft.trim()
);
const hasPendingVideoConfig = Boolean(
videoModelBaseUrlDraft.trim() !== (config?.expertModelConfig.video.baseUrl ?? "").trim()
|| videoModelModelIdDraft.trim() !== (config?.expertModelConfig.video.modelId ?? "").trim()
|| videoModelApiKeyDraft.trim()
);
const hasPendingDigitalHumanConfig = Boolean(
digitalHumanVolcAccessKeyDraft.trim()
|| digitalHumanVolcSecretKeyDraft.trim()
......@@ -544,14 +586,35 @@ export default function App() {
setXhsFeishuTableIdDraft(drafts.xhsFeishuTableId);
}, []);
const resetCopywritingSettingsDrafts = useCallback(() => {
setCopywritingModelApiKeyDraft(getResetCopywritingSettingsDrafts().copywritingModelApiKey);
}, []);
if (!config) {
return;
}
const drafts = getResetCopywritingSettingsDrafts(config);
setCopywritingModelBaseUrlDraft(drafts.copywritingModelBaseUrl);
setCopywritingModelModelIdDraft(drafts.copywritingModelModelId);
setCopywritingModelApiKeyDraft(drafts.copywritingModelApiKey);
}, [config]);
const resetImageSettingsDrafts = useCallback(() => {
setImageModelApiKeyDraft(getResetImageSettingsDrafts().imageModelApiKey);
}, []);
if (!config) {
return;
}
const drafts = getResetImageSettingsDrafts(config);
setImageModelBaseUrlDraft(drafts.imageModelBaseUrl);
setImageModelModelIdDraft(drafts.imageModelModelId);
setImageModelApiKeyDraft(drafts.imageModelApiKey);
}, [config]);
const resetVideoSettingsDrafts = useCallback(() => {
setVideoModelApiKeyDraft(getResetVideoSettingsDrafts().videoModelApiKey);
}, []);
if (!config) {
return;
}
const drafts = getResetVideoSettingsDrafts(config);
setVideoModelBaseUrlDraft(drafts.videoModelBaseUrl);
setVideoModelModelIdDraft(drafts.videoModelModelId);
setVideoModelApiKeyDraft(drafts.videoModelApiKey);
}, [config]);
const resetDigitalHumanSettingsDrafts = useCallback(() => {
const drafts = getResetDigitalHumanSettingsDrafts();
setDigitalHumanVolcAccessKeyDraft(drafts.digitalHumanVolcAccessKey);
......@@ -1419,8 +1482,14 @@ export default function App() {
xhsFeishuAppSecret: xhsFeishuAppSecretDraft,
xhsFeishuAppToken: xhsFeishuAppTokenDraft,
xhsFeishuTableId: xhsFeishuTableIdDraft,
copywritingModelBaseUrl: copywritingModelBaseUrlDraft,
copywritingModelModelId: copywritingModelModelIdDraft,
copywritingModelApiKey: copywritingModelApiKeyDraft,
imageModelBaseUrl: imageModelBaseUrlDraft,
imageModelModelId: imageModelModelIdDraft,
imageModelApiKey: imageModelApiKeyDraft,
videoModelBaseUrl: videoModelBaseUrlDraft,
videoModelModelId: videoModelModelIdDraft,
videoModelApiKey: videoModelApiKeyDraft,
digitalHumanVolcAccessKey: digitalHumanVolcAccessKeyDraft,
digitalHumanVolcSecretKey: digitalHumanVolcSecretKeyDraft,
......@@ -1442,8 +1511,14 @@ export default function App() {
setXhsFeishuAppSecret: setXhsFeishuAppSecretDraft,
setXhsFeishuAppToken: setXhsFeishuAppTokenDraft,
setXhsFeishuTableId: setXhsFeishuTableIdDraft,
setCopywritingModelBaseUrl: setCopywritingModelBaseUrlDraft,
setCopywritingModelModelId: setCopywritingModelModelIdDraft,
setCopywritingModelApiKey: setCopywritingModelApiKeyDraft,
setImageModelBaseUrl: setImageModelBaseUrlDraft,
setImageModelModelId: setImageModelModelIdDraft,
setImageModelApiKey: setImageModelApiKeyDraft,
setVideoModelBaseUrl: setVideoModelBaseUrlDraft,
setVideoModelModelId: setVideoModelModelIdDraft,
setVideoModelApiKey: setVideoModelApiKeyDraft,
setDigitalHumanVolcAccessKey: setDigitalHumanVolcAccessKeyDraft,
setDigitalHumanVolcSecretKey: setDigitalHumanVolcSecretKeyDraft,
......@@ -1459,18 +1534,18 @@ export default function App() {
setVectcutFileBaseUrl: setVectcutFileBaseUrlDraft,
setVectcutApiKey: setVectcutApiKeyDraft
},
onSaveLobsterKey: () => void saveLobsterKey(),
onSaveXhsFeishuConfig: () => void saveXhsFeishuConfig(),
onSaveLobsterKey: saveLobsterKey,
onSaveXhsFeishuConfig: saveXhsFeishuConfig,
onResetXhsFeishuConfig: resetXhsFeishuSettingsDrafts,
onSaveCopywritingConfig: () => void saveCopywritingConfig(),
onSaveCopywritingConfig: saveCopywritingConfig,
onResetCopywritingConfig: resetCopywritingSettingsDrafts,
onSaveImageConfig: () => void saveImageConfig(),
onSaveImageConfig: saveImageConfig,
onResetImageConfig: resetImageSettingsDrafts,
onSaveVideoConfig: () => void saveVideoConfig(),
onSaveVideoConfig: saveVideoConfig,
onResetVideoConfig: resetVideoSettingsDrafts,
onSaveDigitalHumanConfig: () => void saveDigitalHumanConfig(),
onSaveDigitalHumanConfig: saveDigitalHumanConfig,
onResetDigitalHumanConfig: resetDigitalHumanSettingsDrafts,
onSaveDouyinRuntimeConfig: () => void saveDouyinRuntimeConfig(),
onSaveDouyinRuntimeConfig: saveDouyinRuntimeConfig,
onResetDouyinRuntimeConfig: resetDouyinRuntimeSettingsDrafts,
onRevealSecret: revealConfigSecret,
pickWorkspaceDirectory,
......
......@@ -3,7 +3,7 @@ import type { AppConfig, ConfigSecretId } from "@qjclaw/shared-types"
import { Tabs, type TabItem } from "../../components/ui/Tabs"
type SetDraft = (value: string) => void
type SettingsActionHandler = () => void | Promise<void>
type SettingsActionHandler = () => void | boolean | Promise<void | boolean>
type SecretRevealState = Partial<Record<ConfigSecretId, boolean>>
type SecretValueState = Partial<Record<ConfigSecretId, string>>
......@@ -13,8 +13,14 @@ interface SettingsDrafts {
xhsFeishuAppSecret: string
xhsFeishuAppToken: string
xhsFeishuTableId: string
copywritingModelBaseUrl: string
copywritingModelModelId: string
copywritingModelApiKey: string
imageModelBaseUrl: string
imageModelModelId: string
imageModelApiKey: string
videoModelBaseUrl: string
videoModelModelId: string
videoModelApiKey: string
digitalHumanVolcAccessKey: string
digitalHumanVolcSecretKey: string
......@@ -37,8 +43,14 @@ interface SettingsDraftSetters {
setXhsFeishuAppSecret: SetDraft
setXhsFeishuAppToken: SetDraft
setXhsFeishuTableId: SetDraft
setCopywritingModelBaseUrl: SetDraft
setCopywritingModelModelId: SetDraft
setCopywritingModelApiKey: SetDraft
setImageModelBaseUrl: SetDraft
setImageModelModelId: SetDraft
setImageModelApiKey: SetDraft
setVideoModelBaseUrl: SetDraft
setVideoModelModelId: SetDraft
setVideoModelApiKey: SetDraft
setDigitalHumanVolcAccessKey: SetDraft
setDigitalHumanVolcSecretKey: SetDraft
......@@ -101,7 +113,9 @@ interface SectionActions {
}
type SettingsTabId = "basic" | "xhsFeishu" | "copywriting" | "image" | "video" | "digitalHuman" | "douyinRuntime"
type EditableSettingsSectionId = "copywriting" | "image" | "video" | "douyinRuntime"
type SettingsConfigSource = "cloud" | "local"
type EditingSettingsSections = Partial<Record<EditableSettingsSectionId, boolean>>
const settingsTabs: TabItem[] = [
{ id: "basic", label: "基础配置" },
......@@ -163,6 +177,7 @@ export function SettingsPanels({
const [revealedSecrets, setRevealedSecrets] = useState<SecretRevealState>({})
const [revealedSecretValues, setRevealedSecretValues] = useState<SecretValueState>({})
const [copiedConfigValue, setCopiedConfigValue] = useState<string | null>(null)
const [editingSettingsSections, setEditingSettingsSections] = useState<EditingSettingsSections>({})
const handleConfigSourceChange = (source: SettingsConfigSource) => {
setConfigSource(source)
......@@ -193,6 +208,46 @@ export function SettingsPanels({
</div>
)
const renderEditableActions = ({
sectionId,
hasPending,
onReset,
onSave,
className
}: SectionActions & { sectionId: EditableSettingsSectionId }) => {
const editing = Boolean(editingSettingsSections[sectionId])
return (
<div className={["settings-actions-row", className].filter(Boolean).join(" ")}>
<button
type="button"
className="settings-action-button settings-action-button-secondary"
disabled={saving}
onClick={() => {
void onReset()
setEditingSettingsSections((current) => ({ ...current, [sectionId]: !editing }))
}}
>
{editing ? "取消" : "编辑"}
</button>
<button
type="button"
className="settings-action-button settings-action-button-primary"
disabled={saving || !editing || !hasPending}
onClick={() => {
void Promise.resolve(onSave()).then((saved) => {
if (saved !== false) {
setEditingSettingsSections((current) => ({ ...current, [sectionId]: false }))
}
})
}}
>
{saving ? "保存中" : "保存"}
</button>
</div>
)
}
const handleSecretRevealToggle = async (secretId: ConfigSecretId, value: string, setValue: SetDraft) => {
if (revealedSecrets[secretId]) {
setRevealedSecrets((current) => ({ ...current, [secretId]: false }))
......@@ -220,7 +275,8 @@ export function SettingsPanels({
setValue,
placeholder,
inputClassName,
labelClassName
labelClassName,
editable = true
}: {
secretId: ConfigSecretId
configured: boolean
......@@ -230,6 +286,7 @@ export function SettingsPanels({
placeholder: string
inputClassName?: string
labelClassName?: string
editable?: boolean
}) => {
const visible = configured && Boolean(revealedSecrets[secretId])
const secretPlaceholder = configured && !value && !visible ? "••••••••••••" : placeholder
......@@ -243,7 +300,12 @@ export function SettingsPanels({
value={value}
title={value || undefined}
placeholder={secretPlaceholder}
onChange={(event) => setValue(event.target.value)}
readOnly={!editable}
onChange={(event) => {
if (editable) {
setValue(event.target.value)
}
}}
/>
{configured ? (
<button
......@@ -309,6 +371,31 @@ export function SettingsPanels({
)
}
const renderEditableConfigValue = (label: string, value: string, setValue: SetDraft, fallbackValue: string | undefined, editing: boolean, placeholder = "") => {
if (!editing) {
return renderReadonlyConfigValue(label, value || fallbackValue)
}
return (
<label className="settings-input-label settings-editable-config-field">
<span className="settings-input-label-text">{label}</span>
<input
className="settings-truncated-input"
type="text"
value={value}
title={value || undefined}
placeholder={placeholder}
onChange={(event) => setValue(event.target.value)}
/>
</label>
)
}
const editingCopywritingConfig = Boolean(editingSettingsSections.copywriting)
const editingImageConfig = Boolean(editingSettingsSections.image)
const editingVideoConfig = Boolean(editingSettingsSections.video)
const editingDouyinRuntimeConfig = Boolean(editingSettingsSections.douyinRuntime)
return (
<div className="settings-tabs-layout">
<div className="settings-tabs-toolbar">
......@@ -467,22 +554,24 @@ export function SettingsPanels({
</div>
<div className="model-config-card-body">
<div className="settings-field-grid">
{renderReadonlyConfigValue("base_url", config?.expertModelConfig.copywriting.baseUrl)}
{renderReadonlyConfigValue("model_id", config?.expertModelConfig.copywriting.modelId)}
{renderEditableConfigValue("base_url", drafts.copywritingModelBaseUrl, setters.setCopywritingModelBaseUrl, config?.expertModelConfig.copywriting.baseUrl, editingCopywritingConfig, "请输入文案模型 base_url")}
{renderEditableConfigValue("model_id", drafts.copywritingModelModelId, setters.setCopywritingModelModelId, config?.expertModelConfig.copywriting.modelId, editingCopywritingConfig, "请输入文案模型 model_id")}
{renderSecretInput({
secretId: "copywritingModelApiKey",
configured: Boolean(config?.expertModelConfig.copywriting.apiKeyConfigured),
label: "api_key",
value: drafts.copywritingModelApiKey,
setValue: setters.setCopywritingModelApiKey,
placeholder: config?.expertModelConfig.copywriting.apiKeyConfigured ? "留空则保持当前已保存密钥" : "请输入文案模型 API Key"
placeholder: config?.expertModelConfig.copywriting.apiKeyConfigured ? "留空则保持当前已保存密钥" : "请输入文案模型 API Key",
editable: editingCopywritingConfig
})}
</div>
</div>
{renderActions({
{renderEditableActions({
sectionId: "copywriting",
hasPending: hasPendingCopywritingConfig,
onReset: () => void onResetCopywritingConfig(),
onSave: () => void onSaveCopywritingConfig()
onReset: onResetCopywritingConfig,
onSave: onSaveCopywritingConfig
})}
</article>
</div>
......@@ -502,22 +591,24 @@ export function SettingsPanels({
</div>
<div className="model-config-card-body">
<div className="settings-field-grid">
{renderReadonlyConfigValue("base_url", config?.expertModelConfig.image.baseUrl)}
{renderReadonlyConfigValue("model_id", config?.expertModelConfig.image.modelId)}
{renderEditableConfigValue("base_url", drafts.imageModelBaseUrl, setters.setImageModelBaseUrl, config?.expertModelConfig.image.baseUrl, editingImageConfig, "请输入生图模型 base_url")}
{renderEditableConfigValue("model_id", drafts.imageModelModelId, setters.setImageModelModelId, config?.expertModelConfig.image.modelId, editingImageConfig, "请输入生图模型 model_id")}
{renderSecretInput({
secretId: "imageModelApiKey",
configured: Boolean(config?.expertModelConfig.image.apiKeyConfigured),
label: "api_key",
value: drafts.imageModelApiKey,
setValue: setters.setImageModelApiKey,
placeholder: config?.expertModelConfig.image.apiKeyConfigured ? "留空则保持当前已保存密钥" : "请输入生图模型 API Key"
placeholder: config?.expertModelConfig.image.apiKeyConfigured ? "留空则保持当前已保存密钥" : "请输入生图模型 API Key",
editable: editingImageConfig
})}
</div>
</div>
{renderActions({
{renderEditableActions({
sectionId: "image",
hasPending: hasPendingImageConfig,
onReset: () => void onResetImageConfig(),
onSave: () => void onSaveImageConfig()
onReset: onResetImageConfig,
onSave: onSaveImageConfig
})}
</article>
</div>
......@@ -537,22 +628,24 @@ export function SettingsPanels({
</div>
<div className="model-config-card-body">
<div className="settings-field-grid">
{renderReadonlyConfigValue("base_url", config?.expertModelConfig.video.baseUrl)}
{renderReadonlyConfigValue("model_id", config?.expertModelConfig.video.modelId)}
{renderEditableConfigValue("base_url", drafts.videoModelBaseUrl, setters.setVideoModelBaseUrl, config?.expertModelConfig.video.baseUrl, editingVideoConfig, "请输入视频模型 base_url")}
{renderEditableConfigValue("model_id", drafts.videoModelModelId, setters.setVideoModelModelId, config?.expertModelConfig.video.modelId, editingVideoConfig, "请输入视频模型 model_id")}
{renderSecretInput({
secretId: "videoModelApiKey",
configured: Boolean(config?.expertModelConfig.video.apiKeyConfigured),
label: "api_key",
value: drafts.videoModelApiKey,
setValue: setters.setVideoModelApiKey,
placeholder: config?.expertModelConfig.video.apiKeyConfigured ? "留空则保持当前已保存密钥" : "请输入视频模型 API Key"
placeholder: config?.expertModelConfig.video.apiKeyConfigured ? "留空则保持当前已保存密钥" : "请输入视频模型 API Key",
editable: editingVideoConfig
})}
</div>
</div>
{renderActions({
{renderEditableActions({
sectionId: "video",
hasPending: hasPendingVideoConfig,
onReset: () => void onResetVideoConfig(),
onSave: () => void onSaveVideoConfig()
onReset: onResetVideoConfig,
onSave: onSaveVideoConfig
})}
</article>
</div>
......@@ -629,15 +722,16 @@ export function SettingsPanels({
</div>
<div className="model-config-card-body">
<div className="settings-field-grid">
{renderReadonlyConfigValue("base_url", config?.douyinRuntimeConfig.videoAnalyzer.baseUrl)}
{renderReadonlyConfigValue("model_id", config?.douyinRuntimeConfig.videoAnalyzer.modelId || "doubao-vision")}
{renderEditableConfigValue("base_url", drafts.videoAnalyzerBaseUrl, setters.setVideoAnalyzerBaseUrl, config?.douyinRuntimeConfig.videoAnalyzer.baseUrl, editingDouyinRuntimeConfig, "请输入 Video Analyzer base_url")}
{renderEditableConfigValue("model_id", drafts.videoAnalyzerModelId, setters.setVideoAnalyzerModelId, config?.douyinRuntimeConfig.videoAnalyzer.modelId || "doubao-vision", editingDouyinRuntimeConfig, "请输入 Video Analyzer model_id")}
{renderSecretInput({
secretId: "videoAnalyzerApiKey",
configured: Boolean(config?.douyinRuntimeConfig.videoAnalyzer.apiKeyConfigured),
label: "api_key",
value: drafts.videoAnalyzerApiKey,
setValue: setters.setVideoAnalyzerApiKey,
placeholder: config?.douyinRuntimeConfig.videoAnalyzer.apiKeyConfigured ? "留空则保持当前已保存密钥" : "请输入 Video Analyzer API Key"
placeholder: config?.douyinRuntimeConfig.videoAnalyzer.apiKeyConfigured ? "留空则保持当前已保存密钥" : "请输入 Video Analyzer API Key",
editable: editingDouyinRuntimeConfig
})}
</div>
</div>
......@@ -650,15 +744,16 @@ export function SettingsPanels({
</div>
<div className="model-config-card-body">
<div className="settings-field-grid">
{renderReadonlyConfigValue("base_url", config?.douyinRuntimeConfig.replicationBrief.baseUrl)}
{renderReadonlyConfigValue("model_id", config?.douyinRuntimeConfig.replicationBrief.modelId || "qwen-max")}
{renderEditableConfigValue("base_url", drafts.replicationBriefBaseUrl, setters.setReplicationBriefBaseUrl, config?.douyinRuntimeConfig.replicationBrief.baseUrl, editingDouyinRuntimeConfig, "请输入 Replication Brief base_url")}
{renderEditableConfigValue("model_id", drafts.replicationBriefModelId, setters.setReplicationBriefModelId, config?.douyinRuntimeConfig.replicationBrief.modelId || "qwen-max", editingDouyinRuntimeConfig, "请输入 Replication Brief model_id")}
{renderSecretInput({
secretId: "replicationBriefApiKey",
configured: Boolean(config?.douyinRuntimeConfig.replicationBrief.apiKeyConfigured),
label: "api_key",
value: drafts.replicationBriefApiKey,
setValue: setters.setReplicationBriefApiKey,
placeholder: config?.douyinRuntimeConfig.replicationBrief.apiKeyConfigured ? "留空则保持当前已保存密钥" : "请输入 Replication Brief API Key"
placeholder: config?.douyinRuntimeConfig.replicationBrief.apiKeyConfigured ? "留空则保持当前已保存密钥" : "请输入 Replication Brief API Key",
editable: editingDouyinRuntimeConfig
})}
</div>
</div>
......@@ -671,24 +766,26 @@ export function SettingsPanels({
</div>
<div className="model-config-card-body">
<div className="settings-field-grid">
{renderReadonlyConfigValue("base_url", config?.douyinRuntimeConfig.vectcut.baseUrl)}
{renderReadonlyConfigValue("file_base_url", config?.douyinRuntimeConfig.vectcut.fileBaseUrl)}
{renderEditableConfigValue("base_url", drafts.vectcutBaseUrl, setters.setVectcutBaseUrl, config?.douyinRuntimeConfig.vectcut.baseUrl, editingDouyinRuntimeConfig, "请输入 VectCut base_url")}
{renderEditableConfigValue("file_base_url", drafts.vectcutFileBaseUrl, setters.setVectcutFileBaseUrl, config?.douyinRuntimeConfig.vectcut.fileBaseUrl, editingDouyinRuntimeConfig, "请输入 VectCut file_base_url")}
{renderSecretInput({
secretId: "vectcutApiKey",
configured: Boolean(config?.douyinRuntimeConfig.vectcut.apiKeyConfigured),
label: "api_key",
value: drafts.vectcutApiKey,
setValue: setters.setVectcutApiKey,
placeholder: config?.douyinRuntimeConfig.vectcut.apiKeyConfigured ? "留空则保持当前已保存密钥" : "请输入 VectCut API Key"
placeholder: config?.douyinRuntimeConfig.vectcut.apiKeyConfigured ? "留空则保持当前已保存密钥" : "请输入 VectCut API Key",
editable: editingDouyinRuntimeConfig
})}
</div>
</div>
</article>
<div className="settings-runtime-actions-panel">
{renderActions({
{renderEditableActions({
sectionId: "douyinRuntime",
hasPending: hasPendingDouyinRuntimeConfig,
onReset: () => void onResetDouyinRuntimeConfig(),
onSave: () => void onSaveDouyinRuntimeConfig(),
onReset: onResetDouyinRuntimeConfig,
onSave: onSaveDouyinRuntimeConfig,
className: "settings-runtime-actions"
})}
</div>
......
......@@ -22,6 +22,24 @@ export interface DigitalHumanResetSettingsDrafts {
digitalHumanQiniuSecretKey: string
}
export interface CopywritingResetSettingsDrafts {
copywritingModelBaseUrl: string
copywritingModelModelId: string
copywritingModelApiKey: string
}
export interface ImageResetSettingsDrafts {
imageModelBaseUrl: string
imageModelModelId: string
imageModelApiKey: string
}
export interface VideoResetSettingsDrafts {
videoModelBaseUrl: string
videoModelModelId: string
videoModelApiKey: string
}
export interface DouyinRuntimeResetSettingsDrafts {
videoAnalyzerBaseUrl: string
videoAnalyzerModelId: string
......@@ -58,20 +76,26 @@ export function getResetBasicSettingsDrafts(config: AppConfig): BasicResetSettin
}
}
export function getResetCopywritingSettingsDrafts() {
export function getResetCopywritingSettingsDrafts(config: AppConfig): CopywritingResetSettingsDrafts {
return {
copywritingModelBaseUrl: config.expertModelConfig.copywriting.baseUrl,
copywritingModelModelId: config.expertModelConfig.copywriting.modelId ?? "",
copywritingModelApiKey: ""
}
}
export function getResetImageSettingsDrafts() {
export function getResetImageSettingsDrafts(config: AppConfig): ImageResetSettingsDrafts {
return {
imageModelBaseUrl: config.expertModelConfig.image.baseUrl,
imageModelModelId: config.expertModelConfig.image.modelId ?? "",
imageModelApiKey: ""
}
}
export function getResetVideoSettingsDrafts() {
export function getResetVideoSettingsDrafts(config: AppConfig): VideoResetSettingsDrafts {
return {
videoModelBaseUrl: config.expertModelConfig.video.baseUrl,
videoModelModelId: config.expertModelConfig.video.modelId ?? "",
videoModelApiKey: ""
}
}
......
......@@ -9,8 +9,14 @@ interface UseSaveSettingsOptions {
lobsterKey: string
workspacePath: string
imageModelApiKey: string
imageModelBaseUrl: string
imageModelModelId: string
videoModelApiKey: string
videoModelBaseUrl: string
videoModelModelId: string
copywritingModelApiKey: string
copywritingModelBaseUrl: string
copywritingModelModelId: string
digitalHumanVolcAccessKey: string
digitalHumanVolcSecretKey: string
digitalHumanQiniuAccessKey: string
......@@ -37,14 +43,26 @@ interface UseSaveSettingsOptions {
setWorkspacePathDraft(value: string): void
setLobsterKeyDraft(value: string): void
setImageModelApiKeyDraft(value: string): void
setImageModelBaseUrlDraft(value: string): void
setImageModelModelIdDraft(value: string): void
setVideoModelApiKeyDraft(value: string): void
setVideoModelBaseUrlDraft(value: string): void
setVideoModelModelIdDraft(value: string): void
setCopywritingModelApiKeyDraft(value: string): void
setCopywritingModelBaseUrlDraft(value: string): void
setCopywritingModelModelIdDraft(value: string): void
setDigitalHumanVolcAccessKeyDraft(value: string): void
setDigitalHumanVolcSecretKeyDraft(value: string): void
setDigitalHumanQiniuAccessKeyDraft(value: string): void
setDigitalHumanQiniuSecretKeyDraft(value: string): void
setVideoAnalyzerBaseUrlDraft(value: string): void
setVideoAnalyzerModelIdDraft(value: string): void
setVideoAnalyzerApiKeyDraft(value: string): void
setReplicationBriefBaseUrlDraft(value: string): void
setReplicationBriefModelIdDraft(value: string): void
setReplicationBriefApiKeyDraft(value: string): void
setVectcutBaseUrlDraft(value: string): void
setVectcutFileBaseUrlDraft(value: string): void
setVectcutApiKeyDraft(value: string): void
setXhsFeishuAppIdDraft(value: string): void
setXhsFeishuAppSecretDraft(value: string): void
......@@ -80,7 +98,7 @@ export function useSaveSettings({
resetDrafts?: (savedConfig: AppConfig) => void
}) => {
if (!config || saving) {
return
return false
}
const input: SaveConfigInput = {
......@@ -119,15 +137,17 @@ export function useSaveSettings({
options?.resetDrafts?.(savedConfig)
setters.setInfoText(options?.successMessage ?? (trimmedLobsterKey ? labels.saveSuccessPending : labels.saveSuccessApplied))
void refresh(false)
return true
} catch (error) {
setters.setErrorText(normalizeError(error))
return false
} finally {
setters.setSaving(false)
}
}, [config, desktopApi, labels.saveSuccessApplied, labels.saveSuccessPending, normalizeError, refresh, saving, setters])
const saveLobsterKey = useCallback(async () => {
await saveConfig({
return await saveConfig({
lobsterKey: drafts.lobsterKey,
resetDrafts: () => {
setters.setLobsterKeyDraft("")
......@@ -165,7 +185,7 @@ export function useSaveSettings({
}, [config, setters])
const saveWorkspaceDirectory = useCallback(async () => {
await saveConfig({
return await saveConfig({
workspacePath: drafts.workspacePath,
successMessage: labels.workspaceSaved,
resetDrafts: (savedConfig) => {
......@@ -175,7 +195,7 @@ export function useSaveSettings({
}, [drafts.workspacePath, labels.workspaceSaved, saveConfig, setters])
const saveXhsFeishuConfig = useCallback(async () => {
await saveConfig({
return await saveConfig({
xhsFeishuConfig: {
appId: drafts.xhsFeishuAppId.trim() || undefined,
appSecret: drafts.xhsFeishuAppSecret.trim() || undefined,
......@@ -192,46 +212,58 @@ export function useSaveSettings({
}, [drafts.xhsFeishuAppId, drafts.xhsFeishuAppSecret, drafts.xhsFeishuAppToken, drafts.xhsFeishuTableId, saveConfig, setters])
const saveCopywritingConfig = useCallback(async () => {
await saveConfig({
return await saveConfig({
expertModelConfig: {
copywriting: {
baseUrl: drafts.copywritingModelBaseUrl.trim() || undefined,
modelId: drafts.copywritingModelModelId.trim() || undefined,
apiKey: drafts.copywritingModelApiKey.trim() || undefined
}
},
resetDrafts: () => {
resetDrafts: (savedConfig) => {
setters.setCopywritingModelApiKeyDraft("")
setters.setCopywritingModelBaseUrlDraft(savedConfig.expertModelConfig.copywriting.baseUrl)
setters.setCopywritingModelModelIdDraft(savedConfig.expertModelConfig.copywriting.modelId ?? "")
}
})
}, [drafts.copywritingModelApiKey, saveConfig, setters])
}, [drafts.copywritingModelApiKey, drafts.copywritingModelBaseUrl, drafts.copywritingModelModelId, saveConfig, setters])
const saveImageConfig = useCallback(async () => {
await saveConfig({
return await saveConfig({
expertModelConfig: {
image: {
baseUrl: drafts.imageModelBaseUrl.trim() || undefined,
modelId: drafts.imageModelModelId.trim() || undefined,
apiKey: drafts.imageModelApiKey.trim() || undefined
}
},
resetDrafts: () => {
resetDrafts: (savedConfig) => {
setters.setImageModelApiKeyDraft("")
setters.setImageModelBaseUrlDraft(savedConfig.expertModelConfig.image.baseUrl)
setters.setImageModelModelIdDraft(savedConfig.expertModelConfig.image.modelId ?? "")
}
})
}, [drafts.imageModelApiKey, saveConfig, setters])
}, [drafts.imageModelApiKey, drafts.imageModelBaseUrl, drafts.imageModelModelId, saveConfig, setters])
const saveVideoConfig = useCallback(async () => {
await saveConfig({
return await saveConfig({
expertModelConfig: {
video: {
baseUrl: drafts.videoModelBaseUrl.trim() || undefined,
modelId: drafts.videoModelModelId.trim() || undefined,
apiKey: drafts.videoModelApiKey.trim() || undefined
}
},
resetDrafts: () => {
resetDrafts: (savedConfig) => {
setters.setVideoModelApiKeyDraft("")
setters.setVideoModelBaseUrlDraft(savedConfig.expertModelConfig.video.baseUrl)
setters.setVideoModelModelIdDraft(savedConfig.expertModelConfig.video.modelId ?? "")
}
})
}, [drafts.videoModelApiKey, saveConfig, setters])
}, [drafts.videoModelApiKey, drafts.videoModelBaseUrl, drafts.videoModelModelId, saveConfig, setters])
const saveDigitalHumanConfig = useCallback(async () => {
await saveConfig({
return await saveConfig({
expertModelConfig: {
digitalHuman: {
volcAccessKey: drafts.digitalHumanVolcAccessKey.trim() || undefined,
......@@ -250,7 +282,7 @@ export function useSaveSettings({
}, [drafts.digitalHumanQiniuAccessKey, drafts.digitalHumanQiniuSecretKey, drafts.digitalHumanVolcAccessKey, drafts.digitalHumanVolcSecretKey, saveConfig, setters])
const saveDouyinRuntimeConfig = useCallback(async () => {
await saveConfig({
return await saveConfig({
douyinRuntimeConfig: {
videoAnalyzer: {
baseUrl: drafts.videoAnalyzerBaseUrl.trim() || undefined,
......@@ -268,9 +300,15 @@ export function useSaveSettings({
apiKey: drafts.vectcutApiKey.trim() || undefined
}
},
resetDrafts: () => {
resetDrafts: (savedConfig) => {
setters.setVideoAnalyzerBaseUrlDraft(savedConfig.douyinRuntimeConfig.videoAnalyzer.baseUrl)
setters.setVideoAnalyzerModelIdDraft(savedConfig.douyinRuntimeConfig.videoAnalyzer.modelId ?? "")
setters.setVideoAnalyzerApiKeyDraft("")
setters.setReplicationBriefBaseUrlDraft(savedConfig.douyinRuntimeConfig.replicationBrief.baseUrl)
setters.setReplicationBriefModelIdDraft(savedConfig.douyinRuntimeConfig.replicationBrief.modelId ?? "")
setters.setReplicationBriefApiKeyDraft("")
setters.setVectcutBaseUrlDraft(savedConfig.douyinRuntimeConfig.vectcut.baseUrl)
setters.setVectcutFileBaseUrlDraft(savedConfig.douyinRuntimeConfig.vectcut.fileBaseUrl)
setters.setVectcutApiKeyDraft("")
}
})
......
......@@ -15,8 +15,14 @@ export function useSettingsState(config: AppConfig | null) {
const [lobsterKeyDraft, setLobsterKeyDraft] = useState("");
const [workspacePathDraft, setWorkspacePathDraft] = useState("");
const [imageModelApiKeyDraft, setImageModelApiKeyDraft] = useState("");
const [imageModelBaseUrlDraft, setImageModelBaseUrlDraft] = useState("");
const [imageModelModelIdDraft, setImageModelModelIdDraft] = useState("");
const [videoModelApiKeyDraft, setVideoModelApiKeyDraft] = useState("");
const [videoModelBaseUrlDraft, setVideoModelBaseUrlDraft] = useState("");
const [videoModelModelIdDraft, setVideoModelModelIdDraft] = useState("");
const [copywritingModelApiKeyDraft, setCopywritingModelApiKeyDraft] = useState("");
const [copywritingModelBaseUrlDraft, setCopywritingModelBaseUrlDraft] = useState("");
const [copywritingModelModelIdDraft, setCopywritingModelModelIdDraft] = useState("");
const [digitalHumanVolcAccessKeyDraft, setDigitalHumanVolcAccessKeyDraft] = useState("");
const [digitalHumanVolcSecretKeyDraft, setDigitalHumanVolcSecretKeyDraft] = useState("");
const [digitalHumanQiniuAccessKeyDraft, setDigitalHumanQiniuAccessKeyDraft] = useState("");
......@@ -45,8 +51,14 @@ export function useSettingsState(config: AppConfig | null) {
);
const hasPendingModelKeys = Boolean(
imageModelApiKeyDraft.trim()
|| imageModelBaseUrlDraft.trim() !== (config?.expertModelConfig.image.baseUrl ?? "").trim()
|| imageModelModelIdDraft.trim() !== (config?.expertModelConfig.image.modelId ?? "").trim()
|| videoModelApiKeyDraft.trim()
|| videoModelBaseUrlDraft.trim() !== (config?.expertModelConfig.video.baseUrl ?? "").trim()
|| videoModelModelIdDraft.trim() !== (config?.expertModelConfig.video.modelId ?? "").trim()
|| copywritingModelApiKeyDraft.trim()
|| copywritingModelBaseUrlDraft.trim() !== (config?.expertModelConfig.copywriting.baseUrl ?? "").trim()
|| copywritingModelModelIdDraft.trim() !== (config?.expertModelConfig.copywriting.modelId ?? "").trim()
|| digitalHumanVolcAccessKeyDraft.trim()
|| digitalHumanVolcSecretKeyDraft.trim()
|| digitalHumanQiniuAccessKeyDraft.trim()
......@@ -69,10 +81,22 @@ export function useSettingsState(config: AppConfig | null) {
setWorkspacePathDraft,
imageModelApiKeyDraft,
setImageModelApiKeyDraft,
imageModelBaseUrlDraft,
setImageModelBaseUrlDraft,
imageModelModelIdDraft,
setImageModelModelIdDraft,
videoModelApiKeyDraft,
setVideoModelApiKeyDraft,
videoModelBaseUrlDraft,
setVideoModelBaseUrlDraft,
videoModelModelIdDraft,
setVideoModelModelIdDraft,
copywritingModelApiKeyDraft,
setCopywritingModelApiKeyDraft,
copywritingModelBaseUrlDraft,
setCopywritingModelBaseUrlDraft,
copywritingModelModelIdDraft,
setCopywritingModelModelIdDraft,
digitalHumanVolcAccessKeyDraft,
setDigitalHumanVolcAccessKeyDraft,
digitalHumanVolcSecretKeyDraft,
......
......@@ -67,7 +67,7 @@ interface UseSmokeActionHandlersDeps {
douyinRuntimeConfig?: SaveConfigInput["douyinRuntimeConfig"]
xhsFeishuConfig?: SaveConfigInput["xhsFeishuConfig"]
successMessage?: string
}) => Promise<void>
}) => Promise<boolean>
waitForSmokeConfigPublish: (expected: AppConfig["expertModelConfig"]) => Promise<void>
resolveHomeIntentSuggestion: (prompt?: string) => Promise<{
visible: boolean
......
......@@ -306,15 +306,18 @@ export const mockDesktopApi = {
runtimeMode: input.runtimeMode,
expertModelConfig: {
image: {
...FIXED_EXPERT_MODEL_ENDPOINTS.image,
baseUrl: input.expertModelConfig?.image?.baseUrl?.trim() || FIXED_EXPERT_MODEL_ENDPOINTS.image.baseUrl,
modelId: input.expertModelConfig?.image?.modelId?.trim() || FIXED_EXPERT_MODEL_ENDPOINTS.image.modelId,
apiKeyConfigured: Boolean(input.expertModelConfig?.image?.apiKey?.trim()),
},
video: {
...FIXED_EXPERT_MODEL_ENDPOINTS.video,
baseUrl: input.expertModelConfig?.video?.baseUrl?.trim() || FIXED_EXPERT_MODEL_ENDPOINTS.video.baseUrl,
modelId: input.expertModelConfig?.video?.modelId?.trim() || FIXED_EXPERT_MODEL_ENDPOINTS.video.modelId,
apiKeyConfigured: Boolean(input.expertModelConfig?.video?.apiKey?.trim())
},
copywriting: {
...FIXED_EXPERT_MODEL_ENDPOINTS.copywriting,
baseUrl: input.expertModelConfig?.copywriting?.baseUrl?.trim() || FIXED_EXPERT_MODEL_ENDPOINTS.copywriting.baseUrl,
modelId: input.expertModelConfig?.copywriting?.modelId?.trim() || FIXED_EXPERT_MODEL_ENDPOINTS.copywriting.modelId,
apiKeyConfigured: Boolean(input.expertModelConfig?.copywriting?.apiKey?.trim()),
},
digitalHuman: {
......
......@@ -87,14 +87,20 @@ test("basic reset only affects lobster key and workspace path", () => {
})
})
test("single model resets only clear the current module key", () => {
assert.deepEqual(getResetCopywritingSettingsDrafts(), {
test("single model resets restore saved endpoints and clear the current module key", () => {
assert.deepEqual(getResetCopywritingSettingsDrafts(config), {
copywritingModelBaseUrl: "https://copy.example.test",
copywritingModelModelId: "copy-model",
copywritingModelApiKey: "",
})
assert.deepEqual(getResetImageSettingsDrafts(), {
assert.deepEqual(getResetImageSettingsDrafts(config), {
imageModelBaseUrl: "https://image.example.test",
imageModelModelId: "image-model",
imageModelApiKey: "",
})
assert.deepEqual(getResetVideoSettingsDrafts(), {
assert.deepEqual(getResetVideoSettingsDrafts(config), {
videoModelBaseUrl: "https://video.example.test",
videoModelModelId: "video-model",
videoModelApiKey: "",
})
})
......
......@@ -72,6 +72,8 @@ test("settings secret inputs default hidden and gate reveal buttons behind saved
assert.match(settingsPanelsSource, /const visible = configured && Boolean\(revealedSecrets\[secretId\]\)/)
assert.match(settingsPanelsSource, /const secretPlaceholder = configured && !value && !visible \? "••••••••••••" : placeholder/)
assert.match(settingsPanelsSource, /placeholder=\{secretPlaceholder\}/)
assert.match(settingsPanelsSource, /editable = true/)
assert.match(settingsPanelsSource, /readOnly=\{!editable\}/)
assert.match(settingsPanelsSource, /configured \? \(/)
assert.match(settingsPanelsSource, /secretId: "lobsterKey"/)
assert.match(settingsPanelsSource, /configured: workspaceApiKeyConfigured/)
......@@ -98,29 +100,37 @@ test("settings secret inputs default hidden and gate reveal buttons behind saved
assert.match(settingsStylesSource, /\.settings-secret-reveal-button svg\s*\{[\s\S]*?width:\s*22px;[\s\S]*?height:\s*22px;/m)
})
test("fixed expert model cards show base_url and model_id as read-only values", () => {
assert.match(settingsPanelsSource, /config\?\.expertModelConfig\.copywriting\.baseUrl/)
assert.match(settingsPanelsSource, /config\?\.expertModelConfig\.copywriting\.modelId/)
assert.match(settingsPanelsSource, /config\?\.expertModelConfig\.image\.baseUrl/)
assert.match(settingsPanelsSource, /config\?\.expertModelConfig\.image\.modelId/)
assert.match(settingsPanelsSource, /config\?\.expertModelConfig\.video\.baseUrl/)
assert.match(settingsPanelsSource, /config\?\.expertModelConfig\.video\.modelId/)
test("expert model cards become editable after clicking edit", () => {
assert.match(settingsPanelsSource, /const \[editingSettingsSections, setEditingSettingsSections\] = useState<EditingSettingsSections>\(\{\}\)/)
assert.match(settingsPanelsSource, /const renderEditableActions = \(/)
assert.match(settingsPanelsSource, /\{editing \? "取消" : "编辑"\}/)
assert.match(settingsPanelsSource, /disabled=\{saving \|\| !editing \|\| !hasPending\}/)
assert.match(settingsPanelsSource, /Promise\.resolve\(onSave\(\)\)\.then\(\(saved\) =>/)
assert.match(settingsPanelsSource, /if \(saved !== false\)/)
assert.match(settingsPanelsSource, /sectionId: "copywriting"/)
assert.match(settingsPanelsSource, /sectionId: "image"/)
assert.match(settingsPanelsSource, /sectionId: "video"/)
assert.match(settingsPanelsSource, /renderEditableConfigValue\("base_url", drafts\.copywritingModelBaseUrl, setters\.setCopywritingModelBaseUrl, config\?\.expertModelConfig\.copywriting\.baseUrl, editingCopywritingConfig/)
assert.match(settingsPanelsSource, /renderEditableConfigValue\("model_id", drafts\.copywritingModelModelId, setters\.setCopywritingModelModelId, config\?\.expertModelConfig\.copywriting\.modelId, editingCopywritingConfig/)
assert.match(settingsPanelsSource, /renderEditableConfigValue\("base_url", drafts\.imageModelBaseUrl, setters\.setImageModelBaseUrl, config\?\.expertModelConfig\.image\.baseUrl, editingImageConfig/)
assert.match(settingsPanelsSource, /renderEditableConfigValue\("model_id", drafts\.imageModelModelId, setters\.setImageModelModelId, config\?\.expertModelConfig\.image\.modelId, editingImageConfig/)
assert.match(settingsPanelsSource, /renderEditableConfigValue\("base_url", drafts\.videoModelBaseUrl, setters\.setVideoModelBaseUrl, config\?\.expertModelConfig\.video\.baseUrl, editingVideoConfig/)
assert.match(settingsPanelsSource, /renderEditableConfigValue\("model_id", drafts\.videoModelModelId, setters\.setVideoModelModelId, config\?\.expertModelConfig\.video\.modelId, editingVideoConfig/)
assert.match(settingsPanelsSource, /onSave: onSaveCopywritingConfig/)
assert.match(settingsPanelsSource, /onSave: onSaveImageConfig/)
assert.match(settingsPanelsSource, /onSave: onSaveVideoConfig/)
assert.match(settingsPanelsSource, /settings-readonly-config-value/)
})
test("douyin runtime base urls, model ids, and file base url use copyable read-only values", () => {
assert.match(settingsPanelsSource, /renderReadonlyConfigValue\("base_url", config\?\.douyinRuntimeConfig\.videoAnalyzer\.baseUrl\)/)
assert.match(settingsPanelsSource, /renderReadonlyConfigValue\("model_id", config\?\.douyinRuntimeConfig\.videoAnalyzer\.modelId \|\| "doubao-vision"\)/)
assert.match(settingsPanelsSource, /renderReadonlyConfigValue\("base_url", config\?\.douyinRuntimeConfig\.replicationBrief\.baseUrl\)/)
assert.match(settingsPanelsSource, /renderReadonlyConfigValue\("model_id", config\?\.douyinRuntimeConfig\.replicationBrief\.modelId \|\| "qwen-max"\)/)
assert.match(settingsPanelsSource, /renderReadonlyConfigValue\("base_url", config\?\.douyinRuntimeConfig\.vectcut\.baseUrl\)/)
assert.match(settingsPanelsSource, /renderReadonlyConfigValue\("file_base_url", config\?\.douyinRuntimeConfig\.vectcut\.fileBaseUrl\)/)
assert.doesNotMatch(settingsPanelsSource, /setVideoAnalyzerBaseUrl\(event\.target\.value\)/)
assert.doesNotMatch(settingsPanelsSource, /setVideoAnalyzerModelId\(event\.target\.value\)/)
assert.doesNotMatch(settingsPanelsSource, /setReplicationBriefBaseUrl\(event\.target\.value\)/)
assert.doesNotMatch(settingsPanelsSource, /setReplicationBriefModelId\(event\.target\.value\)/)
assert.doesNotMatch(settingsPanelsSource, /setVectcutBaseUrl\(event\.target\.value\)/)
assert.doesNotMatch(settingsPanelsSource, /setVectcutFileBaseUrl\(event\.target\.value\)/)
test("douyin runtime base urls, model ids, and file base url enter editable mode", () => {
assert.match(settingsPanelsSource, /sectionId: "douyinRuntime"/)
assert.match(settingsPanelsSource, /renderEditableConfigValue\("base_url", drafts\.videoAnalyzerBaseUrl, setters\.setVideoAnalyzerBaseUrl, config\?\.douyinRuntimeConfig\.videoAnalyzer\.baseUrl, editingDouyinRuntimeConfig/)
assert.match(settingsPanelsSource, /renderEditableConfigValue\("model_id", drafts\.videoAnalyzerModelId, setters\.setVideoAnalyzerModelId, config\?\.douyinRuntimeConfig\.videoAnalyzer\.modelId \|\| "doubao-vision", editingDouyinRuntimeConfig/)
assert.match(settingsPanelsSource, /renderEditableConfigValue\("base_url", drafts\.replicationBriefBaseUrl, setters\.setReplicationBriefBaseUrl, config\?\.douyinRuntimeConfig\.replicationBrief\.baseUrl, editingDouyinRuntimeConfig/)
assert.match(settingsPanelsSource, /renderEditableConfigValue\("model_id", drafts\.replicationBriefModelId, setters\.setReplicationBriefModelId, config\?\.douyinRuntimeConfig\.replicationBrief\.modelId \|\| "qwen-max", editingDouyinRuntimeConfig/)
assert.match(settingsPanelsSource, /renderEditableConfigValue\("base_url", drafts\.vectcutBaseUrl, setters\.setVectcutBaseUrl, config\?\.douyinRuntimeConfig\.vectcut\.baseUrl, editingDouyinRuntimeConfig/)
assert.match(settingsPanelsSource, /renderEditableConfigValue\("file_base_url", drafts\.vectcutFileBaseUrl, setters\.setVectcutFileBaseUrl, config\?\.douyinRuntimeConfig\.vectcut\.fileBaseUrl, editingDouyinRuntimeConfig/)
assert.match(settingsPanelsSource, /onSave: onSaveDouyinRuntimeConfig/)
})
test("read-only config values expose full values by title and copy on click or keyboard", () => {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment