14 Commits

Author SHA1 Message Date
boos be1f388b26 feat: add desktop video and ppt skills 2026-06-29 12:54:57 +08:00
boos bdfb9b8dc2 fix: align desktop chat with BoosAPI client flow 2026-06-29 12:27:27 +08:00
boos 2a5eca83c8 design: refresh desktop app logo 2026-06-29 11:59:18 +08:00
boos 8c9fbea822 fix: update desktop links and skills market 2026-06-29 11:43:49 +08:00
boos 6863ccd16c fix: tolerate stale workspace asset owners 2026-06-29 11:34:24 +08:00
boos 7e1bfdd7fe perf: index workflow route model lookup 2026-06-29 11:27:56 +08:00
boos 1fd7af1246 fix: allow large digital human uploads 2026-06-29 11:20:22 +08:00
boos 9ae9224669 refactor: merge agent workbench into chat 2026-06-29 10:56:07 +08:00
boos cd08ac931d feat: bundle desktop skills 2026-06-26 14:28:13 +08:00
boos 424aaa9b1f feat: add desktop agent workbench 2026-06-26 13:08:00 +08:00
boos bc99c678f9 feat: improve agent workbench and digital human flows 2026-06-25 16:32:21 +08:00
boos 22e6c162b6 ci: pin pnpm for macos dmg workflow 2026-06-24 14:35:44 +08:00
boos 397274be0c ci: add bun macos arm64 dmg build 2026-06-24 14:32:08 +08:00
boos 86bbfe2f3c fix: render registration errors as text 2026-06-24 10:52:20 +08:00
141 changed files with 7117 additions and 1442 deletions
@@ -0,0 +1,152 @@
name: Build macOS ARM64 DMG (Bun)
on:
workflow_dispatch:
inputs:
upload_to_release:
description: "Upload DMG to an existing GitHub release/tag"
required: false
default: "false"
type: choice
options:
- "false"
- "true"
tag_name:
description: "Release tag to upload to when upload_to_release=true"
required: false
default: ""
push:
branches:
- main
paths:
- ".github/workflows/build-macos-arm64-dmg-bun.yml"
- "desktop/**"
permissions:
contents: write
env:
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short
NODE_OPTIONS: "--max_old_space_size=4096"
defaults:
run:
working-directory: desktop
concurrency:
group: "${{ github.workflow }}-${{ github.ref }}"
cancel-in-progress: true
jobs:
build-macos-arm64-dmg:
name: Build DMG (aarch64-apple-darwin)
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin
- name: Rust cache
uses: Swatinem/rust-cache@v2
with:
prefix-key: "v1"
key: "macos-14-aarch64-apple-darwin-bun-dmg"
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
# The client has local `link:` dependencies. Bun 1.3 cannot install those
# from the existing pnpm lockfile yet, so pnpm remains the install/link
# layer while Bun drives the final build command.
- uses: pnpm/action-setup@v4
with:
version: 10.32.1
run_install: false
- name: Get linked dependency versions
id: dep-versions
shell: bash
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 }}-aarch64-apple-darwin-bun-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
run: |
pnpm --dir libs/stream-monaco install --frozen-lockfile
pnpm --dir libs/stream-monaco build
pnpm --dir libs/markstream-vue install --frozen-lockfile
pnpm --dir libs/markstream-vue run build:parser
pnpm --dir libs/markstream-vue/packages/markstream-react build
- name: Install client dependencies
run: pnpm install --frozen-lockfile
- name: Build macOS ARM64 DMG with Bun
env:
APPLE_SIGNING_IDENTITY: "-"
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 --target aarch64-apple-darwin --bundles dmg
- name: Collect DMG
id: collect
shell: bash
run: |
mkdir -p dist/macos-arm64
shopt -s nullglob
files=(src-tauri/target/aarch64-apple-darwin/release/bundle/dmg/*.dmg)
if (( ${#files[@]} == 0 )); then
echo "No DMG found under src-tauri/target/aarch64-apple-darwin/release/bundle/dmg" >&2
exit 1
fi
for file in "${files[@]}"; do
cp "$file" "dist/macos-arm64/"
done
echo "dmg-path=dist/macos-arm64" >> "$GITHUB_OUTPUT"
ls -lh dist/macos-arm64
- name: Upload DMG artifact
uses: actions/upload-artifact@v4
with:
name: BoosAPI-Desktop-macos-arm64-dmg
path: desktop/dist/macos-arm64/*.dmg
if-no-files-found: error
- name: Upload DMG to release
if: ${{ inputs.upload_to_release == 'true' && inputs.tag_name != '' }}
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ inputs.tag_name }}
token: ${{ secrets.GITHUB_TOKEN }}
files: desktop/dist/macos-arm64/*.dmg
+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,42 @@
# Desktop Agent Workbench Integration Design
## Scope
This deliverable adds the desktop Chat empty-state creator composer 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
- `components/chat/AgentWorkbenchComposer.tsx` owns the creator composer, local attachment state, mode state, model selection, Agent handoff, and placeholder result/status for media modes.
- `components/chat/ChatView.tsx` renders the composer in the Chat empty state and keeps the normal Chat input for existing conversations.
- `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.
- The first composer UI can use Chinese literal copy to match current product direction; it does not add a standalone nav label.
## 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,33 @@
# Implementation Plan
1. Integrate Agent Workbench into the existing Chat empty state instead of adding a standalone `agent-workbench` page/nav module.
2. Create `components/chat/AgentWorkbenchComposer.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. Bundle the curated Desktop skill pack in `desktop/src-tauri/builtin-skills`, seed it into `~/.boosapi/skills`, and expose it through marketplace fallback results:
- JianYing editor automation
- Seedance 2 prompt/video skills
- DWG MCP server wrapper
- CAD AI agent wrapper
- Gorden PPT skill
6. Run validation:
- `bun run typecheck`
- `bun run build`
7. 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.
- `pnpm --dir desktop exec tsc --noEmit` passed after bundling built-in skills.
- `pnpm --dir desktop run build` passed after bundling built-in skills.
- `cargo check -p open-agent-sdk --manifest-path desktop/src-tauri/Cargo.toml` passed after bundling built-in skills.
- `pnpm --dir desktop exec tsc --noEmit` passed after merging Agent Workbench into Chat.
@@ -0,0 +1,36 @@
# Desktop Agent Workbench integration
## Goal
Merge the first usable Agent Workbench creator surface into the desktop Chat entry so users start from one conversation-first screen: choose a creation mode, select a model, attach references, and type a prompt.
## Requirements
- Integrate the Agent Workbench composer into the existing Chat page instead of adding a separate Agent navigation module.
- 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 composer must follow the desktop app’s existing React/Ant Design/Zustand conventions and remain reachable through the Chat sidebar entry.
- 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.
- Desktop should bundle the first-party curated skill pack in the project so JianYing, Seedance, DWG/CAD, and PPT skills are available even when the online `skills.sh` market does not return them.
## Acceptance Criteria
- [x] Chat empty state contains the Agent Workbench composer; no separate Agent Workbench sidebar entry is required.
- [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] Desktop seeds bundled skills into the local skills directory and mirrors them in the marketplace/browser preview.
- [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:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-node@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
node-version: '20'
- uses: pnpm/action-setup@v4
with:
run_install: false
bun-version: latest
- name: Get linked dependency versions
id: dep-versions
@@ -168,15 +165,15 @@ jobs:
if: steps.cache-linked-deps.outputs.cache-hit != 'true'
shell: bash
run: |
pnpm --dir libs/stream-monaco install --frozen-lockfile
pnpm --dir libs/stream-monaco build
bun --cwd libs/stream-monaco install --frozen-lockfile
bun --cwd libs/stream-monaco run build
pnpm --dir libs/markstream-vue install --frozen-lockfile
pnpm --dir libs/markstream-vue run build:parser
pnpm --dir libs/markstream-vue/packages/markstream-react build
bun --cwd libs/markstream-vue install --frozen-lockfile
bun --cwd libs/markstream-vue run build:parser
bun --cwd libs/markstream-vue/packages/markstream-react run build
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
run: bun install --frozen-lockfile
- name: Build with Tauri
uses: tauri-apps/tauri-action@v0
@@ -192,7 +189,7 @@ jobs:
releaseBody: ${{ needs.draft-release-notes.outputs.body }}
releaseDraft: true
prerelease: ${{ contains(github.ref_name, '-rc') }}
tauriScript: pnpm tauri
tauriScript: bun run tauri
args: --target ${{ matrix.target }}
includeUpdaterJson: true
@@ -268,13 +265,10 @@ jobs:
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-node@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
node-version: '20'
- uses: pnpm/action-setup@v4
with:
run_install: false
bun-version: latest
- name: Get linked dependency versions
id: dep-versions
@@ -300,22 +294,22 @@ jobs:
- name: Build linked dependencies
if: steps.cache-linked-deps.outputs.cache-hit != 'true'
run: |
pnpm --dir libs/stream-monaco install --frozen-lockfile
pnpm --dir libs/stream-monaco build
bun --cwd libs/stream-monaco install --frozen-lockfile
bun --cwd libs/stream-monaco run build
pnpm --dir libs/markstream-vue install --frozen-lockfile
pnpm --dir libs/markstream-vue run build:parser
pnpm --dir libs/markstream-vue/packages/markstream-react build
bun --cwd libs/markstream-vue install --frozen-lockfile
bun --cwd libs/markstream-vue run build:parser
bun --cwd libs/markstream-vue/packages/markstream-react run build
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
run: bun install --frozen-lockfile
- name: Build for Linux ARM64
run: |
export PKG_CONFIG_ALLOW_CROSS=1
export PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/:$PKG_CONFIG_PATH
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:
NODE_OPTIONS: '--max_old_space_size=4096'
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
+10 -13
View File
@@ -41,13 +41,10 @@ jobs:
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-node@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
node-version: '20'
- uses: pnpm/action-setup@v4
with:
run_install: false
bun-version: latest
- name: Get linked dependency versions
id: dep-versions
@@ -73,15 +70,15 @@ jobs:
- name: Build linked dependencies
if: steps.cache-linked-deps.outputs.cache-hit != 'true'
run: |
pnpm --dir libs/stream-monaco install --frozen-lockfile
pnpm --dir libs/stream-monaco build
bun --cwd libs/stream-monaco install --frozen-lockfile
bun --cwd libs/stream-monaco run build
pnpm --dir libs/markstream-vue install --frozen-lockfile
pnpm --dir libs/markstream-vue run build:parser
pnpm --dir libs/markstream-vue/packages/markstream-react build
bun --cwd libs/markstream-vue install --frozen-lockfile
bun --cwd libs/markstream-vue run build:parser
bun --cwd libs/markstream-vue/packages/markstream-react run build
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
run: bun install --frozen-lockfile
- name: Build with Tauri
uses: tauri-apps/tauri-action@v0
@@ -97,5 +94,5 @@ jobs:
releaseBody: 'Internal test build for scrollbar fix verification.'
releaseDraft: true
prerelease: true
tauriScript: pnpm tauri
tauriScript: bun run tauri
args: --target aarch64-apple-darwin
+10 -13
View File
@@ -51,13 +51,10 @@ jobs:
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-node@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
node-version: '20'
- uses: pnpm/action-setup@v4
with:
run_install: false
bun-version: latest
- name: Get linked dependency versions
id: dep-versions
@@ -84,18 +81,18 @@ jobs:
- name: Build linked dependencies
if: steps.cache-linked-deps.outputs.cache-hit != 'true'
run: |
pnpm --dir libs/stream-monaco install --frozen-lockfile
pnpm --dir libs/stream-monaco build
bun --cwd libs/stream-monaco install --frozen-lockfile
bun --cwd libs/stream-monaco run build
pnpm --dir libs/markstream-vue install --frozen-lockfile
pnpm --dir libs/markstream-vue run build:parser
pnpm --dir libs/markstream-vue/packages/markstream-react build
bun --cwd libs/markstream-vue install --frozen-lockfile
bun --cwd libs/markstream-vue run build:parser
bun --cwd libs/markstream-vue/packages/markstream-react run build
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
run: bun install --frozen-lockfile
- 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:
NODE_OPTIONS: '--max_old_space_size=4096'
BOOSAPI_TEST_BUILD: '1'
+10 -13
View File
@@ -42,13 +42,10 @@ jobs:
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-node@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
node-version: '20'
- uses: pnpm/action-setup@v4
with:
run_install: false
bun-version: latest
- name: Get linked dependency versions
id: dep-versions
@@ -76,15 +73,15 @@ jobs:
if: steps.cache-linked-deps.outputs.cache-hit != 'true'
shell: bash
run: |
pnpm --dir libs/stream-monaco install --frozen-lockfile
pnpm --dir libs/stream-monaco build
bun --cwd libs/stream-monaco install --frozen-lockfile
bun --cwd libs/stream-monaco run build
pnpm --dir libs/markstream-vue install --frozen-lockfile
pnpm --dir libs/markstream-vue run build:parser
pnpm --dir libs/markstream-vue/packages/markstream-react build
bun --cwd libs/markstream-vue install --frozen-lockfile
bun --cwd libs/markstream-vue run build:parser
bun --cwd libs/markstream-vue/packages/markstream-react run build
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
run: bun install --frozen-lockfile
- name: Build with Tauri
uses: tauri-apps/tauri-action@v0
@@ -98,7 +95,7 @@ jobs:
releaseName: 'BoosApi Test ${{ github.ref_name }}'
releaseDraft: true
prerelease: true
tauriScript: pnpm tauri
tauriScript: bun run tauri
args: --target x86_64-pc-windows-msvc
- name: Package Windows portable (zip)
+2 -2
View File
@@ -6,7 +6,7 @@
A local-first AI desktop client for the Boos ecosystem — multi-provider chat, AI agents, knowledge base, MCP, and a built-in API gateway, in one app.
[![Release](https://img.shields.io/github/v/release/Boos4721/boos-chat?style=flat-square)](https://github.com/Boos4721/boos-chat/releases)
[![Release](https://img.shields.io/github/v/release/Boos4721/BoosAPI?style=flat-square)](https://github.com/Boos4721/BoosAPI/releases)
[![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Windows%20%7C%20Linux-blue?style=flat-square)](#platform-support)
</div>
@@ -68,7 +68,7 @@ BoosAPI Desktop is a desktop AI workspace that runs on your own machine. It ship
## Quick start
Download the build for your platform from the [Releases](https://github.com/Boos4721/boos-chat/releases) page. On first launch, BoosAPI is preconfigured as the default provider — add your key in Settings and start chatting.
Download the build for your platform from the [Releases](https://github.com/Boos4721/BoosAPI/releases) page. On first launch, BoosAPI is preconfigured as the default provider — add your key in Settings and start chatting.
## Build from source
+2 -2
View File
@@ -6,7 +6,7 @@
面向 Boos 生态的本地优先 AI 桌面客户端 —— 多服务商对话、AI Agent、知识库、MCP 与内置 API 网关,集于一身。
[![Release](https://img.shields.io/github/v/release/Boos4721/boos-chat?style=flat-square)](https://github.com/Boos4721/boos-chat/releases)
[![Release](https://img.shields.io/github/v/release/Boos4721/BoosAPI?style=flat-square)](https://github.com/Boos4721/BoosAPI/releases)
[![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Windows%20%7C%20Linux-blue?style=flat-square)](#平台支持)
</div>
@@ -68,7 +68,7 @@ BoosAPI Desktop 是一款运行在你本机的 AI 桌面工作台。它默认接
## 快速开始
前往 [Releases](https://github.com/Boos4721/boos-chat/releases) 页面下载适合你平台的安装包。首次启动时 BoosAPI 已预置为默认服务商 —— 在设置中填入你的密钥即可开始对话。
前往 [Releases](https://github.com/Boos4721/BoosAPI/releases) 页面下载适合你平台的安装包。首次启动时 BoosAPI 已预置为默认服务商 —— 在设置中填入你的密钥即可开始对话。
## 从源码构建
+32
View File
@@ -10,6 +10,38 @@
"source": "vercel-labs/agent-skills",
"sourceType": "github",
"computedHash": "a6a44d5498f7e8f68289902f3dedfc6f38ae0cee1e96527c80724cf27f727c2a"
},
"jianying-editor": {
"source": "luoluoluo22/jianying-editor-skill",
"sourceType": "github",
"bundled": true
},
"seedance": {
"source": "MapleShaw/seedance2.0-prompt-skill",
"sourceType": "github",
"bundled": true
},
"seedance-prompt-en": {
"source": "dexhunter/seedance2-skill",
"sourceType": "github",
"bundled": true
},
"dwg-mcp-server": {
"source": "dimitrovakulenko/dwg-mcp-server",
"sourceType": "github",
"bundled": true,
"wrapper": true
},
"cad-ai-agent": {
"source": "jeremylongshore/cad-ai-agent",
"sourceType": "github",
"bundled": true,
"wrapper": true
},
"gorden-ppt-skill": {
"source": "GordenSun/GordenPPTSkill",
"sourceType": "github",
"bundled": true
}
}
}
+1
View File
@@ -1184,6 +1184,7 @@ dependencies = [
"sea-orm",
"serde",
"serde_json",
"serde_yaml",
"tauri",
"tauri-build",
"tauri-plugin-autostart",
+1
View File
@@ -47,6 +47,7 @@ tauri-plugin-process = "2"
tauri-plugin-updater = "2"
serde = { workspace = true }
serde_json = { workspace = true }
serde_yaml = "0.9"
boosapi-core = { path = "crates/core" }
boosapi-providers = { path = "crates/providers" }
boosapi-agent = { path = "crates/agent" }
@@ -0,0 +1,32 @@
---
name: cad-ai-agent
description: CAD/DXF drawing intelligence workflow for summaries, compliance checks, quantity takeoffs, RFIs, health reports, revision comparison, and safe structured edit planning.
---
# CAD AI Agent Skill
Use this skill when a user asks for CAD/DXF drawing intelligence: summaries, compliance checks, quantity takeoffs, RFIs, health reports, revision comparison, or safe edit planning.
Upstream project: https://github.com/jeremylongshore/cad-ai-agent
## Setup
Clone and install the upstream project when the user wants to run the full local pipeline:
```bash
git clone https://github.com/jeremylongshore/cad-ai-agent.git
cd cad-ai-agent
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
```
## Workflow
1. Identify the drawing format: DXF is directly supported by the upstream project; DWG/PDF may need conversion or a different inspection pipeline.
2. Ask what the user wants: summarize, edit, compare, query, generate documentation, compliance, coordination, estimation, or quality.
3. Preserve originals. Prefer save-as outputs and structured reports.
4. For edits, produce structured operation plans rather than modifying raw CAD data directly.
5. For analysis, return layer/entity summaries, issues, measurements, and concrete next actions.
This bundled skill is a BoosAPI Desktop wrapper around the upstream project and keeps CAD operations explicit and reversible.
@@ -0,0 +1,35 @@
---
name: dwg-mcp-server
description: Inspect AutoCAD DWG files through DWG MCP Server. Use when users ask to open, inspect, query, summarize, or extract objects/properties from DWG drawings.
---
# DWG MCP Server Skill
Use this skill when a user wants to inspect AutoCAD DWG drawings with an AI agent.
Upstream project: https://github.com/dimitrovakulenko/dwg-mcp-server
## Setup
Install the MCP server in your MCP client or run it with npx:
```bash
npx -y @dmytro-prototypes/dwg-mcp-server
```
For clients without MCP roots, configure explicit allowed roots:
```bash
DWG_MCP_ALLOWED_ROOTS="$HOME" npx -y @dmytro-prototypes/dwg-mcp-server
```
## Workflow
1. Ask the user for the DWG file path and confirm it is under an allowed root.
2. Use `dwg.list_roots` to inspect accessible folders.
3. Use `dwg.open_file` to open the drawing and get a `documentId`.
4. Use `dwg.list_file_types`, `dwg.describe_type`, `dwg.get_objects`, and `dwg.query_objects` to inspect contents.
5. Summarize entities, layers, blocks, layouts, references, and notable properties.
6. Close the document with `dwg.close_file` when done.
This bundled skill is a BoosAPI Desktop wrapper around the upstream MCP server. It does not edit DWG files directly.
@@ -0,0 +1,28 @@
---
name: ffmpeg-media
description: FFmpeg media processing workflow for professional video/audio analysis, compression, conversion, clipping, transcoding, timestamp summaries, subtitles, audio extraction, concat, normalization, and command generation. Use when the user asks to inspect, cut, compress, convert, batch process, or repair media files with FFmpeg.
---
# FFmpeg Media Skill
Use this skill when the task needs reliable command-line video or audio processing.
Primary tool: https://ffmpeg.org
Example agent skill: https://github.com/fabriqaai/ffmpeg-analyse-video-skill
## Workflow
1. Inspect the input media first with `ffprobe` when duration, streams, codecs, dimensions, frame rate, or timestamps matter.
2. Preserve quality by copying streams with `-c copy` for simple trims/containers when possible.
3. Re-encode only when required for compression, compatibility, scaling, burn-in subtitles, filters, or codec changes.
4. For social/video platform outputs, specify codec, pixel format, audio codec, dimensions, frame rate, and faststart metadata explicitly.
5. For destructive edits, write to a new output file and never overwrite the source unless the user explicitly asks.
## Common Commands
- Analyze: `ffprobe -hide_banner -show_streams -show_format input.mp4`
- Trim copy: `ffmpeg -ss 00:00:05 -to 00:00:20 -i input.mp4 -c copy output.mp4`
- H.264 web MP4: `ffmpeg -i input.mov -c:v libx264 -pix_fmt yuv420p -crf 20 -preset medium -c:a aac -b:a 160k -movflags +faststart output.mp4`
- Extract audio: `ffmpeg -i input.mp4 -vn -c:a pcm_s16le output.wav`
On macOS install system FFmpeg with `brew install ffmpeg`; on Windows use `winget install Gyan.FFmpeg`.
@@ -0,0 +1,20 @@
---
name: gorden-ppt-skill
description: 中文 PPT 生成与编辑 Skill,支持工作汇报、年终总结、述职竞聘、项目复盘、商务提案、教学课件、模板套用、版式保持和 PowerPoint 成品导出。
---
# Gorden PPT Skill
Use this skill when the user wants to create or edit a PowerPoint / PPT / slides / `.pptx` deck.
Upstream full skill: https://github.com/GordenSun/GordenPPTSkill
## Workflow
1. Clarify deck purpose, audience, language, page count, format, and whether the user has a template.
2. If using a template, preserve layout, typography, colors, and spacing; replace content without damaging the design.
3. If creating from scratch, propose a concise structure first: cover, agenda, context, key points, data pages, conclusion, and appendix as needed.
4. Keep slide text short and presentation-ready.
5. Check text overflow, same-level heading consistency, and visual hierarchy before final export.
Install `GordenSun/GordenPPTSkill` from the Skills marketplace for the full template library and update scripts.
@@ -0,0 +1,29 @@
---
name: hyperframes
description: HyperFrames AI video generation workflow for turning a prompt, launch story, explainer script, product update, or motion graphics brief into deterministic HTML/CSS/JS animation rendered as MP4. Use when the user asks for text-to-video, product release videos, explainers, UI motion graphics, or agent-authored animated videos with HeyGen HyperFrames.
---
# HyperFrames Skill
Use this skill when the user wants an AI-generated short video where an agent writes HTML, CSS, and JavaScript animation and renders it deterministically to MP4.
Upstream full skill: https://github.com/heygen-com/hyperframes
Docs: https://hyperframes.heygen.com
Install full bundle: `npx skills add heygen-com/hyperframes` or `npx skills add heygen-com/hyperframes --all`
## Workflow
1. Clarify target duration, audience, aspect ratio, tone, brand constraints, and required call to action.
2. Convert the request into a compact video plan: scenes, timing, on-screen text, motion direction, and visual system.
3. Prefer deterministic HTML/CSS/JS animation over raster-only generation when text clarity, brand control, or repeatable rendering matters.
4. Keep each scene simple enough to render cleanly: one core idea, one motion focus, and short text.
5. Export or hand off a renderable project and call out where HyperFrames should render the final MP4.
## Good Fits
- Product launch or release-note videos.
- Explainer videos with diagrams, UI callouts, or animated typography.
- SaaS feature walkthroughs and marketing motion graphics.
- Social clips that need consistent brand text and layout.
Install the upstream HyperFrames skill for renderer commands, templates, and the complete 19-skill pack.
@@ -0,0 +1,26 @@
---
name: jianying-editor
description: 剪映 AI 自动化剪辑 Skill,支持草稿生成、素材导入、字幕、配音、特效、转场、录屏和导出规划。Use when the user wants to automate JianYing/CapCut editing, create a draft, assemble clips, add captions, voiceover, music, transitions, or export a video.
---
# JianYing Editor Skill
Use this skill when the user wants to automate video editing with JianYing/CapCut.
Upstream full skill: https://github.com/luoluoluo22/jianying-editor-skill
## Workflow
1. Clarify the target: platform, duration, aspect ratio, source assets, narration, subtitle style, music, and export format.
2. Build an edit plan before scripting: timeline structure, clips, captions, voiceover, music, effects, transitions, and export path.
3. Keep generated scripts in the user's project/workspace, not inside the skill directory.
4. Preserve original media. Generate a new draft/project for each requested edit.
5. When the full upstream skill is installed, follow its detailed `rules/` and `docs/agent-playbook.md`.
## Install Full Upstream Version
```bash
git clone https://github.com/luoluoluo22/jianying-editor-skill.git
```
In BoosAPI Desktop, install `luoluoluo22/jianying-editor-skill` from the Skills marketplace to replace this bundled quick-start with the full implementation.
@@ -0,0 +1,27 @@
---
name: knowledge-cat-ppt
description: Knowledge Cat PPT workflow for knowledge-base-driven Chinese presentation creation, structured slide generation, teaching decks, reports, summaries, and PPTX refinement. Use when the user asks to turn documents, notes, knowledge cards, or research into a polished PowerPoint deck.
---
# Knowledge Cat PPT Skill
Use this skill when the user wants to turn knowledge, notes, or documents into a structured deck.
Upstream full skill: https://github.com/gnipbao/knowledge-cat-ppt-skill
## Workflow
1. Identify the deck goal, audience, language, duration, page count, and source materials.
2. Extract the source into a hierarchy: topic, subtopics, arguments, examples, data, and conclusion.
3. Build a slide outline before drafting slide content.
4. Keep each slide to one point, one visual idea, and presentation-ready wording.
5. Check consistency, text overflow, title hierarchy, and logical flow before final export.
## Good Fits
- Knowledge-base summaries into PPT.
- Training and teaching decks.
- Research notes into executive presentations.
- Chinese reports, courseware, and structured explanation decks.
Install `gnipbao/knowledge-cat-ppt-skill` from the Skills marketplace for the full implementation.
@@ -0,0 +1,28 @@
---
name: manim-animation
description: Manim animation workflow for math, science, education, explainer, and 3Blue1Brown-style knowledge videos. Use when the user asks for mathematical animations, diagrams, step-by-step visual proofs, concept explanations, or high-quality programmatic educational videos.
---
# Manim Animation Skill
Use this skill when the video should explain a concept through programmatic animation.
Manim Community: https://www.manim.community
Recommended skill repo: https://github.com/adithya-s-k/manim_skill
## Workflow
1. Convert the concept into a short storyboard: definitions, visual objects, transformations, and narration beats.
2. Keep each scene focused on one mathematical or conceptual transition.
3. Use Manim primitives for equations, axes, graphs, geometric objects, arrows, highlights, and camera motion.
4. Prefer clean timing, readable labels, and stepwise reveals over excessive effects.
5. Render short previews before final render when changing layout, camera, or equation density.
## Good Fits
- Math and science explainers.
- 3Blue1Brown-style animations.
- Diagrammatic product or architecture explanations.
- Teaching clips where visual transformation is the core value.
Install `adithya-s-k/manim_skill/skills/manimce-best-practices` for full ManimCE conventions.
@@ -0,0 +1,27 @@
---
name: openmontage
description: OpenMontage agentic video production workflow for intelligent editing, automatic montage assembly, vlog creation, research-to-script-to-assets-to-edit pipelines, and multi-agent video production. Use when the user asks to turn raw footage, references, or a topic into an edited video using OpenMontage-style pipelines.
---
# OpenMontage Skill
Use this skill when the user wants an agentic editing pipeline rather than a single render command.
Upstream project: https://github.com/calesthio/OpenMontage
## Workflow
1. Inventory available materials: raw footage, images, scripts, voiceover, music, brand assets, and target platform.
2. Build a pipeline plan: research, script, asset selection/generation, rough cut, captions, music, color, final render.
3. Split long videos into scenes or beats with clear inputs and outputs for each step.
4. Use FFmpeg or the project’s tools for deterministic trimming, concat, audio mixing, subtitle burn-in, and export.
5. Keep an edit decision list with source file, in/out time, reason, caption text, and transition notes.
## Good Fits
- Vlogs and recap videos from many clips.
- Topic-to-video production where research and script are part of the job.
- Short-form social edits needing subtitles, pacing, and overlays.
- Multi-asset projects where the user wants an AI editing assistant.
Install the upstream OpenMontage repository for its full 12-pipeline / tool system.
@@ -0,0 +1,29 @@
---
name: remotion-video
description: Remotion code-driven video workflow for React-based videos, reusable templates, dynamic props, batch rendering, data-driven motion graphics, product explainers, and programmatic video generation. Use when the user asks to create or render videos with React, Remotion, templates, captions, charts, or repeatable variants.
---
# Remotion Video Skill
Use this skill when the output should be a maintainable React video project or a batch-renderable template.
Remotion: https://www.remotion.dev
Repo: https://github.com/remotion-dev/remotion
Skills: `npx skills add remotion-dev/skills`
## Workflow
1. Define composition size, FPS, duration, and props schema before coding.
2. Create reusable scene components with predictable timing and layout.
3. Put user-editable text, colors, media paths, and data in props rather than hardcoding.
4. Use Remotion for deterministic charts, captions, UI walkthroughs, and templated social variants.
5. Validate render constraints: text overflow, safe areas, asset paths, audio duration, and final codec.
## Good Fits
- Product demos and UI motion.
- Data-driven video variants.
- Branded social clips with repeatable templates.
- Developer-controlled motion graphics.
Install the upstream Remotion skills for full project scaffolding and render commands.
@@ -0,0 +1,30 @@
---
name: seedance-prompt-en
description: English Seedance 2.0 multimodal AI video prompt guide. Use for text/image/video/audio reference prompts with @Image, @Video, and @Audio syntax.
---
# Seedance 2.0 English Prompt Skill
Use this skill when the user wants English prompts for Jimeng / Seedance 2.0 multimodal AI video generation.
Upstream full skill: https://github.com/dexhunter/seedance2-skill
## Reference Syntax
Use explicit reference roles:
- `@Image1 as the first frame`
- `@Image1's character as the subject`
- `scene references @Image2`
- `reference @Video1's camera movement`
- `BGM references @Audio1`
## Prompt Blueprint
```text
[Subject setup] + [Scene/environment] + [Action/motion] + [Camera movement] + [Style/lighting] + [Audio/sound] + [Constraints]
```
Keep reference-heavy prompts precise. Say what each uploaded asset controls and what the model may invent.
Install `dexhunter/seedance2-skill` from the Skills marketplace for the full guide.
@@ -0,0 +1,29 @@
---
name: seedance
description: 中文 Seedance 2.0 / 即梦视频与图片提示词工程,支持 @素材引用、分镜、运镜、首帧图、图生视频、短剧、广告视频和长视频流水线。
---
# Seedance 2.0 中文提示词 Skill
Use this skill when the user asks for 即梦 / Seedance / Dreamina video prompts, image-to-video prompts, storyboard prompts, first-frame image prompts, camera motion, short video ads, short drama clips, or multimodal reference prompts.
Upstream full skill: https://github.com/MapleShaw/seedance2.0-prompt-skill
## Core Rules
1. Write prompts in Chinese unless the user asks otherwise.
2. Use official reference syntax: `@图片1`-`@图片9`, `@视频1`-`@视频3`, `@音频1`-`@音频3`.
3. For uploaded images, explain the role of each reference: first frame, character, scene, product, action, camera, rhythm, or style.
4. For short clips under 15 seconds, focus on one impossible or highly attractive moment with a strong first two seconds.
5. For longer videos, split into character reference, storyboard, key frames, per-shot prompts, and editing instructions.
6. Avoid realistic identifiable human faces in uploaded references unless the platform and user confirm compliance.
## Prompt Shape
```text
[主体/角色] + [场景/光影/质感] + [动作节奏] + [运镜] + [镜头/景别] + [声音/情绪] + [硬约束]
```
For image-to-video, keep the text concise and only add what the image does not already specify.
Install `MapleShaw/seedance2.0-prompt-skill` from the Skills marketplace for the full reference library.
@@ -0,0 +1,27 @@
---
name: video-use
description: Video-Use workflow for automatic raw footage editing, multi-source video assembly, image/text-to-video composition, filler-word removal, subtitles, color grading, and final.mp4 generation for coding agents. Use when the user gives footage or assets and asks for an edited video, montage, social clip, or creator-ready final.
---
# Video-Use Skill
Use this skill when the user wants to drop raw media into a folder and have an agent produce a polished final video.
Upstream project: https://github.com/browser-use/video-use
## Workflow
1. Ask for or locate the project media folder and target output format.
2. Inventory clips, audio, images, transcripts, and any reference examples.
3. Produce an edit plan: selects, cuts, captions, overlays, color, music, and final duration.
4. Use deterministic media tools such as FFmpeg for trims, concat, subtitles, audio, and export.
5. When animation overlays are needed, combine with HyperFrames, Remotion, or Manim depending on the visual style.
## Good Fits
- Creator clips from raw footage.
- Product demos assembled from screen recordings and overlays.
- Multi-material social videos with captions and music.
- “Here is a folder, make final.mp4” workflows.
Install the upstream Video-Use project for its full coding-agent media pipeline.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 975 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 260 KiB

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 17 KiB

+372 -9
View File
@@ -4,6 +4,136 @@ use boosapi_core::types::*;
use std::path::{Path, PathBuf};
use tauri::State;
struct BuiltinMarketplaceSkill {
name: &'static str,
description: &'static str,
repo: &'static str,
stars: i64,
installs: i64,
keywords: &'static [&'static str],
fallback_skill_name: Option<&'static str>,
}
const BUILTIN_MARKETPLACE_SKILLS: &[BuiltinMarketplaceSkill] = &[
BuiltinMarketplaceSkill {
name: "jianying-editor",
description: "剪映 AI 自动化剪辑 Skill,支持草稿生成、素材导入、字幕、配音、特效和导出。",
repo: "luoluoluo22/jianying-editor-skill",
stars: 0,
installs: 0,
keywords: &["jianying", "剪映", "capcut", "video", "editing", "视频", "剪辑", "boosapi"],
fallback_skill_name: None,
},
BuiltinMarketplaceSkill {
name: "seedance",
description: "中文 Seedance 2.0 / 即梦视频与图片提示词工程,支持 @素材引用、分镜、运镜和长视频流水线。",
repo: "MapleShaw/seedance2.0-prompt-skill",
stars: 0,
installs: 0,
keywords: &["seedance", "即梦", "jimeng", "video", "prompt", "视频", "提示词", "boosapi"],
fallback_skill_name: None,
},
BuiltinMarketplaceSkill {
name: "seedance-prompt-en",
description: "English Seedance 2.0 multimodal AI video prompt writing guide with @Image/@Video/@Audio reference syntax.",
repo: "dexhunter/seedance2-skill",
stars: 0,
installs: 0,
keywords: &["seedance", "jimeng", "dreamina", "video", "prompt", "english", "boosapi"],
fallback_skill_name: None,
},
BuiltinMarketplaceSkill {
name: "dwg-mcp-server",
description: "DWG MCP Server 包装技能:指导 Agent 安装并使用 dwg-mcp 检查 AutoCAD DWG 文件。",
repo: "dimitrovakulenko/dwg-mcp-server",
stars: 0,
installs: 0,
keywords: &["dwg", "cad", "autocad", "mcp", "drawing", "图纸", "建筑", "boosapi"],
fallback_skill_name: Some("dwg-mcp-server"),
},
BuiltinMarketplaceSkill {
name: "cad-ai-agent",
description: "CAD/DXF Agent 包装技能:用于 DXF/DWG/PDF 图纸摘要、合规检查、工程量统计、RFI 和安全编辑流程。",
repo: "jeremylongshore/cad-ai-agent",
stars: 0,
installs: 0,
keywords: &["cad", "dxf", "dwg", "rfi", "takeoff", "compliance", "drawing", "图纸", "boosapi"],
fallback_skill_name: Some("cad-ai-agent"),
},
BuiltinMarketplaceSkill {
name: "gorden-ppt-skill",
description: "中文 PPT 生成与编辑 Skill,内置模板、版式保持、出框检测和 PowerPoint 成品导出流程。",
repo: "GordenSun/GordenPPTSkill",
stars: 0,
installs: 0,
keywords: &["ppt", "powerpoint", "slides", "演示", "幻灯片", "汇报", "boosapi"],
fallback_skill_name: None,
},
BuiltinMarketplaceSkill {
name: "hyperframes",
description: "HyperFrames 文本进成片出:Agent 编写 HTML/CSS/JS 动画并确定性渲染 MP4,适合发布视频、explainers 和 motion graphics。",
repo: "heygen-com/hyperframes",
stars: 0,
installs: 0,
keywords: &["hyperframes", "heygen", "video", "mp4", "motion", "explainer", "html", "animation", "视频", "成片"],
fallback_skill_name: None,
},
BuiltinMarketplaceSkill {
name: "ffmpeg-media",
description: "FFmpeg 专业视频/音频处理 Skill:分析、压缩、转码、剪辑、拼接、字幕、音频提取和批处理命令生成。",
repo: "fabriqaai/ffmpeg-analyse-video-skill",
stars: 0,
installs: 0,
keywords: &["ffmpeg", "ffprobe", "video", "audio", "compress", "transcode", "subtitle", "剪辑", "压缩"],
fallback_skill_name: Some("ffmpeg-media"),
},
BuiltinMarketplaceSkill {
name: "openmontage",
description: "OpenMontage agentic 视频生产系统包装技能,用于智能剪辑、自动拼接、Vlog、研究到脚本到资产到剪辑的流水线。",
repo: "calesthio/OpenMontage",
stars: 0,
installs: 0,
keywords: &["openmontage", "video", "agentic", "vlog", "editing", "montage", "pipeline", "智能剪辑"],
fallback_skill_name: None,
},
BuiltinMarketplaceSkill {
name: "remotion-video",
description: "Remotion React 代码驱动视频 Skill:模板化批量出片、动态 props、数据视频、产品演示和 motion graphics。",
repo: "remotion-dev/skills",
stars: 0,
installs: 0,
keywords: &["remotion", "react", "video", "template", "motion", "render", "batch", "代码视频"],
fallback_skill_name: Some("remotion-video"),
},
BuiltinMarketplaceSkill {
name: "video-use",
description: "Video-Use 素材混剪 Skill:raw footage 到 final.mp4,支持素材组合、字幕、去口癖、color grading 和动画叠加规划。",
repo: "browser-use/video-use",
stars: 0,
installs: 0,
keywords: &["video-use", "browser-use", "footage", "final.mp4", "subtitle", "montage", "素材", "混剪"],
fallback_skill_name: None,
},
BuiltinMarketplaceSkill {
name: "manim-animation",
description: "Manim 数学/知识动画 Skill:3Blue1Brown 风格、公式、图形、概念解释、教学视频和程序化动画。",
repo: "adithya-s-k/manim_skill",
stars: 0,
installs: 0,
keywords: &["manim", "math", "animation", "3blue1brown", "education", "数学", "知识动画", "教学"],
fallback_skill_name: Some("manim-animation"),
},
BuiltinMarketplaceSkill {
name: "knowledge-cat-ppt",
description: "Knowledge Cat PPT Skill:把知识库、文档、笔记和研究材料整理成结构化中文 PPT / PowerPoint。",
repo: "gnipbao/knowledge-cat-ppt-skill",
stars: 0,
installs: 0,
keywords: &["knowledge-cat", "ppt", "powerpoint", "slides", "knowledge", "文档", "知识库", "课件"],
fallback_skill_name: None,
},
];
fn home_dir() -> PathBuf {
dirs::home_dir().expect("Could not determine home directory")
}
@@ -12,8 +142,114 @@ fn skills_dir() -> PathBuf {
boosapi_home().join("skills")
}
struct BundledSkill {
name: &'static str,
source_ref: &'static str,
body: &'static str,
}
const BUNDLED_SKILLS: &[BundledSkill] = &[
BundledSkill {
name: "jianying-editor",
source_ref: "luoluoluo22/jianying-editor-skill",
body: include_str!("../../builtin-skills/jianying-editor/SKILL.md"),
},
BundledSkill {
name: "seedance",
source_ref: "MapleShaw/seedance2.0-prompt-skill",
body: include_str!("../../builtin-skills/seedance/SKILL.md"),
},
BundledSkill {
name: "seedance-prompt-en",
source_ref: "dexhunter/seedance2-skill",
body: include_str!("../../builtin-skills/seedance-prompt-en/SKILL.md"),
},
BundledSkill {
name: "dwg-mcp-server",
source_ref: "dimitrovakulenko/dwg-mcp-server",
body: include_str!("../../builtin-skills/dwg-mcp-server/SKILL.md"),
},
BundledSkill {
name: "cad-ai-agent",
source_ref: "jeremylongshore/cad-ai-agent",
body: include_str!("../../builtin-skills/cad-ai-agent/SKILL.md"),
},
BundledSkill {
name: "gorden-ppt-skill",
source_ref: "GordenSun/GordenPPTSkill",
body: include_str!("../../builtin-skills/gorden-ppt-skill/SKILL.md"),
},
BundledSkill {
name: "hyperframes",
source_ref: "heygen-com/hyperframes",
body: include_str!("../../builtin-skills/hyperframes/SKILL.md"),
},
BundledSkill {
name: "ffmpeg-media",
source_ref: "fabriqaai/ffmpeg-analyse-video-skill",
body: include_str!("../../builtin-skills/ffmpeg-media/SKILL.md"),
},
BundledSkill {
name: "openmontage",
source_ref: "calesthio/OpenMontage",
body: include_str!("../../builtin-skills/openmontage/SKILL.md"),
},
BundledSkill {
name: "remotion-video",
source_ref: "remotion-dev/skills",
body: include_str!("../../builtin-skills/remotion-video/SKILL.md"),
},
BundledSkill {
name: "video-use",
source_ref: "browser-use/video-use",
body: include_str!("../../builtin-skills/video-use/SKILL.md"),
},
BundledSkill {
name: "manim-animation",
source_ref: "adithya-s-k/manim_skill",
body: include_str!("../../builtin-skills/manim-animation/SKILL.md"),
},
BundledSkill {
name: "knowledge-cat-ppt",
source_ref: "gnipbao/knowledge-cat-ppt-skill",
body: include_str!("../../builtin-skills/knowledge-cat-ppt/SKILL.md"),
},
];
pub fn seed_builtin_skills() -> Result<(), String> {
let target_root = skills_dir();
std::fs::create_dir_all(&target_root).map_err(|e| e.to_string())?;
for skill in BUNDLED_SKILLS {
let skill_dir = target_root.join(skill.name);
let skill_file = skill_dir.join("SKILL.md");
if skill_file.exists() {
continue;
}
std::fs::create_dir_all(&skill_dir).map_err(|e| e.to_string())?;
std::fs::write(&skill_file, skill.body).map_err(|e| e.to_string())?;
let manifest = serde_json::json!({
"source_kind": "builtin",
"source_ref": skill.source_ref,
"installed_at": chrono::Utc::now().to_rfc3339(),
"installed_via": "boosapi-desktop"
});
std::fs::write(
skill_dir.join("skill-manifest.json"),
serde_json::to_string_pretty(&manifest).unwrap(),
)
.map_err(|e| e.to_string())?;
}
Ok(())
}
#[tauri::command]
pub async fn list_skills(state: State<'_, AppState>) -> Result<Vec<SkillInfo>, String> {
seed_builtin_skills()?;
let home = home_dir();
let skills = open_agent_sdk::skills::load_all_global(&home);
let disabled = boosapi_core::repo::skill::get_disabled_skills(&state.sea_db)
@@ -220,6 +456,8 @@ async fn install_from_github(owner: &str, repo: &str, target_dir: &Path) -> Resu
}
copy_dir_recursive(&extracted, &skill_target)?;
let installed_name =
ensure_skill_entrypoint(&skill_target, owner, repo).unwrap_or_else(|| repo.to_string());
let manifest = serde_json::json!({
"source_kind": "github",
@@ -236,7 +474,45 @@ async fn install_from_github(owner: &str, repo: &str, target_dir: &Path) -> Resu
)
.map_err(|e| e.to_string())?;
Ok(repo.to_string())
Ok(installed_name)
}
fn ensure_skill_entrypoint(skill_target: &Path, owner: &str, repo: &str) -> Option<String> {
let existing_skill = skill_target.join("SKILL.md");
if existing_skill.exists() {
return read_skill_name(&existing_skill).or_else(|| Some(repo.to_string()));
}
let source_ref = format!("{}/{}", owner, repo);
let builtin = BUILTIN_MARKETPLACE_SKILLS
.iter()
.find(|skill| skill.repo.eq_ignore_ascii_case(&source_ref))?;
let name = builtin.fallback_skill_name?;
let body = BUNDLED_SKILLS
.iter()
.find(|skill| skill.name == name)
.map(|skill| skill.body)?;
if std::fs::write(&existing_skill, body).is_ok() {
Some(name.to_string())
} else {
None
}
}
fn read_skill_name(skill_file: &Path) -> Option<String> {
let text = std::fs::read_to_string(skill_file).ok()?;
let trimmed = text.trim_start();
if !trimmed.starts_with("---") {
return None;
}
let rest = trimmed[3..].trim_start_matches(['\r', '\n']);
let end_idx = rest.find("\n---")?;
let yaml = &rest[..end_idx];
let value: serde_yaml::Value = serde_yaml::from_str(yaml).ok()?;
value
.get("name")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
}
async fn install_from_local(source: &str, target_dir: &Path) -> Result<String, String> {
@@ -407,12 +683,14 @@ pub async fn search_marketplace(
source: Option<String>,
) -> Result<Vec<MarketplaceSkill>, String> {
let installed_refs = installed_source_refs();
let query = query.trim();
let builtin_results = builtin_marketplace_results(query, &installed_refs);
match source.as_deref().unwrap_or("skills.sh") {
"github" => {
let url = format!(
"https://api.github.com/search/repositories?q={}+topic:agent-skill&sort=stars&per_page=20",
urlencoding::encode(&query)
urlencoding::encode(query)
);
let client = reqwest::Client::new();
@@ -431,7 +709,7 @@ pub async fn search_marketplace(
let body: serde_json::Value = response.json().await.map_err(|e| e.to_string())?;
let items = body["items"].as_array().cloned().unwrap_or_default();
let results: Vec<MarketplaceSkill> = items
let mut results: Vec<MarketplaceSkill> = items
.into_iter()
.map(|item| {
let skill_name = item["name"].as_str().unwrap_or("").to_string();
@@ -449,12 +727,17 @@ pub async fn search_marketplace(
})
.collect();
merge_marketplace_results(&mut results, builtin_results);
Ok(results)
}
_ => {
if query.chars().count() < 2 {
return Ok(builtin_results);
}
let url = format!(
"https://skills.sh/api/search?q={}",
urlencoding::encode(&query)
urlencoding::encode(query)
);
let client = reqwest::Client::new();
@@ -470,31 +753,111 @@ pub async fn search_marketplace(
}
let body: serde_json::Value = response.json().await.map_err(|e| e.to_string())?;
let items = body["skills"].as_array().cloned().unwrap_or_default();
let items = body
.get("skills")
.or_else(|| body.get("results"))
.or_else(|| body.get("items"))
.and_then(|value| value.as_array())
.cloned()
.unwrap_or_default();
let results: Vec<MarketplaceSkill> = items
let mut results: Vec<MarketplaceSkill> = items
.into_iter()
.map(|item| {
let skill_name = item["name"].as_str().unwrap_or("").to_string();
let repo = item["source"].as_str().unwrap_or("").to_string();
let skill_name = item
.get("name")
.or_else(|| item.get("skillId"))
.and_then(|value| value.as_str())
.unwrap_or("")
.to_string();
let repo = item
.get("source")
.or_else(|| item.get("repo"))
.or_else(|| item.get("repository"))
.and_then(|value| value.as_str())
.unwrap_or("")
.trim()
.trim_start_matches("https://github.com/")
.trim_end_matches(".git")
.trim_end_matches('/')
.to_string();
let installed =
installed_refs.contains(&repo.trim().trim_end_matches('/').to_lowercase());
MarketplaceSkill {
name: skill_name,
description: String::new(),
description: item["description"].as_str().unwrap_or("").to_string(),
repo,
stars: 0,
installs: item["installs"].as_i64().unwrap_or(0),
installed,
}
})
.filter(|item| !item.repo.trim().is_empty())
.collect();
merge_marketplace_results(&mut results, builtin_results);
Ok(results)
}
}
}
fn builtin_marketplace_results(
query: &str,
installed_refs: &std::collections::HashSet<String>,
) -> Vec<MarketplaceSkill> {
let normalized_query = query.to_lowercase();
let tokens: Vec<&str> = normalized_query
.split(|ch: char| ch.is_whitespace() || ch == '/' || ch == '-' || ch == '_' || ch == '.')
.filter(|token| !token.is_empty())
.collect();
BUILTIN_MARKETPLACE_SKILLS
.iter()
.filter(|skill| {
if tokens.is_empty() {
return true;
}
let haystack = format!(
"{} {} {} {}",
skill.name,
skill.description,
skill.repo,
skill.keywords.join(" ")
)
.to_lowercase();
tokens.iter().all(|token| haystack.contains(token))
})
.map(|skill| MarketplaceSkill {
name: skill.name.to_string(),
description: skill.description.to_string(),
repo: skill.repo.to_string(),
stars: skill.stars,
installs: skill.installs,
installed: installed_refs.contains(&skill.repo.to_lowercase()),
})
.collect()
}
fn merge_marketplace_results(
results: &mut Vec<MarketplaceSkill>,
builtin_results: Vec<MarketplaceSkill>,
) {
for builtin in builtin_results.into_iter().rev() {
if let Some(existing) = results
.iter_mut()
.find(|item| item.repo.eq_ignore_ascii_case(&builtin.repo))
{
if existing.description.is_empty() {
existing.description = builtin.description;
}
existing.name = builtin.name;
existing.installed = existing.installed || builtin.installed;
} else {
results.insert(0, builtin);
}
}
}
#[tauri::command]
pub async fn check_skill_updates() -> Result<Vec<SkillUpdateInfo>, String> {
let skills_path = skills_dir();
+2
View File
@@ -520,6 +520,8 @@ pub fn run() {
version = %app.package_info().version,
"BoosApi setup started"
);
commands::skills::seed_builtin_skills().expect("failed to seed built-in skills");
tracing::info!("BoosApi built-in skills seeded");
// Ensure ~/Documents/boosapi/{images,files,backups}/ exist
boosapi_core::storage_paths::ensure_documents_dirs()
+1 -1
View File
@@ -56,7 +56,7 @@
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDU2QzNGRjRBQkQ4QzlENDQKUldSRW5ZeTlTdi9EVm1CRnUyNWF4d3oxeS9qWmlIQm12VmIvWmdDSEZiR21rMm9hNWRVU2VUTU4K",
"endpoints": [
"https://github.com/Boos4721/boos-chat/releases/latest/download/latest.json"
"https://github.com/Boos4721/BoosAPI/releases/latest/download/latest.json"
]
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 80 KiB

@@ -0,0 +1,523 @@
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 } 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 AgentWorkbenchComposer() {
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 [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([]);
} 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,
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 -70
View File
@@ -2,16 +2,13 @@ import React, { useMemo, useCallback, useRef, useState, useEffect, useSyncExtern
import { CloseCircleFilled, SyncOutlined } from '@ant-design/icons';
import { Typography, Button, Dropdown, Input, App, Avatar, Alert, Popconfirm, Popover, theme, Tag, Image, Tooltip, Modal, Spin } from 'antd';
import type { InputRef } from 'antd';
import { Pencil, Share2, FileImage, FileCode, FileText, FileType, Bot, Brain, Lightbulb, Code, Languages, Copy, Check, RotateCcw, User, Trash2, ChevronLeft, ChevronRight, ChevronDown, Scissors, Paperclip, AlertCircle, X, ArrowDown, ArrowUp, ArrowLeftRight, Zap, Sparkles, TextCursorInput, GitBranch, ChartNoAxesColumn, MessageSquare, ArrowUpRight, ArrowDownRight, Coins, Clock, Timer, Download, PanelLeftClose, PanelLeftOpen } from 'lucide-react';
import { Pencil, Share2, FileImage, FileCode, FileText, FileType, Bot, Brain, Code, Copy, Check, RotateCcw, User, Trash2, ChevronLeft, ChevronRight, ChevronDown, Scissors, Paperclip, AlertCircle, X, ArrowDown, ArrowUp, ArrowLeftRight, Zap, Sparkles, TextCursorInput, GitBranch, ChartNoAxesColumn, MessageSquare, ArrowUpRight, ArrowDownRight, Coins, Clock, Timer, Download, PanelLeftClose, PanelLeftOpen } from 'lucide-react';
import { ModelIcon } from '@lobehub/icons';
import { getConvIcon } from '@/lib/convIcon';
import { normalizeAutoConversationTitle } from '@/lib/conversationTitle';
import Bubble from '@ant-design/x/es/bubble';
import Prompts from '@ant-design/x/es/prompts';
import Actions from '@ant-design/x/es/actions';
import Think from '@ant-design/x/es/think';
import type { BubbleItemType, BubbleListRef, RoleType } from '@ant-design/x/es/bubble/interface';
import type { PromptsItemType } from '@ant-design/x/es/prompts';
import NodeRenderer, { setCustomComponents, withMarkstreamComponentDisplay, type NodeComponentProps, type CodeBlockActionContext, type CodeBlockPreviewPayload, type MermaidBlockActionContext, type InfographicBlockActionContext } from 'markstream-react';
import { useTranslation } from 'react-i18next';
import { CodeBlockHeaderActions } from './CodeBlockHeaderActions';
@@ -2111,7 +2108,6 @@ export function ChatView() {
const [mermaidPreviewSvg, setMermaidPreviewSvg] = useState<string | null>(null);
const [mermaidPreviewOpen, setMermaidPreviewOpen] = useState(false);
const [streamStatusNow, setStreamStatusNow] = useState(() => Date.now());
const createConversation = useConversationStore((s) => s.createConversation);
const providers = useProviderStore((s) => s.providers);
const settings = useSettingsStore((s) => s.settings);
const saveSettings = useSettingsStore((s) => s.saveSettings);
@@ -2780,62 +2776,6 @@ export function ChatView() {
[messages, activeConversation, t, messageApi],
);
// ── Welcome prompt items ───────────────────────────────────────────
const greetingText = useMemo(() => {
const hour = new Date().getHours();
let key: string;
if (hour >= 5 && hour < 12) key = 'chat.greetingMorning';
else if (hour >= 12 && hour < 14) key = 'chat.greetingNoon';
else if (hour >= 14 && hour < 18) key = 'chat.greetingAfternoon';
else key = 'chat.greetingEvening';
return `👋 ${t(key)}`;
}, [t]);
const promptItems: PromptsItemType[] = useMemo(
() => [
{ key: '1', icon: <Lightbulb size={16} />, label: t('chat.welcomePrompt1') },
{ key: '2', icon: <Languages size={16} />, label: t('chat.welcomePrompt2') },
{ key: '3', icon: <Code size={16} />, label: t('chat.welcomePrompt3') },
{ key: '4', icon: <Lightbulb size={16} />, label: t('chat.welcomePrompt4') },
],
[t],
);
const handlePromptClick = useCallback(
async (info: { data: PromptsItemType }) => {
const text = typeof info.data.label === 'string' ? info.data.label : '';
if (!text) return;
try {
if (!activeConversationId) {
// Prefer settings default model, fall back to first enabled
let provider = settings.default_provider_id
? providers.find((p) => p.id === settings.default_provider_id && p.enabled)
: undefined;
let model = provider?.models.find(
(m) => m.model_id === settings.default_model_id && m.enabled,
);
if (!provider || !model) {
provider = providers.find((p) => p.enabled && p.models.some((m) => m.enabled));
model = provider?.models.find((m) => m.enabled);
}
if (!provider || !model) {
messageApi.warning(t('chat.noModel'));
return;
}
await createConversation(normalizeAutoConversationTitle(text), model.model_id, provider.id);
}
// Route through InputArea's send pipeline so companion models are respected
useConversationStore.getState().setPendingPromptText(text);
} catch (e) {
console.error('[handlePromptClick] error:', e);
messageApi.error(String(e));
}
},
[activeConversationId, providers, settings, createConversation, messageApi, t],
);
// ── Bubble items (only show active messages) ────────────────────────
const activeMessages = useMemo(
() => messages.filter((msg) => msg.is_active !== false),
@@ -4200,16 +4140,16 @@ export function ChatView() {
</Typography.Text>
</div>
) : (
<div className="flex flex-col items-center justify-center h-full" style={{ padding: '0 24px' }}>
<Typography.Title level={3} style={{ marginBottom: 24, fontWeight: 500 }}>
{greetingText}
<div
className="flex flex-col items-center justify-center h-full"
style={{ gap: 8, padding: '0 24px', color: token.colorTextSecondary }}
>
<Typography.Title level={3} style={{ margin: 0, color: token.colorText }}>
{t('chat.welcome')}
</Typography.Title>
<Prompts
items={promptItems}
onItemClick={handlePromptClick}
wrap
style={{ marginTop: 16 }}
/>
<Typography.Text type="secondary">
{t('chat.emptyPrompt', '输入消息开始对话,或在下方切换 Agent / 创作模式。')}
</Typography.Text>
</div>
)
) : (
+236 -45
View File
@@ -1,9 +1,9 @@
import React, { useState, useRef, useCallback, useEffect, useMemo } from 'react';
import { Button, Tooltip, App, theme, Dropdown, Tag, Popover, Checkbox, Badge, Popconfirm } from 'antd';
import type { MenuProps } from 'antd';
import { Paperclip, Trash2, Mic, Eraser, Scissors, Globe, Brain, Atom, Plug, SlidersHorizontal, ArrowUp, Square, Check, Zap, ZapOff, Shrink, Upload, GitCompareArrows, X, BookOpen, GripHorizontal, CircleOff, SignalLow, SignalMedium, SignalHigh, Signal, Bot, MessageSquare, Shield, ShieldCheck, ShieldAlert, FolderOpen, ExternalLink } from 'lucide-react';
import { Paperclip, Trash2, Mic, Eraser, Scissors, Globe, Brain, Atom, Plug, SlidersHorizontal, ArrowUp, Square, Check, Zap, ZapOff, Shrink, Upload, GitCompareArrows, X, BookOpen, GripHorizontal, CircleOff, SignalLow, SignalMedium, SignalHigh, Signal, MessageSquare, Shield, ShieldCheck, ShieldAlert, FolderOpen, ExternalLink, ImagePlus, Clapperboard, Music2, Mic2, UserRound, WandSparkles, Sparkles, Wrench } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useConversationStore, useProviderStore, useSettingsStore, useSearchStore, useMcpStore, useMemoryStore, useKnowledgeStore } from '@/stores';
import { useConversationStore, useProviderStore, useSettingsStore, useSearchStore, useMcpStore, useMemoryStore, useKnowledgeStore, useSkillStore } from '@/stores';
import { useUIStore } from '@/stores/uiStore';
import { findModelByIds, supportsReasoning, modelHasCapability } from '@/lib/modelCapabilities';
import {
@@ -41,6 +41,22 @@ const DOCUMENT_ATTACHMENT_MIME_BY_EXTENSION: Record<string, string> = {
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
};
type ComposerCreationMode = 'chat' | 'agent' | 'image' | 'video' | 'music' | 'voice' | 'digital-human' | 'action';
const MEDIA_ATTACHMENT_ACCEPT = 'image/*,video/*,audio/*';
const ANY_ATTACHMENT_ACCEPT = `${MEDIA_ATTACHMENT_ACCEPT},${DOCUMENT_ATTACHMENT_ACCEPT}`;
const JIANYING_SKILL_NAME = 'jianying-editor';
const JIANYING_SKILL_SOURCE = 'luoluoluo22/jianying-editor-skill';
const JIANYING_PROMPT_PREFIX = '请使用 jianying-editor Skill 自动创建剪映草稿:';
const CREATIVE_PROMPT_PREFIX: Record<Exclude<ComposerCreationMode, 'chat' | 'agent'>, string> = {
image: '请使用 BoosAPI 的图片生成能力创作:',
video: '请使用 BoosAPI 的视频生成能力创作:',
music: '请为下面内容生成配乐方案:',
voice: '请使用配音/TTS 能力生成语音:',
'digital-human': '请使用数字人能力生成口播视频:',
action: '请使用动作模仿/动作迁移能力处理:',
};
function getAttachmentMimeType(fileName: string, mimeType?: string): string {
if (mimeType) return mimeType;
const extension = fileName.split('.').pop()?.toLowerCase() || '';
@@ -50,11 +66,15 @@ function getAttachmentMimeType(fileName: string, mimeType?: string): string {
function isAllowedAttachmentFile(
fileName: string,
mimeType: string | undefined,
hasVision: boolean,
_hasVision: boolean,
documentAttachmentReadingEnabled: boolean,
): boolean {
const effectiveMimeType = getAttachmentMimeType(fileName, mimeType);
if (hasVision && effectiveMimeType.startsWith('image/')) return true;
if (
effectiveMimeType.startsWith('image/')
|| effectiveMimeType.startsWith('video/')
|| effectiveMimeType.startsWith('audio/')
) return true;
if (!documentAttachmentReadingEnabled) return false;
return Object.values(DOCUMENT_ATTACHMENT_MIME_BY_EXTENSION).includes(effectiveMimeType);
}
@@ -90,6 +110,9 @@ export function InputArea() {
const [settingsOpen, setSettingsOpen] = useState(false);
const [mcpPopoverOpen, setMcpPopoverOpen] = useState(false);
const [searchDropdownOpen, setSearchDropdownOpen] = useState(false);
const [pendingMode, setPendingMode] = useState<ComposerCreationMode>('chat');
const [skillsDropdownOpen, setSkillsDropdownOpen] = useState(false);
const [installingJianying, setInstallingJianying] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const valueRef = useRef(value);
@@ -143,6 +166,10 @@ export function InputArea() {
const conversations = useConversationStore((s) => s.conversations);
const providers = useProviderStore((s) => s.providers);
const settings = useSettingsStore((s) => s.settings);
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 shortcutHint = useCallback((label: string, action: ShortcutAction) => {
if (!settings) return label;
@@ -226,6 +253,10 @@ export function InputArea() {
const activeConversation = conversations.find((c) => c.id === activeConversationId);
const currentMode = activeConversation?.mode || 'chat';
const selectedComposerMode: ComposerCreationMode = currentMode === 'agent' ? 'agent' : pendingMode;
const jianyingSkill = useMemo(() => (
skills.find((item) => item.name === JIANYING_SKILL_NAME) ?? null
), [skills]);
const setActivePage = useUIStore((s) => s.setActivePage);
const setSettingsSection = useUIStore((s) => s.setSettingsSection);
@@ -761,11 +792,63 @@ export function InputArea() {
hasVision: modelHasCapability(currentModel, 'Vision'),
}), [activeConversation, currentModel, providers]);
const documentAttachmentReadingEnabled = settings.document_attachment_reading_enabled ?? false;
const canAttachFiles = hasVision || documentAttachmentReadingEnabled;
const fileInputAccept = [
hasVision ? 'image/*' : null,
documentAttachmentReadingEnabled ? DOCUMENT_ATTACHMENT_ACCEPT : null,
].filter(Boolean).join(',');
const fileInputAccept = documentAttachmentReadingEnabled ? ANY_ATTACHMENT_ACCEPT : MEDIA_ATTACHMENT_ACCEPT;
const creationModeItems = useMemo<MenuProps['items']>(() => [
{
key: 'chat',
icon: <MessageSquare size={15} />,
label: (
<div>
<strong>{t('common.chatMode')}</strong>
<div style={{ fontSize: 12, color: token.colorTextSecondary }}>普通问答与多模型对话</div>
</div>
),
},
{
key: 'agent',
icon: <WandSparkles size={15} />,
label: (
<div>
<strong>{t('common.agentMode')} <Tag color="blue" style={{ fontSize: 10, lineHeight: '16px', padding: '0 4px', marginLeft: 4 }}>Beta</Tag></strong>
<div style={{ fontSize: 12, color: token.colorTextSecondary }}>拆解任务、读写文件、执行工具</div>
</div>
),
},
{
type: 'divider',
},
{
key: 'image',
icon: <ImagePlus size={15} />,
label: <div><strong>图片生成</strong><div style={{ fontSize: 12, color: token.colorTextSecondary }}>即梦图片、参考图与风格控制</div></div>,
},
{
key: 'video',
icon: <Clapperboard size={15} />,
label: <div><strong>视频生成</strong><div style={{ fontSize: 12, color: token.colorTextSecondary }}>即梦视频、图生视频与分镜</div></div>,
},
{
key: 'music',
icon: <Music2 size={15} />,
label: <div><strong>音乐生成</strong><div style={{ fontSize: 12, color: token.colorTextSecondary }}>配乐、氛围与短片 BGM</div></div>,
},
{
key: 'voice',
icon: <Mic2 size={15} />,
label: <div><strong>配音生成</strong><div style={{ fontSize: 12, color: token.colorTextSecondary }}>TTS 音色、语速与试听</div></div>,
},
{
key: 'digital-human',
icon: <UserRound size={15} />,
label: <div><strong>数字人</strong><div style={{ fontSize: 12, color: token.colorTextSecondary }}>口播、形象与视频工作流</div></div>,
},
{
key: 'action',
icon: <Sparkles size={15} />,
label: <div><strong>动作模仿</strong><div style={{ fontSize: 12, color: token.colorTextSecondary }}>动作迁移与主体参考</div></div>,
},
], [t, token.colorTextSecondary]);
// Current model key for excluding from multi-select (no longer used - users can select any model)
@@ -821,6 +904,7 @@ export function InputArea() {
);
const handleModeSwitch = useCallback(async (mode: 'chat' | 'agent') => {
setPendingMode(mode);
if (!activeConversation) return;
await updateConversation(activeConversation.id, { mode });
@@ -853,6 +937,50 @@ export function InputArea() {
}
}, [activeConversation, updateConversation, companionModels, companionStorageKey]);
const handleCreationModeSelect = useCallback(async (mode: ComposerCreationMode) => {
if (mode === 'chat' || mode === 'agent') {
await handleModeSwitch(mode);
return;
}
setPendingMode(mode);
if (activeConversation) {
await updateConversation(activeConversation.id, { mode: 'chat' });
}
setValue((current) => {
const prefix = CREATIVE_PROMPT_PREFIX[mode];
const text = current.trim();
if (!text || text.startsWith(prefix)) return current;
return `${prefix}${text}`;
});
}, [activeConversation, handleModeSwitch, updateConversation]);
const applyJianyingSkill = useCallback(async () => {
if (installingJianying) return;
setInstallingJianying(true);
try {
if (!jianyingSkill) {
await installSkill(JIANYING_SKILL_SOURCE);
await loadSkills();
messageApi.success('剪映 Skill 已安装');
} else if (!jianyingSkill.enabled) {
await toggleSkill(JIANYING_SKILL_NAME, true);
await loadSkills();
messageApi.success('剪映 Skill 已启用');
}
await handleModeSwitch('agent');
setValue((current) => {
const text = current.trim();
if (text.includes(JIANYING_SKILL_NAME) || text.includes('剪映')) return current;
return text ? `${JIANYING_PROMPT_PREFIX}${text}` : JIANYING_PROMPT_PREFIX;
});
setSkillsDropdownOpen(false);
} catch (error) {
messageApi.error(`剪映 Skill 集成失败:${String(error)}`);
} finally {
setInstallingJianying(false);
}
}, [handleModeSwitch, installSkill, installingJianying, jianyingSkill, loadSkills, messageApi, toggleSkill]);
const handleSend = useCallback(async () => {
const trimmed = value.trim();
if (!trimmed) return;
@@ -894,12 +1022,30 @@ export function InputArea() {
textareaRef.current.style.height = 'auto';
}
});
if (currentMode === 'agent') {
if (selectedComposerMode === 'agent') {
const nextConversation = useConversationStore.getState().conversations.find(
(conversation) => conversation.id === useConversationStore.getState().activeConversationId,
);
const nextConversationId = useConversationStore.getState().activeConversationId;
if (nextConversation && nextConversation.mode !== 'agent') {
await updateConversation(nextConversation.id, { mode: 'agent' });
}
if (nextConversationId) {
try {
await invoke('agent_update_session', { conversationId: nextConversationId });
} catch (e) {
console.warn('Failed to init agent session before send:', e);
}
}
await sendAgentMessage(trimmed, attachments);
} else if (companionModels.length > 0) {
await sendMultiModelMessage(trimmed, companionModels, attachments, searchEnabled ? searchProviderId : null);
const creativePrefix = selectedComposerMode !== 'chat' ? CREATIVE_PROMPT_PREFIX[selectedComposerMode] : '';
const content = creativePrefix && !trimmed.startsWith(creativePrefix) ? `${creativePrefix}${trimmed}` : trimmed;
await sendMultiModelMessage(content, companionModels, attachments, searchEnabled ? searchProviderId : null);
} else {
await sendMessage(trimmed, attachments, searchEnabled ? searchProviderId : null);
const creativePrefix = selectedComposerMode !== 'chat' ? CREATIVE_PROMPT_PREFIX[selectedComposerMode] : '';
const content = creativePrefix && !trimmed.startsWith(creativePrefix) ? `${creativePrefix}${trimmed}` : trimmed;
await sendMessage(content, attachments, searchEnabled ? searchProviderId : null);
}
} catch (e) {
setValue((current) => current || trimmed);
@@ -918,7 +1064,7 @@ export function InputArea() {
}
});
}
}, [value, attachedFiles, sendMessage, sendAgentMessage, sendMultiModelMessage, companionModels, activeConversationId, providers, settings, createConversation, messageApi, t, searchEnabled, searchProviderId, currentMode]);
}, [value, attachedFiles, sendMessage, sendAgentMessage, sendMultiModelMessage, companionModels, activeConversationId, providers, settings, createConversation, messageApi, t, searchEnabled, searchProviderId, selectedComposerMode, updateConversation]);
const handleFillLastMessage = useCallback(() => {
if (streaming) return;
@@ -969,7 +1115,6 @@ export function InputArea() {
}, []);
const handlePaste = useCallback((e: React.ClipboardEvent<HTMLTextAreaElement>) => {
if (!canAttachFiles) return;
const items = e.clipboardData?.items;
if (!items) return;
const files: File[] = [];
@@ -993,14 +1138,12 @@ export function InputArea() {
e.preventDefault();
setAttachedFiles((prev) => [...prev, ...files]);
}
}, [canAttachFiles, documentAttachmentReadingEnabled, hasVision]);
}, [documentAttachmentReadingEnabled, hasVision]);
// Drag-and-drop overlay (Tauri native)
const [isDragging, setIsDragging] = useState(false);
useEffect(() => {
if (!canAttachFiles) return;
let unlisten: (() => void) | undefined;
(async () => {
@@ -1025,6 +1168,8 @@ export function InputArea() {
png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg',
gif: 'image/gif', webp: 'image/webp', svg: 'image/svg+xml',
bmp: 'image/bmp', ico: 'image/x-icon',
mp4: 'video/mp4', mov: 'video/quicktime', webm: 'video/webm',
mp3: 'audio/mpeg', wav: 'audio/wav', m4a: 'audio/mp4',
pdf: 'application/pdf',
doc: 'application/msword',
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
@@ -1061,7 +1206,7 @@ export function InputArea() {
return () => {
unlisten?.();
};
}, [canAttachFiles, documentAttachmentReadingEnabled, hasVision]);
}, [documentAttachmentReadingEnabled, hasVision]);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
@@ -1404,17 +1549,15 @@ export function InputArea() {
</Tooltip>
</Dropdown>
)}
{canAttachFiles && (
<Tooltip title={t('chat.attachFile')}>
<Button
aria-label={t('chat.attachFile')}
type="text"
size="small"
icon={<Paperclip size={14} />}
onClick={handleFileSelect}
/>
</Tooltip>
)}
<Tooltip title={t('chat.attachFile')}>
<Button
aria-label={t('chat.attachFile')}
type="text"
size="small"
icon={<Paperclip size={14} />}
onClick={handleFileSelect}
/>
</Tooltip>
<Popover
trigger="click"
placement="topLeft"
@@ -1599,30 +1742,78 @@ export function InputArea() {
<div className="flex items-center gap-1">
<Dropdown
menu={{
items: [
{
key: 'chat',
icon: <MessageSquare size={14} />,
label: t('common.chatMode'),
},
{
key: 'agent',
icon: <Bot size={14} />,
label: <>{t('common.agentMode')} <Tag color="blue" style={{ fontSize: 10, lineHeight: '16px', padding: '0 4px', marginLeft: 2 }}>Beta</Tag></>,
},
],
selectedKeys: [currentMode],
onClick: ({ key }) => handleModeSwitch(key as 'chat' | 'agent'),
items: creationModeItems,
selectedKeys: [selectedComposerMode],
onClick: ({ key }) => void handleCreationModeSelect(key as ComposerCreationMode),
}}
trigger={['click']}
placement="topLeft"
>
<Button
type="text"
size="small"
icon={currentMode === 'agent' ? <Bot size={14} /> : <MessageSquare size={14} />}
icon={
selectedComposerMode === 'agent' ? <WandSparkles size={14} />
: selectedComposerMode === 'image' ? <ImagePlus size={14} />
: selectedComposerMode === 'video' ? <Clapperboard size={14} />
: selectedComposerMode === 'music' ? <Music2 size={14} />
: selectedComposerMode === 'voice' ? <Mic2 size={14} />
: selectedComposerMode === 'digital-human' ? <UserRound size={14} />
: selectedComposerMode === 'action' ? <Sparkles size={14} />
: <MessageSquare size={14} />
}
style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 12 }}
>
{currentMode === 'agent' ? <>{t('common.agentMode')} <Tag color="blue" style={{ fontSize: 10, lineHeight: '16px', padding: '0 4px', marginLeft: 2, marginRight: 0 }}>Beta</Tag></> : t('common.chatMode')}
{selectedComposerMode === 'agent' ? <>{t('common.agentMode')} <Tag color="blue" style={{ fontSize: 10, lineHeight: '16px', padding: '0 4px', marginLeft: 2, marginRight: 0 }}>Beta</Tag></>
: selectedComposerMode === 'image' ? '图片生成'
: selectedComposerMode === 'video' ? '视频生成'
: selectedComposerMode === 'music' ? '音乐生成'
: selectedComposerMode === 'voice' ? '配音生成'
: selectedComposerMode === 'digital-human' ? '数字人'
: selectedComposerMode === 'action' ? '动作模仿'
: t('common.chatMode')}
</Button>
</Dropdown>
<Dropdown
open={skillsDropdownOpen}
onOpenChange={(open) => {
setSkillsDropdownOpen(open);
if (open) void loadSkills();
}}
menu={{
items: [
{
key: 'jianying',
icon: <Wrench size={14} />,
label: (
<div>
<strong>剪映短片</strong>
<div style={{ fontSize: 12, color: token.colorTextSecondary }}>
{jianyingSkill?.enabled ? '已启用,可直接用 Agent 创建剪映草稿' : '安装/启用 jianying-editor Skill'}
</div>
</div>
),
onClick: () => void applyJianyingSkill(),
},
{
key: 'skills-page',
icon: <BookOpen size={14} />,
label: '管理技能',
onClick: () => setActivePage('skills'),
},
],
}}
trigger={['click']}
placement="topLeft"
>
<Button
type="text"
size="small"
loading={installingJianying}
icon={<Wrench size={14} />}
style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 12 }}
>
使用技能
</Button>
</Dropdown>
{currentMode === 'agent' && (
+3 -2
View File
@@ -303,7 +303,8 @@ export function TitleBar() {
}
}, [message, t]);
const GITHUB_REPO = 'https://github.com/Boos4721/boos-chat';
const GITHUB_PROFILE = 'https://github.com/Boos4721/';
const GITHUB_REPO = 'https://github.com/Boos4721/BoosAPI';
const githubMenuItems: MenuProps['items'] = [
{
key: 'feature',
@@ -323,7 +324,7 @@ export function TitleBar() {
},
];
const handleGithubClick: MenuProps['onClick'] = ({ key }) => {
let url = GITHUB_REPO;
let url = GITHUB_PROFILE;
if (key === 'feature') url = `${GITHUB_REPO}/issues/new?labels=enhancement&template=feature_request.yml`;
else if (key === 'bug') url = `${GITHUB_REPO}/issues/new?labels=bug&template=bug_report.yml`;
if (isTauri()) {
@@ -86,7 +86,7 @@ export function AboutPage() {
<span>{t('settings.website')}</span>
<Button
icon={<Globe size={16} />}
href="https://boos.lat"
href="https://tybossai.com/"
target="_blank"
type="link"
>
@@ -98,7 +98,7 @@ export function AboutPage() {
<span>GitHub</span>
<Button
icon={<Github size={16} />}
href="https://github.com/Boos4721/boos-chat"
href="https://github.com/Boos4721/"
target="_blank"
type="link"
>
+1
View File
@@ -4,6 +4,7 @@
},
"nav": {
"chat": "المحادثة",
"agentWorkbench": "Agent",
"knowledge": "قاعدة المعرفة",
"memory": "الذاكرة",
"gateway": "البوابة",
+1
View File
@@ -4,6 +4,7 @@
},
"nav": {
"chat": "Chat",
"agentWorkbench": "Agent",
"knowledge": "Wissen",
"memory": "Gedächtnis",
"gateway": "Gateway",
+1
View File
@@ -4,6 +4,7 @@
},
"nav": {
"chat": "Chat",
"agentWorkbench": "Agent",
"knowledge": "Knowledge",
"memory": "Memory",
"gateway": "Gateway",
+1
View File
@@ -4,6 +4,7 @@
},
"nav": {
"chat": "Chat",
"agentWorkbench": "Agent",
"knowledge": "Base de conocimientos",
"memory": "Memoria",
"gateway": "Pasarela",
+1
View File
@@ -4,6 +4,7 @@
},
"nav": {
"chat": "Chat",
"agentWorkbench": "Agent",
"knowledge": "Connaissances",
"memory": "Mémoire",
"gateway": "Passerelle",
+1
View File
@@ -4,6 +4,7 @@
},
"nav": {
"chat": "चैट",
"agentWorkbench": "Agent",
"knowledge": "ज्ञान आधार",
"memory": "स्मृति",
"gateway": "गेटवे",
+1
View File
@@ -4,6 +4,7 @@
},
"nav": {
"chat": "チャット",
"agentWorkbench": "Agent",
"knowledge": "ナレッジ",
"memory": "メモリ",
"gateway": "ゲートウェイ",
+1
View File
@@ -4,6 +4,7 @@
},
"nav": {
"chat": "채팅",
"agentWorkbench": "Agent",
"knowledge": "지식",
"memory": "메모리",
"gateway": "게이트웨이",
+1
View File
@@ -4,6 +4,7 @@
},
"nav": {
"chat": "Чат",
"agentWorkbench": "Agent",
"knowledge": "База знаний",
"memory": "Память",
"gateway": "Шлюз",
+1
View File
@@ -4,6 +4,7 @@
},
"nav": {
"chat": "对话",
"agentWorkbench": "Agent",
"knowledge": "知识库",
"memory": "记忆",
"gateway": "网关",

Some files were not shown because too many files have changed in this diff Show More