Compare commits
14 Commits
main
...
be1f388b26
| Author | SHA1 | Date | |
|---|---|---|---|
| be1f388b26 | |||
| bdfb9b8dc2 | |||
| 2a5eca83c8 | |||
| 8c9fbea822 | |||
| 6863ccd16c | |||
| 7e1bfdd7fe | |||
| 1fd7af1246 | |||
| 9ae9224669 | |||
| cd08ac931d | |||
| 424aaa9b1f | |||
| bc99c678f9 | |||
| 22e6c162b6 | |||
| 397274be0c | |||
| 86bbfe2f3c |
@@ -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
|
||||||
@@ -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": {}
|
||||||
|
}
|
||||||
@@ -134,13 +134,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
bun-version: latest
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
run_install: false
|
|
||||||
|
|
||||||
- name: Get linked dependency versions
|
- name: Get linked dependency versions
|
||||||
id: dep-versions
|
id: dep-versions
|
||||||
@@ -168,15 +165,15 @@ jobs:
|
|||||||
if: steps.cache-linked-deps.outputs.cache-hit != 'true'
|
if: steps.cache-linked-deps.outputs.cache-hit != 'true'
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
pnpm --dir libs/stream-monaco install --frozen-lockfile
|
bun --cwd libs/stream-monaco install --frozen-lockfile
|
||||||
pnpm --dir libs/stream-monaco build
|
bun --cwd libs/stream-monaco run build
|
||||||
|
|
||||||
pnpm --dir libs/markstream-vue install --frozen-lockfile
|
bun --cwd libs/markstream-vue install --frozen-lockfile
|
||||||
pnpm --dir libs/markstream-vue run build:parser
|
bun --cwd libs/markstream-vue run build:parser
|
||||||
pnpm --dir libs/markstream-vue/packages/markstream-react build
|
bun --cwd libs/markstream-vue/packages/markstream-react run build
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build with Tauri
|
- name: Build with Tauri
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
@@ -192,7 +189,7 @@ jobs:
|
|||||||
releaseBody: ${{ needs.draft-release-notes.outputs.body }}
|
releaseBody: ${{ needs.draft-release-notes.outputs.body }}
|
||||||
releaseDraft: true
|
releaseDraft: true
|
||||||
prerelease: ${{ contains(github.ref_name, '-rc') }}
|
prerelease: ${{ contains(github.ref_name, '-rc') }}
|
||||||
tauriScript: pnpm tauri
|
tauriScript: bun run tauri
|
||||||
args: --target ${{ matrix.target }}
|
args: --target ${{ matrix.target }}
|
||||||
includeUpdaterJson: true
|
includeUpdaterJson: true
|
||||||
|
|
||||||
@@ -268,13 +265,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
bun-version: latest
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
run_install: false
|
|
||||||
|
|
||||||
- name: Get linked dependency versions
|
- name: Get linked dependency versions
|
||||||
id: dep-versions
|
id: dep-versions
|
||||||
@@ -300,22 +294,22 @@ jobs:
|
|||||||
- name: Build linked dependencies
|
- name: Build linked dependencies
|
||||||
if: steps.cache-linked-deps.outputs.cache-hit != 'true'
|
if: steps.cache-linked-deps.outputs.cache-hit != 'true'
|
||||||
run: |
|
run: |
|
||||||
pnpm --dir libs/stream-monaco install --frozen-lockfile
|
bun --cwd libs/stream-monaco install --frozen-lockfile
|
||||||
pnpm --dir libs/stream-monaco build
|
bun --cwd libs/stream-monaco run build
|
||||||
|
|
||||||
pnpm --dir libs/markstream-vue install --frozen-lockfile
|
bun --cwd libs/markstream-vue install --frozen-lockfile
|
||||||
pnpm --dir libs/markstream-vue run build:parser
|
bun --cwd libs/markstream-vue run build:parser
|
||||||
pnpm --dir libs/markstream-vue/packages/markstream-react build
|
bun --cwd libs/markstream-vue/packages/markstream-react run build
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build for Linux ARM64
|
- name: Build for Linux ARM64
|
||||||
run: |
|
run: |
|
||||||
export PKG_CONFIG_ALLOW_CROSS=1
|
export PKG_CONFIG_ALLOW_CROSS=1
|
||||||
export PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/:$PKG_CONFIG_PATH
|
export PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/:$PKG_CONFIG_PATH
|
||||||
export PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/
|
export PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/
|
||||||
pnpm tauri build --target aarch64-unknown-linux-gnu --bundles deb,rpm
|
bun run tauri build --target aarch64-unknown-linux-gnu --bundles deb,rpm
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: '--max_old_space_size=4096'
|
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||||
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
|
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
|
||||||
|
|||||||
@@ -41,13 +41,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
bun-version: latest
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
run_install: false
|
|
||||||
|
|
||||||
- name: Get linked dependency versions
|
- name: Get linked dependency versions
|
||||||
id: dep-versions
|
id: dep-versions
|
||||||
@@ -73,15 +70,15 @@ jobs:
|
|||||||
- name: Build linked dependencies
|
- name: Build linked dependencies
|
||||||
if: steps.cache-linked-deps.outputs.cache-hit != 'true'
|
if: steps.cache-linked-deps.outputs.cache-hit != 'true'
|
||||||
run: |
|
run: |
|
||||||
pnpm --dir libs/stream-monaco install --frozen-lockfile
|
bun --cwd libs/stream-monaco install --frozen-lockfile
|
||||||
pnpm --dir libs/stream-monaco build
|
bun --cwd libs/stream-monaco run build
|
||||||
|
|
||||||
pnpm --dir libs/markstream-vue install --frozen-lockfile
|
bun --cwd libs/markstream-vue install --frozen-lockfile
|
||||||
pnpm --dir libs/markstream-vue run build:parser
|
bun --cwd libs/markstream-vue run build:parser
|
||||||
pnpm --dir libs/markstream-vue/packages/markstream-react build
|
bun --cwd libs/markstream-vue/packages/markstream-react run build
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build with Tauri
|
- name: Build with Tauri
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
@@ -97,5 +94,5 @@ jobs:
|
|||||||
releaseBody: 'Internal test build for scrollbar fix verification.'
|
releaseBody: 'Internal test build for scrollbar fix verification.'
|
||||||
releaseDraft: true
|
releaseDraft: true
|
||||||
prerelease: true
|
prerelease: true
|
||||||
tauriScript: pnpm tauri
|
tauriScript: bun run tauri
|
||||||
args: --target aarch64-apple-darwin
|
args: --target aarch64-apple-darwin
|
||||||
|
|||||||
@@ -51,13 +51,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
bun-version: latest
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
run_install: false
|
|
||||||
|
|
||||||
- name: Get linked dependency versions
|
- name: Get linked dependency versions
|
||||||
id: dep-versions
|
id: dep-versions
|
||||||
@@ -84,18 +81,18 @@ jobs:
|
|||||||
- name: Build linked dependencies
|
- name: Build linked dependencies
|
||||||
if: steps.cache-linked-deps.outputs.cache-hit != 'true'
|
if: steps.cache-linked-deps.outputs.cache-hit != 'true'
|
||||||
run: |
|
run: |
|
||||||
pnpm --dir libs/stream-monaco install --frozen-lockfile
|
bun --cwd libs/stream-monaco install --frozen-lockfile
|
||||||
pnpm --dir libs/stream-monaco build
|
bun --cwd libs/stream-monaco run build
|
||||||
|
|
||||||
pnpm --dir libs/markstream-vue install --frozen-lockfile
|
bun --cwd libs/markstream-vue install --frozen-lockfile
|
||||||
pnpm --dir libs/markstream-vue run build:parser
|
bun --cwd libs/markstream-vue run build:parser
|
||||||
pnpm --dir libs/markstream-vue/packages/markstream-react build
|
bun --cwd libs/markstream-vue/packages/markstream-react run build
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build Linux bundles
|
- name: Build Linux bundles
|
||||||
run: pnpm tauri build --target x86_64-unknown-linux-gnu --bundles appimage,rpm,deb
|
run: bun run tauri build --target x86_64-unknown-linux-gnu --bundles appimage,rpm,deb
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: '--max_old_space_size=4096'
|
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||||
BOOSAPI_TEST_BUILD: '1'
|
BOOSAPI_TEST_BUILD: '1'
|
||||||
|
|||||||
@@ -42,13 +42,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
bun-version: latest
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
run_install: false
|
|
||||||
|
|
||||||
- name: Get linked dependency versions
|
- name: Get linked dependency versions
|
||||||
id: dep-versions
|
id: dep-versions
|
||||||
@@ -76,15 +73,15 @@ jobs:
|
|||||||
if: steps.cache-linked-deps.outputs.cache-hit != 'true'
|
if: steps.cache-linked-deps.outputs.cache-hit != 'true'
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
pnpm --dir libs/stream-monaco install --frozen-lockfile
|
bun --cwd libs/stream-monaco install --frozen-lockfile
|
||||||
pnpm --dir libs/stream-monaco build
|
bun --cwd libs/stream-monaco run build
|
||||||
|
|
||||||
pnpm --dir libs/markstream-vue install --frozen-lockfile
|
bun --cwd libs/markstream-vue install --frozen-lockfile
|
||||||
pnpm --dir libs/markstream-vue run build:parser
|
bun --cwd libs/markstream-vue run build:parser
|
||||||
pnpm --dir libs/markstream-vue/packages/markstream-react build
|
bun --cwd libs/markstream-vue/packages/markstream-react run build
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build with Tauri
|
- name: Build with Tauri
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
@@ -98,7 +95,7 @@ jobs:
|
|||||||
releaseName: 'BoosApi Test ${{ github.ref_name }}'
|
releaseName: 'BoosApi Test ${{ github.ref_name }}'
|
||||||
releaseDraft: true
|
releaseDraft: true
|
||||||
prerelease: true
|
prerelease: true
|
||||||
tauriScript: pnpm tauri
|
tauriScript: bun run tauri
|
||||||
args: --target x86_64-pc-windows-msvc
|
args: --target x86_64-pc-windows-msvc
|
||||||
|
|
||||||
- name: Package Windows portable (zip)
|
- name: Package Windows portable (zip)
|
||||||
|
|||||||
@@ -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.
|
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.
|
||||||
|
|
||||||
[](https://github.com/Boos4721/boos-chat/releases)
|
[](https://github.com/Boos4721/BoosAPI/releases)
|
||||||
[](#platform-support)
|
[](#platform-support)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -68,7 +68,7 @@ BoosAPI Desktop is a desktop AI workspace that runs on your own machine. It ship
|
|||||||
|
|
||||||
## Quick start
|
## 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
|
## Build from source
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
面向 Boos 生态的本地优先 AI 桌面客户端 —— 多服务商对话、AI Agent、知识库、MCP 与内置 API 网关,集于一身。
|
面向 Boos 生态的本地优先 AI 桌面客户端 —— 多服务商对话、AI Agent、知识库、MCP 与内置 API 网关,集于一身。
|
||||||
|
|
||||||
[](https://github.com/Boos4721/boos-chat/releases)
|
[](https://github.com/Boos4721/BoosAPI/releases)
|
||||||
[](#平台支持)
|
[](#平台支持)
|
||||||
|
|
||||||
</div>
|
</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 已预置为默认服务商 —— 在设置中填入你的密钥即可开始对话。
|
||||||
|
|
||||||
## 从源码构建
|
## 从源码构建
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,38 @@
|
|||||||
"source": "vercel-labs/agent-skills",
|
"source": "vercel-labs/agent-skills",
|
||||||
"sourceType": "github",
|
"sourceType": "github",
|
||||||
"computedHash": "a6a44d5498f7e8f68289902f3dedfc6f38ae0cee1e96527c80724cf27f727c2a"
|
"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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1184,6 +1184,7 @@ dependencies = [
|
|||||||
"sea-orm",
|
"sea-orm",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"serde_yaml",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-autostart",
|
"tauri-plugin-autostart",
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ tauri-plugin-process = "2"
|
|||||||
tauri-plugin-updater = "2"
|
tauri-plugin-updater = "2"
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
serde_yaml = "0.9"
|
||||||
boosapi-core = { path = "crates/core" }
|
boosapi-core = { path = "crates/core" }
|
||||||
boosapi-providers = { path = "crates/providers" }
|
boosapi-providers = { path = "crates/providers" }
|
||||||
boosapi-agent = { path = "crates/agent" }
|
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.
|
||||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 975 B |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 260 KiB After Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 17 KiB |
@@ -4,6 +4,136 @@ use boosapi_core::types::*;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use tauri::State;
|
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 {
|
fn home_dir() -> PathBuf {
|
||||||
dirs::home_dir().expect("Could not determine home directory")
|
dirs::home_dir().expect("Could not determine home directory")
|
||||||
}
|
}
|
||||||
@@ -12,8 +142,114 @@ fn skills_dir() -> PathBuf {
|
|||||||
boosapi_home().join("skills")
|
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]
|
#[tauri::command]
|
||||||
pub async fn list_skills(state: State<'_, AppState>) -> Result<Vec<SkillInfo>, String> {
|
pub async fn list_skills(state: State<'_, AppState>) -> Result<Vec<SkillInfo>, String> {
|
||||||
|
seed_builtin_skills()?;
|
||||||
|
|
||||||
let home = home_dir();
|
let home = home_dir();
|
||||||
let skills = open_agent_sdk::skills::load_all_global(&home);
|
let skills = open_agent_sdk::skills::load_all_global(&home);
|
||||||
let disabled = boosapi_core::repo::skill::get_disabled_skills(&state.sea_db)
|
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)?;
|
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!({
|
let manifest = serde_json::json!({
|
||||||
"source_kind": "github",
|
"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())?;
|
.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> {
|
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>,
|
source: Option<String>,
|
||||||
) -> Result<Vec<MarketplaceSkill>, String> {
|
) -> Result<Vec<MarketplaceSkill>, String> {
|
||||||
let installed_refs = installed_source_refs();
|
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") {
|
match source.as_deref().unwrap_or("skills.sh") {
|
||||||
"github" => {
|
"github" => {
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"https://api.github.com/search/repositories?q={}+topic:agent-skill&sort=stars&per_page=20",
|
"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();
|
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 body: serde_json::Value = response.json().await.map_err(|e| e.to_string())?;
|
||||||
let items = body["items"].as_array().cloned().unwrap_or_default();
|
let items = body["items"].as_array().cloned().unwrap_or_default();
|
||||||
|
|
||||||
let results: Vec<MarketplaceSkill> = items
|
let mut results: Vec<MarketplaceSkill> = items
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|item| {
|
.map(|item| {
|
||||||
let skill_name = item["name"].as_str().unwrap_or("").to_string();
|
let skill_name = item["name"].as_str().unwrap_or("").to_string();
|
||||||
@@ -449,12 +727,17 @@ pub async fn search_marketplace(
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
merge_marketplace_results(&mut results, builtin_results);
|
||||||
Ok(results)
|
Ok(results)
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
if query.chars().count() < 2 {
|
||||||
|
return Ok(builtin_results);
|
||||||
|
}
|
||||||
|
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"https://skills.sh/api/search?q={}",
|
"https://skills.sh/api/search?q={}",
|
||||||
urlencoding::encode(&query)
|
urlencoding::encode(query)
|
||||||
);
|
);
|
||||||
|
|
||||||
let client = reqwest::Client::new();
|
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 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()
|
.into_iter()
|
||||||
.map(|item| {
|
.map(|item| {
|
||||||
let skill_name = item["name"].as_str().unwrap_or("").to_string();
|
let skill_name = item
|
||||||
let repo = item["source"].as_str().unwrap_or("").to_string();
|
.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 =
|
let installed =
|
||||||
installed_refs.contains(&repo.trim().trim_end_matches('/').to_lowercase());
|
installed_refs.contains(&repo.trim().trim_end_matches('/').to_lowercase());
|
||||||
MarketplaceSkill {
|
MarketplaceSkill {
|
||||||
name: skill_name,
|
name: skill_name,
|
||||||
description: String::new(),
|
description: item["description"].as_str().unwrap_or("").to_string(),
|
||||||
repo,
|
repo,
|
||||||
stars: 0,
|
stars: 0,
|
||||||
installs: item["installs"].as_i64().unwrap_or(0),
|
installs: item["installs"].as_i64().unwrap_or(0),
|
||||||
installed,
|
installed,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.filter(|item| !item.repo.trim().is_empty())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
merge_marketplace_results(&mut results, builtin_results);
|
||||||
Ok(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]
|
#[tauri::command]
|
||||||
pub async fn check_skill_updates() -> Result<Vec<SkillUpdateInfo>, String> {
|
pub async fn check_skill_updates() -> Result<Vec<SkillUpdateInfo>, String> {
|
||||||
let skills_path = skills_dir();
|
let skills_path = skills_dir();
|
||||||
|
|||||||
@@ -520,6 +520,8 @@ pub fn run() {
|
|||||||
version = %app.package_info().version,
|
version = %app.package_info().version,
|
||||||
"BoosApi setup started"
|
"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
|
// Ensure ~/Documents/boosapi/{images,files,backups}/ exist
|
||||||
boosapi_core::storage_paths::ensure_documents_dirs()
|
boosapi_core::storage_paths::ensure_documents_dirs()
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
"updater": {
|
"updater": {
|
||||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDU2QzNGRjRBQkQ4QzlENDQKUldSRW5ZeTlTdi9EVm1CRnUyNWF4d3oxeS9qWmlIQm12VmIvWmdDSEZiR21rMm9hNWRVU2VUTU4K",
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDU2QzNGRjRBQkQ4QzlENDQKUldSRW5ZeTlTdi9EVm1CRnUyNWF4d3oxeS9qWmlIQm12VmIvWmdDSEZiR21rMm9hNWRVU2VUTU4K",
|
||||||
"endpoints": [
|
"endpoints": [
|
||||||
"https://github.com/Boos4721/boos-chat/releases/latest/download/latest.json"
|
"https://github.com/Boos4721/BoosAPI/releases/latest/download/latest.json"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 174 KiB |
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,16 +2,13 @@ import React, { useMemo, useCallback, useRef, useState, useEffect, useSyncExtern
|
|||||||
import { CloseCircleFilled, SyncOutlined } from '@ant-design/icons';
|
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 { Typography, Button, Dropdown, Input, App, Avatar, Alert, Popconfirm, Popover, theme, Tag, Image, Tooltip, Modal, Spin } from 'antd';
|
||||||
import type { InputRef } 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 { ModelIcon } from '@lobehub/icons';
|
||||||
import { getConvIcon } from '@/lib/convIcon';
|
import { getConvIcon } from '@/lib/convIcon';
|
||||||
import { normalizeAutoConversationTitle } from '@/lib/conversationTitle';
|
|
||||||
import Bubble from '@ant-design/x/es/bubble';
|
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 Actions from '@ant-design/x/es/actions';
|
||||||
import Think from '@ant-design/x/es/think';
|
import Think from '@ant-design/x/es/think';
|
||||||
import type { BubbleItemType, BubbleListRef, RoleType } from '@ant-design/x/es/bubble/interface';
|
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 NodeRenderer, { setCustomComponents, withMarkstreamComponentDisplay, type NodeComponentProps, type CodeBlockActionContext, type CodeBlockPreviewPayload, type MermaidBlockActionContext, type InfographicBlockActionContext } from 'markstream-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { CodeBlockHeaderActions } from './CodeBlockHeaderActions';
|
import { CodeBlockHeaderActions } from './CodeBlockHeaderActions';
|
||||||
@@ -2111,7 +2108,6 @@ export function ChatView() {
|
|||||||
const [mermaidPreviewSvg, setMermaidPreviewSvg] = useState<string | null>(null);
|
const [mermaidPreviewSvg, setMermaidPreviewSvg] = useState<string | null>(null);
|
||||||
const [mermaidPreviewOpen, setMermaidPreviewOpen] = useState(false);
|
const [mermaidPreviewOpen, setMermaidPreviewOpen] = useState(false);
|
||||||
const [streamStatusNow, setStreamStatusNow] = useState(() => Date.now());
|
const [streamStatusNow, setStreamStatusNow] = useState(() => Date.now());
|
||||||
const createConversation = useConversationStore((s) => s.createConversation);
|
|
||||||
const providers = useProviderStore((s) => s.providers);
|
const providers = useProviderStore((s) => s.providers);
|
||||||
const settings = useSettingsStore((s) => s.settings);
|
const settings = useSettingsStore((s) => s.settings);
|
||||||
const saveSettings = useSettingsStore((s) => s.saveSettings);
|
const saveSettings = useSettingsStore((s) => s.saveSettings);
|
||||||
@@ -2780,62 +2776,6 @@ export function ChatView() {
|
|||||||
[messages, activeConversation, t, messageApi],
|
[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) ────────────────────────
|
// ── Bubble items (only show active messages) ────────────────────────
|
||||||
const activeMessages = useMemo(
|
const activeMessages = useMemo(
|
||||||
() => messages.filter((msg) => msg.is_active !== false),
|
() => messages.filter((msg) => msg.is_active !== false),
|
||||||
@@ -4200,16 +4140,16 @@ export function ChatView() {
|
|||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center h-full" style={{ padding: '0 24px' }}>
|
<div
|
||||||
<Typography.Title level={3} style={{ marginBottom: 24, fontWeight: 500 }}>
|
className="flex flex-col items-center justify-center h-full"
|
||||||
{greetingText}
|
style={{ gap: 8, padding: '0 24px', color: token.colorTextSecondary }}
|
||||||
|
>
|
||||||
|
<Typography.Title level={3} style={{ margin: 0, color: token.colorText }}>
|
||||||
|
{t('chat.welcome')}
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
<Prompts
|
<Typography.Text type="secondary">
|
||||||
items={promptItems}
|
{t('chat.emptyPrompt', '输入消息开始对话,或在下方切换 Agent / 创作模式。')}
|
||||||
onItemClick={handlePromptClick}
|
</Typography.Text>
|
||||||
wrap
|
|
||||||
style={{ marginTop: 16 }}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React, { useState, useRef, useCallback, useEffect, useMemo } from 'react';
|
import React, { useState, useRef, useCallback, useEffect, useMemo } from 'react';
|
||||||
import { Button, Tooltip, App, theme, Dropdown, Tag, Popover, Checkbox, Badge, Popconfirm } from 'antd';
|
import { Button, Tooltip, App, theme, Dropdown, Tag, Popover, Checkbox, Badge, Popconfirm } from 'antd';
|
||||||
import type { MenuProps } 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 { 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 { useUIStore } from '@/stores/uiStore';
|
||||||
import { findModelByIds, supportsReasoning, modelHasCapability } from '@/lib/modelCapabilities';
|
import { findModelByIds, supportsReasoning, modelHasCapability } from '@/lib/modelCapabilities';
|
||||||
import {
|
import {
|
||||||
@@ -41,6 +41,22 @@ const DOCUMENT_ATTACHMENT_MIME_BY_EXTENSION: Record<string, string> = {
|
|||||||
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
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 {
|
function getAttachmentMimeType(fileName: string, mimeType?: string): string {
|
||||||
if (mimeType) return mimeType;
|
if (mimeType) return mimeType;
|
||||||
const extension = fileName.split('.').pop()?.toLowerCase() || '';
|
const extension = fileName.split('.').pop()?.toLowerCase() || '';
|
||||||
@@ -50,11 +66,15 @@ function getAttachmentMimeType(fileName: string, mimeType?: string): string {
|
|||||||
function isAllowedAttachmentFile(
|
function isAllowedAttachmentFile(
|
||||||
fileName: string,
|
fileName: string,
|
||||||
mimeType: string | undefined,
|
mimeType: string | undefined,
|
||||||
hasVision: boolean,
|
_hasVision: boolean,
|
||||||
documentAttachmentReadingEnabled: boolean,
|
documentAttachmentReadingEnabled: boolean,
|
||||||
): boolean {
|
): boolean {
|
||||||
const effectiveMimeType = getAttachmentMimeType(fileName, mimeType);
|
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;
|
if (!documentAttachmentReadingEnabled) return false;
|
||||||
return Object.values(DOCUMENT_ATTACHMENT_MIME_BY_EXTENSION).includes(effectiveMimeType);
|
return Object.values(DOCUMENT_ATTACHMENT_MIME_BY_EXTENSION).includes(effectiveMimeType);
|
||||||
}
|
}
|
||||||
@@ -90,6 +110,9 @@ export function InputArea() {
|
|||||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||||
const [mcpPopoverOpen, setMcpPopoverOpen] = useState(false);
|
const [mcpPopoverOpen, setMcpPopoverOpen] = useState(false);
|
||||||
const [searchDropdownOpen, setSearchDropdownOpen] = 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 fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const valueRef = useRef(value);
|
const valueRef = useRef(value);
|
||||||
@@ -143,6 +166,10 @@ export function InputArea() {
|
|||||||
const conversations = useConversationStore((s) => s.conversations);
|
const conversations = useConversationStore((s) => s.conversations);
|
||||||
const providers = useProviderStore((s) => s.providers);
|
const providers = useProviderStore((s) => s.providers);
|
||||||
const settings = useSettingsStore((s) => s.settings);
|
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) => {
|
const shortcutHint = useCallback((label: string, action: ShortcutAction) => {
|
||||||
if (!settings) return label;
|
if (!settings) return label;
|
||||||
@@ -226,6 +253,10 @@ export function InputArea() {
|
|||||||
|
|
||||||
const activeConversation = conversations.find((c) => c.id === activeConversationId);
|
const activeConversation = conversations.find((c) => c.id === activeConversationId);
|
||||||
const currentMode = activeConversation?.mode || 'chat';
|
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 setActivePage = useUIStore((s) => s.setActivePage);
|
||||||
const setSettingsSection = useUIStore((s) => s.setSettingsSection);
|
const setSettingsSection = useUIStore((s) => s.setSettingsSection);
|
||||||
@@ -761,11 +792,63 @@ export function InputArea() {
|
|||||||
hasVision: modelHasCapability(currentModel, 'Vision'),
|
hasVision: modelHasCapability(currentModel, 'Vision'),
|
||||||
}), [activeConversation, currentModel, providers]);
|
}), [activeConversation, currentModel, providers]);
|
||||||
const documentAttachmentReadingEnabled = settings.document_attachment_reading_enabled ?? false;
|
const documentAttachmentReadingEnabled = settings.document_attachment_reading_enabled ?? false;
|
||||||
const canAttachFiles = hasVision || documentAttachmentReadingEnabled;
|
const fileInputAccept = documentAttachmentReadingEnabled ? ANY_ATTACHMENT_ACCEPT : MEDIA_ATTACHMENT_ACCEPT;
|
||||||
const fileInputAccept = [
|
|
||||||
hasVision ? 'image/*' : null,
|
const creationModeItems = useMemo<MenuProps['items']>(() => [
|
||||||
documentAttachmentReadingEnabled ? DOCUMENT_ATTACHMENT_ACCEPT : null,
|
{
|
||||||
].filter(Boolean).join(',');
|
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)
|
// 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') => {
|
const handleModeSwitch = useCallback(async (mode: 'chat' | 'agent') => {
|
||||||
|
setPendingMode(mode);
|
||||||
if (!activeConversation) return;
|
if (!activeConversation) return;
|
||||||
|
|
||||||
await updateConversation(activeConversation.id, { mode });
|
await updateConversation(activeConversation.id, { mode });
|
||||||
@@ -853,6 +937,50 @@ export function InputArea() {
|
|||||||
}
|
}
|
||||||
}, [activeConversation, updateConversation, companionModels, companionStorageKey]);
|
}, [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 handleSend = useCallback(async () => {
|
||||||
const trimmed = value.trim();
|
const trimmed = value.trim();
|
||||||
if (!trimmed) return;
|
if (!trimmed) return;
|
||||||
@@ -894,12 +1022,30 @@ export function InputArea() {
|
|||||||
textareaRef.current.style.height = 'auto';
|
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);
|
await sendAgentMessage(trimmed, attachments);
|
||||||
} else if (companionModels.length > 0) {
|
} 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 {
|
} 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) {
|
} catch (e) {
|
||||||
setValue((current) => current || trimmed);
|
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(() => {
|
const handleFillLastMessage = useCallback(() => {
|
||||||
if (streaming) return;
|
if (streaming) return;
|
||||||
@@ -969,7 +1115,6 @@ export function InputArea() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handlePaste = useCallback((e: React.ClipboardEvent<HTMLTextAreaElement>) => {
|
const handlePaste = useCallback((e: React.ClipboardEvent<HTMLTextAreaElement>) => {
|
||||||
if (!canAttachFiles) return;
|
|
||||||
const items = e.clipboardData?.items;
|
const items = e.clipboardData?.items;
|
||||||
if (!items) return;
|
if (!items) return;
|
||||||
const files: File[] = [];
|
const files: File[] = [];
|
||||||
@@ -993,14 +1138,12 @@ export function InputArea() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setAttachedFiles((prev) => [...prev, ...files]);
|
setAttachedFiles((prev) => [...prev, ...files]);
|
||||||
}
|
}
|
||||||
}, [canAttachFiles, documentAttachmentReadingEnabled, hasVision]);
|
}, [documentAttachmentReadingEnabled, hasVision]);
|
||||||
|
|
||||||
// Drag-and-drop overlay (Tauri native)
|
// Drag-and-drop overlay (Tauri native)
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!canAttachFiles) return;
|
|
||||||
|
|
||||||
let unlisten: (() => void) | undefined;
|
let unlisten: (() => void) | undefined;
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
@@ -1025,6 +1168,8 @@ export function InputArea() {
|
|||||||
png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg',
|
png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg',
|
||||||
gif: 'image/gif', webp: 'image/webp', svg: 'image/svg+xml',
|
gif: 'image/gif', webp: 'image/webp', svg: 'image/svg+xml',
|
||||||
bmp: 'image/bmp', ico: 'image/x-icon',
|
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',
|
pdf: 'application/pdf',
|
||||||
doc: 'application/msword',
|
doc: 'application/msword',
|
||||||
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
@@ -1061,7 +1206,7 @@ export function InputArea() {
|
|||||||
return () => {
|
return () => {
|
||||||
unlisten?.();
|
unlisten?.();
|
||||||
};
|
};
|
||||||
}, [canAttachFiles, documentAttachmentReadingEnabled, hasVision]);
|
}, [documentAttachmentReadingEnabled, hasVision]);
|
||||||
|
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
@@ -1404,7 +1549,6 @@ export function InputArea() {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
)}
|
)}
|
||||||
{canAttachFiles && (
|
|
||||||
<Tooltip title={t('chat.attachFile')}>
|
<Tooltip title={t('chat.attachFile')}>
|
||||||
<Button
|
<Button
|
||||||
aria-label={t('chat.attachFile')}
|
aria-label={t('chat.attachFile')}
|
||||||
@@ -1414,7 +1558,6 @@ export function InputArea() {
|
|||||||
onClick={handleFileSelect}
|
onClick={handleFileSelect}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
|
||||||
<Popover
|
<Popover
|
||||||
trigger="click"
|
trigger="click"
|
||||||
placement="topLeft"
|
placement="topLeft"
|
||||||
@@ -1599,30 +1742,78 @@ export function InputArea() {
|
|||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Dropdown
|
<Dropdown
|
||||||
menu={{
|
menu={{
|
||||||
items: [
|
items: creationModeItems,
|
||||||
{
|
selectedKeys: [selectedComposerMode],
|
||||||
key: 'chat',
|
onClick: ({ key }) => void handleCreationModeSelect(key as ComposerCreationMode),
|
||||||
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'),
|
|
||||||
}}
|
}}
|
||||||
trigger={['click']}
|
trigger={['click']}
|
||||||
|
placement="topLeft"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
size="small"
|
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 }}
|
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>
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
{currentMode === 'agent' && (
|
{currentMode === 'agent' && (
|
||||||
|
|||||||
@@ -303,7 +303,8 @@ export function TitleBar() {
|
|||||||
}
|
}
|
||||||
}, [message, t]);
|
}, [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'] = [
|
const githubMenuItems: MenuProps['items'] = [
|
||||||
{
|
{
|
||||||
key: 'feature',
|
key: 'feature',
|
||||||
@@ -323,7 +324,7 @@ export function TitleBar() {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
const handleGithubClick: MenuProps['onClick'] = ({ key }) => {
|
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`;
|
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`;
|
else if (key === 'bug') url = `${GITHUB_REPO}/issues/new?labels=bug&template=bug_report.yml`;
|
||||||
if (isTauri()) {
|
if (isTauri()) {
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export function AboutPage() {
|
|||||||
<span>{t('settings.website')}</span>
|
<span>{t('settings.website')}</span>
|
||||||
<Button
|
<Button
|
||||||
icon={<Globe size={16} />}
|
icon={<Globe size={16} />}
|
||||||
href="https://boos.lat"
|
href="https://tybossai.com/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
type="link"
|
type="link"
|
||||||
>
|
>
|
||||||
@@ -98,7 +98,7 @@ export function AboutPage() {
|
|||||||
<span>GitHub</span>
|
<span>GitHub</span>
|
||||||
<Button
|
<Button
|
||||||
icon={<Github size={16} />}
|
icon={<Github size={16} />}
|
||||||
href="https://github.com/Boos4721/boos-chat"
|
href="https://github.com/Boos4721/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
type="link"
|
type="link"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"chat": "المحادثة",
|
"chat": "المحادثة",
|
||||||
|
"agentWorkbench": "Agent",
|
||||||
"knowledge": "قاعدة المعرفة",
|
"knowledge": "قاعدة المعرفة",
|
||||||
"memory": "الذاكرة",
|
"memory": "الذاكرة",
|
||||||
"gateway": "البوابة",
|
"gateway": "البوابة",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"chat": "Chat",
|
"chat": "Chat",
|
||||||
|
"agentWorkbench": "Agent",
|
||||||
"knowledge": "Wissen",
|
"knowledge": "Wissen",
|
||||||
"memory": "Gedächtnis",
|
"memory": "Gedächtnis",
|
||||||
"gateway": "Gateway",
|
"gateway": "Gateway",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"chat": "Chat",
|
"chat": "Chat",
|
||||||
|
"agentWorkbench": "Agent",
|
||||||
"knowledge": "Knowledge",
|
"knowledge": "Knowledge",
|
||||||
"memory": "Memory",
|
"memory": "Memory",
|
||||||
"gateway": "Gateway",
|
"gateway": "Gateway",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"chat": "Chat",
|
"chat": "Chat",
|
||||||
|
"agentWorkbench": "Agent",
|
||||||
"knowledge": "Base de conocimientos",
|
"knowledge": "Base de conocimientos",
|
||||||
"memory": "Memoria",
|
"memory": "Memoria",
|
||||||
"gateway": "Pasarela",
|
"gateway": "Pasarela",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"chat": "Chat",
|
"chat": "Chat",
|
||||||
|
"agentWorkbench": "Agent",
|
||||||
"knowledge": "Connaissances",
|
"knowledge": "Connaissances",
|
||||||
"memory": "Mémoire",
|
"memory": "Mémoire",
|
||||||
"gateway": "Passerelle",
|
"gateway": "Passerelle",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"chat": "चैट",
|
"chat": "चैट",
|
||||||
|
"agentWorkbench": "Agent",
|
||||||
"knowledge": "ज्ञान आधार",
|
"knowledge": "ज्ञान आधार",
|
||||||
"memory": "स्मृति",
|
"memory": "स्मृति",
|
||||||
"gateway": "गेटवे",
|
"gateway": "गेटवे",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"chat": "チャット",
|
"chat": "チャット",
|
||||||
|
"agentWorkbench": "Agent",
|
||||||
"knowledge": "ナレッジ",
|
"knowledge": "ナレッジ",
|
||||||
"memory": "メモリ",
|
"memory": "メモリ",
|
||||||
"gateway": "ゲートウェイ",
|
"gateway": "ゲートウェイ",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"chat": "채팅",
|
"chat": "채팅",
|
||||||
|
"agentWorkbench": "Agent",
|
||||||
"knowledge": "지식",
|
"knowledge": "지식",
|
||||||
"memory": "메모리",
|
"memory": "메모리",
|
||||||
"gateway": "게이트웨이",
|
"gateway": "게이트웨이",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"chat": "Чат",
|
"chat": "Чат",
|
||||||
|
"agentWorkbench": "Agent",
|
||||||
"knowledge": "База знаний",
|
"knowledge": "База знаний",
|
||||||
"memory": "Память",
|
"memory": "Память",
|
||||||
"gateway": "Шлюз",
|
"gateway": "Шлюз",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"chat": "对话",
|
"chat": "对话",
|
||||||
|
"agentWorkbench": "Agent",
|
||||||
"knowledge": "知识库",
|
"knowledge": "知识库",
|
||||||
"memory": "记忆",
|
"memory": "记忆",
|
||||||
"gateway": "网关",
|
"gateway": "网关",
|
||||||
|
|||||||