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 )
}
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 ( ) ) ;
}
let k = crate ::memory ::Skill { name : name . clone ( ) , description : s ( " description " ) . unwrap_or_default ( ) , instructions } ;
match state . skills . iter_mut ( ) . find ( | x | x . name = = name ) {
Some ( x ) = > * x = k ,
None = > state . skills . push ( k ) ,
}
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 ( ) ) ;
}
ok ( format! ( " 已更新「 {} 」的 {} (現在: {} · {} ) " , b . name , changed . join ( " 、 " ) , b . provider . label ( ) , b . model ) )
}
" 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
}
" list_computers " = > {
let mut lines = vec! [
format! ( " {} :你自己的電腦(獨立的資料夾),一律可用 " , model ::BOT_COMPUTER_NAME ) ,
format! ( " {} :這台 Mac, {} " , model ::THIS_COMPUTER_NAME , if state . local_execution { " 已允許 " } else { " 使用者還沒在設定裡允許(設定 → 電腦 → 在這台電腦上執行) " } ) ,
] ;
lines . extend ( state . remote_computers . iter ( ) . map ( | c | format! ( " {} :透過 SSH 連到 {} " , c . name , c . ssh_target ) ) ) ;
ok ( lines . join ( " \n " ) )
}
" 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 " ) {
state . theme = match t . as_str ( ) {
" dark " = > Theme ::Dark ,
" system " = > Theme ::System ,
_ = > Theme ::Light ,
} ;
out . theme_changed = true ;
changed . push ( format! ( " 主題 → {t} " ) ) ;
}
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 " ) ;
assert! ( apply ( & mut st , 1 , " list_computers " , " {} " ) . text . contains ( " 還沒 " ) ) ;
}
#[ 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 ) ;
}
}