Commit f492464b authored by AI-甘富林's avatar AI-甘富林

feat(client): add douyin runtime settings and attachment support

parent c5b18b81
This diff is collapsed.
......@@ -2,8 +2,11 @@ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
import path from "node:path";
import {
FIXED_DIGITAL_HUMAN_CONFIG,
FIXED_DOUYIN_RUNTIME_CONFIG,
FIXED_EXPERT_MODEL_ENDPOINTS,
type AppConfig,
type DouyinRuntimeConfig,
type DouyinTextModelConfig,
type DigitalHumanModelConfig,
type ExpertModelConfig,
type ModelEndpointConfig,
......@@ -62,6 +65,15 @@ interface LegacyConfig {
copywriting?: Partial<ModelEndpointConfig>;
digitalHuman?: Partial<DigitalHumanModelConfig>;
};
douyinRuntimeConfig?: {
videoAnalyzer?: Partial<DouyinTextModelConfig>;
replicationBrief?: Partial<DouyinTextModelConfig>;
vectcut?: {
baseUrl?: string;
fileBaseUrl?: string;
apiKeyConfigured?: boolean;
};
};
}
function normalizeGatewayUrl(raw: string): string {
......@@ -173,6 +185,27 @@ function createDefaultExpertModelConfig(): ExpertModelConfig {
};
}
function createDefaultDouyinTextModelConfig(kind: "videoAnalyzer" | "replicationBrief"): DouyinTextModelConfig {
const config = FIXED_DOUYIN_RUNTIME_CONFIG[kind];
return {
baseUrl: config.baseUrl,
apiKeyConfigured: false,
modelId: config.modelId
};
}
function createDefaultDouyinRuntimeConfig(): DouyinRuntimeConfig {
return {
videoAnalyzer: createDefaultDouyinTextModelConfig("videoAnalyzer"),
replicationBrief: createDefaultDouyinTextModelConfig("replicationBrief"),
vectcut: {
baseUrl: FIXED_DOUYIN_RUNTIME_CONFIG.vectcut.baseUrl,
fileBaseUrl: FIXED_DOUYIN_RUNTIME_CONFIG.vectcut.fileBaseUrl,
apiKeyConfigured: false
}
};
}
function mergeExpertModelConfig(
current: ExpertModelConfig,
input?: SaveConfigInput["expertModelConfig"]
......@@ -203,6 +236,47 @@ function mergeExpertModelConfig(
};
}
function mergeDouyinRuntimeConfig(
current: DouyinRuntimeConfig,
input?: SaveConfigInput["douyinRuntimeConfig"]
): DouyinRuntimeConfig {
return {
videoAnalyzer: {
baseUrl: typeof input?.videoAnalyzer?.baseUrl === "string"
? input.videoAnalyzer.baseUrl.trim()
: current.videoAnalyzer.baseUrl,
apiKeyConfigured: typeof input?.videoAnalyzer?.apiKey === "string"
? Boolean(input.videoAnalyzer.apiKey.trim())
: current.videoAnalyzer.apiKeyConfigured,
modelId: typeof input?.videoAnalyzer?.modelId === "string"
? input.videoAnalyzer.modelId.trim()
: current.videoAnalyzer.modelId
},
replicationBrief: {
baseUrl: typeof input?.replicationBrief?.baseUrl === "string"
? input.replicationBrief.baseUrl.trim()
: current.replicationBrief.baseUrl,
apiKeyConfigured: typeof input?.replicationBrief?.apiKey === "string"
? Boolean(input.replicationBrief.apiKey.trim())
: current.replicationBrief.apiKeyConfigured,
modelId: typeof input?.replicationBrief?.modelId === "string"
? input.replicationBrief.modelId.trim()
: current.replicationBrief.modelId
},
vectcut: {
baseUrl: typeof input?.vectcut?.baseUrl === "string"
? input.vectcut.baseUrl.trim()
: current.vectcut.baseUrl,
fileBaseUrl: typeof input?.vectcut?.fileBaseUrl === "string"
? input.vectcut.fileBaseUrl.trim()
: current.vectcut.fileBaseUrl,
apiKeyConfigured: typeof input?.vectcut?.apiKey === "string"
? Boolean(input.vectcut.apiKey.trim())
: current.vectcut.apiKeyConfigured
}
};
}
export function getRuntimeCloudApiTarget(config: Pick<AppConfig, "runtimeCloudApiBaseUrl">): RuntimeCloudApiTarget {
return resolveRuntimeCloudApiTarget(config.runtimeCloudApiBaseUrl);
}
......@@ -235,7 +309,8 @@ export class AppConfigService {
cloudApiBaseUrl: normalizeCloudApiBaseUrl(input.cloudApiBaseUrl),
runtimeCloudApiBaseUrl: migrateDeprecatedRuntimeCloudApiBaseUrl(input.runtimeCloudApiBaseUrl),
runtimeMode: normalizeRuntimeMode(input.runtimeMode),
expertModelConfig: mergeExpertModelConfig(current.expertModelConfig, input.expertModelConfig)
expertModelConfig: mergeExpertModelConfig(current.expertModelConfig, input.expertModelConfig),
douyinRuntimeConfig: mergeDouyinRuntimeConfig(current.douyinRuntimeConfig, input.douyinRuntimeConfig)
};
await this.writeConfig(config);
......@@ -243,6 +318,12 @@ export class AppConfigService {
});
}
async persist(config: AppConfig): Promise<void> {
return this.runExclusive(async () => {
await this.writeConfig(config);
});
}
getDataPath(...segments: string[]): string {
return path.join(this.userDataPath, ...segments);
}
......@@ -265,12 +346,14 @@ export class AppConfigService {
cloudApiBaseUrl: normalizeCloudApiBaseUrl(process.env.QJCLAW_CLOUD_API_BASE_URL ?? ""),
runtimeCloudApiBaseUrl: "",
runtimeMode: normalizeRuntimeMode(process.env.QJCLAW_RUNTIME_MODE),
expertModelConfig: createDefaultExpertModelConfig()
expertModelConfig: createDefaultExpertModelConfig(),
douyinRuntimeConfig: createDefaultDouyinRuntimeConfig()
};
}
private normalizeConfig(config: LegacyConfig): AppConfig {
const defaultExpertModelConfig = createDefaultExpertModelConfig();
const defaultDouyinRuntimeConfig = createDefaultDouyinRuntimeConfig();
return {
setupMode: normalizeSetupMode(config.setupMode),
provider: config.provider ?? "openai",
......@@ -307,6 +390,31 @@ export class AppConfigService {
qiniuAccessKeyConfigured: Boolean(config.expertModelConfig?.digitalHuman?.qiniuAccessKeyConfigured),
qiniuSecretKeyConfigured: Boolean(config.expertModelConfig?.digitalHuman?.qiniuSecretKeyConfigured)
}
},
douyinRuntimeConfig: {
videoAnalyzer: {
baseUrl: typeof config.douyinRuntimeConfig?.videoAnalyzer?.baseUrl === "string"
? config.douyinRuntimeConfig.videoAnalyzer.baseUrl
: defaultDouyinRuntimeConfig.videoAnalyzer.baseUrl,
apiKeyConfigured: Boolean(config.douyinRuntimeConfig?.videoAnalyzer?.apiKeyConfigured),
modelId: config.douyinRuntimeConfig?.videoAnalyzer?.modelId ?? defaultDouyinRuntimeConfig.videoAnalyzer.modelId
},
replicationBrief: {
baseUrl: typeof config.douyinRuntimeConfig?.replicationBrief?.baseUrl === "string"
? config.douyinRuntimeConfig.replicationBrief.baseUrl
: defaultDouyinRuntimeConfig.replicationBrief.baseUrl,
apiKeyConfigured: Boolean(config.douyinRuntimeConfig?.replicationBrief?.apiKeyConfigured),
modelId: config.douyinRuntimeConfig?.replicationBrief?.modelId ?? defaultDouyinRuntimeConfig.replicationBrief.modelId
},
vectcut: {
baseUrl: typeof config.douyinRuntimeConfig?.vectcut?.baseUrl === "string"
? config.douyinRuntimeConfig.vectcut.baseUrl
: defaultDouyinRuntimeConfig.vectcut.baseUrl,
fileBaseUrl: typeof config.douyinRuntimeConfig?.vectcut?.fileBaseUrl === "string"
? config.douyinRuntimeConfig.vectcut.fileBaseUrl
: defaultDouyinRuntimeConfig.vectcut.fileBaseUrl,
apiKeyConfigured: Boolean(config.douyinRuntimeConfig?.vectcut?.apiKeyConfigured)
}
}
};
}
......
......@@ -14,6 +14,9 @@ interface SecretRecord {
digitalHumanVolcSecretKey?: string;
digitalHumanQiniuAccessKey?: string;
digitalHumanQiniuSecretKey?: string;
videoAnalyzerApiKey?: string;
replicationBriefApiKey?: string;
vectCutApiKey?: string;
}
interface SecretAccessor {
......@@ -32,7 +35,10 @@ type SecretName =
| "digitalHumanVolcAccessKey"
| "digitalHumanVolcSecretKey"
| "digitalHumanQiniuAccessKey"
| "digitalHumanQiniuSecretKey";
| "digitalHumanQiniuSecretKey"
| "videoAnalyzerApiKey"
| "replicationBriefApiKey"
| "vectCutApiKey";
type KeytarModule = typeof import("keytar");
const KEYTAR_SERVICE = "QianjiangClaw";
......@@ -48,7 +54,10 @@ const KEYTAR_ACCOUNT_MAP: Record<SecretName, string> = {
digitalHumanVolcAccessKey: "digital-human-volc-access-key",
digitalHumanVolcSecretKey: "digital-human-volc-secret-key",
digitalHumanQiniuAccessKey: "digital-human-qiniu-access-key",
digitalHumanQiniuSecretKey: "digital-human-qiniu-secret-key"
digitalHumanQiniuSecretKey: "digital-human-qiniu-secret-key",
videoAnalyzerApiKey: "douyin-video-analyzer-api-key",
replicationBriefApiKey: "douyin-replication-brief-api-key",
vectCutApiKey: "douyin-vectcut-api-key"
};
class FileSecretStore implements SecretAccessor {
......@@ -244,6 +253,30 @@ export class SecretManager {
return this.store.get("digitalHumanQiniuSecretKey");
}
async setVideoAnalyzerApiKey(value?: string): Promise<void> {
await this.store.set("videoAnalyzerApiKey", value);
}
async getVideoAnalyzerApiKey(): Promise<string | undefined> {
return this.store.get("videoAnalyzerApiKey");
}
async setReplicationBriefApiKey(value?: string): Promise<void> {
await this.store.set("replicationBriefApiKey", value);
}
async getReplicationBriefApiKey(): Promise<string | undefined> {
return this.store.get("replicationBriefApiKey");
}
async setVectCutApiKey(value?: string): Promise<void> {
await this.store.set("vectCutApiKey", value);
}
async getVectCutApiKey(): Promise<string | undefined> {
return this.store.get("vectCutApiKey");
}
private async tryLoadKeytar(): Promise<KeytarModule | null> {
try {
const imported = await import("keytar");
......@@ -265,7 +298,10 @@ export class SecretManager {
"digitalHumanVolcAccessKey",
"digitalHumanVolcSecretKey",
"digitalHumanQiniuAccessKey",
"digitalHumanQiniuSecretKey"
"digitalHumanQiniuSecretKey",
"videoAnalyzerApiKey",
"replicationBriefApiKey",
"vectCutApiKey"
] as const) {
const existing = await this.store.get(secretName);
if (existing) {
......
......@@ -85,6 +85,7 @@ const desktopApi: DesktopApi = {
createSessionForProject: (projectId: string, title?: string) => ipcRenderer.invoke(IPC_CHANNELS.chatCreateSessionForProject, projectId, title),
closeSession: (sessionId: string) => ipcRenderer.invoke(IPC_CHANNELS.chatCloseSession, sessionId),
listMessages: (sessionId: string) => ipcRenderer.invoke(IPC_CHANNELS.chatListMessages, sessionId),
pickAttachments: () => ipcRenderer.invoke(IPC_CHANNELS.chatPickAttachments),
pickImageAttachment: () => ipcRenderer.invoke(IPC_CHANNELS.chatPickImageAttachment),
sendPrompt: (sessionId: string, prompt: string, skillId?: string, attachments?: ChatAttachment[]) => ipcRenderer.invoke(IPC_CHANNELS.chatSendPrompt, sessionId, prompt, skillId, attachments),
streamPrompt: (sessionId: string, prompt: string, skillId?: string, attachments?: ChatAttachment[]) => ipcRenderer.invoke(IPC_CHANNELS.chatStreamPrompt, sessionId, prompt, skillId, attachments),
......
This diff is collapsed.
......@@ -3945,40 +3945,12 @@ button.secondary {
position: relative;
min-height: 0;
height: 100%;
padding: 8px;
padding: 0;
overflow: hidden;
border-radius: 30px;
border: 1px solid rgba(190, 205, 255, 0.88);
background:
radial-gradient(circle at 12% 12%, rgba(94, 203, 255, 0.22), transparent 28%),
radial-gradient(circle at 88% 10%, rgba(139, 125, 255, 0.18), transparent 24%),
linear-gradient(145deg, #f7f9ff 0%, #eef4ff 52%, #f3efff 100%);
box-shadow: 0 26px 56px rgba(109, 124, 255, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.88);
}
.settings-page-shell::before,
.settings-page-shell::after {
content: "";
position: absolute;
border-radius: 999px;
pointer-events: none;
filter: blur(12px);
}
.settings-page-shell::before {
width: 240px;
height: 240px;
top: -88px;
right: 11%;
background: rgba(109, 124, 255, 0.16);
}
.settings-page-shell::after {
width: 220px;
height: 220px;
left: -76px;
bottom: -84px;
background: rgba(94, 203, 255, 0.16);
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
}
.settings-page-shell > * {
......@@ -4137,7 +4109,7 @@ button.secondary {
display: grid;
gap: 10px;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-rows: minmax(164px, 0.72fr) minmax(0, 1.28fr);
grid-template-rows: minmax(164px, 0.43fr) minmax(0, 1.57fr);
grid-template-areas:
"connection diagnostics"
"models models";
......@@ -4173,7 +4145,7 @@ button.secondary {
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 14px 30px rgba(109, 124, 255, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.9);
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);
}
......@@ -4200,11 +4172,11 @@ button.secondary {
min-height: 0;
height: 100%;
gap: 10px;
padding: 14px;
border-radius: 20px;
border: 1px solid rgba(175, 196, 255, 0.55);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.9), rgba(247, 249, 255, 0.76));
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.92), 0 8px 22px rgba(94, 125, 255, 0.06);
padding: 10px;
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
}
.settings-section-card-compact {
......@@ -4274,6 +4246,24 @@ button.secondary {
grid-template-columns: 1fr;
}
.settings-inline-key-row {
display: grid;
grid-template-columns: minmax(0, 520px) auto;
align-items: end;
gap: 10px;
}
.settings-inline-key-row .settings-input-label {
display: grid;
}
.settings-inline-save-button {
min-width: 88px;
padding-inline: 16px;
justify-self: start;
white-space: nowrap;
}
.settings-input-label {
min-width: 0;
gap: 5px;
......@@ -4315,12 +4305,34 @@ button.secondary {
}
.model-config-grid-four {
min-height: 0;
height: 100%;
overflow: auto;
padding-right: 2px;
grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.08fr);
grid-template-rows: repeat(3, minmax(0, 1fr));
align-content: stretch;
overflow-y: auto;
overflow-x: hidden;
padding-right: 8px;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-auto-rows: minmax(188px, auto);
align-content: start;
scrollbar-width: thin;
scrollbar-color: rgba(125, 143, 255, 0.34) transparent;
}
.model-config-grid-four::-webkit-scrollbar {
width: 8px;
}
.model-config-grid-four::-webkit-scrollbar-track {
background: transparent;
}
.model-config-grid-four::-webkit-scrollbar-thumb {
border-radius: 999px;
background: rgba(125, 143, 255, 0.28);
border: 1px solid rgba(255, 255, 255, 0.22);
}
.model-config-grid-four::-webkit-scrollbar-thumb:hover {
background: rgba(125, 143, 255, 0.44);
}
.model-config-card {
......@@ -4382,26 +4394,6 @@ button.secondary {
align-content: start;
}
.model-config-card-copywriting {
grid-column: 1;
grid-row: 1;
}
.model-config-card-image {
grid-column: 1;
grid-row: 2;
}
.model-config-card-video {
grid-column: 1;
grid-row: 3;
}
.model-config-card-digital-human {
grid-column: 2;
grid-row: 1 / span 3;
}
.model-config-card-body-digital-human {
overflow: visible;
padding-right: 0;
......@@ -4510,27 +4502,7 @@ button.secondary {
@media (max-width: 1180px) {
.model-config-grid-four {
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-rows: auto auto auto;
}
.model-config-card-copywriting {
grid-column: 1;
grid-row: 1;
}
.model-config-card-image {
grid-column: 2;
grid-row: 1;
}
.model-config-card-video {
grid-column: 1 / -1;
grid-row: 2;
}
.model-config-card-digital-human {
grid-column: 1 / -1;
grid-row: 3;
grid-auto-rows: minmax(188px, auto);
}
}
......@@ -4572,15 +4544,11 @@ button.secondary {
.model-config-grid,
.model-config-grid-four {
grid-template-columns: 1fr;
grid-template-rows: none;
grid-auto-rows: minmax(0, auto);
}
.model-config-card-copywriting,
.model-config-card-image,
.model-config-card-video,
.model-config-card-digital-human {
grid-column: auto;
grid-row: auto;
.settings-inline-key-row {
grid-template-columns: minmax(0, 1fr);
}
.settings-field-grid-digital-human {
......
......@@ -32,6 +32,7 @@
chatCreateSessionForProject: "chat:create-session-for-project",
chatCloseSession: "chat:close-session",
chatListMessages: "chat:list-messages",
chatPickAttachments: "chat:pick-attachments",
chatPickImageAttachment: "chat:pick-image-attachment",
chatSendPrompt: "chat:send-prompt",
chatStreamPrompt: "chat:stream-prompt",
......@@ -416,7 +417,7 @@ export interface ProjectSessionState {
}
export interface ChatAttachment {
kind: "image";
kind: "image" | "file";
name: string;
mimeType: string;
localPath: string;
......@@ -563,6 +564,18 @@ export interface DigitalHumanModelConfig {
qiniuSecretKeyConfigured: boolean;
}
export interface DouyinTextModelConfig {
baseUrl: string;
apiKeyConfigured: boolean;
modelId?: string;
}
export interface VectCutModelConfig {
baseUrl: string;
fileBaseUrl: string;
apiKeyConfigured: boolean;
}
export interface ExpertModelConfig {
image: ModelEndpointConfig;
video: ModelEndpointConfig;
......@@ -570,6 +583,12 @@ export interface ExpertModelConfig {
digitalHuman: DigitalHumanModelConfig;
}
export interface DouyinRuntimeConfig {
videoAnalyzer: DouyinTextModelConfig;
replicationBrief: DouyinTextModelConfig;
vectcut: VectCutModelConfig;
}
export const FIXED_EXPERT_MODEL_ENDPOINTS = {
copywriting: {
baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1",
......@@ -596,6 +615,21 @@ export const FIXED_DIGITAL_HUMAN_CONFIG = {
qiniuKeyPrefix: "omnihuman"
} as const;
export const FIXED_DOUYIN_RUNTIME_CONFIG = {
videoAnalyzer: {
baseUrl: "https://ark.cn-beijing.volces.com/api/v3",
modelId: "",
},
replicationBrief: {
baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1",
modelId: "",
},
vectcut: {
baseUrl: "https://open.vectcut.com/cut_jianying",
fileBaseUrl: "https://open.vectcut.com"
}
} as const;
export interface AppConfig {
setupMode: SetupMode;
provider: string;
......@@ -610,6 +644,7 @@ export interface AppConfig {
runtimeCloudApiBaseUrl: string;
runtimeMode: RuntimeModePreference;
expertModelConfig: ExpertModelConfig;
douyinRuntimeConfig: DouyinRuntimeConfig;
}
export interface DiagnosticsExportResult {
......@@ -631,6 +666,18 @@ export interface DigitalHumanModelInput {
qiniuSecretKey?: string;
}
export interface DouyinTextModelInput {
baseUrl?: string;
apiKey?: string;
modelId?: string;
}
export interface VectCutModelInput {
baseUrl?: string;
fileBaseUrl?: string;
apiKey?: string;
}
export interface SaveConfigInput {
setupMode: SetupMode;
provider: string;
......@@ -650,6 +697,11 @@ export interface SaveConfigInput {
copywriting?: ModelEndpointInput;
digitalHuman?: DigitalHumanModelInput;
};
douyinRuntimeConfig?: {
videoAnalyzer?: DouyinTextModelInput;
replicationBrief?: DouyinTextModelInput;
vectcut?: VectCutModelInput;
};
}
export interface AuthSessionSummary {
......@@ -844,6 +896,7 @@ export interface DesktopApi {
createSessionForProject(projectId: string, title?: string): Promise<ProjectSessionSummary>;
closeSession(sessionId: string): Promise<ProjectSessionSummary[]>;
listMessages(sessionId: string): Promise<ChatMessage[]>;
pickAttachments(): Promise<ChatAttachment[]>;
pickImageAttachment(): Promise<ChatAttachment | null>;
sendPrompt(sessionId: string, prompt: string, skillId?: string, attachments?: ChatAttachment[]): Promise<PromptResult>;
streamPrompt(sessionId: string, prompt: string, skillId?: string, attachments?: ChatAttachment[]): Promise<ChatStreamPromptResult>;
......
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