mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-14 11:40:07 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a52669fee5 | |||
| eb256c0c1c | |||
| 3ce25eafd4 | |||
| 88bc6fc250 | |||
| 91484953ff | |||
| 639ff347ae |
@@ -37,10 +37,6 @@ description: 'Code review checklist for LobeHub. Use when reviewing PRs, diffs,
|
||||
- Keys added to `src/locales/default/{namespace}.ts` with `{feature}.{context}.{action|status}` naming
|
||||
- For PRs: `locales/` translations for all languages updated (`pnpm i18n`)
|
||||
|
||||
### SPA / routing
|
||||
|
||||
- **`desktopRouter` pair:** If the diff touches `src/spa/router/desktopRouter.config.tsx`, does it also update `src/spa/router/desktopRouter.config.desktop.tsx` with the same route paths and nesting? Single-file edits often cause drift and blank screens.
|
||||
|
||||
### Reuse
|
||||
|
||||
- Newly written code duplicates existing utilities in `packages/utils` or shared modules?
|
||||
|
||||
@@ -32,28 +32,15 @@ Hybrid routing: Next.js App Router (static pages) + React Router DOM (main SPA).
|
||||
| Route Type | Use Case | Implementation |
|
||||
| ------------------ | --------------------------------- | ---------------------------- |
|
||||
| Next.js App Router | Auth pages (login, signup, oauth) | `src/app/[variants]/(auth)/` |
|
||||
| React Router DOM | Main SPA (chat, settings) | `desktopRouter.config.tsx` + `desktopRouter.config.desktop.tsx` (must match) |
|
||||
| React Router DOM | Main SPA (chat, settings) | `desktopRouter.config.tsx` |
|
||||
|
||||
### Key Files
|
||||
|
||||
- Entry: `src/spa/entry.web.tsx` (web), `src/spa/entry.mobile.tsx`, `src/spa/entry.desktop.tsx`
|
||||
- Desktop router (pair — **always edit both** when changing routes): `src/spa/router/desktopRouter.config.tsx` (dynamic imports) and `src/spa/router/desktopRouter.config.desktop.tsx` (sync imports). Drift can cause unregistered routes / blank screen.
|
||||
- Desktop router: `src/spa/router/desktopRouter.config.tsx`
|
||||
- Mobile router: `src/spa/router/mobileRouter.config.tsx`
|
||||
- Router utilities: `src/utils/router.tsx`
|
||||
|
||||
### `.desktop.{ts,tsx}` File Sync Rule
|
||||
|
||||
**CRITICAL**: Some files have a `.desktop.ts(x)` variant that Electron uses instead of the base file. When editing a base file, **always check** if a `.desktop` counterpart exists and update it in sync. Drift causes blank pages or missing features in Electron.
|
||||
|
||||
Known pairs that must stay in sync:
|
||||
|
||||
| Base file (web, dynamic imports) | Desktop file (Electron, sync imports) |
|
||||
| --- | --- |
|
||||
| `src/spa/router/desktopRouter.config.tsx` | `src/spa/router/desktopRouter.config.desktop.tsx` |
|
||||
| `src/routes/(main)/settings/features/componentMap.ts` | `src/routes/(main)/settings/features/componentMap.desktop.ts` |
|
||||
|
||||
**How to check**: After editing any `.ts` / `.tsx` file, run `Glob` for `<filename>.desktop.{ts,tsx}` in the same directory. If a match exists, update it with the equivalent sync-import change.
|
||||
|
||||
### Router Utilities
|
||||
|
||||
```tsx
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: spa-routes
|
||||
description: MUST use when editing src/routes/ segments, src/spa/router/desktopRouter.config.tsx or desktopRouter.config.desktop.tsx (always change both together), mobileRouter.config.tsx, or when moving UI/logic between routes and src/features/.
|
||||
description: SPA route and feature structure. Use when adding or modifying SPA routes in src/routes, defining new route segments, or moving route logic into src/features. Covers how to keep routes thin and how to divide files between routes and features.
|
||||
---
|
||||
|
||||
# SPA Routes and Features Guide
|
||||
@@ -13,8 +13,6 @@ SPA structure:
|
||||
|
||||
This project uses a **roots vs features** split: `src/routes/` only holds page segments; business logic and UI live in `src/features/` by domain.
|
||||
|
||||
**Agent constraint — desktop router parity:** Edits to the desktop route tree must update **both** `src/spa/router/desktopRouter.config.tsx` and `src/spa/router/desktopRouter.config.desktop.tsx` in the same change (same paths, nesting, index routes, and segment registration). Updating only one causes drift; the missing tree can fail to register routes and surface as a **blank screen** or broken navigation on the affected build.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Adding a new SPA route or route segment
|
||||
@@ -75,21 +73,8 @@ Each feature should:
|
||||
- Layout: `export { default } from '@/features/MyFeature/MyLayout'` or compose a few feature components + `<Outlet />`.
|
||||
- Page: import from `@/features/MyFeature` (or a specific subpath) and render; no business logic in the route file.
|
||||
|
||||
5. **Register the route (desktop — two files, always)**
|
||||
- **`desktopRouter.config.tsx`:** Add the segment with `dynamicElement` / `dynamicLayout` pointing at route modules (e.g. `@/routes/(main)/my-feature`).
|
||||
- **`desktopRouter.config.desktop.tsx`:** Mirror the **same** `RouteObject` shape: identical `path` / `index` / parent-child structure. Use the static imports and elements already used in that file (see neighboring routes). Do **not** register in only one of these files.
|
||||
- **Mobile-only flows:** use `mobileRouter.config.tsx` instead (no need to duplicate into the desktop pair unless the route truly exists on both).
|
||||
|
||||
---
|
||||
|
||||
## 3a. Desktop router pair (`desktopRouter.config` × 2)
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `desktopRouter.config.tsx` | Dynamic imports via `dynamicElement` / `dynamicLayout` — code-splitting; used by `entry.web.tsx` and `entry.desktop.tsx`. |
|
||||
| `desktopRouter.config.desktop.tsx` | Same route tree with **synchronous** imports — kept for Electron / local parity and predictable bundling. |
|
||||
|
||||
Anything that changes the tree (new segment, renamed `path`, moved layout, new child route) must be reflected in **both** files in one PR or commit. Remove routes from both when deleting.
|
||||
5. **Register the route**
|
||||
- Add the segment to `src/spa/router/desktopRouter.config.tsx` (or the right router config) with `dynamicElement` / `dynamicLayout` pointing at the new route paths (e.g. `@/routes/(main)/my-feature`).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -2,64 +2,6 @@
|
||||
|
||||
# Changelog
|
||||
|
||||
### [Version 2.1.45](https://github.com/lobehub/lobe-chat/compare/v2.1.44...v2.1.45)
|
||||
|
||||
<sup>Released on **2026-03-26**</sup>
|
||||
|
||||
#### 👷 Build System
|
||||
|
||||
- **misc**: add agent task system database schema.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### Build System
|
||||
|
||||
- **misc**: add agent task system database schema, closes [#13280](https://github.com/lobehub/lobe-chat/issues/13280) ([b005a9c](https://github.com/lobehub/lobe-chat/commit/b005a9c))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
### [Version 2.1.44](https://github.com/lobehub/lobe-chat/compare/v2.2.0-nightly.202603200623...v2.1.44)
|
||||
|
||||
<sup>Released on **2026-03-20**</sup>
|
||||
|
||||
#### 🐛 Bug Fixes
|
||||
|
||||
- **misc**: misc UI/UX improvements and bug fixes.
|
||||
|
||||
#### 💄 Styles
|
||||
|
||||
- **misc**: add image/video switch.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### What's fixed
|
||||
|
||||
- **misc**: misc UI/UX improvements and bug fixes, closes [#13153](https://github.com/lobehub/lobe-chat/issues/13153) ([abd152b](https://github.com/lobehub/lobe-chat/commit/abd152b))
|
||||
|
||||
#### Styles
|
||||
|
||||
- **misc**: add image/video switch, closes [#13152](https://github.com/lobehub/lobe-chat/issues/13152) ([2067cb2](https://github.com/lobehub/lobe-chat/commit/2067cb2))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
### [Version 2.1.43](https://github.com/lobehub/lobe-chat/compare/v2.1.42...v2.1.43)
|
||||
|
||||
<sup>Released on **2026-03-16**</sup>
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
# @lobehub/cli
|
||||
|
||||
LobeHub command-line interface.
|
||||
|
||||
## Local Development
|
||||
|
||||
| Task | Command |
|
||||
| ------------------------------------------ | -------------------------- |
|
||||
| Run in dev mode | `bun run dev -- <command>` |
|
||||
| Build the CLI | `bun run build` |
|
||||
| Link `lh`/`lobe`/`lobehub` into your shell | `bun run cli:link` |
|
||||
| Remove the global link | `bun run cli:unlink` |
|
||||
|
||||
- `bun run build` only generates `dist/index.js`.
|
||||
- To make `lh` available in your shell, run `bun run cli:link`.
|
||||
- After linking, if your shell still cannot find `lh`, run `rehash` in `zsh`.
|
||||
|
||||
## Shell Completion
|
||||
|
||||
### Install completion for a linked CLI
|
||||
|
||||
| Shell | Command |
|
||||
| ------ | ------------------------------ |
|
||||
| `zsh` | `source <(lh completion zsh)` |
|
||||
| `bash` | `source <(lh completion bash)` |
|
||||
|
||||
### Use completion during local development
|
||||
|
||||
| Shell | Command |
|
||||
| ------ | -------------------------------------------- |
|
||||
| `zsh` | `source <(bun src/index.ts completion zsh)` |
|
||||
| `bash` | `source <(bun src/index.ts completion bash)` |
|
||||
|
||||
- Completion is context-aware. For example, `lh agent <Tab>` shows agent subcommands instead of top-level commands.
|
||||
- If you update completion logic locally, re-run the corresponding `source <(...)` command to reload it in the current shell session.
|
||||
- Completion only registers shell functions. It does not install the `lh` binary by itself.
|
||||
|
||||
## Quick Check
|
||||
|
||||
```bash
|
||||
which lh
|
||||
lh --help
|
||||
lh agent <TAB>
|
||||
```
|
||||
@@ -1,160 +0,0 @@
|
||||
.\" 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.1\-canary.12" "User Commands"
|
||||
.SH NAME
|
||||
lh \- LobeHub CLI \- manage and connect to LobeHub services
|
||||
.SH SYNOPSIS
|
||||
.B lh
|
||||
[\fIOPTION\fR]...
|
||||
[\fICOMMAND\fR]
|
||||
.br
|
||||
.B lobe
|
||||
[\fIOPTION\fR]...
|
||||
[\fICOMMAND\fR]
|
||||
.br
|
||||
.B lobehub
|
||||
[\fIOPTION\fR]...
|
||||
[\fICOMMAND\fR]
|
||||
.SH DESCRIPTION
|
||||
lh is the command\-line interface for LobeHub. It provides authentication, device gateway connectivity, content generation, resource search, and management commands for agents, files, models, providers, plugins, knowledge bases, threads, topics, and related resources.
|
||||
.PP
|
||||
For command-specific manuals, use the built-in manual command:
|
||||
.PP
|
||||
.RS
|
||||
.B lh man
|
||||
[\fICOMMAND\fR]...
|
||||
.RE
|
||||
.SH COMMANDS
|
||||
.TP
|
||||
.B login
|
||||
Log in to LobeHub via browser (Device Code Flow)
|
||||
.TP
|
||||
.B logout
|
||||
Log out and remove stored credentials
|
||||
.TP
|
||||
.B completion
|
||||
Output shell completion script
|
||||
.TP
|
||||
.B man
|
||||
Show a manual page for the CLI or a subcommand
|
||||
.TP
|
||||
.B connect
|
||||
Connect to the device gateway and listen for tool calls
|
||||
.TP
|
||||
.B device
|
||||
Manage connected devices
|
||||
.TP
|
||||
.B status
|
||||
Check if gateway connection can be established
|
||||
.TP
|
||||
.B doc
|
||||
Manage documents
|
||||
.TP
|
||||
.B search
|
||||
Search across local resources or the web
|
||||
.TP
|
||||
.B kb
|
||||
Manage knowledge bases, folders, documents, and files
|
||||
.TP
|
||||
.B memory
|
||||
Manage user memories
|
||||
.TP
|
||||
.B agent
|
||||
Manage agents
|
||||
.TP
|
||||
.B agent\-group
|
||||
Manage agent groups
|
||||
.TP
|
||||
.B bot
|
||||
Manage bot integrations
|
||||
.TP
|
||||
.B cron
|
||||
Manage agent cron jobs
|
||||
.TP
|
||||
.B generate
|
||||
Generate content (text, image, video, speech) Alias: gen.
|
||||
.TP
|
||||
.B file
|
||||
Manage files
|
||||
.TP
|
||||
.B skill
|
||||
Manage agent skills
|
||||
.TP
|
||||
.B session\-group
|
||||
Manage agent session groups
|
||||
.TP
|
||||
.B thread
|
||||
Manage message threads
|
||||
.TP
|
||||
.B topic
|
||||
Manage conversation topics
|
||||
.TP
|
||||
.B message
|
||||
Manage messages
|
||||
.TP
|
||||
.B model
|
||||
Manage AI models
|
||||
.TP
|
||||
.B provider
|
||||
Manage AI providers
|
||||
.TP
|
||||
.B plugin
|
||||
Manage plugins
|
||||
.TP
|
||||
.B user
|
||||
Manage user account and settings
|
||||
.TP
|
||||
.B whoami
|
||||
Display current user information
|
||||
.TP
|
||||
.B usage
|
||||
View usage statistics
|
||||
.TP
|
||||
.B eval
|
||||
Manage evaluation workflows
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.B \-V, \-\-version
|
||||
output the version number
|
||||
.TP
|
||||
.B \-h, \-\-help
|
||||
display help for command
|
||||
.SH FILES
|
||||
.TP
|
||||
.I ~/.lobehub/credentials.json
|
||||
Encrypted access and refresh tokens.
|
||||
.TP
|
||||
.I ~/.lobehub/settings.json
|
||||
CLI settings such as server and gateway URLs.
|
||||
.TP
|
||||
.I ~/.lobehub/daemon.pid
|
||||
Background daemon PID file.
|
||||
.TP
|
||||
.I ~/.lobehub/daemon.status
|
||||
Background daemon status metadata.
|
||||
.TP
|
||||
.I ~/.lobehub/daemon.log
|
||||
Background daemon log output.
|
||||
.PP
|
||||
The base directory can be overridden with the
|
||||
.B LOBEHUB_CLI_HOME
|
||||
environment variable.
|
||||
.SH EXAMPLES
|
||||
.TP
|
||||
.B lh login
|
||||
Start interactive login in the browser.
|
||||
.TP
|
||||
.B lh connect \-\-daemon
|
||||
Start the device gateway connection in the background.
|
||||
.TP
|
||||
.B lh search \-q "gpt\-5"
|
||||
Search local resources for a query.
|
||||
.TP
|
||||
.B lh generate text "Write release notes"
|
||||
Generate text from a prompt.
|
||||
.TP
|
||||
.B lh man generate
|
||||
Show the built\-in manual for the generate command group.
|
||||
.SH SEE ALSO
|
||||
.BR lobe (1),
|
||||
.BR lobehub (1)
|
||||
@@ -1 +0,0 @@
|
||||
.so man1/lh.1
|
||||
@@ -1 +0,0 @@
|
||||
.so man1/lh.1
|
||||
+12
-17
@@ -7,42 +7,37 @@
|
||||
"lobe": "./dist/index.js",
|
||||
"lobehub": "./dist/index.js"
|
||||
},
|
||||
"man": [
|
||||
"./man/man1/lh.1",
|
||||
"./man/man1/lobe.1",
|
||||
"./man/man1/lobehub.1"
|
||||
],
|
||||
"files": [
|
||||
"dist",
|
||||
"man"
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsdown",
|
||||
"build": "npx tsup",
|
||||
"cli:link": "bun link",
|
||||
"cli:unlink": "bun unlink",
|
||||
"dev": "LOBEHUB_CLI_HOME=.lobehub-dev bun src/index.ts",
|
||||
"man:generate": "bun src/man/generate.ts",
|
||||
"prepublishOnly": "npm run build && npm run man:generate",
|
||||
"prepublishOnly": "npm run build",
|
||||
"test": "bunx vitest run --config vitest.config.mts --silent='passed-only'",
|
||||
"test:coverage": "bunx vitest run --config vitest.config.mts --coverage",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lobechat/device-gateway-client": "workspace:*",
|
||||
"@lobechat/local-file-shell": "workspace:*",
|
||||
"dependencies": {
|
||||
"@trpc/client": "^11.8.1",
|
||||
"@types/node": "^22.13.5",
|
||||
"@types/ws": "^8.18.1",
|
||||
"commander": "^13.1.0",
|
||||
"debug": "^4.4.0",
|
||||
"diff": "^8.0.3",
|
||||
"fast-glob": "^3.3.3",
|
||||
"picocolors": "^1.1.1",
|
||||
"superjson": "^2.2.6",
|
||||
"tsdown": "^0.21.4",
|
||||
"typescript": "^5.9.3",
|
||||
"ws": "^8.18.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lobechat/device-gateway-client": "workspace:*",
|
||||
"@lobechat/local-file-shell": "workspace:*",
|
||||
"@types/node": "^22.13.5",
|
||||
"@types/ws": "^8.18.1",
|
||||
"tsup": "^8.4.0",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org"
|
||||
|
||||
+14
-28
@@ -5,8 +5,8 @@ import type { LambdaRouter } from '@/server/routers/lambda';
|
||||
import type { ToolsRouter } from '@/server/routers/tools';
|
||||
|
||||
import { getValidToken } from '../auth/refresh';
|
||||
import { CLI_API_KEY_ENV } from '../constants/auth';
|
||||
import { resolveServerUrl } from '../settings';
|
||||
import { OFFICIAL_SERVER_URL } from '../constants/urls';
|
||||
import { loadSettings } from '../settings';
|
||||
import { log } from '../utils/logger';
|
||||
|
||||
export type TrpcClient = ReturnType<typeof createTRPCClient<LambdaRouter>>;
|
||||
@@ -19,46 +19,31 @@ async function getAuthAndServer() {
|
||||
// LOBEHUB_JWT + LOBEHUB_SERVER env vars (used by server-side sandbox execution)
|
||||
const envJwt = process.env.LOBEHUB_JWT;
|
||||
if (envJwt) {
|
||||
const serverUrl = resolveServerUrl();
|
||||
|
||||
return {
|
||||
headers: { 'Oidc-Auth': envJwt },
|
||||
serverUrl,
|
||||
};
|
||||
}
|
||||
|
||||
const envApiKey = process.env[CLI_API_KEY_ENV];
|
||||
if (envApiKey) {
|
||||
const serverUrl = resolveServerUrl();
|
||||
|
||||
return {
|
||||
headers: { 'X-API-Key': envApiKey },
|
||||
serverUrl,
|
||||
};
|
||||
const serverUrl = process.env.LOBEHUB_SERVER || OFFICIAL_SERVER_URL;
|
||||
return { accessToken: envJwt, serverUrl: serverUrl.replace(/\/$/, '') };
|
||||
}
|
||||
|
||||
const result = await getValidToken();
|
||||
if (!result) {
|
||||
log.error(`No authentication found. Run 'lh login' first, or set ${CLI_API_KEY_ENV}.`);
|
||||
log.error("No authentication found. Run 'lh login' first.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const serverUrl = resolveServerUrl();
|
||||
const accessToken = result.credentials.accessToken;
|
||||
const serverUrl = loadSettings()?.serverUrl || OFFICIAL_SERVER_URL;
|
||||
|
||||
return {
|
||||
headers: { 'Oidc-Auth': result.credentials.accessToken },
|
||||
serverUrl,
|
||||
};
|
||||
return { accessToken, serverUrl: serverUrl.replace(/\/$/, '') };
|
||||
}
|
||||
|
||||
export async function getTrpcClient(): Promise<TrpcClient> {
|
||||
if (_client) return _client;
|
||||
|
||||
const { headers, serverUrl } = await getAuthAndServer();
|
||||
const { accessToken, serverUrl } = await getAuthAndServer();
|
||||
|
||||
_client = createTRPCClient<LambdaRouter>({
|
||||
links: [
|
||||
httpLink({
|
||||
headers,
|
||||
headers: { 'Oidc-Auth': accessToken },
|
||||
transformer: superjson,
|
||||
url: `${serverUrl}/trpc/lambda`,
|
||||
}),
|
||||
@@ -71,11 +56,12 @@ export async function getTrpcClient(): Promise<TrpcClient> {
|
||||
export async function getToolsTrpcClient(): Promise<ToolsTrpcClient> {
|
||||
if (_toolsClient) return _toolsClient;
|
||||
|
||||
const { headers, serverUrl } = await getAuthAndServer();
|
||||
const { accessToken, serverUrl } = await getAuthAndServer();
|
||||
|
||||
_toolsClient = createTRPCClient<ToolsRouter>({
|
||||
links: [
|
||||
httpLink({
|
||||
headers,
|
||||
headers: { 'Oidc-Auth': accessToken },
|
||||
transformer: superjson,
|
||||
url: `${serverUrl}/trpc/tools`,
|
||||
}),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getValidToken } from '../auth/refresh';
|
||||
import { CLI_API_KEY_ENV } from '../constants/auth';
|
||||
import { resolveServerUrl } from '../settings';
|
||||
import { OFFICIAL_SERVER_URL } from '../constants/urls';
|
||||
import { loadSettings } from '../settings';
|
||||
import { log } from '../utils/logger';
|
||||
|
||||
// Must match the server's SECRET_XOR_KEY (src/envs/auth.ts)
|
||||
@@ -33,19 +33,12 @@ export interface AuthInfo {
|
||||
export async function getAuthInfo(): Promise<AuthInfo> {
|
||||
const result = await getValidToken();
|
||||
if (!result) {
|
||||
if (process.env[CLI_API_KEY_ENV]) {
|
||||
log.error(
|
||||
`API key auth from ${CLI_API_KEY_ENV} is not supported for /webapi/* routes. Run OIDC login instead.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
log.error("No authentication found. Run 'lh login' first.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const accessToken = result!.credentials.accessToken;
|
||||
const serverUrl = resolveServerUrl();
|
||||
const serverUrl = loadSettings()?.serverUrl || OFFICIAL_SERVER_URL;
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
@@ -54,6 +47,6 @@ export async function getAuthInfo(): Promise<AuthInfo> {
|
||||
'Oidc-Auth': accessToken,
|
||||
'X-lobe-chat-auth': obfuscatePayloadWithXOR({}),
|
||||
},
|
||||
serverUrl,
|
||||
serverUrl: serverUrl.replace(/\/$/, ''),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { normalizeUrl, resolveServerUrl } from '../settings';
|
||||
|
||||
interface CurrentUserResponse {
|
||||
data?: {
|
||||
id?: string;
|
||||
userId?: string;
|
||||
};
|
||||
error?: string;
|
||||
message?: string;
|
||||
success?: boolean;
|
||||
}
|
||||
|
||||
export async function getUserIdFromApiKey(apiKey: string, serverUrl?: string): Promise<string> {
|
||||
const normalizedServerUrl = normalizeUrl(serverUrl) || resolveServerUrl();
|
||||
|
||||
const response = await fetch(`${normalizedServerUrl}/api/v1/users/me`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
let body: CurrentUserResponse | undefined;
|
||||
try {
|
||||
body = (await response.json()) as CurrentUserResponse;
|
||||
} catch {
|
||||
throw new Error(`Failed to parse response from ${normalizedServerUrl}/api/v1/users/me.`);
|
||||
}
|
||||
|
||||
if (!response.ok || body?.success === false) {
|
||||
throw new Error(
|
||||
body?.error || body?.message || `Request failed with status ${response.status}.`,
|
||||
);
|
||||
}
|
||||
|
||||
const userId = body?.data?.id || body?.data?.userId;
|
||||
if (!userId) {
|
||||
throw new Error('Current user response did not include a user id.');
|
||||
}
|
||||
|
||||
return userId;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { resolveServerUrl } from '../settings';
|
||||
import { OFFICIAL_SERVER_URL } from '../constants/urls';
|
||||
import { loadSettings } from '../settings';
|
||||
import { loadCredentials, saveCredentials, type StoredCredentials } from './credentials';
|
||||
|
||||
const CLIENT_ID = 'lobehub-cli';
|
||||
@@ -19,7 +20,7 @@ export async function getValidToken(): Promise<{ credentials: StoredCredentials
|
||||
// Token expired — try refresh
|
||||
if (!credentials.refreshToken) return null;
|
||||
|
||||
const serverUrl = resolveServerUrl();
|
||||
const serverUrl = loadSettings()?.serverUrl || OFFICIAL_SERVER_URL;
|
||||
const refreshed = await refreshAccessToken(serverUrl, credentials.refreshToken);
|
||||
if (!refreshed) return null;
|
||||
|
||||
|
||||
@@ -1,21 +1,12 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { getUserIdFromApiKey } from './apiKey';
|
||||
import { getValidToken } from './refresh';
|
||||
import { resolveToken } from './resolveToken';
|
||||
|
||||
vi.mock('./apiKey', () => ({
|
||||
getUserIdFromApiKey: vi.fn(),
|
||||
}));
|
||||
vi.mock('./refresh', () => ({
|
||||
getValidToken: vi.fn(),
|
||||
}));
|
||||
vi.mock('../settings', () => ({
|
||||
loadSettings: vi.fn().mockReturnValue({ serverUrl: 'https://app.lobehub.com' }),
|
||||
resolveServerUrl: vi.fn(() =>
|
||||
(process.env.LOBEHUB_SERVER || 'https://app.lobehub.com').replace(/\/$/, ''),
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../utils/logger', () => ({
|
||||
log: {
|
||||
debug: vi.fn(),
|
||||
@@ -34,23 +25,14 @@ function makeJwt(sub: string): string {
|
||||
|
||||
describe('resolveToken', () => {
|
||||
let exitSpy: ReturnType<typeof vi.spyOn>;
|
||||
const originalApiKey = process.env.LOBEHUB_CLI_API_KEY;
|
||||
const originalJwt = process.env.LOBEHUB_JWT;
|
||||
const originalServer = process.env.LOBEHUB_SERVER;
|
||||
|
||||
beforeEach(() => {
|
||||
exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
|
||||
throw new Error('process.exit');
|
||||
});
|
||||
delete process.env.LOBEHUB_CLI_API_KEY;
|
||||
delete process.env.LOBEHUB_JWT;
|
||||
delete process.env.LOBEHUB_SERVER;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env.LOBEHUB_CLI_API_KEY = originalApiKey;
|
||||
process.env.LOBEHUB_JWT = originalJwt;
|
||||
process.env.LOBEHUB_SERVER = originalServer;
|
||||
exitSpy.mockRestore();
|
||||
});
|
||||
|
||||
@@ -60,12 +42,7 @@ describe('resolveToken', () => {
|
||||
|
||||
const result = await resolveToken({ token });
|
||||
|
||||
expect(result).toEqual({
|
||||
serverUrl: 'https://app.lobehub.com',
|
||||
token,
|
||||
tokenType: 'jwt',
|
||||
userId: 'user-123',
|
||||
});
|
||||
expect(result).toEqual({ token, userId: 'user-123' });
|
||||
});
|
||||
|
||||
it('should exit if JWT has no sub claim', async () => {
|
||||
@@ -90,12 +67,7 @@ describe('resolveToken', () => {
|
||||
userId: 'user-456',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
serverUrl: 'https://app.lobehub.com',
|
||||
token: 'svc-token',
|
||||
tokenType: 'serviceToken',
|
||||
userId: 'user-456',
|
||||
});
|
||||
expect(result).toEqual({ token: 'svc-token', userId: 'user-456' });
|
||||
});
|
||||
|
||||
it('should exit if --user-id is not provided', async () => {
|
||||
@@ -104,37 +76,6 @@ describe('resolveToken', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('with environment api key', () => {
|
||||
it('should return API key from environment', async () => {
|
||||
process.env.LOBEHUB_CLI_API_KEY = 'sk-lh-test';
|
||||
vi.mocked(getUserIdFromApiKey).mockResolvedValue('user-789');
|
||||
|
||||
const result = await resolveToken({});
|
||||
|
||||
expect(getUserIdFromApiKey).toHaveBeenCalledWith('sk-lh-test', 'https://app.lobehub.com');
|
||||
expect(result).toEqual({
|
||||
serverUrl: 'https://app.lobehub.com',
|
||||
token: 'sk-lh-test',
|
||||
tokenType: 'apiKey',
|
||||
userId: 'user-789',
|
||||
});
|
||||
});
|
||||
|
||||
it('should prefer LOBEHUB_SERVER when validating the API key', async () => {
|
||||
process.env.LOBEHUB_CLI_API_KEY = 'sk-lh-test';
|
||||
process.env.LOBEHUB_SERVER = 'https://self-hosted.example.com/';
|
||||
vi.mocked(getUserIdFromApiKey).mockResolvedValue('user-789');
|
||||
|
||||
const result = await resolveToken({});
|
||||
|
||||
expect(getUserIdFromApiKey).toHaveBeenCalledWith(
|
||||
'sk-lh-test',
|
||||
'https://self-hosted.example.com',
|
||||
);
|
||||
expect(result.serverUrl).toBe('https://self-hosted.example.com');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with stored credentials', () => {
|
||||
it('should return stored credentials token', async () => {
|
||||
const token = makeJwt('stored-user');
|
||||
@@ -146,12 +87,7 @@ describe('resolveToken', () => {
|
||||
|
||||
const result = await resolveToken({});
|
||||
|
||||
expect(result).toEqual({
|
||||
serverUrl: 'https://app.lobehub.com',
|
||||
token,
|
||||
tokenType: 'jwt',
|
||||
userId: 'stored-user',
|
||||
});
|
||||
expect(result).toEqual({ token, userId: 'stored-user' });
|
||||
});
|
||||
|
||||
it('should exit if stored token has no sub', async () => {
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { CLI_API_KEY_ENV } from '../constants/auth';
|
||||
import { resolveServerUrl } from '../settings';
|
||||
import { log } from '../utils/logger';
|
||||
import { getUserIdFromApiKey } from './apiKey';
|
||||
import { getValidToken } from './refresh';
|
||||
|
||||
interface ResolveTokenOptions {
|
||||
@@ -11,9 +8,7 @@ interface ResolveTokenOptions {
|
||||
}
|
||||
|
||||
interface ResolvedAuth {
|
||||
serverUrl: string;
|
||||
token: string;
|
||||
tokenType: 'apiKey' | 'jwt' | 'serviceToken';
|
||||
userId: string;
|
||||
}
|
||||
|
||||
@@ -30,21 +25,20 @@ function parseJwtSub(token: string): string | undefined {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an access token from explicit options, environment variables, or stored credentials.
|
||||
* Resolve an access token from explicit options or stored credentials.
|
||||
* Exits the process if no token can be resolved.
|
||||
*/
|
||||
export async function resolveToken(options: ResolveTokenOptions): Promise<ResolvedAuth> {
|
||||
// LOBEHUB_JWT env var takes highest priority (used by server-side sandbox execution)
|
||||
const envJwt = process.env.LOBEHUB_JWT;
|
||||
if (envJwt) {
|
||||
const serverUrl = resolveServerUrl();
|
||||
const userId = parseJwtSub(envJwt);
|
||||
if (!userId) {
|
||||
log.error('Could not extract userId from LOBEHUB_JWT.');
|
||||
process.exit(1);
|
||||
}
|
||||
log.debug('Using LOBEHUB_JWT from environment');
|
||||
return { serverUrl, token: envJwt, tokenType: 'jwt', userId };
|
||||
return { token: envJwt, userId };
|
||||
}
|
||||
|
||||
// Explicit token takes priority
|
||||
@@ -54,7 +48,7 @@ export async function resolveToken(options: ResolveTokenOptions): Promise<Resolv
|
||||
log.error('Could not extract userId from token. Provide --user-id explicitly.');
|
||||
process.exit(1);
|
||||
}
|
||||
return { serverUrl: resolveServerUrl(), token: options.token, tokenType: 'jwt', userId };
|
||||
return { token: options.token, userId };
|
||||
}
|
||||
|
||||
if (options.serviceToken) {
|
||||
@@ -62,46 +56,22 @@ export async function resolveToken(options: ResolveTokenOptions): Promise<Resolv
|
||||
log.error('--user-id is required when using --service-token');
|
||||
process.exit(1);
|
||||
}
|
||||
return {
|
||||
serverUrl: resolveServerUrl(),
|
||||
token: options.serviceToken,
|
||||
tokenType: 'serviceToken',
|
||||
userId: options.userId,
|
||||
};
|
||||
}
|
||||
|
||||
const envApiKey = process.env[CLI_API_KEY_ENV];
|
||||
if (envApiKey) {
|
||||
try {
|
||||
const serverUrl = resolveServerUrl();
|
||||
const userId = await getUserIdFromApiKey(envApiKey, serverUrl);
|
||||
log.debug(`Using ${CLI_API_KEY_ENV} from environment`);
|
||||
return { serverUrl, token: envApiKey, tokenType: 'apiKey', userId };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
log.error(`Failed to validate ${CLI_API_KEY_ENV}: ${message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
return { token: options.serviceToken, userId: options.userId };
|
||||
}
|
||||
|
||||
// Try stored credentials
|
||||
const result = await getValidToken();
|
||||
if (result) {
|
||||
log.debug('Using stored credentials');
|
||||
const { credentials } = result;
|
||||
const serverUrl = resolveServerUrl();
|
||||
|
||||
const userId = parseJwtSub(credentials.accessToken);
|
||||
const token = result.credentials.accessToken;
|
||||
const userId = parseJwtSub(token);
|
||||
if (!userId) {
|
||||
log.error("Stored token is invalid. Run 'lh login' again.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return { serverUrl, token: credentials.accessToken, tokenType: 'jwt', userId };
|
||||
return { token, userId };
|
||||
}
|
||||
|
||||
log.error(
|
||||
`No authentication found. Run 'lh login' first, or set ${CLI_API_KEY_ENV}, or provide --token.`,
|
||||
);
|
||||
log.error("No authentication found. Run 'lh login' first, or provide --token.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getTrpcClient } from '../api/client';
|
||||
import { confirm, outputJson, printTable } from '../utils/format';
|
||||
import { log } from '../utils/logger';
|
||||
|
||||
const SUPPORTED_PLATFORMS = ['discord', 'slack', 'telegram', 'lark', 'feishu', 'wechat'];
|
||||
const SUPPORTED_PLATFORMS = ['discord', 'slack', 'telegram', 'lark', 'feishu'];
|
||||
|
||||
const PLATFORM_CREDENTIAL_FIELDS: Record<string, string[]> = {
|
||||
discord: ['botToken', 'publicKey'],
|
||||
@@ -13,7 +13,6 @@ const PLATFORM_CREDENTIAL_FIELDS: Record<string, string[]> = {
|
||||
lark: ['appSecret'],
|
||||
slack: ['botToken', 'signingSecret'],
|
||||
telegram: ['botToken'],
|
||||
wechat: ['botToken', 'botId'],
|
||||
};
|
||||
|
||||
function parseCredentials(
|
||||
@@ -23,7 +22,6 @@ function parseCredentials(
|
||||
const creds: Record<string, string> = {};
|
||||
|
||||
if (options.botToken) creds.botToken = options.botToken;
|
||||
if (options.botId) creds.botId = options.botId;
|
||||
if (options.publicKey) creds.publicKey = options.publicKey;
|
||||
if (options.signingSecret) creds.signingSecret = options.signingSecret;
|
||||
if (options.appSecret) creds.appSecret = options.appSecret;
|
||||
@@ -127,7 +125,6 @@ export function registerBotCommand(program: Command) {
|
||||
.requiredOption('--platform <platform>', `Platform: ${SUPPORTED_PLATFORMS.join(', ')}`)
|
||||
.requiredOption('--app-id <appId>', 'Application ID for webhook routing')
|
||||
.option('--bot-token <token>', 'Bot token')
|
||||
.option('--bot-id <id>', 'Bot ID (WeChat)')
|
||||
.option('--public-key <key>', 'Public key (Discord)')
|
||||
.option('--signing-secret <secret>', 'Signing secret (Slack)')
|
||||
.option('--app-secret <secret>', 'App secret (Lark/Feishu)')
|
||||
@@ -136,7 +133,6 @@ export function registerBotCommand(program: Command) {
|
||||
agent: string;
|
||||
appId: string;
|
||||
appSecret?: string;
|
||||
botId?: string;
|
||||
botToken?: string;
|
||||
platform: string;
|
||||
publicKey?: string;
|
||||
@@ -179,7 +175,6 @@ export function registerBotCommand(program: Command) {
|
||||
.command('update <botId>')
|
||||
.description('Update a bot integration')
|
||||
.option('--bot-token <token>', 'New bot token')
|
||||
.option('--bot-id <id>', 'New bot ID (WeChat)')
|
||||
.option('--public-key <key>', 'New public key')
|
||||
.option('--signing-secret <secret>', 'New signing secret')
|
||||
.option('--app-secret <secret>', 'New app secret')
|
||||
@@ -191,7 +186,6 @@ export function registerBotCommand(program: Command) {
|
||||
options: {
|
||||
appId?: string;
|
||||
appSecret?: string;
|
||||
botId?: string;
|
||||
botToken?: string;
|
||||
platform?: string;
|
||||
publicKey?: string;
|
||||
@@ -202,7 +196,6 @@ export function registerBotCommand(program: Command) {
|
||||
|
||||
const credentials: Record<string, string> = {};
|
||||
if (options.botToken) credentials.botToken = options.botToken;
|
||||
if (options.botId) credentials.botId = options.botId;
|
||||
if (options.publicKey) credentials.publicKey = options.publicKey;
|
||||
if (options.signingSecret) credentials.signingSecret = options.signingSecret;
|
||||
if (options.appSecret) credentials.appSecret = options.appSecret;
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
import { Command } from 'commander';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { registerCompletionCommand } from './completion';
|
||||
|
||||
describe('completion command', () => {
|
||||
let consoleSpy: ReturnType<typeof vi.spyOn>;
|
||||
const originalShell = process.env.SHELL;
|
||||
|
||||
beforeEach(() => {
|
||||
consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleSpy.mockRestore();
|
||||
delete process.env.LOBEHUB_COMP_CWORD;
|
||||
process.env.SHELL = originalShell;
|
||||
});
|
||||
|
||||
function createProgram() {
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
|
||||
program
|
||||
.command('agent')
|
||||
.description('Agent commands')
|
||||
.command('list')
|
||||
.description('List agents');
|
||||
program.command('generate').alias('gen').description('Generate content');
|
||||
program.command('usage').description('Usage').option('--month <YYYY-MM>', 'Month to query');
|
||||
program.command('internal', { hidden: true });
|
||||
|
||||
registerCompletionCommand(program);
|
||||
|
||||
return program;
|
||||
}
|
||||
|
||||
it('should output zsh completion script by default', async () => {
|
||||
process.env.SHELL = '/bin/zsh';
|
||||
|
||||
const program = createProgram();
|
||||
await program.parseAsync(['node', 'test', 'completion']);
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('compdef _lobehub_completion'));
|
||||
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('lh lobe lobehub'));
|
||||
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('"${(@)words[@]:1}"'));
|
||||
});
|
||||
|
||||
it('should output bash completion script when requested', async () => {
|
||||
const program = createProgram();
|
||||
await program.parseAsync(['node', 'test', 'completion', 'bash']);
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('complete -o nosort'));
|
||||
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('__complete'));
|
||||
});
|
||||
|
||||
it('should suggest root commands and aliases', async () => {
|
||||
process.env.LOBEHUB_COMP_CWORD = '0';
|
||||
|
||||
const program = createProgram();
|
||||
await program.parseAsync(['node', 'test', '__complete', 'g']);
|
||||
|
||||
expect(consoleSpy.mock.calls.map(([value]) => value)).toEqual(['gen', 'generate']);
|
||||
});
|
||||
|
||||
it('should suggest nested subcommands in the current command context', async () => {
|
||||
process.env.LOBEHUB_COMP_CWORD = '1';
|
||||
|
||||
const program = createProgram();
|
||||
await program.parseAsync(['node', 'test', '__complete', 'agent']);
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith('list');
|
||||
});
|
||||
|
||||
it('should suggest command options after leaf commands', async () => {
|
||||
process.env.LOBEHUB_COMP_CWORD = '1';
|
||||
|
||||
const program = createProgram();
|
||||
await program.parseAsync(['node', 'test', '__complete', 'usage']);
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith('--month');
|
||||
});
|
||||
|
||||
it('should not suggest commands while completing an option value', async () => {
|
||||
process.env.LOBEHUB_COMP_CWORD = '2';
|
||||
|
||||
const program = createProgram();
|
||||
await program.parseAsync(['node', 'test', '__complete', 'usage', '--month']);
|
||||
|
||||
expect(consoleSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not expose hidden commands', async () => {
|
||||
process.env.LOBEHUB_COMP_CWORD = '0';
|
||||
|
||||
const program = createProgram();
|
||||
await program.parseAsync(['node', 'test', '__complete']);
|
||||
|
||||
expect(consoleSpy.mock.calls.map(([value]) => value)).not.toContain('internal');
|
||||
expect(consoleSpy.mock.calls.map(([value]) => value)).not.toContain('__complete');
|
||||
});
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
import type { Command } from 'commander';
|
||||
|
||||
import {
|
||||
getCompletionCandidates,
|
||||
parseCompletionWordIndex,
|
||||
renderCompletionScript,
|
||||
resolveCompletionShell,
|
||||
} from '../utils/completion';
|
||||
|
||||
export function registerCompletionCommand(program: Command) {
|
||||
program
|
||||
.command('completion [shell]')
|
||||
.description('Output shell completion script')
|
||||
.action((shell?: string) => {
|
||||
console.log(renderCompletionScript(resolveCompletionShell(shell)));
|
||||
});
|
||||
|
||||
program
|
||||
.command('__complete', { hidden: true })
|
||||
.allowUnknownOption()
|
||||
.argument('[words...]')
|
||||
.action((words: string[] = []) => {
|
||||
const currentWordIndex = parseCompletionWordIndex(process.env.LOBEHUB_COMP_CWORD, words);
|
||||
const candidates = getCompletionCandidates(program, words, currentWordIndex);
|
||||
|
||||
for (const candidate of candidates) {
|
||||
console.log(candidate);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2,16 +2,10 @@ import { Command } from 'commander';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock('../auth/resolveToken', () => ({
|
||||
resolveToken: vi.fn().mockResolvedValue({
|
||||
serverUrl: 'https://app.lobehub.com',
|
||||
token: 'test-token',
|
||||
tokenType: 'jwt',
|
||||
userId: 'test-user',
|
||||
}),
|
||||
resolveToken: vi.fn().mockResolvedValue({ token: 'test-token', userId: 'test-user' }),
|
||||
}));
|
||||
vi.mock('../settings', () => ({
|
||||
loadSettings: vi.fn().mockReturnValue(null),
|
||||
normalizeUrl: vi.fn((url?: string) => (url ? url.replace(/\/$/, '') : undefined)),
|
||||
saveSettings: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -167,12 +161,6 @@ describe('connect command', () => {
|
||||
serverUrl: 'https://self-hosted.example.com',
|
||||
});
|
||||
});
|
||||
it('should pass the resolved serverUrl to GatewayClient', async () => {
|
||||
const program = createProgram();
|
||||
await program.parseAsync(['node', 'test', 'connect']);
|
||||
|
||||
expect(clientOptions.serverUrl).toBe('https://app.lobehub.com');
|
||||
});
|
||||
|
||||
it('should handle tool call requests', async () => {
|
||||
const program = createProgram();
|
||||
@@ -220,12 +208,7 @@ describe('connect command', () => {
|
||||
});
|
||||
|
||||
it('should handle auth_expired', async () => {
|
||||
vi.mocked(resolveToken).mockResolvedValueOnce({
|
||||
serverUrl: 'https://app.lobehub.com',
|
||||
token: 'new-tok',
|
||||
tokenType: 'jwt',
|
||||
userId: 'user',
|
||||
});
|
||||
vi.mocked(resolveToken).mockResolvedValueOnce({ token: 'new-tok', userId: 'user' });
|
||||
|
||||
const program = createProgram();
|
||||
await program.parseAsync(['node', 'test', 'connect']);
|
||||
@@ -237,24 +220,6 @@ describe('connect command', () => {
|
||||
expect(exitSpy).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('should ignore auth_expired for api key auth', async () => {
|
||||
vi.mocked(resolveToken).mockResolvedValueOnce({
|
||||
serverUrl: 'https://self-hosted.example.com',
|
||||
token: 'test-api-key',
|
||||
tokenType: 'apiKey',
|
||||
userId: 'user',
|
||||
});
|
||||
|
||||
const program = createProgram();
|
||||
await program.parseAsync(['node', 'test', 'connect']);
|
||||
|
||||
await clientEventHandlers['auth_expired']?.();
|
||||
|
||||
expect(log.error).not.toHaveBeenCalled();
|
||||
expect(cleanupAllProcesses).not.toHaveBeenCalled();
|
||||
expect(exitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle error event', async () => {
|
||||
const program = createProgram();
|
||||
await program.parseAsync(['node', 'test', 'connect']);
|
||||
|
||||
@@ -11,7 +11,6 @@ import { GatewayClient } from '@lobechat/device-gateway-client';
|
||||
import type { Command } from 'commander';
|
||||
|
||||
import { resolveToken } from '../auth/resolveToken';
|
||||
import { CLI_API_KEY_ENV } from '../constants/auth';
|
||||
import { OFFICIAL_GATEWAY_URL } from '../constants/urls';
|
||||
import {
|
||||
appendLog,
|
||||
@@ -24,7 +23,7 @@ import {
|
||||
stopDaemon,
|
||||
writeStatus,
|
||||
} from '../daemon/manager';
|
||||
import { loadSettings, normalizeUrl, saveSettings } from '../settings';
|
||||
import { loadSettings, saveSettings } from '../settings';
|
||||
import { executeToolCall } from '../tools';
|
||||
import { cleanupAllProcesses } from '../tools/shell';
|
||||
import { log, setVerbose } from '../utils/logger';
|
||||
@@ -175,7 +174,7 @@ function buildDaemonArgs(options: ConnectOptions): string[] {
|
||||
async function runConnect(options: ConnectOptions, isDaemonChild: boolean) {
|
||||
const auth = await resolveToken(options);
|
||||
const settings = loadSettings();
|
||||
const gatewayUrl = normalizeUrl(options.gateway) || settings?.gatewayUrl;
|
||||
const gatewayUrl = options.gateway?.replace(/\/$/, '') || settings?.gatewayUrl;
|
||||
|
||||
if (!gatewayUrl && settings?.serverUrl) {
|
||||
log.error(
|
||||
@@ -195,9 +194,7 @@ async function runConnect(options: ConnectOptions, isDaemonChild: boolean) {
|
||||
deviceId: options.deviceId,
|
||||
gatewayUrl: resolvedGatewayUrl,
|
||||
logger: isDaemonChild ? createDaemonLogger() : log,
|
||||
serverUrl: auth.serverUrl,
|
||||
token: auth.token,
|
||||
tokenType: auth.tokenType,
|
||||
userId: auth.userId,
|
||||
});
|
||||
|
||||
@@ -217,7 +214,7 @@ async function runConnect(options: ConnectOptions, isDaemonChild: boolean) {
|
||||
info(` Hostname : ${os.hostname()}`);
|
||||
info(` Platform : ${process.platform}`);
|
||||
info(` Gateway : ${resolvedGatewayUrl}`);
|
||||
info(` Auth : ${auth.tokenType}`);
|
||||
info(` Auth : jwt`);
|
||||
info(` Mode : ${isDaemonChild ? 'daemon' : 'foreground'}`);
|
||||
info('───────────────────');
|
||||
|
||||
@@ -288,19 +285,13 @@ async function runConnect(options: ConnectOptions, isDaemonChild: boolean) {
|
||||
// Handle auth failed
|
||||
client.on('auth_failed', (reason) => {
|
||||
error(`Authentication failed: ${reason}`);
|
||||
error(
|
||||
`Run 'lh login', or set ${CLI_API_KEY_ENV} and run 'lh login --server <url>' to configure API key authentication.`,
|
||||
);
|
||||
error("Run 'lh login' to re-authenticate.");
|
||||
cleanup();
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Handle auth expired
|
||||
client.on('auth_expired', async () => {
|
||||
if (auth.tokenType === 'apiKey') {
|
||||
return;
|
||||
}
|
||||
|
||||
error('Authentication expired. Attempting to refresh...');
|
||||
const refreshed = await resolveToken({});
|
||||
if (refreshed) {
|
||||
|
||||
@@ -3,15 +3,11 @@ import fs from 'node:fs';
|
||||
import { Command } from 'commander';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { getUserIdFromApiKey } from '../auth/apiKey';
|
||||
import { saveCredentials } from '../auth/credentials';
|
||||
import { loadSettings, saveSettings } from '../settings';
|
||||
import { log } from '../utils/logger';
|
||||
import { registerLoginCommand, resolveCommandExecutable } from './login';
|
||||
|
||||
vi.mock('../auth/apiKey', () => ({
|
||||
getUserIdFromApiKey: vi.fn(),
|
||||
}));
|
||||
vi.mock('../auth/credentials', () => ({
|
||||
saveCredentials: vi.fn(),
|
||||
}));
|
||||
@@ -41,7 +37,6 @@ vi.mock('node:child_process', () => ({
|
||||
|
||||
describe('login command', () => {
|
||||
let exitSpy: ReturnType<typeof vi.spyOn>;
|
||||
const originalApiKey = process.env.LOBEHUB_CLI_API_KEY;
|
||||
const originalPath = process.env.PATH;
|
||||
const originalPathext = process.env.PATHEXT;
|
||||
const originalSystemRoot = process.env.SystemRoot;
|
||||
@@ -51,13 +46,11 @@ describe('login command', () => {
|
||||
vi.stubGlobal('fetch', vi.fn());
|
||||
exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {}) as any);
|
||||
vi.mocked(loadSettings).mockReturnValue(null);
|
||||
delete process.env.LOBEHUB_CLI_API_KEY;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
exitSpy.mockRestore();
|
||||
process.env.LOBEHUB_CLI_API_KEY = originalApiKey;
|
||||
process.env.PATH = originalPath;
|
||||
process.env.PATHEXT = originalPathext;
|
||||
process.env.SystemRoot = originalSystemRoot;
|
||||
@@ -109,12 +102,8 @@ describe('login command', () => {
|
||||
} as any;
|
||||
}
|
||||
|
||||
async function runLogin(program: Command, args: string[] = []) {
|
||||
return program.parseAsync(['node', 'test', 'login', ...args]);
|
||||
}
|
||||
|
||||
async function runLoginAndAdvanceTimers(program: Command, args: string[] = []) {
|
||||
const parsePromise = runLogin(program, args);
|
||||
const parsePromise = program.parseAsync(['node', 'test', 'login', ...args]);
|
||||
// Advance timers to let sleep resolve in the polling loop
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await vi.advanceTimersByTimeAsync(2000);
|
||||
@@ -141,19 +130,6 @@ describe('login command', () => {
|
||||
expect(log.info).toHaveBeenCalledWith(expect.stringContaining('Login successful'));
|
||||
});
|
||||
|
||||
it('should use environment api key without storing credentials', async () => {
|
||||
process.env.LOBEHUB_CLI_API_KEY = 'sk-lh-env-test';
|
||||
vi.mocked(getUserIdFromApiKey).mockResolvedValue('user-123');
|
||||
|
||||
const program = createProgram();
|
||||
await runLogin(program);
|
||||
|
||||
expect(getUserIdFromApiKey).toHaveBeenCalledWith('sk-lh-env-test', 'https://app.lobehub.com');
|
||||
expect(saveCredentials).not.toHaveBeenCalled();
|
||||
expect(saveSettings).toHaveBeenCalledWith({ serverUrl: 'https://app.lobehub.com' });
|
||||
expect(log.info).toHaveBeenCalledWith(expect.stringContaining('Login successful'));
|
||||
});
|
||||
|
||||
it('should persist custom server into settings', async () => {
|
||||
vi.mocked(fetch)
|
||||
.mockResolvedValueOnce(deviceAuthResponse())
|
||||
@@ -183,23 +159,6 @@ describe('login command', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should preserve existing gateway for environment api key on the same server', async () => {
|
||||
process.env.LOBEHUB_CLI_API_KEY = 'sk-lh-env-test';
|
||||
vi.mocked(getUserIdFromApiKey).mockResolvedValue('user-123');
|
||||
vi.mocked(loadSettings).mockReturnValueOnce({
|
||||
gatewayUrl: 'https://gateway.example.com',
|
||||
serverUrl: 'https://test.com',
|
||||
});
|
||||
|
||||
const program = createProgram();
|
||||
await runLogin(program, ['--server', 'https://test.com/']);
|
||||
|
||||
expect(saveSettings).toHaveBeenCalledWith({
|
||||
gatewayUrl: 'https://gateway.example.com',
|
||||
serverUrl: 'https://test.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('should clear existing gateway when logging into a different server', async () => {
|
||||
vi.mocked(loadSettings).mockReturnValueOnce({
|
||||
gatewayUrl: 'https://gateway.example.com',
|
||||
|
||||
@@ -4,11 +4,9 @@ import path from 'node:path';
|
||||
|
||||
import type { Command } from 'commander';
|
||||
|
||||
import { getUserIdFromApiKey } from '../auth/apiKey';
|
||||
import { saveCredentials } from '../auth/credentials';
|
||||
import { CLI_API_KEY_ENV } from '../constants/auth';
|
||||
import { OFFICIAL_SERVER_URL } from '../constants/urls';
|
||||
import { loadSettings, normalizeUrl, saveSettings } from '../settings';
|
||||
import { loadSettings, saveSettings } from '../settings';
|
||||
import { log } from '../utils/logger';
|
||||
|
||||
const CLIENT_ID = 'lobehub-cli';
|
||||
@@ -53,43 +51,13 @@ async function parseJsonResponse<T>(res: Response, endpoint: string): Promise<T>
|
||||
export function registerLoginCommand(program: Command) {
|
||||
program
|
||||
.command('login')
|
||||
.description('Log in to LobeHub via browser (Device Code Flow) or configure API key server')
|
||||
.description('Log in to LobeHub via browser (Device Code Flow)')
|
||||
.option('--server <url>', 'LobeHub server URL', OFFICIAL_SERVER_URL)
|
||||
.action(async (options: LoginOptions) => {
|
||||
const serverUrl = normalizeUrl(options.server) || OFFICIAL_SERVER_URL;
|
||||
const serverUrl = options.server.replace(/\/$/, '');
|
||||
|
||||
log.info('Starting login...');
|
||||
|
||||
const apiKey = process.env[CLI_API_KEY_ENV];
|
||||
if (apiKey) {
|
||||
try {
|
||||
await getUserIdFromApiKey(apiKey, serverUrl);
|
||||
|
||||
const existingSettings = loadSettings();
|
||||
const shouldPreserveGateway = existingSettings?.serverUrl === serverUrl;
|
||||
|
||||
saveSettings(
|
||||
shouldPreserveGateway
|
||||
? {
|
||||
gatewayUrl: existingSettings.gatewayUrl,
|
||||
serverUrl,
|
||||
}
|
||||
: {
|
||||
// Gateway auth is tied to the login server's token issuer/JWKS.
|
||||
// When server changes, clear old gateway to avoid stale cross-environment config.
|
||||
serverUrl,
|
||||
},
|
||||
);
|
||||
log.info('Login successful! Credentials saved.');
|
||||
return;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
log.error(`API key validation failed: ${message}`);
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 1: Request device code
|
||||
let deviceAuth: DeviceAuthResponse;
|
||||
try {
|
||||
@@ -196,7 +164,6 @@ export function registerLoginCommand(program: Command) {
|
||||
: undefined,
|
||||
refreshToken: body.refresh_token,
|
||||
});
|
||||
|
||||
const existingSettings = loadSettings();
|
||||
const shouldPreserveGateway = existingSettings?.serverUrl === serverUrl;
|
||||
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
import { Command } from 'commander';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { registerManCommand } from './man';
|
||||
|
||||
describe('man command', () => {
|
||||
let consoleSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(() => {
|
||||
consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
function createProgram() {
|
||||
const program = new Command();
|
||||
|
||||
program.name('lh').description('Sample CLI').version('1.0.0');
|
||||
|
||||
const generate = program
|
||||
.command('generate')
|
||||
.alias('gen')
|
||||
.description('Generate content')
|
||||
.option('-m, --model <model>', 'Model to use');
|
||||
|
||||
generate
|
||||
.command('text <prompt>')
|
||||
.description('Generate text from a prompt')
|
||||
.option('--json', 'Output raw JSON');
|
||||
|
||||
program.command('login').description('Log in to LobeHub');
|
||||
|
||||
registerManCommand(program);
|
||||
program.exitOverride();
|
||||
|
||||
return program;
|
||||
}
|
||||
|
||||
it('renders a manual page for the root command', async () => {
|
||||
const program = createProgram();
|
||||
|
||||
await program.parseAsync(['node', 'test', 'man']);
|
||||
|
||||
const output = consoleSpy.mock.calls.at(0)?.[0];
|
||||
|
||||
expect(output).toContain('LH(1)');
|
||||
expect(output).toContain('NAME\n lh - Sample CLI');
|
||||
expect(output).toContain('ALIASES\n lobe, lobehub');
|
||||
expect(output).toContain('SYNOPSIS\n lh [options] [command]');
|
||||
expect(output).toContain('generate|gen [options] [command]');
|
||||
expect(output).toContain('man [options] [command...]');
|
||||
});
|
||||
|
||||
it('renders a manual page for a command with subcommands', async () => {
|
||||
const program = createProgram();
|
||||
|
||||
await program.parseAsync(['node', 'test', 'man', 'generate']);
|
||||
|
||||
const output = consoleSpy.mock.calls.at(0)?.[0];
|
||||
|
||||
expect(output).toContain('LH-GENERATE(1)');
|
||||
expect(output).toContain('NAME\n lh generate - Generate content');
|
||||
expect(output).toContain('ALIASES\n gen');
|
||||
expect(output).toContain('SYNOPSIS\n lh generate [options] [command]');
|
||||
expect(output).toContain('text [options] <prompt>');
|
||||
expect(output).toContain('-m, --model <model>');
|
||||
});
|
||||
|
||||
it('renders arguments for a leaf command', async () => {
|
||||
const program = createProgram();
|
||||
|
||||
await program.parseAsync(['node', 'test', 'man', 'generate', 'text']);
|
||||
|
||||
const output = consoleSpy.mock.calls.at(0)?.[0];
|
||||
|
||||
expect(output).toContain('LH-GENERATE-TEXT(1)');
|
||||
expect(output).toContain('NAME\n lh generate text - Generate text from a prompt');
|
||||
expect(output).toContain('ARGUMENTS');
|
||||
expect(output).toContain('<prompt>');
|
||||
expect(output).toContain('Required argument');
|
||||
expect(output).toContain('SEE ALSO');
|
||||
});
|
||||
});
|
||||
@@ -1,212 +0,0 @@
|
||||
import type { Argument, Command } from 'commander';
|
||||
|
||||
const ROOT_ALIASES = ['lobe', 'lobehub'];
|
||||
const HELP_COMMAND_NAME = 'help';
|
||||
|
||||
interface DefinitionItem {
|
||||
description: string;
|
||||
term: string;
|
||||
}
|
||||
|
||||
interface ResolutionResult {
|
||||
command?: Command;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export function registerManCommand(program: Command) {
|
||||
program
|
||||
.command('man [command...]')
|
||||
.description('Show a manual page for the CLI or a subcommand')
|
||||
.action((commandPath: string[] | undefined) => {
|
||||
const segments = commandPath ?? [];
|
||||
const resolution = resolveCommandPath(program, segments);
|
||||
|
||||
if (!resolution.command) {
|
||||
program.error(resolution.error || 'Unknown command path.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(renderManualPage(program, resolution.command));
|
||||
});
|
||||
}
|
||||
|
||||
function resolveCommandPath(root: Command, segments: string[]): ResolutionResult {
|
||||
let current = root;
|
||||
|
||||
for (const segment of segments) {
|
||||
const next = getVisibleCommands(current).find(
|
||||
(command) => command.name() === segment || command.aliases().includes(segment),
|
||||
);
|
||||
|
||||
if (!next) {
|
||||
const currentPath = buildCommandPath(current).join(' ');
|
||||
const available = getVisibleCommands(current)
|
||||
.map((command) => command.name())
|
||||
.join(', ');
|
||||
|
||||
return {
|
||||
error: `Unknown command "${segment}" under "${currentPath}". Available: ${available || 'none'}.`,
|
||||
};
|
||||
}
|
||||
|
||||
current = next;
|
||||
}
|
||||
|
||||
return { command: current };
|
||||
}
|
||||
|
||||
function renderManualPage(root: Command, command: Command) {
|
||||
const sections = [
|
||||
formatManualHeader(command),
|
||||
formatNameSection(command),
|
||||
formatSynopsisSection(root, command),
|
||||
formatAliasesSection(command),
|
||||
formatDescriptionSection(command),
|
||||
formatArgumentsSection(command),
|
||||
formatCommandsSection(command),
|
||||
formatOptionsSection(command),
|
||||
formatSeeAlsoSection(root, command),
|
||||
].filter(Boolean);
|
||||
|
||||
return sections.join('\n\n');
|
||||
}
|
||||
|
||||
function formatManualHeader(command: Command) {
|
||||
return `${buildCommandPath(command).join('-').toUpperCase()}(1)`;
|
||||
}
|
||||
|
||||
function formatNameSection(command: Command) {
|
||||
return ['NAME', ` ${buildCommandPath(command).join(' ')} - ${command.description()}`].join('\n');
|
||||
}
|
||||
|
||||
function formatSynopsisSection(root: Command, command: Command) {
|
||||
return ['SYNOPSIS', ` ${buildSynopsis(root, command)}`].join('\n');
|
||||
}
|
||||
|
||||
function formatAliasesSection(command: Command) {
|
||||
const aliases = command.parent ? command.aliases() : ROOT_ALIASES;
|
||||
|
||||
if (aliases.length === 0) return '';
|
||||
|
||||
return ['ALIASES', ` ${aliases.join(', ')}`].join('\n');
|
||||
}
|
||||
|
||||
function formatDescriptionSection(command: Command) {
|
||||
const description = command.description() || 'No description available.';
|
||||
|
||||
return ['DESCRIPTION', ` ${description}`].join('\n');
|
||||
}
|
||||
|
||||
function formatArgumentsSection(command: Command) {
|
||||
if (command.registeredArguments.length === 0) return '';
|
||||
|
||||
const items = command.registeredArguments.map((argument) => ({
|
||||
description: describeArgument(argument),
|
||||
term: formatArgumentTerm(argument),
|
||||
}));
|
||||
|
||||
return ['ARGUMENTS', ...formatDefinitionList(items)].join('\n');
|
||||
}
|
||||
|
||||
function formatCommandsSection(command: Command) {
|
||||
const help = command.createHelp();
|
||||
const items = getVisibleCommands(command).map((subcommand) => ({
|
||||
description: help.subcommandDescription(subcommand),
|
||||
term: buildSubcommandTerm(subcommand),
|
||||
}));
|
||||
|
||||
if (items.length === 0) return '';
|
||||
|
||||
return ['COMMANDS', ...formatDefinitionList(items)].join('\n');
|
||||
}
|
||||
|
||||
function formatOptionsSection(command: Command) {
|
||||
const help = command.createHelp();
|
||||
const items = help.visibleOptions(command).map((option) => ({
|
||||
description: help.optionDescription(option),
|
||||
term: help.optionTerm(option),
|
||||
}));
|
||||
|
||||
if (items.length === 0) return '';
|
||||
|
||||
return ['OPTIONS', ...formatDefinitionList(items)].join('\n');
|
||||
}
|
||||
|
||||
function formatSeeAlsoSection(root: Command, command: Command) {
|
||||
const items = new Set<string>();
|
||||
const currentPath = buildCommandPath(command);
|
||||
|
||||
items.add(`${currentPath.join(' ')} --help`);
|
||||
|
||||
const parent = command.parent;
|
||||
if (parent) {
|
||||
const parentPath = buildCommandPath(parent).slice(1).join(' ');
|
||||
items.add(parentPath ? `lh man ${parentPath}` : 'lh man');
|
||||
}
|
||||
|
||||
for (const subcommand of getVisibleCommands(command).slice(0, 5)) {
|
||||
items.add(`lh man ${buildCommandPath(subcommand).slice(1).join(' ')}`);
|
||||
}
|
||||
|
||||
return ['SEE ALSO', ...Array.from(items).map((item) => ` ${item}`)].join('\n');
|
||||
}
|
||||
|
||||
function getVisibleCommands(command: Command) {
|
||||
const help = command.createHelp();
|
||||
|
||||
return help
|
||||
.visibleCommands(command)
|
||||
.filter((subcommand) => subcommand.name() !== HELP_COMMAND_NAME);
|
||||
}
|
||||
|
||||
function buildSynopsis(root: Command, command: Command) {
|
||||
const path = buildCommandPath(command);
|
||||
|
||||
if (command === root) {
|
||||
return `${path[0]} ${command.usage()}`.trim();
|
||||
}
|
||||
|
||||
return `${path.join(' ')} ${command.usage()}`.trim();
|
||||
}
|
||||
|
||||
function buildCommandPath(command: Command): string[] {
|
||||
const path: string[] = [];
|
||||
let current: Command | null = command;
|
||||
|
||||
while (current) {
|
||||
path.unshift(current.name());
|
||||
current = current.parent || null;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
function buildSubcommandTerm(command: Command) {
|
||||
const name = [command.name(), ...command.aliases()].join('|');
|
||||
const usage = command.usage();
|
||||
|
||||
return usage ? `${name} ${usage}` : name;
|
||||
}
|
||||
|
||||
function formatDefinitionList(items: DefinitionItem[]) {
|
||||
const width = Math.max(...items.map((item) => item.term.length));
|
||||
|
||||
return items.map((item) => ` ${item.term.padEnd(width)} ${item.description}`);
|
||||
}
|
||||
|
||||
function formatArgumentTerm(argument: Argument) {
|
||||
const name = argument.name();
|
||||
|
||||
if (argument.required) {
|
||||
return argument.variadic ? `<${name}...>` : `<${name}>`;
|
||||
}
|
||||
|
||||
return argument.variadic ? `[${name}...]` : `[${name}]`;
|
||||
}
|
||||
|
||||
function describeArgument(argument: Argument) {
|
||||
const required = argument.required ? 'Required' : 'Optional';
|
||||
const variadic = argument.variadic ? 'variadic ' : '';
|
||||
|
||||
return `${required} ${variadic}argument`;
|
||||
}
|
||||
@@ -3,16 +3,10 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
// Mock resolveToken
|
||||
vi.mock('../auth/resolveToken', () => ({
|
||||
resolveToken: vi.fn().mockResolvedValue({
|
||||
serverUrl: 'https://app.lobehub.com',
|
||||
token: 'test-token',
|
||||
tokenType: 'jwt',
|
||||
userId: 'test-user',
|
||||
}),
|
||||
resolveToken: vi.fn().mockResolvedValue({ token: 'test-token', userId: 'test-user' }),
|
||||
}));
|
||||
vi.mock('../settings', () => ({
|
||||
loadSettings: vi.fn().mockReturnValue(null),
|
||||
normalizeUrl: vi.fn((url?: string) => (url ? url.replace(/\/$/, '') : undefined)),
|
||||
saveSettings: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -121,16 +115,6 @@ describe('status command', () => {
|
||||
serverUrl: 'https://self-hosted.example.com',
|
||||
});
|
||||
});
|
||||
it('should pass the resolved serverUrl to GatewayClient', async () => {
|
||||
const program = createProgram();
|
||||
const parsePromise = program.parseAsync(['node', 'test', 'status']);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
clientEventHandlers['connected']?.();
|
||||
|
||||
await parsePromise;
|
||||
expect(clientOptions.serverUrl).toBe('https://app.lobehub.com');
|
||||
});
|
||||
|
||||
it('should log CONNECTED on successful connection', async () => {
|
||||
const program = createProgram();
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Command } from 'commander';
|
||||
|
||||
import { resolveToken } from '../auth/resolveToken';
|
||||
import { OFFICIAL_GATEWAY_URL } from '../constants/urls';
|
||||
import { loadSettings, normalizeUrl, saveSettings } from '../settings';
|
||||
import { loadSettings, saveSettings } from '../settings';
|
||||
import { log, setVerbose } from '../utils/logger';
|
||||
|
||||
interface StatusOptions {
|
||||
@@ -30,7 +30,7 @@ export function registerStatusCommand(program: Command) {
|
||||
|
||||
const auth = await resolveToken(options);
|
||||
const settings = loadSettings();
|
||||
const gatewayUrl = normalizeUrl(options.gateway) || settings?.gatewayUrl;
|
||||
const gatewayUrl = options.gateway?.replace(/\/$/, '') || settings?.gatewayUrl;
|
||||
|
||||
if (!gatewayUrl && settings?.serverUrl) {
|
||||
log.error(
|
||||
@@ -50,9 +50,7 @@ export function registerStatusCommand(program: Command) {
|
||||
autoReconnect: false,
|
||||
gatewayUrl: gatewayUrl || OFFICIAL_GATEWAY_URL,
|
||||
logger: log,
|
||||
serverUrl: auth.serverUrl,
|
||||
token: auth.token,
|
||||
tokenType: auth.tokenType,
|
||||
userId: auth.userId,
|
||||
});
|
||||
|
||||
|
||||
@@ -139,72 +139,11 @@ export function registerTaskCommand(program: Command) {
|
||||
|
||||
task
|
||||
.command('view <id>')
|
||||
.description('View task details (by ID or identifier like T-1)')
|
||||
.description('View task details (by ID or identifier like TASK-1)')
|
||||
.option('--json [fields]', 'Output JSON')
|
||||
.action(async (id: string, options: { json?: string | boolean }) => {
|
||||
const client = await getTrpcClient();
|
||||
|
||||
// ── Auto-detect by id prefix ──
|
||||
|
||||
// docs_ → show document content
|
||||
if (id.startsWith('docs_')) {
|
||||
const doc = await client.document.getDocumentDetail.query({ id });
|
||||
|
||||
if (options.json !== undefined) {
|
||||
outputJson(doc, options.json);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!doc) {
|
||||
log.error('Document not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`\n📄 ${pc.bold(doc.title || 'Untitled')} ${pc.dim(doc.id)}`);
|
||||
if (doc.fileType) console.log(`${pc.dim('Type:')} ${doc.fileType}`);
|
||||
if (doc.totalCharCount) console.log(`${pc.dim('Size:')} ${doc.totalCharCount} chars`);
|
||||
console.log(`${pc.dim('Updated:')} ${timeAgo(doc.updatedAt)}`);
|
||||
console.log();
|
||||
if (doc.content) {
|
||||
console.log(doc.content);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// tpc_ → show topic messages
|
||||
if (id.startsWith('tpc_')) {
|
||||
const messages = await client.message.getMessages.query({ topicId: id });
|
||||
const items = Array.isArray(messages) ? messages : [];
|
||||
|
||||
if (options.json !== undefined) {
|
||||
outputJson(items, options.json);
|
||||
return;
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
log.info('No messages in this topic.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log();
|
||||
for (const msg of items) {
|
||||
const role =
|
||||
msg.role === 'assistant'
|
||||
? pc.green('Assistant')
|
||||
: msg.role === 'user'
|
||||
? pc.blue('User')
|
||||
: pc.dim(msg.role);
|
||||
|
||||
console.log(`${pc.bold(role)} ${pc.dim(timeAgo(msg.createdAt))}`);
|
||||
if (msg.content) {
|
||||
console.log(msg.content);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Default: task detail
|
||||
const result = await client.task.detail.query({ id });
|
||||
|
||||
if (options.json !== undefined) {
|
||||
@@ -340,74 +279,83 @@ export function registerTaskCommand(program: Command) {
|
||||
const LEFT_COL = 56;
|
||||
const FROM_WIDTH = 10;
|
||||
|
||||
const renderNodes = (list: typeof nodes, indent: string, isChild: boolean) => {
|
||||
const renderNodes = (list: typeof nodes, indent: string) => {
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const node = list[i];
|
||||
const isFolder = node.fileType === 'custom/folder';
|
||||
const isLast = i === list.length - 1;
|
||||
const icon = isFolder ? '📁' : '📄';
|
||||
const connector = isChild ? (isLast ? '└── ' : '├── ') : '';
|
||||
const prefix = `${indent}${connector}${icon} `;
|
||||
const prefix = `${indent}${icon} `;
|
||||
const titleStr = truncate(node.title || 'Untitled', LEFT_COL - displayWidth(prefix));
|
||||
const titlePad = ' '.repeat(
|
||||
Math.max(1, LEFT_COL - displayWidth(prefix) - displayWidth(titleStr)),
|
||||
);
|
||||
|
||||
const fromStr = node.sourceTaskIdentifier ? `← ${node.sourceTaskIdentifier}` : '';
|
||||
const fromPad = ' '.repeat(Math.max(1, FROM_WIDTH - fromStr.length + 1));
|
||||
const fromPad = fromStr
|
||||
? ' '.repeat(Math.max(1, FROM_WIDTH - fromStr.length + 1))
|
||||
: '';
|
||||
const size =
|
||||
!isFolder && node.size
|
||||
? formatSize(node.size).padStart(6) + ' chars'
|
||||
: ''.padStart(12);
|
||||
|
||||
const ago = node.createdAt ? ` ${timeAgo(node.createdAt)}` : '';
|
||||
!isFolder && node.size ? formatSize(node.size).padStart(6) + ' chars' : '';
|
||||
|
||||
console.log(
|
||||
`${prefix}${titleStr}${titlePad}${pc.dim(`(${node.documentId})`)} ${fromStr}${fromPad}${pc.dim(size)}${pc.dim(ago)}`,
|
||||
`${prefix}${titleStr}${titlePad}${pc.dim(`(${node.documentId})`)} ${fromStr}${fromPad}${pc.dim(size)}`,
|
||||
);
|
||||
|
||||
if (node.children && node.children.length > 0) {
|
||||
const childIndent = isChild ? indent + (isLast ? ' ' : '│ ') : indent;
|
||||
renderNodes(node.children, childIndent, true);
|
||||
const childIndent = indent + (isLast ? ' ' : ' ');
|
||||
renderNodes(node.children, childIndent);
|
||||
}
|
||||
}
|
||||
};
|
||||
renderNodes(nodes, ' ', false);
|
||||
renderNodes(nodes, ' ');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Activities (already sorted desc by service) ──
|
||||
// ── Activities ──
|
||||
{
|
||||
const tl = t.timeline;
|
||||
const activities: { text: string; time: string }[] = [];
|
||||
|
||||
for (const tp of tl?.topics || []) {
|
||||
const sBadge = statusBadge(tp.status || 'running');
|
||||
activities.push({
|
||||
text: ` 💬 ${pc.dim((tp.time || '').padStart(7))} Topic #${tp.seq || '?'} ${tp.title || 'Untitled'} ${sBadge} ${pc.dim(tp.id || '')}`,
|
||||
time: tp.time || '',
|
||||
});
|
||||
}
|
||||
|
||||
for (const b of tl?.briefs || []) {
|
||||
const icon = briefIcon(b.type);
|
||||
const pri =
|
||||
b.priority === 'urgent'
|
||||
? pc.red(' [urgent]')
|
||||
: b.priority === 'normal'
|
||||
? pc.yellow(' [normal]')
|
||||
: '';
|
||||
const resolved = b.resolvedAction ? pc.green(` ✏️ ${b.resolvedAction}`) : '';
|
||||
const typeLabel = pc.dim(`[${b.type}]`);
|
||||
activities.push({
|
||||
text: ` ${icon} ${pc.dim((b.time || '').padStart(7))} Brief ${typeLabel} ${b.title}${pri}${resolved} ${pc.dim(b.id || '')}`,
|
||||
time: b.time || '',
|
||||
});
|
||||
}
|
||||
|
||||
for (const c of tl?.comments || []) {
|
||||
const author = c.agentId ? `🤖 ${c.agentId}` : '👤 user';
|
||||
activities.push({
|
||||
text: ` 💭 ${pc.dim((c.time || '').padStart(7))} ${pc.cyan(author)} ${c.content}`,
|
||||
time: c.time || '',
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`\n${pc.bold('Activities:')}`);
|
||||
const acts = t.activities || [];
|
||||
if (acts.length === 0) {
|
||||
if (activities.length === 0) {
|
||||
console.log(` ${pc.dim('No activities yet.')}`);
|
||||
} else {
|
||||
for (const act of acts) {
|
||||
const ago = act.time ? timeAgo(act.time) : '';
|
||||
const idSuffix = act.id ? ` ${pc.dim(act.id)}` : '';
|
||||
if (act.type === 'topic') {
|
||||
const sBadge = statusBadge(act.status || 'running');
|
||||
console.log(
|
||||
` 💬 ${pc.dim(ago.padStart(7))} Topic #${act.seq || '?'} ${act.title || 'Untitled'} ${sBadge}${idSuffix}`,
|
||||
);
|
||||
} else if (act.type === 'brief') {
|
||||
const icon = briefIcon(act.briefType || '');
|
||||
const pri =
|
||||
act.priority === 'urgent'
|
||||
? pc.red(' [urgent]')
|
||||
: act.priority === 'normal'
|
||||
? pc.yellow(' [normal]')
|
||||
: '';
|
||||
const resolved = act.resolvedAction ? pc.green(` ✏️ ${act.resolvedAction}`) : '';
|
||||
const typeLabel = pc.dim(`[${act.briefType}]`);
|
||||
console.log(
|
||||
` ${icon} ${pc.dim(ago.padStart(7))} Brief ${typeLabel} ${act.title}${pri}${resolved}${idSuffix}`,
|
||||
);
|
||||
} else if (act.type === 'comment') {
|
||||
const author = act.agentId ? `🤖 ${act.agentId}` : '👤 user';
|
||||
console.log(` 💭 ${pc.dim(ago.padStart(7))} ${pc.cyan(author)} ${act.content}`);
|
||||
}
|
||||
// Activities are already sorted by the service
|
||||
for (const act of activities) {
|
||||
console.log(act.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -425,7 +373,7 @@ export function registerTaskCommand(program: Command) {
|
||||
.option('--agent <id>', 'Assign to agent')
|
||||
.option('--parent <id>', 'Parent task ID')
|
||||
.option('--priority <n>', 'Priority (0=none, 1=urgent, 2=high, 3=normal, 4=low)', '0')
|
||||
.option('--prefix <prefix>', 'Identifier prefix', 'T')
|
||||
.option('--prefix <prefix>', 'Identifier prefix', 'TASK')
|
||||
.option('--json [fields]', 'Output JSON')
|
||||
.action(
|
||||
async (options: {
|
||||
@@ -471,10 +419,6 @@ export function registerTaskCommand(program: Command) {
|
||||
.option('--heartbeat-interval <n>', 'Heartbeat interval in seconds')
|
||||
.option('--heartbeat-timeout <n>', 'Heartbeat timeout in seconds (0 to disable)')
|
||||
.option('--description <text>', 'Task description')
|
||||
.option(
|
||||
'--status <status>',
|
||||
'Set status (backlog, running, paused, completed, failed, canceled)',
|
||||
)
|
||||
.option('--json [fields]', 'Output JSON')
|
||||
.action(
|
||||
async (
|
||||
@@ -488,23 +432,10 @@ export function registerTaskCommand(program: Command) {
|
||||
json?: string | boolean;
|
||||
name?: string;
|
||||
priority?: string;
|
||||
status?: string;
|
||||
},
|
||||
) => {
|
||||
const client = await getTrpcClient();
|
||||
|
||||
// Handle --status separately (uses updateStatus API)
|
||||
if (options.status) {
|
||||
const valid = ['backlog', 'running', 'paused', 'completed', 'failed', 'canceled'];
|
||||
if (!valid.includes(options.status)) {
|
||||
log.error(`Invalid status "${options.status}". Must be one of: ${valid.join(', ')}`);
|
||||
return;
|
||||
}
|
||||
const result = await client.task.updateStatus.mutate({ id, status: options.status });
|
||||
log.info(`${pc.bold(result.data.identifier)} → ${options.status}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const input: Record<string, any> = { id };
|
||||
if (options.name) input.name = options.name;
|
||||
if (options.instruction) input.instruction = options.instruction;
|
||||
|
||||
@@ -258,7 +258,7 @@ export function registerLifecycleCommands(task: Command) {
|
||||
|
||||
task
|
||||
.command('sort <id> <identifiers...>')
|
||||
.description('Reorder subtasks (e.g. lh task sort T-1 T-2 T-4 T-3)')
|
||||
.description('Reorder subtasks (e.g. lh task sort TASK-1 TASK-2 TASK-4 TASK-3)')
|
||||
.action(async (id: string, identifiers: string[]) => {
|
||||
const client = await getTrpcClient();
|
||||
const result = (await client.task.reorderSubtasks.mutate({
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const CLI_API_KEY_ENV = 'LOBEHUB_CLI_API_KEY';
|
||||
+100
-2
@@ -1,3 +1,101 @@
|
||||
import { createProgram } from './program';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
createProgram().parse();
|
||||
import { Command } from 'commander';
|
||||
|
||||
import { registerAgentCommand } from './commands/agent';
|
||||
import { registerAgentGroupCommand } from './commands/agent-group';
|
||||
import { registerBotCommand } from './commands/bot';
|
||||
import { registerBriefCommand } from './commands/brief';
|
||||
import { registerConfigCommand } from './commands/config';
|
||||
import { registerConnectCommand } from './commands/connect';
|
||||
import { registerCronCommand } from './commands/cron';
|
||||
import { registerDeviceCommand } from './commands/device';
|
||||
import { registerDocCommand } from './commands/doc';
|
||||
import { registerEvalCommand } from './commands/eval';
|
||||
import { registerFileCommand } from './commands/file';
|
||||
import { registerGenerateCommand } from './commands/generate';
|
||||
import { registerKbCommand } from './commands/kb';
|
||||
import { registerLoginCommand } from './commands/login';
|
||||
import { registerLogoutCommand } from './commands/logout';
|
||||
import { registerMemoryCommand } from './commands/memory';
|
||||
import { registerMessageCommand } from './commands/message';
|
||||
import { registerModelCommand } from './commands/model';
|
||||
import { registerPluginCommand } from './commands/plugin';
|
||||
import { registerProviderCommand } from './commands/provider';
|
||||
import { registerSearchCommand } from './commands/search';
|
||||
import { registerSessionGroupCommand } from './commands/session-group';
|
||||
import { registerSkillCommand } from './commands/skill';
|
||||
import { registerStatusCommand } from './commands/status';
|
||||
import { registerTaskCommand } from './commands/task';
|
||||
import { registerThreadCommand } from './commands/thread';
|
||||
import { registerTopicCommand } from './commands/topic';
|
||||
import { registerUserCommand } from './commands/user';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { version } = require('../package.json');
|
||||
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.name('lh')
|
||||
.description('LobeHub CLI - manage and connect to LobeHub services')
|
||||
.version(version);
|
||||
|
||||
registerLoginCommand(program);
|
||||
registerLogoutCommand(program);
|
||||
registerConnectCommand(program);
|
||||
registerDeviceCommand(program);
|
||||
registerStatusCommand(program);
|
||||
registerDocCommand(program);
|
||||
registerSearchCommand(program);
|
||||
registerKbCommand(program);
|
||||
registerMemoryCommand(program);
|
||||
registerAgentCommand(program);
|
||||
registerAgentGroupCommand(program);
|
||||
registerBotCommand(program);
|
||||
registerBriefCommand(program);
|
||||
registerCronCommand(program);
|
||||
registerGenerateCommand(program);
|
||||
registerFileCommand(program);
|
||||
registerSkillCommand(program);
|
||||
registerSessionGroupCommand(program);
|
||||
registerTaskCommand(program);
|
||||
registerThreadCommand(program);
|
||||
registerTopicCommand(program);
|
||||
registerMessageCommand(program);
|
||||
registerModelCommand(program);
|
||||
registerProviderCommand(program);
|
||||
registerPluginCommand(program);
|
||||
registerUserCommand(program);
|
||||
registerConfigCommand(program);
|
||||
registerEvalCommand(program);
|
||||
|
||||
// Global error handler for clean TRPC error output
|
||||
process.on('uncaughtException', (error: any) => {
|
||||
if (error?.name === 'TRPCClientError' || error?.constructor?.name?.includes('TRPCClientError')) {
|
||||
const message = error.message || 'Unknown error';
|
||||
const code = error.data?.code || error.shape?.data?.code || '';
|
||||
const path = error.data?.path || error.shape?.data?.path || '';
|
||||
console.error(`\x1B[31mError${path ? ` [${path}]` : ''}: ${message}\x1B[0m`);
|
||||
if (code) console.error(`\x1B[2mCode: ${code}\x1B[0m`);
|
||||
process.exit(1);
|
||||
}
|
||||
// Re-throw non-TRPC errors
|
||||
console.error(error?.message || error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (error: any) => {
|
||||
if (error?.name === 'TRPCClientError' || error?.constructor?.name?.includes('TRPCClientError')) {
|
||||
const message = error.message || 'Unknown error';
|
||||
const code = error.data?.code || error.shape?.data?.code || '';
|
||||
const path = error.data?.path || error.shape?.data?.path || '';
|
||||
console.error(`\x1B[31mError${path ? ` [${path}]` : ''}: ${message}\x1B[0m`);
|
||||
if (code) console.error(`\x1B[2mCode: ${code}\x1B[0m`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.error(error?.message || error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
program.parse();
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { cliVersion, createProgram } from '../program';
|
||||
import { generateAliasManPage, generateRootManPage } from './roff';
|
||||
|
||||
const outputDir = fileURLToPath(new URL('../../man/man1/', import.meta.url));
|
||||
|
||||
await mkdir(outputDir, { recursive: true });
|
||||
|
||||
const program = createProgram();
|
||||
|
||||
await Promise.all([
|
||||
writeFile(`${outputDir}lh.1`, generateRootManPage(program, cliVersion)),
|
||||
writeFile(`${outputDir}lobe.1`, generateAliasManPage('lh')),
|
||||
writeFile(`${outputDir}lobehub.1`, generateAliasManPage('lh')),
|
||||
]);
|
||||
@@ -1,28 +0,0 @@
|
||||
import { Command } from 'commander';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { generateAliasManPage, generateRootManPage } from './roff';
|
||||
|
||||
describe('roff manual generator', () => {
|
||||
it('renders a root man page from the command tree', () => {
|
||||
const program = new Command();
|
||||
|
||||
program.name('lh').description('Sample CLI').version('1.0.0');
|
||||
|
||||
program.command('generate').alias('gen').description('Generate content');
|
||||
program.command('login').description('Log in');
|
||||
|
||||
const output = generateRootManPage(program, '1.2.3');
|
||||
|
||||
expect(output).toContain('.TH LH 1 "" "@lobehub/cli 1.2.3" "User Commands"');
|
||||
expect(output).toContain('.SH COMMANDS');
|
||||
expect(output).toContain('.B generate');
|
||||
expect(output).toContain('Generate content Alias: gen.');
|
||||
expect(output).toContain('.B login');
|
||||
expect(output).toContain('.SH OPTIONS');
|
||||
});
|
||||
|
||||
it('renders alias man pages as so links', () => {
|
||||
expect(generateAliasManPage('lh')).toBe('.so man1/lh.1\n');
|
||||
});
|
||||
});
|
||||
@@ -1,148 +0,0 @@
|
||||
import type { Command } from 'commander';
|
||||
|
||||
const ROOT_ALIASES = ['lobe', 'lobehub'];
|
||||
const HELP_COMMAND_NAME = 'help';
|
||||
|
||||
interface RoffDefinition {
|
||||
description: string;
|
||||
term: string;
|
||||
}
|
||||
|
||||
const FILE_ENTRIES = [
|
||||
{
|
||||
description: 'Encrypted access and refresh tokens.',
|
||||
path: '~/.lobehub/credentials.json',
|
||||
},
|
||||
{
|
||||
description: 'CLI settings such as server and gateway URLs.',
|
||||
path: '~/.lobehub/settings.json',
|
||||
},
|
||||
{
|
||||
description: 'Background daemon PID file.',
|
||||
path: '~/.lobehub/daemon.pid',
|
||||
},
|
||||
{
|
||||
description: 'Background daemon status metadata.',
|
||||
path: '~/.lobehub/daemon.status',
|
||||
},
|
||||
{
|
||||
description: 'Background daemon log output.',
|
||||
path: '~/.lobehub/daemon.log',
|
||||
},
|
||||
] as const;
|
||||
|
||||
const EXAMPLES = [
|
||||
{
|
||||
command: 'lh login',
|
||||
description: 'Start interactive login in the browser.',
|
||||
},
|
||||
{
|
||||
command: 'lh connect --daemon',
|
||||
description: 'Start the device gateway connection in the background.',
|
||||
},
|
||||
{
|
||||
command: 'lh search -q "gpt-5"',
|
||||
description: 'Search local resources for a query.',
|
||||
},
|
||||
{
|
||||
command: 'lh generate text "Write release notes"',
|
||||
description: 'Generate text from a prompt.',
|
||||
},
|
||||
{
|
||||
command: 'lh man generate',
|
||||
description: 'Show the built-in manual for the generate command group.',
|
||||
},
|
||||
] as const;
|
||||
|
||||
export function generateRootManPage(program: Command, version: string) {
|
||||
const help = program.createHelp();
|
||||
const commands = getVisibleCommands(program).map((command) => ({
|
||||
description: formatCommandDescription(help.subcommandDescription(command), command.aliases()),
|
||||
term: command.name(),
|
||||
}));
|
||||
const options = help.visibleOptions(program).map((option) => ({
|
||||
description: help.optionDescription(option),
|
||||
term: help.optionTerm(option),
|
||||
}));
|
||||
|
||||
const lines = [
|
||||
'.\\" Code generated by `npm run man:generate`; DO NOT EDIT.',
|
||||
'.\\" Manual command details come from the Commander command tree.',
|
||||
`.TH LH 1 "" "${escapeRoff(`@lobehub/cli ${version}`)}" "User Commands"`,
|
||||
'.SH NAME',
|
||||
`lh \\- ${escapeRoff(program.description() || 'LobeHub CLI')}`,
|
||||
'.SH SYNOPSIS',
|
||||
...formatSynopsisLines(),
|
||||
'.SH DESCRIPTION',
|
||||
escapeRoff(
|
||||
`${program.name()} is the command-line interface for LobeHub. It provides authentication, device gateway connectivity, content generation, resource search, and management commands for agents, files, models, providers, plugins, knowledge bases, threads, topics, and related resources.`,
|
||||
),
|
||||
'.PP',
|
||||
'For command-specific manuals, use the built-in manual command:',
|
||||
'.PP',
|
||||
'.RS',
|
||||
'.B lh man',
|
||||
'[\\fICOMMAND\\fR]...',
|
||||
'.RE',
|
||||
'.SH COMMANDS',
|
||||
...formatDefinitionSection(commands, 'B'),
|
||||
'.SH OPTIONS',
|
||||
...formatDefinitionSection(options, 'B'),
|
||||
'.SH FILES',
|
||||
...FILE_ENTRIES.flatMap((entry) => [
|
||||
'.TP',
|
||||
`.I ${escapeRoff(entry.path)}`,
|
||||
escapeRoff(entry.description),
|
||||
]),
|
||||
'.PP',
|
||||
'The base directory can be overridden with the',
|
||||
'.B LOBEHUB_CLI_HOME',
|
||||
'environment variable.',
|
||||
'.SH EXAMPLES',
|
||||
...EXAMPLES.flatMap((example) => [
|
||||
'.TP',
|
||||
`.B ${escapeRoff(example.command)}`,
|
||||
escapeRoff(example.description),
|
||||
]),
|
||||
'.SH SEE ALSO',
|
||||
'.BR lobe (1),',
|
||||
'.BR lobehub (1)',
|
||||
];
|
||||
|
||||
return `${lines.join('\n')}\n`;
|
||||
}
|
||||
|
||||
export function generateAliasManPage(target: string) {
|
||||
return `.so man1/${target}.1\n`;
|
||||
}
|
||||
|
||||
function formatSynopsisLines() {
|
||||
return ['lh', ...ROOT_ALIASES]
|
||||
.flatMap((binary) => [`.B ${binary}`, '[\\fIOPTION\\fR]...', '[\\fICOMMAND\\fR]', '.br'])
|
||||
.slice(0, -1);
|
||||
}
|
||||
|
||||
function getVisibleCommands(command: Command) {
|
||||
return command
|
||||
.createHelp()
|
||||
.visibleCommands(command)
|
||||
.filter((subcommand) => subcommand.name() !== HELP_COMMAND_NAME);
|
||||
}
|
||||
|
||||
function formatCommandDescription(description: string, aliases: string[]) {
|
||||
if (aliases.length === 0) return description;
|
||||
|
||||
return `${description} Alias: ${aliases.join(', ')}.`;
|
||||
}
|
||||
|
||||
function formatDefinitionSection(items: RoffDefinition[], macro: 'B' | 'I') {
|
||||
return items.flatMap((item) => [
|
||||
'.TP',
|
||||
`.${macro} ${escapeRoff(item.term)}`,
|
||||
escapeRoff(item.description),
|
||||
]);
|
||||
}
|
||||
|
||||
function escapeRoff(value: string) {
|
||||
return value.replaceAll('\\', '\\\\').replaceAll('-', '\\-');
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
import { Command } from 'commander';
|
||||
|
||||
import { registerAgentCommand } from './commands/agent';
|
||||
import { registerAgentGroupCommand } from './commands/agent-group';
|
||||
import { registerBotCommand } from './commands/bot';
|
||||
import { registerCompletionCommand } from './commands/completion';
|
||||
import { registerConfigCommand } from './commands/config';
|
||||
import { registerConnectCommand } from './commands/connect';
|
||||
import { registerCronCommand } from './commands/cron';
|
||||
import { registerDeviceCommand } from './commands/device';
|
||||
import { registerDocCommand } from './commands/doc';
|
||||
import { registerEvalCommand } from './commands/eval';
|
||||
import { registerFileCommand } from './commands/file';
|
||||
import { registerGenerateCommand } from './commands/generate';
|
||||
import { registerKbCommand } from './commands/kb';
|
||||
import { registerLoginCommand } from './commands/login';
|
||||
import { registerLogoutCommand } from './commands/logout';
|
||||
import { registerManCommand } from './commands/man';
|
||||
import { registerMemoryCommand } from './commands/memory';
|
||||
import { registerMessageCommand } from './commands/message';
|
||||
import { registerModelCommand } from './commands/model';
|
||||
import { registerPluginCommand } from './commands/plugin';
|
||||
import { registerProviderCommand } from './commands/provider';
|
||||
import { registerSearchCommand } from './commands/search';
|
||||
import { registerSessionGroupCommand } from './commands/session-group';
|
||||
import { registerSkillCommand } from './commands/skill';
|
||||
import { registerStatusCommand } from './commands/status';
|
||||
import { registerThreadCommand } from './commands/thread';
|
||||
import { registerTopicCommand } from './commands/topic';
|
||||
import { registerUserCommand } from './commands/user';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { version } = require('../package.json');
|
||||
|
||||
export function createProgram() {
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.name('lh')
|
||||
.description('LobeHub CLI - manage and connect to LobeHub services')
|
||||
.version(version);
|
||||
|
||||
registerLoginCommand(program);
|
||||
registerLogoutCommand(program);
|
||||
registerCompletionCommand(program);
|
||||
registerManCommand(program);
|
||||
registerConnectCommand(program);
|
||||
registerDeviceCommand(program);
|
||||
registerStatusCommand(program);
|
||||
registerDocCommand(program);
|
||||
registerSearchCommand(program);
|
||||
registerKbCommand(program);
|
||||
registerMemoryCommand(program);
|
||||
registerAgentCommand(program);
|
||||
registerAgentGroupCommand(program);
|
||||
registerBotCommand(program);
|
||||
registerCronCommand(program);
|
||||
registerGenerateCommand(program);
|
||||
registerFileCommand(program);
|
||||
registerSkillCommand(program);
|
||||
registerSessionGroupCommand(program);
|
||||
registerThreadCommand(program);
|
||||
registerTopicCommand(program);
|
||||
registerMessageCommand(program);
|
||||
registerModelCommand(program);
|
||||
registerProviderCommand(program);
|
||||
registerPluginCommand(program);
|
||||
registerUserCommand(program);
|
||||
registerConfigCommand(program);
|
||||
registerEvalCommand(program);
|
||||
|
||||
return program;
|
||||
}
|
||||
|
||||
export { version as cliVersion };
|
||||
@@ -5,19 +5,18 @@ import path from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { log } from '../utils/logger';
|
||||
import { loadSettings, normalizeUrl, resolveServerUrl, saveSettings } from './index';
|
||||
import { loadSettings, saveSettings } from './index';
|
||||
|
||||
const tmpDir = path.join(os.tmpdir(), 'lobehub-cli-test-settings');
|
||||
const settingsDir = path.join(tmpDir, '.lobehub');
|
||||
const settingsFile = path.join(settingsDir, 'settings.json');
|
||||
const originalServer = process.env.LOBEHUB_SERVER;
|
||||
|
||||
vi.mock('node:os', async (importOriginal) => {
|
||||
const actual = await importOriginal<Record<string, any>>();
|
||||
return {
|
||||
...actual,
|
||||
default: {
|
||||
...actual.default,
|
||||
...actual['default'],
|
||||
homedir: () => path.join(os.tmpdir(), 'lobehub-cli-test-settings'),
|
||||
},
|
||||
};
|
||||
@@ -32,12 +31,10 @@ vi.mock('../utils/logger', () => ({
|
||||
describe('settings', () => {
|
||||
beforeEach(() => {
|
||||
fs.mkdirSync(tmpDir, { recursive: true });
|
||||
delete process.env.LOBEHUB_SERVER;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { force: true, recursive: true });
|
||||
process.env.LOBEHUB_SERVER = originalServer;
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
@@ -67,28 +64,4 @@ describe('settings', () => {
|
||||
expect(loadSettings()).toBeNull();
|
||||
expect(log.warn).toHaveBeenCalledWith(expect.stringContaining('Please delete this file'));
|
||||
});
|
||||
|
||||
it('should normalize trailing slashes', () => {
|
||||
expect(normalizeUrl('https://self-hosted.example.com/')).toBe(
|
||||
'https://self-hosted.example.com',
|
||||
);
|
||||
expect(normalizeUrl(undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should prefer LOBEHUB_SERVER over settings', () => {
|
||||
saveSettings({ serverUrl: 'https://settings.example.com/' });
|
||||
process.env.LOBEHUB_SERVER = 'https://env.example.com/';
|
||||
|
||||
expect(resolveServerUrl()).toBe('https://env.example.com');
|
||||
});
|
||||
|
||||
it('should fall back to settings then official server', () => {
|
||||
saveSettings({ serverUrl: 'https://settings.example.com/' });
|
||||
|
||||
expect(resolveServerUrl()).toBe('https://settings.example.com');
|
||||
|
||||
fs.unlinkSync(settingsFile);
|
||||
|
||||
expect(resolveServerUrl()).toBe('https://app.lobehub.com');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,17 +14,10 @@ const LOBEHUB_DIR_NAME = process.env.LOBEHUB_CLI_HOME || '.lobehub';
|
||||
const SETTINGS_DIR = path.join(os.homedir(), LOBEHUB_DIR_NAME);
|
||||
const SETTINGS_FILE = path.join(SETTINGS_DIR, 'settings.json');
|
||||
|
||||
export function normalizeUrl(url: string | undefined): string | undefined {
|
||||
function normalizeUrl(url: string | undefined): string | undefined {
|
||||
return url ? url.replace(/\/$/, '') : undefined;
|
||||
}
|
||||
|
||||
export function resolveServerUrl(): string {
|
||||
const envServerUrl = normalizeUrl(process.env.LOBEHUB_SERVER);
|
||||
const settingsServerUrl = normalizeUrl(loadSettings()?.serverUrl);
|
||||
|
||||
return envServerUrl || settingsServerUrl || OFFICIAL_SERVER_URL;
|
||||
}
|
||||
|
||||
export function saveSettings(settings: StoredSettings): void {
|
||||
const serverUrl = normalizeUrl(settings.serverUrl);
|
||||
const gatewayUrl = normalizeUrl(settings.gatewayUrl);
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
import type { Command, Option } from 'commander';
|
||||
import { InvalidArgumentError } from 'commander';
|
||||
|
||||
const CLI_BIN_NAMES = ['lh', 'lobe', 'lobehub'] as const;
|
||||
const SUPPORTED_SHELLS = ['bash', 'zsh'] as const;
|
||||
|
||||
type SupportedShell = (typeof SUPPORTED_SHELLS)[number];
|
||||
|
||||
interface HiddenCommand extends Command {
|
||||
_hidden?: boolean;
|
||||
}
|
||||
|
||||
interface HiddenOption extends Option {
|
||||
hidden: boolean;
|
||||
}
|
||||
|
||||
function isVisibleCommand(command: Command) {
|
||||
return !(command as HiddenCommand)._hidden;
|
||||
}
|
||||
|
||||
function isVisibleOption(option: Option) {
|
||||
return !(option as HiddenOption).hidden;
|
||||
}
|
||||
|
||||
function listCommandTokens(command: Command) {
|
||||
return [command.name(), ...command.aliases()].filter(Boolean);
|
||||
}
|
||||
|
||||
function listOptionTokens(command: Command) {
|
||||
return command.options
|
||||
.filter(isVisibleOption)
|
||||
.flatMap((option) => [option.short, option.long].filter(Boolean) as string[]);
|
||||
}
|
||||
|
||||
function findSubcommand(command: Command, token: string) {
|
||||
return command.commands.find(
|
||||
(subcommand) => isVisibleCommand(subcommand) && listCommandTokens(subcommand).includes(token),
|
||||
);
|
||||
}
|
||||
|
||||
function findOption(command: Command, token: string) {
|
||||
return command.options.find(
|
||||
(option) =>
|
||||
isVisibleOption(option) && (option.short === token || option.long === token || false),
|
||||
);
|
||||
}
|
||||
|
||||
function filterCandidates(candidates: string[], currentWord: string) {
|
||||
const unique = [...new Set(candidates)];
|
||||
|
||||
if (!currentWord) return unique.sort();
|
||||
|
||||
return unique.filter((candidate) => candidate.startsWith(currentWord)).sort();
|
||||
}
|
||||
|
||||
function resolveCommandContext(program: Command, completedWords: string[]) {
|
||||
let command = program;
|
||||
let expectsOptionValue = false;
|
||||
|
||||
for (const token of completedWords) {
|
||||
if (expectsOptionValue) {
|
||||
expectsOptionValue = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!token) continue;
|
||||
|
||||
if (token.startsWith('-')) {
|
||||
const option = findOption(command, token);
|
||||
|
||||
expectsOptionValue = Boolean(
|
||||
option && (option.required || option.optional || option.variadic),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const subcommand = findSubcommand(command, token);
|
||||
if (subcommand) {
|
||||
command = subcommand;
|
||||
}
|
||||
}
|
||||
|
||||
return { command, expectsOptionValue };
|
||||
}
|
||||
|
||||
export function getCompletionCandidates(
|
||||
program: Command,
|
||||
words: string[],
|
||||
currentWordIndex = words.length,
|
||||
) {
|
||||
const safeCurrentWordIndex = Math.min(Math.max(currentWordIndex, 0), words.length);
|
||||
const completedWords = words.slice(0, safeCurrentWordIndex);
|
||||
const currentWord = safeCurrentWordIndex < words.length ? words[safeCurrentWordIndex] || '' : '';
|
||||
const { command, expectsOptionValue } = resolveCommandContext(program, completedWords);
|
||||
|
||||
if (expectsOptionValue) return [];
|
||||
|
||||
const commandCandidates = currentWord.startsWith('-')
|
||||
? []
|
||||
: command.commands
|
||||
.filter(isVisibleCommand)
|
||||
.flatMap((subcommand) => listCommandTokens(subcommand));
|
||||
|
||||
if (commandCandidates.length > 0) {
|
||||
return filterCandidates(commandCandidates, currentWord);
|
||||
}
|
||||
|
||||
return filterCandidates(listOptionTokens(command), currentWord);
|
||||
}
|
||||
|
||||
export function parseCompletionWordIndex(rawValue: string | undefined, words: string[]) {
|
||||
const parsedValue = rawValue ? Number.parseInt(rawValue, 10) : Number.NaN;
|
||||
|
||||
if (Number.isNaN(parsedValue)) return words.length;
|
||||
|
||||
return Math.min(Math.max(parsedValue, 0), words.length);
|
||||
}
|
||||
|
||||
export function resolveCompletionShell(shell?: string): SupportedShell {
|
||||
const fallbackShell = process.env.SHELL?.split('/').pop() || 'zsh';
|
||||
const resolvedShell = (shell || fallbackShell).toLowerCase();
|
||||
|
||||
if ((SUPPORTED_SHELLS as readonly string[]).includes(resolvedShell)) {
|
||||
return resolvedShell as SupportedShell;
|
||||
}
|
||||
|
||||
throw new InvalidArgumentError(
|
||||
`Unsupported shell "${resolvedShell}". Supported shells: ${SUPPORTED_SHELLS.join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function renderCompletionScript(shell: SupportedShell) {
|
||||
if (shell === 'bash') {
|
||||
return [
|
||||
'# shellcheck shell=bash',
|
||||
'_lobehub_completion() {',
|
||||
" local IFS=$'\\n'",
|
||||
' local current_index=$((COMP_CWORD - 1))',
|
||||
' local completions',
|
||||
' completions=$(LOBEHUB_COMP_CWORD="$current_index" "${COMP_WORDS[0]}" __complete "${COMP_WORDS[@]:1}")',
|
||||
' COMPREPLY=($(printf \'%s\\n\' "$completions"))',
|
||||
'}',
|
||||
`complete -o nosort -F _lobehub_completion ${CLI_BIN_NAMES.join(' ')}`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
return [
|
||||
`#compdef ${CLI_BIN_NAMES.join(' ')}`,
|
||||
'_lobehub_completion() {',
|
||||
' local -a completions',
|
||||
' local current_index=$((CURRENT - 2))',
|
||||
' completions=("${(@f)$(LOBEHUB_COMP_CWORD="$current_index" "$words[1]" __complete "${(@)words[@]:1}")}")',
|
||||
" _describe 'values' completions",
|
||||
'}',
|
||||
`compdef _lobehub_completion ${CLI_BIN_NAMES.join(' ')}`,
|
||||
].join('\n');
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { defineConfig } from 'tsdown';
|
||||
|
||||
export default defineConfig({
|
||||
banner: { js: '#!/usr/bin/env node' },
|
||||
clean: true,
|
||||
deps: {
|
||||
neverBundle: ['@napi-rs/canvas'],
|
||||
},
|
||||
entry: ['src/index.ts'],
|
||||
fixedExtension: false,
|
||||
format: ['esm'],
|
||||
platform: 'node',
|
||||
target: 'node18',
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
banner: { js: '#!/usr/bin/env node' },
|
||||
clean: true,
|
||||
entry: ['src/index.ts'],
|
||||
external: ['@napi-rs/canvas', 'fast-glob', 'diff', 'debug'],
|
||||
format: ['esm'],
|
||||
noExternal: [
|
||||
'@lobechat/device-gateway-client',
|
||||
'@lobechat/local-file-shell',
|
||||
'@lobechat/file-loaders',
|
||||
'@trpc/client',
|
||||
'superjson',
|
||||
],
|
||||
platform: 'node',
|
||||
target: 'node18',
|
||||
});
|
||||
@@ -52,9 +52,8 @@ export default defineConfig({
|
||||
minify: !isDev,
|
||||
outDir: 'dist/main',
|
||||
rollupOptions: {
|
||||
// Native modules must be externalized to work correctly.
|
||||
// bufferutil and utf-8-validate are optional peer deps of ws that may not be installed.
|
||||
external: [...getExternalDependencies(), 'bufferutil', 'utf-8-validate'],
|
||||
// Native modules must be externalized to work correctly
|
||||
external: getExternalDependencies(),
|
||||
output: {
|
||||
// Prevent debug package from being bundled into index.js to avoid side-effect pollution
|
||||
manualChunks(id) {
|
||||
|
||||
@@ -50,7 +50,6 @@
|
||||
"@electron-toolkit/tsconfig": "^2.0.0",
|
||||
"@electron-toolkit/utils": "^4.0.0",
|
||||
"@lobechat/desktop-bridge": "workspace:*",
|
||||
"@lobechat/device-gateway-client": "workspace:*",
|
||||
"@lobechat/electron-client-ipc": "workspace:*",
|
||||
"@lobechat/electron-server-ipc": "workspace:*",
|
||||
"@lobechat/file-loaders": "workspace:*",
|
||||
@@ -67,7 +66,7 @@
|
||||
"consola": "^3.4.2",
|
||||
"cookie": "^1.1.1",
|
||||
"cross-env": "^10.1.0",
|
||||
"diff": "^8.0.4",
|
||||
"diff": "^8.0.2",
|
||||
"electron": "41.0.2",
|
||||
"electron-builder": "^26.8.1",
|
||||
"electron-devtools-installer": "4.0.0",
|
||||
|
||||
@@ -3,6 +3,5 @@ packages:
|
||||
- '../../packages/electron-client-ipc'
|
||||
- '../../packages/file-loaders'
|
||||
- '../../packages/desktop-bridge'
|
||||
- '../../packages/device-gateway-client'
|
||||
- '../../packages/local-file-shell'
|
||||
- '.'
|
||||
|
||||
@@ -28,11 +28,6 @@ export const defaultProxySettings: NetworkProxySettings = {
|
||||
export const STORE_DEFAULTS: ElectronMainStore = {
|
||||
dataSyncConfig: { storageMode: 'cloud' },
|
||||
encryptedTokens: {},
|
||||
gatewayDeviceDescription: '',
|
||||
gatewayDeviceId: '',
|
||||
gatewayDeviceName: '',
|
||||
gatewayEnabled: true,
|
||||
gatewayUrl: 'https://device-gateway.lobehub.com',
|
||||
locale: 'auto',
|
||||
networkProxy: defaultProxySettings,
|
||||
shortcuts: DEFAULT_SHORTCUTS_CONFIG,
|
||||
|
||||
@@ -9,7 +9,6 @@ import type {
|
||||
} from '@lobechat/electron-client-ipc';
|
||||
import { BrowserWindow, shell } from 'electron';
|
||||
|
||||
import GatewayConnectionService from '@/services/gatewayConnectionSrv';
|
||||
import { appendVercelCookie } from '@/utils/http-headers';
|
||||
import { createLogger } from '@/utils/logger';
|
||||
|
||||
@@ -44,14 +43,14 @@ export default class AuthCtr extends ControllerModule {
|
||||
/**
|
||||
* Polling related parameters
|
||||
*/
|
||||
|
||||
|
||||
private pollingInterval: NodeJS.Timeout | null = null;
|
||||
private cachedRemoteUrl: string | null = null;
|
||||
|
||||
/**
|
||||
* Auto-refresh timer
|
||||
*/
|
||||
|
||||
|
||||
private autoRefreshTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
/**
|
||||
@@ -532,9 +531,6 @@ export default class AuthCtr extends ControllerModule {
|
||||
// Start auto-refresh timer
|
||||
this.startAutoRefresh();
|
||||
|
||||
// Connect to device gateway after successful login
|
||||
this.connectGateway();
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
logger.error('Exchanging authorization code failed:', error);
|
||||
@@ -542,19 +538,6 @@ export default class AuthCtr extends ControllerModule {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to device gateway (fire-and-forget)
|
||||
*/
|
||||
private connectGateway() {
|
||||
const gatewaySrv = this.app.getService(GatewayConnectionService);
|
||||
if (gatewaySrv) {
|
||||
logger.info('Triggering gateway connection after login');
|
||||
gatewaySrv.connect().catch((error) => {
|
||||
logger.error('Gateway connection after login failed:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast token refreshed event
|
||||
*/
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
import type { GatewayConnectionStatus } from '@lobechat/electron-client-ipc';
|
||||
|
||||
import GatewayConnectionService from '@/services/gatewayConnectionSrv';
|
||||
|
||||
import { ControllerModule, IpcMethod } from './index';
|
||||
import LocalFileCtr from './LocalFileCtr';
|
||||
import RemoteServerConfigCtr from './RemoteServerConfigCtr';
|
||||
import ShellCommandCtr from './ShellCommandCtr';
|
||||
|
||||
/**
|
||||
* GatewayConnectionCtr
|
||||
*
|
||||
* Thin IPC layer that delegates to GatewayConnectionService.
|
||||
*/
|
||||
export default class GatewayConnectionCtr extends ControllerModule {
|
||||
static override readonly groupName = 'gatewayConnection';
|
||||
|
||||
// ─── Service Accessor ───
|
||||
|
||||
private get service() {
|
||||
return this.app.getService(GatewayConnectionService);
|
||||
}
|
||||
|
||||
private get remoteServerConfigCtr() {
|
||||
return this.app.getController(RemoteServerConfigCtr);
|
||||
}
|
||||
|
||||
private get localFileCtr() {
|
||||
return this.app.getController(LocalFileCtr);
|
||||
}
|
||||
|
||||
private get shellCommandCtr() {
|
||||
return this.app.getController(ShellCommandCtr);
|
||||
}
|
||||
|
||||
// ─── Lifecycle ───
|
||||
|
||||
afterAppReady() {
|
||||
const srv = this.service;
|
||||
|
||||
srv.loadOrCreateDeviceId();
|
||||
|
||||
// Wire up token provider and refresher
|
||||
srv.setTokenProvider(() => this.remoteServerConfigCtr.getAccessToken());
|
||||
srv.setTokenRefresher(() => this.remoteServerConfigCtr.refreshAccessToken());
|
||||
|
||||
// Wire up tool call handler
|
||||
srv.setToolCallHandler((apiName, args) => this.executeToolCall(apiName, args));
|
||||
|
||||
// Auto-connect if already logged in
|
||||
this.tryAutoConnect();
|
||||
}
|
||||
|
||||
// ─── IPC Methods (Renderer → Main) ───
|
||||
|
||||
@IpcMethod()
|
||||
async connect(): Promise<{ error?: string; success: boolean }> {
|
||||
this.app.storeManager.set('gatewayEnabled', true);
|
||||
return this.service.connect();
|
||||
}
|
||||
|
||||
@IpcMethod()
|
||||
async disconnect(): Promise<{ success: boolean }> {
|
||||
this.app.storeManager.set('gatewayEnabled', false);
|
||||
return this.service.disconnect();
|
||||
}
|
||||
|
||||
@IpcMethod()
|
||||
async getConnectionStatus(): Promise<{ status: GatewayConnectionStatus }> {
|
||||
return { status: this.service.getStatus() };
|
||||
}
|
||||
|
||||
@IpcMethod()
|
||||
async getDeviceInfo(): Promise<{
|
||||
description: string;
|
||||
deviceId: string;
|
||||
hostname: string;
|
||||
name: string;
|
||||
platform: string;
|
||||
}> {
|
||||
return this.service.getDeviceInfo();
|
||||
}
|
||||
|
||||
@IpcMethod()
|
||||
async setDeviceName(params: { name: string }): Promise<{ success: boolean }> {
|
||||
this.service.setDeviceName(params.name);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@IpcMethod()
|
||||
async setDeviceDescription(params: { description: string }): Promise<{ success: boolean }> {
|
||||
this.service.setDeviceDescription(params.description);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// ─── Auto Connect ───
|
||||
|
||||
private async tryAutoConnect() {
|
||||
const gatewayEnabled = this.app.storeManager.get('gatewayEnabled');
|
||||
if (!gatewayEnabled) return;
|
||||
|
||||
const isConfigured = await this.remoteServerConfigCtr.isRemoteServerConfigured();
|
||||
if (!isConfigured) return;
|
||||
|
||||
const token = await this.remoteServerConfigCtr.getAccessToken();
|
||||
if (!token) return;
|
||||
|
||||
await this.service.connect();
|
||||
}
|
||||
|
||||
// ─── Tool Call Routing ───
|
||||
|
||||
private async executeToolCall(apiName: string, args: any): Promise<unknown> {
|
||||
const methodMap: Record<string, () => Promise<unknown>> = {
|
||||
editLocalFile: () => this.localFileCtr.handleEditFile(args),
|
||||
globLocalFiles: () => this.localFileCtr.handleGlobFiles(args),
|
||||
grepContent: () => this.localFileCtr.handleGrepContent(args),
|
||||
listLocalFiles: () => this.localFileCtr.listLocalFiles(args),
|
||||
moveLocalFiles: () => this.localFileCtr.handleMoveFiles(args),
|
||||
readLocalFile: () => this.localFileCtr.readFile(args),
|
||||
renameLocalFile: () => this.localFileCtr.handleRenameFile(args),
|
||||
searchLocalFiles: () => this.localFileCtr.handleLocalFilesSearch(args),
|
||||
writeLocalFile: () => this.localFileCtr.handleWriteFile(args),
|
||||
|
||||
getCommandOutput: () => this.shellCommandCtr.handleGetCommandOutput(args),
|
||||
killCommand: () => this.shellCommandCtr.handleKillCommand(args),
|
||||
runCommand: () => this.shellCommandCtr.handleRunCommand(args),
|
||||
};
|
||||
|
||||
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.`,
|
||||
);
|
||||
}
|
||||
|
||||
return handler();
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
import { constants } from 'node:fs';
|
||||
import { access, mkdir, readFile, realpath, rm, writeFile } from 'node:fs/promises';
|
||||
import { access, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
import {
|
||||
type AuditSafePathsParams,
|
||||
type AuditSafePathsResult,
|
||||
type EditLocalFileParams,
|
||||
type EditLocalFileResult,
|
||||
type GlobFilesParams,
|
||||
@@ -54,72 +52,6 @@ import { ControllerModule, IpcMethod } from './index';
|
||||
// Create logger
|
||||
const logger = createLogger('controllers:LocalFileCtr');
|
||||
|
||||
const SAFE_PATH_PREFIXES = ['/tmp', '/var/tmp'] as const;
|
||||
|
||||
const normalizeAbsolutePath = (inputPath: string): string =>
|
||||
path.normalize(path.isAbsolute(inputPath) ? inputPath : `/${inputPath}`);
|
||||
|
||||
const resolvePathWithScope = (inputPath: string, scope: string): string =>
|
||||
path.isAbsolute(inputPath) ? inputPath : path.join(scope, inputPath);
|
||||
|
||||
const isWithinSafePathPrefixes = (targetPath: string, prefixes: readonly string[]): boolean =>
|
||||
prefixes.some((prefix) => targetPath === prefix || targetPath.startsWith(`${prefix}${path.sep}`));
|
||||
|
||||
const resolveNearestExistingRealPath = async (targetPath: string): Promise<string | undefined> => {
|
||||
let currentPath = targetPath;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
await access(currentPath, constants.F_OK);
|
||||
return normalizeAbsolutePath(await realpath(currentPath));
|
||||
} catch {
|
||||
const parentPath = path.dirname(currentPath);
|
||||
if (parentPath === currentPath) return undefined;
|
||||
currentPath = parentPath;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const resolveSafePathRealPrefixes = async (): Promise<string[]> => {
|
||||
const prefixes = new Set<string>(SAFE_PATH_PREFIXES);
|
||||
|
||||
for (const safePrefix of SAFE_PATH_PREFIXES) {
|
||||
try {
|
||||
prefixes.add(normalizeAbsolutePath(await realpath(safePrefix)));
|
||||
} catch {
|
||||
// Keep the lexical prefix if the platform does not expose this directory.
|
||||
}
|
||||
}
|
||||
|
||||
return [...prefixes];
|
||||
};
|
||||
|
||||
const areAllPathsSafeOnDisk = async (
|
||||
paths: string[],
|
||||
resolveAgainstScope: string,
|
||||
): Promise<boolean> => {
|
||||
if (paths.length === 0) return false;
|
||||
|
||||
const safeRealPrefixes = await resolveSafePathRealPrefixes();
|
||||
|
||||
for (const currentPath of paths) {
|
||||
const normalizedPath = normalizeAbsolutePath(
|
||||
resolvePathWithScope(currentPath, resolveAgainstScope),
|
||||
);
|
||||
|
||||
if (!isWithinSafePathPrefixes(normalizedPath, SAFE_PATH_PREFIXES)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const realPath = await resolveNearestExistingRealPath(normalizedPath);
|
||||
if (!realPath || !isWithinSafePathPrefixes(realPath, safeRealPrefixes)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export default class LocalFileCtr extends ControllerModule {
|
||||
static override readonly groupName = 'localSystem';
|
||||
private get searchService() {
|
||||
@@ -308,18 +240,6 @@ export default class LocalFileCtr extends ControllerModule {
|
||||
return writeLocalFile({ content, path: filePath });
|
||||
}
|
||||
|
||||
@IpcMethod()
|
||||
async auditSafePaths({
|
||||
paths,
|
||||
resolveAgainstScope,
|
||||
}: AuditSafePathsParams): Promise<AuditSafePathsResult> {
|
||||
logger.debug('Auditing safe paths', { count: paths.length, resolveAgainstScope });
|
||||
|
||||
return {
|
||||
allSafe: await areAllPathsSafeOnDisk(paths, resolveAgainstScope),
|
||||
};
|
||||
}
|
||||
|
||||
@IpcMethod()
|
||||
async handlePrepareSkillDirectory({
|
||||
forceRefresh,
|
||||
|
||||
@@ -6,7 +6,6 @@ import retry from 'async-retry';
|
||||
import { safeStorage, session as electronSession } from 'electron';
|
||||
|
||||
import { OFFICIAL_CLOUD_SERVER } from '@/const/env';
|
||||
import GatewayConnectionService from '@/services/gatewayConnectionSrv';
|
||||
import { appendVercelCookie } from '@/utils/http-headers';
|
||||
import { createLogger } from '@/utils/logger';
|
||||
|
||||
@@ -320,13 +319,6 @@ export default class RemoteServerConfigCtr extends ControllerModule {
|
||||
// Also clear from persistent storage
|
||||
logger.debug(`Deleting tokens from store key: ${this.encryptedTokensKey}`);
|
||||
this.app.storeManager.delete(this.encryptedTokensKey);
|
||||
|
||||
// Disconnect gateway when tokens are cleared (logout / token refresh failure)
|
||||
const gatewaySrv = this.app.getService(GatewayConnectionService);
|
||||
if (gatewaySrv) {
|
||||
logger.debug('Disconnecting gateway due to token clear');
|
||||
await gatewaySrv.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
import type { DataSyncConfig } from '@lobechat/electron-client-ipc';
|
||||
import { BrowserWindow, shell } from 'electron';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
@@ -99,7 +100,6 @@ const mockApp = {
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
getService: vi.fn(() => null),
|
||||
} as unknown as App;
|
||||
|
||||
describe('AuthCtr', () => {
|
||||
|
||||
@@ -1,606 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { App } from '@/core/App';
|
||||
import GatewayConnectionService from '@/services/gatewayConnectionSrv';
|
||||
|
||||
import GatewayConnectionCtr from '../GatewayConnectionCtr';
|
||||
import LocalFileCtr from '../LocalFileCtr';
|
||||
import RemoteServerConfigCtr from '../RemoteServerConfigCtr';
|
||||
import ShellCommandCtr from '../ShellCommandCtr';
|
||||
|
||||
// ─── Mocks ───
|
||||
|
||||
const { ipcMainHandleMock, MockGatewayClient } = vi.hoisted(() => {
|
||||
const { EventEmitter } = require('node:events');
|
||||
|
||||
// Must be defined inside vi.hoisted so it's available when vi.mock factories run
|
||||
class _MockGatewayClient extends EventEmitter {
|
||||
static lastInstance: _MockGatewayClient | null = null;
|
||||
static lastOptions: any = null;
|
||||
|
||||
connectionStatus = 'disconnected' as string;
|
||||
currentDeviceId: string;
|
||||
|
||||
connect = vi.fn(async () => {
|
||||
this.connectionStatus = 'connecting';
|
||||
this.emit('status_changed', 'connecting');
|
||||
});
|
||||
|
||||
disconnect = vi.fn(async () => {
|
||||
this.connectionStatus = 'disconnected';
|
||||
});
|
||||
|
||||
sendToolCallResponse = vi.fn();
|
||||
|
||||
constructor(options: any) {
|
||||
super();
|
||||
this.currentDeviceId = options.deviceId || 'mock-device-id';
|
||||
_MockGatewayClient.lastInstance = this;
|
||||
_MockGatewayClient.lastOptions = options;
|
||||
}
|
||||
|
||||
// Test helpers
|
||||
simulateConnected() {
|
||||
this.connectionStatus = 'connected';
|
||||
this.emit('status_changed', 'connected');
|
||||
this.emit('connected');
|
||||
}
|
||||
|
||||
simulateStatusChanged(status: string) {
|
||||
this.connectionStatus = status;
|
||||
this.emit('status_changed', status);
|
||||
}
|
||||
|
||||
simulateToolCallRequest(apiName: string, args: object, requestId = 'req-1') {
|
||||
this.emit('tool_call_request', {
|
||||
requestId,
|
||||
toolCall: {
|
||||
apiName,
|
||||
arguments: JSON.stringify(args),
|
||||
identifier: 'test-tool',
|
||||
},
|
||||
type: 'tool_call_request',
|
||||
});
|
||||
}
|
||||
|
||||
simulateAuthExpired() {
|
||||
this.emit('auth_expired');
|
||||
}
|
||||
|
||||
simulateError(message: string) {
|
||||
this.emit('error', new Error(message));
|
||||
}
|
||||
|
||||
simulateReconnecting(delay: number) {
|
||||
this.connectionStatus = 'reconnecting';
|
||||
this.emit('status_changed', 'reconnecting');
|
||||
this.emit('reconnecting', delay);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
MockGatewayClient: _MockGatewayClient,
|
||||
ipcMainHandleMock: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
app: {
|
||||
getPath: vi.fn((name: string) => `/mock/${name}`),
|
||||
},
|
||||
ipcMain: { handle: ipcMainHandleMock },
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/logger', () => ({
|
||||
createLogger: () => ({
|
||||
debug: vi.fn(),
|
||||
error: vi.fn(),
|
||||
info: vi.fn(),
|
||||
verbose: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('electron-is', () => ({
|
||||
macOS: vi.fn(() => false),
|
||||
windows: vi.fn(() => false),
|
||||
linux: vi.fn(() => false),
|
||||
}));
|
||||
|
||||
vi.mock('@/const/env', () => ({
|
||||
OFFICIAL_CLOUD_SERVER: 'https://lobehub-cloud.com',
|
||||
isMac: false,
|
||||
isWindows: false,
|
||||
isLinux: false,
|
||||
isDev: false,
|
||||
}));
|
||||
|
||||
vi.mock('node:crypto', () => ({
|
||||
randomUUID: vi.fn(() => 'mock-device-uuid'),
|
||||
}));
|
||||
|
||||
vi.mock('node:os', () => ({
|
||||
default: { hostname: vi.fn(() => 'mock-hostname') },
|
||||
}));
|
||||
|
||||
vi.mock('@lobechat/device-gateway-client', () => ({
|
||||
GatewayClient: MockGatewayClient,
|
||||
}));
|
||||
|
||||
// ─── Mock Controllers ───
|
||||
|
||||
const mockLocalFileCtr = {
|
||||
handleEditFile: vi.fn().mockResolvedValue({ success: true }),
|
||||
handleGlobFiles: vi.fn().mockResolvedValue({ files: [] }),
|
||||
handleGrepContent: vi.fn().mockResolvedValue({ matches: [] }),
|
||||
handleLocalFilesSearch: vi.fn().mockResolvedValue([]),
|
||||
handleMoveFiles: vi.fn().mockResolvedValue([]),
|
||||
handleRenameFile: vi.fn().mockResolvedValue({ newPath: '/mock/renamed.txt', success: true }),
|
||||
handleWriteFile: vi.fn().mockResolvedValue({ success: true }),
|
||||
listLocalFiles: vi.fn().mockResolvedValue([]),
|
||||
readFile: vi.fn().mockResolvedValue({
|
||||
charCount: 12,
|
||||
content: 'file content',
|
||||
createdTime: new Date('2024-01-01'),
|
||||
filename: 'test.txt',
|
||||
fileType: '.txt',
|
||||
lineCount: 1,
|
||||
loc: [1, 1] as [number, number],
|
||||
modifiedTime: new Date('2024-01-01'),
|
||||
totalCharCount: 12,
|
||||
totalLineCount: 1,
|
||||
}),
|
||||
} as unknown as LocalFileCtr;
|
||||
|
||||
const mockShellCommandCtr = {
|
||||
handleGetCommandOutput: vi.fn().mockResolvedValue({ output: '' }),
|
||||
handleKillCommand: vi.fn().mockResolvedValue({ success: true }),
|
||||
handleRunCommand: vi.fn().mockResolvedValue({ success: true, stdout: '' }),
|
||||
} as unknown as ShellCommandCtr;
|
||||
|
||||
const mockRemoteServerConfigCtr = {
|
||||
getAccessToken: vi.fn().mockResolvedValue('mock-access-token'),
|
||||
isRemoteServerConfigured: vi.fn().mockResolvedValue(true),
|
||||
refreshAccessToken: vi.fn().mockResolvedValue({ success: true }),
|
||||
} as unknown as RemoteServerConfigCtr;
|
||||
|
||||
const mockBroadcast = vi.fn();
|
||||
const mockStoreGet = vi.fn();
|
||||
const mockStoreSet = vi.fn();
|
||||
|
||||
const mockApp = {
|
||||
browserManager: { broadcastToAllWindows: mockBroadcast },
|
||||
getController: vi.fn((Cls) => {
|
||||
if (Cls === RemoteServerConfigCtr) return mockRemoteServerConfigCtr;
|
||||
if (Cls === LocalFileCtr) return mockLocalFileCtr;
|
||||
if (Cls === ShellCommandCtr) return mockShellCommandCtr;
|
||||
return null;
|
||||
}),
|
||||
getService: vi.fn((Cls) => {
|
||||
if (Cls === GatewayConnectionService) return mockGatewayConnectionSrv;
|
||||
return null;
|
||||
}),
|
||||
storeManager: { get: mockStoreGet, set: mockStoreSet },
|
||||
} as unknown as App;
|
||||
|
||||
// Lazily initialized — created in beforeEach so it uses the current mockApp
|
||||
let mockGatewayConnectionSrv: GatewayConnectionService;
|
||||
|
||||
// ─── Test Suite ───
|
||||
|
||||
describe('GatewayConnectionCtr', () => {
|
||||
let ctr: GatewayConnectionCtr;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useFakeTimers();
|
||||
MockGatewayClient.lastInstance = null;
|
||||
MockGatewayClient.lastOptions = null;
|
||||
mockStoreGet.mockImplementation((key: string) => {
|
||||
if (key === 'gatewayEnabled') return true;
|
||||
return undefined;
|
||||
});
|
||||
|
||||
mockGatewayConnectionSrv = new GatewayConnectionService(mockApp);
|
||||
ctr = new GatewayConnectionCtr(mockApp);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
ctr.disconnect();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
// ─── Connection ───
|
||||
|
||||
describe('connect', () => {
|
||||
it('should create GatewayClient with correct options', async () => {
|
||||
mockStoreGet.mockImplementation((key: string) => {
|
||||
if (key === 'gatewayEnabled') return true;
|
||||
if (key === 'gatewayDeviceId') return 'stored-device-id';
|
||||
if (key === 'gatewayUrl') return undefined;
|
||||
return undefined;
|
||||
});
|
||||
|
||||
ctr = new GatewayConnectionCtr(mockApp);
|
||||
ctr.afterAppReady();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
const options = MockGatewayClient.lastOptions;
|
||||
expect(options).not.toBeNull();
|
||||
expect(options.token).toBe('mock-access-token');
|
||||
expect(options.deviceId).toBe('stored-device-id');
|
||||
expect(options.gatewayUrl).toBe('https://device-gateway.lobehub.com');
|
||||
expect(options.logger).toBeDefined();
|
||||
});
|
||||
|
||||
it('should use custom gateway URL from store when set', async () => {
|
||||
mockStoreGet.mockImplementation((key: string) => {
|
||||
if (key === 'gatewayEnabled') return true;
|
||||
if (key === 'gatewayUrl') return 'http://localhost:8787';
|
||||
return undefined;
|
||||
});
|
||||
|
||||
ctr = new GatewayConnectionCtr(mockApp);
|
||||
ctr.afterAppReady();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
expect(MockGatewayClient.lastOptions.gatewayUrl).toBe('http://localhost:8787');
|
||||
});
|
||||
|
||||
it('should return success:false when no access token', async () => {
|
||||
// Prevent auto-connect, then set up providers manually
|
||||
vi.mocked(mockRemoteServerConfigCtr.isRemoteServerConfigured).mockResolvedValueOnce(false);
|
||||
ctr.afterAppReady();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
vi.mocked(mockRemoteServerConfigCtr.getAccessToken).mockResolvedValueOnce(null);
|
||||
|
||||
const result = await ctr.connect();
|
||||
expect(result).toEqual({ error: 'No access token available', success: false });
|
||||
expect(MockGatewayClient.lastInstance).toBeNull();
|
||||
});
|
||||
|
||||
it('should persist gatewayEnabled=true on connect', async () => {
|
||||
vi.mocked(mockRemoteServerConfigCtr.isRemoteServerConfigured).mockResolvedValueOnce(false);
|
||||
ctr.afterAppReady();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
mockStoreSet.mockClear();
|
||||
|
||||
await ctr.connect();
|
||||
expect(mockStoreSet).toHaveBeenCalledWith('gatewayEnabled', true);
|
||||
});
|
||||
|
||||
it('should no-op when already connected', async () => {
|
||||
ctr.afterAppReady();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
const firstClient = MockGatewayClient.lastInstance;
|
||||
firstClient!.simulateConnected();
|
||||
|
||||
const result = await ctr.connect();
|
||||
expect(result).toEqual({ success: true });
|
||||
// No new client created
|
||||
expect(MockGatewayClient.lastInstance).toBe(firstClient);
|
||||
});
|
||||
|
||||
it('should broadcast status changes: disconnected → connecting → connected', async () => {
|
||||
ctr.afterAppReady();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
expect(mockBroadcast).toHaveBeenCalledWith('gatewayConnectionStatusChanged', {
|
||||
status: 'connecting',
|
||||
});
|
||||
|
||||
MockGatewayClient.lastInstance!.simulateConnected();
|
||||
expect(mockBroadcast).toHaveBeenCalledWith('gatewayConnectionStatusChanged', {
|
||||
status: 'connected',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Disconnect ───
|
||||
|
||||
describe('disconnect', () => {
|
||||
it('should disconnect client and set status to disconnected', async () => {
|
||||
ctr.afterAppReady();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
const client = MockGatewayClient.lastInstance!;
|
||||
client.simulateConnected();
|
||||
mockBroadcast.mockClear();
|
||||
|
||||
await ctr.disconnect();
|
||||
|
||||
expect(client.disconnect).toHaveBeenCalled();
|
||||
expect(mockBroadcast).toHaveBeenCalledWith('gatewayConnectionStatusChanged', {
|
||||
status: 'disconnected',
|
||||
});
|
||||
});
|
||||
|
||||
it('should persist gatewayEnabled=false on disconnect', async () => {
|
||||
ctr.afterAppReady();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
MockGatewayClient.lastInstance!.simulateConnected();
|
||||
mockStoreSet.mockClear();
|
||||
|
||||
await ctr.disconnect();
|
||||
expect(mockStoreSet).toHaveBeenCalledWith('gatewayEnabled', false);
|
||||
});
|
||||
|
||||
it('should not trigger reconnect after intentional disconnect', async () => {
|
||||
ctr.afterAppReady();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
const client = MockGatewayClient.lastInstance!;
|
||||
client.simulateConnected();
|
||||
|
||||
await ctr.disconnect();
|
||||
mockBroadcast.mockClear();
|
||||
|
||||
// Advance timers — no reconnect should happen
|
||||
await vi.advanceTimersByTimeAsync(60_000);
|
||||
expect(mockBroadcast).not.toHaveBeenCalledWith('gatewayConnectionStatusChanged', {
|
||||
status: 'reconnecting',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Auto-Connect ───
|
||||
|
||||
describe('afterAppReady (auto-connect)', () => {
|
||||
it('should auto-connect when server is configured and token exists', async () => {
|
||||
ctr.afterAppReady();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
expect(MockGatewayClient.lastInstance).not.toBeNull();
|
||||
expect(MockGatewayClient.lastInstance!.connect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip auto-connect when gatewayEnabled is false', async () => {
|
||||
mockStoreGet.mockImplementation((key: string) => {
|
||||
if (key === 'gatewayEnabled') return false;
|
||||
return undefined;
|
||||
});
|
||||
|
||||
ctr = new GatewayConnectionCtr(mockApp);
|
||||
ctr.afterAppReady();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
expect(MockGatewayClient.lastInstance).toBeNull();
|
||||
});
|
||||
|
||||
it('should skip auto-connect when remote server not configured', async () => {
|
||||
vi.mocked(mockRemoteServerConfigCtr.isRemoteServerConfigured).mockResolvedValueOnce(false);
|
||||
|
||||
ctr.afterAppReady();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
expect(MockGatewayClient.lastInstance).toBeNull();
|
||||
});
|
||||
|
||||
it('should skip auto-connect when no access token', async () => {
|
||||
vi.mocked(mockRemoteServerConfigCtr.getAccessToken).mockResolvedValueOnce(null);
|
||||
|
||||
ctr.afterAppReady();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
expect(MockGatewayClient.lastInstance).toBeNull();
|
||||
});
|
||||
|
||||
it('should create device ID on first launch and persist it', () => {
|
||||
mockStoreGet.mockReturnValue(undefined);
|
||||
ctr.afterAppReady();
|
||||
|
||||
expect(mockStoreSet).toHaveBeenCalledWith('gatewayDeviceId', 'mock-device-uuid');
|
||||
});
|
||||
|
||||
it('should reuse persisted device ID', () => {
|
||||
mockStoreGet.mockImplementation((key: string) => {
|
||||
if (key === 'gatewayEnabled') return true;
|
||||
if (key === 'gatewayDeviceId') return 'existing-id';
|
||||
return undefined;
|
||||
});
|
||||
ctr = new GatewayConnectionCtr(mockApp);
|
||||
ctr.afterAppReady();
|
||||
|
||||
expect(mockStoreSet).not.toHaveBeenCalledWith('gatewayDeviceId', expect.anything());
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Reconnection ───
|
||||
|
||||
describe('reconnection', () => {
|
||||
it('should broadcast reconnecting status when client emits reconnecting', async () => {
|
||||
ctr.afterAppReady();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
const client = MockGatewayClient.lastInstance!;
|
||||
client.simulateConnected();
|
||||
mockBroadcast.mockClear();
|
||||
|
||||
client.simulateReconnecting(1000);
|
||||
|
||||
expect(mockBroadcast).toHaveBeenCalledWith('gatewayConnectionStatusChanged', {
|
||||
status: 'reconnecting',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Tool Call Routing ───
|
||||
|
||||
describe('tool call routing', () => {
|
||||
async function connectAndOpen() {
|
||||
ctr.afterAppReady();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
const client = MockGatewayClient.lastInstance!;
|
||||
client.simulateConnected();
|
||||
return client;
|
||||
}
|
||||
|
||||
it.each([
|
||||
['readLocalFile', 'readFile', mockLocalFileCtr],
|
||||
['listLocalFiles', 'listLocalFiles', mockLocalFileCtr],
|
||||
['moveLocalFiles', 'handleMoveFiles', mockLocalFileCtr],
|
||||
['renameLocalFile', 'handleRenameFile', mockLocalFileCtr],
|
||||
['searchLocalFiles', 'handleLocalFilesSearch', mockLocalFileCtr],
|
||||
['writeLocalFile', 'handleWriteFile', mockLocalFileCtr],
|
||||
['editLocalFile', 'handleEditFile', mockLocalFileCtr],
|
||||
['globLocalFiles', 'handleGlobFiles', mockLocalFileCtr],
|
||||
['grepContent', 'handleGrepContent', mockLocalFileCtr],
|
||||
['runCommand', 'handleRunCommand', mockShellCommandCtr],
|
||||
['getCommandOutput', 'handleGetCommandOutput', mockShellCommandCtr],
|
||||
['killCommand', 'handleKillCommand', mockShellCommandCtr],
|
||||
] as const)('should route %s to %s', async (apiName, methodName, controller) => {
|
||||
const client = await connectAndOpen();
|
||||
const args = { test: 'arg' };
|
||||
|
||||
client.simulateToolCallRequest(apiName, args);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
expect((controller as any)[methodName]).toHaveBeenCalledWith(args);
|
||||
});
|
||||
|
||||
it('should send tool_call_response with success result', async () => {
|
||||
vi.mocked(mockLocalFileCtr.readFile).mockResolvedValueOnce({
|
||||
charCount: 5,
|
||||
content: 'hello',
|
||||
createdTime: new Date('2024-01-01'),
|
||||
filename: 'a.txt',
|
||||
fileType: '.txt',
|
||||
lineCount: 1,
|
||||
loc: [1, 1] as [number, number],
|
||||
modifiedTime: new Date('2024-01-01'),
|
||||
totalCharCount: 5,
|
||||
totalLineCount: 1,
|
||||
});
|
||||
const client = await connectAndOpen();
|
||||
|
||||
client.simulateToolCallRequest('readLocalFile', { 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,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should send tool_call_response with error on failure', async () => {
|
||||
vi.mocked(mockLocalFileCtr.readFile).mockRejectedValueOnce(new Error('File not found'));
|
||||
const client = await connectAndOpen();
|
||||
|
||||
client.simulateToolCallRequest('readLocalFile', { path: '/missing' }, 'req-err');
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
expect(client.sendToolCallResponse).toHaveBeenCalledWith({
|
||||
requestId: 'req-err',
|
||||
result: {
|
||||
content: 'File not found',
|
||||
error: 'File not found',
|
||||
success: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should send error for unknown apiName', async () => {
|
||||
const client = await connectAndOpen();
|
||||
|
||||
client.simulateToolCallRequest('unknownApi', {}, 'req-unknown');
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
const errorMsg =
|
||||
'Tool "unknownApi" is not available on this device. It may not be supported in the current desktop version. Please skip this tool and try alternative approaches.';
|
||||
expect(client.sendToolCallResponse).toHaveBeenCalledWith({
|
||||
requestId: 'req-unknown',
|
||||
result: {
|
||||
content: errorMsg,
|
||||
error: errorMsg,
|
||||
success: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Auth Expired ───
|
||||
|
||||
describe('auth_expired handling', () => {
|
||||
it('should refresh token and reconnect on auth_expired', async () => {
|
||||
ctr.afterAppReady();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
const client1 = MockGatewayClient.lastInstance!;
|
||||
client1.simulateConnected();
|
||||
|
||||
client1.simulateAuthExpired();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
expect(mockRemoteServerConfigCtr.refreshAccessToken).toHaveBeenCalled();
|
||||
// Should have created a new GatewayClient for reconnection
|
||||
expect(MockGatewayClient.lastInstance).not.toBe(client1);
|
||||
expect(MockGatewayClient.lastInstance!.connect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set status to disconnected when token refresh fails', async () => {
|
||||
vi.mocked(mockRemoteServerConfigCtr.refreshAccessToken).mockResolvedValueOnce({
|
||||
error: 'invalid_grant',
|
||||
success: false,
|
||||
});
|
||||
|
||||
ctr.afterAppReady();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
const client = MockGatewayClient.lastInstance!;
|
||||
client.simulateConnected();
|
||||
mockBroadcast.mockClear();
|
||||
|
||||
client.simulateAuthExpired();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
expect(mockBroadcast).toHaveBeenCalledWith('gatewayConnectionStatusChanged', {
|
||||
status: 'disconnected',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ─── IPC Methods ───
|
||||
|
||||
describe('getConnectionStatus', () => {
|
||||
it('should return current status', async () => {
|
||||
expect(await ctr.getConnectionStatus()).toEqual({ status: 'disconnected' });
|
||||
|
||||
ctr.afterAppReady();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
expect(await ctr.getConnectionStatus()).toEqual({ status: 'connecting' });
|
||||
|
||||
MockGatewayClient.lastInstance!.simulateConnected();
|
||||
expect(await ctr.getConnectionStatus()).toEqual({ status: 'connected' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDeviceInfo', () => {
|
||||
it('should return device information', async () => {
|
||||
mockStoreGet.mockImplementation((key: string) => {
|
||||
if (key === 'gatewayEnabled') return true;
|
||||
if (key === 'gatewayDeviceId') return 'my-device';
|
||||
return undefined;
|
||||
});
|
||||
ctr = new GatewayConnectionCtr(mockApp);
|
||||
ctr.afterAppReady();
|
||||
|
||||
const info = await ctr.getDeviceInfo();
|
||||
expect(info).toEqual({
|
||||
description: '',
|
||||
deviceId: 'my-device',
|
||||
hostname: 'mock-hostname',
|
||||
name: 'mock-hostname',
|
||||
platform: process.platform,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -45,7 +45,6 @@ vi.mock('node:fs/promises', () => ({
|
||||
mkdir: vi.fn(),
|
||||
readFile: vi.fn(),
|
||||
readdir: vi.fn(),
|
||||
realpath: vi.fn(),
|
||||
rename: vi.fn(),
|
||||
rm: vi.fn(),
|
||||
stat: vi.fn(),
|
||||
@@ -302,46 +301,6 @@ describe('LocalFileCtr', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('auditSafePaths', () => {
|
||||
it('should treat real temporary paths as safe', async () => {
|
||||
vi.mocked(mockFsPromises.access).mockResolvedValue(undefined);
|
||||
vi.mocked(mockFsPromises.realpath).mockImplementation(async (targetPath: string) => {
|
||||
if (targetPath === '/tmp') return '/private/tmp';
|
||||
if (targetPath === '/var/tmp') return '/private/var/tmp';
|
||||
if (targetPath === '/tmp/out') return '/private/tmp/out';
|
||||
return targetPath;
|
||||
});
|
||||
|
||||
const result = await localFileCtr.auditSafePaths({
|
||||
paths: ['/tmp/out'],
|
||||
resolveAgainstScope: '/Users/me/project',
|
||||
});
|
||||
|
||||
expect(result).toEqual({ allSafe: true });
|
||||
});
|
||||
|
||||
it('should reject safe-path candidates whose real target escapes the temporary roots', async () => {
|
||||
vi.mocked(mockFsPromises.access).mockImplementation(async (targetPath: string) => {
|
||||
if (targetPath === '/tmp/out/config') {
|
||||
throw new Error('ENOENT');
|
||||
}
|
||||
});
|
||||
vi.mocked(mockFsPromises.realpath).mockImplementation(async (targetPath: string) => {
|
||||
if (targetPath === '/tmp') return '/private/tmp';
|
||||
if (targetPath === '/var/tmp') return '/private/var/tmp';
|
||||
if (targetPath === '/tmp/out') return '/Users/me/.ssh';
|
||||
return targetPath;
|
||||
});
|
||||
|
||||
const result = await localFileCtr.auditSafePaths({
|
||||
paths: ['/tmp/out/config'],
|
||||
resolveAgainstScope: '/Users/me/project',
|
||||
});
|
||||
|
||||
expect(result).toEqual({ allSafe: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe('handlePrepareSkillDirectory', () => {
|
||||
it('should download and extract a skill zip into a local cache directory', async () => {
|
||||
const zipped = zipSync({
|
||||
|
||||
@@ -47,14 +47,8 @@ const mockBrowserManager = {
|
||||
broadcastToAllWindows: vi.fn(),
|
||||
};
|
||||
|
||||
const mockGatewayConnectionSrv = {
|
||||
disconnect: vi.fn().mockResolvedValue({ success: true }),
|
||||
};
|
||||
|
||||
const mockApp = {
|
||||
browserManager: mockBrowserManager,
|
||||
getController: vi.fn(),
|
||||
getService: vi.fn().mockReturnValue(mockGatewayConnectionSrv),
|
||||
storeManager: mockStoreManager,
|
||||
} as unknown as App;
|
||||
|
||||
@@ -300,13 +294,6 @@ describe('RemoteServerConfigCtr', () => {
|
||||
const accessToken = await controller.getAccessToken();
|
||||
expect(accessToken).toBeNull();
|
||||
});
|
||||
|
||||
it('should disconnect gateway when tokens are cleared', async () => {
|
||||
await controller.saveTokens('access', 'refresh', 3600);
|
||||
await controller.clearTokens();
|
||||
|
||||
expect(mockGatewayConnectionSrv.disconnect).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTokenExpiresAt', () => {
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { CreateServicesResult, IpcServiceConstructor, MergeIpcService } fro
|
||||
import AuthCtr from './AuthCtr';
|
||||
import BrowserWindowsCtr from './BrowserWindowsCtr';
|
||||
import DevtoolsCtr from './DevtoolsCtr';
|
||||
import GatewayConnectionCtr from './GatewayConnectionCtr';
|
||||
import LocalFileCtr from './LocalFileCtr';
|
||||
import McpCtr from './McpCtr';
|
||||
import McpInstallCtr from './McpInstallCtr';
|
||||
@@ -24,7 +23,6 @@ export const controllerIpcConstructors = [
|
||||
AuthCtr,
|
||||
BrowserWindowsCtr,
|
||||
DevtoolsCtr,
|
||||
GatewayConnectionCtr,
|
||||
LocalFileCtr,
|
||||
McpCtr,
|
||||
McpInstallCtr,
|
||||
|
||||
@@ -1,317 +0,0 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import os from 'node:os';
|
||||
|
||||
import type {
|
||||
SystemInfoRequestMessage,
|
||||
ToolCallRequestMessage,
|
||||
} from '@lobechat/device-gateway-client';
|
||||
import { GatewayClient } from '@lobechat/device-gateway-client';
|
||||
import type { GatewayConnectionStatus } from '@lobechat/electron-client-ipc';
|
||||
import { app } from 'electron';
|
||||
|
||||
import { createLogger } from '@/utils/logger';
|
||||
|
||||
import { ServiceModule } from './index';
|
||||
|
||||
const logger = createLogger('services:GatewayConnectionSrv');
|
||||
|
||||
const DEFAULT_GATEWAY_URL = 'https://device-gateway.lobehub.com';
|
||||
|
||||
interface ToolCallHandler {
|
||||
(apiName: string, args: any): Promise<unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* GatewayConnectionService
|
||||
*
|
||||
* Core business logic for managing WebSocket connection to the cloud device-gateway.
|
||||
* Extracted from GatewayConnectionCtr so other controllers can reuse connect/disconnect.
|
||||
*/
|
||||
export default class GatewayConnectionService extends ServiceModule {
|
||||
private client: GatewayClient | null = null;
|
||||
private status: GatewayConnectionStatus = 'disconnected';
|
||||
private deviceId: string | null = null;
|
||||
|
||||
private tokenProvider: (() => Promise<string | null>) | null = null;
|
||||
private tokenRefresher: (() => Promise<{ error?: string; success: boolean }>) | null = null;
|
||||
private toolCallHandler: ToolCallHandler | null = null;
|
||||
|
||||
// ─── Configuration ───
|
||||
|
||||
/**
|
||||
* Set token provider function (to decouple from RemoteServerConfigCtr)
|
||||
*/
|
||||
setTokenProvider(provider: () => Promise<string | null>) {
|
||||
this.tokenProvider = provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set token refresher function (for auth_expired handling)
|
||||
*/
|
||||
setTokenRefresher(refresher: () => Promise<{ error?: string; success: boolean }>) {
|
||||
this.tokenRefresher = refresher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set tool call handler (to route tool calls to LocalFileCtr/ShellCommandCtr)
|
||||
*/
|
||||
setToolCallHandler(handler: ToolCallHandler) {
|
||||
this.toolCallHandler = handler;
|
||||
}
|
||||
|
||||
// ─── Device ID ───
|
||||
|
||||
loadOrCreateDeviceId() {
|
||||
const stored = this.app.storeManager.get('gatewayDeviceId') as string | undefined;
|
||||
if (stored) {
|
||||
this.deviceId = stored;
|
||||
} else {
|
||||
this.deviceId = randomUUID();
|
||||
this.app.storeManager.set('gatewayDeviceId', this.deviceId);
|
||||
}
|
||||
logger.debug(`Device ID: ${this.deviceId}`);
|
||||
}
|
||||
|
||||
getDeviceId(): string {
|
||||
return this.deviceId || 'unknown';
|
||||
}
|
||||
|
||||
// ─── Connection Status ───
|
||||
|
||||
getStatus(): GatewayConnectionStatus {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
getDeviceInfo() {
|
||||
return {
|
||||
description: this.getDeviceDescription(),
|
||||
deviceId: this.getDeviceId(),
|
||||
hostname: os.hostname(),
|
||||
name: this.getDeviceName(),
|
||||
platform: process.platform,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Device Name & Description ───
|
||||
|
||||
getDeviceName(): string {
|
||||
return (this.app.storeManager.get('gatewayDeviceName') as string) || os.hostname();
|
||||
}
|
||||
|
||||
setDeviceName(name: string) {
|
||||
this.app.storeManager.set('gatewayDeviceName', name);
|
||||
}
|
||||
|
||||
getDeviceDescription(): string {
|
||||
return (this.app.storeManager.get('gatewayDeviceDescription') as string) || '';
|
||||
}
|
||||
|
||||
setDeviceDescription(description: string) {
|
||||
this.app.storeManager.set('gatewayDeviceDescription', description);
|
||||
}
|
||||
|
||||
// ─── Connection Logic ───
|
||||
|
||||
async connect(): Promise<{ error?: string; success: boolean }> {
|
||||
if (this.status === 'connected' || this.status === 'connecting') {
|
||||
return { success: true };
|
||||
}
|
||||
return this.doConnect();
|
||||
}
|
||||
|
||||
async disconnect(): Promise<{ success: boolean }> {
|
||||
if (this.client) {
|
||||
await this.client.disconnect();
|
||||
this.client = null;
|
||||
}
|
||||
this.setStatus('disconnected');
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
private async doConnect(): Promise<{ error?: string; success: boolean }> {
|
||||
// Clean up any existing client
|
||||
if (this.client) {
|
||||
await this.client.disconnect();
|
||||
this.client = null;
|
||||
}
|
||||
|
||||
if (!this.tokenProvider) {
|
||||
logger.warn('Cannot connect: no token provider configured');
|
||||
return { error: 'No token provider configured', success: false };
|
||||
}
|
||||
|
||||
const token = await this.tokenProvider();
|
||||
if (!token) {
|
||||
logger.warn('Cannot connect: no access token');
|
||||
return { error: 'No access token available', success: false };
|
||||
}
|
||||
|
||||
const gatewayUrl = this.getGatewayUrl();
|
||||
const userId = this.extractUserIdFromToken(token);
|
||||
logger.info(`Connecting to device gateway: ${gatewayUrl}, userId: ${userId || 'unknown'}`);
|
||||
|
||||
const client = new GatewayClient({
|
||||
deviceId: this.getDeviceId(),
|
||||
gatewayUrl,
|
||||
logger,
|
||||
token,
|
||||
userId: userId || undefined,
|
||||
});
|
||||
|
||||
this.setupClientEvents(client);
|
||||
this.client = client;
|
||||
|
||||
await client.connect();
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
private setupClientEvents(client: GatewayClient) {
|
||||
client.on('status_changed', (status) => {
|
||||
this.setStatus(status);
|
||||
});
|
||||
|
||||
client.on('tool_call_request', (request) => {
|
||||
this.handleToolCallRequest(request, client);
|
||||
});
|
||||
|
||||
client.on('system_info_request', (request) => {
|
||||
this.handleSystemInfoRequest(client, request);
|
||||
});
|
||||
|
||||
client.on('auth_expired', () => {
|
||||
logger.warn('Received auth_expired, will reconnect with refreshed token');
|
||||
this.handleAuthExpired();
|
||||
});
|
||||
|
||||
client.on('error', (error) => {
|
||||
logger.error('WebSocket error:', error.message);
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Auth Expired Handling ───
|
||||
|
||||
private async handleAuthExpired() {
|
||||
// Disconnect the current client
|
||||
if (this.client) {
|
||||
await this.client.disconnect();
|
||||
this.client = null;
|
||||
}
|
||||
|
||||
if (!this.tokenRefresher) {
|
||||
logger.error('No token refresher configured, cannot handle auth_expired');
|
||||
this.setStatus('disconnected');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('Attempting token refresh before reconnect');
|
||||
const result = await this.tokenRefresher();
|
||||
|
||||
if (result.success) {
|
||||
logger.info('Token refreshed, reconnecting');
|
||||
await this.doConnect();
|
||||
} else {
|
||||
logger.error('Token refresh failed:', result.error);
|
||||
this.setStatus('disconnected');
|
||||
}
|
||||
}
|
||||
|
||||
// ─── System Info ───
|
||||
|
||||
private handleSystemInfoRequest(client: GatewayClient, request: SystemInfoRequestMessage) {
|
||||
logger.info(`Received system_info_request: requestId=${request.requestId}`);
|
||||
client.sendSystemInfoResponse({
|
||||
requestId: request.requestId,
|
||||
result: {
|
||||
success: true,
|
||||
systemInfo: {
|
||||
arch: os.arch(),
|
||||
desktopPath: app.getPath('desktop'),
|
||||
documentsPath: app.getPath('documents'),
|
||||
downloadsPath: app.getPath('downloads'),
|
||||
homePath: app.getPath('home'),
|
||||
musicPath: app.getPath('music'),
|
||||
picturesPath: app.getPath('pictures'),
|
||||
userDataPath: app.getPath('userData'),
|
||||
videosPath: app.getPath('videos'),
|
||||
workingDirectory: process.cwd(),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Tool Call Routing ───
|
||||
|
||||
private handleToolCallRequest = async (
|
||||
request: ToolCallRequestMessage,
|
||||
client: GatewayClient,
|
||||
) => {
|
||||
const { requestId, toolCall } = request;
|
||||
const { apiName, arguments: argsStr } = toolCall;
|
||||
|
||||
logger.info(`Received tool call: apiName=${apiName}, requestId=${requestId}`);
|
||||
|
||||
try {
|
||||
if (!this.toolCallHandler) {
|
||||
throw new Error('No tool call handler configured');
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
logger.error(`Tool call failed: apiName=${apiName}, error=${errorMsg}`);
|
||||
|
||||
client.sendToolCallResponse({
|
||||
requestId,
|
||||
result: {
|
||||
content: errorMsg,
|
||||
error: errorMsg,
|
||||
success: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ─── Status Broadcasting ───
|
||||
|
||||
private setStatus(status: GatewayConnectionStatus) {
|
||||
if (this.status === status) return;
|
||||
|
||||
logger.info(`Connection status: ${this.status} → ${status}`);
|
||||
this.status = status;
|
||||
this.app.browserManager.broadcastToAllWindows('gatewayConnectionStatusChanged', { status });
|
||||
}
|
||||
|
||||
// ─── Gateway URL ───
|
||||
|
||||
private getGatewayUrl(): string {
|
||||
return this.app.storeManager.get('gatewayUrl') || DEFAULT_GATEWAY_URL;
|
||||
}
|
||||
|
||||
// ─── Token Helpers ───
|
||||
|
||||
/**
|
||||
* Extract userId (sub claim) from JWT without verification.
|
||||
* The token will be verified server-side; we just need the userId for routing.
|
||||
*/
|
||||
private extractUserIdFromToken(token: string): string | null {
|
||||
try {
|
||||
const parts = token.split('.');
|
||||
if (parts.length !== 3) return null;
|
||||
|
||||
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8'));
|
||||
return payload.sub || null;
|
||||
} catch {
|
||||
logger.warn('Failed to extract userId from JWT token');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,11 +12,6 @@ export interface ElectronMainStore {
|
||||
lastRefreshAt?: number;
|
||||
refreshToken?: string;
|
||||
};
|
||||
gatewayDeviceDescription: string;
|
||||
gatewayDeviceId: string;
|
||||
gatewayDeviceName: string;
|
||||
gatewayEnabled: boolean;
|
||||
gatewayUrl: string;
|
||||
locale: string;
|
||||
networkProxy: NetworkProxySettings;
|
||||
shortcuts: Record<string, string>;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DurableObject } from 'cloudflare:workers';
|
||||
import { Hono } from 'hono';
|
||||
|
||||
import { resolveSocketAuth, verifyApiKeyToken, verifyDesktopToken } from './auth';
|
||||
import { verifyDesktopToken } from './auth';
|
||||
import type { DeviceAttachment, Env } from './types';
|
||||
|
||||
const AUTH_TIMEOUT = 10_000; // 10s to authenticate after connect
|
||||
@@ -58,25 +58,24 @@ export class DeviceGatewayDO extends DurableObject<Env> {
|
||||
if (att.authenticated) return; // Already authenticated, ignore
|
||||
|
||||
try {
|
||||
const token = data.token as string | undefined;
|
||||
const tokenType = data.tokenType as 'apiKey' | 'jwt' | 'serviceToken' | undefined;
|
||||
const serverUrl = data.serverUrl as string | undefined;
|
||||
const storedUserId = await this.ctx.storage.get<string>('_userId');
|
||||
const token = data.token as string;
|
||||
if (!token) throw new Error('Missing token');
|
||||
|
||||
const verifiedUserId = await resolveSocketAuth({
|
||||
serverUrl,
|
||||
serviceToken: this.env.SERVICE_TOKEN,
|
||||
storedUserId,
|
||||
token,
|
||||
tokenType,
|
||||
verifyApiKey: verifyApiKeyToken,
|
||||
verifyJwt: async (jwt) => {
|
||||
const result = await verifyDesktopToken(this.env, jwt);
|
||||
return { userId: result.userId };
|
||||
},
|
||||
});
|
||||
let verifiedUserId: string;
|
||||
|
||||
if (token === this.env.SERVICE_TOKEN) {
|
||||
// Service token auth (for CLI debugging)
|
||||
const storedUserId = await this.ctx.storage.get<string>('_userId');
|
||||
if (!storedUserId) throw new Error('Missing userId');
|
||||
verifiedUserId = storedUserId;
|
||||
} else {
|
||||
// JWT auth (normal desktop flow)
|
||||
const result = await verifyDesktopToken(this.env, token);
|
||||
verifiedUserId = result.userId;
|
||||
}
|
||||
|
||||
// Verify userId matches the DO routing
|
||||
const storedUserId = await this.ctx.storage.get<string>('_userId');
|
||||
if (storedUserId && verifiedUserId !== storedUserId) {
|
||||
throw new Error('userId mismatch');
|
||||
}
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { resolveSocketAuth } from './auth';
|
||||
|
||||
describe('resolveSocketAuth', () => {
|
||||
it('rejects missing token', async () => {
|
||||
const verifyApiKey = vi.fn();
|
||||
const verifyJwt = vi.fn();
|
||||
|
||||
await expect(
|
||||
resolveSocketAuth({
|
||||
serviceToken: 'service-secret',
|
||||
storedUserId: 'user-123',
|
||||
verifyApiKey,
|
||||
verifyJwt,
|
||||
}),
|
||||
).rejects.toThrow('Missing token');
|
||||
|
||||
expect(verifyApiKey).not.toHaveBeenCalled();
|
||||
expect(verifyJwt).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects the real service token when storedUserId is missing', async () => {
|
||||
const verifyApiKey = vi.fn();
|
||||
const verifyJwt = vi.fn();
|
||||
|
||||
await expect(
|
||||
resolveSocketAuth({
|
||||
serviceToken: 'service-secret',
|
||||
token: 'service-secret',
|
||||
tokenType: 'serviceToken',
|
||||
verifyApiKey,
|
||||
verifyJwt,
|
||||
}),
|
||||
).rejects.toThrow('Missing userId');
|
||||
|
||||
expect(verifyApiKey).not.toHaveBeenCalled();
|
||||
expect(verifyJwt).not.toHaveBeenCalled();
|
||||
});
|
||||
it('rejects clients that only self-declare serviceToken mode', async () => {
|
||||
const verifyApiKey = vi.fn();
|
||||
const verifyJwt = vi.fn().mockRejectedValue(new Error('invalid jwt'));
|
||||
|
||||
await expect(
|
||||
resolveSocketAuth({
|
||||
serviceToken: 'service-secret',
|
||||
storedUserId: 'user-123',
|
||||
token: 'attacker-token',
|
||||
tokenType: 'serviceToken',
|
||||
verifyApiKey,
|
||||
verifyJwt,
|
||||
}),
|
||||
).rejects.toThrow('invalid jwt');
|
||||
|
||||
expect(verifyApiKey).not.toHaveBeenCalled();
|
||||
expect(verifyJwt).toHaveBeenCalledWith('attacker-token');
|
||||
});
|
||||
|
||||
it('treats a forged serviceToken claim with a valid JWT as JWT auth', async () => {
|
||||
const verifyApiKey = vi.fn();
|
||||
const verifyJwt = vi.fn().mockResolvedValue({ userId: 'user-123' });
|
||||
|
||||
await expect(
|
||||
resolveSocketAuth({
|
||||
serviceToken: 'service-secret',
|
||||
storedUserId: 'user-123',
|
||||
token: 'valid-jwt',
|
||||
tokenType: 'serviceToken',
|
||||
verifyApiKey,
|
||||
verifyJwt,
|
||||
}),
|
||||
).resolves.toBe('user-123');
|
||||
|
||||
expect(verifyApiKey).not.toHaveBeenCalled();
|
||||
expect(verifyJwt).toHaveBeenCalledWith('valid-jwt');
|
||||
});
|
||||
|
||||
it('accepts the real service token', async () => {
|
||||
const verifyApiKey = vi.fn();
|
||||
const verifyJwt = vi.fn();
|
||||
|
||||
await expect(
|
||||
resolveSocketAuth({
|
||||
serviceToken: 'service-secret',
|
||||
storedUserId: 'user-123',
|
||||
token: 'service-secret',
|
||||
tokenType: 'serviceToken',
|
||||
verifyApiKey,
|
||||
verifyJwt,
|
||||
}),
|
||||
).resolves.toBe('user-123');
|
||||
|
||||
expect(verifyApiKey).not.toHaveBeenCalled();
|
||||
expect(verifyJwt).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -4,26 +4,6 @@ import type { Env } from './types';
|
||||
|
||||
let cachedKey: CryptoKey | null = null;
|
||||
|
||||
interface CurrentUserResponse {
|
||||
data?: {
|
||||
id?: string;
|
||||
userId?: string;
|
||||
};
|
||||
error?: string;
|
||||
message?: string;
|
||||
success?: boolean;
|
||||
}
|
||||
|
||||
export interface ResolveSocketAuthOptions {
|
||||
serverUrl?: string;
|
||||
serviceToken: string;
|
||||
storedUserId?: string;
|
||||
token?: string;
|
||||
tokenType?: 'apiKey' | 'jwt' | 'serviceToken';
|
||||
verifyApiKey: (serverUrl: string, token: string) => Promise<{ userId: string }>;
|
||||
verifyJwt: (token: string) => Promise<{ userId: string }>;
|
||||
}
|
||||
|
||||
async function getPublicKey(env: Env): Promise<CryptoKey> {
|
||||
if (cachedKey) return cachedKey;
|
||||
|
||||
@@ -54,57 +34,3 @@ export async function verifyDesktopToken(
|
||||
userId: payload.sub,
|
||||
};
|
||||
}
|
||||
|
||||
export async function verifyApiKeyToken(
|
||||
serverUrl: string,
|
||||
token: string,
|
||||
): Promise<{ userId: string }> {
|
||||
const normalizedServerUrl = new URL(serverUrl).toString().replace(/\/$/, '');
|
||||
|
||||
const response = await fetch(`${normalizedServerUrl}/api/v1/users/me`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
let body: CurrentUserResponse | undefined;
|
||||
try {
|
||||
body = (await response.json()) as CurrentUserResponse;
|
||||
} catch {
|
||||
throw new Error(`Failed to parse response from ${normalizedServerUrl}/api/v1/users/me.`);
|
||||
}
|
||||
|
||||
if (!response.ok || body?.success === false) {
|
||||
throw new Error(
|
||||
body?.error || body?.message || `Request failed with status ${response.status}.`,
|
||||
);
|
||||
}
|
||||
|
||||
const userId = body?.data?.id || body?.data?.userId;
|
||||
if (!userId) {
|
||||
throw new Error('Current user response did not include a user id.');
|
||||
}
|
||||
|
||||
return { userId };
|
||||
}
|
||||
|
||||
export async function resolveSocketAuth(options: ResolveSocketAuthOptions): Promise<string> {
|
||||
const { serverUrl, serviceToken, storedUserId, token, tokenType, verifyApiKey, verifyJwt } =
|
||||
options;
|
||||
|
||||
if (!token) throw new Error('Missing token');
|
||||
|
||||
if (tokenType === 'apiKey') {
|
||||
if (!serverUrl) throw new Error('Missing serverUrl');
|
||||
const result = await verifyApiKey(serverUrl, token);
|
||||
return result.userId;
|
||||
}
|
||||
|
||||
if (token === serviceToken) {
|
||||
if (!storedUserId) throw new Error('Missing userId');
|
||||
return storedUserId;
|
||||
}
|
||||
|
||||
const result = await verifyJwt(token);
|
||||
return result.userId;
|
||||
}
|
||||
|
||||
@@ -20,9 +20,7 @@ export interface DeviceAttachment {
|
||||
|
||||
// Desktop → CF
|
||||
export interface AuthMessage {
|
||||
serverUrl?: string;
|
||||
token: string;
|
||||
tokenType?: 'apiKey' | 'jwt' | 'serviceToken';
|
||||
type: 'auth';
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,4 @@
|
||||
[
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["add agent task system database schema."]
|
||||
},
|
||||
"date": "2026-03-26",
|
||||
"version": "2.1.45"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"fixes": ["misc UI/UX improvements and bug fixes."],
|
||||
"improvements": ["add image/video switch."]
|
||||
},
|
||||
"date": "2026-03-20",
|
||||
"version": "2.1.44"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": [
|
||||
|
||||
@@ -34,8 +34,6 @@
|
||||
"https://file.rene.wang/clipboard-1769050853107-750be5f83cbe3.png": "/blog/assetse6139c4d5b1b26b05f41a579d98fc6f3.webp",
|
||||
"https://file.rene.wang/clipboard-1769052898732-b7bb78ae1f1f8.png": "/blog/assetsafa74c85aafea8a057e6047b0823e280.webp",
|
||||
"https://file.rene.wang/clipboard-1769056077960-cac34bc157a65.png": "/blog/assetsa8e173bec038d1d21d413f6fa0ace342.webp",
|
||||
"https://file.rene.wang/clipboard-1769137275089-21cf7ab42d52b.png": "/blog/assets095af3a0a0f850fc206fc3bbc19a4095.webp",
|
||||
"https://file.rene.wang/clipboard-1769137300488-0b894cc8c7a67.png": "/blog/assetsebc1ebe8330d982f6a0b757aafb3f4a1.webp",
|
||||
"https://file.rene.wang/clipboard-1769155711708-710967bee57bc.png": "/blog/assets7f3b38c1d76cceb91edb29d6b1eb60db.webp",
|
||||
"https://file.rene.wang/clipboard-1769155737647-1b4fc6558f029.png": "/blog/assets3a7f0b29839603336e39e923b423409b.webp",
|
||||
"https://file.rene.wang/clipboard-1769155791342-7f43b72cc6b42.png": "/blog/assets35e6aa692b0c16009c61964279514166.webp",
|
||||
@@ -46,8 +44,6 @@
|
||||
"https://file.rene.wang/clipboard-1769156005535-c2e79e11f4b56.png": "/blog/assets2a36d86a4eed6e7938dd6e9c684701ed.webp",
|
||||
"https://file.rene.wang/clipboard-1769156036607-2b4fe37c4b56c.png": "/blog/assetsc0efdb82443556ae3acefe00099b3f23.webp",
|
||||
"https://file.rene.wang/clipboard-1769156050787-ecf4f48474ae2.png": "/blog/assetse743f0a47127390dde766a0a790476db.webp",
|
||||
"https://file.rene.wang/clipboard-1770261091677-74b74e4d6bf23.png": "/blog/assets3059f679eef80c5e777085db3d2d056e.webp",
|
||||
"https://file.rene.wang/clipboard-1770266335710-1fec523143aab.png": "/blog/assets636c78daf95c590cd7d80284c68eb6d9.webp",
|
||||
"https://file.rene.wang/lobehub/467951f5-ad65-498d-aea9-fca8f35a4314.png": "/blog/assets907ea775d228958baca38e2dbb65939a.webp",
|
||||
"https://file.rene.wang/lobehub/58d91528-373a-4a42-b520-cf6cb1f8ce1e.png": "/blog/assets7dccdd4df55aede71001da649639437f.webp",
|
||||
"https://file.rene.wang/lobehub/ee700103-3c08-41dc-9ddf-c7705bb7bc6a.png": "/blog/assets196d679bc7071abbf71f2a8566f05aa3.webp",
|
||||
@@ -262,7 +258,6 @@
|
||||
"https://github.com/user-attachments/assets/22e1a039-5e6e-4c40-8266-19821677618a": "/blog/assets89b45345c84f8b7c3bf4d554169689ac.webp",
|
||||
"https://github.com/user-attachments/assets/237864d6-cc5d-4fe4-8a2b-c278016855c5": "/blog/assetsf3e7c2e961d1d2886fe231a4ac59e2f1.webp",
|
||||
"https://github.com/user-attachments/assets/2787824c-a13c-466c-ba6f-820bddfe099f": "/blog/assets/8d6c17a6ea5e784edf4449fb18ca3f76.webp",
|
||||
"https://github.com/user-attachments/assets/27c37617-a813-4de5-b0bf-c7167999c856": "/blog/assetsc958eae64465451c4374cdee8f6fd596.webp",
|
||||
"https://github.com/user-attachments/assets/28590f7f-bfee-4215-b50b-8feddbf72366": "/blog/assets89a8dadc85902334ce8d2d5b78abf709.webp",
|
||||
"https://github.com/user-attachments/assets/29508dda-2382-430f-bc81-fb23f02149f8": "/blog/assets/29b13dc042e3b839ad8865354afe2fac.webp",
|
||||
"https://github.com/user-attachments/assets/2a4116a7-15ad-43e5-b801-cc62d8da2012": "/blog/assets/37d85fdfccff9ed56e9c6827faee01c7.webp",
|
||||
@@ -291,7 +286,6 @@
|
||||
"https://github.com/user-attachments/assets/4c792f62-5203-4f13-8f23-df228f70d67f": "/blog/assets94f55c97a24a08c7a5923c23ee2d7eef.webp",
|
||||
"https://github.com/user-attachments/assets/4cbbbcce-36be-48ff-bb0b-31607a0bba5c": "/blog/assetsb33085e7553d2b7194005b102184553e.webp",
|
||||
"https://github.com/user-attachments/assets/4d671a7c-5d94-4c4b-b4fd-71a5a0e9d227": "/blog/assetsc74cf5c8daee1515c37a85bce087f0d6.webp",
|
||||
"https://github.com/user-attachments/assets/4dde41ec-985b-4781-8c77-aac65555a32f": "/blog/assets04fecea4e5f4ce3490bf11bec66ff477.webp",
|
||||
"https://github.com/user-attachments/assets/4e04928d-0171-48d1-afff-e22fc2faaf4e": "/blog/assetsb26b68a4875a6510ddc202dd4b40d010.webp",
|
||||
"https://github.com/user-attachments/assets/530c7c96-bac3-456d-a429-f60e7d2ade66": "/blog/assets6541bab7e0047f9c5dbad98dc272d64d.webp",
|
||||
"https://github.com/user-attachments/assets/5321f987-2c64-4211-8549-bd30ca9b59b9": "/blog/assetsaf57d31364a41634b10c243ed9b1f8f8.webp",
|
||||
@@ -333,7 +327,6 @@
|
||||
"https://github.com/user-attachments/assets/7cb3019b-78c1-48e0-a64c-a6a4836affd9": "/blog/assets3ca963d92475f34b0789cfa50071bc52.webp",
|
||||
"https://github.com/user-attachments/assets/808f8849-5738-4a60-8ccf-01e300b0dc88": "/blog/assets0f893c504377ba45a9f5cdbb5ccb1612.webp",
|
||||
"https://github.com/user-attachments/assets/81d0349a-44fe-4dfc-bbc4-8e9a1e09567d": "/blog/assets29de82efbe7657a8b9ba7daf0904585d.webp",
|
||||
"https://github.com/user-attachments/assets/81f18b20-3918-4f77-8571-07d0c4a79aec": "/blog/assets43d66c62b79a027895b5a6127b2f2de2.webp",
|
||||
"https://github.com/user-attachments/assets/82a7ebe0-69ad-43b6-8767-1316b443fa03": "/blog/assets5374759bfe39ca7fc864e72ddfce98d0.webp",
|
||||
"https://github.com/user-attachments/assets/82bfc467-e0c6-4d99-9b1f-18e4aea24285": "/blog/assets/eb477e62217f4d1b644eff975c7ac168.webp",
|
||||
"https://github.com/user-attachments/assets/840442b1-bf56-4a5f-9700-b3608b16a8a5": "/blog/assetsc6ff27b7134f280727e1fd7ff83ed2fa.webp",
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
---
|
||||
title: "LobeHub v2.0 — Group Chat & Multi-Agent Collaboration \U0001F389"
|
||||
description: >-
|
||||
LobeHub v2.0 brings major upgrades including multi-agent group chat, enhanced
|
||||
model settings, SSO-only mode, and desktop improvements.
|
||||
title: 'LobeHub v2.0 — Group Chat & Multi-Agent Collaboration 🎉'
|
||||
description: LobeHub v2.0 brings major upgrades including multi-agent group chat, enhanced model settings, SSO-only mode, and desktop improvements.
|
||||
tags:
|
||||
- v2.0
|
||||
- Group Chat
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
---
|
||||
title: "LobeHub v2.0 — Group Chat & Multi-Agent Collaboration \U0001F389"
|
||||
description: >-
|
||||
LobeHub v2.0 brings major upgrades including multi-agent group chat, enhanced
|
||||
model settings, SSO-only mode, and desktop improvements.
|
||||
title: 'LobeHub v2.0 — Group Chat & Multi-Agent Collaboration 🎉'
|
||||
description: LobeHub v2.0 brings major upgrades including multi-agent group chat, enhanced model settings, SSO-only mode, and desktop improvements.
|
||||
tags:
|
||||
- v2.0
|
||||
- Group Chat
|
||||
@@ -18,7 +16,7 @@ LobeHub v2.0 正式发布,带来强大的多智能体群聊功能、优化的
|
||||
|
||||
- 重大版本升级,架构重新设计,功能增强
|
||||
- 多智能体协作:将多个专业智能体汇聚于同一对话中。它们可以共同讨论、推理并解决复杂问题,速度更快、更智能。
|
||||
- 智能体构建器:描述您的需求,LobeHub 将构建完整的智能体 —— 包括技能、行为、工具和个性。无需任何设置。
|
||||
- 智能体构建器:描述您的需求,LobeHub 将构建完整的智能体——包括技能、行为、工具和个性。无需任何设置。
|
||||
- 页面:使用 Lobe AI 编写、阅读和整理文档
|
||||
- 记忆:您的智能体会记住您的偏好、风格、目标和过往项目,提供个性化的专属帮助,并随着时间的推移不断优化。
|
||||
- 全新知识库:使用文件夹整理您的知识和资源
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
---
|
||||
title: "Model Runtime & Authentication Improvements \U0001F527"
|
||||
description: >-
|
||||
Enhanced model runtime with Claude Opus 4.6 on Bedrock, improved
|
||||
authentication flows, and better mobile experience.
|
||||
title: "Model Runtime & Authentication Improvements 🔧"
|
||||
description: Enhanced model runtime with Claude Opus 4.6 on Bedrock, improved authentication flows, and better mobile experience.
|
||||
tags:
|
||||
- Model Runtime
|
||||
- Authentication
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: "模型运行时与认证改进 \U0001F527"
|
||||
title: "模型运行时与认证改进 🔧"
|
||||
description: 增强模型运行时并支持 Bedrock 上的 Claude Opus 4.6,改进认证流程,优化移动端体验。
|
||||
tags:
|
||||
- 模型运行时
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
---
|
||||
title: "Search Optimization & Agent Documents \U0001F50D"
|
||||
description: >-
|
||||
Introduces BM25 search indexes, agent document storage, and full-text search
|
||||
capabilities.
|
||||
title: "Search Optimization & Agent Documents 🔍"
|
||||
description: Introduces BM25 search indexes, agent document storage, and full-text search capabilities.
|
||||
tags:
|
||||
- Search
|
||||
- BM25
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: "搜索优化与智能体文档 \U0001F50D"
|
||||
title: "搜索优化与智能体文档 🔍"
|
||||
description: 引入 BM25 搜索索引、智能体文档存储和全文检索能力。
|
||||
tags:
|
||||
- 搜索
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
title: 添加新的 Bot 平台
|
||||
description: 了解如何向 LobeHub 的渠道系统添加新的 Bot 平台(如 Slack、WhatsApp),包括 Schema 定义、客户端实现和平台注册。
|
||||
description: >-
|
||||
了解如何向 LobeHub 的渠道系统添加新的 Bot 平台(如 Slack、WhatsApp),包括 Schema 定义、客户端实现和平台注册。
|
||||
tags:
|
||||
- Bot 平台
|
||||
- 消息渠道
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
title: Code Style and Contribution Guidelines
|
||||
description: >-
|
||||
Learn about LobeHub's code style and contribution process for consistent
|
||||
coding.
|
||||
Learn about LobeHub's code style and contribution process for consistent coding.
|
||||
|
||||
tags:
|
||||
- Code Style
|
||||
- Contribution Guidelines
|
||||
@@ -95,12 +95,12 @@ Use the following emojis to prefix your commit messages:
|
||||
|
||||
| Emoji | Code | Type | Description | Triggers Release? |
|
||||
| ----- | ------------------------ | -------- | ------------------------ | ----------------- |
|
||||
| ✨ | `:sparkles:` | feat | New feature | Yes |
|
||||
| ✨ | `:sparkles:` | feat | New feature | Yes |
|
||||
| 🐛 | `:bug:` | fix | Bug fix | Yes |
|
||||
| 📝 | `:memo:` | docs | Documentation | No |
|
||||
| 💄 | `:lipstick:` | style | UI/styling changes | No |
|
||||
| ♻️ | `:recycle:` | refactor | Code refactoring | No |
|
||||
| ✅ | `:white_check_mark:` | test | Tests | No |
|
||||
| ✅ | `:white_check_mark:` | test | Tests | No |
|
||||
| 🔨 | `:hammer:` | chore | Maintenance tasks | No |
|
||||
| 🚀 | `:rocket:` | perf | Performance improvements | No |
|
||||
| 🌐 | `:globe_with_meridians:` | i18n | Internationalization | No |
|
||||
|
||||
@@ -465,6 +465,37 @@ table verifications {
|
||||
}
|
||||
}
|
||||
|
||||
table briefs {
|
||||
id uuid [pk, not null, default: `gen_random_uuid()`]
|
||||
user_id text [not null]
|
||||
task_id text
|
||||
cron_job_id text
|
||||
topic_id text
|
||||
agent_id text
|
||||
type text [not null]
|
||||
priority text [default: 'info']
|
||||
title text [not null]
|
||||
summary text [not null]
|
||||
artifacts jsonb
|
||||
actions jsonb
|
||||
comment_type text
|
||||
resolved_action text
|
||||
resolved_comment text
|
||||
read_at "timestamp with time zone"
|
||||
resolved_at "timestamp with time zone"
|
||||
created_at "timestamp with time zone" [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
user_id [name: 'briefs_user_id_idx']
|
||||
task_id [name: 'briefs_task_id_idx']
|
||||
cron_job_id [name: 'briefs_cron_job_id_idx']
|
||||
agent_id [name: 'briefs_agent_id_idx']
|
||||
type [name: 'briefs_type_idx']
|
||||
priority [name: 'briefs_priority_idx']
|
||||
(user_id, resolved_at) [name: 'briefs_unresolved_idx']
|
||||
}
|
||||
}
|
||||
|
||||
table chat_groups {
|
||||
id text [pk, not null]
|
||||
title text
|
||||
@@ -907,46 +938,6 @@ table nextauth_verificationtokens {
|
||||
}
|
||||
}
|
||||
|
||||
table notification_deliveries {
|
||||
id uuid [pk, not null, default: `gen_random_uuid()`]
|
||||
notification_id uuid [not null]
|
||||
channel text [not null]
|
||||
status text [not null]
|
||||
provider_message_id text
|
||||
failed_reason text
|
||||
sent_at "timestamp with time zone"
|
||||
created_at "timestamp with time zone" [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
notification_id [name: 'idx_deliveries_notification']
|
||||
channel [name: 'idx_deliveries_channel']
|
||||
status [name: 'idx_deliveries_status']
|
||||
}
|
||||
}
|
||||
|
||||
table notifications {
|
||||
id uuid [pk, not null, default: `gen_random_uuid()`]
|
||||
user_id text [not null]
|
||||
category text [not null]
|
||||
type text [not null]
|
||||
title text [not null]
|
||||
content text [not null]
|
||||
dedupe_key text
|
||||
action_url text
|
||||
is_read boolean [not null, default: false]
|
||||
is_archived boolean [not null, default: false]
|
||||
created_at "timestamp with time zone" [not null, default: `now()`]
|
||||
updated_at "timestamp with time zone" [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
user_id [name: 'idx_notifications_user']
|
||||
(user_id, created_at) [name: 'idx_notifications_user_active']
|
||||
user_id [name: 'idx_notifications_user_unread']
|
||||
(user_id, dedupe_key) [name: 'idx_notifications_dedupe', unique]
|
||||
(updated_at, created_at, id) [name: 'idx_notifications_archived_cleanup']
|
||||
}
|
||||
}
|
||||
|
||||
table oauth_handoffs {
|
||||
id text [pk, not null]
|
||||
client varchar(50) [not null]
|
||||
@@ -1380,45 +1371,14 @@ table sessions {
|
||||
}
|
||||
}
|
||||
|
||||
table briefs {
|
||||
id text [pk, not null]
|
||||
user_id text [not null]
|
||||
task_id text
|
||||
cron_job_id text
|
||||
topic_id text
|
||||
agent_id text
|
||||
type text [not null]
|
||||
priority text [default: 'info']
|
||||
title text [not null]
|
||||
summary text [not null]
|
||||
artifacts jsonb
|
||||
actions jsonb
|
||||
resolved_action text
|
||||
resolved_comment text
|
||||
read_at "timestamp with time zone"
|
||||
resolved_at "timestamp with time zone"
|
||||
created_at "timestamp with time zone" [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
user_id [name: 'briefs_user_id_idx']
|
||||
task_id [name: 'briefs_task_id_idx']
|
||||
cron_job_id [name: 'briefs_cron_job_id_idx']
|
||||
agent_id [name: 'briefs_agent_id_idx']
|
||||
type [name: 'briefs_type_idx']
|
||||
priority [name: 'briefs_priority_idx']
|
||||
(user_id, resolved_at) [name: 'briefs_unresolved_idx']
|
||||
}
|
||||
}
|
||||
|
||||
table task_comments {
|
||||
id text [pk, not null]
|
||||
id uuid [pk, not null, default: `gen_random_uuid()`]
|
||||
task_id text [not null]
|
||||
user_id text [not null]
|
||||
author_user_id text
|
||||
author_agent_id text
|
||||
user_id text
|
||||
agent_id text
|
||||
content text [not null]
|
||||
editor_data jsonb
|
||||
brief_id text
|
||||
brief_id uuid
|
||||
topic_id text
|
||||
accessed_at "timestamp with time zone" [not null, default: `now()`]
|
||||
created_at "timestamp with time zone" [not null, default: `now()`]
|
||||
@@ -1427,8 +1387,7 @@ table task_comments {
|
||||
indexes {
|
||||
task_id [name: 'task_comments_task_id_idx']
|
||||
user_id [name: 'task_comments_user_id_idx']
|
||||
author_user_id [name: 'task_comments_author_user_id_idx']
|
||||
author_agent_id [name: 'task_comments_agent_id_idx']
|
||||
agent_id [name: 'task_comments_agent_id_idx']
|
||||
brief_id [name: 'task_comments_brief_id_idx']
|
||||
topic_id [name: 'task_comments_topic_id_idx']
|
||||
}
|
||||
@@ -1438,7 +1397,6 @@ table task_dependencies {
|
||||
id uuid [pk, not null, default: `gen_random_uuid()`]
|
||||
task_id text [not null]
|
||||
depends_on_id text [not null]
|
||||
user_id text [not null]
|
||||
type text [not null, default: 'blocks']
|
||||
condition jsonb
|
||||
created_at "timestamp with time zone" [not null, default: `now()`]
|
||||
@@ -1447,7 +1405,6 @@ table task_dependencies {
|
||||
(task_id, depends_on_id) [name: 'task_deps_unique_idx', unique]
|
||||
task_id [name: 'task_deps_task_id_idx']
|
||||
depends_on_id [name: 'task_deps_depends_on_id_idx']
|
||||
user_id [name: 'task_deps_user_id_idx']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1455,7 +1412,6 @@ table task_documents {
|
||||
id uuid [pk, not null, default: `gen_random_uuid()`]
|
||||
task_id text [not null]
|
||||
document_id text [not null]
|
||||
user_id text [not null]
|
||||
pinned_by text [not null, default: 'agent']
|
||||
created_at "timestamp with time zone" [not null, default: `now()`]
|
||||
|
||||
@@ -1463,27 +1419,27 @@ table task_documents {
|
||||
(task_id, document_id) [name: 'task_docs_unique_idx', unique]
|
||||
task_id [name: 'task_docs_task_id_idx']
|
||||
document_id [name: 'task_docs_document_id_idx']
|
||||
user_id [name: 'task_docs_user_id_idx']
|
||||
}
|
||||
}
|
||||
|
||||
table task_topics {
|
||||
id uuid [pk, not null, default: `gen_random_uuid()`]
|
||||
task_id text [not null]
|
||||
topic_id text
|
||||
topic_id text [not null]
|
||||
user_id text [not null]
|
||||
seq integer [not null]
|
||||
operation_id text
|
||||
status text [not null, default: 'running']
|
||||
handoff jsonb
|
||||
handoff_title text
|
||||
handoff_summary text
|
||||
handoff_key_findings jsonb
|
||||
handoff_next_action text
|
||||
review_passed integer
|
||||
review_score integer
|
||||
review_scores jsonb
|
||||
review_iteration integer
|
||||
reviewed_at "timestamp with time zone"
|
||||
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 {
|
||||
(task_id, topic_id) [name: 'task_topics_unique_idx', unique]
|
||||
@@ -1527,7 +1483,6 @@ table tasks {
|
||||
updated_at "timestamp with time zone" [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
|
||||
(identifier, created_by_user_id) [name: 'tasks_identifier_idx', unique]
|
||||
created_by_user_id [name: 'tasks_created_by_user_id_idx']
|
||||
created_by_agent_id [name: 'tasks_created_by_agent_id_idx']
|
||||
@@ -1663,7 +1618,6 @@ table user_settings {
|
||||
memory jsonb
|
||||
tool jsonb
|
||||
image jsonb
|
||||
notification jsonb
|
||||
}
|
||||
|
||||
table users {
|
||||
@@ -2029,4 +1983,4 @@ ref: topic_documents.document_id > documents.id
|
||||
|
||||
ref: topic_documents.topic_id > topics.id
|
||||
|
||||
ref: topics.session_id - sessions.id
|
||||
ref: topics.session_id - sessions.id
|
||||
@@ -65,7 +65,7 @@ We need to configure an S3-compatible storage service in the server-side databas
|
||||
|
||||
Click `Object Storage` in the left sidebar, then the `Create Bucket` button in the top-right corner to create a new bucket. This example uses the name `lobe`. Leave Versioning and Object Lock disabled (default settings).
|
||||
|
||||
<Image alt={"Create Bucket"} src={'/blog/assetsc958eae64465451c4374cdee8f6fd596.webp'} />
|
||||
<Image alt={"Create Bucket"} src={'https://github.com/user-attachments/assets/27c37617-a813-4de5-b0bf-c7167999c856'} />
|
||||
|
||||
Go to the bucket and click `Settings`, choose `Custom` for the policy, and paste the following JSON to make the bucket public-read/private-write:
|
||||
|
||||
@@ -108,9 +108,9 @@ We need to configure an S3-compatible storage service in the server-side databas
|
||||
|
||||
Copy the generated Access Key and Secret Key (the `Export` button lets you save the JSON locally). The English labels in the UI are confusing, but remember the shorter string is the Access Key and the longer string is the Secret Key (the exported JSON is correct).
|
||||
|
||||
<Image alt={"Add Key"} src={'/blog/assets43d66c62b79a027895b5a6127b2f2de2.webp'} />
|
||||
<Image alt={"Add Key"} src={'https://github.com/user-attachments/assets/81f18b20-3918-4f77-8571-07d0c4a79aec'} />
|
||||
|
||||
<Image alt={"Export Key"} src={'/blog/assets04fecea4e5f4ce3490bf11bec66ff477.webp'} />
|
||||
<Image alt={"Export Key"} src={'https://github.com/user-attachments/assets/4dde41ec-985b-4781-8c77-aac65555a32f'} />
|
||||
|
||||
### Configure Reverse Proxy
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ tags:
|
||||
|
||||
点击左侧边栏的 `对象存储` 菜单,右上角 `创建存储桶` 按钮,创建一个新的存储桶(Bucket)。创建存储桶时将指定其名称,下文以 `lobe` 为例。版本、对象锁依照默认配置不开启。
|
||||
|
||||
<Image alt={"Create Bucket"} src={'/blog/assetsc958eae64465451c4374cdee8f6fd596.webp'} />
|
||||
<Image alt={"Create Bucket"} src={'https://github.com/user-attachments/assets/27c37617-a813-4de5-b0bf-c7167999c856'} />
|
||||
|
||||
点击存储桶 - `配置` 按钮,选择策略为 `自定义`,然后填入如下 JSON,设置存储桶的权限为 `公有读私有写`:
|
||||
|
||||
@@ -108,9 +108,9 @@ tags:
|
||||
|
||||
记录好得到的访问密钥和密钥(你可以点击 `导出` 按钮以在本地保存)。这里 RustFS 的翻译有点迷惑,但你只需要记住上面那个短的是 `Access Key`,长的是 `Secret Key` 即可(导出的 JSON 中是对的)。
|
||||
|
||||
<Image alt={"Add Key"} src={'/blog/assets43d66c62b79a027895b5a6127b2f2de2.webp'} />
|
||||
<Image alt={"Add Key"} src={'https://github.com/user-attachments/assets/81f18b20-3918-4f77-8571-07d0c4a79aec'} />
|
||||
|
||||
<Image alt={"Export Key"} src={'/blog/assets04fecea4e5f4ce3490bf11bec66ff477.webp'} />
|
||||
<Image alt={"Export Key"} src={'https://github.com/user-attachments/assets/4dde41ec-985b-4781-8c77-aac65555a32f'} />
|
||||
|
||||
### 配置反向代理
|
||||
|
||||
|
||||
@@ -1,63 +1,129 @@
|
||||
---
|
||||
title: Scheduled Tasks
|
||||
description: >-
|
||||
Learn how to use scheduled tasks, including creating, editing, and deleting
|
||||
them.
|
||||
Schedule agents to run tasks automatically at specified times — recurring
|
||||
reports, monitoring, content generation, and time-based workflows.
|
||||
tags:
|
||||
- LobeHub
|
||||
- CronJob
|
||||
- Scheduled Tasks
|
||||
- Create
|
||||
- Edit
|
||||
- Delete
|
||||
- Automation
|
||||
- Task Scheduling
|
||||
---
|
||||
|
||||
# Scheduled Tasks
|
||||
|
||||
Scheduled tasks are jobs that run periodically in the cloud. In short, you can have an Agent run on your prompt on a schedule — for example, checking social media regularly and sending notifications. Instead of manually triggering the same workflow over and over, set it once and let it run automatically — daily, weekly, or hourly.
|
||||
Scheduled tasks are jobs that run periodically in the cloud. Configure an Agent to execute tasks based on your prompt at regular intervals — daily, weekly, or hourly. Instead of manually triggering the same workflow repeatedly, schedule it once and let it run automatically.
|
||||
|
||||
## What Are Scheduled Tasks?
|
||||
|
||||
A scheduled task is an automated agent run that:
|
||||
|
||||
- **Runs automatically**: Executes at your specified time without manual triggering
|
||||
- **Follows a schedule**: Daily, weekly, hourly, or custom patterns
|
||||
- **Maintains context**: Each run creates a conversation with full agent context
|
||||
- **Works while you're away**: Runs even when you're not logged in
|
||||
- **Sends notifications**: Alerts you when tasks complete (if configured)
|
||||
|
||||
## Why Use Scheduled Tasks?
|
||||
|
||||
### Recurring Tasks
|
||||
|
||||
Automate tasks that need to happen regularly:
|
||||
|
||||
- Daily market research summaries
|
||||
- Weekly competitive analysis reports
|
||||
- Monthly performance reviews
|
||||
- Hourly monitoring and alerts
|
||||
|
||||
### Time-Based Workflows
|
||||
|
||||
Execute tasks at optimal times:
|
||||
|
||||
- Generate reports first thing Monday morning
|
||||
- Send summaries at end of business day
|
||||
- Run analysis during off-peak hours
|
||||
|
||||
### Consistency and Reliability
|
||||
|
||||
- Never forget routine tasks
|
||||
- Maintain regular cadence for important workflows
|
||||
- Reduce manual overhead
|
||||
|
||||
## Creating a Task
|
||||
|
||||
Find **Scheduled Tasks** in the left panel of the Agent conversation page, and click `Add Scheduled Task` to start creating a task.
|
||||
Find Scheduled Tasks in the left panel of the Agent conversation page, and click `Add Scheduled Task` to start creating a task.
|
||||
|
||||

|
||||

|
||||
|
||||
### Configuration fields
|
||||
<Steps>
|
||||
### Select an Agent
|
||||
|
||||
**Task name** — Give the task a descriptive name so you can recognize it at a glance:
|
||||
Navigate to the agent you want to schedule. Open the agent profile or settings panel.
|
||||
|
||||
- ✅ "Daily Market Summary - 9am"
|
||||
- ✅ "Weekly Competitor Analysis"
|
||||
- ❌ "Task 1"
|
||||
### Access Scheduling
|
||||
|
||||
**Task content** — Enter the prompt or instructions the Agent should run each time the task fires. Be specific and complete — this exact prompt runs on every scheduled execution. For example:
|
||||
Look for the **Scheduled Tasks** section and click **Add Scheduled Task**.
|
||||
|
||||
```
|
||||
Analyze today's top tech news and summarize:
|
||||
1. Major product launches
|
||||
2. Funding announcements
|
||||
3. Industry trends
|
||||
Format as a brief executive summary.
|
||||
```
|
||||
### Configure the Task
|
||||
|
||||
**Frequency** — Choose how often the task runs:
|
||||
#### Task Name
|
||||
|
||||
- **Daily** — Every day at a specified time
|
||||
- **Weekly** — On selected weekdays at a specified time (you can pick multiple days)
|
||||
- **Hourly** — Every 1, 2, 6, 12, or 24 hours
|
||||
Give your task a descriptive name so you can identify it at a glance:
|
||||
|
||||
**Time and timezone** — Set the exact time and timezone so the task runs at the correct local time. Times use 24-hour format. For distributed teams, getting the timezone right matters.
|
||||
- ✅ "Daily Market Summary - 9am EST"
|
||||
- ✅ "Weekly Competitor Analysis"
|
||||
- ❌ "Task 1"
|
||||
|
||||
**Max executions** — Optionally cap how many times the task runs in total. Ongoing tasks often need no limit; for time-boxed campaigns (e.g. 30 days), you might set 30 — the task disables itself after reaching the limit.
|
||||
#### Task Content
|
||||
|
||||
After you create a task, you can change its configuration at any time.
|
||||
Enter the prompt or instructions for the Agent to execute each time the task runs. Be specific and complete — this exact prompt runs every scheduled execution:
|
||||
|
||||
## Schedule configuration examples
|
||||
```
|
||||
Analyze today's top tech news and summarize:
|
||||
1. Major product launches
|
||||
2. Funding announcements
|
||||
3. Industry trends
|
||||
Format as a brief executive summary.
|
||||
```
|
||||
|
||||
#### Frequency
|
||||
|
||||
Choose how often the task runs:
|
||||
|
||||
- **Daily** — Every day at a specified time
|
||||
- **Weekly** — Specific days of the week at a specified time (you can select multiple days)
|
||||
- **Hourly** — Every 1, 2, 6, 12, or 24 hours
|
||||
|
||||
### Set the Time
|
||||
|
||||
Specify the exact time of day and your timezone so the task runs at the correct local time. Times are in 24-hour format.
|
||||
|
||||
For **weekly** schedules, select which days of the week to run. You can select multiple days (e.g., Monday, Wednesday, Friday).
|
||||
|
||||
For **hourly** schedules, set the interval and the minute when it runs.
|
||||
|
||||
### Configure Advanced Options
|
||||
|
||||
#### Timezone
|
||||
|
||||
Select your timezone so tasks run at the correct local time. Especially important for teams across multiple regions.
|
||||
|
||||
#### Max Executions
|
||||
|
||||
Optionally limit how many times the task runs total. Leave unlimited for ongoing tasks. Set a number (e.g., 30) for time-limited campaigns — the task disables automatically after reaching the limit.
|
||||
|
||||
### Save and Enable
|
||||
|
||||
Click **Save** to create the scheduled task. New tasks are typically enabled by default. After creation, you can modify the configuration at any time.
|
||||
</Steps>
|
||||
|
||||
## Schedule Configuration Examples
|
||||
|
||||
**Daily morning report:**
|
||||
|
||||
- Frequency: Daily at 08:00 in your timezone
|
||||
- Prompt: "Summarize yesterday's key metrics and list today's priorities."
|
||||
- Prompt: "Generate a summary of yesterday's key metrics and action items for today."
|
||||
|
||||
**Weekly planning session:**
|
||||
|
||||
@@ -71,67 +137,157 @@ After you create a task, you can change its configuration at any time.
|
||||
|
||||
**End-of-month review:**
|
||||
|
||||
- Frequency: Monthly — set Max Executions to once per month, or combine with a specific day
|
||||
- Frequency: Monthly — set Max Executions to 1 per month, or use day-of-month scheduling
|
||||
- Prompt: "Analyze this month's performance data and generate an executive report."
|
||||
|
||||
## Managing tasks
|
||||
## Managing Tasks
|
||||
|
||||
### Viewing run history
|
||||
### Viewing Run History
|
||||
|
||||
Each scheduled run creates an entry in that Agent's conversation history, labeled with the task name and timestamp. You can review outputs, check for errors, and track past results.
|
||||
Each scheduled run creates a conversation in the agent's conversation history, labeled with the task name and timestamp. Review outputs, check for errors, and track results over time.
|
||||
|
||||
### Editing a schedule
|
||||
### Editing a Schedule
|
||||
|
||||
Click a scheduled task to edit it — update the prompt, change frequency or time, or adjust the timezone. Changes apply from the next scheduled run onward.
|
||||
Click on a scheduled task to modify it — update the prompt, change the frequency or time, or adjust the timezone. Changes take effect on the next scheduled execution.
|
||||
|
||||
### Pausing a task
|
||||
### Pausing a Task
|
||||
|
||||
If you temporarily don't need a scheduled task, turn off its enabled state. While off, it won't run automatically; the schedule and prompt stay saved. When you turn it back on, the task continues as configured.
|
||||
If you temporarily don't need a scheduled task, you can disable it. After disabling, the task will no longer execute automatically, but the task's execution plan and prompt configuration will be preserved. The task resumes after re-enabling.
|
||||
|
||||

|
||||

|
||||
|
||||
### Deleting a task
|
||||
### Deleting a Task
|
||||
|
||||
If you no longer need a scheduled task, you can delete it. Deletion removes the schedule and prompt configuration; the system will not trigger further runs. Past conversation history is kept.
|
||||
If you no longer need a scheduled task, you can delete it. After deletion, the task's execution plan and prompt configuration are removed, and the system will no longer trigger any subsequent executions. Past conversation history is preserved.
|
||||
|
||||
## Best practices
|
||||
## Use Cases
|
||||
|
||||
**Write clear, self-contained prompts** — The scheduled task prompt runs with no prior conversation context. Everything the Agent needs must be in the prompt:
|
||||
<Tabs>
|
||||
<Tab title="News & Research">
|
||||
- **Daily tech news digest**: Summarize top stories every morning
|
||||
- **Competitor tracking**: Weekly analysis of competitor announcements
|
||||
- **Industry trends**: Monthly deep-dive into emerging trends
|
||||
- **Academic monitoring**: Track new papers in your field
|
||||
</Tab>
|
||||
|
||||
<Tab title="Content Generation">
|
||||
- **Social media drafts**: Daily post ideas based on current events
|
||||
- **Newsletter content**: Weekly roundup of relevant topics
|
||||
- **Blog post outlines**: Bi-weekly topic suggestions
|
||||
- **Report drafts**: Auto-generate periodic report templates
|
||||
</Tab>
|
||||
|
||||
<Tab title="Reporting & Analytics">
|
||||
- **Daily metrics summary**: KPI updates each morning
|
||||
- **Weekly performance review**: Analyze data and surface insights
|
||||
- **Monthly executive summary**: High-level overview for leadership
|
||||
- **Anomaly detection**: Flag unusual patterns in data
|
||||
</Tab>
|
||||
|
||||
<Tab title="Personal Productivity">
|
||||
- **Morning briefing**: Weather, calendar, priorities at 7am
|
||||
- **End-of-day review**: Summarize accomplishments at 5pm
|
||||
- **Weekly planning**: Sunday evening prep for the week ahead
|
||||
- **Reminder notifications**: Important milestones and check tasks
|
||||
</Tab>
|
||||
|
||||
<Tab title="Monitoring & Alerts">
|
||||
- **Hourly health checks**: Monitor systems or metrics
|
||||
- **Social media monitoring**: Track brand mentions and sentiment
|
||||
- **Price tracking**: Watch for changes in competitors or markets
|
||||
- **Security alerts**: High-frequency checks for critical issues
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Best Practices
|
||||
|
||||
**Write clear, self-contained prompts** — The task prompt runs without any prior conversation context. Every detail the Agent needs must be in the prompt itself:
|
||||
|
||||
- ✅ "Search for news about electric vehicles published in the last 24 hours and summarize the top 3 developments."
|
||||
- ❌ "Check the news like we discussed." (The Agent has no access to earlier chats when the schedule runs.)
|
||||
- ❌ "Check the news like we discussed." (Agent has no conversation context when scheduled)
|
||||
|
||||
**Choose appropriate frequency** — Match the schedule to how fast the information actually changes. Hourly checks for daily news add unnecessary load; weekly reports for real-time metrics miss important updates.
|
||||
**Choose appropriate frequency** — Match the schedule to the actual cadence of the information you're monitoring. Hourly monitoring for daily news is unnecessary overhead; weekly reports for real-time metrics miss the point.
|
||||
|
||||
**Use descriptive task names** — Put purpose and timing in the name: "Weekly Competitor Analysis - Monday 9am" beats "Task 2".
|
||||
**Use descriptive task names** — Include the purpose and schedule in the name: "Weekly Competitor Analysis - Monday 9am" is far more useful than "Task 2".
|
||||
|
||||
**Set max executions while experimenting** — When testing a new scheduled task, use a max execution count of 5–10 so it doesn't run forever if the prompt needs tuning.
|
||||
**Set max executions for experiments** — When testing a new scheduled task, set a max execution count of 5–10 so it doesn't run indefinitely if the prompt doesn't work as expected.
|
||||
|
||||
**Timezone awareness** — Always set the correct timezone. "09:00" is interpreted in the configured timezone, which may differ from your local clock. Wrong timezone is a common cause of unexpected run times.
|
||||
**Timezone awareness** — Always set the correct timezone. A task scheduled for "9:00 AM" defaults to the server timezone, which may differ from your local time. Account for daylight saving time changes.
|
||||
|
||||
## Use cases
|
||||
**Monitor results regularly** — Review scheduled run outputs to check if the agent is producing useful results and refine prompts based on actual outputs.
|
||||
|
||||
### Regularly check social media and notify you
|
||||
## Advanced Scheduling
|
||||
|
||||
Schedule a task to periodically check social content for given platforms or keywords. It can fetch recent activity, filter what matters, and summarize when there's something important — useful for brand monitoring, competitor tracking, or creator update alerts.
|
||||
### Custom Cron Patterns
|
||||
|
||||
### Periodic summaries and reports
|
||||
For advanced users, some interfaces support custom cron expressions:
|
||||
|
||||
For work that needs regular review — analytics, project status, or content performance — a scheduled task can gather information on a cadence and produce structured takeaways so you keep sight of trends.
|
||||
```
|
||||
0 9 * * 1-5 # Monday-Friday at 9:00am
|
||||
0 */6 * * * # Every 6 hours
|
||||
0 0 1 * * # First day of every month at midnight
|
||||
```
|
||||
|
||||
### Timed reminders
|
||||
### Chaining Scheduled Tasks
|
||||
|
||||
Set reminders for milestones, recurring checks, or follow-ups. LobeHub can generate reminder messages and notify you (for example by email) without you triggering the flow manually.
|
||||
Create workflows by scheduling multiple agents in sequence:
|
||||
|
||||
1. **Agent A** (8am): Gather data
|
||||
2. **Agent B** (9am): Analyze data from Agent A
|
||||
3. **Agent C** (10am): Generate report from Agent B's analysis
|
||||
|
||||
Coordinate timing so each task has inputs ready.
|
||||
|
||||
### Conditional Execution
|
||||
|
||||
Advanced setups may support conditions:
|
||||
|
||||
- Only run if certain criteria are met
|
||||
- Skip runs on holidays
|
||||
- Adjust frequency based on results
|
||||
|
||||
## Notifications and Integrations
|
||||
|
||||
Depending on your workspace configuration:
|
||||
|
||||
- **Email notifications**: Get alerts when runs complete
|
||||
- **Webhook integrations**: Send results to other tools
|
||||
- **Slack/Discord bots**: Post summaries to team channels
|
||||
- **Export options**: Download or share run outputs
|
||||
|
||||
Check your workspace settings for available integration options.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Task didn't run when expected** — Check the timezone. Scheduled times are relative to the configured timezone, not necessarily "now" on your device. Also confirm the task is enabled.
|
||||
<AccordionGroup>
|
||||
<Accordion title="Task Didn't Run at Expected Time">
|
||||
**Check if the task is enabled** — Disabled tasks won't execute. Toggle it back on if needed.
|
||||
|
||||
**Runs at surprising times** — Double-check 24-hour time (e.g. 17:00 is 5:00 PM, not 5:00 AM).
|
||||
**Verify the schedule configuration** — Is the time correct in your timezone? For weekly schedules, are the right days selected? Has it reached max executions?
|
||||
|
||||
**Poor output quality** — Scheduled prompts run without chat history. Rewrite the prompt so it is fully self-contained, with background, data sources, and format requirements spelled out.
|
||||
**Check for errors** — Look at the conversation history for failed runs.
|
||||
</Accordion>
|
||||
|
||||
**Too many runs** — While experimenting, set a **Max executions** cap. If a task has already run more than intended, delete it and create a new one with the right limits.
|
||||
<Accordion title="Unexpected Run Times">
|
||||
**Timezone mismatch** — Ensure the task timezone matches your expectations. Verify you haven't confused AM/PM in 24-hour format (e.g., 17:00 = 5:00 PM).
|
||||
|
||||
**Daylight Saving Time** — Some timezones shift with DST. Tasks may run an hour earlier/later after DST changes.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Poor Quality Outputs">
|
||||
**Refine your prompt** — Be more specific about what you want. Add examples of good outputs. Specify format and length.
|
||||
|
||||
**Wrong agent** — Ensure the agent is properly configured for the task and has necessary plugins or knowledge bases.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Too Many Runs">
|
||||
**Reduce frequency** — Change from hourly to daily, or daily to weekly.
|
||||
|
||||
**Set max executions** — Limit total runs to avoid runaway tasks.
|
||||
|
||||
**Disable temporarily** — Turn off the task while you reassess.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
<Cards>
|
||||
<Card href={'/docs/usage/agent/web-search'} title={'Web Search'} />
|
||||
|
||||
@@ -18,7 +18,7 @@ tags:
|
||||
|
||||
在 Agent 会话页面左侧面板找到定时任务,点击 `添加定时任务` 开始创建任务。
|
||||
|
||||

|
||||

|
||||
|
||||
### 配置字段说明
|
||||
|
||||
@@ -86,7 +86,7 @@ tags:
|
||||
|
||||
如果暂时不需要某个定时任务,可以关闭启用状态。关闭后,任务不再自动执行,执行计划和 Prompt 配置会保留。恢复启用后,该任务将继续执行。
|
||||
|
||||

|
||||

|
||||
|
||||
### 删除任务
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
title: Connect LobeHub to Discord
|
||||
description: >-
|
||||
Learn how to create a Discord bot and connect it to your LobeHub agent as a
|
||||
message channel, allowing your AI assistant to interact with users directly in
|
||||
Discord servers and direct messages.
|
||||
Learn how to create a Discord bot and connect it to your LobeHub agent as a message channel, allowing your AI assistant to interact with users directly in Discord servers and direct messages.
|
||||
|
||||
|
||||
tags:
|
||||
- Discord
|
||||
- Message Channels
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
title: Connect LobeHub to Feishu (飞书)
|
||||
description: >-
|
||||
Learn how to create a Feishu custom app and connect it to your LobeHub agent
|
||||
as a message channel, enabling your AI assistant to interact with team members
|
||||
in Feishu chats.
|
||||
Learn how to create a Feishu custom app and connect it to your LobeHub
|
||||
agent as a message channel, enabling your AI assistant to interact with team
|
||||
members in Feishu chats.
|
||||
tags:
|
||||
- Feishu
|
||||
- 飞书
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
title: Connect LobeHub to Lark
|
||||
description: >-
|
||||
Learn how to create a Lark custom app and connect it to your LobeHub agent as
|
||||
a message channel, enabling your AI assistant to interact with team members in
|
||||
Lark chats.
|
||||
Learn how to create a Lark custom app and connect it to your LobeHub
|
||||
agent as a message channel, enabling your AI assistant to interact with team
|
||||
members in Lark chats.
|
||||
tags:
|
||||
- Lark
|
||||
- Message Channels
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
title: Channels Overview
|
||||
description: >-
|
||||
Connect your LobeHub agents to external messaging platforms like Discord,
|
||||
Slack, Telegram, QQ, WeChat, Feishu, and Lark, allowing users to interact with
|
||||
AI assistants directly in their favorite chat apps.
|
||||
Slack, Telegram, QQ, Feishu, and Lark, allowing users to interact with AI
|
||||
assistants directly in their favorite chat apps.
|
||||
tags:
|
||||
- Channels
|
||||
- Message Channels
|
||||
@@ -12,7 +12,6 @@ tags:
|
||||
- Slack
|
||||
- Telegram
|
||||
- QQ
|
||||
- WeChat
|
||||
- Feishu
|
||||
- Lark
|
||||
---
|
||||
@@ -33,7 +32,6 @@ Channels allow you to connect your LobeHub agents to external messaging platform
|
||||
| [Slack](/docs/usage/channels/slack) | Connect to Slack for channel and direct message conversations |
|
||||
| [Telegram](/docs/usage/channels/telegram) | Connect to Telegram for private and group conversations |
|
||||
| [QQ](/docs/usage/channels/qq) | Connect to QQ for group chats and direct messages |
|
||||
| [WeChat (微信)](/docs/usage/channels/wechat) | Connect to WeChat via iLink Bot for private and group chats |
|
||||
| [Feishu (飞书)](/docs/usage/channels/feishu) | Connect to Feishu for team collaboration (Chinese version) |
|
||||
| [Lark](/docs/usage/channels/lark) | Connect to Lark for team collaboration (international version) |
|
||||
|
||||
@@ -42,7 +40,7 @@ Channels allow you to connect your LobeHub agents to external messaging platform
|
||||
Each channel integration works by linking a bot account on the target platform to a LobeHub agent. When a user sends a message to the bot, LobeHub processes it through the agent and sends the response back to the same conversation.
|
||||
|
||||
- **Per-agent configuration** — Each agent can have its own set of channel connections, so different agents can serve different platforms or communities.
|
||||
- **Multiple channels simultaneously** — A single agent can be connected to Discord, Slack, Telegram, QQ, WeChat, Feishu, and Lark at the same time. LobeHub routes messages to the correct agent automatically.
|
||||
- **Multiple channels simultaneously** — A single agent can be connected to Discord, Slack, Telegram, QQ, Feishu, and Lark at the same time. LobeHub routes messages to the correct agent automatically.
|
||||
- **Secure credential storage** — All bot tokens and app secrets are encrypted before being stored.
|
||||
|
||||
## Getting Started
|
||||
@@ -54,7 +52,6 @@ Each channel integration works by linking a bot account on the target platform t
|
||||
- [Slack](/docs/usage/channels/slack)
|
||||
- [Telegram](/docs/usage/channels/telegram)
|
||||
- [QQ](/docs/usage/channels/qq)
|
||||
- [WeChat (微信)](/docs/usage/channels/wechat)
|
||||
- [Feishu (飞书)](/docs/usage/channels/feishu)
|
||||
- [Lark](/docs/usage/channels/lark)
|
||||
|
||||
@@ -62,10 +59,10 @@ Each channel integration works by linking a bot account on the target platform t
|
||||
|
||||
Text messages are supported across all platforms. Some features vary by platform:
|
||||
|
||||
| Feature | Discord | Slack | Telegram | QQ | WeChat | Feishu | Lark |
|
||||
| ---------------------- | ------- | ----- | -------- | --- | ------ | ------- | ------- |
|
||||
| Text messages | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Direct messages | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Group chats | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Reactions | Yes | Yes | Yes | No | No | Partial | Partial |
|
||||
| Image/file attachments | Yes | Yes | Yes | Yes | No | Yes | Yes |
|
||||
| Feature | Discord | Slack | Telegram | QQ | Feishu | Lark |
|
||||
| ---------------------- | ------- | ----- | -------- | --- | ------- | ------- |
|
||||
| Text messages | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Direct messages | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Group chats | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Reactions | Yes | Yes | Yes | No | Partial | Partial |
|
||||
| Image/file attachments | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
---
|
||||
title: 渠道概览
|
||||
description: >-
|
||||
将 LobeHub 代理连接到外部消息平台,如 Discord、Slack、Telegram、QQ、微信、飞书和
|
||||
Lark,让用户可以直接在他们喜欢的聊天应用中与 AI 助手互动。
|
||||
description: 将 LobeHub 代理连接到外部消息平台,如 Discord、Slack、Telegram、QQ、飞书和 Lark,让用户可以直接在他们喜欢的聊天应用中与 AI 助手互动。
|
||||
tags:
|
||||
- 渠道
|
||||
- 消息渠道
|
||||
@@ -11,7 +9,6 @@ tags:
|
||||
- Slack
|
||||
- Telegram
|
||||
- QQ
|
||||
- 微信
|
||||
- 飞书
|
||||
- Lark
|
||||
---
|
||||
@@ -26,22 +23,21 @@ tags:
|
||||
|
||||
## 支持的平台
|
||||
|
||||
| 平台 | 描述 |
|
||||
| ----------------------------------------- | -------------------------- |
|
||||
| [Discord](/docs/usage/channels/discord) | 连接到 Discord 服务器,用于频道聊天和私信 |
|
||||
| [Slack](/docs/usage/channels/slack) | 连接到 Slack,用于频道和私信对话 |
|
||||
| [Telegram](/docs/usage/channels/telegram) | 连接到 Telegram,用于私人和群组对话 |
|
||||
| [QQ](/docs/usage/channels/qq) | 连接到 QQ,用于群聊和私信 |
|
||||
| [微信](/docs/usage/channels/wechat) | 通过 iLink Bot 连接到微信,用于私聊和群聊 |
|
||||
| [飞书](/docs/usage/channels/feishu) | 连接到飞书,用于团队协作(中国版) |
|
||||
| [Lark](/docs/usage/channels/lark) | 连接到 Lark,用于团队协作(国际版) |
|
||||
| 平台 | 描述 |
|
||||
| ----------------------------------------- | ------------------------- |
|
||||
| [Discord](/docs/usage/channels/discord) | 连接到 Discord 服务器,用于频道聊天和私信 |
|
||||
| [Slack](/docs/usage/channels/slack) | 连接到 Slack,用于频道和私信对话 |
|
||||
| [Telegram](/docs/usage/channels/telegram) | 连接到 Telegram,用于私人和群组对话 |
|
||||
| [QQ](/docs/usage/channels/qq) | 连接到 QQ,用于群聊和私信 |
|
||||
| [飞书](/docs/usage/channels/feishu) | 连接到飞书,用于团队协作(中国版) |
|
||||
| [Lark](/docs/usage/channels/lark) | 连接到 Lark,用于团队协作(国际版) |
|
||||
|
||||
## 工作原理
|
||||
|
||||
每个渠道集成都通过将目标平台上的机器人账户与 LobeHub 代理连接来实现。当用户向机器人发送消息时,LobeHub 会通过代理处理消息并将响应发送回同一对话。
|
||||
|
||||
- **按代理配置** — 每个代理可以拥有自己的一组渠道连接,因此不同的代理可以服务于不同的平台或社区。
|
||||
- **同时支持多个渠道** — 单个代理可以同时连接到 Discord、Slack、Telegram、QQ、微信、飞书和 Lark。LobeHub 会自动将消息路由到正确的代理。
|
||||
- **同时支持多个渠道** — 单个代理可以同时连接到 Discord、Slack、Telegram、QQ、飞书和 Lark。LobeHub 会自动将消息路由到正确的代理。
|
||||
- **安全的凭据存储** — 所有机器人令牌和应用密钥在存储前都会被加密。
|
||||
|
||||
## 快速开始
|
||||
@@ -53,7 +49,6 @@ tags:
|
||||
- [Slack](/docs/usage/channels/slack)
|
||||
- [Telegram](/docs/usage/channels/telegram)
|
||||
- [QQ](/docs/usage/channels/qq)
|
||||
- [微信](/docs/usage/channels/wechat)
|
||||
- [飞书](/docs/usage/channels/feishu)
|
||||
- [Lark](/docs/usage/channels/lark)
|
||||
|
||||
@@ -61,10 +56,10 @@ tags:
|
||||
|
||||
所有平台均支持文本消息。某些功能因平台而异:
|
||||
|
||||
| 功能 | Discord | Slack | Telegram | QQ | 微信 | 飞书 | Lark |
|
||||
| --------- | ------- | ----- | -------- | -- | -- | ---- | ---- |
|
||||
| 文本消息 | 是 | 是 | 是 | 是 | 是 | 是 | 是 |
|
||||
| 私人消息 | 是 | 是 | 是 | 是 | 是 | 是 | 是 |
|
||||
| 群组聊天 | 是 | 是 | 是 | 是 | 是 | 是 | 是 |
|
||||
| 表情反应 | 是 | 是 | 是 | 否 | 否 | 部分支持 | 部分支持 |
|
||||
| 图片 / 文件附件 | 是 | 是 | 是 | 是 | 否 | 是 | 是 |
|
||||
| 功能 | Discord | Slack | Telegram | QQ | 飞书 | Lark |
|
||||
| --------- | ------- | ----- | -------- | -- | ---- | ---- |
|
||||
| 文本消息 | 是 | 是 | 是 | 是 | 是 | 是 |
|
||||
| 私人消息 | 是 | 是 | 是 | 是 | 是 | 是 |
|
||||
| 群组聊天 | 是 | 是 | 是 | 是 | 是 | 是 |
|
||||
| 表情反应 | 是 | 是 | 是 | 否 | 部分支持 | 部分支持 |
|
||||
| 图片 / 文件附件 | 是 | 是 | 是 | 是 | 是 | 是 |
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
title: Connect LobeHub to QQ
|
||||
description: >-
|
||||
Learn how to create a QQ bot and connect it to your LobeHub agent as a message
|
||||
channel, enabling your AI assistant to chat with users in QQ group chats and
|
||||
direct messages.
|
||||
Learn how to create a QQ bot and connect it to your LobeHub agent as a
|
||||
message channel, enabling your AI assistant to chat with users in QQ
|
||||
group chats and direct messages.
|
||||
tags:
|
||||
- QQ
|
||||
- Message Channels
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
---
|
||||
title: 将 LobeHub 连接到 Slack
|
||||
description: 了解如何创建一个 Slack 应用并将其连接到您的 LobeHub 代理作为消息渠道,使您的 AI 助手能够直接在 Slack 频道和私信中与用户互动。
|
||||
description: >-
|
||||
了解如何创建一个 Slack 应用并将其连接到您的 LobeHub 代理作为消息渠道,使您的 AI 助手能够直接在 Slack
|
||||
频道和私信中与用户互动。
|
||||
tags:
|
||||
- Slack
|
||||
- 消息渠道
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
---
|
||||
title: Connect LobeHub to WeChat
|
||||
description: >-
|
||||
Learn how to connect a WeChat bot to your LobeHub agent via the iLink Bot API,
|
||||
enabling your AI assistant to chat with users in WeChat private and group
|
||||
conversations.
|
||||
tags:
|
||||
- WeChat
|
||||
- Message Channels
|
||||
- Bot Setup
|
||||
- Integration
|
||||
---
|
||||
|
||||
# Connect LobeHub to WeChat
|
||||
|
||||
<Callout type={'info'}>
|
||||
This feature is currently in development and may not be fully stable. You can enable it by turning
|
||||
on **Developer Mode** in **Settings** → **Advanced Settings** → **Developer Mode**.
|
||||
</Callout>
|
||||
|
||||
By connecting a WeChat channel to your LobeHub agent, users can interact with the AI assistant through WeChat private chats and group conversations.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A LobeHub account with an active subscription
|
||||
- A WeChat account
|
||||
|
||||
## Step 1: Open Channel Settings
|
||||
|
||||
In LobeHub, navigate to your agent's settings, then select the **Channels** tab. Click **WeChat** from the platform list.
|
||||
|
||||
## Step 2: Scan QR Code to Connect
|
||||
|
||||
<Steps>
|
||||
### Click "Scan QR Code to Connect"
|
||||
|
||||
On the WeChat channel page, click the **Scan QR Code to Connect** button. A modal dialog will appear displaying a QR code.
|
||||
|
||||
### Scan with WeChat
|
||||
|
||||
Open WeChat on your phone, go to **Scan** (via the + button in the top right), and scan the QR code displayed in LobeHub.
|
||||
|
||||
### Confirm Login
|
||||
|
||||
After scanning, a confirmation prompt will appear in WeChat. Tap **Confirm** to authorize the connection.
|
||||
|
||||
### Connection Complete
|
||||
|
||||
Once confirmed, LobeHub will automatically save your credentials and connect the bot. You should see a success message in the channel settings.
|
||||
</Steps>
|
||||
|
||||
## Step 3: Test the Bot
|
||||
|
||||
Open WeChat, find your bot contact, and send a message. The bot should respond through your LobeHub agent.
|
||||
|
||||
## Adding the Bot to Group Chats
|
||||
|
||||
To use the bot in WeChat groups:
|
||||
|
||||
1. Add the bot to a WeChat group
|
||||
2. @mention the bot or send a message in the group to trigger a response
|
||||
3. The bot will reply in the group conversation
|
||||
|
||||
## Advanced Settings
|
||||
|
||||
| Setting | Default | Description |
|
||||
| ------------------------ | ------- | -------------------------------------------------------- |
|
||||
| **Character Limit** | 2000 | Maximum characters per message (range: 100–2000) |
|
||||
| **Message Merge Window** | 2000 ms | How long to wait for additional messages before replying |
|
||||
| **Show Usage Stats** | Off | Display token/cost stats in replies |
|
||||
|
||||
## How It Works
|
||||
|
||||
Unlike webhook-based platforms (Telegram, Slack), WeChat uses a **long-polling** mechanism via the iLink Bot API:
|
||||
|
||||
1. When you scan the QR code, LobeHub obtains a bot token from WeChat's iLink API
|
||||
2. LobeHub continuously polls the iLink API for new messages (\~35 second intervals)
|
||||
3. When a message arrives, it is routed through the LobeHub agent for processing
|
||||
4. The agent's response is sent back to WeChat via the iLink API
|
||||
|
||||
This polling is managed by a background cron job, so the connection is maintained automatically.
|
||||
|
||||
## Limitations
|
||||
|
||||
- **No message editing** — WeChat does not support editing sent messages. Updated responses will be sent as new messages.
|
||||
- **No reactions** — WeChat iLink Bot API does not support emoji reactions.
|
||||
- **Text only** — Only text messages are currently supported. Image and file attachments are not yet available.
|
||||
- **Message length limit** — Messages exceeding 2000 characters will be automatically split into multiple messages.
|
||||
- **Session expiration** — The bot session may expire and require re-authentication by scanning a new QR code.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **QR code expired:** Click **Refresh QR Code** in the modal to generate a new one.
|
||||
- **Bot not responding:** The session may have expired. Go to the WeChat channel settings and re-scan the QR code to reconnect.
|
||||
- **Delayed responses:** Long-polling has a natural delay of up to 35 seconds between polls. This is expected behavior.
|
||||
- **Connection lost after some time:** WeChat sessions expire periodically. Re-authenticate by clicking "Scan QR Code to Connect" again.
|
||||
@@ -1,93 +0,0 @@
|
||||
---
|
||||
title: 将 LobeHub 连接到微信
|
||||
description: 了解如何通过 iLink Bot API 将微信机器人连接到您的 LobeHub 代理,使您的 AI 助手能够在微信私聊和群聊中与用户互动。
|
||||
tags:
|
||||
- 微信
|
||||
- 消息渠道
|
||||
- 机器人设置
|
||||
- 集成
|
||||
---
|
||||
|
||||
# 将 LobeHub 连接到微信
|
||||
|
||||
<Callout type={'info'}>
|
||||
此功能目前正在开发中,可能尚未完全稳定。您可以通过在 **设置** → **高级设置** → **开发者模式**
|
||||
中启用 **开发者模式** 来使用此功能。
|
||||
</Callout>
|
||||
|
||||
通过将微信渠道连接到您的 LobeHub 代理,用户可以通过微信私聊和群聊与 AI 助手互动。
|
||||
|
||||
## 前置条件
|
||||
|
||||
- 一个拥有有效订阅的 LobeHub 账户
|
||||
- 一个微信账户
|
||||
|
||||
## 第一步:打开渠道设置
|
||||
|
||||
在 LobeHub 中,导航到您的代理设置,然后选择 **渠道** 标签页。从平台列表中点击 **微信**。
|
||||
|
||||
## 第二步:扫码连接
|
||||
|
||||
<Steps>
|
||||
### 点击 "扫码连接"
|
||||
|
||||
在微信渠道页面中,点击 **扫码连接** 按钮。将弹出一个显示二维码的对话框。
|
||||
|
||||
### 使用微信扫码
|
||||
|
||||
打开手机微信,点击右上角的 **+** 按钮,选择 **扫一扫**,扫描 LobeHub 中显示的二维码。
|
||||
|
||||
### 确认登录
|
||||
|
||||
扫码后,微信中会出现确认提示。点击 **确认** 授权连接。
|
||||
|
||||
### 连接完成
|
||||
|
||||
确认后,LobeHub 将自动保存凭证并连接机器人。您应该会在渠道设置中看到成功消息。
|
||||
</Steps>
|
||||
|
||||
## 第三步:测试机器人
|
||||
|
||||
打开微信,找到您的机器人联系人,发送一条消息。机器人应通过您的 LobeHub 代理进行响应。
|
||||
|
||||
## 将机器人添加到群聊
|
||||
|
||||
要在微信群聊中使用机器人:
|
||||
|
||||
1. 将机器人添加到微信群聊中
|
||||
2. @提及机器人或在群中发送消息以触发响应
|
||||
3. 机器人将在群聊中回复
|
||||
|
||||
## 高级设置
|
||||
|
||||
| 设置 | 默认值 | 描述 |
|
||||
| ---------- | ------- | ----------------------- |
|
||||
| **字符限制** | 2000 | 每条消息的最大字符数(范围:100–2000) |
|
||||
| **消息合并窗口** | 2000 毫秒 | 等待更多消息再回复的时间 |
|
||||
| **显示使用统计** | 关闭 | 在回复中显示 Token 用量 / 成本统计 |
|
||||
|
||||
## 工作原理
|
||||
|
||||
与基于 Webhook 的平台(Telegram、Slack)不同,微信使用 iLink Bot API 的 **长轮询** 机制:
|
||||
|
||||
1. 当您扫描二维码时,LobeHub 从微信 iLink API 获取 bot token
|
||||
2. LobeHub 持续轮询 iLink API 获取新消息(约 35 秒间隔)
|
||||
3. 当消息到达时,通过 LobeHub 代理进行处理
|
||||
4. 代理的响应通过 iLink API 发送回微信
|
||||
|
||||
此轮询由后台定时任务管理,连接会自动维护。
|
||||
|
||||
## 功能限制
|
||||
|
||||
- **不支持消息编辑** — 微信不支持编辑已发送的消息。更新的回复将作为新消息发送。
|
||||
- **不支持表情回应** — 微信 iLink Bot API 不支持表情回应功能。
|
||||
- **仅支持文本** — 目前仅支持文本消息。图片和文件附件暂不可用。
|
||||
- **消息长度限制** — 超过 2000 个字符的消息将被自动拆分为多条消息发送。
|
||||
- **会话过期** — 机器人会话可能会过期,需要重新扫码认证。
|
||||
|
||||
## 故障排除
|
||||
|
||||
- **二维码已过期:** 在弹窗中点击 **刷新二维码** 生成新的二维码。
|
||||
- **机器人未响应:** 会话可能已过期。前往微信渠道设置,重新扫码连接。
|
||||
- **响应延迟:** 长轮询在两次轮询之间有最多 35 秒的自然延迟。这是预期行为。
|
||||
- **一段时间后连接断开:** 微信会话会定期过期。再次点击 "扫码连接" 重新认证。
|
||||
@@ -24,7 +24,7 @@ The Command Menu is LobeHub's quick action center. Press `⌘ + K` (Mac) or `Ctr
|
||||
|
||||
The menu appears as an overlay in the center of the screen.
|
||||
|
||||
<Image alt={'Command Menu'} src={'/blog/assets095af3a0a0f850fc206fc3bbc19a4095.webp'} />
|
||||
<Image alt={'Command Menu'} src={'https://file.rene.wang/clipboard-1769137275089-21cf7ab42d52b.png'} />
|
||||
|
||||
## What You Can Search
|
||||
|
||||
@@ -38,7 +38,7 @@ The menu appears as an overlay in the center of the screen.
|
||||
|
||||
**Keyboard navigation:** Use `↑` and `↓` to move through results, `Enter` to execute, `Esc` to close. `Tab` switches between result categories when you're typing a message.
|
||||
|
||||
<Image alt={'Command Menu Search and Navigation'} src={'/blog/assetsebc1ebe8330d982f6a0b757aafb3f4a1.webp'} />
|
||||
<Image alt={'Command Menu Search and Navigation'} src={'https://file.rene.wang/clipboard-1769137300488-0b894cc8c7a67.png'} />
|
||||
|
||||
## Ask an Agent
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ tags:
|
||||
|
||||
菜单会以浮层形式出现在屏幕中央。
|
||||
|
||||
<Image alt={'命令菜单'} src={'/blog/assets095af3a0a0f850fc206fc3bbc19a4095.webp'} />
|
||||
<Image alt={'命令菜单'} src={'https://file.rene.wang/clipboard-1769137275089-21cf7ab42d52b.png'} />
|
||||
|
||||
## 可以搜索什么
|
||||
|
||||
@@ -36,7 +36,7 @@ tags:
|
||||
|
||||
**键盘导航:** 用 `↑` 和 `↓` 在结果间移动,`Enter` 执行,`Esc` 关闭。输入消息时,`Tab` 可在结果类别间切换。
|
||||
|
||||
<Image alt={'命令菜单搜索和导航'} src={'/blog/assetsebc1ebe8330d982f6a0b757aafb3f4a1.webp'} />
|
||||
<Image alt={'命令菜单搜索和导航'} src={'https://file.rene.wang/clipboard-1769137300488-0b894cc8c7a67.png'} />
|
||||
|
||||
## 向助理提问
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ async function createTestAgent(title: string = 'Test Agent'): Promise<string> {
|
||||
// Given Steps
|
||||
// ============================================
|
||||
|
||||
Given('用户在 Home 页面有一个 Agent', { timeout: 30_000 }, async function (this: CustomWorld) {
|
||||
Given('用户在 Home 页面有一个 Agent', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 在数据库中创建测试 Agent...');
|
||||
const agentId = await createTestAgent('E2E Test Agent');
|
||||
this.testContext.createdAgentId = agentId;
|
||||
|
||||
@@ -166,7 +166,7 @@ async function clickNewPageButton(world: CustomWorld): Promise<void> {
|
||||
// Given Steps
|
||||
// ============================================
|
||||
|
||||
Given('用户在 Page 页面', { timeout: 30_000 }, async function (this: CustomWorld) {
|
||||
Given('用户在 Page 页面', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 导航到 Page 页面...');
|
||||
await this.page.goto('/page');
|
||||
await this.page.waitForLoadState('networkidle', { timeout: 15_000 });
|
||||
|
||||
+1
-37
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"channel.appSecret": "سر التطبيق",
|
||||
"channel.appSecretHint": "سر التطبيق لتطبيق الروبوت الخاص بك. سيتم تشفيره وتخزينه بأمان.",
|
||||
"channel.appSecretPlaceholder": "الصق سر التطبيق هنا",
|
||||
"channel.applicationId": "معرف التطبيق / اسم المستخدم للبوت",
|
||||
"channel.applicationIdHint": "معرف فريد لتطبيق البوت الخاص بك.",
|
||||
@@ -10,31 +9,14 @@
|
||||
"channel.botTokenHowToGet": "كيف تحصل عليه؟",
|
||||
"channel.botTokenPlaceholderExisting": "الرمز مخفي لأسباب أمنية",
|
||||
"channel.botTokenPlaceholderNew": "الصق رمز البوت هنا",
|
||||
"channel.charLimit": "حد الأحرف",
|
||||
"channel.charLimitHint": "الحد الأقصى لعدد الأحرف لكل رسالة",
|
||||
"channel.connectFailed": "فشل اتصال الروبوت",
|
||||
"channel.connectSuccess": "تم الاتصال بالروبوت بنجاح",
|
||||
"channel.connecting": "جارٍ الاتصال...",
|
||||
"channel.connectionConfig": "إعدادات الاتصال",
|
||||
"channel.copied": "تم النسخ إلى الحافظة",
|
||||
"channel.copy": "نسخ",
|
||||
"channel.credentials": "بيانات الاعتماد",
|
||||
"channel.debounceMs": "نافذة دمج الرسائل (مللي ثانية)",
|
||||
"channel.debounceMsHint": "مدة الانتظار للرسائل الإضافية قبل إرسالها إلى الوكيل (مللي ثانية)",
|
||||
"channel.deleteConfirm": "هل أنت متأكد أنك تريد إزالة هذه القناة؟",
|
||||
"channel.deleteConfirmDesc": "سيؤدي هذا الإجراء إلى إزالة قناة الرسائل وتكوينها بشكل دائم. لا يمكن التراجع عن ذلك.",
|
||||
"channel.devWebhookProxyUrl": "عنوان URL لنفق HTTPS",
|
||||
"channel.devWebhookProxyUrlHint": "اختياري. عنوان URL لنفق HTTPS لإعادة توجيه طلبات الويب هوك إلى خادم التطوير المحلي.",
|
||||
"channel.disabled": "معطل",
|
||||
"channel.discord.description": "قم بتوصيل هذا المساعد بخادم Discord للدردشة في القنوات والرسائل المباشرة.",
|
||||
"channel.dm": "الرسائل المباشرة",
|
||||
"channel.dmEnabled": "تمكين الرسائل المباشرة",
|
||||
"channel.dmEnabledHint": "السماح للروبوت بتلقي الرسائل المباشرة والرد عليها",
|
||||
"channel.dmPolicy": "سياسة الرسائل المباشرة",
|
||||
"channel.dmPolicyAllowlist": "القائمة المسموح بها",
|
||||
"channel.dmPolicyDisabled": "معطل",
|
||||
"channel.dmPolicyHint": "التحكم في من يمكنه إرسال الرسائل المباشرة إلى الروبوت",
|
||||
"channel.dmPolicyOpen": "مفتوح",
|
||||
"channel.documentation": "التوثيق",
|
||||
"channel.enabled": "مفعّل",
|
||||
"channel.encryptKey": "مفتاح التشفير",
|
||||
@@ -44,7 +26,6 @@
|
||||
"channel.endpointUrlHint": "يرجى نسخ هذا العنوان ولصقه في الحقل <bold>{{fieldName}}</bold> في بوابة مطوري {{name}}.",
|
||||
"channel.feishu.description": "قم بتوصيل هذا المساعد بـ Feishu للدردشة الخاصة والجماعية.",
|
||||
"channel.lark.description": "قم بتوصيل هذا المساعد بـ Lark للدردشة الخاصة والجماعية.",
|
||||
"channel.openPlatform": "منصة مفتوحة",
|
||||
"channel.platforms": "المنصات",
|
||||
"channel.publicKey": "المفتاح العام",
|
||||
"channel.publicKeyHint": "اختياري. يُستخدم للتحقق من طلبات التفاعل من Discord.",
|
||||
@@ -61,16 +42,6 @@
|
||||
"channel.secretToken": "رمز سر الويب هوك",
|
||||
"channel.secretTokenHint": "اختياري. يُستخدم للتحقق من طلبات الويب هوك من Telegram.",
|
||||
"channel.secretTokenPlaceholder": "السر الاختياري للتحقق من الويب هوك",
|
||||
"channel.settings": "الإعدادات المتقدمة",
|
||||
"channel.settingsResetConfirm": "هل أنت متأكد أنك تريد إعادة تعيين الإعدادات المتقدمة إلى الوضع الافتراضي؟",
|
||||
"channel.settingsResetDefault": "إعادة إلى الوضع الافتراضي",
|
||||
"channel.setupGuide": "دليل الإعداد",
|
||||
"channel.showUsageStats": "عرض إحصائيات الاستخدام",
|
||||
"channel.showUsageStatsHint": "عرض استخدام الرموز، التكلفة، وإحصائيات المدة في ردود الروبوت",
|
||||
"channel.signingSecret": "سر التوقيع",
|
||||
"channel.signingSecretHint": "يُستخدم للتحقق من طلبات الويب هوك.",
|
||||
"channel.slack.appIdHint": "معرف تطبيق Slack الخاص بك من لوحة تحكم API Slack (يبدأ بـ A).",
|
||||
"channel.slack.description": "قم بتوصيل هذا المساعد بـ Slack للمحادثات القنوية والرسائل المباشرة.",
|
||||
"channel.telegram.description": "قم بتوصيل هذا المساعد بـ Telegram للدردشة الخاصة والجماعية.",
|
||||
"channel.testConnection": "اختبار الاتصال",
|
||||
"channel.testFailed": "فشل اختبار الاتصال",
|
||||
@@ -79,12 +50,5 @@
|
||||
"channel.validationError": "يرجى ملء معرف التطبيق والرمز",
|
||||
"channel.verificationToken": "رمز التحقق",
|
||||
"channel.verificationTokenHint": "اختياري. يُستخدم للتحقق من مصدر أحداث الويب هوك.",
|
||||
"channel.verificationTokenPlaceholder": "الصق رمز التحقق هنا",
|
||||
"channel.wechat.description": "قم بتوصيل هذا المساعد بـ WeChat عبر iLink Bot للمحادثات الخاصة والجماعية.",
|
||||
"channel.wechatQrExpired": "انتهت صلاحية رمز الاستجابة السريعة. يرجى التحديث للحصول على رمز جديد.",
|
||||
"channel.wechatQrRefresh": "تحديث رمز الاستجابة السريعة",
|
||||
"channel.wechatQrScaned": "تم مسح رمز الاستجابة السريعة. يرجى تأكيد تسجيل الدخول في WeChat.",
|
||||
"channel.wechatQrWait": "افتح WeChat وقم بمسح رمز الاستجابة السريعة للاتصال.",
|
||||
"channel.wechatScanTitle": "توصيل روبوت WeChat",
|
||||
"channel.wechatScanToConnect": "مسح رمز الاستجابة السريعة للاتصال"
|
||||
"channel.verificationTokenPlaceholder": "الصق رمز التحقق هنا"
|
||||
}
|
||||
|
||||
@@ -397,6 +397,7 @@
|
||||
"sync.status.unconnected": "فشل الاتصال",
|
||||
"sync.title": "حالة المزامنة",
|
||||
"sync.unconnected.tip": "فشل الاتصال بخادم الإشارة، ولا يمكن إنشاء قناة اتصال من نظير إلى نظير. يرجى التحقق من الشبكة والمحاولة مرة أخرى.",
|
||||
"tab.aiImage": "الرسومات",
|
||||
"tab.audio": "الصوت",
|
||||
"tab.chat": "الدردشة",
|
||||
"tab.community": "المجتمع",
|
||||
@@ -404,7 +405,6 @@
|
||||
"tab.eval": "مختبر التقييم",
|
||||
"tab.files": "الملفات",
|
||||
"tab.home": "الرئيسية",
|
||||
"tab.image": "صورة",
|
||||
"tab.knowledgeBase": "المكتبة",
|
||||
"tab.marketplace": "السوق",
|
||||
"tab.me": "أنا",
|
||||
@@ -432,7 +432,6 @@
|
||||
"userPanel.billing": "إدارة الفوترة",
|
||||
"userPanel.cloud": "تشغيل {{name}}",
|
||||
"userPanel.community": "المجتمع",
|
||||
"userPanel.credits": "إدارة الرصيد",
|
||||
"userPanel.data": "تخزين البيانات",
|
||||
"userPanel.defaultNickname": "مستخدم المجتمع",
|
||||
"userPanel.discord": "دعم المجتمع",
|
||||
@@ -444,7 +443,6 @@
|
||||
"userPanel.plans": "خطط الاشتراك",
|
||||
"userPanel.profile": "الحساب",
|
||||
"userPanel.setting": "الإعدادات",
|
||||
"userPanel.upgradePlan": "ترقية الخطة",
|
||||
"userPanel.usages": "إحصائيات الاستخدام",
|
||||
"version": "الإصدار"
|
||||
}
|
||||
|
||||
@@ -83,11 +83,6 @@
|
||||
"preference.empty": "لا توجد ذكريات تفضيل متاحة",
|
||||
"preference.source": "المصدر",
|
||||
"preference.suggestions": "الإجراءات التي قد يتخذها الوكيل",
|
||||
"purge.action": "حذف الكل",
|
||||
"purge.confirm": "هل أنت متأكد أنك تريد حذف جميع الذكريات؟ سيؤدي ذلك إلى إزالة كل إدخال للذكريات بشكل دائم ولا يمكن التراجع عنه.",
|
||||
"purge.error": "فشل في حذف الذكريات. يرجى المحاولة مرة أخرى.",
|
||||
"purge.success": "تم حذف جميع الذكريات.",
|
||||
"purge.title": "حذف جميع الذكريات",
|
||||
"tab.activities": "الأنشطة",
|
||||
"tab.contexts": "السياقات",
|
||||
"tab.experiences": "التجارب",
|
||||
|
||||
@@ -231,8 +231,6 @@
|
||||
"providerModels.item.modelConfig.extendParams.options.imageResolution.hint": "لنماذج توليد الصور من Gemini 3؛ يتحكم في دقة الصور المُولدة.",
|
||||
"providerModels.item.modelConfig.extendParams.options.imageResolution2.hint": "لـ نماذج الصور Gemini 3.1 Flash؛ يتحكم في دقة الصور المُنشأة (يدعم 512 بكسل).",
|
||||
"providerModels.item.modelConfig.extendParams.options.reasoningBudgetToken.hint": "لنماذج Claude وQwen3 وما شابهها؛ يتحكم في ميزانية الرموز المخصصة للاستدلال.",
|
||||
"providerModels.item.modelConfig.extendParams.options.reasoningBudgetToken32k.hint": "لـ GLM-5 و GLM-4.7؛ يتحكم في ميزانية الرموز للتفكير (الحد الأقصى 32k).",
|
||||
"providerModels.item.modelConfig.extendParams.options.reasoningBudgetToken80k.hint": "لسلسلة Qwen3؛ يتحكم في ميزانية الرموز للتفكير (الحد الأقصى 80k).",
|
||||
"providerModels.item.modelConfig.extendParams.options.reasoningEffort.hint": "لنماذج OpenAI وغيرها من النماذج القادرة على الاستدلال؛ يتحكم في جهد الاستدلال.",
|
||||
"providerModels.item.modelConfig.extendParams.options.textVerbosity.hint": "لسلسلة GPT-5+؛ يتحكم في تفصيل النص الناتج.",
|
||||
"providerModels.item.modelConfig.extendParams.options.thinking.hint": "لبعض نماذج Doubao؛ يسمح للنموذج بتحديد ما إذا كان يجب التفكير بعمق.",
|
||||
|
||||
+35
-43
@@ -53,14 +53,7 @@
|
||||
"FLUX.1-Kontext-dev.description": "FLUX.1-Kontext-dev هو نموذج توليد وتحرير صور متعدد الوسائط من Black Forest Labs، مبني على بنية Rectified Flow Transformer ويحتوي على 12 مليار معامل. يركز على توليد الصور، إعادة بنائها، تحسينها أو تحريرها ضمن شروط سياقية محددة. يجمع بين قدرات التوليد القابلة للتحكم لنماذج الانتشار ونمذجة السياق باستخدام Transformer، ويدعم مخرجات عالية الجودة لمهام مثل inpainting، outpainting، وإعادة بناء المشاهد البصرية.",
|
||||
"FLUX.1-Kontext-pro.description": "FLUX.1 Kontext [pro]",
|
||||
"FLUX.1-dev.description": "FLUX.1-dev هو نموذج لغة متعدد الوسائط مفتوح المصدر من Black Forest Labs، محسن لمهام النص والصورة، ويجمع بين فهم وتوليد النصوص/الصور. مبني على نماذج LLM متقدمة (مثل Mistral-7B)، ويستخدم مشفر رؤية مصمم بعناية وضبط تعليمات متعدد المراحل لتمكين التنسيق متعدد الوسائط والاستدلال المعقد.",
|
||||
"GLM-4.5-Air.description": "GLM-4.5-Air: إصدار خفيف الوزن للاستجابات السريعة.",
|
||||
"GLM-4.5.description": "GLM-4.5: نموذج عالي الأداء للمنطق، البرمجة، ومهام الوكلاء.",
|
||||
"GLM-4.6.description": "GLM-4.6: نموذج الجيل السابق.",
|
||||
"GLM-4.7.description": "GLM-4.7 هو النموذج الرائد الأحدث من Zhipu، معزز لسيناريوهات البرمجة الوكيلية مع تحسين قدرات البرمجة، تخطيط المهام طويلة الأمد، والتعاون مع الأدوات.",
|
||||
"GLM-5-Turbo.description": "GLM-5-Turbo: إصدار محسن من GLM-5 مع استدلال أسرع لمهام البرمجة.",
|
||||
"GLM-5.description": "GLM-5 هو نموذج الأساس الرائد من الجيل التالي لـ Zhipu، مصمم خصيصًا للهندسة الوكيلية. يوفر إنتاجية موثوقة في هندسة الأنظمة المعقدة ومهام الوكلاء طويلة الأمد. في قدرات البرمجة والوكلاء، يحقق GLM-5 أداءً رائدًا بين النماذج مفتوحة المصدر.",
|
||||
"Gryphe/MythoMax-L2-13b.description": "MythoMax-L2 (13B) هو نموذج مبتكر لمجالات متنوعة ومهام معقدة.",
|
||||
"HY-Image-V3.0.description": "قدرات قوية لاستخراج الميزات من الصور الأصلية والحفاظ على التفاصيل، مما يوفر نسيجًا بصريًا أكثر ثراءً وينتج صورًا عالية الدقة ومتقنة ومناسبة للإنتاج.",
|
||||
"HelloMeme.description": "HelloMeme هي أداة ذكاء اصطناعي لإنشاء الميمات، الصور المتحركة (GIFs)، أو مقاطع الفيديو القصيرة من الصور أو الحركات التي تقدمها. لا تتطلب مهارات رسم أو برمجة—فقط صورة مرجعية—لإنتاج محتوى ممتع وجذاب ومتناسق من حيث الأسلوب.",
|
||||
"HiDream-E1-Full.description": "HiDream-E1-Full هو نموذج مفتوح المصدر لتحرير الصور متعدد الوسائط من HiDream.ai، يعتمد على بنية Diffusion Transformer المتقدمة وفهم قوي للغة (مدمج LLaMA 3.1-8B-Instruct). يدعم إنشاء الصور باستخدام اللغة الطبيعية، ونقل الأنماط، والتحرير المحلي، وإعادة الطلاء، مع فهم وتنفيذ ممتازين للنصوص والصور.",
|
||||
"HiDream-I1-Full.description": "HiDream-I1 هو نموذج جديد مفتوح المصدر لإنشاء الصور تم إصداره من قبل HiDream. مع 17 مليار معلمة (Flux يحتوي على 12 مليار)، يمكنه تقديم جودة صور رائدة في الصناعة في ثوانٍ.",
|
||||
@@ -88,17 +81,17 @@
|
||||
"MiniMax-M1.description": "نموذج استدلال داخلي جديد بسلسلة تفكير تصل إلى 80K ومدخلات حتى 1M، يقدم أداءً مماثلاً لأفضل النماذج العالمية.",
|
||||
"MiniMax-M2-Stable.description": "مصمم لتدفقات العمل البرمجية والوكلاء بكفاءة عالية، مع قدرة تزامن أعلى للاستخدام التجاري.",
|
||||
"MiniMax-M2.1-Lightning.description": "قدرات برمجة متعددة اللغات قوية وتجربة برمجة مطورة بالكامل. أسرع وأكثر كفاءة.",
|
||||
"MiniMax-M2.1-highspeed.description": "قدرات برمجة متعددة اللغات قوية مع استدلال أسرع وأكثر كفاءة.",
|
||||
"MiniMax-M2.1-highspeed.description": "قدرات برمجة متعددة اللغات قوية مع استنتاج أسرع وأكثر كفاءة.",
|
||||
"MiniMax-M2.1.description": "MiniMax-M2.1 هو نموذج مفتوح المصدر رائد من MiniMax، يركز على حل المهام الواقعية المعقدة. يتميز بقدرات برمجة متعددة اللغات والقدرة على أداء المهام المعقدة كوكلاء ذكي.",
|
||||
"MiniMax-M2.5-Lightning.description": "M2.5 Lightning: نفس الأداء، أسرع وأكثر رشاقة (تقريباً 100 tps).",
|
||||
"MiniMax-M2.5-highspeed.description": "MiniMax M2.5 Highspeed: نفس أداء M2.5 مع استدلال أسرع.",
|
||||
"MiniMax-M2.5-highspeed.description": "نفس أداء M2.5 مع استنتاج أسرع بشكل ملحوظ.",
|
||||
"MiniMax-M2.5.description": "أداء من الدرجة الأولى وفعالية تكلفة قصوى، يتعامل بسهولة مع المهام المعقدة (تقريباً 60 tps).",
|
||||
"MiniMax-M2.7-highspeed.description": "MiniMax M2.7 Highspeed: نفس أداء M2.7 مع استدلال أسرع بشكل ملحوظ.",
|
||||
"MiniMax-M2.7.description": "MiniMax M2.7: بداية رحلة التحسين الذاتي التكراري، قدرات هندسية واقعية رائدة.",
|
||||
"MiniMax-M2.description": "MiniMax M2: نموذج الجيل السابق.",
|
||||
"MiniMax-M2.7-highspeed.description": "نفس أداء M2.7 مع استدلال أسرع بشكل ملحوظ (~100 tps).",
|
||||
"MiniMax-M2.7.description": "أول نموذج ذاتي التطور بأداء عالي المستوى في البرمجة والوكالة (~60 tps).",
|
||||
"MiniMax-M2.description": "مصمم خصيصًا للبرمجة الفعالة وتدفقات عمل الوكلاء.",
|
||||
"MiniMax-Text-01.description": "MiniMax-01 يقدم انتباهًا خطيًا واسع النطاق يتجاوز Transformers التقليدية، مع 456 مليار معامل و45.9 مليار مفعّلة في كل تمرير. يحقق أداءً من الدرجة الأولى ويدعم حتى 4 ملايين رمز سياقي (32× GPT-4o، 20× Claude-3.5-Sonnet).",
|
||||
"MiniMaxAI/MiniMax-M1-80k.description": "MiniMax-M1 هو نموذج استدلال كبير مفتوح الأوزان مع 456 مليار معلمة إجمالية وحوالي 45.9 مليار نشطة لكل رمز. يدعم سياق 1 مليون بشكل طبيعي ويستخدم Flash Attention لتقليل FLOPs بنسبة 75% على توليد 100 ألف رمز مقارنة بـ DeepSeek R1. مع بنية MoE بالإضافة إلى CISPO وتدريب RL الهجين، يحقق أداءً رائدًا في الاستدلال طويل المدخلات ومهام الهندسة البرمجية الواقعية.",
|
||||
"MiniMaxAI/MiniMax-M2.description": "MiniMax-M2 يعيد تعريف كفاءة الوكلاء. إنه نموذج MoE مضغوط وسريع وفعال من حيث التكلفة مع 230 مليار معلمة إجمالية و10 مليارات معلمة نشطة، مصمم لمهام البرمجة والوكلاء من الدرجة الأولى مع الحفاظ على ذكاء عام قوي. مع 10 مليارات معلمة نشطة فقط، ينافس النماذج الأكبر بكثير، مما يجعله مثاليًا للتطبيقات عالية الكفاءة.",
|
||||
"MiniMaxAI/MiniMax-M1-80k.description": "MiniMax-M1 هو نموذج استدلال واسع النطاق بوزن مفتوح يستخدم انتباهًا هجينًا، يحتوي على 456 مليار معامل إجماليًا و~45.9 مليار مفعّلة لكل رمز. يدعم سياقًا يصل إلى 1M ويستخدم Flash Attention لتقليل FLOPs بنسبة 75% عند توليد 100K رمز مقارنة بـ DeepSeek R1. بهيكل MoE وتدريب RL هجين، يحقق أداءً رائدًا في الاستدلال طويل المدخلات ومهام هندسة البرمجيات الواقعية.",
|
||||
"MiniMaxAI/MiniMax-M2.description": "MiniMax-M2 يعيد تعريف كفاءة الوكلاء. هو نموذج MoE مدمج وسريع وفعال من حيث التكلفة يحتوي على 230 مليار معامل إجماليًا و10 مليار مفعّلة، مصمم لمهام البرمجة والوكلاء من الدرجة الأولى مع الحفاظ على ذكاء عام قوي. مع 10 مليار معامل مفعّلة فقط، ينافس نماذج أكبر بكثير، مما يجعله مثاليًا للتطبيقات عالية الكفاءة.",
|
||||
"Moonshot-Kimi-K2-Instruct.description": "يحتوي على 1 تريليون معامل إجماليًا و32 مليار مفعّلة. من بين النماذج غير المفكرة، يتصدر في المعرفة المتقدمة، الرياضيات، والبرمجة، وأقوى في مهام الوكلاء العامة. محسن لأعباء عمل الوكلاء، يمكنه اتخاذ إجراءات وليس فقط الإجابة على الأسئلة. الأفضل للمحادثات العامة الارتجالية وتجارب الوكلاء كنموذج يعمل بردود فعل دون تفكير طويل.",
|
||||
"NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO.description": "Nous Hermes 2 - Mixtral 8x7B-DPO (46.7B) هو نموذج تعليمات عالي الدقة للحسابات المعقدة.",
|
||||
"OmniConsistency.description": "تحسّن OmniConsistency التناسق الأسلوبي والتعميم في مهام تحويل الصور إلى صور من خلال إدخال محولات الانتشار واسعة النطاق (DiTs) وبيانات مزدوجة النمط، مما يمنع تدهور الأسلوب.",
|
||||
@@ -112,14 +105,14 @@
|
||||
"Phi-3.5-mini-instruct.description": "إصدار محدث من نموذج Phi-3-mini.",
|
||||
"Phi-3.5-vision-instrust.description": "إصدار محدث من نموذج Phi-3-vision.",
|
||||
"Pro/MiniMaxAI/MiniMax-M2.1.description": "MiniMax-M2.1 هو نموذج لغوي مفتوح المصدر ومتقدم، مُحسَّن لقدرات الوكلاء، ويتفوق في البرمجة، واستخدام الأدوات، واتباع التعليمات، والتخطيط طويل الأمد. يدعم النموذج تطوير البرمجيات متعددة اللغات وتنفيذ سير العمل المعقد متعدد الخطوات، وحقق نتيجة 74.0 على SWE-bench Verified، متفوقًا على Claude Sonnet 4.5 في السيناريوهات متعددة اللغات.",
|
||||
"Pro/MiniMaxAI/MiniMax-M2.5.description": "MiniMax-M2.5 هو أحدث نموذج لغة كبير تم تطويره بواسطة MiniMax، تم تدريبه من خلال التعلم المعزز واسع النطاق عبر مئات الآلاف من البيئات الواقعية المعقدة. يتميز ببنية MoE مع 229 مليار معلمة، ويحقق أداءً رائدًا في الصناعة في مهام مثل البرمجة، استدعاء أدوات الوكلاء، البحث، وسيناريوهات المكتب.",
|
||||
"Pro/MiniMaxAI/MiniMax-M2.5.description": "MiniMax-M2.5 هو أحدث نموذج لغة كبير تم تطويره بواسطة MiniMax، تم تدريبه من خلال التعلم المعزز واسع النطاق عبر مئات الآلاف من البيئات المعقدة في العالم الحقيقي. يتميز بهيكل MoE مع 229 مليار معلمة، ويحقق أداءً رائدًا في الصناعة في مهام مثل البرمجة، واستدعاء أدوات الوكيل، والبحث، والسيناريوهات المكتبية.",
|
||||
"Pro/Qwen/Qwen2-7B-Instruct.description": "Qwen2-7B-Instruct هو نموذج لغوي كبير (LLM) موجه للتعليمات ضمن سلسلة Qwen2. يستخدم بنية Transformer مع SwiGLU، وانحياز QKV في الانتباه، وانتباه الاستعلامات المجمعة، ويعالج مدخلات كبيرة. يتميز بأداء قوي في فهم اللغة، التوليد، المهام متعددة اللغات، البرمجة، الرياضيات، والاستدلال، متفوقًا على معظم النماذج المفتوحة ومنافسًا للنماذج التجارية. يتفوق على Qwen1.5-7B-Chat في العديد من المعايير.",
|
||||
"Pro/Qwen/Qwen2.5-7B-Instruct.description": "Qwen2.5-7B-Instruct هو جزء من أحدث سلسلة نماذج لغوية كبيرة من Alibaba Cloud. يقدم هذا النموذج ذو 7 مليارات معلمة تحسينات ملحوظة في البرمجة والرياضيات، ويدعم أكثر من 29 لغة، ويعزز اتباع التعليمات، وفهم البيانات المنظمة، وإنتاج المخرجات المنظمة (خصوصًا JSON).",
|
||||
"Pro/Qwen/Qwen2.5-Coder-7B-Instruct.description": "Qwen2.5-Coder-7B-Instruct هو أحدث نموذج لغوي كبير من Alibaba Cloud يركز على البرمجة. مبني على Qwen2.5 ومدرب على 5.5 تريليون رمز، يعزز بشكل كبير توليد الشيفرة، الاستدلال، والإصلاح، مع الحفاظ على القوة في الرياضيات والقدرات العامة، مما يوفر أساسًا قويًا لوكلاء البرمجة.",
|
||||
"Pro/Qwen/Qwen2.5-VL-7B-Instruct.description": "Qwen2.5-VL هو نموذج رؤية-لغة جديد من Qwen يتمتع بفهم بصري قوي. يحلل النصوص، الرسوم البيانية، والتخطيطات في الصور، ويفهم مقاطع الفيديو الطويلة والأحداث، ويدعم الاستدلال واستخدام الأدوات، وتحديد الكائنات عبر تنسيقات متعددة، وإنتاج مخرجات منظمة. يعزز فهم الفيديو من خلال تحسينات في الدقة الديناميكية ومعدل الإطارات، ويزيد من كفاءة مشفر الرؤية.",
|
||||
"Pro/THUDM/GLM-4.1V-9B-Thinking.description": "GLM-4.1V-9B-Thinking هو نموذج رؤية-لغة مفتوح المصدر من Zhipu AI ومختبر KEG في جامعة تسينغهوا، مصمم للإدراك متعدد الوسائط المعقد. مبني على GLM-4-9B-0414، ويضيف استدلال سلسلة الأفكار والتعلم المعزز (RL) لتحسين الاستدلال عبر الوسائط والاستقرار بشكل كبير.",
|
||||
"Pro/THUDM/glm-4-9b-chat.description": "GLM-4-9B-Chat هو النموذج المفتوح المصدر من سلسلة GLM-4 من Zhipu AI. يتميز بأداء قوي في الدلالات، الرياضيات، الاستدلال، البرمجة، والمعرفة. بالإضافة إلى المحادثة متعددة الأدوار، يدعم تصفح الويب، تنفيذ الشيفرة، استدعاء الأدوات المخصصة، والاستدلال على النصوص الطويلة. يدعم 26 لغة (بما في ذلك الصينية، الإنجليزية، اليابانية، الكورية، والألمانية). يحقق نتائج جيدة في AlignBench-v2، MT-Bench، MMLU، وC-Eval، ويدعم سياقًا يصل إلى 128 ألف رمز للاستخدام الأكاديمي والتجاري.",
|
||||
"Pro/deepseek-ai/DeepSeek-R1-Distill-Qwen-7B.description": "DeepSeek-R1-Distill-Qwen-7B مستخلص من Qwen2.5-Math-7B ومُحسن على 800 ألف عينة مختارة من DeepSeek-R1. يقدم أداءً قويًا، بنسبة 92.8% على MATH-500، و55.5% على AIME 2024، وتصنيف 1189 على CodeForces لنموذج 7B.",
|
||||
"Pro/deepseek-ai/DeepSeek-R1-Distill-Qwen-7B.description": "تم تقطير DeepSeek-R1-Distill-Qwen-7B من Qwen2.5-Math-7B وتم تحسينه باستخدام 800 ألف عينة مختارة من DeepSeek-R1. يتميز بأداء قوي، حيث يحقق 92.8٪ في MATH-500، و55.5٪ في AIME 2024، وتصنيف 1189 في CodeForces لنموذج بحجم 7 مليارات معلمة.",
|
||||
"Pro/deepseek-ai/DeepSeek-R1.description": "DeepSeek-R1 هو نموذج استدلال مدفوع بالتعلم المعزز يقلل التكرار ويحسن قابلية القراءة. يستخدم بيانات بداية باردة قبل التعلم المعزز لتعزيز الاستدلال، ويضاهي OpenAI-o1 في مهام الرياضيات، البرمجة، والاستدلال، ويحقق نتائج أفضل من خلال تدريب دقيق.",
|
||||
"Pro/deepseek-ai/DeepSeek-V3.1-Terminus.description": "DeepSeek-V3.1-Terminus هو إصدار محدث من نموذج V3.1، مصمم كنموذج وكيل هجين. يعالج المشكلات التي أبلغ عنها المستخدمون، ويحسن الاستقرار، وتناسق اللغة، ويقلل من الخلط بين الصينية/الإنجليزية والرموز غير الطبيعية. يدمج أوضاع التفكير وغير التفكير مع قوالب محادثة للتبديل المرن. كما يعزز أداء وكلاء الشيفرة والبحث لاستخدام أدوات أكثر موثوقية ومهام متعددة الخطوات.",
|
||||
"Pro/deepseek-ai/DeepSeek-V3.2.description": "DeepSeek-V3.2 هو نموذج يجمع بين الكفاءة الحسابية العالية وأداء التفكير والوكيل الممتاز. يعتمد نهجه على ثلاثة اختراقات تكنولوجية رئيسية: DeepSeek Sparse Attention (DSA)، وهي آلية انتباه فعالة تقلل بشكل كبير من التعقيد الحسابي مع الحفاظ على أداء النموذج، ومُحسنة خصيصًا للسيناريوهات ذات السياق الطويل؛ إطار عمل للتعلم المعزز القابل للتوسع يمكن من خلاله أن ينافس أداء النموذج GPT-5، مع نسخته عالية الحوسبة التي تضاهي Gemini-3.0-Pro في قدرات التفكير؛ وخط أنابيب واسع النطاق لتوليف مهام الوكيل يهدف إلى دمج قدرات التفكير في سيناريوهات استخدام الأدوات، مما يحسن اتباع التعليمات والتعميم في البيئات التفاعلية المعقدة. حقق النموذج أداءً متميزًا في الأولمبياد الدولي للرياضيات (IMO) وأولمبياد المعلوماتية الدولي (IOI) لعام 2025.",
|
||||
@@ -127,10 +120,10 @@
|
||||
"Pro/moonshotai/Kimi-K2-Instruct-0905.description": "Kimi K2-Instruct-0905 هو أحدث وأقوى إصدار من Kimi K2. إنه نموذج MoE من الدرجة الأولى يحتوي على إجمالي 1 تريليون و32 مليار معلمة نشطة. من أبرز ميزاته الذكاء البرمجي القوي مع تحسينات كبيرة في المعايير ومهام الوكلاء الواقعية، بالإضافة إلى تحسينات في جمالية واجهة الشيفرة وسهولة الاستخدام.",
|
||||
"Pro/moonshotai/Kimi-K2-Thinking.description": "Kimi K2 Thinking Turbo هو إصدار Turbo محسّن لسرعة الاستدلال والإنتاجية مع الحفاظ على قدرات التفكير متعدد الخطوات واستخدام الأدوات في K2 Thinking. إنه نموذج MoE يحتوي على حوالي 1 تريليون معلمة إجمالية، ويدعم سياقًا أصليًا بطول 256 ألف رمز، واستدعاء أدوات واسع النطاق ومستقر لسيناريوهات الإنتاج التي تتطلب زمن استجابة وتزامنًا صارمين.",
|
||||
"Pro/moonshotai/Kimi-K2.5.description": "Kimi K2.5 هو نموذج وكيل متعدد الوسائط مفتوح المصدر، مبني على Kimi-K2-Base، ومدرب على حوالي 1.5 تريليون رمز من النصوص والرؤية. يستخدم بنية MoE بعدد إجمالي 1 تريليون مع 32 مليار معلمات نشطة، ويدعم نافذة سياق تصل إلى 256 ألف، مما يدمج الفهم البصري واللغوي بسلاسة.",
|
||||
"Pro/zai-org/glm-4.7.description": "GLM-4.7 هو النموذج الرائد الجديد من Zhipu مع 355 مليار معلمة إجمالية و32 مليار معلمة نشطة، تم ترقيته بالكامل في الحوار العام، المنطق، وقدرات الوكلاء. يعزز GLM-4.7 التفكير المتداخل ويقدم التفكير المحفوظ والتفكير على مستوى الدور.",
|
||||
"Pro/zai-org/glm-4.7.description": "GLM-4.7 هو النموذج الرائد من الجيل الجديد لشركة Zhipu، يحتوي على 355 مليار معلمة إجمالية و32 مليار معلمة نشطة، وقد تم تطويره بالكامل في مجالات الحوار العام، والاستدلال، وقدرات الوكلاء. يعزز GLM-4.7 التفكير المتداخل ويقدم مفاهيم التفكير المحفوظ والتفكير على مستوى الدور.",
|
||||
"Pro/zai-org/glm-5.description": "GLM-5 هو نموذج اللغة الكبير من الجيل التالي من Zhipu، يركز على هندسة الأنظمة المعقدة ومهام الوكيل طويلة المدة. تم توسيع معلمات النموذج إلى 744 مليار (40 مليار نشطة) وتدمج DeepSeek Sparse Attention.",
|
||||
"QwQ-32B-Preview.description": "Qwen QwQ هو نموذج بحث تجريبي يركز على تحسين الاستدلال.",
|
||||
"Qwen/QVQ-72B-Preview.description": "QVQ-72B-Preview هو نموذج بحثي من Qwen يركز على الاستدلال البصري، مع قوة في فهم المشاهد المعقدة ومسائل الرياضيات البصرية.",
|
||||
"Qwen/QVQ-72B-Preview.description": "QVQ-72B-Preview هو نموذج بحث من Qwen يركز على الاستدلال البصري، يتميز بفهم المشاهد المعقدة وحل مسائل الرياضيات البصرية.",
|
||||
"Qwen/QwQ-32B-Preview.description": "Qwen QwQ هو نموذج بحث تجريبي يركز على تحسين استدلال الذكاء الاصطناعي.",
|
||||
"Qwen/QwQ-32B.description": "QwQ هو نموذج استدلال ضمن عائلة Qwen. مقارنة بالنماذج التقليدية الموجهة للتعليمات، يضيف QwQ قدرات تفكير واستدلال تعزز الأداء بشكل كبير في المهام الصعبة. QwQ-32B هو نموذج استدلال متوسط الحجم ينافس نماذج استدلال رائدة مثل DeepSeek-R1 وo1-mini. يستخدم RoPE، SwiGLU، RMSNorm، وانحياز QKV في الانتباه، مع 64 طبقة و40 رأس انتباه (8 KV في GQA).",
|
||||
"Qwen/Qwen-Image-Edit-2509.description": "Qwen-Image-Edit-2509 هو أحدث إصدار لتحرير الصور من فريق Qwen. مبني على نموذج Qwen-Image بحجم 20 مليار معلمة، ويمتد من قدرات عرض النصوص القوية إلى تحرير الصور بدقة. يستخدم بنية تحكم مزدوجة، حيث تُرسل المدخلات إلى Qwen2.5-VL للتحكم الدلالي وإلى مشفر VAE للتحكم في المظهر، مما يتيح تحريرًا على مستوى الدلالة والمظهر. يدعم التعديلات المحلية (إضافة/إزالة/تعديل) والتعديلات الدلالية المتقدمة مثل إنشاء الملكية الفكرية ونقل الأسلوب مع الحفاظ على المعنى. يحقق نتائج رائدة في العديد من المعايير.",
|
||||
@@ -214,11 +207,11 @@
|
||||
"Skylark2-pro-turbo-8k.description": "الجيل الثاني من نموذج Skylark. يوفر Skylark2-pro-turbo-8k استدلالًا أسرع بتكلفة أقل مع نافذة سياق تصل إلى 8 آلاف رمز.",
|
||||
"THUDM/GLM-4-32B-0414.description": "GLM-4-32B-0414 هو نموذج GLM من الجيل التالي يحتوي على 32 مليار معامل، ويقارن في الأداء مع نماذج OpenAI GPT وسلسلة DeepSeek V3/R1.",
|
||||
"THUDM/GLM-4-9B-0414.description": "GLM-4-9B-0414 هو نموذج GLM يحتوي على 9 مليارات معامل، ويعتمد على تقنيات GLM-4-32B مع إمكانية نشر أخف. يتميز في توليد الشيفرات، وتصميم الويب، وتوليد SVG، والكتابة المعتمدة على البحث.",
|
||||
"THUDM/GLM-4.1V-9B-Thinking.description": "GLM-4.1V-9B-Thinking هو نموذج مفتوح المصدر من Zhipu AI ومختبر Tsinghua KEG، مصمم للإدراك متعدد الوسائط المعقد. يعتمد على GLM-4-9B-0414، ويضيف التفكير المتسلسل والتعلم المعزز لتحسين الاستدلال عبر الوسائط والثبات بشكل كبير.",
|
||||
"THUDM/GLM-4.1V-9B-Thinking.description": "GLM-4.1V-9B-Thinking هو نموذج رؤية-لغة مفتوح المصدر من Zhipu AI ومختبر KEG بجامعة تسينغهوا، مصمم للإدراك المعقد متعدد الوسائط. يعتمد على GLM-4-9B-0414 ويضيف سلسلة التفكير والتعلم المعزز لتحسين الاستدلال عبر الوسائط والاستقرار بشكل كبير.",
|
||||
"THUDM/GLM-Z1-32B-0414.description": "GLM-Z1-32B-0414 هو نموذج استدلال عميق مبني على GLM-4-32B-0414 باستخدام بيانات بدء باردة وتوسيع التعلم المعزز، وتم تدريبه بشكل إضافي على الرياضيات والبرمجة والمنطق. يُظهر تحسنًا كبيرًا في القدرة على حل المسائل الرياضية والمهام المعقدة مقارنة بالنموذج الأساسي.",
|
||||
"THUDM/GLM-Z1-9B-0414.description": "GLM-Z1-9B-0414 هو نموذج GLM صغير يحتوي على 9 مليارات معامل، يحتفظ بقوة المصدر المفتوح ويقدم أداءً مميزًا. يتميز في الاستدلال الرياضي والمهام العامة، ويتفوق على النماذج المفتوحة من نفس الفئة الحجمية.",
|
||||
"THUDM/glm-4-9b-chat.description": "GLM-4-9B-Chat هو النموذج مفتوح المصدر من Zhipu AI ضمن سلسلة GLM-4. يتميز بقوة في الفهم الدلالي، والرياضيات، والاستدلال، والبرمجة، والمعرفة. بالإضافة إلى الدردشة متعددة الأدوار، يدعم تصفح الويب، وتنفيذ الشيفرات، واستدعاء الأدوات المخصصة، والاستدلال على النصوص الطويلة. يدعم 26 لغة (بما في ذلك الصينية، والإنجليزية، واليابانية، والكورية، والألمانية). يحقق أداءً جيدًا في AlignBench-v2 وMT-Bench وMMLU وC-Eval، ويدعم نافذة سياق تصل إلى 128 ألف رمز للاستخدام الأكاديمي والتجاري.",
|
||||
"Tongyi-Zhiwen/QwenLong-L1-32B.description": "QwenLong-L1-32B هو أول نموذج استدلال طويل السياق (LRM) تم تدريبه باستخدام التعلم المعزز، مُحسن للاستدلال النصي الطويل. يتيح التوسع التدريجي للسياق عبر التعلم المعزز انتقالًا مستقرًا من السياق القصير إلى الطويل. يتفوق على OpenAI-o3-mini وQwen3-235B-A22B في سبعة معايير استدلال وثائق طويلة السياق، منافسًا Claude-3.7-Sonnet-Thinking. يتميز بقوة خاصة في الرياضيات، المنطق، والاستدلال متعدد الخطوات.",
|
||||
"Tongyi-Zhiwen/QwenLong-L1-32B.description": "QwenLong-L1-32B هو أول نموذج استدلال طويل السياق (LRM) تم تدريبه باستخدام التعلم المعزز، ومُحسَّن للاستدلال على النصوص الطويلة. تتيح استراتيجية التوسيع التدريجي للسياق انتقالًا مستقرًا من السياقات القصيرة إلى الطويلة. يتفوق على OpenAI-o3-mini وQwen3-235B-A22B في سبعة اختبارات استدلال على مستندات طويلة، ويضاهي Claude-3.7-Sonnet-Thinking. يتميز بقوة خاصة في الرياضيات والمنطق والاستدلال متعدد الخطوات.",
|
||||
"Yi-34B-Chat.description": "Yi-1.5-34B يحتفظ بقدرات اللغة العامة القوية للسلسلة، ويستخدم تدريبًا تدريجيًا على 500 مليار رمز عالي الجودة لتحسين كبير في المنطق الرياضي والبرمجة.",
|
||||
"abab5.5-chat.description": "مصمم لسيناريوهات الإنتاجية، مع قدرة على التعامل مع المهام المعقدة وتوليد نصوص فعالة للاستخدام المهني.",
|
||||
"abab5.5s-chat.description": "مصمم للدردشة بشخصيات صينية، ويقدم حوارات صينية عالية الجودة لمجموعة متنوعة من التطبيقات.",
|
||||
@@ -310,16 +303,16 @@
|
||||
"claude-3.5-sonnet.description": "يتميز Claude 3.5 Sonnet بقدرات عالية في البرمجة والكتابة والتفكير المعقد.",
|
||||
"claude-3.7-sonnet-thought.description": "Claude 3.7 Sonnet مزود بقدرات تفكير موسعة للمهام التي تتطلب استدلالًا معقدًا.",
|
||||
"claude-3.7-sonnet.description": "Claude 3.7 Sonnet هو إصدار مطور يتمتع بسياق موسع وقدرات محسّنة.",
|
||||
"claude-haiku-4-5-20251001.description": "Claude Haiku 4.5 هو النموذج الأسرع والأكثر ذكاءً من Anthropic، مع سرعة فائقة وتفكير ممتد.",
|
||||
"claude-haiku-4-5-20251001.description": "Claude Haiku 4.5 هو أسرع وأذكى نموذج Haiku من Anthropic، يتميز بسرعة البرق والتفكير الممتد.",
|
||||
"claude-haiku-4.5.description": "Claude Haiku 4.5 نموذج سريع وفعّال لمجموعة متنوعة من المهام.",
|
||||
"claude-opus-4-1-20250805-thinking.description": "Claude Opus 4.1 Thinking هو إصدار متقدم يمكنه عرض عملية تفكيره.",
|
||||
"claude-opus-4-1-20250805.description": "Claude Opus 4.1 هو أحدث وأقوى نموذج من Anthropic للمهام المعقدة للغاية، يتميز بالأداء، الذكاء، الطلاقة، والفهم.",
|
||||
"claude-opus-4-20250514.description": "Claude Opus 4 هو النموذج الأكثر قوة من Anthropic للمهام المعقدة للغاية، يتميز بالأداء، الذكاء، الطلاقة، والفهم.",
|
||||
"claude-opus-4-1-20250805.description": "Claude Opus 4.1 هو أحدث وأقوى نموذج من Anthropic للمهام المعقدة للغاية، يتميز بالأداء والذكاء والطلاقة والفهم.",
|
||||
"claude-opus-4-20250514.description": "Claude Opus 4 هو أقوى نموذج من Anthropic للمهام المعقدة للغاية، يتميز بالأداء والذكاء والطلاقة والفهم.",
|
||||
"claude-opus-4-5-20251101.description": "Claude Opus 4.5 هو النموذج الرائد من Anthropic، يجمع بين الذكاء الاستثنائي والأداء القابل للتوسع، مثالي للمهام المعقدة التي تتطلب استجابات عالية الجودة وتفكير متقدم.",
|
||||
"claude-opus-4-6.description": "Claude Opus 4.6 هو النموذج الأكثر ذكاءً من Anthropic لبناء الوكلاء والبرمجة.",
|
||||
"claude-opus-4-6.description": "Claude Opus 4.6 هو أذكى نموذج من Anthropic لبناء الوكلاء والبرمجة.",
|
||||
"claude-sonnet-4-20250514-thinking.description": "Claude Sonnet 4 Thinking يمكنه تقديم استجابات شبه فورية أو تفكير متسلسل مرئي.",
|
||||
"claude-sonnet-4-20250514.description": "Claude Sonnet 4 هو النموذج الأكثر ذكاءً من Anthropic حتى الآن، يقدم استجابات شبه فورية أو تفكيرًا ممتدًا خطوة بخطوة مع تحكم دقيق لمستخدمي API.",
|
||||
"claude-sonnet-4-5-20250929.description": "Claude Sonnet 4.5 هو النموذج الأكثر ذكاءً من Anthropic حتى الآن.",
|
||||
"claude-sonnet-4-20250514.description": "Claude Sonnet 4 هو أذكى نموذج من Anthropic حتى الآن، يقدم استجابات شبه فورية أو تفكير ممتد خطوة بخطوة مع تحكم دقيق لمستخدمي API.",
|
||||
"claude-sonnet-4-5-20250929.description": "Claude Sonnet 4.5 هو أذكى نموذج من Anthropic حتى الآن.",
|
||||
"claude-sonnet-4-6.description": "Claude Sonnet 4.6 هو أفضل مزيج من السرعة والذكاء من Anthropic.",
|
||||
"claude-sonnet-4.description": "Claude Sonnet 4 هو الجيل الأحدث مع أداء محسّن في جميع المهام.",
|
||||
"codegeex-4.description": "CodeGeeX-4 هو مساعد برمجة ذكي يدعم الأسئلة والأجوبة متعددة اللغات وإكمال الشيفرة لزيادة إنتاجية المطورين.",
|
||||
@@ -377,7 +370,7 @@
|
||||
"deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B.description": "تستخدم نماذج DeepSeek-R1 المستخلصة التعلم المعزز وبيانات البداية الباردة لتحسين التفكير وتحديد معايير جديدة للنماذج المفتوحة متعددة المهام.",
|
||||
"deepseek-ai/DeepSeek-R1-Distill-Qwen-14B.description": "تستخدم نماذج DeepSeek-R1 المستخلصة التعلم المعزز وبيانات البداية الباردة لتحسين التفكير وتحديد معايير جديدة للنماذج المفتوحة متعددة المهام.",
|
||||
"deepseek-ai/DeepSeek-R1-Distill-Qwen-32B.description": "تم استخلاص DeepSeek-R1-Distill-Qwen-32B من Qwen2.5-32B وتم تحسينه باستخدام 800 ألف عينة مختارة من DeepSeek-R1. يتميز في الرياضيات، والبرمجة، والتفكير، ويحقق نتائج قوية في AIME 2024، وMATH-500 (بدقة 94.3٪)، وGPQA Diamond.",
|
||||
"deepseek-ai/DeepSeek-R1-Distill-Qwen-7B.description": "DeepSeek-R1-Distill-Qwen-7B مستخلص من Qwen2.5-Math-7B ومُحسن على 800 ألف عينة مختارة من DeepSeek-R1. يقدم أداءً قويًا، بنسبة 92.8% على MATH-500، و55.5% على AIME 2024، وتصنيف 1189 على CodeForces لنموذج 7B.",
|
||||
"deepseek-ai/DeepSeek-R1-Distill-Qwen-7B.description": "تم استخلاص DeepSeek-R1-Distill-Qwen-7B من Qwen2.5-Math-7B وتم تحسينه باستخدام 800 ألف عينة مختارة من DeepSeek-R1. يحقق أداءً قويًا بنسبة 92.8٪ في MATH-500، و55.5٪ في AIME 2024، وتصنيف 1189 في CodeForces لنموذج بحجم 7B.",
|
||||
"deepseek-ai/DeepSeek-R1.description": "يعزز DeepSeek-R1 قدرات التفكير باستخدام التعلم المعزز وبيانات البداية الباردة، ويحدد معايير جديدة للنماذج المفتوحة متعددة المهام متفوقًا على OpenAI-o1-mini.",
|
||||
"deepseek-ai/DeepSeek-V2.5.description": "يعمل DeepSeek-V2.5 على ترقية DeepSeek-V2-Chat وDeepSeek-Coder-V2-Instruct، ويمزج بين القدرات العامة والبرمجية. يحسن الكتابة واتباع التعليمات لمواءمة التفضيلات بشكل أفضل، ويظهر تحسنًا ملحوظًا في AlpacaEval 2.0 وArenaHard وAlignBench وMT-Bench.",
|
||||
"deepseek-ai/DeepSeek-V3.1-Terminus.description": "DeepSeek-V3.1-Terminus هو إصدار محدث من V3.1 كنموذج وكيل هجين. يعالج المشكلات التي أبلغ عنها المستخدمون ويحسن الاستقرار واتساق اللغة ويقلل من الخلط بين الصينية/الإنجليزية والرموز غير الطبيعية. يدمج أوضاع التفكير وغير التفكير مع قوالب المحادثة للتبديل المرن. كما يعزز أداء وكلاء الكود والبحث لاستخدام الأدوات بشكل أكثر موثوقية وتنفيذ المهام متعددة الخطوات.",
|
||||
@@ -390,7 +383,7 @@
|
||||
"deepseek-ai/deepseek-v3.1.description": "DeepSeek V3.1 هو نموذج تفكير من الجيل التالي يتمتع بقدرات أقوى في التفكير المعقد وسلسلة التفكير لمهام التحليل العميق.",
|
||||
"deepseek-ai/deepseek-v3.2.description": "DeepSeek V3.2 هو نموذج استدلال من الجيل التالي يتميز بقدرات استدلال معقدة وسلسلة التفكير.",
|
||||
"deepseek-ai/deepseek-vl2.description": "DeepSeek-VL2 هو نموذج رؤية-لغة MoE يعتمد على DeepSeekMoE-27B مع تنشيط متفرق، ويحقق أداءً قويًا باستخدام 4.5 مليار معلمة نشطة فقط. يتميز في الأسئلة البصرية، وOCR، وفهم المستندات/الجداول/المخططات، والتأريض البصري.",
|
||||
"deepseek-chat.description": "DeepSeek V3.2 يوازن بين التفكير وطول المخرجات لمهام الأسئلة اليومية والوكلاء. تصل المعايير العامة إلى مستويات GPT-5، وهو الأول الذي يدمج التفكير في استخدام الأدوات، مما يؤدي إلى تقييمات الوكلاء مفتوحة المصدر.",
|
||||
"deepseek-chat.description": "DeepSeek V3.2 يوازن بين التفكير وطول المخرجات لمهام الأسئلة اليومية ووكلاء المهام. تصل المعايير العامة إلى مستويات GPT-5، وهو الأول في دمج التفكير في استخدام الأدوات، مما يؤدي إلى تقييمات وكلاء مفتوحة المصدر.",
|
||||
"deepseek-coder-33B-instruct.description": "DeepSeek Coder 33B هو نموذج لغة برمجية تم تدريبه على 2 تريليون رمز (87٪ كود، 13٪ نص صيني/إنجليزي). يقدم نافذة سياق 16K ومهام الإكمال في المنتصف، ويوفر إكمال كود على مستوى المشاريع وملء مقاطع الكود.",
|
||||
"deepseek-coder-v2.description": "DeepSeek Coder V2 هو نموذج كود MoE مفتوح المصدر يتميز بأداء قوي في مهام البرمجة، ويضاهي GPT-4 Turbo.",
|
||||
"deepseek-coder-v2:236b.description": "DeepSeek Coder V2 هو نموذج كود MoE مفتوح المصدر يتميز بأداء قوي في مهام البرمجة، ويضاهي GPT-4 Turbo.",
|
||||
@@ -413,7 +406,7 @@
|
||||
"deepseek-r1-fast-online.description": "الإصدار الكامل السريع من DeepSeek R1 مع بحث ويب في الوقت الحقيقي، يجمع بين قدرات بحجم 671B واستجابة أسرع.",
|
||||
"deepseek-r1-online.description": "الإصدار الكامل من DeepSeek R1 مع 671 مليار معلمة وبحث ويب في الوقت الحقيقي، يوفر فهمًا وتوليدًا أقوى.",
|
||||
"deepseek-r1.description": "يستخدم DeepSeek-R1 بيانات البداية الباردة قبل التعلم المعزز ويؤدي أداءً مماثلًا لـ OpenAI-o1 في الرياضيات، والبرمجة، والتفكير.",
|
||||
"deepseek-reasoner.description": "DeepSeek V3.2 Thinking هو نموذج استدلال عميق يولد سلسلة من الأفكار قبل المخرجات لتحقيق دقة أعلى، مع نتائج تنافسية رائدة واستدلال قابل للمقارنة مع Gemini-3.0-Pro.",
|
||||
"deepseek-reasoner.description": "DeepSeek V3.2 Thinking هو نموذج تفكير عميق يولد سلسلة من الأفكار قبل المخرجات لتحقيق دقة أعلى، مع نتائج تنافسية عالية وتفكير مشابه لـ Gemini-3.0-Pro.",
|
||||
"deepseek-v2.description": "DeepSeek V2 هو نموذج MoE فعال لمعالجة منخفضة التكلفة.",
|
||||
"deepseek-v2:236b.description": "DeepSeek V2 236B هو نموذج DeepSeek الموجه للبرمجة مع قدرات قوية في توليد الكود.",
|
||||
"deepseek-v3-0324.description": "DeepSeek-V3-0324 هو نموذج MoE يحتوي على 671 مليار معلمة يتميز بقوة في البرمجة، والقدرات التقنية، وفهم السياق، والتعامل مع النصوص الطويلة.",
|
||||
@@ -424,7 +417,7 @@
|
||||
"deepseek-v3.2-exp.description": "deepseek-v3.2-exp يقدم انتباهاً متفرقاً لتحسين كفاءة التدريب والاستدلال على النصوص الطويلة، بسعر أقل من deepseek-v3.1.",
|
||||
"deepseek-v3.2-speciale.description": "في المهام شديدة التعقيد، يتفوق نموذج Speciale بشكل كبير على النسخة القياسية، ولكنه يستهلك عددًا كبيرًا من الرموز ويتكبد تكاليف أعلى. حاليًا، يتم استخدام DeepSeek-V3.2-Speciale للأبحاث فقط، ولا يدعم استدعاء الأدوات، ولم يتم تحسينه بشكل خاص للمحادثات اليومية أو مهام الكتابة.",
|
||||
"deepseek-v3.2-think.description": "DeepSeek V3.2 Think هو نموذج تفكير عميق كامل يتميز باستدلال طويل السلسلة أقوى.",
|
||||
"deepseek-v3.2.description": "DeepSeek-V3.2 هو أحدث نموذج برمجة من DeepSeek مع قدرات استدلال قوية.",
|
||||
"deepseek-v3.2.description": "DeepSeek-V3.2 هو أول نموذج استدلال هجين من DeepSeek يدمج التفكير في استخدام الأدوات. يستخدم بنية فعالة لتقليل الحسابات، وتعلم تقوية واسع النطاق لتعزيز القدرات، وبيانات مهام تركيبية ضخمة لتعزيز التعميم. يجمع بين هذه العناصر الثلاثة لتحقيق أداء مماثل لـ GPT-5-High، مع تقليل كبير في طول المخرجات، مما يقلل من عبء الحوسبة وأوقات انتظار المستخدمين.",
|
||||
"deepseek-v3.description": "DeepSeek-V3 هو نموذج MoE قوي بإجمالي 671 مليار معلمة و37 مليار معلمة نشطة لكل رمز.",
|
||||
"deepseek-vl2-small.description": "DeepSeek VL2 Small هو إصدار متعدد الوسائط خفيف الوزن للاستخدام في البيئات ذات الموارد المحدودة أو التزامن العالي.",
|
||||
"deepseek-vl2.description": "DeepSeek VL2 هو نموذج متعدد الوسائط لفهم النصوص والصور والإجابة البصرية الدقيقة.",
|
||||
@@ -513,8 +506,8 @@
|
||||
"ernie-x1-turbo-32k.description": "ERNIE X1 Turbo 32K هو نموذج تفكير سريع بسياق 32K للاستدلال المعقد والدردشة متعددة الأدوار.",
|
||||
"ernie-x1.1-preview.description": "معاينة ERNIE X1.1 هو نموذج تفكير مخصص للتقييم والاختبار.",
|
||||
"ernie-x1.1.description": "ERNIE X1.1 هو نموذج تفكير تجريبي للتقييم والاختبار.",
|
||||
"fal-ai/bytedance/seedream/v4.5.description": "Seedream 4.5، تم تطويره بواسطة فريق ByteDance Seed، يدعم تحرير وتكوين الصور المتعددة. يتميز باتساق الموضوع المعزز، اتباع التعليمات بدقة، فهم المنطق المكاني، التعبير الجمالي، تخطيط الملصقات وتصميم الشعارات مع تقديم نصوص وصور عالية الدقة.",
|
||||
"fal-ai/bytedance/seedream/v4.description": "Seedream 4.0، تم تطويره بواسطة ByteDance Seed، يدعم إدخال النصوص والصور لتوليد صور عالية الجودة وقابلة للتحكم من المطالبات.",
|
||||
"fal-ai/bytedance/seedream/v4.5.description": "Seedream 4.5، الذي تم تطويره بواسطة فريق ByteDance Seed، يدعم تحرير الصور المتعددة والتكوين. يتميز بتناسق الموضوع المحسن، اتباع التعليمات بدقة، فهم المنطق المكاني، التعبير الجمالي، تصميم الملصقات والشعارات مع تقديم نصوص وصور عالية الدقة.",
|
||||
"fal-ai/bytedance/seedream/v4.description": "Seedream 4.0، الذي تم تطويره بواسطة ByteDance Seed، يدعم إدخال النصوص والصور لإنشاء صور عالية الجودة وقابلة للتحكم بناءً على التعليمات.",
|
||||
"fal-ai/flux-kontext/dev.description": "نموذج FLUX.1 يركز على تحرير الصور، ويدعم إدخال النصوص والصور.",
|
||||
"fal-ai/flux-pro/kontext.description": "FLUX.1 Kontext [pro] يقبل النصوص وصور مرجعية كمدخلات، مما يتيح تعديلات محلية مستهدفة وتحولات معقدة في المشهد العام.",
|
||||
"fal-ai/flux/krea.description": "Flux Krea [dev] هو نموذج لتوليد الصور يتميز بميول جمالية نحو صور أكثر واقعية وطبيعية.",
|
||||
@@ -522,8 +515,8 @@
|
||||
"fal-ai/hunyuan-image/v3.description": "نموذج قوي لتوليد الصور متعدد الوسائط أصلي.",
|
||||
"fal-ai/imagen4/preview.description": "نموذج عالي الجودة لتوليد الصور من Google.",
|
||||
"fal-ai/nano-banana.description": "Nano Banana هو أحدث وأسرع وأكثر نماذج Google كفاءةً لتوليد وتحرير الصور من خلال المحادثة.",
|
||||
"fal-ai/qwen-image-edit.description": "نموذج تحرير الصور الاحترافي من فريق Qwen، يدعم التعديلات الدلالية والمظهرية، تحرير النصوص الدقيقة باللغتين الصينية والإنجليزية، نقل الأنماط، التدوير، والمزيد.",
|
||||
"fal-ai/qwen-image.description": "نموذج توليد الصور القوي من فريق Qwen مع تقديم نصوص صينية قوية وأنماط بصرية متنوعة.",
|
||||
"fal-ai/qwen-image-edit.description": "نموذج تحرير الصور الاحترافي من فريق Qwen، يدعم التعديلات الدلالية والمظهرية، تحرير النصوص الدقيقة باللغتين الصينية والإنجليزية، نقل الأسلوب، الدوران، والمزيد.",
|
||||
"fal-ai/qwen-image.description": "نموذج قوي لإنشاء الصور من فريق Qwen يتميز بتقديم نصوص صينية قوية وأنماط بصرية متنوعة.",
|
||||
"flux-1-schnell.description": "نموذج تحويل النص إلى صورة يحتوي على 12 مليار معلمة من Black Forest Labs يستخدم تقنيات تقطير الانتشار العدائي الكامن لتوليد صور عالية الجودة في 1-4 خطوات. ينافس البدائل المغلقة ومتاح بموجب ترخيص Apache-2.0 للاستخدام الشخصي والبحثي والتجاري.",
|
||||
"flux-dev.description": "FLUX.1 [dev] هو نموذج مفتوح الأوزان ومقطر للاستخدام غير التجاري. يحافظ على جودة صور قريبة من المستوى الاحترافي واتباع التعليمات مع كفاءة تشغيل أعلى مقارنة بالنماذج القياسية من نفس الحجم.",
|
||||
"flux-kontext-max.description": "توليد وتحرير صور سياقية متقدمة، تجمع بين النصوص والصور لتحقيق نتائج دقيقة ومتسقة.",
|
||||
@@ -567,10 +560,10 @@
|
||||
"gemini-2.5-pro.description": "Gemini 2.5 Pro هو النموذج الرائد من Google في مجال الاستدلال، يدعم السياق الطويل للمهام المعقدة.",
|
||||
"gemini-3-flash-preview.description": "Gemini 3 Flash هو أذكى نموذج تم تصميمه للسرعة، يجمع بين الذكاء المتقدم وأساس بحث ممتاز.",
|
||||
"gemini-3-pro-image-preview.description": "Gemini 3 Pro Image (Nano Banana Pro) هو نموذج توليد الصور من Google ويدعم المحادثة متعددة الوسائط.",
|
||||
"gemini-3-pro-image-preview:image.description": "Gemini 3 Pro Image (Nano Banana Pro) هو نموذج توليد الصور من Google ويدعم أيضًا الدردشة متعددة الوسائط.",
|
||||
"gemini-3-pro-image-preview:image.description": "Gemini 3 Pro Image (Nano Banana Pro) هو نموذج إنشاء الصور من Google ويدعم أيضًا الدردشة متعددة الوسائط.",
|
||||
"gemini-3-pro-preview.description": "Gemini 3 Pro هو أقوى نموذج من Google للوكيل الذكي والبرمجة الإبداعية، يقدم تفاعلاً أعمق وصورًا أغنى مع استدلال متقدم.",
|
||||
"gemini-3.1-flash-image-preview.description": "Gemini 3.1 Flash Image (Nano Banana 2) يقدم جودة صور احترافية بسرعة فائقة مع دعم الدردشة متعددة الوسائط.",
|
||||
"gemini-3.1-flash-image-preview:image.description": "Gemini 3.1 Flash Image (Nano Banana 2) يقدم جودة صور بمستوى Pro بسرعة Flash مع دعم الدردشة متعددة الوسائط.",
|
||||
"gemini-3.1-flash-image-preview:image.description": "Gemini 3.1 Flash Image (Nano Banana 2) يقدم جودة صور بمستوى احترافي بسرعة Flash مع دعم الدردشة متعددة الوسائط.",
|
||||
"gemini-3.1-flash-lite-preview.description": "Gemini 3.1 Flash-Lite Preview هو النموذج الأكثر كفاءة من حيث التكلفة من Google، مُحسّن للمهام الوكيلة ذات الحجم الكبير، الترجمة، ومعالجة البيانات.",
|
||||
"gemini-3.1-pro-preview.description": "Gemini 3.1 Pro Preview يحسن من Gemini 3 Pro مع قدرات استدلال محسّنة ويضيف دعم مستوى التفكير المتوسط.",
|
||||
"gemini-flash-latest.description": "أحدث إصدار من Gemini Flash",
|
||||
@@ -805,7 +798,7 @@
|
||||
"kimi-k2-thinking-turbo.description": "إصدار K2 عالي السرعة للتفكير الطويل مع نافذة سياق 256k، استدلال عميق قوي، وإخراج 60–100 رمز/ثانية.",
|
||||
"kimi-k2-thinking.description": "kimi-k2-thinking هو نموذج تفكير من Moonshot AI يتمتع بقدرات عامة في الوكالة والاستدلال. يتفوق في الاستدلال العميق ويمكنه حل المشكلات الصعبة باستخدام أدوات متعددة الخطوات.",
|
||||
"kimi-k2-turbo-preview.description": "kimi-k2 هو نموذج MoE أساسي يتمتع بقدرات قوية في البرمجة والوكالة (1 تريليون معلمة إجمالية، 32 مليار نشطة)، ويتفوق على النماذج المفتوحة السائدة في اختبارات الاستدلال، البرمجة، الرياضيات، والوكالة.",
|
||||
"kimi-k2.5.description": "Kimi K2.5 هو النموذج الأكثر تنوعًا من Kimi حتى الآن، يتميز ببنية متعددة الوسائط تدعم المدخلات البصرية والنصية، أوضاع \"التفكير\" و\"غير التفكير\"، ومهام المحادثة والوكلاء.",
|
||||
"kimi-k2.5.description": "Kimi K2.5 هو أقوى نموذج من سلسلة Kimi، يقدم أداءً رائدًا مفتوح المصدر في مهام الوكلاء، البرمجة، وفهم الرؤية. يدعم الإدخال متعدد الوسائط وأنماط التفكير وغير التفكير.",
|
||||
"kimi-k2.description": "Kimi-K2 هو نموذج MoE أساسي من Moonshot AI يتمتع بقدرات قوية في البرمجة والوكالة، بإجمالي 1 تريليون معلمة و32 مليار نشطة. يتفوق على النماذج المفتوحة السائدة في اختبارات الاستدلال العام، البرمجة، الرياضيات، ومهام الوكالة.",
|
||||
"kimi-k2:1t.description": "Kimi K2 هو نموذج LLM كبير من نوع MoE من Moonshot AI بإجمالي 1 تريليون معلمة و32 مليار نشطة لكل تمرير أمامي. مُحسّن لقدرات الوكالة بما في ذلك استخدام الأدوات المتقدمة، الاستدلال، وتوليد الشيفرة.",
|
||||
"kuaishou/kat-coder-pro-v1.description": "KAT-Coder-Pro-V1 (مجاني لفترة محدودة) يركز على فهم الشيفرة والأتمتة لوكلاء البرمجة الفعالة.",
|
||||
@@ -967,7 +960,7 @@
|
||||
"moonshot-v1-32k.description": "Moonshot V1 32K يدعم 32,768 رمزًا لسياق متوسط الطول، وهو مثالي للوثائق الطويلة والحوارات المعقدة في إنشاء المحتوى، والتقارير، وأنظمة الدردشة.",
|
||||
"moonshot-v1-8k-vision-preview.description": "نماذج Kimi للرؤية (بما في ذلك moonshot-v1-8k-vision-preview/moonshot-v1-32k-vision-preview/moonshot-v1-128k-vision-preview) قادرة على فهم محتوى الصور مثل النصوص، الألوان، وأشكال الكائنات.",
|
||||
"moonshot-v1-8k.description": "Moonshot V1 8K مُحسّن لتوليد النصوص القصيرة بكفاءة عالية، حيث يتعامل مع 8,192 رمزًا للمحادثات القصيرة، والملاحظات، والمحتوى السريع.",
|
||||
"moonshotai/Kimi-Dev-72B.description": "Kimi-Dev-72B هو نموذج برمجة مفتوح المصدر مُحسن باستخدام التعلم المعزز واسع النطاق لإنتاج تصحيحات قوية وجاهزة للإنتاج. يسجل 60.4% على SWE-bench Verified، محققًا رقمًا قياسيًا جديدًا للنماذج المفتوحة في مهام هندسة البرمجيات الآلية مثل إصلاح الأخطاء ومراجعة الكود.",
|
||||
"moonshotai/Kimi-Dev-72B.description": "Kimi-Dev-72B هو نموذج مفتوح المصدر للبرمجة تم تحسينه باستخدام التعلم المعزز على نطاق واسع لإنتاج تصحيحات قوية وجاهزة للإنتاج. يحقق نسبة 60.4٪ على SWE-bench Verified، مسجلاً رقمًا قياسيًا جديدًا للنماذج المفتوحة في مهام هندسة البرمجيات الآلية مثل إصلاح الأخطاء ومراجعة الشيفرة.",
|
||||
"moonshotai/Kimi-K2-Instruct-0905.description": "Kimi K2-Instruct-0905 هو أحدث وأقوى إصدار من Kimi K2. إنه نموذج MoE من الدرجة الأولى يحتوي على تريليون معلمة إجمالية و32 مليار معلمة نشطة. من أبرز ميزاته الذكاء البرمجي القوي، وتحسينات كبيرة في اختبارات الأداء والمهام الواقعية، بالإضافة إلى تحسينات في جمالية واجهات الاستخدام وسهولة البرمجة الأمامية.",
|
||||
"moonshotai/Kimi-K2-Thinking.description": "Kimi K2 Thinking هو أحدث وأقوى نموذج تفكير مفتوح المصدر. يوسع بشكل كبير عمق التفكير متعدد الخطوات ويحافظ على استخدام الأدوات المستقر عبر 200-300 استدعاء متتالي، محققًا أرقامًا قياسية جديدة في Humanity's Last Exam (HLE)، BrowseComp، ومعايير أخرى. يتفوق في البرمجة، الرياضيات، المنطق، وسيناريوهات الوكيل. يعتمد على بنية MoE مع ~1 تريليون معلمة إجمالية، ويدعم نافذة سياق 256K واستدعاء الأدوات.",
|
||||
"moonshotai/kimi-k2-0711.description": "Kimi K2 0711 هو إصدار موجه من سلسلة Kimi، مناسب للبرمجة عالية الجودة واستخدام الأدوات.",
|
||||
@@ -1170,7 +1163,6 @@
|
||||
"qwen3-coder-next.description": "الجيل التالي من Qwen coder محسن لتوليد الأكواد المعقدة متعددة الملفات، وتصحيح الأخطاء، وسير العمل عالي الإنتاجية للوكلاء. مصمم لتكامل الأدوات القوي وتحسين أداء الاستدلال.",
|
||||
"qwen3-coder-plus.description": "نموذج Qwen للبرمجة. سلسلة Qwen3-Coder الأحدث مبنية على Qwen3 وتوفر قدرات قوية كوكلاء برمجة، واستخدام الأدوات، والتفاعل مع البيئة للبرمجة الذاتية، مع أداء ممتاز في البرمجة وقدرات عامة قوية.",
|
||||
"qwen3-coder:480b.description": "نموذج عالي الأداء من Alibaba لمعالجة المهام المتعلقة بالوكلاء والبرمجة مع دعم لسياقات طويلة.",
|
||||
"qwen3-max-2026-01-23.description": "Qwen3 Max: النموذج الأفضل أداءً من Qwen للمهام البرمجية المعقدة متعددة الخطوات مع دعم التفكير.",
|
||||
"qwen3-max-preview.description": "أفضل نموذج Qwen للأداء في المهام المعقدة متعددة الخطوات. المعاينة تدعم التفكير.",
|
||||
"qwen3-max.description": "نماذج Qwen3 Max تقدم تحسينات كبيرة مقارنة بسلسلة 2.5 في القدرات العامة، وفهم اللغة الصينية/الإنجليزية، واتباع التعليمات المعقدة، والمهام المفتوحة الذاتية، والقدرات متعددة اللغات، واستخدام الأدوات، مع تقليل الهلوسة. الإصدار الأحدث qwen3-max يعزز البرمجة الوكيلة واستخدام الأدوات مقارنة بـ qwen3-max-preview. هذا الإصدار يحقق أداءً رائداً في المجال ويستهدف احتياجات الوكلاء المعقدة.",
|
||||
"qwen3-next-80b-a3b-instruct.description": "نموذج Qwen3 من الجيل التالي مفتوح المصدر غير مخصص للتفكير. مقارنة بالإصدار السابق (Qwen3-235B-A22B-Instruct-2507)، يتميز بفهم أفضل للغة الصينية، واستدلال منطقي أقوى، وتحسين في توليد النصوص.",
|
||||
@@ -1200,8 +1192,8 @@
|
||||
"qwq.description": "QwQ هو نموذج استدلال من عائلة Qwen. مقارنة بالنماذج المضبوطة على التعليمات، يقدم قدرات تفكير واستدلال تعزز الأداء بشكل كبير، خاصة في المشكلات الصعبة. QwQ-32B هو نموذج متوسط الحجم ينافس أفضل نماذج الاستدلال مثل DeepSeek-R1 و o1-mini.",
|
||||
"qwq_32b.description": "نموذج استدلال متوسط الحجم من عائلة Qwen. مقارنة بالنماذج المضبوطة على التعليمات، تعزز قدرات التفكير والاستدلال في QwQ الأداء بشكل كبير، خاصة في المشكلات الصعبة.",
|
||||
"r1-1776.description": "R1-1776 هو إصدار ما بعد التدريب من DeepSeek R1 مصمم لتقديم معلومات واقعية غير خاضعة للرقابة أو التحيز.",
|
||||
"seedance-1-5-pro-251215.description": "Seedance 1.5 Pro من ByteDance يدعم تحويل النص إلى فيديو، الصورة إلى فيديو (الإطار الأول، الإطار الأول + الأخير)، وتوليد الصوت المتزامن مع المرئيات.",
|
||||
"seedream-5-0-260128.description": "ByteDance-Seedream-5.0-lite من BytePlus يتميز بتوليد معزز بالمعلومات المسترجعة من الويب للحصول على معلومات في الوقت الفعلي، تفسير المطالبات المعقدة بشكل محسن، وتحسين اتساق المراجع لإنشاء مرئيات احترافية.",
|
||||
"seedance-1-5-pro-251215.description": "Seedance 1.5 Pro من ByteDance يدعم تحويل النص إلى فيديو، الصورة إلى فيديو (الإطار الأول، الإطار الأول + الأخير)، وإنشاء الصوت متزامنًا مع المرئيات.",
|
||||
"seedream-5-0-260128.description": "ByteDance-Seedream-5.0-lite من BytePlus يتميز بإنشاء معزز بالاسترجاع عبر الويب للحصول على معلومات في الوقت الحقيقي، تفسير محسّن للتعليمات المعقدة، وتحسين تناسق المراجع لإنشاء بصري احترافي.",
|
||||
"solar-mini-ja.description": "Solar Mini (Ja) يوسع Solar Mini مع تركيز على اللغة اليابانية مع الحفاظ على الأداء القوي والكفاءة في الإنجليزية والكورية.",
|
||||
"solar-mini.description": "Solar Mini هو نموذج لغة مدمج يتفوق على GPT-3.5، يتميز بقدرات متعددة اللغات قوية تدعم الإنجليزية والكورية، ويقدم حلاً فعالاً بصمة صغيرة.",
|
||||
"solar-pro.description": "Solar Pro هو نموذج لغة عالي الذكاء من Upstage، يركز على اتباع التعليمات باستخدام وحدة معالجة رسومات واحدة، مع درجات IFEval تتجاوز 80. حالياً يدعم اللغة الإنجليزية؛ وكان من المقرر إصدار النسخة الكاملة في نوفمبر 2024 مع دعم لغات موسع وسياق أطول.",
|
||||
@@ -1237,7 +1229,7 @@
|
||||
"step-3.5-flash.description": "نموذج التفكير اللغوي الرائد من Stepfun. يتميز بقدرات تفكير من الدرجة الأولى وقدرات تنفيذ سريعة وموثوقة. قادر على تحليل وتخطيط المهام المعقدة، واستدعاء الأدوات بسرعة وموثوقية لأداء المهام، والتعامل مع مختلف المهام المعقدة مثل التفكير المنطقي، الرياضيات، هندسة البرمجيات، والبحث المتعمق.",
|
||||
"step-3.description": "يتمتع هذا النموذج بإدراك بصري قوي واستدلال معقد، ويتعامل بدقة مع فهم المعرفة عبر المجالات، وتحليل الرياضيات والرؤية، ومجموعة واسعة من مهام التحليل البصري اليومية.",
|
||||
"step-r1-v-mini.description": "نموذج استدلال يتمتع بفهم قوي للصور، يمكنه معالجة الصور والنصوص، ثم توليد نص بعد استدلال عميق. يتفوق في الاستدلال البصري ويقدم أداءً رائدًا في الرياضيات والبرمجة والاستدلال النصي، مع نافذة سياق تصل إلى 100 ألف.",
|
||||
"stepfun-ai/step3.description": "Step3 هو نموذج استدلال متعدد الوسائط متقدم من StepFun، يعتمد على بنية MoE مع 321 مليار معلمة إجمالية و38 مليار معلمة نشطة. تصميمه الشامل يقلل من تكلفة فك التشفير مع تقديم استدلال رؤية-لغة من الدرجة الأولى. مع تصميم MFA وAFD، يظل فعالًا على كل من المسرعات الرائدة والمنخفضة. يستخدم التدريب المسبق أكثر من 20 تريليون رمز نصي و4 تريليون رمز نصي-صوري عبر العديد من اللغات. يحقق أداءً رائدًا للنماذج المفتوحة في الرياضيات، البرمجة، ومعايير متعددة الوسائط.",
|
||||
"stepfun-ai/step3.description": "Step3 هو نموذج استدلال متعدد الوسائط متقدم من StepFun، مبني على بنية MoE بسعة إجمالية 321B و38B نشطة. تصميمه الشامل يقلل من تكلفة فك التشفير مع تقديم استدلال رؤية-لغة من الدرجة الأولى. بفضل تصميم MFA وAFD، يظل فعالًا على المسرعات القوية والضعيفة. تم تدريبه مسبقًا على أكثر من 20 تريليون رمز نصي و4 تريليون رمز صورة-نص بعدة لغات. يحقق أداءً رائدًا في النماذج المفتوحة في اختبارات الرياضيات والبرمجة ومتعددة الوسائط.",
|
||||
"taichu4_vl_2b_nothinking.description": "الإصدار بدون التفكير من نموذج Taichu4.0-VL 2B يتميز باستخدام ذاكرة أقل، تصميم خفيف الوزن، سرعة استجابة سريعة، وقدرات فهم متعددة الوسائط قوية.",
|
||||
"taichu4_vl_32b.description": "الإصدار التفكير من نموذج Taichu4.0-VL 32B مناسب لمهام الفهم والاستدلال متعددة الوسائط المعقدة، ويظهر أداءً رائعًا في الاستدلال الرياضي متعدد الوسائط، قدرات الوكيل متعدد الوسائط، والفهم العام للصور والبصريات.",
|
||||
"taichu4_vl_32b_nothinking.description": "الإصدار بدون التفكير من نموذج Taichu4.0-VL 32B مصمم لفهم النصوص والصور المعقدة وسيناريوهات الإجابة على الأسئلة المعرفية البصرية، ويتفوق في وصف الصور، الإجابة على الأسئلة البصرية، فهم الفيديو، ومهام تحديد المواقع البصرية.",
|
||||
@@ -1324,7 +1316,7 @@
|
||||
"zai-org/GLM-4.5-Air.description": "GLM-4.5-Air هو نموذج أساسي لتطبيقات الوكلاء يستخدم بنية Mixture-of-Experts. مُحسّن لاستخدام الأدوات، وتصفح الويب، والهندسة البرمجية، وبرمجة الواجهات، ويتكامل مع وكلاء البرمجة مثل Claude Code وRoo Code. يستخدم استدلالًا هجينًا للتعامل مع السيناريوهات المعقدة واليومية.",
|
||||
"zai-org/GLM-4.5V.description": "GLM-4.5V هو أحدث نموذج رؤية من Zhipu AI، مبني على نموذج النص الرائد GLM-4.5-Air (إجمالي 106 مليار، 12 مليار نشط) باستخدام بنية MoE لأداء قوي بتكلفة أقل. يتبع مسار GLM-4.1V-Thinking ويضيف 3D-RoPE لتحسين الاستدلال المكاني ثلاثي الأبعاد. مُحسّن من خلال التدريب المسبق، والتعلم الخاضع للإشراف، والتعلم المعزز، ويتعامل مع الصور، والفيديو، والمستندات الطويلة، ويتصدر النماذج المفتوحة في 41 معيارًا متعدد الوسائط. يتيح وضع التفكير للمستخدمين التوازن بين السرعة والعمق.",
|
||||
"zai-org/GLM-4.6.description": "مقارنة بـ GLM-4.5، يوسّع GLM-4.6 السياق من 128 ألف إلى 200 ألف لمهام الوكلاء المعقدة. يحقق نتائج أعلى في اختبارات البرمجة ويُظهر أداءً أقوى في التطبيقات الواقعية مثل Claude Code وCline وRoo Code وKilo Code، بما في ذلك توليد صفحات الواجهة الأمامية بشكل أفضل. تم تحسين الاستدلال ودعم استخدام الأدوات أثناء التفكير، مما يعزز القدرات العامة. يتكامل بشكل أفضل مع أطر الوكلاء، ويحسّن وكلاء الأدوات/البحث، ويتميز بأسلوب كتابة مفضل بشريًا وطبيعية في تقمص الأدوار.",
|
||||
"zai-org/GLM-4.6V.description": "GLM-4.6V يحقق دقة فهم بصري رائدة بالنسبة لحجم معلماته وهو الأول الذي يدمج قدرات استدعاء الوظائف بشكل طبيعي في بنية نموذج الرؤية، مما يجسر الفجوة بين \"الإدراك البصري\" و\"الإجراءات القابلة للتنفيذ\" ويوفر أساسًا تقنيًا موحدًا للوكلاء متعدد الوسائط في سيناريوهات الأعمال الواقعية. يتم تمديد نافذة السياق البصري إلى 128 ألف، مما يدعم معالجة تدفقات الفيديو الطويلة وتحليل الصور المتعددة عالية الدقة.",
|
||||
"zai-org/GLM-4.6V.description": "GLM-4.6V يحقق دقة فهم بصري رائدة بالنسبة لحجم معلماته وهو الأول الذي يدمج قدرات استدعاء الوظائف بشكل طبيعي في بنية نموذج الرؤية، مما يجسر الفجوة بين \"الإدراك البصري\" و\"الإجراءات القابلة للتنفيذ\" ويوفر أساسًا تقنيًا موحدًا للوكلاء متعدد الوسائط في سيناريوهات الأعمال الواقعية. تم تمديد نافذة السياق البصري إلى 128k، مما يدعم معالجة تدفقات الفيديو الطويلة وتحليل الصور عالية الدقة متعددة.",
|
||||
"zai/glm-4.5-air.description": "GLM-4.5 وGLM-4.5-Air هما أحدث النماذج الرائدة لدينا لتطبيقات الوكلاء، وكلاهما يستخدم بنية MoE. يحتوي GLM-4.5 على 355 مليار إجمالي و32 مليار نشط لكل تمرير؛ بينما GLM-4.5-Air أنحف بإجمالي 106 مليار و12 مليار نشط.",
|
||||
"zai/glm-4.5.description": "سلسلة GLM-4.5 مصممة للوكلاء. النموذج الرائد GLM-4.5 يجمع بين الاستدلال، والبرمجة، ومهارات الوكلاء مع 355 مليار معلمة إجمالية (32 مليار نشطة) ويقدّم أوضاع تشغيل مزدوجة كنظام استدلال هجين.",
|
||||
"zai/glm-4.5v.description": "GLM-4.5V مبني على GLM-4.5-Air، ويَرِث تقنيات GLM-4.1V-Thinking المثبتة، ويتوسع ببنية MoE قوية بسعة 106 مليار.",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"arguments.moreParams": "إجمالي {{count}} من المعاملات",
|
||||
"arguments.title": "المعلمات",
|
||||
"builtins.lobe-activator.apiName.activateTools": "تفعيل الأدوات",
|
||||
"builtins.lobe-agent-builder.apiName.getAvailableModels": "الحصول على النماذج المتاحة",
|
||||
"builtins.lobe-agent-builder.apiName.getAvailableTools": "الحصول على المهارات المتاحة",
|
||||
"builtins.lobe-agent-builder.apiName.getConfig": "الحصول على الإعدادات",
|
||||
@@ -210,6 +209,7 @@
|
||||
"builtins.lobe-skills.apiName.runCommand": "تشغيل الأمر",
|
||||
"builtins.lobe-skills.apiName.searchSkill": "البحث عن المهارات",
|
||||
"builtins.lobe-skills.title": "المهارات",
|
||||
"builtins.lobe-tools.apiName.activateTools": "تفعيل الأدوات",
|
||||
"builtins.lobe-topic-reference.apiName.getTopicContext": "الحصول على سياق الموضوع",
|
||||
"builtins.lobe-topic-reference.title": "مرجع الموضوع",
|
||||
"builtins.lobe-user-memory.apiName.addContextMemory": "إضافة ذاكرة السياق",
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"azure.description": "تقدم Azure نماذج ذكاء اصطناعي متقدمة، بما في ذلك سلسلة GPT-3.5 وGPT-4، لمعالجة أنواع بيانات متنوعة ومهام معقدة مع التركيز على الأمان والموثوقية والاستدامة.",
|
||||
"azureai.description": "توفر Azure نماذج ذكاء اصطناعي متقدمة، بما في ذلك سلسلة GPT-3.5 وGPT-4، لمعالجة أنواع بيانات متنوعة ومهام معقدة مع التركيز على الأمان والموثوقية والاستدامة.",
|
||||
"baichuan.description": "تركز Baichuan AI على النماذج الأساسية ذات الأداء القوي في المعرفة الصينية، ومعالجة السياقات الطويلة، والتوليد الإبداعي. تم تحسين نماذجها (Baichuan 4 وBaichuan 3 Turbo وBaichuan 3 Turbo 128k) لسيناريوهات مختلفة وتقدم قيمة عالية.",
|
||||
"bailiancodingplan.description": "خطة الترميز علي بابليون هي خدمة ذكاء اصطناعي متخصصة توفر الوصول إلى نماذج محسّنة للترميز من Qwen وGLM وKimi وMiniMax عبر نقطة نهاية مخصصة.",
|
||||
"bedrock.description": "توفر Amazon Bedrock للمؤسسات نماذج لغوية وبصرية متقدمة، بما في ذلك Anthropic Claude وMeta Llama 3.1، بدءًا من الخيارات الخفيفة إلى عالية الأداء لمهام النصوص والدردشة والصور.",
|
||||
"bfl.description": "مختبر أبحاث رائد في مجال الذكاء الاصطناعي المتقدم، يعمل على بناء البنية التحتية البصرية للمستقبل.",
|
||||
"cerebras.description": "Cerebras هي منصة استدلال تعتمد على نظام CS-3، تركز على تقديم خدمات نماذج لغوية كبيرة بزمن استجابة منخفض جدًا وسرعة عالية لمهام الوقت الحقيقي مثل توليد الأكواد والمهام التفاعلية.",
|
||||
@@ -22,7 +21,6 @@
|
||||
"giteeai.description": "توفر Gitee AI واجهات برمجة تطبيقات بدون خوادم لخدمات استدلال النماذج اللغوية الكبيرة، جاهزة للاستخدام من قبل المطورين.",
|
||||
"github.description": "مع نماذج GitHub، يمكن للمطورين العمل كمهندسي ذكاء اصطناعي باستخدام نماذج رائدة في الصناعة.",
|
||||
"githubcopilot.description": "يمكنك الوصول إلى نماذج Claude وGPT وGemini من خلال اشتراكك في GitHub Copilot.",
|
||||
"glmcodingplan.description": "خطة الترميز GLM توفر الوصول إلى نماذج الذكاء الاصطناعي Zhipu بما في ذلك GLM-5 وGLM-4.7 لأداء مهام الترميز عبر اشتراك ثابت الرسوم.",
|
||||
"google.description": "عائلة Gemini من Google هي أكثر نماذج الذكاء الاصطناعي تطورًا للأغراض العامة، تم تطويرها بواسطة Google DeepMind للاستخدام متعدد الوسائط عبر النصوص والرموز والصور والصوت والفيديو. يمكن تشغيلها من مراكز البيانات إلى الأجهزة المحمولة بكفاءة عالية وانتشار واسع.",
|
||||
"groq.description": "توفر محركات الاستدلال LPU من Groq أداءً متميزًا في المعايير مع سرعة وكفاءة استثنائية، مما يضع معيارًا عاليًا للاستدلال منخفض الكمون في السحابة.",
|
||||
"higress.description": "Higress هو بوابة API سحابية أصلية تم تطويرها داخل Alibaba لمعالجة تأثير إعادة تحميل Tengine على الاتصالات طويلة الأمد وسد الفجوات في موازنة تحميل gRPC/Dubbo.",
|
||||
@@ -31,12 +29,10 @@
|
||||
"infiniai.description": "توفر خدمات نماذج لغوية كبيرة عالية الأداء وسهلة الاستخدام وآمنة لمطوري التطبيقات، تغطي كامل دورة العمل من تطوير النموذج إلى نشره في الإنتاج.",
|
||||
"internlm.description": "منظمة مفتوحة المصدر تركز على أبحاث النماذج الكبيرة والأدوات، وتوفر منصة فعالة وسهلة الاستخدام تتيح الوصول إلى أحدث النماذج والخوارزميات.",
|
||||
"jina.description": "تأسست Jina AI في عام 2020، وهي شركة رائدة في مجال البحث الذكي. تشمل تقنياتها نماذج المتجهات، ومعيدو الترتيب، ونماذج لغوية صغيرة لبناء تطبيقات بحث توليدية ومتعددة الوسائط عالية الجودة.",
|
||||
"kimicodingplan.description": "كود Kimi من Moonshot AI يوفر الوصول إلى نماذج Kimi بما في ذلك K2.5 لأداء مهام الترميز.",
|
||||
"lmstudio.description": "LM Studio هو تطبيق سطح مكتب لتطوير وتجربة النماذج اللغوية الكبيرة على جهازك.",
|
||||
"lobehub.description": "LobeHub Cloud يستخدم واجهات برمجية رسمية للوصول إلى نماذج الذكاء الاصطناعي ويقيس الاستخدام عبر أرصدة مرتبطة برموز النماذج.",
|
||||
"lobehub.description": "يستخدم LobeHub Cloud واجهات برمجة التطبيقات الرسمية للوصول إلى نماذج الذكاء الاصطناعي ويقيس الاستخدام باستخدام أرصدة مرتبطة برموز النماذج.",
|
||||
"longcat.description": "LongCat هو سلسلة من نماذج الذكاء الاصطناعي التوليدية الكبيرة التي تم تطويرها بشكل مستقل بواسطة Meituan. تم تصميمه لتعزيز إنتاجية المؤسسة الداخلية وتمكين التطبيقات المبتكرة من خلال بنية حسابية فعالة وقدرات متعددة الوسائط قوية.",
|
||||
"minimax.description": "تأسست MiniMax في عام 2021، وتبني نماذج ذكاء اصطناعي متعددة الوسائط للأغراض العامة، بما في ذلك نماذج نصية بمليارات المعلمات، ونماذج صوتية وبصرية، بالإضافة إلى تطبيقات مثل Hailuo AI.",
|
||||
"minimaxcodingplan.description": "خطة الرموز MiniMax توفر الوصول إلى نماذج MiniMax بما في ذلك M2.7 لأداء مهام الترميز عبر اشتراك ثابت الرسوم.",
|
||||
"mistral.description": "تقدم Mistral نماذج متقدمة عامة ومتخصصة وبحثية للتفكير المعقد، والمهام متعددة اللغات، وتوليد الأكواد، مع دعم استدعاء الوظائف للتكامل المخصص.",
|
||||
"modelscope.description": "ModelScope هي منصة نماذج كخدمة من Alibaba Cloud، تقدم مجموعة واسعة من النماذج وخدمات الاستدلال.",
|
||||
"moonshot.description": "تقدم Moonshot، من Moonshot AI (شركة Beijing Moonshot Technology)، نماذج معالجة لغة طبيعية متعددة لحالات استخدام مثل إنشاء المحتوى، والبحث، والتوصيات، والتحليل الطبي، مع دعم قوي للسياقات الطويلة والتوليد المعقد.",
|
||||
@@ -69,7 +65,6 @@
|
||||
"vertexai.description": "عائلة Gemini من Google هي أكثر نماذج الذكاء الاصطناعي تطورًا للأغراض العامة، تم تطويرها بواسطة Google DeepMind للاستخدام متعدد الوسائط عبر النصوص والرموز والصور والصوت والفيديو. يمكن تشغيلها من مراكز البيانات إلى الأجهزة المحمولة، مما يعزز الكفاءة ومرونة النشر.",
|
||||
"vllm.description": "vLLM مكتبة سريعة وسهلة الاستخدام لاستدلال وخدمة النماذج اللغوية الكبيرة.",
|
||||
"volcengine.description": "توفر منصة نماذج ByteDance وصولًا آمنًا وغنيًا بالميزات وفعالًا من حيث التكلفة إلى النماذج، بالإضافة إلى أدوات شاملة للبيانات، والتخصيص، والاستدلال، والتقييم.",
|
||||
"volcenginecodingplan.description": "خطة الترميز Volcengine من ByteDance توفر الوصول إلى نماذج ترميز متعددة بما في ذلك Doubao-Seed-Code وGLM-4.7 وDeepSeek-V3.2 وKimi-K2.5 عبر اشتراك ثابت الرسوم.",
|
||||
"wenxin.description": "منصة متكاملة للمؤسسات لتطوير النماذج الأساسية والتطبيقات الذكية، تقدم أدوات شاملة لسير عمل النماذج التوليدية وتطبيقاتها.",
|
||||
"xai.description": "تقوم xAI ببناء ذكاء اصطناعي لتسريع الاكتشاف العلمي، بهدف تعميق فهم البشرية للكون.",
|
||||
"xiaomimimo.description": "تقدم Xiaomi MiMo خدمة نموذج محادثة متوافقة مع واجهة برمجة تطبيقات OpenAI. يدعم نموذج mimo-v2-flash التفكير العميق، الإخراج المتدفق، استدعاء الوظائف، نافذة سياق بسعة 256 ألف، وإخراجًا أقصى يصل إلى 128 ألف.",
|
||||
|
||||
@@ -193,70 +193,6 @@
|
||||
"analytics.title": "التحليلات",
|
||||
"checking": "جارٍ التحقق...",
|
||||
"checkingPermissions": "جارٍ التحقق من الأذونات...",
|
||||
"creds.actions.delete": "حذف",
|
||||
"creds.actions.deleteConfirm.cancel": "إلغاء",
|
||||
"creds.actions.deleteConfirm.content": "سيتم حذف بيانات الاعتماد هذه بشكل دائم. لا يمكن التراجع عن هذا الإجراء.",
|
||||
"creds.actions.deleteConfirm.ok": "حذف",
|
||||
"creds.actions.deleteConfirm.title": "حذف بيانات الاعتماد؟",
|
||||
"creds.actions.edit": "تعديل",
|
||||
"creds.actions.view": "عرض",
|
||||
"creds.create": "بيانات اعتماد جديدة",
|
||||
"creds.createModal.fillForm": "املأ التفاصيل",
|
||||
"creds.createModal.selectType": "اختر النوع",
|
||||
"creds.createModal.title": "إنشاء بيانات اعتماد",
|
||||
"creds.edit.title": "تعديل بيانات الاعتماد",
|
||||
"creds.empty": "لم يتم تكوين أي بيانات اعتماد حتى الآن",
|
||||
"creds.file.authRequired": "يرجى تسجيل الدخول إلى السوق أولاً",
|
||||
"creds.file.uploadFailed": "فشل تحميل الملف",
|
||||
"creds.file.uploadSuccess": "تم تحميل الملف بنجاح",
|
||||
"creds.file.uploading": "جارٍ التحميل...",
|
||||
"creds.form.addPair": "إضافة زوج مفتاح-قيمة",
|
||||
"creds.form.back": "رجوع",
|
||||
"creds.form.cancel": "إلغاء",
|
||||
"creds.form.connectionRequired": "يرجى اختيار اتصال OAuth",
|
||||
"creds.form.description": "الوصف",
|
||||
"creds.form.descriptionPlaceholder": "وصف اختياري لهذه البيانات",
|
||||
"creds.form.file": "ملف بيانات الاعتماد",
|
||||
"creds.form.fileRequired": "يرجى تحميل ملف",
|
||||
"creds.form.key": "المعرف",
|
||||
"creds.form.keyPattern": "يمكن أن يحتوي المعرف فقط على أحرف وأرقام وشرطات سفلية وشرطات",
|
||||
"creds.form.keyRequired": "المعرف مطلوب",
|
||||
"creds.form.name": "اسم العرض",
|
||||
"creds.form.nameRequired": "اسم العرض مطلوب",
|
||||
"creds.form.save": "حفظ",
|
||||
"creds.form.selectConnection": "اختر اتصال OAuth",
|
||||
"creds.form.selectConnectionPlaceholder": "اختر حسابًا متصلاً",
|
||||
"creds.form.selectedFile": "الملف المحدد",
|
||||
"creds.form.submit": "إنشاء",
|
||||
"creds.form.uploadDesc": "يدعم تنسيقات ملفات JSON وPEM وغيرها من ملفات بيانات الاعتماد",
|
||||
"creds.form.uploadHint": "انقر أو اسحب الملف لتحميله",
|
||||
"creds.form.valuePlaceholder": "أدخل القيمة",
|
||||
"creds.form.values": "أزواج المفتاح-القيمة",
|
||||
"creds.oauth.noConnections": "لا توجد اتصالات OAuth متاحة. يرجى توصيل حساب أولاً.",
|
||||
"creds.signIn": "تسجيل الدخول إلى السوق",
|
||||
"creds.signInRequired": "يرجى تسجيل الدخول إلى السوق لإدارة بيانات الاعتماد الخاصة بك",
|
||||
"creds.table.actions": "الإجراءات",
|
||||
"creds.table.key": "المعرف",
|
||||
"creds.table.lastUsed": "آخر استخدام",
|
||||
"creds.table.name": "الاسم",
|
||||
"creds.table.neverUsed": "لم يتم الاستخدام",
|
||||
"creds.table.preview": "معاينة",
|
||||
"creds.table.type": "النوع",
|
||||
"creds.typeDesc.file": "تحميل ملفات بيانات الاعتماد مثل حسابات الخدمة أو الشهادات",
|
||||
"creds.typeDesc.kv-env": "تخزين مفاتيح API والرموز كمتغيرات بيئية",
|
||||
"creds.typeDesc.kv-header": "تخزين قيم التفويض كعناوين HTTP",
|
||||
"creds.typeDesc.oauth": "ربط باتصال OAuth موجود",
|
||||
"creds.types.all": "الكل",
|
||||
"creds.types.file": "ملف",
|
||||
"creds.types.kv-env": "بيئة",
|
||||
"creds.types.kv-header": "رأس",
|
||||
"creds.types.oauth": "OAuth",
|
||||
"creds.view.error": "فشل في تحميل بيانات الاعتماد",
|
||||
"creds.view.noValues": "لا توجد قيم",
|
||||
"creds.view.oauthNote": "يتم إدارة بيانات اعتماد OAuth بواسطة الخدمة المتصلة.",
|
||||
"creds.view.title": "عرض بيانات الاعتماد: {{name}}",
|
||||
"creds.view.values": "قيم بيانات الاعتماد",
|
||||
"creds.view.warning": "هذه القيم حساسة. لا تشاركها مع الآخرين.",
|
||||
"danger.clear.action": "مسح الآن",
|
||||
"danger.clear.confirm": "هل تريد مسح جميع بيانات الدردشة؟ لا يمكن التراجع عن هذا الإجراء.",
|
||||
"danger.clear.desc": "سيتم حذف جميع البيانات، بما في ذلك الوكلاء والملفات والرسائل والمهارات. لن يتم حذف حسابك.",
|
||||
@@ -795,7 +731,6 @@
|
||||
"tab.appearance": "المظهر",
|
||||
"tab.chatAppearance": "مظهر المحادثة",
|
||||
"tab.common": "المظهر",
|
||||
"tab.creds": "بيانات الاعتماد",
|
||||
"tab.experiment": "تجريبي",
|
||||
"tab.hotkey": "اختصارات لوحة المفاتيح",
|
||||
"tab.image": "خدمة توليد الصور",
|
||||
|
||||
@@ -199,8 +199,6 @@
|
||||
"plans.btn.paymentDesc": "يدعم بطاقات الائتمان / Alipay / WeChat Pay",
|
||||
"plans.btn.paymentDescForZarinpal": "يدعم بطاقات الائتمان",
|
||||
"plans.btn.soon": "قريبًا",
|
||||
"plans.cancelDowngrade": "إلغاء التخفيض المجدول",
|
||||
"plans.cancelDowngradeSuccess": "تم إلغاء التخفيض المجدول",
|
||||
"plans.changePlan": "اختر خطة",
|
||||
"plans.cloud.history": "سجل محادثات غير محدود",
|
||||
"plans.cloud.sync": "مزامنة سحابية عالمية",
|
||||
@@ -217,7 +215,6 @@
|
||||
"plans.current": "الخطة الحالية",
|
||||
"plans.downgradePlan": "خطة التخفيض المستهدفة",
|
||||
"plans.downgradeTip": "لقد قمت بالفعل بتغيير الاشتراك. لا يمكنك تنفيذ عمليات أخرى حتى يكتمل التبديل",
|
||||
"plans.downgradeWillCancel": "سيؤدي هذا الإجراء إلى إلغاء تخفيض الخطة المجدول",
|
||||
"plans.embeddingStorage.embeddings": "مدخلات",
|
||||
"plans.embeddingStorage.title": "تخزين المتجهات",
|
||||
"plans.embeddingStorage.tooltip": "تنتج صفحة مستند واحدة (1000-1500 حرف) حوالي إدخال متجه واحد. (تقدير باستخدام OpenAI Embeddings، وقد يختلف حسب النموذج)",
|
||||
@@ -256,7 +253,6 @@
|
||||
"plans.payonce.ok": "تأكيد الاختيار",
|
||||
"plans.payonce.popconfirm": "بعد الدفع لمرة واحدة، يجب الانتظار حتى انتهاء الاشتراك لتغيير الخطة أو دورة الفوترة. يرجى تأكيد اختيارك.",
|
||||
"plans.payonce.tooltip": "يتطلب الدفع لمرة واحدة الانتظار حتى انتهاء الاشتراك لتغيير الخطة أو دورة الفوترة",
|
||||
"plans.pendingDowngrade": "تخفيض قيد الانتظار",
|
||||
"plans.plan.enterprise.contactSales": "اتصل بالمبيعات",
|
||||
"plans.plan.enterprise.title": "الشركات",
|
||||
"plans.plan.free.desc": "للمستخدمين الجدد",
|
||||
@@ -370,7 +366,6 @@
|
||||
"summary.title": "ملخص الفوترة",
|
||||
"summary.usageThisMonth": "عرض استخدامك هذا الشهر.",
|
||||
"summary.viewBillingHistory": "عرض سجل المدفوعات",
|
||||
"switchDowngradeTarget": "تغيير هدف التخفيض",
|
||||
"switchPlan": "تبديل الخطة",
|
||||
"switchToMonthly.desc": "بعد التبديل، ستبدأ الفوترة الشهرية بعد انتهاء الخطة السنوية الحالية.",
|
||||
"switchToMonthly.title": "التبديل إلى الفوترة الشهرية",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"channel.appSecret": "Секрет на приложението",
|
||||
"channel.appSecretHint": "Тайният ключ на вашето бот приложение. Той ще бъде криптиран и съхранен сигурно.",
|
||||
"channel.appSecretPlaceholder": "Поставете вашия секрет на приложението тук",
|
||||
"channel.applicationId": "ID на приложението / Потребителско име на бота",
|
||||
"channel.applicationIdHint": "Уникален идентификатор за вашето бот приложение.",
|
||||
@@ -10,31 +9,14 @@
|
||||
"channel.botTokenHowToGet": "Как да го получите?",
|
||||
"channel.botTokenPlaceholderExisting": "Токенът е скрит за сигурност",
|
||||
"channel.botTokenPlaceholderNew": "Поставете вашия токен на бота тук",
|
||||
"channel.charLimit": "Ограничение на символите",
|
||||
"channel.charLimitHint": "Максимален брой символи на съобщение",
|
||||
"channel.connectFailed": "Свързването на бота не успя",
|
||||
"channel.connectSuccess": "Ботът е успешно свързан",
|
||||
"channel.connecting": "Свързване...",
|
||||
"channel.connectionConfig": "Конфигурация на връзката",
|
||||
"channel.copied": "Копирано в клипборда",
|
||||
"channel.copy": "Копирай",
|
||||
"channel.credentials": "Удостоверения",
|
||||
"channel.debounceMs": "Прозорец за обединяване на съобщения (ms)",
|
||||
"channel.debounceMsHint": "Колко време да се изчака за допълнителни съобщения преди изпращане към агента (ms)",
|
||||
"channel.deleteConfirm": "Сигурни ли сте, че искате да премахнете този канал?",
|
||||
"channel.deleteConfirmDesc": "Това действие ще премахне окончателно този канал за съобщения и неговата конфигурация. Това не може да бъде отменено.",
|
||||
"channel.devWebhookProxyUrl": "HTTPS тунел URL",
|
||||
"channel.devWebhookProxyUrlHint": "По избор. HTTPS тунел URL за пренасочване на заявки за уебхук към локален dev сървър.",
|
||||
"channel.disabled": "Деактивиран",
|
||||
"channel.discord.description": "Свържете този асистент с Discord сървър за канален чат и директни съобщения.",
|
||||
"channel.dm": "Директни съобщения",
|
||||
"channel.dmEnabled": "Активиране на директни съобщения",
|
||||
"channel.dmEnabledHint": "Позволете на бота да получава и отговаря на директни съобщения",
|
||||
"channel.dmPolicy": "Политика за директни съобщения",
|
||||
"channel.dmPolicyAllowlist": "Списък с позволени",
|
||||
"channel.dmPolicyDisabled": "Деактивирано",
|
||||
"channel.dmPolicyHint": "Контролирайте кой може да изпраща директни съобщения до бота",
|
||||
"channel.dmPolicyOpen": "Отворено",
|
||||
"channel.documentation": "Документация",
|
||||
"channel.enabled": "Активиран",
|
||||
"channel.encryptKey": "Ключ за криптиране",
|
||||
@@ -44,7 +26,6 @@
|
||||
"channel.endpointUrlHint": "Моля, копирайте този URL и го поставете в полето <bold>{{fieldName}}</bold> в {{name}} Developer Portal.",
|
||||
"channel.feishu.description": "Свържете този асистент с Feishu за лични и групови чатове.",
|
||||
"channel.lark.description": "Свържете този асистент с Lark за лични и групови чатове.",
|
||||
"channel.openPlatform": "Отворена платформа",
|
||||
"channel.platforms": "Платформи",
|
||||
"channel.publicKey": "Публичен ключ",
|
||||
"channel.publicKeyHint": "По избор. Използва се за проверка на заявки за взаимодействие от Discord.",
|
||||
@@ -61,16 +42,6 @@
|
||||
"channel.secretToken": "Секретен токен на уебхук",
|
||||
"channel.secretTokenHint": "По избор. Използва се за проверка на заявки за уебхук от Telegram.",
|
||||
"channel.secretTokenPlaceholder": "По избор секрет за проверка на уебхук",
|
||||
"channel.settings": "Разширени настройки",
|
||||
"channel.settingsResetConfirm": "Сигурни ли сте, че искате да върнете разширените настройки към техните стойности по подразбиране?",
|
||||
"channel.settingsResetDefault": "Връщане към стойности по подразбиране",
|
||||
"channel.setupGuide": "Ръководство за настройка",
|
||||
"channel.showUsageStats": "Показване на статистики за използване",
|
||||
"channel.showUsageStatsHint": "Показване на статистики за използване на токени, разходи и продължителност в отговорите на бота",
|
||||
"channel.signingSecret": "Тайна за подписване",
|
||||
"channel.signingSecretHint": "Използва се за проверка на заявки към уебхук.",
|
||||
"channel.slack.appIdHint": "Вашият Slack App ID от таблото за управление на Slack API (започва с A).",
|
||||
"channel.slack.description": "Свържете този асистент със Slack за разговори в канали и директни съобщения.",
|
||||
"channel.telegram.description": "Свържете този асистент с Telegram за лични и групови чатове.",
|
||||
"channel.testConnection": "Тестване на връзката",
|
||||
"channel.testFailed": "Тестът на връзката неуспешен",
|
||||
@@ -79,12 +50,5 @@
|
||||
"channel.validationError": "Моля, попълнете ID на приложението и токен",
|
||||
"channel.verificationToken": "Токен за проверка",
|
||||
"channel.verificationTokenHint": "По избор. Използва се за проверка на източника на събития за уебхук.",
|
||||
"channel.verificationTokenPlaceholder": "Поставете вашия токен за проверка тук",
|
||||
"channel.wechat.description": "Свържете този асистент с WeChat чрез iLink Bot за лични и групови чатове.",
|
||||
"channel.wechatQrExpired": "QR кодът е изтекъл. Моля, обновете, за да получите нов.",
|
||||
"channel.wechatQrRefresh": "Обновяване на QR код",
|
||||
"channel.wechatQrScaned": "QR кодът е сканиран. Моля, потвърдете влизането в WeChat.",
|
||||
"channel.wechatQrWait": "Отворете WeChat и сканирайте QR кода, за да се свържете.",
|
||||
"channel.wechatScanTitle": "Свързване на WeChat бот",
|
||||
"channel.wechatScanToConnect": "Сканирайте QR кода, за да се свържете"
|
||||
"channel.verificationTokenPlaceholder": "Поставете вашия токен за проверка тук"
|
||||
}
|
||||
|
||||
@@ -397,6 +397,7 @@
|
||||
"sync.status.unconnected": "Неуспешна връзка",
|
||||
"sync.title": "Статус на синхронизация",
|
||||
"sync.unconnected.tip": "Неуспешна връзка със сървъра за сигнализация, не може да се установи P2P комуникация. Моля, проверете мрежата и опитайте отново.",
|
||||
"tab.aiImage": "Изкуство",
|
||||
"tab.audio": "Аудио",
|
||||
"tab.chat": "Чат",
|
||||
"tab.community": "Общност",
|
||||
@@ -404,7 +405,6 @@
|
||||
"tab.eval": "Оценителна лаборатория",
|
||||
"tab.files": "Файлове",
|
||||
"tab.home": "Начало",
|
||||
"tab.image": "Изображение",
|
||||
"tab.knowledgeBase": "Библиотека",
|
||||
"tab.marketplace": "Пазар",
|
||||
"tab.me": "Аз",
|
||||
@@ -432,7 +432,6 @@
|
||||
"userPanel.billing": "Управление на плащания",
|
||||
"userPanel.cloud": "Стартирай {{name}}",
|
||||
"userPanel.community": "Общност",
|
||||
"userPanel.credits": "Управление на кредити",
|
||||
"userPanel.data": "Съхранение на данни",
|
||||
"userPanel.defaultNickname": "Потребител от общността",
|
||||
"userPanel.discord": "Поддръжка в общността",
|
||||
@@ -444,7 +443,6 @@
|
||||
"userPanel.plans": "Абонаментни планове",
|
||||
"userPanel.profile": "Акаунт",
|
||||
"userPanel.setting": "Настройки",
|
||||
"userPanel.upgradePlan": "Надграждане на плана",
|
||||
"userPanel.usages": "Статистика на използване",
|
||||
"version": "Версия"
|
||||
}
|
||||
|
||||
@@ -83,11 +83,6 @@
|
||||
"preference.empty": "Няма налични спомени за предпочитания",
|
||||
"preference.source": "Източник",
|
||||
"preference.suggestions": "Действия, които агентът може да предприеме",
|
||||
"purge.action": "Изчисти всичко",
|
||||
"purge.confirm": "Сигурни ли сте, че искате да изтриете всички спомени? Това ще премахне всички записи на спомени завинаги и не може да бъде отменено.",
|
||||
"purge.error": "Неуспешно изчистване на спомените. Моля, опитайте отново.",
|
||||
"purge.success": "Всички спомени са изтрити.",
|
||||
"purge.title": "Изчисти всички спомени",
|
||||
"tab.activities": "Дейности",
|
||||
"tab.contexts": "Контексти",
|
||||
"tab.experiences": "Изживявания",
|
||||
|
||||
@@ -231,8 +231,6 @@
|
||||
"providerModels.item.modelConfig.extendParams.options.imageResolution.hint": "За моделите Gemini 3 за генериране на изображения; контролира резолюцията на генерираните изображения.",
|
||||
"providerModels.item.modelConfig.extendParams.options.imageResolution2.hint": "За модели Gemini 3.1 Flash Image; контролира резолюцията на генерираните изображения (поддържа 512px).",
|
||||
"providerModels.item.modelConfig.extendParams.options.reasoningBudgetToken.hint": "За Claude, Qwen3 и подобни; контролира бюджета от токени за разсъждение.",
|
||||
"providerModels.item.modelConfig.extendParams.options.reasoningBudgetToken32k.hint": "За GLM-5 и GLM-4.7; контролира бюджета за токени за разсъждение (максимум 32k).",
|
||||
"providerModels.item.modelConfig.extendParams.options.reasoningBudgetToken80k.hint": "За серията Qwen3; контролира бюджета за токени за разсъждение (максимум 80k).",
|
||||
"providerModels.item.modelConfig.extendParams.options.reasoningEffort.hint": "За OpenAI и други модели с логическо мислене; контролира усилието за разсъждение.",
|
||||
"providerModels.item.modelConfig.extendParams.options.textVerbosity.hint": "За серията GPT-5+; контролира обемността на изходния текст.",
|
||||
"providerModels.item.modelConfig.extendParams.options.thinking.hint": "За някои модели Doubao; позволява на модела да реши дали да мисли задълбочено.",
|
||||
|
||||
+32
-40
@@ -53,14 +53,7 @@
|
||||
"FLUX.1-Kontext-dev.description": "FLUX.1-Kontext-dev е мултимодален модел за генериране и редактиране на изображения от Black Forest Labs, базиран на архитектура Rectified Flow Transformer с 12B параметъра. Фокусира се върху генериране, реконструкция, подобрение и редакция на изображения според зададен контекст. Комбинира контролираната генерация на дифузионни модели с контекстното моделиране на Transformer, поддържайки висококачествени резултати за задачи като inpainting, outpainting и реконструкция на визуални сцени.",
|
||||
"FLUX.1-Kontext-pro.description": "FLUX.1 Kontext [pro]",
|
||||
"FLUX.1-dev.description": "FLUX.1-dev е мултимодален езиков модел с отворен код (MLLM) от Black Forest Labs, оптимизиран за задачи с изображения и текст, комбиниращ разбиране и генериране на изображения/текст. Изграден върху напреднали LLM модели (като Mistral-7B), използва внимателно проектиран визуален енкодер и многоетапна настройка с инструкции за постигане на мултимодална координация и логическо мислене при сложни задачи.",
|
||||
"GLM-4.5-Air.description": "GLM-4.5-Air: Олекотена версия за бързи отговори.",
|
||||
"GLM-4.5.description": "GLM-4.5: Високопроизводителен модел за разсъждения, програмиране и задачи с агенти.",
|
||||
"GLM-4.6.description": "GLM-4.6: Модел от предишно поколение.",
|
||||
"GLM-4.7.description": "GLM-4.7 е най-новият водещ модел на Zhipu, подобрен за сценарии на агентно програмиране с усъвършенствани възможности за кодиране, дългосрочно планиране на задачи и сътрудничество с инструменти.",
|
||||
"GLM-5-Turbo.description": "GLM-5-Turbo: Оптимизирана версия на GLM-5 с по-бързо извеждане за задачи по програмиране.",
|
||||
"GLM-5.description": "GLM-5 е водещ модел от следващо поколение на Zhipu, създаден за агентно инженерство. Той осигурява надеждна продуктивност в сложни системни инженерни задачи и дългосрочни агентни задачи. В областта на програмирането и агентните способности GLM-5 постига най-добри резултати сред моделите с отворен код.",
|
||||
"Gryphe/MythoMax-L2-13b.description": "MythoMax-L2 (13B) е иновативен модел за разнообразни области и сложни задачи.",
|
||||
"HY-Image-V3.0.description": "Мощни възможности за извличане на характеристики от оригиналното изображение и запазване на детайлите, предоставящи по-богата визуална текстура и създаващи високоточни, добре композирани, продукционни визуализации.",
|
||||
"HelloMeme.description": "HelloMeme е AI инструмент, който генерира мемета, GIF-ове или кратки видеа от предоставени изображения или движения. Не изисква умения за рисуване или програмиране — само референтно изображение — за създаване на забавно, атрактивно и стилово консистентно съдържание.",
|
||||
"HiDream-E1-Full.description": "HiDream-E1-Full е модел за отворен код за мултимодално редактиране на изображения от HiDream.ai, базиран на усъвършенствана архитектура Diffusion Transformer и силно езиково разбиране (вграден LLaMA 3.1-8B-Instruct). Той поддържа генериране на изображения, трансфер на стилове, локални редакции и прерисуване, управлявани от естествен език, с отлично разбиране и изпълнение на текст и изображения.",
|
||||
"HiDream-I1-Full.description": "HiDream-I1 е нов модел за генериране на изображения с отворен код, пуснат от HiDream. С 17 милиарда параметри (Flux има 12 милиарда), той може да предостави водещо в индустрията качество на изображенията за секунди.",
|
||||
@@ -91,14 +84,14 @@
|
||||
"MiniMax-M2.1-highspeed.description": "Мощни многоезични програмни възможности с по-бързо и ефективно извеждане.",
|
||||
"MiniMax-M2.1.description": "MiniMax-M2.1 е водеща отворена голяма езикова система от MiniMax, фокусирана върху решаването на сложни реални задачи. Основните ѝ предимства са възможностите за програмиране на множество езици и способността да действа като агент за решаване на сложни задачи.",
|
||||
"MiniMax-M2.5-Lightning.description": "M2.5 Lightning: Същата производителност, по-бърз и по-агилен (приблизително 100 tps).",
|
||||
"MiniMax-M2.5-highspeed.description": "MiniMax M2.5 Highspeed: Същата производителност като M2.5, но с по-бързо извеждане.",
|
||||
"MiniMax-M2.5-highspeed.description": "Същата производителност като M2.5, но с значително по-бързо извеждане.",
|
||||
"MiniMax-M2.5.description": "MiniMax-M2.5 е водещ модел с отворен код от MiniMax, фокусиран върху решаването на сложни реални задачи. Основните му предимства са мултиезиковите програмни възможности и способността да решава сложни задачи като агент.",
|
||||
"MiniMax-M2.7-highspeed.description": "MiniMax M2.7 Highspeed: Същата производителност като M2.7, но със значително по-бързо извеждане.",
|
||||
"MiniMax-M2.7.description": "MiniMax M2.7: Начало на пътя към рекурсивно самоусъвършенстване, водещи инженерни способности в реалния свят.",
|
||||
"MiniMax-M2.description": "MiniMax M2: Модел от предишно поколение.",
|
||||
"MiniMax-M2.7-highspeed.description": "Същата производителност като M2.7, но със значително по-бързо извеждане (~100 tps).",
|
||||
"MiniMax-M2.7.description": "Първият саморазвиващ се модел с първокласна производителност в кодирането и агентните задачи (~60 tps).",
|
||||
"MiniMax-M2.description": "Създаден специално за ефективно програмиране и работни потоци с агенти",
|
||||
"MiniMax-Text-01.description": "MiniMax-01 въвежда мащабно линейно внимание отвъд класическите трансформери, с 456B параметри и 45.9B активирани на преминаване. Постига водеща производителност и поддържа до 4M токена контекст (32× GPT-4o, 20× Claude-3.5-Sonnet).",
|
||||
"MiniMaxAI/MiniMax-M1-80k.description": "MiniMax-M1 е модел за хибридно внимание с отворени тегла, съдържащ 456 милиарда общи параметри и ~45.9 милиарда активни на токен. Той поддържа контекст от 1 милион токена и използва Flash Attention за намаляване на FLOPs с 75% при генериране на 100K токена спрямо DeepSeek R1. С архитектура MoE плюс CISPO и обучение с хибридно внимание RL, той постига водещи резултати в задачи за дългосрочно разсъждение и реално софтуерно инженерство.",
|
||||
"MiniMaxAI/MiniMax-M2.description": "MiniMax-M2 преосмисля ефективността на агентите. Това е компактен, бърз и икономичен модел MoE с 230 милиарда общи и 10 милиарда активни параметри, създаден за водещи задачи по програмиране и агенти, като същевременно запазва силен общ интелект. Със само 10 милиарда активни параметри, той съперничи на много по-големи модели, което го прави идеален за приложения с висока ефективност.",
|
||||
"MiniMaxAI/MiniMax-M1-80k.description": "MiniMax-M1 е отворен модел с голям мащаб и хибридно внимание, с общо 456B параметри и ~45.9B активни на токен. Поддържа нативно 1M контекст и използва Flash Attention за 75% по-малко FLOPs при генериране на 100K токена спрямо DeepSeek R1. С MoE архитектура, CISPO и хибридно обучение с внимание и RL, постига водеща производителност при дълги входове и реални задачи по софтуерно инженерство.",
|
||||
"MiniMaxAI/MiniMax-M2.description": "MiniMax-M2 преосмисля ефективността на агентите. Това е компактен, бърз и икономичен MoE модел с 230B общо и 10B активни параметри, създаден за водещи задачи по програмиране и агенти, като същевременно запазва силен общ интелект. Със само 10B активни параметри, съперничи на много по-големи модели, което го прави идеален за приложения с висока ефективност.",
|
||||
"Moonshot-Kimi-K2-Instruct.description": "1T общи параметри с 32B активни. Сред немислещите модели е водещ в гранични знания, математика и програмиране, и по-силен в общи агентски задачи. Оптимизиран за агентски натоварвания, може да предприема действия, а не само да отговаря на въпроси. Най-подходящ за импровизационен, общ чат и агентски преживявания като модел на рефлексно ниво без дълго мислене.",
|
||||
"NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO.description": "Nous Hermes 2 - Mixtral 8x7B-DPO (46.7B) е високоточен модел с инструкции за сложни изчисления.",
|
||||
"OmniConsistency.description": "OmniConsistency подобрява стиловата последователност и обобщението при задачи от изображение към изображение чрез въвеждане на мащабни дифузионни трансформери (DiTs) и сдвоени стилизирани данни, избягвайки влошаване на стила.",
|
||||
@@ -112,14 +105,14 @@
|
||||
"Phi-3.5-mini-instruct.description": "Актуализирана версия на модела Phi-3-mini.",
|
||||
"Phi-3.5-vision-instrust.description": "Актуализирана версия на модела Phi-3-vision.",
|
||||
"Pro/MiniMaxAI/MiniMax-M2.1.description": "MiniMax-M2.1 е отворен модел с голям езиков капацитет, оптимизиран за агентни способности, с изключителни резултати в програмиране, използване на инструменти, следване на инструкции и дългосрочно планиране. Моделът поддържа многоезична разработка на софтуер и изпълнение на сложни многoетапни работни потоци, постигайки резултат от 74.0 в SWE-bench Verified и надминава Claude Sonnet 4.5 в многоезични сценарии.",
|
||||
"Pro/MiniMaxAI/MiniMax-M2.5.description": "MiniMax-M2.5 е най-новият голям езиков модел, разработен от MiniMax, обучен чрез мащабно обучение с подсилване в стотици хиляди сложни, реални среди. С архитектура MoE и 229 милиарда параметри, той постига водещи резултати в задачи като програмиране, използване на инструменти от агенти, търсене и офис сценарии.",
|
||||
"Pro/MiniMaxAI/MiniMax-M2.5.description": "MiniMax-M2.5 е най-новият голям езиков модел, разработен от MiniMax, обучен чрез мащабно подсилващо обучение в стотици хиляди сложни реални среди. С архитектура MoE и 229 милиарда параметри, той постига водещи в индустрията резултати в задачи като програмиране, извикване на инструменти от агенти, търсене и офис сценарии.",
|
||||
"Pro/Qwen/Qwen2-7B-Instruct.description": "Qwen2-7B-Instruct е 7B модел с инструкции от серията Qwen2. Използва трансформерна архитектура със SwiGLU, QKV bias и групирано внимание, и обработва големи входове. Постига отлични резултати в езиково разбиране, генериране, многоезични задачи, програмиране, математика и разсъждение, надминавайки повечето отворени модели и конкурирайки се със затворени.",
|
||||
"Pro/Qwen/Qwen2.5-7B-Instruct.description": "Qwen2.5-7B-Instruct е част от най-новата серия LLM на Alibaba Cloud. Моделът с 7B параметри носи значителни подобрения в програмирането и математиката, поддържа над 29 езика и подобрява следването на инструкции, разбирането на структурирани данни и генерирането на структурирани изходи (особено JSON).",
|
||||
"Pro/Qwen/Qwen2.5-Coder-7B-Instruct.description": "Qwen2.5-Coder-7B-Instruct е най-новият LLM на Alibaba Cloud, фокусиран върху програмиране. Изграден върху Qwen2.5 и обучен с 5.5T токена, значително подобрява генерирането на код, разсъждението и поправката, като същевременно запазва силни математически и общи способности, осигурявайки стабилна основа за кодови агенти.",
|
||||
"Pro/Qwen/Qwen2.5-VL-7B-Instruct.description": "Qwen2.5-VL е нов модел за визия и език от серията Qwen с мощно визуално разбиране. Анализира текст, графики и оформления в изображения, разбира дълги видеа и събития, поддържа разсъждение и използване на инструменти, обвързване на обекти във формати, и структурирани изходи. Подобрява динамичната резолюция и обучението с честота на кадрите за видео разбиране и повишава ефективността на визуалния енкодер.",
|
||||
"Pro/THUDM/GLM-4.1V-9B-Thinking.description": "GLM-4.1V-9B-Thinking е отворен VLM модел, разработен от Zhipu AI и лабораторията KEG на университета Цинхуа, създаден за сложна мултимодална когниция. Базиран на GLM-4-9B-0414, той добавя верижно разсъждение (chain-of-thought) и обучение чрез подсилване (RL), което значително подобрява между-модалното разсъждение и стабилността.",
|
||||
"Pro/THUDM/glm-4-9b-chat.description": "GLM-4-9B-Chat е отворен GLM-4 модел от Zhipu AI. Демонстрира високи резултати в семантика, математика, логическо мислене, програмиране и знания. Освен многозавойни разговори, поддържа уеб сърфиране, изпълнение на код, извикване на персонализирани инструменти и разсъждение върху дълги текстове. Поддържа 26 езика (включително китайски, английски, японски, корейски, немски). Представя се отлично в AlignBench-v2, MT-Bench, MMLU и C-Eval и поддържа до 128K контекст за академични и бизнес приложения.",
|
||||
"Pro/deepseek-ai/DeepSeek-R1-Distill-Qwen-7B.description": "DeepSeek-R1-Distill-Qwen-7B е дистилиран от Qwen2.5-Math-7B и фино настроен върху 800K подбрани проби от DeepSeek-R1. Той показва силни резултати: 92.8% на MATH-500, 55.5% на AIME 2024 и рейтинг 1189 на CodeForces за модел с 7 милиарда параметри.",
|
||||
"Pro/deepseek-ai/DeepSeek-R1-Distill-Qwen-7B.description": "DeepSeek-R1-Distill-Qwen-7B е дестилиран от Qwen2.5-Math-7B и фино настроен с 800K подбрани проби от DeepSeek-R1. Постига отлични резултати: 92.8% на MATH-500, 55.5% на AIME 2024 и рейтинг 1189 в CodeForces за 7B модел.",
|
||||
"Pro/deepseek-ai/DeepSeek-R1.description": "DeepSeek-R1 е модел за разсъждение, базиран на обучение чрез подсилване (RL), който намалява повторенията и подобрява четимостта. Използва cold-start данни преди RL, за да засили разсъждението, съпоставя се с OpenAI-o1 при задачи по математика, код и логика и подобрява общите резултати чрез внимателно обучение.",
|
||||
"Pro/deepseek-ai/DeepSeek-V3.1-Terminus.description": "DeepSeek-V3.1-Terminus е обновен модел от серията V3.1, позициониран като хибриден агентен LLM. Отстранява докладвани от потребители проблеми и подобрява стабилността, езиковата последователност и намалява смесването на китайски/английски и аномални символи. Интегрира режими с и без разсъждение с шаблони за чат за гъвкаво превключване. Подобрява и производителността на Code Agent и Search Agent за по-надеждно използване на инструменти и многoетапни задачи.",
|
||||
"Pro/deepseek-ai/DeepSeek-V3.2.description": "DeepSeek-V3.2 е модел, който съчетава висока изчислителна ефективност с отлично разсъждение и производителност като агент. Подходът му се основава на три ключови технологични пробива: DeepSeek Sparse Attention (DSA), ефективен механизъм за внимание, който значително намалява изчислителната сложност, като същевременно поддържа производителността на модела и е специално оптимизиран за сценарии с дълъг контекст; мащабируема рамка за подсилващо обучение, чрез която производителността на модела може да съперничи на GPT-5, а версията с висока изчислителна мощност съответства на Gemini-3.0-Pro по способности за разсъждение; и мащабна тръбопроводна система за синтез на задачи за агенти, насочена към интегриране на способности за разсъждение в сценарии за използване на инструменти, като по този начин подобрява следването на инструкции и обобщаването в сложни интерактивни среди. Моделът постигна златен медал на Международната математическа олимпиада (IMO) и Международната олимпиада по информатика (IOI) през 2025 г.",
|
||||
@@ -127,10 +120,10 @@
|
||||
"Pro/moonshotai/Kimi-K2-Instruct-0905.description": "Kimi K2-Instruct-0905 е най-новият и най-мощен модел от серията Kimi K2. Това е MoE модел от най-висок клас с 1T общо и 32B активни параметъра. Основните му предимства включват по-силна агентна интелигентност при програмиране с значителни подобрения в бенчмаркове и реални задачи, както и подобрена естетика и използваемост на фронтенд кода.",
|
||||
"Pro/moonshotai/Kimi-K2-Thinking.description": "Kimi K2 Thinking Turbo е ускорен вариант, оптимизиран за скорост на разсъждение и пропускателна способност, като запазва многoетапното разсъждение и използване на инструменти от K2 Thinking. Това е MoE модел с ~1T общи параметри, роден 256K контекст и стабилно мащабируемо извикване на инструменти за производствени сценарии с по-строги изисквания за латентност и едновременност.",
|
||||
"Pro/moonshotai/Kimi-K2.5.description": "Kimi K2.5 е отворен мултимодален агентен модел, базиран на Kimi-K2-Base, обучен върху приблизително 1.5 трилиона смесени визуални и текстови токени. Моделът използва MoE архитектура с общо 1T параметри и 32B активни параметри, поддържа контекстен прозорец от 256K и безпроблемно интегрира визуално и езиково разбиране.",
|
||||
"Pro/zai-org/glm-4.7.description": "GLM-4.7 е новото поколение водещ модел на Zhipu с 355 милиарда общи параметри и 32 милиарда активни параметри, напълно обновен за общ диалог, разсъждения и агентни способности. GLM-4.7 подобрява преплетеното мислене и въвежда запазено мислене и мислене на ниво завой.",
|
||||
"Pro/zai-org/glm-4.7.description": "GLM-4.7 е най-новият флагмански модел на Zhipu с общо 355 милиарда параметъра и 32 милиарда активни параметъра. Той е напълно обновен в областите на общ диалог, логическо мислене и агентни способности. GLM-4.7 подобрява Междинното мислене и въвежда Запазено мислене и Мислене на ниво обръщение.",
|
||||
"Pro/zai-org/glm-5.description": "GLM-5 е следващото поколение голям езиков модел на Zhipu, фокусиран върху сложното системно инженерство и задачи на агенти с дълга продължителност. Параметрите на модела са разширени до 744 милиарда (40 милиарда активни) и интегрират DeepSeek Sparse Attention.",
|
||||
"QwQ-32B-Preview.description": "Qwen QwQ е експериментален изследователски модел, фокусиран върху подобряване на разсъждението.",
|
||||
"Qwen/QVQ-72B-Preview.description": "QVQ-72B-Preview е изследователски модел от Qwen, фокусиран върху визуално разсъждение, със силни страни в разбирането на сложни сцени и визуални математически задачи.",
|
||||
"Qwen/QVQ-72B-Preview.description": "QVQ-72B-Preview е изследователски модел от Qwen, насочен към визуално разсъждение, със силни страни в разбирането на сложни сцени и визуални математически задачи.",
|
||||
"Qwen/QwQ-32B-Preview.description": "Qwen QwQ е експериментален изследователски модел, фокусиран върху подобрено AI разсъждение.",
|
||||
"Qwen/QwQ-32B.description": "QwQ е модел за разсъждение от семейството Qwen. В сравнение със стандартните модели, настроени по инструкции, той добавя мисловни и логически способности, които значително подобряват представянето при трудни задачи. QwQ-32B е среден по размер модел, съпоставим с водещи модели за разсъждение като DeepSeek-R1 и o1-mini. Използва RoPE, SwiGLU, RMSNorm и QKV bias в вниманието, с 64 слоя и 40 Q глави (8 KV в GQA).",
|
||||
"Qwen/Qwen-Image-Edit-2509.description": "Qwen-Image-Edit-2509 е най-новата версия за редактиране на изображения от екипа на Qwen. Базиран на 20B модела Qwen-Image, той разширява силното текстово рендиране към редактиране на изображения за прецизни текстови промени. Използва двуканална архитектура – входовете се подават към Qwen2.5-VL за семантичен контрол и към VAE енкодер за контрол на външния вид, което позволява редакции както на семантично, така и на визуално ниво. Поддържа локални редакции (добавяне/премахване/промяна) и по-високо ниво на семантични промени като създаване на IP и трансфер на стил, като същевременно запазва смисъла. Постига SOTA резултати в множество бенчмаркове.",
|
||||
@@ -214,11 +207,11 @@
|
||||
"Skylark2-pro-turbo-8k.description": "Модел от второ поколение Skylark. Skylark2-pro-turbo-8k предлага по-бърза инференция на по-ниска цена с контекстен прозорец от 8K.",
|
||||
"THUDM/GLM-4-32B-0414.description": "GLM-4-32B-0414 е следващо поколение отворен GLM модел с 32 милиарда параметъра, сравним по производителност с OpenAI GPT и сериите DeepSeek V3/R1.",
|
||||
"THUDM/GLM-4-9B-0414.description": "GLM-4-9B-0414 е 9-милиарден GLM модел, който наследява технологиите на GLM-4-32B, като същевременно предлага по-леко внедряване. Представя се добре в генериране на код, уеб дизайн, създаване на SVG и писане, базирано на търсене.",
|
||||
"THUDM/GLM-4.1V-9B-Thinking.description": "GLM-4.1V-9B-Thinking е модел с отворен код от Zhipu AI и лабораторията KEG на университета Цинхуа, създаден за сложна мултимодална когниция. Построен върху GLM-4-9B-0414, той добавя разсъждения чрез верига от мисли и RL за значително подобряване на кръстомодалното разсъждение и стабилност.",
|
||||
"THUDM/GLM-4.1V-9B-Thinking.description": "GLM-4.1V-9B-Thinking е отворен VLM модел от Zhipu AI и лабораторията KEG на Цинхуа, създаден за сложна мултимодална когниция. Изграден върху GLM-4-9B-0414, добавя верижно разсъждение и подсилено обучение (RL), значително подобрявайки между-модалното разсъждение и стабилността.",
|
||||
"THUDM/GLM-Z1-32B-0414.description": "GLM-Z1-32B-0414 е модел за дълбоко разсъждение, изграден от GLM-4-32B-0414 с данни за студен старт и разширено подсилено обучение, допълнително обучен върху математика, код и логика. Значително подобрява способността за решаване на сложни задачи спрямо базовия модел.",
|
||||
"THUDM/GLM-Z1-9B-0414.description": "GLM-Z1-9B-0414 е компактен GLM модел с 9 милиарда параметъра, който запазва силните страни на отворения код, като същевременно предлага впечатляващи възможности. Представя се отлично в математическо разсъждение и общи задачи, водещ в своя клас сред отворените модели.",
|
||||
"THUDM/glm-4-9b-chat.description": "GLM-4-9B-Chat е отвореният GLM-4 модел от Zhipu AI. Представя се силно в семантика, математика, разсъждение, код и знания. Освен многозавойни чатове, поддържа уеб браузване, изпълнение на код, извикване на персонализирани инструменти и разсъждение върху дълги текстове. Поддържа 26 езика (включително китайски, английски, японски, корейски, немски). Представя се добре в AlignBench-v2, MT-Bench, MMLU и C-Eval и поддържа до 128K контекст за академична и бизнес употреба.",
|
||||
"Tongyi-Zhiwen/QwenLong-L1-32B.description": "QwenLong-L1-32B е първият модел за разсъждение с дълъг контекст (LRM), обучен с RL, оптимизиран за разсъждение върху дълги текстове. Неговото прогресивно разширяване на контекста чрез RL позволява стабилен преход от кратък към дълъг контекст. Той надминава OpenAI-o3-mini и Qwen3-235B-A22B на седем бенчмарка за QA върху документи с дълъг контекст, съперничейки на Claude-3.7-Sonnet-Thinking. Особено силен е в математика, логика и многократни разсъждения.",
|
||||
"Tongyi-Zhiwen/QwenLong-L1-32B.description": "QwenLong-L1-32B е първият модел за разсъждение с дълъг контекст (LRM), обучен с подсилено обучение, оптимизиран за разсъждение върху дълги текстове. Неговото прогресивно разширяване на контекста чрез RL позволява стабилен преход от кратък към дълъг контекст. Надминава OpenAI-o3-mini и Qwen3-235B-A22B в седем бенчмарка за въпроси и отговори върху документи с дълъг контекст, съперничи на Claude-3.7-Sonnet-Thinking. Особено силен е в математика, логика и многозвенно разсъждение.",
|
||||
"Yi-34B-Chat.description": "Yi-1.5-34B запазва силните езикови способности на серията, като използва инкрементално обучение върху 500 милиарда висококачествени токена, за да подобри значително логиката в математиката и програмирането.",
|
||||
"abab5.5-chat.description": "Създаден за продуктивни сценарии с обработка на сложни задачи и ефективно генериране на текст за професионална употреба.",
|
||||
"abab5.5s-chat.description": "Проектиран за чат с китайски персонажи, осигуряващ висококачествен диалог на китайски език за различни приложения.",
|
||||
@@ -310,15 +303,15 @@
|
||||
"claude-3.5-sonnet.description": "Claude 3.5 Sonnet се отличава в програмиране, писане и сложни разсъждения.",
|
||||
"claude-3.7-sonnet-thought.description": "Claude 3.7 Sonnet с разширено мислене за задачи, изискващи сложни разсъждения.",
|
||||
"claude-3.7-sonnet.description": "Claude 3.7 Sonnet е надградена версия с разширен контекст и възможности.",
|
||||
"claude-haiku-4-5-20251001.description": "Claude Haiku 4.5 е най-бързият и интелигентен модел Haiku на Anthropic, с мълниеносна скорост и разширено мислене.",
|
||||
"claude-haiku-4-5-20251001.description": "Claude Haiku 4.5 е най-бързият и интелигентен Haiku модел на Anthropic, с мълниеносна скорост и разширено мислене.",
|
||||
"claude-haiku-4.5.description": "Claude Haiku 4.5 е бърз и ефективен модел за различни задачи.",
|
||||
"claude-opus-4-1-20250805-thinking.description": "Claude Opus 4.1 Thinking е усъвършенстван вариант, който може да разкрие процеса си на разсъждение.",
|
||||
"claude-opus-4-1-20250805.description": "Claude Opus 4.1 е най-новият и най-способен модел на Anthropic за силно сложни задачи, отличаващ се с производителност, интелигентност, плавност и разбиране.",
|
||||
"claude-opus-4-20250514.description": "Claude Opus 4 е най-мощният модел на Anthropic за силно сложни задачи, отличаващ се с производителност, интелигентност, плавност и разбиране.",
|
||||
"claude-opus-4-1-20250805.description": "Claude Opus 4.1 е най-новият и най-способен модел на Anthropic за изключително сложни задачи, превъзхождащ в производителност, интелигентност, плавност и разбиране.",
|
||||
"claude-opus-4-20250514.description": "Claude Opus 4 е най-мощният модел на Anthropic за изключително сложни задачи, превъзхождащ в производителност, интелигентност, плавност и разбиране.",
|
||||
"claude-opus-4-5-20251101.description": "Claude Opus 4.5 е флагманският модел на Anthropic, комбиниращ изключителна интелигентност с мащабируема производителност, идеален за сложни задачи, изискващи най-висококачествени отговори и разсъждение.",
|
||||
"claude-opus-4-6.description": "Claude Opus 4.6 е най-интелигентният модел на Anthropic за изграждане на агенти и програмиране.",
|
||||
"claude-sonnet-4-20250514-thinking.description": "Claude Sonnet 4 Thinking може да генерира почти мигновени отговори или разширено стъпково мислене с видим процес.",
|
||||
"claude-sonnet-4-20250514.description": "Claude Sonnet 4 е най-интелигентният модел на Anthropic досега, предлагащ почти мигновени отговори или разширено мислене стъпка по стъпка с фино управление за API потребители.",
|
||||
"claude-sonnet-4-20250514.description": "Claude Sonnet 4 е най-интелигентният модел на Anthropic досега, предлагащ почти мигновени отговори или разширено стъпка по стъпка мислене с прецизен контрол за API потребители.",
|
||||
"claude-sonnet-4-5-20250929.description": "Claude Sonnet 4.5 е най-интелигентният модел на Anthropic досега.",
|
||||
"claude-sonnet-4-6.description": "Claude Sonnet 4.6 е най-добрата комбинация от скорост и интелигентност на Anthropic.",
|
||||
"claude-sonnet-4.description": "Claude Sonnet 4 е най-новото поколение с подобрена производителност във всички задачи.",
|
||||
@@ -377,7 +370,7 @@
|
||||
"deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B.description": "Дестилираните модели DeepSeek-R1 използват RL и cold-start данни за подобряване на разсъждението и поставят нови бенчмарк стандарти за отворени модели с много задачи.",
|
||||
"deepseek-ai/DeepSeek-R1-Distill-Qwen-14B.description": "Дестилираните модели DeepSeek-R1 използват RL и cold-start данни за подобряване на разсъждението и поставят нови бенчмарк стандарти за отворени модели с много задачи.",
|
||||
"deepseek-ai/DeepSeek-R1-Distill-Qwen-32B.description": "DeepSeek-R1-Distill-Qwen-32B е дестилиран от Qwen2.5-32B и фино настроен върху 800K подбрани проби от DeepSeek-R1. Отличава се в математика, програмиране и разсъждение, постигайки силни резултати на AIME 2024, MATH-500 (94.3% точност) и GPQA Diamond.",
|
||||
"deepseek-ai/DeepSeek-R1-Distill-Qwen-7B.description": "DeepSeek-R1-Distill-Qwen-7B е дистилиран от Qwen2.5-Math-7B и фино настроен върху 800K подбрани проби от DeepSeek-R1. Той показва силни резултати: 92.8% на MATH-500, 55.5% на AIME 2024 и рейтинг 1189 на CodeForces за модел с 7 милиарда параметри.",
|
||||
"deepseek-ai/DeepSeek-R1-Distill-Qwen-7B.description": "DeepSeek-R1-Distill-Qwen-7B е дестилиран от Qwen2.5-Math-7B и фино настроен върху 800K подбрани проби от DeepSeek-R1. Представя се силно с 92.8% на MATH-500, 55.5% на AIME 2024 и рейтинг 1189 в CodeForces за 7B модел.",
|
||||
"deepseek-ai/DeepSeek-R1.description": "DeepSeek-R1 подобрява разсъждението с RL и cold-start данни, поставяйки нови бенчмарк стандарти за отворени модели с много задачи и надминава OpenAI-o1-mini.",
|
||||
"deepseek-ai/DeepSeek-V2.5.description": "DeepSeek-V2.5 надгражда DeepSeek-V2-Chat и DeepSeek-Coder-V2-Instruct, комбинирайки общи и кодови способности. Подобрява писането и следването на инструкции за по-добро съответствие с предпочитанията и показва значителни подобрения в AlpacaEval 2.0, ArenaHard, AlignBench и MT-Bench.",
|
||||
"deepseek-ai/DeepSeek-V3.1-Terminus.description": "DeepSeek-V3.1-Terminus е обновен модел V3.1, позициониран като хибриден агентен LLM. Отстранява докладвани от потребители проблеми и подобрява стабилността, езиковата последователност и намалява смесените китайски/английски и аномални символи. Интегрира режими на мислене и немислене с шаблони за чат за гъвкаво превключване. Подобрява и производителността на Code Agent и Search Agent за по-надеждно използване на инструменти и многоетапни задачи.",
|
||||
@@ -390,7 +383,7 @@
|
||||
"deepseek-ai/deepseek-v3.1.description": "DeepSeek V3.1 е модел за разсъждение от ново поколение с по-силни способности за сложни разсъждения и верига от мисли за задълбочени аналитични задачи.",
|
||||
"deepseek-ai/deepseek-v3.2.description": "DeepSeek V3.2 е модел за разсъждение от следващо поколение с по-силни способности за сложни разсъждения и верига на мисълта.",
|
||||
"deepseek-ai/deepseek-vl2.description": "DeepSeek-VL2 е MoE модел за визия и език, базиран на DeepSeekMoE-27B със слаба активация, постигайки висока производителност с едва 4.5 милиарда активни параметъра. Отличава се в визуални въпроси и отговори, OCR, разбиране на документи/таблици/графики и визуално привързване.",
|
||||
"deepseek-chat.description": "DeepSeek V3.2 балансира разсъжденията и дължината на изхода за ежедневни QA и задачи с агенти. Публичните бенчмаркове достигат нивата на GPT-5, и той е първият, който интегрира мислене в използването на инструменти, водещ в оценките на агенти с отворен код.",
|
||||
"deepseek-chat.description": "DeepSeek V3.2 балансира разсъжденията и дължината на изхода за ежедневни QA и агентски задачи. Публичните бенчмаркове достигат нива на GPT-5 и това е първият модел, който интегрира мислене в използването на инструменти, водещ до високи оценки в отворените източници за агенти.",
|
||||
"deepseek-coder-33B-instruct.description": "DeepSeek Coder 33B е езиков модел за програмиране, обучен върху 2 трилиона токени (87% код, 13% китайски/английски текст). Въвежда 16K контекстен прозорец и задачи за попълване в средата, осигурявайки допълване на код на ниво проект и попълване на фрагменти.",
|
||||
"deepseek-coder-v2.description": "DeepSeek Coder V2 е отворен MoE модел за програмиране, който се представя на ниво GPT-4 Turbo.",
|
||||
"deepseek-coder-v2:236b.description": "DeepSeek Coder V2 е отворен MoE модел за програмиране, който се представя на ниво GPT-4 Turbo.",
|
||||
@@ -413,7 +406,7 @@
|
||||
"deepseek-r1-fast-online.description": "Пълна бърза версия на DeepSeek R1 с търсене в реално време в уеб, комбинираща възможности от мащаб 671B и по-бърз отговор.",
|
||||
"deepseek-r1-online.description": "Пълна версия на DeepSeek R1 с 671 милиарда параметъра и търсене в реално време в уеб, предлагаща по-силно разбиране и генериране.",
|
||||
"deepseek-r1.description": "DeepSeek-R1 използва данни от студен старт преди подсиленото обучение и се представя наравно с OpenAI-o1 в математика, програмиране и разсъждение.",
|
||||
"deepseek-reasoner.description": "DeepSeek V3.2 Thinking е модел за дълбоко разсъждение, който генерира верига от мисли преди изходите за по-висока точност, с водещи резултати в състезания и разсъждения, сравними с Gemini-3.0-Pro.",
|
||||
"deepseek-reasoner.description": "DeepSeek V3.2 Thinking е модел за дълбоко разсъждение, който генерира верига от мисли преди изходите за по-висока точност, с топ резултати в конкуренцията и разсъждения, сравними с Gemini-3.0-Pro.",
|
||||
"deepseek-v2.description": "DeepSeek V2 е ефективен MoE модел за икономична обработка.",
|
||||
"deepseek-v2:236b.description": "DeepSeek V2 236B е модел на DeepSeek, фокусиран върху програмиране, с висока производителност при генериране на код.",
|
||||
"deepseek-v3-0324.description": "DeepSeek-V3-0324 е MoE модел с 671 милиарда параметъра, с изключителни способности в програмиране, технически задачи, разбиране на контекст и обработка на дълги текстове.",
|
||||
@@ -424,7 +417,7 @@
|
||||
"deepseek-v3.2-exp.description": "deepseek-v3.2-exp въвежда разредено внимание за подобряване на ефективността при обучение и извеждане върху дълги текстове, на по-ниска цена от deepseek-v3.1.",
|
||||
"deepseek-v3.2-speciale.description": "При силно сложни задачи, моделът Speciale значително превъзхожда стандартната версия, но консумира значително повече токени и води до по-високи разходи. В момента DeepSeek-V3.2-Speciale е предназначен само за изследователска употреба, не поддържа използване на инструменти и не е специално оптимизиран за ежедневни разговори или задачи за писане.",
|
||||
"deepseek-v3.2-think.description": "DeepSeek V3.2 Think е пълен модел за дълбоко мислене с по-силно дълговерижно разсъждение.",
|
||||
"deepseek-v3.2.description": "DeepSeek-V3.2 е най-новият модел за програмиране на DeepSeek със силни способности за разсъждение.",
|
||||
"deepseek-v3.2.description": "DeepSeek-V3.2 е първият хибриден модел за логическо мислене от DeepSeek, който интегрира мисленето в използването на инструменти. Използва ефективна архитектура за намаляване на изчислителните ресурси, мащабно обучение с подсилване за повишаване на способностите и синтетични задачи в голям мащаб за по-добра обобщаемост. Комбинацията от тези три елемента постига производителност, сравнима с GPT-5-High, със значително по-кратки изходни текстове, което намалява изчислителното натоварване и времето за изчакване на потребителя.",
|
||||
"deepseek-v3.description": "DeepSeek-V3 е мощен MoE модел с общо 671 милиарда параметъра и 37 милиарда активни на токен.",
|
||||
"deepseek-vl2-small.description": "DeepSeek VL2 Small е лек мултимодален вариант за среди с ограничени ресурси и висока едновременност.",
|
||||
"deepseek-vl2.description": "DeepSeek VL2 е мултимодален модел за разбиране на изображения и текст и прецизни визуални въпроси и отговори.",
|
||||
@@ -513,8 +506,8 @@
|
||||
"ernie-x1-turbo-32k.description": "ERNIE X1 Turbo 32K е бърз мислещ модел с 32K контекст за сложни разсъждения и многозавойни разговори.",
|
||||
"ernie-x1.1-preview.description": "ERNIE X1.1 Preview е предварителен модел за мислене, предназначен за оценка и тестване.",
|
||||
"ernie-x1.1.description": "ERNIE X1.1 е мисловен модел за предварителен преглед за оценка и тестване.",
|
||||
"fal-ai/bytedance/seedream/v4.5.description": "Seedream 4.5, създаден от екипа Seed на ByteDance, поддържа редактиране и композиция на множество изображения. Характеризира се с подобрена консистентност на обектите, прецизно следване на инструкции, разбиране на пространствена логика, естетично изразяване, оформление на плакати и дизайн на лого с високопрецизно текстово-изображение рендиране.",
|
||||
"fal-ai/bytedance/seedream/v4.description": "Seedream 4.0, създаден от ByteDance Seed, поддържа текстови и визуални входове за силно контролируемо, висококачествено генериране на изображения от подсказки.",
|
||||
"fal-ai/bytedance/seedream/v4.5.description": "Seedream 4.5, създаден от екипа на ByteDance Seed, поддържа редактиране и композиция на множество изображения. Характеризира се с подобрена консистентност на обектите, прецизно следване на инструкции, разбиране на пространствена логика, естетическо изразяване, оформление на плакати и дизайн на лога с високопрецизно текстово-изображение рендиране.",
|
||||
"fal-ai/bytedance/seedream/v4.description": "Seedream 4.0, създаден от ByteDance Seed, поддържа текстови и визуални входове за високо контролируемо, висококачествено генериране на изображения от подсказки.",
|
||||
"fal-ai/flux-kontext/dev.description": "FLUX.1 модел, фокусиран върху редактиране на изображения, поддържащ вход от текст и изображения.",
|
||||
"fal-ai/flux-pro/kontext.description": "FLUX.1 Kontext [pro] приема текст и референтни изображения като вход, позволявайки целенасочени локални редакции и сложни глобални трансформации на сцени.",
|
||||
"fal-ai/flux/krea.description": "Flux Krea [dev] е модел за генериране на изображения с естетично предпочитание към по-реалистични и естествени изображения.",
|
||||
@@ -522,8 +515,8 @@
|
||||
"fal-ai/hunyuan-image/v3.description": "Мощен роден мултимодален модел за генериране на изображения.",
|
||||
"fal-ai/imagen4/preview.description": "Модел за висококачествено генериране на изображения от Google.",
|
||||
"fal-ai/nano-banana.description": "Nano Banana е най-новият, най-бърз и най-ефективен роден мултимодален модел на Google, позволяващ генериране и редактиране на изображения чрез разговор.",
|
||||
"fal-ai/qwen-image-edit.description": "Професионален модел за редактиране на изображения от екипа Qwen, поддържащ семантични и визуални редакции, прецизно редактиране на текст на китайски/английски, трансфер на стил, ротация и други.",
|
||||
"fal-ai/qwen-image.description": "Мощен модел за генериране на изображения от екипа Qwen със силно рендиране на китайски текст и разнообразни визуални стилове.",
|
||||
"fal-ai/qwen-image-edit.description": "Професионален модел за редактиране на изображения от екипа на Qwen, поддържащ семантични и визуални редакции, прецизно редактиране на текст на китайски/английски, трансфер на стил, ротация и други.",
|
||||
"fal-ai/qwen-image.description": "Мощен модел за генериране на изображения от екипа на Qwen със силно рендиране на китайски текст и разнообразни визуални стилове.",
|
||||
"flux-1-schnell.description": "Модел за преобразуване на текст в изображение с 12 милиарда параметъра от Black Forest Labs, използващ латентна дифузионна дестилация за генериране на висококачествени изображения в 1–4 стъпки. Съперничи на затворени алтернативи и е пуснат под лиценз Apache-2.0 за лична, изследователска и търговска употреба.",
|
||||
"flux-dev.description": "FLUX.1 [dev] е дестилиран модел с отворени тегла за нетърговска употреба. Запазва почти професионално качество на изображенията и следване на инструкции, като същевременно работи по-ефективно и използва ресурсите по-добре от стандартни модели със същия размер.",
|
||||
"flux-kontext-max.description": "Съвременно генериране и редактиране на изображения с контекст, комбиниращо текст и изображения за прецизни и последователни резултати.",
|
||||
@@ -567,10 +560,10 @@
|
||||
"gemini-2.5-pro.description": "Gemini 2.5 Pro е най-усъвършенстваният модел за разсъждение на Google, способен да разсъждава върху код, математика и STEM проблеми и да анализира големи набори от данни, кодови бази и документи с дълъг контекст.",
|
||||
"gemini-3-flash-preview.description": "Gemini 3 Flash е най-интелигентният модел, създаден за скорост, съчетаващ авангардна интелигентност с отлично търсене и обоснованост.",
|
||||
"gemini-3-pro-image-preview.description": "Gemini 3 Pro Image (Nano Banana Pro) е модел за генериране на изображения на Google, който също поддържа мултимодален диалог.",
|
||||
"gemini-3-pro-image-preview:image.description": "Gemini 3 Pro Image (Nano Banana Pro) е модел на Google за генериране на изображения, който също поддържа мултимодален чат.",
|
||||
"gemini-3-pro-image-preview:image.description": "Gemini 3 Pro Image (Nano Banana Pro) е моделът на Google за генериране на изображения и също така поддържа мултимодален чат.",
|
||||
"gemini-3-pro-preview.description": "Gemini 3 Pro е най-мощният агентен и „vibe-coding“ модел на Google, който предлага по-богати визуализации и по-дълбоко взаимодействие, базирано на съвременно логическо мислене.",
|
||||
"gemini-3.1-flash-image-preview.description": "Gemini 3.1 Flash Image (Nano Banana 2) е най-бързият модел на Google за генериране на изображения с поддръжка на мислене, разговорно генериране и редактиране на изображения.",
|
||||
"gemini-3.1-flash-image-preview:image.description": "Gemini 3.1 Flash Image (Nano Banana 2) предлага качество на изображения от ниво Pro с Flash скорост и поддръжка на мултимодален чат.",
|
||||
"gemini-3.1-flash-image-preview:image.description": "Gemini 3.1 Flash Image (Nano Banana 2) предоставя Pro-качество на изображения с Flash скорост и поддръжка на мултимодален чат.",
|
||||
"gemini-3.1-flash-lite-preview.description": "Gemini 3.1 Flash-Lite Preview е най-икономичният мултимодален модел на Google, оптимизиран за задачи с голям обем, превод и обработка на данни.",
|
||||
"gemini-3.1-pro-preview.description": "Gemini 3.1 Pro Preview подобрява Gemini 3 Pro с усъвършенствани способности за разсъждение и добавя поддръжка за средно ниво на мислене.",
|
||||
"gemini-flash-latest.description": "Най-новата версия на Gemini Flash",
|
||||
@@ -805,7 +798,7 @@
|
||||
"kimi-k2-thinking-turbo.description": "Високоскоростен вариант на K2 с дълбоко мислене, 256k контекст, силно дълбоко разсъждение и скорост на изход от 60–100 токена/сек.",
|
||||
"kimi-k2-thinking.description": "kimi-k2-thinking е мисловен модел на Moonshot AI с общи агентни и разсъждателни способности. Отличава се с дълбоко разсъждение и може да решава трудни задачи чрез многостъпкова употреба на инструменти.",
|
||||
"kimi-k2-turbo-preview.description": "kimi-k2 е MoE базов модел с мощни способности за програмиране и агентни задачи (1T общи параметри, 32B активни), надминаващ други водещи отворени модели в области като разсъждение, програмиране, математика и агентни бенчмаркове.",
|
||||
"kimi-k2.5.description": "Kimi K2.5 е най-универсалният модел на Kimi досега, с родна мултимодална архитектура, която поддържа както визуални, така и текстови входове, режими 'мислене' и 'немислене', както и задачи за разговори и агенти.",
|
||||
"kimi-k2.5.description": "Kimi K2.5 е най-способният модел на Kimi, предоставящ водещи резултати с отворен код в агентни задачи, програмиране и визуално разбиране. Поддържа мултимодални входове и режими с и без мислене.",
|
||||
"kimi-k2.description": "Kimi-K2 е MoE базов модел от Moonshot AI с мощни способности за програмиране и агентни задачи, с общо 1T параметри и 32B активни. В бенчмаркове за общо разсъждение, програмиране, математика и агентни задачи надминава други водещи отворени модели.",
|
||||
"kimi-k2:1t.description": "Kimi K2 е голям MoE LLM от Moonshot AI с 1T общи параметри и 32B активни на всяко преминаване. Оптимизиран е за агентни способности, включително напреднало използване на инструменти, разсъждение и синтез на код.",
|
||||
"kuaishou/kat-coder-pro-v1.description": "KAT-Coder-Pro-V1 (ограничено безплатен) се фокусира върху разбиране на код и автоматизация за ефективни кодиращи агенти.",
|
||||
@@ -967,7 +960,7 @@
|
||||
"moonshot-v1-32k.description": "Moonshot V1 32K поддържа 32 768 токена за средно дълъг контекст, идеален за дълги документи и сложни диалози в създаване на съдържание, отчети и чат системи.",
|
||||
"moonshot-v1-8k-vision-preview.description": "Моделите Kimi vision (включително moonshot-v1-8k-vision-preview/moonshot-v1-32k-vision-preview/moonshot-v1-128k-vision-preview) разбират съдържание на изображения като текст, цветове и форми на обекти.",
|
||||
"moonshot-v1-8k.description": "Moonshot V1 8K е оптимизиран за генериране на кратки текстове с висока ефективност, обработвайки 8 192 токена за кратки чатове, бележки и бързо съдържание.",
|
||||
"moonshotai/Kimi-Dev-72B.description": "Kimi-Dev-72B е модел за програмиране с отворен код, оптимизиран с мащабно RL за създаване на надеждни, готови за производство корекции. Той постига 60.4% на SWE-bench Verified, поставяйки нов рекорд за модели с отворен код в автоматизирани задачи като поправка на грешки и преглед на код.",
|
||||
"moonshotai/Kimi-Dev-72B.description": "Kimi-Dev-72B е отворен кодов езиков модел, оптимизиран с мащабно подсилващо обучение за създаване на стабилни, готови за продукция корекции. Постига 60.4% в SWE-bench Verified, поставяйки нов рекорд сред отворените модели за автоматизирани задачи като отстраняване на грешки и преглед на код.",
|
||||
"moonshotai/Kimi-K2-Instruct-0905.description": "Kimi K2-Instruct-0905 е най-новият и най-мощен модел от серията Kimi K2. Това е MoE модел от най-висок клас с 1T общо и 32B активни параметъра. Основни характеристики включват по-силна агентна интелигентност при програмиране, значителни подобрения в бенчмаркове и реални задачи, както и подобрена естетика и използваемост на фронтенд кода.",
|
||||
"moonshotai/Kimi-K2-Thinking.description": "Kimi K2 Thinking е най-новият и най-мощен модел за мислене с отворен код. Той значително разширява дълбочината на многократното разсъждение и поддържа стабилно използване на инструменти в 200–300 последователни извиквания, поставяйки нови рекорди на Humanity's Last Exam (HLE), BrowseComp и други бенчмаркове. Превъзхожда в кодиране, математика, логика и сценарии с агенти. Изграден на архитектура MoE с ~1 трилион общи параметри, поддържа 256K контекстен прозорец и извикване на инструменти.",
|
||||
"moonshotai/kimi-k2-0711.description": "Kimi K2 0711 е instruct вариант от серията Kimi, подходящ за висококачествен код и използване на инструменти.",
|
||||
@@ -1170,7 +1163,6 @@
|
||||
"qwen3-coder-next.description": "Следващо поколение Qwen кодер, оптимизиран за сложна многокодова генерация, дебъгване и високопроизводителни работни потоци на агенти. Създаден за силна интеграция на инструменти и подобрена производителност на разсъждения.",
|
||||
"qwen3-coder-plus.description": "Модел за програмиране Qwen. Най-новата серия Qwen3-Coder е базирана на Qwen3 и предлага силни способности за програмиране чрез агенти, използване на инструменти и взаимодействие със среди за автономно програмиране, с отлично представяне при код и стабилни общи възможности.",
|
||||
"qwen3-coder:480b.description": "Високопроизводителен модел на Alibaba с дълъг контекст за задачи с агенти и програмиране.",
|
||||
"qwen3-max-2026-01-23.description": "Qwen3 Max: Най-добре представящият се модел Qwen за сложни, многократни задачи по програмиране с поддръжка на мислене.",
|
||||
"qwen3-max-preview.description": "Най-добре представящият се модел Qwen за сложни, многоетапни задачи. Прегледната версия поддържа разсъждение.",
|
||||
"qwen3-max.description": "Моделите Qwen3 Max предлагат значителни подобрения спрямо серията 2.5 в общите способности, разбиране на китайски/английски, следване на сложни инструкции, субективни отворени задачи, многоезичност и използване на инструменти, с по-малко халюцинации. Най-новият qwen3-max подобрява програмирането чрез агенти и използването на инструменти спрямо qwen3-max-preview. Тази версия достига водещи резултати в индустрията и е насочена към по-сложни нужди на агентите.",
|
||||
"qwen3-next-80b-a3b-instruct.description": "Следващо поколение отворен модел Qwen3 без мисловни способности. В сравнение с предишната версия (Qwen3-235B-A22B-Instruct-2507), предлага по-добро разбиране на китайски, по-силна логическа аргументация и подобрено генериране на текст.",
|
||||
@@ -1200,8 +1192,8 @@
|
||||
"qwq.description": "QwQ е модел за аргументация от семейството на Qwen. В сравнение със стандартните модели, обучени с инструкции, предлага мисловни и логически способности, които значително подобряват ефективността при трудни задачи. QwQ-32B е среден по размер модел, който се конкурира с водещи модели като DeepSeek-R1 и o1-mini.",
|
||||
"qwq_32b.description": "Среден по размер модел за аргументация от семейството на Qwen. В сравнение със стандартните модели, обучени с инструкции, мисловните и логическите способности на QwQ значително подобряват ефективността при трудни задачи.",
|
||||
"r1-1776.description": "R1-1776 е дообучен вариант на DeepSeek R1, създаден да предоставя неконфронтирана, обективна и фактическа информация.",
|
||||
"seedance-1-5-pro-251215.description": "Seedance 1.5 Pro от ByteDance поддържа текст към видео, изображение към видео (първа рамка, първа+последна рамка) и генериране на аудио, синхронизирано с визуализации.",
|
||||
"seedream-5-0-260128.description": "ByteDance-Seedream-5.0-lite от BytePlus предлага генериране, обогатено с уеб търсене за реална информация, подобрена интерпретация на сложни подсказки и подобрена консистентност на препратките за професионално визуално създаване.",
|
||||
"seedance-1-5-pro-251215.description": "Seedance 1.5 Pro от ByteDance поддържа текст-към-видео, изображение-към-видео (първи кадър, първи+последен кадър) и генериране на аудио, синхронизирано с визуализации.",
|
||||
"seedream-5-0-260128.description": "ByteDance-Seedream-5.0-lite от BytePlus предлага генериране, обогатено с уеб търсене за реална информация, подобрена интерпретация на сложни подсказки и подобрена консистентност на референциите за професионално визуално създаване.",
|
||||
"solar-mini-ja.description": "Solar Mini (Ja) разширява Solar Mini с фокус върху японски език, като запазва ефективността и силната производителност на английски и корейски.",
|
||||
"solar-mini.description": "Solar Mini е компактен LLM, който превъзхожда GPT-3.5, с мощни многоезични възможности, поддържащ английски и корейски, и предлага ефективно решение с малък отпечатък.",
|
||||
"solar-pro.description": "Solar Pro е интелигентен LLM от Upstage, фокусиран върху следване на инструкции на един GPU, с IFEval резултати над 80. Понастоящем поддържа английски; пълното издание е планирано за ноември 2024 с разширена езикова поддръжка и по-дълъг контекст.",
|
||||
@@ -1237,7 +1229,7 @@
|
||||
"step-3.5-flash.description": "Флагманският модел за езиково разсъждение на Stepfun. Този модел има първокласни способности за разсъждение и бързи и надеждни изпълнителни възможности. Може да разлага и планира сложни задачи, бързо и надеждно да извиква инструменти за изпълнение на задачи и да бъде компетентен в различни сложни задачи като логическо разсъждение, математика, софтуерно инженерство и задълбочени изследвания.",
|
||||
"step-3.description": "Този модел притежава силно визуално възприятие и сложна логика, точно обработва междудомейново знание, анализ между математика и визия и широк спектър от ежедневни визуални задачи.",
|
||||
"step-r1-v-mini.description": "Модел за логическо разсъждение със силно визуално разбиране, който може да обработва изображения и текст, след което да генерира текст след дълбоко разсъждение. Отличава се във визуално разсъждение и предоставя водещи резултати в математика, програмиране и текстово разсъждение, с контекстен прозорец от 100K.",
|
||||
"stepfun-ai/step3.description": "Step3 е авангарден модел за мултимодално разсъждение от StepFun, построен върху архитектура MoE с 321 милиарда общи и 38 милиарда активни параметри. Неговият дизайн от край до край минимизира разходите за декодиране, като същевременно осигурява водещо разсъждение за визия и език. С дизайна MFA и AFD, той остава ефективен както на водещи, така и на нискобюджетни ускорители. Предварителното обучение използва над 20 трилиона текстови токени и 4 трилиона токени за изображения-текстове на много езици. Той достига водещи резултати сред модели с отворен код в математика, код и мултимодални бенчмаркове.",
|
||||
"stepfun-ai/step3.description": "Step3 е авангарден мултимодален модел за разсъждение от StepFun, изграден върху MoE архитектура с общо 321B и 38B активни параметъра. Дизайнът от край до край минимизира разходите за декодиране, като същевременно осигурява водещо разсъждение между визия и език. С MFA и AFD дизайн, остава ефективен както на флагмански, така и на нискобюджетни ускорители. Предобучен с над 20T текстови токени и 4T токени от изображения и текст на множество езици. Постига водеща производителност сред отворените модели в математика, код и мултимодални бенчмаркове.",
|
||||
"taichu4_vl_2b_nothinking.description": "Версията без мислене на модела Taichu4.0-VL 2B се отличава с по-ниска употреба на памет, лек дизайн, бърза скорост на отговор и силни способности за мултимодално разбиране.",
|
||||
"taichu4_vl_32b.description": "Версията с мислене на модела Taichu4.0-VL 32B е подходяща за сложни задачи за мултимодално разбиране и разсъждение, демонстрирайки изключителна производителност в мултимодално математическо разсъждение, мултимодални способности на агенти и общо разбиране на изображения и визуализации.",
|
||||
"taichu4_vl_32b_nothinking.description": "Версията без мислене на модела Taichu4.0-VL 32B е предназначена за сложни сценарии за разбиране на изображения и текст и визуални въпроси и отговори, превъзхождайки в описания на изображения, визуални въпроси и отговори, разбиране на видео и задачи за визуална локализация.",
|
||||
@@ -1324,7 +1316,7 @@
|
||||
"zai-org/GLM-4.5-Air.description": "GLM-4.5-Air е базов модел за агентни приложения с архитектура Mixture-of-Experts. Оптимизиран е за използване на инструменти, уеб браузване, софтуерно инженерство и фронтенд програмиране, и се интегрира с кодови агенти като Claude Code и Roo Code. Използва хибридно разсъждение за справяне както със сложни, така и с ежедневни задачи.",
|
||||
"zai-org/GLM-4.5V.description": "GLM-4.5V е най-новият визуален езиков модел (VLM) на Zhipu AI, изграден върху флагманския текстов модел GLM-4.5-Air (106B общо, 12B активни) с MoE архитектура за висока производителност при по-ниска цена. Следва пътя на GLM-4.1V-Thinking и добавя 3D-RoPE за подобрено пространствено разсъждение в 3D. Оптимизиран чрез предварително обучение, SFT и RL, обработва изображения, видео и дълги документи и е сред водещите отворени модели в 41 публични мултимодални бенчмарка. Режимът Thinking позволява на потребителите да балансират между скорост и дълбочина.",
|
||||
"zai-org/GLM-4.6.description": "В сравнение с GLM-4.5, GLM-4.6 разширява контекста от 128K до 200K за по-сложни агентни задачи. Постига по-високи резултати в кодови бенчмаркове и показва по-добра реална производителност в приложения като Claude Code, Cline, Roo Code и Kilo Code, включително по-добро генериране на фронтенд страници. Разсъждението е подобрено и се поддържа използване на инструменти по време на разсъждение, което засилва цялостните възможности. По-добре се интегрира в агентни рамки, подобрява инструментите/търсещите агенти и има по-предпочитан от хора стил на писане и естественост в ролевите сценарии.",
|
||||
"zai-org/GLM-4.6V.description": "GLM-4.6V постига водеща точност във визуалното разбиране за своя мащаб на параметрите и е първият, който нативно интегрира възможности за извикване на функции в архитектурата на визуалния модел, преодолявайки разликата между \"визуално възприятие\" и \"изпълними действия\" и предоставяйки унифицирана техническа основа за мултимодални агенти в реални бизнес сценарии. Визуалният контекстен прозорец е разширен до 128 хиляди, поддържайки обработка на дълги видео потоци и анализ на изображения с висока резолюция.",
|
||||
"zai-org/GLM-4.6V.description": "GLM-4.6V постига SOTA точност за визуално разбиране за своя мащаб на параметрите и е първият, който нативно интегрира способности за извикване на функции в архитектурата на модела за визия, преодолявайки разликата между „визуално възприятие“ и „изпълними действия“ и предоставяйки унифицирана техническа основа за мултимодални агенти в реални бизнес сценарии. Визуалният контекстен прозорец е разширен до 128k, поддържащ обработка на дълги видео потоци и анализ на изображения с висока резолюция.",
|
||||
"zai/glm-4.5-air.description": "GLM-4.5 и GLM-4.5-Air са най-новите ни флагмани за агентни приложения, и двата използват MoE. GLM-4.5 има 355B общо и 32B активни параметри на стъпка; GLM-4.5-Air е по-лек с 106B общо и 12B активни.",
|
||||
"zai/glm-4.5.description": "Серията GLM-4.5 е проектирана за агенти. Флагманският GLM-4.5 комбинира разсъждение, програмиране и агентни умения с 355B общи параметри (32B активни) и предлага два режима на работа като хибридна система за разсъждение.",
|
||||
"zai/glm-4.5v.description": "GLM-4.5V надгражда GLM-4.5-Air, наследявайки доказани техники от GLM-4.1V-Thinking и мащабира с мощна MoE архитектура с 106 милиарда параметъра.",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user