mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-18 05:18:31 +00:00
Compare commits
66 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 248a4dcab5 | |||
| 6532cd1ee0 | |||
| 72ea0f94f7 | |||
| a3a08c2395 | |||
| 643ad16a5d | |||
| 5761d20637 | |||
| fd3c6cf8fc | |||
| d81e5e703e | |||
| 2a4b6e4974 | |||
| 2fb0970cf9 | |||
| 7a93df9e44 | |||
| d9673c3c41 | |||
| dd2e32cf6f | |||
| a5ab99f055 | |||
| 41bccc4aa8 | |||
| 1ce4e026a7 | |||
| 89c55bf658 | |||
| 2eb9e34fda | |||
| 13ce3c52ec | |||
| f9eb48feea | |||
| 8dee729f9f | |||
| 359b348989 | |||
| 0c3450de7c | |||
| cbc259094d | |||
| ccf33e8b98 | |||
| d1a6ffaf30 | |||
| 66c9339e98 | |||
| 857aaf4766 | |||
| 4e91a3181d | |||
| c9ca46e1e0 | |||
| 37db828c17 | |||
| 0208c0adfe | |||
| 09a57d4618 | |||
| 73dd0ef136 | |||
| d2e4833f1e | |||
| 5119c0802d | |||
| 3e51b87b1e | |||
| 1e8b5959da | |||
| 5b25b8d8bb | |||
| fd82f6fd0e | |||
| 80c11a09e2 | |||
| c8096590c4 | |||
| dda527926d | |||
| 5f0fa7bf50 | |||
| c50d790feb | |||
| 4d030e9db1 | |||
| 56ce192c61 | |||
| 21a73b22b2 | |||
| 818e67d1f0 | |||
| e14f2e96f6 | |||
| cf5ec7b96a | |||
| c3f91f10ac | |||
| 650a178709 | |||
| e1d6b30127 | |||
| 7d1086b096 | |||
| 58c671b7ac | |||
| e0ead38c20 | |||
| f71be63bea | |||
| 4d840e9071 | |||
| d382df1b2c | |||
| d75e193ea0 | |||
| 7989952d2e | |||
| 480f6a8e7b | |||
| 45a6f2b440 | |||
| 4bc77fc103 | |||
| 049c81d53b |
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: agent-runtime-hooks
|
||||
description: "Agent runtime lifecycle hooks for observing and intercepting agent execution. Use when adding hooks to agent operations, mocking tool calls, logging step events, handling human intervention, sub-agent calls, context compression, or building eval/tracing integrations. Triggers on 'hooks', 'beforeToolCall', 'afterToolCall', 'beforeStep', 'afterStep', 'onComplete', 'onError', 'tool mock', 'agent lifecycle', 'human intervention', 'callAgent', 'compact'."
|
||||
description: 'Agent runtime lifecycle hooks. Use for before/after tool or step hooks, tool mocks, human intervention, sub-agent calls, context compression, evals, tracing, callAgent, or lifecycle events.'
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: agent-signal
|
||||
description: Build or extend LobeHub Agent Signal pipelines for background or quiet agent work driven by event sources, semantic signals, and action handlers. Use when adding a new Agent Signal source, signal or action type, policy, middleware handler, workflow handoff, dedupe or scope behavior, or observability around `src/server/services/agentSignal/**`, `packages/agent-signal`, or `packages/observability-otel/src/modules/agent-signal`.
|
||||
description: 'Build or extend LobeHub Agent Signal pipelines. Use for signal sources, signal/action types, policies, middleware, workflow handoff, dedupe, scope behavior, or observability.'
|
||||
---
|
||||
|
||||
# Agent Signal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: agent-tracing
|
||||
description: "Agent tracing CLI for inspecting agent execution snapshots. Use when user mentions 'agent-tracing', 'trace', 'snapshot', wants to debug agent execution, inspect LLM calls, view context engine data, or analyze agent steps. Triggers on agent debugging, trace inspection, or execution analysis tasks."
|
||||
description: 'Agent tracing CLI for execution snapshots. Use for agent-tracing, traces, snapshots, LLM call inspection, context engine data, agent step analysis, or execution debugging.'
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: builtin-tool
|
||||
description: Build a new builtin tool package under `packages/builtin-tool-<name>/`. Use when adding a new agent-callable toolset, designing its API surface (manifest / ApiName / Params / State), implementing the Executor + ExecutionRuntime, building the Inspector / Render / Placeholder / Streaming / Intervention / Portal UI, or wiring a tool into the central registries (`packages/builtin-tools/src/{index,identifiers,inspectors,renders,placeholders,streamings,interventions,portals}.ts` and `src/store/tool/slices/builtin/executors/index.ts`). Triggers on "new builtin tool", "add a tool", "tool inspector", "tool render", "tool placeholder", "tool streaming", "tool intervention", "BuiltinToolManifest", "BaseExecutor", "ExecutionRuntime".
|
||||
description: 'Build LobeHub builtin tool packages. Use when adding agent-callable tools, manifests, executors, runtimes, inspectors, renders, placeholders, streaming, interventions, portals, or tool registries.'
|
||||
---
|
||||
|
||||
# Builtin Tool Authoring Guide
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: chat-sdk
|
||||
description: "Build multi-platform chat bots with the Chat SDK (`chat` npm package) — Slack, Teams, Google Chat, Discord, GitHub, Linear. Use when building a chat bot, handling mentions / messages / reactions / slash commands / cards / modals / streaming, setting up a webhook handler, or sending interactive cards / streaming AI responses to a chat platform. Triggers on `@chat-adapter`, 'chat sdk', 'chat bot', 'slack bot', 'teams bot', 'discord bot', 'webhook handler', 'cross-platform bot'."
|
||||
description: 'Build multi-platform chat bots with the chat SDK. Use for Slack, Teams, Google Chat, Discord, GitHub, Linear bots, webhooks, mentions, slash commands, cards, modals, or streaming responses.'
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: data-fetching-architecture
|
||||
description: Standardized data-fetching pipeline guide — Service layer + Zustand Store + SWR. Use when implementing a data-fetching feature, creating a `xxxService`, adding a `useFetchXxx` hook, wiring `useClientDataSWR`, or migrating ad-hoc `useEffect + fetch` to the standard pipeline. Triggers on `lambdaClient`, `useClientDataSWR`, `xxxService`, `useFetchXxx`, 'data fetching', 'fetch architecture', 'service layer', 'SWR hook', 'migrate useEffect'.
|
||||
description: 'LobeHub data-fetching pipeline guide. Use for service layer, Zustand store, SWR, lambdaClient, useClientDataSWR, useFetchXxx hooks, or migrating useEffect fetches.'
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: db-migrations
|
||||
description: 'Use when generating or regenerating Drizzle migration files, changing database schema tables or columns, resolving migration sequence conflicts after rebase, reviewing migration SQL for idempotent patterns, or renaming migration files.'
|
||||
description: 'Use for Drizzle migrations: schema/table/column changes, migration generation or regeneration, sequence conflicts after rebase, idempotent SQL review, or migration renames.'
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: debug-package
|
||||
description: "Guide for the `debug` npm package and LobeHub log namespaces (lobe-server:*, lobe-desktop:*, lobe-client:*, lobe-*-router:*). Use whenever adding a `debug(...)` logger, picking a namespace for new server/desktop/client/router code, troubleshooting why DEBUG=lobe-* logs don't show up, or when the user asks to 'add logging', 'add a logger', 'instrument this', 'trace this call', 'why isn't my log printing', or mentions `debug(`, `DEBUG=`, `localStorage.debug`, or log format specifiers like %O / %o / %s / %d in a LobeHub codebase."
|
||||
description: 'LobeHub debug package and log namespace guide. Use when adding debug() logging, choosing lobe-* namespaces, troubleshooting DEBUG output, localStorage.debug, or log format specifiers.'
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: docs-changelog
|
||||
description: "Writing guide for website changelog pages under `docs/changelog/*.mdx` (NOT GitHub Release notes — those live in the `version-release` skill). Use when creating or editing a product update post in EN/ZH. Triggers on `docs/changelog/*.mdx`, 'changelog post', 'product update post', 'add a changelog', '更新日志', 'changelog 文案'."
|
||||
description: 'Write website changelog pages under docs/changelog/*.mdx. Use for EN/ZH product update posts, changelog posts, update-log copy, or docs changelog edits; not GitHub Release notes.'
|
||||
---
|
||||
|
||||
# Docs Changelog Writing Guide
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: drizzle
|
||||
description: "Drizzle ORM schema authoring and query style for LobeHub (postgres, strict mode). Use when editing anything under `src/database/schemas/`, defining `pgTable` columns/indexes/junction tables, spreading `...timestamps`, generating `createInsertSchema`/`$inferSelect`/`$inferInsert` types, writing `db.select().from(...).leftJoin(...)` queries, or deciding when to split a relational `with:` into two queries. Triggers on `pgTable`, `db.select`, `db.query`, `eq()`/`and()`/`inArray()`, `uniqueIndex`, `primaryKey`, `references({ onDelete })`, 'add a column', 'new table', 'foreign key', 'junction table', 'schema field'. For migration files specifically, see the `db-migrations` skill."
|
||||
description: 'LobeHub Drizzle ORM schema and query style. Use for pgTable schemas, indexes, joins, inferred types, db.select/db.query, schema fields, foreign keys, junction tables, or postgres query patterns.'
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: heterogeneous-agent
|
||||
description: Guide for implementing and debugging LobeHub heterogeneous agent integrations such as Claude Code, Codex, and future external CLI agents. Use when working on adapter event mapping, Electron IPC transport, renderer persistence, tool-call chaining, subagent threads, resume/session handling, or regressions like mixed multi-tool messages, broken step boundaries, stuck tool loading, and orphan tool messages. Triggers on 'heterogeneous agent', 'hetero agent', '异构 agent', 'claude code adapter', 'codex adapter', 'external agent CLI', '孤立 tool 消息', 'raw Codex trace', or adapter/executor bugs.
|
||||
description: 'Implement or debug LobeHub heterogeneous agents. Use for Claude Code/Codex adapters, external CLI agents, event mapping, IPC, persistence, tool-call chains, sessions, traces, or adapter bugs.'
|
||||
---
|
||||
|
||||
# Heterogeneous Agent Development
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: hotkey
|
||||
description: "Adding or editing keyboard shortcuts in LobeHub. Use when registering a new hotkey, changing a key combo, scoping a shortcut to chat vs global, or wiring a hotkey hook + tooltip. Covers the 5-step flow: add to `HotkeyEnum` in `src/types/hotkey.ts`, register in `HOTKEYS_REGISTRATION` (`src/const/hotkeys.ts`) with `combineKeys([Key.Mod, …])`, add i18n in `src/locales/default/hotkey.ts`, expose via `useHotkeyById` in `src/hooks/useHotkeys/`, and render `<Tooltip hotkey={…}>`. Triggers on `HotkeyEnum`, `HOTKEYS_REGISTRATION`, `useHotkeyById`, `combineKeys`, `Key.Mod`/`Key.Shift`, 'add a hotkey', 'add a shortcut', '加快捷键', '快捷键', 'Cmd+K', 'keyboard shortcut', 'hotkey scope', 'hotkey conflict'."
|
||||
description: 'Add or edit LobeHub keyboard shortcuts. Use for HotkeyEnum, HOTKEYS_REGISTRATION, combineKeys, useHotkeyById, tooltip hotkeys, shortcut scope, conflicts, or Cmd/Ctrl key combos.'
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: i18n
|
||||
description: "LobeHub internationalization with react-i18next. Use when adding any user-facing string in `.tsx`/`.ts` files, creating or renaming a key under `src/locales/default/{namespace}.ts`, deciding the `{feature}.{context}.{action}` flat-key pattern, wiring a new namespace into `src/locales/default/index.ts`, or translating zh-CN/en-US JSON for dev preview. Triggers on `useTranslation`, `t('foo.bar')`, `i18next.t`, `{{variable}}` interpolation, hardcoded UI strings (zh or en) that should be extracted, 'add i18n', '加 i18n key', '翻译', 'locale key', 'namespace', 'pnpm i18n'."
|
||||
description: 'LobeHub i18n with react-i18next. Use for user-facing strings, locale keys, namespaces, useTranslation, t(), interpolation, zh-CN/en-US previews, hardcoded UI copy, or pnpm i18n.'
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: linear
|
||||
description: "Linear issue management. Use when the user mentions LOBE-xxx issue IDs (e.g. LOBE-4540), says 'linear' / 'linear issue' / 'link linear', or when creating PRs that reference Linear issues. Covers retrieving issues, updating status, adding completion comments, and creating sub-issue trees."
|
||||
description: 'Linear issue management. Use for LOBE-xxx issues, Linear links, PRs referencing Linear, retrieving issues, updating status, completion comments, or sub-issue trees.'
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: microcopy
|
||||
description: UI copy and microcopy guidelines. Use when writing UI text, buttons, error messages, empty states, onboarding, or any user-facing copy. Triggers on i18n translation, UI text writing, or copy improvement tasks. Supports both Chinese and English.
|
||||
description: 'UI copy and microcopy guidelines. Use for user-facing copy, buttons, errors, empty states, onboarding, i18n wording, translation, or copy improvements in Chinese or English.'
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: modal
|
||||
description: "LobeHub imperative-modal conventions. Use whenever creating, editing, opening, or migrating a modal/dialog/popup — prefer `createModal` / `confirmModal` / `useModalContext` from `@lobehub/ui/base-ui` (headless) over the legacy root `@lobehub/ui` `createModal` (antd Modal props) and over any declarative `open` state + `<Modal />` pattern. Covers required `ModalHost` mounting, the `Content` + `index.tsx` file layout, `content` vs `children` slot, i18n inside `createModal()` (`import { t } from 'i18next'`), and migration notes. Triggers on `createModal`, `confirmModal`, `useModalContext`, `ModalHost`, `antd Modal`, `<Modal open>`, 'open a modal', 'popup', 'dialog', 'confirm dialog', '弹框', '弹窗', '确认框', 'migrate to base-ui'."
|
||||
description: 'LobeHub imperative modal conventions. Use when creating or migrating modals, dialogs, popups, confirm flows, ModalHost wiring, createModal, confirmModal, useModalContext, or base-ui modal APIs.'
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: project-overview
|
||||
description: "LobeHub open-source monorepo architecture map — flat `apps/` + `packages/@lobechat/*` + `src/` layout, per-layer location table, and `src/business/` stubs that the cloud repo overrides. Use when exploring an unfamiliar part of the codebase, locating where a layer lives (store / service / router / schema / etc.), or onboarding to the monorepo. Triggers on 'where does X live', 'project structure', 'monorepo layout', `src/business/` stub, 'architecture overview', '项目结构', '架构总览'."
|
||||
description: 'LobeHub open-source monorepo architecture map. Use when locating code layers, understanding apps/packages/src layout, business stubs, project structure, or onboarding to the repository.'
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: react
|
||||
description: "LobeHub React component conventions — base-ui (`@lobehub/ui/base-ui`) first for headless primitives (Select, Modal, DropdownMenu, ContextMenu, Popover, ScrollArea, Switch, Toast, FloatingSheet), then `@lobehub/ui` root, antd as last resort; styling via `antd-style` `createStaticStyles` + `cssVar.*` (zero-runtime preferred over `createStyles` + `token`); routing via `react-router-dom` (not `next/link`). Use when writing or editing any `.tsx` under `src/**`. Triggers on `createStaticStyles`, `createStyles`, `cssVar`, `antd-style`, `Flexbox`, `Center`, `Select`, `Modal`, `Drawer`, `Button`, `Tooltip`, `DropdownMenu`, `ContextMenu`, `Popover`, `Switch`, `ScrollArea`, `Toast`, `FloatingSheet`, `Link`, `useNavigate`, `react-router-dom`, `next/link`, `desktopRouter`, `componentMap.desktop`, `.desktop.tsx`, `base-ui`, `@lobehub/ui/base-ui`, 'new component', 'new page', 'edit layout', 'add styles', 'zustand selector', '@lobehub/ui', 'antd import'."
|
||||
description: 'LobeHub React component conventions. Use when editing TSX UI, choosing base-ui vs @lobehub/ui vs antd, styling with antd-style, routing, desktop variants, layouts, or component state.'
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
@@ -53,6 +53,10 @@ For Modal specifically, see the dedicated **modal** skill — use the imperative
|
||||
| Layout | Center, DraggablePanel, Flexbox, Grid, Header, MaskShadow |
|
||||
| Navigation | Burger, Menu, SideNav, Tabs |
|
||||
|
||||
## State
|
||||
|
||||
When a feature component manages more than 3 pieces of state (`useState`/`useReducer`/derived state), extract the logic into a custom hook (e.g. `useXxx`). Keep the component focused on rendering — the hook holds state and handlers, so logic can be unit-tested without rendering the component.
|
||||
|
||||
## Layout
|
||||
|
||||
Use `Flexbox` and `Center` from `@lobehub/ui`. See `references/layout-kit.md` for full props and examples.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: response-compliance
|
||||
description: OpenResponses API compliance testing. Use when testing the Response API endpoint, running compliance tests, or debugging Response API schema issues. Triggers on 'compliance', 'response api test', 'openresponses test'.
|
||||
description: 'OpenResponses API compliance testing. Use for Response API endpoint tests, compliance runs, schema debugging, response api test, or openresponses test tasks.'
|
||||
---
|
||||
|
||||
# OpenResponses Compliance Test
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: review-checklist
|
||||
description: "Common recurring mistakes in LobeHub code review — `console.*` leftovers, missing `return await`, hardcoded secrets, hardcoded i18n strings, desktop router pair drift, antd vs `@lobehub/ui`, non-idempotent migrations, cloud impact red flags. Use as a quick checklist when reviewing a PR, diff, or branch change. Triggers on 'code review', 'review the diff', 'review this PR', 'review changes', 'PR review checklist', '审一下', '审 PR'."
|
||||
description: 'LobeHub code review checklist. Use when reviewing a PR, diff, or branch for console leftovers, return await, secrets, i18n, desktop router drift, UI imports, migrations, or cloud impact.'
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: skills-audit
|
||||
description: Weekly audit of `.agents/skills/*/SKILL.md` — surfaces duplicate / overlapping / stale skills, inconsistent descriptions, broken cross-references, and merge/delete candidates. Run as a recurring health-check, not during normal feature work.
|
||||
description: 'Audit .agents/skills SKILL.md files. Use for recurring checks of duplicate, overlapping, stale, inconsistent, or broken skills and merge/delete candidates.'
|
||||
disable-model-invocation: true
|
||||
argument-hint: '[--verbose | --apply]'
|
||||
---
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: spa-routes
|
||||
description: "SPA roots-vs-features split for LobeHub — thin route segments under `src/routes/` delegate to domain components under `src/features/`. Use when editing `src/routes/` segments, `src/spa/router/desktopRouter.config.tsx` or `desktopRouter.config.desktop.tsx` (MUST update both together — `desktopRouter.sync.test.tsx` enforces this), `mobileRouter.config.tsx`, `popupRouter.config.tsx`, any colocated `<name>.desktop.{ts,tsx}` variant (e.g. settings `componentMap.ts` × `componentMap.desktop.ts`, page-level `index.tsx` × `index.desktop.tsx`), or moving UI/logic between `routes/` and `features/`. Triggers on `desktopRouter.config`, `mobileRouter.config`, `popupRouter.config`, `componentMap.desktop`, `index.desktop.tsx`, `.desktop.tsx` variant, `src/routes/**`, `src/features/**`, 'add a route', 'new page', 'route segment', '路由'."
|
||||
description: 'LobeHub SPA route architecture. Use when editing src/routes, src/features delegation, desktop/mobile/popup router configs, .desktop variants, route segments, redirects, or new pages.'
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: store-data-structures
|
||||
description: "Zustand store data-shape patterns for LobeHub — List vs Detail split, Map + Reducer, type definitions sourced from `@lobechat/types` (not `@lobechat/database`). Use when designing store state, choosing between Array (list) and `Record<string, Detail>` (detail map), or implementing a list/detail page pair. Triggers on `messagesMap`, `topicsMap`, `Record<string, Detail>`, 'list vs detail', 'store data shape', 'normalize state', 'state structure'."
|
||||
description: 'LobeHub Zustand store data-shape patterns. Use when designing store state, list/detail splits, normalized maps, reducers, messagesMap, topicsMap, or choosing shared type sources.'
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: testing
|
||||
description: Testing guide using Vitest. Use when writing tests (.test.ts, .test.tsx), fixing failing tests, improving test coverage, or debugging test issues. Triggers on test creation, test debugging, mock setup, or test-related questions.
|
||||
description: 'Vitest testing guide. Use when writing or updating tests, fixing failing tests, improving coverage, debugging test issues, or setting up mocks.'
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: trpc-router
|
||||
description: TRPC router development guide. Use when creating or modifying TRPC routers (src/server/routers/**), adding procedures, or working with server-side API endpoints. Triggers on TRPC router creation, procedure implementation, or API endpoint tasks.
|
||||
description: 'TRPC router development guide. Use when creating or modifying src/server/routers, adding procedures, or implementing server-side API endpoints.'
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: typescript
|
||||
description: "TypeScript code style and type-safety guide for LobeHub. Read before writing or editing any `.ts` / `.tsx` / `.mts` — covers `interface` vs `type`, `Record<PropertyKey, unknown>` over `any`/`object`, `as const satisfies`, `@ts-expect-error` over `@ts-ignore`, `import type` (`separate-type-imports`), `async`/`await` + `Promise.all`, `for…of` over indexed `for`, and the no-silent-`.catch(() => fallback)` rule. Also use when reviewing type quality, deciding module augmentation (`declare module`) over `namespace`, or designing extensible types (e.g. `PipelineContext.metadata`). Triggers on any TypeScript file edit, 'fix the type', 'why is this `any`', 'should this be interface or type', 'eslint type-import', 'ts-expect-error'."
|
||||
description: 'LobeHub TypeScript style and type-safety guide. Use when editing TS/TSX/MTS, fixing types, choosing interface vs type, avoiding any/object, import type, async flow, or ts-expect-error.'
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: upstash-workflow
|
||||
description: "Upstash Workflow + QStash implementation guide for LobeHub — 3-layer architecture (process → paginate → execute), fan-out patterns. Use when creating an async workflow, implementing fan-out (paginate → execute), or wiring `serve()` + `context.run` / `context.call` steps. Triggers on `serve()`, `context.run`, `context.call`, `context.sleep`, `qstash`, 'async workflow', 'fan-out workflow', 'QStash workflow'."
|
||||
description: 'LobeHub Upstash Workflow and QStash guide. Use for async workflows, process/paginate/execute fan-out, serve handlers, context.run/call/sleep, or workflow triggers.'
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: zustand
|
||||
description: "LobeHub Zustand store conventions: public/internal/dispatch action layers, optimistic update pattern, slice composition via `flattenActions`, and class-based action migration. Use whenever working under `src/store/**`, adding a `createXxxSlice`, writing `internal_*` or `internal_dispatch*` actions, designing `messagesMap`/`topicsMap` reducers, refactoring a `StateCreator` object slice into a `XxxActionImpl` class, or debugging stale store reads. Triggers on `useChatStore`/`useUserStore`/`useGlobalStore`, `createStore`, `flattenActions`, `StoreSetter`, `internal_dispatch`, 'add an action', 'zustand selector', 'store slice', 'class action', 'optimistic update'."
|
||||
description: 'LobeHub Zustand store conventions. Use when editing src/store, store slices, public/internal actions, dispatch actions, flattenActions, optimistic updates, selectors, maps, or class action migration.'
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
|
||||
@@ -124,6 +124,10 @@ cd packages/database && bunx vitest run --silent='passed-only' '[file]'
|
||||
- For dev preview: translate `locales/zh-CN/` and `locales/en-US/`
|
||||
- `pnpm i18n` is slow; run it manually when locale keys need updating (e.g. before opening a PR).
|
||||
|
||||
### Code Style
|
||||
|
||||
- When a single file grows beyond \~800 lines, consider splitting it into multiple files (extract sub-components, hooks, helpers, or types). Smaller, focused files are friendly to humans and agents.
|
||||
|
||||
### Code Review
|
||||
|
||||
Before reviewing a PR / diff / branch change, read the **review-checklist** skill (`.agents/skills/review-checklist/SKILL.md`) — it lists the recurring mistakes specific to this codebase.
|
||||
|
||||
@@ -2,6 +2,35 @@
|
||||
|
||||
# Changelog
|
||||
|
||||
## [Version 2.2.1](https://github.com/lobehub/lobe-chat/compare/v0.0.0-nightly.pr15228.13999...v2.2.1)
|
||||
|
||||
<sup>Released on **2026-05-29**</sup>
|
||||
|
||||
#### ✨ Features
|
||||
|
||||
- **device**: device registry TRPC (register / list / update / remove).
|
||||
- **bot**: add iMessage Desktop setup and bridge.
|
||||
- **desktop**: show zoom level HUD on Cmd+/- and Cmd+0.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### What's improved
|
||||
|
||||
- **device**: device registry TRPC (register / list / update / remove), closes [#15299](https://github.com/lobehub/lobe-chat/issues/15299) ([671b252](https://github.com/lobehub/lobe-chat/commit/671b252))
|
||||
- **bot**: add iMessage Desktop setup and bridge, closes [#15228](https://github.com/lobehub/lobe-chat/issues/15228) ([6d94635](https://github.com/lobehub/lobe-chat/commit/6d94635))
|
||||
- **desktop**: show zoom level HUD on Cmd+/- and Cmd+0, closes [#15294](https://github.com/lobehub/lobe-chat/issues/15294) ([109545c](https://github.com/lobehub/lobe-chat/commit/109545c))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
### [Version 2.2.0](https://github.com/lobehub/lobe-chat/compare/v2.1.59-canary.27...v2.2.0)
|
||||
|
||||
<sup>Released on **2026-05-18**</sup>
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { execSync } from 'node:child_process';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
assertGoldenFinalState,
|
||||
extractGoldenOutcomes,
|
||||
} from './fixtures/agent-signal/assertGoldenFinalState';
|
||||
|
||||
/**
|
||||
* E2E tests for `lh agent-signal trigger`.
|
||||
*
|
||||
* The "golden fixture" block runs fully offline — it is the structural
|
||||
* regression baseline that the execAgent migration asserts
|
||||
* against. The "live trigger" block requires a running server + authenticated
|
||||
* CLI and is gated behind AGENT_SIGNAL_AGENT_ID (or AGENT_ID).
|
||||
*
|
||||
* Prerequisites for the live block:
|
||||
* - `lh` (or LH_CLI_PATH) points at the built CLI
|
||||
* - User is authenticated (`lh login`) against a dev server with Agent Signal enabled
|
||||
* - AGENT_SIGNAL_AGENT_ID=<agentId> identifies a target agent the user owns
|
||||
*/
|
||||
|
||||
const CLI = process.env.LH_CLI_PATH || 'lh';
|
||||
const AGENT_ID = process.env.AGENT_SIGNAL_AGENT_ID || process.env.AGENT_ID;
|
||||
const TIMEOUT = 60_000;
|
||||
|
||||
const goldenPath = fileURLToPath(
|
||||
new URL('./fixtures/agent-signal/nightly-review.golden.json', import.meta.url),
|
||||
);
|
||||
const golden = JSON.parse(readFileSync(goldenPath, 'utf-8'));
|
||||
|
||||
function run(args: string): string {
|
||||
return execSync(`${CLI} ${args}`, {
|
||||
encoding: 'utf-8',
|
||||
env: { ...process.env, PATH: `${process.env.HOME}/.bun/bin:${process.env.PATH}` },
|
||||
timeout: TIMEOUT,
|
||||
}).trim();
|
||||
}
|
||||
|
||||
describe('agent-signal golden fixture - structural regression', () => {
|
||||
it('captures a recognizable nightly-review source payload', () => {
|
||||
expect(golden.source.sourceType).toBe('agent.nightly_review.requested');
|
||||
expect(golden.source.payload.agentId).toBeTruthy();
|
||||
expect(golden.source.payload.userId).toBeTruthy();
|
||||
expect(golden.source.scopeKey).toContain('agent:');
|
||||
});
|
||||
|
||||
it('extracts ideas / write outcomes / brief from finalState', () => {
|
||||
const outcomes = extractGoldenOutcomes(golden.finalState);
|
||||
|
||||
expect(outcomes.ideas.length).toBeGreaterThanOrEqual(1);
|
||||
expect(outcomes.writeOutcomes.length).toBeGreaterThanOrEqual(1);
|
||||
expect(outcomes.brief).toBeDefined();
|
||||
});
|
||||
|
||||
it('passes the shared structural assertion', () => {
|
||||
expect(() => assertGoldenFinalState(golden.finalState)).not.toThrow();
|
||||
});
|
||||
|
||||
it('rejects an empty finalState', () => {
|
||||
expect(() => assertGoldenFinalState({ messages: [] })).toThrow(/artifact/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!AGENT_ID)('lh agent-signal trigger - live', () => {
|
||||
it('triggers a nightly review and returns a workflow run id', () => {
|
||||
const output = run(
|
||||
`agent-signal trigger --source-type agent.nightly_review.requested --agent ${AGENT_ID} --json`,
|
||||
);
|
||||
const result = JSON.parse(output);
|
||||
expect(result).toHaveProperty('accepted');
|
||||
expect(result).toHaveProperty('scopeKey');
|
||||
// When Agent Signal is enabled for the account, a workflow run id is returned.
|
||||
if (result.accepted) {
|
||||
expect(typeof result.workflowRunId).toBe('string');
|
||||
expect(result.workflowRunId.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('exits non-zero on an invalid source type', () => {
|
||||
expect(() =>
|
||||
run(`agent-signal trigger --source-type not.a.real.type --agent ${AGENT_ID}`),
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* Standalone structural assertions for self-iteration finalState snapshots.
|
||||
*
|
||||
* Dependency-free on purpose: the execAgent migration PRs
|
||||
* import this from server tests AND the CLI e2e suite, so it must not pull in
|
||||
* vitest or any server-only module. Mirrors the `kind` discrimination used by
|
||||
* `src/server/services/agentSignal/services/selfIteration/finalStateExtractor.ts`.
|
||||
*/
|
||||
|
||||
export type ToolResultKind = 'artifact' | 'mutation' | 'read';
|
||||
|
||||
export interface ToolResultWithKind {
|
||||
apiName?: string;
|
||||
data: Record<string, unknown> | unknown;
|
||||
kind: ToolResultKind;
|
||||
toolCallId?: string;
|
||||
}
|
||||
|
||||
export interface GoldenOutcomes {
|
||||
/** The single brief mutation, if any (apiName matches /brief/i). */
|
||||
brief?: ToolResultWithKind;
|
||||
/** Artifact tool results whose apiName mentions an idea. */
|
||||
ideas: ToolResultWithKind[];
|
||||
/** Artifact tool results whose apiName mentions an intent. */
|
||||
intents: ToolResultWithKind[];
|
||||
/** Durable mutation tool results, excluding the brief. */
|
||||
writeOutcomes: ToolResultWithKind[];
|
||||
}
|
||||
|
||||
interface FinalStateLike {
|
||||
messages?: unknown[];
|
||||
}
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
|
||||
const parseContent = (content: unknown): unknown => {
|
||||
if (typeof content !== 'string') return content;
|
||||
try {
|
||||
return JSON.parse(content);
|
||||
} catch {
|
||||
return content;
|
||||
}
|
||||
};
|
||||
|
||||
/** Extract every tool result of `kind` from a finalState, in message order. */
|
||||
export const extractFromFinalState = (
|
||||
finalState: FinalStateLike,
|
||||
kind: ToolResultKind,
|
||||
): ToolResultWithKind[] => {
|
||||
const results: ToolResultWithKind[] = [];
|
||||
|
||||
for (const message of finalState.messages ?? []) {
|
||||
if (!isRecord(message)) continue;
|
||||
if (message.role !== 'tool') continue;
|
||||
|
||||
const content = parseContent(message.content);
|
||||
const contentRecord = isRecord(content) ? content : undefined;
|
||||
const pluginState = isRecord(message.pluginState) ? message.pluginState : undefined;
|
||||
const resultKind = contentRecord?.kind ?? pluginState?.kind;
|
||||
|
||||
if (resultKind !== kind) continue;
|
||||
|
||||
results.push({
|
||||
apiName: typeof message.apiName === 'string' ? message.apiName : undefined,
|
||||
data: contentRecord ?? content,
|
||||
kind,
|
||||
toolCallId: typeof message.tool_call_id === 'string' ? message.tool_call_id : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
const matchesApiName = (result: ToolResultWithKind, pattern: RegExp): boolean =>
|
||||
typeof result.apiName === 'string' && pattern.test(result.apiName);
|
||||
|
||||
const briefText = (brief?: ToolResultWithKind): string => {
|
||||
if (!brief || !isRecord(brief.data)) return '';
|
||||
const summary = typeof brief.data.summary === 'string' ? brief.data.summary : '';
|
||||
const body = typeof brief.data.body === 'string' ? brief.data.body : '';
|
||||
return `${summary}${body}`.trim();
|
||||
};
|
||||
|
||||
/** Partition a finalState into ideas / intents / writeOutcomes / brief buckets. */
|
||||
export const extractGoldenOutcomes = (finalState: FinalStateLike): GoldenOutcomes => {
|
||||
const artifacts = extractFromFinalState(finalState, 'artifact');
|
||||
const mutations = extractFromFinalState(finalState, 'mutation');
|
||||
|
||||
const brief = mutations.find((m) => matchesApiName(m, /brief/i));
|
||||
|
||||
return {
|
||||
brief,
|
||||
ideas: artifacts.filter((a) => matchesApiName(a, /idea/i)),
|
||||
intents: artifacts.filter((a) => matchesApiName(a, /intent/i)),
|
||||
writeOutcomes: mutations.filter((m) => !matchesApiName(m, /brief/i)),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Structural regression assertion for a self-iteration finalState.
|
||||
*
|
||||
* Throws (with a descriptive message) when the run produced no structured
|
||||
* output: it requires at least one artifact (idea or intent), at least one
|
||||
* durable write outcome, and a non-empty brief. Never compares text verbatim.
|
||||
*/
|
||||
export const assertGoldenFinalState = (finalState: FinalStateLike): GoldenOutcomes => {
|
||||
const outcomes = extractGoldenOutcomes(finalState);
|
||||
const artifactCount = outcomes.ideas.length + outcomes.intents.length;
|
||||
|
||||
if (artifactCount < 1) {
|
||||
throw new Error(`Expected >= 1 artifact (idea/intent) in finalState, found ${artifactCount}`);
|
||||
}
|
||||
|
||||
if (outcomes.writeOutcomes.length < 1) {
|
||||
throw new Error(
|
||||
`Expected >= 1 write outcome (mutation) in finalState, found ${outcomes.writeOutcomes.length}`,
|
||||
);
|
||||
}
|
||||
|
||||
const text = briefText(outcomes.brief);
|
||||
if (text.length === 0) {
|
||||
throw new Error('Expected a non-empty brief in finalState, found none');
|
||||
}
|
||||
|
||||
return outcomes;
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"description": "Desensitized golden snapshot of one nightly-review self-iteration run. Used as a structural regression baseline by the execAgent migration which converges all agent execution paths (chat, self-iteration, memoryWriter, skillManagement) onto a single execAgent entry point. Assert structure, never byte-for-byte: the LLM output is non-deterministic.",
|
||||
"finalState": {
|
||||
"messages": [
|
||||
{
|
||||
"content": "Run the nightly self-review for the local window.",
|
||||
"role": "user"
|
||||
},
|
||||
{
|
||||
"apiName": "getEvidenceDigest",
|
||||
"content": "{\"kind\":\"read\",\"topicCount\":3,\"messageCount\":42,\"window\":\"2026-05-30/2026-05-31\"}",
|
||||
"role": "tool",
|
||||
"tool_call_id": "call_read_1"
|
||||
},
|
||||
{
|
||||
"apiName": "recordSelfReviewIdea",
|
||||
"content": "{\"kind\":\"artifact\",\"idempotencyKey\":\"idea:pref:tone\",\"title\":\"Prefer concise replies\",\"rationale\":\"User repeatedly asked to shorten answers in topic tpc_demo\",\"risk\":\"low\"}",
|
||||
"role": "tool",
|
||||
"tool_call_id": "call_idea_1"
|
||||
},
|
||||
{
|
||||
"apiName": "recordSelfReviewIdea",
|
||||
"content": "{\"kind\":\"artifact\",\"idempotencyKey\":\"idea:skill:drizzle\",\"title\":\"Document Drizzle join helper\",\"rationale\":\"Recurring question about leftJoin usage\",\"risk\":\"medium\"}",
|
||||
"role": "tool",
|
||||
"tool_call_id": "call_idea_2"
|
||||
},
|
||||
{
|
||||
"apiName": "writeMemory",
|
||||
"content": "{\"kind\":\"mutation\",\"status\":\"applied\",\"resourceId\":\"mem_001\",\"summary\":\"Stored tone preference: prefer concise replies\"}",
|
||||
"pluginState": { "kind": "mutation" },
|
||||
"role": "tool",
|
||||
"tool_call_id": "call_mut_1"
|
||||
},
|
||||
{
|
||||
"apiName": "createSelfReviewBrief",
|
||||
"content": "{\"kind\":\"mutation\",\"briefId\":\"brief_001\",\"summary\":\"Nightly review captured 2 ideas and wrote 1 memory.\",\"body\":\"## Highlights\\n- Prefer concise replies\\n- Document Drizzle join helper\"}",
|
||||
"role": "tool",
|
||||
"tool_call_id": "call_brief_1"
|
||||
},
|
||||
{
|
||||
"content": "Nightly review complete. Captured 2 ideas and wrote 1 memory.",
|
||||
"role": "assistant"
|
||||
}
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"payload": {
|
||||
"agentId": "agent_demo",
|
||||
"localDate": "2026-05-30",
|
||||
"requestedAt": "2026-05-31T04:00:00.000Z",
|
||||
"reviewWindowEnd": "2026-05-31T04:00:00.000Z",
|
||||
"reviewWindowStart": "2026-05-30T04:00:00.000Z",
|
||||
"timezone": "UTC",
|
||||
"userId": "user_demo"
|
||||
},
|
||||
"scopeKey": "agent:agent_demo:user:user_demo",
|
||||
"sourceId": "nightly-review:user_demo:agent_demo:2026-05-30",
|
||||
"sourceType": "agent.nightly_review.requested",
|
||||
"timestamp": 1748664000000
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Code generated by `npm run man:generate`; DO NOT EDIT.
|
||||
.\" Manual command details come from the Commander command tree.
|
||||
.TH LH 1 "" "@lobehub/cli 0.0.22" "User Commands"
|
||||
.TH LH 1 "" "@lobehub/cli 0.0.24" "User Commands"
|
||||
.SH NAME
|
||||
lh \- LobeHub CLI \- manage and connect to LobeHub services
|
||||
.SH SYNOPSIS
|
||||
@@ -65,6 +65,9 @@ Manage agents
|
||||
.B agent\-group
|
||||
Manage agent groups
|
||||
.TP
|
||||
.B agent\-signal
|
||||
Inspect and trigger Agent Signal source events
|
||||
.TP
|
||||
.B bot
|
||||
Manage bot integrations
|
||||
.TP
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@lobehub/cli",
|
||||
"version": "0.0.22",
|
||||
"version": "0.0.24",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"lh": "./dist/index.js",
|
||||
@@ -33,6 +33,7 @@
|
||||
"@lobechat/device-identity": "workspace:*",
|
||||
"@lobechat/heterogeneous-agents": "workspace:*",
|
||||
"@lobechat/local-file-shell": "workspace:*",
|
||||
"@lobechat/tool-runtime": "workspace:*",
|
||||
"@trpc/client": "^11.8.1",
|
||||
"@types/node": "^22.13.5",
|
||||
"@types/ws": "^8.18.1",
|
||||
|
||||
@@ -13,7 +13,7 @@ interface CurrentUserResponse {
|
||||
export async function getUserIdFromApiKey(apiKey: string, serverUrl?: string): Promise<string> {
|
||||
const normalizedServerUrl = normalizeUrl(serverUrl) || resolveServerUrl();
|
||||
|
||||
const response = await fetch(`${normalizedServerUrl}/api/v1/users/me`, {
|
||||
const response = await fetch(`${normalizedServerUrl}/api/v1/users/me?includeCount=0`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
@@ -23,7 +23,9 @@ export async function getUserIdFromApiKey(apiKey: string, serverUrl?: string): P
|
||||
try {
|
||||
body = (await response.json()) as CurrentUserResponse;
|
||||
} catch {
|
||||
throw new Error(`Failed to parse response from ${normalizedServerUrl}/api/v1/users/me.`);
|
||||
throw new Error(
|
||||
`Failed to parse response from ${normalizedServerUrl}/api/v1/users/me?includeCount=0.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.ok || body?.success === false) {
|
||||
|
||||
@@ -20,7 +20,7 @@ interface ResolvedAuth {
|
||||
/**
|
||||
* Parse the `sub` claim from a JWT without verifying the signature.
|
||||
*/
|
||||
function parseJwtSub(token: string): string | undefined {
|
||||
export function parseJwtSub(token: string): string | undefined {
|
||||
try {
|
||||
const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString());
|
||||
return payload.sub;
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import type { Command } from 'commander';
|
||||
import pc from 'picocolors';
|
||||
|
||||
import { getTrpcClient } from '../../api/client';
|
||||
import { log } from '../../utils/logger';
|
||||
|
||||
/**
|
||||
* Producer source types a developer may trigger manually for local testing.
|
||||
* Mirrors `AGENT_SIGNAL_TRIGGER_SOURCE_TYPES` on the server; kept inline so the
|
||||
* CLI bundle does not pull in server-only modules.
|
||||
*/
|
||||
const TRIGGER_SOURCE_TYPES = [
|
||||
'agent.nightly_review.requested',
|
||||
'agent.self_reflection.requested',
|
||||
'agent.self_feedback_intent.declared',
|
||||
'agent.user.message',
|
||||
'tool.outcome.completed',
|
||||
'tool.outcome.failed',
|
||||
] as const;
|
||||
|
||||
type TriggerSourceType = (typeof TRIGGER_SOURCE_TYPES)[number];
|
||||
|
||||
export function registerAgentSignalCommand(program: Command) {
|
||||
const agentSignal = program
|
||||
.command('agent-signal')
|
||||
.description('Inspect and trigger Agent Signal source events');
|
||||
|
||||
agentSignal
|
||||
.command('trigger')
|
||||
.description('Trigger an Agent Signal source event for the authenticated user')
|
||||
.requiredOption(
|
||||
'--source-type <type>',
|
||||
`Source type to emit. One of:\n ${TRIGGER_SOURCE_TYPES.join('\n ')}`,
|
||||
)
|
||||
.option('--agent <agentId>', 'Target agent ID (required for agent-scoped source types)')
|
||||
.option('--topic <topicId>', 'Topic ID to scope the event to')
|
||||
.option('--payload-json <json>', 'JSON object shallow-merged over the default payload')
|
||||
.option('--source-id <id>', 'Override the auto-derived dedupe source id')
|
||||
.option('--scope-key <key>', 'Override the auto-derived scope key')
|
||||
.option('--timestamp <ms>', 'Event timestamp in milliseconds')
|
||||
.option('--json', 'Output JSON')
|
||||
.action(
|
||||
async (options: {
|
||||
agent?: string;
|
||||
json?: boolean;
|
||||
payloadJson?: string;
|
||||
scopeKey?: string;
|
||||
sourceId?: string;
|
||||
sourceType: string;
|
||||
timestamp?: string;
|
||||
topic?: string;
|
||||
}) => {
|
||||
const sourceType = options.sourceType as TriggerSourceType;
|
||||
|
||||
if (!TRIGGER_SOURCE_TYPES.includes(sourceType)) {
|
||||
console.error(
|
||||
`${pc.red('✗')} Invalid --source-type "${options.sourceType}". Expected one of: ${TRIGGER_SOURCE_TYPES.join(', ')}`,
|
||||
);
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
let payloadOverride: Record<string, unknown> | undefined;
|
||||
if (options.payloadJson) {
|
||||
try {
|
||||
const parsed = JSON.parse(options.payloadJson);
|
||||
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
||||
throw new Error('payload must be a JSON object');
|
||||
}
|
||||
payloadOverride = parsed as Record<string, unknown>;
|
||||
} catch (error: any) {
|
||||
console.error(`${pc.red('✗')} Failed to parse --payload-json: ${error.message}`);
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let timestamp: number | undefined;
|
||||
if (options.timestamp !== undefined) {
|
||||
timestamp = Number(options.timestamp);
|
||||
if (!Number.isFinite(timestamp)) {
|
||||
console.error(`${pc.red('✗')} --timestamp must be a number (milliseconds)`);
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
log.debug(
|
||||
'agent-signal trigger: sourceType=%s agent=%s topic=%s',
|
||||
sourceType,
|
||||
options.agent,
|
||||
options.topic,
|
||||
);
|
||||
|
||||
const client = await getTrpcClient();
|
||||
|
||||
try {
|
||||
const result = await client.agentSignal.triggerSourceEvent.mutate({
|
||||
agentId: options.agent,
|
||||
payloadOverride,
|
||||
scopeKey: options.scopeKey,
|
||||
sourceId: options.sourceId,
|
||||
sourceType,
|
||||
timestamp,
|
||||
topicId: options.topic,
|
||||
});
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.accepted) {
|
||||
console.log(
|
||||
`${pc.yellow('!')} Agent Signal is disabled for this account — event was not enqueued (scopeKey: ${pc.bold(result.scopeKey)})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`${pc.green('✓')} Triggered ${pc.bold(sourceType)}`);
|
||||
console.log(` Scope key: ${result.scopeKey}`);
|
||||
console.log(` Workflow run id: ${result.workflowRunId}`);
|
||||
} catch (error: any) {
|
||||
console.error(`${pc.red('✗')} Failed to trigger source event: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -8,11 +8,8 @@ import type {
|
||||
ToolCallRequestMessage,
|
||||
} from '@lobechat/device-gateway-client';
|
||||
import { GatewayClient } from '@lobechat/device-gateway-client';
|
||||
import type { IdentitySource } from '@lobechat/device-identity';
|
||||
import { deriveDeviceId } from '@lobechat/device-identity';
|
||||
import type { Command } from 'commander';
|
||||
|
||||
import { createLambdaClient } from '../api/client';
|
||||
import { getValidToken } from '../auth/refresh';
|
||||
import { resolveToken } from '../auth/resolveToken';
|
||||
import { CLI_API_KEY_ENV } from '../constants/auth';
|
||||
@@ -28,6 +25,7 @@ import {
|
||||
stopDaemon,
|
||||
writeStatus,
|
||||
} from '../daemon/manager';
|
||||
import { registerDevice, resolveDeviceIdentity } from '../device/register';
|
||||
import { loadOrCreateConnectionId, loadSettings, normalizeUrl, saveSettings } from '../settings';
|
||||
import { executeToolCall } from '../tools';
|
||||
import { cleanupAllProcesses } from '../tools/shell';
|
||||
@@ -198,12 +196,7 @@ async function runConnect(options: ConnectOptions, isDaemonChild: boolean) {
|
||||
// Resolve a stable device identity. An explicit `--device-id` wins (lets a
|
||||
// user pin a VM to a fixed identity); otherwise derive from the machine id so
|
||||
// the same machine + user maps to one device across reconnects.
|
||||
const identity: { deviceId: string; identitySource: IdentitySource } | undefined =
|
||||
options.deviceId
|
||||
? { deviceId: options.deviceId, identitySource: 'fallback' }
|
||||
: auth.userId
|
||||
? deriveDeviceId(auth.userId)
|
||||
: undefined;
|
||||
const identity = resolveDeviceIdentity(auth.userId, options.deviceId);
|
||||
|
||||
// Freeform channel label (`cli` by default); `LOBEHUB_CLI_CHANNEL` lets a
|
||||
// dev build tag itself `cli-dev` so the gateway can prioritise / display it.
|
||||
@@ -287,6 +280,7 @@ async function runConnect(options: ConnectOptions, isDaemonChild: boolean) {
|
||||
result: {
|
||||
content: result.content,
|
||||
error: result.error,
|
||||
state: result.state,
|
||||
success: result.success,
|
||||
},
|
||||
});
|
||||
@@ -406,19 +400,15 @@ async function runConnect(options: ConnectOptions, isDaemonChild: boolean) {
|
||||
});
|
||||
|
||||
// Register this device in the server registry before opening the WS, so the
|
||||
// row exists by the time the gateway reports it online. Best-effort: a
|
||||
// failure must not block the connection.
|
||||
// row exists by the time the gateway reports it online. `lh login` already
|
||||
// registers, but re-running here is cheap (idempotent upsert) and covers
|
||||
// `--token` sessions that never went through login. Best-effort: a failure
|
||||
// must not block the connection.
|
||||
if (identity) {
|
||||
try {
|
||||
// Reuse the already-resolved auth (respects `--token` mode) instead of
|
||||
// getTrpcClient(), which re-discovers creds and exits when none are found.
|
||||
const trpc = createLambdaClient(auth);
|
||||
await trpc.device.register.mutate({
|
||||
deviceId: identity.deviceId,
|
||||
hostname: os.hostname(),
|
||||
identitySource: identity.identitySource,
|
||||
platform: process.platform,
|
||||
});
|
||||
// Reuse the already-resolved auth (respects `--token` mode) so we don't
|
||||
// re-discover creds and exit when none are found.
|
||||
await registerDevice(auth, identity);
|
||||
} catch (err) {
|
||||
error(`Device registration failed (non-fatal): ${(err as Error).message}`);
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@ import type { Command } from 'commander';
|
||||
|
||||
import { getUserIdFromApiKey } from '../auth/apiKey';
|
||||
import { saveCredentials } from '../auth/credentials';
|
||||
import { parseJwtSub } from '../auth/resolveToken';
|
||||
import { CLI_API_KEY_ENV } from '../constants/auth';
|
||||
import { OFFICIAL_SERVER_URL } from '../constants/urls';
|
||||
import { registerDevice, resolveDeviceIdentity } from '../device/register';
|
||||
import { loadSettings, normalizeUrl, saveSettings } from '../settings';
|
||||
import { log } from '../utils/logger';
|
||||
|
||||
@@ -213,6 +215,30 @@ export function registerLoginCommand(program: Command) {
|
||||
},
|
||||
);
|
||||
|
||||
// Register this device in the server registry right after auth, so
|
||||
// the device row exists without waiting for a later `lh connect`
|
||||
// (which only adds the channel-online step). Mirrors the desktop
|
||||
// app, which registers on login. Best-effort: a failure here must
|
||||
// not fail the login.
|
||||
//
|
||||
// Skip the `fallback` source: `lh login` has no `--device-id` and
|
||||
// persists no fallback id, so a machine without a readable
|
||||
// machine-id would derive a *fresh random* id on every login —
|
||||
// registering it just spawns orphan device rows that never match
|
||||
// the id a later `lh connect` resolves. Defer registration to
|
||||
// `connect` in that case, where the same id is reused for the WS.
|
||||
const identity = resolveDeviceIdentity(parseJwtSub(body.access_token));
|
||||
if (identity && identity.identitySource !== 'fallback') {
|
||||
try {
|
||||
await registerDevice(
|
||||
{ serverUrl, token: body.access_token, tokenType: 'jwt' },
|
||||
identity,
|
||||
);
|
||||
} catch (err) {
|
||||
log.warn(`Device registration failed (non-fatal): ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
log.info('Login successful! Credentials saved.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import os from 'node:os';
|
||||
|
||||
import type { DeviceIdentity } from '@lobechat/device-identity';
|
||||
import { deriveDeviceId } from '@lobechat/device-identity';
|
||||
|
||||
import { createLambdaClient } from '../api/client';
|
||||
|
||||
/**
|
||||
* Resolve a stable device identity. An explicit `--device-id` wins (lets a user
|
||||
* pin a VM to a fixed identity); otherwise derive from the machine id so the
|
||||
* same machine + user maps to one device across reconnects. Returns undefined
|
||||
* when neither an explicit id nor a userId is available.
|
||||
*/
|
||||
export function resolveDeviceIdentity(
|
||||
userId: string | undefined,
|
||||
explicitDeviceId?: string,
|
||||
): DeviceIdentity | undefined {
|
||||
if (explicitDeviceId) return { deviceId: explicitDeviceId, identitySource: 'fallback' };
|
||||
if (userId) return deriveDeviceId(userId);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register this device in the server registry. Shared by `lh login` (so the
|
||||
* device row exists right after auth) and `lh connect` (so the row exists
|
||||
* before the WS opens). Best-effort by contract: callers should wrap this in a
|
||||
* try/catch and treat any failure as non-fatal.
|
||||
*/
|
||||
export async function registerDevice(
|
||||
auth: { serverUrl: string; token: string; tokenType: 'apiKey' | 'jwt' | 'serviceToken' },
|
||||
identity: DeviceIdentity,
|
||||
): Promise<void> {
|
||||
const trpc = createLambdaClient(auth);
|
||||
await trpc.device.register.mutate({
|
||||
deviceId: identity.deviceId,
|
||||
hostname: os.hostname(),
|
||||
identitySource: identity.identitySource,
|
||||
platform: process.platform,
|
||||
});
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { Command } from 'commander';
|
||||
|
||||
import { registerAgentCommand } from './commands/agent';
|
||||
import { registerAgentGroupCommand } from './commands/agent-group';
|
||||
import { registerAgentSignalCommand } from './commands/agent-signal';
|
||||
import { registerBotCommand } from './commands/bot';
|
||||
import { registerCompletionCommand } from './commands/completion';
|
||||
import { registerConfigCommand } from './commands/config';
|
||||
@@ -58,6 +59,7 @@ export function createProgram() {
|
||||
registerMemoryCommand(program);
|
||||
registerAgentCommand(program);
|
||||
registerAgentGroupCommand(program);
|
||||
registerAgentSignalCommand(program);
|
||||
registerBotCommand(program);
|
||||
registerGenerateCommand(program);
|
||||
registerFileCommand(program);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import { ShellProcessManager } from '@lobechat/local-file-shell';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { executeToolCall } from './index';
|
||||
@@ -27,15 +28,17 @@ describe('executeToolCall', () => {
|
||||
fs.rmSync(tmpDir, { force: true, recursive: true });
|
||||
});
|
||||
|
||||
it('should dispatch readFile', async () => {
|
||||
it('should dispatch readFile with formatted content and structured state', async () => {
|
||||
const filePath = path.join(tmpDir, 'test.txt');
|
||||
await writeFile(filePath, 'hello world');
|
||||
|
||||
const result = await executeToolCall('readFile', JSON.stringify({ path: filePath }));
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const parsed = JSON.parse(result.content);
|
||||
expect(parsed.content).toContain('hello world');
|
||||
// content is now the formatted prompt text, not raw JSON
|
||||
expect(result.content).toContain('hello world');
|
||||
// structured payload travels in `state` for client renders
|
||||
expect((result.state as { content: string }).content).toContain('hello world');
|
||||
});
|
||||
|
||||
it('should dispatch writeFile', async () => {
|
||||
@@ -47,6 +50,7 @@ describe('executeToolCall', () => {
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect((result.state as { path: string }).path).toBe(filePath);
|
||||
expect(fs.readFileSync(filePath, 'utf8')).toBe('written');
|
||||
});
|
||||
|
||||
@@ -57,8 +61,7 @@ describe('executeToolCall', () => {
|
||||
const result = await executeToolCall('readLocalFile', JSON.stringify({ path: filePath }));
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const parsed = JSON.parse(result.content);
|
||||
expect(parsed.content).toContain('legacy hello');
|
||||
expect((result.state as { content: string }).content).toContain('legacy hello');
|
||||
});
|
||||
|
||||
it('should dispatch runCommand', async () => {
|
||||
@@ -68,8 +71,9 @@ describe('executeToolCall', () => {
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const parsed = JSON.parse(result.content);
|
||||
expect(parsed.stdout).toContain('dispatched');
|
||||
expect(result.content).toContain('dispatched');
|
||||
const state = result.state as { output?: string; stdout?: string };
|
||||
expect(state.stdout ?? state.output).toContain('dispatched');
|
||||
});
|
||||
|
||||
it('should dispatch listFiles', async () => {
|
||||
@@ -78,8 +82,7 @@ describe('executeToolCall', () => {
|
||||
const result = await executeToolCall('listFiles', JSON.stringify({ path: tmpDir }));
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const parsed = JSON.parse(result.content);
|
||||
expect(parsed.totalCount).toBeGreaterThan(0);
|
||||
expect((result.state as { totalCount: number }).totalCount).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should dispatch globFiles', async () => {
|
||||
@@ -91,8 +94,7 @@ describe('executeToolCall', () => {
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const parsed = JSON.parse(result.content);
|
||||
expect(parsed.files).toContain('test.ts');
|
||||
expect((result.state as { files: string[] }).files).toContain('test.ts');
|
||||
});
|
||||
|
||||
it('should dispatch editFile', async () => {
|
||||
@@ -109,6 +111,7 @@ describe('executeToolCall', () => {
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect((result.state as { replacements: number }).replacements).toBeGreaterThan(0);
|
||||
expect(fs.readFileSync(filePath, 'utf8')).toBe('new content');
|
||||
});
|
||||
|
||||
@@ -119,19 +122,15 @@ describe('executeToolCall', () => {
|
||||
expect(result.error).toContain('Unknown tool API');
|
||||
});
|
||||
|
||||
it('should handle tool that returns a string result', async () => {
|
||||
// runCommand returns an object, but we test the string branch by mocking
|
||||
// Actually, none of the tools return plain strings, so the JSON.stringify branch
|
||||
// is always taken. The string check is for future-proofing.
|
||||
// Let's verify the JSON output path
|
||||
it('should carry structured state on file reads', async () => {
|
||||
const filePath = path.join(tmpDir, 'str.txt');
|
||||
await writeFile(filePath, 'content');
|
||||
|
||||
const result = await executeToolCall('readFile', JSON.stringify({ path: filePath }));
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
// Result should be valid JSON
|
||||
expect(() => JSON.parse(result.content)).not.toThrow();
|
||||
expect(result.state).toBeDefined();
|
||||
expect(typeof result.content).toBe('string');
|
||||
});
|
||||
|
||||
it('should return error for invalid JSON arguments', async () => {
|
||||
@@ -150,6 +149,7 @@ describe('executeToolCall', () => {
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.state).toBeDefined();
|
||||
});
|
||||
|
||||
it('should dispatch searchFiles', async () => {
|
||||
@@ -161,6 +161,7 @@ describe('executeToolCall', () => {
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.state).toBeDefined();
|
||||
});
|
||||
|
||||
it('should dispatch getCommandOutput', async () => {
|
||||
@@ -169,9 +170,21 @@ describe('executeToolCall', () => {
|
||||
JSON.stringify({ shell_id: 'nonexistent' }),
|
||||
);
|
||||
|
||||
// The runtime envelopes a failed lookup as success:true with the failure in state
|
||||
expect(result.success).toBe(true);
|
||||
const parsed = JSON.parse(result.content);
|
||||
expect(parsed.success).toBe(false);
|
||||
expect((result.state as { success: boolean }).success).toBe(false);
|
||||
});
|
||||
|
||||
it('should forward the gateway timeout to getCommandOutput polling', async () => {
|
||||
const spy = vi
|
||||
.spyOn(ShellProcessManager.prototype, 'getOutput')
|
||||
.mockResolvedValue({ exit_code: 0, output: '', stderr: '', stdout: '', success: true });
|
||||
|
||||
// 3rd arg is the gateway per-call timeout; executeToolCall injects it into args
|
||||
await executeToolCall('getCommandOutput', JSON.stringify({ shell_id: 'sid' }), 5000);
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(expect.objectContaining({ shell_id: 'sid', timeout: 5000 }));
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it('should dispatch killCommand', async () => {
|
||||
@@ -181,7 +194,6 @@ describe('executeToolCall', () => {
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const parsed = JSON.parse(result.content);
|
||||
expect(parsed.success).toBe(false);
|
||||
expect((result.state as { success: boolean }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
+39
-36
@@ -1,41 +1,19 @@
|
||||
import { log } from '../utils/logger';
|
||||
import { checkPlatformCapability } from './checkPlatformCapability';
|
||||
import {
|
||||
editLocalFile,
|
||||
globLocalFiles,
|
||||
grepContent,
|
||||
listLocalFiles,
|
||||
readLocalFile,
|
||||
searchLocalFiles,
|
||||
writeLocalFile,
|
||||
} from './file';
|
||||
import { getAgentProfile } from './getAgentProfile';
|
||||
import { cancelHeteroTask, runHeteroTask } from './heteroTask';
|
||||
import { getCommandOutput, killCommand, runCommand } from './shell';
|
||||
import { runLocalSystemTool } from './localSystemRuntime';
|
||||
|
||||
/**
|
||||
* CLI-only tools (platform agents). File/shell tools are handled separately by
|
||||
* {@link runLocalSystemTool}, which routes them through
|
||||
* `LocalSystemExecutionRuntime` so the result carries structured `state`.
|
||||
*/
|
||||
const methodMap: Record<string, (args: any) => Promise<unknown>> = {
|
||||
cancelHeteroTask,
|
||||
checkPlatformCapability,
|
||||
getAgentProfile,
|
||||
editFile: editLocalFile,
|
||||
getCommandOutput,
|
||||
globFiles: globLocalFiles,
|
||||
grepContent,
|
||||
killCommand,
|
||||
listFiles: listLocalFiles,
|
||||
readFile: readLocalFile,
|
||||
runCommand,
|
||||
runHeteroTask,
|
||||
searchFiles: searchLocalFiles,
|
||||
writeFile: writeLocalFile,
|
||||
|
||||
// Legacy aliases — older Gateway versions may still send the long form
|
||||
editLocalFile,
|
||||
globLocalFiles,
|
||||
listLocalFiles,
|
||||
readLocalFile,
|
||||
searchLocalFiles,
|
||||
writeLocalFile,
|
||||
};
|
||||
|
||||
export async function executeToolCall(
|
||||
@@ -45,19 +23,44 @@ export async function executeToolCall(
|
||||
): Promise<{
|
||||
content: string;
|
||||
error?: string;
|
||||
state?: unknown;
|
||||
success: boolean;
|
||||
}> {
|
||||
const handler = methodMap[apiName];
|
||||
if (!handler) {
|
||||
return { content: '', error: `Unknown tool API: ${apiName}`, success: false };
|
||||
let args: Record<string, any>;
|
||||
try {
|
||||
args = JSON.parse(argsStr);
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
log.error(`Tool call failed: ${apiName} - ${errorMsg}`);
|
||||
return { content: '', error: errorMsg, success: false };
|
||||
}
|
||||
|
||||
const finalArgs =
|
||||
typeof timeout === 'number' && Number.isFinite(timeout) && !('timeout' in args)
|
||||
? { ...args, timeout }
|
||||
: args;
|
||||
|
||||
try {
|
||||
const args = JSON.parse(argsStr);
|
||||
const finalArgs =
|
||||
typeof timeout === 'number' && Number.isFinite(timeout) && !('timeout' in args)
|
||||
? { ...args, timeout }
|
||||
: args;
|
||||
// File/shell tools route through LocalSystemExecutionRuntime so `content` is
|
||||
// the formatted prompt text and `state` carries the structured payload for
|
||||
// client renders — matching the desktop gateway path (PR #15114).
|
||||
const localResult = await runLocalSystemTool(apiName, finalArgs);
|
||||
if (localResult) {
|
||||
const { error } = localResult;
|
||||
return {
|
||||
content: localResult.content,
|
||||
error:
|
||||
error instanceof Error ? error.message : typeof error === 'string' ? error : undefined,
|
||||
state: localResult.state,
|
||||
success: localResult.success,
|
||||
};
|
||||
}
|
||||
|
||||
// CLI-only tools return raw domain payloads, serialized into `content`.
|
||||
const handler = methodMap[apiName];
|
||||
if (!handler) {
|
||||
return { content: '', error: `Unknown tool API: ${apiName}`, success: false };
|
||||
}
|
||||
|
||||
const result = await handler(finalArgs);
|
||||
const content = typeof result === 'string' ? result : JSON.stringify(result);
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import type {
|
||||
EditFileParams,
|
||||
GetCommandOutputParams,
|
||||
GlobFilesParams,
|
||||
GrepContentParams,
|
||||
KillCommandParams,
|
||||
ListFilesParams,
|
||||
ReadFileParams,
|
||||
RunCommandParams,
|
||||
SearchFilesParams,
|
||||
WriteFileParams,
|
||||
} from '@lobechat/local-file-shell';
|
||||
import { type ILocalSystemService, LocalSystemExecutionRuntime } from '@lobechat/tool-runtime';
|
||||
|
||||
import {
|
||||
editLocalFile,
|
||||
globLocalFiles,
|
||||
grepContent,
|
||||
listLocalFiles,
|
||||
readLocalFile,
|
||||
searchLocalFiles,
|
||||
writeLocalFile,
|
||||
} from './file';
|
||||
import { getCommandOutput, killCommand, runCommand } from './shell';
|
||||
|
||||
/**
|
||||
* Output envelope produced by {@link runLocalSystemTool}. Mirrors
|
||||
* `@lobechat/types`' `BuiltinServerRuntimeOutput`: `content` is the formatted
|
||||
* prompt text fed to the LLM, while `state` carries the structured payload that
|
||||
* client renders consume as `pluginState`.
|
||||
*/
|
||||
export interface LocalSystemToolOutput {
|
||||
content: string;
|
||||
error?: unknown;
|
||||
state?: unknown;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stub for `ILocalSystemService` methods the CLI does not expose (batch read,
|
||||
* move, rename). These are never routed by {@link runLocalSystemTool}; the
|
||||
* interface just requires them, so we fail loudly if one is ever reached.
|
||||
*/
|
||||
const unsupported = (method: string) => (): Promise<never> =>
|
||||
Promise.reject(new Error(`${method} is not supported by the LobeHub CLI`));
|
||||
|
||||
/**
|
||||
* Adapter wiring the CLI's `@lobechat/local-file-shell` functions (file ops) and
|
||||
* shell wrappers (with the shared `ShellProcessManager`) into the shape the
|
||||
* runtime expects. The runtime denormalizes its camelCase params back to the
|
||||
* snake_case IPC shapes these functions consume — see `LocalSystemExecutionRuntime`.
|
||||
*/
|
||||
const localSystemService: ILocalSystemService = {
|
||||
editLocalFile,
|
||||
getCommandOutput,
|
||||
globFiles: globLocalFiles,
|
||||
grepContent,
|
||||
killCommand,
|
||||
listLocalFiles,
|
||||
moveLocalFiles: unsupported('moveLocalFiles'),
|
||||
readLocalFile,
|
||||
readLocalFiles: unsupported('readLocalFiles'),
|
||||
renameLocalFile: unsupported('renameLocalFile'),
|
||||
runCommand,
|
||||
searchLocalFiles,
|
||||
writeFile: writeLocalFile,
|
||||
};
|
||||
|
||||
const runtime = new LocalSystemExecutionRuntime(localSystemService);
|
||||
|
||||
/**
|
||||
* Legacy API name aliases used by older gateway versions. Normalized to the
|
||||
* current tool names before dispatch.
|
||||
*/
|
||||
const LEGACY_API_ALIASES: Record<string, string> = {
|
||||
editLocalFile: 'editFile',
|
||||
globLocalFiles: 'globFiles',
|
||||
listLocalFiles: 'listFiles',
|
||||
readLocalFile: 'readFile',
|
||||
searchLocalFiles: 'searchFiles',
|
||||
writeLocalFile: 'writeFile',
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve a relative path against a scope (CWD). Mirrors the desktop gateway's
|
||||
* inline copy of the renderer-side `resolveArgsWithScope` helper so the CLI and
|
||||
* desktop produce identical scoping for search/grep tools.
|
||||
*/
|
||||
const resolveArgsWithScope = <T extends { scope?: string }>(args: T, pathField: string): T => {
|
||||
const scope = args.scope;
|
||||
const bag = args as Record<PropertyKey, unknown>;
|
||||
const currentPath = typeof bag[pathField] === 'string' ? (bag[pathField] as string) : undefined;
|
||||
if (!scope) return args;
|
||||
if (!currentPath) return { ...args, [pathField]: scope };
|
||||
if (path.isAbsolute(currentPath)) return args;
|
||||
return { ...args, [pathField]: path.join(scope, currentPath) };
|
||||
};
|
||||
|
||||
/**
|
||||
* Route file/shell tool calls through `LocalSystemExecutionRuntime` so the
|
||||
* result carries structured `state` (for client renders) and `content` is the
|
||||
* formatted prompt text — matching the desktop gateway path (PR #15114).
|
||||
*
|
||||
* Returns `null` when `apiName` is not a local-system tool, so the caller can
|
||||
* fall back to CLI-only tools (platform agents).
|
||||
*/
|
||||
export async function runLocalSystemTool(
|
||||
apiName: string,
|
||||
args: Record<string, any>,
|
||||
): Promise<LocalSystemToolOutput | null> {
|
||||
const normalized = LEGACY_API_ALIASES[apiName] ?? apiName;
|
||||
|
||||
switch (normalized) {
|
||||
case 'listFiles': {
|
||||
const p = args as ListFilesParams;
|
||||
return runtime.listFiles({
|
||||
directoryPath: p.path,
|
||||
limit: p.limit,
|
||||
sortBy: p.sortBy,
|
||||
sortOrder: p.sortOrder,
|
||||
} as never);
|
||||
}
|
||||
|
||||
case 'readFile': {
|
||||
const p = args as ReadFileParams;
|
||||
return runtime.readFile({
|
||||
endLine: p.loc?.[1],
|
||||
path: p.path,
|
||||
startLine: p.loc?.[0],
|
||||
});
|
||||
}
|
||||
|
||||
case 'writeFile': {
|
||||
return runtime.writeFile(args as WriteFileParams);
|
||||
}
|
||||
|
||||
case 'editFile': {
|
||||
const p = args as EditFileParams;
|
||||
return runtime.editFile({
|
||||
all: p.replace_all,
|
||||
path: p.file_path,
|
||||
replace: p.new_string,
|
||||
search: p.old_string,
|
||||
});
|
||||
}
|
||||
|
||||
case 'searchFiles': {
|
||||
const resolved = resolveArgsWithScope(
|
||||
args as SearchFilesParams & { scope?: string },
|
||||
'directory',
|
||||
);
|
||||
return runtime.searchFiles({ ...resolved, directory: resolved.directory || '' } as never);
|
||||
}
|
||||
|
||||
case 'grepContent': {
|
||||
const resolved = resolveArgsWithScope(args as GrepContentParams, 'path');
|
||||
return runtime.grepContent(resolved as never);
|
||||
}
|
||||
|
||||
case 'globFiles': {
|
||||
const p = args as GlobFilesParams;
|
||||
// Honor both `scope` (current manifest) and the `cwd` legacy alias.
|
||||
return runtime.globFiles({ directory: p.scope ?? p.cwd, pattern: p.pattern });
|
||||
}
|
||||
|
||||
case 'runCommand': {
|
||||
// ComputerRuntime's RunCommandState reads `args.background`; the manifest
|
||||
// exposes `run_in_background`. Without this normalize the state would
|
||||
// always show foreground even for background commands.
|
||||
const p = args as RunCommandParams;
|
||||
return runtime.runCommand({ ...p, background: p.run_in_background } as never);
|
||||
}
|
||||
|
||||
case 'getCommandOutput': {
|
||||
// Forward `timeout` (gateway per-call budget, injected into args by
|
||||
// executeToolCall) so polling a running command honors it instead of the
|
||||
// service's default wait. The runtime carries it through to getOutput.
|
||||
const p = args as GetCommandOutputParams;
|
||||
return runtime.getCommandOutput({
|
||||
commandId: p.shell_id,
|
||||
filter: p.filter,
|
||||
timeout: p.timeout,
|
||||
} as never);
|
||||
}
|
||||
|
||||
case 'killCommand': {
|
||||
const p = args as KillCommandParams;
|
||||
return runtime.killCommand({ commandId: p.shell_id });
|
||||
}
|
||||
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
"paths": {
|
||||
"@lobechat/device-gateway-client": ["../../packages/device-gateway-client/src"],
|
||||
"@lobechat/local-file-shell": ["../../packages/local-file-shell/src"],
|
||||
"@lobechat/tool-runtime": ["../../packages/tool-runtime/src"],
|
||||
"@/*": ["../../src/*"]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -17,6 +17,10 @@ export default defineConfig({
|
||||
find: '@lobechat/file-loaders',
|
||||
replacement: path.resolve(__dirname, '../../packages/file-loaders/src/index.ts'),
|
||||
},
|
||||
{
|
||||
find: '@lobechat/tool-runtime',
|
||||
replacement: path.resolve(__dirname, '../../packages/tool-runtime/src/index.ts'),
|
||||
},
|
||||
],
|
||||
},
|
||||
test: {
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
"@lobechat/file-loaders": "workspace:*",
|
||||
"@lobechat/heterogeneous-agents": "workspace:*",
|
||||
"@lobechat/local-file-shell": "workspace:*",
|
||||
"@lobechat/tool-runtime": "workspace:*",
|
||||
"@lobehub/i18n-cli": "^1.25.1",
|
||||
"@modelcontextprotocol/sdk": "^1.24.3",
|
||||
"@t3-oss/env-core": "^0.13.8",
|
||||
@@ -110,7 +111,7 @@
|
||||
"undici": "^7.16.0",
|
||||
"uuid": "^14.0.0",
|
||||
"vite": "8.0.14",
|
||||
"vitest": "^3.2.4",
|
||||
"vitest": "3.2.4",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
@@ -125,7 +126,8 @@
|
||||
],
|
||||
"overrides": {
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4"
|
||||
"react-dom": "19.2.4",
|
||||
"vitest": "3.2.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ packages:
|
||||
- '../../packages/device-gateway-client'
|
||||
- '../../packages/device-identity'
|
||||
- '../../packages/local-file-shell'
|
||||
- '../../packages/tool-runtime'
|
||||
- '../../packages/prompts'
|
||||
- './stubs/business-const'
|
||||
- './stubs/types'
|
||||
- '.'
|
||||
|
||||
@@ -4,7 +4,23 @@ import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import type { AgentRunRequestMessage } from '@lobechat/device-gateway-client';
|
||||
import type { GatewayConnectionStatus } from '@lobechat/electron-client-ipc';
|
||||
import type {
|
||||
EditLocalFileParams,
|
||||
GatewayConnectionStatus,
|
||||
GetCommandOutputParams,
|
||||
GlobFilesParams,
|
||||
GrepContentParams,
|
||||
KillCommandParams,
|
||||
ListLocalFileParams,
|
||||
LocalReadFileParams,
|
||||
LocalReadFilesParams,
|
||||
LocalSearchFilesParams,
|
||||
MoveLocalFilesParams,
|
||||
RenameLocalFileParams,
|
||||
RunCommandParams,
|
||||
WriteLocalFileParams,
|
||||
} from '@lobechat/electron-client-ipc';
|
||||
import { type ILocalSystemService, LocalSystemExecutionRuntime } from '@lobechat/tool-runtime';
|
||||
|
||||
import GatewayConnectionService from '@/services/gatewayConnectionSrv';
|
||||
import ImessageBridgeService from '@/services/imessageBridgeSrv';
|
||||
@@ -55,8 +71,62 @@ interface PlatformTaskEntry {
|
||||
topicId: string;
|
||||
}
|
||||
|
||||
type ToolCallHandler = () => Promise<unknown>;
|
||||
type ToolCallHandlerMap = Record<string, ToolCallHandler>;
|
||||
/**
|
||||
* Local mirror of `@lobechat/types`' `BuiltinServerRuntimeOutput`. Inlined
|
||||
* because the desktop tsconfig doesn't expose `@lobechat/types`, and the shape
|
||||
* is tiny + stable.
|
||||
*/
|
||||
interface BuiltinServerRuntimeOutput {
|
||||
content: string;
|
||||
error?: unknown;
|
||||
state?: unknown;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy API name aliases used by older gateway versions. Normalized to the
|
||||
* current `LocalSystemApiEnum` names before dispatch. `renameLocalFile` is
|
||||
* intentionally absent — it has no equivalent on the new surface and is
|
||||
* handled by a dedicated branch below.
|
||||
*/
|
||||
const LEGACY_API_ALIASES: Record<string, string> = {
|
||||
editLocalFile: 'editFile',
|
||||
globLocalFiles: 'globFiles',
|
||||
listLocalFiles: 'listFiles',
|
||||
moveLocalFiles: 'moveFiles',
|
||||
readLocalFile: 'readFile',
|
||||
searchLocalFiles: 'searchFiles',
|
||||
writeLocalFile: 'writeFile',
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse a JSON string, returning `undefined` on failure. Used to surface the
|
||||
* structured shape of platform-agent tool results (which return pre-stringified
|
||||
* JSON) as `state` for the renderer, without crashing on malformed input.
|
||||
*/
|
||||
const safeJsonParse = (input: string): unknown => {
|
||||
try {
|
||||
return JSON.parse(input);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve a relative path against a scope (CWD). Mirrors the renderer-side
|
||||
* `resolveArgsWithScope` helper in `@lobechat/builtin-tool-local-system` — kept
|
||||
* here as a small inline copy to avoid pulling the renderer-side `./client`
|
||||
* subpath (which transitively requires React + antd) into the main process.
|
||||
*/
|
||||
const resolveArgsWithScope = <T extends { scope?: string }>(args: T, pathField: string): T => {
|
||||
const scope = args.scope;
|
||||
const bag = args as Record<PropertyKey, unknown>;
|
||||
const currentPath = typeof bag[pathField] === 'string' ? (bag[pathField] as string) : undefined;
|
||||
if (!scope) return args;
|
||||
if (!currentPath) return { ...args, [pathField]: scope };
|
||||
if (path.isAbsolute(currentPath)) return args;
|
||||
return { ...args, [pathField]: path.join(scope, currentPath) };
|
||||
};
|
||||
|
||||
/**
|
||||
* GatewayConnectionCtr
|
||||
@@ -72,6 +142,8 @@ export default class GatewayConnectionCtr extends ControllerModule {
|
||||
/** Maps topicId → hermes session_id for multi-turn conversation continuity. */
|
||||
private readonly hermesSessionMap = new Map<string, string>();
|
||||
|
||||
private localSystemRuntime: LocalSystemExecutionRuntime | null = null;
|
||||
|
||||
// ─── Service Accessor ───
|
||||
|
||||
private get service() {
|
||||
@@ -219,21 +291,208 @@ export default class GatewayConnectionCtr extends ControllerModule {
|
||||
|
||||
// ─── Tool Call Routing ───
|
||||
|
||||
private async executeToolCall(apiName: string, args: any): Promise<unknown> {
|
||||
const methodMap = {
|
||||
...this.getLocalFileToolHandlers(args),
|
||||
...this.getShellCommandToolHandlers(args),
|
||||
...this.getPlatformAgentToolHandlers(args),
|
||||
} satisfies ToolCallHandlerMap;
|
||||
|
||||
const handler = methodMap[apiName];
|
||||
if (!handler) {
|
||||
throw new Error(
|
||||
`Tool "${apiName}" is not available on this device. It may not be supported in the current desktop version. Please skip this tool and try alternative approaches.`,
|
||||
);
|
||||
/**
|
||||
* Lazy-construct the LocalSystemExecutionRuntime backed by a thin service
|
||||
* adapter over the existing controllers. The runtime is the same one the
|
||||
* renderer uses, so remote tool calls produce identical
|
||||
* `{ content, state, success }` envelopes — `content` is the LLM-facing
|
||||
* prompt text, `state` is the structured payload, both flow downstream
|
||||
* intact (the gateway / DeviceProxy / RuntimeExecutors paths preserve them
|
||||
* and write `state` to the tool message's `pluginState`).
|
||||
*/
|
||||
private getLocalSystemRuntime(): LocalSystemExecutionRuntime {
|
||||
if (!this.localSystemRuntime) {
|
||||
const local = this.localFileCtr;
|
||||
const shell = this.shellCommandCtr;
|
||||
const service: ILocalSystemService = {
|
||||
editLocalFile: (p) => local.handleEditFile(p),
|
||||
getCommandOutput: (p) => shell.handleGetCommandOutput(p),
|
||||
globFiles: (p) => local.handleGlobFiles(p),
|
||||
grepContent: (p) => local.handleGrepContent(p),
|
||||
killCommand: (p) => shell.handleKillCommand(p),
|
||||
listLocalFiles: (p) => local.listLocalFiles(p),
|
||||
moveLocalFiles: (p) => local.handleMoveFiles(p),
|
||||
readLocalFile: (p) => local.readFile(p),
|
||||
readLocalFiles: (p) => local.readFiles(p),
|
||||
renameLocalFile: (p) => local.handleRenameFile(p),
|
||||
runCommand: (p) => shell.handleRunCommand(p),
|
||||
searchLocalFiles: (p) => local.handleLocalFilesSearch(p),
|
||||
writeFile: (p) => local.handleWriteFile(p),
|
||||
};
|
||||
this.localSystemRuntime = new LocalSystemExecutionRuntime(service);
|
||||
}
|
||||
return this.localSystemRuntime;
|
||||
}
|
||||
|
||||
return handler();
|
||||
private async executeToolCall(
|
||||
apiName: string,
|
||||
args: unknown,
|
||||
): Promise<BuiltinServerRuntimeOutput> {
|
||||
const runtime = this.getLocalSystemRuntime();
|
||||
const normalized = LEGACY_API_ALIASES[apiName] ?? apiName;
|
||||
|
||||
// Each case narrows `args` to its IPC param type — the manifest guarantees
|
||||
// the gateway sends params matching the apiName. The `as never` casts on
|
||||
// runtime calls are legitimate widenings: the runtime's typed signatures
|
||||
// (e.g. `ListFilesParams`) are narrower than what the IPC layer accepts
|
||||
// (`limit`, `run_in_background`, etc.), and the same casts exist in the
|
||||
// renderer-side `LocalSystemExecutor`.
|
||||
switch (normalized) {
|
||||
case 'listFiles': {
|
||||
const p = args as ListLocalFileParams;
|
||||
return runtime.listFiles({
|
||||
directoryPath: p.path,
|
||||
limit: p.limit,
|
||||
sortBy: p.sortBy,
|
||||
sortOrder: p.sortOrder,
|
||||
} as never);
|
||||
}
|
||||
|
||||
case 'readFile': {
|
||||
const p = args as LocalReadFileParams;
|
||||
return runtime.readFile({
|
||||
endLine: p.loc?.[1],
|
||||
path: p.path,
|
||||
startLine: p.loc?.[0],
|
||||
});
|
||||
}
|
||||
|
||||
case 'readFiles': {
|
||||
return runtime.readFiles(args as LocalReadFilesParams);
|
||||
}
|
||||
|
||||
case 'searchFiles': {
|
||||
const resolved = resolveArgsWithScope(args as LocalSearchFilesParams, 'directory');
|
||||
return runtime.searchFiles({
|
||||
...resolved,
|
||||
directory: resolved.directory || '',
|
||||
});
|
||||
}
|
||||
|
||||
case 'moveFiles': {
|
||||
const p = args as MoveLocalFilesParams;
|
||||
return runtime.moveFiles({
|
||||
operations: p.items?.map((item) => ({
|
||||
destination: item.newPath,
|
||||
source: item.oldPath,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
case 'writeFile': {
|
||||
return runtime.writeFile(args as WriteLocalFileParams);
|
||||
}
|
||||
|
||||
case 'editFile': {
|
||||
const p = args as EditLocalFileParams;
|
||||
return runtime.editFile({
|
||||
all: p.replace_all,
|
||||
path: p.file_path,
|
||||
replace: p.new_string,
|
||||
search: p.old_string,
|
||||
});
|
||||
}
|
||||
|
||||
case 'runCommand': {
|
||||
// ComputerRuntime's RunCommandState reads `args.background`; the manifest
|
||||
// exposes `run_in_background`. Without this normalize the state would
|
||||
// always show foreground even for background commands.
|
||||
const p = args as RunCommandParams;
|
||||
return runtime.runCommand({
|
||||
...p,
|
||||
background: p.run_in_background,
|
||||
} as never);
|
||||
}
|
||||
|
||||
case 'getCommandOutput': {
|
||||
const p = args as GetCommandOutputParams;
|
||||
return runtime.getCommandOutput({
|
||||
commandId: p.shell_id,
|
||||
filter: p.filter,
|
||||
} as never);
|
||||
}
|
||||
|
||||
case 'killCommand': {
|
||||
const p = args as KillCommandParams;
|
||||
return runtime.killCommand({
|
||||
commandId: p.shell_id,
|
||||
});
|
||||
}
|
||||
|
||||
case 'grepContent': {
|
||||
const resolved = resolveArgsWithScope(args as GrepContentParams, 'path');
|
||||
return runtime.grepContent(resolved as never);
|
||||
}
|
||||
|
||||
case 'globFiles': {
|
||||
const p = args as GlobFilesParams;
|
||||
return runtime.globFiles({
|
||||
directory: p.scope,
|
||||
pattern: p.pattern,
|
||||
});
|
||||
}
|
||||
|
||||
case 'renameLocalFile': {
|
||||
// ComputerRuntime has no public rename method — new surface uses
|
||||
// `moveFiles`. Legacy gateway versions may still emit this name, so we
|
||||
// call the IPC handler directly and wrap the raw result into the
|
||||
// BuiltinServerRuntimeOutput shape so `state` still flows downstream.
|
||||
const raw = await this.localFileCtr.handleRenameFile(args as RenameLocalFileParams);
|
||||
return {
|
||||
content: raw.success
|
||||
? `Renamed to ${raw.newPath}`
|
||||
: `Rename failed: ${raw.error ?? 'unknown error'}`,
|
||||
state: raw,
|
||||
success: raw.success,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Platform agent tools (openclaw / hermes) ───
|
||||
// These don't go through LocalSystemExecutionRuntime — they return raw
|
||||
// domain payloads that we envelope into BuiltinServerRuntimeOutput here.
|
||||
// `content` is the JSON-serialized payload (what the LLM reads); `state`
|
||||
// carries the parsed object so the renderer can render structured UI.
|
||||
|
||||
case 'checkPlatformCapability': {
|
||||
const result = await this.checkPlatformCapability(args as { platform: string });
|
||||
return { content: JSON.stringify(result), state: result, success: true };
|
||||
}
|
||||
|
||||
case 'getAgentProfile': {
|
||||
const result = await this.getAgentProfile(
|
||||
args as { agentId?: string; platform: string },
|
||||
);
|
||||
return { content: JSON.stringify(result), state: result, success: true };
|
||||
}
|
||||
|
||||
case 'runHeteroTask': {
|
||||
// runHeteroTask returns a pre-stringified JSON payload — pass it through
|
||||
// as `content` and surface the parsed shape as `state`.
|
||||
const json = await this.runHeteroTask(
|
||||
args as {
|
||||
agentId?: string;
|
||||
agentType: string;
|
||||
cwd?: string;
|
||||
operationId: string;
|
||||
prompt: string;
|
||||
taskId: string;
|
||||
topicId: string;
|
||||
},
|
||||
);
|
||||
return { content: json, state: safeJsonParse(json), success: true };
|
||||
}
|
||||
|
||||
case 'cancelHeteroTask': {
|
||||
const json = await this.cancelHeteroTask(args as { signal?: string; taskId: string });
|
||||
return { content: json, state: safeJsonParse(json), success: true };
|
||||
}
|
||||
|
||||
default: {
|
||||
throw new Error(
|
||||
`Tool "${apiName}" is not available on this device. It may not be supported in the current desktop version. Please skip this tool and try alternative approaches.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async executeMessageApi(
|
||||
@@ -250,59 +509,6 @@ export default class GatewayConnectionCtr extends ControllerModule {
|
||||
);
|
||||
}
|
||||
|
||||
private getLocalFileToolHandlers(args: any): ToolCallHandlerMap {
|
||||
const editFile = () => this.localFileCtr.handleEditFile(args);
|
||||
const globFiles = () => this.localFileCtr.handleGlobFiles(args);
|
||||
const listFiles = () => this.localFileCtr.listLocalFiles(args);
|
||||
const moveFiles = () => this.localFileCtr.handleMoveFiles(args);
|
||||
const readFile = () => this.localFileCtr.readFile(args);
|
||||
const searchFiles = () => this.localFileCtr.handleLocalFilesSearch(args);
|
||||
const writeFile = () => this.localFileCtr.handleWriteFile(args);
|
||||
|
||||
return {
|
||||
editFile,
|
||||
globFiles,
|
||||
grepContent: () => this.localFileCtr.handleGrepContent(args),
|
||||
listFiles,
|
||||
moveFiles,
|
||||
readFile,
|
||||
searchFiles,
|
||||
writeFile,
|
||||
|
||||
// Legacy aliases — keep these so older Gateway versions sending the long
|
||||
// names continue to route correctly. `renameLocalFile` is also kept even
|
||||
// though the new surface drops rename (it's now handled by `moveFiles`).
|
||||
editLocalFile: editFile,
|
||||
globLocalFiles: globFiles,
|
||||
listLocalFiles: listFiles,
|
||||
moveLocalFiles: moveFiles,
|
||||
readLocalFile: readFile,
|
||||
renameLocalFile: () => this.localFileCtr.handleRenameFile(args),
|
||||
searchLocalFiles: searchFiles,
|
||||
writeLocalFile: writeFile,
|
||||
};
|
||||
}
|
||||
|
||||
private getShellCommandToolHandlers(args: any): ToolCallHandlerMap {
|
||||
return {
|
||||
getCommandOutput: () => this.shellCommandCtr.handleGetCommandOutput(args),
|
||||
killCommand: () => this.shellCommandCtr.handleKillCommand(args),
|
||||
runCommand: () => this.shellCommandCtr.handleRunCommand(args),
|
||||
};
|
||||
}
|
||||
|
||||
private getPlatformAgentToolHandlers(args: any): ToolCallHandlerMap {
|
||||
return {
|
||||
// Platform agent capability probing
|
||||
checkPlatformCapability: () => this.checkPlatformCapability(args),
|
||||
getAgentProfile: () => this.getAgentProfile(args),
|
||||
|
||||
// Platform agent task execution (openclaw / hermes)
|
||||
cancelHeteroTask: () => this.cancelHeteroTask(args),
|
||||
runHeteroTask: () => this.runHeteroTask(args),
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Platform Capability Probing ───
|
||||
|
||||
private async checkPlatformCapability(args: {
|
||||
|
||||
@@ -526,15 +526,18 @@ describe('GatewayConnectionCtr', () => {
|
||||
['renameLocalFile', 'handleRenameFile', mockLocalFileCtr],
|
||||
] as const)('should route %s to %s', async (apiName, methodName, controller) => {
|
||||
const client = await connectAndOpen();
|
||||
const args = { test: 'arg' };
|
||||
|
||||
client.simulateToolCallRequest(apiName, args);
|
||||
// Each tool's args are domain-shaped (path, file_path, items, etc.).
|
||||
// The runtime denormalizes them before calling the controller, so this
|
||||
// test only asserts that the *right* controller method runs — see the
|
||||
// envelope-shape test below for end-to-end content/state coverage.
|
||||
client.simulateToolCallRequest(apiName, { test: 'arg' });
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
expect((controller as any)[methodName]).toHaveBeenCalledWith(args);
|
||||
expect((controller as any)[methodName]).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should send tool_call_response with success result', async () => {
|
||||
it('should send tool_call_response with content + state envelope on success', async () => {
|
||||
vi.mocked(mockLocalFileCtr.readFile).mockResolvedValueOnce({
|
||||
charCount: 5,
|
||||
content: 'hello',
|
||||
@@ -552,23 +555,20 @@ describe('GatewayConnectionCtr', () => {
|
||||
client.simulateToolCallRequest('readFile', { path: '/a.txt' }, 'req-42');
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
expect(client.sendToolCallResponse).toHaveBeenCalledWith({
|
||||
requestId: 'req-42',
|
||||
result: {
|
||||
content: JSON.stringify({
|
||||
charCount: 5,
|
||||
content: 'hello',
|
||||
createdTime: new Date('2024-01-01'),
|
||||
filename: 'a.txt',
|
||||
fileType: '.txt',
|
||||
lineCount: 1,
|
||||
loc: [1, 1],
|
||||
modifiedTime: new Date('2024-01-01'),
|
||||
totalCharCount: 5,
|
||||
totalLineCount: 1,
|
||||
}),
|
||||
success: true,
|
||||
},
|
||||
// The runtime produces a formatted prompt string for `content` and a
|
||||
// structured snapshot for `state`. We only assert envelope shape here
|
||||
// — the exact prompt format is owned by the runtime/prompts packages.
|
||||
expect(client.sendToolCallResponse).toHaveBeenCalledTimes(1);
|
||||
const response = client.sendToolCallResponse.mock.calls[0][0];
|
||||
expect(response.requestId).toBe('req-42');
|
||||
expect(response.result.success).toBe(true);
|
||||
expect(typeof response.result.content).toBe('string');
|
||||
expect(response.result.content.length).toBeGreaterThan(0);
|
||||
expect(response.result.content).toContain('hello');
|
||||
expect(response.result.state).toMatchObject({
|
||||
content: 'hello',
|
||||
filename: 'a.txt',
|
||||
path: '/a.txt',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -976,6 +976,7 @@ describe('GatewayConnectionCtr', () => {
|
||||
requestId: 'req-cap',
|
||||
result: {
|
||||
content: JSON.stringify({ available: true, version: 'openclaw 1.2.3' }),
|
||||
state: { available: true, version: 'openclaw 1.2.3' },
|
||||
success: true,
|
||||
},
|
||||
});
|
||||
@@ -1000,6 +1001,7 @@ describe('GatewayConnectionCtr', () => {
|
||||
requestId: 'req-cap-nover',
|
||||
result: {
|
||||
content: JSON.stringify({ available: true }),
|
||||
state: { available: true },
|
||||
success: true,
|
||||
},
|
||||
});
|
||||
@@ -1025,6 +1027,10 @@ describe('GatewayConnectionCtr', () => {
|
||||
available: false,
|
||||
reason: 'openclaw is not installed on this device',
|
||||
}),
|
||||
state: {
|
||||
available: false,
|
||||
reason: 'openclaw is not installed on this device',
|
||||
},
|
||||
success: true,
|
||||
},
|
||||
});
|
||||
@@ -1043,6 +1049,7 @@ describe('GatewayConnectionCtr', () => {
|
||||
requestId: 'req-unknown-plat',
|
||||
result: {
|
||||
content: JSON.stringify({ available: false, reason: 'Unknown platform: unknownBot' }),
|
||||
state: { available: false, reason: 'Unknown platform: unknownBot' },
|
||||
success: true,
|
||||
},
|
||||
});
|
||||
@@ -1057,6 +1064,7 @@ describe('GatewayConnectionCtr', () => {
|
||||
requestId: 'req-profile',
|
||||
result: {
|
||||
content: JSON.stringify({}),
|
||||
state: {},
|
||||
success: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -181,5 +181,46 @@ describe('cliAgentDetectors', () => {
|
||||
expect(execMock).not.toHaveBeenCalled();
|
||||
expect(execFileMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('falls back to the login shell PATH for tools installed by shell setup', async () => {
|
||||
const originalPath = process.env.PATH;
|
||||
const originalShell = process.env.SHELL;
|
||||
process.env.PATH = '/usr/bin:/bin';
|
||||
process.env.SHELL = '/bin/zsh';
|
||||
|
||||
try {
|
||||
callExecFileError(new Error('not found'));
|
||||
callExecFile('/opt/homebrew/bin:/Users/Hanam/.local/share/mise/shims:/usr/bin:/bin');
|
||||
callExecFile('/Users/Hanam/.local/share/mise/shims/gemini\n');
|
||||
callExecFile('gemini 0.2.0');
|
||||
|
||||
const { geminiCliDetector } = await import('../cliAgentDetectors');
|
||||
const status = await geminiCliDetector.detect();
|
||||
|
||||
expect(status.available).toBe(true);
|
||||
expect(status.path).toBe('/Users/Hanam/.local/share/mise/shims/gemini');
|
||||
expect(status.version).toBe('gemini 0.2.0');
|
||||
|
||||
expect(execFileMock).toHaveBeenCalledTimes(4);
|
||||
expect(execFileMock.mock.calls[0]![0]).toBe('which');
|
||||
expect(execFileMock.mock.calls[1]![0]).toBe('/bin/zsh');
|
||||
expect(execFileMock.mock.calls[1]![1]).toEqual(['-ilc', 'printf "%s" "$PATH"']);
|
||||
expect(execFileMock.mock.calls[2]![0]).toBe('which');
|
||||
expect(execFileMock.mock.calls[2]![2]).toMatchObject({
|
||||
env: {
|
||||
PATH: '/opt/homebrew/bin:/Users/Hanam/.local/share/mise/shims:/usr/bin:/bin',
|
||||
},
|
||||
});
|
||||
expect(execFileMock.mock.calls[3]![0]).toBe('/Users/Hanam/.local/share/mise/shims/gemini');
|
||||
expect(execFileMock.mock.calls[3]![2]).toMatchObject({
|
||||
env: {
|
||||
PATH: '/opt/homebrew/bin:/Users/Hanam/.local/share/mise/shims:/usr/bin:/bin',
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
process.env.PATH = originalPath;
|
||||
process.env.SHELL = originalShell;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,7 +19,13 @@ interface ValidatedDetectorOptions {
|
||||
validateKeywords: string[];
|
||||
}
|
||||
|
||||
interface ResolvedCommand {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
path: string;
|
||||
}
|
||||
|
||||
const isWindows = () => platform() === 'win32';
|
||||
let shellPathPromise: Promise<string | undefined> | undefined;
|
||||
|
||||
// Reject anything that could break out of the `cmd /c "<path>" --version`
|
||||
// shell line we build for Windows .cmd shims (see `detectValidatedCommand`).
|
||||
@@ -40,36 +46,109 @@ const pickWindowsRunnable = (lines: string[]): string | undefined => {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const resolveCommandPath = async (command: string): Promise<string | undefined> => {
|
||||
const trimmedCommand = command.trim();
|
||||
if (!trimmedCommand) return;
|
||||
const getLoginShellPath = async (): Promise<string | undefined> => {
|
||||
if (isWindows()) return undefined;
|
||||
|
||||
if (path.isAbsolute(trimmedCommand) || trimmedCommand.includes(path.sep)) {
|
||||
return trimmedCommand;
|
||||
}
|
||||
|
||||
const whichCommand = isWindows() ? 'where' : 'which';
|
||||
const shell = process.env.SHELL;
|
||||
if (!shell || !path.isAbsolute(shell)) return undefined;
|
||||
|
||||
try {
|
||||
const { stdout } = await execFilePromise(whichCommand, [trimmedCommand], { timeout: 3000 });
|
||||
const { stdout } = await execFilePromise(shell, ['-ilc', 'printf "%s" "$PATH"'], {
|
||||
timeout: 3000,
|
||||
windowsHide: true,
|
||||
});
|
||||
|
||||
return stdout
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.reverse()
|
||||
.find((line) => line.includes(path.delimiter));
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const getCachedLoginShellPath = async (): Promise<string | undefined> => {
|
||||
shellPathPromise ??= getLoginShellPath();
|
||||
return shellPathPromise;
|
||||
};
|
||||
|
||||
const mergePathValues = (...values: Array<string | undefined>): string | undefined => {
|
||||
const seen = new Set<string>();
|
||||
const segments = values
|
||||
.flatMap((value) => value?.split(path.delimiter) ?? [])
|
||||
.map((segment) => segment.trim())
|
||||
.filter((segment) => {
|
||||
if (!segment || seen.has(segment)) return false;
|
||||
seen.add(segment);
|
||||
return true;
|
||||
});
|
||||
|
||||
return segments.length > 0 ? segments.join(path.delimiter) : undefined;
|
||||
};
|
||||
|
||||
const getCommandPathLines = async (
|
||||
whichCommand: 'where' | 'which',
|
||||
command: string,
|
||||
env?: NodeJS.ProcessEnv,
|
||||
): Promise<string[] | undefined> => {
|
||||
try {
|
||||
const { stdout } = await execFilePromise(whichCommand, [command], {
|
||||
env,
|
||||
timeout: 3000,
|
||||
windowsHide: true,
|
||||
});
|
||||
const lines = stdout
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
if (lines.length === 0) return undefined;
|
||||
|
||||
// Windows `where` lists every PATHEXT match (e.g. for `codex` npm ships
|
||||
// a Unix shell wrapper alongside `codex.cmd` and `codex.ps1`). Picking
|
||||
// the first line can land us on something we can't execute, so prefer a
|
||||
// runnable extension and bail otherwise.
|
||||
if (isWindows()) return pickWindowsRunnable(lines);
|
||||
|
||||
return lines[0];
|
||||
return lines.length > 0 ? lines : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const resolveCommandPath = async (command: string): Promise<ResolvedCommand | undefined> => {
|
||||
const trimmedCommand = command.trim();
|
||||
if (!trimmedCommand) return;
|
||||
|
||||
if (path.isAbsolute(trimmedCommand) || trimmedCommand.includes(path.sep)) {
|
||||
return { path: trimmedCommand };
|
||||
}
|
||||
|
||||
const whichCommand = isWindows() ? 'where' : 'which';
|
||||
let lines = await getCommandPathLines(whichCommand, trimmedCommand);
|
||||
let lookupEnv: NodeJS.ProcessEnv | undefined;
|
||||
|
||||
if (!lines && !isWindows()) {
|
||||
const shellPath = await getCachedLoginShellPath();
|
||||
const lookupPath = mergePathValues(shellPath, process.env.PATH);
|
||||
|
||||
if (lookupPath && lookupPath !== process.env.PATH) {
|
||||
const fallbackEnv = {
|
||||
...process.env,
|
||||
PATH: lookupPath,
|
||||
};
|
||||
lines = await getCommandPathLines(whichCommand, trimmedCommand, fallbackEnv);
|
||||
if (lines) lookupEnv = fallbackEnv;
|
||||
}
|
||||
}
|
||||
|
||||
if (!lines) return undefined;
|
||||
|
||||
// Windows `where` lists every PATHEXT match (e.g. for `codex` npm ships
|
||||
// a Unix shell wrapper alongside `codex.cmd` and `codex.ps1`). Picking
|
||||
// the first line can land us on something we can't execute, so prefer a
|
||||
// runnable extension and bail otherwise.
|
||||
if (isWindows()) {
|
||||
const runnablePath = pickWindowsRunnable(lines);
|
||||
return runnablePath ? { path: runnablePath } : undefined;
|
||||
}
|
||||
|
||||
return { env: lookupEnv, path: lines[0] };
|
||||
};
|
||||
|
||||
const detectValidatedCommand = async (
|
||||
command: string,
|
||||
options: Pick<ValidatedDetectorOptions, 'validateFlag' | 'validateKeywords'>,
|
||||
@@ -83,17 +162,21 @@ const detectValidatedCommand = async (
|
||||
// Resolve via where/which BEFORE invoking. On Windows this is what discovers
|
||||
// npm-installed shims like `claude.cmd` under %APPDATA%\npm — `execFile`
|
||||
// alone won't apply PATHEXT and can't run .cmd files directly.
|
||||
const resolvedPath = await resolveCommandPath(trimmedCommand);
|
||||
if (!resolvedPath) return { available: false };
|
||||
const resolvedCommand = await resolveCommandPath(trimmedCommand);
|
||||
if (!resolvedCommand) return { available: false };
|
||||
|
||||
const { env, path: resolvedPath } = resolvedCommand;
|
||||
|
||||
try {
|
||||
const needsShell = isWindows() && /\.(?:cmd|bat)$/i.test(resolvedPath);
|
||||
const { stderr, stdout } = needsShell
|
||||
? await execPromise(`"${resolvedPath}" ${validateFlag}`, {
|
||||
env,
|
||||
timeout: 5000,
|
||||
windowsHide: true,
|
||||
})
|
||||
: await execFilePromise(resolvedPath, [validateFlag], {
|
||||
env,
|
||||
timeout: 5000,
|
||||
windowsHide: true,
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
MessageApiRequestMessage,
|
||||
SystemInfoRequestMessage,
|
||||
ToolCallRequestMessage,
|
||||
ToolCallResponseMessage,
|
||||
} from '@lobechat/device-gateway-client';
|
||||
import { GatewayClient } from '@lobechat/device-gateway-client';
|
||||
import type { IdentitySource } from '@lobechat/device-identity';
|
||||
@@ -22,14 +23,46 @@ const logger = createLogger('services:GatewayConnectionSrv');
|
||||
|
||||
const DEFAULT_GATEWAY_URL = 'https://device-gateway.lobehub.com';
|
||||
|
||||
interface ToolCallHandler {
|
||||
(apiName: string, args: any): Promise<unknown>;
|
||||
/**
|
||||
* Result envelope a tool-call handler must return. Mirrors
|
||||
* `BuiltinServerRuntimeOutput` so the renderer-side and remote-device paths
|
||||
* stay symmetric: `content` is the LLM-facing prompt text; `state` carries the
|
||||
* structured payload that downstream persists into `pluginState`.
|
||||
*/
|
||||
interface ToolCallResult {
|
||||
content: string;
|
||||
error?: unknown;
|
||||
state?: unknown;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
interface MessageApiHandler {
|
||||
(platform: string, apiName: string, payload: Record<string, unknown>): Promise<unknown>;
|
||||
}
|
||||
|
||||
interface ToolCallHandler {
|
||||
(apiName: string, args: unknown): Promise<ToolCallResult>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Coerce a runtime error (which may be an Error, string, or `{ message }`
|
||||
* object) into the string shape the wire protocol expects. Returns undefined
|
||||
* when there's no error to transmit.
|
||||
*/
|
||||
const serializeWireError = (err: unknown): string | undefined => {
|
||||
if (err === undefined || err === null) return undefined;
|
||||
if (typeof err === 'string') return err;
|
||||
if (err instanceof Error) return err.message;
|
||||
if (typeof err === 'object' && 'message' in err && typeof err.message === 'string') {
|
||||
return err.message;
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(err);
|
||||
} catch {
|
||||
return String(err);
|
||||
}
|
||||
};
|
||||
|
||||
interface AgentRunHandler {
|
||||
(request: AgentRunRequestMessage): Promise<{ reason?: string; status: 'accepted' | 'rejected' }>;
|
||||
}
|
||||
@@ -387,13 +420,21 @@ export default class GatewayConnectionService extends ServiceModule {
|
||||
const args = JSON.parse(argsStr);
|
||||
const result = await this.toolCallHandler(apiName, args);
|
||||
|
||||
client.sendToolCallResponse({
|
||||
requestId,
|
||||
result: {
|
||||
content: typeof result === 'string' ? result : JSON.stringify(result),
|
||||
success: true,
|
||||
},
|
||||
});
|
||||
// Forward the typed envelope unchanged. Critically, do NOT stringify the
|
||||
// whole result into `content` — that would bury the structured payload
|
||||
// inside a JSON blob and lose `state`. The wire protocol carries each
|
||||
// field separately so downstream (`DeviceProxy` → `RuntimeExecutors`)
|
||||
// can persist `state` to `pluginState`. Optional fields are only set
|
||||
// when present so payloads stay minimal.
|
||||
const wireResult: ToolCallResponseMessage['result'] = {
|
||||
content: result.content,
|
||||
success: result.success,
|
||||
};
|
||||
const wireError = serializeWireError(result.error);
|
||||
if (wireError !== undefined) wireResult.error = wireError;
|
||||
if (result.state !== undefined) wireResult.state = result.state;
|
||||
|
||||
client.sendToolCallResponse({ requestId, result: wireResult });
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
logger.error(`Tool call failed: apiName=${apiName}, error=${errorMsg}`);
|
||||
|
||||
@@ -17,3 +17,16 @@ export interface DesktopHotkeyItem {
|
||||
}
|
||||
|
||||
export type DesktopHotkeyConfig = Record<DesktopHotkeyId, string>;
|
||||
|
||||
/**
|
||||
* Mirror of `@lobechat/types`' `BuiltinServerRuntimeOutput`. Reached by
|
||||
* `@lobechat/tool-runtime` (the runtime the gateway controller reuses) via
|
||||
* `import type`, so only the shape is needed. Keep in sync with
|
||||
* `packages/types/src/tool/builtin.ts`.
|
||||
*/
|
||||
export interface BuiltinServerRuntimeOutput {
|
||||
content: string;
|
||||
error?: unknown;
|
||||
state?: unknown;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
[
|
||||
{
|
||||
"children": {},
|
||||
"date": "2026-05-29",
|
||||
"version": "2.2.1"
|
||||
},
|
||||
{
|
||||
"children": {},
|
||||
"date": "2026-05-18",
|
||||
|
||||
@@ -472,5 +472,6 @@
|
||||
"https://github.com/user-attachments/assets/facdc83c-e789-4649-8060-7f7a10a1b1dd": "/blog/assets05b20e40c03ced0ec8707fed2e8e0f25.webp",
|
||||
"https://github.com/user-attachments/assets/fcdfb9c5-819a-488f-b28d-0857fe861219": "/blog/assets8477415ecec1f37e38ab38ff1217d0a7.webp",
|
||||
"https://github.com/user-attachments/assets/fd60ab55-ead2-4930-ad00-fdf77662f5a0": "/blog/assets276a4e8748e9bd300b30dcd9d0e24980.webp",
|
||||
"https://file.rene.wang/task.png": "/blog/assets4aa1732a45832afc780600e6e329860c.webp"
|
||||
"https://file.rene.wang/task.png": "/blog/assets4aa1732a45832afc780600e6e329860c.webp",
|
||||
"https://file.rene.wang/Platform Agent.png": "/blog/assets10cadd434aeb36bd1beb3c7b3d371fbd.webp"
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
---
|
||||
title: Introducing CAO — Your Chief Agent Operator
|
||||
description: >-
|
||||
Meet CAO: agents that review their own work, recruit teammates when they need help, and only stop to ask you when it really matters.
|
||||
Meet CAO: agents that review their own work, recruit teammates when they need
|
||||
help, and only stop to ask you when it really matters.
|
||||
tags:
|
||||
- CAO
|
||||
- Agent Teams
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
title: Platform Agents & Drag-Drop Skills
|
||||
description: >-
|
||||
Run agents locally or on a remote device and drop skills straight into the
|
||||
message box. v2.2.1 is here.
|
||||
tags:
|
||||
- Platform Agents
|
||||
- Skills
|
||||
- Models
|
||||
---
|
||||
|
||||
# Platform Agents & Drag-Drop Skills
|
||||
|
||||
## Platform agents, local or remote (beta)
|
||||
|
||||
You can now create platform agents like **OpenClaw** and **Hermes** and choose, right from the composer, whether they run on your own machine or on a remote device. A new device switcher in the chat input lets you swap targets without leaving the conversation, and your registered devices are remembered so you can pick up where you left off.
|
||||
|
||||
On desktop, the recent-directories list can be reordered by drag-and-drop, and devices auto-register with a stable ID — set it once, use it everywhere.
|
||||
|
||||
> Platform agents are in beta. Head to **Settings → Advanced → Labs** and turn on the platform-agent flag to try them.
|
||||
|
||||
## Drag-and-drop skills & folders
|
||||
|
||||
The chat input got more direct. Drag a skill from the right panel into the message box and it becomes an action tag — no menu hunting. Typing `/` mid-sentence pops up a slash menu of every skill you have installed, from built-ins to ones from the Skill Market or your own agents.
|
||||
|
||||
On desktop, drag a whole folder into chat and it shows up as a `@localFile` reference instead of trying to upload every file inside it.
|
||||
|
||||
## Other improvements
|
||||
|
||||
- **Bots that send files**: Discord, Telegram, Slack, Feishu, WeChat, LINE, and QQ can now exchange images, videos, voice, and files — not just text
|
||||
- **Page Agent sharing**: share an agent's working pages with one click
|
||||
- **Document highlights**: non-markdown documents render as read-only highlighted code; you can open a thread chat inside the document preview
|
||||
- **Tasks with attachments**: drop files and images directly into a task
|
||||
- **Export an agent**: download any agent's profile as a Markdown file
|
||||
- **New models**: Claude Opus 4.8, DeepSeek V4 Flash/Pro, Gemini 3.5 Flash, Qwen 3.7 Max, intern-s2-preview, step-3.7-flash
|
||||
- **Chat cost estimates** shown alongside replies
|
||||
- **Smoother first run**: guided agent creation, and new topics aren't created until you send your first message
|
||||
- **Multi-select delete** in the agent documents explorer
|
||||
- **Follow-up suggestions** in general chats, not just agent ones
|
||||
|
||||
## Fixes & polish
|
||||
|
||||
- Input drafts persist when you switch tabs.
|
||||
- Action bar stays open while you hover the next message.
|
||||
- Copying a user message no longer leaves escaped Markdown.
|
||||
- Cmd +/−/0 shows a zoom HUD on desktop, and `~` paths expand correctly.
|
||||
- Empty replies retry instead of silently finishing; market OAuth re-login pops the right modal when a session expires.
|
||||
- Topic list pagination is preserved after creating, deleting, or moving topics.
|
||||
- File preview now covers `.cjs`, `.mjs`, and extension-less text files; Bedrock structured output and Gemini diagnostics fixes also landed.
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
title: 平台智能体与拖拽即用的技能
|
||||
description: 智能体可以在本地或远程设备上运行,技能拖一下就能进消息框。v2.2.1 上线。
|
||||
tags:
|
||||
- 平台智能体
|
||||
- 技能
|
||||
- 模型
|
||||
---
|
||||
|
||||
# 平台智能体与拖拽即用的技能
|
||||
|
||||
## 平台智能体:本地或远程(Beta)
|
||||
|
||||
你现在可以创建 **OpenClaw**、**Hermes** 这类平台智能体,并直接在输入框选择它们运行在你的本机还是某台远程设备上。聊天输入区新增了执行设备切换器,无需离开会话就能切换目标;注册过的设备会被记住,下次直接接着用。
|
||||
|
||||
桌面端的「最近目录」支持拖拽重新排序;设备会基于稳定的机器 ID 自动注册——只设置一次,到哪都能用。
|
||||
|
||||
> 平台智能体目前为 Beta。前往 **设置 → 高级 → Labs** 开启对应开关后即可体验。
|
||||
|
||||
## 拖拽即用的技能与文件夹
|
||||
|
||||
聊天输入更直接了。从右侧面板把技能拖进消息框,它会变成一枚动作标签——不用再翻菜单。在句子中间输入 `/` 也会弹出包含全部已安装技能的菜单,无论是内置的、来自技能市场的,还是你自己 Agent 提供的。
|
||||
|
||||
桌面端可以把整个文件夹拖进聊天,它会以 `@localFile` 引用形式出现,而不是把里面的每个文件都上传一遍。
|
||||
|
||||
## 其他改进
|
||||
|
||||
- **会发文件的 Bot**:Discord、Telegram、Slack、飞书、微信、LINE、QQ 现在都能收发图片、视频、语音和文件,不再仅限于文字
|
||||
- **Page Agent 共享**:一键分享智能体的工作页面
|
||||
- **文档高亮**:非 Markdown 文档以只读的代码高亮形式呈现,并可在文档预览中直接开启线程对话
|
||||
- **任务支持附件**:图片和文件可以直接挂到任务上
|
||||
- **导出智能体**:把任意智能体导出成 Markdown 文件
|
||||
- **新模型**:Claude Opus 4.8、DeepSeek V4 Flash / Pro、Gemini 3.5 Flash、Qwen 3.7 Max、intern-s2-preview、step-3.7-flash
|
||||
- **回复旁显示费用预估**
|
||||
- **更顺手的初次体验**:新建智能体有引导界面,发送第一条消息后才会真正创建话题
|
||||
- 智能体文档浏览器支持**多选删除**
|
||||
- **后续建议**现在也会出现在普通对话里,不再仅限于 Agent 对话
|
||||
|
||||
## 修复与打磨
|
||||
|
||||
- 切换标签页后,输入草稿仍会保留。
|
||||
- 鼠标悬停到下一条消息时,操作栏不会闪退。
|
||||
- 复制用户消息时不再带出转义的 Markdown 字符。
|
||||
- 桌面端 Cmd +/−/0 显示缩放 HUD;`~` 路径会被正确展开。
|
||||
- 空回复会自动重试,而不是悄悄结束;技能市场会话过期时会弹出正确的重新登录窗口。
|
||||
- 创建、删除、移动话题后,话题列表的分页状态会被保留。
|
||||
- 桌面端现在可预览 `.cjs`、`.mjs` 和无后缀文本文件;同时修复了 Bedrock 结构化输出与 Gemini 诊断相关问题。
|
||||
@@ -2,6 +2,12 @@
|
||||
"$schema": "https://github.com/lobehub/lobe-chat/blob/main/docs/changelog/schema.json",
|
||||
"cloud": [],
|
||||
"community": [
|
||||
{
|
||||
"image": "/blog/assets10cadd434aeb36bd1beb3c7b3d371fbd.webp",
|
||||
"id": "2026-05-31-drag-and-drop-skills",
|
||||
"date": "2026-05-31",
|
||||
"versionRange": ["2.2.0", "2.2.1"]
|
||||
},
|
||||
{
|
||||
"image": "https://hub-apac-1.lobeobjects.space/billboard/covers/1778838542538-MDEMAEav.png",
|
||||
"id": "2026-05-19-chief-agent-operator",
|
||||
|
||||
@@ -358,6 +358,22 @@ table agent_operations {
|
||||
}
|
||||
}
|
||||
|
||||
table agent_shares {
|
||||
id uuid [pk, not null, default: `gen_random_uuid()`]
|
||||
agent_id text [not null]
|
||||
visibility text [not null, default: 'private']
|
||||
share_config jsonb
|
||||
user_view_count integer [not null, default: 0]
|
||||
accessed_at "timestamp with time zone" [not null, default: `now()`]
|
||||
created_at "timestamp with time zone" [not null, default: `now()`]
|
||||
updated_at "timestamp with time zone" [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
agent_id [name: 'agent_shares_agent_id_unique', unique]
|
||||
visibility [name: 'agent_shares_visibility_idx']
|
||||
}
|
||||
}
|
||||
|
||||
table agent_skills {
|
||||
id text [pk, not null]
|
||||
name text [not null]
|
||||
@@ -1052,6 +1068,7 @@ table messages {
|
||||
reasoning jsonb
|
||||
search jsonb
|
||||
metadata jsonb
|
||||
usage jsonb
|
||||
model text
|
||||
provider text
|
||||
favorite boolean [default: false]
|
||||
@@ -1086,6 +1103,8 @@ table messages {
|
||||
agent_id [name: 'messages_agent_id_idx']
|
||||
group_id [name: 'messages_group_id_idx']
|
||||
message_group_id [name: 'messages_message_group_id_idx']
|
||||
() [name: 'messages_usage_cost_idx']
|
||||
() [name: 'messages_usage_total_tokens_idx']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1958,6 +1977,7 @@ table topics {
|
||||
usage jsonb
|
||||
model text
|
||||
provider text
|
||||
sender_id text
|
||||
accessed_at "timestamp with time zone" [not null, default: `now()`]
|
||||
created_at "timestamp with time zone" [not null, default: `now()`]
|
||||
updated_at "timestamp with time zone" [not null, default: `now()`]
|
||||
@@ -1974,6 +1994,7 @@ table topics {
|
||||
model [name: 'topics_model_idx']
|
||||
provider [name: 'topics_provider_idx']
|
||||
(user_id, completed_at) [name: 'topics_user_id_completed_at_idx']
|
||||
sender_id [name: 'topics_sender_id_idx']
|
||||
() [name: 'topics_extract_status_gin_idx']
|
||||
}
|
||||
}
|
||||
@@ -2267,6 +2288,74 @@ table user_memory_persona_documents {
|
||||
}
|
||||
}
|
||||
|
||||
table workspace_audit_logs {
|
||||
id text [pk, not null]
|
||||
workspace_id text [not null]
|
||||
user_id text
|
||||
action text [not null]
|
||||
resource_type text
|
||||
resource_id text
|
||||
metadata jsonb [default: `{}`]
|
||||
ip_address text
|
||||
created_at "timestamp with time zone" [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
workspace_id [name: 'workspace_audit_logs_workspace_id_idx']
|
||||
action [name: 'workspace_audit_logs_action_idx']
|
||||
created_at [name: 'workspace_audit_logs_created_at_idx']
|
||||
}
|
||||
}
|
||||
|
||||
table workspace_invitations {
|
||||
id text [pk, not null]
|
||||
workspace_id text [not null]
|
||||
inviter_id text [not null]
|
||||
email text
|
||||
role text [not null, default: 'member']
|
||||
token text [not null, unique]
|
||||
status text [not null, default: 'pending']
|
||||
expires_at "timestamp with time zone" [not null]
|
||||
created_at "timestamp with time zone" [not null, default: `now()`]
|
||||
updated_at "timestamp with time zone" [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
workspace_id [name: 'workspace_invitations_workspace_id_idx']
|
||||
email [name: 'workspace_invitations_email_idx']
|
||||
token [name: 'workspace_invitations_token_idx']
|
||||
}
|
||||
}
|
||||
|
||||
table workspace_members {
|
||||
workspace_id text [not null]
|
||||
user_id text [not null]
|
||||
role text [not null, default: 'member']
|
||||
joined_at "timestamp with time zone" [not null, default: `now()`]
|
||||
updated_at "timestamp with time zone" [not null, default: `now()`]
|
||||
deleted_at "timestamp with time zone"
|
||||
|
||||
indexes {
|
||||
(workspace_id, user_id) [pk]
|
||||
user_id [name: 'workspace_members_user_id_idx']
|
||||
}
|
||||
}
|
||||
|
||||
table workspaces {
|
||||
id text [pk, not null]
|
||||
slug varchar(100) [not null]
|
||||
name varchar(255) [not null]
|
||||
description varchar(1000)
|
||||
avatar text
|
||||
primary_owner_id text [not null]
|
||||
settings jsonb [default: `{}`]
|
||||
created_at "timestamp with time zone" [not null, default: `now()`]
|
||||
updated_at "timestamp with time zone" [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
slug [name: 'workspaces_slug_idx', unique]
|
||||
primary_owner_id [name: 'workspaces_primary_owner_id_idx']
|
||||
}
|
||||
}
|
||||
|
||||
ref: agent_skills.user_id - users.id
|
||||
|
||||
ref: agent_skills.zip_file_hash - global_files.hash_id
|
||||
@@ -2305,6 +2394,8 @@ ref: agents_knowledge_bases.knowledge_base_id - knowledge_bases.id
|
||||
|
||||
ref: agents_knowledge_bases.agent_id > agents.id
|
||||
|
||||
ref: agents.id - agent_shares.agent_id
|
||||
|
||||
ref: agents_to_sessions.session_id > sessions.id
|
||||
|
||||
ref: agents_to_sessions.agent_id > agents.id
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
---
|
||||
title: Claude Code
|
||||
description: >-
|
||||
Delegate Anthropic's Claude Code inside LobeHub — chat with the Claude Code
|
||||
CLI from your desktop app, watch tasks, todos, skills, and tool calls stream
|
||||
in real time, and resume sessions across turns.
|
||||
tags:
|
||||
- LobeHub
|
||||
- Claude Code
|
||||
- Coding Agent
|
||||
- Desktop
|
||||
- CLI
|
||||
- Anthropic
|
||||
---
|
||||
|
||||
# Claude Code
|
||||
|
||||
Claude Code is Anthropic's coding agent that reads, writes, and runs code from your terminal. In LobeHub, you can delegate Claude Code from the desktop app — keep the chat UX you already use, while Claude Code does the work locally with full access to your project.
|
||||
|
||||
Send a prompt and Claude Code reads files, makes edits, runs commands, and reports back. Tasks, todos, skills, and tool calls stream into the chat as the agent moves; sessions resume across turns so a long task can span many messages.
|
||||
|
||||
## What Is Claude Code in LobeHub?
|
||||
|
||||
A bridge between LobeHub's chat UI and the Claude Code CLI running on your machine. LobeHub spawns the `claude` command as a local subprocess, streams its events into a chat conversation, and renders Claude Code's output — partial messages, tasks, todos, skills, sub-agent threads — as first-class chat blocks. You drive the agent in natural language; Claude Code executes locally with your environment, credentials, and project context.
|
||||
|
||||
## Requirements
|
||||
|
||||
- **LobeHub desktop app** — Claude Code agents only work in the desktop build. The web app cannot spawn local processes.
|
||||
- **Claude Code CLI installed** — the `claude` command must be available on your `PATH`.
|
||||
- **Signed in** — you must run `claude` once in a terminal to authenticate before LobeHub can drive it. Requires an Anthropic account.
|
||||
|
||||
## Install the Claude Code CLI
|
||||
|
||||
Pick one of the install paths:
|
||||
|
||||
**Recommended (install script)**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://claude.ai/install.sh | bash
|
||||
```
|
||||
|
||||
**Homebrew (macOS)**
|
||||
|
||||
```bash
|
||||
brew install --cask claude-code
|
||||
```
|
||||
|
||||
After installing, run `claude` once in a terminal to sign in. See the [Claude Code setup guide](https://docs.anthropic.com/en/docs/claude-code/setup) for details.
|
||||
|
||||
If LobeHub can't find the CLI, it shows an **Install Claude Code CLI** prompt with the same commands and an **Open System Tools** button — click it after installing to re-detect the CLI.
|
||||
|
||||
## Add Claude Code in LobeHub
|
||||
|
||||
When LobeHub detects the Claude Code CLI on your machine, an **Add Claude Code** recommendation card appears on the home page tagged "Coding Agent". Click it to create a Claude Code agent in one step.
|
||||
|
||||
You can also create one manually from the **Create Agent** menu and pick **Claude Code** as the type.
|
||||
|
||||
Each agent is independent, so you can keep multiple Claude Code agents pinned to different projects or workflows.
|
||||
|
||||
## Working Directory
|
||||
|
||||
Every Claude Code session is pinned to a working directory — the folder Claude Code sees as the project root. Set it from the chat input bar before sending your first message. Switching mid-conversation triggers a **Switch working directory?** confirmation: chat messages stay, but the previous session context cannot be resumed and a new session starts for this topic.
|
||||
|
||||
If you change folders and the saved Claude Code session can't be resumed, LobeHub shows: *"Working directory changed. Previous Claude Code session can only be resumed from its original directory, so a new conversation has started."*
|
||||
|
||||
Inside the working directory, Claude Code runs with **Full access** — read and write to anything in the folder. Switching permission modes from inside LobeHub is not yet supported.
|
||||
|
||||
## What Gets Rendered in Chat
|
||||
|
||||
LobeHub renders Claude Code's tool calls and structured output with purpose-built blocks instead of raw JSON:
|
||||
|
||||
**Tasks** — When Claude Code uses its task manager, tasks render as a live progress card. Watch items move through pending → in-progress → completed as Claude Code works.
|
||||
|
||||
**Todos** — `TodoWrite` plans render as a progress card with completion counts and check states. Useful for tracking multi-step work.
|
||||
|
||||
**Skills** — When Claude Code invokes a built-in or user-installed skill, the call appears in a Skill block showing inputs, outputs, and any artifacts.
|
||||
|
||||
**Tool calls** — Reads, edits, shell runs, web fetches, and other tool uses get their own block in the conversation. Streamed partial output appears as Claude Code generates it.
|
||||
|
||||
**Sub-agents** — Claude Code can spawn sub-agents to handle parallel or scoped work. Their threads render in isolation inside the conversation without leaking into the main bubble.
|
||||
|
||||
**Interventions** — When Claude Code needs to ask you something mid-run, it shows a prompt inline so you can answer without leaving the chat.
|
||||
|
||||
## Sessions and Resume
|
||||
|
||||
Claude Code sessions persist across messages in the same topic. LobeHub captures the underlying session ID and reuses it on every follow-up, so you can pick up a long-running task at any point.
|
||||
|
||||
A session can't be resumed if:
|
||||
|
||||
- The working directory changed since the session was created
|
||||
- The Claude Code CLI returns a resume error (session no longer exists, credentials expired, etc.)
|
||||
|
||||
In either case, LobeHub starts a fresh conversation automatically.
|
||||
|
||||
## Where It Can Run
|
||||
|
||||
The **Execution Device** selector lets you pick where the Claude Code agent runs:
|
||||
|
||||
- **This device** — runs Claude Code as a local process inside the desktop app. Default.
|
||||
- **Cloud sandbox** — runs Claude Code in an ephemeral cloud sandbox. Useful when you don't want the agent touching your local filesystem.
|
||||
- **Remote device** — drives a remote machine you've connected with `lh connect`. Useful when the project lives on a different machine.
|
||||
|
||||
## Limitations
|
||||
|
||||
- **Desktop only** — the Claude Code agent runs in the LobeHub desktop app. The web app cannot spawn the CLI.
|
||||
- **One sign-in per machine** — Claude Code shares its authentication with the global CLI. If `claude` works in your terminal, it works in LobeHub.
|
||||
- **Working-directory-bound** — sessions don't follow you across folders or machines.
|
||||
- **Full access only** — switching permission modes from inside LobeHub is not yet supported.
|
||||
|
||||
## Tips
|
||||
|
||||
- **Run `claude` once in a terminal first** — sign-in happens at the CLI level, not in LobeHub.
|
||||
- **Pick the working directory before your first message** — switching it later starts a new session.
|
||||
- **Use one Claude Code agent per project** — pinning each agent to a specific repo keeps sessions tidy and resumable.
|
||||
- **Watch the task card** — when Claude Code uses its task manager, the card is the fastest read on what's done, what's running, and what's queued.
|
||||
|
||||
<Cards>
|
||||
<Card href={'/docs/usage/agent/codex'} title={'Codex'} />
|
||||
|
||||
<Card href={'/docs/usage/agent/agent-team'} title={'Agent Groups'} />
|
||||
|
||||
<Card href={'/docs/usage/agent/sandbox'} title={'Cloud Sandbox'} />
|
||||
</Cards>
|
||||
@@ -0,0 +1,120 @@
|
||||
---
|
||||
title: Claude Code
|
||||
description: 在 LobeHub 中委派 Anthropic Claude Code —— 通过桌面应用与 Claude Code CLI 对话,实时查看任务、待办、技能与工具调用,并跨轮次恢复会话。
|
||||
tags:
|
||||
- LobeHub
|
||||
- Claude Code
|
||||
- 编程助理
|
||||
- 桌面端
|
||||
- CLI
|
||||
- Anthropic
|
||||
---
|
||||
|
||||
# Claude Code
|
||||
|
||||
Claude Code 是 Anthropic 推出的编程助理,能在终端中读取、修改、运行代码。在 LobeHub 中,你可以通过桌面应用委派 Claude Code —— 保留熟悉的对话界面,让 Claude Code 在本地完成实际工作,并完整访问你的项目。
|
||||
|
||||
发送一条提示,Claude Code 会读取文件、修改代码、运行命令,并把过程反馈给你。任务、待办、技能与工具调用会随着助理推进实时进入聊天;会话能跨轮次恢复,一项长任务可以分布在多条消息中持续推进。
|
||||
|
||||
## 什么是 LobeHub 中的 Claude Code?
|
||||
|
||||
它是 LobeHub 对话界面与本地 Claude Code CLI 之间的桥梁。LobeHub 在本地以子进程形式启动 `claude` 命令,把它的事件流接入聊天会话,并将 Claude Code 的输出 —— 增量消息、任务、待办、技能、子助理线程 —— 渲染为一等公民的聊天块。你用自然语言指挥助理,Claude Code 在本地用你的环境、凭据与项目上下文执行。
|
||||
|
||||
## 使用条件
|
||||
|
||||
- **LobeHub 桌面应用** —— Claude Code 助理只在桌面版可用,Web 端无法启动本地进程。
|
||||
- **已安装 Claude Code CLI** —— `claude` 命令需要在你的 `PATH` 中可用。
|
||||
- **已登录** —— 在 LobeHub 调用前,需在终端中先运行一次 `claude` 完成认证,需要 Anthropic 账号。
|
||||
|
||||
## 安装 Claude Code CLI
|
||||
|
||||
任选一种方式:
|
||||
|
||||
**推荐(安装脚本)**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://claude.ai/install.sh | bash
|
||||
```
|
||||
|
||||
**Homebrew(macOS)**
|
||||
|
||||
```bash
|
||||
brew install --cask claude-code
|
||||
```
|
||||
|
||||
安装完成后,在终端中运行一次 `claude` 完成登录。详情见 [Claude Code 安装指南](https://docs.anthropic.com/en/docs/claude-code/setup)。
|
||||
|
||||
若 LobeHub 未能检测到 CLI,会弹出**安装 Claude Code CLI** 引导,并提供**打开系统工具**按钮 —— 安装完成后点击即可重新检测。
|
||||
|
||||
## 在 LobeHub 中添加 Claude Code
|
||||
|
||||
当 LobeHub 检测到本机已安装 Claude Code CLI,首页会出现一张标记为「编程助理」的**添加 Claude Code** 推荐卡片,点击即可一步创建 Claude Code 助理。
|
||||
|
||||
你也可以手动创建:从**创建助理**菜单中选择 **Claude Code** 类型即可。
|
||||
|
||||
每个助理彼此独立,可以分别绑定到不同的项目或工作流。
|
||||
|
||||
## 工作目录
|
||||
|
||||
每个 Claude Code 会话都绑定一个工作目录 —— 即 Claude Code 视为项目根的文件夹。在发出第一条消息前,先在聊天输入区域设置工作目录。会话进行中切换目录会触发**切换工作目录?**确认:聊天记录会保留,但旧会话的上下文无法恢复,将为该话题开启新的会话。
|
||||
|
||||
如果切换目录后,已保存的 Claude Code 会话无法恢复,LobeHub 会提示:**「工作目录已更改。之前的 Claude Code 会话只能在原始目录下恢复,已开启新的对话。」**
|
||||
|
||||
在工作目录内,Claude Code 以**完全访问**权限运行 —— 可对文件夹内任何文件进行读写。LobeHub 内部暂不支持切换权限模式。
|
||||
|
||||
## 聊天中会渲染什么
|
||||
|
||||
LobeHub 不会把 Claude Code 的工具调用渲染成原始 JSON,而是用专用区块呈现:
|
||||
|
||||
**任务** —— Claude Code 使用任务管理器时,任务会渲染为实时进度卡片。可以看到条目在「待办 → 进行中 → 已完成」之间流转。
|
||||
|
||||
**待办** —— `TodoWrite` 计划会渲染为进度卡片,展示完成数量与勾选状态。适合追踪多步骤工作。
|
||||
|
||||
**技能** —— Claude Code 调用内置或用户安装的技能时,调用会呈现为 Skill 区块,展示输入、输出与产物。
|
||||
|
||||
**工具调用** —— 文件读取、编辑、命令执行、网页抓取等工具使用都会在对话中拥有独立区块,并随 Claude Code 输出实时增量展示。
|
||||
|
||||
**子助理** —— Claude Code 可以派生子助理处理并行或局部任务。它们的线程在会话中以独立线程呈现,不会污染主对话气泡。
|
||||
|
||||
**询问** —— 当 Claude Code 需要在过程中向你提问时,会在聊天中内联呈现,让你无需离开对话即可回答。
|
||||
|
||||
## 会话与恢复
|
||||
|
||||
Claude Code 会话在同一话题中跨消息持续。LobeHub 会捕获底层 session ID 并在每次追问时复用,因此你可以随时回到长任务的任意进度点继续。
|
||||
|
||||
下列情况下,会话无法恢复:
|
||||
|
||||
- 自会话创建以来工作目录被更改
|
||||
- Claude Code CLI 返回恢复错误(会话已不存在、凭据过期等)
|
||||
|
||||
任一情况发生时,LobeHub 都会自动开启一段新会话。
|
||||
|
||||
## 它在哪里运行
|
||||
|
||||
**执行设备**选择器让你决定 Claude Code 助理在哪里运行:
|
||||
|
||||
- **本机** —— Claude Code 在桌面应用内作为本地进程运行,默认选项。
|
||||
- **云沙箱** —— Claude Code 在临时云沙箱中运行。当你不希望助理触碰本地文件时适用。
|
||||
- **远程设备** —— 驱动你通过 `lh connect` 接入的另一台机器。当项目位于另一台设备上时适用。
|
||||
|
||||
## 限制
|
||||
|
||||
- **仅桌面端** —— Claude Code 助理只在 LobeHub 桌面应用中可用,Web 端无法启动 CLI。
|
||||
- **每台机器一次登录** —— Claude Code 与全局 CLI 共享认证。终端里 `claude` 能用,LobeHub 里就能用。
|
||||
- **绑定工作目录** —— 会话不会跨文件夹或机器跟随你。
|
||||
- **仅支持完全访问** —— LobeHub 内部暂不支持切换权限模式。
|
||||
|
||||
## 使用技巧
|
||||
|
||||
- **先在终端中运行一次 `claude`** —— 登录在 CLI 层面完成,不在 LobeHub 里。
|
||||
- **第一条消息前先选好工作目录** —— 之后切换会开启新会话。
|
||||
- **一个项目用一个 Claude Code 助理** —— 每个助理绑定一个仓库,会话更整洁也更容易恢复。
|
||||
- **多关注任务卡片** —— Claude Code 使用任务管理器时,这张卡片是了解「已完成、进行中、待办」的最快方式。
|
||||
|
||||
<Cards>
|
||||
<Card href={'/zh/docs/usage/agent/codex'} title={'Codex'} />
|
||||
|
||||
<Card href={'/zh/docs/usage/agent/agent-team'} title={'群组'} />
|
||||
|
||||
<Card href={'/zh/docs/usage/agent/sandbox'} title={'云沙箱'} />
|
||||
</Cards>
|
||||
@@ -0,0 +1,117 @@
|
||||
---
|
||||
title: Codex
|
||||
description: >-
|
||||
Delegate OpenAI Codex inside LobeHub — chat with the Codex CLI from your
|
||||
desktop app, watch file changes, todos, and command output stream in real
|
||||
time, and resume sessions across turns.
|
||||
tags:
|
||||
- LobeHub
|
||||
- Codex
|
||||
- Coding Agent
|
||||
- Desktop
|
||||
- CLI
|
||||
- OpenAI
|
||||
---
|
||||
|
||||
# Codex
|
||||
|
||||
Codex is OpenAI's coding agent that edits files, runs commands, and ships changes from your terminal. In LobeHub, you can delegate Codex from the desktop app — keep the chat UX you already use, while Codex does the work locally with full access to your project.
|
||||
|
||||
Send a prompt and Codex opens files, makes edits, runs tests, and reports back. File changes, todos, and command output stream into the chat as the agent moves; sessions resume across turns so a long task can span many messages.
|
||||
|
||||
## What Is Codex in LobeHub?
|
||||
|
||||
A bridge between LobeHub's chat UI and the Codex CLI running on your machine. LobeHub spawns the Codex CLI as a local subprocess, streams its events into a chat conversation, and renders Codex's tool output — file changes, todo lists, command runs — as first-class chat blocks. You drive the agent in natural language; Codex executes locally with your environment, credentials, and project context.
|
||||
|
||||
## Requirements
|
||||
|
||||
- **LobeHub desktop app** — Codex agents only work in the desktop build. The web app cannot spawn local processes.
|
||||
- **Codex CLI installed** — the `codex` command must be available on your `PATH`.
|
||||
- **Signed in** — you must run `codex` once in a terminal to authenticate before LobeHub can drive it.
|
||||
|
||||
## Install the Codex CLI
|
||||
|
||||
Pick one of the install paths:
|
||||
|
||||
**Recommended (npm)**
|
||||
|
||||
```bash
|
||||
npm install -g @openai/codex
|
||||
```
|
||||
|
||||
**Homebrew (macOS)**
|
||||
|
||||
```bash
|
||||
brew install --cask codex
|
||||
```
|
||||
|
||||
After installing, run `codex` once in a terminal to sign in. See the [Codex installation guide](https://github.com/openai/codex#installing-and-running-codex-cli) for details.
|
||||
|
||||
If LobeHub can't find the CLI, it shows an **Install Codex CLI** prompt with the same commands and an **Open System Tools** button — click it after installing to re-detect the CLI.
|
||||
|
||||
## Add Codex in LobeHub
|
||||
|
||||
When LobeHub detects the Codex CLI on your machine, an **Add Codex** recommendation card appears on the home page tagged "Coding Agent". Click it to create a Codex agent in one step.
|
||||
|
||||
You can also create one manually from the **Create Agent** menu and pick **Codex** as the type.
|
||||
|
||||
Each agent is independent, so you can keep multiple Codex agents pinned to different projects or workflows.
|
||||
|
||||
## Working Directory
|
||||
|
||||
Every Codex session is pinned to a working directory — the folder Codex sees as the project root. Set it from the chat input bar before sending your first message. Switching the working directory mid-conversation starts a new Codex session for the topic; chat history stays, but the previous session context cannot be resumed.
|
||||
|
||||
If you change folders and the saved Codex thread can't be resumed safely, LobeHub shows: *"The saved Codex thread could not be resumed safely, so a new conversation has started for this topic."*
|
||||
|
||||
## What Gets Rendered in Chat
|
||||
|
||||
LobeHub renders Codex's tool calls with purpose-built blocks instead of raw JSON:
|
||||
|
||||
**File changes** — Codex's edits show up as an expandable list with the operation kind (added, deleted, modified, renamed), the file path, and a per-file line count delta (+/−). Click to see what changed.
|
||||
|
||||
**Todo lists** — When Codex plans a multi-step task, the plan renders as a progress card with completed / in-progress / pending items and a running count (e.g. "3/5 completed"). Watch tasks tick off as Codex finishes them.
|
||||
|
||||
**Command execution** — Shell commands Codex runs show the command, exit code, and stdout / stderr output. Success and failure states are clearly marked.
|
||||
|
||||
**Subagents** — Codex can spawn subagents to work in parallel. Their work appears in isolated threads inside the conversation without leaking into the main bubble.
|
||||
|
||||
## Sessions and Resume
|
||||
|
||||
Codex sessions persist across messages in the same topic. You can send a follow-up like "now also update the tests" and Codex picks up where it left off — same files, same context, same plan.
|
||||
|
||||
A session can't be resumed if:
|
||||
|
||||
- The working directory changed since the saved thread was created
|
||||
- The original Codex thread no longer exists
|
||||
- The CLI returns a "no conversation found" or "thread not found" error
|
||||
|
||||
In any of these cases, LobeHub starts a fresh conversation automatically.
|
||||
|
||||
## Where It Can Run
|
||||
|
||||
The **Execution Device** selector lets you pick where the Codex agent runs:
|
||||
|
||||
- **This device** — runs Codex as a local process inside the desktop app. Default.
|
||||
- **Cloud sandbox** — runs Codex in an ephemeral cloud sandbox. Useful when you don't want the agent touching your local filesystem.
|
||||
- **Remote device** — drives a remote machine you've connected with `lh connect`. Useful when the project lives on a different machine.
|
||||
|
||||
## Limitations
|
||||
|
||||
- **Desktop only** — the Codex agent runs in the LobeHub desktop app. The web app cannot spawn the CLI.
|
||||
- **One sign-in per machine** — Codex shares its authentication with the global CLI. If `codex` works in your terminal, it works in LobeHub.
|
||||
- **Working-directory-bound** — sessions don't follow you across folders or machines.
|
||||
|
||||
## Tips
|
||||
|
||||
- **Run `codex` once in a terminal first** — sign-in happens at the CLI level, not in LobeHub.
|
||||
- **Pick the working directory before your first message** — switching it later starts a new session.
|
||||
- **Watch the todo card** — it's the fastest read on what Codex thinks it still has to do.
|
||||
- **Use one Codex agent per project** — pinning each agent to a specific repo keeps sessions tidy and resumable.
|
||||
|
||||
<Cards>
|
||||
<Card href={'/docs/usage/agent/claude-code'} title={'Claude Code'} />
|
||||
|
||||
<Card href={'/docs/usage/agent/agent-team'} title={'Agent Groups'} />
|
||||
|
||||
<Card href={'/docs/usage/agent/sandbox'} title={'Cloud Sandbox'} />
|
||||
</Cards>
|
||||
@@ -0,0 +1,114 @@
|
||||
---
|
||||
title: Codex
|
||||
description: 在 LobeHub 中委派 OpenAI Codex —— 通过桌面应用与 Codex CLI 对话,实时查看文件变更、待办与命令输出,并跨轮次恢复会话。
|
||||
tags:
|
||||
- LobeHub
|
||||
- Codex
|
||||
- 编程助理
|
||||
- 桌面端
|
||||
- CLI
|
||||
- OpenAI
|
||||
---
|
||||
|
||||
# Codex
|
||||
|
||||
Codex 是 OpenAI 推出的编程助理,能在终端中编辑文件、运行命令、提交改动。在 LobeHub 中,你可以通过桌面应用委派 Codex —— 保留熟悉的对话界面,让 Codex 在本地完成实际工作,并完整访问你的项目。
|
||||
|
||||
发送一条提示,Codex 会打开文件、修改代码、运行测试,并把过程反馈给你。文件变更、待办列表、命令输出会随着助理推进实时进入聊天;会话能跨轮次恢复,一项长任务可以分布在多条消息中持续推进。
|
||||
|
||||
## 什么是 LobeHub 中的 Codex?
|
||||
|
||||
它是 LobeHub 对话界面与本地 Codex CLI 之间的桥梁。LobeHub 在本地以子进程形式启动 Codex CLI,把它的事件流接入聊天会话,并将 Codex 的工具输出 —— 文件变更、待办列表、命令执行 —— 渲染为一等公民的聊天块。你用自然语言指挥助理,Codex 在本地用你的环境、凭据与项目上下文执行。
|
||||
|
||||
## 使用条件
|
||||
|
||||
- **LobeHub 桌面应用** —— Codex 助理只在桌面版可用,Web 端无法启动本地进程。
|
||||
- **已安装 Codex CLI** —— `codex` 命令需要在你的 `PATH` 中可用。
|
||||
- **已登录** —— 在 LobeHub 调用前,需在终端中先运行一次 `codex` 完成认证。
|
||||
|
||||
## 安装 Codex CLI
|
||||
|
||||
任选一种方式:
|
||||
|
||||
**推荐(npm)**
|
||||
|
||||
```bash
|
||||
npm install -g @openai/codex
|
||||
```
|
||||
|
||||
**Homebrew(macOS)**
|
||||
|
||||
```bash
|
||||
brew install --cask codex
|
||||
```
|
||||
|
||||
安装完成后,在终端中运行一次 `codex` 完成登录。详情见 [Codex 安装指南](https://github.com/openai/codex#installing-and-running-codex-cli)。
|
||||
|
||||
若 LobeHub 未能检测到 CLI,会弹出**安装 Codex CLI** 引导,并提供**打开系统工具**按钮 —— 安装完成后点击即可重新检测。
|
||||
|
||||
## 在 LobeHub 中添加 Codex
|
||||
|
||||
当 LobeHub 检测到本机已安装 Codex CLI,首页会出现一张标记为「编程助理」的**添加 Codex** 推荐卡片,点击即可一步创建 Codex 助理。
|
||||
|
||||
你也可以手动创建:从**创建助理**菜单中选择 **Codex** 类型即可。
|
||||
|
||||
每个助理彼此独立,可以分别绑定到不同的项目或工作流。
|
||||
|
||||
## 工作目录
|
||||
|
||||
每个 Codex 会话都绑定一个工作目录 —— 即 Codex 视为项目根的文件夹。在发出第一条消息前,先在聊天输入区域设置工作目录。会话进行中切换目录会为该话题开启一个新的 Codex 会话;聊天记录会保留,但旧会话的上下文无法恢复。
|
||||
|
||||
如果切换目录后,已保存的 Codex 线程无法安全恢复,LobeHub 会提示:**「已保存的 Codex 线程无法安全恢复,已为该话题开启新的会话。」**
|
||||
|
||||
## 聊天中会渲染什么
|
||||
|
||||
LobeHub 不会把 Codex 的工具调用渲染成原始 JSON,而是用专用区块呈现:
|
||||
|
||||
**文件变更** —— Codex 对文件的修改会展示为可展开的列表,包含操作类型(新增、删除、修改、重命名)、文件路径,以及每个文件的行数变化(+/−)。点击可查看改动详情。
|
||||
|
||||
**待办列表** —— Codex 规划多步任务时,待办会渲染为进度卡片,列出已完成、进行中和待办项,并显示完成进度(如「3/5 已完成」)。Codex 完成任务时,待办会自动勾选。
|
||||
|
||||
**命令执行** —— Codex 运行的 shell 命令会显示命令本身、退出码以及 stdout / stderr 输出。成功与失败状态一目了然。
|
||||
|
||||
**子助理** —— Codex 可以派生子助理并行工作。它们的输出在会话中以独立线程呈现,不会污染主对话气泡。
|
||||
|
||||
## 会话与恢复
|
||||
|
||||
Codex 会话在同一话题中跨消息持续。你可以发出追问,例如「顺便也更新一下测试」,Codex 会接着上一次的进度继续 —— 同样的文件、同样的上下文、同样的计划。
|
||||
|
||||
下列情况下,会话无法恢复:
|
||||
|
||||
- 自上次保存以来工作目录被更改
|
||||
- 原始 Codex 线程已不存在
|
||||
- CLI 报错「no conversation found」或「thread not found」
|
||||
|
||||
任一情况发生时,LobeHub 都会自动开启一段新会话。
|
||||
|
||||
## 它在哪里运行
|
||||
|
||||
**执行设备**选择器让你决定 Codex 助理在哪里运行:
|
||||
|
||||
- **本机** —— Codex 在桌面应用内作为本地进程运行,默认选项。
|
||||
- **云沙箱** —— Codex 在临时云沙箱中运行。当你不希望助理触碰本地文件时适用。
|
||||
- **远程设备** —— 驱动你通过 `lh connect` 接入的另一台机器。当项目位于另一台设备上时适用。
|
||||
|
||||
## 限制
|
||||
|
||||
- **仅桌面端** —— Codex 助理只在 LobeHub 桌面应用中可用,Web 端无法启动 CLI。
|
||||
- **每台机器一次登录** —— Codex 与全局 CLI 共享认证。终端里 `codex` 能用,LobeHub 里就能用。
|
||||
- **绑定工作目录** —— 会话不会跨文件夹或机器跟随你。
|
||||
|
||||
## 使用技巧
|
||||
|
||||
- **先在终端中运行一次 `codex`** —— 登录在 CLI 层面完成,不在 LobeHub 里。
|
||||
- **第一条消息前先选好工作目录** —— 之后切换会开启新会话。
|
||||
- **多关注待办卡片** —— 这是了解 Codex 还剩什么任务的最快方式。
|
||||
- **一个项目用一个 Codex 助理** —— 每个助理绑定一个仓库,会话更整洁也更容易恢复。
|
||||
|
||||
<Cards>
|
||||
<Card href={'/zh/docs/usage/agent/claude-code'} title={'Claude Code'} />
|
||||
|
||||
<Card href={'/zh/docs/usage/agent/agent-team'} title={'群组'} />
|
||||
|
||||
<Card href={'/zh/docs/usage/agent/sandbox'} title={'云沙箱'} />
|
||||
</Cards>
|
||||
@@ -0,0 +1,212 @@
|
||||
---
|
||||
title: Image & Video Generation
|
||||
description: >-
|
||||
Create high-quality images and videos from text descriptions using AI models
|
||||
like DALL-E 3, Flux, Sora, Veo, Kling, and more. Learn how to write effective
|
||||
prompts, choose the right model, and configure parameters for each medium.
|
||||
tags:
|
||||
- LobeHub
|
||||
- Image Generation
|
||||
- Video Generation
|
||||
- AI Drawing
|
||||
- AI Video
|
||||
- DALL-E
|
||||
- Sora
|
||||
- Veo
|
||||
- Kling
|
||||
- Text to Image
|
||||
- Text to Video
|
||||
- Prompt Writing
|
||||
---
|
||||
|
||||
# Image & Video Generation
|
||||
|
||||
Describe what you want — LobeHub turns text into images and videos. Product prototypes, design inspiration, illustrations, motion concepts, short clips, or creative exploration: choose a model, set your parameters, and generate in seconds. All output lands in your generation feed and can be downloaded or saved to your Resource Library.
|
||||
|
||||
LobeHub ships two parallel workspaces — **Image** and **Video** — built on the same generation pipeline but tuned for each medium.
|
||||
|
||||
## Get Started
|
||||
|
||||
From the LobeHub sidebar:
|
||||
|
||||
- Click **Image** (the picture icon) to open the image generation workspace at `/image`.
|
||||
- Click **Video** (the video icon) to open the video generation workspace at `/video`.
|
||||
|
||||
Each workspace has the same three-pane layout: prompt input, configuration panel, and a generation feed for past results.
|
||||
|
||||
## Image Generation
|
||||
|
||||
### Enter a Prompt
|
||||
|
||||
Describe the image you want in the input box. The more specific your description, the more accurate the result.
|
||||
|
||||
**Effective prompt structure:**
|
||||
|
||||
```
|
||||
[Subject] [Style/Medium] [Setting/Background] [Lighting] [Mood] [Technical details]
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
"A futuristic city skyline at sunset, digital art, cyberpunk style, neon lights reflecting on wet streets, cinematic lighting, 4K detail"
|
||||
|
||||
"A cozy coffee shop interior, watercolor illustration, warm golden light streaming through windows, potted plants on windowsills, soft and inviting atmosphere"
|
||||
|
||||
"A product photo of a minimalist leather wallet on a clean white background, studio lighting, sharp focus, commercial photography style"
|
||||
```
|
||||
|
||||
**Prompt tips:**
|
||||
|
||||
- **Be specific about style** — "oil painting", "watercolor", "digital art", "photorealistic", "anime", "vector illustration"
|
||||
- **Describe lighting** — "dramatic shadows", "soft diffused light", "golden hour", "studio lighting"
|
||||
- **Specify composition** — "portrait view", "wide angle", "close-up", "bird's eye view"
|
||||
- **Add quality modifiers** — "high detail", "4K", "sharp focus", "professional quality"
|
||||
- **Avoid vagueness** — "beautiful", "nice", "good" add little — describe what you actually want
|
||||
|
||||
### Choose an AI Model
|
||||
|
||||
LobeHub offers multiple AI image generation models. Different models have different strengths:
|
||||
|
||||

|
||||
|
||||
| Model | Best For |
|
||||
| -------------------- | ------------------------------------------------------------- |
|
||||
| **DALL-E 3** | Realistic photos, illustrations, following prompts accurately |
|
||||
| **GPT Image** | High-fidelity edits, text rendering inside images |
|
||||
| **Flux** | Artistic styles, creative images, fast generation |
|
||||
| **Stable Diffusion** | Highly customizable, community styles and fine-tuned models |
|
||||
| **Gemini Imagen** | Photoreal scenes, strong global composition |
|
||||
| **fal.ai models** | Various specialized styles and fast generation |
|
||||
|
||||
Try different models with the same prompt to see which gives the best results for your use case.
|
||||
|
||||
### Reference Images (Optional)
|
||||
|
||||
If you have reference images, upload them to guide the generation process. Click the upload button or drag and drop your reference images directly. You can upload multiple reference images depending on the model.
|
||||
|
||||

|
||||
|
||||
Reference images help the model understand your desired style, composition, or color palette — and many models also support reference-based **edits** (e.g. swap the background, change the outfit) when you describe the change in the prompt.
|
||||
|
||||
### Configure Generation Parameters
|
||||
|
||||
The right-hand config panel exposes everything the selected model supports. Common controls:
|
||||
|
||||
- **Aspect Ratio** — `1:1`, `16:9`, `9:16`, `4:3`, `3:2`. Lock or unlock to free-form size.
|
||||
- **Size / Resolution** — pick a preset (`512px`, `1K`, `2K`, `4K`) or set width × height directly.
|
||||
- **Number of Images** — generate 1–4 variations per run.
|
||||
- **Quality** — Standard or High Definition (model-dependent).
|
||||
- **Seed** — leave random for variety, or paste a fixed seed to reproduce a previous result.
|
||||
- **Steps / Guidance Intensity (CFG)** — fine-tune the speed-vs-quality and prompt-adherence tradeoffs.
|
||||
- **Watermark** — toggle on/off where supported.
|
||||
- **Web Search** / **Prompt Extend** — let an LLM enrich your prompt with current references before generation.
|
||||
|
||||
**Aspect ratio cheatsheet:**
|
||||
|
||||
- **1:1** — Social media posts, profile pictures
|
||||
- **16:9** — Widescreen, presentations, banners
|
||||
- **9:16** — Mobile screens, stories, reels
|
||||
- **4:3** — General use, older display formats
|
||||
- **3:2** — Photography standard, prints
|
||||
|
||||
### View and Download Images
|
||||
|
||||
Once generated, images appear in the generation feed. You can:
|
||||
|
||||
- Preview any image at full size by clicking it
|
||||
- Download, copy the seed, copy the prompt, or reuse the full settings on a new run
|
||||
- Delete a single image or the whole batch
|
||||
|
||||

|
||||
|
||||
## Video Generation
|
||||
|
||||
The Video workspace mirrors Image — same prompt-first flow, same config panel, same feed — but with controls tuned for motion.
|
||||
|
||||
### Enter a Prompt
|
||||
|
||||
Describe the **scene, motion, and camera**, not just the subject. Models reward verbs and shot language.
|
||||
|
||||
```
|
||||
"A red fox trotting through fresh snow at golden hour, breath visible in the cold air, slow tracking shot, cinematic"
|
||||
|
||||
"An astronaut floating into a colorful nebula, slow dolly-in, dreamy atmosphere, soft volumetric light"
|
||||
|
||||
"A cup of coffee being poured in macro slow motion, steam rising, shallow depth of field, commercial product shot"
|
||||
```
|
||||
|
||||
**Prompt tips for video:**
|
||||
|
||||
- **Describe motion explicitly** — "slow tracking shot", "dolly-in", "handheld", "static wide", "pan left"
|
||||
- **Set a time progression** — "starts misty then clears", "the door slowly opens"
|
||||
- **Reference cinematography** — "shallow depth of field", "anamorphic lens flare", "golden hour"
|
||||
- **Keep it focused** — one main action per clip works better than several
|
||||
|
||||
### Choose an AI Model
|
||||
|
||||
LobeHub integrates the major text-to-video and image-to-video providers:
|
||||
|
||||
| Model | Best For |
|
||||
| ------------------------------ | ------------------------------------------------------------ |
|
||||
| **OpenAI Sora 2 / Sora 2 Pro** | Coherent multi-second clips, strong scene understanding |
|
||||
| **Google Veo 3 / 3.1** | Photoreal motion, native audio generation, cinematic look |
|
||||
| **Kling V3** | High-motion fidelity, image-to-video and omni-video |
|
||||
| **MiniMax Hailuo 2.3** | Fast text-to-video, expressive characters |
|
||||
| **Qwen / Wan** | Text-to-video with strong Chinese prompt understanding |
|
||||
| **fal.ai models** | Specialised models, fast turnaround |
|
||||
|
||||
Different models support different parameter sets — switching models updates the config panel automatically.
|
||||
|
||||
### Start & End Frames (Optional)
|
||||
|
||||
Many video models support image conditioning:
|
||||
|
||||
- **Start Frame** — upload an image to use as the first frame of the clip. Great for animating a still you generated in the Image workspace.
|
||||
- **End Frame** — upload an image to land on as the final frame. Requires a start frame.
|
||||
|
||||
When a start frame is set, the prompt placeholder shifts to "Describe the scene you want to generate with the image".
|
||||
|
||||
### Configure Generation Parameters
|
||||
|
||||
Controls vary by model, but typically include:
|
||||
|
||||
- **Duration** — clip length in seconds (model-dependent, e.g. 4s / 6s / 8s).
|
||||
- **Aspect Ratio** — `16:9`, `9:16`, `1:1`, `4:3`, `3:4`, `21:9`.
|
||||
- **Resolution** — `480p`, `720p`, `1080p`.
|
||||
- **Fixed Camera** — lock the camera in place instead of letting the model animate it.
|
||||
- **Generate Audio** — produce a synced soundtrack alongside the video (model-dependent, e.g. Veo).
|
||||
- **Seed** — random or fixed for reproducibility.
|
||||
- **Watermark** — toggle on/off where supported.
|
||||
- **Web Search** / **Prompt Extend** — same LLM-assisted prompt enrichment as the image flow.
|
||||
|
||||
### View and Download Videos
|
||||
|
||||
Generated clips appear in the feed and play inline. You can:
|
||||
|
||||
- Play, pause, and scrub through the clip
|
||||
- Download the video
|
||||
- Copy the error message to clipboard if a generation fails
|
||||
- Delete a single clip or the whole batch
|
||||
|
||||
A "🎁 N free videos today" badge shows your remaining free quota; once it's used up, credits are consumed per generation.
|
||||
|
||||
## Tips for Better Results
|
||||
|
||||
**Iterate on prompts** — If the first result isn't quite right, adjust one element at a time rather than rewriting the whole prompt. Add more detail, change the style descriptor, or specify what you don't want.
|
||||
|
||||
**Use a reference image or start frame** — Uploading a reference helps the model match your intended style, color palette, composition, or — for video — your opening shot.
|
||||
|
||||
**Try multiple variations** — Generate several images per run, or re-generate videos with the same seed and a tweaked prompt. AI generation has inherent randomness — some variations will be significantly better than others.
|
||||
|
||||
**Match model to task** — Photorealistic models (DALL-E 3, Flux, Imagen) for product photos and realistic scenes; style-focused models for artistic illustrations; Veo or Sora for cinematic motion; Kling or Hailuo for character-heavy clips.
|
||||
|
||||
**Bridge image → video** — Generate a strong still in the Image workspace, then feed it into the Video workspace as a start frame to animate it.
|
||||
|
||||
<Cards>
|
||||
<Card href={'/docs/usage/getting-started/resource'} title={'Resource Library'} />
|
||||
|
||||
<Card href={'/docs/usage/getting-started/vision'} title={'Vision & Image Understanding'} />
|
||||
|
||||
<Card href={'/docs/usage/providers'} title={'AI Providers'} />
|
||||
</Cards>
|
||||
@@ -0,0 +1,209 @@
|
||||
---
|
||||
title: 图像与视频生成
|
||||
description: 使用 DALL-E 3、Flux、Sora、Veo、Kling 等 AI 模型,通过文字描述生成高质量图像和视频。学习如何编写有效的提示词、选择合适的模型,并配置每种媒介的参数。
|
||||
tags:
|
||||
- LobeHub
|
||||
- 图像生成
|
||||
- 视频生成
|
||||
- AI 画图
|
||||
- AI 视频
|
||||
- DALL-E
|
||||
- Sora
|
||||
- Veo
|
||||
- Kling
|
||||
- 文字生成图像
|
||||
- 文字生成视频
|
||||
- 提示词写作
|
||||
---
|
||||
|
||||
# 图像与视频生成
|
||||
|
||||
用文字描述你想要的内容 ——LobeHub 帮你把想法变成图像和视频。产品原型、设计灵感、插图配图、动态概念、短片创作、创意探索:选择模型、设置参数,几秒钟内获得结果。所有生成内容都会出现在生成流中,可以下载或保存到你的资源库。
|
||||
|
||||
LobeHub 提供两个并行的工作区 ——**图像**与**视频**——基于同一套生成管线,但针对各自的媒介进行了优化。
|
||||
|
||||
## 开始生成
|
||||
|
||||
在 LobeHub 侧边栏:
|
||||
|
||||
- 点击**图像**(图片图标)进入 `/image` 的图像生成工作区。
|
||||
- 点击**视频**(视频图标)进入 `/video` 的视频生成工作区。
|
||||
|
||||
两个工作区采用相同的三栏布局:提示词输入、配置面板、历史生成流。
|
||||
|
||||
## 图像生成
|
||||
|
||||
### 输入提示词
|
||||
|
||||
在输入框中描述你想要的图像。描述越具体,结果越符合预期。
|
||||
|
||||
**有效的提示词结构:**
|
||||
|
||||
```
|
||||
[主体] [风格/媒介] [场景/背景] [光线] [氛围] [技术细节]
|
||||
```
|
||||
|
||||
示例:
|
||||
|
||||
```
|
||||
"赛博朋克风格的未来城市天际线,日落时分,霓虹灯在湿润街道上的倒影,数字艺术,电影级光线,4K 细节"
|
||||
|
||||
"温馨咖啡馆室内,水彩插画风格,阳光透过窗户洒入,窗台上摆放绿植,柔和温暖的氛围"
|
||||
|
||||
"极简皮革钱包产品照,白色干净背景,棚拍灯光,对焦清晰,商业摄影风格"
|
||||
```
|
||||
|
||||
**提示词技巧:**
|
||||
|
||||
- **明确指定风格** — "油画"、"水彩"、"数字艺术"、"照片写实"、"动漫"、"矢量插画"
|
||||
- **描述光线** — "戏剧性阴影"、"柔和漫射光"、"黄金时段"、"棚拍灯光"
|
||||
- **指定构图** — "竖拍人像"、"广角"、"特写"、"俯拍鸟瞰"
|
||||
- **加入质量词** — "高细节"、"4K"、"对焦清晰"、"专业品质"
|
||||
- **避免模糊描述** — "漂亮"、"好看"、"不错" 对结果帮助有限 —— 要具体描述你真正想要的内容
|
||||
|
||||
### 选择 AI 模型
|
||||
|
||||
LobeHub 提供多个 AI 画图模型,不同模型各有所长:
|
||||
|
||||

|
||||
|
||||
| 模型 | 最适合 |
|
||||
| -------------------- | ------------------ |
|
||||
| **DALL-E 3** | 写实照片、插画、精准遵循提示词 |
|
||||
| **GPT Image** | 高保真编辑、图像内文本渲染 |
|
||||
| **Flux** | 艺术风格、创意图像、快速生成 |
|
||||
| **Stable Diffusion** | 高度可定制,支持社区风格和微调模型 |
|
||||
| **Gemini Imagen** | 真实场景,整体构图能力强 |
|
||||
| **fal.ai 系列模型** | 多种专业风格,生成速度快 |
|
||||
|
||||
用同一个提示词尝试不同模型,找到最适合你使用场景的。
|
||||
|
||||
### 参考图片(可选)
|
||||
|
||||
如果你有参考图片,可以上传作为生成的参考。点击上传按钮或直接拖入参考图片即可。根据模型不同,可以上传多张参考图片。
|
||||
|
||||

|
||||
|
||||
参考图片有助于模型理解你期望的风格、构图或配色方案 —— 配合提示词描述(例如替换背景、更换服饰),许多模型还支持基于参考图的**编辑**。
|
||||
|
||||
### 配置生成参数
|
||||
|
||||
右侧配置面板会展示当前模型支持的全部参数。常见控件:
|
||||
|
||||
- **比例(Aspect Ratio)** — `1:1`、`16:9`、`9:16`、`4:3`、`3:2`。可锁定比例或解锁自由调整。
|
||||
- **尺寸 / 分辨率** — 选择预设(`512px`、`1K`、`2K`、`4K`),或直接设定宽 × 高。
|
||||
- **生成数量** — 一次生成 1–4 张变体。
|
||||
- **质量** — 标准 / 高清(取决于模型)。
|
||||
- **Seed(随机种子)** — 随机以获得多样性,或粘贴固定 seed 复现之前的结果。
|
||||
- **Steps / 引导强度(CFG)** — 调节速度 vs 质量、提示词遵循程度的权衡。
|
||||
- **水印** — 在支持的模型上开启或关闭。
|
||||
- **联网搜索** / **提示词扩写** — 让 LLM 在生成前为你的提示词补充最新参考信息。
|
||||
|
||||
**比例速查:**
|
||||
|
||||
- **1:1** — 社交媒体发帖、头像
|
||||
- **16:9** — 宽屏、演示文稿、横幅
|
||||
- **9:16** — 手机屏幕、动态、竖屏视频
|
||||
- **4:3** — 通用用途、旧显示格式
|
||||
- **3:2** — 摄影标准、打印
|
||||
|
||||
### 查看和下载图片
|
||||
|
||||
图像生成完成后,会显示在生成流中。你可以:
|
||||
|
||||
- 点击任意图片查看全尺寸预览
|
||||
- 下载、复制 seed、复制提示词,或在新一轮生成中复用完整参数
|
||||
- 删除单张图片或整批
|
||||
|
||||

|
||||
|
||||
## 视频生成
|
||||
|
||||
视频工作区与图像工作区结构一致 —— 同样以提示词为先、同样的配置面板、同样的生成流 —— 只是参数针对动态画面做了调整。
|
||||
|
||||
### 输入提示词
|
||||
|
||||
描述**场景、运动和镜头**,不只是主体。模型对动词和镜头语言更敏感。
|
||||
|
||||
```
|
||||
"金色时分一只红狐在新鲜雪地上小跑,呼气在冷空气中清晰可见,缓慢跟拍镜头,电影感"
|
||||
|
||||
"宇航员漂入色彩斑斓的星云,缓慢推进镜头,梦幻氛围,柔和的体积光"
|
||||
|
||||
"咖啡杯被慢动作微距倒入,蒸汽升腾,浅景深,商业产品镜头"
|
||||
```
|
||||
|
||||
**视频提示词技巧:**
|
||||
|
||||
- **明确描述运动** — "缓慢跟拍"、"推进"、"手持"、"静态远景"、"向左横摇"
|
||||
- **设置时间推进** — "起初有雾随后散去"、"门缓缓打开"
|
||||
- **借用电影语言** — "浅景深"、"变形宽银幕镜头眩光"、"黄金时段"
|
||||
- **保持焦点** — 一个镜头一个核心动作往往比塞进多个动作效果更好
|
||||
|
||||
### 选择 AI 模型
|
||||
|
||||
LobeHub 接入了主流的文生视频与图生视频提供商:
|
||||
|
||||
| 模型 | 最适合 |
|
||||
| ------------------------------ | ------------------------------ |
|
||||
| **OpenAI Sora 2 / Sora 2 Pro** | 连贯的多秒镜头,强场景理解能力 |
|
||||
| **Google Veo 3 / 3.1** | 真实运动质感,原生音频生成,电影级画面 |
|
||||
| **Kling V3** | 高质量运动表现,支持图生视频和 omni-video |
|
||||
| **MiniMax Hailuo 2.3** | 快速文生视频,表现力强的人物 |
|
||||
| **Qwen / Wan** | 文生视频,对中文提示词理解强 |
|
||||
| **fal.ai 系列模型** | 多种专业模型,出片快 |
|
||||
|
||||
不同模型支持的参数不同,切换模型时配置面板会自动更新。
|
||||
|
||||
### 起始帧与结束帧(可选)
|
||||
|
||||
许多视频模型支持图像条件输入:
|
||||
|
||||
- **起始帧(Start Frame)** —— 上传一张图作为视频的第一帧。非常适合把图像工作区生成的静帧动起来。
|
||||
- **结束帧(End Frame)** —— 上传一张图作为视频的最后一帧。必须先设置起始帧。
|
||||
|
||||
设置起始帧后,提示词占位文案会变为"描述你想要基于该图像生成的场景"。
|
||||
|
||||
### 配置生成参数
|
||||
|
||||
参数因模型而异,常见包括:
|
||||
|
||||
- **时长(Duration)** —— 视频长度(秒),取决于模型(如 4s / 6s / 8s)。
|
||||
- **比例** —— `16:9`、`9:16`、`1:1`、`4:3`、`3:4`、`21:9`。
|
||||
- **分辨率** —— `480p`、`720p`、`1080p`。
|
||||
- **固定镜头(Fixed Camera)** —— 锁定镜头不动,而非让模型自由运镜。
|
||||
- **生成音频(Generate Audio)** —— 同步生成配音(取决于模型,例如 Veo)。
|
||||
- **Seed** —— 随机或固定以复现结果。
|
||||
- **水印** —— 在支持的模型上开启或关闭。
|
||||
- **联网搜索** / **提示词扩写** —— 与图像流程相同的 LLM 辅助扩写。
|
||||
|
||||
### 查看和下载视频
|
||||
|
||||
生成的视频会出现在生成流中并可直接内嵌播放。你可以:
|
||||
|
||||
- 播放、暂停、拖动进度
|
||||
- 下载视频
|
||||
- 生成失败时复制错误信息到剪贴板
|
||||
- 删除单条视频或整批
|
||||
|
||||
"🎁 今日剩余 N 条免费视频"角标显示你的免费额度;用完后每次生成将按额度扣费。
|
||||
|
||||
## 获得更好结果的技巧
|
||||
|
||||
**迭代优化提示词** —— 如果第一次的结果不够理想,每次只调整一个要素,而不是重写整个提示词。可以增加细节、改变风格词,或指定你不想要的内容。
|
||||
|
||||
**使用参考图或起始帧** —— 上传参考能帮助模型匹配你期望的风格、配色、构图,或者 —— 对视频而言 —— 你想要的起始画面。
|
||||
|
||||
**多变体对比** —— 一次生成多张图片,或用相同 seed + 微调提示词重生视频。AI 生成本身具有随机性 —— 不同变体的质量可能差异明显。
|
||||
|
||||
**根据任务选模型** —— 产品照和写实场景选写实系模型(DALL-E 3、Flux、Imagen);艺术插画选风格化模型;电影感运动镜头选 Veo 或 Sora;人物为主的短片选 Kling 或 Hailuo。
|
||||
|
||||
**串联图像 → 视频** —— 先在图像工作区生成满意的静帧,再把它作为起始帧送入视频工作区,让它动起来。
|
||||
|
||||
<Cards>
|
||||
<Card href={'/zh/docs/usage/getting-started/resource'} title={'资源库'} />
|
||||
|
||||
<Card href={'/zh/docs/usage/getting-started/vision'} title={'视觉与图像理解'} />
|
||||
|
||||
<Card href={'/zh/docs/usage/providers'} title={'AI 提供商'} />
|
||||
</Cards>
|
||||
@@ -1,117 +0,0 @@
|
||||
---
|
||||
title: Image Generation
|
||||
description: >-
|
||||
Create high-quality images from text descriptions using AI models like DALL-E
|
||||
3, Flux, and more. Learn how to write effective prompts and choose the right
|
||||
model.
|
||||
tags:
|
||||
- LobeHub
|
||||
- Image Generation
|
||||
- AI Drawing
|
||||
- DALL-E
|
||||
- Text to Image
|
||||
- Prompt Writing
|
||||
---
|
||||
|
||||
# Image Generation
|
||||
|
||||
Describe what you want — LobeHub turns text into images. Product prototypes, design inspiration, illustrations, or creative exploration: choose a model, set your parameters, and get high-quality images in seconds. All generated images are automatically saved to your Resource Library.
|
||||
|
||||
## Get Started
|
||||
|
||||
Click **Drawing** on the LobeHub main interface to open the image generation page.
|
||||
|
||||
## Enter a Prompt
|
||||
|
||||
Describe the image you want in the input box. The more specific your description, the more accurate the result.
|
||||
|
||||
**Effective prompt structure:**
|
||||
|
||||
```
|
||||
[Subject] [Style/Medium] [Setting/Background] [Lighting] [Mood] [Technical details]
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
"A futuristic city skyline at sunset, digital art, cyberpunk style, neon lights reflecting on wet streets, cinematic lighting, 4K detail"
|
||||
|
||||
"A cozy coffee shop interior, watercolor illustration, warm golden light streaming through windows, potted plants on windowsills, soft and inviting atmosphere"
|
||||
|
||||
"A product photo of a minimalist leather wallet on a clean white background, studio lighting, sharp focus, commercial photography style"
|
||||
```
|
||||
|
||||
**Prompt tips:**
|
||||
|
||||
- **Be specific about style** — "oil painting", "watercolor", "digital art", "photorealistic", "anime", "vector illustration"
|
||||
- **Describe lighting** — "dramatic shadows", "soft diffused light", "golden hour", "studio lighting"
|
||||
- **Specify composition** — "portrait view", "wide angle", "close-up", "bird's eye view"
|
||||
- **Add quality modifiers** — "high detail", "4K", "sharp focus", "professional quality"
|
||||
- **Avoid vagueness** — "beautiful", "nice", "good" add little — describe what you actually want
|
||||
|
||||
## Choose an AI Model
|
||||
|
||||
LobeHub offers multiple AI image generation models. Different models have different strengths:
|
||||
|
||||

|
||||
|
||||
| Model | Best For |
|
||||
| -------------------- | ------------------------------------------------------------- |
|
||||
| **DALL-E 3** | Realistic photos, illustrations, following prompts accurately |
|
||||
| **Flux** | Artistic styles, creative images, fast generation |
|
||||
| **Stable Diffusion** | Highly customizable, community styles and fine-tuned models |
|
||||
| **fal.ai models** | Various specialized styles and fast generation |
|
||||
|
||||
Try different models with the same prompt to see which gives the best results for your use case.
|
||||
|
||||
## Select Reference Images (Optional)
|
||||
|
||||
If you have reference images, upload them to guide the generation process. Click the upload button or drag and drop your reference images directly. You can upload multiple reference images.
|
||||
|
||||

|
||||
|
||||
Reference images help the model understand your desired style, composition, or color palette.
|
||||
|
||||
## Choose Image Aspect Ratio
|
||||
|
||||
Select an aspect ratio based on your intended use:
|
||||
|
||||
- **1:1** — Social media posts, profile pictures
|
||||
- **16:9** — Widescreen, presentations, banners
|
||||
- **9:16** — Mobile screens, stories, reels
|
||||
- **4:3** — General use, older display formats
|
||||
- **3:2** — Photography standard, prints
|
||||
|
||||
## Set Number of Images
|
||||
|
||||
Choose how many images to generate in one go. Generating multiple images at once gives you variations to choose from. Start with 2–4 to find the best result.
|
||||
|
||||
## View and Download Images
|
||||
|
||||
Once generated, images appear on the drawing page. You can:
|
||||
|
||||
- Preview any image at full size by clicking it
|
||||
- Select favorites and download them
|
||||
- Share directly from the image viewer
|
||||
|
||||
All generated images are automatically saved to your Resource Library.
|
||||
|
||||

|
||||
|
||||
## Tips for Better Results
|
||||
|
||||
**Iterate on prompts** — If the first result isn't quite right, adjust one element at a time rather than rewriting the whole prompt. Add more detail, change the style descriptor, or specify what you don't want.
|
||||
|
||||
**Use a reference image** — Uploading a reference image with your prompt helps the model match your intended style, color palette, or composition.
|
||||
|
||||
**Try multiple variations** — Generate 4+ images at once and pick the best one. AI image generation has inherent randomness — some variations will be significantly better than others.
|
||||
|
||||
**Match model to task** — Use photorealistic models (DALL-E 3, Flux) for product photos and realistic scenes; use style-focused models for artistic illustrations.
|
||||
|
||||
<Cards>
|
||||
<Card href={'/docs/usage/getting-started/resource'} title={'Resource Library'} />
|
||||
|
||||
<Card href={'/docs/usage/getting-started/vision'} title={'Vision & Image Understanding'} />
|
||||
|
||||
<Card href={'/docs/usage/providers'} title={'AI Providers'} />
|
||||
</Cards>
|
||||
@@ -1,114 +0,0 @@
|
||||
---
|
||||
title: 图像生成
|
||||
description: 使用 DALL-E 3、Flux 等 AI 模型,通过文字描述生成高质量图像。学习如何编写有效的提示词并选择合适的模型。
|
||||
tags:
|
||||
- LobeHub
|
||||
- 图像生成
|
||||
- AI 画图
|
||||
- DALL-E
|
||||
- 文字生成图像
|
||||
- 提示词写作
|
||||
---
|
||||
|
||||
# 图像生成
|
||||
|
||||
用文字描述你想要的内容 ——LobeHub 帮你把想法变成图像。产品原型、设计灵感、插图配图、创意探索:选择模型、设置参数,几秒钟内获得高质量图像。生成的图片会自动保存到你的资源库。
|
||||
|
||||
## 开始画图
|
||||
|
||||
在 LobeHub 主界面点击**绘画**板块,进入画图页面。
|
||||
|
||||
## 输入提示词
|
||||
|
||||
在输入框中描述你想要的图像。描述越具体,结果越符合预期。
|
||||
|
||||
**有效的提示词结构:**
|
||||
|
||||
```
|
||||
[主体] [风格/媒介] [场景/背景] [光线] [氛围] [技术细节]
|
||||
```
|
||||
|
||||
示例:
|
||||
|
||||
```
|
||||
"赛博朋克风格的未来城市天际线,日落时分,霓虹灯在湿润街道上的倒影,数字艺术,电影级光线,4K 细节"
|
||||
|
||||
"温馨咖啡馆室内,水彩插画风格,阳光透过窗户洒入,窗台上摆放绿植,柔和温暖的氛围"
|
||||
|
||||
"极简皮革钱包产品照,白色干净背景,棚拍灯光,对焦清晰,商业摄影风格"
|
||||
```
|
||||
|
||||
**提示词技巧:**
|
||||
|
||||
- **明确指定风格** — "油画"、"水彩"、"数字艺术"、"照片写实"、"动漫"、"矢量插画"
|
||||
- **描述光线** — "戏剧性阴影"、"柔和漫射光"、"黄金时段"、"棚拍灯光"
|
||||
- **指定构图** — "竖拍人像"、"广角"、"特写"、"俯拍鸟瞰"
|
||||
- **加入质量词** — "高细节"、"4K"、"对焦清晰"、"专业品质"
|
||||
- **避免模糊描述** — "漂亮"、"好看"、"不错" 对结果帮助有限 —— 要具体描述你真正想要的内容
|
||||
|
||||
## 选择 AI 模型
|
||||
|
||||
LobeHub 提供多个 AI 画图模型,不同模型各有所长:
|
||||
|
||||

|
||||
|
||||
| 模型 | 最适合 |
|
||||
| -------------------- | ----------------- |
|
||||
| **DALL-E 3** | 写实照片、插画、精准遵循提示词 |
|
||||
| **Flux** | 艺术风格、创意图像、快速生成 |
|
||||
| **Stable Diffusion** | 高度可定制,支持社区风格和微调模型 |
|
||||
| **fal.ai 系列模型** | 多种专业风格,生成速度快 |
|
||||
|
||||
用同一个提示词尝试不同模型,找到最适合你使用场景的。
|
||||
|
||||
## 选择参考图片(可选)
|
||||
|
||||
如果你有参考图片,可以上传作为生成的参考。点击上传按钮或直接拖入参考图片即可。可以上传多张参考图片。
|
||||
|
||||

|
||||
|
||||
参考图片有助于模型理解你期望的风格、构图或配色方案。
|
||||
|
||||
## 选择图片比例
|
||||
|
||||
根据使用场景选择合适的比例:
|
||||
|
||||
- **1:1** — 社交媒体发帖、头像
|
||||
- **16:9** — 宽屏、演示文稿、横幅
|
||||
- **9:16** — 手机屏幕、动态、竖屏视频
|
||||
- **4:3** — 通用用途、旧显示格式
|
||||
- **3:2** — 摄影标准、打印
|
||||
|
||||
## 设置生成数量
|
||||
|
||||
选择一次生成多少张图片。一次生成多张可以获得不同变体供你选择。建议从 2–4 张开始,从中挑选最佳结果。
|
||||
|
||||
## 查看和下载图片
|
||||
|
||||
图像生成完成后,会显示在画图页面。你可以:
|
||||
|
||||
- 点击任意图片查看全尺寸预览
|
||||
- 选择满意的图片并下载
|
||||
- 在图片查看器中直接分享
|
||||
|
||||
生成的图片会自动保存到你的资源库。
|
||||
|
||||

|
||||
|
||||
## 获得更好结果的技巧
|
||||
|
||||
**迭代优化提示词** — 如果第一次的结果不够理想,每次只调整一个要素,而不是重写整个提示词。可以增加细节、改变风格词,或指定你不想要的内容。
|
||||
|
||||
**使用参考图片** — 上传参考图配合提示词,帮助模型匹配你期望的风格、配色或构图。
|
||||
|
||||
**多变体对比** — 一次生成 4 张以上,从中挑选最佳。AI 图像生成本身具有随机性 —— 不同变体的质量可能差异明显。
|
||||
|
||||
**根据任务选模型** — 产品照和写实场景选写实系模型(DALL-E 3、Flux);艺术插画选风格化模型。
|
||||
|
||||
<Cards>
|
||||
<Card href={'/zh/docs/usage/getting-started/resource'} title={'资源库'} />
|
||||
|
||||
<Card href={'/zh/docs/usage/getting-started/vision'} title={'视觉与图像理解'} />
|
||||
|
||||
<Card href={'/zh/docs/usage/providers'} title={'AI 提供商'} />
|
||||
</Cards>
|
||||
@@ -281,5 +281,5 @@ When answering product questions:
|
||||
|
||||
<Card href={'/docs/usage/getting-started/page'} title={'Pages'} />
|
||||
|
||||
<Card href={'/docs/usage/getting-started/image-generation'} title={'Image Generation'} />
|
||||
<Card href={'/docs/usage/getting-started/generation'} title={'Image & Video Generation'} />
|
||||
</Cards>
|
||||
|
||||
@@ -106,5 +106,5 @@ tags:
|
||||
|
||||
<Card href={'/zh/docs/usage/getting-started/page'} title={'文稿'} />
|
||||
|
||||
<Card href={'/zh/docs/usage/getting-started/image-generation'} title={'图像生成'} />
|
||||
<Card href={'/zh/docs/usage/getting-started/generation'} title={'图像与视频生成'} />
|
||||
</Cards>
|
||||
|
||||
@@ -210,7 +210,7 @@ Other providers may also offer vision models — check the model's capability ta
|
||||
<Cards>
|
||||
<Card href={'/docs/usage/getting-started/resource'} title={'Resource Library'} />
|
||||
|
||||
<Card href={'/docs/usage/getting-started/image-generation'} title={'Image Generation'} />
|
||||
<Card href={'/docs/usage/getting-started/generation'} title={'Image & Video Generation'} />
|
||||
|
||||
<Card href={'/docs/usage/providers'} title={'AI Providers'} />
|
||||
</Cards>
|
||||
|
||||
@@ -147,7 +147,7 @@ LobeHub 支持视觉功能 —— 助理能够 "看见" 并理解你分享的图
|
||||
<Cards>
|
||||
<Card href={'/zh/docs/usage/getting-started/resource'} title={'资源库'} />
|
||||
|
||||
<Card href={'/zh/docs/usage/getting-started/image-generation'} title={'图像生成'} />
|
||||
<Card href={'/zh/docs/usage/getting-started/generation'} title={'图像与视频生成'} />
|
||||
|
||||
<Card href={'/zh/docs/usage/providers'} title={'AI 提供商'} />
|
||||
</Cards>
|
||||
|
||||
@@ -234,7 +234,7 @@
|
||||
"exportType.allAgentWithMessage": "تصدير جميع الوكلاء والرسائل",
|
||||
"exportType.globalSetting": "تصدير الإعدادات العامة",
|
||||
"feedback": "ملاحظات",
|
||||
"feedback.emailContact": "يمكنك أيضًا مراسلتنا عبر البريد الإلكتروني على {{email}}",
|
||||
"feedback.emailContact": "يمكنك أيضًا مراسلتنا عبر البريد الإلكتروني على <email>{{email}}</email>",
|
||||
"feedback.errors.fileTooLarge": "الملف يتجاوز الحجم المسموح به (5 ميغابايت)",
|
||||
"feedback.errors.submitFailed": "فشل في الإرسال. حاول مرة أخرى.",
|
||||
"feedback.errors.teamNotFound": "خطأ في التكوين",
|
||||
|
||||
@@ -478,6 +478,8 @@
|
||||
"notification.item.agent_cron_job_failed": "فشل المهمة المجدولة",
|
||||
"notification.item.image_generation_completed": "اكتمل إنشاء الصورة",
|
||||
"notification.item.video_generation_completed": "اكتمل إنشاء الفيديو",
|
||||
"notification.push.desc": "إرسال إشعارات دفع إلى أجهزتك المحمولة (يتطلب تطبيق LobeHub المحمول)",
|
||||
"notification.push.title": "إشعارات الدفع للجوال",
|
||||
"notification.title": "قنوات الإشعارات",
|
||||
"platformAgentConfig.availability.available": "متاح",
|
||||
"platformAgentConfig.availability.checking": "جارٍ التحقق...",
|
||||
|
||||
@@ -234,7 +234,7 @@
|
||||
"exportType.allAgentWithMessage": "Експортиране на всички агенти и съобщения",
|
||||
"exportType.globalSetting": "Експортиране на глобални настройки",
|
||||
"feedback": "Обратна връзка",
|
||||
"feedback.emailContact": "Можете също да ни пишете на {{email}}",
|
||||
"feedback.emailContact": "Можете също да ни пишете на <email>{{email}}</email>",
|
||||
"feedback.errors.fileTooLarge": "Файлът надвишава 5MB",
|
||||
"feedback.errors.submitFailed": "Изпращането не бе успешно. Опитайте отново.",
|
||||
"feedback.errors.teamNotFound": "Грешка в конфигурацията",
|
||||
|
||||
@@ -478,6 +478,8 @@
|
||||
"notification.item.agent_cron_job_failed": "Планираната задача не успя",
|
||||
"notification.item.image_generation_completed": "Генерирането на изображение завърши",
|
||||
"notification.item.video_generation_completed": "Генерирането на видео завърши",
|
||||
"notification.push.desc": "Изпращайте push известия до вашите мобилни устройства (изисква се мобилното приложение LobeHub)",
|
||||
"notification.push.title": "Мобилни push известия",
|
||||
"notification.title": "Канали за известия",
|
||||
"platformAgentConfig.availability.available": "Наличен",
|
||||
"platformAgentConfig.availability.checking": "Проверка...",
|
||||
|
||||
@@ -234,7 +234,7 @@
|
||||
"exportType.allAgentWithMessage": "Alle Agenten und Nachrichten exportieren",
|
||||
"exportType.globalSetting": "Globale Einstellungen exportieren",
|
||||
"feedback": "Feedback",
|
||||
"feedback.emailContact": "Sie können uns auch per E-Mail unter {{email}} erreichen",
|
||||
"feedback.emailContact": "Sie können uns auch per E-Mail unter <email>{{email}}</email> erreichen",
|
||||
"feedback.errors.fileTooLarge": "Datei überschreitet 5 MB",
|
||||
"feedback.errors.submitFailed": "Senden fehlgeschlagen. Bitte versuchen Sie es erneut.",
|
||||
"feedback.errors.teamNotFound": "Konfigurationsfehler",
|
||||
|
||||
@@ -478,6 +478,8 @@
|
||||
"notification.item.agent_cron_job_failed": "Geplanter Task fehlgeschlagen",
|
||||
"notification.item.image_generation_completed": "Bilderstellung abgeschlossen",
|
||||
"notification.item.video_generation_completed": "Videoerstellung abgeschlossen",
|
||||
"notification.push.desc": "Senden Sie Push-Benachrichtigungen an Ihre mobilen Geräte (LobeHub Mobile App erforderlich)",
|
||||
"notification.push.title": "Mobile Push-Benachrichtigungen",
|
||||
"notification.title": "Benachrichtigungskanäle",
|
||||
"platformAgentConfig.availability.available": "Verfügbar",
|
||||
"platformAgentConfig.availability.checking": "Überprüfen...",
|
||||
|
||||
@@ -151,7 +151,9 @@
|
||||
"heatmaps.months.oct": "Oct",
|
||||
"heatmaps.months.sep": "Sep",
|
||||
"heatmaps.tooltip": "{{count}} messages were created on {{date}}",
|
||||
"heatmaps.tooltipTokens": "{{count}} tokens were used on {{date}}",
|
||||
"heatmaps.totalCount": "A total of {{count}} messages sent in the past year",
|
||||
"heatmaps.totalCountTokens": "A total of {{count}} tokens used in the past year",
|
||||
"login": "Log In",
|
||||
"loginGuide.f1": "Get free usage",
|
||||
"loginGuide.f2": "Sync messages across devices",
|
||||
@@ -211,6 +213,11 @@
|
||||
"stats.days": "days",
|
||||
"stats.empty.desc": "Please accumulate more chat data to view",
|
||||
"stats.empty.title": "No Data",
|
||||
"stats.heatmapStats.currentStreak": "Current Streak",
|
||||
"stats.heatmapStats.longestStreak": "Longest Streak",
|
||||
"stats.heatmapStats.longestTask": "Longest Task",
|
||||
"stats.heatmapStats.peakTokens": "Peak Daily Tokens",
|
||||
"stats.heatmapStats.totalTokens": "Total Tokens",
|
||||
"stats.lastYearActivity": "Activity in the past year",
|
||||
"stats.loginGuide.f1": "Get free usage",
|
||||
"stats.loginGuide.f2": "Sync messages across devices",
|
||||
@@ -222,6 +229,7 @@
|
||||
"stats.modelsRank.right": "Messages",
|
||||
"stats.modelsRank.title": "Model Usage Rank",
|
||||
"stats.share.title": "My AI Activity Index",
|
||||
"stats.tokens": "Tokens",
|
||||
"stats.topics": "Topics",
|
||||
"stats.topicsRank.left": "Topic",
|
||||
"stats.topicsRank.right": "Messages",
|
||||
|
||||
@@ -217,6 +217,7 @@
|
||||
"heteroAgent.executionTarget.online": "Online",
|
||||
"heteroAgent.executionTarget.sandbox": "Cloud sandbox",
|
||||
"heteroAgent.executionTarget.sandboxDesc": "Run in an ephemeral cloud sandbox",
|
||||
"heteroAgent.executionTarget.downloadDesktop": "Get Desktop App",
|
||||
"heteroAgent.executionTarget.title": "Execution Device",
|
||||
"heteroAgent.executionTarget.unknownDevice": "Unknown device",
|
||||
"heteroAgent.fullAccess.label": "Full access",
|
||||
|
||||
@@ -234,7 +234,7 @@
|
||||
"exportType.allAgentWithMessage": "Export All Agents and Messages",
|
||||
"exportType.globalSetting": "Export Global Settings",
|
||||
"feedback": "Feedback",
|
||||
"feedback.emailContact": "You can also email us at {{email}}",
|
||||
"feedback.emailContact": "You can also email us at <email>{{email}}</email>",
|
||||
"feedback.errors.fileTooLarge": "File exceeds 5MB",
|
||||
"feedback.errors.submitFailed": "Submit failed. Try again.",
|
||||
"feedback.errors.teamNotFound": "Configuration error",
|
||||
|
||||
@@ -129,6 +129,14 @@
|
||||
"upload.desc": "Details: {{detail}}",
|
||||
"upload.fileOnlySupportInServerMode": "The current deployment mode does not support uploading non-image files. To upload files in {{ext}} format, please switch to server database deployment or use the {{cloud}} service.",
|
||||
"upload.networkError": "Please check your network connection and ensure that the file storage service's cross-origin configuration is correct.",
|
||||
"upload.storageBlock.monthlyCapReached": "Your monthly storage spending cap has been reached.",
|
||||
"upload.storageBlock.noPaymentMethod": "Please add a payment method to continue uploading.",
|
||||
"upload.storageBlock.overageNotEnabled": "Your storage is full. Enable pay-as-you-go billing to continue uploading.",
|
||||
"upload.storageBlock.subscriptionPastDue": "Your subscription payment has failed. Please update your payment method.",
|
||||
"upload.storageBlock.subscriptionUnpaid": "Your subscription has been suspended. Please resolve the outstanding payment.",
|
||||
"upload.storageBlock.upgradeRequired": "Your file storage has reached the plan limit. Please upgrade your plan or delete unused files.",
|
||||
"upload.storageBlock.viewPlans": "View plans",
|
||||
"upload.storageBlock.viewUsage": "View storage usage",
|
||||
"upload.storageLimitExceeded": "Your file storage has reached the plan limit. Please upgrade your plan or delete unused files to free up space.",
|
||||
"upload.title": "File upload failed. Please check your network connection or try again later",
|
||||
"upload.unknownError": "Error reason: {{reason}}",
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
"inbox.filterUnread": "Show unread only",
|
||||
"inbox.markAllRead": "Mark all as read",
|
||||
"inbox.title": "Notifications",
|
||||
"storage_overage_cap_reached": "Your storage pay-as-you-go monthly cap of ${{monthlyCap}} has been reached. Estimated charges this cycle are ${{estimatedCycleCharge}}. New uploads beyond your included storage will be blocked until the next billing cycle or until you increase the cap.",
|
||||
"storage_overage_cap_reached_title": "Storage pay-as-you-go cap reached",
|
||||
"video_generation_completed": "Your video \"{{prompt}}\" is ready.",
|
||||
"video_generation_completed_title": "Video generation completed"
|
||||
}
|
||||
|
||||
@@ -494,6 +494,7 @@
|
||||
"myAgents.status.published": "Published",
|
||||
"myAgents.status.unpublished": "Unpublished",
|
||||
"myAgents.title": "My Published Agents",
|
||||
"notification.category.billing.title": "Billing",
|
||||
"notification.category.generation.title": "Generation",
|
||||
"notification.category.schedule.title": "Scheduled tasks",
|
||||
"notification.email.desc": "Receive email notifications when important events occur",
|
||||
@@ -503,7 +504,10 @@
|
||||
"notification.inbox.title": "Inbox Notifications",
|
||||
"notification.item.agent_cron_job_failed": "Scheduled task failed",
|
||||
"notification.item.image_generation_completed": "Image generation completed",
|
||||
"notification.item.storage_overage_cap_reached": "Storage pay-as-you-go cap reached",
|
||||
"notification.item.video_generation_completed": "Video generation completed",
|
||||
"notification.push.desc": "Send push notifications to your mobile devices (LobeHub mobile app required)",
|
||||
"notification.push.title": "Mobile Push Notifications",
|
||||
"notification.title": "Notification Channels",
|
||||
"platformAgentConfig.availability.available": "Available",
|
||||
"platformAgentConfig.availability.checking": "Checking...",
|
||||
@@ -846,6 +850,42 @@
|
||||
"storage.embeddings.used": "Vector Storage",
|
||||
"storage.title": "Data Storage",
|
||||
"storage.used": "Storage Usage",
|
||||
"storageOverage.addPaymentMethod": "Add payment method",
|
||||
"storageOverage.capUpdateFailed": "Failed to update monthly cap.",
|
||||
"storageOverage.capUpdated": "Monthly cap updated.",
|
||||
"storageOverage.capped": "Monthly spending cap reached. Overage uploads are paused.",
|
||||
"storageOverage.consent.billedTogether": "Billed together with your subscription",
|
||||
"storageOverage.consent.canDisable": "Can be disabled anytime",
|
||||
"storageOverage.consent.description": "Storage exceeding your plan quota will be charged:",
|
||||
"storageOverage.consent.enable": "Agree and enable",
|
||||
"storageOverage.consent.onlyOverage": "Only overage is charged",
|
||||
"storageOverage.consent.rate": "About {{limitedMonthlyRate}}/GB/month for a limited time (regular {{regularMonthlyRate}}).",
|
||||
"storageOverage.consent.title": "Enable Storage Pay-as-you-go",
|
||||
"storageOverage.currentPlanLocked.desc": "Your current plan does not support storage pay-as-you-go. After it ends, subscribe to a plan to enable it.",
|
||||
"storageOverage.desc": "Subscription plans can enable metered billing for storage beyond the included quota.",
|
||||
"storageOverage.disableConfirm.blockUploads": "If your storage is still above the included quota, new uploads will be blocked.",
|
||||
"storageOverage.disableConfirm.canEnableAgain": "You can enable storage pay-as-you-go again later.",
|
||||
"storageOverage.disableConfirm.confirm": "Disable",
|
||||
"storageOverage.disableConfirm.description": "After disabling, storage overage will no longer be billed.",
|
||||
"storageOverage.disableConfirm.existingCharges": "Charges already incurred in this billing cycle will still be included on your subscription invoice.",
|
||||
"storageOverage.disableConfirm.title": "Disable storage pay-as-you-go?",
|
||||
"storageOverage.disableFailed": "Failed to disable storage pay-as-you-go.",
|
||||
"storageOverage.disabled": "Storage pay-as-you-go disabled.",
|
||||
"storageOverage.enableFailed": "Failed to enable storage pay-as-you-go.",
|
||||
"storageOverage.enabled": "Storage pay-as-you-go enabled.",
|
||||
"storageOverage.monthlyCap": "Monthly Spending Cap",
|
||||
"storageOverage.monthlyCapDesc": "Stop overage uploads after this cap is reached in the current billing cycle. Leave empty for no cap.",
|
||||
"storageOverage.noPaymentMethod": "Please add a payment method to enable storage pay-as-you-go.",
|
||||
"storageOverage.rate": "About {{limitedMonthlyRate}}/GB/month for a limited time (regular {{regularMonthlyRate}}).",
|
||||
"storageOverage.subscriptionRequired.action": "View plans",
|
||||
"storageOverage.subscriptionRequired.desc": "Subscribe to a plan to enable storage pay-as-you-go for overage usage.",
|
||||
"storageOverage.title": "File Storage Pay-as-you-go",
|
||||
"storageOverage.toggle": "Enable overage billing",
|
||||
"storageOverage.unlimited": "Unlimited",
|
||||
"storageOverage.usage.current": "Usage",
|
||||
"storageOverage.usage.estimatedCharge": "Est. Cycle Charge",
|
||||
"storageOverage.usage.incurredCharge": "Incurred This Cycle",
|
||||
"storageOverage.usage.overage": "Overage",
|
||||
"submitAgentModal.button": "Submit Agent",
|
||||
"submitAgentModal.identifier": "Agent Identifier",
|
||||
"submitAgentModal.metaMiss": "Please complete the agent information before submitting. It should include name, description, and tags",
|
||||
|
||||
@@ -257,6 +257,7 @@
|
||||
"plans.features.plugins": "Exclusive Premium Plugins",
|
||||
"plans.features.showAll": "View All Features",
|
||||
"plans.features.title": "Premium Features",
|
||||
"plans.fileStorage.storagePayAsYouGo": "Storage overages support pay-as-you-go billing",
|
||||
"plans.fileStorage.title": "File Storage",
|
||||
"plans.fileStorage.tooltip": "File storage for storing files, images, and other data",
|
||||
"plans.free": "Free",
|
||||
@@ -283,7 +284,7 @@
|
||||
"plans.navs.yearly": "Yearly",
|
||||
"plans.payonce.cancel": "Cancel",
|
||||
"plans.payonce.ok": "Confirm Selection",
|
||||
"plans.payonce.popconfirm": "After one-time payment, you can upgrade anytime but downgrade requires waiting for expiration. Please confirm your selection.",
|
||||
"plans.payonce.popconfirm": "After one-time payment, you can upgrade anytime, but downgrade requires waiting for expiration. Storage pay-as-you-go is not supported. Please confirm your selection.",
|
||||
"plans.payonce.tooltip": "One-time payment only supports upgrading to a higher tier or longer duration",
|
||||
"plans.payonce.upgradeOk": "Confirm Upgrade",
|
||||
"plans.payonce.upgradePopconfirm": "Remaining value from your current plan will be applied as a discount to the new plan.",
|
||||
@@ -394,10 +395,11 @@
|
||||
"referral.table.status.suspected": "Suspected Anomaly",
|
||||
"referral.table.title": "Referral History",
|
||||
"sessionCard.title": "Ready to leave the free plan? Upgrade to enjoy premium features.",
|
||||
"summary.desc": "This amount only includes subscription service expenses.",
|
||||
"summary.desc": "This amount includes your subscription fee and any storage overage charges for this billing period.",
|
||||
"summary.dueBy": "Due on {{date}}",
|
||||
"summary.nextPayment": "Your Next Payment",
|
||||
"summary.paymentInformation": "Billing Information",
|
||||
"summary.storageSettings": "Storage pay-as-you-go",
|
||||
"summary.title": "Billing Summary",
|
||||
"summary.usageThisMonth": "View your usage this month.",
|
||||
"summary.viewBillingHistory": "View Payment History",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"favorite": "Favorite",
|
||||
"filter.filter": "Filter",
|
||||
"filter.groupMode.byProject": "By project",
|
||||
"filter.groupMode.byStatus": "By status",
|
||||
"filter.groupMode.byTime": "By time",
|
||||
"filter.groupMode.flat": "Flat",
|
||||
"filter.organize": "Organize",
|
||||
@@ -37,6 +38,13 @@
|
||||
"filter.sortBy.createdAt": "Created time",
|
||||
"filter.sortBy.updatedAt": "Updated time",
|
||||
"groupTitle.byProject.noProject": "No directory",
|
||||
"groupTitle.byStatus.active": "Active",
|
||||
"groupTitle.byStatus.archived": "Archived",
|
||||
"groupTitle.byStatus.completed": "Completed",
|
||||
"groupTitle.byStatus.failed": "Failed",
|
||||
"groupTitle.byStatus.paused": "Paused",
|
||||
"groupTitle.byStatus.running": "Running",
|
||||
"groupTitle.byStatus.waitingForHuman": "Awaiting input",
|
||||
"groupTitle.byTime.month": "This Month",
|
||||
"groupTitle.byTime.today": "Today",
|
||||
"groupTitle.byTime.week": "This Week",
|
||||
|
||||
@@ -234,7 +234,7 @@
|
||||
"exportType.allAgentWithMessage": "Exportar todos los agentes y mensajes",
|
||||
"exportType.globalSetting": "Exportar configuración global",
|
||||
"feedback": "Comentarios",
|
||||
"feedback.emailContact": "También puedes enviarnos un correo electrónico a {{email}}",
|
||||
"feedback.emailContact": "También puedes enviarnos un correo electrónico a <email>{{email}}</email>",
|
||||
"feedback.errors.fileTooLarge": "El archivo supera los 5 MB",
|
||||
"feedback.errors.submitFailed": "Error al enviar. Inténtalo de nuevo.",
|
||||
"feedback.errors.teamNotFound": "Error de configuración",
|
||||
|
||||
@@ -478,6 +478,8 @@
|
||||
"notification.item.agent_cron_job_failed": "La tarea programada falló",
|
||||
"notification.item.image_generation_completed": "Generación de imagen completada",
|
||||
"notification.item.video_generation_completed": "Generación de vídeo completada",
|
||||
"notification.push.desc": "Envía notificaciones push a tus dispositivos móviles (se requiere la aplicación móvil LobeHub)",
|
||||
"notification.push.title": "Notificaciones Push Móviles",
|
||||
"notification.title": "Canales de Notificación",
|
||||
"platformAgentConfig.availability.available": "Disponible",
|
||||
"platformAgentConfig.availability.checking": "Comprobando...",
|
||||
|
||||
@@ -234,7 +234,7 @@
|
||||
"exportType.allAgentWithMessage": "خروجی تمام نمایندگان و پیامها",
|
||||
"exportType.globalSetting": "خروجی تنظیمات کلی",
|
||||
"feedback": "بازخورد",
|
||||
"feedback.emailContact": "همچنین میتوانید به ما ایمیل بزنید به {{email}}",
|
||||
"feedback.emailContact": "همچنین میتوانید به ما ایمیل بزنید به <email>{{email}}</email>",
|
||||
"feedback.errors.fileTooLarge": "فایل بیش از ۵ مگابایت است",
|
||||
"feedback.errors.submitFailed": "ارسال ناموفق بود. دوباره تلاش کنید.",
|
||||
"feedback.errors.teamNotFound": "خطای پیکربندی",
|
||||
|
||||
@@ -478,6 +478,8 @@
|
||||
"notification.item.agent_cron_job_failed": "وظیفه زمانبندیشده شکست خورد",
|
||||
"notification.item.image_generation_completed": "تولید تصویر با موفقیت انجام شد",
|
||||
"notification.item.video_generation_completed": "تولید ویدئو با موفقیت انجام شد",
|
||||
"notification.push.desc": "ارسال اعلانهای فشاری به دستگاههای موبایل شما (اپلیکیشن موبایل LobeHub مورد نیاز است)",
|
||||
"notification.push.title": "اعلانهای فشاری موبایل",
|
||||
"notification.title": "کانالهای اعلان",
|
||||
"platformAgentConfig.availability.available": "در دسترس",
|
||||
"platformAgentConfig.availability.checking": "در حال بررسی...",
|
||||
|
||||
@@ -234,7 +234,7 @@
|
||||
"exportType.allAgentWithMessage": "Exporter tous les agents et les messages",
|
||||
"exportType.globalSetting": "Exporter les paramètres globaux",
|
||||
"feedback": "Retour",
|
||||
"feedback.emailContact": "Vous pouvez également nous envoyer un e-mail à {{email}}",
|
||||
"feedback.emailContact": "Vous pouvez également nous envoyer un e-mail à <email>{{email}}</email>",
|
||||
"feedback.errors.fileTooLarge": "Le fichier dépasse 5 Mo",
|
||||
"feedback.errors.submitFailed": "Échec de l’envoi. Veuillez réessayer.",
|
||||
"feedback.errors.teamNotFound": "Erreur de configuration",
|
||||
|
||||
@@ -478,6 +478,8 @@
|
||||
"notification.item.agent_cron_job_failed": "La tâche planifiée a échoué",
|
||||
"notification.item.image_generation_completed": "Génération d’image terminée",
|
||||
"notification.item.video_generation_completed": "Génération de vidéo terminée",
|
||||
"notification.push.desc": "Envoyez des notifications push à vos appareils mobiles (application mobile LobeHub requise)",
|
||||
"notification.push.title": "Notifications Push Mobiles",
|
||||
"notification.title": "Canaux de notification",
|
||||
"platformAgentConfig.availability.available": "Disponible",
|
||||
"platformAgentConfig.availability.checking": "Vérification...",
|
||||
|
||||
@@ -234,7 +234,7 @@
|
||||
"exportType.allAgentWithMessage": "Esporta tutti gli agenti e i messaggi",
|
||||
"exportType.globalSetting": "Esporta impostazioni globali",
|
||||
"feedback": "Feedback",
|
||||
"feedback.emailContact": "Puoi anche inviarci un'email a {{email}}",
|
||||
"feedback.emailContact": "Puoi anche inviarci un'email a <email>{{email}}</email>",
|
||||
"feedback.errors.fileTooLarge": "Il file supera i 5MB",
|
||||
"feedback.errors.submitFailed": "Invio non riuscito. Riprova.",
|
||||
"feedback.errors.teamNotFound": "Errore di configurazione",
|
||||
|
||||
@@ -478,6 +478,8 @@
|
||||
"notification.item.agent_cron_job_failed": "Attività pianificata non riuscita",
|
||||
"notification.item.image_generation_completed": "Generazione immagine completata",
|
||||
"notification.item.video_generation_completed": "Generazione video completata",
|
||||
"notification.push.desc": "Invia notifiche push ai tuoi dispositivi mobili (è richiesta l'app mobile LobeHub)",
|
||||
"notification.push.title": "Notifiche Push Mobile",
|
||||
"notification.title": "Canali di Notifica",
|
||||
"platformAgentConfig.availability.available": "Disponibile",
|
||||
"platformAgentConfig.availability.checking": "Verifica in corso...",
|
||||
|
||||
@@ -234,7 +234,7 @@
|
||||
"exportType.allAgentWithMessage": "すべてのアシスタントとメッセージをエクスポート",
|
||||
"exportType.globalSetting": "グローバル設定をエクスポート",
|
||||
"feedback": "フィードバック",
|
||||
"feedback.emailContact": "また、{{email}} にメールを送ることもできます",
|
||||
"feedback.emailContact": "また、<email>{{email}}</email> にメールを送ることもできます",
|
||||
"feedback.errors.fileTooLarge": "ファイルサイズが5MBを超えています",
|
||||
"feedback.errors.submitFailed": "送信に失敗しました。もう一度お試しください。",
|
||||
"feedback.errors.teamNotFound": "設定エラー",
|
||||
|
||||
@@ -478,6 +478,8 @@
|
||||
"notification.item.agent_cron_job_failed": "スケジュールされたタスクが失敗しました",
|
||||
"notification.item.image_generation_completed": "画像生成が完了しました",
|
||||
"notification.item.video_generation_completed": "動画生成が完了しました",
|
||||
"notification.push.desc": "モバイルデバイスにプッシュ通知を送信します(LobeHubモバイルアプリが必要です)",
|
||||
"notification.push.title": "モバイルプッシュ通知",
|
||||
"notification.title": "通知チャンネル",
|
||||
"platformAgentConfig.availability.available": "利用可能",
|
||||
"platformAgentConfig.availability.checking": "確認中...",
|
||||
|
||||
@@ -234,7 +234,7 @@
|
||||
"exportType.allAgentWithMessage": "모든 도우미 및 메시지 내보내기",
|
||||
"exportType.globalSetting": "전체 설정 내보내기",
|
||||
"feedback": "피드백 및 제안",
|
||||
"feedback.emailContact": "{{email}}로 이메일을 보내주실 수도 있습니다",
|
||||
"feedback.emailContact": "<email>{{email}}</email>로 이메일을 보내주실 수도 있습니다",
|
||||
"feedback.errors.fileTooLarge": "파일 크기가 5MB를 초과합니다",
|
||||
"feedback.errors.submitFailed": "제출에 실패했습니다. 다시 시도해 주세요.",
|
||||
"feedback.errors.teamNotFound": "설정 오류",
|
||||
|
||||
@@ -478,6 +478,8 @@
|
||||
"notification.item.agent_cron_job_failed": "예약된 작업이 실패했습니다",
|
||||
"notification.item.image_generation_completed": "이미지 생성 완료",
|
||||
"notification.item.video_generation_completed": "동영상 생성 완료",
|
||||
"notification.push.desc": "모바일 장치로 푸시 알림을 보냅니다 (LobeHub 모바일 앱 필요)",
|
||||
"notification.push.title": "모바일 푸시 알림",
|
||||
"notification.title": "알림 채널",
|
||||
"platformAgentConfig.availability.available": "사용 가능",
|
||||
"platformAgentConfig.availability.checking": "확인 중...",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user