Compare commits

...

66 Commits

Author SHA1 Message Date
lobehubbot 248a4dcab5 🔖 chore(release): release version v2.2.2 [skip ci] 2026-06-04 03:59:37 +00:00
Arvin Xu 6532cd1ee0 🚀 release: 20260604 (#15447)
# 🚀 LobeHub Release (20260604)

**Release Date:** June 4, 2026  
**Since v2.2.1:** 88 merged PRs · 11 contributors

> This week brings Execution Devices out of the lab — run agents and
Claude Code on any configured local or remote machine — alongside Claude
Opus 4.8, token-usage analytics, and Page sharing.

---

##  Highlights

- **Execution Devices** — Pick where an agent runs. Desktop and CLI
devices auto-register with a stable machine ID, route through the
gateway by channel, and surface a device switcher in the chat input. Run
remote Claude Code on a configured device, with a recent-directory
picker you can drag to reorder. (#15300, #15315, #15322, #15343, #15351,
#15371)
- **Claude Opus 4.8** — Day-one support for Anthropic's latest model.
(#15314)
- **Token-usage analytics** — A new token-usage mode on the activity
heatmap, backed by a denormalized topic usage/cost rollup so totals stay
accurate without recomputing from messages. (#15365, #15417, #15425)
- **Page sharing** — Share a Page through a dedicated document share
flow, plus new Workspace and Agent share tables. (#15309, #15439)
- **Self-iteration agents** — Agent Signal's execAgent migration lands a
server-runtime bridge, async memory writer, and a registered
self-iteration tool package, with a CLI trigger command for testing.
(#15360, #15364, #15392)
- **Knowledge search** — BM25 search now extends to file-backed
documents, and the portal ships an editable CodeMirror viewer for local
files with document highlighting. (#15247, #15298)

---

## 🏗️ Core Agent & Architecture

### Agent Signal & Runtime

- **execAgent migration** — Server-runtime bridge, completion
projection, async memory writer, and removal of the legacy
`executeSelfIteration` path. (#15392)
- Registered the self-iteration builtin tool package and restored the
three mode-specific self-iteration agent slugs. (#15202, #15364)
- Added a CLI trigger command with a golden-snapshot fixture for Agent
Signal. (#15360)
- **Skill priority** — Agent Builder now emits a skill-priority
instruction with matching server runtime. (#15409)
- Retry empty LLM completions instead of silently finishing the turn.
(#15355)
- Classify topic/agent/session foreign-key violations as
`ConversationParentMissing` for clearer recovery. (#15408)
- Persist canonical nested usage/performance on assistant messages, and
re-link orphan tool messages at the raw bucket write boundary. (#15359,
#15438)
- Guard `createAgent` against LLM double-encoded array fields. (#15381)

---

## 🖥️ Execution Devices & Gateway

- Auto-register desktop and CLI devices with a stable machine ID, and
add the `@lobechat/device-identity` package. (#15300, #15321)
- New Devices settings page behind the Execution Device Switcher lab,
with a device switcher shown for all agents in the chat input. (#15315,
#15371)
- `connectionId` + channel routing across the gateway client and device
list; preset the local device on the first LLM request for the 本机
target. (#15322, #15435)
- Run remote Claude Code on a configured device, with drag-to-reorder
recent-directory management and client renders for device tool results.
(#15343, #15351, #15437)
- Preserve content and state across gateway tool calls, and prevent
duplicate streaming from stale reconnects. (#15114, #15354)

---

## 🖥️ CLI & Desktop

- Preserve content/state for connect local file and shell tools; render
the `runCommand` tool result card. (#15441, #15442)
- New `lh topic view` command; CLI now auto-registers its device on
login, matching desktop. (#15340, #15377)
- Resolve CLI tools from the shell `PATH`, and clarify local command
session handling. (#15368, #15389)
- Relocate visual-ref helpers to `@lobechat/const` to fix a renderer
crash; upload `.blockmap` files to S3 for differential updates. (#15326,
#15369)
- Fix a market OAuth expiry that triggered the wrong re-login modal, and
kill dev child processes on parent shutdown. (#15246, #15290)

---

## 🗂️ Pages, Library & Knowledge

- Document share flow with business slot stubs, plus Workspace and Agent
share tables. (#15309, #15439)
- Export Agent profiles as Markdown, preserving an empty agent prompt on
export. (#15312, #15316)
- Editable CodeMirror viewer for local files with document highlighting;
BM25 search extended to file-backed documents. (#15247, #15298)
- Default new Agent-doc files to `.md` and preserve IME composition;
refresh folder data on slug switch and dedupe breadcrumb fetches.
(#15335, #15427)

---

## 💬 Chat & User Experience

- Group-by-status mode for the Topic sidebar; dropped the legacy
session→agentId compatibility path from Topic queries. (#15366, #15378)
- Restore editor focus after the file picker closes, and close the skill
dropdown before navigating to settings. (#15391, #15394)
- Strip markdown tokens from fallback Topic titles; keep an open
ActionBar popup when hovering another message. (#15303, #15372)
- Stabilize home starter loading and stop transliterating model names in
the home starter; show artifact source while streaming. (#15310, #15324,
#15386)
- Group the sidebar spacer with recents and agents. (#15373)

---

## 📊 Analytics, Tasks & Notifications

- Token-usage mode on the activity heatmap, backed by a denormalized
topic usage/cost rollup. (#15365, #15417, #15425)
- Push: new `PushChannel`, receipt cron, and `pushToken` tRPC API.
(#15233)
- Tasks now support file and image attachments. (#15141)

---

## 🧩 Models & Providers

- Support Claude Opus 4.8 and configurable model routing with starters.
(#15314, #15384)
- MiniMax M3: new model entry and an Anthropic video runtime. (#15380,
#15403)
- Add `intern-s2-preview` with `thinking_mode`, and `step-3.7-flash`
support. (#15308, #15317)
- Block disabling the official provider; fix default provider setup in
business mode. (#15379, #15382)

---

## 🎨 UI & Modals

- Migrate modals to `@lobehub/ui/base-ui` (LOBE-9711 + eval batch),
including the create-custom-model and feedback/changelog modals.
(#15401, #15416)
- Restructure confirmModal title and content across deletion flows;
polish the service-model form and migrate its Switch to base-ui.
(#15426, #15440)
- Wrap the BlueBubbles bridge config into a connection card; update
`@lobehub/ui` to v5.15.5. (#15325, #15342)

---

## 🔒 Reliability

- Replace hardcoded `session_context` values with template variables in
credentials. (#15352)
- Point `CHANGELOG_URL` to `/changelog`. (#15428)

---

## 👥 Contributors

Huge thanks to **11 contributors** who shipped **88 merged PRs** this
cycle.

@hezhijie0327 · @qybaihe · @sxjeru · @arvinxx · @Innei · @tjx666 ·
@LiJian · @sudongyuer · @cy948 · @rivertwilight · @AmAzing129

Plus @lobehubbot and renovate[bot] for maintenance.

---

**Full Changelog**: v2.2.1...release/weekly-20260604
2026-06-04 11:56:58 +08:00
Arvin Xu 72ea0f94f7 🐛 fix(cli): preserve content/state for connect local file/shell tools (#15442)
* 🐛 fix(cli): preserve content/state for connect local file/shell tools

Route file/shell tool calls in connect mode through LocalSystemExecutionRuntime
so the result carries formatted prompt `content` plus structured `state`, and
forward `state` over the gateway tool-call response — aligning the CLI with the
desktop gateway path (PR #15114).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(cli): preserve getCommandOutput timeout when polling running commands

Routing getCommandOutput through the runtime dropped the per-call/gateway
timeout: the CLI mapping didn't forward it and LocalSystemExecutionRuntime's
denormalizeParams stripped it before ShellProcessManager.getOutput, so polling
fell back to the 30s default and could block past the gateway budget. Carry
timeout through the runtime param type, denormalize, and the CLI mapping.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 01:28:11 +08:00
Arvin Xu a3a08c2395 🐛 fix(chat): re-link orphan tool messages at the raw bucket write boundary (#15438)
A fast hetero-agent (Claude Code) tool can have its parent assistant's
`tools[]` momentarily dropped (stale/out-of-order `replaceMessages` snapshot,
or an optimistic `updateMessage{tools}` on the wrong assistant during a step
boundary) while the `role:'tool'` row + parentId survive. Since conversation-
flow binds a tool into its assistant solely via `assistant.tools[].id`, the
tool then renders as a top-level orphan bubble (`inspector.orphanedToolCall`).

Fix at the RAW `dbMessagesMap` write boundary — shared by `replaceMessages`
and `internal_dispatchMessage` (the optimistic-update path) — so the Source of
Truth stays consistent for optimistic updates, not just the parsed display.
`reconcileAssistantToolLinks` re-attaches the missing `tools[]` entry for any
present tool row whose parentId resolves to an assistant in the same bucket;
it only acts on present rows (never resurrects deletions) and never removes or
reorders entries.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 00:59:12 +08:00
Arvin Xu 643ad16a5d 🐛 fix(github): render runCommand tool result card (#15441)
The github render/inspector were registered under the snake_case
`run_command` key, but the tool call emits the camelCase `runCommand`
apiName, so the lookup missed and fell back to the generic collapsed
pill. Register both casings so the custom card renders.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 00:33:28 +08:00
Arvin Xu 5761d20637 feat(db): add workspace and agent share table (#15439)
*  feat(db): add usage column to messages table

Promote token usage/cost out of `metadata.usage` into a dedicated
`messages.usage` jsonb column, with btree expression indexes on
`usage.cost` and `usage.totalTokens`. Additive only — no data backfill;
`metadata.usage` stays the source of truth during the transition.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(db): add agent share schema (picked from #15430)

Bring the agent-share schema layer over from #15430: new `agent_shares`
table + `topics.sender_id` column/index, schema relations and barrel
export. Migration renumbered to 0106 to sit after the usage column.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(db): add workspace schema (picked from #15414)

Bring over only the standalone `workspace.ts` schema from #15414 — the
workspaces / workspace_members / workspace_invitations / workspace_audit_logs
tables (self-contained, FK to users only). None of #15414's workspaceId
column additions across other tables are included. Migration is 0108-safe,
renumbered to 0107.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🗃️ chore(db): squash usage/agent-share/workspace into one migration

Collapse the three stacked migrations (0105 usage, 0106 agent_share,
0107 workspace) into a single idempotent 0105_add_usage_agent_share_workspace.
Schema source is unchanged; only the migration files/snapshot/journal are
consolidated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  test(db): add senderId to expected topic shape in create test

The picked agent-share schema added topics.senderId, so the created row
now returns it; update the two toEqual assertions accordingly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 00:24:09 +08:00
Innei fd3c6cf8fc 🐛 fix(ui): restructure confirmModal title and content across deletion flows (#15440)
🚸 fix(ui): restructure confirmModal title and content across deletion flows

Move long warning sentences from `title` into `content` and use short verb titles
("Delete", "Uninstall", "Wipe Data", etc.). Add `okText`/`cancelText` i18n for all
fixed sites so confirm buttons match the action language.

Covers topic/thread/agent/group/library/file/model/skill/storage delete flows.
2026-06-04 00:23:58 +08:00
Arvin Xu d81e5e703e feat(remote-device): add client renders for device tool results (#15437)
*  feat(remote-device): add client renders for listOnlineDevices and activateDevice

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🐛 fix(utils): make SVG event-handler stripping engine-independent

DOMPurify's FORBID_ATTR / SVG-profile allowlist path relies on the underlying DOM's
attribute + namespace handling, which differs across engines (jsdom vs happy-dom) and
DOMPurify versions — in some CI environments on* handlers on SVG-namespaced nodes slipped
through. Add a scoped uponSanitizeAttribute hook to drop every on* attribute deterministically,
and assert by security property instead of exact serialization to drop whitespace brittleness.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🐛 fix(remote-device): render activation failure content when no device state

activateDevice returns success:false with explanatory content but no error and no state when
the target is offline/unknown. The tool detail view only skips custom rendering when result.error
is set, so the custom renderer's `return null` rendered a blank result. Fall back to the failure
content so the user/model still sees the message.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🐛 fix(utils): deterministically scrub SVG on* handlers via post-pass

The DOMPurify uponSanitizeAttribute hook still failed in CI: <script> is removed (tag filtering)
but on* handlers survive, because the attribute-sanitization phase doesn't run for SVG-namespaced
nodes in CI's DOM engine — so the hook never fires. Replace it with an explicit regex scrub on the
serialized output, which strips every on* event-handler attribute independent of the DOM engine.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🔒 fix(utils): loop SVG on* scrub until stable to close recombination bypass

A single-pass regex replace can leave a fresh handler behind when removing one splices the
surrounding text back together (` on onclick="x"click="y"` → ` onclick="y"`) — the CodeQL
js/incomplete-multi-character-sanitization case. Repeat the scrub until the string stops changing
so no on*= token can survive. Adds a regression test for the recombination input.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 23:59:35 +08:00
Innei 2a4b6e4974 🐛 fix(agent-doc): default new files to .md and preserve IME composition (#15427)
* 🐛 fix(agent-doc): default new files to .md and preserve IME composition

- Append `.md` to newly-created agent documents; pre-select only the stem
  in the inline rename input so the extension stays intact.
- Wire `useIMECompositionEvent` on the explorer container so Enter pressed
  during IME composition (e.g. Chinese pinyin) no longer commits the
  half-formed name through pierre/trees' shadow-DOM input.

* 🐛 fix(agent-doc): use native capture listener for IME guard

React `onKeyDownCapture` can lose to pierre/trees' bubble handler in some
event ordering edge cases, and the original guard missed IMEs that report
`keyCode === 229` or fire Enter just after compositionend in the same task.

- Bind a native `keydown` capture listener on the container so we can
  inspect `composedPath()` and confirm the keydown originated inside the
  shadow-DOM rename input.
- Extend the IME guard with an `imeSessionRef` that stays true through one
  extra microtask after compositionend.
- Drop the React `onKeyDownCapture` prop in favour of the native listener.

*  revert(agent-doc): drop IME guard pending pierre/trees upstream fix

The inline rename input lives in pierre/trees' shadow DOM and we can't
reliably suppress its IME-composing Enter commit from the outside. Roll
back the local hack and track the issue upstream instead. The default
`.md` extension and stem-only selection on rename stay in place.

*  feat(agent-doc): preselect stem on inline rename too

Existing files entering inline rename (right-click → Rename, or F2) now
narrow the selection to the stem after pierre/trees' `input.select()`,
matching the new-file flow so the user never has to retype `.md`.

* 🐛 fix(agent-doc): preserve extension on filename collisions
2026-06-03 23:54:39 +08:00
Arvin Xu 2fb0970cf9 💄 style(stats): add token-usage mode to activity heatmap (#15425)
* 💄 feat(stats): ladder shorten number up to B and T tiers

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 feat(stats): move token summary below overview and surface cumulative tokens

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(stats): add 12px gap between overview cards and token summary

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(stats): move heatmap summary under the activity title

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 21:51:57 +08:00
Arvin Xu 7a93df9e44 ️ perf(device): preset local device on first LLM request for 本机 target (#15435)
* ️ perf(device): preset local device on first LLM request for 本机 target

When the desktop runs an agent against the local machine (executionTarget
'local'), resolve this desktop's own gateway deviceId client-side and pass it
as the run's `deviceId`. The server then presets `activeDeviceId` and injects
`lobe-local-system` into the very first LLM payload, skipping the extra
`activateDevice` round-trip the model was forced to make whenever more than one
device was online.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  test(device): cover local deviceId resolution in executeGatewayAgent

Asserts the client forwards this desktop's deviceId only for the local (本机)
target — including the unset-on-desktop fallback — and never for sandbox,
explicit remote device, or off-desktop runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(device): gate local-device binding on effective runtime mode

`resolveLocalDeviceId` defaulted an unset `agencyConfig.executionTarget` to
'local' and sent this desktop's deviceId. But the legacy ModeSelector writes
only `runtimeMode`, leaving executionTarget unset — so an explicit cloud/none
run would still get a deviceId, which the server turns into activeDeviceId and
injects lobe-local-system, wrongly routing a cloud run to the local machine.

Gate on `isLocalSystemEnabledById` (effective runtimeMode === 'local'), the
source of truth both selectors agree on.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🚨 fix(test): use import-type alias instead of inline import() type

Satisfies @typescript-eslint/consistent-type-imports (CI lint).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 20:31:46 +08:00
Arvin Xu d9673c3c41 ♻️ refactor(agent-signal): execAgent migration — serverRuntime bridge + completion projection + async memoryWriter + executeSelfIteration removal (#15392)
* 🚧 wip(agent-signal): S1 — self-iteration tools as static primitives, no side-channel

Rewrite all three self-iteration execAgent tool surfaces (review / reflection /
feedback-intent) as static, named primitives instead of reusing the dynamic
createServerToolSet / createToolSet factory (which carries the legacy
reserveOperation / receipt / completeOperation side channel the migration removes).

Package (builtin-tool-agent-signal):
- AgentSignalToolService.invoke (generic bag) → AgentSignalRuntimeService, a
  narrow named DB-primitive seam (skillManagement precedent). Artifact recorders
  echo their input; reads/mutations route to one primitive each. The runtime
  carries no dedupe / receipt / operation-state side channel — idempotency and
  receipt projection live on the completion path, not the tool call.

Server primitives (pure live-DB reads + writes, keyed to api names):
- review/server.ts createReviewRuntimePrimitives — proposal lifecycle + resource
  tools, parameterized by window scalars from the operation marker, reusing the
  existing snapshot/preflight/projection/brief helpers.
- tools/runtimePrimitives.ts createResourceRuntimePrimitives — the skill-read /
  skill-write / writeMemory surface shared by reflection and feedback-intent.
- No context blob and no getEvidenceDigest: evidence is embedded in the agent
  prompt, so tools only touch live state.

serverRuntimes: agentSignalReview / agentSignalReflection / agentSignalFeedbackIntent
thin factories wiring ToolExecutionContext → primitives → package runtime, all
registered. createServerToolSet / createToolSet left untouched (legacy
executeSelfIteration path, removed in S4).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🚧 wip(agent-signal): S2 — completion-path receipt projection from finalState

Replace the in-runtime receipt accumulator with finalState-driven projection on
the completion path. finalState is only in hand inside the completion lifecycle
(S3 final snapshots are write-only — get() is a null stub; the operation row has
no messages; prod webhook hooks strip finalState), so receipts must be projected
from the one point state exists.

- CompletionLifecycle.emitSignalEvents: extract the compact, kind-tagged tool
  outcomes from the terminal state (extractSelfIterationCompletionPayload) and
  carry them on the agent.execution.completed payload — only for marked
  self-iteration runs, never the full message history.
- completionPolicy: forward the payload to onSelfIterationCompleted.
- completion/buildSelfIterationReceipts: project mutations + artifacts into
  user-visible receipts, mirroring the legacy createReceipts kind/status/target
  mapping. Deterministic receipt ids (sourceId + tool call id) → idempotent
  re-projection; the store dedupes by id.
- completion/selfIterationCompletionHandler: build + persist receipts.
- orchestrator: wire the handler into createDefaultAgentSignalPolicies.
- agent-signal source type: add an opaque selfIteration field to the
  agent.execution.completed payload.

Inert until the dispatch side stamps the operation marker (S3 / S4): without a
marker the extractor returns undefined and the handler no-ops.

Tests: buildSelfIterationReceipts (5) + extractCompletionPayload (4); completion
policy + CompletionLifecycle + orchestrator suites green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🚧 wip(agent-signal): S3 part 1 — completion-side memory receipt support (inert)

Foundation for migrating the memory writer to the async execAgent path: teach
the completion path to project a memory receipt from a finished memory-writer
run. Inert until the dispatch side stamps a kind:'memory' marker (part 2).

- completion routing is now keyed on the operation MARKER (the selfIteration
  payload), not the agent slug — a memory writer runs as the user's own agent,
  so a slug check would miss it. completionPolicy gates on payload presence;
  agentId loosened to string.
- extractCompletionPayload: for a kind:'memory' run, synthesize a writeMemory
  mutation from the run's finalState (the memory builtin tool results are not
  kind-tagged, so extractMutations finds nothing) via resolveMemoryActionResultFromState.
- buildSelfIterationReceipts: a memory run surfaces as just its action receipt,
  no aggregate review summary.
- extract the pure memory finalState parsers into a dependency-light
  ./memoryActionResult module so the completion lifecycle can reuse them without
  dragging the heavy memory-runner module (ModelRuntime/AgentService/…) into its
  graph. userMemory re-exports them for backward compat.
- bump a too-tight (5s) timeout on the real-orchestration integration test.

Tests: completion (12) + completionPolicy (8) + userMemory (12) green; agentSignal
policies + orchestrator suites (138) green; type-check clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(agent-signal): S3 — migrate memoryWriter to async execAgent + completion receipt

Flip the memory-writer action from a blocking executeSync run to an async
queued operation (autoStart) stamped with an agent-signal `memory` marker.
The user-visible "memory saved" receipt is no longer projected synchronously
from the action result — it is projected on the completion path from the run's
finalState (extractMemoryMutations → buildSelfIterationReceipts), so the receipt
appears a few seconds later once the run completes.

- userMemory.ts: add `dispatch` path enqueuing via createOperation(autoStart),
  stamping appContext.agentSignal so completion can project the receipt.
- receiptService.ts: drop the synchronous memory receipt projection (would
  duplicate the async one, with a premature empty target).
- types.ts: add `agentSignal` marker to OperationCreationParams.appContext.
- tests: cover the memory-kind completion loop end-to-end (single memory
  receipt, correct target + anchor, no aggregate summary).

Note: the memory run uses createOperation (not execAgent), so it never
synthesises a user message and cannot recurse into analyzeIntent — no
suppressSignal needed on this path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🚧 wip(agent-signal): S4 step 0 — forward agentSignal marker through execAgent

Foundation for migrating self-iteration onto execAgent: let a background run
carry its agent-signal marker so the S2 completion path can project receipts.

- Move AgentSignalOperationMarker / AgentSignalOperationKind into @lobechat/types
  (ExecAgentAppContext can now reference it); operationMarker.ts re-exports the
  type and keeps the runtime parse/validate helpers.
- ExecAgentAppContext: add `agentSignal?` field.
- execAgent: forward `appContext.agentSignal` into createOperation's appContext
  (it was dropped by the curated passthrough), so it lands in
  state.metadata.agentSignal — the key the completion extractor reads.

No behaviour change yet: nothing sets appContext.agentSignal on the execAgent
path until the self-iteration dispatch helper lands.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🚧 wip(agent-signal): S4 step 0b — self-iteration execAgent dispatch helper

Shared primitive for migrating the 3 self-iteration modes off the hand-rolled
runtime onto async execAgent (used by reflection/feedback/nightly-review next).

- enqueueSelfIterationRun(): create an isolated thread (when anchored), then
  execAgent the builtin slug with suppressSignal + the agent-signal marker on
  appContext, autoStart, headless. Returns immediately (fire-and-forget).
- marker: add `agentId` (the reviewed user agent). A slug run resolves the
  operation agentId to the builtin agent, so receipts must attribute to the
  reviewed agent carried on the marker.
- buildSelfIterationReceipts: attribute to `marker.agentId ?? agentId` (memory
  runs leave it unset and fall back to the run agentId — unchanged).

Not wired into the mode handlers yet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(agent-signal): S4 — migrate executeSelfIteration to async execAgent

Replace the hand-rolled `executeSelfIteration` runtime (new AgentRuntime +
custom call_llm executor + 6 closure side-channels) with the standard async
`execAgent` queue path. nightly-review / self-reflection / self-feedback-intent
now enqueue via `enqueueSelfIterationRun → execAgent` and project their
receipts/briefs on the `agent.execution.completed` completion path.

- Delete `execute.ts` (1500 lines) + `execute.test.ts`; gut the three server
  adapters (review/reflection/feedback) to drop the synchronous run path and
  the legacy receipt/runtime wiring.
- `aiAgent`: background runs execute under a builtin slug but attribute their
  resource tools + receipts to the *reviewed* user agent via the run marker.
- Drop the orchestrator's `writeDailyBrief` default — nightly review writes its
  brief in-run via the builtin review serverRuntime primitive.
- Add `ReviewRunStatus.Dispatched` for enqueued background runs.
- Completion-path debug logging across CompletionLifecycle / completionPolicy /
  completion handler.

Part of LOBE-9434 (S4 · LOBE-9876).

* 🐛 fix(agent-signal): make execAgent resolve builtin slugs + give self-iteration agents a mini model

Live-testing the S4 self-iteration → execAgent path surfaced two gaps that kept
background runs (nightly-review / self-reflection / self-feedback-intent) from
ever dispatching:

- execAgent threw `Agent not found: <slug>` when addressed purely by a builtin
  slug (the self-iteration dispatch path) because getAgentConfig only resolves
  persisted rows. Lazily materialize the virtual builtin row via
  AgentModel.getBuiltinAgent — mirrors the inbox/task precedent — then re-resolve.
- The three self-iteration builtin agents had no `persist` model, so runs fell
  back to the user's default chat model. Give them `persist: { DEFAULT_MINI_MODEL,
  DEFAULT_MINI_PROVIDER }` (gpt-5.4-mini), matching the legacy executeSelfIteration
  behavior.

Verified live: self-reflection now dispatches, the async operation reaches `done`,
and a `review` completion receipt is projected on the completion path. Adds two
execAgent.builtinRuntime tests (builtin-slug materialization + unknown-id still
throws).

Part of LOBE-9434 (S4).

* 🚨 fix(agent-signal): use type-only import for createServerSelfReviewBriefWriter

After the S4 gutting, review/server.ts only uses createServerSelfReviewBriefWriter
in a `ReturnType<typeof ...>` position — split it into a type-only import to
satisfy @typescript-eslint/consistent-type-imports (the lone lint:ts error).

* 🐛 fix(agent-signal): carry tool apiName in result content so action receipts project

The agent runtime persists tool messages with only content/role/tool_call_id (no
message-level apiName), so the completion extractor's `message.apiName` read was
always undefined in live runs — buildSelfIterationReceipts then dropped every
mutation via `if (!apiName) return []`, so durable skill/proposal writes produced
no action receipt (only the summary survived; memory was exempt via a hard-coded
apiName).

Fix the extraction channel, not the shared runtime:
- ExecutionRuntime stamps `apiName` into the result content alongside `kind`.
- extractFromFinalState reads apiName from the content (message.apiName fallback).

Tests reworked to the real persisted shape (apiName in content, no message-level
apiName) — the prior mocks hid the bug.

Part of LOBE-9434 (S4).

* 🐛 fix(agent-signal): persist run marker to operation metadata for server tools

Self-iteration server tools (nightly-review etc.) read the run marker from
`agent_operations.metadata` via readAgentSignalMarker, but recordStart only
persisted a trimmed appContext and never wrote metadata — so in live runs the
marker was always undefined and review/proposal writes fell back to a 1970
window/localDate + operationId source (non-idempotent).

recordStart now persists `metadata: { agentSignal }` from appContext.agentSignal,
so the tool path matches the completion path (which reads it from finalState).

Part of LOBE-9434 (S4).

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 20:12:59 +08:00
sxjeru dd2e32cf6f 💄 style: Add new MiniMax-M3 model (#15403)
*  feat(minimax): add MiniMax M3 model with pricing and update tests

* Update minimax.ts

* fix test
2026-06-03 19:29:02 +08:00
YuTengjing a5ab99f055 📝 docs: add agent code style guidance (#15434)
* 📝 docs: add code style guidance for hook extraction and file splitting

* 📝 docs: tighten file-splitting guidance

* 📝 docs: clarify agent guidance wording
2026-06-03 18:45:40 +08:00
Arvin Xu 41bccc4aa8 chore: remove LOBE-XXX markers from code comments (#15422)
chore: remove LOBE-XXX markers from code comments

- match.test.ts: replace (LOBE-9913) marker with inline comment context
- nightly-review.golden.json: replace (LOBE-9434) marker with execAgent migration context

Co-authored-by: Arvin Xu <arvin@lobehub.com>
2026-06-03 17:02:24 +08:00
AmAzing- 1ce4e026a7 🐛 fix(const): point CHANGELOG_URL to /changelog (#15428) 2026-06-03 15:25:48 +08:00
Innei 89c55bf658 💄 style(service-model): polish form layout & migrate Switch to base-ui (#15426)
- align memory model InputNumber height (32px) with base-ui Select trigger via local ConfigProvider
- stack optional features as switch above model select, switch right-aligned
- migrate optional features Switch from antd to @lobehub/ui/base-ui
2026-06-03 14:18:36 +08:00
Arvin Xu 2eb9e34fda feat(stats): add daily token-usage mode to activity heatmap (#15417)
*  feat(stats): add daily token-usage mode to activity heatmap

Add a Messages/Tokens toggle to the stats activity heatmap. The token
mode sums assistant messages' `metadata.usage.totalTokens` (the source of
truth for usage) bucketed by the day each message was created, so tokens
land on the day they were actually consumed rather than on a topic's
creation date. Aggregation runs in SQL (SUM over the jsonb path, GROUP BY
date) and levels are scaled relative to the busiest day.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 💄 feat(stats): format heatmap token counts and add token stat row

- Format tooltip token counts compactly (e.g. 44.2K, 12.5M) via the chart's
  customTooltip; message counts get thousand separators.
- Add a token-dimension summary row (cumulative / peak daily / current streak
  / longest streak) shown in token mode, derived client-side from the heatmap
  data over the past year.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

*  feat(stats): add longest-task duration to token heatmap stats

Add the "longest task" figure to the token-mode stats row, computed from
the longest wall-clock agent operation (completedAt - startedAt) over the
past year — MAX in SQL on the agent_operations table, scoped by user and
using the (user_id, created_at) index. Rendered as a compact 1h 15m / 45s
duration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 💄 feat(stats): default heatmap to token mode and move toggle beside title

- Token is now the first/default segmented option (Messages second); the
  share card keeps Messages as its default.
- Move the Messages/Tokens toggle next to the section title (left) via a new
  StatsFormGroup `afterTitle` slot; day tags stay on the right.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 01:54:07 +08:00
Innei 13ce3c52ec ♻️ refactor: migrate modals to @lobehub/ui/base-ui (LOBE-9711 + eval batch) (#15416)
* ♻️ refactor: migrate modals to @lobehub/ui/base-ui (LOBE-9711 + eval)

Move 5 root createModal sites (LibraryModal/AddFilesToKnowledgeBase,
LibraryModal/CreateNew, Electron/AuthRequiredModal, SkillStore,
SkillStore/SkillDetail) to base-ui imperative createModal. Drop
allowFullscreen/destroyOnHidden/getContainer (base-ui handles them),
rename children→content, afterClose→onOpenChangeComplete, styles.body
→styles.content.

For AuthRequiredModal, base-ui imperative ModalInstance.update only
accepts Partial<BaseModalProps>, so the previous closable/keyboard
dynamic lock is reduced to maskClosable only — Esc/X close cannot be
blocked during sign-in.

Convert 11 declarative <Modal open … /> sites under eval/bench to
imperative createXxxModal factories, splitting each into Content.tsx
(body) + index.tsx (factory). Update callers in eval/index.tsx,
bench/[id]/{datasets/[id],features/{BenchmarkHeader,DatasetsTab,
RunsTab,TestCasesTab},runs/[id]/{index,features/RunHeader}} to call
factories on click instead of toggling local open state.

Delete unused TestCasePreviewModal.tsx (dead code); extract the
inline preview Modal from TestCasesTab into a new
TestCasePreviewModal feature folder.

* ♻️ refactor: move modal actions to base-ui footer slot, drop content padding overrides

Per @innei feedback on PR #15416:

- base-ui's ModalContent already has 12px/16px default padding; remove
  manual paddingBlock/paddingInline wrappers in Content components and
  drop styles.content.padding=0 overrides in factories.
- Move OK/Cancel (and other actions) into the createModal({footer}) slot
  using base-ui's ModalFooter atom for proper flex/justify-end styling.
- Form submit wired via antd Form's name + Button form=name htmlType=submit
  so the footer button outside Form can submit it. Shared loading state
  flows from Content to Footer via a per-modal closure that calls
  instance.update({footer: ...}).

New helper src/utils/createFormModal.tsx encapsulates the common pattern
for plain form modals (Cancel + Submit). Custom factories (RunCreate
split-button, BatchResume with selection counter, DatasetImport step-aware
footer, AuthRequired sign-in flow) use inline closure plumbing.

Touched files: 11 modal pairs (Content + Footer + index) + 1 helper.

* 🔥 chore: remove dead AddFilesToKnowledgeBase modal

`useAddFilesToKnowledgeBaseModal` exported from
`src/features/LibraryModal/AddFilesToKnowledgeBase/` had no callers in
the main codebase — only its own test referenced it. Remove the entire
folder (index, SelectForm, index.test) and drop the re-export from
`LibraryModal/index.ts`.

* 💄 style: bleed SkillStore scroll viewport past modal content padding

base-ui ModalContent has 12px/16px default padding, which insets the
SkillStore scroll viewport and makes the scrollbar look blocked. Pull
the body wrapper out with negative margins (marginInline: -16,
marginBlockEnd: -12) so the inner scroll container sits flush with the
modal edge. Grid items inside the scroll keep their own 16px padding.

* 🐛 fix: cast Modal.update to ImperativeModalProps for footer typing

base-ui's ModalInstance.update is typed as Partial<BaseModalProps>,
which excludes the `footer` and `content` fields that only
ImperativeModalProps carries. At runtime the imperative updateModal
spreads any shape, so the cast is sound — narrow it at each call site.

Also delete src/routes/(main)/eval/bench/[benchmarkId]/features/
DatasetRunCreateModal/, an orphaned re-export of RunCreateModal's
removed default export.
2026-06-03 00:17:48 +09:00
YuTengjing f9eb48feea feat: add limited offer & original price locale keys for top-up (#15415) 2026-06-02 21:00:12 +08:00
YuTengjing 8dee729f9f feat: add storage pay-as-you-go stubs and locale keys (#13501) 2026-06-02 20:45:15 +08:00
LiJian 359b348989 feat(agent-builder): add skill priority instruction and server runtime (#15409)
*  feat(agent-builder): add skill priority instruction and server runtime

- Add <skill_coexistence> section to agent-builder system prompt so the
  model always prefers Agent Builder tools over LobeHub skills for
  agent configuration tasks when both are active simultaneously
- Add agentBuilder server runtime to support background (QStash)
  execution: implements updateConfig, updatePrompt, searchMarketTools,
  getAvailableModels (DB-backed, LobeHub provider first, max 20 chat
  models), and installPlugin (market source only; official/OAuth tools
  return a clear unsupported error)
- Register agentBuilderRuntime in the server runtime registry

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

*  feat(agent-builder): fix identity confusion when user provides agent name/purpose

Add <identity_boundary> section and example to prevent the AgentBuilder
from roleplaying as the agent being configured. Short phrases like
"健康助手,咨询健康问题" must be interpreted as configuration requests,
not service requests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(agent-builder): address three server runtime issues

- getAvailableModels: use AiInfraRepos instead of raw AiProviderModel +
  AiModelModel so builtin providers (DEFAULT_MODEL_PROVIDER_LIST) are
  included even when the user has no DB-customized providers
- installPlugin (official): allow builtin tools (lobe-web-browsing etc.)
  to be enabled directly; only block OAuth-requiring tools (Klavis,
  LobehubSkill) that cannot be installed in background context
- installPlugin (market): fetch and persist the marketplace MCP manifest
  on install so server tool discovery can find and execute the plugin

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 17:10:30 +08:00
Arvin Xu 0c3450de7c test(model-runtime): classify ollamacloud "context window exceeds limit" as ExceededContextWindow (#15411)
* 🐛 fix(model-runtime): classify ollamacloud "context window exceeds limit" as ExceededContextWindow

ollamacloud surfaces context-window overflow as a generic 400 the upstream
labels ProviderBizError. Document the ollamacloud provenance on the existing
`context window exceeds` ECW pattern and add a regression test asserting the
message wins over the 400 / ProviderBizError catch-alls.

Fixes LOBE-9913

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🔥 chore(model-runtime): drop redundant ollamacloud note on ECW pattern

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 16:07:43 +08:00
René Wang cbc259094d 📝 docs: add Codex & Claude Code agent guides; merge image+video generation (#15407)
- New docs/usage/agent/{codex,claude-code}.{mdx,zh-CN.mdx} cover how to
  delegate the Codex and Claude Code CLIs from the LobeHub desktop app
  (install, sign-in, working-directory pinning, in-chat tool renderers,
  resume behavior, execution targets, limitations).
- Rename docs/usage/getting-started/image-generation.{mdx,zh-CN.mdx} to
  generation.{mdx,zh-CN.mdx} and expand to cover the Video workspace
  alongside Image.
- Update <Card> links in sibling resource/vision docs to point at the
  new /generation slug.
2026-06-02 13:55:32 +08:00
Arvin Xu ccf33e8b98 🐛 fix(agent-runtime): classify topic/agent/session FK violations as ConversationParentMissing (#15408)
When a user deletes a topic (or agent/session/thread) while an agent operation
is still running, the assistant/tool-message INSERT fails with a Postgres 23503
foreign_key_violation on the corresponding `messages` FK. The persist-error
guard only recognised the `messages_parent_id_messages_id_fk` self-FK, so every
other reference deletion slipped through as a raw `Failed query: insert into
"messages"` 500 — surfacing to the user as a driver/SQL error and polluting the
error dashboard as DatabasePersistError noise (one of the longest-standing
top error categories).

Generalise `isParentMessageMissingError` → `isMidOperationReferenceMissingError`
to match a 23503 violation on any of the mid-operation-deletable `messages`
references (parent / quota message, topic, agent, session, thread). These all
mean "the referenced context was deleted mid-flight" — a lost race against the
user, not a runtime failure — so they are normalised to the typed, user-side
`ConversationParentMissing` error like the parent case already was.

Out-of-scope FKs (e.g. `messages_user_id_users_id_fk`, other tables) stay real
failures.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 13:23:19 +08:00
YuTengjing d1a6ffaf30 🔨 chore: tighten skill descriptions for triggering (#15397) 2026-06-02 13:00:52 +08:00
qybaihe 66c9339e98 fix(desktop): resolve CLI tools from shell PATH (#15368)
* fix(desktop): resolve CLI tools from shell PATH

* fix(desktop): validate resolved CLI with fallback PATH
2026-06-02 11:29:57 +08:00
LiJian 857aaf4766 feat(chat-input): show execution-device switcher for all agents (#15371)
 feat(chat-input): show execution-device switcher for all agents and add desktop download link

- Remove `!isHeterogeneous` guard so the device switcher surfaces for every agent type (not just non-heterogeneous), controlled by the existing Lab toggle
- Make the sandbox/runtime-env mode selector mutually exclusive with the device switcher: hide it when `enableExecutionDeviceSwitcher` is on
- Add a "下载桌面端 / Get Desktop App" quick link in the execution-device popover header (right side) linking to https://lobehub.com/downloads

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 11:20:52 +08:00
Innei 4e91a3181d ♻️ refactor(modal): convert create custom model modal to base-ui imperative API (#15401)
* ♻️ refactor(modal): convert create custom model modal to base-ui imperative API

* ♻️ refactor(modal): convert edit model modal to base-ui imperative API

* 🐛 fix: make extend params preview read-only
2026-06-02 12:18:27 +09:00
Arvin Xu c9ca46e1e0 chore: remove LOBE-XXX annotations from code comments (#15398)
Replaced all LOBE-XXX references in comments with descriptive context
instead of internal Linear issue markers. As an open-source project, these
internal references should not be exposed.

Changes:
- LOBE-9834 (empty completion): replaced with inline descriptions of the
  "empty completion" failure mode
- LOBE-6587 (task scheduler): replaced with "task scheduler infra" ref
- LOBE-6634 (getTaskDetail model/provider): updated TODO description
- LOBE-9434 #5/#7 (execAgent migration): removed issue markers

12 files changed

Co-authored-by: Arvin Xu <arvinxx@users.noreply.github.com>
2026-06-02 10:46:44 +08:00
Innei 37db828c17 ♻️ refactor(modal): convert feedback & changelog modals to base-ui imperative API
Migrate `FeedbackModal` and `ChangelogModal` from declarative `@lobehub/ui`
modals + a `useFeedbackModal` zustand store to the `@lobehub/ui/base-ui`
imperative `createModal()` API. Call sites now invoke `openFeedbackModal()`
/ `openChangelogModal()` directly — no more open/close state plumbing
through `(main)/_layout` or `(mobile)/me/(home)`. The `useFeedbackModal`
hook is removed.

Also:
- Wrap the email address in `feedback.emailContact` with a `<email>` tag
  (Trans component → mailto link); all 18 locale files updated.
- Restore the external link on the changelog modal header pointing to
  `CHANGELOG_URL`; the previous Button used `onClick={onClose}` despite
  the ArrowUpRight icon suggesting an external navigation.
- Footer test mocks updated to match the new module exports.
2026-06-02 03:00:48 +09:00
Innei 0208c0adfe 🐛 fix(chat-input): restore editor focus after file picker closes (#15394)
Picking files via the antd `Upload` dropdown (paperclip / plus menus) and via
the AgentTasks attachment helpers leaves focus on `document.body` once the OS
file picker dismisses, so the editor cursor disappears and users do not know
they can keep typing.

Refocus the editor right after the file picker yields:

- `ChatInput/ActionBar/Upload`: subscribe to `editor` from the chat input
  store and call `editor?.focus()` in the three `beforeUpload` handlers
  (image / file / folder).
- `ChatInput/ActionBar/Plus`: same fix for the unified file-or-image upload
  entry; add `editor` to the items `useMemo` deps.
- `EditorCanvas/editorAttachments`: refocus inside `insertFilesIntoEditor`
  so every AgentTasks composer (CommentInput, CommentCard, FeedbackInput,
  CreateTask*, TaskInstruction via `pickAndInsertAttachments`) recovers
  the cursor too.

Fixes LOBE-9862
2026-06-02 01:19:36 +08:00
Rylan Cai 09a57d4618 🐛 fix: clarify local command session handling (#15389) 2026-06-02 00:55:17 +08:00
Arvin Xu 73dd0ef136 🔖 chore(cli): bump @lobehub/cli to 0.0.24 (#15393)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 00:45:24 +08:00
Innei d2e4833f1e 🐛 fix(chat-input): close skill dropdown before navigating to settings (#15391)
The skill market dropdown's settings button navigates to /settings/skill
but does not close the controlled dropdown first, so the popup persists
after the trigger has unmounted (LOBE-9852).

Also restore the negative margins on the skill market footer (regressed
in #15214 when bumping @lobehub/ui to 5.15.1) so the stats row aligns
with the dropdown's outer padding.

Apply the same defensive close-before-navigate to ModelSwitchPanel:
- ListItemRenderer no-provider / empty-model rows previously navigated
  without calling onClose at all.
- Footer and GenerationListItemRenderer now close before navigate
  instead of after, for a consistent ordering.

Closes LOBE-9852
2026-06-02 00:32:12 +08:00
Innei 5119c0802d 🐛 fix(topic): strip markdown tokens from fallback titles (#15372)
Sliced raw user input was leaking syntax (#, **, ``` etc.) into topic / thread / agent / group / document titles whenever LLM summarization had not yet produced a clean title. Run the source string through `markdownToTxt` (remove-markdown) before slicing so the visible fallback is plain text.
2026-06-01 23:22:46 +08:00
Innei 3e51b87b1e 💄 style(sidebar): group spacer with recents and agents (#15373) 2026-06-01 23:22:07 +08:00
Arvin Xu 1e8b5959da ♻️ refactor(topic): drop legacy session→agentId compatibility from topic queries (#15378)
* ♻️ refactor(topic): drop legacy session→agentId compatibility in topic queries

Topic ownership is fully migrated to `topics.agentId`, so the
`agentsToSessions` lookup that mapped a legacy `sessionId` back to an agent
is no longer reachable in practice. Remove it from the agent query, count,
and batch-delete paths — they now match `topics.agentId` directly.

- `query()`: drop the `agentsToSessions` pre-query and the `sessionId` OR
  branch; keep the inbox fully-orphan fallback (all owner columns null),
  which is unrelated to session linkage.
- `count()` / `batchDeleteByAgentId()`: match `topics.agentId` only.
- Remove the now-unused `agentsToSessions` import.

Tests updated to assert session-only legacy topics are no longer matched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(topic): make topic rank/recent agent-centric, drop returned sessionId

Topic ownership is `topics.agentId`, so the topic ranking and recent-topic
queries no longer need to expose or resolve a legacy `sessionId`.

- `TopicModel.rank()` now selects `topics.agentId` instead of `sessionId`;
  `TopicRankItem.sessionId` → `agentId`.
- `TopicModel.queryRecent()` stops selecting `sessionId`.
- `recentTopics` TRPC procedure: drop the `agentsToSessions` batch resolve
  and the `after()` runtime agentId backfill — both keyed off the legacy
  session mapping. Agent topics now map straight through `topic.agentId`.
- Topic ranking UI navigates to `SESSION_CHAT_TOPIC_URL(agentId, topicId)`
  (`/agent/:aid/:topicId`), falling back to the inbox agent id when a topic
  has no agentId, replacing the old `/agent?session=...` query-param link.

Rank test asserts `agentId`; the broader `getTopics` session-resolution
path is intentionally left untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  revert(topic): restore session→agentId resolution in query/count/delete

The integration tests (topic.integration.test.ts) showed this compatibility
is still load-bearing: the topic write path (createTopic / batchCreateTopics
/ updateTopic) persists `sessionId` with `agentId = null`, so dropping the
read-side session→agentId resolution made freshly-created topics
unqueryable/undeletable by agentId.

Revert the read-side removal from `query()` / `count()` /
`batchDeleteByAgentId()` (and their tests) until the write path is migrated to
store `agentId` directly. The agent-centric `rank()` / `queryRecent()` /
`recentTopics` surface changes are kept.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(topic): drop session→agentId compatibility from topic read paths

Topic ownership is fully migrated to `topics.agentId` (old data backfilled,
new app no longer depends on sessionId), so the legacy session resolution in
the topic read paths is dead and can go.

- `query()` / `count()` / `batchDeleteByAgentId()`: match `topics.agentId`
  directly; drop the `agentsToSessions` lookup + `topics.sessionId` OR branch.
  The inbox fully-orphan fallback (all owner columns null) is kept.
- `getTopics` TRPC procedure: drop the `after()` runtime agentId backfill and
  the now-unused `AgentMigrationRepo` wiring / `after` import. The sessionId→
  agentId reverse-resolution of the query *filter* is kept for clients that
  still pass a sessionId.
- Update topic integration + model tests to agent-native fixtures; remove the
  legacy-session and runtime-migration cases that exercised the removed path.

The write path (createTopic/batchCreate/updateTopic) is intentionally left
unchanged per scope; no data migration is performed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(topic): keep getTopics runtime agentId backfill during transition

Restore the `after()` runtime migration in `getTopics` (and the
`AgentMigrationRepo` wiring / `after` import). The read paths no longer
resolve sessionId, but the backfill is still needed to migrate straggler
legacy (sessionId-only) topics over the transition window; a legacy topic is
backfilled on first query and becomes agentId-queryable thereafter.

Restore the migration integration tests, adjusted: they assert the agentId
backfill happens after the query rather than expecting legacy rows in the
first (now agentId-only) response.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(topic): keep recentTopics runtime agentId backfill

Restore the recentTopics session→agentId backfill removed earlier: re-select
`sessionId` in `queryRecent` (internal only — not exposed in the RecentTopic
response) and re-add the `batchResolveAgentIdFromSessions` resolution + the
`after()` migrateAgentId backfill. Like the getTopics backfill, this keeps
migrating straggler legacy (sessionId-only) topics during the transition.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 chore(topic): drop unnecessary comment churn in topic router/model

Revert the migration/backfill comments to their original wording so the
restored getTopics/recentTopics blocks are byte-identical to canary, and drop
the extra queryRecent select comment. No logic change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(agent): replace session-based assistant ranking with agent-native rank

The assistant usage ranking was session-centric (SessionModel.rank joining
agentsToSessions, returning a sessionId; UI linked /agent?session=...). Rework
it as agent-native:

- Add `AgentRankItem` type (id = agentId); remove `SessionRankItem`.
- Add `AgentModel.rank`: count topics grouped by `topics.agentId`, joined to
  agents for avatar/title, ordered by count. Mirrors the recents filter
  (real agents + inbox, excluding other virtual agents). No sessions involved.
- Add `agent.rankAgents` TRPC procedure + `agentService.rankAgents`; remove
  `session.rankSessions`, `sessionService.rankSessions`, `SessionModel.rank/_rank`.
- AssistantsRank UI: navigate to `SESSION_CHAT_URL(agentId)` → `/agent/:aid`,
  resolving the inbox title via the store's inboxAgentId.

Move the rank tests from session.test.ts to agent.test.ts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 22:51:28 +08:00
Rylan Cai 5b25b8d8bb ️ perf: skip user count on api key checks (#15385) 2026-06-01 22:25:38 +08:00
YuTengjing fd82f6fd0e 🐛 fix: add restore subscription copy (#15388) 2026-06-01 21:31:22 +08:00
YuTengjing 80c11a09e2 🐛 fix: stabilize home starter loading (#15386) 2026-06-01 20:48:48 +08:00
Arvin Xu c8096590c4 feat(topic): add group-by-status mode to topic sidebar (#15366)
*  feat(topic): add group-by-status mode to topic sidebar

Add a new "By status" grouping option to the agent topic sidebar. Topics
bucket into fixed-priority groups — waitingForHuman first, then running,
then active, with the remaining states below. Topics without a status are
treated as active. Only non-empty groups render.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(topic): resolve group-by-status ordering on the server

The sidebar only loads the first page of topics, so grouping that partial
list client-side could hide high-priority topics (awaiting-human / running)
that live on a later page. Push the ordering to the query instead.

- Add `sortBy: 'updatedAt' | 'status'` to the topics query (TRPC + model).
  `status` orders by a priority CASE (waitingForHuman → running → active →
  paused → failed → completed → archived) before the updatedAt tiebreaker,
  so the most important topics always land on the first page.
- Plumb `sortBy` through the service, store fetch action (incl. SWR key),
  and the shared topic hooks; `useFetchChatTopics` requests `status` ordering
  only when the resolved agent group mode is `byStatus` (group sessions keep
  the default). The client still buckets for display, now over a correctly
  ordered page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(topic): bucket streaming topics under "running" in group-by-status

A topic generating a response shows the loading ring via the client-only
`topicLoadingIds` state, not a persisted `status`, so it was landing in the
"active" group. Mirror the sidebar TopicItem icon precedence when bucketing:
waitingForHuman wins, then a topic that is streaming on this client (or
persisted as running) goes to "running", then the persisted status.

The loading overlay stays client-side (the server can't know a given client
is mid-stream); the selector passes `topicLoadingIds` into the byStatus
grouping only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 19:44:38 +08:00
YuTengjing dda527926d feat: support configurable model routing and starters (#15384) 2026-06-01 18:46:00 +08:00
YuTengjing 5f0fa7bf50 🐛 fix: block disabling official provider (#15382) 2026-06-01 17:39:54 +08:00
YuTengjing c50d790feb 🔨 chore: pin Vitest 3.2.4 (#15383) 2026-06-01 17:30:31 +08:00
LiJian 4d030e9db1 🐛 fix(agent-manager): guard createAgent against LLM double-encoded array fields (#15381)
* 🐛 fix(agent-manager): guard createAgent against LLM double-encoded array fields

When an LLM calls createAgent, it can send plugins/tags/openingQuestions
as a JSON string (e.g. '["lobe-cloud-sandbox"]') instead of a proper
array. This caused jsonb to store a double-serialized string rather than
an array, breaking downstream ETL queries with "cannot get array length
of a scalar".

updateAgentConfig already had this guard (line 130+); apply the same
parseArrayParam helper to all three array fields in createAgent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(agent-manager): guard server-side createAgent against double-encoded array fields

Same LLM double-encoding guard applied to the server-side execution path
(src/server/services/toolExecution/serverRuntimes/agentManagement.ts),
which directly calls agentModel.create() and was equally vulnerable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 17:00:42 +08:00
YuTengjing 56ce192c61 🐛 fix: default provider setup in business mode (#15379) 2026-06-01 16:03:14 +08:00
YuTengjing 21a73b22b2 feat: support MiniMax M3 Anthropic video runtime (#15380) 2026-06-01 16:00:06 +08:00
LiJian 818e67d1f0 🐛 fix(gateway): prevent duplicate streaming from stale reconnects (#15354)
* 🐛 fix(gateway): prevent duplicate streaming from stale reconnects

When a new agent execution starts for a topic that has a stale
`runningOperation` in its metadata, `useGatewayReconnect` would still
attempt to reconnect to the old operation concurrently with the new one,
producing duplicate streaming events.

Fix by:
1. Optimistically updating the topic's `runningOperation` to the new op
   immediately after `executeGatewayAgent` creates it, and disconnecting
   any live reconnect connection for the stale op.
2. Skipping `connectToGateway` in the reconnect path when the topic
   already has a newer running operation ID.

* 🐛 fix(gateway): add post-refresh stale-op guard and fix test mocks

Two issues fixed:

1. Race condition: the `reconnectToGatewayOperation` guard only ran before
   `refreshGatewayToken`. A stale reconnect that passed the initial check
   could still proceed after the token refresh if `executeGatewayAgent`
   started a new operation during the await. Re-check `runningOperation`
   after the token refresh to bail out in that case.

2. Test failures: the `executeGatewayAgent` mock state was missing
   `topicDataMap`, causing `topicSelectors.getTopicById` to crash with
   "Cannot read properties of undefined". Added `topicDataMap: {}` and
   `internal_dispatchTopic` to both mock setups.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 15:18:06 +08:00
Arvin Xu e14f2e96f6 🐛 fix(cli): auto-register device on login, matching desktop (#15377)
* 🐛 fix(cli): auto-register device on login, matching desktop

Device registration previously only ran in `lh connect`, so `lh login`
left no device row until the user separately connected the gateway. The
desktop app registers on login; this aligns the CLI.

Extract the shared identity-resolution + register logic into
`device/register.ts` (`resolveDeviceIdentity` + `registerDevice`) and call
it from `login` right after auth (best-effort, non-fatal). `connect` keeps
its own call as an idempotent fallback for `--token` sessions that never
went through login.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(cli): skip login-time device registration for fallback identities

When node-machine-id can't read a machine id, deriveDeviceId returns a
fresh random id with identitySource 'fallback'. Since `lh login` has no
--device-id and persists no fallback id, registering it on every login
spawns orphan device rows that never match the id a later `lh connect`
resolves. Defer registration to connect in that case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 13:24:22 +08:00
Arvin Xu cf5ec7b96a test(hetero-agent): use canonical usage fields in persistence fixtures (#15375)
The HeterogeneousPersistenceHandler fixtures wrote `metadata.usage` as
`{ inputTokens, outputTokens }` — a shape the het adapters never emit. Both
claudeCode/codex build canonical `UsageData`
(`totalInputTokens`/`totalOutputTokens`/`totalTokens`) via `toUsageData()` and
`handleTurnMetadata` persists it unchanged, so production het messages already
carry canonical fields that the topic usage rollup sums correctly.

The unrealistic alias fixtures made it look like rollups would store
`total_* = 0` for Claude Code/Codex topics. Align the fixtures with real
adapter output.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 12:51:31 +08:00
Arvin Xu c3f91f10ac feat(database): maintain denormalized topic usage/cost rollup from messages (#15365)
*  feat(database): maintain denormalized topic usage/cost rollup from messages

Topics carry usage/cost aggregate columns (total_input_tokens /
total_output_tokens / total_tokens / total_cost / usage / cost / model /
provider) mirroring agent_operations, but nothing populated them. Add a
canonical derived-projection rollup maintained live from the topic's messages.

- `recomputeTopicUsage(trx, userId, topicId)` (new `models/topicUsage.ts`):
  sums the topic's `role='assistant'` messages (thread messages included — they
  carry topic_id too) over the canonical `metadata.usage`, grouped by
  (provider, model). Writes the same shape as agent_operations: scalar totals,
  a flat `usage` jsonb ({ llm:{ apiCalls, processingTimeMs, tokens }, tools,
  humanInteraction }), and a `cost` jsonb ({ total, currency, llm:{ byModel[] },
  tools }) — NULL when no model reported cost. `model`/`provider` = the
  dominant model by total tokens. Pure derived & idempotent: resets to NULL
  when no measurable usage remains, so deletes/regenerations are reflected.

- Hook it into MessageModel at the shared chokepoints, inside the existing
  transactions: `update()` (only when the incoming payload carries
  `metadata.usage`, i.e. assistant finalize / hetero step — streaming
  content-only updates don't trigger it) and `deleteMessage()` /
  `deleteMessages()` (recompute affected topics). This covers all LLM-call
  write paths since they funnel through MessageModel.update.

- `TopicModel.recomputeUsage(id)` wraps the canonical fn in a transaction for
  external callers (e.g. the historical backfill).

Tool/human-interaction sub-totals are left as a zero skeleton (not
reconstructable from assistant messages); the historical backfill will reuse
the same aggregation via raw SQL without bumping updated_at.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  test(database): cover topic usage/cost rollup recompute

Add tests for the denormalized topic usage rollup: core
`recomputeTopicUsage` aggregation (per-model grouping, dominant model,
cost-null path, userId scoping, NULL reset), the `TopicModel.recomputeUsage`
wrapper, and the `MessageModel` update/delete hooks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 12:21:47 +08:00
Arvin Xu 650a178709 feat(agent-signal): register self-iteration builtin tool package (#15364)
Part of LOBE-9434 (#3). Gives the three (currently dormant) self-iteration
builtin agents a declarative tool surface so they no longer run with no tools.

One package `@lobechat/builtin-tool-agent-signal` with internal subdirs:
- `shared/`: the 3 stable identifiers, JSON-schema specs for the full tool
  surface (resource / review / reflection), a result-kind map (read | artifact
  | mutation — the LOBE-9434 #5 discriminator), `createAgentSignalManifest`,
  and one shared `AgentSignalToolExecutionRuntime` that dispatches per api name
  and stamps every result with its kind so `extractFromFinalState` can
  partition outcomes from a persisted snapshot.
- `review/` `reflection/` `feedback-intent/`: per-mode manifests assembled from
  the shared specs + a mode-specific system prompt, exported under their three
  stable identifiers. Review = resource + proposal/idea tools; reflection &
  feedback-intent share the resource + reflection-recorder surface.

Registered all three manifests in `@lobechat/builtin-tools`. `executors` is
omitted on purpose — BuiltinToolManifest defaults to server-only execution.

The server-side execution bridge (wiring the ExecutionRuntime to the existing
createToolSet(adapters) from ToolExecutionContext) lands with the
executeSelfIteration -> execAgent migration (#7); the ExecutionRuntime is
exported (./executionRuntime) and ready for it. No production self-iteration
path is touched — executeSelfIteration still serves all current runs.

Tested: shared ExecutionRuntime (dispatch + kind stamping + error handling) and
review manifest structure. bun run type-check clean for everything touched.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 12:09:06 +08:00
Arvin Xu e1d6b30127 🐛 fix(desktop): relocate visual-ref helpers to @lobechat/const to fix renderer crash (#15369)
🐛 fix(desktop): relocate visual-ref helpers to @lobechat/const

PR #15114 added @lobechat/prompts + @lobechat/tool-runtime to the desktop
nested pnpm workspace. pnpm then linked their @lobechat/types dependency to
the desktop type-only stub (apps/desktop/stubs/types) inside the SHARED
packages/*/node_modules, which the renderer/web build also consumes. The
runtime value import `createVisualFileRef` (in prompts/files/{image,video}.ts)
resolved to the stub — which only surfaces types — so the renderer crashed on
boot with `SyntaxError: ... does not provide an export named createVisualFileRef`.

The stub is load-bearing: pointing the desktop workspace at the real
@lobechat/types fails install (model-bank@workspace:* dependency cascade), so
the stub must stay. Fix the contract instead: visual-ref helpers are runtime
logic, not types, so they don't belong in @lobechat/types. Move the
zero-dependency helpers to @lobechat/const/visualRef (already a real desktop
member, no cascade) and import them via the narrow subpath. prompts/tool-runtime
now only `import type` from @lobechat/types, so the stub link is harmless.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 11:46:03 +08:00
Arvin Xu 7d1086b096 🐛 fix(remote-device): preserve content/state across gateway tool calls (#15114)
The cloud→gateway→desktop path was JSON.stringify-ing the entire IPC result
into `content`, dropping `state` and leaking `{success: true, ...}` into the
LLM-facing prompt. Routes remote tool calls through `LocalSystemExecutionRuntime`
(same runtime the renderer uses) so `content` is the formatted prompt and
`state` flows through `DeviceProxy` → `RuntimeExecutors` into `pluginState`.

Also moves `LocalSystemExecutionRuntime` from `@lobechat/builtin-tool-local-system`
(renderer-coupled, React/antd peers) into `@lobechat/tool-runtime` so the
desktop main process can reuse it without pulling UI deps.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 11:01:25 +08:00
LiJian 58c671b7ac 🐛 fix(agent-builder): explicitly sync editing agent ID to chatStore (#15357)
The Agent Builder reads the wrong agent's context because
`getChatStoreState().activeAgentId` — which the chat service uses to
build `agentBuilderContext` — can drift from the agent currently open in
the profile editor under certain timing conditions (SWR cache hits,
navigation order, React effect scheduling).

Fix: `AgentBuilderProvider` now accepts an `editingAgentId` prop and
writes it to `chatStore.activeAgentId` in a `useEffect`. This makes
the data flow explicit instead of relying on `AgentIdSync` alone.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 10:19:38 +08:00
Arvin Xu e0ead38c20 ♻️ refactor(agent-signal): restore 3 mode-specific self-iteration agent slugs (#15202)
The Phase 1 consolidation into a single `self-iteration` slug (PR #15187,
inheriting commit 627f899895 from the closed #15116) conflated three
distinct background flows that have:

- Independent receipt tables and idempotency Redis namespaces
- Different preflight / brief projection paths
- Different audit pipelines

`one identifier = one behavior` is a load-bearing contract once these
agents are routed through the standard execAgent plugin lookup. Restore
the 3 mode-specific slugs so each agent declares its own tool surface:

| slug                    | future plugin identifier        |
| ----------------------- | ------------------------------- |
| `nightly-review`        | `agent-signal-review`           |
| `self-reflection`       | `agent-signal-reflection`       |
| `self-feedback-intent`  | `agent-signal-feedback-intent`  |

`SELF_ITERATION_AGENT_SLUGS` now contains all three; `completionPolicy`
dispatches on slug membership rather than equality; callback receives
the resolved `agentId` so mode-specific bookkeeping can route from it.

Plugin arrays reference the future identifiers but the tool packages
are not yet registered — invoking any of these agents today runs the
LLM with no tools (dormant by design). Tool-package registration
follows in a separate PR.

No behavior change for existing callers (none invoke these slugs yet).
2026-06-01 10:03:42 +08:00
René Wang f71be63bea 📝 docs: add May 26 weekly changelog (#15183) 2026-06-01 09:36:32 +08:00
Tsuki 4d840e9071 feat(push): add PushChannel, receipt cron, and pushToken tRPC API (#15233)
Send-side machinery for mobile push notifications (LOBE-8771), stacked
on top of the schema PR (#15186).

### tRPC
- `pushToken.register` / `pushToken.unregister` exposed on both
  `MobileRouter` and `LambdaRouter`.

### `PushChannel`
- Structurally compatible with cloud's `NotificationChannel` so cloud
  can register it without casts.
- Fans a single notification out to all of a user's tokens, chunks via
  `expo-server-sdk`, respects the 600 msg/sec project limit with 100ms
  throttle between chunks.
- Embeds `(ticketId, expoToken)` pairs in `providerMessageId` for
  receipt reconciliation.
- Returns `no_tokens` / `invalid_tokens` / `rate_limited` /
  `all_send_failed` so callers can distinguish.

### `processPushReceipts`
- Pure helper to be called by cloud's Vercel cron (companion PR).
- Polls Expo receipts in parallel (`Promise.all` across chunks),
  updates `notification_deliveries` in bulk, prunes `push_tokens` rows
  flagged `DeviceNotRegistered`.
- Configurable lookback window + min-age guard (default: 24h / 15min).

### Dev tooling
- `/api/dev/test-push` (404s in production) lets you fire a real push
  directly to a user's registered tokens, bypassing `NotificationService`.
  Useful for end-to-end verification before cloud submodule sync.

### Types
- `NotificationSettings` gains an optional `push` channel.

Tests: 21 added (router 7, PushChannel 7, processPushReceipts 7).

Linear: https://linear.app/lobehub/issue/LOBE-8771

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 01:49:04 +08:00
Arvin Xu d382df1b2c ♻️ refactor(agent-runtime): persist canonical nested usage/performance on assistant messages (#15359)
 feat(agent-runtime): persist canonical nested usage/performance on assistant messages

The standard agent chat path (RuntimeExecutors) only flattened token usage
onto message metadata and never persisted performance metrics, while the
heterogeneous and client store paths already wrote the canonical nested
`metadata.usage` / `metadata.performance`. Converge the server path so all
writers produce the same shape:

- capture `data.speed` (ModelPerformance) from the model-runtime onCompletion
  callback and write `metadata.performance`
- write nested `metadata.usage` alongside the existing flat fields (kept for
  backward-compatible readers) on both the normal and interrupted finalize
- read usage/performance from the nested shape first (flat fallback) in the
  usage service

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 01:28:53 +08:00
Zhijie He d75e193ea0 💄 style: add intern-s2-preview support, support thinking_mode (#15308)
* style: add intern-s2-preview support, support thinking_mode

* chore: remove stream limited with tools

* fix: fix search missing for intern-s1-pro

* chore: migrate to processModelList for model fetch

fix: fix ci error
2026-06-01 01:20:00 +08:00
Arvin Xu 7989952d2e feat(agent-signal): add CLI trigger command + golden snapshot fixture (#15360)
Part of LOBE-9434 (#1 / LOBE-9435). Adds the local verification tooling the
execAgent migration depends on.

- `lh agent-signal trigger`: enqueue any producer-side Agent Signal source
  event for the authenticated user (nightly_review / self_reflection /
  self_feedback_intent / user.message / tool.outcome.*)
- server: `buildTriggerSourceEvent` default-payload builder +
  `AGENT_SIGNAL_TRIGGER_SOURCE_TYPES` allowlist, surfaced through a new
  authed `agentSignal.triggerSourceEvent` tRPC procedure that re-derives
  userId from context (owner-scoped, override can't repoint it)
- golden snapshot fixture + dependency-free `assertGoldenFinalState`
  structural assertion (ideas/intents/writeOutcomes >= 1, brief non-empty)
  for use by the migration regression tests
- builder unit tests + offline/live e2e, regenerated man page

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 01:06:09 +08:00
Tsuki 480f6a8e7b feat(task): support file & image attachments (#15141)
*  feat(task): support file & image attachments (LOBE-8967)

Adds attachment / image upload to all four Task input surfaces (Create
Modal, Inline Entry, Task Instruction, Comment Input, Feedback Input)
plus comment edit. Attachments persist in `tasks.editor_data` /
`task_comments.editor_data` as part of the Lexical JSON state and flow
into agent runs via `execAgent.fileIds` — images as multimodal vision
content, documents through `documentService.parseFile` for text
extraction.

Server-side fileId resolution rides on the editor's
`extractMediaFromEditorState` (`@lobehub/editor/headless` 4.15.1), so
no junction tables are needed — editor_data is the single source of
truth. The /f/{fileId} proxy URL contract from the file router stays
the bridge between editor URLs and backend file lookup.

Five UI surfaces share `EditorCanvas` + `editorAttachments` for inline
attachment insertion. Comment display renders the Lexical state via
`@lobehub/editor/renderer`'s `LexicalRenderer` so image sizes round-
trip without the EditorCanvas hydration flash.

DB schema (`tasks.editor_data jsonb` column) landed separately via
#15280.

Fixes LOBE-8967

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(task): correct fileId prefix + accept nodes without status

Real-world editor_data exposed two bugs in the regex-based extract:

1. `fileId` prefix was wrong — the regex looked for `fle_…` but
   `idGenerator('files')` actually produces `file_…`, so every proxy
   URL `/f/file_…` silently failed to match.
2. `@lobehub/editor`'s `extractMediaFromEditorState` requires
   `status === 'uploaded'` strictly. Editor data from the cloud upload
   path and from historical inserts omits the `status` field entirely,
   so the upstream helper silently dropped everything. Walk the tree
   ourselves and treat a missing `status` as uploaded.

Verified against real `tasks.editor_data` rows: T-6 (proxy URL form)
now extracts `file_…` correctly. T-8 (cloud R2 signed URL form) still
returns `[]` — that requires either aligning cloud's `createFile` to
return the proxy URL or adding a DB-fallback resolver, tracked as a
follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(task): resolve fileIds from pre-signed editor URLs via files.url lookup

Root cause: `fileService.getFileAccessUrl()` returns different URL forms
depending on the environment:

- prod / non-dev → `getFileProxyUrl(fileId)` = `${APP_URL}/f/{fileId}`
- dev → `getFullFileUrl(file.url)` = a pre-signed R2/S3 URL

The dev branch is intentional so remote model providers can fetch the
file directly (proxy URLs point to localhost and aren't reachable). But
the pre-signed URL doesn't contain the fileId anywhere, so our regex
extract silently returned [] for every local upload — agent never saw
any attached image.

Same shape happens for historical cloud data where the editor stored
pre-signed URLs.

Fix: make `extractFileIdsFromEditorData` async and take a `{ db, userId }`
context. Fast path stays the proxy-URL regex; URLs that don't match fall
back to a single batched `SELECT id FROM files WHERE user_id = ? AND url
IN (…)` keyed on the storage path extracted from each URL's pathname.

Verified against real local data:

  T-6 (proxy URL form)         → file_2vFD2sdzW9VO   (regex fast path)
  T-8 (pre-signed R2 URL)      → file_cAQ4naT8G8r5   (DB fallback)
  T-9 (pre-signed R2 URL × 2)  → file_…, file_…      (DB fallback)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(task): dedupe fileIds by storage key in DB fallback

Same bytes re-uploaded by the same user produce multiple `files` rows
with identical `url` + `file_hash`. The DB fallback in
`extractFileIdsFromEditorData` was returning every matching row, so a
task with one inline image but three historical upload attempts fed
the agent three copies of the same image — wasteful multimodal tokens
and noisy provider input.

Group results by `files.url` and keep the first row per key. Verified
against real local data:

  T-6  (1 img, 1 upload)              → 1 fileId
  T-8  (1 img, 1 upload)              → 1 fileId
  T-9  (1 img, 2 dup uploads)         → 1 fileId (was 2)
  T-10 (1 img, 3 dup uploads)         → 1 fileId (was 3)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(editor): render inline file nodes as block-level cards

The default @lobehub/editor `ReactFile` decorator paints file attachments
as a tiny inline pill (icon + filename in monospace, inline-block with
0.4em padding), so a single PDF on its own line looked cramped and
hugged the surrounding text.

Override the upstream styling via the `className` prop the plugin
already exposes: full-width flex row, 10px gap, 14px padding,
`borderRadiusLG` corner, subtle hover, primary tint on `.selected`.
Aligns the editor's file attachment row with the Linear attachment
card look — and with the LexicalRenderer card the comment thread
already uses, so the same file looks consistent across surfaces.

The upstream component still only renders icon + name (no size), but
the layout change is the main UX win.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(editor): Linear-style file card with hover download

Replace the upstream inline pill FileNode UI with a full-width card
(icon + name + size + hover-revealed download button) wired in both the
live editor and the read-only LexicalRenderer for saved comments.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(editor): use existing editor:file.* keys for file card states

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 00:34:18 +08:00
Arvin Xu 45a6f2b440 🐛 fix(agent-runtime): retry empty LLM completions instead of silent done (#15355)
When a stalled tool loop made the model return an empty completion (no
content, no tool calls, ~0 output tokens), the harness finalized the
operation to `done` and persisted a blank assistant message — an empty
bubble with `status=done, error=null`, completely silent.

The call_llm executor now detects this "gave up" turn and throws
`ModelEmptyError`, which its existing LLM retry loop catches and re-issues
(a retry usually yields real content). Empty completions use a dedicated
retry budget (EMPTY_COMPLETION_MAX_RETRIES) so the branded provider — which
has 0 general retries because its own fallback chain re-routes failed
requests — still re-issues an HTTP-200-but-empty turn (the LOBE-9834 repro
path). If every retry is also empty, it propagates to a readable,
dashboard-visible terminal error (`ModelEmptyCompletion`, E8014, provider
attribution, countAsFailure) instead of a silent done.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 23:37:36 +08:00
LiJian 4bc77fc103 🐛 fix(creds): replace hardcoded session_context values with template variables (#15352)
* 🐛 fix(creds): replace hardcoded session_context values with template variables

- Replace hardcoded `Current user`, `Session date`, `Sandbox mode` in
  systemRole.ts with {{username}}, {{session_date}}, {{sandbox_enabled}}
- Inject {{session_date}} via Intl.DateTimeFormat in RuntimeExecutors
- Remove isCredsEnabled gate so {{CREDS_LIST}} / {{KLAVIS_SERVICES_LIST}}
  are always substituted when userId is available, regardless of execution path

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🧪 test(creds): mock klavisEnv to prevent t3-oss jsdom throws in tests

klavisEnv uses @t3-oss/env-nextjs which throws in jsdom (vitest treats
it as a client context). Previously the isCredsEnabled gate short-circuited
before the access; now that the gate is removed, the mock is needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(creds): add client-side generators and restore isCredsEnabled gate

- Add session_date and sandbox_enabled variable generators to
  contextEngineering.ts so client-side renders substitute them correctly
- Restore isCredsEnabled gate in RuntimeExecutors to avoid fetching creds
  on every call_llm step; now checks both enabledToolIds (client-activated
  path) and manifestMap (execAgent path) to cover all execution paths

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🔨 chore(creds): revert isCredsEnabled gate in RuntimeExecutors

Remove the isCredsEnabled OR-condition that caused execAgent test failures.
Keep session_date, sandbox_enabled, and always-inject CREDS_LIST/KLAVIS_SERVICES_LIST.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 20:03:51 +08:00
lobehubbot 049c81d53b 🔖 chore(release): release version v2.2.1 [skip ci] 2026-05-29 01:54:38 +00:00
542 changed files with 36966 additions and 12734 deletions
+1 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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
---
+5 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 (paginateexecute), 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 -1
View File
@@ -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
---
+4
View File
@@ -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.
+29
View File
@@ -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">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#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>
+88
View File
@@ -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
}
}
+4 -1
View File
@@ -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
+2 -1
View File
@@ -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",
+4 -2
View File
@@ -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) {
+1 -1
View File
@@ -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;
+129
View File
@@ -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);
}
},
);
}
+10 -20
View File
@@ -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}`);
}
+26
View File
@@ -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;
}
+40
View File
@@ -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,
});
}
+2
View File
@@ -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);
+34 -22
View File
@@ -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
View File
@@ -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);
+197
View File
@@ -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;
}
}
}
+1
View File
@@ -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/*"]
}
},
+4
View File
@@ -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: {
+4 -2
View File
@@ -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"
}
}
}
+2
View File
@@ -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}`);
+13
View File
@@ -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;
}
+5
View File
@@ -1,4 +1,9 @@
[
{
"children": {},
"date": "2026-05-29",
"version": "2.2.1"
},
{
"children": {},
"date": "2026-05-18",
+2 -1
View File
@@ -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 诊断相关问题。
+6
View File
@@ -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",
+91
View File
@@ -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
+123
View File
@@ -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>
+120
View File
@@ -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
```
**HomebrewmacOS**
```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>
+117
View File
@@ -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>
+114
View File
@@ -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
```
**HomebrewmacOS**
```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>
+212
View File
@@ -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:
![Choose a Model](/blog/assetsdd913561927c64d32bd390cee6846f9a.webp)
| 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.
![Upload Reference Images for Image Generation](/blog/assets3c160860feef0bd7c653eeb46f683445.webp)
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 14 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
![Generated Images in Asset Library](/blog/assets974acc551878f2f395518a3fbb9bd924.webp)
## 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 画图模型,不同模型各有所长:
![选择模型](/blog/assetsdd913561927c64d32bd390cee6846f9a.webp)
| 模型 | 最适合 |
| -------------------- | ------------------ |
| **DALL-E 3** | 写实照片、插画、精准遵循提示词 |
| **GPT Image** | 高保真编辑、图像内文本渲染 |
| **Flux** | 艺术风格、创意图像、快速生成 |
| **Stable Diffusion** | 高度可定制,支持社区风格和微调模型 |
| **Gemini Imagen** | 真实场景,整体构图能力强 |
| **fal.ai 系列模型** | 多种专业风格,生成速度快 |
用同一个提示词尝试不同模型,找到最适合你使用场景的。
### 参考图片(可选)
如果你有参考图片,可以上传作为生成的参考。点击上传按钮或直接拖入参考图片即可。根据模型不同,可以上传多张参考图片。
![上传参考图片](/blog/assets3c160860feef0bd7c653eeb46f683445.webp)
参考图片有助于模型理解你期望的风格、构图或配色方案 —— 配合提示词描述(例如替换背景、更换服饰),许多模型还支持基于参考图的**编辑**。
### 配置生成参数
右侧配置面板会展示当前模型支持的全部参数。常见控件:
- **比例(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、复制提示词,或在新一轮生成中复用完整参数
- 删除单张图片或整批
![生成的图片在资源库中](/blog/assets974acc551878f2f395518a3fbb9bd924.webp)
## 视频生成
视频工作区与图像工作区结构一致 —— 同样以提示词为先、同样的配置面板、同样的生成流 —— 只是参数针对动态画面做了调整。
### 输入提示词
描述**场景、运动和镜头**,不只是主体。模型对动词和镜头语言更敏感。
```
"金色时分一只红狐在新鲜雪地上小跑,呼气在冷空气中清晰可见,缓慢跟拍镜头,电影感"
"宇航员漂入色彩斑斓的星云,缓慢推进镜头,梦幻氛围,柔和的体积光"
"咖啡杯被慢动作微距倒入,蒸汽升腾,浅景深,商业产品镜头"
```
**视频提示词技巧:**
- **明确描述运动** — "缓慢跟拍"、"推进"、"手持"、"静态远景"、"向左横摇"
- **设置时间推进** — "起初有雾随后散去"、"门缓缓打开"
- **借用电影语言** — "浅景深"、"变形宽银幕镜头眩光"、"黄金时段"
- **保持焦点** — 一个镜头一个核心动作往往比塞进多个动作效果更好
### 选择 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:
![Choose a Model](/blog/assetsdd913561927c64d32bd390cee6846f9a.webp)
| 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.
![Upload Reference Images for Image Generation](/blog/assets3c160860feef0bd7c653eeb46f683445.webp)
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 24 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.
![Generated Images in Asset Library](/blog/assets974acc551878f2f395518a3fbb9bd924.webp)
## 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 画图模型,不同模型各有所长:
![选择模型](/blog/assetsdd913561927c64d32bd390cee6846f9a.webp)
| 模型 | 最适合 |
| -------------------- | ----------------- |
| **DALL-E 3** | 写实照片、插画、精准遵循提示词 |
| **Flux** | 艺术风格、创意图像、快速生成 |
| **Stable Diffusion** | 高度可定制,支持社区风格和微调模型 |
| **fal.ai 系列模型** | 多种专业风格,生成速度快 |
用同一个提示词尝试不同模型,找到最适合你使用场景的。
## 选择参考图片(可选)
如果你有参考图片,可以上传作为生成的参考。点击上传按钮或直接拖入参考图片即可。可以上传多张参考图片。
![上传参考图片](/blog/assets3c160860feef0bd7c653eeb46f683445.webp)
参考图片有助于模型理解你期望的风格、构图或配色方案。
## 选择图片比例
根据使用场景选择合适的比例:
- **1:1** — 社交媒体发帖、头像
- **16:9** — 宽屏、演示文稿、横幅
- **9:16** — 手机屏幕、动态、竖屏视频
- **4:3** — 通用用途、旧显示格式
- **3:2** — 摄影标准、打印
## 设置生成数量
选择一次生成多少张图片。一次生成多张可以获得不同变体供你选择。建议从 2–4 张开始,从中挑选最佳结果。
## 查看和下载图片
图像生成完成后,会显示在画图页面。你可以:
- 点击任意图片查看全尺寸预览
- 选择满意的图片并下载
- 在图片查看器中直接分享
生成的图片会自动保存到你的资源库。
![生成的图片在资源库中](/blog/assets974acc551878f2f395518a3fbb9bd924.webp)
## 获得更好结果的技巧
**迭代优化提示词** — 如果第一次的结果不够理想,每次只调整一个要素,而不是重写整个提示词。可以增加细节、改变风格词,或指定你不想要的内容。
**使用参考图片** — 上传参考图配合提示词,帮助模型匹配你期望的风格、配色或构图。
**多变体对比** — 一次生成 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>
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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": "خطأ في التكوين",
+2
View File
@@ -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": "جارٍ التحقق...",
+1 -1
View File
@@ -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": "Грешка в конфигурацията",
+2
View File
@@ -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": "Проверка...",
+1 -1
View File
@@ -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",
+2
View File
@@ -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...",
+8
View File
@@ -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",
+1
View File
@@ -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",
+1 -1
View File
@@ -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",
+8
View File
@@ -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}}",
+2
View File
@@ -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"
}
+40
View File
@@ -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",
+4 -2
View File
@@ -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",
+8
View File
@@ -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",
+1 -1
View File
@@ -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",
+2
View File
@@ -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...",
+1 -1
View File
@@ -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": "خطای پیکربندی",
+2
View File
@@ -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": "در حال بررسی...",
+1 -1
View File
@@ -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 lenvoi. Veuillez réessayer.",
"feedback.errors.teamNotFound": "Erreur de configuration",
+2
View File
@@ -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 dimage 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...",
+1 -1
View File
@@ -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",
+2
View File
@@ -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...",
+1 -1
View File
@@ -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": "設定エラー",
+2
View File
@@ -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": "確認中...",
+1 -1
View File
@@ -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": "설정 오류",
+2
View File
@@ -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