mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-26 13:19:34 +07:00
✨ feat(cli): CLI Phase 3 - bot integration, search & device (#12904)
* fix cli alias * 🐛 fix(cli): fix gen text non-streaming mode and streaming SSE parsing - Add `responseMode: 'json'` for non-streaming requests to get plain JSON instead of SSE - Fix streaming SSE parser to handle LobeHub's JSON string format (e.g. `"Hello"`) - Support both OpenAI and Anthropic response formats in non-streaming mode - Add E2E tests for all generate commands (text, list, tts, asr, alias) - Update skills knowledge.md docs with new kb commands Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * ✨ feat(cli): unify skill install command and add e2e tests Merge import-github/import-url/import-market into a single `skill install <source>` command with auto-detection (GitHub URL/shorthand, ZIP URL, or marketplace identifier). Add alias `skill i`. Add comprehensive e2e and unit tests for skill commands. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * 🔨 chore: fix linter formatting in memory e2e test Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * 🐛 fix: add vitest-environment node declaration to aiProvider test Fix server-side env variable access error by declaring node environment. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix cli review * fix test * ✨ feat(cli): add web search and crawl support to search command Add --web flag for web search via tools TRPC client, and search view subcommand for viewing results (URLs via crawl, local resources by type:id). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * ✨ feat(cli): add device management command with TRPC endpoints Add `lh device` command for managing connected devices via server-side TRPC API, complementing the existing `lh connect` (device-as-client). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * ✨ feat(cli): add bot integration management command Add `lh bot` top-level command for managing agent bot integrations (Discord, Slack, Telegram, Lark/Feishu). Includes list, view, add, update, remove, enable/disable, and connect subcommands. Also adds `list` procedure to agentBotProvider TRPC router for querying all bots with optional agent/platform filters. Closes LOBE-5900 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@lobehub/cli",
|
||||
"version": "0.0.1-canary.8",
|
||||
"version": "0.0.1-canary.10",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"lh": "./dist/index.js",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createTRPCClient, httpLink } from '@trpc/client';
|
||||
import superjson from 'superjson';
|
||||
|
||||
import type { LambdaRouter } from '@/server/routers/lambda';
|
||||
import type { ToolsRouter } from '@/server/routers/tools';
|
||||
|
||||
import { getValidToken } from '../auth/refresh';
|
||||
import { OFFICIAL_SERVER_URL } from '../constants/urls';
|
||||
@@ -9,12 +10,12 @@ import { loadSettings } from '../settings';
|
||||
import { log } from '../utils/logger';
|
||||
|
||||
export type TrpcClient = ReturnType<typeof createTRPCClient<LambdaRouter>>;
|
||||
export type ToolsTrpcClient = ReturnType<typeof createTRPCClient<ToolsRouter>>;
|
||||
|
||||
let _client: TrpcClient | undefined;
|
||||
let _toolsClient: ToolsTrpcClient | undefined;
|
||||
|
||||
export async function getTrpcClient(): Promise<TrpcClient> {
|
||||
if (_client) return _client;
|
||||
|
||||
async function getAuthAndServer() {
|
||||
const result = await getValidToken();
|
||||
if (!result) {
|
||||
log.error("No authentication found. Run 'lh login' first.");
|
||||
@@ -24,17 +25,41 @@ export async function getTrpcClient(): Promise<TrpcClient> {
|
||||
const accessToken = result.credentials.accessToken;
|
||||
const serverUrl = loadSettings()?.serverUrl || OFFICIAL_SERVER_URL;
|
||||
|
||||
return { accessToken, serverUrl: serverUrl.replace(/\/$/, '') };
|
||||
}
|
||||
|
||||
export async function getTrpcClient(): Promise<TrpcClient> {
|
||||
if (_client) return _client;
|
||||
|
||||
const { accessToken, serverUrl } = await getAuthAndServer();
|
||||
|
||||
_client = createTRPCClient<LambdaRouter>({
|
||||
links: [
|
||||
httpLink({
|
||||
headers: {
|
||||
'Oidc-Auth': accessToken,
|
||||
},
|
||||
headers: { 'Oidc-Auth': accessToken },
|
||||
transformer: superjson,
|
||||
url: `${serverUrl.replace(/\/$/, '')}/trpc/lambda`,
|
||||
url: `${serverUrl}/trpc/lambda`,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
return _client;
|
||||
}
|
||||
|
||||
export async function getToolsTrpcClient(): Promise<ToolsTrpcClient> {
|
||||
if (_toolsClient) return _toolsClient;
|
||||
|
||||
const { accessToken, serverUrl } = await getAuthAndServer();
|
||||
|
||||
_toolsClient = createTRPCClient<ToolsRouter>({
|
||||
links: [
|
||||
httpLink({
|
||||
headers: { 'Oidc-Auth': accessToken },
|
||||
transformer: superjson,
|
||||
url: `${serverUrl}/trpc/tools`,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
return _toolsClient;
|
||||
}
|
||||
|
||||
345
apps/cli/src/commands/bot.test.ts
Normal file
345
apps/cli/src/commands/bot.test.ts
Normal file
@@ -0,0 +1,345 @@
|
||||
import { Command } from 'commander';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { log } from '../utils/logger';
|
||||
import { registerBotCommand } from './bot';
|
||||
|
||||
const { mockTrpcClient } = vi.hoisted(() => ({
|
||||
mockTrpcClient: {
|
||||
agentBotProvider: {
|
||||
connectBot: { mutate: vi.fn() },
|
||||
create: { mutate: vi.fn() },
|
||||
delete: { mutate: vi.fn() },
|
||||
getByAgentId: { query: vi.fn() },
|
||||
list: { query: vi.fn() },
|
||||
update: { mutate: vi.fn() },
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const { getTrpcClient: mockGetTrpcClient } = vi.hoisted(() => ({
|
||||
getTrpcClient: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../api/client', () => ({ getTrpcClient: mockGetTrpcClient }));
|
||||
vi.mock('../utils/logger', () => ({
|
||||
log: { debug: vi.fn(), error: vi.fn(), info: vi.fn(), warn: vi.fn() },
|
||||
setVerbose: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('bot command', () => {
|
||||
let exitSpy: ReturnType<typeof vi.spyOn>;
|
||||
let consoleSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(() => {
|
||||
exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {}) as any);
|
||||
consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
mockGetTrpcClient.mockResolvedValue(mockTrpcClient);
|
||||
for (const method of Object.values(mockTrpcClient.agentBotProvider)) {
|
||||
for (const fn of Object.values(method)) {
|
||||
(fn as ReturnType<typeof vi.fn>).mockReset();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
exitSpy.mockRestore();
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
function createProgram() {
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
registerBotCommand(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
describe('list', () => {
|
||||
it('should list all bot integrations', async () => {
|
||||
mockTrpcClient.agentBotProvider.list.query.mockResolvedValue([
|
||||
{
|
||||
agentId: 'agent1',
|
||||
applicationId: 'app123',
|
||||
enabled: true,
|
||||
id: 'b1',
|
||||
platform: 'discord',
|
||||
},
|
||||
]);
|
||||
|
||||
const program = createProgram();
|
||||
await program.parseAsync(['node', 'test', 'bot', 'list']);
|
||||
|
||||
expect(mockTrpcClient.agentBotProvider.list.query).toHaveBeenCalledWith({});
|
||||
expect(consoleSpy).toHaveBeenCalledTimes(2); // header + 1 row
|
||||
expect(consoleSpy.mock.calls[0][0]).toContain('ID');
|
||||
});
|
||||
|
||||
it('should filter by agent', async () => {
|
||||
mockTrpcClient.agentBotProvider.list.query.mockResolvedValue([]);
|
||||
|
||||
const program = createProgram();
|
||||
await program.parseAsync(['node', 'test', 'bot', 'list', '--agent', 'agent1']);
|
||||
|
||||
expect(mockTrpcClient.agentBotProvider.list.query).toHaveBeenCalledWith({
|
||||
agentId: 'agent1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter by platform', async () => {
|
||||
mockTrpcClient.agentBotProvider.list.query.mockResolvedValue([]);
|
||||
|
||||
const program = createProgram();
|
||||
await program.parseAsync(['node', 'test', 'bot', 'list', '--platform', 'discord']);
|
||||
|
||||
expect(mockTrpcClient.agentBotProvider.list.query).toHaveBeenCalledWith({
|
||||
platform: 'discord',
|
||||
});
|
||||
});
|
||||
|
||||
it('should output JSON', async () => {
|
||||
const items = [{ id: 'b1', platform: 'discord' }];
|
||||
mockTrpcClient.agentBotProvider.list.query.mockResolvedValue(items);
|
||||
|
||||
const program = createProgram();
|
||||
await program.parseAsync(['node', 'test', 'bot', 'list', '--json']);
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(JSON.stringify(items, null, 2));
|
||||
});
|
||||
|
||||
it('should show message when no bots found', async () => {
|
||||
mockTrpcClient.agentBotProvider.list.query.mockResolvedValue([]);
|
||||
|
||||
const program = createProgram();
|
||||
await program.parseAsync(['node', 'test', 'bot', 'list']);
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith('No bot integrations found.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('view', () => {
|
||||
it('should display bot details', async () => {
|
||||
mockTrpcClient.agentBotProvider.getByAgentId.query.mockResolvedValue([
|
||||
{
|
||||
applicationId: 'app123',
|
||||
credentials: { botToken: 'tok_12345678' },
|
||||
enabled: true,
|
||||
id: 'b1',
|
||||
platform: 'discord',
|
||||
},
|
||||
]);
|
||||
|
||||
const program = createProgram();
|
||||
await program.parseAsync(['node', 'test', 'bot', 'view', 'b1', '--agent', 'agent1']);
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('discord'));
|
||||
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('app123'));
|
||||
});
|
||||
|
||||
it('should error when bot not found', async () => {
|
||||
mockTrpcClient.agentBotProvider.getByAgentId.query.mockResolvedValue([]);
|
||||
|
||||
const program = createProgram();
|
||||
await program.parseAsync(['node', 'test', 'bot', 'view', 'nonexistent', '--agent', 'agent1']);
|
||||
|
||||
expect(log.error).toHaveBeenCalledWith(expect.stringContaining('not found'));
|
||||
expect(exitSpy).toHaveBeenCalledWith(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('add', () => {
|
||||
it('should add a discord bot', async () => {
|
||||
mockTrpcClient.agentBotProvider.create.mutate.mockResolvedValue({ id: 'new-bot' });
|
||||
|
||||
const program = createProgram();
|
||||
await program.parseAsync([
|
||||
'node',
|
||||
'test',
|
||||
'bot',
|
||||
'add',
|
||||
'--agent',
|
||||
'agent1',
|
||||
'--platform',
|
||||
'discord',
|
||||
'--app-id',
|
||||
'app123',
|
||||
'--bot-token',
|
||||
'tok123',
|
||||
'--public-key',
|
||||
'pk123',
|
||||
]);
|
||||
|
||||
expect(mockTrpcClient.agentBotProvider.create.mutate).toHaveBeenCalledWith({
|
||||
agentId: 'agent1',
|
||||
applicationId: 'app123',
|
||||
credentials: { botToken: 'tok123', publicKey: 'pk123' },
|
||||
platform: 'discord',
|
||||
});
|
||||
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Added'));
|
||||
});
|
||||
|
||||
it('should add a telegram bot', async () => {
|
||||
mockTrpcClient.agentBotProvider.create.mutate.mockResolvedValue({ id: 'new-bot' });
|
||||
|
||||
const program = createProgram();
|
||||
await program.parseAsync([
|
||||
'node',
|
||||
'test',
|
||||
'bot',
|
||||
'add',
|
||||
'--agent',
|
||||
'agent1',
|
||||
'--platform',
|
||||
'telegram',
|
||||
'--app-id',
|
||||
'tg123',
|
||||
'--bot-token',
|
||||
'tok123',
|
||||
]);
|
||||
|
||||
expect(mockTrpcClient.agentBotProvider.create.mutate).toHaveBeenCalledWith({
|
||||
agentId: 'agent1',
|
||||
applicationId: 'tg123',
|
||||
credentials: { botToken: 'tok123' },
|
||||
platform: 'telegram',
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject invalid platform', async () => {
|
||||
const program = createProgram();
|
||||
await program.parseAsync([
|
||||
'node',
|
||||
'test',
|
||||
'bot',
|
||||
'add',
|
||||
'--agent',
|
||||
'agent1',
|
||||
'--platform',
|
||||
'invalid',
|
||||
'--app-id',
|
||||
'x',
|
||||
'--bot-token',
|
||||
'x',
|
||||
]);
|
||||
|
||||
expect(log.error).toHaveBeenCalledWith(expect.stringContaining('Invalid platform'));
|
||||
expect(exitSpy).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('should reject missing required credentials', async () => {
|
||||
const program = createProgram();
|
||||
await program.parseAsync([
|
||||
'node',
|
||||
'test',
|
||||
'bot',
|
||||
'add',
|
||||
'--agent',
|
||||
'agent1',
|
||||
'--platform',
|
||||
'discord',
|
||||
'--app-id',
|
||||
'app123',
|
||||
'--bot-token',
|
||||
'tok123',
|
||||
// missing --public-key
|
||||
]);
|
||||
|
||||
expect(log.error).toHaveBeenCalledWith(expect.stringContaining('Missing required'));
|
||||
expect(exitSpy).toHaveBeenCalledWith(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update bot credentials', async () => {
|
||||
mockTrpcClient.agentBotProvider.update.mutate.mockResolvedValue({});
|
||||
|
||||
const program = createProgram();
|
||||
await program.parseAsync(['node', 'test', 'bot', 'update', 'b1', '--bot-token', 'new-token']);
|
||||
|
||||
expect(mockTrpcClient.agentBotProvider.update.mutate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
credentials: { botToken: 'new-token' },
|
||||
id: 'b1',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should error when no changes specified', async () => {
|
||||
const program = createProgram();
|
||||
await program.parseAsync(['node', 'test', 'bot', 'update', 'b1']);
|
||||
|
||||
expect(log.error).toHaveBeenCalledWith(expect.stringContaining('No changes'));
|
||||
expect(exitSpy).toHaveBeenCalledWith(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('should remove with --yes', async () => {
|
||||
mockTrpcClient.agentBotProvider.delete.mutate.mockResolvedValue({});
|
||||
|
||||
const program = createProgram();
|
||||
await program.parseAsync(['node', 'test', 'bot', 'remove', 'b1', '--yes']);
|
||||
|
||||
expect(mockTrpcClient.agentBotProvider.delete.mutate).toHaveBeenCalledWith({ id: 'b1' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('enable / disable', () => {
|
||||
it('should enable a bot', async () => {
|
||||
mockTrpcClient.agentBotProvider.update.mutate.mockResolvedValue({});
|
||||
|
||||
const program = createProgram();
|
||||
await program.parseAsync(['node', 'test', 'bot', 'enable', 'b1']);
|
||||
|
||||
expect(mockTrpcClient.agentBotProvider.update.mutate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ enabled: true, id: 'b1' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should disable a bot', async () => {
|
||||
mockTrpcClient.agentBotProvider.update.mutate.mockResolvedValue({});
|
||||
|
||||
const program = createProgram();
|
||||
await program.parseAsync(['node', 'test', 'bot', 'disable', 'b1']);
|
||||
|
||||
expect(mockTrpcClient.agentBotProvider.update.mutate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ enabled: false, id: 'b1' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('connect', () => {
|
||||
it('should connect a bot', async () => {
|
||||
mockTrpcClient.agentBotProvider.getByAgentId.query.mockResolvedValue([
|
||||
{ applicationId: 'app123', id: 'b1', platform: 'discord' },
|
||||
]);
|
||||
mockTrpcClient.agentBotProvider.connectBot.mutate.mockResolvedValue({ status: 'connected' });
|
||||
|
||||
const program = createProgram();
|
||||
await program.parseAsync(['node', 'test', 'bot', 'connect', 'b1', '--agent', 'agent1']);
|
||||
|
||||
expect(mockTrpcClient.agentBotProvider.connectBot.mutate).toHaveBeenCalledWith({
|
||||
applicationId: 'app123',
|
||||
platform: 'discord',
|
||||
});
|
||||
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Connected'));
|
||||
});
|
||||
|
||||
it('should error when bot not found', async () => {
|
||||
mockTrpcClient.agentBotProvider.getByAgentId.query.mockResolvedValue([]);
|
||||
|
||||
const program = createProgram();
|
||||
await program.parseAsync([
|
||||
'node',
|
||||
'test',
|
||||
'bot',
|
||||
'connect',
|
||||
'nonexistent',
|
||||
'--agent',
|
||||
'agent1',
|
||||
]);
|
||||
|
||||
expect(log.error).toHaveBeenCalledWith(expect.stringContaining('not found'));
|
||||
expect(exitSpy).toHaveBeenCalledWith(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
298
apps/cli/src/commands/bot.ts
Normal file
298
apps/cli/src/commands/bot.ts
Normal file
@@ -0,0 +1,298 @@
|
||||
import type { Command } from 'commander';
|
||||
import pc from 'picocolors';
|
||||
|
||||
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'];
|
||||
|
||||
const PLATFORM_CREDENTIAL_FIELDS: Record<string, string[]> = {
|
||||
discord: ['botToken', 'publicKey'],
|
||||
feishu: ['appId', 'appSecret'],
|
||||
lark: ['appId', 'appSecret'],
|
||||
slack: ['botToken', 'signingSecret'],
|
||||
telegram: ['botToken'],
|
||||
};
|
||||
|
||||
function parseCredentials(
|
||||
platform: string,
|
||||
options: Record<string, string | undefined>,
|
||||
): Record<string, string> {
|
||||
const creds: Record<string, string> = {};
|
||||
|
||||
if (options.botToken) creds.botToken = options.botToken;
|
||||
if (options.publicKey) creds.publicKey = options.publicKey;
|
||||
if (options.signingSecret) creds.signingSecret = options.signingSecret;
|
||||
if (options.appSecret) creds.appSecret = options.appSecret;
|
||||
|
||||
// For lark/feishu, --app-id maps to credentials.appId (distinct from --app-id as applicationId)
|
||||
if ((platform === 'lark' || platform === 'feishu') && options.appId) {
|
||||
creds.appId = options.appId;
|
||||
}
|
||||
|
||||
return creds;
|
||||
}
|
||||
|
||||
export function registerBotCommand(program: Command) {
|
||||
const bot = program.command('bot').description('Manage bot integrations');
|
||||
|
||||
// ── list ──────────────────────────────────────────────
|
||||
|
||||
bot
|
||||
.command('list')
|
||||
.description('List bot integrations')
|
||||
.option('-a, --agent <agentId>', 'Filter by agent ID')
|
||||
.option('--platform <platform>', 'Filter by platform')
|
||||
.option('--json [fields]', 'Output JSON, optionally specify fields (comma-separated)')
|
||||
.action(async (options: { agent?: string; json?: string | boolean; platform?: string }) => {
|
||||
const client = await getTrpcClient();
|
||||
|
||||
const input: { agentId?: string; platform?: string } = {};
|
||||
if (options.agent) input.agentId = options.agent;
|
||||
if (options.platform) input.platform = options.platform;
|
||||
|
||||
const result = await client.agentBotProvider.list.query(input);
|
||||
const items = Array.isArray(result) ? result : [];
|
||||
|
||||
if (options.json !== undefined) {
|
||||
const fields = typeof options.json === 'string' ? options.json : undefined;
|
||||
outputJson(items, fields);
|
||||
return;
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
console.log('No bot integrations found.');
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = items.map((b: any) => [
|
||||
b.id || '',
|
||||
b.platform || '',
|
||||
b.applicationId || '',
|
||||
b.agentId || '',
|
||||
b.enabled ? pc.green('enabled') : pc.dim('disabled'),
|
||||
]);
|
||||
|
||||
printTable(rows, ['ID', 'PLATFORM', 'APP ID', 'AGENT', 'STATUS']);
|
||||
});
|
||||
|
||||
// ── view ──────────────────────────────────────────────
|
||||
|
||||
bot
|
||||
.command('view <botId>')
|
||||
.description('View bot integration details')
|
||||
.requiredOption('-a, --agent <agentId>', 'Agent ID')
|
||||
.option('--json [fields]', 'Output JSON, optionally specify fields (comma-separated)')
|
||||
.action(async (botId: string, options: { agent: string; json?: string | boolean }) => {
|
||||
const client = await getTrpcClient();
|
||||
const result = await client.agentBotProvider.getByAgentId.query({
|
||||
agentId: options.agent,
|
||||
});
|
||||
const items = Array.isArray(result) ? result : [];
|
||||
const item = items.find((b: any) => b.id === botId);
|
||||
|
||||
if (!item) {
|
||||
log.error(`Bot integration not found: ${botId}`);
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.json !== undefined) {
|
||||
const fields = typeof options.json === 'string' ? options.json : undefined;
|
||||
outputJson(item, fields);
|
||||
return;
|
||||
}
|
||||
|
||||
const b = item as any;
|
||||
console.log(pc.bold(`${b.platform} bot`));
|
||||
console.log(pc.dim(`ID: ${b.id}`));
|
||||
console.log(`Application ID: ${b.applicationId}`);
|
||||
console.log(`Status: ${b.enabled ? pc.green('enabled') : pc.dim('disabled')}`);
|
||||
|
||||
if (b.credentials && typeof b.credentials === 'object') {
|
||||
console.log();
|
||||
console.log(pc.bold('Credentials:'));
|
||||
for (const [key, value] of Object.entries(b.credentials)) {
|
||||
const val = String(value);
|
||||
const masked = val.length > 8 ? val.slice(0, 4) + '****' + val.slice(-4) : '****';
|
||||
console.log(` ${key}: ${masked}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ── add ───────────────────────────────────────────────
|
||||
|
||||
bot
|
||||
.command('add')
|
||||
.description('Add a bot integration to an agent')
|
||||
.requiredOption('-a, --agent <agentId>', 'Agent ID')
|
||||
.requiredOption('--platform <platform>', `Platform: ${SUPPORTED_PLATFORMS.join(', ')}`)
|
||||
.requiredOption('--app-id <appId>', 'Application ID for webhook routing')
|
||||
.option('--bot-token <token>', 'Bot token')
|
||||
.option('--public-key <key>', 'Public key (Discord)')
|
||||
.option('--signing-secret <secret>', 'Signing secret (Slack)')
|
||||
.option('--app-secret <secret>', 'App secret (Lark/Feishu)')
|
||||
.action(
|
||||
async (options: {
|
||||
agent: string;
|
||||
appId: string;
|
||||
appSecret?: string;
|
||||
botToken?: string;
|
||||
platform: string;
|
||||
publicKey?: string;
|
||||
signingSecret?: string;
|
||||
}) => {
|
||||
if (!SUPPORTED_PLATFORMS.includes(options.platform)) {
|
||||
log.error(`Invalid platform. Must be one of: ${SUPPORTED_PLATFORMS.join(', ')}`);
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
const credentials = parseCredentials(options.platform, options);
|
||||
const requiredFields = PLATFORM_CREDENTIAL_FIELDS[options.platform] || [];
|
||||
const missing = requiredFields.filter((f) => !credentials[f]);
|
||||
if (missing.length > 0) {
|
||||
log.error(
|
||||
`Missing required credentials for ${options.platform}: ${missing.map((f) => '--' + f.replaceAll(/([A-Z])/g, '-$1').toLowerCase()).join(', ')}`,
|
||||
);
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
const client = await getTrpcClient();
|
||||
const result = await client.agentBotProvider.create.mutate({
|
||||
agentId: options.agent,
|
||||
applicationId: options.appId,
|
||||
credentials,
|
||||
platform: options.platform,
|
||||
});
|
||||
const r = result as any;
|
||||
console.log(
|
||||
`${pc.green('✓')} Added ${pc.bold(options.platform)} bot ${pc.bold(r.id || '')}`,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// ── update ────────────────────────────────────────────
|
||||
|
||||
bot
|
||||
.command('update <botId>')
|
||||
.description('Update a bot integration')
|
||||
.option('--bot-token <token>', 'New bot token')
|
||||
.option('--public-key <key>', 'New public key')
|
||||
.option('--signing-secret <secret>', 'New signing secret')
|
||||
.option('--app-secret <secret>', 'New app secret')
|
||||
.option('--app-id <appId>', 'New application ID')
|
||||
.option('--platform <platform>', 'New platform')
|
||||
.action(
|
||||
async (
|
||||
botId: string,
|
||||
options: {
|
||||
appId?: string;
|
||||
appSecret?: string;
|
||||
botToken?: string;
|
||||
platform?: string;
|
||||
publicKey?: string;
|
||||
signingSecret?: string;
|
||||
},
|
||||
) => {
|
||||
const input: Record<string, any> = { id: botId };
|
||||
|
||||
const credentials: Record<string, string> = {};
|
||||
if (options.botToken) credentials.botToken = options.botToken;
|
||||
if (options.publicKey) credentials.publicKey = options.publicKey;
|
||||
if (options.signingSecret) credentials.signingSecret = options.signingSecret;
|
||||
if (options.appSecret) credentials.appSecret = options.appSecret;
|
||||
|
||||
if (Object.keys(credentials).length > 0) input.credentials = credentials;
|
||||
if (options.appId) input.applicationId = options.appId;
|
||||
if (options.platform) input.platform = options.platform;
|
||||
|
||||
if (Object.keys(input).length <= 1) {
|
||||
log.error('No changes specified.');
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
const client = await getTrpcClient();
|
||||
await client.agentBotProvider.update.mutate(input as any);
|
||||
console.log(`${pc.green('✓')} Updated bot ${pc.bold(botId)}`);
|
||||
},
|
||||
);
|
||||
|
||||
// ── remove ────────────────────────────────────────────
|
||||
|
||||
bot
|
||||
.command('remove <botId>')
|
||||
.description('Remove a bot integration')
|
||||
.option('--yes', 'Skip confirmation prompt')
|
||||
.action(async (botId: string, options: { yes?: boolean }) => {
|
||||
if (!options.yes) {
|
||||
const confirmed = await confirm('Are you sure you want to remove this bot integration?');
|
||||
if (!confirmed) {
|
||||
console.log('Cancelled.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const client = await getTrpcClient();
|
||||
await client.agentBotProvider.delete.mutate({ id: botId });
|
||||
console.log(`${pc.green('✓')} Removed bot ${pc.bold(botId)}`);
|
||||
});
|
||||
|
||||
// ── enable / disable ──────────────────────────────────
|
||||
|
||||
bot
|
||||
.command('enable <botId>')
|
||||
.description('Enable a bot integration')
|
||||
.action(async (botId: string) => {
|
||||
const client = await getTrpcClient();
|
||||
await client.agentBotProvider.update.mutate({ enabled: true, id: botId } as any);
|
||||
console.log(`${pc.green('✓')} Enabled bot ${pc.bold(botId)}`);
|
||||
});
|
||||
|
||||
bot
|
||||
.command('disable <botId>')
|
||||
.description('Disable a bot integration')
|
||||
.action(async (botId: string) => {
|
||||
const client = await getTrpcClient();
|
||||
await client.agentBotProvider.update.mutate({ enabled: false, id: botId } as any);
|
||||
console.log(`${pc.green('✓')} Disabled bot ${pc.bold(botId)}`);
|
||||
});
|
||||
|
||||
// ── connect ───────────────────────────────────────────
|
||||
|
||||
bot
|
||||
.command('connect <botId>')
|
||||
.description('Connect and start a bot')
|
||||
.requiredOption('-a, --agent <agentId>', 'Agent ID')
|
||||
.action(async (botId: string, options: { agent: string }) => {
|
||||
// First fetch the bot to get platform and applicationId
|
||||
const client = await getTrpcClient();
|
||||
const result = await client.agentBotProvider.getByAgentId.query({
|
||||
agentId: options.agent,
|
||||
});
|
||||
const items = Array.isArray(result) ? result : [];
|
||||
const item = items.find((b: any) => b.id === botId);
|
||||
|
||||
if (!item) {
|
||||
log.error(`Bot integration not found: ${botId}`);
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
const b = item as any;
|
||||
const connectResult = await client.agentBotProvider.connectBot.mutate({
|
||||
applicationId: b.applicationId,
|
||||
platform: b.platform,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`${pc.green('✓')} Connected ${pc.bold(b.platform)} bot ${pc.bold(b.applicationId)}`,
|
||||
);
|
||||
if ((connectResult as any)?.status) {
|
||||
console.log(` Status: ${(connectResult as any).status}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
97
apps/cli/src/commands/device.ts
Normal file
97
apps/cli/src/commands/device.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import type { Command } from 'commander';
|
||||
import pc from 'picocolors';
|
||||
|
||||
import { getTrpcClient } from '../api/client';
|
||||
import { outputJson, printTable, timeAgo } from '../utils/format';
|
||||
import { log } from '../utils/logger';
|
||||
|
||||
export function registerDeviceCommand(program: Command) {
|
||||
const device = program.command('device').description('Manage connected devices');
|
||||
|
||||
// ── list ──────────────────────────────────────────────
|
||||
|
||||
device
|
||||
.command('list')
|
||||
.description('List all online devices')
|
||||
.option('--json [fields]', 'Output JSON, optionally specify fields (comma-separated)')
|
||||
.action(async (options: { json?: string | boolean }) => {
|
||||
const client = await getTrpcClient();
|
||||
const devices = await client.device.listDevices.query();
|
||||
|
||||
if (options.json !== undefined) {
|
||||
const fields = typeof options.json === 'string' ? options.json : undefined;
|
||||
outputJson(devices, fields);
|
||||
return;
|
||||
}
|
||||
|
||||
if (devices.length === 0) {
|
||||
console.log('No online devices found.');
|
||||
console.log(pc.dim("Use 'lh connect' to connect this device."));
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = devices.map((d: any) => [
|
||||
d.deviceId || '',
|
||||
d.hostname || '',
|
||||
d.platform || '',
|
||||
d.online ? pc.green('online') : pc.dim('offline'),
|
||||
d.lastSeen ? timeAgo(d.lastSeen) : '',
|
||||
]);
|
||||
|
||||
printTable(rows, ['DEVICE ID', 'HOSTNAME', 'PLATFORM', 'STATUS', 'CONNECTED']);
|
||||
});
|
||||
|
||||
// ── info ──────────────────────────────────────────────
|
||||
|
||||
device
|
||||
.command('info <deviceId>')
|
||||
.description('Show system info of a specific device')
|
||||
.option('--json [fields]', 'Output JSON, optionally specify fields (comma-separated)')
|
||||
.action(async (deviceId: string, options: { json?: string | boolean }) => {
|
||||
const client = await getTrpcClient();
|
||||
const info = await client.device.getDeviceSystemInfo.query({ deviceId });
|
||||
|
||||
if (!info) {
|
||||
log.error(`Device "${deviceId}" is not reachable or does not exist.`);
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.json !== undefined) {
|
||||
const fields = typeof options.json === 'string' ? options.json : undefined;
|
||||
outputJson(info, fields);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(pc.bold('Device System Info'));
|
||||
console.log(` Architecture : ${info.arch}`);
|
||||
console.log(` Working Directory : ${info.workingDirectory}`);
|
||||
console.log(` Home : ${info.homePath}`);
|
||||
console.log(` Desktop : ${info.desktopPath}`);
|
||||
console.log(` Documents : ${info.documentsPath}`);
|
||||
console.log(` Downloads : ${info.downloadsPath}`);
|
||||
console.log(` Music : ${info.musicPath}`);
|
||||
console.log(` Pictures : ${info.picturesPath}`);
|
||||
console.log(` Videos : ${info.videosPath}`);
|
||||
});
|
||||
|
||||
// ── status ────────────────────────────────────────────
|
||||
|
||||
device
|
||||
.command('status')
|
||||
.description('Show device connection overview')
|
||||
.option('--json', 'Output JSON')
|
||||
.action(async (options: { json?: boolean }) => {
|
||||
const client = await getTrpcClient();
|
||||
const status = await client.device.status.query();
|
||||
|
||||
if (options.json) {
|
||||
outputJson(status);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(pc.bold('Device Status'));
|
||||
console.log(` Online : ${status.online ? pc.green('yes') : pc.dim('no')}`);
|
||||
console.log(` Devices : ${status.deviceCount}`);
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Command } from 'commander';
|
||||
import pc from 'picocolors';
|
||||
|
||||
import { getTrpcClient } from '../api/client';
|
||||
import { getToolsTrpcClient, getTrpcClient } from '../api/client';
|
||||
import { outputJson, printTable, truncate } from '../utils/format';
|
||||
|
||||
const SEARCH_TYPES = [
|
||||
@@ -36,67 +36,283 @@ function renderResultGroup(type: string, items: any[]) {
|
||||
}
|
||||
|
||||
export function registerSearchCommand(program: Command) {
|
||||
program
|
||||
.command('search <query>')
|
||||
.description('Search across topics, agents, files, knowledge bases, and more')
|
||||
const search = program
|
||||
.command('search')
|
||||
.description('Search across local resources or the web')
|
||||
.option('-q, --query <query>', 'Search query')
|
||||
.option('-w, --web', 'Search the web instead of local resources')
|
||||
.option('-t, --type <type>', `Filter by type: ${SEARCH_TYPES.join(', ')}`)
|
||||
.option('-L, --limit <n>', 'Results per type', '10')
|
||||
.option('-e, --engines <engines>', 'Web search engines (comma-separated, requires --web)')
|
||||
.option(
|
||||
'-c, --categories <categories>',
|
||||
'Web search categories (comma-separated, requires --web)',
|
||||
)
|
||||
.option(
|
||||
'-T, --time-range <range>',
|
||||
'Time range filter (e.g. day, week, month, year, requires --web)',
|
||||
)
|
||||
.option('--json [fields]', 'Output JSON, optionally specify fields (comma-separated)')
|
||||
.action(
|
||||
async (
|
||||
query: string,
|
||||
options: { json?: string | boolean; limit?: string; type?: string },
|
||||
) => {
|
||||
if (options.type && !SEARCH_TYPES.includes(options.type as SearchType)) {
|
||||
console.error(
|
||||
`Invalid type: ${options.type}. Must be one of: ${SEARCH_TYPES.join(', ')}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = await getTrpcClient();
|
||||
|
||||
const input: { limitPerType?: number; query: string; type?: SearchType } = { query };
|
||||
if (options.type) input.type = options.type as SearchType;
|
||||
if (options.limit) input.limitPerType = Number.parseInt(options.limit, 10);
|
||||
|
||||
const result = await client.search.query.query(input);
|
||||
|
||||
if (options.json !== undefined) {
|
||||
const fields = typeof options.json === 'string' ? options.json : undefined;
|
||||
outputJson(result, fields);
|
||||
async (options: {
|
||||
categories?: string;
|
||||
engines?: string;
|
||||
json?: string | boolean;
|
||||
limit?: string;
|
||||
query?: string;
|
||||
timeRange?: string;
|
||||
type?: string;
|
||||
web?: boolean;
|
||||
}) => {
|
||||
if (!options.query) {
|
||||
search.help();
|
||||
return;
|
||||
}
|
||||
|
||||
// result is expected to be an object grouped by type or an array
|
||||
if (Array.isArray(result)) {
|
||||
if (result.length === 0) {
|
||||
console.log('No results found.');
|
||||
return;
|
||||
}
|
||||
// Group by type if available
|
||||
const groups: Record<string, any[]> = {};
|
||||
for (const item of result) {
|
||||
const t = item.type || 'other';
|
||||
if (!groups[t]) groups[t] = [];
|
||||
groups[t].push(item);
|
||||
}
|
||||
for (const [type, items] of Object.entries(groups)) {
|
||||
renderResultGroup(type, items);
|
||||
}
|
||||
} else if (result && typeof result === 'object') {
|
||||
const groups = result as Record<string, any[]>;
|
||||
let hasResults = false;
|
||||
for (const [type, items] of Object.entries(groups)) {
|
||||
if (Array.isArray(items) && items.length > 0) {
|
||||
hasResults = true;
|
||||
renderResultGroup(type, items);
|
||||
}
|
||||
}
|
||||
if (!hasResults) {
|
||||
console.log('No results found.');
|
||||
}
|
||||
if (options.web) {
|
||||
await webSearch(options.query, options);
|
||||
} else {
|
||||
await localSearch(options.query, options);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// ── search view ──────────────────────────────────────
|
||||
search
|
||||
.command('view <target>')
|
||||
.description('View details of a search result (URL for web results, or type:id for local)')
|
||||
.option('--json [fields]', 'Output JSON, optionally specify fields (comma-separated)')
|
||||
.option(
|
||||
'-i, --impl <impls>',
|
||||
'Crawler implementations for web URLs (comma-separated: browserless, exa, firecrawl, jina, naive, search1api, tavily)',
|
||||
)
|
||||
.action(
|
||||
async (
|
||||
target: string,
|
||||
options: {
|
||||
impl?: string;
|
||||
json?: string | boolean;
|
||||
},
|
||||
) => {
|
||||
if (target.startsWith('http://') || target.startsWith('https://')) {
|
||||
await crawlView(target, options);
|
||||
return;
|
||||
}
|
||||
|
||||
await localView(target, options);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// ── local search ──────────────────────────────────────
|
||||
|
||||
async function localSearch(
|
||||
query: string,
|
||||
options: { json?: string | boolean; limit?: string; type?: string },
|
||||
) {
|
||||
if (options.type && !SEARCH_TYPES.includes(options.type as SearchType)) {
|
||||
console.error(`Invalid type: ${options.type}. Must be one of: ${SEARCH_TYPES.join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = await getTrpcClient();
|
||||
|
||||
const input: { limitPerType?: number; query: string; type?: SearchType } = { query };
|
||||
if (options.type) input.type = options.type as SearchType;
|
||||
if (options.limit) input.limitPerType = Number.parseInt(options.limit, 10);
|
||||
|
||||
const result = await client.search.query.query(input);
|
||||
|
||||
if (options.json !== undefined) {
|
||||
const fields = typeof options.json === 'string' ? options.json : undefined;
|
||||
outputJson(result, fields);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(result)) {
|
||||
if (result.length === 0) {
|
||||
console.log('No results found.');
|
||||
return;
|
||||
}
|
||||
const groups: Record<string, any[]> = {};
|
||||
for (const item of result) {
|
||||
const t = item.type || 'other';
|
||||
if (!groups[t]) groups[t] = [];
|
||||
groups[t].push(item);
|
||||
}
|
||||
for (const [type, items] of Object.entries(groups)) {
|
||||
renderResultGroup(type, items);
|
||||
}
|
||||
} else if (result && typeof result === 'object') {
|
||||
const groups = result as Record<string, any[]>;
|
||||
let hasResults = false;
|
||||
for (const [type, items] of Object.entries(groups)) {
|
||||
if (Array.isArray(items) && items.length > 0) {
|
||||
hasResults = true;
|
||||
renderResultGroup(type, items);
|
||||
}
|
||||
}
|
||||
if (!hasResults) {
|
||||
console.log('No results found.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── web search ────────────────────────────────────────
|
||||
|
||||
async function webSearch(
|
||||
query: string,
|
||||
options: {
|
||||
categories?: string;
|
||||
engines?: string;
|
||||
json?: string | boolean;
|
||||
timeRange?: string;
|
||||
},
|
||||
) {
|
||||
const toolsClient = await getToolsTrpcClient();
|
||||
|
||||
const input: {
|
||||
query: string;
|
||||
searchCategories?: string[];
|
||||
searchEngines?: string[];
|
||||
searchTimeRange?: string;
|
||||
} = { query };
|
||||
|
||||
if (options.engines) input.searchEngines = options.engines.split(',').map((s) => s.trim());
|
||||
if (options.categories)
|
||||
input.searchCategories = options.categories.split(',').map((s) => s.trim());
|
||||
if (options.timeRange) input.searchTimeRange = options.timeRange;
|
||||
|
||||
const result = await toolsClient.search.webSearch.query(input);
|
||||
|
||||
if (options.json !== undefined) {
|
||||
const fields = typeof options.json === 'string' ? options.json : undefined;
|
||||
outputJson(result, fields);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = result as any;
|
||||
|
||||
console.log(
|
||||
pc.dim(
|
||||
`Found ${res.resultNumbers ?? res.results?.length ?? 0} results in ${res.costTime ?? '?'}ms`,
|
||||
),
|
||||
);
|
||||
|
||||
if (!res.results || res.results.length === 0) {
|
||||
console.log('No results found.');
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = res.results.map((item: any) => [
|
||||
truncate(item.title || '', 50),
|
||||
truncate(item.url || '', 60),
|
||||
item.score != null ? String(item.score) : '',
|
||||
truncate(item.content || '', 60),
|
||||
]);
|
||||
|
||||
printTable(rows, ['TITLE', 'URL', 'SCORE', 'CONTENT']);
|
||||
}
|
||||
|
||||
// ── crawl view (for web URLs) ─────────────────────────
|
||||
|
||||
async function crawlView(url: string, options: { impl?: string; json?: string | boolean }) {
|
||||
const toolsClient = await getToolsTrpcClient();
|
||||
|
||||
const input: {
|
||||
impls?: ('browserless' | 'exa' | 'firecrawl' | 'jina' | 'naive' | 'search1api' | 'tavily')[];
|
||||
urls: string[];
|
||||
} = { urls: [url] };
|
||||
|
||||
if (options.impl) {
|
||||
input.impls = options.impl.split(',').map((s) => s.trim()) as typeof input.impls;
|
||||
}
|
||||
|
||||
const result = await toolsClient.search.crawlPages.mutate(input);
|
||||
|
||||
if (options.json !== undefined) {
|
||||
const fields = typeof options.json === 'string' ? options.json : undefined;
|
||||
outputJson(result, fields);
|
||||
return;
|
||||
}
|
||||
|
||||
const pages = Array.isArray(result) ? result : [result];
|
||||
|
||||
for (const page of pages) {
|
||||
const p = page as any;
|
||||
console.log();
|
||||
console.log(pc.bold(pc.cyan(p.title || p.url || 'Untitled')));
|
||||
if (p.url) console.log(pc.dim(p.url));
|
||||
if (p.content) {
|
||||
console.log();
|
||||
console.log(p.content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── local view (by type:id) ───────────────────────────
|
||||
|
||||
async function localView(target: string, options: { json?: string | boolean }) {
|
||||
const sep = target.indexOf(':');
|
||||
if (sep === -1) {
|
||||
console.error(
|
||||
'Invalid target. Use type:id (e.g. agent:abc123) for local resources, or a URL for web results.',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const type = target.slice(0, sep);
|
||||
const id = target.slice(sep + 1);
|
||||
|
||||
if (!id) {
|
||||
console.error('Missing id. Format: type:id');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = await getTrpcClient();
|
||||
|
||||
let result: any;
|
||||
|
||||
switch (type) {
|
||||
case 'agent': {
|
||||
result = await client.agent.getAgentConfigById.query({ agentId: id });
|
||||
break;
|
||||
}
|
||||
case 'file': {
|
||||
result = await client.file.getFileItemById.query({ id });
|
||||
break;
|
||||
}
|
||||
case 'knowledgeBase': {
|
||||
result = await client.knowledgeBase.getKnowledgeBaseById.query({ id });
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
console.error(`View not supported for type "${type}". Supported: agent, file, knowledgeBase`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
console.error(`${type} not found: ${id}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (options.json !== undefined) {
|
||||
const fields = typeof options.json === 'string' ? options.json : undefined;
|
||||
outputJson(result, fields);
|
||||
return;
|
||||
}
|
||||
|
||||
const r = result as any;
|
||||
console.log();
|
||||
console.log(pc.bold(r.title || r.name || r.identifier || id));
|
||||
if (r.description) console.log(pc.dim(r.description));
|
||||
if (r.type) console.log(`Type: ${r.type}`);
|
||||
if (r.createdAt) console.log(`Created: ${pc.dim(String(r.createdAt))}`);
|
||||
if (r.updatedAt) console.log(`Updated: ${pc.dim(String(r.updatedAt))}`);
|
||||
if (r.systemRole) {
|
||||
console.log();
|
||||
console.log(pc.cyan('System Role:'));
|
||||
console.log(r.systemRole);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,15 @@ import { createRequire } from 'node:module';
|
||||
import { Command } from 'commander';
|
||||
|
||||
import { registerAgentCommand } from './commands/agent';
|
||||
import { registerBotCommand } from './commands/bot';
|
||||
import { registerConfigCommand } from './commands/config';
|
||||
import { registerConnectCommand } from './commands/connect';
|
||||
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 { registerEvalCommand } from './commands/eval';
|
||||
import { registerLoginCommand } from './commands/login';
|
||||
import { registerLogoutCommand } from './commands/logout';
|
||||
import { registerMemoryCommand } from './commands/memory';
|
||||
@@ -35,12 +37,14 @@ program
|
||||
registerLoginCommand(program);
|
||||
registerLogoutCommand(program);
|
||||
registerConnectCommand(program);
|
||||
registerDeviceCommand(program);
|
||||
registerStatusCommand(program);
|
||||
registerDocCommand(program);
|
||||
registerSearchCommand(program);
|
||||
registerKbCommand(program);
|
||||
registerMemoryCommand(program);
|
||||
registerAgentCommand(program);
|
||||
registerBotCommand(program);
|
||||
registerGenerateCommand(program);
|
||||
registerFileCommand(program);
|
||||
registerSkillCommand(program);
|
||||
|
||||
@@ -55,6 +55,19 @@ export const agentBotProviderRouter = router({
|
||||
return ctx.agentBotProviderModel.findByAgentId(input.agentId);
|
||||
}),
|
||||
|
||||
list: agentBotProviderProcedure
|
||||
.input(
|
||||
z
|
||||
.object({
|
||||
agentId: z.string().optional(),
|
||||
platform: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
return ctx.agentBotProviderModel.query(input);
|
||||
}),
|
||||
|
||||
connectBot: agentBotProviderProcedure
|
||||
.input(z.object({ applicationId: z.string(), platform: z.string() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
|
||||
28
src/server/routers/lambda/device.ts
Normal file
28
src/server/routers/lambda/device.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { authedProcedure, router } from '@/libs/trpc/lambda';
|
||||
import { deviceProxy } from '@/server/services/toolExecution/deviceProxy';
|
||||
|
||||
const deviceProcedure = authedProcedure.use(async (opts) => {
|
||||
const { ctx } = opts;
|
||||
|
||||
return opts.next({
|
||||
ctx: { userId: ctx.userId },
|
||||
});
|
||||
});
|
||||
|
||||
export const deviceRouter = router({
|
||||
getDeviceSystemInfo: deviceProcedure
|
||||
.input(z.object({ deviceId: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
return deviceProxy.queryDeviceSystemInfo(ctx.userId, input.deviceId);
|
||||
}),
|
||||
|
||||
listDevices: deviceProcedure.query(async ({ ctx }) => {
|
||||
return deviceProxy.queryDeviceList(ctx.userId);
|
||||
}),
|
||||
|
||||
status: deviceProcedure.query(async ({ ctx }) => {
|
||||
return deviceProxy.queryDeviceStatus(ctx.userId);
|
||||
}),
|
||||
});
|
||||
@@ -23,6 +23,7 @@ import { apiKeyRouter } from './apiKey';
|
||||
import { chunkRouter } from './chunk';
|
||||
import { comfyuiRouter } from './comfyui';
|
||||
import { configRouter } from './config';
|
||||
import { deviceRouter } from './device';
|
||||
import { documentRouter } from './document';
|
||||
import { exporterRouter } from './exporter';
|
||||
import { fileRouter } from './file';
|
||||
@@ -69,6 +70,7 @@ export const lambdaRouter = router({
|
||||
chunk: chunkRouter,
|
||||
comfyui: comfyuiRouter,
|
||||
config: configRouter,
|
||||
device: deviceRouter,
|
||||
document: documentRouter,
|
||||
exporter: exporterRouter,
|
||||
file: fileRouter,
|
||||
|
||||
Reference in New Issue
Block a user