import { getInstalledToolSchema } from './marketplace/index.js'

// 所有工具的 schema 定义
export const TOOL_SCHEMAS = {
  express: {
    type: 'function',
    function: {
      name: 'express',
      description: 'Express content to an individual by ID. This is the behavior-layer communication outlet. Supports text or voice format.',
      parameters: {
        type: 'object',
        properties: {
          target_id: {
            type: 'string',
            description: 'Recipient ID, such as ID:000001.'
          },
          content: {
            type: 'string',
            description: 'Content to express.'
          },
          format: {
            type: 'string',
            enum: ['text', 'voice'],
            description: 'Expression format, default text.'
          }
        },
        required: ['target_id', 'content']
      }
    }
  },

  send_message: {
    type: 'function',
    function: {
      name: 'send_message',
      description: 'Send a message to an individual by ID. All outbound communication must use this tool; do not output reply content directly. The same canonical user ID (e.g. ID:000001) may be reachable on multiple channels — use the optional channel parameter to override the default routing.',
      parameters: {
        type: 'object',
        properties: {
          target_id: {
            type: 'string',
            description: 'Recipient ID, such as ID:000001.'
          },
          content: {
            type: 'string',
            description: 'Message content.'
          },
          channel: {
            type: 'string',
            enum: ['WECHAT', 'DISCORD', 'FEISHU', 'WECOM', 'TUI', 'AUTO'],
            description: 'Optional delivery channel. AUTO (default) follows the channel of the user\'s most recent message — if they last reached you on WECHAT, your message goes to WECHAT (this also holds for follow-ups triggered later by reminders or ticks). Pass an explicit channel (e.g. TUI for long-form output that belongs on the local UI) to override.'
          }
        },
        required: ['target_id', 'content']
      }
    }
  },

  read_file: {
    type: 'function',
    function: {
      name: 'read_file',
      description: 'Read the contents of a file at the specified path.',
      parameters: {
        type: 'object',
        properties: {
          path: {
            type: 'string',
            description: 'Absolute or relative file path.'
          }
        },
        required: ['path']
      }
    }
  },

  list_dir: {
    type: 'function',
    function: {
      name: 'list_dir',
      description: 'List files and folders under the specified directory.',
      parameters: {
        type: 'object',
        properties: {
          path: {
            type: 'string',
            description: 'Directory path, defaults to the current directory.'
          }
        },
        required: []
      }
    }
  },

  write_file: {
    type: 'function',
    function: {
      name: 'write_file',
      description: 'Write content to the specified file. Creates the file automatically if it does not exist.',
      parameters: {
        type: 'object',
        properties: {
          path: {
            type: 'string',
            description: 'File path.'
          },
          content: {
            type: 'string',
            description: 'Content to write.'
          }
        },
        required: ['path', 'content']
      }
    }
  },

  web_search: {
    type: 'function',
    function: {
      name: 'web_search',
      description: 'Search the web for current or unknown information. Use this before fetch_url when you do not already know the exact reliable URL. Returns structured JSON with result titles, URLs, snippets, and ok/error status.',
      parameters: {
        type: 'object',
        properties: {
          query: {
            type: 'string',
            description: 'Search query. Be specific, include product/version/date keywords when relevant.'
          },
          limit: {
            type: 'number',
            description: 'Maximum results to return, default 5, max 8.'
          }
        },
        required: ['query']
      }
    }
  },

  fetch_url: {
    type: 'function',
    recognizer_highlights: ['body_path', 'title', 'url', 'content_length'],
    function: {
      name: 'fetch_url',
      description: 'Open a known URL with a lightweight HTTP request. Returns structured JSON with ok/status/title/content/body_path/error. Long articles (>=2000 chars) are auto-saved to sandbox/articles/ and content is truncated to a short excerpt; use the returned body_path with read_file to open the full text. Do not use this tool as a search engine. If ok is false because content is empty, blocked, or JS-rendered, try browser_read or another URL; never summarize an error as page content.',
      parameters: {
        type: 'object',
        properties: {
          url: {
            type: 'string',
            description: 'URL to open. Prefer reliable source pages found through web_search.'
          }
        },
        required: ['url']
      }
    }
  },

  browser_read: {
    type: 'function',
    recognizer_highlights: ['body_path', 'title', 'url', 'content_length'],
    function: {
      name: 'browser_read',
      description: 'Use a real headless Chromium browser to open and render a webpage, wait for JavaScript, scroll, and extract readable text. Use this when fetch_url returns no readable content, a waiting page, or a JS-rendered page. Returns structured JSON with ok/title/content/body_path/error. Long articles (>=2000 chars) are auto-saved to sandbox/articles/ and content is truncated to a short excerpt; use body_path with read_file to open the full text.',
      parameters: {
        type: 'object',
        properties: {
          url: {
            type: 'string',
            description: 'URL to open in the browser.'
          },
          timeout_ms: {
            type: 'number',
            description: 'Navigation/render timeout in milliseconds, default 20000, max 45000.'
          },
          max_chars: {
            type: 'number',
            description: 'Maximum extracted characters to return, default 8000, max 12000.'
          }
        },
        required: ['url']
      }
    }
  },

  delete_file: {
    type: 'function',
    function: {
      name: 'delete_file',
      description: 'Delete a file or directory inside the sandbox. Directories are removed recursively. System files such as readme.txt and world.txt cannot be deleted.',
      parameters: {
        type: 'object',
        properties: {
          path: { type: 'string', description: 'File or directory path to delete, relative to the sandbox.' }
        },
        required: ['path']
      }
    }
  },

  make_dir: {
    type: 'function',
    function: {
      name: 'make_dir',
      description: 'Create a directory inside the sandbox. Nested paths such as projects/myapp/src are supported.',
      parameters: {
        type: 'object',
        properties: {
          path: { type: 'string', description: 'Directory path to create.' }
        },
        required: ['path']
      }
    }
  },

  exec_command: {
    type: 'function',
    function: {
      name: 'exec_command',
      description: 'Run a shell command. Returns structured JSON with ok, mode, exit_code, stdout, stderr, timed_out, pid. On Windows runs in PowerShell — use PowerShell syntax (e.g. Get-ChildItem, $env:USERPROFILE, Write-Output). Use background=true for long-running servers. Use cwd to run in a sandbox subdirectory instead of cd-chaining. Use promote_to_background=true so a foreground timeout converts the process to background instead of killing it.',
      parameters: {
        type: 'object',
        properties: {
          command: { type: 'string', description: 'Command to run, such as "node server.js", "npm install", or "python main.py".' },
          background: { type: 'boolean', description: 'Run in the background, default false. Set true when starting a server.' },
          timeout: { type: 'number', description: 'Foreground execution timeout in seconds, default 30, max 120.' },
          cwd: { type: 'string', description: 'Subdirectory within the sandbox to run the command in, e.g. "myproject". Avoids cd-chaining. Must be a relative path.' },
          promote_to_background: { type: 'boolean', description: 'When foreground execution times out, promote to background instead of killing the process. Returns the new pid.' }
        },
        required: ['command']
      }
    }
  },

  kill_process: {
    type: 'function',
    function: {
      name: 'kill_process',
      description: 'Stop a background process by PID. Returns structured JSON with ok, pid, command, stopped, or error.',
      parameters: {
        type: 'object',
        properties: {
          pid: { type: 'number', description: 'PID of the process to stop.' }
        },
        required: ['pid']
      }
    }
  },

  list_processes: {
    type: 'function',
    function: {
      name: 'list_processes',
      description: 'List current background processes with their recent output. Returns ok, count, and processes (each with pid, command, started_at, recent_output). Use tail to control how many output lines to include per process (default 20, max 200).',
      parameters: {
        type: 'object',
        properties: {
          tail: { type: 'number', description: 'Number of recent output lines to return per process, default 20.' }
        }
      }
    }
  },

  speak: {
    type: 'function',
    function: {
      name: 'speak',
      description: 'Convert text to speech and save it as an audio file. Use only for creative content such as poems, prose, narration, or lyric reading. Do not use for normal chat replies; voice replies are handled automatically by the system. Keep text under 500 Chinese characters.',
      parameters: {
        type: 'object',
        properties: {
          text: { type: 'string', description: 'Text to convert to speech.' },
          voice_id: { type: 'string', description: 'Optional voice ID. Available values: male-qn-qingse, male-qn-jingying, male-qn-badao, female-shaonv, female-yujie, female-chengshu, presenter_male, presenter_female. Default: male-qn-qingse.' },
          filename: { type: 'string', description: 'Optional output filename without extension.' },
        },
        required: ['text']
      }
    }
  },

  generate_lyrics: {
    type: 'function',
    function: {
      name: 'generate_lyrics',
      description: 'Generate complete song lyrics from a creative direction, including title, style tags, and lyric structure. The result is saved automatically under sandbox/lyrics/ and can be passed to generate_music.',
      parameters: {
        type: 'object',
        properties: {
          prompt: { type: 'string', description: 'Creative direction, theme, or emotional description for the lyrics.' },
          mode: { type: 'string', description: 'Mode: write_full_song by default, generating complete lyrics.' },
        },
        required: ['prompt']
      }
    }
  },

  set_tick_interval: {
    type: 'function',
    function: {
      name: 'set_tick_interval',
      description: 'Adjust your own thinking rhythm by setting the TICK interval for the next span of time. Use shorter intervals during urgent or important work and longer intervals when idle or reflecting. seconds range [2, 3600], ttl range [1, 50]; out-of-range values are clamped.',
      parameters: {
        type: 'object',
        properties: {
          seconds: { type: 'number', description: 'TICK interval in seconds, range [2, 3600].' },
          ttl: { type: 'number', description: 'Number of turns to keep this rhythm, range [1, 50]. Defaults to 10 and then returns to the default rhythm.' },
          reason: { type: 'string', description: 'Optional short reason for later self-reference.' },
        },
        required: ['seconds']
      }
    }
  },

  media_mode: {
    type: 'function',
    function: {
      name: 'media_mode',
      description: `Control the brain-ui media stage. video opens from the right, image opens from the left, and music opens a record-player card from the right.
Platform selection (check Country Code / Timezone from Supplemental Context):
  - China (CN / Asia/Shanghai etc.) → prefer Bilibili for videos.
  - Other regions → prefer YouTube for videos.
Video URL rules, important because violations can cause a blank player:
  - YouTube: use a full watch URL such as https://www.youtube.com/watch?v=xxx or a youtu.be short link. A bare videoId string is invalid. The video must be public and embeddable, not private, region-locked, or login-gated.
  - Bilibili: the URL must include a BV id, such as https://www.bilibili.com/video/BVxxxxx.
  - Direct video links: must be directly accessible .mp4/.webm or similar URLs; confirm the link works and allows cross-origin access.
  - Never pass guessed URLs, inaccessible private videos, or platform share pages that are not embeddable playback links.
  - Recommended: use search first to find and confirm the video, then call media_mode. Prefer official channels and high-view public videos.
Pressing V only pauses and collapses the panel while preserving content; close/hide actions actually destroy the video.
Music mode rules:
  - src should be a local absolute file path with file:// prefix, or a direct HTTP audio URL. Confirm the file exists before playing.
  - lrc is optional LRC-format lyric text, such as [mm:ss.xx]lyric line.
  - When playing music, no chat reply is needed; call the tool directly.
  - Press M to collapse or expand the panel.`,
      parameters: {
        type: 'object',
        properties: {
          mode: { type: 'string', enum: ['video', 'camera', 'image', 'music'], description: 'video=right-side video mode; camera=right-side camera video; image=left-side image mode; music=right-side record-player mode.' },
          action: { type: 'string', enum: ['show', 'hide', 'close', 'play', 'pause', 'seek', 'set_volume', 'update'], description: 'show loads media; hide/close closes and destroys it; play/pause controls playback; seek jumps; set_volume adjusts volume.' },
          url: { type: 'string', description: 'Media URL for video/image. Must be a complete accessible URL following the tool rules.' },
          src: { type: 'string', description: 'Audio file path for music mode. Use file:///absolute/path for local files or an HTTP direct audio link.' },
          title: { type: 'string', description: 'Optional media title.' },
          artist: { type: 'string', description: 'Optional artist name for music mode.' },
          lrc: { type: 'string', description: 'Optional LRC-format lyrics for music mode, e.g. [mm:ss.xx]lyric line.' },
          cover: { type: 'string', description: 'Optional cover image path or URL for music mode.' },
          alt: { type: 'string', description: 'Optional image alt description.' },
          autoplay: { type: 'boolean', description: 'Autoplay, default true.' },
          muted: { type: 'boolean', description: 'Mute direct-link video, default false.' },
          volume: { type: 'number', description: 'Volume 0-1.' },
          currentTime: { type: 'number', description: 'Seconds to seek to.' },
          camera: { type: 'boolean', description: 'Explicitly open camera when mode=video; default false.' },
        },
        required: ['mode']
      }
    }
  },

  hotspot_mode: {
    type: 'function',
    function: {
      name: 'hotspot_mode',
      description: 'Control the hotspot panel. Use only when the user explicitly asks, when a demo/roleplay needs it, or when the current task truly needs a visual hotspot scene. Do not proactively open it for ordinary Q&A. status checks current state.',
      parameters: {
        type: 'object',
        properties: {
          action: { type: 'string', enum: ['show', 'open', 'hide', 'close', 'toggle', 'status'], description: 'show/open opens the hotspot panel; hide/close closes it; toggle switches it; status only checks state.' },
          reason: { type: 'string', description: 'Optional short reason for opening or closing.' },
        },
        required: ['action']
      }
    }
  },

  open_doc_panel: {
    type: 'function',
    function: {
      name: 'open_doc_panel',
      description: 'Control the configuration documentation panel. Open it when the user needs voice, model, WeChat, or social-platform configuration help, or explicitly asks to open documentation. Close it when it is open but the conversation is unrelated to any configuration topic. Panel contents are injected as context for 30 minutes.',
      parameters: {
        type: 'object',
        properties: {
          action: {
            type: 'string',
            enum: ['open', 'close'],
            description: 'open opens the panel; close closes the panel.'
          },
          topic: {
            type: 'string',
            enum: ['voice_asr', 'voice_tts', 'voice_config', 'model_config', 'wechat_config', 'self_architecture'],
            description: 'Required when action=open. Choose one topic: voice_asr, voice_tts, voice_config, model_config, wechat_config, or self_architecture. Do not invent other values. Optional when action=close.'
          },
          reason: { type: 'string', description: 'Optional short reason.' },
        },
        required: ['action']
      }
    }
  },

  person_card_mode: {
    type: 'function',
    function: {
      name: 'person_card_mode',
      description: 'Control the person-card panel. Use only when the user says they do not know someone, asks who someone is or why they are popular, or when the current conversation truly needs a public-figure explanation. Do not proactively open it for ordinary Q&A. Basic profile data can update the card.',
      parameters: {
        type: 'object',
        properties: {
          action: { type: 'string', enum: ['show', 'open', 'hide', 'close', 'update', 'toggle', 'status'], description: 'show/open/update opens or updates the person card; hide/close closes it; toggle switches it; status only checks state.' },
          name: { type: 'string', description: 'Person name, e.g. Jay Chou.' },
          title: { type: 'string', description: 'Identity or title, e.g. singer / musician.' },
          summary: { type: 'string', description: 'One or two sentence summary. Avoid inventing uncertain information.' },
          knownFor: { type: 'array', items: { type: 'string' }, description: 'Representative works, events, or recognition points the user most needs.' },
          tags: { type: 'array', items: { type: 'string' }, description: 'Short tags, e.g. actor or Mandopop.' },
          aliases: { type: 'array', items: { type: 'string' }, description: 'Aliases, English names, or common nicknames.' },
          image: { type: 'string', description: 'Optional large image URL, preferred for the card hero image.' },
          avatar: { type: 'string', description: 'Optional avatar or person image URL.' },
          reason: { type: 'string', description: 'Optional short reason for opening or closing.' },
        },
        required: ['action']
      }
    }
  },

  music: {
    type: 'function',
    function: {
      name: 'music',
      description: `Manage and play the local music library. Music files are stored under the music directory.
Supported actions:
  - list: list all tracks in the library, including id, title, artist, and file_path.
  - search: search by song title or artist.
  - download: use yt-dlp to download a YouTube/BiliBili URL as mp3 and add it to the library. Lyrics are fetched automatically when possible.
  - add: add an existing local audio file, such as mp3/flac/wav/aac, to the library.
  - scan: scan the music directory and add all audio files in batch.
  - get_lyrics: fetch LRC lyrics from lrclib.net and save them to the library. Requires title + artist.
  - delete: remove a track from the library by id without deleting the actual file.
To play music, use media_mode with mode=music and src=file_path to show the record player. No chat reply is needed before playback; execute directly.`,
      parameters: {
        type: 'object',
        properties: {
          action: { type: 'string', enum: ['list', 'search', 'download', 'add', 'scan', 'get_lyrics', 'delete'], description: 'Action type.' },
          query:  { type: 'string', description: 'Search query for search, usually song title or artist.' },
          url:    { type: 'string', description: 'YouTube/BiliBili URL for download.' },
          path:   { type: 'string', description: 'Absolute local audio file path for add.' },
          title:  { type: 'string', description: 'Track title, useful for add/download/get_lyrics.' },
          artist: { type: 'string', description: 'Artist name, useful for add/download/get_lyrics.' },
          album:  { type: 'string', description: 'Optional album name.' },
          id:     { type: 'number', description: 'Track id for get_lyrics/delete.' },
          limit:  { type: 'number', description: 'Maximum rows returned by list/search, default 50.' },
        },
        required: ['action']
      }
    }
  },

  manage_reminder: {
    type: 'function',
    function: {
      name: 'manage_reminder',
      description: 'Manage reminders: create one-off/daily/weekly/monthly reminders, list them, or cancel them. When due, the system sends you a system message so you can continue execution. One-off reminders with the same target_id and minute are merged to avoid duplicate triggers. After creating a reminder, call send_message to tell the user.',
      parameters: {
        type: 'object',
        properties: {
          action: {
            type: 'string',
            enum: ['create', 'list', 'cancel'],
            description: 'create creates a reminder; list lists pending reminders; cancel cancels by id.'
          },
          kind: {
            type: 'string',
            enum: ['once', 'daily', 'weekly', 'monthly'],
            description: 'For create only: once requires due_at; daily requires time; weekly requires time + weekday; monthly requires time + day_of_month. Defaults to once.'
          },
          task: {
            type: 'string',
            description: 'For create only: task to execute when the reminder fires.'
          },
          target_id: {
            type: 'string',
            description: 'For create only: final user ID served by this reminder, such as ID:000001. Defaults to the current conversation target.'
          },
          due_at: {
            type: 'string',
            description: 'For kind=once only: trigger time as an absolute ISO 8601 timestamp, e.g. 2026-04-21T06:00:00+08:00.'
          },
          time: {
            type: 'string',
            description: 'For daily/weekly/monthly only: trigger time in local timezone, HH:MM format, e.g. 09:00.'
          },
          weekday: {
            type: 'integer',
            description: 'For kind=weekly only: weekday, 0=Sunday, 1=Monday, ..., 6=Saturday.',
            minimum: 0,
            maximum: 6
          },
          day_of_month: {
            type: 'integer',
            description: 'For kind=monthly only: day of month, 1-31. If a month lacks that day, such as the 31st, the reminder jumps to the next month that has it.',
            minimum: 1,
            maximum: 31
          },
          id: {
            type: 'integer',
            description: 'For cancel only: reminder id to cancel, obtained from list.'
          }
        },
        required: ['action']
      }
    }
  },

  manage_prefetch_task: {
    type: 'function',
    function: {
      name: 'manage_prefetch_task',
      description: 'Manage prefetch tasks. The system automatically fetches these URLs before each startup and injects them into context, so fetch_url is not needed again. Suitable for recurring information such as weather, news, and prices.',
      parameters: {
        type: 'object',
        properties: {
          action: {
            type: 'string',
            enum: ['add', 'remove', 'list'],
            description: 'add adds or updates a task; remove deletes a task; list shows all tasks.',
          },
          source: {
            type: 'string',
            description: 'Unique task identifier, recommended format like "weather:Beijing" or "news:36kr". Required for add/remove.',
          },
          label: {
            type: 'string',
            description: 'Display label, e.g. "Beijing weather". Required for add.',
          },
          url: {
            type: 'string',
            description: 'URL to prefetch. Required for add.',
          },
          ttl_minutes: {
            type: 'number',
            description: 'Cache TTL in minutes, default 60. Suggested: weather 60, news 30, calendar 720.',
          },
          tags: {
            type: 'array',
            items: { type: 'string' },
            description: 'Tags such as ["weather", "Beijing"] for easier retrieval.',
          },
        },
        required: ['action'],
      },
    },
  },

  generate_music: {
    type: 'function',
    function: {
      name: 'generate_music',
      description: 'Generate music from a description and optional lyrics, then save it as an audio file. You can generate lyrics first with generate_lyrics, then pass them here to create a full song.',
      parameters: {
        type: 'object',
        properties: {
          prompt: { type: 'string', description: 'Music style and emotional description, such as melancholic piano or upbeat pop.' },
          lyrics: { type: 'string', description: 'Optional lyrics. Omit to generate instrumental music, usually with instrumental=true.' },
          instrumental: { type: 'boolean', description: 'Generate instrumental music without vocals, default false.' },
        },
        required: ['prompt']
      }
    }
  },

  generate_image: {
    type: 'function',
    function: {
      name: 'generate_image',
      description: 'Generate images from a text description. Daily image generation limit is 50.',
      parameters: {
        type: 'object',
        properties: {
          prompt: { type: 'string', description: 'Image description. More detail is better.' },
          aspect_ratio: { type: 'string', description: 'Aspect ratio, optional values: 1:1 default, 16:9, 4:3, 3:4, 9:16.' },
          n: { type: 'number', description: 'Number of images to generate, 1-4, default 1.' },
        },
        required: ['prompt']
      }
    }
  },

  search_memory: {
    type: 'function',
    function: {
      name: 'search_memory',
      description: 'Search the memory database in batch by multiple keywords using FTS5 full-text search. Each keyword is searched independently, then results are merged and deduplicated. Each result includes matched_by. The recognizer must call this before writing new memories to deduplicate; existing mem_id means update, no match means insert.',
      parameters: {
        type: 'object',
        properties: {
          keywords: {
            type: 'array',
            items: { type: 'string' },
            description: 'Keyword list, 1-8 items. Include Chinese/English synonyms where useful to improve recall.'
          },
          limit_per_keyword: {
            type: 'number',
            description: 'Maximum hits per keyword, default 5.'
          },
          type_filter: {
            type: 'string',
            enum: ['fact', 'person', 'object', 'knowledge', 'article'],
            description: 'Optional memory type filter.'
          }
        },
        required: ['keywords']
      }
    }
  },

  upsert_memory: {
    type: 'function',
    function: {
      name: 'upsert_memory',
      description: 'Batch insert or update memory nodes. Deduplicates by mem_id: existing mem_id means PATCH while omitted fields are preserved; new mem_id means INSERT. Use search_memory first to decide mem_id. Naming rules: person_{ID}, object_{slug}, article_{url_hash8}, concept_{snake}, fact_{snake}.',
      parameters: {
        type: 'object',
        properties: {
          memories: {
            type: 'array',
            items: {
              type: 'object',
              properties: {
                mem_id:        { type: 'string', description: 'Stable ID following the naming rules.' },
                type:          { type: 'string', enum: ['fact', 'person', 'object', 'knowledge', 'article'], description: 'Memory type. Required for new memories.' },
                title:         { type: 'string', description: 'Title. For articles, use the article title. Required for new memories.' },
                content:       { type: 'string', description: 'Summary, <= 200 Chinese characters. Required for new memories.' },
                detail:        { type: 'string', description: 'Optional detailed explanation.' },
                entities:      { type: 'array', items: { type: 'string' }, description: 'Entity IDs this memory is about. For memories about the user, include their sender ID (e.g. "ID:000001"). For memories about other people, include their person ID. This enables entity-based memory retrieval.' },
                tags:          { type: 'array', items: { type: 'string' }, description: 'Optional tag array.' },
                parent_mem_id: { type: 'string', description: 'Optional parent node mem_id.' },
                links:         {
                  type: 'array',
                  items: {
                    type: 'object',
                    properties: {
                      target_mem_id: { type: 'string' },
                      relation:      { type: 'string', description: 'Relation such as related_to, cites, or contradicts.' }
                    }
                  },
                  description: 'Optional links to other memory nodes.'
                },
                salience: {
                  type: 'integer',
                  minimum: 1,
                  maximum: 5,
                  description: 'Importance score 1-5. 1=trivial detail, 2=ordinary fact, 3=default stable info, 4=meaningful pattern or recurring preference, 5=identity-level / load-bearing belief. Defaults to 3 if omitted.'
                },
                body_path:     { type: 'string', description: 'For article type: full-text file path from fetch_url/browser_read body_path.' }
              },
              required: ['mem_id']
            },
            description: 'Memory array for batch insert/update, supports 1-N items.'
          }
        },
        required: ['memories']
      }
    }
  },

  skip_recognition: {
    type: 'function',
    function: {
      name: 'skip_recognition',
      description: 'Recognizer-only tool. Call when this turn contains nothing worth long-term storage, explicitly meaning "reviewed, no write needed." This is a valid stop signal; do not force weak content into memory.',
      parameters: {
        type: 'object',
        properties: {
          reason: { type: 'string', description: 'Optional short reason.' }
        }
      }
    }
  },

  merge_memories: {
    type: 'function',
    function: {
      name: 'merge_memories',
      description: 'Consolidator-only. Merge multiple semantically-duplicate memories into one. The keep memory is updated with merged_content; drop memories become hidden (visibility=0, merged_into=keep_mem_id) — NOT deleted. Rows + FTS + embedding fully preserved and still reachable by recovery flows; routine search/get* just stops surfacing them. Entities from hidden memories are union-merged into keep. Salience defaults to max(involved) unless merged_salience is set. Use this when 2+ memories say the same thing in different words, or when an old memory is superseded by a newer fact that subsumes it.',
      parameters: {
        type: 'object',
        properties: {
          keep_mem_id:     { type: 'string', description: 'mem_id of the memory to keep (will be updated).' },
          drop_mem_ids:    { type: 'array', items: { type: 'string' }, description: 'mem_ids of memories to hide (soft-delete) after merging their semantic content into keep.' },
          merged_content:  { type: 'string', description: 'New content for the keep memory, <=200 Chinese characters, covering everything the dropped memories said that is still true.' },
          merged_salience: { type: 'integer', minimum: 1, maximum: 5, description: 'Optional. Overrides default max(involved) salience.' },
          reason:          { type: 'string', description: 'Short reason for the merge, for logs.' },
        },
        required: ['keep_mem_id', 'drop_mem_ids', 'merged_content']
      }
    }
  },

  downgrade_memory: {
    type: 'function',
    function: {
      name: 'downgrade_memory',
      description: 'Consolidator-only. Lower the salience of a memory that has become stale or less central but is not outright contradicted (otherwise merge it into the contradicting memory). Reserve aggressive downgrades for memories that no longer seem load-bearing.',
      parameters: {
        type: 'object',
        properties: {
          mem_id:       { type: 'string' },
          new_salience: { type: 'integer', minimum: 1, maximum: 5 },
          reason:       { type: 'string' },
        },
        required: ['mem_id', 'new_salience']
      }
    }
  },

  skip_consolidation: {
    type: 'function',
    function: {
      name: 'skip_consolidation',
      description: 'Consolidator-only. Call when the inspected memory batch contains no duplicates and no stale entries. Valid stop signal.',
      parameters: {
        type: 'object',
        properties: {
          reason: { type: 'string' }
        }
      }
    }
  },

  ui_show: {
    type: 'function',
    function: {
      name: 'ui_show',
      description: 'Push a registered visual card to the user interface. Always specify component + props matching the registered component\'s propsSchema. Use only when UI expression is clearer than plain text.',
      parameters: {
        type: 'object',
        properties: {
          component: { type: 'string', description: 'Registered component type name, e.g. WeatherCard. Required.' },
          props:     { type: 'object', description: 'Component props following the component\'s propsSchema.' },
          hint: {
            type: 'object',
            description: 'Optional display hint. All fields have reasonable defaults.',
            properties: {
              placement: { type: 'string', enum: ['notification', 'center', 'floating', 'stage'], description: 'notification=top-right stacked slide-in (default); center=centered with overlay; floating=free draggable; stage=fullscreen.' },
              size:      { description: 'Size: sm | md | lg | xl, or pixel object { w, h }.', oneOf: [{ type: 'string', enum: ['sm', 'md', 'lg', 'xl'] }, { type: 'object', properties: { w: { type: ['number', 'string'] }, h: { type: ['number', 'string'] } } }] },
              draggable: { type: 'boolean', description: 'Whether draggable. floating defaults true.' },
              modal:     { type: 'boolean', description: 'Show translucent overlay. center defaults true.' },
              enter:     { type: 'string', description: 'Enter animation, inferred from placement by default.' },
              exit:      { type: 'string', description: 'Exit animation, inferred from placement by default.' }
            }
          }
        },
        required: ['component']
      }
    }
  },

  ui_hide: {
    type: 'function',
    function: {
      name: 'ui_hide',
      description: 'Close a displayed card with its exit animation. Usually let the user close cards; proactively call only when the card information is stale.',
      parameters: {
        type: 'object',
        properties: {
          id: { type: 'string', description: 'Card instance id returned by ui_show.' }
        },
        required: ['id']
      }
    }
  },

  ui_update: {
    type: 'function',
    function: {
      name: 'ui_update',
      description: 'Update a displayed card without replaying the enter animation. Common use: change props when the user asks about another city weather instead of opening a new card.',
      parameters: {
        type: 'object',
        properties: {
          id:    { type: 'string', description: 'Card instance id returned by ui_show.' },
          props: { type: 'object', description: 'New props, shallow-merged with existing props.' }
        },
        required: ['id', 'props']
      }
    }
  },

  manage_app: {
    type: 'function',
    function: {
      name: 'manage_app',
      description: 'Manage generated interactive apps such as games/tools: save as permanent app, reopen, list, or delete. inline-script component code is saved as a draft when generated; use save to promote it to a formal app that can be reopened later.',
      parameters: {
        type: 'object',
        properties: {
          action: {
            type: 'string',
            enum: ['save', 'open', 'list', 'delete'],
            description: 'save promotes an inline-script draft to a permanent app; open remounts a saved app with automatic state restore; list lists saved apps; delete removes an app.'
          },
          name: {
            type: 'string',
            description: 'App name in lowercase snake_case, used as the storage directory, e.g. chess or todo_app. Required for save/open/delete.'
          },
          label: {
            type: 'string',
            description: 'Optional display label, e.g. Chinese chess. Provide when saving.'
          },
          draft_id: {
            type: 'string',
            description: 'Required for save: component instance id returned by ui_show(mode="inline-script"), e.g. scratch-xxx.'
          },
          state: {
            type: 'object',
            description: 'Optional state included when saving or opening. For save, pass current game/app state; for open, this overrides persisted state.'
          },
          hint: {
            type: 'object',
            description: 'Optional UI display hint, such as placement / size / draggable. Written to metadata during save and reused during open.'
          }
        },
        required: ['action']
      }
    }
  },

  ui_patch: {
    type: 'function',
    function: {
      name: 'ui_patch',
      description: 'Send operation commands or state updates to a mounted app component. The component listens with this._app.onPatch(). Use for game turns, state machines, canvas updates, and other cases where the agent proactively pushes changes.',
      parameters: {
        type: 'object',
        properties: {
          id:   { type: 'string', description: 'Component instance id returned by ui_show.' },
          op:   { type: 'string', description: 'Operation name defined by the component, such as applyMove, setState, or nextRound.' },
          data: { type: 'object', description: 'Operation data interpreted by the component.' },
        },
        required: ['id', 'op']
      }
    }
  },

  ui_register: {
    type: 'function',
    function: {
      name: 'ui_register',
      description: 'Promote a verified inline component to a permanent component: write a .js file, update registry, write ui-components.json, and seed one skill.ui memory. Usually call after the inline component succeeds at least twice, the user does not immediately close it, and dwell signals are good. After registration, future similar needs can use ui_show directly.',
      parameters: {
        type: 'object',
        properties: {
          component_name: { type: 'string', description: 'Unused PascalCase component name, e.g. TodoCard or VideoPlayer.' },
          code:           { type: 'string', description: 'Complete Web Component class code. Must include static tagName / static propsSchema / static enter / static exit and end with customElements.define.' },
          props_schema:   { type: 'object', description: 'Object matching propsSchema in code, used as backend validation mirror, e.g. { field: { type, required } }.' },
          use_case:       { type: 'string', description: 'When to use this component. Written into skill.ui memory as matching conditions.' },
          example_call:   { type: 'string', description: 'Example ui_show call.' }
        },
        required: ['component_name', 'code', 'props_schema', 'use_case', 'example_call']
      }
    }
  },

  set_task: {
    type: 'function',
    function: {
      name: 'set_task',
      description: 'Start a multi-step task. Provide the overall goal and ordered steps. The system persistently tracks each step and restores after restart. Calling this accelerates TICK rhythm to keep progressing. Only one active task can exist at a time.',
      parameters: {
        type: 'object',
        properties: {
          description: { type: 'string', description: 'Overall task goal: what should be completed in the end.' },
          steps: {
            type: 'array',
            items: { type: 'string' },
            description: 'Ordered concrete steps, each describing what to do.'
          }
        },
        required: ['description', 'steps']
      }
    }
  },

  complete_task: {
    type: 'function',
    function: {
      name: 'complete_task',
      description: 'Mark the current task fully complete. Stops accelerated TICK, writes a completion record, and clears task state. Call after all steps are complete.',
      parameters: {
        type: 'object',
        properties: {
          summary: { type: 'string', description: 'Optional short completion summary.' }
        },
        required: []
      }
    }
  },

  update_task_step: {
    type: 'function',
    function: {
      name: 'update_task_step',
      description: 'Update completion status for one step of the current task. Call immediately when a step is done, failed, or skipped so progress is tracked in real time.',
      parameters: {
        type: 'object',
        properties: {
          step_index: { type: 'number', description: 'Step index starting from 0.' },
          status: {
            type: 'string',
            enum: ['done', 'failed', 'skipped'],
            description: 'Step status: done, failed, or skipped.'
          },
          note: { type: 'string', description: 'Optional note about the step result.' }
        },
        required: ['step_index', 'status']
      }
    }
  },

  recall_memory: {
    type: 'function',
    function: {
      name: 'recall_memory',
      description: 'Deeply retrieve memories related to a topic, return results immediately, and keep focusing on this topic in the next turn. Deeper than search_memory because it affects the next memory injection direction. Use when you need to recall an experience or concept in depth.',
      parameters: {
        type: 'object',
        properties: {
          query: { type: 'string', description: 'Content or topic to recall.' }
        },
        required: ['query']
      }
    }
  },

  complete_startup_self_check: {
    type: 'function',
    function: {
      name: 'complete_startup_self_check',
      description: 'Mark the one-time L2 startup self-check as complete after environment exploration and capability checks have finished. This persists a config flag and memory so the check will not repeat on future startups.',
      parameters: {
        type: 'object',
        properties: {
          summary: {
            type: 'string',
            description: 'Brief human-readable summary of the startup self-check result.'
          },
          results: {
            type: 'object',
            description: 'Per-capability result map. Suggested keys: filesystem, web_search, hotspot_panel, music_player, focus_banner, ui_card. Each value should include status and detail.',
            additionalProperties: {
              type: 'object',
              properties: {
                status: {
                  type: 'string',
                  description: 'ok, degraded, error, skipped_no_tracks, skipped_no_ui_client, or another concise status.'
                },
                detail: {
                  type: 'string',
                  description: 'Short detail from the check.'
                }
              }
            }
          }
        },
        required: ['summary', 'results']
      }
    }
  },

  focus_banner: {
    type: 'function',
    function: {
      name: 'focus_banner',
      description: 'Show a translucent desktop focus banner sticker reminding the user what to focus on. Call when the user says they want to focus on something, enter focus mode, or asks for help focusing on X. The banner can expand to show a task list with checkboxes.',
      parameters: {
        type: 'object',
        properties: {
          action: {
            type: 'string',
            enum: ['show', 'update', 'hide'],
            description: 'show displays the banner; update changes content when it already exists; hide closes it.'
          },
          task: {
            type: 'string',
            description: 'Main task title, one short sentence.'
          },
          current_step: {
            type: 'string',
            description: 'Optional current step, shown under the main task when collapsed.'
          },
          tasks: {
            type: 'array',
            items: {
              type: 'object',
              properties: {
                text: { type: 'string', description: 'Subtask text.' },
                done: { type: 'boolean', description: 'Whether completed, default false.' }
              },
              required: ['text']
            },
            description: 'Optional subtask list shown when the banner is expanded.'
          }
        },
        required: ['action']
      }
    }
  },

  set_agent_name: {
    type: 'function',
    function: {
      name: 'set_agent_name',
      description: 'Update your display name and self-reference name. Call when the user explicitly asks you to rename yourself, change what they call you, or gives you a new name. Do NOT call for questions like "what is your name?".',
      parameters: {
        type: 'object',
        properties: {
          name: {
            type: 'string',
            description: 'The new name, 1–32 characters, Chinese/English/digits/spaces/underscores/hyphens allowed.'
          }
        },
        required: ['name']
      }
    }
  },

  set_location: {
    type: 'function',
    function: {
      name: 'set_location',
      description: 'Record the user current city or region for weather and other location-related features. Call when the user tells you their location.',
      parameters: {
        type: 'object',
        properties: {
          city: {
            type: 'string',
            description: 'City name, such as Beijing, Shanghai, or London.'
          }
        },
        required: ['city']
      }
    }
  },

  delegate_to_agent: {
    type: 'function',
    function: {
      name: 'delegate_to_agent',
      description: '将子任务委托给另一个本地 AI Agent 执行。仅在已获得用户授权（agent_delegation_allowed）时可用。适合代码开发、自动化任务等超出自身能力范围的场景。调用前必须通过 send_message 告知用户你打算让谁做什么。',
      parameters: {
        type: 'object',
        properties: {
          agent_id: {
            type: 'string',
            description: 'Agent ID，如 claude-code、codex、hermes、openclaw。',
            enum: ['claude-code', 'codex', 'hermes', 'openclaw']
          },
          prompt: {
            type: 'string',
            description: '发送给目标 Agent 的完整任务指令，应包含足够的上下文。'
          },
          context: {
            type: 'string',
            description: '可选：附加背景信息，会拼接到 prompt 前面。'
          },
          timeout: {
            type: 'number',
            description: '等待 Agent 响应的超时秒数，默认 60，最大 300。'
          }
        },
        required: ['agent_id', 'prompt']
      }
    }
  },

  grant_agent_delegation: {
    type: 'function',
    function: {
      name: 'grant_agent_delegation',
      description: '记录用户对 Agent 委托权限的决定。当用户明确表示同意或拒绝让 Bailongma 指挥其他 AI 小伙伴工作时调用此工具落盘。只调用一次，之后不再重复询问。',
      parameters: {
        type: 'object',
        properties: {
          allowed: {
            type: 'boolean',
            description: 'true 表示用户同意授权，false 表示用户拒绝。'
          },
          note: {
            type: 'string',
            description: '可选：用户原话或简短备注。'
          }
        },
        required: ['allowed']
      }
    }
  },

  install_tool: {
    type: 'function',
    function: {
      name: 'install_tool',
      description: '安装一个新工具并立即注册，下一轮对话起即可调用。工具代码是 async 函数体，可用变量：args（参数对象）、helpers.fetch（HTTP 请求）、helpers.exec(cmd)（运行 shell 命令，返回 stdout 字符串）、helpers.log(msg)（调试日志）。代码最终需要 return 一个字符串作为工具结果。',
      parameters: {
        type: 'object',
        properties: {
          name: {
            type: 'string',
            description: '工具名称，只能含小写字母、数字、下划线，以字母开头，长度 2-50。如 "weather_query"。'
          },
          description: {
            type: 'string',
            description: '工具描述：说明这个工具做什么、何时该调用它。'
          },
          parameters_schema: {
            type: 'object',
            description: 'JSON Schema 对象，描述工具的输入参数。格式：{ "type": "object", "properties": { ... }, "required": [...] }'
          },
          code: {
            type: 'string',
            description: 'async 函数体代码（不含 async function 声明头）。示例：const { city } = args; const r = await helpers.fetch(`https://wttr.in/${city}?format=3`); return await r.text();'
          }
        },
        required: ['name', 'description', 'parameters_schema', 'code']
      }
    }
  },

  uninstall_tool: {
    type: 'function',
    function: {
      name: 'uninstall_tool',
      description: '卸载一个已安装的工具，立即生效，同时删除其持久化文件。',
      parameters: {
        type: 'object',
        properties: {
          name: {
            type: 'string',
            description: '要卸载的工具名称。'
          }
        },
        required: ['name']
      }
    }
  },

  list_tools: {
    type: 'function',
    function: {
      name: 'list_tools',
      description: '列出所有可用工具（内置 + 已安装），含名称、描述、来源。适合安装前确认是否已存在、或排查工具问题。',
      parameters: {
        type: 'object',
        properties: {},
        required: []
      }
    }
  },

  connect_wechat: {
    type: 'function',
    function: {
      name: 'connect_wechat',
      description: 'Show the WeChat ClawBot connection popup so the user can scan a QR code to bind their personal WeChat account. Call ONLY when the user explicitly asks to connect, bind, or set up WeChat. Do not call speculatively.',
      parameters: {
        type: 'object',
        properties: {},
        required: []
      }
    }
  },

  set_security: {
    type: 'function',
    function: {
      name: 'set_security',
      description: 'Request a sandbox security setting change. Shows a confirmation card to the user — the change only takes effect after explicit user approval. Call ONLY when the user explicitly asks to disable or enable the file sandbox or exec sandbox. Do not call speculatively.',
      parameters: {
        type: 'object',
        properties: {
          file_sandbox: {
            type: 'boolean',
            description: 'New value for file sandbox. false = disable (allow access outside sandbox dir). Omit if not changing.'
          },
          exec_sandbox: {
            type: 'boolean',
            description: 'New value for exec sandbox. false = disable (allow absolute paths and home dir). Omit if not changing.'
          },
          reason: {
            type: 'string',
            description: 'Brief explanation shown to the user explaining why this change is needed.'
          }
        },
        required: ['reason']
      }
    }
  },
}

// 根据名称列表获取 schema 数组（含已安装工具）
export function getToolSchemas(toolNames) {
  return toolNames
    // `express` remains as a backward-compatible executor alias,
    // but we don't expose it to the model. The model should use
    // `send_message` for outbound text messages.
    .filter(name => name !== 'express')
    .map(name => TOOL_SCHEMAS[name] ?? getInstalledToolSchema(name))
    .filter(Boolean)
    // 剥离识别器专用元数据，避免发给 LLM API
    .map(({ recognizer_highlights, ...rest }) => rest)
}
