Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Q
qjclaw-dmg
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
AI-甘富林
qjclaw-dmg
Commits
f492464b
Commit
f492464b
authored
Apr 27, 2026
by
AI-甘富林
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(client): add douyin runtime settings and attachment support
parent
c5b18b81
Changes
8
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
929 additions
and
209 deletions
+929
-209
ipc.ts
apps/desktop/src/main/ipc.ts
+246
-52
app-config.ts
apps/desktop/src/main/services/app-config.ts
+110
-2
project-model-runtime.ts
apps/desktop/src/main/services/project-model-runtime.ts
+134
-13
secrets.ts
apps/desktop/src/main/services/secrets.ts
+39
-3
index.ts
apps/desktop/src/preload/index.ts
+1
-0
App.tsx
apps/ui/src/App.tsx
+284
-45
styles.css
apps/ui/src/styles.css
+61
-93
index.ts
packages/shared-types/src/index.ts
+54
-1
No files found.
apps/desktop/src/main/ipc.ts
View file @
f492464b
...
...
@@ -148,6 +148,94 @@ function didWorkspacePathChange(previousConfig: AppConfig, nextConfig: AppConfig
return normalizeComparablePath(previousConfig.workspacePath) !== normalizeComparablePath(nextConfig.workspacePath);
}
const IMAGE_ATTACHMENT_EXTENSIONS = new Set(["
.
png
", "
.
jpg
", "
.
jpeg
", "
.
webp
", "
.
gif
", "
.
bmp
"]);
const DOCUMENT_ATTACHMENT_EXTENSIONS = new Set(["
.
pdf
", "
.
ppt
", "
.
pptx
", "
.
xls
", "
.
xlsx
", "
.
csv
", "
.
tsv
", "
.
doc
", "
.
docx
", "
.
txt
", "
.
md
", "
.
json
", "
.
mp3
"]);
const SUPPORTED_ATTACHMENT_EXTENSIONS = new Set([
...IMAGE_ATTACHMENT_EXTENSIONS,
...DOCUMENT_ATTACHMENT_EXTENSIONS
]);
function inferAttachmentMimeType(localPath: string, name?: string): string {
const extension = path.extname(name || localPath).toLowerCase();
switch (extension) {
case "
.
png
":
return "
image
/
png
";
case "
.
jpg
":
case "
.
jpeg
":
return "
image
/
jpeg
";
case "
.
webp
":
return "
image
/
webp
";
case "
.
gif
":
return "
image
/
gif
";
case "
.
bmp
":
return "
image
/
bmp
";
case "
.
pdf
":
return "
application
/
pdf
";
case "
.
ppt
":
return "
application
/
vnd
.
ms
-
powerpoint
";
case "
.
pptx
":
return "
application
/
vnd
.
openxmlformats
-
officedocument
.
presentationml
.
presentation
";
case "
.
xls
":
return "
application
/
vnd
.
ms
-
excel
";
case "
.
xlsx
":
return "
application
/
vnd
.
openxmlformats
-
officedocument
.
spreadsheetml
.
sheet
";
case "
.
csv
":
return "
text
/
csv
";
case "
.
tsv
":
return "
text
/
tab
-
separated
-
values
";
case "
.
doc
":
return "
application
/
msword
";
case "
.
docx
":
return "
application
/
vnd
.
openxmlformats
-
officedocument
.
wordprocessingml
.
document
";
case "
.
txt
":
return "
text
/
plain
";
case "
.
md
":
return "
text
/
markdown
";
case "
.
json
":
return "
application
/
json
";
case "
.
mp3
":
return "
audio
/
mpeg
";
default:
return "
application
/
octet
-
stream
";
}
}
function inferAttachmentKind(localPath: string, name?: string, mimeType?: string): ChatAttachment["
kind
"] | null {
const normalizedMimeType = mimeType?.trim().toLowerCase() || "";
const extension = path.extname(name || localPath).toLowerCase();
if (normalizedMimeType.startsWith("
image
/
") || IMAGE_ATTACHMENT_EXTENSIONS.has(extension)) {
return "
image
";
}
if (SUPPORTED_ATTACHMENT_EXTENSIONS.has(extension)) {
return "
file
";
}
return null;
}
function normalizeChatAttachmentCandidate(attachment: Partial<ChatAttachment> | null | undefined): ChatAttachment | null {
if (!attachment) {
return null;
}
const localPath = attachment.localPath?.trim();
if (!localPath) {
return null;
}
const name = attachment.name?.trim() || path.basename(localPath);
const mimeType = attachment.mimeType?.trim() || inferAttachmentMimeType(localPath, name);
const kind = attachment.kind && attachment.kind !== "
image
" && attachment.kind !== "
file
"
? null
: inferAttachmentKind(localPath, name, mimeType);
if (!kind) {
return null;
}
return {
kind,
name,
mimeType,
localPath
};
}
async function pickImageAttachment(window: BrowserWindow | null): Promise<ChatAttachment | null> {
const dialogOptions: OpenDialogOptions = {
title: "
Select
image
",
...
...
@@ -168,26 +256,46 @@ async function pickImageAttachment(window: BrowserWindow | null): Promise<ChatAt
return null;
}
const name = path.basename(localPath) || "
image
";
const extension = path.extname(name).toLowerCase();
const mimeType = extension === "
.
png
"
? "
image
/
png
"
: extension === "
.
jpg
" || extension === "
.
jpeg
"
? "
image
/
jpeg
"
: extension === "
.
webp
"
? "
image
/
webp
"
: extension === "
.
gif
"
? "
image
/
gif
"
: extension === "
.
bmp
"
? "
image
/
bmp
"
: "
application
/
octet
-
stream
";
return {
return normalizeChatAttachmentCandidate({
kind: "
image
",
name,
mimeType,
name
: path.basename(localPath) || "
image
"
,
mimeType
: inferAttachmentMimeType(localPath)
,
localPath
});
}
async function pickAttachments(window: BrowserWindow | null): Promise<ChatAttachment[]> {
const dialogOptions: OpenDialogOptions = {
title: "
Select
attachments
",
properties: ["
openFile
", "
multiSelections
"],
filters: [
{ name: "
Supported
files
", extensions: [...SUPPORTED_ATTACHMENT_EXTENSIONS].map((value) => value.slice(1)) },
{ name: "
Images
", extensions: [...IMAGE_ATTACHMENT_EXTENSIONS].map((value) => value.slice(1)) },
{ name: "
Audio
", extensions: ["
mp3
"] },
{ name: "
Documents
", extensions: ["
pdf
", "
mp3
", "
doc
", "
docx
", "
txt
", "
md
", "
json
"] },
{ name: "
Presentations
", extensions: ["
ppt
", "
pptx
"] },
{ name: "
Spreadsheets
", extensions: ["
xls
", "
xlsx
", "
csv
", "
tsv
"] }
]
};
const result = window
? await dialog.showOpenDialog(window, dialogOptions)
: await dialog.showOpenDialog(dialogOptions);
if (result.canceled || !result.filePaths.length) {
return [];
}
return result.filePaths.flatMap((filePath) => {
const localPath = filePath?.trim();
if (!localPath) {
return [];
}
const attachment = normalizeChatAttachmentCandidate({
name: path.basename(localPath),
mimeType: inferAttachmentMimeType(localPath),
localPath
});
return attachment ? [attachment] : [];
});
}
async function pickWorkspaceDirectory(window: BrowserWindow | null, currentPath?: string): Promise<string | null> {
...
...
@@ -213,21 +321,18 @@ function normalizeChatAttachments(attachments?: ChatAttachment[]): ChatAttachmen
return [];
}
const seen = new Set<string>();
return attachments.flatMap((attachment) => {
if (!attachment || attachment.kind !== "
image
") {
const normalized = normalizeChatAttachmentCandidate(attachment);
if (!normalized) {
return [];
}
const
localPath = attachment.localPath?.trim(
);
if (
!localPath
) {
const
comparablePath = normalizeComparablePath(normalized.localPath
);
if (
seen.has(comparablePath)
) {
return [];
}
const name = attachment.name?.trim() || path.basename(localPath);
return [{
kind: "
image
" as const,
name,
mimeType: attachment.mimeType?.trim() || "
application
/
octet
-
stream
",
localPath
}];
seen.add(comparablePath);
return [normalized];
});
}
...
...
@@ -242,13 +347,15 @@ async function materializeProjectAttachments(
}
const sessionSlug = sanitizeAttachmentFileComponent(sessionId.replace(/[:]/g, "
-
"));
const imagesRoot = path.join(projectRoot, "
inputs
", "
images
", "
main
");
await mkdir(imagesRoot, { recursive: true });
return await Promise.all(normalized.map(async (attachment, index) => {
const sourceExt = path.extname(attachment.name || attachment.localPath) || path.extname(attachment.localPath) || "
.
bin
";
const targetRoot = attachment.kind === "
image
"
? path.join(projectRoot, "
inputs
", "
images
", "
main
")
: path.join(projectRoot, "
inputs
", "
assets
", "
manual
");
await mkdir(targetRoot, { recursive: true });
const fileName = `${sessionSlug}-${String(index + 1).padStart(2, "
0
")}${sourceExt.toLowerCase()}`;
const targetPath = path.join(
images
Root, fileName);
const targetPath = path.join(
target
Root, fileName);
await copyFile(attachment.localPath, targetPath);
return {
...attachment,
...
...
@@ -415,39 +522,106 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
);
};
const getEffectiveConfig = async () => {
const config = await configService.load();
return {
const applySecretStateToConfig = async (config: AppConfig): Promise<AppConfig> => {
const [
apiKey,
gatewayToken,
authToken,
imageModelApiKey,
videoModelApiKey,
copywritingModelApiKey,
digitalHumanVolcAccessKey,
digitalHumanVolcSecretKey,
digitalHumanQiniuAccessKey,
digitalHumanQiniuSecretKey,
videoAnalyzerApiKey,
replicationBriefApiKey,
vectCutApiKey
] = await Promise.all([
secretManager.getApiKey(),
getEffectiveGatewayToken(config),
secretManager.getAuthToken(),
secretManager.getImageModelApiKey(),
secretManager.getVideoModelApiKey(),
secretManager.getCopywritingModelApiKey(),
secretManager.getDigitalHumanVolcAccessKey(),
secretManager.getDigitalHumanVolcSecretKey(),
secretManager.getDigitalHumanQiniuAccessKey(),
secretManager.getDigitalHumanQiniuSecretKey(),
secretManager.getVideoAnalyzerApiKey(),
secretManager.getReplicationBriefApiKey(),
secretManager.getVectCutApiKey()
]);
const nextConfig: AppConfig = {
...config,
gatewayUrl: resolveEffectiveGatewayUrl(config.gatewayUrl, getDiscoveredGatewayUrl(config.runtimeMode)),
apiKeyConfigured: Boolean(
(await secretManager.getApiKey()) || config.apiKeyConfigured
),
gatewayTokenConfigured: Boolean(
(await getEffectiveGatewayToken(config)) || config.gatewayTokenConfigured
),
authTokenConfigured: Boolean(
(await secretManager.getAuthToken()) || config.authTokenConfigured
),
apiKeyConfigured: Boolean(
apiKey
),
gatewayTokenConfigured: Boolean(
gatewayToken
),
authTokenConfigured: Boolean(
authToken
),
expertModelConfig: {
image: {
baseUrl: config.expertModelConfig.image.baseUrl,
apiKeyConfigured: Boolean((await secretManager.getImageModelApiKey()) || config.expertModelConfig.image.apiKeyConfigured),
modelId: config.expertModelConfig.image.modelId
...config.expertModelConfig.image,
apiKeyConfigured: Boolean(imageModelApiKey)
},
video: {
baseUrl: config.expertModelConfig.video.baseUrl,
apiKeyConfigured: Boolean((await secretManager.getVideoModelApiKey()) || config.expertModelConfig.video.apiKeyConfigured),
modelId: config.expertModelConfig.video.modelId
...config.expertModelConfig.video,
apiKeyConfigured: Boolean(videoModelApiKey)
},
copywriting: {
baseUrl: config.expertModelConfig.copywriting.baseUrl,
apiKeyConfigured: Boolean((await secretManager.getCopywritingModelApiKey()) || config.expertModelConfig.copywriting.apiKeyConfigured),
modelId: config.expertModelConfig.copywriting.modelId
...config.expertModelConfig.copywriting,
apiKeyConfigured: Boolean(copywritingModelApiKey)
},
digitalHuman: {
...config.expertModelConfig.digitalHuman,
volcAccessKeyConfigured: Boolean((await secretManager.getDigitalHumanVolcAccessKey()) || config.expertModelConfig.digitalHuman.volcAccessKeyConfigured),
volcSecretKeyConfigured: Boolean((await secretManager.getDigitalHumanVolcSecretKey()) || config.expertModelConfig.digitalHuman.volcSecretKeyConfigured),
qiniuAccessKeyConfigured: Boolean((await secretManager.getDigitalHumanQiniuAccessKey()) || config.expertModelConfig.digitalHuman.qiniuAccessKeyConfigured),
qiniuSecretKeyConfigured: Boolean((await secretManager.getDigitalHumanQiniuSecretKey()) || config.expertModelConfig.digitalHuman.qiniuSecretKeyConfigured)
volcAccessKeyConfigured: Boolean(digitalHumanVolcAccessKey),
volcSecretKeyConfigured: Boolean(digitalHumanVolcSecretKey),
qiniuAccessKeyConfigured: Boolean(digitalHumanQiniuAccessKey),
qiniuSecretKeyConfigured: Boolean(digitalHumanQiniuSecretKey)
}
},
douyinRuntimeConfig: {
videoAnalyzer: {
...config.douyinRuntimeConfig.videoAnalyzer,
apiKeyConfigured: Boolean(videoAnalyzerApiKey)
},
replicationBrief: {
...config.douyinRuntimeConfig.replicationBrief,
apiKeyConfigured: Boolean(replicationBriefApiKey)
},
vectcut: {
...config.douyinRuntimeConfig.vectcut,
apiKeyConfigured: Boolean(vectCutApiKey)
}
}
};
const secretStateChanged = nextConfig.apiKeyConfigured !== config.apiKeyConfigured
|| nextConfig.gatewayTokenConfigured !== config.gatewayTokenConfigured
|| nextConfig.authTokenConfigured !== config.authTokenConfigured
|| nextConfig.expertModelConfig.image.apiKeyConfigured !== config.expertModelConfig.image.apiKeyConfigured
|| nextConfig.expertModelConfig.video.apiKeyConfigured !== config.expertModelConfig.video.apiKeyConfigured
|| nextConfig.expertModelConfig.copywriting.apiKeyConfigured !== config.expertModelConfig.copywriting.apiKeyConfigured
|| nextConfig.expertModelConfig.digitalHuman.volcAccessKeyConfigured !== config.expertModelConfig.digitalHuman.volcAccessKeyConfigured
|| nextConfig.expertModelConfig.digitalHuman.volcSecretKeyConfigured !== config.expertModelConfig.digitalHuman.volcSecretKeyConfigured
|| nextConfig.expertModelConfig.digitalHuman.qiniuAccessKeyConfigured !== config.expertModelConfig.digitalHuman.qiniuAccessKeyConfigured
|| nextConfig.expertModelConfig.digitalHuman.qiniuSecretKeyConfigured !== config.expertModelConfig.digitalHuman.qiniuSecretKeyConfigured
|| nextConfig.douyinRuntimeConfig.videoAnalyzer.apiKeyConfigured !== config.douyinRuntimeConfig.videoAnalyzer.apiKeyConfigured
|| nextConfig.douyinRuntimeConfig.replicationBrief.apiKeyConfigured !== config.douyinRuntimeConfig.replicationBrief.apiKeyConfigured
|| nextConfig.douyinRuntimeConfig.vectcut.apiKeyConfigured !== config.douyinRuntimeConfig.vectcut.apiKeyConfigured;
if (secretStateChanged) {
await configService.persist({
...nextConfig,
gatewayUrl: config.gatewayUrl
});
}
return nextConfig;
};
const getEffectiveConfig = async () => {
return applySecretStateToConfig(await configService.load());
};
const prepareProjectModelRuntime = async (projectId: string, projectRoot: string): Promise<Record<string, string>> => {
...
...
@@ -459,7 +633,10 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
digitalHumanVolcAccessKey,
digitalHumanVolcSecretKey,
digitalHumanQiniuAccessKey,
digitalHumanQiniuSecretKey
digitalHumanQiniuSecretKey,
videoAnalyzerApiKey,
replicationBriefApiKey,
vectCutApiKey
] = await Promise.all([
secretManager.getCopywritingModelApiKey(),
secretManager.getImageModelApiKey(),
...
...
@@ -467,7 +644,10 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
secretManager.getDigitalHumanVolcAccessKey(),
secretManager.getDigitalHumanVolcSecretKey(),
secretManager.getDigitalHumanQiniuAccessKey(),
secretManager.getDigitalHumanQiniuSecretKey()
secretManager.getDigitalHumanQiniuSecretKey(),
secretManager.getVideoAnalyzerApiKey(),
secretManager.getReplicationBriefApiKey(),
secretManager.getVectCutApiKey()
]);
const runtime = buildProjectModelRuntime(projectId, config, {
copywritingApiKey,
...
...
@@ -476,7 +656,10 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
digitalHumanVolcAccessKey,
digitalHumanVolcSecretKey,
digitalHumanQiniuAccessKey,
digitalHumanQiniuSecretKey
digitalHumanQiniuSecretKey,
videoAnalyzerApiKey,
replicationBriefApiKey,
vectCutApiKey
});
const envFilePath = await materializeProjectModelRuntime(projectRoot, runtime);
...
...
@@ -841,6 +1024,15 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
if (typeof input.expertModelConfig?.digitalHuman?.qiniuSecretKey === "
string
") {
await secretManager.setDigitalHumanQiniuSecretKey(input.expertModelConfig.digitalHuman.qiniuSecretKey || undefined);
}
if (typeof input.douyinRuntimeConfig?.videoAnalyzer?.apiKey === "
string
") {
await secretManager.setVideoAnalyzerApiKey(input.douyinRuntimeConfig.videoAnalyzer.apiKey || undefined);
}
if (typeof input.douyinRuntimeConfig?.replicationBrief?.apiKey === "
string
") {
await secretManager.setReplicationBriefApiKey(input.douyinRuntimeConfig.replicationBrief.apiKey || undefined);
}
if (typeof input.douyinRuntimeConfig?.vectcut?.apiKey === "
string
") {
await secretManager.setVectCutApiKey(input.douyinRuntimeConfig.vectcut.apiKey || undefined);
}
if (
config.setupMode === "
direct
-
provider
"
|| previousConfig.setupMode !== config.setupMode
...
...
@@ -1875,6 +2067,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
return sessions;
});
ipcMain.handle(IPC_CHANNELS.chatListMessages, async (_event, sessionId: string) => listChatMessages(sessionId));
ipcMain.handle(IPC_CHANNELS.chatPickAttachments, async (event) => pickAttachments(BrowserWindow.fromWebContents(event.sender)));
ipcMain.handle(IPC_CHANNELS.chatPickImageAttachment, async (event) => pickImageAttachment(BrowserWindow.fromWebContents(event.sender)));
ipcMain.handle(IPC_CHANNELS.chatSendPrompt, async (_event, sessionId: string, prompt: string, skillId?: string, attachments?: ChatAttachment[]) => {
return sendPrompt(sessionId, prompt, skillId, attachments);
...
...
@@ -1992,6 +2185,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
return sessions;
},
listMessages: (sessionId: string) => listChatMessages(sessionId),
pickAttachments: async () => pickAttachments(BrowserWindow.getFocusedWindow() ?? null),
pickImageAttachment: async () => pickImageAttachment(BrowserWindow.getFocusedWindow() ?? null),
sendPrompt: async (sessionId: string, prompt: string, skillId?: string, attachments?: ChatAttachment[]) => sendPrompt(sessionId, prompt, skillId, attachments),
streamPrompt: async (sessionId: string, prompt: string, skillId?: string, attachments?: ChatAttachment[]) => streamPrompt(sessionId, prompt, skillId, attachments),
...
...
apps/desktop/src/main/services/app-config.ts
View file @
f492464b
...
...
@@ -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)
}
}
};
}
...
...
apps/desktop/src/main/services/project-model-runtime.ts
View file @
f492464b
...
...
@@ -10,6 +10,9 @@ export interface ProjectModelRuntimeSecrets {
digitalHumanVolcSecretKey
?:
string
;
digitalHumanQiniuAccessKey
?:
string
;
digitalHumanQiniuSecretKey
?:
string
;
videoAnalyzerApiKey
?:
string
;
replicationBriefApiKey
?:
string
;
vectCutApiKey
?:
string
;
}
export
interface
ProjectModelRuntimePreparation
{
...
...
@@ -40,7 +43,7 @@ const DOUYIN_PROJECT_IDS = new Set([
"douyin"
]);
const
CLIENT_SETTINGS_HINT
=
"
请在客户端设置中完成模型配置后重试。
"
;
const
CLIENT_SETTINGS_HINT
=
"
Please complete the required settings in the client and try again.
"
;
function
normalizeValue
(
raw
?:
string
):
string
{
return
raw
?.
trim
()
??
""
;
...
...
@@ -92,12 +95,12 @@ function formatMissingSection(label: string, missing: string[]): string | null {
return
null
;
}
return
`
${
label
}
缺少
${
missing
.
join
(
"、
"
)}
`
;
return
`
${
label
}
missing
${
missing
.
join
(
",
"
)}
`
;
}
export
function
validateProjectModelRuntime
(
projectId
:
string
,
config
:
Pick
<
AppConfig
,
"expertModelConfig"
>
,
config
:
Pick
<
AppConfig
,
"expertModelConfig"
|
"douyinRuntimeConfig"
>
,
secrets
:
ProjectModelRuntimeSecrets
):
ProjectModelRuntimeValidationResult
{
const
normalizedProjectId
=
normalizeValue
(
projectId
).
toLowerCase
();
...
...
@@ -137,11 +140,57 @@ export function validateProjectModelRuntime(
imageMissing
.
push
(
"modelId"
);
}
const
sections
=
[
formatMissingSection
(
"文案模型"
,
copywritingMissing
),
formatMissingSection
(
"生图模型"
,
imageMissing
)
const
missingFields
:
string
[]
=
[
...
copywritingMissing
.
map
((
field
)
=>
`copywriting.
${
field
}
`
),
...
imageMissing
.
map
((
field
)
=>
`image.
${
field
}
`
)
];
const
sections
:
string
[]
=
[
formatMissingSection
(
"Copywriting model"
,
copywritingMissing
),
formatMissingSection
(
"Image model"
,
imageMissing
)
].
filter
((
value
):
value
is
string
=>
Boolean
(
value
));
if
(
DOUYIN_PROJECT_IDS
.
has
(
normalizedProjectId
))
{
const
videoAnalyzerBaseUrl
=
normalizeOpenAiCompatibleBaseUrl
(
config
.
douyinRuntimeConfig
.
videoAnalyzer
.
baseUrl
);
const
videoAnalyzerModelId
=
normalizeValue
(
config
.
douyinRuntimeConfig
.
videoAnalyzer
.
modelId
);
const
videoAnalyzerApiKey
=
normalizeValue
(
secrets
.
videoAnalyzerApiKey
);
const
replicationBriefBaseUrl
=
normalizeOpenAiCompatibleBaseUrl
(
config
.
douyinRuntimeConfig
.
replicationBrief
.
baseUrl
);
const
replicationBriefModelId
=
normalizeValue
(
config
.
douyinRuntimeConfig
.
replicationBrief
.
modelId
);
const
replicationBriefApiKey
=
normalizeValue
(
secrets
.
replicationBriefApiKey
);
const
videoAnalyzerMissing
:
string
[]
=
[];
if
(
!
videoAnalyzerBaseUrl
)
{
videoAnalyzerMissing
.
push
(
"baseUrl"
);
}
if
(
!
videoAnalyzerApiKey
)
{
videoAnalyzerMissing
.
push
(
"apiKey"
);
}
if
(
!
videoAnalyzerModelId
)
{
videoAnalyzerMissing
.
push
(
"modelId"
);
}
const
replicationBriefMissing
:
string
[]
=
[];
if
(
!
replicationBriefBaseUrl
)
{
replicationBriefMissing
.
push
(
"baseUrl"
);
}
if
(
!
replicationBriefApiKey
)
{
replicationBriefMissing
.
push
(
"apiKey"
);
}
if
(
!
replicationBriefModelId
)
{
replicationBriefMissing
.
push
(
"modelId"
);
}
missingFields
.
push
(
...
videoAnalyzerMissing
.
map
((
field
)
=>
`douyinRuntimeConfig.videoAnalyzer.
${
field
}
`
),
...
replicationBriefMissing
.
map
((
field
)
=>
`douyinRuntimeConfig.replicationBrief.
${
field
}
`
)
);
sections
.
push
(
...[
formatMissingSection
(
"Video Analyzer"
,
videoAnalyzerMissing
),
formatMissingSection
(
"Replication Brief"
,
replicationBriefMissing
)
].
filter
((
value
):
value
is
string
=>
Boolean
(
value
))
);
}
if
(
sections
.
length
===
0
)
{
return
{
ok
:
true
,
...
...
@@ -149,20 +198,16 @@ export function validateProjectModelRuntime(
};
}
const
projectLabel
=
XHS_PROJECT_IDS
.
has
(
normalizedProjectId
)
?
"小红书专家"
:
"抖音专家"
;
return
{
ok
:
false
,
message
:
`
${
projectLabel
}
缺少客户端模型配置:
${
sections
.
join
(
";"
)}
。
${
CLIENT_SETTINGS_HINT
}
`
,
missingFields
:
[
...
copywritingMissing
.
map
((
field
)
=>
`copywriting.
${
field
}
`
),
...
imageMissing
.
map
((
field
)
=>
`image.
${
field
}
`
)
]
message
:
`
${
XHS_PROJECT_IDS
.
has
(
normalizedProjectId
)
?
"XHS"
:
"Douyin"
}
project is missing client settings:
${
sections
.
join
(
"; "
)}
.
${
CLIENT_SETTINGS_HINT
}
`
,
missingFields
};
}
export
function
buildProjectModelRuntime
(
projectId
:
string
,
config
:
Pick
<
AppConfig
,
"expertModelConfig"
>
,
config
:
Pick
<
AppConfig
,
"expertModelConfig"
|
"douyinRuntimeConfig"
>
,
secrets
:
ProjectModelRuntimeSecrets
):
ProjectModelRuntimePreparation
{
const
normalizedProjectId
=
normalizeValue
(
projectId
).
toLowerCase
();
...
...
@@ -170,6 +215,7 @@ export function buildProjectModelRuntime(
if
(
!
validation
.
ok
)
{
throw
new
Error
(
validation
.
message
);
}
const
copywritingBaseUrl
=
normalizeOpenAiCompatibleBaseUrl
(
config
.
expertModelConfig
.
copywriting
.
baseUrl
);
const
copywritingModelId
=
normalizeValue
(
config
.
expertModelConfig
.
copywriting
.
modelId
);
const
copywritingApiKey
=
normalizeValue
(
secrets
.
copywritingApiKey
);
...
...
@@ -183,6 +229,15 @@ export function buildProjectModelRuntime(
const
digitalHumanVolcSecretKey
=
normalizeValue
(
secrets
.
digitalHumanVolcSecretKey
);
const
digitalHumanQiniuAccessKey
=
normalizeValue
(
secrets
.
digitalHumanQiniuAccessKey
);
const
digitalHumanQiniuSecretKey
=
normalizeValue
(
secrets
.
digitalHumanQiniuSecretKey
);
const
videoAnalyzerBaseUrl
=
normalizeOpenAiCompatibleBaseUrl
(
config
.
douyinRuntimeConfig
.
videoAnalyzer
.
baseUrl
);
const
videoAnalyzerModelId
=
normalizeValue
(
config
.
douyinRuntimeConfig
.
videoAnalyzer
.
modelId
);
const
videoAnalyzerApiKey
=
normalizeValue
(
secrets
.
videoAnalyzerApiKey
);
const
replicationBriefBaseUrl
=
normalizeOpenAiCompatibleBaseUrl
(
config
.
douyinRuntimeConfig
.
replicationBrief
.
baseUrl
);
const
replicationBriefModelId
=
normalizeValue
(
config
.
douyinRuntimeConfig
.
replicationBrief
.
modelId
);
const
replicationBriefApiKey
=
normalizeValue
(
secrets
.
replicationBriefApiKey
);
const
vectCutBaseUrl
=
withoutTrailingSlash
(
config
.
douyinRuntimeConfig
.
vectcut
.
baseUrl
);
const
vectCutFileBaseUrl
=
withoutTrailingSlash
(
config
.
douyinRuntimeConfig
.
vectcut
.
fileBaseUrl
);
const
vectCutApiKey
=
normalizeValue
(
secrets
.
vectCutApiKey
);
const
env
:
Record
<
string
,
string
>
=
{};
if
(
XHS_PROJECT_IDS
.
has
(
normalizedProjectId
))
{
...
...
@@ -213,54 +268,120 @@ export function buildProjectModelRuntime(
if
(
DOUYIN_PROJECT_IDS
.
has
(
normalizedProjectId
))
{
const
writerBaseUrl
=
normalizeChatCompletionsBaseUrl
(
copywritingBaseUrl
);
const
seedreamBaseUrl
=
normalizeArkBaseUrl
(
imageBaseUrl
);
env
.
QJC_CLIENT_CONFIG_ACTIVE
=
"1"
;
if
(
writerBaseUrl
)
{
env
.
DOUYIN_WRITER_BASE_URL
=
writerBaseUrl
;
env
.
DOUYIN_WRITER_LLM_BASE_URL
=
writerBaseUrl
;
}
if
(
copywritingApiKey
)
{
env
.
DOUYIN_WRITER_API_KEY
=
copywritingApiKey
;
env
.
DASHSCOPE_API_KEY
=
copywritingApiKey
;
env
.
QWEN_API_KEY
=
copywritingApiKey
;
}
if
(
copywritingModelId
)
{
env
.
DOUYIN_WRITER_MODEL
=
copywritingModelId
;
env
.
DOUYIN_WRITER_LLM_MODEL
=
copywritingModelId
;
}
if
(
seedreamBaseUrl
)
{
env
.
SEEDREAM_BASE_URL
=
seedreamBaseUrl
;
env
.
SEEDREAM_ARK_BASE_URL
=
seedreamBaseUrl
;
}
if
(
imageApiKey
)
{
env
.
SEEDREAM_API_KEY
=
imageApiKey
;
env
.
SEEDREAM_ARK_API_KEY
=
imageApiKey
;
}
if
(
imageModelId
)
{
env
.
SEEDREAM_MODEL
=
imageModelId
;
}
if
(
videoBaseUrl
)
{
env
.
SEEDANCE_BASE_URL
=
videoBaseUrl
;
env
.
SEEDANCE_ARK_BASE_URL
=
videoBaseUrl
;
}
if
(
videoApiKey
)
{
env
.
SEEDANCE_API_KEY
=
videoApiKey
;
env
.
SEEDANCE_ARK_API_KEY
=
videoApiKey
;
}
if
(
videoModelId
)
{
env
.
SEEDANCE_MODEL
=
videoModelId
;
}
if
(
videoAnalyzerBaseUrl
)
{
env
.
VIDEO_LLM_ANALYZER_BASE_URL
=
videoAnalyzerBaseUrl
;
env
.
ARK_BASE_URL
=
videoAnalyzerBaseUrl
;
}
else
if
(
videoBaseUrl
)
{
env
.
ARK_BASE_URL
=
videoBaseUrl
;
}
if
(
videoAnalyzerApiKey
)
{
env
.
VIDEO_LLM_ANALYZER_API_KEY
=
videoAnalyzerApiKey
;
env
.
ARK_API_KEY
=
videoAnalyzerApiKey
;
}
else
if
(
videoApiKey
)
{
env
.
ARK_API_KEY
=
videoApiKey
;
}
if
(
videoAnalyzerModelId
)
{
env
.
VIDEO_LLM_ANALYZER_MODEL
=
videoAnalyzerModelId
;
}
if
(
replicationBriefBaseUrl
)
{
env
.
REPLICATION_BRIEF_BASE_URL
=
replicationBriefBaseUrl
;
}
if
(
replicationBriefApiKey
)
{
env
.
REPLICATION_BRIEF_API_KEY
=
replicationBriefApiKey
;
}
if
(
replicationBriefModelId
)
{
env
.
REPLICATION_BRIEF_MODEL
=
replicationBriefModelId
;
}
if
(
vectCutBaseUrl
)
{
env
.
VECTCUT_BASE_URL
=
vectCutBaseUrl
;
}
if
(
vectCutFileBaseUrl
)
{
env
.
VECTCUT_FILE_BASE_URL
=
vectCutFileBaseUrl
;
}
if
(
vectCutApiKey
)
{
env
.
VECTCUT_API_KEY
=
vectCutApiKey
;
}
if
(
digitalHumanVolcAccessKey
)
{
env
.
OMNIHUMAN_VOLC_ACCESS_KEY
=
digitalHumanVolcAccessKey
;
env
.
VOLC_ACCESS_KEY
=
digitalHumanVolcAccessKey
;
}
if
(
digitalHumanVolcSecretKey
)
{
env
.
OMNIHUMAN_VOLC_SECRET_KEY
=
digitalHumanVolcSecretKey
;
env
.
VOLC_SECRET_KEY
=
digitalHumanVolcSecretKey
;
}
env
.
OMNIHUMAN_VOLC_REGION
=
FIXED_DIGITAL_HUMAN_CONFIG
.
volcRegion
;
env
.
OMNIHUMAN_VOLC_SERVICE
=
FIXED_DIGITAL_HUMAN_CONFIG
.
volcService
;
env
.
OMNIHUMAN_VOLC_HOST
=
FIXED_DIGITAL_HUMAN_CONFIG
.
volcHost
;
env
.
OMNIHUMAN_VOLC_SCHEME
=
FIXED_DIGITAL_HUMAN_CONFIG
.
volcScheme
;
env
.
OMNIHUMAN_TTS_VOICE
=
FIXED_DIGITAL_HUMAN_CONFIG
.
ttsVoice
;
env
.
VOLC_REGION
=
FIXED_DIGITAL_HUMAN_CONFIG
.
volcRegion
;
env
.
VOLC_SERVICE
=
FIXED_DIGITAL_HUMAN_CONFIG
.
volcService
;
env
.
VOLC_HOST
=
FIXED_DIGITAL_HUMAN_CONFIG
.
volcHost
;
env
.
VOLC_SCHEME
=
FIXED_DIGITAL_HUMAN_CONFIG
.
volcScheme
;
if
(
digitalHumanQiniuAccessKey
)
{
env
.
OMNIHUMAN_QINIU_ACCESS_KEY
=
digitalHumanQiniuAccessKey
;
env
.
QINIU_ACCESS_KEY
=
digitalHumanQiniuAccessKey
;
env
.
SEEDANCE_QINIU_ACCESS_KEY
=
digitalHumanQiniuAccessKey
;
}
if
(
digitalHumanQiniuSecretKey
)
{
env
.
OMNIHUMAN_QINIU_SECRET_KEY
=
digitalHumanQiniuSecretKey
;
env
.
QINIU_SECRET_KEY
=
digitalHumanQiniuSecretKey
;
env
.
SEEDANCE_QINIU_SECRET_KEY
=
digitalHumanQiniuSecretKey
;
}
env
.
OMNIHUMAN_QINIU_BUCKET
=
FIXED_DIGITAL_HUMAN_CONFIG
.
qiniuBucket
;
env
.
OMNIHUMAN_QINIU_DOMAIN
=
FIXED_DIGITAL_HUMAN_CONFIG
.
qiniuDomain
;
env
.
OMNIHUMAN_QINIU_KEY_PREFIX
=
FIXED_DIGITAL_HUMAN_CONFIG
.
qiniuKeyPrefix
;
env
.
QINIU_BUCKET
=
FIXED_DIGITAL_HUMAN_CONFIG
.
qiniuBucket
;
env
.
QINIU_DOMAIN
=
FIXED_DIGITAL_HUMAN_CONFIG
.
qiniuDomain
;
env
.
QINIU_KEY_PREFIX
=
FIXED_DIGITAL_HUMAN_CONFIG
.
qiniuKeyPrefix
;
env
.
SEEDANCE_QINIU_BUCKET
=
FIXED_DIGITAL_HUMAN_CONFIG
.
qiniuBucket
;
env
.
SEEDANCE_QINIU_DOMAIN
=
FIXED_DIGITAL_HUMAN_CONFIG
.
qiniuDomain
;
env
.
SEEDANCE_QINIU_KEY_PREFIX
=
FIXED_DIGITAL_HUMAN_CONFIG
.
qiniuKeyPrefix
;
}
const
envKeys
=
Object
.
keys
(
env
).
sort
();
...
...
apps/desktop/src/main/services/secrets.ts
View file @
f492464b
...
...
@@ -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
)
{
...
...
apps/desktop/src/preload/index.ts
View file @
f492464b
...
...
@@ -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
),
...
...
apps/ui/src/App.tsx
View file @
f492464b
...
...
@@ -26,6 +26,7 @@ import type {
}
from
"@qjclaw/shared-types"
;
import
{
FIXED_DIGITAL_HUMAN_CONFIG
,
FIXED_DOUYIN_RUNTIME_CONFIG
,
FIXED_EXPERT_MODEL_ENDPOINTS
}
from
"@qjclaw/shared-types"
;
...
...
@@ -133,6 +134,10 @@ const SUCCESS_NOTICE_TIMEOUT_MS = 2400;
const
TYPEWRITER_CHARS_PER_FRAME
=
3
;
const
MAX_TRACE_ITEMS
=
60
;
const
HOME_EXPERT_SUGGESTION_PROJECT_IDS
=
new
Set
([
"xhs"
,
"douyin"
]);
const
IMAGE_ATTACHMENT_EXTENSIONS
=
new
Set
([
".png"
,
".jpg"
,
".jpeg"
,
".webp"
,
".gif"
,
".bmp"
]);
const
DOCUMENT_ATTACHMENT_EXTENSIONS
=
new
Set
([
".pdf"
,
".ppt"
,
".pptx"
,
".xls"
,
".xlsx"
,
".csv"
,
".tsv"
,
".doc"
,
".docx"
,
".txt"
,
".md"
,
".json"
,
".mp3"
]);
const
SUPPORTED_ATTACHMENT_EXTENSIONS
=
new
Set
([...
IMAGE_ATTACHMENT_EXTENSIONS
,
...
DOCUMENT_ATTACHMENT_EXTENSIONS
]);
const
COMPOSER_ATTACHMENT_ACCEPT
=
[...
SUPPORTED_ATTACHMENT_EXTENSIONS
].
join
(
","
);
function
shouldOfferHomeExpertSwitch
(
prompt
:
string
):
boolean
{
const
normalized
=
prompt
.
normalize
(
"NFKC"
).
toLowerCase
();
...
...
@@ -988,6 +993,20 @@ const mockDesktopApi = {
qiniuAccessKeyConfigured
:
false
,
qiniuSecretKeyConfigured
:
false
}
},
douyinRuntimeConfig
:
{
videoAnalyzer
:
{
...
FIXED_DOUYIN_RUNTIME_CONFIG
.
videoAnalyzer
,
apiKeyConfigured
:
false
,
},
replicationBrief
:
{
...
FIXED_DOUYIN_RUNTIME_CONFIG
.
replicationBrief
,
apiKeyConfigured
:
false
,
},
vectcut
:
{
...
FIXED_DOUYIN_RUNTIME_CONFIG
.
vectcut
,
apiKeyConfigured
:
false
}
}
}),
pickWorkspaceDirectory
:
async
(
currentPath
?:
string
)
=>
currentPath
||
"D:/workspace"
,
...
...
@@ -1024,6 +1043,23 @@ const mockDesktopApi = {
qiniuAccessKeyConfigured
:
Boolean
(
input
.
expertModelConfig
?.
digitalHuman
?.
qiniuAccessKey
?.
trim
()),
qiniuSecretKeyConfigured
:
Boolean
(
input
.
expertModelConfig
?.
digitalHuman
?.
qiniuSecretKey
?.
trim
())
}
},
douyinRuntimeConfig
:
{
videoAnalyzer
:
{
baseUrl
:
input
.
douyinRuntimeConfig
?.
videoAnalyzer
?.
baseUrl
?.
trim
()
||
FIXED_DOUYIN_RUNTIME_CONFIG
.
videoAnalyzer
.
baseUrl
,
apiKeyConfigured
:
Boolean
(
input
.
douyinRuntimeConfig
?.
videoAnalyzer
?.
apiKey
?.
trim
()),
modelId
:
input
.
douyinRuntimeConfig
?.
videoAnalyzer
?.
modelId
?.
trim
()
||
""
},
replicationBrief
:
{
baseUrl
:
input
.
douyinRuntimeConfig
?.
replicationBrief
?.
baseUrl
?.
trim
()
||
FIXED_DOUYIN_RUNTIME_CONFIG
.
replicationBrief
.
baseUrl
,
apiKeyConfigured
:
Boolean
(
input
.
douyinRuntimeConfig
?.
replicationBrief
?.
apiKey
?.
trim
()),
modelId
:
input
.
douyinRuntimeConfig
?.
replicationBrief
?.
modelId
?.
trim
()
||
""
},
vectcut
:
{
baseUrl
:
input
.
douyinRuntimeConfig
?.
vectcut
?.
baseUrl
?.
trim
()
||
FIXED_DOUYIN_RUNTIME_CONFIG
.
vectcut
.
baseUrl
,
fileBaseUrl
:
input
.
douyinRuntimeConfig
?.
vectcut
?.
fileBaseUrl
?.
trim
()
||
FIXED_DOUYIN_RUNTIME_CONFIG
.
vectcut
.
fileBaseUrl
,
apiKeyConfigured
:
Boolean
(
input
.
douyinRuntimeConfig
?.
vectcut
?.
apiKey
?.
trim
())
}
}
})
},
...
...
@@ -1061,6 +1097,7 @@ const mockDesktopApi = {
createSessionForProject
:
async
(
projectId
:
string
,
title
?:
string
)
=>
({
id
:
`project:
${
projectId
}
:
${
createClientMessageId
(
"session"
)}
`
,
projectId
,
title
:
title
||
"
\
u65b0
\
u5bf9
\
u8bdd"
,
updatedAt
:
new
Date
().
toISOString
()
}),
closeSession
:
async
(
sessionId
:
string
)
=>
getMockSessions
(
sessionId
.
split
(
":"
)[
1
]),
listMessages
:
async
()
=>
[],
pickAttachments
:
async
()
=>
[],
pickImageAttachment
:
async
()
=>
null
,
sendPrompt
:
async
(
sessionId
:
string
,
prompt
:
string
,
skillId
?:
string
,
_attachments
?:
ChatAttachment
[])
=>
({
sessionId
:
sessionId
||
"project:xiaohongshu:default"
,
reply
:
{
id
:
"reply-1"
,
role
:
"assistant"
,
content
:
"Mock: "
+
prompt
,
createdAt
:
new
Date
().
toISOString
()
},
executionPolicy
:
{
source
:
"client-config"
,
modelId
:
"qwen3.5-plus"
,
modelLabel
:
"qwen3.5-plus"
,
routingMode
:
"platform-managed"
,
skillId
,
skillName
:
skillId
,
message
:
"mock"
}
}),
streamPrompt
:
async
(
_sessionId
:
string
,
prompt
:
string
,
skillId
?:
string
,
_attachments
?:
ChatAttachment
[])
=>
{
...
...
@@ -1215,9 +1252,11 @@ declare global {
lobsterKey
?:
string
;
workspacePath
?:
string
;
expertModelConfig
?:
SaveConfigInput
[
"expertModelConfig"
];
douyinRuntimeConfig
?:
SaveConfigInput
[
"douyinRuntimeConfig"
];
}):
Promise
<
{
workspacePath
:
string
;
expertModelConfig
:
AppConfig
[
"expertModelConfig"
];
douyinRuntimeConfig
:
AppConfig
[
"douyinRuntimeConfig"
];
apiKeyConfigured
:
boolean
;
}
>
;
createProjectSession
(
projectId
?:
string
,
title
?:
string
):
Promise
<
SessionSummary
>
;
...
...
@@ -2030,7 +2069,7 @@ export default function App() {
const [projectActionPending, setProjectActionPending] = useState(false);
const [selectedSkillId, setSelectedSkillId] = useState(DEFAULT_SKILL.id);
const [prompt, setPrompt] = useState("");
const [composerAttachment
, setComposerAttachment] = useState<ChatAttachment | null>(null
);
const [composerAttachment
s, setComposerAttachments] = useState<ChatAttachment[]>([]
);
const [lobsterKeyDraft, setLobsterKeyDraft] = useState("");
const [workspacePathDraft, setWorkspacePathDraft] = useState("");
const [imageModelBaseUrlDraft, setImageModelBaseUrlDraft] = useState<string>(FIXED_EXPERT_MODEL_ENDPOINTS.image.baseUrl);
...
...
@@ -2045,6 +2084,15 @@ export default function App() {
const [digitalHumanVolcSecretKeyDraft, setDigitalHumanVolcSecretKeyDraft] = useState("");
const [digitalHumanQiniuAccessKeyDraft, setDigitalHumanQiniuAccessKeyDraft] = useState("");
const [digitalHumanQiniuSecretKeyDraft, setDigitalHumanQiniuSecretKeyDraft] = useState("");
const [videoAnalyzerBaseUrlDraft, setVideoAnalyzerBaseUrlDraft] = useState("");
const [videoAnalyzerModelIdDraft, setVideoAnalyzerModelIdDraft] = useState("");
const [videoAnalyzerApiKeyDraft, setVideoAnalyzerApiKeyDraft] = useState("");
const [replicationBriefBaseUrlDraft, setReplicationBriefBaseUrlDraft] = useState("");
const [replicationBriefModelIdDraft, setReplicationBriefModelIdDraft] = useState("");
const [replicationBriefApiKeyDraft, setReplicationBriefApiKeyDraft] = useState("");
const [vectcutBaseUrlDraft, setVectcutBaseUrlDraft] = useState("");
const [vectcutFileBaseUrlDraft, setVectcutFileBaseUrlDraft] = useState("");
const [vectcutApiKeyDraft, setVectcutApiKeyDraft] = useState("");
const [refreshing, setRefreshing] = useState(false);
const [saving, setSaving] = useState(false);
const [sendPhase, setSendPhase] = useState<SendPhase>("idle");
...
...
@@ -2172,7 +2220,7 @@ export default function App() {
const hasVisibleConversation = messages.length > 0 || sendPhase !== "idle";
const showStartupOverlay = startupStateActive && !hasVisibleConversation;
const sending = sendPhase !== "idle";
const canSend = isBound && hasConversationProject && (prompt.trim().length > 0 ||
Boolean(composerAttachment)
) && !sending && !saving;
const canSend = isBound && hasConversationProject && (prompt.trim().length > 0 ||
composerAttachments.length > 0
) && !sending && !saving;
const hasPendingLobsterKey = lobsterKeyDraft.trim().length > 0;
const hasPendingModelKeys = Boolean(
imageModelApiKeyDraft.trim()
...
...
@@ -2182,6 +2230,15 @@ export default function App() {
|| digitalHumanVolcSecretKeyDraft.trim()
|| digitalHumanQiniuAccessKeyDraft.trim()
|| digitalHumanQiniuSecretKeyDraft.trim()
|| videoAnalyzerBaseUrlDraft.trim() !== (config?.douyinRuntimeConfig.videoAnalyzer.baseUrl ?? "").trim()
|| videoAnalyzerModelIdDraft.trim() !== (config?.douyinRuntimeConfig.videoAnalyzer.modelId ?? "").trim()
|| videoAnalyzerApiKeyDraft.trim()
|| replicationBriefBaseUrlDraft.trim() !== (config?.douyinRuntimeConfig.replicationBrief.baseUrl ?? "").trim()
|| replicationBriefModelIdDraft.trim() !== (config?.douyinRuntimeConfig.replicationBrief.modelId ?? "").trim()
|| replicationBriefApiKeyDraft.trim()
|| vectcutBaseUrlDraft.trim() !== (config?.douyinRuntimeConfig.vectcut.baseUrl ?? "").trim()
|| vectcutFileBaseUrlDraft.trim() !== (config?.douyinRuntimeConfig.vectcut.fileBaseUrl ?? "").trim()
|| vectcutApiKeyDraft.trim()
);
const sendButtonLabel = sendPhase === "preparing"
? ui.preparing
...
...
@@ -2560,6 +2617,12 @@ export default function App() {
setVideoModelBaseUrlDraft(config.expertModelConfig.video.baseUrl);
setCopywritingModelBaseUrlDraft(config.expertModelConfig.copywriting.baseUrl);
setCopywritingModelIdDraft(config.expertModelConfig.copywriting.modelId ?? FIXED_EXPERT_MODEL_ENDPOINTS.copywriting.modelId);
setVideoAnalyzerBaseUrlDraft(config.douyinRuntimeConfig.videoAnalyzer.baseUrl);
setVideoAnalyzerModelIdDraft(config.douyinRuntimeConfig.videoAnalyzer.modelId ?? "");
setReplicationBriefBaseUrlDraft(config.douyinRuntimeConfig.replicationBrief.baseUrl);
setReplicationBriefModelIdDraft(config.douyinRuntimeConfig.replicationBrief.modelId ?? "");
setVectcutBaseUrlDraft(config.douyinRuntimeConfig.vectcut.baseUrl);
setVectcutFileBaseUrlDraft(config.douyinRuntimeConfig.vectcut.fileBaseUrl);
}, [config]);
useEffect(() => {
...
...
@@ -2787,7 +2850,7 @@ export default function App() {
? options.attachments.filter((attachment): attachment is ChatAttachment =>
Boolean(
attachment
&&
attachment.kind === "image"
&&
(attachment.kind === "image" || attachment.kind === "file")
&& attachment.localPath?.trim()
))
: undefined;
...
...
@@ -2885,9 +2948,11 @@ export default function App() {
lobsterKey?: string;
workspacePath?: string;
expertModelConfig?: SaveConfigInput["expertModelConfig"];
douyinRuntimeConfig?: SaveConfigInput["douyinRuntimeConfig"];
}) => {
const nextWorkspacePath = options?.workspacePath;
const nextExpertModelConfig = options?.expertModelConfig;
const nextDouyinRuntimeConfig = options?.douyinRuntimeConfig;
if (typeof nextWorkspacePath === "string") {
setWorkspacePathDraft(nextWorkspacePath);
}
...
...
@@ -2906,6 +2971,21 @@ export default function App() {
setDigitalHumanQiniuAccessKeyDraft(nextExpertModelConfig.digitalHuman.qiniuAccessKey ?? "");
setDigitalHumanQiniuSecretKeyDraft(nextExpertModelConfig.digitalHuman.qiniuSecretKey ?? "");
}
if (nextDouyinRuntimeConfig?.videoAnalyzer) {
setVideoAnalyzerBaseUrlDraft(nextDouyinRuntimeConfig.videoAnalyzer.baseUrl ?? "");
setVideoAnalyzerModelIdDraft(nextDouyinRuntimeConfig.videoAnalyzer.modelId ?? "");
setVideoAnalyzerApiKeyDraft(nextDouyinRuntimeConfig.videoAnalyzer.apiKey ?? "");
}
if (nextDouyinRuntimeConfig?.replicationBrief) {
setReplicationBriefBaseUrlDraft(nextDouyinRuntimeConfig.replicationBrief.baseUrl ?? "");
setReplicationBriefModelIdDraft(nextDouyinRuntimeConfig.replicationBrief.modelId ?? "");
setReplicationBriefApiKeyDraft(nextDouyinRuntimeConfig.replicationBrief.apiKey ?? "");
}
if (nextDouyinRuntimeConfig?.vectcut) {
setVectcutBaseUrlDraft(nextDouyinRuntimeConfig.vectcut.baseUrl ?? "");
setVectcutFileBaseUrlDraft(nextDouyinRuntimeConfig.vectcut.fileBaseUrl ?? "");
setVectcutApiKeyDraft(nextDouyinRuntimeConfig.vectcut.apiKey ?? "");
}
if (typeof options?.lobsterKey === "string") {
setLobsterKeyDraft(options.lobsterKey);
}
...
...
@@ -2913,7 +2993,8 @@ export default function App() {
await saveConfig({
lobsterKey: options?.lobsterKey,
workspacePath: nextWorkspacePath,
expertModelConfig: nextExpertModelConfig
expertModelConfig: nextExpertModelConfig,
douyinRuntimeConfig: nextDouyinRuntimeConfig
});
const latestConfig = await desktopApi.config.load();
...
...
@@ -2926,11 +3007,15 @@ export default function App() {
setDigitalHumanVolcSecretKeyDraft("");
setDigitalHumanQiniuAccessKeyDraft("");
setDigitalHumanQiniuSecretKeyDraft("");
setVideoAnalyzerApiKeyDraft("");
setReplicationBriefApiKeyDraft("");
setVectcutApiKeyDraft("");
await waitForSmokeConfigPublish(latestConfig.expertModelConfig);
return {
workspacePath: latestConfig.workspacePath,
expertModelConfig: latestConfig.expertModelConfig,
douyinRuntimeConfig: latestConfig.douyinRuntimeConfig,
apiKeyConfigured: latestConfig.apiKeyConfigured
};
},
...
...
@@ -2960,7 +3045,7 @@ export default function App() {
suggestion,
prompt: currentPrompt,
skillId: selectedSkill.id === DEFAULT_SKILL.id ? undefined : selectedSkill.id,
attachments: composerAttachment
? [composerAttachment]
: undefined
attachments: composerAttachment
s.length ? composerAttachments
: undefined
} satisfies PendingHomeIntentSuggestion;
setPendingHomeIntentSuggestion(nextPendingSuggestion);
setSkillMenuOpen(false);
...
...
@@ -3431,6 +3516,7 @@ export default function App() {
lobsterKey?: string;
workspacePath?: string;
expertModelConfig?: SaveConfigInput["expertModelConfig"];
douyinRuntimeConfig?: SaveConfigInput["douyinRuntimeConfig"];
successMessage?: string;
}) {
if (!config) {
...
...
@@ -3456,6 +3542,23 @@ export default function App() {
qiniuSecretKey: digitalHumanQiniuSecretKeyDraft.trim() || undefined
}
};
const resolvedDouyinRuntimeConfig = options?.douyinRuntimeConfig ?? {
videoAnalyzer: {
baseUrl: videoAnalyzerBaseUrlDraft.trim() || undefined,
modelId: videoAnalyzerModelIdDraft.trim() || undefined,
apiKey: videoAnalyzerApiKeyDraft.trim() || undefined
},
replicationBrief: {
baseUrl: replicationBriefBaseUrlDraft.trim() || undefined,
modelId: replicationBriefModelIdDraft.trim() || undefined,
apiKey: replicationBriefApiKeyDraft.trim() || undefined
},
vectcut: {
baseUrl: vectcutBaseUrlDraft.trim() || undefined,
fileBaseUrl: vectcutFileBaseUrlDraft.trim() || undefined,
apiKey: vectcutApiKeyDraft.trim() || undefined
}
};
const input: SaveConfigInput = {
setupMode: config.setupMode,
provider: config.provider,
...
...
@@ -3467,6 +3570,7 @@ export default function App() {
runtimeCloudApiBaseUrl: config.runtimeCloudApiBaseUrl.trim(),
runtimeMode: "bundled-runtime",
expertModelConfig: resolvedExpertModelConfig,
douyinRuntimeConfig: resolvedDouyinRuntimeConfig,
...(trimmedLobsterKey ? { apiKey: trimmedLobsterKey } : {})
};
...
...
@@ -3486,6 +3590,9 @@ export default function App() {
setDigitalHumanVolcSecretKeyDraft("");
setDigitalHumanQiniuAccessKeyDraft("");
setDigitalHumanQiniuSecretKeyDraft("");
setVideoAnalyzerApiKeyDraft("");
setReplicationBriefApiKeyDraft("");
setVectcutApiKeyDraft("");
setInfoText(options?.successMessage ?? (trimmedLobsterKey ? ui.saveSuccessPending : ui.saveSuccessApplied));
void refresh(false);
} catch (error) {
...
...
@@ -3585,15 +3692,15 @@ export default function App() {
const trimmedPrompt = promptText.trim();
const attachmentsToSend = forcedAttachments?.length
? forcedAttachments
: composerAttachment
?
[composerAttachment]
: composerAttachment
s.length
?
composerAttachments
: undefined;
if ((!trimmedPrompt && !attachmentsToSend?.length) || sending || saving) {
return;
}
const skillId = requestedSkillId === DEFAULT_SKILL.id ? undefined : requestedSkillId;
const renderedPrompt = trimmedPrompt || (attachmentsToSend?.length ?
`
[
图片
]
$
{
attachmentsToSend
[
0
].
name
}
`
: "");
const renderedPrompt = trimmedPrompt || (attachmentsToSend?.length ?
buildAttachmentPromptSummary(attachmentsToSend)
: "");
const userMessage = buildUserMessage(renderedPrompt);
const assistantMessage = buildAssistantPlaceholder(ui.preparingReply);
...
...
@@ -3744,7 +3851,7 @@ export default function App() {
const skillId = selectedSkill.id === DEFAULT_SKILL.id ? undefined : selectedSkill.id;
const trimmedPrompt = prompt.trim();
const attachmentsToSend = composerAttachment
? [composerAttachment]
: undefined;
const attachmentsToSend = composerAttachment
s.length ? composerAttachments
: undefined;
const shouldSuggestExpert = !options?.skipHomeIntentSuggestion
&& viewMode === "chat"
&& sessionScopeProjectId === HOME_CHAT_PROJECT_ID
...
...
@@ -3873,32 +3980,91 @@ export default function App() {
setPendingHomeIntentSuggestion(null);
}
function resolveComposerAttachmentKind(file: File, localPath: string): ChatAttachment["kind"] | null {
if (file.type.startsWith("image/")) {
return "image";
}
const extension = (file.name ? file.name.slice(file.name.lastIndexOf(".")) : localPath.slice(localPath.lastIndexOf("."))).toLowerCase();
if (IMAGE_ATTACHMENT_EXTENSIONS.has(extension)) {
return "image";
}
if (SUPPORTED_ATTACHMENT_EXTENSIONS.has(extension)) {
return "file";
}
return null;
}
function appendComposerAttachments(nextAttachments: ChatAttachment[]) {
setComposerAttachments((current) => {
const merged = [...current];
const seen = new Set(current.map((attachment) => attachment.localPath.toLowerCase()));
for (const attachment of nextAttachments) {
const key = attachment.localPath.toLowerCase();
if (seen.has(key)) {
continue;
}
seen.add(key);
merged.push(attachment);
}
return merged;
});
}
function buildAttachmentPromptSummary(attachments: ChatAttachment[]): string {
if (attachments.length === 1) {
return `
[
Attachment
]
$
{
attachments
[
0
].
name
}
`;
}
return `
[
Attachment
]
$
{
attachments
.
length
}
files
`;
}
function summarizeAttachmentPrompt(attachments: ChatAttachment[]): string {
if (attachments.length === 1) {
return `
[
附件
]
$
{
attachments
[
0
].
name
}
`;
}
return `
[
附件
]
$
{
attachments
.
length
}
个文件
`;
}
function clearComposerAttachment() {
setComposerAttachment(null);
setErrorText("");
setComposerAttachments([]);
if (attachmentInputRef.current) {
attachmentInputRef.current.value = "";
}
}
function removeComposerAttachment(localPath: string) {
setComposerAttachments((current) => current.filter((attachment) => attachment.localPath !== localPath));
}
function acceptComposerAttachmentFile(file: File) {
const localPath = (file as File & { path?: string }).path?.trim();
if (!localPath) {
setErrorText("当前客户端未提供本地图片路径,无法把图片透传到项目工作区。");
setErrorText("The desktop client did not provide a local file path, so this attachment cannot be sent into the project workspace.");
return;
}
if (!localPath) {
setErrorText("当前客户端未提供本地文件路径,无法把附件透传到项目工作区。");
return;
}
if (file.type && !file.type.startsWith("image/")) {
setErrorText("当前附件只支持图片。");
const kind = resolveComposerAttachmentKind(file, localPath);
if (!kind) {
setErrorText("Supported attachments: images, MP3, PDF, PPT, Excel, Word, CSV, TXT, Markdown, and JSON.");
return;
}
if (!kind) {
setErrorText("当前附件仅支持图片、PDF、PPT、Excel、Word、CSV、TXT、Markdown、JSON。");
return;
}
setErrorText("");
setComposerAttachment(
{
kind
: "image"
,
name: file.name || localPath.split(/[\\/]/).pop() || "
image
",
appendComposerAttachments([
{
kind,
name: file.name || localPath.split(/[\\/]/).pop() || "
attachment
",
mimeType: file.type || "application/octet-stream",
localPath
});
}
]
);
}
function handleComposerDragEnter(event: ReactDragEvent<HTMLFormElement>) {
...
...
@@ -3940,20 +4106,20 @@ export default function App() {
event.preventDefault();
composerDragDepthRef.current = 0;
setIsComposerDragOver(false);
const file
= event.dataTransfer.files[0]
;
if (file
) {
const file
s = Array.from(event.dataTransfer.files)
;
for (const file of files
) {
acceptComposerAttachmentFile(file);
}
}
async function openAttachmentPicker() {
if (window.qjcDesktop) {
const attachment
= await desktopApi.chat.pickImageAttachment
();
if (!attachment) {
const attachment
s = await desktopApi.chat.pickAttachments
();
if (!attachment
s.length
) {
return;
}
setErrorText("");
setComposerAttachment(attachment
);
appendComposerAttachments(attachments
);
return;
}
...
...
@@ -3961,12 +4127,14 @@ export default function App() {
}
function handleAttachmentSelection(event: ChangeEvent<HTMLInputElement>) {
const file
= event.target.files?.[0
];
if (!file) {
const file
s = event.target.files ? Array.from(event.target.files) : [
];
if (!file
s.length
) {
return;
}
acceptComposerAttachmentFile(file);
for (const file of files) {
acceptComposerAttachmentFile(file);
}
event.target.value = "";
return;
...
...
@@ -4505,11 +4673,12 @@ export default function App() {
ref={attachmentInputRef}
className="composer-attachment-input"
type="file"
accept="image/*"
accept={COMPOSER_ATTACHMENT_ACCEPT}
multiple
tabIndex={-1}
onChange={handleAttachmentSelection}
/>
{isComposerDragOver ? <div className="composer-drop-indicator">释放以上传
图片
</div> : null}
{isComposerDragOver ? <div className="composer-drop-indicator">释放以上传
附件
</div> : null}
<div className="composer-surface">
<label className="composer-field">
<textarea
...
...
@@ -4521,19 +4690,21 @@ export default function App() {
className="composer-textarea"
/>
</label>
{composerAttachment ? (
{composerAttachment
s.length
? (
<div className="composer-attachment-strip">
<span className="composer-attachment-chip">
<span className="composer-attachment-chip-label">{composerAttachment.name}</span>
<button type="button" className="composer-attachment-remove" onClick={() => clearComposerAttachment()} aria-label="移除图片附件">
x
</button>
</span>
{composerAttachments.map((attachment) => (
<span key={attachment.localPath} className="composer-attachment-chip">
<span className="composer-attachment-chip-label">{attachment.name}</span>
<button type="button" className="composer-attachment-remove" onClick={() => removeComposerAttachment(attachment.localPath)} aria-label={`
移除附件
$
{
attachment
.
name
}
`}>
x
</button>
</span>
))}
</div>
) : null}
<div className="composer-footer">
<div className="composer-left-tools" ref={skillMenuRef}>
<button type="button" className="attachment-trigger icon-only" disabled={!isBound || sending} onClick={openAttachmentPicker} aria-label="上传
图片" title="上传图片
">
<button type="button" className="attachment-trigger icon-only" disabled={!isBound || sending} onClick={openAttachmentPicker} aria-label="上传
附件" title="上传附件
">
<AttachmentIcon />
</button>
<button type="button" className="skill-trigger" disabled={!isBound} aria-label={ui.skillMenuTitle} aria-expanded={skillMenuOpen} onClick={() => setSkillMenuOpen((current) => !current)}>
...
...
@@ -4865,7 +5036,7 @@ export default function App() {
</div>
<StatusChip tone={workspace?.apiKeyConfigured ? "positive" : "warning"}>{workspace?.apiKeyConfigured ? "已绑定" : "未绑定"}</StatusChip>
</div>
<div className="settings-
field-grid single
">
<div className="settings-
inline-key-row
">
<label className="settings-input-label">
<span className="settings-input-label-text">龙虾密钥</span>
<input
...
...
@@ -4875,9 +5046,7 @@ export default function App() {
onChange={(event) => setLobsterKeyDraft(event.target.value)}
/>
</label>
</div>
<div className="button-row settings-actions">
<button className="settings-primary-button" disabled={saving || !hasPendingLobsterKey} onClick={() => void saveConfig({ lobsterKey: lobsterKeyDraft })}>{saving ? ui.saving : "保存龙虾密钥"}</button>
<button className="settings-primary-button settings-inline-save-button" disabled={saving || !hasPendingLobsterKey} onClick={() => void saveConfig({ lobsterKey: lobsterKeyDraft })}>{saving ? ui.saving : "保存"}</button>
</div>
</div>
</section>
...
...
@@ -4886,15 +5055,13 @@ export default function App() {
<div className="settings-section-headline">
<div>
<span className="settings-section-kicker">诊断与工作区</span>
{/* <h4>当前生效目录</h4> */}
<h4>工作目录</h4>
</div>
<StatusChip tone={hasPendingWorkspacePathChange ? "warning" : "info"}>{hasPendingWorkspacePathChange ? "待保存" : "已同步"}</StatusChip>
</div>
<div className="workspace-directory-card">
<div className="workspace-directory-panel">
<span className="workspace-directory-eyebrow">当前生效目录</span>
<strong className="workspace-directory-path">{displayedWorkspacePath}</strong>
{/* <p className="workspace-directory-hint">项目会从该目录加载;保存后会重新预热工作区。</p> */}
</div>
{hasPendingWorkspacePathChange ? (
<div className="workspace-directory-draft-row">
...
...
@@ -4929,7 +5096,6 @@ export default function App() {
<div className="model-config-card-body">
<div className="settings-field-grid single">
<label className="settings-input-label">
{/* <span className="settings-input-label-text">API Key</span> */}
<input type="password" value={copywritingModelApiKeyDraft} placeholder={config?.expertModelConfig.copywriting.apiKeyConfigured ? "留空则保持当前已保存密钥" : "请输入文案模型 API Key"} onChange={(event) => setCopywritingModelApiKeyDraft(event.target.value)} />
</label>
</div>
...
...
@@ -4946,7 +5112,6 @@ export default function App() {
<div className="model-config-card-body">
<div className="settings-field-grid single">
<label className="settings-input-label">
{/* <span className="settings-input-label-text">API Key</span> */}
<input type="password" value={imageModelApiKeyDraft} placeholder={config?.expertModelConfig.image.apiKeyConfigured ? "留空则保持当前已保存密钥" : "请输入生图模型 API Key"} onChange={(event) => setImageModelApiKeyDraft(event.target.value)} />
</label>
</div>
...
...
@@ -4963,7 +5128,6 @@ export default function App() {
<div className="model-config-card-body">
<div className="settings-field-grid single">
<label className="settings-input-label">
{/* <span className="settings-input-label-text">API Key</span> */}
<input type="password" value={videoModelApiKeyDraft} placeholder={config?.expertModelConfig.video.apiKeyConfigured ? "留空则保持当前已保存密钥" : "请输入视频模型 API Key"} onChange={(event) => setVideoModelApiKeyDraft(event.target.value)} />
</label>
</div>
...
...
@@ -5012,6 +5176,81 @@ export default function App() {
</div>
</div>
</article>
<article className="model-config-card model-config-card-video-analyzer">
<div className="model-config-card-head">
<div>
<strong>Video Analyzer</strong>
<p>用于抖音样本分析阶段。</p>
</div>
<StatusChip tone={config?.douyinRuntimeConfig.videoAnalyzer.apiKeyConfigured ? "positive" : "warning"}>{config?.douyinRuntimeConfig.videoAnalyzer.apiKeyConfigured ? "已配置" : "未配置"}</StatusChip>
</div>
<div className="model-config-card-body">
<div className="settings-field-grid">
<label className="settings-input-label">
<span className="settings-input-label-text">base_url</span>
<input value={videoAnalyzerBaseUrlDraft} placeholder="https://ark.cn-beijing.volces.com/api/v3" onChange={(event) => setVideoAnalyzerBaseUrlDraft(event.target.value)} />
</label>
<label className="settings-input-label">
<span className="settings-input-label-text">model_id</span>
<input value={videoAnalyzerModelIdDraft} placeholder="doubao-vision" onChange={(event) => setVideoAnalyzerModelIdDraft(event.target.value)} />
</label>
<label className="settings-input-label">
<span className="settings-input-label-text">api_key</span>
<input type="password" value={videoAnalyzerApiKeyDraft} placeholder={config?.douyinRuntimeConfig.videoAnalyzer.apiKeyConfigured ? "留空则保持当前已保存密钥" : "请输入 Video Analyzer API Key"} onChange={(event) => setVideoAnalyzerApiKeyDraft(event.target.value)} />
</label>
</div>
</div>
</article>
<article className="model-config-card model-config-card-replication-brief">
<div className="model-config-card-head">
<div>
<strong>Replication Brief</strong>
<p>用于抖音 brief 生成阶段。</p>
</div>
<StatusChip tone={config?.douyinRuntimeConfig.replicationBrief.apiKeyConfigured ? "positive" : "warning"}>{config?.douyinRuntimeConfig.replicationBrief.apiKeyConfigured ? "已配置" : "未配置"}</StatusChip>
</div>
<div className="model-config-card-body">
<div className="settings-field-grid">
<label className="settings-input-label">
<span className="settings-input-label-text">base_url</span>
<input value={replicationBriefBaseUrlDraft} placeholder="https://dashscope.aliyuncs.com/compatible-mode/v1" onChange={(event) => setReplicationBriefBaseUrlDraft(event.target.value)} />
</label>
<label className="settings-input-label">
<span className="settings-input-label-text">model_id</span>
<input value={replicationBriefModelIdDraft} placeholder="qwen-max" onChange={(event) => setReplicationBriefModelIdDraft(event.target.value)} />
</label>
<label className="settings-input-label">
<span className="settings-input-label-text">api_key</span>
<input type="password" value={replicationBriefApiKeyDraft} placeholder={config?.douyinRuntimeConfig.replicationBrief.apiKeyConfigured ? "留空则保持当前已保存密钥" : "请输入 Replication Brief API Key"} onChange={(event) => setReplicationBriefApiKeyDraft(event.target.value)} />
</label>
</div>
</div>
</article>
<article className="model-config-card model-config-card-vectcut">
<div className="model-config-card-head">
<div>
<strong>VectCut</strong>
<p>用于抖音后处理剪辑阶段。</p>
</div>
<StatusChip tone={config?.douyinRuntimeConfig.vectcut.apiKeyConfigured ? "positive" : "warning"}>{config?.douyinRuntimeConfig.vectcut.apiKeyConfigured ? "已配置" : "未配置"}</StatusChip>
</div>
<div className="model-config-card-body">
<div className="settings-field-grid">
<label className="settings-input-label">
<span className="settings-input-label-text">base_url</span>
<input value={vectcutBaseUrlDraft} placeholder="https://open.vectcut.com/cut_jianying" onChange={(event) => setVectcutBaseUrlDraft(event.target.value)} />
</label>
<label className="settings-input-label">
<span className="settings-input-label-text">file_base_url</span>
<input value={vectcutFileBaseUrlDraft} placeholder="https://open.vectcut.com" onChange={(event) => setVectcutFileBaseUrlDraft(event.target.value)} />
</label>
<label className="settings-input-label">
<span className="settings-input-label-text">api_key</span>
<input type="password" value={vectcutApiKeyDraft} placeholder={config?.douyinRuntimeConfig.vectcut.apiKeyConfigured ? "留空则保持当前已保存密钥" : "请输入 VectCut API Key"} onChange={(event) => setVectcutApiKeyDraft(event.target.value)} />
</label>
</div>
</div>
</article>
</div>
<div className="button-row settings-actions">
<button className="settings-primary-button" disabled={saving || !hasPendingModelKeys} onClick={() => void saveConfig()}>{saving ? ui.saving : "保存模型配置"}</button>
...
...
apps/ui/src/styles.css
View file @
f492464b
...
...
@@ -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
,
1
fr
));
grid-template-rows
:
minmax
(
164px
,
0.
72
fr
)
minmax
(
0
,
1.28
fr
);
grid-template-rows
:
minmax
(
164px
,
0.
43
fr
)
minmax
(
0
,
1.57
fr
);
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
1
4px
30px
rgba
(
109
,
124
,
255
,
0.08
),
inset
0
1px
0
rgba
(
255
,
255
,
255
,
0.9
);
box-shadow
:
0
1
0px
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
:
1
4
px
;
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
:
1
0
px
;
border
:
0
;
border
-radius
:
0
;
background
:
transparent
;
box-shadow
:
none
;
}
.settings-section-card-compact
{
...
...
@@ -4274,6 +4246,24 @@ button.secondary {
grid-template-columns
:
1
fr
;
}
.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.92
fr
)
minmax
(
0
,
1.08
fr
);
grid-template-rows
:
repeat
(
3
,
minmax
(
0
,
1
fr
));
align-content
:
stretch
;
overflow-y
:
auto
;
overflow-x
:
hidden
;
padding-right
:
8px
;
grid-template-columns
:
repeat
(
2
,
minmax
(
0
,
1
fr
));
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
,
1
fr
));
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
:
1
fr
;
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
,
1
fr
);
}
.settings-field-grid-digital-human
{
...
...
packages/shared-types/src/index.ts
View file @
f492464b
...
...
@@ -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
>
;
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment