feat: add desktop agent workbench

This commit is contained in:
boos
2026-06-26 13:08:00 +08:00
parent bc99c678f9
commit 424aaa9b1f
28 changed files with 1247 additions and 71 deletions
+128
View File
@@ -0,0 +1,128 @@
name: Desktop CI Build
on:
workflow_dispatch:
push:
branches:
- main
- feat/embed-desktop
paths:
- ".github/workflows/desktop-ci-build.yml"
- "desktop/**"
permissions:
contents: read
env:
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short
NODE_OPTIONS: "--max_old_space_size=4096"
concurrency:
group: "${{ github.workflow }}-${{ github.ref }}"
cancel-in-progress: true
jobs:
build:
name: Build ${{ matrix.name }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- name: macOS ARM64 DMG
os: macos-14
target: aarch64-apple-darwin
artifact_name: BoosAPI-Desktop-macos-arm64-dmg
artifact_path: desktop/src-tauri/target/aarch64-apple-darwin/release/bundle/dmg/*.dmg
tauri_args: --target aarch64-apple-darwin --bundles dmg
- name: Windows x64
os: windows-latest
target: x86_64-pc-windows-msvc
artifact_name: BoosAPI-Desktop-windows-x64
artifact_path: |
desktop/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi
desktop/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe
desktop/src-tauri/target/x86_64-pc-windows-msvc/release/BoosApi.exe
tauri_args: --target x86_64-pc-windows-msvc
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Rust cache
uses: Swatinem/rust-cache@v2
with:
prefix-key: "v1"
key: "${{ runner.os }}-${{ matrix.target }}-desktop-bun"
workspaces: desktop/src-tauri -> target
- name: Install Protoc
uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Get linked dependency versions
id: dep-versions
shell: bash
working-directory: desktop
run: |
SM_SHA=$(git -C libs/stream-monaco rev-parse HEAD)
MS_SHA=$(git -C libs/markstream-vue rev-parse HEAD)
echo "cache-key=${{ runner.os }}-${{ matrix.target }}-desktop-linked-deps-${SM_SHA:0:12}-${MS_SHA:0:12}" >> "$GITHUB_OUTPUT"
- name: Cache linked dependencies
id: cache-linked-deps
uses: actions/cache@v4
with:
path: |
desktop/libs/stream-monaco/node_modules
desktop/libs/stream-monaco/dist
desktop/libs/markstream-vue/node_modules
desktop/libs/markstream-vue/packages/markdown-parser/node_modules
desktop/libs/markstream-vue/packages/markdown-parser/dist
desktop/libs/markstream-vue/packages/markstream-react/node_modules
desktop/libs/markstream-vue/packages/markstream-react/dist
key: ${{ steps.dep-versions.outputs.cache-key }}
- name: Build linked dependencies
if: steps.cache-linked-deps.outputs.cache-hit != 'true'
shell: bash
working-directory: desktop
run: |
bun --cwd libs/stream-monaco install
bun --cwd libs/stream-monaco run build
bun --cwd libs/markstream-vue install
bun --cwd libs/markstream-vue run build:parser
bun --cwd libs/markstream-vue/packages/markstream-react run build
- name: Install frontend dependencies
working-directory: desktop
run: bun install
- name: Build Desktop
working-directory: desktop
env:
APPLE_SIGNING_IDENTITY: ${{ startsWith(matrix.os, 'macos') && '-' || '' }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: bun run tauri build ${{ matrix.tauri_args }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact_name }}
path: ${{ matrix.artifact_path }}
if-no-files-found: error
@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
@@ -0,0 +1,44 @@
# Desktop Agent Workbench Integration Design
## Scope
This deliverable adds the desktop UI shell and local interactions for Agent Workbench, and connects Agent mode to the existing desktop Agent command path. It intentionally avoids new Rust commands, database tables, billing, and remote media generation until the page contract is stable.
## Frontend Boundaries
- `types/index.ts` owns the `PageKey` addition.
- `components/layout/Sidebar.tsx` owns navigation entry placement.
- `components/layout/ContentArea.tsx` owns routing from `PageKey` to page component.
- `pages/AgentWorkbenchPage.tsx` owns the creator composer, local attachment state, mode state, model selection, Agent handoff, and placeholder result/status for media modes.
- `stores/providerStore.ts` remains the source of provider/model data. The page reads `providers` and calls `fetchProviders`; it does not duplicate provider fetching logic.
- `stores/conversationStore.ts` remains the source for conversation creation and Agent streaming. The Workbench reuses `createConversation`, `updateConversation`, and `sendAgentMessage`.
- `stores/skillStore.ts` remains the source for skill installation/listing. The Workbench reuses `loadSkills`, `installSkill`, and `toggleSkill` for third-party skills.
- `lib/browserMock.ts` mirrors skill install/list/toggle enough for browser preview.
- `i18n/locales/*.json` owns navigation labels. The first page UI can use Chinese literal copy to match current product direction, but nav should use locale keys.
## Data Flow
Provider store -> enabled providers -> enabled chat models -> AgentWorkbenchPage model options -> selected provider/model or custom model fallback.
File input -> browser `File` objects -> base64 `AttachmentInput` payload -> local attachment chips -> existing Agent attachment persistence during `agent_query`.
Mode selector -> local `creationMode` -> mode label/model controls/result copy.
Agent submit -> create conversation -> set `mode: "agent"` -> upsert Agent session via `agent_update_session` -> `sendAgentMessage` -> Chat page streaming display.
Non-Agent submit -> local result/status card. No external side effects yet.
Skills menu -> detect `jianying-editor` -> if missing install `luoluoluo22/jianying-editor-skill` via existing `install_skill` command -> if disabled enable it -> insert `请使用 jianying-editor Skill 自动创建剪映草稿:` into the prompt -> Agent submit loads enabled skills in the backend and can invoke the skill tool.
## Compatibility
Existing pages and settings should be unaffected. Adding a `PageKey` requires exhaustive switch and sidebar updates in the same change.
## Follow-up Hooks
Future work can replace the non-Agent local submit handler with:
- BoosAPI `/api/agent/models` for remote model catalog.
- BoosAPI `/api/jimeng/*` for image/video generation.
- Tauri file staging command for persistent uploads.
- A dedicated JianYing setup/status panel for Python dependency checks, JianYing draft path detection, and platform/version warnings.
@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
@@ -0,0 +1,23 @@
# Implementation Plan
1. Add `agent-workbench` to desktop page types, sidebar nav, content routing, and nav locale files.
2. Create `AgentWorkbenchPage.tsx` with:
- mode definitions
- provider-derived model options
- custom model fallback
- local attachment selection/removal with `AttachmentInput` conversion
- mode dropdown, auto toggle, skills and subject popovers
- Agent mode submit that creates an Agent conversation/session and calls existing streaming flow
- skills menu action for `luoluoluo22/jianying-editor-skill` install/enable and prompt priming
- placeholder submit status/result for media modes
3. Extend browser preview mock skill commands so install/list/toggle reflects the JianYing skill integration.
4. Add focused CSS to `desktop/src/index.css`, using restrained desktop styling and avoiding global side effects.
5. Run validation:
- `bun run typecheck`
- `bun run build`
6. Review diff for unrelated churn before final report.
## Validation
- `bun run typecheck` passed in `/root/metapi/desktop` after JianYing skill integration.
- `bun run build` passed in `/root/metapi/desktop` after JianYing skill integration.
@@ -0,0 +1,34 @@
# Desktop Agent Workbench integration
## Goal
Add a first usable Agent Workbench entry to the desktop client so users can start from the same creator-style surface used in the BoosAPI web UI: choose a creation mode, select a model, attach references, and type a prompt.
## Requirements
- Add an Agent Workbench page to the desktop navigation without disrupting existing Chat, Drawing, Skills, Knowledge, Memory, Gateway, Files, or Settings flows.
- The first screen should match the creator-composer direction: centered prompt, large input surface, upload affordance, creation mode selector, auto toggle, model selector, skills button, subject button, and submit button.
- Creation modes must include at least Agent, image generation, video generation, music generation, voice generation, digital human, and action imitation. Unsupported modes may show “coming soon” copy but must remain selectable.
- Agent mode should expose a conversation model selector populated from existing enabled desktop provider models where possible, with a custom model fallback.
- Upload affordance should accept local reference files, display removable attachment chips, and pass attachments into existing Agent chat flow when Agent mode submits.
- The page must follow the desktop app’s existing React/Ant Design/Zustand conventions and be reachable from the sidebar.
- The implementation must avoid new backend schema. Existing Tauri commands may be reused.
- The Workbench skills menu should expose a JianYing/剪映 automation entry that installs/enables `luoluoluo22/jianying-editor-skill` through the existing Desktop skills system and primes the Agent prompt.
## Acceptance Criteria
- [x] Sidebar contains an Agent Workbench entry and selecting it renders the new page.
- [x] Agent Workbench page has a centered creator composer with mode dropdown, model control, upload button, `@` subject popover, and submit button.
- [x] Available chat models are derived from existing enabled desktop providers/models, with a custom model option.
- [x] Selecting a mode updates the composer label and mode-specific model controls where applicable.
- [x] Local file selection displays attachment chips and supports removing them.
- [x] Agent mode creates an Agent conversation/session and sends the prompt plus attachments through the existing Agent stream flow.
- [x] Skills menu can install/enable the JianYing editor skill and insert a matching Agent prompt prefix.
- [x] Unsupported submit paths do not crash; they show a clear in-page result/status.
- [x] `bun run typecheck` and `bun run build` pass in `/root/metapi/desktop`.
## Notes
- Keep `prd.md` focused on requirements, constraints, and acceptance criteria.
- Lightweight tasks can remain PRD-only.
- For complex tasks, add `design.md` for technical design and `implement.md` for execution planning before `task.py start`.
@@ -0,0 +1,26 @@
{
"id": "desktop-agent-workbench",
"name": "desktop-agent-workbench",
"title": "Desktop Agent Workbench integration",
"description": "",
"status": "in_progress",
"dev_type": null,
"scope": null,
"package": "vendor/ComfyUI_frontend",
"priority": "P2",
"creator": "boos4721",
"assignee": "boos4721",
"createdAt": "2026-06-25",
"completedAt": null,
"branch": null,
"base_branch": "feat/embed-desktop",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}
+20 -26
View File
@@ -134,13 +134,10 @@ jobs:
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-node@v4 - name: Setup Bun
uses: oven-sh/setup-bun@v2
with: with:
node-version: '20' bun-version: latest
- uses: pnpm/action-setup@v4
with:
run_install: false
- name: Get linked dependency versions - name: Get linked dependency versions
id: dep-versions id: dep-versions
@@ -168,15 +165,15 @@ jobs:
if: steps.cache-linked-deps.outputs.cache-hit != 'true' if: steps.cache-linked-deps.outputs.cache-hit != 'true'
shell: bash shell: bash
run: | run: |
pnpm --dir libs/stream-monaco install --frozen-lockfile bun --cwd libs/stream-monaco install --frozen-lockfile
pnpm --dir libs/stream-monaco build bun --cwd libs/stream-monaco run build
pnpm --dir libs/markstream-vue install --frozen-lockfile bun --cwd libs/markstream-vue install --frozen-lockfile
pnpm --dir libs/markstream-vue run build:parser bun --cwd libs/markstream-vue run build:parser
pnpm --dir libs/markstream-vue/packages/markstream-react build bun --cwd libs/markstream-vue/packages/markstream-react run build
- name: Install frontend dependencies - name: Install frontend dependencies
run: pnpm install --frozen-lockfile run: bun install --frozen-lockfile
- name: Build with Tauri - name: Build with Tauri
uses: tauri-apps/tauri-action@v0 uses: tauri-apps/tauri-action@v0
@@ -192,7 +189,7 @@ jobs:
releaseBody: ${{ needs.draft-release-notes.outputs.body }} releaseBody: ${{ needs.draft-release-notes.outputs.body }}
releaseDraft: true releaseDraft: true
prerelease: ${{ contains(github.ref_name, '-rc') }} prerelease: ${{ contains(github.ref_name, '-rc') }}
tauriScript: pnpm tauri tauriScript: bun run tauri
args: --target ${{ matrix.target }} args: --target ${{ matrix.target }}
includeUpdaterJson: true includeUpdaterJson: true
@@ -268,13 +265,10 @@ jobs:
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-node@v4 - name: Setup Bun
uses: oven-sh/setup-bun@v2
with: with:
node-version: '20' bun-version: latest
- uses: pnpm/action-setup@v4
with:
run_install: false
- name: Get linked dependency versions - name: Get linked dependency versions
id: dep-versions id: dep-versions
@@ -300,22 +294,22 @@ jobs:
- name: Build linked dependencies - name: Build linked dependencies
if: steps.cache-linked-deps.outputs.cache-hit != 'true' if: steps.cache-linked-deps.outputs.cache-hit != 'true'
run: | run: |
pnpm --dir libs/stream-monaco install --frozen-lockfile bun --cwd libs/stream-monaco install --frozen-lockfile
pnpm --dir libs/stream-monaco build bun --cwd libs/stream-monaco run build
pnpm --dir libs/markstream-vue install --frozen-lockfile bun --cwd libs/markstream-vue install --frozen-lockfile
pnpm --dir libs/markstream-vue run build:parser bun --cwd libs/markstream-vue run build:parser
pnpm --dir libs/markstream-vue/packages/markstream-react build bun --cwd libs/markstream-vue/packages/markstream-react run build
- name: Install frontend dependencies - name: Install frontend dependencies
run: pnpm install --frozen-lockfile run: bun install --frozen-lockfile
- name: Build for Linux ARM64 - name: Build for Linux ARM64
run: | run: |
export PKG_CONFIG_ALLOW_CROSS=1 export PKG_CONFIG_ALLOW_CROSS=1
export PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/:$PKG_CONFIG_PATH export PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/:$PKG_CONFIG_PATH
export PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/ export PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/
pnpm tauri build --target aarch64-unknown-linux-gnu --bundles deb,rpm bun run tauri build --target aarch64-unknown-linux-gnu --bundles deb,rpm
env: env:
NODE_OPTIONS: '--max_old_space_size=4096' NODE_OPTIONS: '--max_old_space_size=4096'
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
+10 -13
View File
@@ -41,13 +41,10 @@ jobs:
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-node@v4 - name: Setup Bun
uses: oven-sh/setup-bun@v2
with: with:
node-version: '20' bun-version: latest
- uses: pnpm/action-setup@v4
with:
run_install: false
- name: Get linked dependency versions - name: Get linked dependency versions
id: dep-versions id: dep-versions
@@ -73,15 +70,15 @@ jobs:
- name: Build linked dependencies - name: Build linked dependencies
if: steps.cache-linked-deps.outputs.cache-hit != 'true' if: steps.cache-linked-deps.outputs.cache-hit != 'true'
run: | run: |
pnpm --dir libs/stream-monaco install --frozen-lockfile bun --cwd libs/stream-monaco install --frozen-lockfile
pnpm --dir libs/stream-monaco build bun --cwd libs/stream-monaco run build
pnpm --dir libs/markstream-vue install --frozen-lockfile bun --cwd libs/markstream-vue install --frozen-lockfile
pnpm --dir libs/markstream-vue run build:parser bun --cwd libs/markstream-vue run build:parser
pnpm --dir libs/markstream-vue/packages/markstream-react build bun --cwd libs/markstream-vue/packages/markstream-react run build
- name: Install frontend dependencies - name: Install frontend dependencies
run: pnpm install --frozen-lockfile run: bun install --frozen-lockfile
- name: Build with Tauri - name: Build with Tauri
uses: tauri-apps/tauri-action@v0 uses: tauri-apps/tauri-action@v0
@@ -97,5 +94,5 @@ jobs:
releaseBody: 'Internal test build for scrollbar fix verification.' releaseBody: 'Internal test build for scrollbar fix verification.'
releaseDraft: true releaseDraft: true
prerelease: true prerelease: true
tauriScript: pnpm tauri tauriScript: bun run tauri
args: --target aarch64-apple-darwin args: --target aarch64-apple-darwin
+10 -13
View File
@@ -51,13 +51,10 @@ jobs:
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-node@v4 - name: Setup Bun
uses: oven-sh/setup-bun@v2
with: with:
node-version: '20' bun-version: latest
- uses: pnpm/action-setup@v4
with:
run_install: false
- name: Get linked dependency versions - name: Get linked dependency versions
id: dep-versions id: dep-versions
@@ -84,18 +81,18 @@ jobs:
- name: Build linked dependencies - name: Build linked dependencies
if: steps.cache-linked-deps.outputs.cache-hit != 'true' if: steps.cache-linked-deps.outputs.cache-hit != 'true'
run: | run: |
pnpm --dir libs/stream-monaco install --frozen-lockfile bun --cwd libs/stream-monaco install --frozen-lockfile
pnpm --dir libs/stream-monaco build bun --cwd libs/stream-monaco run build
pnpm --dir libs/markstream-vue install --frozen-lockfile bun --cwd libs/markstream-vue install --frozen-lockfile
pnpm --dir libs/markstream-vue run build:parser bun --cwd libs/markstream-vue run build:parser
pnpm --dir libs/markstream-vue/packages/markstream-react build bun --cwd libs/markstream-vue/packages/markstream-react run build
- name: Install frontend dependencies - name: Install frontend dependencies
run: pnpm install --frozen-lockfile run: bun install --frozen-lockfile
- name: Build Linux bundles - name: Build Linux bundles
run: pnpm tauri build --target x86_64-unknown-linux-gnu --bundles appimage,rpm,deb run: bun run tauri build --target x86_64-unknown-linux-gnu --bundles appimage,rpm,deb
env: env:
NODE_OPTIONS: '--max_old_space_size=4096' NODE_OPTIONS: '--max_old_space_size=4096'
BOOSAPI_TEST_BUILD: '1' BOOSAPI_TEST_BUILD: '1'
+10 -13
View File
@@ -42,13 +42,10 @@ jobs:
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-node@v4 - name: Setup Bun
uses: oven-sh/setup-bun@v2
with: with:
node-version: '20' bun-version: latest
- uses: pnpm/action-setup@v4
with:
run_install: false
- name: Get linked dependency versions - name: Get linked dependency versions
id: dep-versions id: dep-versions
@@ -76,15 +73,15 @@ jobs:
if: steps.cache-linked-deps.outputs.cache-hit != 'true' if: steps.cache-linked-deps.outputs.cache-hit != 'true'
shell: bash shell: bash
run: | run: |
pnpm --dir libs/stream-monaco install --frozen-lockfile bun --cwd libs/stream-monaco install --frozen-lockfile
pnpm --dir libs/stream-monaco build bun --cwd libs/stream-monaco run build
pnpm --dir libs/markstream-vue install --frozen-lockfile bun --cwd libs/markstream-vue install --frozen-lockfile
pnpm --dir libs/markstream-vue run build:parser bun --cwd libs/markstream-vue run build:parser
pnpm --dir libs/markstream-vue/packages/markstream-react build bun --cwd libs/markstream-vue/packages/markstream-react run build
- name: Install frontend dependencies - name: Install frontend dependencies
run: pnpm install --frozen-lockfile run: bun install --frozen-lockfile
- name: Build with Tauri - name: Build with Tauri
uses: tauri-apps/tauri-action@v0 uses: tauri-apps/tauri-action@v0
@@ -98,7 +95,7 @@ jobs:
releaseName: 'BoosApi Test ${{ github.ref_name }}' releaseName: 'BoosApi Test ${{ github.ref_name }}'
releaseDraft: true releaseDraft: true
prerelease: true prerelease: true
tauriScript: pnpm tauri tauriScript: bun run tauri
args: --target x86_64-pc-windows-msvc args: --target x86_64-pc-windows-msvc
- name: Package Windows portable (zip) - name: Package Windows portable (zip)
@@ -1,4 +1,5 @@
import type { PageKey } from '@/types'; import type { PageKey } from '@/types';
import { AgentWorkbenchPage } from '@/pages/AgentWorkbenchPage';
import { ChatPage } from '@/pages/ChatPage'; import { ChatPage } from '@/pages/ChatPage';
import { DrawingPage } from '@/pages/DrawingPage'; import { DrawingPage } from '@/pages/DrawingPage';
import { KnowledgePage } from '@/pages/KnowledgePage'; import { KnowledgePage } from '@/pages/KnowledgePage';
@@ -16,6 +17,8 @@ export function ContentArea({ activePage }: ContentAreaProps) {
switch (activePage) { switch (activePage) {
case 'chat': case 'chat':
return <ChatPage />; return <ChatPage />;
case 'agent-workbench':
return <AgentWorkbenchPage />;
case 'drawing': case 'drawing':
return <DrawingPage />; return <DrawingPage />;
case 'knowledge': case 'knowledge':
+2 -1
View File
@@ -1,6 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
import { Tooltip, Avatar, theme } from 'antd'; import { Tooltip, Avatar, theme } from 'antd';
import { MessageSquare, BookOpen, Brain, Router, FolderOpen, User, Sparkles, ImagePlus } from 'lucide-react'; import { MessageSquare, BookOpen, Brain, Router, FolderOpen, User, Sparkles, ImagePlus, WandSparkles } from 'lucide-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useUIStore, useSettingsStore } from '@/stores'; import { useUIStore, useSettingsStore } from '@/stores';
import { useUserProfileStore } from '@/stores/userProfileStore'; import { useUserProfileStore } from '@/stores/userProfileStore';
@@ -12,6 +12,7 @@ import type { PageKey } from '@/types';
const mainNavItems: { key: PageKey; icon: React.ReactNode; labelKey: string }[] = [ const mainNavItems: { key: PageKey; icon: React.ReactNode; labelKey: string }[] = [
{ key: 'chat', icon: <MessageSquare size={18} />, labelKey: 'nav.chat' }, { key: 'chat', icon: <MessageSquare size={18} />, labelKey: 'nav.chat' },
{ key: 'agent-workbench', icon: <WandSparkles size={18} />, labelKey: 'nav.agentWorkbench' },
{ key: 'drawing', icon: <ImagePlus size={18} />, labelKey: 'nav.drawing' }, { key: 'drawing', icon: <ImagePlus size={18} />, labelKey: 'nav.drawing' },
{ key: 'skills', icon: <Sparkles size={18} />, labelKey: 'nav.skills' }, { key: 'skills', icon: <Sparkles size={18} />, labelKey: 'nav.skills' },
{ key: 'knowledge', icon: <BookOpen size={18} />, labelKey: 'nav.knowledge' }, { key: 'knowledge', icon: <BookOpen size={18} />, labelKey: 'nav.knowledge' },
+1
View File
@@ -4,6 +4,7 @@
}, },
"nav": { "nav": {
"chat": "المحادثة", "chat": "المحادثة",
"agentWorkbench": "Agent",
"knowledge": "قاعدة المعرفة", "knowledge": "قاعدة المعرفة",
"memory": "الذاكرة", "memory": "الذاكرة",
"gateway": "البوابة", "gateway": "البوابة",
+1
View File
@@ -4,6 +4,7 @@
}, },
"nav": { "nav": {
"chat": "Chat", "chat": "Chat",
"agentWorkbench": "Agent",
"knowledge": "Wissen", "knowledge": "Wissen",
"memory": "Gedächtnis", "memory": "Gedächtnis",
"gateway": "Gateway", "gateway": "Gateway",
+1
View File
@@ -4,6 +4,7 @@
}, },
"nav": { "nav": {
"chat": "Chat", "chat": "Chat",
"agentWorkbench": "Agent",
"knowledge": "Knowledge", "knowledge": "Knowledge",
"memory": "Memory", "memory": "Memory",
"gateway": "Gateway", "gateway": "Gateway",
+1
View File
@@ -4,6 +4,7 @@
}, },
"nav": { "nav": {
"chat": "Chat", "chat": "Chat",
"agentWorkbench": "Agent",
"knowledge": "Base de conocimientos", "knowledge": "Base de conocimientos",
"memory": "Memoria", "memory": "Memoria",
"gateway": "Pasarela", "gateway": "Pasarela",
+1
View File
@@ -4,6 +4,7 @@
}, },
"nav": { "nav": {
"chat": "Chat", "chat": "Chat",
"agentWorkbench": "Agent",
"knowledge": "Connaissances", "knowledge": "Connaissances",
"memory": "Mémoire", "memory": "Mémoire",
"gateway": "Passerelle", "gateway": "Passerelle",
+1
View File
@@ -4,6 +4,7 @@
}, },
"nav": { "nav": {
"chat": "चैट", "chat": "चैट",
"agentWorkbench": "Agent",
"knowledge": "ज्ञान आधार", "knowledge": "ज्ञान आधार",
"memory": "स्मृति", "memory": "स्मृति",
"gateway": "गेटवे", "gateway": "गेटवे",
+1
View File
@@ -4,6 +4,7 @@
}, },
"nav": { "nav": {
"chat": "チャット", "chat": "チャット",
"agentWorkbench": "Agent",
"knowledge": "ナレッジ", "knowledge": "ナレッジ",
"memory": "メモリ", "memory": "メモリ",
"gateway": "ゲートウェイ", "gateway": "ゲートウェイ",
+1
View File
@@ -4,6 +4,7 @@
}, },
"nav": { "nav": {
"chat": "채팅", "chat": "채팅",
"agentWorkbench": "Agent",
"knowledge": "지식", "knowledge": "지식",
"memory": "메모리", "memory": "메모리",
"gateway": "게이트웨이", "gateway": "게이트웨이",
+1
View File
@@ -4,6 +4,7 @@
}, },
"nav": { "nav": {
"chat": "Чат", "chat": "Чат",
"agentWorkbench": "Agent",
"knowledge": "База знаний", "knowledge": "База знаний",
"memory": "Память", "memory": "Память",
"gateway": "Шлюз", "gateway": "Шлюз",
+1
View File
@@ -4,6 +4,7 @@
}, },
"nav": { "nav": {
"chat": "对话", "chat": "对话",
"agentWorkbench": "Agent",
"knowledge": "知识库", "knowledge": "知识库",
"memory": "记忆", "memory": "记忆",
"gateway": "网关", "gateway": "网关",
+1
View File
@@ -4,6 +4,7 @@
}, },
"nav": { "nav": {
"chat": "對話", "chat": "對話",
"agentWorkbench": "Agent",
"knowledge": "知識庫", "knowledge": "知識庫",
"memory": "記憶", "memory": "記憶",
"gateway": "閘道", "gateway": "閘道",
+359
View File
@@ -29,6 +29,365 @@ body {
-webkit-user-select: none; -webkit-user-select: none;
} }
.agent-workbench-desktop {
position: relative;
height: 100%;
overflow: hidden;
color: var(--color-text);
}
.agent-workbench-desktop-bg {
position: absolute;
inset: 0;
background:
linear-gradient(180deg, rgba(247, 249, 251, 0.96), rgba(248, 250, 252, 0.98)),
radial-gradient(circle at 18% 18%, rgba(22, 119, 255, 0.08), transparent 28%),
radial-gradient(circle at 82% 78%, rgba(20, 184, 166, 0.08), transparent 30%);
}
html[data-theme="dark"] .agent-workbench-desktop-bg {
background:
linear-gradient(180deg, rgba(12, 18, 24, 0.98), rgba(14, 18, 22, 0.98)),
radial-gradient(circle at 18% 18%, rgba(22, 119, 255, 0.12), transparent 28%),
radial-gradient(circle at 82% 78%, rgba(20, 184, 166, 0.1), transparent 30%);
}
.agent-workbench-desktop-hero {
position: relative;
display: grid;
place-items: center;
align-content: center;
gap: 54px;
height: 100%;
padding: 48px 24px;
}
.agent-workbench-desktop-hero h1 {
margin: 0;
color: var(--color-text);
font-size: clamp(28px, 4vw, 42px);
font-weight: 780;
letter-spacing: 0;
}
.agent-workbench-desktop-composer {
position: relative;
display: grid;
grid-template-columns: 96px minmax(0, 1fr);
grid-template-rows: minmax(118px, auto) auto auto;
gap: 10px 18px;
width: min(78vw, 1280px);
min-height: 232px;
padding: 24px;
border: 1px solid color-mix(in srgb, var(--border-color) 86%, transparent);
border-radius: 26px;
background: color-mix(in srgb, var(--color-bg-elevated) 94%, transparent);
box-shadow: 0 24px 80px rgba(15, 23, 42, 0.1);
}
.agent-workbench-file-input {
display: none;
}
.agent-workbench-upload {
display: grid;
place-items: center;
width: 72px;
height: 92px;
margin-top: 4px;
border: 1px solid color-mix(in srgb, var(--border-color) 92%, transparent);
border-radius: 4px;
background: color-mix(in srgb, var(--color-fill-alter) 80%, var(--color-bg-elevated));
color: var(--color-text-secondary);
cursor: pointer;
transform: rotate(-8deg);
transition: transform 0.16s ease, border-color 0.16s ease, color 0.16s ease;
}
.agent-workbench-upload:hover {
border-color: var(--color-primary);
color: var(--color-primary);
transform: rotate(-5deg) translateY(-1px);
}
.agent-workbench-prompt {
align-self: stretch;
border: 0 !important;
background: transparent !important;
box-shadow: none !important;
color: var(--color-text) !important;
font-size: 18px;
line-height: 1.7;
resize: none !important;
}
.agent-workbench-prompt textarea,
.agent-workbench-prompt.ant-input {
background: transparent;
}
.agent-workbench-attachments {
display: flex;
flex-wrap: wrap;
gap: 8px;
grid-column: 1 / -1;
}
.agent-workbench-attachments .ant-tag {
display: inline-flex;
gap: 6px;
align-items: center;
max-width: 260px;
margin: 0;
padding: 5px 8px;
border-radius: 999px;
}
.agent-workbench-attachments .ant-tag span {
color: var(--color-text-secondary);
}
.agent-workbench-toolbar {
display: flex;
justify-content: space-between;
gap: 16px;
align-items: center;
grid-column: 1 / -1;
min-width: 0;
overflow: visible;
}
.agent-workbench-tool-left {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
min-width: 0;
}
.agent-workbench-pill {
min-height: 44px;
border-radius: 12px !important;
font-weight: 720;
}
.agent-workbench-pill.active {
color: var(--color-primary);
background: color-mix(in srgb, var(--color-primary) 8%, var(--color-bg-elevated));
}
.agent-workbench-pill .ant-btn-icon,
.agent-workbench-pill > span:not(.ant-btn-icon) {
display: inline-flex;
align-items: center;
}
.agent-workbench-pill .ant-switch {
margin-left: 8px;
}
.agent-workbench-pill.square {
width: 44px;
padding-inline: 0;
}
.agent-workbench-model-select {
min-width: 220px;
}
.agent-workbench-model-select .ant-select-selector,
.agent-workbench-model-input {
min-height: 44px;
border-radius: 12px !important;
}
.agent-workbench-model-input {
width: 220px;
}
.agent-workbench-submit {
width: 92px;
min-height: 44px;
border-radius: 12px !important;
font-weight: 800;
}
.agent-workbench-result {
width: min(78vw, 920px);
padding: 18px 20px;
border: 1px solid color-mix(in srgb, var(--color-primary) 20%, var(--border-color));
border-radius: 18px;
background: color-mix(in srgb, var(--color-primary) 5%, var(--color-bg-elevated));
box-shadow: 0 18px 50px rgba(15, 23, 42, 0.08);
}
.agent-workbench-result strong {
display: block;
margin-bottom: 4px;
}
.agent-workbench-result p {
margin: 0;
color: var(--color-text-secondary);
line-height: 1.6;
}
.agent-workbench-mode-menu .ant-dropdown-menu {
width: 300px;
padding: 8px;
border-radius: 16px;
}
.agent-workbench-mode-item {
display: grid;
grid-template-columns: 24px minmax(0, 1fr) 18px;
gap: 10px;
align-items: center;
padding: 4px 2px;
}
.agent-workbench-mode-item > span {
display: inline-flex;
color: var(--color-primary);
}
.agent-workbench-mode-item div {
display: grid;
min-width: 0;
}
.agent-workbench-mode-item strong {
font-size: 14px;
}
.agent-workbench-mode-item small {
overflow: hidden;
color: var(--color-text-secondary);
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.agent-workbench-mode-item em {
color: var(--color-primary);
font-style: normal;
font-weight: 900;
}
.agent-workbench-popover,
.agent-workbench-subject-popover {
border: 1px solid var(--border-color);
border-radius: 18px;
background: var(--color-bg-elevated);
box-shadow: 0 24px 80px rgba(15, 23, 42, 0.16);
}
.agent-workbench-popover {
display: grid;
gap: 8px;
width: 420px;
padding: 14px;
}
.agent-workbench-popover button {
display: grid;
grid-template-columns: 24px minmax(0, 1fr);
gap: 10px;
align-items: center;
width: 100%;
padding: 10px;
border: 0;
border-radius: 12px;
background: transparent;
color: var(--color-text);
cursor: pointer;
text-align: left;
}
.agent-workbench-popover button:hover {
background: var(--color-fill-alter);
}
.agent-workbench-popover small {
display: block;
color: var(--color-text-secondary);
}
.agent-workbench-subject-popover {
display: grid;
gap: 18px;
width: 310px;
min-height: 300px;
padding: 18px;
}
.agent-workbench-subject-popover > span {
color: var(--color-text-secondary);
font-size: 13px;
}
.agent-workbench-subject-popover > div {
display: grid;
place-items: center;
align-content: center;
gap: 16px;
min-height: 220px;
color: var(--color-text-secondary);
text-align: center;
}
.agent-workbench-subject-popover button {
min-height: 38px;
padding: 0 16px;
border: 0;
border-radius: 10px;
background: var(--color-fill-alter);
color: var(--color-text);
cursor: pointer;
font-weight: 760;
}
@media (max-width: 980px) {
.agent-workbench-desktop-composer,
.agent-workbench-result {
width: min(92vw, 980px);
}
.agent-workbench-desktop-composer {
grid-template-columns: 74px minmax(0, 1fr);
}
.agent-workbench-upload {
width: 58px;
height: 78px;
}
}
@media (max-width: 720px) {
.agent-workbench-desktop-hero {
justify-content: stretch;
padding: 28px 14px;
}
.agent-workbench-desktop-composer {
grid-template-columns: 1fr;
}
.agent-workbench-upload {
width: 54px;
height: 68px;
}
.agent-workbench-toolbar {
align-items: stretch;
flex-direction: column;
}
.agent-workbench-submit {
width: 100%;
}
}
/* Remove antd focus outline (green border) globally */ /* Remove antd focus outline (green border) globally */
*:focus-visible { *:focus-visible {
outline: none !important; outline: none !important;
+29 -4
View File
@@ -1584,7 +1584,7 @@ export async function handleCommand<T>(cmd: string, args?: Record<string, unknow
// ── Skills ──────────────────────────────────────────────────────── // ── Skills ────────────────────────────────────────────────────────
case 'list_skills': case 'list_skills':
return [] as T; return getStore<any[]>('skills', []) as T;
case 'get_skill': case 'get_skill':
return { return {
@@ -1602,11 +1602,36 @@ export async function handleCommand<T>(cmd: string, args?: Record<string, unknow
manifest: null, manifest: null,
} as T; } as T;
case 'toggle_skill': case 'toggle_skill': {
const { name, enabled } = args as any;
const skills = getStore<any[]>('skills', []);
setStore('skills', skills.map((skill: any) => (
skill.name === name ? { ...skill, enabled } : skill
)));
return undefined as T; return undefined as T;
}
case 'install_skill': case 'install_skill': {
return ((args as any)?.source || 'installed-skill') as T; const source = (args as any)?.source || 'installed-skill';
const name = source.includes('jianying-editor-skill') ? 'jianying-editor' : String(source).split('/').pop();
const skills = getStore<any[]>('skills', []);
const nextSkill = {
name,
description: name === 'jianying-editor'
? '剪映 AI 自动化剪辑 Skill,支持草稿生成、素材导入、字幕、配音、特效和导出。'
: 'Installed skill',
source: 'boosapi',
sourcePath: `/mock/skills/${name}`,
enabled: true,
hasUpdate: false,
userInvocable: true,
};
setStore('skills', [
nextSkill,
...skills.filter((skill: any) => skill.name !== name),
]);
return source as T;
}
case 'uninstall_skill': case 'uninstall_skill':
return undefined as T; return undefined as T;
+526
View File
@@ -0,0 +1,526 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { App, Button, Dropdown, Input, Select, Switch, Tag, theme } from 'antd';
import type { MenuProps } from 'antd';
import {
ArrowUp,
AtSign,
ChevronDown,
Clapperboard,
ImagePlus,
Mic2,
Music2,
Plus,
SlidersHorizontal,
Sparkles,
UserRound,
WandSparkles,
Wrench,
X,
} from 'lucide-react';
import { invoke } from '@/lib/invoke';
import { useConversationStore, useProviderStore, useSkillStore, useUIStore } from '@/stores';
import type { AttachmentInput, Model } from '@/types';
type CreationMode = 'agent' | 'image' | 'video' | 'music' | 'voice' | 'digital-human' | 'action';
interface CreationModeOption {
key: CreationMode;
label: string;
hint: string;
icon: React.ReactNode;
}
interface AttachmentItem {
id: string;
name: string;
type: string;
size: number;
data: string;
}
interface ChatModelOption {
value: string;
label: string;
providerId: string;
modelId: string;
}
const CREATION_MODES: CreationModeOption[] = [
{ key: 'agent', label: 'Agent 模式', hint: '拆解任务、读写文件、执行工具', icon: <WandSparkles size={16} /> },
{ key: 'image', label: '图片生成', hint: '即梦图片、参考图与风格控制', icon: <ImagePlus size={16} /> },
{ key: 'video', label: '视频生成', hint: '即梦视频、图生视频与分镜', icon: <Clapperboard size={16} /> },
{ key: 'music', label: '音乐生成', hint: '配乐、氛围与短片 BGM', icon: <Music2 size={16} /> },
{ key: 'voice', label: '配音生成', hint: 'TTS 音色、语速与试听', icon: <Mic2 size={16} /> },
{ key: 'digital-human', label: '数字人', hint: '口播、形象与视频工作流', icon: <UserRound size={16} /> },
{ key: 'action', label: '动作模仿', hint: '动作迁移与主体参考', icon: <Sparkles size={16} /> },
];
const JIMENG_IMAGE_MODELS = [
{ value: 'jimeng_t2i_v40', label: '即梦图片 4.0' },
{ value: 'custom', label: '自定义 req_key' },
];
const JIMENG_VIDEO_MODELS = [
{ value: 'jimeng_ti2v_v30_pro', label: '即梦图生视频 3.0 Pro' },
{ value: 'custom', label: '自定义 req_key' },
];
const JIANYING_SKILL_NAME = 'jianying-editor';
const JIANYING_SKILL_SOURCE = 'luoluoluo22/jianying-editor-skill';
const JIANYING_PROMPT_PREFIX = '请使用 jianying-editor Skill 自动创建剪映草稿:';
function formatFileSize(size: number): string {
if (size < 1024) return `${size} B`;
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
return `${(size / 1024 / 1024).toFixed(1)} MB`;
}
function modelOptionLabel(model: Model, providerName: string): string {
return `${model.name || model.model_id} · ${providerName}`;
}
function isUsableChatModel(model: Model): boolean {
return model.enabled && (model.model_type === 'Chat' || model.capabilities.includes('TextChat'));
}
function getAttachmentMimeType(fileName: string, mimeType?: string): string {
if (mimeType) return mimeType;
const extension = fileName.split('.').pop()?.toLowerCase();
if (extension === 'pdf') return 'application/pdf';
if (extension === 'txt') return 'text/plain';
if (extension === 'md') return 'text/markdown';
if (extension === 'json') return 'application/json';
return 'application/octet-stream';
}
async function fileToAttachmentItem(file: File): Promise<AttachmentItem> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = () => reject(reader.error ?? new Error(`读取文件失败:${file.name}`));
reader.onload = () => {
const base64 = (reader.result as string).split(',')[1] || '';
resolve({
id: `${file.name}-${file.size}-${file.lastModified}-${globalThis.crypto?.randomUUID?.() ?? Date.now()}`,
name: file.name,
type: getAttachmentMimeType(file.name, file.type),
size: file.size,
data: base64,
});
};
reader.readAsDataURL(file);
});
}
function toAttachmentInput(item: AttachmentItem): AttachmentInput {
return {
file_name: item.name,
file_type: item.type,
file_size: item.size,
data: item.data,
};
}
export function AgentWorkbenchPage() {
const { token } = theme.useToken();
const { message } = App.useApp();
const fileInputRef = useRef<HTMLInputElement>(null);
const providers = useProviderStore((s) => s.providers);
const fetchProviders = useProviderStore((s) => s.fetchProviders);
const createConversation = useConversationStore((s) => s.createConversation);
const updateConversation = useConversationStore((s) => s.updateConversation);
const sendAgentMessage = useConversationStore((s) => s.sendAgentMessage);
const skills = useSkillStore((s) => s.skills);
const loadSkills = useSkillStore((s) => s.loadSkills);
const installSkill = useSkillStore((s) => s.installSkill);
const toggleSkill = useSkillStore((s) => s.toggleSkill);
const setActivePage = useUIStore((s) => s.setActivePage);
const [creationMode, setCreationMode] = useState<CreationMode>('agent');
const [autoMode, setAutoMode] = useState(true);
const [prompt, setPrompt] = useState('');
const [agentModel, setAgentModel] = useState('custom');
const [customAgentModel, setCustomAgentModel] = useState('');
const [imageModel, setImageModel] = useState('jimeng_t2i_v40');
const [videoModel, setVideoModel] = useState('jimeng_ti2v_v30_pro');
const [customJimengModel, setCustomJimengModel] = useState('');
const [attachments, setAttachments] = useState<AttachmentItem[]>([]);
const [subjectOpen, setSubjectOpen] = useState(false);
const [skillOpen, setSkillOpen] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [installingJianying, setInstallingJianying] = useState(false);
const [result, setResult] = useState('');
useEffect(() => {
if (providers.length === 0) {
void fetchProviders().catch((error) => message.error(String(error)));
}
}, [fetchProviders, message, providers.length]);
useEffect(() => {
void loadSkills();
}, [loadSkills]);
const chatModelOptions = useMemo<ChatModelOption[]>(() => {
const rows = providers
.filter((provider) => provider.enabled)
.flatMap((provider) =>
provider.models
.filter(isUsableChatModel)
.map((model) => ({
value: `${provider.id}::${model.model_id}`,
label: modelOptionLabel(model, provider.name),
providerId: provider.id,
modelId: model.model_id,
})),
);
const seen = new Set<string>();
return rows.filter((item) => {
if (seen.has(item.value)) return false;
seen.add(item.value);
return true;
});
}, [providers]);
useEffect(() => {
if (agentModel !== 'custom') return;
if (customAgentModel || chatModelOptions.length === 0) return;
setAgentModel(chatModelOptions[0].value);
}, [agentModel, chatModelOptions, customAgentModel]);
const currentMode = CREATION_MODES.find((item) => item.key === creationMode) ?? CREATION_MODES[0];
const selectedModel = creationMode === 'agent'
? (agentModel === 'custom' ? customAgentModel.trim() || '自定义模型' : agentModel)
: creationMode === 'image'
? (imageModel === 'custom' ? customJimengModel.trim() || '自定义 req_key' : imageModel)
: creationMode === 'video'
? (videoModel === 'custom' ? customJimengModel.trim() || '自定义 req_key' : videoModel)
: '待配置';
const modeItems: MenuProps['items'] = CREATION_MODES.map((item) => ({
key: item.key,
label: (
<div className="agent-workbench-mode-item">
<span>{item.icon}</span>
<div>
<strong>{item.label}</strong>
<small>{item.hint}</small>
</div>
{item.key === creationMode && <em>✓</em>}
</div>
),
}));
const selectedChatModel = useMemo(() => (
chatModelOptions.find((item) => item.value === agentModel) ?? null
), [agentModel, chatModelOptions]);
const jianyingSkill = useMemo(() => (
skills.find((item) => item.name === JIANYING_SKILL_NAME) ?? null
), [skills]);
const handleFilesSelected = useCallback((files: FileList | null) => {
if (!files || files.length === 0) return;
void Promise.all(Array.from(files).map(fileToAttachmentItem))
.then((next) => {
setAttachments((current) => [...current, ...next]);
})
.catch((error) => message.error(String(error)))
.finally(() => {
if (fileInputRef.current) fileInputRef.current.value = '';
});
}, [message]);
const removeAttachment = useCallback((id: string) => {
setAttachments((current) => current.filter((item) => item.id !== id));
}, []);
const applyJianyingSkill = useCallback(async () => {
if (installingJianying) return;
setInstallingJianying(true);
try {
if (!jianyingSkill) {
await installSkill(JIANYING_SKILL_SOURCE);
message.success('剪映 Skill 已安装');
} else if (!jianyingSkill.enabled) {
await toggleSkill(JIANYING_SKILL_NAME, true);
message.success('剪映 Skill 已启用');
}
setCreationMode('agent');
setPrompt((value) => {
const text = value.trim();
if (text.includes(JIANYING_SKILL_NAME) || text.includes('剪映')) return value;
return text ? `${JIANYING_PROMPT_PREFIX}${text}` : `${JIANYING_PROMPT_PREFIX}`;
});
setSkillOpen(false);
} catch (error) {
message.error(`剪映 Skill 集成失败:${String(error)}`);
} finally {
setInstallingJianying(false);
}
}, [installSkill, installingJianying, jianyingSkill, message, toggleSkill]);
const submit = useCallback(async () => {
const text = prompt.trim();
if (!text) {
message.warning('先输入想法、剧本或任务说明');
return;
}
if (submitting) return;
if (creationMode !== 'agent') {
setResult(`${currentMode.label} 已准备好。模型:${selectedModel};参考素材:${attachments.length} 个。即梦/媒体生成链路下一步接入。`);
return;
}
const fallbackProvider = providers.find((provider) => provider.enabled);
const providerId = selectedChatModel?.providerId ?? fallbackProvider?.id;
const modelId = selectedChatModel?.modelId ?? customAgentModel.trim();
if (!providerId || !modelId) {
message.warning('先在供应商里启用一个对话模型,或填写自定义模型名');
return;
}
setSubmitting(true);
try {
const title = text.slice(0, 24) || 'Agent 创作';
const conversation = await createConversation(title, modelId, providerId);
await updateConversation(conversation.id, { mode: 'agent', provider_id: providerId, model_id: modelId });
await invoke('agent_update_session', {
conversationId: conversation.id,
permissionMode: autoMode ? 'accept_edits' : 'default',
});
await sendAgentMessage(text, attachments.map(toAttachmentInput));
setResult(`Agent 已开始执行。模型:${modelId};参考素材:${attachments.length} 个。`);
setPrompt('');
setAttachments([]);
setActivePage('chat');
} catch (error) {
message.error(String(error));
setResult(`Agent 启动失败:${String(error)}`);
} finally {
setSubmitting(false);
}
}, [
attachments,
autoMode,
createConversation,
creationMode,
currentMode.label,
customAgentModel,
message,
prompt,
providers,
selectedChatModel,
selectedModel,
sendAgentMessage,
setActivePage,
submitting,
updateConversation,
]);
return (
<div className="agent-workbench-desktop" style={{ background: token.colorBgContainer }}>
<div className="agent-workbench-desktop-bg" />
<section className="agent-workbench-desktop-hero">
<h1>你好,想创作什么?</h1>
<div className="agent-workbench-desktop-composer">
<input
ref={fileInputRef}
className="agent-workbench-file-input"
type="file"
multiple
accept="image/*,video/*,audio/*,.pdf,.txt,.md,.json"
onChange={(event) => handleFilesSelected(event.target.files)}
/>
<button
className="agent-workbench-upload"
type="button"
onClick={() => fileInputRef.current?.click()}
aria-label="上传参考素材"
>
<Plus size={24} />
</button>
<Input.TextArea
className="agent-workbench-prompt"
value={prompt}
onChange={(event) => setPrompt(event.target.value)}
onPressEnter={(event) => {
if (event.metaKey || event.ctrlKey) {
event.preventDefault();
void submit();
}
}}
placeholder="输入想法、剧本或上传参考,支持 “/” 使用技能,@ 添加主体,和 Agent 一起创作"
autoSize={{ minRows: 4, maxRows: 8 }}
/>
{attachments.length > 0 && (
<div className="agent-workbench-attachments">
{attachments.map((item) => (
<Tag
key={item.id}
closeIcon={<X size={12} />}
onClose={(event) => {
event.preventDefault();
removeAttachment(item.id);
}}
>
{item.name} <span>{formatFileSize(item.size)}</span>
</Tag>
))}
</div>
)}
<div className="agent-workbench-toolbar">
<div className="agent-workbench-tool-left">
<Dropdown
trigger={['click']}
menu={{
items: modeItems,
selectable: true,
selectedKeys: [creationMode],
onClick: ({ key }) => setCreationMode(key as CreationMode),
}}
overlayClassName="agent-workbench-mode-menu"
>
<Button className="agent-workbench-pill active" icon={currentMode.icon}>
{currentMode.label} <ChevronDown size={14} />
</Button>
</Dropdown>
<Button className="agent-workbench-pill" icon={<SlidersHorizontal size={15} />} onClick={() => setAutoMode((value) => !value)}>
自动
<Switch size="small" checked={autoMode} onChange={setAutoMode} onClick={(_, event) => event.stopPropagation()} />
</Button>
{creationMode === 'agent' && (
<>
<Select
className="agent-workbench-model-select"
value={agentModel}
onChange={(value) => {
setAgentModel(value);
if (value !== 'custom') setCustomAgentModel('');
}}
options={[
...chatModelOptions,
{ value: 'custom', label: '自定义模型' },
]}
popupMatchSelectWidth={false}
/>
{agentModel === 'custom' && (
<Input
className="agent-workbench-model-input"
value={customAgentModel}
onChange={(event) => setCustomAgentModel(event.target.value)}
placeholder="输入 BoosAPI 模型名"
/>
)}
</>
)}
{creationMode === 'image' && (
<>
<Select
className="agent-workbench-model-select"
value={imageModel}
onChange={setImageModel}
options={JIMENG_IMAGE_MODELS}
popupMatchSelectWidth={false}
/>
{imageModel === 'custom' && (
<Input
className="agent-workbench-model-input"
value={customJimengModel}
onChange={(event) => setCustomJimengModel(event.target.value)}
placeholder="输入火山 req_key"
/>
)}
</>
)}
{creationMode === 'video' && (
<>
<Select
className="agent-workbench-model-select"
value={videoModel}
onChange={setVideoModel}
options={JIMENG_VIDEO_MODELS}
popupMatchSelectWidth={false}
/>
{videoModel === 'custom' && (
<Input
className="agent-workbench-model-input"
value={customJimengModel}
onChange={(event) => setCustomJimengModel(event.target.value)}
placeholder="输入火山 req_key"
/>
)}
</>
)}
<Dropdown
open={skillOpen}
onOpenChange={setSkillOpen}
trigger={['click']}
dropdownRender={() => (
<div className="agent-workbench-popover">
<Input placeholder="搜索技能" prefix={<Wrench size={15} />} />
<button type="button" onClick={() => void applyJianyingSkill()}>
<Clapperboard size={16} />
剪映自动剪辑
<small>
{installingJianying
? '正在安装/启用...'
: jianyingSkill?.enabled
? '已启用,生成草稿、字幕、配音、特效和导出'
: jianyingSkill
? '已安装,点击启用并写入提示'
: '一键安装 luoluoluo22/jianying-editor-skill'}
</small>
</button>
<button type="button"><Wrench size={16} /> 剧情短片 <small>自动生成故事大纲、分镜脚本并产出短片</small></button>
<button type="button"><Wrench size={16} /> 电商套图 <small>生成风格统一的商品全套视觉素材</small></button>
<button type="button"><Plus size={16} /> 用 Agent 创建技能</button>
</div>
)}
>
<Button className="agent-workbench-pill" icon={<Wrench size={15} />}>使用技能</Button>
</Dropdown>
<Dropdown
open={subjectOpen}
onOpenChange={setSubjectOpen}
trigger={['click']}
dropdownRender={() => (
<div className="agent-workbench-subject-popover">
<span>可能@的内容</span>
<div>
<AtSign size={36} />
<strong>你还没有创建过主体</strong>
<button
type="button"
onClick={() => {
setPrompt((value) => `${value}${value && !value.endsWith(' ') ? ' ' : ''}@主体 `);
setSubjectOpen(false);
}}
>
+ 创建主体
</button>
</div>
</div>
)}
>
<Button className="agent-workbench-pill square" icon={<AtSign size={16} />} />
</Dropdown>
</div>
<Button
className="agent-workbench-submit"
type="primary"
icon={<ArrowUp size={18} />}
loading={submitting}
onClick={() => void submit()}
/>
</div>
</div>
{result && (
<div className="agent-workbench-result">
<strong>{currentMode.label}</strong>
<p>{result}</p>
</div>
)}
</section>
</div>
);
}
+10 -1
View File
@@ -656,7 +656,16 @@ export interface RealtimeConfig {
} }
// === UI State === // === UI State ===
export type PageKey = 'chat' | 'drawing' | 'knowledge' | 'memory' | 'gateway' | 'files' | 'settings' | 'skills'; export type PageKey =
| 'chat'
| 'agent-workbench'
| 'drawing'
| 'knowledge'
| 'memory'
| 'gateway'
| 'files'
| 'settings'
| 'skills';
// === Drawing === // === Drawing ===
export type DrawingModelId = 'gpt-image-2' | 'gpt-image-1.5' | 'gpt-image-1' | 'gpt-image-1-mini'; export type DrawingModelId = 'gpt-image-2' | 'gpt-image-1.5' | 'gpt-image-1' | 'gpt-image-1-mini';