Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 73 additions & 9 deletions src-tauri/src/commands/messaging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,17 +59,28 @@ fn gateway_auth_value(cfg: &Value, key: &str) -> Option<String> {
}

/// 读取指定平台的当前配置(从 openclaw.json 中提取表单可用的值)
/// account_id: 可选,指定时读取 channels.<platform>.accounts.<account_id>(多账号模式)
#[tauri::command]
pub async fn read_platform_config(platform: String) -> Result<Value, String> {
pub async fn read_platform_config(
platform: String,
account_id: Option<String>,
) -> Result<Value, String> {
let cfg = super::config::load_openclaw_json()?;
let storage_key = platform_storage_key(&platform);

// 从已有配置中提取用户可编辑字段
let saved = cfg
.get("channels")
.and_then(|c| c.get(storage_key))
.cloned()
.unwrap_or(Value::Null);
// 多账号模式:优先从 accounts.<account_id> 读取
let channel_val = cfg.get("channels").and_then(|c| c.get(storage_key));

let saved = match (&account_id, channel_val) {
(Some(acct), Some(ch)) if !acct.is_empty() => ch
.get("accounts")
.and_then(|a| a.get(acct.as_str()))
.cloned()
.unwrap_or(Value::Null),
(_, Some(ch)) => ch.clone(),
_ => Value::Null,
};

let mut form = Map::new();
let exists = !saved.is_null();
Expand Down Expand Up @@ -443,16 +454,54 @@ pub async fn save_messaging_platform(
}

/// 删除指定平台配置
/// account_id: 可选,指定时仅删除 channels.<platform>.accounts.<account_id>(多账号模式)
/// 未指定时删除整个平台配置
#[tauri::command]
pub async fn remove_messaging_platform(
platform: String,
account_id: Option<String>,
app: tauri::AppHandle,
) -> Result<Value, String> {
let mut cfg = super::config::load_openclaw_json()?;
let storage_key = platform_storage_key(&platform);

if let Some(channels) = cfg.get_mut("channels").and_then(|c| c.as_object_mut()) {
channels.remove(storage_key);
match &account_id {
Some(acct) if !acct.is_empty() => {
// 多账号模式:仅删除指定账号
if let Some(channel) = cfg.get_mut("channels").and_then(|c| c.get_mut(storage_key)) {
if let Some(accounts) = channel.get_mut("accounts").and_then(|a| a.as_object_mut())
{
accounts.remove(acct.as_str());
}
}
}
_ => {
// 整平台删除
if let Some(channels) = cfg.get_mut("channels").and_then(|c| c.as_object_mut()) {
channels.remove(storage_key);
}
}
}

// 清理对应的 bindings 条目
let binding_channel = platform_list_id(&platform);
if let Some(bindings) = cfg.get_mut("bindings").and_then(|b| b.as_array_mut()) {
bindings.retain(|b| {
let m = match b.get("match") {
Some(m) => m,
None => return true,
};
if m.get("channel").and_then(|v| v.as_str()) != Some(binding_channel) {
return true; // 不同渠道,保留
}
match &account_id {
Some(acct) if !acct.is_empty() => {
// 仅移除匹配该 accountId 的 binding
m.get("accountId").and_then(|v| v.as_str()) != Some(acct.as_str())
}
_ => false, // 整平台删除,移除该渠道所有 binding
}
});
}

super::config::save_openclaw_json(&cfg)?;
Expand Down Expand Up @@ -516,6 +565,7 @@ pub async fn verify_bot_token(platform: String, form: Value) -> Result<Value, St
}

/// 列出当前已配置的平台清单
/// 若平台包含 accounts 子对象(多账号模式),返回各账号的安全显示字段
#[tauri::command]
pub async fn list_configured_platforms() -> Result<Value, String> {
let cfg = super::config::load_openclaw_json()?;
Expand All @@ -524,9 +574,23 @@ pub async fn list_configured_platforms() -> Result<Value, String> {
if let Some(channels) = cfg.get("channels").and_then(|c| c.as_object()) {
for (name, val) in channels {
let enabled = val.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true);
let mut accounts: Vec<Value> = vec![];

// 提取多账号信息(仅安全字段,不含 appSecret 等敏感数据)
if let Some(accts) = val.get("accounts").and_then(|a| a.as_object()) {
for (acct_id, acct_val) in accts {
let mut entry = json!({ "accountId": acct_id });
if let Some(app_id) = acct_val.get("appId").and_then(|v| v.as_str()) {
entry["appId"] = Value::String(app_id.to_string());
}
accounts.push(entry);
}
}

result.push(json!({
"id": platform_list_id(name),
"enabled": enabled
"enabled": enabled,
"accounts": accounts
}));
}
}
Expand Down
8 changes: 8 additions & 0 deletions src/lib/channel-labels.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** 渠道 key → 中文显示名(供多页面复用) */
export const CHANNEL_LABELS = {
qqbot: 'QQ 机器人',
telegram: 'Telegram',
feishu: '飞书',
dingtalk: '钉钉',
discord: 'Discord',
}
4 changes: 2 additions & 2 deletions src/lib/tauri-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,9 @@ export const api = {
exportMemoryZip: (category, agentId) => invoke('export_memory_zip', { category, agentId: agentId || null }),

// 消息渠道管理
readPlatformConfig: (platform) => invoke('read_platform_config', { platform }),
readPlatformConfig: (platform, accountId) => invoke('read_platform_config', { platform, accountId: accountId || null }),
saveMessagingPlatform: (platform, form, accountId) => { invalidate('list_configured_platforms', 'read_platform_config'); return invoke('save_messaging_platform', { platform, form, accountId: accountId || null }) },
removeMessagingPlatform: (platform) => { invalidate('list_configured_platforms', 'read_platform_config'); return invoke('remove_messaging_platform', { platform }) },
removeMessagingPlatform: (platform, accountId) => { invalidate('list_configured_platforms', 'read_platform_config'); return invoke('remove_messaging_platform', { platform, accountId: accountId || null }) },
toggleMessagingPlatform: (platform, enabled) => { invalidate('list_configured_platforms', 'read_openclaw_config', 'read_platform_config'); return invoke('toggle_messaging_platform', { platform, enabled }) },
verifyBotToken: (platform, form) => invoke('verify_bot_token', { platform, form }),
listConfiguredPlatforms: () => cachedInvoke('list_configured_platforms', {}, 5000),
Expand Down
29 changes: 27 additions & 2 deletions src/pages/agents.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { api, invalidate } from '../lib/tauri-api.js'
import { toast } from '../components/toast.js'
import { showModal, showConfirm } from '../components/modal.js'
import { CHANNEL_LABELS } from '../lib/channel-labels.js'

export async function render() {
const page = document.createElement('div')
Expand All @@ -25,7 +26,7 @@ export async function render() {
</div>
`

const state = { agents: [] }
const state = { agents: [], bindings: [] }
// 非阻塞:先返回 DOM,后台加载数据
loadAgents(page, state)

Expand All @@ -52,7 +53,12 @@ async function loadAgents(page, state) {
const container = page.querySelector('#agents-list')
renderSkeleton(container)
try {
state.agents = await api.listAgents()
const [agents, config] = await Promise.all([
api.listAgents(),
api.readOpenclawConfig().catch(() => null),
])
state.agents = agents
state.bindings = Array.isArray(config?.bindings) ? config.bindings : []
renderAgents(page, state)

// 只在第一次加载时绑定事件(避免重复绑定)
Expand All @@ -66,6 +72,21 @@ async function loadAgents(page, state) {
}
}

/** 为指定 agent 生成绑定渠道的 badge HTML */
function renderBindingBadges(agentId, bindings) {
const matched = (bindings || []).filter(b => (b.agentId || 'main') === agentId)
if (!matched.length) {
return '<span style="color:var(--text-tertiary)">未绑定渠道</span>'
}
return matched.map(b => {
const channel = b.match?.channel || ''
const label = CHANNEL_LABELS[channel] || channel
const accountId = b.match?.accountId
const text = accountId ? `${label} · ${accountId}` : label
return `<span style="font-size:var(--font-size-xs);color:var(--accent);background:var(--accent-muted);padding:1px 6px;border-radius:10px;white-space:nowrap">${text}</span>`
}).join(' ')
}

function renderAgents(page, state) {
const container = page.querySelector('#agents-list')
if (!state.agents.length) {
Expand Down Expand Up @@ -102,6 +123,10 @@ function renderAgents(page, state) {
<span class="agent-info-label">工作区:</span>
<span class="agent-info-value" style="font-family:var(--font-mono);font-size:var(--font-size-xs)">${a.workspace || '未设置'}</span>
</div>
<div class="agent-info-row">
<span class="agent-info-label">绑定渠道:</span>
<span class="agent-info-value">${renderBindingBadges(a.id, state.bindings)}</span>
</div>
</div>
</div>
`
Expand Down
Loading
Loading