Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions .github/release-drafter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,27 @@ change-title-escapes: '\<*_&'

header: |
<!-- dreamapp-release-header:start -->
## 当前版本
## 当前版本 / Current Version
`v$RESOLVED_VERSION`

## 下载地址
- macOS Apple Silicon: [点击下载](https://github.com/arnoldhao/dreamcreator/releases/download/v$RESOLVED_VERSION/dreamcreator-macos-arm64-$RESOLVED_VERSION.zip)
- macOS Intel: [点击下载](https://github.com/arnoldhao/dreamcreator/releases/download/v$RESOLVED_VERSION/dreamcreator-macos-x64-$RESOLVED_VERSION.zip)
- Windows 安装版: [点击下载](https://github.com/arnoldhao/dreamcreator/releases/download/v$RESOLVED_VERSION/dreamcreator-windows-x64-$RESOLVED_VERSION-installer.exe)
- Windows 便携版: [点击下载](https://github.com/arnoldhao/dreamcreator/releases/download/v$RESOLVED_VERSION/dreamcreator-windows-x64-$RESOLVED_VERSION.zip)
## 下载地址 / Downloads
- macOS Apple Silicon: [点击下载 / Download](https://github.com/arnoldhao/dreamcreator/releases/download/v$RESOLVED_VERSION/dreamcreator-macos-arm64-$RESOLVED_VERSION.zip)
- macOS Intel: [点击下载 / Download](https://github.com/arnoldhao/dreamcreator/releases/download/v$RESOLVED_VERSION/dreamcreator-macos-x64-$RESOLVED_VERSION.zip)
- Windows 安装版 / Installer: [点击下载 / Download](https://github.com/arnoldhao/dreamcreator/releases/download/v$RESOLVED_VERSION/dreamcreator-windows-x64-$RESOLVED_VERSION-installer.exe)
- Windows 便携版 / Portable: [点击下载 / Download](https://github.com/arnoldhao/dreamcreator/releases/download/v$RESOLVED_VERSION/dreamcreator-windows-x64-$RESOLVED_VERSION.zip)

## macOS 首次运行说明
## macOS 首次运行说明 / First Launch on macOS
将 `DreamCreator.app` 移动到“应用程序”目录后,若提示“无法打开”或“已损坏”,请在终端执行:
Move `DreamCreator.app` to the Applications folder. If macOS says the app can't be opened or is damaged, run:

`sudo xattr -rd com.apple.quarantine /Applications/DreamCreator.app`

---
<!-- dreamapp-release-header:end -->

template: |
## 版本变更 / Changelog

$CHANGES

version-resolver:
Expand Down
45 changes: 30 additions & 15 deletions frontend/scripts/audit-i18n.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,18 @@ const rootDir = process.cwd();
const strict = process.argv.includes("--strict");
const jsonOnly = process.argv.includes("--json");
const pruneUnused = process.argv.includes("--prune-unused");
const fixEnglishStyle = process.argv.includes("--fix-english-style");

const localeDir = path.join(rootDir, "src", "shared", "i18n", "locales");
const sourceDir = path.join(rootDir, "src");
const englishStyleSkipKeys = new Set(["app.name"]);
const englishStyleSkipKeys = new Set([
"app.name",
"library.config.subtitleStyles.exportProfileStyleDocument",
"library.workspace.header.videoEditing",
"library.workspace.header.subtitleEditing",
"library.workspace.header.speechToSubtitle",
"library.workspace.dialogs.exportSubtitle.style",
]);
const englishStylePreservePhrases = [
"Dream Creator",
"Apple",
Expand Down Expand Up @@ -148,19 +156,12 @@ const englishTitleCaseLowerWords = new Set([
"via",
]);
const englishSentenceBoundaryChars = new Set([".", "!", "?"]);
// Apple HIG defines title-style capitalization for compact actionable controls like
// buttons and menu items, but uses sentence-style capitalization for labels and most prose.
const englishTitleStyleExplicitPatterns = [
/^app\.settings\.title\./,
/^settings\.gateway\.detailsPanel\.sections\./,
/^settings\.gateway\.detailsPanel\.(contextTabs|httpTabs|talkTabs|heartbeatTabs)\./,
/^settings\.gateway\.detailsPanel\.(gateway\.controlPlane|runtime\.(maxSteps|toolLoop\.(enabled|warn|critical|global|history|detectorGeneric|detectorPoll|detectorPingPong)|contextWarn|contextHard|compactionMode|compactionReserveTokens|compactionKeepRecent|compactionReserveFloor|compactionMaxHistoryShare|compactionMemoryFlushEnabled|compactionMemoryFlushSoft|compactionMemoryFlushPrompt|compactionMemoryFlushSystem)|queue\.(globalConcurrency|sessionConcurrency|laneMain|laneSubagent|laneCron)|cron\.(enabled|maxConcurrentRuns|sessionRetention|runLogMaxBytes|runLogKeepLines)|heartbeat\.(enabled|periodicEnabled|runSession|notificationCenter|openNotifications|triggerLabel|lastStatus\.label|lastNotice\.label|promptAppend|includeReasoning|suppressToolErrorWarnings|activeStart|activeEnd|activeTimezone|trigger|spec\.items|spec\.updatedAt)|subagents\.(maxDepth|maxChildren|maxConcurrent|model)|http\.(maxBodyBytes|maxUrlParts|files\.(urlAllowlist|allowedMimes|maxBytes|maxChars|maxRedirects|pdfMaxPages|pdfMaxPixels|pdfMinTextChars)|images\.(urlAllowlist|allowedMimes|maxBytes|maxRedirects))|channelHealth\.minutes|voice\.(enabled)|voiceWake\.(enabled)|talk\.(voiceAliases|outputFormat|apiKey|interruptOnSpeech)|tts\.(status|provider|voiceId|modelId|format)|voiceWakeTriggers\.label)$/,
/^settings\.gateway\.(change3davatar\.pickTitle|change3dmotion\.pickTitle|changeName\.title|readiness\.(readyTitle|incompleteTitle)|model\.(agentTitle|embeddingTitle|imageTitle))$/,
/^settings\.(memory\.summary\.title|usage\.overview\.title|general\.download\.dialogTitle|general\.proxy\.dialogTitle|general\.advanced\.menuBarVisibility\.label|calls\.skills\.sources\.emptyTitle|calls\.skills\.security\.groups\.(package_write|deps_write|config_write|source_write)\.label|tools\.browserControl\.ssrfSection|skills\.listTitle|connectors\.loginTitle|externalTools\.releaseNotesTitle|provider\.models\.manage\.title|provider\.models\.manage\.invalidTitle|provider\.custom\.title|provider\.delete\.title|about\.advanced\.unlockedTitle)$/,
/^settings\.integration\.channels\.(config\.groups\.columns\.requireMention|reset\.button)$/,
/^cron\.(runs\.detailTitle|dialog\.(deleteTitle|bulkDeleteTitle|editTitle|viewTitle|newTitle)|columns\.|overview\.chart\.title$)/,
/^library\.(columns\.|tools\.optionalTitle|resources\.(libraryInfoTitle|fileInfoTitle|recordInfoTitle|currentRecordsTitle|deleteLibraryTitle)|download\.(title|inputTitle)|task\.(deleteFilesTitle|deleteSuccessTitle|deleteFailedTitle|bulkDeleteSuccessTitle|bulkDeleteFailedTitle|failedCheckTitle)|file\.(deleteFilesTitle|deleteSuccessTitle|deleteFailedTitle|bulkDeleteSuccessTitle|bulkDeleteFailedTitle)|rowMenu\.renameTitle|overview\.chart\.title|preview\.imageTitle|import\.(videoPickerTitle|subtitlePickerTitle|unsupportedTitle|dialog\.targetTitle)|config\.(saveFailedTitle|taskRuntime\.(translateTitle|proofreadTitle)|videoExportPresets\.(savedTitle|saveFailedTitle|deletedTitle|deleteFailedTitle)|subtitleStyles\.(allStylesTitle|createFormTitle|unsavedTitle|importFailedTitle|importGuideTitle|exportSucceededTitle|exportFailedTitle|previewInfoTitle|monoStyleSectionTitle|bilingualMetaSectionTitle|primarySourceTitle|secondarySourceTitle|primaryStyleTitle|secondaryStyleTitle|overviewTitle|defaultsTitle|deliveryReadinessTitle|fontManagementTitle|referencedFontsTitle|styleSourcesTitle|browseSourceFailedTitle|fontSourcesTitle|syncFontSourceSuccessTitle|syncFontSourceFailedTitle|installUserFontSuccessTitle|installUserFontFailedTitle|installMachineFontSuccessTitle|installMachineFontFailedTitle|importSourceItemSuccessTitle|importSourceItemFailedTitle))|workspace\.(emptyTitle|table\.(timelineTitle|editorTitle)|dialogs\.(exportVideo\.(subtitleHandlingTitle|trackMappingTitle)|languageTask\.(title|qaRealtimeTitle|issueBreakdownTitle|restoreOriginalTitle)|importSubtitle\.(title|normalizationTitle|guidelineInheritanceTitle))|preview\.placeholderTitle|waveform\.title|review\.(lockedTitle|applySuccessTitle|applyFailedTitle|discardSuccessTitle|discardFailedTitle)|notifications\.(translationReadyTitle|proofreadReadyTitle|qaReviewReadyTitle|saveFailedTitle|noSubtitleTrackTitle|originalRestoredTitle|restoreFailedTitle|noFileSelectedTitle|openFileFailedTitle|noVideoSelectedTitle|noPresetSelectedTitle|exportQueuedTitle|exportFailedTitle|moduleConfigUnavailableTitle|promptProfileSavedTitle|savePromptProfileFailedTitle|translationQueuedTitle|translationFailedTitle|proofreadQueuedTitle|proofreadFailedTitle|importProfileReadyTitle)))$/,
/^(gateway\.logs\.title|debug\.(status\.title|channels\.title|message\.frontend\.(toastTitle|notificationTitle|dialogTitle)|message\.realtime\.notifyTitle)|chat\.composer\.attachDialogTitle|chat\.welcome\.entry\.items\.(assistant|providers|model|generic)\.action|chat\.tools\.approvalTool\.title|productMode\.options\.(full|download)\.(title|action)|notifications\.footer\.codes\.(appUpdate|externalToolsUpdate)\.title|notifications\.empty\.title)$/,
];
const englishTitleStyleHeuristicPattern = /(\.title$|Title$|\.label$|\.button$|\.action$|columns\.|Tabs\.|sections\.|Section$|dialogTitle$)/;
const englishTitleStyleHeuristicPattern = /(\.button$|\.action$|columns\.|Tabs\.|sections\.|Section$|dialogTitle$|pickTitle$)/;
const englishTitleStyleSentencePattern = /\?|\.{1,3}$|\b(is|are|was|were|am|can't|cannot|need|needs|shows?\s+up|please)\b/i;
const zhGlossaryReplacements = [
["External Tools", "外部工具"],
Expand Down Expand Up @@ -333,9 +334,6 @@ function shouldUseEnglishTitleCase(key, value) {
if (englishTitleStyleSentencePattern.test(value)) {
return false;
}
if (/^chat\.welcome\.entry\./.test(key) || /^notifications\.center\.codes\./.test(key) || /^productMode\.title$/.test(key)) {
return false;
}
return true;
}

Expand Down Expand Up @@ -471,7 +469,24 @@ function filterLocaleTree(input, usedSet, prefix = "") {
return output;
}

const enSource = JSON.parse(fs.readFileSync(path.join(localeDir, "en.json"), "utf8"));
function mapLocaleTree(input, mapper, prefix = "") {
const output = {};
for (const [key, value] of Object.entries(input)) {
const nextKey = prefix ? `${prefix}.${key}` : key;
if (value && typeof value === "object" && !Array.isArray(value)) {
output[key] = mapLocaleTree(value, mapper, nextKey);
continue;
}
output[key] = mapper(nextKey, String(value));
}
return output;
}

let enSource = JSON.parse(fs.readFileSync(path.join(localeDir, "en.json"), "utf8"));
if (fixEnglishStyle) {
enSource = mapLocaleTree(enSource, normalizeEnglishLocaleValue);
fs.writeFileSync(path.join(localeDir, "en.json"), `${JSON.stringify(enSource, null, 2)}\n`);
}
const zhSource = JSON.parse(fs.readFileSync(path.join(localeDir, "zh-CN.json"), "utf8"));
const en = flatten(enSource);
const zh = flatten(zhSource);
Expand Down
40 changes: 33 additions & 7 deletions frontend/src/app/ws/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ export class WebSocketClient {
if (this.reconnectTimer || !this.shouldReconnect) {
return;
}
this.onMetric?.("reconnect");
this.emitMetric("reconnect");
this.reconnectTimer = window.setTimeout(() => {
this.reconnectTimer = null;
this.connect();
Expand Down Expand Up @@ -235,35 +235,45 @@ export class WebSocketClient {
if (seq > 0) {
const lastSeq = this.topicSeq.get(realtimeEvent.topic) ?? 0;
if (seq <= lastSeq) {
this.onMetric?.("duplicate-drop");
this.emitMetric("duplicate-drop");
return;
}
this.topicSeq.set(realtimeEvent.topic, seq);
}

if (realtimeEvent.replay) {
this.onMetric?.("replay");
this.emitMetric("replay");
}
if ((realtimeEvent.type ?? "").trim().toLowerCase() === "resync-required") {
this.onMetric?.("resync-required");
this.emitMetric("resync-required");
}

this.onMessage?.(realtimeEvent);
this.emitMessage(realtimeEvent);

const handlers = this.subscriptions.get(realtimeEvent.topic);
if (!handlers || handlers.size === 0) {
return;
}

handlers.forEach((handler) => handler(realtimeEvent));
handlers.forEach((handler) => {
try {
handler(realtimeEvent);
} catch (error) {
console.error("[ws] subscription handler failed", error);
}
});
}

private setStatus(status: ConnectionStatus) {
if (this.status === status) {
return;
}
this.status = status;
this.onStatusChange?.(status);
try {
this.onStatusChange?.(status);
} catch (error) {
console.error("[ws] status handler failed", error);
}
}

private clearPending(reason: Error) {
Expand All @@ -273,4 +283,20 @@ export class WebSocketClient {
});
this.pending.clear();
}

private emitMetric(kind: "reconnect" | "replay" | "resync-required" | "duplicate-drop") {
try {
this.onMetric?.(kind);
} catch (error) {
console.error("[ws] metric handler failed", error);
}
}

private emitMessage(event: RealtimeEvent) {
try {
this.onMessage?.(event);
} catch (error) {
console.error("[ws] message handler failed", error);
}
}
}
49 changes: 31 additions & 18 deletions frontend/src/features/chat/components/ComposerBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,9 @@ const assistantSelectLabel = (assistant: Assistant, fallback: string) => {
return emoji ? `${emoji} ${name}` : name;
};

const assistantSelectEmoji = (assistant: Assistant | null | undefined) =>
assistant?.identity?.emoji?.trim() || "";

function ThemedProviderIcon({ icon, className }: { icon: string; className?: string }) {
return (
<span
Expand Down Expand Up @@ -532,6 +535,8 @@ function AssistantModelDropdown({
const selectedAssistantLabel = selectedAssistant
? assistantSelectLabel(selectedAssistant, t("chat.composer.untitledAssistant"))
: t("chat.composer.selectAssistant");
const selectedAssistantEmoji = assistantSelectEmoji(selectedAssistant);
const selectedAssistantTriggerText = selectedAssistantEmoji || selectedAssistantLabel;
return (
<AuiSelectRoot
value={assistantId}
Expand All @@ -542,10 +547,18 @@ function AssistantModelDropdown({
variant="ghost"
size="sm"
title={selectedAssistantLabel}
className="w-fit min-w-0 max-w-full focus-visible:ring-0 focus-visible:ring-offset-0"
aria-label={selectedAssistantLabel}
className="w-fit min-w-0 max-w-full focus-visible:ring-0 focus-visible:ring-offset-0 [&>svg]:hidden"
style={ASSISTANT_TRIGGER_MAX_WIDTH_STYLE}
>
<span className="min-w-0 truncate">{selectedAssistantLabel}</span>
<span
className={cn(
"min-w-0 truncate",
selectedAssistantEmoji ? "text-base leading-none" : undefined
)}
>
{selectedAssistantTriggerText}
</span>
</AuiSelectTrigger>
<AuiSelectContent align="center" className="max-w-[320px]">
{assistants.length === 0 ? (
Expand Down Expand Up @@ -577,7 +590,7 @@ function AssistantModelDropdown({
variant="ghost"
size="sm"
title={selectedModelLabel}
className="w-fit min-w-0 max-w-full focus-visible:ring-0 focus-visible:ring-offset-0"
className="w-fit min-w-0 max-w-full focus-visible:ring-0 focus-visible:ring-offset-0 [&>svg]:hidden"
style={MODEL_TRIGGER_MAX_WIDTH_STYLE}
disabled={disabled || !selectedAssistant || updateAssistant.isPending}
>
Expand Down Expand Up @@ -623,13 +636,13 @@ function PermissionModeSelector({
variant="ghost"
size="sm"
title={title}
className="w-fit min-w-0"
aria-label={title}
className="w-fit min-w-0 [&>svg]:hidden"
style={PERMISSION_TRIGGER_MAX_WIDTH_STYLE}
>
<div className="flex min-w-0 items-center gap-1.5 whitespace-nowrap">
<span className="inline-flex items-center justify-center">
<selected.Icon className="size-3.5 text-muted-foreground" />
<span className="truncate">{selected.label}</span>
</div>
</span>
</AuiSelectTrigger>
<AuiSelectContent align="center" className="max-w-[260px]">
{options.map((option) => (
Expand Down Expand Up @@ -976,17 +989,6 @@ export function ComposerBar({
disabled={isRunning}
mode="assistant"
/>
<AssistantModelDropdown
assistants={assistants}
assistantId={assistantId}
selectedAssistant={selectedAssistant}
setAssistantId={setAssistantId}
modelGroups={modelGroups}
agentProviderId={agentProviderId}
agentModelName={agentModelName}
disabled={isRunning}
mode="model"
/>
<div className="shrink-0">
<PermissionModeSelector
value={execPermissionMode}
Expand Down Expand Up @@ -1029,6 +1031,17 @@ export function ComposerBar({
</TooltipProvider>
</div>
) : null}
<AssistantModelDropdown
assistants={assistants}
assistantId={assistantId}
selectedAssistant={selectedAssistant}
setAssistantId={setAssistantId}
modelGroups={modelGroups}
agentProviderId={agentProviderId}
agentModelName={agentModelName}
disabled={isRunning}
mode="model"
/>
</div>

<div className="flex shrink-0 items-center gap-2">
Expand Down
Loading
Loading