名称:molykit 描述:| 关键:用于MolyKit AI聊天工具包。触发词: BotClient, OpenAI, SSE流式传输, AI聊天, molykit, PlatformSend, spawn(), ThreadToken, 跨平台异步, 聊天组件, Messages, PromptInput, Avatar, LLM
MolyKit 技能
使用MolyKit和Makepad构建AI聊天界面的最佳实践 - 一个用于跨平台AI聊天应用程序的工具包。
源代码库:/Users/zhangalex/Work/Projects/FW/robius/moly/moly-kit
触发词
在以下情况使用此技能:
- 使用Makepad构建AI聊天界面
- 集成OpenAI或其他LLM API
- 实现跨平台异步(本地和WASM)
- 创建聊天组件(消息、提示输入、头像)
- 处理SSE流式响应
- 关键词:molykit, moly-kit, ai chat, bot client, openai makepad, chat widget, sse streaming
概述
MolyKit提供:
- 跨平台异步工具(PlatformSend, spawn(), ThreadToken)
- 即用型聊天组件(Chat, Messages, PromptInput, Avatar)
- BotClient特性用于AI提供商集成
- 兼容OpenAI的客户端,支持SSE流式传输
- 协议类型,用于消息、机器人和工具调用
- MCP(模型上下文协议)支持
跨平台异步模式
PlatformSend - 仅在本地实现Send
/// 意味着仅在本地平台实现Send,不在WASM上
/// - 在本地:由实现Send的类型实现
/// - 在WASM:由所有类型实现
pub trait PlatformSend: PlatformSendInner {}
/// 用于跨平台使用的盒装future类型
pub type BoxPlatformSendFuture<'a, T> = Pin<Box<dyn PlatformSendFuture<Output = T> + 'a>>;
/// 用于跨平台使用的盒装stream类型
pub type BoxPlatformSendStream<'a, T> = Pin<Box<dyn PlatformSendStream<Item = T> + 'a>>;
平台无关的spawn
/// 独立运行一个future
/// - 在本地使用tokio(需要Send)
/// - 在WASM使用wasm-bindgen-futures(不需要Send)
pub fn spawn(fut: impl PlatformSendFuture<Output = ()> + 'static);
// 用法
spawn(async move {
let result = fetch_data().await;
Cx::post_action(DataReady(result));
SignalToUI::set_ui_signal();
});
使用AbortOnDropHandle进行任务取消
/// 当丢弃时终止其future的句柄
pub struct AbortOnDropHandle(AbortHandle);
// 用法 - 当组件丢弃时任务取消
#[rust]
task_handle: Option<AbortOnDropHandle>,
fn start_task(&mut self) {
let (future, handle) = abort_on_drop(async move {
// 异步工作...
});
self.task_handle = Some(handle);
spawn(async move { let _ = future.await; });
}
ThreadToken用于WASM上的非Send类型
/// 将非Send值存储在thread-local中,通过token访问
pub struct ThreadToken<T: 'static>;
impl<T> ThreadToken<T> {
pub fn new(value: T) -> Self;
pub fn peek<R>(&self, f: impl FnOnce(&T) -> R) -> R;
pub fn peek_mut<R>(&self, f: impl FnOnce(&mut T) -> R) -> R;
}
// 用法 - 包装非Send类型以跨Send边界使用
let token = ThreadToken::new(non_send_value);
spawn(async move {
token.peek(|value| {
// 使用值...
});
});
BotClient 特性
实现AI提供商集成
pub trait BotClient: Send {
/// 发送消息并获取流式响应
fn send(
&mut self,
bot_id: &BotId,
messages: &[Message],
tools: &[Tool],
) -> BoxPlatformSendStream<'static, ClientResult<MessageContent>>;
/// 获取可用的机器人/模型
fn bots(&self) -> BoxPlatformSendFuture<'static, ClientResult<Vec<Bot>>>;
/// 克隆以传递
fn clone_box(&self) -> Box<dyn BotClient>;
}
// 用法
let client = OpenAIClient::new("https://api.openai.com/v1".into());
client.set_key("sk-...")?;
let context = BotContext::from(client);
BotContext - 可共享包装器
/// 带有已加载机器人的可共享包装器,用于同步UI访问
pub struct BotContext(Arc<Mutex<InnerBotContext>>);
impl BotContext {
pub fn load(&mut self) -> BoxPlatformSendFuture<ClientResult<()>>;
pub fn bots(&self) -> Vec<Bot>;
pub fn get_bot(&self, id: &BotId) -> Option<Bot>;
pub fn client(&self) -> Box<dyn BotClient>;
}
// 用法
let mut context = BotContext::from(client);
spawn(async move {
if let Err(errors) = context.load().await.into_result() {
// 处理错误
}
Cx::post_action(BotsLoaded);
});
协议类型
消息结构
pub struct Message {
pub from: EntityId, // 用户、系统、机器人(BotId)、应用
pub metadata: MessageMetadata,
pub content: MessageContent,
}
pub struct MessageContent {
pub text: String, // 主要内容(markdown)
pub reasoning: String, // AI推理/思考
pub citations: Vec<String>, // 源URL
pub attachments: Vec<Attachment>,
pub tool_calls: Vec<ToolCall>,
pub tool_results: Vec<ToolResult>,
}
pub struct MessageMetadata {
pub is_writing: bool, // 仍在流式传输中
pub created_at: DateTime<Utc>,
}
机器人标识
/// 全局唯一机器人ID:<len>;<id>@<provider>
pub struct BotId(Arc<str>);
impl BotId {
pub fn new(id: &str, provider: &str) -> Self;
pub fn id(&self) -> &str; // 提供商本地id
pub fn provider(&self) -> &str; // 提供商域
}
// 示例:BotId::new("gpt-4", "api.openai.com")
// -> "5;gpt-4@api.openai.com"
组件模式
Slot组件 - 运行时内容替换
live_design! {
pub Slot = {{Slot}} {
width: Fill, height: Fit,
slot = <View> {} // 默认内容
}
}
// 用法 - 在运行时替换内容
let mut slot = widget.slot(id!(content));
if let Some(custom) = client.content_widget(cx, ...) {
slot.replace(custom);
} else {
slot.restore(); // 恢复默认
slot.default().as_standard_message_content().set_content(cx, &content);
}
Avatar组件 - 文本/图像切换
live_design! {
pub Avatar = {{Avatar}} <View> {
grapheme = <RoundedView> {
visible: false,
label = <Label> { text: "P" }
}
dependency = <RoundedView> {
visible: false,
image = <Image> {}
}
}
}
impl Widget for Avatar {
fn draw_walk(&mut self, cx: &mut Cx2d, ...) -> DrawStep {
if let Some(avatar) = &self.avatar {
match avatar {
Picture::Grapheme(g) => {
self.view(id!(grapheme)).set_visible(cx, true);
self.view(id!(dependency)).set_visible(cx, false);
self.label(id!(label)).set_text(cx, &g);
}
Picture::Dependency(d) => {
self.view(id!(dependency)).set_visible(cx, true);
self.view(id!(grapheme)).set_visible(cx, false);
self.image(id!(image)).load_image_dep_by_path(cx, d.as_str());
}
}
}
self.deref.draw_walk(cx, scope, walk)
}
}
PromptInput组件
#[derive(Live, Widget)]
pub struct PromptInput {
#[deref] deref: CommandTextInput,
#[live] pub send_icon: LiveValue,
#[live] pub stop_icon: LiveValue,
#[rust] pub task: Task, // 发送或停止
#[rust] pub interactivity: Interactivity,
}
impl PromptInput {
pub fn submitted(&self, actions: &Actions) -> bool;
pub fn reset(&mut self, cx: &mut Cx);
pub fn set_send(&mut self);
pub fn set_stop(&mut self);
pub fn enable(&mut self);
pub fn disable(&mut self);
}
Messages组件 - 对话视图
#[derive(Live, Widget)]
pub struct Messages {
#[deref] deref: View,
#[rust] pub messages: Vec<Message>,
#[rust] pub bot_context: Option<BotContext>,
}
impl Messages {
pub fn set_messages(&mut self, messages: Vec<Message>, scroll_to_bottom: bool);
pub fn scroll_to_bottom(&mut self, cx: &mut Cx, triggered_by_stream: bool);
pub fn is_at_bottom(&self) -> bool;
}
UiRunner模式用于异步到UI
impl Widget for PromptInput {
fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
self.deref.handle_event(cx, event, scope);
self.ui_runner().handle(cx, event, scope, self);
if self.button(id!(attach)).clicked(event.actions()) {
let ui = self.ui_runner();
Attachment::pick_multiple(move |result| match result {
Ok(attachments) => {
ui.defer_with_redraw(move |me, cx, _| {
me.attachment_list_ref().write().attachments.extend(attachments);
});
}
Err(_) => {}
});
}
}
}
SSE流式传输
/// 将SSE字节流解析为消息流
pub fn parse_sse<S, B, E>(s: S) -> impl Stream<Item = Result<String, E>>
where
S: Stream<Item = Result<B, E>>,
B: AsRef<[u8]>,
{
// 在"
"处分割,提取"data:"内容
// 过滤注释和[DONE]消息
}
// 在BotClient::send中的用法
fn send(&mut self, ...) -> BoxPlatformSendStream<...> {
let stream = stream! {
let response = client.post(url).send().await?;
let events = parse_sse(response.bytes_stream());
for await event in events {
let completion: Completion = serde_json::from_str(&event)?;
content.text.push_str(&completion.delta.content);
yield ClientResult::new_ok(content.clone());
}
};
Box::pin(stream)
}
最佳实践
- 使用PlatformSend实现跨平台:相同代码在本地和WASM上工作
- 使用spawn()而不是tokio::spawn:平台无关的任务启动
- 使用AbortOnDropHandle:当组件丢弃时取消任务
- 使用ThreadToken处理WASM上的非Send类型:通过token访问的thread-local存储
- 使用Slot实现自定义内容:允许BotClient提供自定义组件
- 使用read()/write()模式:通过WidgetRef安全借用访问
- 使用UiRunner::defer_with_redraw:从异步上下文更新组件
- 处理ClientResult的部分成功:可能同时有值和错误
参考文件
llms.txt- 完整的MolyKit API参考