AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
// Tools that let a bot run the app itself: set up routines, manage bots, talk to other bots,
// change settings and list computers. In the original, routines are made by asking in chat
// ("例行任務是這個 Bot 依排程重複執行的任務。請在聊天中請它設定一個。", ZJmAzX) and the primary bot
// "也能和其他 Bot 協調工作" (mQQ/Uq). These run on the window's thread because they change the app.
use serde_json ::{ json , Value } ;
use crate ::model ::{ self , Bot , Message , State , Theme , Accent } ;
use crate ::providers ::{ ProviderKind , ALL_PROVIDERS } ;
use crate ::routines ::{ self , Routine , Schedule } ;
pub const NAMES : [ & str ; 20 ] = [
" list_routines " , " create_routine " , " update_routine " , " delete_routine " , " run_routine_now " ,
" list_bots " , " create_bot " , " update_bot " , " message_bot " , " list_computers " , " update_settings " ,
" remember " , " forget " , " save_skill " , " use_skill " , " delete_skill " ,
" create_group " , " move_to_group " , " rename_group " , " delete_group " ,
] ;
pub fn is_app_tool ( name : & str ) -> bool {
頁面: pages a bot makes and keeps up to date (like the original's tracking pages)
- write_page / read_page / list_pages: Markdown pages with versions (last 10 kept), per bot;
the reply gets a card (title, fading preview, page icon, 頁面 · 更新時間, 最近更新), and
[標題](page:ID) links open the page
- The page opens large: summary, what changed, the version picker, 複製 / 存成檔案 / 刪除;
listed under 資料庫 in the right panel
- Page blocks for richer pages, in the chat's Markdown too: ```stats tiles, ```steps progress,
> [!NOTE]/[!TIP]/[!WARNING]/[!IMPORTANT] call-outs (with tables and Mermaid charts)
- Phone: the card in chats, page links, a page view with the same blocks; charts drawn by the
AnyBot as SVG (GET /api/pages/<id>[/chart/<n>]); members may read pages
- Bots are told to make a page for things to track and to update it from a routine only when
there is real news
- Screenshot views: ANYBOT_SHOT_VIEW=pagecard / page
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 17:41:41 +00:00
NAMES . contains ( & name ) | | crate ::pages ::NAMES . contains ( & name )
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
}
pub fn label ( name : & str ) -> & 'static str {
match name {
" list_routines " = > " 查看例行任務 " ,
" create_routine " = > " 建立例行任務 " ,
" update_routine " = > " 修改例行任務 " ,
" delete_routine " = > " 刪除例行任務 " ,
" run_routine_now " = > " 立即執行例行任務 " ,
" list_bots " = > " 查看 Bot " ,
" create_bot " = > " 建立 Bot " ,
" update_bot " = > " 修改 Bot " ,
" message_bot " = > " 傳訊息給其他 Bot " ,
" list_computers " = > " 查看電腦 " ,
" update_settings " = > " 修改設定 " ,
" remember " = > " 記住 " ,
" forget " = > " 忘記 " ,
" save_skill " = > " 存成技能 " ,
" use_skill " = > " 讀取技能 " ,
" delete_skill " = > " 刪除技能 " ,
" create_group " = > " 建立群組 " ,
" move_to_group " = > " 移到群組 " ,
" rename_group " = > " 重新命名群組 " ,
" delete_group " = > " 刪除群組 " ,
頁面: pages a bot makes and keeps up to date (like the original's tracking pages)
- write_page / read_page / list_pages: Markdown pages with versions (last 10 kept), per bot;
the reply gets a card (title, fading preview, page icon, 頁面 · 更新時間, 最近更新), and
[標題](page:ID) links open the page
- The page opens large: summary, what changed, the version picker, 複製 / 存成檔案 / 刪除;
listed under 資料庫 in the right panel
- Page blocks for richer pages, in the chat's Markdown too: ```stats tiles, ```steps progress,
> [!NOTE]/[!TIP]/[!WARNING]/[!IMPORTANT] call-outs (with tables and Mermaid charts)
- Phone: the card in chats, page links, a page view with the same blocks; charts drawn by the
AnyBot as SVG (GET /api/pages/<id>[/chart/<n>]); members may read pages
- Bots are told to make a page for things to track and to update it from a routine only when
there is real news
- Screenshot views: ANYBOT_SHOT_VIEW=pagecard / page
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 17:41:41 +00:00
n if crate ::pages ::NAMES . contains ( & n ) = > crate ::pages ::label ( n ) ,
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
_ = > " App 操作 " ,
}
}
fn schedule_schema ( ) -> Value {
json! ( {
" type " : " object " ,
" description " : " When it repeats, in the person's words turned into fields. " ,
" properties " : {
" frequency " : { " type " : " string " , " enum " : [ " minutely " , " hourly " , " daily " , " weekly " , " monthly " , " yearly " ] } ,
" interval " : { " type " : " integer " , " description " : " Every N minutes (at least 5)/hours/days/weeks/months/years (default 1). " } ,
" once " : { " type " : " boolean " , " description " : " 僅一次: run once at the next matching time, then stop. " } ,
" flexible " : { " type " : " boolean " , " description " : " 「左右」: the exact minute doesn't matter (runs within ±15 minutes). " } ,
" daysOfWeek " : { " type " : " array " , " items " : { " type " : " string " , " enum " : routines ::WEEKDAYS } , " description " : " For weekly. Workdays = MO..FR. " } ,
" monthDay " : { " type " : " integer " , " description " : " Day of the month (monthly/yearly). " } ,
" weekdayOfMonth " : { " type " : " object " , " properties " : { " week " : { " type " : " integer " , " description " : " 1-4, or -1 for the last " } , " day " : { " type " : " string " , " enum " : routines ::WEEKDAYS } } } ,
" month " : { " type " : " integer " , " description " : " 1-12 (yearly). " } ,
" time " : { " type " : " string " , " description " : " 24-hour HH:MM (hourly: only the minutes count; minutely: ignored, use 00:00). " } ,
" timezone " : { " type " : " string " , " description " : " IANA name; leave empty for the person's time zone. " }
} ,
" required " : [ " frequency " , " time " ]
} )
}
pub fn specs ( ) -> Vec < Value > {
vec! [
json! ( { " name " : " list_routines " , " description " : " List this bot's routines (scheduled, repeating tasks). " , " parameters " : { " type " : " object " , " properties " : { } } } ) ,
json! ( { " name " : " remember " , " description " : " Keep a fact or preference the person wants you to remember across chats. " , " parameters " : { " type " : " object " , " properties " : { " fact " : { " type " : " string " } } , " required " : [ " fact " ] } } ) ,
json! ( { " name " : " forget " , " description " : " Forget a remembered fact (its number or words in it). " , " parameters " : { " type " : " object " , " properties " : { " what " : { " type " : " string " } } , " required " : [ " what " ] } } ) ,
json! ( { " name " : " save_skill " , " description " : " Save a reusable procedure as a skill every bot can use (replaces one with the same name). " , " parameters " : { " type " : " object " , " properties " : { " name " : { " type " : " string " } , " description " : { " type " : " string " , " description " : " One line: when to use it " } , " instructions " : { " type " : " string " , " description " : " The full steps " } } , " required " : [ " name " , " description " , " instructions " ] } } ) ,
json! ( { " name " : " use_skill " , " description " : " Load a skill's full instructions before doing that kind of task. " , " parameters " : { " type " : " object " , " properties " : { " name " : { " type " : " string " } } , " required " : [ " name " ] } } ) ,
json! ( { " name " : " delete_skill " , " description " : " Delete a skill. " , " parameters " : { " type " : " object " , " properties " : { " name " : { " type " : " string " } } , " required " : [ " name " ] } } ) ,
json! ( { " name " : " create_routine " , " description " : " Set up a routine when the person wants something done on a schedule (every morning, every Monday, on the 1st…) or when something happens (a PR is merged, CI fails, a Slack message mentions a word, a Sentry/PagerDuty/Linear change, a new email, a webhook call): give schedule or trigger. The prompt is what you will be asked each time it runs. " ,
" parameters " : { " type " : " object " , " properties " : {
" title " : { " type " : " string " } , " prompt " : { " type " : " string " } , " schedule " : schedule_schema ( ) ,
" trigger " : crate ::triggers ::schema ( ) ,
" computer " : { " type " : " string " , " description " : " Computer to work on when it runs (optional). " } } , " required " : [ " title " , " prompt " ] } } ) ,
json! ( { " name " : " update_routine " , " description " : " Change, pause or resume a routine. " ,
" parameters " : { " type " : " object " , " properties " : {
" id " : { " type " : " integer " } , " title " : { " type " : " string " } , " prompt " : { " type " : " string " } , " schedule " : schedule_schema ( ) , " trigger " : crate ::triggers ::schema ( ) , " paused " : { " type " : " boolean " } } , " required " : [ " id " ] } } ) ,
json! ( { " name " : " delete_routine " , " description " : " Delete a routine. The person is asked first. " , " parameters " : { " type " : " object " , " properties " : { " id " : { " type " : " integer " } } , " required " : [ " id " ] } } ) ,
json! ( { " name " : " run_routine_now " , " description " : " Run a routine once right away. " , " parameters " : { " type " : " object " , " properties " : { " id " : { " type " : " integer " } } , " required " : [ " id " ] } } ) ,
json! ( { " name " : " list_bots " , " description " : " List the person's bots with their provider, model and what they do. " , " parameters " : { " type " : " object " , " properties " : { } } } ) ,
json! ( { " name " : " create_bot " , " description " : " Create a new bot, optionally from a template (a ready role and instructions). " ,
" parameters " : { " type " : " object " , " properties " : {
" template " : { " type " : " string " , " enum " : crate ::memory ::TEMPLATES . iter ( ) . map ( | t | t . name ) . collect ::< Vec < _ > > ( ) } ,
" name " : { " type " : " string " } , " description " : { " type " : " string " } , " instructions " : { " type " : " string " } ,
" provider " : { " type " : " string " , " enum " : ALL_PROVIDERS . iter ( ) . map ( | p | p . key ( ) ) . collect ::< Vec < _ > > ( ) } , " model " : { " type " : " string " } } , " required " : [ ] } } ) ,
json! ( { " name " : " update_bot " , " description " : " Change a bot (yourself when bot is empty): name, description, instructions, provider, model, pin, default computer. " ,
" parameters " : { " type " : " object " , " properties " : {
" bot " : { " type " : " string " , " description " : " Bot name; empty = yourself. " } ,
" name " : { " type " : " string " } , " description " : { " type " : " string " } , " instructions " : { " type " : " string " } ,
" provider " : { " type " : " string " , " enum " : ALL_PROVIDERS . iter ( ) . map ( | p | p . key ( ) ) . collect ::< Vec < _ > > ( ) } , " model " : { " type " : " string " } ,
" pinned " : { " type " : " boolean " } , " default_computer " : { " type " : " string " } } } } ) ,
json! ( { " name " : " create_group " , " description " : " Make a group (a folder in the sidebar that holds bots, e.g. 文宣組). Optionally put bots in it right away. " ,
" parameters " : { " type " : " object " , " properties " : { " name " : { " type " : " string " } , " bots " : { " type " : " array " , " items " : { " type " : " string " } , " description " : " Bot names to move into it " } } , " required " : [ " name " ] } } ) ,
json! ( { " name " : " move_to_group " , " description " : " Move bots into a sidebar group (made if it doesn't exist). An empty group name moves them back to 未分區. " ,
" parameters " : { " type " : " object " , " properties " : { " group " : { " type " : " string " } , " bots " : { " type " : " array " , " items " : { " type " : " string " } } } , " required " : [ " group " , " bots " ] } } ) ,
json! ( { " name " : " rename_group " , " description " : " Rename a sidebar group. " , " parameters " : { " type " : " object " , " properties " : { " group " : { " type " : " string " } , " new_name " : { " type " : " string " } } , " required " : [ " group " , " new_name " ] } } ) ,
json! ( { " name " : " delete_group " , " description " : " Delete a sidebar group; its bots move to 未分區 (nothing is deleted). " , " parameters " : { " type " : " object " , " properties " : { " group " : { " type " : " string " } } , " required " : [ " group " ] } } ) ,
json! ( { " name " : " message_bot " , " description " : " Hand work to another bot: sends it a message and wakes it up. Its answer appears in its own chat. " ,
" parameters " : { " type " : " object " , " properties " : { " bot " : { " type " : " string " } , " text " : { " type " : " string " } } , " required " : [ " bot " , " text " ] } } ) ,
json! ( { " name " : " list_computers " , " description " : " List the computers you can work on and whether each is available. " , " parameters " : { " type " : " object " , " properties " : { } } } ) ,
json! ( { " name " : " update_settings " , " description " : " Change app settings: theme (light/dark/system), accent (bot_color/black), timezone (IANA). Running tasks on the person's own computer can only be turned on by the person. " ,
" parameters " : { " type " : " object " , " properties " : {
" theme " : { " type " : " string " , " enum " : [ " light " , " dark " , " system " ] } , " accent " : { " type " : " string " , " enum " : [ " bot_color " , " black " ] } , " timezone " : { " type " : " string " } } } } ) ,
]
頁面: pages a bot makes and keeps up to date (like the original's tracking pages)
- write_page / read_page / list_pages: Markdown pages with versions (last 10 kept), per bot;
the reply gets a card (title, fading preview, page icon, 頁面 · 更新時間, 最近更新), and
[標題](page:ID) links open the page
- The page opens large: summary, what changed, the version picker, 複製 / 存成檔案 / 刪除;
listed under 資料庫 in the right panel
- Page blocks for richer pages, in the chat's Markdown too: ```stats tiles, ```steps progress,
> [!NOTE]/[!TIP]/[!WARNING]/[!IMPORTANT] call-outs (with tables and Mermaid charts)
- Phone: the card in chats, page links, a page view with the same blocks; charts drawn by the
AnyBot as SVG (GET /api/pages/<id>[/chart/<n>]); members may read pages
- Bots are told to make a page for things to track and to update it from a routine only when
there is real news
- Screenshot views: ANYBOT_SHOT_VIEW=pagecard / page
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 17:41:41 +00:00
. into_iter ( )
. chain ( crate ::pages ::specs ( ) )
. collect ( )
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
}
/// One line for the chat row.
pub fn describe ( name : & str , args : & str ) -> String {
let v : Value = serde_json ::from_str ( args ) . unwrap_or ( Value ::Null ) ;
let s = | k : & str | v . get ( k ) . and_then ( Value ::as_str ) . unwrap_or ( " " ) . to_string ( ) ;
match name {
" create_routine " | " update_routine " = > {
let t = s ( " title " ) ;
let when = v . get ( " schedule " ) . and_then ( | x | serde_json ::from_value ::< Schedule > ( x . clone ( ) ) . ok ( ) ) . map ( | x | routines ::describe ( & x ) ) . unwrap_or_default ( ) ;
format! ( " {t} {when} " ) . trim ( ) . to_string ( )
}
" delete_routine " | " run_routine_now " = > format! ( " # {} " , v . get ( " id " ) . and_then ( Value ::as_u64 ) . unwrap_or ( 0 ) ) ,
" message_bot " = > format! ( " 給 {} : {} " , s ( " bot " ) , s ( " text " ) . chars ( ) . take ( 40 ) . collect ::< String > ( ) ) ,
" create_bot " | " update_bot " = > s ( " name " ) . to_string ( ) + & s ( " bot " ) ,
頁面: pages a bot makes and keeps up to date (like the original's tracking pages)
- write_page / read_page / list_pages: Markdown pages with versions (last 10 kept), per bot;
the reply gets a card (title, fading preview, page icon, 頁面 · 更新時間, 最近更新), and
[標題](page:ID) links open the page
- The page opens large: summary, what changed, the version picker, 複製 / 存成檔案 / 刪除;
listed under 資料庫 in the right panel
- Page blocks for richer pages, in the chat's Markdown too: ```stats tiles, ```steps progress,
> [!NOTE]/[!TIP]/[!WARNING]/[!IMPORTANT] call-outs (with tables and Mermaid charts)
- Phone: the card in chats, page links, a page view with the same blocks; charts drawn by the
AnyBot as SVG (GET /api/pages/<id>[/chart/<n>]); members may read pages
- Bots are told to make a page for things to track and to update it from a routine only when
there is real news
- Screenshot views: ANYBOT_SHOT_VIEW=pagecard / page
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 17:41:41 +00:00
n if crate ::pages ::NAMES . contains ( & n ) = > crate ::pages ::describe ( n , & v ) ,
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
_ = > String ::new ( ) ,
}
}
/// App actions that need a yes from the person first (the original confirms deletions:
/// "這會刪除例行任務並停止其後續執行。此操作無法復原。", VAgxC4).
pub fn needs_approval ( name : & str , args : & str , state : & State ) -> Option < String > {
let v : Value = serde_json ::from_str ( args ) . unwrap_or ( Value ::Null ) ;
if name = = " delete_routine " {
let id = v . get ( " id " ) . and_then ( Value ::as_u64 ) . unwrap_or ( 0 ) ;
let title = state . bots . iter ( ) . flat_map ( | b | b . routines . iter ( ) ) . find ( | r | r . id = = id ) . map ( | r | r . title . clone ( ) ) . unwrap_or_else ( | | format! ( " # {id} " ) ) ;
return Some ( format! ( " 刪除例行任務:「 {title} 」。這會停止它之後的執行,無法復原。 " ) ) ;
}
None
}
pub struct Outcome {
pub text : String ,
pub ok : bool ,
/// Bots to wake with a new turn (message_bot, run_routine_now).
pub wake : Vec < ( u64 , Message ) > ,
pub theme_changed : bool ,
頁面: pages a bot makes and keeps up to date (like the original's tracking pages)
- write_page / read_page / list_pages: Markdown pages with versions (last 10 kept), per bot;
the reply gets a card (title, fading preview, page icon, 頁面 · 更新時間, 最近更新), and
[標題](page:ID) links open the page
- The page opens large: summary, what changed, the version picker, 複製 / 存成檔案 / 刪除;
listed under 資料庫 in the right panel
- Page blocks for richer pages, in the chat's Markdown too: ```stats tiles, ```steps progress,
> [!NOTE]/[!TIP]/[!WARNING]/[!IMPORTANT] call-outs (with tables and Mermaid charts)
- Phone: the card in chats, page links, a page view with the same blocks; charts drawn by the
AnyBot as SVG (GET /api/pages/<id>[/chart/<n>]); members may read pages
- Bots are told to make a page for things to track and to update it from a routine only when
there is real news
- Screenshot views: ANYBOT_SHOT_VIEW=pagecard / page
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 17:41:41 +00:00
/// write_page: the page made or changed (its card goes under the reply).
pub page : Option < u64 > ,
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
}
fn ok ( text : String ) -> Outcome {
頁面: pages a bot makes and keeps up to date (like the original's tracking pages)
- write_page / read_page / list_pages: Markdown pages with versions (last 10 kept), per bot;
the reply gets a card (title, fading preview, page icon, 頁面 · 更新時間, 最近更新), and
[標題](page:ID) links open the page
- The page opens large: summary, what changed, the version picker, 複製 / 存成檔案 / 刪除;
listed under 資料庫 in the right panel
- Page blocks for richer pages, in the chat's Markdown too: ```stats tiles, ```steps progress,
> [!NOTE]/[!TIP]/[!WARNING]/[!IMPORTANT] call-outs (with tables and Mermaid charts)
- Phone: the card in chats, page links, a page view with the same blocks; charts drawn by the
AnyBot as SVG (GET /api/pages/<id>[/chart/<n>]); members may read pages
- Bots are told to make a page for things to track and to update it from a routine only when
there is real news
- Screenshot views: ANYBOT_SHOT_VIEW=pagecard / page
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 17:41:41 +00:00
Outcome { text , ok : true , wake : vec ! [ ] , theme_changed : false , page : None }
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
}
fn err ( text : String ) -> Outcome {
頁面: pages a bot makes and keeps up to date (like the original's tracking pages)
- write_page / read_page / list_pages: Markdown pages with versions (last 10 kept), per bot;
the reply gets a card (title, fading preview, page icon, 頁面 · 更新時間, 最近更新), and
[標題](page:ID) links open the page
- The page opens large: summary, what changed, the version picker, 複製 / 存成檔案 / 刪除;
listed under 資料庫 in the right panel
- Page blocks for richer pages, in the chat's Markdown too: ```stats tiles, ```steps progress,
> [!NOTE]/[!TIP]/[!WARNING]/[!IMPORTANT] call-outs (with tables and Mermaid charts)
- Phone: the card in chats, page links, a page view with the same blocks; charts drawn by the
AnyBot as SVG (GET /api/pages/<id>[/chart/<n>]); members may read pages
- Bots are told to make a page for things to track and to update it from a routine only when
there is real news
- Screenshot views: ANYBOT_SHOT_VIEW=pagecard / page
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 17:41:41 +00:00
Outcome { text , ok : false , wake : vec ! [ ] , theme_changed : false , page : None }
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
}
fn find_bot < ' a > ( state : & ' a State , name : & str ) -> Option < & ' a Bot > {
let n = name . trim ( ) . to_lowercase ( ) ;
state . bots . iter ( ) . find ( | b | b . name . to_lowercase ( ) = = n ) . or_else ( | | state . bots . iter ( ) . find ( | b | b . name . to_lowercase ( ) . contains ( & n ) ) )
}
fn provider_of ( key : & str ) -> Option < ProviderKind > {
ALL_PROVIDERS . iter ( ) . copied ( ) . find ( | p | p . key ( ) = = key )
}
Phone: a home with the computers, pages and routines; each bot's settings a tap away; long jobs fold up
The phone page was bare: one row per bot, a chat listing every step, a menu with four lines.
- Home: the main bot large at the top with its model and last words, the other bots with times,
then its computers (own box and the account's, online or not, 看畫面), the newest pages and the
routines with their next run.
- Chat: the bot's name opens its settings — model (chat models only), computer, past conversations
(restore), regenerate, new chat. A turn's steps fold into 「已完成 N 個步驟」 when done (the newest
three stay while it works); copy and regenerate under replies; four ready starts in a new chat;
a jump back to the newest message.
- Menu: 外觀 (follow the phone, light, dark).
- API: api/state carries pages and models; each bot its model, computers, routines, past
conversations; POST api/bots/{id}/set | regenerate | restore. The home's last line skips errors.
Checked on a local copy of the brain's data (cut off from the account and Docker) at iPhone size.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha2RVfSYmrhqNnqx9Z4
2026-11-06 12:28:08 +00:00
/// When a routine runs next, in words (for the phone's list).
pub fn routine_when ( r : & Routine ) -> String {
if let Some ( t ) = & r . trigger {
return format! ( " 事件觸發: {} " , t . describe ( ) ) ;
}
match ( r . paused , r . next_run ) {
( true , _ ) = > " 已暫停 " . to_string ( ) ,
( false , Some ( t ) ) = > format! ( " 下次 {} " , routines ::describe_when ( t , & r . schedule . timezone ) ) ,
_ = > " 沒有下次 " . to_string ( ) ,
}
}
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
pub fn routine_line ( r : & Routine ) -> String {
let next = match ( r . paused , r . next_run ) {
( true , _ ) = > " 已暫停 " . to_string ( ) ,
( false , Some ( t ) ) = > format! ( " 下次 {} " , routines ::describe_when ( t , & r . schedule . timezone ) ) ,
_ = > " 沒有下次 " . to_string ( ) ,
} ;
if let Some ( t ) = & r . trigger {
return format! ( " # {} 「 {} 」:事件觸發( {} ) {} " , r . id , r . title , t . describe ( ) , if r . paused { " · 已暫停 " } else { " " } ) ;
}
format! ( " # {} 「 {} 」: {} · {} " , r . id , r . title , routines ::describe ( & r . schedule ) , next )
}
/// A routine firing, as the message that wakes the bot.
pub fn wake_message ( r : & Routine ) -> Message {
Message { wake : true , .. Message ::user ( & format! ( " [例行任務]「 {} 」時間到了: {} " , r . title , r . prompt ) ) }
}
pub fn apply ( state : & mut State , bot_id : u64 , name : & str , args : & str ) -> Outcome {
let v : Value = match serde_json ::from_str ( if args . trim ( ) . is_empty ( ) { " {} " } else { args } ) {
Ok ( v ) = > v ,
Err ( _ ) = > return err ( " 參數不是有效的 JSON " . into ( ) ) ,
} ;
let s = | k : & str | v . get ( k ) . and_then ( Value ::as_str ) . map ( str ::to_string ) ;
let now = routines ::now ( ) ;
match name {
頁面: pages a bot makes and keeps up to date (like the original's tracking pages)
- write_page / read_page / list_pages: Markdown pages with versions (last 10 kept), per bot;
the reply gets a card (title, fading preview, page icon, 頁面 · 更新時間, 最近更新), and
[標題](page:ID) links open the page
- The page opens large: summary, what changed, the version picker, 複製 / 存成檔案 / 刪除;
listed under 資料庫 in the right panel
- Page blocks for richer pages, in the chat's Markdown too: ```stats tiles, ```steps progress,
> [!NOTE]/[!TIP]/[!WARNING]/[!IMPORTANT] call-outs (with tables and Mermaid charts)
- Phone: the card in chats, page links, a page view with the same blocks; charts drawn by the
AnyBot as SVG (GET /api/pages/<id>[/chart/<n>]); members may read pages
- Bots are told to make a page for things to track and to update it from a routine only when
there is real news
- Screenshot views: ANYBOT_SHOT_VIEW=pagecard / page
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 17:41:41 +00:00
" write_page " = > match crate ::pages ::write ( & mut state . pages , & mut state . next_page_id , bot_id , & v , crate ::model ::now_ms ( ) ) {
Ok ( w ) = > Outcome { text : w . text , ok : true , wake : vec ! [ ] , theme_changed : false , page : Some ( w . id ) } ,
Err ( e ) = > err ( e ) ,
} ,
" read_page " = > match crate ::pages ::read ( & state . pages , bot_id , & v ) {
Ok ( t ) = > ok ( t ) ,
Err ( e ) = > err ( e ) ,
} ,
" list_pages " = > ok ( crate ::pages ::list ( & state . pages , bot_id ) ) ,
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
" list_routines " = > {
let Some ( b ) = state . bot ( bot_id ) else { return err ( " 找不到這個 Bot " . into ( ) ) } ;
if b . routines . is_empty ( ) {
return ok ( " 還沒有例行任務。 " . into ( ) ) ;
}
ok ( b . routines . iter ( ) . map ( routine_line ) . collect ::< Vec < _ > > ( ) . join ( " \n " ) )
}
" create_routine " = > {
let trigger = match v . get ( " trigger " ) . filter ( | t | ! t . is_null ( ) ) {
Some ( t ) = > match serde_json ::from_value ::< crate ::triggers ::Trigger > ( t . clone ( ) ) . map_err ( | e | e . to_string ( ) ) . and_then ( | t | t . validate ( ) . map ( | _ | t ) ) {
Ok ( t ) = > Some ( t ) ,
Err ( e ) = > return err ( format! ( " 觸發條件有問題: {e} " ) ) ,
} ,
None = > None ,
} ;
let default_sched = json! ( { " frequency " : " daily " , " time " : " 09:00 " } ) ;
let raw = match ( v . get ( " schedule " ) , & trigger ) {
( Some ( r ) , _ ) = > r ,
( None , Some ( _ ) ) = > & default_sched ,
( None , None ) = > return err ( " 缺少 schedule( 或用 trigger 設定事件觸發) " . into ( ) ) ,
} ;
let mut raw = raw . clone ( ) ;
if raw . get ( " timezone " ) . and_then ( Value ::as_str ) . unwrap_or ( " " ) . is_empty ( ) {
raw [ " timezone " ] = json! ( state . timezone ) ;
}
let sched = match serde_json ::from_value ::< Schedule > ( raw ) . map_err ( | e | e . to_string ( ) ) . and_then ( routines ::validate ) {
Ok ( x ) = > x ,
Err ( e ) = > return err ( format! ( " 排程有問題: {e} " ) ) ,
} ;
let id = state . next_routine_id ;
state . next_routine_id + = 1 ;
let r = Routine {
id ,
title : s ( " title " ) . unwrap_or_else ( | | " 例行任務 " . into ( ) ) ,
prompt : s ( " prompt " ) . unwrap_or_default ( ) ,
next_run : if trigger . is_some ( ) { None } else { routines ::next_run ( & sched , now , now ) } ,
schedule : sched ,
paused : false ,
created_at : now ,
last_run : None ,
computer : s ( " computer " ) . unwrap_or_default ( ) ,
trigger ,
} ;
let line = routine_line ( & r ) ;
match state . bot_mut ( bot_id ) {
Some ( b ) = > b . routines . push ( r ) ,
None = > return err ( " 找不到這個 Bot " . into ( ) ) ,
}
ok ( format! ( " 已建立例行任務 {line} " ) )
}
" update_routine " | " delete_routine " | " run_routine_now " = > {
let id = v . get ( " id " ) . and_then ( Value ::as_u64 ) . unwrap_or ( 0 ) ;
let Some ( owner ) = state . routine_owner ( id ) else { return err ( format! ( " 找不到例行任務 # {id} " ) ) } ;
let tz = state . timezone . clone ( ) ;
let b = state . bot_mut ( owner ) . unwrap ( ) ;
let idx = b . routines . iter ( ) . position ( | r | r . id = = id ) . unwrap ( ) ;
match name {
" delete_routine " = > {
let r = b . routines . remove ( idx ) ;
ok ( format! ( " 已刪除例行任務「 {} 」 " , r . title ) )
}
" run_routine_now " = > {
let r = & mut b . routines [ idx ] ;
r . last_run = Some ( now ) ;
let msg = wake_message ( r ) ;
頁面: pages a bot makes and keeps up to date (like the original's tracking pages)
- write_page / read_page / list_pages: Markdown pages with versions (last 10 kept), per bot;
the reply gets a card (title, fading preview, page icon, 頁面 · 更新時間, 最近更新), and
[標題](page:ID) links open the page
- The page opens large: summary, what changed, the version picker, 複製 / 存成檔案 / 刪除;
listed under 資料庫 in the right panel
- Page blocks for richer pages, in the chat's Markdown too: ```stats tiles, ```steps progress,
> [!NOTE]/[!TIP]/[!WARNING]/[!IMPORTANT] call-outs (with tables and Mermaid charts)
- Phone: the card in chats, page links, a page view with the same blocks; charts drawn by the
AnyBot as SVG (GET /api/pages/<id>[/chart/<n>]); members may read pages
- Bots are told to make a page for things to track and to update it from a routine only when
there is real news
- Screenshot views: ANYBOT_SHOT_VIEW=pagecard / page
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 17:41:41 +00:00
Outcome { text : format ! ( " 已開始執行「{}」 " , r . title ) , ok : true , wake : vec ! [ ( owner , msg ) ] , theme_changed : false , page : None }
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
}
_ = > {
let r = & mut b . routines [ idx ] ;
if let Some ( t ) = s ( " title " ) {
r . title = t ;
}
if let Some ( p ) = s ( " prompt " ) {
r . prompt = p ;
}
if let Some ( raw ) = v . get ( " schedule " ) {
let mut raw = raw . clone ( ) ;
if raw . get ( " timezone " ) . and_then ( Value ::as_str ) . unwrap_or ( " " ) . is_empty ( ) {
raw [ " timezone " ] = json! ( tz ) ;
}
match serde_json ::from_value ::< Schedule > ( raw ) . map_err ( | e | e . to_string ( ) ) . and_then ( routines ::validate ) {
Ok ( x ) = > r . schedule = x ,
Err ( e ) = > return err ( format! ( " 排程有問題: {e} " ) ) ,
}
}
if let Some ( p ) = v . get ( " paused " ) . and_then ( Value ::as_bool ) {
r . paused = p ;
}
if let Some ( t ) = v . get ( " trigger " ) {
if t . is_null ( ) {
r . trigger = None ;
} else {
match serde_json ::from_value ::< crate ::triggers ::Trigger > ( t . clone ( ) ) . map_err ( | e | e . to_string ( ) ) . and_then ( | t | t . validate ( ) . map ( | _ | t ) ) {
Ok ( t ) = > r . trigger = Some ( t ) ,
Err ( e ) = > return err ( format! ( " 觸發條件有問題: {e} " ) ) ,
}
}
}
r . next_run = if r . trigger . is_some ( ) { None } else { routines ::next_run ( & r . schedule , r . created_at , now ) } ;
ok ( format! ( " 已更新 {} " , routine_line ( r ) ) )
}
}
}
" create_group " | " move_to_group " = > {
let group = s ( if name = = " create_group " { " name " } else { " group " } ) . unwrap_or_default ( ) . trim ( ) . to_string ( ) ;
if name = = " create_group " & & group . is_empty ( ) {
return err ( " 請給群組名稱 " . into ( ) ) ;
}
if ! group . is_empty ( ) & & ! state . folders . contains ( & group ) {
state . folders . push ( group . clone ( ) ) ;
}
let names : Vec < String > = v . get ( " bots " ) . and_then ( Value ::as_array ) . map ( | a | a . iter ( ) . filter_map ( | x | x . as_str ( ) . map ( String ::from ) ) . collect ( ) ) . unwrap_or_default ( ) ;
let mut moved = vec! [ ] ;
let mut missing = vec! [ ] ;
for n in names {
match find_bot ( state , & n ) . map ( | b | b . id ) {
Some ( id ) = > {
if let Some ( b ) = state . bot_mut ( id ) {
b . folder = group . clone ( ) ;
moved . push ( b . name . clone ( ) ) ;
}
}
None = > missing . push ( n ) ,
}
}
let mut text = if name = = " create_group " { format! ( " 已建立群組「 {group} 」。 " ) } else { String ::new ( ) } ;
if ! moved . is_empty ( ) {
text . push_str ( & format! ( " 已把 {} 移到 {} 。 " , moved . join ( " 、 " ) , if group . is_empty ( ) { " 未分區 " . to_string ( ) } else { format! ( " 群組「 {group} 」 " ) } ) ) ;
}
if ! missing . is_empty ( ) {
text . push_str ( & format! ( " 找不到: {} 。 " , missing . join ( " 、 " ) ) ) ;
}
頁面: pages a bot makes and keeps up to date (like the original's tracking pages)
- write_page / read_page / list_pages: Markdown pages with versions (last 10 kept), per bot;
the reply gets a card (title, fading preview, page icon, 頁面 · 更新時間, 最近更新), and
[標題](page:ID) links open the page
- The page opens large: summary, what changed, the version picker, 複製 / 存成檔案 / 刪除;
listed under 資料庫 in the right panel
- Page blocks for richer pages, in the chat's Markdown too: ```stats tiles, ```steps progress,
> [!NOTE]/[!TIP]/[!WARNING]/[!IMPORTANT] call-outs (with tables and Mermaid charts)
- Phone: the card in chats, page links, a page view with the same blocks; charts drawn by the
AnyBot as SVG (GET /api/pages/<id>[/chart/<n>]); members may read pages
- Bots are told to make a page for things to track and to update it from a routine only when
there is real news
- Screenshot views: ANYBOT_SHOT_VIEW=pagecard / page
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 17:41:41 +00:00
Outcome { text , ok : missing . is_empty ( ) | | ! moved . is_empty ( ) , wake : vec ! [ ] , theme_changed : false , page : None }
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
}
" rename_group " = > {
let ( old , new ) = ( s ( " group " ) . unwrap_or_default ( ) , s ( " new_name " ) . unwrap_or_default ( ) . trim ( ) . to_string ( ) ) ;
if new . is_empty ( ) {
return err ( " 請給新名稱 " . into ( ) ) ;
}
let exists = state . folders . contains ( & old ) | | state . bots . iter ( ) . any ( | b | b . folder = = old ) ;
if ! exists {
return err ( format! ( " 沒有叫「 {old} 」的群組 " ) ) ;
}
for f in state . folders . iter_mut ( ) {
if * f = = old {
* f = new . clone ( ) ;
}
}
if ! state . folders . contains ( & new ) {
state . folders . push ( new . clone ( ) ) ;
}
for b in state . bots . iter_mut ( ) {
if b . folder = = old {
b . folder = new . clone ( ) ;
}
}
ok ( format! ( " 已把群組「 {old} 」改名為「 {new} 」。 " ) )
}
" delete_group " = > {
let g = s ( " group " ) . unwrap_or_default ( ) ;
let n = state . bots . iter ( ) . filter ( | b | b . folder = = g ) . count ( ) ;
if ! state . folders . contains ( & g ) & & n = = 0 {
return err ( format! ( " 沒有叫「 {g} 」的群組 " ) ) ;
}
state . folders . retain ( | f | * f ! = g ) ;
for b in state . bots . iter_mut ( ) {
if b . folder = = g {
b . folder . clear ( ) ;
}
}
ok ( format! ( " 已刪除群組「 {g} 」,裡面的 {n} 個 Bot 移到未分區。 " ) )
}
" list_bots " = > ok ( state
. bots
. iter ( )
. map ( | b | {
let mut line = format! ( " 「 {} 」 {} {} · {} · {} " , b . name , if b . id = = state . primary_bot { " (主要 Bot) " } else { " " } , if b . folder . is_empty ( ) { String ::new ( ) } else { format! ( " [群組: {} ] " , b . folder ) } , b . provider . label ( ) , b . model ) ;
if ! b . description . is_empty ( ) {
line . push_str ( & format! ( " : {} " , b . description ) ) ;
}
if b . id = = bot_id {
line . push_str ( " (就是你) " ) ;
}
line
} )
. collect ::< Vec < _ > > ( )
. join ( " \n " ) ) ,
" remember " = > {
let Some ( b ) = state . bot_mut ( bot_id ) else { return err ( " 找不到這個 Bot " . into ( ) ) } ;
match crate ::memory ::remember ( & mut b . memories , & s ( " fact " ) . unwrap_or_default ( ) ) {
Ok ( t ) = > ok ( t ) ,
Err ( e ) = > err ( e ) ,
}
}
" forget " = > {
let Some ( b ) = state . bot_mut ( bot_id ) else { return err ( " 找不到這個 Bot " . into ( ) ) } ;
match crate ::memory ::forget ( & mut b . memories , & s ( " what " ) . unwrap_or_default ( ) ) {
Ok ( t ) = > ok ( t ) ,
Err ( e ) = > err ( e ) ,
}
}
" save_skill " = > {
let name = s ( " name " ) . unwrap_or_default ( ) . trim ( ) . to_string ( ) ;
let instructions = s ( " instructions " ) . unwrap_or_default ( ) ;
if name . is_empty ( ) | | instructions . trim ( ) . is_empty ( ) {
return err ( " 技能要有名稱和做法 " . into ( ) ) ;
}
2026-11-06 15:12:53 +00:00
crate ::memory ::upsert_skill ( & mut state . skills , crate ::memory ::Skill { name : name . clone ( ) , description : s ( " description " ) . unwrap_or_default ( ) , instructions } ) ;
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
ok ( format! ( " 已存成技能「 {name} 」,所有 Bot 都能用。 " ) )
}
" use_skill " = > {
let name = s ( " name " ) . unwrap_or_default ( ) ;
match state . skills . iter ( ) . find ( | k | k . name = = name . trim ( ) ) . or_else ( | | state . skills . iter ( ) . find ( | k | k . name . contains ( name . trim ( ) ) & & ! name . trim ( ) . is_empty ( ) ) ) {
Some ( k ) = > ok ( format! ( " 技能「 {} 」的做法: \n {} " , k . name , k . instructions ) ) ,
None = > err ( format! ( " 沒有叫「 {name} 」的技能。有: {} " , state . skills . iter ( ) . map ( | k | k . name . as_str ( ) ) . collect ::< Vec < _ > > ( ) . join ( " 、 " ) ) ) ,
}
}
" delete_skill " = > {
let name = s ( " name " ) . unwrap_or_default ( ) ;
let before = state . skills . len ( ) ;
state . skills . retain ( | k | k . name ! = name . trim ( ) ) ;
if state . skills . len ( ) < before { ok ( format! ( " 已刪除技能「 {name} 」 " ) ) } else { err ( format! ( " 沒有叫「 {name} 」的技能 " ) ) }
}
" create_bot " = > {
// 範本: a ready role and instructions.
let template = s ( " template " ) . and_then ( | t | crate ::memory ::TEMPLATES . iter ( ) . find ( | x | x . name = = t . trim ( ) ) ) ;
let name = s ( " name " ) . or_else ( | | template . map ( | t | t . name . to_string ( ) ) ) . unwrap_or_else ( | | " 新 Bot " . into ( ) ) ;
let base = state . bot ( bot_id ) . cloned ( ) ;
let provider = s ( " provider " ) . and_then ( | p | provider_of ( & p ) ) . or ( base . as_ref ( ) . map ( | b | b . provider ) ) . unwrap_or ( ProviderKind ::Xai ) ;
let model = s ( " model " ) . unwrap_or_else ( | | if Some ( provider ) = = base . as_ref ( ) . map ( | b | b . provider ) { base . as_ref ( ) . unwrap ( ) . model . clone ( ) } else { provider . suggested_model ( ) . into ( ) } ) ;
let id = state . next_id ;
state . next_id + = 1 ;
let shape = crate ::avatar ::SHAPES [ ( id as usize ) % crate ::avatar ::SHAPES . len ( ) ] ;
let color = crate ::avatar ::COLORS [ ( id as usize ) % crate ::avatar ::COLORS . len ( ) ] . 0 ;
state . bots . push ( Bot {
id ,
name : name . clone ( ) ,
description : s ( " description " ) . or_else ( | | template . map ( | t | t . description . to_string ( ) ) ) . unwrap_or_default ( ) ,
instructions : s ( " instructions " ) . or_else ( | | template . map ( | t | t . instructions . to_string ( ) ) ) . unwrap_or_default ( ) ,
shape : template . map ( | t | t . shape ) . unwrap_or ( shape ) . into ( ) ,
color : template . map ( | t | t . color ) . unwrap_or ( color ) . into ( ) ,
provider ,
model : model . clone ( ) ,
pinned : false ,
messages : vec ! [ ] ,
unread : false ,
tools_enabled : false ,
workdir : model ::default_workdir ( ) ,
allow_rules : vec ! [ ] ,
demos : vec ! [ ] ,
speak_replies : false ,
voice : String ::new ( ) ,
voice_rate : 1.0 ,
voice_lang : String ::new ( ) ,
folder : String ::new ( ) ,
memories : vec ! [ ] ,
members : vec ! [ ] ,
threads : vec ! [ ] ,
context_tokens : model ::default_context_tokens ( ) ,
routines : vec ! [ ] ,
last_turn : None ,
default_computer : model ::default_computer ( ) ,
} ) ;
ok ( format! ( " 已建立 Bot「 {name} 」( {} · {model} ) " , provider . label ( ) ) )
}
" update_bot " = > {
let target = match s ( " bot " ) . filter ( | n | ! n . trim ( ) . is_empty ( ) ) {
Some ( n ) = > match find_bot ( state , & n ) {
Some ( b ) = > b . id ,
None = > return err ( format! ( " 找不到叫「 {n} 」的 Bot " ) ) ,
} ,
None = > bot_id ,
} ;
let computers = state . computer_names ( ) ;
let b = state . bot_mut ( target ) . unwrap ( ) ;
let mut changed = vec! [ ] ;
if let Some ( x ) = s ( " name " ) {
b . name = x ;
changed . push ( " 名稱 " ) ;
}
if let Some ( x ) = s ( " description " ) {
b . description = x ;
changed . push ( " 描述 " ) ;
}
if let Some ( x ) = s ( " instructions " ) {
b . instructions = x ;
changed . push ( " 指示 " ) ;
}
if let Some ( p ) = s ( " provider " ) {
match provider_of ( & p ) {
Some ( k ) = > {
b . provider = k ;
if s ( " model " ) . is_none ( ) {
b . model = k . suggested_model ( ) . into ( ) ;
}
changed . push ( " AI 供應商 " ) ;
}
None = > return err ( format! ( " 沒有「 {p} 」這個供應商 " ) ) ,
}
}
if let Some ( m ) = s ( " model " ) {
b . model = m ;
changed . push ( " 模型 " ) ;
}
if let Some ( p ) = v . get ( " pinned " ) . and_then ( Value ::as_bool ) {
b . pinned = p ;
changed . push ( " 釘選 " ) ;
}
if let Some ( c ) = s ( " default_computer " ) {
if ! computers . contains ( & c ) {
return err ( format! ( " 沒有「 {c} 」這台電腦。可以用: {} " , computers . join ( " 、 " ) ) ) ;
}
b . default_computer = c ;
changed . push ( " 預設電腦 " ) ;
}
if changed . is_empty ( ) {
return err ( " 沒有要改的項目 " . into ( ) ) ;
}
2026-11-05 06:46:42 +00:00
ok ( format! ( " 已更新「 {} 」的 {} (現在: {} · {} ,預設電腦「 {} 」) " , b . name , changed . join ( " 、 " ) , b . provider . label ( ) , b . model , b . default_computer ) )
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
}
" message_bot " = > {
let Some ( n ) = s ( " bot " ) else { return err ( " 要指定 Bot " . into ( ) ) } ;
let Some ( target ) = find_bot ( state , & n ) . map ( | b | b . id ) else { return err ( format! ( " 找不到叫「 {n} 」的 Bot " ) ) } ;
if target = = bot_id {
return err ( " 不能傳訊息給自己 " . into ( ) ) ;
}
let from = state . bot ( bot_id ) . map ( | b | b . name . clone ( ) ) . unwrap_or_default ( ) ;
let text = s ( " text " ) . unwrap_or_default ( ) ;
let msg = Message { wake : true , .. Message ::user ( & format! ( " [來自 Bot] 「 {from} 」: {text} " ) ) } ;
let name = state . bot ( target ) . map ( | b | b . name . clone ( ) ) . unwrap_or_default ( ) ;
頁面: pages a bot makes and keeps up to date (like the original's tracking pages)
- write_page / read_page / list_pages: Markdown pages with versions (last 10 kept), per bot;
the reply gets a card (title, fading preview, page icon, 頁面 · 更新時間, 最近更新), and
[標題](page:ID) links open the page
- The page opens large: summary, what changed, the version picker, 複製 / 存成檔案 / 刪除;
listed under 資料庫 in the right panel
- Page blocks for richer pages, in the chat's Markdown too: ```stats tiles, ```steps progress,
> [!NOTE]/[!TIP]/[!WARNING]/[!IMPORTANT] call-outs (with tables and Mermaid charts)
- Phone: the card in chats, page links, a page view with the same blocks; charts drawn by the
AnyBot as SVG (GET /api/pages/<id>[/chart/<n>]); members may read pages
- Bots are told to make a page for things to track and to update it from a routine only when
there is real news
- Screenshot views: ANYBOT_SHOT_VIEW=pagecard / page
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 17:41:41 +00:00
Outcome { text : format ! ( " 已交給「{name}」,它的回覆會出現在它自己的聊天裡。 " ) , ok : true , wake : vec ! [ ( target , msg ) ] , theme_changed : false , page : None }
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
}
" update_settings " = > {
頁面: pages a bot makes and keeps up to date (like the original's tracking pages)
- write_page / read_page / list_pages: Markdown pages with versions (last 10 kept), per bot;
the reply gets a card (title, fading preview, page icon, 頁面 · 更新時間, 最近更新), and
[標題](page:ID) links open the page
- The page opens large: summary, what changed, the version picker, 複製 / 存成檔案 / 刪除;
listed under 資料庫 in the right panel
- Page blocks for richer pages, in the chat's Markdown too: ```stats tiles, ```steps progress,
> [!NOTE]/[!TIP]/[!WARNING]/[!IMPORTANT] call-outs (with tables and Mermaid charts)
- Phone: the card in chats, page links, a page view with the same blocks; charts drawn by the
AnyBot as SVG (GET /api/pages/<id>[/chart/<n>]); members may read pages
- Bots are told to make a page for things to track and to update it from a routine only when
there is real news
- Screenshot views: ANYBOT_SHOT_VIEW=pagecard / page
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 17:41:41 +00:00
let mut out = Outcome { text : String ::new ( ) , ok : true , wake : vec ! [ ] , theme_changed : false , page : None } ;
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
let mut changed = vec! [ ] ;
if let Some ( t ) = s ( " theme " ) {
2026-11-05 07:48:20 +00:00
// An unknown name used to mean Light with no word about it.
let ( th , known ) = match t . as_str ( ) {
" dark " = > ( Theme ::Dark , true ) ,
" system " = > ( Theme ::System , true ) ,
" light " = > ( Theme ::Light , true ) ,
_ = > ( state . theme , false ) ,
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
} ;
2026-11-05 07:48:20 +00:00
if known {
state . theme = th ;
out . theme_changed = true ;
changed . push ( format! ( " 主題 → {t} " ) ) ;
} else {
out . text . push_str ( & format! ( " 主題「 {t} 」不存在(可用 dark、light、system) 。 \n " ) ) ;
}
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
}
if let Some ( a ) = s ( " accent " ) {
state . accent = if a = = " bot_color " { Accent ::BotColor } else { Accent ::Black } ;
changed . push ( format! ( " 強調色 → {a} " ) ) ;
}
if let Some ( z ) = s ( " timezone " ) {
if z . parse ::< chrono_tz ::Tz > ( ) . is_err ( ) {
return err ( format! ( " 「 {z} 」不是有效的時區名稱(例如 Asia/Taipei) " ) ) ;
}
state . timezone = z . clone ( ) ;
changed . push ( format! ( " 時區 → {z} " ) ) ;
}
if changed . is_empty ( ) {
return err ( " 沒有要改的設定 " . into ( ) ) ;
}
out . text = format! ( " 已更新: {} " , changed . join ( " 、 " ) ) ;
out
}
other = > err ( format! ( " 沒有「 {other} 」這個操作 " ) ) ,
}
}
#[ cfg(test) ]
mod tests {
use super ::* ;
#[ test ]
fn chat_sets_up_and_manages_routines ( ) {
2026-11-05 06:25:01 +00:00
let mut st = State ::samples ( ) ;
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
let r = apply ( & mut st , 1 , " create_routine " , r # "{"title":"早報","prompt":"整理新聞","schedule":{"frequency":"weekly","daysOfWeek":["MO","TU","WE","TH","FR"],"time":"09:00"}}"# ) ;
assert! ( r . ok , " {} " , r . text ) ;
assert! ( r . text . contains ( " 每個工作日 09:00 " ) , " {} " , r . text ) ;
let b = st . bot ( 1 ) . unwrap ( ) ;
assert_eq! ( b . routines . len ( ) , 1 ) ;
assert! ( b . routines [ 0 ] . next_run . unwrap ( ) > routines ::now ( ) ) ;
let id = b . routines [ 0 ] . id ;
assert! ( apply ( & mut st , 1 , " update_routine " , & format! ( r # "{{"id":{id},"paused":true}}"# ) ) . text . contains ( " 已暫停 " ) ) ;
assert! ( needs_approval ( " delete_routine " , & format! ( r # "{{"id":{id}}}"# ) , & st ) . unwrap ( ) . contains ( " 早報 " ) ) ;
let w = apply ( & mut st , 1 , " run_routine_now " , & format! ( r # "{{"id":{id}}}"# ) ) ;
assert_eq! ( w . wake [ 0 ] . 0 , 1 ) ;
assert! ( w . wake [ 0 ] . 1. wake ) ;
assert! ( apply ( & mut st , 1 , " delete_routine " , & format! ( r # "{{"id":{id}}}"# ) ) . ok ) ;
assert! ( st . bot ( 1 ) . unwrap ( ) . routines . is_empty ( ) ) ;
assert! ( ! apply ( & mut st , 1 , " create_routine " , r # "{"title":"x","prompt":"y","schedule":{"frequency":"weekly","time":"09:00"}}"# ) . ok ) ;
}
/// The person's case: the app closed while two replies were coming in, leaving empty
/// replies that looked stuck. Loading repairs them so they can be retried.
#[ test ]
fn interrupted_replies_are_repaired ( ) {
2026-11-05 06:25:01 +00:00
let mut st = State ::samples ( ) ;
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
let b = st . bot_mut ( 1 ) . unwrap ( ) ;
b . messages = vec! [
Message ::user ( " 你好 " ) ,
Message ::assistant ( " 你好! " , " x " ) ,
Message ::user ( " 找宮古島攻略 " ) ,
Message { thinking : " 想一下 " . into ( ) , .. Message ::assistant ( " " , " x " ) } ,
Message ::user ( " 嗨 " ) ,
Message ::assistant ( " " , " x " ) ,
] ;
assert! ( b . repair_interrupted ( ) ) ;
assert_eq! ( b . messages [ 1 ] . text , " 你好! " ) ;
assert! ( b . messages [ 3 ] . error & & b . messages [ 5 ] . error ) ;
assert_eq! ( b . messages [ 5 ] . text , model ::INTERRUPTED_NOTE ) ;
assert_eq! ( b . last_turn . as_ref ( ) . unwrap ( ) . outcome , model ::TurnOutcome ::Error ) ;
assert! ( ! b . repair_interrupted ( ) , " a second pass changes nothing " ) ;
}
#[ test ]
fn chat_manages_bots_and_settings ( ) {
2026-11-05 06:25:01 +00:00
let mut st = State ::samples ( ) ;
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
assert! ( apply ( & mut st , 1 , " update_bot " , r # "{"provider":"openai","model":"gpt-5-mini"}"# ) . text . contains ( " OpenAI · gpt-5-mini " ) ) ;
assert! ( apply ( & mut st , 1 , " create_bot " , r # "{"name":"翻譯官","provider":"gemini"}"# ) . ok ) ;
assert! ( st . bots . iter ( ) . any ( | b | b . name = = " 翻譯官 " & & b . provider = = ProviderKind ::Gemini ) ) ;
let m = apply ( & mut st , 1 , " message_bot " , r # "{"bot":"寫作","text":"幫我想標題"}"# ) ;
assert! ( m . ok & & m . wake [ 0 ] . 0 = = 3 , " {} " , m . text ) ;
assert! ( apply ( & mut st , 1 , " update_bot " , r # "{"default_computer":"火星"}"# ) . text . contains ( " 沒有 " ) ) ;
assert! ( apply ( & mut st , 1 , " update_settings " , r # "{"theme":"dark","timezone":"Asia/Tokyo"}"# ) . theme_changed ) ;
assert_eq! ( st . timezone , " Asia/Tokyo " ) ;
}
#[ test ]
fn groups_from_the_chat ( ) {
2026-11-05 06:25:01 +00:00
let mut st = State ::samples ( ) ;
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
let names : Vec < String > = st . bots . iter ( ) . take ( 2 ) . map ( | b | b . name . clone ( ) ) . collect ( ) ;
let o = apply ( & mut st , 1 , " create_group " , & serde_json ::json! ( { " name " : " 文宣組 " , " bots " : names } ) . to_string ( ) ) ;
assert! ( o . ok & & o . text . contains ( " 文宣組 " ) , " {} " , o . text ) ;
assert_eq! ( st . bots . iter ( ) . filter ( | b | b . folder = = " 文宣組 " ) . count ( ) , 2 ) ;
assert! ( st . folders . contains ( & " 文宣組 " . to_string ( ) ) ) ;
assert! ( apply ( & mut st , 1 , " rename_group " , r # "{"group":"文宣組","new_name":"行銷組"}"# ) . ok ) ;
assert_eq! ( st . bots . iter ( ) . filter ( | b | b . folder = = " 行銷組 " ) . count ( ) , 2 ) ;
assert! ( apply ( & mut st , 1 , " list_bots " , " {} " ) . text . contains ( " [群組:行銷組] " ) ) ;
assert! ( apply ( & mut st , 1 , " delete_group " , r # "{"group":"行銷組"}"# ) . ok ) ;
assert! ( st . bots . iter ( ) . all ( | b | b . folder . is_empty ( ) ) & & st . folders . is_empty ( ) ) ;
assert! ( ! apply ( & mut st , 1 , " rename_group " , r # "{"group":"沒有","new_name":"x"}"# ) . ok ) ;
}
}