Skip to content

Commit 2746007

Browse files
authored
feat(channels): add channel schema, RPC controllers, and definition-driven UI (tinyhumansai#88)
* refactor(tests): rename native_dispatcher test for clarity and update assertions - Renamed the test function to better reflect its purpose: checking that the native dispatcher returns protocol-only instructions. - Updated assertions to ensure the instructions contain the expected protocol information and do not inline tool names. * feat(channels): introduce channel controllers and enhance channel management - Added new `controllers` module for channel definitions, connection management, and RPC controllers. - Implemented channel connection logic, including connection status and credential management. - Updated existing functions to extend support for new channel schemas and definitions. - Enhanced documentation for channel-related functionalities, including descriptions for new RPC methods. * feat(messaging): enhance MessagingPanel with backend-driven channel definitions and status management - Introduced backend-driven channel definitions and status retrieval in MessagingPanel. - Replaced hardcoded channel and auth mode definitions with dynamic data from the backend. - Improved error handling and loading states for channel connections. - Updated API service to support fetching channel definitions and connection statuses. - Refactored state management for better clarity and maintainability. * feat(messaging): implement fallback definitions for channel connections in MessagingPanel - Added fallback definitions for Telegram and Discord channels to enhance messaging capabilities when backend is unreachable. - Updated MessagingPanel to utilize fallback definitions if backend data is unavailable, improving error handling and user experience. - Refactored API service methods to return results directly, streamlining data retrieval for channel definitions and statuses. * feat(channels): add web channel support and enhance routing logic - Introduced a new 'web' channel definition in MessagingPanel, allowing chat via the built-in web UI. - Updated channel connection state management to include the web channel, ensuring proper handling of connections. - Enhanced routing logic to support fallback mechanisms across all defined channels, improving resilience in connection handling. - Refactored channel type definitions to accommodate the new web channel, ensuring consistency across the application.
1 parent 5bd1aa2 commit 2746007

14 files changed

Lines changed: 1593 additions & 340 deletions

File tree

app/src/components/settings/panels/MessagingPanel.tsx

Lines changed: 329 additions & 178 deletions
Large diffs are not rendered by default.

app/src/lib/channels/routing.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import type {
88

99
const SEND_PRIORITY: ChannelAuthMode[] = ['managed_dm', 'oauth', 'bot_token', 'api_key'];
1010

11+
const ALL_CHANNELS: ChannelType[] = ['telegram', 'discord', 'web'];
12+
1113
function isConnected(connection: ChannelConnection | undefined): boolean {
1214
return connection?.status === 'connected';
1315
}
@@ -17,6 +19,7 @@ export function resolvePreferredAuthModeForChannel(
1719
channel: ChannelType
1820
): ChannelAuthMode | null {
1921
const channelModes = state.connections[channel];
22+
if (!channelModes) return null;
2023
for (const authMode of SEND_PRIORITY) {
2124
if (isConnected(channelModes[authMode])) {
2225
return authMode;
@@ -35,9 +38,14 @@ export function resolveOutboundRoute(
3538
return { channel, authMode: mode };
3639
}
3740

38-
const fallbackChannel: ChannelType = channel === 'telegram' ? 'discord' : 'telegram';
39-
const fallbackMode = resolvePreferredAuthModeForChannel(state, fallbackChannel);
40-
if (!fallbackMode) return null;
41+
// Try other channels as fallback.
42+
for (const fallback of ALL_CHANNELS) {
43+
if (fallback === channel) continue;
44+
const fallbackMode = resolvePreferredAuthModeForChannel(state, fallback);
45+
if (fallbackMode) {
46+
return { channel: fallback, authMode: fallbackMode };
47+
}
48+
}
4149

42-
return { channel: fallbackChannel, authMode: fallbackMode };
50+
return null;
4351
}

app/src/services/api/channelConnectionsApi.ts

Lines changed: 41 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -1,163 +1,69 @@
11
import type {
22
ChannelAuthMode,
3-
ChannelConnection,
4-
ChannelConnectionsByMode,
3+
ChannelConnectionResult,
4+
ChannelDefinition,
5+
ChannelStatusEntry,
56
ChannelType,
67
} from '../../types/channels';
78
import { callCoreRpc } from '../coreRpcClient';
89

910
interface ConnectChannelPayload {
1011
authMode: ChannelAuthMode;
11-
credentials?: { botToken?: string; apiKey?: string };
12-
}
13-
14-
interface ConnectChannelResponse {
15-
connection?: ChannelConnection;
16-
oauthUrl?: string;
17-
}
18-
19-
interface ChannelConnectionsResponse {
20-
defaultMessagingChannel: ChannelType;
21-
connections: Record<ChannelType, ChannelConnectionsByMode>;
22-
}
23-
24-
interface AuthProfileSummary {
25-
provider: string;
26-
profileName: string;
27-
}
28-
29-
interface OAuthIntegrationSummary {
30-
id: string;
31-
provider: string;
32-
}
33-
34-
const SUPPORTED_CHANNELS: ChannelType[] = ['telegram', 'discord'];
35-
const SUPPORTED_AUTH_MODES: ChannelAuthMode[] = ['managed_dm', 'oauth', 'bot_token', 'api_key'];
36-
37-
function emptyChannelConnectionsResponse(): ChannelConnectionsResponse {
38-
return { defaultMessagingChannel: 'telegram', connections: { telegram: {}, discord: {} } };
39-
}
40-
41-
function isSupportedChannel(value: string): value is ChannelType {
42-
return SUPPORTED_CHANNELS.includes(value as ChannelType);
43-
}
44-
45-
function makeConnectedChannelConnection(
46-
channel: ChannelType,
47-
authMode: ChannelAuthMode
48-
): ChannelConnection {
49-
return {
50-
channel,
51-
authMode,
52-
status: 'connected',
53-
selectedDefault: false,
54-
lastError: undefined,
55-
capabilities: authMode === 'managed_dm' ? ['dm'] : ['read', 'write'],
56-
updatedAt: new Date().toISOString(),
57-
};
12+
credentials?: Record<string, string>;
5813
}
5914

6015
export const channelConnectionsApi = {
61-
listConnections: async (): Promise<ChannelConnectionsResponse> => {
62-
const [profilesResponse, integrationsResponse] = await Promise.all([
63-
callCoreRpc<{ result: AuthProfileSummary[] }>({
64-
method: 'openhuman.auth.list_provider_credentials',
65-
params: {},
66-
}),
67-
callCoreRpc<{ result: OAuthIntegrationSummary[] }>({
68-
method: 'openhuman.auth.oauth_list_integrations',
69-
params: {},
70-
}),
71-
]);
72-
73-
const output = emptyChannelConnectionsResponse();
74-
const profiles = profilesResponse.result ?? [];
75-
const integrations = integrationsResponse.result ?? [];
76-
77-
for (const profile of profiles) {
78-
if (!isSupportedChannel(profile.provider)) continue;
79-
const authMode = profile.profileName as ChannelAuthMode;
80-
if (!SUPPORTED_AUTH_MODES.includes(authMode) || authMode === 'oauth') continue;
81-
output.connections[profile.provider][authMode] = makeConnectedChannelConnection(
82-
profile.provider,
83-
authMode
84-
);
85-
}
86-
87-
for (const integration of integrations) {
88-
if (!isSupportedChannel(integration.provider)) continue;
89-
output.connections[integration.provider].oauth = makeConnectedChannelConnection(
90-
integration.provider,
91-
'oauth'
92-
);
93-
}
16+
/** Fetch all available channel definitions from the backend. */
17+
listDefinitions: async (): Promise<ChannelDefinition[]> => {
18+
const result = await callCoreRpc<ChannelDefinition[]>({
19+
method: 'openhuman.channels_list',
20+
params: {},
21+
});
22+
return result;
23+
},
9424

95-
return output;
25+
/** Get connection status for one or all channels. */
26+
listStatus: async (channel?: ChannelType): Promise<ChannelStatusEntry[]> => {
27+
const params: Record<string, string> = {};
28+
if (channel) params.channel = channel;
29+
const result = await callCoreRpc<ChannelStatusEntry[]>({
30+
method: 'openhuman.channels_status',
31+
params,
32+
});
33+
return result;
9634
},
9735

36+
/** Connect a channel with the given auth mode and credentials. */
9837
connectChannel: async (
9938
channel: ChannelType,
10039
payload: ConnectChannelPayload
101-
): Promise<ConnectChannelResponse> => {
102-
if (payload.authMode === 'oauth') {
103-
const response = await callCoreRpc<{ result: { oauthUrl: string } }>({
104-
method: 'openhuman.auth.oauth_connect',
105-
params: { provider: channel, skillId: channel },
106-
});
107-
return {
108-
oauthUrl: response.result.oauthUrl,
109-
connection: makeConnectedChannelConnection(channel, payload.authMode),
110-
};
111-
}
112-
113-
const token =
114-
payload.authMode === 'bot_token'
115-
? payload.credentials?.botToken?.trim()
116-
: payload.authMode === 'api_key'
117-
? payload.credentials?.apiKey?.trim()
118-
: undefined;
119-
120-
await callCoreRpc({
121-
method: 'openhuman.auth.store_provider_credentials',
122-
params: {
123-
provider: channel,
124-
profile: payload.authMode,
125-
token,
126-
fields: { authMode: payload.authMode },
127-
setActive: true,
128-
},
40+
): Promise<ChannelConnectionResult> => {
41+
const result = await callCoreRpc<ChannelConnectionResult>({
42+
method: 'openhuman.channels_connect',
43+
params: { channel, authMode: payload.authMode, credentials: payload.credentials ?? {} },
12944
});
130-
131-
return { connection: makeConnectedChannelConnection(channel, payload.authMode) };
45+
return result;
13246
},
13347

48+
/** Disconnect a channel for a given auth mode. */
13449
disconnectChannel: async (channel: ChannelType, authMode: ChannelAuthMode): Promise<void> => {
135-
if (authMode === 'oauth') {
136-
const listResponse = await callCoreRpc<{ result: OAuthIntegrationSummary[] }>({
137-
method: 'openhuman.auth.oauth_list_integrations',
138-
params: {},
139-
});
140-
const integrationIds = (listResponse.result ?? [])
141-
.filter(item => item.provider === channel)
142-
.map(item => item.id);
143-
144-
await Promise.all(
145-
integrationIds.map(integrationId =>
146-
callCoreRpc({
147-
method: 'openhuman.auth.oauth_revoke_integration',
148-
params: { integrationId },
149-
})
150-
)
151-
);
152-
return;
153-
}
50+
await callCoreRpc({ method: 'openhuman.channels_disconnect', params: { channel, authMode } });
51+
},
15452

155-
await callCoreRpc({
156-
method: 'openhuman.auth.remove_provider_credentials',
157-
params: { provider: channel, profile: authMode },
53+
/** Test channel credentials without persisting. */
54+
testChannel: async (
55+
channel: ChannelType,
56+
authMode: ChannelAuthMode,
57+
credentials: Record<string, string>
58+
): Promise<{ success: boolean; message: string }> => {
59+
const result = await callCoreRpc<{ success: boolean; message: string }>({
60+
method: 'openhuman.channels_test',
61+
params: { channel, authMode, credentials },
15862
});
63+
return result;
15964
},
16065

66+
/** Placeholder for default channel preference sync. */
16167
updatePreferences: async (defaultMessagingChannel: ChannelType): Promise<void> => {
16268
void defaultMessagingChannel;
16369
},

app/src/store/channelConnectionsSlice.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ const initialState: ChannelConnectionsState = {
2121
schemaVersion: SCHEMA_VERSION,
2222
migrationCompleted: false,
2323
defaultMessagingChannel: 'telegram',
24-
connections: { telegram: makeEmptyChannelModes(), discord: makeEmptyChannelModes() },
24+
connections: {
25+
telegram: makeEmptyChannelModes(),
26+
discord: makeEmptyChannelModes(),
27+
web: makeEmptyChannelModes(),
28+
},
2529
};
2630

2731
function touchConnection(
@@ -47,6 +51,7 @@ const channelConnectionsSlice = createSlice({
4751
if (state.migrationCompleted) return;
4852
state.connections.telegram = makeEmptyChannelModes();
4953
state.connections.discord = makeEmptyChannelModes();
54+
state.connections.web = makeEmptyChannelModes();
5055
state.defaultMessagingChannel = 'telegram';
5156
state.migrationCompleted = true;
5257
state.schemaVersion = SCHEMA_VERSION;

app/src/types/channels.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export type ChannelType = 'telegram' | 'discord';
1+
export type ChannelType = 'telegram' | 'discord' | 'web';
22

33
export type ChannelAuthMode = 'managed_dm' | 'oauth' | 'bot_token' | 'api_key';
44

@@ -32,3 +32,53 @@ export interface OutboundRoute {
3232
channel: ChannelType;
3333
authMode: ChannelAuthMode;
3434
}
35+
36+
// --- Backend-driven definitions (from openhuman.channels_list) ---
37+
38+
export interface FieldRequirement {
39+
key: string;
40+
label: string;
41+
field_type: string; // "string" | "secret" | "boolean"
42+
required: boolean;
43+
placeholder: string;
44+
}
45+
46+
export interface AuthModeSpec {
47+
mode: ChannelAuthMode;
48+
description: string;
49+
fields: FieldRequirement[];
50+
auth_action?: string; // e.g. "telegram_managed_dm", "discord_oauth"
51+
}
52+
53+
export type ChannelCapability =
54+
| 'send_text'
55+
| 'send_rich_text'
56+
| 'receive_text'
57+
| 'typing'
58+
| 'draft_updates'
59+
| 'threaded_replies'
60+
| 'file_attachments'
61+
| 'reactions';
62+
63+
export interface ChannelDefinition {
64+
id: string;
65+
display_name: string;
66+
description: string;
67+
icon: string;
68+
auth_modes: AuthModeSpec[];
69+
capabilities: ChannelCapability[];
70+
}
71+
72+
export interface ChannelStatusEntry {
73+
channel_id: string;
74+
auth_mode: ChannelAuthMode;
75+
connected: boolean;
76+
has_credentials: boolean;
77+
}
78+
79+
export interface ChannelConnectionResult {
80+
status: string; // "connected" | "pending_auth"
81+
restart_required: boolean;
82+
auth_action?: string;
83+
message?: string;
84+
}

docs/TODO.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ todo
1717
[] Integrate our custom memory engine into core - sanil
1818
[] Integrate our skills registry into core - steve
1919
[x] Integrate accessibility service installation
20-
[] Add as a step and setting in the UI - cyrus
21-
[x] Remove mentions of zeroclaw from the codebaes
22-
[x] Integrate local LLM into core
20+
[] Add as a step and setting in the UI - cyrus
21+
[x] Remove mentions of zeroclaw from the codebaes
22+
[x] Integrate local LLM into core
2323
[x] Handle process/deamon properly
2424
[x] install the linux philosophy of few modules that do their own thing really well sort of..
2525
[x] Remove android / ios support from the codebase.
@@ -29,9 +29,13 @@ todo
2929
[] Add icon and app name to the various permission settings - mithil
3030
[] add self update based on github release. create a update action on the cli - aniketh
3131
[] for each skill show information on how much data has been synced locally and information on how much syncs have happened so far etc.. - mithil/elvin
32-
[x] redo the docs once everything is done.
32+
[x] redo the docs once everything is done.
3333
[x] remove unwanted feature flags from the rust binary
3434
[] fix the config properly - mithil
3535
[] Allow for Migrating from OpenClaw - steve done - to be tested
3636
[] allow users to choose which version of LLM model they'd like to choose based on their CPU. better ram and gpu means higher parameter model can be used. - mithil
37-
[x] in the client side app, make console.log follow a logger style logging where there's a namespace for every logger (like python) - steve
37+
[x] in the client side app, make console.log follow a logger style logging where there's a namespace for every logger (like python) - steve
38+
39+
--- e2e tests to write up
40+
41+
- [ ] connecting a channel like telegram/discord works properly

src/core/all.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ fn build_registered_controllers() -> Vec<RegisteredController> {
4949
controllers.extend(
5050
crate::openhuman::channels::providers::web::all_web_channel_registered_controllers(),
5151
);
52+
controllers
53+
.extend(crate::openhuman::channels::controllers::all_channels_registered_controllers());
5254
controllers.extend(crate::openhuman::config::all_config_registered_controllers());
5355
controllers.extend(crate::openhuman::credentials::all_credentials_registered_controllers());
5456
controllers.extend(crate::openhuman::service::all_service_registered_controllers());
@@ -76,6 +78,7 @@ fn build_declared_controller_schemas() -> Vec<ControllerSchema> {
7678
schemas.extend(crate::openhuman::autocomplete::all_autocomplete_controller_schemas());
7779
schemas
7880
.extend(crate::openhuman::channels::providers::web::all_web_channel_controller_schemas());
81+
schemas.extend(crate::openhuman::channels::controllers::all_channels_controller_schemas());
7982
schemas.extend(crate::openhuman::config::all_config_controller_schemas());
8083
schemas.extend(crate::openhuman::credentials::all_credentials_controller_schemas());
8184
schemas.extend(crate::openhuman::service::all_service_controller_schemas());
@@ -108,6 +111,7 @@ pub fn namespace_description(namespace: &str) -> Option<&'static str> {
108111
match namespace {
109112
"auth" => Some("Manage app session and provider credentials."),
110113
"autocomplete" => Some("Inline autocomplete engine controls and style settings."),
114+
"channels" => Some("Channel definitions, connections, and lifecycle management."),
111115
"config" => Some("Read and update persisted runtime configuration."),
112116
"cron" => Some("Manage scheduled jobs and run history."),
113117
"decrypt" => Some("Decrypt secure values managed by secret storage."),

0 commit comments

Comments
 (0)