Commit 9d7f1c0a authored by edy's avatar edy

feat(ui): port Windows settings panel to Mac, add Feishu Mobile config

- Add Feishu Mobile Config feature across all layers (types, secrets, config, IPC, UI)
- Replace Mac glassmorphism design with Windows flat design (settings.css)
- Improve secret reveal UX: loading spinner, error handling, touched state tracking
- Change basic tab layout from single compact card to 3-card bento layout
- Add Modal component (apps/ui/src/components/ui/Modal.tsx)
- Start editable sections in edit mode by default
- Add workspace directory save/restore buttons to SettingsPanels
- Add reveal error display in settings toolbar
- Clean up dead CSS classes from old basic tab layout
- Fix modal panel to use flat design matching settings style
Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
parent 703f338b
......@@ -609,7 +609,9 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
xhsFeishuAppId,
xhsFeishuAppSecret,
xhsFeishuAppToken,
xhsFeishuTableId
xhsFeishuTableId,
feishuMobileAppId,
feishuMobileAppSecret
] = await Promise.all([
secretManager.getApiKey(),
getEffectiveGatewayToken(config),
......@@ -627,7 +629,9 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
secretManager.getXhsFeishuAppId(),
secretManager.getXhsFeishuAppSecret(),
secretManager.getXhsFeishuAppToken(),
secretManager.getXhsFeishuTableId()
secretManager.getXhsFeishuTableId(),
secretManager.getFeishuMobileAppId(),
secretManager.getFeishuMobileAppSecret()
]);
const nextConfig: AppConfig = {
......@@ -676,6 +680,10 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
appSecretConfigured: Boolean(xhsFeishuAppSecret),
appTokenConfigured: Boolean(xhsFeishuAppToken),
tableIdConfigured: Boolean(xhsFeishuTableId)
},
feishuMobileConfig: {
appIdConfigured: Boolean(feishuMobileAppId),
appSecretConfigured: Boolean(feishuMobileAppSecret)
}
};
......@@ -695,7 +703,9 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
|| nextConfig.xhsFeishuConfig.appIdConfigured !== config.xhsFeishuConfig.appIdConfigured
|| nextConfig.xhsFeishuConfig.appSecretConfigured !== config.xhsFeishuConfig.appSecretConfigured
|| nextConfig.xhsFeishuConfig.appTokenConfigured !== config.xhsFeishuConfig.appTokenConfigured
|| nextConfig.xhsFeishuConfig.tableIdConfigured !== config.xhsFeishuConfig.tableIdConfigured;
|| nextConfig.xhsFeishuConfig.tableIdConfigured !== config.xhsFeishuConfig.tableIdConfigured
|| nextConfig.feishuMobileConfig.appIdConfigured !== config.feishuMobileConfig.appIdConfigured
|| nextConfig.feishuMobileConfig.appSecretConfigured !== config.feishuMobileConfig.appSecretConfigured;
if (secretStateChanged) {
await configService.persist({
......@@ -760,6 +770,12 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
case "xhsFeishuTableId":
value = await secretManager.getXhsFeishuTableId();
break;
case "feishuMobileAppId":
value = await secretManager.getFeishuMobileAppId();
break;
case "feishuMobileAppSecret":
value = await secretManager.getFeishuMobileAppSecret();
break;
default:
throw new Error("Unsupported config secret id.");
}
......
......@@ -13,7 +13,8 @@ import {
type RuntimeModePreference,
type SaveConfigInput,
type SetupMode,
type XhsFeishuConfig
type XhsFeishuConfig,
type FeishuMobileConfig
} from "@qjclaw/shared-types";
export type RuntimeCloudApiBaseUrlSource = "config" | "env" | "default";
......@@ -76,6 +77,7 @@ interface LegacyConfig {
};
};
xhsFeishuConfig?: Partial<XhsFeishuConfig>;
feishuMobileConfig?: Partial<FeishuMobileConfig>;
}
function normalizeGatewayUrl(raw: string): string {
......@@ -217,6 +219,13 @@ function createDefaultXhsFeishuConfig(): XhsFeishuConfig {
};
}
function createDefaultFeishuMobileConfig(): FeishuMobileConfig {
return {
appIdConfigured: false,
appSecretConfigured: false
};
}
function mergeExpertModelConfig(
current: ExpertModelConfig,
input?: SaveConfigInput["expertModelConfig"]
......@@ -300,6 +309,16 @@ function mergeXhsFeishuConfig(
};
}
function mergeFeishuMobileConfig(
current: FeishuMobileConfig,
input?: SaveConfigInput["feishuMobileConfig"]
): FeishuMobileConfig {
return {
appIdConfigured: typeof input?.appId === "string" ? Boolean(input.appId.trim()) : current.appIdConfigured,
appSecretConfigured: typeof input?.appSecret === "string" ? Boolean(input.appSecret.trim()) : current.appSecretConfigured
};
}
export function getRuntimeCloudApiTarget(config: Pick<AppConfig, "runtimeCloudApiBaseUrl">): RuntimeCloudApiTarget {
return resolveRuntimeCloudApiTarget(config.runtimeCloudApiBaseUrl);
}
......@@ -334,7 +353,8 @@ export class AppConfigService {
runtimeMode: normalizeRuntimeMode(input.runtimeMode),
expertModelConfig: mergeExpertModelConfig(current.expertModelConfig, input.expertModelConfig),
douyinRuntimeConfig: mergeDouyinRuntimeConfig(current.douyinRuntimeConfig, input.douyinRuntimeConfig),
xhsFeishuConfig: mergeXhsFeishuConfig(current.xhsFeishuConfig, input.xhsFeishuConfig)
xhsFeishuConfig: mergeXhsFeishuConfig(current.xhsFeishuConfig, input.xhsFeishuConfig),
feishuMobileConfig: mergeFeishuMobileConfig(current.feishuMobileConfig, input.feishuMobileConfig)
};
await this.writeConfig(config);
......@@ -372,7 +392,8 @@ export class AppConfigService {
runtimeMode: normalizeRuntimeMode(process.env.QJCLAW_RUNTIME_MODE),
expertModelConfig: createDefaultExpertModelConfig(),
douyinRuntimeConfig: createDefaultDouyinRuntimeConfig(),
xhsFeishuConfig: createDefaultXhsFeishuConfig()
xhsFeishuConfig: createDefaultXhsFeishuConfig(),
feishuMobileConfig: createDefaultFeishuMobileConfig()
};
}
......@@ -380,6 +401,7 @@ export class AppConfigService {
const defaultExpertModelConfig = createDefaultExpertModelConfig();
const defaultDouyinRuntimeConfig = createDefaultDouyinRuntimeConfig();
const defaultXhsFeishuConfig = createDefaultXhsFeishuConfig();
const defaultFeishuMobileConfig = createDefaultFeishuMobileConfig();
return {
setupMode: normalizeSetupMode(config.setupMode),
provider: config.provider ?? "openai",
......@@ -453,6 +475,10 @@ export class AppConfigService {
appSecretConfigured: Boolean(config.xhsFeishuConfig?.appSecretConfigured ?? defaultXhsFeishuConfig.appSecretConfigured),
appTokenConfigured: Boolean(config.xhsFeishuConfig?.appTokenConfigured ?? defaultXhsFeishuConfig.appTokenConfigured),
tableIdConfigured: Boolean(config.xhsFeishuConfig?.tableIdConfigured ?? defaultXhsFeishuConfig.tableIdConfigured)
},
feishuMobileConfig: {
appIdConfigured: Boolean(config.feishuMobileConfig?.appIdConfigured ?? defaultFeishuMobileConfig.appIdConfigured),
appSecretConfigured: Boolean(config.feishuMobileConfig?.appSecretConfigured ?? defaultFeishuMobileConfig.appSecretConfigured)
}
};
}
......
......@@ -21,6 +21,8 @@ interface SecretRecord {
xhsFeishuAppSecret?: string;
xhsFeishuAppToken?: string;
xhsFeishuTableId?: string;
feishuMobileAppId?: string;
feishuMobileAppSecret?: string;
}
interface SecretAccessor {
......@@ -46,7 +48,9 @@ type SecretName =
| "xhsFeishuAppId"
| "xhsFeishuAppSecret"
| "xhsFeishuAppToken"
| "xhsFeishuTableId";
| "xhsFeishuTableId"
| "feishuMobileAppId"
| "feishuMobileAppSecret";
type KeytarModule = typeof import("keytar");
const KEYTAR_SERVICE = "QianjiangClaw";
......@@ -69,7 +73,9 @@ const KEYTAR_ACCOUNT_MAP: Record<SecretName, string> = {
xhsFeishuAppId: "xhs-feishu-app-id",
xhsFeishuAppSecret: "xhs-feishu-app-secret",
xhsFeishuAppToken: "xhs-feishu-app-token",
xhsFeishuTableId: "xhs-feishu-table-id"
xhsFeishuTableId: "xhs-feishu-table-id",
feishuMobileAppId: "feishu-mobile-app-id",
feishuMobileAppSecret: "feishu-mobile-app-secret"
};
class FileSecretStore implements SecretAccessor {
......@@ -321,6 +327,22 @@ export class SecretManager {
return this.store.get("xhsFeishuTableId");
}
async setFeishuMobileAppId(value?: string): Promise<void> {
await this.store.set("feishuMobileAppId", value);
}
async getFeishuMobileAppId(): Promise<string | undefined> {
return this.store.get("feishuMobileAppId");
}
async setFeishuMobileAppSecret(value?: string): Promise<void> {
await this.store.set("feishuMobileAppSecret", value);
}
async getFeishuMobileAppSecret(): Promise<string | undefined> {
return this.store.get("feishuMobileAppSecret");
}
private async tryLoadKeytar(): Promise<KeytarModule | null> {
try {
const imported = await import("keytar");
......@@ -349,7 +371,9 @@ export class SecretManager {
"xhsFeishuAppId",
"xhsFeishuAppSecret",
"xhsFeishuAppToken",
"xhsFeishuTableId"
"xhsFeishuTableId",
"feishuMobileAppId",
"feishuMobileAppSecret"
] as const) {
const existing = await this.store.get(secretName);
if (existing) {
......
......@@ -82,7 +82,8 @@ import {
getResetDouyinRuntimeSettingsDrafts,
getResetImageSettingsDrafts,
getResetVideoSettingsDrafts,
getResetXhsFeishuSettingsDrafts
getResetXhsFeishuSettingsDrafts,
getResetFeishuMobileSettingsDrafts
} from "./features/settings/settingsDrafts";
import { useSaveSettings } from "./features/settings/useSaveSettings";
import { useSettingsState } from "./features/settings/useSettingsState";
......@@ -425,7 +426,12 @@ export default function App() {
saving,
setSaving,
hasPendingLobsterKey,
hasPendingXhsFeishuConfig
hasPendingXhsFeishuConfig,
feishuMobileAppIdDraft,
setFeishuMobileAppIdDraft,
feishuMobileAppSecretDraft,
setFeishuMobileAppSecretDraft,
hasPendingFeishuMobileConfig
} = useSettingsState(config);
const {
messageTraces,
......@@ -445,6 +451,9 @@ export default function App() {
saveVideoConfig,
saveDigitalHumanConfig,
saveDouyinRuntimeConfig,
saveFeishuMobileConfig,
saveWorkspaceDirectory,
restoreWorkspaceDirectory,
pickWorkspaceDirectory
} = useSaveSettings({
config,
......@@ -478,7 +487,9 @@ export default function App() {
xhsFeishuAppId: xhsFeishuAppIdDraft,
xhsFeishuAppSecret: xhsFeishuAppSecretDraft,
xhsFeishuAppToken: xhsFeishuAppTokenDraft,
xhsFeishuTableId: xhsFeishuTableIdDraft
xhsFeishuTableId: xhsFeishuTableIdDraft,
feishuMobileAppId: feishuMobileAppIdDraft,
feishuMobileAppSecret: feishuMobileAppSecretDraft
},
setters: {
setConfig,
......@@ -512,7 +523,9 @@ export default function App() {
setXhsFeishuAppIdDraft,
setXhsFeishuAppSecretDraft,
setXhsFeishuAppTokenDraft,
setXhsFeishuTableIdDraft
setXhsFeishuTableIdDraft,
setFeishuMobileAppIdDraft,
setFeishuMobileAppSecretDraft
},
labels: {
saveSuccessPending: ui.saveSuccessPending,
......@@ -581,7 +594,8 @@ export default function App() {
hasPendingImageConfig,
hasPendingVideoConfig,
hasPendingDigitalHumanConfig,
hasPendingDouyinRuntimeConfig
hasPendingDouyinRuntimeConfig,
hasPendingFeishuMobileConfig
});
void hasPendingSettingsChange;
const resetXhsFeishuSettingsDrafts = useCallback(() => {
......@@ -591,6 +605,11 @@ export default function App() {
setXhsFeishuAppTokenDraft(drafts.xhsFeishuAppToken);
setXhsFeishuTableIdDraft(drafts.xhsFeishuTableId);
}, []);
const resetFeishuMobileSettingsDrafts = useCallback(() => {
const drafts = getResetFeishuMobileSettingsDrafts();
setFeishuMobileAppIdDraft(drafts.feishuMobileAppId);
setFeishuMobileAppSecretDraft(drafts.feishuMobileAppSecret);
}, []);
const resetCopywritingSettingsDrafts = useCallback(() => {
if (!config) {
return;
......@@ -1562,6 +1581,7 @@ export default function App() {
hasPendingVideoConfig,
hasPendingDigitalHumanConfig,
hasPendingDouyinRuntimeConfig,
hasPendingFeishuMobileConfig,
labels: { export: ui.export },
drafts: {
lobsterKey: lobsterKeyDraft,
......@@ -1590,7 +1610,9 @@ export default function App() {
replicationBriefApiKey: replicationBriefApiKeyDraft,
vectcutBaseUrl: vectcutBaseUrlDraft,
vectcutFileBaseUrl: vectcutFileBaseUrlDraft,
vectcutApiKey: vectcutApiKeyDraft
vectcutApiKey: vectcutApiKeyDraft,
feishuMobileAppId: feishuMobileAppIdDraft,
feishuMobileAppSecret: feishuMobileAppSecretDraft
},
setters: {
setLobsterKey: setLobsterKeyDraft,
......@@ -1619,7 +1641,9 @@ export default function App() {
setReplicationBriefApiKey: setReplicationBriefApiKeyDraft,
setVectcutBaseUrl: setVectcutBaseUrlDraft,
setVectcutFileBaseUrl: setVectcutFileBaseUrlDraft,
setVectcutApiKey: setVectcutApiKeyDraft
setVectcutApiKey: setVectcutApiKeyDraft,
setFeishuMobileAppId: setFeishuMobileAppIdDraft,
setFeishuMobileAppSecret: setFeishuMobileAppSecretDraft
},
onSaveLobsterKey: saveLobsterKey,
onSaveXhsFeishuConfig: saveXhsFeishuConfig,
......@@ -1634,6 +1658,10 @@ export default function App() {
onResetDigitalHumanConfig: resetDigitalHumanSettingsDrafts,
onSaveDouyinRuntimeConfig: saveDouyinRuntimeConfig,
onResetDouyinRuntimeConfig: resetDouyinRuntimeSettingsDrafts,
onSaveFeishuMobileConfig: saveFeishuMobileConfig,
onResetFeishuMobileConfig: resetFeishuMobileSettingsDrafts,
onSaveWorkspaceDirectory: saveWorkspaceDirectory,
onResetWorkspaceDirectory: restoreWorkspaceDirectory,
onRevealSecret: revealConfigSecret,
pickWorkspaceDirectory,
exportDiagnostics
......
import type { ReactNode } from "react"
export interface ModalProps {
open: boolean
onClose: () => void
title?: string
children: ReactNode
}
export function Modal({ open, onClose, title, children }: ModalProps) {
if (!open) {
return null
}
return (
<div className="modal-overlay" role="dialog" aria-modal="true" onClick={onClose}>
<div className="modal-panel" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
{title ? <h2 className="modal-title">{title}</h2> : <span />}
<button
type="button"
className="modal-close-button"
onClick={onClose}
aria-label="关闭"
>
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
<path
d="M18 6L6 18M6 6l12 12"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
</button>
</div>
<div className="modal-body">
{children}
</div>
</div>
</div>
)
}
import { useState, type KeyboardEvent } from "react"
import { memo, useState, type KeyboardEvent } from "react"
import type { AppConfig, ConfigSecretId } from "@qjclaw/shared-types"
import { Tabs, type TabItem } from "../../components/ui/Tabs"
import { Modal } from "../../components/ui/Modal"
type SetDraft = (value: string) => void
type SettingsActionHandler = () => void | boolean | Promise<void | boolean>
......@@ -35,6 +36,8 @@ interface SettingsDrafts {
vectcutBaseUrl: string
vectcutFileBaseUrl: string
vectcutApiKey: string
feishuMobileAppId: string
feishuMobileAppSecret: string
}
interface SettingsDraftSetters {
......@@ -65,6 +68,8 @@ interface SettingsDraftSetters {
setVectcutBaseUrl: SetDraft
setVectcutFileBaseUrl: SetDraft
setVectcutApiKey: SetDraft
setFeishuMobileAppId: SetDraft
setFeishuMobileAppSecret: SetDraft
}
interface SettingsPanelsProps {
......@@ -80,6 +85,7 @@ interface SettingsPanelsProps {
hasPendingVideoConfig: boolean
hasPendingDigitalHumanConfig: boolean
hasPendingDouyinRuntimeConfig: boolean
hasPendingFeishuMobileConfig: boolean
labels: {
export: string
}
......@@ -98,6 +104,10 @@ interface SettingsPanelsProps {
onResetDigitalHumanConfig: SettingsActionHandler
onSaveDouyinRuntimeConfig: SettingsActionHandler
onResetDouyinRuntimeConfig: SettingsActionHandler
onSaveFeishuMobileConfig: SettingsActionHandler
onResetFeishuMobileConfig: SettingsActionHandler
onSaveWorkspaceDirectory: SettingsActionHandler
onResetWorkspaceDirectory: SettingsActionHandler
onRevealSecret: (secretId: ConfigSecretId) => Promise<string | null>
onConfigSourceChange?: (source: SettingsConfigSource) => void | Promise<void>
loadCloudConfig?: () => void | Promise<void>
......@@ -127,7 +137,7 @@ const settingsTabs: TabItem[] = [
{ id: "douyinRuntime", label: "抖音运行时" }
]
function SecretVisibilityIcon({ visible }: { visible: boolean }) {
const SecretVisibilityIcon = memo(function SecretVisibilityIcon({ visible }: { visible: boolean }) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M2.8 12s3.4-5.8 9.2-5.8 9.2 5.8 9.2 5.8-3.4 5.8-9.2 5.8S2.8 12 2.8 12Z" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" />
......@@ -135,7 +145,7 @@ function SecretVisibilityIcon({ visible }: { visible: boolean }) {
{visible ? <path d="m4.5 19.5 15-15" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" /> : null}
</svg>
)
}
})
export function SettingsPanels({
config,
......@@ -150,6 +160,7 @@ export function SettingsPanels({
hasPendingVideoConfig,
hasPendingDigitalHumanConfig,
hasPendingDouyinRuntimeConfig,
hasPendingFeishuMobileConfig,
labels,
drafts,
setters,
......@@ -166,6 +177,10 @@ export function SettingsPanels({
onResetDigitalHumanConfig,
onSaveDouyinRuntimeConfig,
onResetDouyinRuntimeConfig,
onSaveFeishuMobileConfig,
onResetFeishuMobileConfig,
onSaveWorkspaceDirectory,
onResetWorkspaceDirectory,
onRevealSecret,
onConfigSourceChange,
loadCloudConfig,
......@@ -177,7 +192,16 @@ 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 [editingSettingsSections, setEditingSettingsSections] = useState<EditingSettingsSections>({
copywriting: true,
image: true,
video: true,
douyinRuntime: true
})
const [loadingSecrets, setLoadingSecrets] = useState<Partial<Record<ConfigSecretId, boolean>>>({})
const [touchedSecrets, setTouchedSecrets] = useState<Partial<Record<ConfigSecretId, boolean>>>({})
const [revealError, setRevealError] = useState<string | null>(null)
const [feishuMobileManualOpen, setFeishuMobileManualOpen] = useState(false)
const handleConfigSourceChange = (source: SettingsConfigSource) => {
setConfigSource(source)
......@@ -248,23 +272,41 @@ export function SettingsPanels({
)
}
const handleSecretRevealToggle = async (secretId: ConfigSecretId, value: string, setValue: SetDraft) => {
const handleSecretRevealToggle = async (secretId: ConfigSecretId) => {
if (revealedSecrets[secretId]) {
setRevealedSecrets((current) => ({ ...current, [secretId]: false }))
if (revealedSecretValues[secretId] && value === revealedSecretValues[secretId]) {
setValue("")
}
setRevealedSecrets((current) => {
const next = { ...current }
delete next[secretId]
return next
})
setTouchedSecrets((current) => {
const next = { ...current }
delete next[secretId]
return next
})
return
}
const secretValue = await onRevealSecret(secretId)
if (!secretValue) {
return
}
setRevealError(null)
setLoadingSecrets((current) => ({ ...current, [secretId]: true }))
try {
const secretValue = await onRevealSecret(secretId)
if (!secretValue) {
setRevealError("本机未保存该密钥。")
return
}
setValue(secretValue)
setRevealedSecretValues((current) => ({ ...current, [secretId]: secretValue }))
setRevealedSecrets((current) => ({ ...current, [secretId]: true }))
setRevealedSecretValues((current) => ({ ...current, [secretId]: secretValue }))
setRevealedSecrets((current) => ({ ...current, [secretId]: true }))
} catch {
setRevealError("读取密钥失败,请稍后再试。")
} finally {
setLoadingSecrets((current) => {
const next = { ...current }
delete next[secretId]
return next
})
}
}
const renderSecretInput = ({
......@@ -290,6 +332,10 @@ export function SettingsPanels({
}) => {
const visible = configured && Boolean(revealedSecrets[secretId])
const secretPlaceholder = configured && !value && !visible ? "••••••••••••" : placeholder
const hasDraft = value.length > 0
const isTouched = Boolean(touchedSecrets[secretId])
const displayedSecretValue = visible && !hasDraft && !isTouched ? (revealedSecretValues[secretId] ?? value) : value
return (
<label className={["settings-input-label", labelClassName].filter(Boolean).join(" ")}>
{label ? <span className="settings-input-label-text">{label}</span> : null}
......@@ -297,13 +343,14 @@ export function SettingsPanels({
<input
className={["settings-secret-value-input", inputClassName].filter(Boolean).join(" ")}
type={visible ? "text" : "password"}
value={value}
title={value || undefined}
value={displayedSecretValue}
title={visible ? (displayedSecretValue || undefined) : undefined}
placeholder={secretPlaceholder}
readOnly={!editable}
onChange={(event) => {
if (editable) {
setValue(event.target.value)
setTouchedSecrets((current) => ({ ...current, [secretId]: true }))
}
}}
/>
......@@ -313,10 +360,10 @@ export function SettingsPanels({
className="settings-secret-reveal-button settings-secret-reveal-button-adornment"
aria-label={visible ? "隐藏密钥" : "显示密钥"}
title={visible ? "隐藏密钥" : "显示密钥"}
disabled={saving}
onClick={() => void handleSecretRevealToggle(secretId, value, setValue)}
disabled={saving || Boolean(loadingSecrets[secretId])}
onClick={() => void handleSecretRevealToggle(secretId)}
>
<SecretVisibilityIcon visible={visible} />
{loadingSecrets[secretId] ? <span className="settings-secret-reveal-spinner" /> : <SecretVisibilityIcon visible={visible} />}
</button>
) : null}
</div>
......@@ -359,13 +406,13 @@ export function SettingsPanels({
role="button"
tabIndex={value ? 0 : -1}
aria-disabled={!value}
aria-label={value ? `${label} ${copied ? "已复制" : "复制"}` : label}
title={copied ? "已复制" : value || undefined}
aria-label={value ? `${label} ${copied ? "已复制" : "复制"}` : label}
title={copied ? "已复制" : value || undefined}
onClick={() => copyReadonlyConfigValue(value)}
onKeyDown={(event) => handleReadonlyConfigKeyDown(event, value)}
>
<span className="settings-readonly-config-text">{value || "-"}</span>
{copied ? <span className="settings-readonly-config-copy-feedback">已复制</span> : null}
{copied ? <span className="settings-readonly-config-copy-feedback">已复制</span> : null}
</div>
</div>
)
......@@ -397,9 +444,11 @@ export function SettingsPanels({
const editingDouyinRuntimeConfig = Boolean(editingSettingsSections.douyinRuntime)
return (
<>
<div className="settings-tabs-layout">
<div className="settings-tabs-toolbar">
<div className="settings-tabs-row">
{revealError ? <span className="settings-reveal-error">{revealError}</span> : null}
<Tabs
items={settingsTabs}
value={activeTab}
......@@ -430,9 +479,11 @@ export function SettingsPanels({
{activeTab === "basic" ? (
<section className="panel settings-panel settings-panel-modern settings-panel-basic-config">
<div className="settings-section-card settings-section-card-compact settings-basic-config-card">
<div className="settings-basic-config-form">
<div className="settings-basic-config-row settings-basic-config-row-key">
<div className="settings-config-cards">
{/* Card: 龙虾密钥 */}
<div className="settings-config-card">
<div className="settings-card-body">
{renderSecretInput({
secretId: "lobsterKey",
configured: workspaceApiKeyConfigured,
......@@ -440,55 +491,118 @@ export function SettingsPanels({
value: drafts.lobsterKey,
setValue: setters.setLobsterKey,
inputClassName: "settings-truncated-input",
labelClassName: "settings-basic-config-field",
placeholder: workspaceApiKeyConfigured ? "输入新的龙虾密钥或更新绑定" : "请输入龙虾密钥"
})}
<div className="settings-actions-row settings-actions-row-inline-save">
<button
type="button"
className="settings-action-button settings-action-button-primary settings-inline-save-button"
disabled={saving || !hasPendingLobsterKey}
onClick={() => void onSaveLobsterKey()}
>
{saving ? "保存中" : "保存"}
</button>
</div>
</div>
<div className="settings-card-actions">
<button
type="button"
className="settings-action-button settings-action-button-primary"
disabled={saving || !hasPendingLobsterKey}
onClick={() => void onSaveLobsterKey()}
>
{saving ? "保存中" : "保存"}
</button>
</div>
</div>
<div className="settings-basic-config-row settings-basic-config-row-directory">
<div className="settings-input-label settings-directory-label settings-basic-config-field">
{/* Card: 工作目录 */}
<div className="settings-config-card">
<div className="settings-card-body">
<div className="settings-input-label">
<span className="settings-input-label-text">工作目录</span>
<div className="workspace-directory-card settings-basic-directory-card">
<div className="workspace-directory-panel settings-basic-directory-panel settings-readonly-field">
<strong className="workspace-directory-path" title={displayedWorkspacePath}>{displayedWorkspacePath}</strong>
</div>
{hasPendingWorkspacePathChange ? (
<div className="workspace-directory-hint">当前目录已修改。</div>
) : (
<div className="workspace-directory-hint">导出诊断不会参与保存状态。</div>
)}
<div className="workspace-directory-panel settings-readonly-field">
<strong className="workspace-directory-path" title={displayedWorkspacePath}>{displayedWorkspacePath}</strong>
</div>
{hasPendingWorkspacePathChange ? (
<div className="workspace-directory-hint">当前目录已修改。</div>
) : null}
</div>
<div className="button-row settings-actions-row workspace-directory-actions settings-basic-directory-actions">
<button
type="button"
className="settings-action-button settings-action-button-secondary"
disabled={saving || !config}
onClick={() => void pickWorkspaceDirectory()}
>
更改目录
</button>
</div>
<div className="settings-card-actions">
{hasPendingWorkspacePathChange ? (
<>
<button
type="button"
className="settings-action-button settings-action-button-primary"
disabled={saving}
onClick={() => void onSaveWorkspaceDirectory()}
>
保存目录
</button>
<button
type="button"
className="settings-action-button settings-action-button-secondary"
disabled={saving}
onClick={() => void onResetWorkspaceDirectory()}
>
恢复当前
</button>
</>
) : null}
<button
type="button"
className="settings-action-button settings-action-button-secondary"
disabled={saving || !config}
onClick={() => void pickWorkspaceDirectory()}
>
更改目录
</button>
<button
type="button"
className="settings-action-button settings-action-button-secondary"
disabled={saving}
onClick={() => void exportDiagnostics()}
>
{labels.export}
</button>
</div>
</div>
{/* Card: 飞书移动端配置 */}
<div className="settings-config-card">
<div className="settings-card-body">
<div className="settings-section-title-row">
<h3 className="settings-section-title">飞书移动端配置</h3>
<button
type="button"
className="settings-action-button settings-action-button-secondary"
disabled={saving}
onClick={() => void exportDiagnostics()}
className="settings-manual-link"
onClick={() => setFeishuMobileManualOpen(true)}
>
{labels.export}
<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17h-2v-2h2v2zm2.07-7.75l-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25z" fill="currentColor"/>
</svg>
操作手册
</button>
</div>
<div className="settings-field-grid settings-field-grid-feishu-mobile">
{renderSecretInput({
secretId: "feishuMobileAppId",
configured: Boolean(config?.feishuMobileConfig?.appIdConfigured),
label: "FEISHU_APP_ID",
value: drafts.feishuMobileAppId,
setValue: setters.setFeishuMobileAppId,
placeholder: config?.feishuMobileConfig?.appIdConfigured ? "留空则保持当前已保存密钥" : "请输入 App ID"
})}
{renderSecretInput({
secretId: "feishuMobileAppSecret",
configured: Boolean(config?.feishuMobileConfig?.appSecretConfigured),
label: "FEISHU_APP_SECRET",
value: drafts.feishuMobileAppSecret,
setValue: setters.setFeishuMobileAppSecret,
placeholder: config?.feishuMobileConfig?.appSecretConfigured ? "留空则保持当前已保存密钥" : "请输入 App Secret"
})}
</div>
</div>
<div className="settings-card-actions">
{renderActions({
hasPending: hasPendingFeishuMobileConfig,
onReset: () => void onResetFeishuMobileConfig(),
onSave: () => void onSaveFeishuMobileConfig()
})}
</div>
</div>
</div>
</section>
) : null}
......@@ -794,5 +908,23 @@ export function SettingsPanels({
</section>
) : null}
</div>
<Modal
open={feishuMobileManualOpen}
onClose={() => setFeishuMobileManualOpen(false)}
title="操作手册 — 飞书移动端配置"
>
<p><strong>前置条件(用户操作)</strong></p>
<p>在飞书开放平台 (open.feishu.cn) 需要:</p>
<ol>
<li>创建企业自建应用(非应用商店应用/ISV 应用)</li>
<li>开启"机器人"能力</li>
<li>添加权限:im:message.p2p_msg:readonly(单聊)、im:message.group_at_msg:readonly(群聊@机器人)、im:message:send_as_bot(发送消息)</li>
<li>事件配置中添加订阅事件 im.message.receive_v1(接收消息 v2.0)</li>
<li>发布并审批应用</li>
</ol>
<p className="modal-manual-footer">以上如有疑问请联系千匠问天团队支持</p>
</Modal>
</>
)
}
......@@ -8,6 +8,7 @@ export interface PendingSettingsFlags {
hasPendingVideoConfig: boolean
hasPendingDigitalHumanConfig: boolean
hasPendingDouyinRuntimeConfig: boolean
hasPendingFeishuMobileConfig: boolean
}
export interface BasicResetSettingsDrafts {
......@@ -59,6 +60,11 @@ export interface XhsFeishuResetSettingsDrafts {
xhsFeishuTableId: string
}
export interface FeishuMobileResetSettingsDrafts {
feishuMobileAppId: string
feishuMobileAppSecret: string
}
export function getHasPendingSettingsChange(flags: PendingSettingsFlags) {
return flags.hasPendingBasicConfig
|| flags.hasPendingXhsFeishuConfig
......@@ -67,6 +73,7 @@ export function getHasPendingSettingsChange(flags: PendingSettingsFlags) {
|| flags.hasPendingVideoConfig
|| flags.hasPendingDigitalHumanConfig
|| flags.hasPendingDouyinRuntimeConfig
|| flags.hasPendingFeishuMobileConfig
}
export function getResetBasicSettingsDrafts(config: AppConfig): BasicResetSettingsDrafts {
......@@ -131,3 +138,10 @@ export function getResetXhsFeishuSettingsDrafts(): XhsFeishuResetSettingsDrafts
xhsFeishuTableId: ""
}
}
export function getResetFeishuMobileSettingsDrafts(): FeishuMobileResetSettingsDrafts {
return {
feishuMobileAppId: "",
feishuMobileAppSecret: ""
}
}
......@@ -34,6 +34,8 @@ interface UseSaveSettingsOptions {
xhsFeishuAppSecret: string
xhsFeishuAppToken: string
xhsFeishuTableId: string
feishuMobileAppId: string
feishuMobileAppSecret: string
}
setters: {
setConfig(config: AppConfig): void
......@@ -68,6 +70,8 @@ interface UseSaveSettingsOptions {
setXhsFeishuAppSecretDraft(value: string): void
setXhsFeishuAppTokenDraft(value: string): void
setXhsFeishuTableIdDraft(value: string): void
setFeishuMobileAppIdDraft(value: string): void
setFeishuMobileAppSecretDraft(value: string): void
}
labels: {
saveSuccessPending: string
......@@ -94,6 +98,7 @@ export function useSaveSettings({
expertModelConfig?: SaveConfigInput["expertModelConfig"]
douyinRuntimeConfig?: SaveConfigInput["douyinRuntimeConfig"]
xhsFeishuConfig?: SaveConfigInput["xhsFeishuConfig"]
feishuMobileConfig?: SaveConfigInput["feishuMobileConfig"]
successMessage?: string
resetDrafts?: (savedConfig: AppConfig) => void
}) => {
......@@ -126,6 +131,9 @@ export function useSaveSettings({
if (options?.xhsFeishuConfig) {
input.xhsFeishuConfig = options.xhsFeishuConfig
}
if (options?.feishuMobileConfig) {
input.feishuMobileConfig = options.feishuMobileConfig
}
setters.setSaving(true)
setters.setErrorText("")
......@@ -211,6 +219,19 @@ export function useSaveSettings({
})
}, [drafts.xhsFeishuAppId, drafts.xhsFeishuAppSecret, drafts.xhsFeishuAppToken, drafts.xhsFeishuTableId, saveConfig, setters])
const saveFeishuMobileConfig = useCallback(async () => {
return await saveConfig({
feishuMobileConfig: {
appId: drafts.feishuMobileAppId.trim() || undefined,
appSecret: drafts.feishuMobileAppSecret.trim() || undefined
},
resetDrafts: () => {
setters.setFeishuMobileAppIdDraft("")
setters.setFeishuMobileAppSecretDraft("")
}
})
}, [drafts.feishuMobileAppId, drafts.feishuMobileAppSecret, saveConfig, setters])
const saveCopywritingConfig = useCallback(async () => {
return await saveConfig({
expertModelConfig: {
......@@ -332,6 +353,7 @@ export function useSaveSettings({
saveWorkspaceDirectory,
restoreWorkspaceDirectory,
saveXhsFeishuConfig,
saveFeishuMobileConfig,
saveCopywritingConfig,
saveImageConfig,
saveVideoConfig,
......
......@@ -40,6 +40,8 @@ export function useSettingsState(config: AppConfig | null) {
const [xhsFeishuAppSecretDraft, setXhsFeishuAppSecretDraft] = useState("");
const [xhsFeishuAppTokenDraft, setXhsFeishuAppTokenDraft] = useState("");
const [xhsFeishuTableIdDraft, setXhsFeishuTableIdDraft] = useState("");
const [feishuMobileAppIdDraft, setFeishuMobileAppIdDraft] = useState("");
const [feishuMobileAppSecretDraft, setFeishuMobileAppSecretDraft] = useState("");
const [saving, setSaving] = useState(false);
const hasPendingLobsterKey = lobsterKeyDraft.trim().length > 0;
......@@ -49,6 +51,10 @@ export function useSettingsState(config: AppConfig | null) {
xhsFeishuAppTokenDraft.trim() ||
xhsFeishuTableIdDraft.trim()
);
const hasPendingFeishuMobileConfig = Boolean(
feishuMobileAppIdDraft.trim() ||
feishuMobileAppSecretDraft.trim()
);
const hasPendingModelKeys = Boolean(
imageModelApiKeyDraft.trim()
|| imageModelBaseUrlDraft.trim() !== (config?.expertModelConfig.image.baseUrl ?? "").trim()
......@@ -131,10 +137,15 @@ export function useSettingsState(config: AppConfig | null) {
setXhsFeishuAppTokenDraft,
xhsFeishuTableIdDraft,
setXhsFeishuTableIdDraft,
feishuMobileAppIdDraft,
setFeishuMobileAppIdDraft,
feishuMobileAppSecretDraft,
setFeishuMobileAppSecretDraft,
saving,
setSaving,
hasPendingLobsterKey,
hasPendingXhsFeishuConfig,
hasPendingFeishuMobileConfig,
hasPendingModelKeys
};
}
......@@ -1520,3 +1520,128 @@ select:focus-visible {
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
/* Modal */
.modal-overlay {
position: fixed;
inset: 0;
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.28);
animation: modal-overlay-in 0.15s ease-out;
}
@keyframes modal-overlay-in {
from { opacity: 0; }
to { opacity: 1; }
}
.modal-panel {
background: #ffffff;
border: 1px solid #e8ecf1;
border-radius: 12px;
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.14);
max-width: 560px;
width: calc(100vw - 48px);
max-height: calc(100vh - 80px);
display: flex;
flex-direction: column;
animation: modal-panel-in 0.18s ease-out;
}
@keyframes modal-panel-in {
from { opacity: 0; transform: translateY(8px) scale(0.98); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18px 22px 0;
}
.modal-title {
font-size: 15px;
font-weight: 600;
color: var(--color-text-primary, #1a1a2e);
margin: 0;
}
.modal-close-button {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 6px;
border: none;
background: transparent;
color: #94a3b8;
cursor: pointer;
flex-shrink: 0;
}
.modal-close-button:hover {
background: rgba(0, 0, 0, 0.06);
color: #1e293b;
}
.modal-body {
padding: 16px 22px 22px;
font-size: 13px;
line-height: 1.7;
color: var(--color-text-secondary, #62759a);
overflow-y: auto;
}
.modal-body ol {
margin: 8px 0 0;
padding-left: 18px;
}
.modal-body li {
margin-bottom: 4px;
}
.modal-body .modal-manual-footer {
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid var(--color-border, rgba(0, 0, 0, 0.08));
color: var(--color-text-tertiary, #94a3b8);
font-size: 12px;
}
.settings-manual-link {
display: inline-flex;
align-items: center;
gap: 4px;
background: none;
border: none;
color: var(--color-accent, #3b82f6);
font-size: 12px;
cursor: pointer;
padding: 2px 6px;
border-radius: 6px;
}
.settings-manual-link:hover {
background: rgba(59, 130, 246, 0.08);
text-decoration: underline;
}
.settings-section-title-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.settings-section-title {
font-size: 13px;
font-weight: 600;
color: var(--color-text-primary, #1a1a2e);
margin: 0;
}
......@@ -9,16 +9,17 @@
}
.settings-page-shell {
--settings-control-height: 44px;
--settings-control-radius: 16px;
--settings-card-radius: 24px;
--settings-control-padding-x: 14px;
--settings-control-border: rgba(167, 183, 224, 0.88);
--settings-control-border-strong: rgba(129, 150, 205, 0.92);
--settings-control-background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(245, 248, 255, 0.92) 100%);
--settings-focus-outline: rgba(59, 130, 246, 0.16);
--settings-focus-shadow: 0 0 0 4px rgba(96, 165, 250, 0.14);
--settings-secondary-shadow: 0 10px 22px rgba(35, 52, 82, 0.08);
--settings-control-height: 36px;
--settings-control-radius: 6px;
--settings-card-radius: 10px;
--settings-control-padding-x: 12px;
--settings-control-border: #d4d9e2;
--settings-control-border-strong: #b0b8c8;
--settings-control-background: #f8fafc;
--settings-focus-outline: rgba(79, 110, 247, 0.22);
--settings-focus-shadow: 0 0 0 3px rgba(79, 110, 247, 0.1);
--settings-button-height: 32px;
--settings-button-radius: 6px;
}
.settings-console-grid {
......@@ -133,23 +134,19 @@
.settings-runtime-hint {
flex: 0 0 auto;
padding: 8px 12px;
border-radius: 16px;
border: 1px solid rgba(173, 192, 245, 0.75);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.8), rgba(245, 248, 255, 0.72));
backdrop-filter: blur(14px);
box-shadow: 0 14px 28px rgba(109, 124, 255, 0.08);
border-radius: 6px;
border: 1px solid #e8ecf1;
background: #f8fafc;
}
.settings-panel {
flex: 1 1 auto;
min-height: 0;
padding: 8px;
border-radius: 24px;
border-radius: 10px;
overflow: hidden;
border: 1px solid rgba(157, 180, 255, 0.45);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.72), rgba(255, 255, 255, 0.5));
box-shadow: 0 10px 22px rgba(109, 124, 255, 0.06), inset 0 1px 0 rgba(255, 255, 255, 0.88);
backdrop-filter: blur(16px);
border: 1px solid #e8ecf1;
background: #ffffff;
}
.settings-panel-body {
......@@ -159,21 +156,15 @@
}
.settings-panel-modern {
background:
radial-gradient(circle at top right, rgba(94, 203, 255, 0.18), transparent 34%),
linear-gradient(180deg, rgba(255, 255, 255, 0.8), rgba(248, 250, 255, 0.62));
background: #ffffff;
}
.settings-panel-secondary {
background:
radial-gradient(circle at top left, rgba(139, 125, 255, 0.18), transparent 32%),
linear-gradient(180deg, rgba(255, 255, 255, 0.8), rgba(248, 246, 255, 0.66));
background: #ffffff;
}
.settings-panel-models {
background:
radial-gradient(circle at top center, rgba(109, 124, 255, 0.18), transparent 36%),
linear-gradient(180deg, rgba(255, 255, 255, 0.82), rgba(247, 249, 255, 0.66));
background: #ffffff;
}
.settings-section-card {
......@@ -244,13 +235,12 @@
min-height: 24px;
padding: 0 10px;
border-radius: 999px;
background: linear-gradient(135deg, rgba(109, 124, 255, 0.14), rgba(94, 203, 255, 0.16));
color: #5166d6;
background: #eef2ff;
color: #4f6ef7;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
box-shadow: inset 0 0 0 1px rgba(125, 143, 255, 0.16);
}
.settings-field-grid {
......@@ -274,55 +264,6 @@
display: grid;
}
.settings-basic-config-card {
grid-template-rows: minmax(0, 1fr);
gap: 8px;
}
.settings-basic-config-form {
min-height: 0;
display: grid;
align-content: start;
gap: 14px;
}
.settings-basic-config-row {
min-width: 0;
display: grid;
grid-template-columns: minmax(0, 560px) auto;
align-items: end;
gap: 12px;
}
.settings-basic-config-row-key {
grid-template-columns: minmax(0, 560px) auto;
}
.settings-basic-config-row-directory {
align-items: end;
}
.settings-basic-config-field {
width: 100%;
}
.settings-basic-directory-card {
gap: 0;
}
.settings-basic-directory-actions {
align-self: end;
justify-self: start;
flex-direction: column;
align-items: stretch;
}
.settings-inline-save-button,
.settings-basic-directory-actions .settings-action-button {
width: 96px;
min-width: 96px;
}
.settings-input-label {
min-width: 0;
gap: 6px;
......@@ -346,7 +287,6 @@
border: 1px solid var(--settings-control-border);
border-radius: var(--settings-control-radius);
background: var(--settings-control-background);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.92);
color: #163152;
font-size: 13px;
line-height: 1.35;
......@@ -389,14 +329,15 @@
.settings-secret-reveal-button {
position: absolute;
top: 50%;
top: 0;
bottom: 0;
right: 4px;
margin: auto 0;
width: calc(var(--settings-control-height) - 8px);
height: calc(var(--settings-control-height) - 8px);
display: inline-flex;
align-items: center;
justify-content: center;
transform: translateY(-50%);
border: 0;
border-radius: 6px;
padding: 0;
......@@ -407,34 +348,14 @@
cursor: pointer;
}
.shell.openclaw-theme .settings-secret-reveal-button {
transform: translateY(-50%);
box-shadow: none;
}
.settings-secret-reveal-button:hover:not(:disabled) {
color: #1f3f6d;
background: rgba(31, 63, 109, 0.08);
transform: translateY(-50%);
box-shadow: none;
}
.shell.openclaw-theme .settings-secret-reveal-button:hover:not(:disabled) {
transform: translateY(-50%);
box-shadow: none;
}
.settings-secret-reveal-button:focus-visible,
.settings-secret-reveal-button:active {
outline: none;
transform: translateY(-50%);
box-shadow: none;
}
.shell.openclaw-theme .settings-secret-reveal-button:focus-visible,
.shell.openclaw-theme .settings-secret-reveal-button:active {
transform: translateY(-50%);
box-shadow: none;
}
.settings-secret-reveal-button:disabled {
......@@ -469,7 +390,6 @@
color: #405679;
font-size: 13px;
line-height: var(--settings-control-height);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9);
}
.settings-readonly-config-text {
......@@ -531,7 +451,7 @@
grid-auto-rows: minmax(214px, auto);
align-content: start;
scrollbar-width: thin;
scrollbar-color: rgba(125, 143, 255, 0.34) transparent;
scrollbar-color: rgba(148, 163, 184, 0.46) transparent;
}
.model-config-grid-single {
......@@ -551,12 +471,12 @@
.model-config-grid-four::-webkit-scrollbar-thumb {
border-radius: 999px;
background: rgba(125, 143, 255, 0.28);
background: rgba(148, 163, 184, 0.46);
border: 1px solid rgba(255, 255, 255, 0.22);
}
.model-config-grid-four::-webkit-scrollbar-thumb:hover {
background: rgba(125, 143, 255, 0.44);
background: rgba(100, 116, 139, 0.62);
}
.model-config-card {
......@@ -570,7 +490,7 @@
background: linear-gradient(180deg, rgba(255, 255, 255, 0.94), rgba(247, 249, 255, 0.84));
box-shadow: 0 10px 22px rgba(109, 124, 255, 0.08);
overflow: hidden;
transition: transform 180ms ease, border-color 180ms ease, box-shadow 180ms ease;
transition: border-color 180ms ease, box-shadow 180ms ease;
}
.settings-actions-row {
......@@ -583,33 +503,28 @@
margin-top: 2px;
}
.settings-actions-row-inline-save {
justify-self: start;
}
.settings-action-button {
min-width: 92px;
min-height: var(--settings-control-height);
height: var(--settings-control-height);
padding: 0 16px;
border-radius: var(--settings-control-radius);
min-width: 72px;
min-height: var(--settings-button-height);
height: var(--settings-button-height);
padding: 0 14px;
border-radius: var(--settings-button-radius);
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid rgba(179, 194, 228, 0.9);
background: rgba(255, 255, 255, 0.94);
color: #2b426d;
font-size: 13px;
font-weight: 700;
border: 1px solid #e2e8f0;
background: #f1f5f9;
color: #475569;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
box-shadow: var(--settings-secondary-shadow);
transition: background 160ms ease, border-color 160ms ease, box-shadow 160ms ease, color 160ms ease, transform 160ms ease;
box-shadow: none;
transition: background 0.15s, border-color 0.15s, box-shadow 0.15s;
}
.settings-action-button:hover:not(:disabled) {
border-color: rgba(150, 171, 223, 0.96);
background: rgba(255, 255, 255, 0.98);
box-shadow: 0 12px 24px rgba(35, 52, 82, 0.1);
background: #e2e8f0;
border-color: #cbd5e1;
}
.settings-action-button:focus-visible {
......@@ -620,20 +535,20 @@
.settings-action-button:disabled {
cursor: not-allowed;
opacity: 0.56;
opacity: 0.5;
box-shadow: none;
}
.settings-action-button-primary {
border-color: transparent;
background: linear-gradient(135deg, #3b82f6, #1e40af);
background: #4f6ef7;
color: #ffffff;
box-shadow: 0 12px 24px rgba(37, 99, 235, 0.24);
box-shadow: none;
}
.settings-action-button-primary:hover:not(:disabled) {
background: linear-gradient(135deg, #4c8dff, #2148bd);
box-shadow: 0 14px 28px rgba(37, 99, 235, 0.28);
background: #3d5bd9;
border-color: transparent;
}
.settings-action-button-primary:focus-visible {
......@@ -641,12 +556,17 @@
}
.settings-action-button-secondary {
background: rgba(255, 255, 255, 0.94);
color: #2b426d;
border-color: #e2e8f0;
background: #f1f5f9;
color: #475569;
}
.settings-action-button-secondary:hover:not(:disabled) {
background: #e2e8f0;
border-color: #cbd5e1;
}
.model-config-card:hover {
transform: translateY(-1px);
border-color: rgba(109, 124, 255, 0.36);
box-shadow: 0 14px 26px rgba(109, 124, 255, 0.12);
}
......@@ -660,7 +580,6 @@
}
.settings-panel-models .model-config-grid-single .model-config-card:hover {
transform: none;
border-color: transparent !important;
box-shadow: none !important;
}
......@@ -741,16 +660,6 @@
margin-top: 0;
}
.settings-page-shell .settings-basic-directory-actions .settings-action-button {
width: 100%;
min-width: 96px;
}
.settings-panel .workspace-directory-card {
min-height: 0;
gap: 8px;
}
.settings-panel .workspace-directory-panel {
min-height: 0;
height: 100%;
......@@ -758,7 +667,6 @@
border-radius: var(--settings-control-radius);
border: 1px solid var(--settings-control-border);
background: var(--settings-control-background);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9), 0 8px 18px rgba(109, 124, 255, 0.06);
}
.settings-panel-connection .settings-section-card,
......@@ -766,14 +674,6 @@
grid-template-rows: auto minmax(0, 1fr) auto;
}
.settings-panel-basic-config .settings-section-card {
grid-template-rows: auto minmax(0, 1fr);
}
.settings-panel-basic-config .workspace-directory-card {
gap: 6px;
}
.settings-panel-basic-config .settings-input-label input,
.settings-panel-basic-config .workspace-directory-panel {
width: 100%;
......@@ -783,7 +683,6 @@
border-radius: var(--settings-control-radius);
border: 1px solid var(--settings-control-border);
background: var(--settings-control-background);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9), 0 8px 18px rgba(109, 124, 255, 0.06);
}
.settings-panel-basic-config .workspace-directory-panel {
......@@ -796,16 +695,6 @@
align-items: center;
}
.settings-basic-directory-panel .workspace-directory-path {
display: block;
min-width: 0;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
word-break: normal;
}
.workspace-directory-eyebrow {
font-size: 10px;
letter-spacing: 0.16em;
......@@ -825,12 +714,6 @@
line-height: 1.5;
}
.workspace-directory-draft-row,
.workspace-directory-inline-actions,
.workspace-directory-actions {
gap: 6px;
}
.model-config-grid-runtime .settings-runtime-actions-panel {
min-height: 0;
height: 100%;
......@@ -895,28 +778,6 @@
grid-template-columns: minmax(0, 1fr);
}
.settings-basic-config-row {
grid-template-columns: minmax(0, 1fr);
}
.settings-basic-config-form {
padding-top: 0;
}
.settings-basic-config-field {
width: 100%;
}
.settings-inline-save-button,
.settings-actions-row-inline-save,
.settings-basic-directory-actions {
justify-self: stretch;
}
.settings-basic-directory-actions {
width: 100%;
}
.model-config-grid-runtime .settings-runtime-actions-panel {
height: auto;
align-items: stretch;
......@@ -968,12 +829,66 @@
flex-direction: column;
}
.settings-actions-row .settings-action-button,
.workspace-directory-actions .settings-action-button {
.settings-actions-row .settings-action-button {
width: 100%;
}
}
.settings-basic-directory-actions .settings-action-button {
min-width: 0;
}
/* --- Bento config cards (basic tab) --- */
.settings-config-cards {
display: grid;
gap: 10px;
}
.settings-config-card {
border: 1px solid #e8ecf1;
border-radius: 10px;
background: #fff;
padding: 16px 18px;
display: grid;
gap: 10px;
}
.settings-card-body {
max-width: 560px;
}
.settings-card-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
/* --- Feishu Mobile config grid --- */
.settings-field-grid-feishu-mobile {
display: grid;
grid-template-columns: 1fr;
gap: 8px;
}
/* --- Secret reveal spinner --- */
.settings-secret-reveal-spinner {
display: inline-block;
width: 16px;
height: 16px;
border-radius: 50%;
border: 2px solid rgba(96, 114, 155, 0.24);
border-top-color: #60729b;
animation: settings-secret-spin 0.7s linear infinite;
}
@keyframes settings-secret-spin {
to { transform: rotate(360deg); }
}
/* --- Reveal error in toolbar --- */
.settings-reveal-error {
font-size: 12px;
color: #ef4444;
background: rgba(239, 68, 68, 0.08);
border-radius: 8px;
padding: 4px 10px;
margin-right: 8px;
white-space: nowrap;
}
......@@ -105,7 +105,9 @@ export type ConfigSecretId =
| "xhsFeishuAppId"
| "xhsFeishuAppSecret"
| "xhsFeishuAppToken"
| "xhsFeishuTableId";
| "xhsFeishuTableId"
| "feishuMobileAppId"
| "feishuMobileAppSecret";
export interface WorkspaceWarmupResult {
accepted: boolean;
......@@ -660,6 +662,11 @@ export interface XhsFeishuConfig {
tableIdConfigured: boolean;
}
export interface FeishuMobileConfig {
appIdConfigured: boolean;
appSecretConfigured: boolean;
}
export const FIXED_EXPERT_MODEL_ENDPOINTS = {
copywriting: {
baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1",
......@@ -717,6 +724,7 @@ export interface AppConfig {
expertModelConfig: ExpertModelConfig;
douyinRuntimeConfig: DouyinRuntimeConfig;
xhsFeishuConfig: XhsFeishuConfig;
feishuMobileConfig: FeishuMobileConfig;
}
export interface DiagnosticsExportResult {
......@@ -757,6 +765,11 @@ export interface XhsFeishuConfigInput {
tableId?: string;
}
export interface FeishuMobileConfigInput {
appId?: string;
appSecret?: string;
}
export interface SaveConfigInput {
setupMode: SetupMode;
provider: string;
......@@ -782,6 +795,7 @@ export interface SaveConfigInput {
vectcut?: VectCutModelInput;
};
xhsFeishuConfig?: XhsFeishuConfigInput;
feishuMobileConfig?: FeishuMobileConfigInput;
}
export interface AuthSessionSummary {
......
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