@sjtdev/koishi-plugin-dota2tracker 2.5.8 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -5,10 +5,10 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __getProtoOf = Object.getPrototypeOf;
6
6
  var __hasOwnProp = Object.prototype.hasOwnProperty;
7
7
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
8
- var __glob = (map) => (path6) => {
9
- var fn = map[path6];
8
+ var __glob = (map) => (path7) => {
9
+ var fn = map[path7];
10
10
  if (fn) return fn();
11
- throw new Error("Module not found in bundle: " + path6);
11
+ throw new Error("Module not found in bundle: " + path7);
12
12
  };
13
13
  var __commonJS = (cb, mod) => function __require() {
14
14
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
@@ -768,21 +768,21 @@ var require_zh_CN_constants = __commonJS({
768
768
  // src/locales/en-US.command.yml
769
769
  var require_en_US_command = __commonJS({
770
770
  "src/locales/en-US.command.yml"(exports2, module2) {
771
- module2.exports = { commands: { dota2tracker: { description: "A series of commands for Dota 2 Tracker. Use dota2tracker -h to see all available commands, or use dota2tracker.help to see a detailed description of all commands.", subscribe: { description: "Subscribes the current channel to Dota 2 match tracking.", usage: "After subscribing, players need to bind their Steam ID in this channel. The bot will then track new matches of bound players and post image-based reports upon completion of parsing by Stratz.", examples: "subscribe", messages: { subscribe_success: "Subscription successful.", subscribed: "This Channel has been subscribed, no need to subscribe again." } }, unsubscribe: { description: "Unsubscribes the current channel from match tracking.", usage: "Unsubscribes the current channel from match tracking.", examples: "unsubscribe", messages: { unsubscribe_success: "Unsubscription successful.", not_subscribed: "This Channel has not been subscribed yet, so there is no need to unsubscribe." } }, bind: { description: "Binds your SteamID to your account in the current channel.", usage: 'Bind your SteamID to your account. If the channel is subscribed, your new match data will be posted automatically. Nicknames containing spaces must be enclosed in double quotes ("").', examples: 'bind 123456789\nbind 123456789 John\nbind 123456789 "John Doe"', messages: { steam_id_invalid: "Invalid SteamID.", bind_success: "Binding successful,\nID: {userId}\nNickname: {nickName}\nSteamID: {steamId}", bind_failed: "Binding failed, {0}", reason_without_match: "Invalid SteamID or no matches found.", reason_fetch_failed: "Poor network conditions or other reasons prevented the verification of the SteamID. Please try again later.", already_binded: "You are already bound, no need to bind again.\nHere is your personal information:\nID: {userId}\nNickname: {nickName}\nSteamID: {steamId}", nick_name_too_long: "Nickname is too long, please limit it to 20 characters or less. (It can also be left blank)", is_anonymous: 'Please note: Your Steam player data is not public, and you will not be able to use the main functions of the BOT, such as "battle report tracking," "query-recent-match commands," etc.\nIf you need to make data public, please set it to public in the DOTA2 game settings.' } }, unbind: { description: "Unbinds your personal information in the current channel.", usage: "Unbind your personal information in the current channel.", examples: "unbind", messages: { unbind_success: "Unbinding successful.", not_binded: "Not bound, no need to unbind." } }, rename: { description: "Changes the nickname set during binding.", usage: 'Change the nickname set during binding. Nicknames containing spaces must be enclosed in double quotes ("").', examples: 'rename John\nrename "John Doe"', messages: { rename_success: "Rename successful, now you are called {nick_name}.", empty_input: "Please enter your nickname.", not_binded: "Please bind first, you can set a nickname during binding.", nick_name_too_long: "Nickname is too long, please limit it to 20 characters.", nick_name_same: "The input content is the same as the original nickname and does not need to be renamed." } }, "query-members": { description: "Queries the players bound in this channel and generates an info image.", usage: "Queries the players bound in this channel and generates an informational image.", examples: "query-members", messages: { title: "Guild DOTA 2 Roster (Total: {count})", table_headers: { nickname: "Nickname", winrate: "Win Rate (L10)", last_match: "Last Match" }, no_members: "No players bound in this group.", query_failed: "Failed to query group members." } }, "query-match": { description: "Query the match data of the specified match ID and generate a picture.", usage: "Query the match data of the specified match ID and generate a picture.", options: { parse: "Whether to wait for match data parsing" }, examples: "query-match 1234567890\nquery-match 1234567890 -p\nquery-match 1234567890 --parse", messages: { empty_input: "Please enter the match ID.", match_id_invalid: "Invalid match ID.", querying_match: "Searching for match details, please wait...", query_failed: "Failed to get match data.", waiting_for_parse: "Match data has not been parsed yet, a parse request has been sent to the server. The battle report will be sent once parsing is complete or times out." } }, "query-recent-match": { description: "Query the most recent match data and generate a picture.", options: { parse: "Whether to wait for match data parsing" }, usage: "Query the most recent match data of the specified player and generate a picture.\nThe parameter can be the player's SteamID or the nickname of a player bound in this group. If no parameter is provided, it will try to query the SteamID of the command caller.", examples: "query-recent-match\nquery-recent-match 123456789\nquery-recent-match John\nquery-recent-match 123456789 -p\nquery-recent-match John --parse", messages: { querying_match: "Searching for match details, please wait...", query_failed: "Failed to get the player's recent match.", is_anonymous: "Your player data is not public, and recent match data cannot be obtained.\nIf you need to make data public, please set it to public in the DOTA2 game settings." } }, "query-player": { description: "Query the player's personal information, optionally specify a hero.", options: { hero: "Query the player's usage of the specified hero (same as querying a hero, can use nickname or ID)" }, usage: "Query the personal information of the specified player and generate a picture, optionally specify a hero.\nThe parameter can be the player's SteamID or the nickname of a player bound in this group. If no parameter is provided, it will try to query the SteamID of the command caller.", examples: "query-player\nquery-player 123456789\nquery-player John\nquery-player John --hero Anti-Mage\nquery-player John -o Anti-Mage", messages: { querying_player: "Retrieving player data, please wait...", query_failed: "Failed to get player information." } }, "query-hero": { description: "Query hero skills/stats information.", options: { random: "Randomly select a hero." }, usage: "Query the hero's skill descriptions and various stats, generate a picture.\nThe parameter can be the hero's ID, name, or common nickname.", examples: "query-hero 15\nquery-hero Razor\nquery-hero -r", messages: { not_found: "Hero not found, please confirm and re-enter.", querying_hero: "Retrieving hero data, please wait...", query_failed: "Failed to get hero data.", empty_input: "Please enter a parameter." } }, "query-item": { description: "Query item information", usage: "Query item descriptions and attributes, then generate and publish an image report.\nParameters can be item name (supports fuzzy search), item alias, or item ID.\nYou can set the maximum number of items to send per query on the configuration page, as well as whether to send the item list when the limit is exceeded or parameters are not entered.", examples: "query-item Vanguard", messages: { query_list_failed: "Failed to retrieve item list data", query_item_failed: "Failed to retrieve data for item '{0}'", querying_item: "Querying item data, please wait...", cache_building: "Initializing or rebuilding item cache for the current version, please wait...", empty_input: "No keywords provided. \n{#if show}Displaying full item list per current configuration\n{:else}No content available\n{/if}", not_found: "No items found matching the keywords, please verify and retry", too_many_items: "Found {count} items, exceeding maximum display limit ({max} items)\n{#if show}(Displaying item list){/if}", finded_items: "Matching items: \n{#each items as item}\n{item.name_loc}{#if item !== items[items.length - 1]}, {/if}\n{/each}" } }, "hero-of-the-day": { description: "Get hero recommendations for the day.", usage: "Fetches recent and lifetime match history to recommend heroes based on parameters like wins, performance score, and hot streaks.\nThe parameter can be a player's SteamID or the nickname of a player bound in this group. If no parameter is provided, it will try to look up the command caller's SteamID.", options: { days: "-d <number> The range of recent days to consider, default is 30." }, examples: 'hero-of-the-day\nhero-of-the-day -d 60\nhero-of-the-day 1234567890\nhero-of-the-day "John Doe"', messages: { title_recommendation: "Today's Recommendation:", recommendation_intro: "The recommended heroes for you today are:", recommendation_heroes: "{#each heroes as hero}{hero}{#if hero !== heroes[heroes.length - 1]}, {/if}{/each}", recommendation_type_lifetime_only: "Your recent match history is empty. This recommendation is based on your lifetime statistics.", recommendation_type_no_record: "Recommendations cannot be generated due to a lack of recent and lifetime match data.", recommendation_type_anonymous: "Recommendations cannot be generated because your profile data is private.", details: { pool_description: "The recommendation is generated by scoring your recent and lifetime hero performance, sorting by total score, and then randomly selecting from the top 10 heroes weighted by their scores.", table_intro: "Below is the detailed score breakdown for the top 10 heroes.", table_headers: { hero: "Hero", recent_wins: "Recent Wins Score", lifetime_wins: "Lifetime Wins<br>(Logarithmic)", imp_bonus: "IMP Bonus", is_hot_streak: "Hot Streak", total_score: "Total Score" }, scoring_formula: "Current Scoring Formula: [Recent Wins x 1] + [log(Lifetime Wins + 1) x 5] + [Recent IMP x 0.1]", hot_streak_desc: "If a hero was played in the last 3 days, it's considered a 'Hot Streak' hero, receiving a 20% bonus to its total score." }, title_meta: "Meta Trends:", meta_intro: "Top 3 advantage heroes for each position with a <b>pick rate ≥2%</b> within ±1 of your rank bracket ({tiers}) over the last week, sorted by win rate:", meta_table_header: "Hero (Pick% Win%)", meta_position: "Pos {pos}:", querying: "Retrieving player stats and hero win rates, please wait...", query_failed: "Command failed." } }, common: { messages: { user_not_binded_in_channel: "By default, it tries to find your information from the bound SteamID players, but it seems you are not bound.\nPlease bind your SteamID in this group. (You can enter [bind -h] for help)\nOr follow the command with the SteamID or nickname of the player you want to query.", user_not_in_group: "Command failed.\nCurrently not in a group chat, you must provide the specified player's SteamID.", invalid_input_include_steam_id: "Invalid SteamID and the player was not found in this group by the given input." } }, help: { description: "Get detailed information for all commands and the link to the online documentation.", usage: "Get detailed information for all commands and the link to the online documentation.", examples: "help", messages: { header: "Below is the full list of plugin commands.\n※Note: <arg> is a required argument, and [arg] is an optional argument. Please see the 'Examples' column for specific usage.", footer: "For more information on plugin configuration, template showcases, and other details, please visit the online documentation:\nhttps://sjtdev.github.io/koishi-plugin-dota2tracker/en-US/", table_headers: { command: "Command", alias: "Alias", arguments: "Arguments", description: "Description", options: "Options", examples: "Examples" } } } } } };
771
+ module2.exports = { commands: { dota2tracker: { description: "A series of commands for Dota 2 Tracker. Use dota2tracker -h to see all available commands, or use dota2tracker.help to see a detailed description of all commands.", subscribe: { description: "Subscribes the current channel to Dota 2 match tracking.", usage: "After subscribing, players need to bind their Steam ID in this channel. The bot will then track new matches of bound players and post image-based reports upon completion of parsing by Stratz.", examples: "subscribe", messages: { subscribe_success: "Subscription successful.", subscribed: "This Channel has been subscribed, no need to subscribe again." } }, unsubscribe: { description: "Unsubscribes the current channel from match tracking.", usage: "Unsubscribes the current channel from match tracking.", examples: "unsubscribe", messages: { unsubscribe_success: "Unsubscription successful.", not_subscribed: "This Channel has not been subscribed yet, so there is no need to unsubscribe." } }, bind: { description: "Binds your SteamID to your account in the current channel.", usage: 'Bind your SteamID to your account. If the channel is subscribed, your new match data will be posted automatically. Nicknames containing spaces must be enclosed in double quotes ("").', examples: 'bind 123456789\nbind 123456789 John\nbind 123456789 "John Doe"', messages: { steam_id_invalid: "Invalid SteamID.", bind_success: "Binding successful,\nID: {userId}\nNickname: {nickName}\nSteamID: {steamId}", bind_failed: "Binding failed, {0}", reason_without_match: "Invalid SteamID or no matches found.", reason_fetch_failed: "Poor network conditions or other reasons prevented the verification of the SteamID. Please try again later.", already_binded: "You are already bound, no need to bind again.\nHere is your personal information:\nID: {userId}\nNickname: {nickName}\nSteamID: {steamId}", nick_name_too_long: "Nickname is too long, please limit it to 20 characters or less. (It can also be left blank)", is_anonymous: 'Please note: Your Steam player data is not public, and you will not be able to use the main functions of the BOT, such as "battle report tracking," "query-recent-match commands," etc.\nIf you need to make data public, please set it to public in the DOTA2 game settings.' } }, unbind: { description: "Unbinds your personal information in the current channel.", usage: "Unbind your personal information in the current channel.", examples: "unbind", messages: { unbind_success: "Unbinding successful.", not_binded: "Not bound, no need to unbind." } }, rename: { description: "Changes the nickname set during binding.", usage: 'Change the nickname set during binding. Nicknames containing spaces must be enclosed in double quotes ("").', examples: 'rename John\nrename "John Doe"', messages: { rename_success: "Rename successful, now you are called {nick_name}.", empty_input: "Please enter your nickname.", not_binded: "Please bind first, you can set a nickname during binding.", nick_name_too_long: "Nickname is too long, please limit it to 20 characters.", nick_name_same: "The input content is the same as the original nickname and does not need to be renamed." } }, "query-members": { description: "Queries the players bound in this channel and generates an info image.", usage: "Queries the players bound in this channel and generates an informational image.", examples: "query-members", messages: { title: "Guild DOTA 2 Roster (Total: {count})", table_headers: { nickname: "Nickname", winrate: "Win Rate (L10)", last_match: "Last Match" }, no_members: "No players bound in this group.", query_failed: "Failed to query group members." } }, "query-match": { description: "Query the match data of the specified match ID and generate a picture.", usage: "Query the match data of the specified match ID and generate a picture.", options: { parse: "Whether to wait for match data parsing" }, examples: "query-match 1234567890\nquery-match 1234567890 -p\nquery-match 1234567890 --parse", messages: { empty_input: "Please enter the match ID.", match_id_invalid: "Invalid match ID.", querying_match: "Searching for match details, please wait...", query_failed: "Failed to get match data.", waiting_for_parse: "Match data has not been parsed yet, a parse request has been sent to the server. The battle report will be sent once parsing is complete or times out." } }, "query-recent-match": { description: "Query the most recent match data and generate a picture.", options: { parse: "Whether to wait for match data parsing" }, usage: "Query the most recent match data of the specified player and generate a picture.\nThe parameter can be the player's SteamID or the nickname of a player bound in this group. If no parameter is provided, it will try to query the SteamID of the command caller.", examples: "query-recent-match\nquery-recent-match 123456789\nquery-recent-match John\nquery-recent-match 123456789 -p\nquery-recent-match John --parse", messages: { querying_match: "Searching for match details, please wait...", query_failed: "Failed to get the player's recent match.", is_anonymous: "Your player data is not public, and recent match data cannot be obtained.\nIf you need to make data public, please set it to public in the DOTA2 game settings." } }, "query-player": { description: "Query the player's personal information, optionally specify a hero.", options: { hero: "Query the player's usage of the specified hero (same as querying a hero, can use nickname or ID)" }, usage: "Query the personal information of the specified player and generate a picture, optionally specify a hero.\nThe parameter can be the player's SteamID or the nickname of a player bound in this group. If no parameter is provided, it will try to query the SteamID of the command caller.", examples: "query-player\nquery-player 123456789\nquery-player John\nquery-player John --hero Anti-Mage\nquery-player John -o Anti-Mage", messages: { querying_player: "Retrieving player data, please wait...", query_failed: "Failed to get player information." } }, "query-hero": { description: "Query hero skills/stats information.", options: { random: "Randomly select a hero." }, usage: "Query the hero's skill descriptions and various stats, generate a picture.\nThe parameter can be the hero's ID, name, or common nickname.", examples: "query-hero 15\nquery-hero Razor\nquery-hero -r", messages: { not_found: "Hero not found, please confirm and re-enter.", querying_hero: "Retrieving hero data, please wait...", fetching_patch_notes: "Fetching hero patch notes and building cache, please wait...", query_failed: "Failed to get hero data.", empty_input: "Please enter a parameter." } }, "query-item": { description: "Query item information", usage: "Query item descriptions and attributes, then generate and publish an image report.\nParameters can be item name (supports fuzzy search), item alias, or item ID.\nYou can set the maximum number of items to send per query on the configuration page, as well as whether to send the item list when the limit is exceeded or parameters are not entered.", examples: "query-item Vanguard", messages: { query_list_failed: "Failed to retrieve item list data", query_item_failed: "Failed to retrieve data for item '{0}'", querying_item: "Querying item data, please wait...", cache_building: "Initializing or rebuilding item cache for the current version, please wait...", empty_input: "No keywords provided. \n{#if show}Displaying full item list per current configuration\n{:else}No content available\n{/if}", not_found: "No items found matching the keywords, please verify and retry", too_many_items: "Found {count} items, exceeding maximum display limit ({max} items)\n{#if show}(Displaying item list){/if}", finded_items: "Matching items: \n{#each items as item}\n{item.name_loc}{#if item !== items[items.length - 1]}, {/if}\n{/each}" } }, "hero-of-the-day": { description: "Get hero recommendations for the day.", usage: "Fetches recent and lifetime match history to recommend heroes based on parameters like wins, performance score, and hot streaks.\nThe parameter can be a player's SteamID or the nickname of a player bound in this group. If no parameter is provided, it will try to look up the command caller's SteamID.", options: { days: "-d <number> The range of recent days to consider, default is 30." }, examples: 'hero-of-the-day\nhero-of-the-day -d 60\nhero-of-the-day 1234567890\nhero-of-the-day "John Doe"', messages: { title_recommendation: "Today's Recommendation:", recommendation_intro: "The recommended heroes for you today are:", recommendation_heroes: "{#each heroes as hero}{hero}{#if hero !== heroes[heroes.length - 1]}, {/if}{/each}", recommendation_type_lifetime_only: "Your recent match history is empty. This recommendation is based on your lifetime statistics.", recommendation_type_no_record: "Recommendations cannot be generated due to a lack of recent and lifetime match data.", recommendation_type_anonymous: "Recommendations cannot be generated because your profile data is private.", details: { pool_description: "The recommendation is generated by scoring your recent and lifetime hero performance, sorting by total score, and then randomly selecting from the top 10 heroes weighted by their scores.", table_intro: "Below is the detailed score breakdown for the top 10 heroes.", table_headers: { hero: "Hero", recent_wins: "Recent Wins Score", lifetime_wins: "Lifetime Wins<br>(Logarithmic)", imp_bonus: "IMP Bonus", is_hot_streak: "Hot Streak", total_score: "Total Score" }, scoring_formula: "Current Scoring Formula: [Recent Wins x 1] + [log(Lifetime Wins + 1) x 5] + [Recent IMP x 0.1]", hot_streak_desc: "If a hero was played in the last 3 days, it's considered a 'Hot Streak' hero, receiving a 20% bonus to its total score." }, title_meta: "Meta Trends:", meta_intro: "Top 3 advantage heroes for each position with a <b>pick rate ≥2%</b> within ±1 of your rank bracket ({tiers}) over the last week, sorted by win rate:", meta_table_header: "Hero (Pick% Win%)", meta_position: "Pos {pos}:", querying: "Retrieving player stats and hero win rates, please wait...", query_failed: "Command failed." } }, common: { messages: { user_not_binded_in_channel: "By default, it tries to find your information from the bound SteamID players, but it seems you are not bound.\nPlease bind your SteamID in this group. (You can enter [bind -h] for help)\nOr follow the command with the SteamID or nickname of the player you want to query.", user_not_in_group: "Command failed.\nCurrently not in a group chat, you must provide the specified player's SteamID.", invalid_input_include_steam_id: "Invalid SteamID and the player was not found in this group by the given input." } }, help: { description: "Get detailed information for all commands and the link to the online documentation.", usage: "Get detailed information for all commands and the link to the online documentation.", examples: "help", messages: { header: "Below is the full list of plugin commands.\n※Note: <arg> is a required argument, and [arg] is an optional argument. Please see the 'Examples' column for specific usage.", footer: "For more information on plugin configuration, template showcases, and other details, please visit the online documentation:\nhttps://sjtdev.github.io/koishi-plugin-dota2tracker/en-US/", table_headers: { command: "Command", alias: "Alias", arguments: "Arguments", description: "Description", options: "Options", examples: "Examples" } } } } } };
772
772
  }
773
773
  });
774
774
 
775
775
  // src/locales/en-US.schema.yml
776
776
  var require_en_US_schema = __commonJS({
777
777
  "src/locales/en-US.schema.yml"(exports2, module2) {
778
- module2.exports = { _config: { base: { $desc: "Basic Settings", STRATZ_API_TOKEN: "Required. API TOKEN from stratz.com, available at https://stratz.com/api.", dataParsingTimeoutMinutes: "Time to wait for match data parsing (in minutes). If the data parsing time exceeds the waiting time, the report will be generated directly without waiting for the parsing to complete.", proxyAddress: "Proxy address. Leave blank if not using a proxy. \n※Cannot use the global proxy address configured by the `proxy-agent` plugin. This option must be set if you want to use a proxy.", suppressStratzNetworkErrors: "**Please use the `suppressApiNetworkErrors` option below, which applies to both Stratz and OpenDota. \nThis option is still effective. If either this or `suppressApiNetworkErrors` is enabled, Stratz/OpenDota logs will be downgraded to debug output. \nThis option will be removed in a future version.**", suppressApiNetworkErrors: "When enabled, Stratz/OpenDota network error logs will be output at the debug level. \n(e.g., timeouts, network connection issues, but excludes 403 Forbidden) \nKoishi does not display debug-level logs by default. To enable debug log display, please see [📖 Configs#suppressapinetworkerrors](https://sjtdev.github.io/koishi-plugin-dota2tracker/en-US/configs.html#suppressapinetworkerrors-boolean)", enableOpenDotaFallback: "Enable OpenDota as a fallback data source for match tracking and query-match features.", OPENDOTA_API_KEY: "Your paid subscription API key for OpenDota. \nSee https://www.opendota.com/api-keys for details. \nFree users should leave this blank.", OpenDotaIPStack: "If you experience frequent failures when accessing the OpenDota API, it might be caused by a faulty IPv6 environment. \nSetting this option will force the use of IPv4 when accessing the OpenDota API to try and resolve the issue." }, message: { $desc: "Message Settings", useHeroNicknames: "When disabled, only the official hero names will be used.", urlInMessageType: { $desc: "Include links in messages, <br/>please select the message type:", $inner: ["Include stratz match page link in match query and report messages", "Include stratz player page link in player information query messages", "Include Dota Encyclopedia hero page link in hero data query messages"] }, rankBroadSwitch: "Rank change broadcast", rankBroadStar: "Star change broadcast", rankBroadLeader: "Leaderboard rank change broadcast", rankBroadFun: "Fun broadcast template", maxSendItemCount: "Maximum number of item images to send<br/>When exceeded, the following option determines whether to send the item list", showItemListAtTooMuchItems: "Send item list when exceeding max count<br/>Controls whether to send the item list image when search results exceed maxSendItemCount", customItemAlias: { $desc: "Custom item aliases<br/>\nAdd additional aliases when built-in list is insufficient. \nFor widely-used missing aliases, please submit issues/pull requests to the source repository.<br/>\n(Example **Keyword**: Blink Dagger,**Alias**: Blink)", keyword: "Keyword", alias: "Alias" }, autoRecallTips: 'Automatically recall tip messages after the command finishes, e.g., "Searching for match details, please wait..."' }, report: { $desc: "Summary Settings", dailyReportSwitch: "Daily Report Function", dailyReportHours: "Daily report time in hours", dailyReportShowCombi: "*Show combinations in daily report*", weeklyReportSwitch: "Weekly Report Function", weeklyReportDayHours: "Weekly report published on (day) at (hour)", weeklyReportShowCombi: "Show combinations in weekly report" }, template: { $desc: "Template Settings", template_match: "Template used for generating match information images. See [📖 Template Info#Match Info Template](https://sjtdev.github.io/koishi-plugin-dota2tracker/en-US/template-match.html) for display effects.", template_player: "Template used for generating player information images. (Currently only one template available)", template_hero: "Template used for generating hero information images. (Currently only one template available)", playerRankEstimate: "Estimate rank for unranked players in the player template <br>Estimated ranks will be displayed as gray images", templateFonts: '**>Deprecated!<** \n**If you need to configure fonts, please use the `fonts.*` configuration option below instead!** \nFont names used in the template. Requires font files to be installed on the device running Koishi. \nMultiple font names can be added, and it will fallback from top to bottom to the first available font; if no fonts are available, the system default font will be used. \nIf a font name contains spaces or special characters, quotes must be added around the name (it is recommended to always use quotes here); \nIf using a generic font family name, you must **NOT use quotes**, e.g.:\n```\n"Microsoft YaHei"\nsans-serif\n```\nFor more information on font-family, please refer to [📖 MDN: font-family](https://developer.mozilla.org/en-US/docs/Web/CSS/font-family)', fontPath: "Font directory path", fonts: { description: "**Interim solution. A more comprehensive and flexible font configuration scheme will be implemented in the console in the future.** \nFont configuration used in the template. \nIt will automatically scan font files in the `fontPath` directory to generate the selectable font list below. \nYou can separately configure fallback lists for sans-serif (sans), serif (serif), and monospace (mono) font families. \nThe main font type used in the templates is **sans-serif**, while some text in certain templates uses **serif** and **monospace** fonts. \nFor more information on font configuration, please refer to [📖 Template Info#Template Fonts](https://sjtdev.github.io/koishi-plugin-dota2tracker/en-US/template-fonts.html)", sans: { $desc: "Sans-serif fonts (sans-serif)" }, serif: { $desc: "Serif fonts (serif)" }, mono: { $desc: "Monospace fonts (monospace)" } } } } };
778
+ module2.exports = { _config: { base: { $desc: "Basic Settings", STRATZ_API_TOKEN: "Required. API TOKEN from stratz.com, available at https://stratz.com/api.", dataParsingTimeoutMinutes: "Time to wait for match data parsing (in minutes). If the data parsing time exceeds the waiting time, the report will be generated directly without waiting for the parsing to complete.", proxyAddress: "Proxy address. Leave blank if not using a proxy. \n※Cannot use the global proxy address configured by the `proxy-agent` plugin. This option must be set if you want to use a proxy.", suppressStratzNetworkErrors: "**Please use the `suppressApiNetworkErrors` option below, which applies to both Stratz and OpenDota. \nThis option is still effective. If either this or `suppressApiNetworkErrors` is enabled, Stratz/OpenDota logs will be downgraded to debug output. \nThis option will be removed in a future version.**", suppressApiNetworkErrors: "When enabled, Stratz/OpenDota network error logs will be output at the debug level. \n(e.g., timeouts, network connection issues, but excludes 403 Forbidden) \nKoishi does not display debug-level logs by default. To enable debug log display, please see [📖 Configs#suppressapinetworkerrors](https://sjtdev.github.io/koishi-plugin-dota2tracker/en-US/configs.html#suppressapinetworkerrors-boolean)", enableOpenDotaFallback: "Enable OpenDota as a fallback data source for match tracking and query-match features.", OPENDOTA_API_KEY: "Your paid subscription API key for OpenDota. \nSee https://www.opendota.com/api-keys for details. \nFree users should leave this blank.", OpenDotaIPStack: "If you experience frequent failures when accessing the OpenDota API, it might be caused by a faulty IPv6 environment. \nSetting this option will force the use of IPv4 when accessing the OpenDota API to try and resolve the issue." }, message: { $desc: "Message Settings", useHeroNicknames: "When disabled, only the official hero names will be used.", urlInMessageType: { $desc: "Include links in messages, <br/>please select the message type:", $inner: ["Include stratz match page link in match query and report messages", "Include stratz player page link in player information query messages", "Include Dota Encyclopedia hero page link in hero data query messages"] }, rankBroadSwitch: "Rank change broadcast", rankBroadStar: "Star change broadcast", rankBroadLeader: "Leaderboard rank change broadcast", rankBroadFun: "Fun broadcast template", maxSendItemCount: "Maximum number of item images to send<br/>When exceeded, the following option determines whether to send the item list", showItemListAtTooMuchItems: "Send item list when exceeding max count<br/>Controls whether to send the item list image when search results exceed maxSendItemCount", customItemAlias: { $desc: "Custom item aliases<br/>\nAdd additional aliases when built-in list is insufficient. \nFor widely-used missing aliases, please submit issues/pull requests to the source repository.<br/>\n(Example **Keyword**: Blink Dagger,**Alias**: Blink)", keyword: "Keyword", alias: "Alias" }, autoRecallTips: 'Automatically recall tip messages after the command finishes, e.g., "Searching for match details, please wait..."' }, report: { $desc: "Summary Settings", dailyReportSwitch: "Daily Report Function", dailyReportHours: "Daily report time in hours", dailyReportShowCombi: "*Show combinations in daily report*", weeklyReportSwitch: "Weekly Report Function", weeklyReportDayHours: "Weekly report published on (day) at (hour)", weeklyReportShowCombi: "Show combinations in weekly report" }, template: { $desc: "Template Settings", template_match: "Template used for generating match information images. See [📖 Template Info#Match Info Template](https://sjtdev.github.io/koishi-plugin-dota2tracker/en-US/template-match.html) for display effects.", template_player: "Template used for generating player information images. (Currently only one template available)", template_hero: "Template used for generating hero information images. (Currently only one template available)", playerRankEstimate: "Estimate rank for unranked players in the player template <br>Estimated ranks will be displayed as gray images", heroPatchNotesRetrievalDepth: "The scope of the update log (major version) used in the hero template to display changes to the hero's panel attributes, skill values, or mechanics. \nFor example, if the current version is 7.41b, setting this to **2** will retrieve the update logs from 7.40 to 7.41b. \nFor display effects, see [📖 Template Info#Hero Info Template](https://sjtdev.github.io/koishi-plugin-dota2tracker/en-US/template-hero.html).", fontPath: "Font directory path", fonts: { description: "Font configuration used in the template. \nIt will automatically scan font files in the `fontPath` directory to generate the selectable font list below. \nYou can separately configure fallback lists for sans-serif (sans), serif (serif), and monospace (mono) font families. \nThe main font type used in the templates is **sans-serif**, while some text in certain templates uses **serif** and **monospace** fonts. \nFor more information on font configuration, please refer to [📖 Template Info#Template Fonts](https://sjtdev.github.io/koishi-plugin-dota2tracker/en-US/template-fonts.html)", sans: { $desc: "Sans-serif fonts (sans-serif)" }, serif: { $desc: "Serif fonts (serif)" }, mono: { $desc: "Monospace fonts (monospace)" } } } } };
779
779
  }
780
780
  });
781
781
 
782
782
  // src/locales/en-US.template.yml
783
783
  var require_en_US_template = __commonJS({
784
784
  "src/locales/en-US.template.yml"(exports2, module2) {
785
- module2.exports = { dota2tracker: { template: { radiant: "Radiant", dire: "Dire", won: "Won", lost: "Lost", match_id_: "Match ID: {0}", game_mode_: "Mode: {0}", start_time_: "Start Time: {0}", end_time_: "End Time: {0}", pick_order: "#{0}", random: "R", hero_damage_: "Damage: {0}", building_damage_: "Building: {0}", damage_received_: "Received: {0}", lasthit_: "LastHit: {0}", deny_: "Deny: {0}", "lh/dn_": "LH/DN: {0}", GPM: "GPM", XPM: "XPM", heal_: "Heal: {0}", crowd_control_duration_: "CCD: {0}", "GPM/XPM_": "GPM/XPM: {0}", lane: "Lane", lane_: "Lane: ", lane_advantage: "Lane +", lane_disadvantage: "Lane -", lane_jungle: "Jungle", lane_stomp: "Lane+++", lane_stomped: "Lane---", lane_tie: "Lane ==", analysis_successful: "Analysis successful", analysis_incomplete: "Analysis incomplete", analysis_by_opendota: "Analysis by OpenDota", kill: "Kill", kill_contribution_: "KC: {0}", position: "Position", position_: "Position: ", position_1: "Carry", position_2: "Mid", position_3: "OffLane", position_4: "Softsup", position_5: "Hardsup", dire_won: "Dire Won", radiant_won: "Radiant Won", total_damage: "Damage", total_experience: "Exp.", total_gold: "Gold", region_: "Region: {0}", duration_: "Duration: {0}", position_undefined: "?", top10_: "Top 10 Heroes by Matches: ", match_count_: "Matches: ", last25matches_: "Last 25 Matches: ", winrate_: "Winrate: ", imp_: "IMP: ", lane_advantage_rate_: "Lane Advantage Rate: ", hero: "Hero", all_matches_: "All Matches: ", match_count: "Matches", winrate: "Winrate", imp: "IMP", win_count: "Wins", lose_count: "Losses", recently_heroes: "Heroes used more than once recently: ", recently_positions: "Performance in the last 25 matches across all positions: ", winning_streak: "Winning Streak", losing_streak: "Losing Streak", id: "ID", mode: "Mode", kda_kc: "KDA(KC)", time: "Time", duration: "Duration", rank: "Rank", un_parsed: "(Unparsed)", combined_win_loss_summary: "Combined Win/Loss Summary: ", yesterdays_summary: "Yesterday's Summary", last_weeks_summary: "Last Week's Summary", report_won: "W", report_lost: "L", report_winrate: "WR", anonymous_player_1: "This profile is private.", anonymous_player_2: "Background is for display purposes only. It is not {player}’s data.", rank_fun_down_message: "AVATAR_PLACEHOLDER<br/>Sad", rank_fun_up_message: "Hip hip hooray! Our awesome member AVATAR_PLACEHOLDER{name} has leveled up from PREV_PLACEHOLDER to CURR_PLACEHOLDER!", titles: { MVP: "MVP-#FFA500", Soul: "Soul-#66CCFF", Rich: "Rich-#FFD700", Wise: "Wise-#8888FF", Controller: "Controller-#FF00FF", Nuker: "Nuker-#CC0088", Breaker: "Breaker-#DD0000", Ghost: "Ghost-#CCCCCC", Utility: "Utility-#20B2AA", Assister: "Assister-#006400", Demolisher: "Demolisher-#FEDCBA", Healer: "Healer-#00FF00", Tank: "Tank-#84A1C7", Idle: "Idle-#DDDDDD" }, situation: "Situation", networth: "Net Worth", experience: "Experience", OUTCOME_MAP: { RADIANT_VICTORY: "RADIANT VICTORY", RADIANT_STOMP: "RADIANT STOMP", DIRE_VICTORY: "DIRE VICTORY", DIRE_STOMP: "DIRE STOMP", TIE: "TIE" }, lane_top: "Top", lane_mid: "Mid", lane_bottom: "Bottom", empty_extra_info: "No extra info", opendota: { networth_unavailable: "Net Worth Chart Unavailable", networth_unavailable_reason: "Data source OpenDota does not provide per-minute net worth data.", lane_outcome_tip: "(Based on gold earned, not net worth; ref only.)", gold_t: "Gold Earned" }, report: { daily: { plugin_name: "Koishi Dota 2 Plugin", title: "Daily Group Recap", meta: { date_format: "cccc, LLLL d, yyyy", summary: "Performance summary for {0}.", footer_format: "ID: #{0} • Server: {1}" }, stats: { matches: "Matches", win_rate: "Win Rate", kills: "Total Kills", duration: "Duration", avg_time: "Avg", matches_subtext: "{0} W - {1} L", vs_yesterday: "vs Yesterday", kills_avg: "Avg {0}" }, spotlight: { mvp_title: "The King", lvp_title: "The Suspect", score_label: "MVP Score" }, squad: { title: "Performance", subtitle: "Ranked by KDA (desc)", header: { rank: "Rank", player_info: "Player Info", hero_pool: "Hero Pool (Sorted by Wins)", kda: "KDA Ratio", impact: "Impact" }, impact: { dmg: "Dmg", gold: "Gold" } }, footer: { generated_by: "Generated by Koishi Bot" } } } } } };
785
+ module2.exports = { dota2tracker: { template: { radiant: "Radiant", dire: "Dire", won: "Won", lost: "Lost", match_id_: "Match ID: {0}", game_mode_: "Mode: {0}", start_time_: "Start Time: {0}", end_time_: "End Time: {0}", pick_order: "#{0}", random: "R", hero_damage_: "Damage: {0}", building_damage_: "Building: {0}", damage_received_: "Received: {0}", lasthit_: "LastHit: {0}", deny_: "Deny: {0}", "lh/dn_": "LH/DN: {0}", GPM: "GPM", XPM: "XPM", heal_: "Heal: {0}", crowd_control_duration_: "CCD: {0}", "GPM/XPM_": "GPM/XPM: {0}", lane: "Lane", lane_: "Lane: ", lane_advantage: "Lane +", lane_disadvantage: "Lane -", lane_jungle: "Jungle", lane_stomp: "Lane+++", lane_stomped: "Lane---", lane_tie: "Lane ==", analysis_successful: "Analysis successful", analysis_incomplete: "Analysis incomplete", analysis_by_opendota: "Analysis by OpenDota", kill: "Kill", kill_contribution_: "KC: {0}", position: "Position", position_: "Position: ", position_1: "Carry", position_2: "Mid", position_3: "OffLane", position_4: "Softsup", position_5: "Hardsup", dire_won: "Dire Won", radiant_won: "Radiant Won", total_damage: "Damage", total_experience: "Exp.", total_gold: "Gold", region_: "Region: {0}", duration_: "Duration: {0}", position_undefined: "?", top10_: "Top 10 Heroes by Matches: ", match_count_: "Matches: ", last25matches_: "Last 25 Matches: ", winrate_: "Winrate: ", imp_: "IMP: ", lane_advantage_rate_: "Lane Advantage Rate: ", hero: "Hero", all_matches_: "All Matches: ", match_count: "Matches", winrate: "Winrate", imp: "IMP", win_count: "Wins", lose_count: "Losses", recently_heroes: "Heroes used more than once recently: ", recently_positions: "Performance in the last 25 matches across all positions: ", winning_streak: "Winning Streak", losing_streak: "Losing Streak", id: "ID", mode: "Mode", kda_kc: "KDA(KC)", time: "Time", duration: "Duration", rank: "Rank", un_parsed: "(Unparsed)", combined_win_loss_summary: "Combined Win/Loss Summary: ", yesterdays_summary: "Yesterday's Summary", last_weeks_summary: "Last Week's Summary", report_won: "W", report_lost: "L", report_winrate: "WR", anonymous_player_1: "This profile is private.", anonymous_player_2: "Background is for display purposes only. It is not {player}’s data.", rank_fun_down_message: "AVATAR_PLACEHOLDER<br/>Sad", rank_fun_up_message: "Hip hip hooray! Our awesome member AVATAR_PLACEHOLDER{name} has leveled up from PREV_PLACEHOLDER to CURR_PLACEHOLDER!", titles: { MVP: "MVP-#FFA500", Soul: "Soul-#66CCFF", Rich: "Rich-#FFD700", Wise: "Wise-#8888FF", Controller: "Controller-#FF00FF", Nuker: "Nuker-#CC0088", Breaker: "Breaker-#DD0000", Ghost: "Ghost-#CCCCCC", Utility: "Utility-#20B2AA", Assister: "Assister-#006400", Demolisher: "Demolisher-#FEDCBA", Healer: "Healer-#00FF00", Tank: "Tank-#84A1C7", Idle: "Idle-#DDDDDD" }, situation: "Situation", networth: "Net Worth", experience: "Experience", OUTCOME_MAP: { RADIANT_VICTORY: "RADIANT VICTORY", RADIANT_STOMP: "RADIANT STOMP", DIRE_VICTORY: "DIRE VICTORY", DIRE_STOMP: "DIRE STOMP", TIE: "TIE" }, lane_top: "Top", lane_mid: "Mid", lane_bottom: "Bottom", empty_extra_info: "No extra info", opendota: { networth_unavailable: "Net Worth Chart Unavailable", networth_unavailable_reason: "Data source OpenDota does not provide per-minute net worth data.", lane_outcome_tip: "(Based on gold earned, not net worth; ref only.)", gold_t: "Gold Earned" }, report: { daily: { plugin_name: "Koishi Dota 2 Plugin", title: "Daily Group Recap", meta: { date_format: "cccc, LLLL d, yyyy", summary: "Performance summary for {0}.", footer_format: "ID: #{0} • Server: {1}" }, stats: { matches: "Matches", win_rate: "Win Rate", kills: "Total Kills", duration: "Duration", avg_time: "Avg", matches_subtext: "{0} W - {1} L", vs_yesterday: "vs Yesterday", kills_avg: "Avg {0}" }, spotlight: { mvp_title: "The King", lvp_title: "The Suspect", score_label: "MVP Score" }, squad: { title: "Performance", subtitle: "Ranked by KDA (desc)", header: { rank: "Rank", player_info: "Player Info", hero_pool: "Hero Pool (Sorted by Wins)", kda: "KDA Ratio", impact: "Impact" }, impact: { dmg: "Dmg", gold: "Gold" } }, footer: { generated_by: "Generated by Koishi Bot" } } }, generated_at: "Generated at" } } };
786
786
  }
787
787
  });
788
788
 
@@ -796,21 +796,21 @@ var require_en_US = __commonJS({
796
796
  // src/locales/zh-CN.command.yml
797
797
  var require_zh_CN_command = __commonJS({
798
798
  "src/locales/zh-CN.command.yml"(exports2, module2) {
799
- module2.exports = { commands: { dota2tracker: { description: "dota2tracker的一系列指令,可以使用dota2tracker -h查看所有可用指令,或者使用`dota2tracker.help`(默认别名`DOTA2帮助`)查看所有指令详细说明表。", subscribe: { description: "[订阅本群]", usage: "订阅后还需玩家在本群绑定SteamID,BOT将订阅本群中已绑定玩家的新比赛数据,在STRATZ比赛解析完成后将比赛数据生成为图片战报发布至本群中。", examples: "订阅本群", messages: { subscribed: "本群已订阅,无需重复订阅。", subscribe_success: "订阅成功。" } }, unsubscribe: { description: "[取消订阅] 取消订阅本群。", usage: "取消订阅本群。", examples: "取消订阅", messages: { unsubscribe_success: "取消订阅成功。", not_subscribed: "本群尚未订阅,无需取消订阅。" } }, bind: { description: "[绑定] 绑定SteamID,并起一个别名(也可以不起)。", usage: '将你的SteamID与你的账号绑定,若本群已订阅将会实时获取你的新比赛数据发布至群中。名称中含有空格时需要使用""引号包裹(英文半角引号)。', examples: '绑定 123456789\n绑定 123456789 张三\n绑定 123456789 "张 三"', messages: { steam_id_invalid: "SteamID无效。", bind_success: "绑定成功,\nID:{userId}\n别名:{nickName}\nSteamID:{steamId}", bind_failed: "绑定失败,{0}", reason_without_match: "SteamID无效或无任何场次。", reason_fetch_failed: "网络状况不佳或其他原因无法验证SteamID,请稍后重试。", already_binded: "你已绑定,无需重复绑定。\n以下是你的个人信息:\nID:{userId}\n别名:{nickName}\nSteamID:{steamId}", nick_name_too_long: "别名过长,请限制在20个字符以内。(也可以留空)", is_anonymous: "请注意:你的Steam玩家数据并未公开,将无法使用BOT的主要功能,如“战报追踪”、“查询最近指令”等。\n如需公开数据,请在DOTA2游戏内设置中公开。" } }, unbind: { description: "[取消绑定] 取消绑定你的个人信息。", usage: "取消绑定你的个人信息。", examples: "取消绑定", messages: { unbind_success: "取消绑定成功。", not_binded: "尚未绑定,无需取消绑定。" } }, rename: { description: "[改名] 修改绑定时设定的别名。", usage: '修改绑定时设定的别名。名称中含有空格时需要使用""引号包裹(英文半角引号)。', examples: '改名 李四\n改名 "李 四"', messages: { rename_success: "改名成功,现在你叫{nick_name}了。", empty_input: "请输入你的别名。", not_binded: "请先绑定,绑定时即可设定别名。", nick_name_too_long: "别名过长,请限制在20个字符以内。", nick_name_same: "目标别名与原始别名相同,无需改名。" } }, "query-members": { description: "[查询群友] 查询本群已绑定的玩家。", usage: "查询本群已绑定的玩家,生成简单信息图片发布。", examples: "查询群友", messages: { title: "本群 DOTA2 玩家名册 (共 {count} 人)", table_headers: { nickname: "昵称/别名", winrate: "胜率 (近10场)", last_match: "最近比赛" }, no_members: "本群尚无绑定玩家。", query_failed: "查询群友失败。" } }, "query-match": { description: "[查询比赛] 查询指定比赛ID的比赛数据,生成图片发布。", usage: "查询指定MatchID的比赛数据,生成图片发布。", options: { parse: "-p 是否等待解析比赛数据" }, examples: "查询比赛 1234567890\n查询比赛 1234567890 -p\n查询比赛 1234567890 --parse", messages: { empty_input: "请输入比赛ID。", match_id_invalid: "比赛ID无效。", querying_match: "正在搜索对局详情,请稍后……", query_failed: "获取比赛数据失败。", waiting_for_parse: "比赛数据尚未解析,已发送解析请求到服务器,战报将在解析完成或超时后发送。" } }, "query-recent-match": { description: "[查询最近比赛] 查询最近的比赛数据,生成图片发布。", options: { parse: "-p 是否等待解析比赛数据" }, usage: "查询指定玩家的最近一场比赛的比赛数据,生成图片发布。\n参数可输入该玩家的SteamID或已在本群绑定玩家的别名,无参数时尝试查询调用指令玩家的SteamID。", examples: "查询最近比赛\n查询最近比赛 123456789\n查询最近比赛 张三\n查询最近比赛 123456789 -p\n查询最近比赛 张三 --parse", messages: { querying_match: "正在搜索对局详情,请稍后……", query_failed: "获取玩家最近比赛失败。", not_in_group: "指令调用失败。\n当前不属于群聊状态,必须提供指定玩家的SteamID。", is_anonymous: "你的比赛数据未公开,无法获取最近比赛数据。\n如需公开数据,请在DOTA2游戏内设置中公开。" } }, "query-player": { description: "[查询玩家] 查询玩家的个人信息,可指定英雄。", options: { hero: "-o 查询玩家指定英雄使用情况(同查询英雄,可用别名或ID)" }, usage: "查询指定玩家的个人信息,生成图片发布,可指定英雄。\n参数可输入该玩家的SteamID或已在本群绑定玩家的别名,无参数时尝试查询调用指令玩家的SteamID。", examples: "查询玩家\n查询玩家 123456789\n查询玩家 张三\n查询玩家 张三 --hero 敌法师\n查询玩家 张三 -o 15", messages: { querying_player: "正在获取玩家数据,请稍后……", query_failed: "获取玩家信息失败。", not_in_group: "指令调用失败。\n当前不属于群聊状态,必须提供指定玩家的SteamID。" } }, "query-hero": { description: "[查询英雄] 查询英雄技能/面板信息。", options: { random: "-r 随机选择英雄" }, usage: "查询英雄的技能说明与各项数据,生成图片发布。\n参数可输入英雄ID、英雄名、英雄常用别名。", examples: "查询英雄 15\n查询英雄 雷泽\n查询英雄 电魂\n查询英雄 -r", messages: { not_found: "未找到输入的英雄,请确认后重新输入。", querying_hero: "正在获取英雄数据,请稍后……", query_failed: "获取英雄数据失败。", empty_input: "请输入参数。" } }, "query-item": { description: "[查询物品] 查询物品信息。", usage: "查询物品的描述与各项数据,生成图片发布。\n参数可输入物品名(可模糊查找)、物品别名、物品ID。\n可在配置页中设置每次查询的最大发送数量、以及是否在超过限制或未输入参数时发送物品列表。", examples: "查询物品 先锋盾", messages: { query_list_failed: "获取物品列表数据失败。", query_item_failed: "获取物品「{0}」数据失败", querying_item: "正在查询物品数据,请稍候…", cache_building: "初次使用或缓存已过期,正在生成当前版本的物品缓存,请稍后……", empty_input: "未输入关键字参数。根据当前配置{#if show},将返回全部物品列表{:else}无内容可发送{/if}。", not_found: "未找到与关键字匹配的物品,请确认后重试。", too_many_items: "找到{count}个物品,超过最大发送限制({max}个){#if show},将发送物品列表{/if}。", finded_items: "找到以下物品:{#each items as item}{item.name_loc}{#if item !== items[items.length - 1]}、{/if}{/each}" } }, "hero-of-the-day": { description: "[今日英雄] 获取今日英雄推荐。", usage: "获取近期比赛记录、生涯比赛记录,根据胜场、表现分、是否手热等参数计算推荐英雄。\n参数可输入该玩家的SteamID或已在本群绑定玩家的别名,无参数时尝试查询调用指令玩家的SteamID。", options: { days: "-d <number> 近期数据的获取范围,单位为天数,默认值为30" }, examples: "今日英雄\n今日英雄 -d 60\n今日英雄 1234567890\n今日英雄 张三", messages: { title_recommendation: "今日推荐:", recommendation_intro: "今日为您推荐的英雄是:", recommendation_heroes: "{#each heroes as hero}{hero}{#if hero !== heroes[heroes.length - 1]}、{/if}{/each}", recommendation_type_lifetime_only: "您的近期数据为空,本次推荐结果基于您的生涯数据。", recommendation_type_no_record: "您的近期与生涯数据为空,无法生成推荐信息。", recommendation_type_anonymous: "您的个人数据未公开,无法生成推荐信息。", details: { pool_description: "推荐结果根据对您的近期与生涯英雄使用记录计分后,按总分排序后对前10位英雄以分数为权重随机取得。", table_intro: "以下是前10位英雄具体得分表。", table_headers: { hero: "英雄名称", recent_wins: "近期胜场分", lifetime_wins: "生涯胜场分<br>(对数)", imp_bonus: "imp奖励分", is_hot_streak: "是否手热", total_score: "总分" }, scoring_formula: "当前计分规则:[近期胜场数 x 1] + [log(生涯胜场数+1) x 5] + [近期imp x 0.1]", hot_streak_desc: "若英雄在3天内使用过,则记为手热英雄,总分提升20%。" }, title_meta: "环境趋势:", meta_intro: "一周内,基于您段位±1 ({tiers}) 范围内各位置<b>选择率≥2%</b>按胜率从高到低前三名优势英雄:", meta_table_header: "英雄名称(选择率% 胜率%)", meta_position: "{pos}号位:", querying: "正在查询玩家数据、英雄胜率等数据,请稍候……", query_failed: "指令调用失败。" } }, common: { messages: { user_not_binded_in_channel: "无参数时默认从已绑定SteamID玩家中寻找你的信息,但你似乎并没有绑定。\n请在本群绑定SteamID。(可输入【绑定 -h】获取帮助)\n或在指令后跟上希望查询的SteamID或已绑定玩家的别名。", user_not_in_group: "指令调用失败。\n当前不属于群聊状态,必须提供指定玩家的SteamID。", invalid_input_include_steam_id: "SteamID无效并且未在本群根据输入信息找到玩家。" } }, help: { description: "[DOTA2指南] 获取插件的全部指令详细信息与在线文档链接。", usage: "获取插件的全部指令详细信息与在线文档链接。", examples: "DOTA2指南\nDOTA2帮助\nDOTA2说明", messages: { header: "以下是插件的全部指令。\n※注意 <arg> 是必须参数,[arg] 是可选参数,具体使用方法请看“用法示例”列。", footer: "插件配置、模板展示等更多信息请访问在线文档:\nhttps://sjtdev.github.io/koishi-plugin-dota2tracker/", table_headers: { command: "指令名", alias: "指令中文", arguments: "参数", description: "说明", options: "选项", examples: "用法示例" } } } } } };
799
+ module2.exports = { commands: { dota2tracker: { description: "dota2tracker的一系列指令,可以使用dota2tracker -h查看所有可用指令,或者使用`dota2tracker.help`(默认别名`DOTA2帮助`)查看所有指令详细说明表。", subscribe: { description: "[订阅本群]", usage: "订阅后还需玩家在本群绑定SteamID,BOT将订阅本群中已绑定玩家的新比赛数据,在STRATZ比赛解析完成后将比赛数据生成为图片战报发布至本群中。", examples: "订阅本群", messages: { subscribed: "本群已订阅,无需重复订阅。", subscribe_success: "订阅成功。" } }, unsubscribe: { description: "[取消订阅] 取消订阅本群。", usage: "取消订阅本群。", examples: "取消订阅", messages: { unsubscribe_success: "取消订阅成功。", not_subscribed: "本群尚未订阅,无需取消订阅。" } }, bind: { description: "[绑定] 绑定SteamID,并起一个别名(也可以不起)。", usage: '将你的SteamID与你的账号绑定,若本群已订阅将会实时获取你的新比赛数据发布至群中。名称中含有空格时需要使用""引号包裹(英文半角引号)。', examples: '绑定 123456789\n绑定 123456789 张三\n绑定 123456789 "张 三"', messages: { steam_id_invalid: "SteamID无效。", bind_success: "绑定成功,\nID:{userId}\n别名:{nickName}\nSteamID:{steamId}", bind_failed: "绑定失败,{0}", reason_without_match: "SteamID无效或无任何场次。", reason_fetch_failed: "网络状况不佳或其他原因无法验证SteamID,请稍后重试。", already_binded: "你已绑定,无需重复绑定。\n以下是你的个人信息:\nID:{userId}\n别名:{nickName}\nSteamID:{steamId}", nick_name_too_long: "别名过长,请限制在20个字符以内。(也可以留空)", is_anonymous: "请注意:你的Steam玩家数据并未公开,将无法使用BOT的主要功能,如“战报追踪”、“查询最近指令”等。\n如需公开数据,请在DOTA2游戏内设置中公开。" } }, unbind: { description: "[取消绑定] 取消绑定你的个人信息。", usage: "取消绑定你的个人信息。", examples: "取消绑定", messages: { unbind_success: "取消绑定成功。", not_binded: "尚未绑定,无需取消绑定。" } }, rename: { description: "[改名] 修改绑定时设定的别名。", usage: '修改绑定时设定的别名。名称中含有空格时需要使用""引号包裹(英文半角引号)。', examples: '改名 李四\n改名 "李 四"', messages: { rename_success: "改名成功,现在你叫{nick_name}了。", empty_input: "请输入你的别名。", not_binded: "请先绑定,绑定时即可设定别名。", nick_name_too_long: "别名过长,请限制在20个字符以内。", nick_name_same: "目标别名与原始别名相同,无需改名。" } }, "query-members": { description: "[查询群友] 查询本群已绑定的玩家。", usage: "查询本群已绑定的玩家,生成简单信息图片发布。", examples: "查询群友", messages: { title: "本群 DOTA2 玩家名册 (共 {count} 人)", table_headers: { nickname: "昵称/别名", winrate: "胜率 (近10场)", last_match: "最近比赛" }, no_members: "本群尚无绑定玩家。", query_failed: "查询群友失败。" } }, "query-match": { description: "[查询比赛] 查询指定比赛ID的比赛数据,生成图片发布。", usage: "查询指定MatchID的比赛数据,生成图片发布。", options: { parse: "-p 是否等待解析比赛数据" }, examples: "查询比赛 1234567890\n查询比赛 1234567890 -p\n查询比赛 1234567890 --parse", messages: { empty_input: "请输入比赛ID。", match_id_invalid: "比赛ID无效。", querying_match: "正在搜索对局详情,请稍后……", query_failed: "获取比赛数据失败。", waiting_for_parse: "比赛数据尚未解析,已发送解析请求到服务器,战报将在解析完成或超时后发送。" } }, "query-recent-match": { description: "[查询最近比赛] 查询最近的比赛数据,生成图片发布。", options: { parse: "-p 是否等待解析比赛数据" }, usage: "查询指定玩家的最近一场比赛的比赛数据,生成图片发布。\n参数可输入该玩家的SteamID或已在本群绑定玩家的别名,无参数时尝试查询调用指令玩家的SteamID。", examples: "查询最近比赛\n查询最近比赛 123456789\n查询最近比赛 张三\n查询最近比赛 123456789 -p\n查询最近比赛 张三 --parse", messages: { querying_match: "正在搜索对局详情,请稍后……", query_failed: "获取玩家最近比赛失败。", not_in_group: "指令调用失败。\n当前不属于群聊状态,必须提供指定玩家的SteamID。", is_anonymous: "你的比赛数据未公开,无法获取最近比赛数据。\n如需公开数据,请在DOTA2游戏内设置中公开。" } }, "query-player": { description: "[查询玩家] 查询玩家的个人信息,可指定英雄。", options: { hero: "-o 查询玩家指定英雄使用情况(同查询英雄,可用别名或ID)" }, usage: "查询指定玩家的个人信息,生成图片发布,可指定英雄。\n参数可输入该玩家的SteamID或已在本群绑定玩家的别名,无参数时尝试查询调用指令玩家的SteamID。", examples: "查询玩家\n查询玩家 123456789\n查询玩家 张三\n查询玩家 张三 --hero 敌法师\n查询玩家 张三 -o 15", messages: { querying_player: "正在获取玩家数据,请稍后……", query_failed: "获取玩家信息失败。", not_in_group: "指令调用失败。\n当前不属于群聊状态,必须提供指定玩家的SteamID。" } }, "query-hero": { description: "[查询英雄] 查询英雄技能/面板信息。", options: { random: "-r 随机选择英雄" }, usage: "查询英雄的技能说明与各项数据,生成图片发布。\n参数可输入英雄ID、英雄名、英雄常用别名。", examples: "查询英雄 15\n查询英雄 雷泽\n查询英雄 电魂\n查询英雄 -r", messages: { not_found: "未找到输入的英雄,请确认后重新输入。", querying_hero: "正在获取英雄数据,请稍后……", fetching_patch_notes: "正在获取英雄历史改动建立缓存,请稍候……", query_failed: "获取英雄数据失败。", empty_input: "请输入参数。" } }, "query-item": { description: "[查询物品] 查询物品信息。", usage: "查询物品的描述与各项数据,生成图片发布。\n参数可输入物品名(可模糊查找)、物品别名、物品ID。\n可在配置页中设置每次查询的最大发送数量、以及是否在超过限制或未输入参数时发送物品列表。", examples: "查询物品 先锋盾", messages: { query_list_failed: "获取物品列表数据失败。", query_item_failed: "获取物品「{0}」数据失败", querying_item: "正在查询物品数据,请稍候…", cache_building: "初次使用或缓存已过期,正在生成当前版本的物品缓存,请稍后……", empty_input: "未输入关键字参数。根据当前配置{#if show},将返回全部物品列表{:else}无内容可发送{/if}。", not_found: "未找到与关键字匹配的物品,请确认后重试。", too_many_items: "找到{count}个物品,超过最大发送限制({max}个){#if show},将发送物品列表{/if}。", finded_items: "找到以下物品:{#each items as item}{item.name_loc}{#if item !== items[items.length - 1]}、{/if}{/each}" } }, "hero-of-the-day": { description: "[今日英雄] 获取今日英雄推荐。", usage: "获取近期比赛记录、生涯比赛记录,根据胜场、表现分、是否手热等参数计算推荐英雄。\n参数可输入该玩家的SteamID或已在本群绑定玩家的别名,无参数时尝试查询调用指令玩家的SteamID。", options: { days: "-d <number> 近期数据的获取范围,单位为天数,默认值为30" }, examples: "今日英雄\n今日英雄 -d 60\n今日英雄 1234567890\n今日英雄 张三", messages: { title_recommendation: "今日推荐:", recommendation_intro: "今日为您推荐的英雄是:", recommendation_heroes: "{#each heroes as hero}{hero}{#if hero !== heroes[heroes.length - 1]}、{/if}{/each}", recommendation_type_lifetime_only: "您的近期数据为空,本次推荐结果基于您的生涯数据。", recommendation_type_no_record: "您的近期与生涯数据为空,无法生成推荐信息。", recommendation_type_anonymous: "您的个人数据未公开,无法生成推荐信息。", details: { pool_description: "推荐结果根据对您的近期与生涯英雄使用记录计分后,按总分排序后对前10位英雄以分数为权重随机取得。", table_intro: "以下是前10位英雄具体得分表。", table_headers: { hero: "英雄名称", recent_wins: "近期胜场分", lifetime_wins: "生涯胜场分<br>(对数)", imp_bonus: "imp奖励分", is_hot_streak: "是否手热", total_score: "总分" }, scoring_formula: "当前计分规则:[近期胜场数 x 1] + [log(生涯胜场数+1) x 5] + [近期imp x 0.1]", hot_streak_desc: "若英雄在3天内使用过,则记为手热英雄,总分提升20%。" }, title_meta: "环境趋势:", meta_intro: "一周内,基于您段位±1 ({tiers}) 范围内各位置<b>选择率≥2%</b>按胜率从高到低前三名优势英雄:", meta_table_header: "英雄名称(选择率% 胜率%)", meta_position: "{pos}号位:", querying: "正在查询玩家数据、英雄胜率等数据,请稍候……", query_failed: "指令调用失败。" } }, common: { messages: { user_not_binded_in_channel: "无参数时默认从已绑定SteamID玩家中寻找你的信息,但你似乎并没有绑定。\n请在本群绑定SteamID。(可输入【绑定 -h】获取帮助)\n或在指令后跟上希望查询的SteamID或已绑定玩家的别名。", user_not_in_group: "指令调用失败。\n当前不属于群聊状态,必须提供指定玩家的SteamID。", invalid_input_include_steam_id: "SteamID无效并且未在本群根据输入信息找到玩家。" } }, help: { description: "[DOTA2指南] 获取插件的全部指令详细信息与在线文档链接。", usage: "获取插件的全部指令详细信息与在线文档链接。", examples: "DOTA2指南\nDOTA2帮助\nDOTA2说明", messages: { header: "以下是插件的全部指令。\n※注意 <arg> 是必须参数,[arg] 是可选参数,具体使用方法请看“用法示例”列。", footer: "插件配置、模板展示等更多信息请访问在线文档:\nhttps://sjtdev.github.io/koishi-plugin-dota2tracker/", table_headers: { command: "指令名", alias: "指令中文", arguments: "参数", description: "说明", options: "选项", examples: "用法示例" } } } } } };
800
800
  }
801
801
  });
802
802
 
803
803
  // src/locales/zh-CN.schema.yml
804
804
  var require_zh_CN_schema = __commonJS({
805
805
  "src/locales/zh-CN.schema.yml"(exports2, module2) {
806
- module2.exports = { _config: { base: { $desc: "基础设置", STRATZ_API_TOKEN: "※必须。stratz.com的API TOKEN,可在 https://stratz.com/api 获取。", dataParsingTimeoutMinutes: "等待比赛数据解析的时间(单位:分钟)。如果数据解析时间超过等待时间,将直接生成战报而不再等待解析完成。", proxyAddress: "代理地址,不使用代理请留空。 \n※无法使用`proxy-agent`插件配置的全局代理地址,欲使用代理必须设置此项。", suppressStratzNetworkErrors: "**请使用下方通用于 stratz 与 opendota 的配置项`suppressApiNetworkErrors`。 \n此配置项仍然生效,当与`suppressApiNetworkErrors`任一启用时将会使 stratz/opendota 日志降级到debug输出。 \n此配置项将于下版本被移除。**", suppressApiNetworkErrors: "开启后将 stratz/opendota 网络错误日志使用debug级别输出。 \n(如超时、网络不通等,但403 Forbidden除外) \nkoishi默认不显示debug级日志,若需要开启debug日志显示,请见 [📖 配置项#suppressapinetworkerrors](http://sjtdev.github.io/koishi-plugin-dota2tracker/configs.html#suppressapinetworkerrors-boolean)", enableOpenDotaFallback: "启用 OpenDota 作为战报追踪与查询比赛功能的备用数据源。", OPENDOTA_API_KEY: "OpenDota 的订阅付费APIKEY, \n可在 https://www.opendota.com/api-keys 查看详情。 \nOpenDota 的免费用户此处请留空。", OpenDotaIPStack: "若访问 OpenDota API 时频繁失败,可能是由于错误的 IPv6 环境导致的。 \n设置此选项可在访问 OpenDota API 时强制使用 IPv4 尝试解决问题。" }, message: { $desc: "消息设置", useHeroNicknames: "是否使用英雄别名。关闭后仅使用英雄正式名称。", urlInMessageType: { $desc: "在消息中附带链接,<br/>请选择消息类型:", $inner: ["在查询比赛与战报消息中附带stratz比赛页面链接", "在查询玩家信息消息中附带stratz玩家页面链接", "在查询英雄数据消息中附带刀塔百科对应英雄页面链接"] }, rankBroadSwitch: "段位变动播报", rankBroadStar: "星级变动播报", rankBroadLeader: "冠绝名次变动播报", rankBroadFun: "整活播报模板", maxSendItemCount: "最大发送物品图片数量,<br/> 当超过指定数量时将由下方选项决定是否发送查询结果的物品列表图片", showItemListAtTooMuchItems: "在查询结果的物品数量超过指定数量时,是否发送查询结果的物品列表图片", customItemAlias: { $desc: "额外物品别名设置<br/>当插件内置的[物品别名列表](https://github.com/sjtdev/koishi-plugin-dota2tracker/blob/master/src/locales/zh-CN.constants.json#L304-L407)中没有想要的物品别名可在此处追加,如果是插件疏漏的广为人知的物品别名推荐到源码仓库提交issue或pull request完善列表。<br/>(例如 **关键词**: 闪烁匕首,**别名**: 跳刀)", keyword: "关键词", alias: "别名" }, autoRecallTips: "在指令调用结束后自动撤回提示消息,如:“正在搜索对局详情,请稍后……”" }, report: { $desc: "总结设置", dailyReportSwitch: "日报功能", dailyReportHours: "日报时间小时", dailyReportShowCombi: "*日报是否显示组合*", weeklyReportSwitch: "周报功能", weeklyReportDayHours: "周报发布于周(几)的(几)点", weeklyReportShowCombi: "周报是否显示组合" }, template: { $desc: "模板设置", template_match: "生成比赛信息图片使用的模板,显示效果见 [📖 模板相关#对局信息模板](https://sjtdev.github.io/koishi-plugin-dota2tracker/template-match.html)。", template_player: "生成玩家信息图片使用的模板。(目前仅有一张模板)", template_hero: "生成英雄信息图片使用的模板。(目前仅有一张模板)", playerRankEstimate: "在player模板中对没有段位的玩家进行段位估算 <br>估算的段位将以灰色图片显示", templateFonts: '**>已弃用!<** \n**如果需要配置字体请使用下方`fonts.*`配置项代替!** \n模板所使用的字体名。需要 koishi 所在设备安装字体文件。 \n可添加多个字体名,将从上到下回退到第一个可用字体;若所有字体都不可用,则使用系统默认字体。 \n其中字体名若包含空格或特殊字符需要在名称首尾添加引号(此处建议尽量强制使用引号); \n若使用字体族名则必须**不使用引号**,如:\n```\n"Microsoft YaHei"\nsans-serif\n```\n有关font-family的更多信息,请查阅 [📖 MDN: font-family](https://developer.mozilla.org/zh-CN/docs/Web/CSS/font-family)', fontPath: "字体文件文件夹路径", fonts: { description: "**过渡方案,之后会重启控制台页面并在其中实现更完善更灵活的字体配置方案。** \n模板所使用的字体配置。\n会自动读取配置项 `fontPath` 目录下的字体文件为下方配置项生成可选字体列表。 \n可分别配置无衬线(sans)、衬线(serif)、等宽(mono)字体族的备选表。 \n模板的主要使用字体类型为**无衬线字体**,部分模板的一些文本会使用到**衬线字体**及**等宽字体**。 \n关于字体文件配置的更多信息,请查阅 [📖 模板相关#模板字体](https://sjtdev.github.io/koishi-plugin-dota2tracker/template-fonts.html)", sans: { $desc: "无衬线字体(sans-serif)" }, serif: { $desc: "衬线字体(serif)" }, mono: { $desc: "等宽字体(monospace)" } } } } };
806
+ module2.exports = { _config: { base: { $desc: "基础设置", STRATZ_API_TOKEN: "※必须。stratz.com的API TOKEN,可在 https://stratz.com/api 获取。", dataParsingTimeoutMinutes: "等待比赛数据解析的时间(单位:分钟)。如果数据解析时间超过等待时间,将直接生成战报而不再等待解析完成。", proxyAddress: "代理地址,不使用代理请留空。 \n※无法使用`proxy-agent`插件配置的全局代理地址,欲使用代理必须设置此项。", suppressStratzNetworkErrors: "**请使用下方通用于 stratz 与 opendota 的配置项`suppressApiNetworkErrors`。 \n此配置项仍然生效,当与`suppressApiNetworkErrors`任一启用时将会使 stratz/opendota 日志降级到debug输出。 \n此配置项将于下版本被移除。**", suppressApiNetworkErrors: "开启后将 stratz/opendota 网络错误日志使用debug级别输出。 \n(如超时、网络不通等,但403 Forbidden除外) \nkoishi默认不显示debug级日志,若需要开启debug日志显示,请见 [📖 配置项#suppressapinetworkerrors](http://sjtdev.github.io/koishi-plugin-dota2tracker/configs.html#suppressapinetworkerrors-boolean)", enableOpenDotaFallback: "启用 OpenDota 作为战报追踪与查询比赛功能的备用数据源。", OPENDOTA_API_KEY: "OpenDota 的订阅付费APIKEY, \n可在 https://www.opendota.com/api-keys 查看详情。 \nOpenDota 的免费用户此处请留空。", OpenDotaIPStack: "若访问 OpenDota API 时频繁失败,可能是由于错误的 IPv6 环境导致的。 \n设置此选项可在访问 OpenDota API 时强制使用 IPv4 尝试解决问题。" }, message: { $desc: "消息设置", useHeroNicknames: "是否使用英雄别名。关闭后仅使用英雄正式名称。", urlInMessageType: { $desc: "在消息中附带链接,<br/>请选择消息类型:", $inner: ["在查询比赛与战报消息中附带stratz比赛页面链接", "在查询玩家信息消息中附带stratz玩家页面链接", "在查询英雄数据消息中附带刀塔百科对应英雄页面链接"] }, rankBroadSwitch: "段位变动播报", rankBroadStar: "星级变动播报", rankBroadLeader: "冠绝名次变动播报", rankBroadFun: "整活播报模板", maxSendItemCount: "最大发送物品图片数量,<br/> 当超过指定数量时将由下方选项决定是否发送查询结果的物品列表图片", showItemListAtTooMuchItems: "在查询结果的物品数量超过指定数量时,是否发送查询结果的物品列表图片", customItemAlias: { $desc: "额外物品别名设置<br/>当插件内置的[物品别名列表](https://github.com/sjtdev/koishi-plugin-dota2tracker/blob/master/src/locales/zh-CN.constants.json#L304-L407)中没有想要的物品别名可在此处追加,如果是插件疏漏的广为人知的物品别名推荐到源码仓库提交issue或pull request完善列表。<br/>(例如 **关键词**: 闪烁匕首,**别名**: 跳刀)", keyword: "关键词", alias: "别名" }, autoRecallTips: "在指令调用结束后自动撤回提示消息,如:“正在搜索对局详情,请稍后……”" }, report: { $desc: "总结设置", dailyReportSwitch: "日报功能", dailyReportHours: "日报时间小时", dailyReportShowCombi: "*日报是否显示组合*", weeklyReportSwitch: "周报功能", weeklyReportDayHours: "周报发布于周(几)的(几)点", weeklyReportShowCombi: "周报是否显示组合" }, template: { $desc: "模板设置", template_match: "生成比赛信息图片使用的模板,显示效果见 [📖 模板相关#对局信息模板](https://sjtdev.github.io/koishi-plugin-dota2tracker/template-match.html)。", template_player: "生成玩家信息图片使用的模板。(目前仅有一张模板)", template_hero: "生成英雄信息图片使用的模板。(目前仅有一张模板)", playerRankEstimate: "在player模板中对没有段位的玩家进行段位估算 <br>估算的段位将以灰色图片显示", heroPatchNotesRetrievalDepth: "在hero模板中用于展示英雄的面板属性与技能数值或机制的改动的更新日志获取范围(大版本)。 \n例如当前版本为7.41b,此项设置为**2**时,将获取7.40~7.41b的更新日志内容。 \n展示效果见 [📖 模板相关#英雄信息模板](https://sjtdev.github.io/koishi-plugin-dota2tracker/template-hero.html)。", fontPath: "字体文件文件夹路径", fonts: { description: "模板所使用的字体配置。\n会自动读取配置项 `fontPath` 目录下的字体文件为下方配置项生成可选字体列表。 \n可分别配置无衬线(sans)、衬线(serif)、等宽(mono)字体族的备选表。 \n模板的主要使用字体类型为**无衬线字体**,部分模板的一些文本会使用到**衬线字体**及**等宽字体**。 \n关于字体文件配置的更多信息,请查阅 [📖 模板相关#模板字体](https://sjtdev.github.io/koishi-plugin-dota2tracker/template-fonts.html)", sans: { $desc: "无衬线字体(sans-serif)" }, serif: { $desc: "衬线字体(serif)" }, mono: { $desc: "等宽字体(monospace)" } } } } };
807
807
  }
808
808
  });
809
809
 
810
810
  // src/locales/zh-CN.template.yml
811
811
  var require_zh_CN_template = __commonJS({
812
812
  "src/locales/zh-CN.template.yml"(exports2, module2) {
813
- module2.exports = { dota2tracker: { template: { radiant: "天辉", dire: "夜魇", won: "获胜", lost: "失败", match_id_: "比赛编号:{0}", game_mode_: "模式:{0}", start_time_: "起始时间:{0}", end_time_: "结束时间:{0}", pick_order: "第{0}手", random: "随机", hero_damage_: "英雄伤害:{0}", building_damage_: "建筑伤害:{0}", damage_received_: "受到伤害:{0}", lasthit_: "补刀:{0}", deny_: "反补:{0}", "lh/dn_": "补刀:{0}/{1}", GPM: "GPM", XPM: "XPM", heal_: "治疗量:{0}", crowd_control_duration_: "控制时间:{0}", "GPM/XPM_": "GPM/XPM:{0}", lane: "对线", lane_: "对线:", lane_advantage: "对线优势", lane_disadvantage: "对线劣势", lane_stomp: "对线碾压", lane_stomped: "对线被碾", lane_tie: "对线平手", lane_jungle: "野区霸主", analysis_successful: "录像分析成功", analysis_incomplete: "分析结果不完整", analysis_by_opendota: "数据解析自 OpenDota", kill: "击杀", kill_contribution_: "参战率:{0}", position: "位置", position_: "位置:", position_1: "优势路", position_2: "中路", position_3: "烈士路", position_4: "采灵芝", position_5: "工具人", position_undefined: "?", total_damage: "总伤害", total_gold: "总经济", total_experience: "总经验", radiant_won: "天辉获胜", dire_won: "夜魇获胜", duration_: "持续时间:{0}", region_: "地区:{0}", match_count_: "场次:", last25matches_: "最近25场:", winrate_: "胜率:", imp_: "表现:", lane_advantage_rate_: "线优率:", top10_: "全期场次前十的英雄:", hero: "英雄", all_matches_: "全期场次:", match_count: "场次", winrate: "胜率", imp: "表现", win_count: "胜场", lose_count: "败场", recently_heroes: "近期使用场次大于1的英雄:", recently_positions: "近25场各个位置的表现:", winning_streak: "连胜", losing_streak: "连败", id: "ID", mode: "模式", kda_kc: "KDA(参战率)", time: "时间", duration: "时长", rank: "段位", un_parsed: "(未解析)", combined_win_loss_summary: "组合胜负情况:", yesterdays_summary: "昨日总结", last_weeks_summary: "上周总结", report_won: "胜", report_lost: "负", report_winrate: "胜率", anonymous_player_1: "数据未公开", anonymous_player_2: "背景仅供展示目的,不属于{player}的数据。", rank_fun_up_message: "热烈祝贺群友 AVATAR_PLACEHOLDER{name} 在天梯中再获进步,<br/>由 PREV_PLACEHOLDER 升为 CURR_PLACEHOLDER,再接再厉,再创辉煌!", rank_fun_down_message: "AVATAR_PLACEHOLDER<br/>寄", titles: { MVP: "MVP-#FFA500", Soul: "魂-#66CCFF", Rich: "富-#FFD700", Wise: "睿-#8888FF", Controller: "控-#FF00FF", Nuker: "爆-#CC0088", Breaker: "破-#DD0000", Ghost: "鬼-#CCCCCC", Utility: "辅-#20B2AA", Assister: "助-#006400", Demolisher: "拆-#FEDCBA", Healer: "奶-#00FF00", Tank: "耐-#84A1C7", Idle: "摸-#DDDDDD" }, situation: "局势", networth: "经济", experience: "经验", OUTCOME_MAP: { RADIANT_VICTORY: "天辉优势", RADIANT_STOMP: "天辉碾压", DIRE_VICTORY: "夜魇优势", DIRE_STOMP: "夜魇碾压", TIE: "势均力敌" }, lane_top: "上路", lane_mid: "中路", lane_bottom: "下路", empty_extra_info: "比赛未解析或信息缺失,无法展示更多数据。", opendota: { networth_unavailable: "经济走势图不可用", networth_unavailable_reason: "数据源 OpenDota 未提供每分钟经济数据。", lane_outcome_tip: "(数据基于累计获得金币而非经济,仅供参考)", gold_t: "累计获得金币" }, report: { daily: { plugin_name: "Koishi Dota 2 Plugin", title: "每日战报", meta: { date_format: "yyyy年L月d日 cccc", summary: "战报生成于 {0}。", footer_format: "ID: #{0} • 服务器: {1}" }, stats: { matches: "总场次", win_rate: "胜率", kills: "总击杀", duration: "总时长", avg_time: "平均时长", matches_subtext: "{0}胜 - {1}负", vs_yesterday: "较昨日", kills_avg: "场均 {0}" }, spotlight: { mvp_title: "全 场 最 佳", lvp_title: "头 号 战 犯", score_label: "综合评分" }, squad: { title: "表现排行", subtitle: "基于 KDA 倒序排列", header: { rank: "排名", player_info: "玩家", hero_pool: "英雄池(基于胜场)", kda: "KDA", impact: "贡献 / 经济" }, impact: { dmg: "伤害", gold: "经济" } }, footer: { generated_by: "Generated by Koishi Bot" } } } } } };
813
+ module2.exports = { dota2tracker: { template: { radiant: "天辉", dire: "夜魇", won: "获胜", lost: "失败", match_id_: "比赛编号:{0}", game_mode_: "模式:{0}", start_time_: "起始时间:{0}", end_time_: "结束时间:{0}", pick_order: "第{0}手", random: "随机", hero_damage_: "英雄伤害:{0}", building_damage_: "建筑伤害:{0}", damage_received_: "受到伤害:{0}", lasthit_: "补刀:{0}", deny_: "反补:{0}", "lh/dn_": "补刀:{0}/{1}", GPM: "GPM", XPM: "XPM", heal_: "治疗量:{0}", crowd_control_duration_: "控制时间:{0}", "GPM/XPM_": "GPM/XPM:{0}", lane: "对线", lane_: "对线:", lane_advantage: "对线优势", lane_disadvantage: "对线劣势", lane_stomp: "对线碾压", lane_stomped: "对线被碾", lane_tie: "对线平手", lane_jungle: "野区霸主", analysis_successful: "录像分析成功", analysis_incomplete: "分析结果不完整", analysis_by_opendota: "数据解析自 OpenDota", kill: "击杀", kill_contribution_: "参战率:{0}", position: "位置", position_: "位置:", position_1: "优势路", position_2: "中路", position_3: "烈士路", position_4: "采灵芝", position_5: "工具人", position_undefined: "?", total_damage: "总伤害", total_gold: "总经济", total_experience: "总经验", radiant_won: "天辉获胜", dire_won: "夜魇获胜", duration_: "持续时间:{0}", region_: "地区:{0}", match_count_: "场次:", last25matches_: "最近25场:", winrate_: "胜率:", imp_: "表现:", lane_advantage_rate_: "线优率:", top10_: "全期场次前十的英雄:", hero: "英雄", all_matches_: "全期场次:", match_count: "场次", winrate: "胜率", imp: "表现", win_count: "胜场", lose_count: "败场", recently_heroes: "近期使用场次大于1的英雄:", recently_positions: "近25场各个位置的表现:", winning_streak: "连胜", losing_streak: "连败", id: "ID", mode: "模式", kda_kc: "KDA(参战率)", time: "时间", duration: "时长", rank: "段位", un_parsed: "(未解析)", combined_win_loss_summary: "组合胜负情况:", yesterdays_summary: "昨日总结", last_weeks_summary: "上周总结", report_won: "胜", report_lost: "负", report_winrate: "胜率", anonymous_player_1: "数据未公开", anonymous_player_2: "背景仅供展示目的,不属于{player}的数据。", rank_fun_up_message: "热烈祝贺群友 AVATAR_PLACEHOLDER{name} 在天梯中再获进步,<br/>由 PREV_PLACEHOLDER 升为 CURR_PLACEHOLDER,再接再厉,再创辉煌!", rank_fun_down_message: "AVATAR_PLACEHOLDER<br/>寄", titles: { MVP: "MVP-#FFA500", Soul: "魂-#66CCFF", Rich: "富-#FFD700", Wise: "睿-#8888FF", Controller: "控-#FF00FF", Nuker: "爆-#CC0088", Breaker: "破-#DD0000", Ghost: "鬼-#CCCCCC", Utility: "辅-#20B2AA", Assister: "助-#006400", Demolisher: "拆-#FEDCBA", Healer: "奶-#00FF00", Tank: "耐-#84A1C7", Idle: "摸-#DDDDDD" }, situation: "局势", networth: "经济", experience: "经验", OUTCOME_MAP: { RADIANT_VICTORY: "天辉优势", RADIANT_STOMP: "天辉碾压", DIRE_VICTORY: "夜魇优势", DIRE_STOMP: "夜魇碾压", TIE: "势均力敌" }, lane_top: "上路", lane_mid: "中路", lane_bottom: "下路", empty_extra_info: "比赛未解析或信息缺失,无法展示更多数据。", opendota: { networth_unavailable: "经济走势图不可用", networth_unavailable_reason: "数据源 OpenDota 未提供每分钟经济数据。", lane_outcome_tip: "(数据基于累计获得金币而非经济,仅供参考)", gold_t: "累计获得金币" }, report: { daily: { plugin_name: "Koishi Dota 2 Plugin", title: "每日战报", meta: { date_format: "yyyy年L月d日 cccc", summary: "战报生成于 {0}。", footer_format: "ID: #{0} • 服务器: {1}" }, stats: { matches: "总场次", win_rate: "胜率", kills: "总击杀", duration: "总时长", avg_time: "平均时长", matches_subtext: "{0}胜 - {1}负", vs_yesterday: "较昨日", kills_avg: "场均 {0}" }, spotlight: { mvp_title: "全 场 最 佳", lvp_title: "头 号 战 犯", score_label: "综合评分" }, squad: { title: "表现排行", subtitle: "基于 KDA 倒序排列", header: { rank: "排名", player_info: "玩家", hero_pool: "英雄池(基于胜场)", kda: "KDA", impact: "贡献 / 经济" }, impact: { dmg: "伤害", gold: "经济" } }, footer: { generated_by: "Generated by Koishi Bot" } } }, generated_at: "生成于" } } };
814
814
  }
815
815
  });
816
816
 
@@ -831,7 +831,7 @@ __export(index_exports, {
831
831
  usage: () => usage
832
832
  });
833
833
  module.exports = __toCommonJS(index_exports);
834
- var import_node_path5 = __toESM(require("node:path"));
834
+ var import_node_path6 = __toESM(require("node:path"));
835
835
 
836
836
  // src/app/common/i18n.ts
837
837
  var import_koishi = require("koishi");
@@ -1137,82 +1137,96 @@ var HeroService = class _HeroService extends import_koishi2.Service {
1137
1137
  this.ctx.dota2tracker.cache.setWweeklyMetaCache(cacheKey, weeklyHeroMeta);
1138
1138
  return weeklyHeroMeta;
1139
1139
  }
1140
- async getHeroDetails(input, languageTag, isRandom = false) {
1140
+ async getHeroDetails(input, languageTag, isRandom = false, onDownloadingStalePatch) {
1141
1141
  const heroId = this.ctx.dota2tracker.i18n.findHeroIdInLocale(isRandom ? import_koishi2.Random.pick(Object.keys(this.ctx.dota2tracker.dotaconstants.heroes)) : input);
1142
1142
  if (!heroId) return;
1143
- return _HeroService.formatHeroDetails(await this.ctx.dota2tracker.valveAPI.queryHeroDetailsFromValve(heroId, languageTag));
1144
- }
1145
- static formatHeroDetails(rawHero) {
1146
- let hero = Object.assign({}, rawHero);
1143
+ const patchNotes = await this.ctx.dota2tracker.staticData.getPatchNotes(languageTag, onDownloadingStalePatch);
1144
+ const rawHero = await this.ctx.dota2tracker.valveAPI.queryHeroDetailsFromValve(heroId, languageTag);
1145
+ return _HeroService.formatHeroDetails(rawHero, patchNotes);
1146
+ }
1147
+ static formatHeroDetails(rawHero, patchNotes = []) {
1148
+ const patchesOfHero = [];
1149
+ for (const patch of patchNotes.reverse()) {
1150
+ const heroPatchData = patch.heroes?.find((hero2) => hero2.hero_id === rawHero.id);
1151
+ if (heroPatchData) {
1152
+ patchesOfHero.push({ patch_number: patch.patch_number, patch_timestamp: patch.patch_timestamp, notes: heroPatchData });
1153
+ }
1154
+ }
1155
+ const hero = Object.assign({}, rawHero);
1156
+ hero.game_version = (patchNotes.length > 0 ? patchNotes[0].patch_number : "Unknown") + " (" + import_luxon2.DateTime.now().toFormat("yyyy-MM-dd HH:mm") + ")";
1157
+ hero.patch_notes = [];
1158
+ for (const patch of patchesOfHero) {
1159
+ const combinedNotes = [];
1160
+ if (patch.notes.hero_notes) {
1161
+ combinedNotes.push(...patch.notes.hero_notes);
1162
+ }
1163
+ if (patch.notes.talent_notes) {
1164
+ combinedNotes.push(...patch.notes.talent_notes);
1165
+ }
1166
+ if (combinedNotes.length > 0) {
1167
+ hero.patch_notes.push({ patch_number: patch.patch_number, patch_timestamp: patch.patch_timestamp, notes: combinedNotes });
1168
+ }
1169
+ }
1147
1170
  hero.abilities.forEach((ab) => {
1148
1171
  ab.desc_loc = this.formatHeroDesc(ab.desc_loc, ab.special_values);
1149
1172
  ab.notes_loc = ab.notes_loc.map((note) => this.formatHeroDesc(note, ab.special_values));
1150
1173
  if (ab.ability_has_scepter) ab.scepter_loc = this.formatHeroDesc(ab.scepter_loc, ab.special_values, "scepter" /* Scepter */);
1151
1174
  if (ab.ability_has_shard) ab.shard_loc = this.formatHeroDesc(ab.shard_loc, ab.special_values, "shard" /* Shard */);
1175
+ ab.patch_notes = [];
1176
+ for (const patch of patchesOfHero) {
1177
+ if (!patch.notes.abilities) continue;
1178
+ const patchAbility = patch.notes.abilities.find((patchAb) => patchAb.ability_id === ab.id);
1179
+ if (patchAbility && patchAbility.ability_notes) {
1180
+ ab.patch_notes.push({ patch_number: patch.patch_number, patch_timestamp: patch.patch_timestamp, notes: patchAbility.ability_notes });
1181
+ }
1182
+ }
1152
1183
  });
1153
1184
  hero.talents.forEach((talent) => {
1154
- const regex = /\{s:(.*?)\}/g;
1155
- let match;
1156
- while ((match = regex.exec(talent.name_loc)) !== null) {
1157
- const specialValueName = match[1];
1185
+ talent.name_loc = talent.name_loc.replace(/\{s:(.*?)\}/g, (match, specialValueName) => {
1158
1186
  const target = talent.special_values?.find((sv) => sv.name === specialValueName);
1159
- if (target) {
1160
- talent.name_loc = talent.name_loc.replace(match[0], target.values_float.join("/"));
1161
- } else {
1162
- const abilities = hero.abilities.filter((ability) => ability.special_values.some((specialValue) => specialValue.bonuses.some((bonus) => bonus.name === talent.name)));
1163
- for (const ability of abilities) {
1164
- const specialValues = ability.special_values.filter((specialValue) => specialValue.bonuses.some((bonus) => bonus.name === talent.name));
1165
- const regex2 = /{s:bonus_(.*?)}/g;
1166
- let match2;
1167
- const replacements = [];
1168
- while ((match2 = regex2.exec(talent.name_loc)) !== null) {
1169
- const specialValue = specialValues.find((sv) => sv.name === String(match2[1]));
1170
- const replacement = specialValue?.bonuses.find((bonus) => bonus.name === talent.name)?.value;
1171
- if (replacement !== void 0) {
1172
- replacements.push({
1173
- original: match2[0],
1174
- replacement
1175
- });
1176
- }
1187
+ if (target) return target.values_float.join("/");
1188
+ const cleanVarName = specialValueName.replace(/^bonus_/, "");
1189
+ for (const ability of hero.abilities) {
1190
+ const svWithBonus = ability.special_values.find((sv) => (sv.name === cleanVarName || sv.name === specialValueName) && sv.bonuses.some((bonus) => bonus.name === talent.name));
1191
+ if (svWithBonus) {
1192
+ const bonusObj = svWithBonus.bonuses.find((bonus) => bonus.name === talent.name);
1193
+ if (bonusObj && bonusObj.value !== void 0) {
1194
+ return bonusObj.value;
1177
1195
  }
1178
- replacements.forEach(({ original, replacement }) => {
1179
- talent.name_loc = talent.name_loc.replace(original, replacement);
1180
- });
1181
1196
  }
1182
1197
  }
1183
- }
1184
- talent.name_loc = talent.name_loc.replace(/\{s:.*?\}/g, "?");
1198
+ return "?";
1199
+ });
1185
1200
  });
1186
1201
  return hero;
1187
1202
  }
1188
1203
  static formatHeroDesc(template, special_values, type = "normal" /* Normal */) {
1204
+ if (!template) return template;
1189
1205
  return template.replace(/%%|%([^%]+)%|\{([^}]+)\}/g, (match, p1, p2) => {
1206
+ if (match === "%%") return "%";
1190
1207
  const field = p1 || p2;
1191
- if (match === "%%") {
1192
- return "%";
1193
- } else {
1194
- const fieldName = field.replace(/^s:/, "").replace(/^shard_/, "").toLowerCase();
1195
- const specialValue = special_values.find((sv) => {
1196
- const nameLower = sv.name.toLowerCase();
1197
- return nameLower === fieldName || nameLower === `bonus_${fieldName}` || nameLower === `shard_${fieldName}` || `bonus_${nameLower}` === fieldName || `shard_${nameLower}` === fieldName;
1198
- });
1199
- if (specialValue) {
1200
- let valuesToUse = "";
1201
- switch (type) {
1202
- case "scepter" /* Scepter */:
1203
- valuesToUse = specialValue.values_scepter.length ? specialValue.values_scepter.join(" / ") : specialValue.values_float.join(" / ");
1204
- break;
1205
- case "shard" /* Shard */:
1206
- valuesToUse = specialValue.values_shard.length ? specialValue.values_shard.join(" / ") : specialValue.values_float.join(" / ");
1207
- break;
1208
- default:
1209
- valuesToUse = specialValue.values_float.join(" / ");
1210
- }
1211
- return `<span class="value">${valuesToUse}</span>`;
1212
- } else {
1213
- return match;
1208
+ const fieldName = field.replace(/^s:/, "").replace(/^shard_/, "").toLowerCase();
1209
+ const strippedFieldName = fieldName.replace(/_tooltip$/, "");
1210
+ const specialValue = special_values.find((sv) => {
1211
+ const nameLower = sv.name.toLowerCase();
1212
+ const strippedNameLower = nameLower.replace(/_tooltip$/, "");
1213
+ return nameLower === fieldName || nameLower === `bonus_${fieldName}` || nameLower === `shard_${fieldName}` || `bonus_${nameLower}` === fieldName || `shard_${nameLower}` === fieldName || strippedNameLower === strippedFieldName || nameLower === `ability${strippedFieldName}`;
1214
+ });
1215
+ if (specialValue) {
1216
+ let valuesToUse = "";
1217
+ switch (type) {
1218
+ case "scepter" /* Scepter */:
1219
+ valuesToUse = specialValue.values_scepter.length ? specialValue.values_scepter.join(" / ") : specialValue.values_float.join(" / ");
1220
+ break;
1221
+ case "shard" /* Shard */:
1222
+ valuesToUse = specialValue.values_shard.length ? specialValue.values_shard.join(" / ") : specialValue.values_float.join(" / ");
1223
+ break;
1224
+ default:
1225
+ valuesToUse = specialValue.values_float.join(" / ");
1214
1226
  }
1227
+ return `<span class="value">${valuesToUse}</span>`;
1215
1228
  }
1229
+ return match;
1216
1230
  });
1217
1231
  }
1218
1232
  };
@@ -1474,6 +1488,7 @@ var MatchService = class _MatchService extends import_koishi4.Service {
1474
1488
  super(ctx, "dota2tracker.match", true);
1475
1489
  this.pluginVersion = pluginVersion;
1476
1490
  }
1491
+ pluginVersion;
1477
1492
  static {
1478
1493
  __name(this, "MatchService");
1479
1494
  }
@@ -2175,6 +2190,12 @@ var CacheService = class extends import_koishi6.Service {
2175
2190
  async getWeeklyMetaCache(key) {
2176
2191
  return this.ctx.cache.get("dt_weekly_metadata", key);
2177
2192
  }
2193
+ setPatchNoteCache(key, value) {
2194
+ this.ctx.cache.set("dt_patch_notes", key, value, 3600 * 1e3 * 6);
2195
+ }
2196
+ async getPatchNoteCache(key) {
2197
+ return this.ctx.cache.get("dt_patch_notes", key);
2198
+ }
2178
2199
  cacheItemListConstants(languageTag, itemList, gameVersion) {
2179
2200
  this.ctx.cache.set("dt_itemlist_constants", languageTag, {
2180
2201
  gameVersion,
@@ -2378,13 +2399,177 @@ var DatabaseService = class extends import_koishi7.Service {
2378
2399
  }
2379
2400
  };
2380
2401
 
2381
- // src/app/data/stratz.api.ts
2382
- var import_node_fs = __toESM(require("node:fs"));
2402
+ // src/app/data/file.ts
2383
2403
  var import_koishi8 = require("koishi");
2384
2404
  var import_node_path = __toESM(require("node:path"));
2405
+ var import_promises = __toESM(require("node:fs/promises"));
2406
+ var import_node_fs = require("node:fs");
2407
+ var FileService = class extends import_koishi8.Service {
2408
+ static {
2409
+ __name(this, "FileService");
2410
+ }
2411
+ baseDir;
2412
+ constructor(ctx) {
2413
+ super(ctx, "dota2tracker.file", true);
2414
+ this.config = ctx.config;
2415
+ this.baseDir = import_node_path.default.join(ctx.baseDir, "data", "dota2tracker");
2416
+ if (!(0, import_node_fs.existsSync)(this.baseDir)) {
2417
+ (0, import_node_fs.mkdirSync)(this.baseDir, { recursive: true });
2418
+ }
2419
+ }
2420
+ async readJSON(subDir, filename) {
2421
+ const targetPath = import_node_path.default.join(this.baseDir, subDir, filename);
2422
+ if (!(0, import_node_fs.existsSync)(targetPath)) {
2423
+ return null;
2424
+ }
2425
+ try {
2426
+ const data = await import_promises.default.readFile(targetPath, "utf-8");
2427
+ return JSON.parse(data);
2428
+ } catch (e) {
2429
+ this.logger.error(`Failed to read JSON file from ${targetPath}:`, e);
2430
+ return null;
2431
+ }
2432
+ }
2433
+ async writeJSON(subDir, filename, data) {
2434
+ const targetDir = import_node_path.default.join(this.baseDir, subDir);
2435
+ const targetPath = import_node_path.default.join(targetDir, filename);
2436
+ try {
2437
+ if (!(0, import_node_fs.existsSync)(targetDir)) {
2438
+ await import_promises.default.mkdir(targetDir, { recursive: true });
2439
+ }
2440
+ await import_promises.default.writeFile(targetPath, JSON.stringify(data, null, 2), "utf-8");
2441
+ return true;
2442
+ } catch (e) {
2443
+ this.logger.error(`Failed to write JSON file to ${targetPath}:`, e);
2444
+ return false;
2445
+ }
2446
+ }
2447
+ };
2448
+
2449
+ // src/app/data/static-data.ts
2450
+ var import_koishi9 = require("koishi");
2451
+ var StaticDataService = class extends import_koishi9.Service {
2452
+ static {
2453
+ __name(this, "StaticDataService");
2454
+ }
2455
+ constructor(ctx) {
2456
+ super(ctx, "dota2tracker.staticData", true);
2457
+ this.config = ctx.config;
2458
+ this.logger = ctx.logger("dota2tracker.staticData");
2459
+ }
2460
+ async getPatchNotes(languageTag, onDownloadingStalePatch) {
2461
+ const patchesList = await this.ctx.dota2tracker.valveAPI.queryPatchList();
2462
+ if (!patchesList || patchesList.length === 0) return [];
2463
+ const latestPatch = patchesList[patchesList.length - 1];
2464
+ const latestMajorMatch = latestPatch.patch_name.match(/^(\d+\.\d+)/);
2465
+ if (!latestMajorMatch) return [];
2466
+ const targetPatches = [];
2467
+ const collectedMajors = /* @__PURE__ */ new Set();
2468
+ for (let i = patchesList.length - 1; i >= 0; i--) {
2469
+ const p = patchesList[i];
2470
+ const majorMatch = p.patch_name.match(/^(\d+\.\d+)/);
2471
+ if (!majorMatch) continue;
2472
+ collectedMajors.add(majorMatch[1]);
2473
+ if (collectedMajors.size > this.config.heroPatchNotesRetrievalDepth) {
2474
+ break;
2475
+ }
2476
+ targetPatches.push(p);
2477
+ }
2478
+ targetPatches.reverse();
2479
+ const result = [];
2480
+ const latestPatchNumber = latestPatch.patch_number;
2481
+ let hasFiredCallback = false;
2482
+ for (const p of targetPatches) {
2483
+ const isLatest = p.patch_number === latestPatchNumber;
2484
+ let notes = null;
2485
+ const fileName = `${p.patch_number}-${languageTag}.json`;
2486
+ if (isLatest) {
2487
+ notes = await this.ctx.dota2tracker.cache.getPatchNoteCache(fileName);
2488
+ if (!notes) {
2489
+ notes = await this.ctx.dota2tracker.valveAPI.queryPatchNotes(p.patch_number, languageTag);
2490
+ if (notes && notes.success) {
2491
+ this.ctx.dota2tracker.cache.setPatchNoteCache(fileName, notes);
2492
+ }
2493
+ }
2494
+ } else {
2495
+ notes = await this.ctx.dota2tracker.file.readJSON("raw_data/patch_notes", fileName);
2496
+ if (!notes) {
2497
+ if (!hasFiredCallback && onDownloadingStalePatch) {
2498
+ onDownloadingStalePatch();
2499
+ hasFiredCallback = true;
2500
+ }
2501
+ notes = await this.ctx.dota2tracker.valveAPI.queryPatchNotes(p.patch_number, languageTag);
2502
+ if (notes && notes.success) {
2503
+ await this.ctx.dota2tracker.file.writeJSON("raw_data/patch_notes", fileName, notes);
2504
+ }
2505
+ }
2506
+ }
2507
+ if (notes) {
2508
+ result.push(notes);
2509
+ }
2510
+ }
2511
+ return result;
2512
+ }
2513
+ };
2514
+
2515
+ // src/app/data/stratz.api.ts
2516
+ var import_node_fs2 = __toESM(require("node:fs"));
2517
+ var import_koishi10 = require("koishi");
2518
+ var import_node_path2 = __toESM(require("node:path"));
2385
2519
  var import_axios2 = __toESM(require("axios"));
2386
2520
  var import_https_proxy_agent = require("https-proxy-agent");
2387
- var StratzAPI = class extends import_koishi8.Service {
2521
+
2522
+ // src/app/common/miniqueue.ts
2523
+ var MiniQueue = class {
2524
+ constructor(ctx, options = { interval: 200 }) {
2525
+ this.ctx = ctx;
2526
+ this.interval = options.interval;
2527
+ }
2528
+ ctx;
2529
+ static {
2530
+ __name(this, "MiniQueue");
2531
+ }
2532
+ queue = [];
2533
+ isProcessing = false;
2534
+ interval;
2535
+ stopped = false;
2536
+ add(task) {
2537
+ if (this.stopped) {
2538
+ return Promise.reject(new Error("Queue has been disposed."));
2539
+ }
2540
+ return new Promise((resolve, reject) => {
2541
+ this.queue.push(async () => {
2542
+ try {
2543
+ const result = await task();
2544
+ resolve(result);
2545
+ } catch (error) {
2546
+ reject(error);
2547
+ }
2548
+ });
2549
+ this._process();
2550
+ });
2551
+ }
2552
+ dispose() {
2553
+ this.stopped = true;
2554
+ this.queue = [];
2555
+ }
2556
+ async _process() {
2557
+ if (this.isProcessing || this.queue.length === 0 || this.stopped) {
2558
+ return;
2559
+ }
2560
+ this.isProcessing = true;
2561
+ const task = this.queue.shift();
2562
+ if (task) {
2563
+ await task();
2564
+ await new Promise((resolve) => this.ctx.setTimeout(resolve, this.interval));
2565
+ }
2566
+ this.isProcessing = false;
2567
+ this._process();
2568
+ }
2569
+ };
2570
+
2571
+ // src/app/data/stratz.api.ts
2572
+ var StratzAPI = class extends import_koishi10.Service {
2388
2573
  static {
2389
2574
  __name(this, "StratzAPI");
2390
2575
  }
@@ -2396,7 +2581,7 @@ var StratzAPI = class extends import_koishi8.Service {
2396
2581
  constructor(ctx, currentDir) {
2397
2582
  super(ctx, "dota2tracker.stratz-api", true);
2398
2583
  this.config = ctx.config;
2399
- this.graphqlQueriesDir = import_node_path.default.join(currentDir, "queries");
2584
+ this.graphqlQueriesDir = import_node_path2.default.join(currentDir, "queries");
2400
2585
  this.queue = new MiniQueue(ctx, { interval: 200 });
2401
2586
  this.http = import_axios2.default.create({ timeout: 15e3, signal: this.abortController.signal });
2402
2587
  ctx.on("dispose", () => this.dispose());
@@ -2420,7 +2605,7 @@ var StratzAPI = class extends import_koishi8.Service {
2420
2605
  }
2421
2606
  async queryPlayersMatchesForDaily_legacy(steamAccountIds, seconds) {
2422
2607
  return this.query(
2423
- "PlayersMatchesForDaily_Legacy",
2608
+ "PlayersMatchesForDaily_legacy",
2424
2609
  {
2425
2610
  steamAccountIds,
2426
2611
  seconds
@@ -2530,100 +2715,56 @@ var StratzAPI = class extends import_koishi8.Service {
2530
2715
  });
2531
2716
  }
2532
2717
  loadGraphqlFile(queryName) {
2533
- return import_node_fs.default.readFileSync(import_node_path.default.join(this.graphqlQueriesDir, `${queryName}.graphql`), { encoding: "utf-8" }).replace(/[\r\n]+/g, " ");
2534
- }
2535
- };
2536
- var MiniQueue = class {
2537
- // 新增一个停止标志
2538
- // 1. 构造函数接收 ctx
2539
- constructor(ctx, options) {
2540
- this.ctx = ctx;
2541
- this.interval = options.interval;
2542
- }
2543
- static {
2544
- __name(this, "MiniQueue");
2545
- }
2546
- queue = [];
2547
- isProcessing = false;
2548
- interval;
2549
- stopped = false;
2550
- add(task) {
2551
- if (this.stopped) {
2552
- return Promise.reject(new Error("Queue has been disposed."));
2553
- }
2554
- return new Promise((resolve, reject) => {
2555
- this.queue.push(async () => {
2556
- try {
2557
- const result = await task();
2558
- resolve(result);
2559
- } catch (error) {
2560
- reject(error);
2561
- }
2562
- });
2563
- this._process();
2564
- });
2565
- }
2566
- // 4. 新增 dispose 方法
2567
- dispose() {
2568
- this.stopped = true;
2569
- this.queue = [];
2570
- }
2571
- async _process() {
2572
- if (this.isProcessing || this.queue.length === 0 || this.stopped) {
2573
- return;
2574
- }
2575
- this.isProcessing = true;
2576
- const task = this.queue.shift();
2577
- if (task) {
2578
- await task();
2579
- await new Promise((resolve) => this.ctx.setTimeout(resolve, this.interval));
2580
- }
2581
- this.isProcessing = false;
2582
- this._process();
2718
+ return import_node_fs2.default.readFileSync(import_node_path2.default.join(this.graphqlQueriesDir, `${queryName}.graphql`), { encoding: "utf-8" }).replace(/[\r\n]+/g, " ");
2583
2719
  }
2584
2720
  };
2585
2721
 
2586
2722
  // src/app/data/valve.api.ts
2587
2723
  var import_axios3 = __toESM(require("axios"));
2588
- var import_koishi9 = require("koishi");
2724
+ var import_koishi11 = require("koishi");
2589
2725
  var import_https_proxy_agent2 = require("https-proxy-agent");
2590
- var ValveAPI = class extends import_koishi9.Service {
2726
+ var ValveAPI = class extends import_koishi11.Service {
2591
2727
  static {
2592
2728
  __name(this, "ValveAPI");
2593
2729
  }
2594
2730
  baseURL = "https://www.dota2.com/datafeed";
2731
+ queue;
2595
2732
  http;
2596
2733
  abortController = new AbortController();
2597
2734
  constructor(ctx) {
2598
2735
  super(ctx, "dota2tracker.valve-api", true);
2599
2736
  this.config = ctx.config;
2737
+ this.queue = new MiniQueue(ctx, { interval: 200 });
2600
2738
  this.http = import_axios3.default.create({ timeout: 15e3, signal: this.abortController.signal, baseURL: this.baseURL });
2601
2739
  ctx.on("dispose", () => this.dispose());
2602
2740
  }
2603
2741
  dispose() {
2742
+ this.queue.dispose();
2604
2743
  this.abortController.abort();
2605
2744
  }
2606
- // 7. 提取通用的 fetchData
2607
- async fetchData(path6, languageTag) {
2608
- const config = {
2609
- headers: {},
2610
- httpAgent: void 0,
2611
- httpsAgent: void 0,
2612
- params: {}
2613
- };
2614
- if (this.config.proxyAddress) {
2615
- config.httpsAgent = new import_https_proxy_agent2.HttpsProxyAgent(this.config.proxyAddress);
2616
- config.httpAgent = new import_https_proxy_agent2.HttpsProxyAgent(this.config.proxyAddress);
2617
- }
2618
- if (languageTag) {
2619
- config.params.language = this.ctx.dota2tracker.i18n.getValveLanguageTag(languageTag);
2620
- }
2621
- try {
2622
- const response = await this.http.get(path6, config);
2623
- return response.data;
2624
- } catch (error) {
2625
- processFetchError(error, this.name, path6);
2626
- }
2745
+ // 提取通用的 fetchData
2746
+ async fetchData(path7, languageTag) {
2747
+ return this.queue.add(async () => {
2748
+ const config = {
2749
+ headers: {},
2750
+ httpAgent: void 0,
2751
+ httpsAgent: void 0,
2752
+ params: {}
2753
+ };
2754
+ if (this.config.proxyAddress) {
2755
+ config.httpsAgent = new import_https_proxy_agent2.HttpsProxyAgent(this.config.proxyAddress);
2756
+ config.httpAgent = new import_https_proxy_agent2.HttpsProxyAgent(this.config.proxyAddress);
2757
+ }
2758
+ if (languageTag) {
2759
+ config.params.language = this.ctx.dota2tracker.i18n.getValveLanguageTag(languageTag);
2760
+ }
2761
+ try {
2762
+ const response = await this.http.get(path7, config);
2763
+ return response.data;
2764
+ } catch (error) {
2765
+ processFetchError(error, this.name, path7);
2766
+ }
2767
+ });
2627
2768
  }
2628
2769
  async queryHeroDetailsFromValve(heroId, languageTag = "zh-CN") {
2629
2770
  const data = await this.fetchData(`/herodata?hero_id=${heroId}`, languageTag);
@@ -2637,17 +2778,25 @@ var ValveAPI = class extends import_koishi9.Service {
2637
2778
  const data = await this.fetchData(`/itemdata?item_id=${itemId}`, languageTag);
2638
2779
  return data.result.data.items[0];
2639
2780
  }
2640
- async queryLastPatchNumber() {
2781
+ async queryPatchList() {
2641
2782
  const data = await this.fetchData("/patchnoteslist", void 0);
2642
- return data.patches.at(-1).patch_number;
2783
+ return data.patches;
2784
+ }
2785
+ async queryPatchNotes(version, languageTag = "zh-CN") {
2786
+ const data = await this.fetchData(`/patchnotes?version=${version}`, languageTag);
2787
+ return data;
2788
+ }
2789
+ async queryLastPatchNumber() {
2790
+ const patches = await this.queryPatchList();
2791
+ return patches.at(-1).patch_number;
2643
2792
  }
2644
2793
  };
2645
2794
 
2646
2795
  // src/app/presentation/view.renderer.ts
2647
- var import_koishi10 = require("koishi");
2796
+ var import_koishi12 = require("koishi");
2648
2797
  var import_ejs = __toESM(require("ejs"));
2649
- var import_node_fs2 = __toESM(require("node:fs"));
2650
- var import_node_path2 = __toESM(require("node:path"));
2798
+ var import_node_fs3 = __toESM(require("node:fs"));
2799
+ var import_node_path3 = __toESM(require("node:path"));
2651
2800
 
2652
2801
  // src/app/common/types.ts
2653
2802
  var ImageType = /* @__PURE__ */ ((ImageType2) => {
@@ -2669,7 +2818,7 @@ var ImageFormat = /* @__PURE__ */ ((ImageFormat2) => {
2669
2818
  // src/app/presentation/view.renderer.ts
2670
2819
  var import_luxon5 = require("luxon");
2671
2820
  var import_node_url = require("node:url");
2672
- var ViewRenderer = class extends import_koishi10.Service {
2821
+ var ViewRenderer = class extends import_koishi12.Service {
2673
2822
  static {
2674
2823
  __name(this, "ViewRenderer");
2675
2824
  }
@@ -2677,7 +2826,7 @@ var ViewRenderer = class extends import_koishi10.Service {
2677
2826
  constructor(ctx, currentDir) {
2678
2827
  super(ctx, "dota2tracker.image", true);
2679
2828
  this.config = ctx.config;
2680
- this.templateDir = import_node_path2.default.join(currentDir, "templates");
2829
+ this.templateDir = import_node_path3.default.join(currentDir, "templates");
2681
2830
  }
2682
2831
  async renderToImageByFile(data, templateName, type, languageTag) {
2683
2832
  const html = await this.generateHTML(data, { source: "FILE", templateName, type }, languageTag);
@@ -2736,13 +2885,13 @@ var ViewRenderer = class extends import_koishi10.Service {
2736
2885
  DateTime: import_luxon5.DateTime,
2737
2886
  $t: /* @__PURE__ */ __name((key, params) => this.ctx.dota2tracker.i18n.$t(languageTag, key, params), "$t"),
2738
2887
  languageTag,
2739
- Random: import_koishi10.Random,
2888
+ Random: import_koishi12.Random,
2740
2889
  getImageUrl: this.getImageUrl.bind(this)
2741
2890
  };
2742
2891
  try {
2743
2892
  let html;
2744
2893
  if (template.source === "FILE") {
2745
- const templatePath = import_node_path2.default.join(this.templateDir, template.type, `${template.templateName}.ejs`);
2894
+ const templatePath = import_node_path3.default.join(this.templateDir, template.type, `${template.templateName}.ejs`);
2746
2895
  html = await import_ejs.default.renderFile(templatePath, templateData, {
2747
2896
  strict: false
2748
2897
  });
@@ -2754,7 +2903,7 @@ var ViewRenderer = class extends import_koishi10.Service {
2754
2903
  }
2755
2904
  if (process.env.NODE_ENV === "development") {
2756
2905
  const debugFileName = template.source === "CODE" ? "code" : template.templateName;
2757
- import_node_fs2.default.writeFileSync(import_node_path2.default.resolve(process.cwd(), "temp", `${debugFileName}.html`), html);
2906
+ import_node_fs3.default.writeFileSync(import_node_path3.default.resolve(process.cwd(), "temp", `${debugFileName}.html`), html);
2758
2907
  }
2759
2908
  return html;
2760
2909
  } catch (error) {
@@ -2765,7 +2914,7 @@ var ViewRenderer = class extends import_koishi10.Service {
2765
2914
  getImageUrl(image, type = "local" /* Local */, format = "png" /* png */) {
2766
2915
  if (type === "local" /* Local */) {
2767
2916
  try {
2768
- const absolutePath = import_node_path2.default.join(this.templateDir, "images", `${image}.${format}`);
2917
+ const absolutePath = import_node_path3.default.join(this.templateDir, "images", `${image}.${format}`);
2769
2918
  return (0, import_node_url.pathToFileURL)(absolutePath).href;
2770
2919
  } catch (error) {
2771
2920
  console.error(error);
@@ -2808,9 +2957,9 @@ var ViewRenderer = class extends import_koishi10.Service {
2808
2957
  };
2809
2958
 
2810
2959
  // src/app/presentation/font.service.ts
2811
- var import_koishi11 = require("koishi");
2812
- var import_node_path3 = __toESM(require("node:path"));
2813
- var import_node_fs3 = __toESM(require("node:fs"));
2960
+ var import_koishi13 = require("koishi");
2961
+ var import_node_path4 = __toESM(require("node:path"));
2962
+ var import_node_fs4 = __toESM(require("node:fs"));
2814
2963
  var import_fontkit = __toESM(require("fontkit"));
2815
2964
  var FontFormats = {
2816
2965
  WEB_OPEN_FONT_FORMAT: "woff",
@@ -2822,7 +2971,7 @@ var FontFormats = {
2822
2971
  GOOGLE_FONT: "google",
2823
2972
  MANIFEST: "manifest"
2824
2973
  };
2825
- var FontService = class extends import_koishi11.Service {
2974
+ var FontService = class extends import_koishi13.Service {
2826
2975
  static {
2827
2976
  __name(this, "FontService");
2828
2977
  }
@@ -2835,10 +2984,10 @@ var FontService = class extends import_koishi11.Service {
2835
2984
  this.ctx.on("ready", async () => await this.initialize());
2836
2985
  }
2837
2986
  async initialize() {
2838
- const fontsPath = import_node_path3.default.resolve(this.ctx.baseDir, this.config.fontPath);
2839
- if (!import_node_fs3.default.existsSync(fontsPath)) {
2987
+ const fontsPath = import_node_path4.default.resolve(this.ctx.baseDir, this.config.fontPath);
2988
+ if (!import_node_fs4.default.existsSync(fontsPath)) {
2840
2989
  try {
2841
- import_node_fs3.default.mkdirSync(fontsPath, { recursive: true });
2990
+ import_node_fs4.default.mkdirSync(fontsPath, { recursive: true });
2842
2991
  } catch (e) {
2843
2992
  this.logger.warn(`Failed to create font directory: ${e.message}`);
2844
2993
  return;
@@ -2846,7 +2995,7 @@ var FontService = class extends import_koishi11.Service {
2846
2995
  }
2847
2996
  await this.loadFonts(fontsPath);
2848
2997
  try {
2849
- this.watcher = import_node_fs3.default.watch(fontsPath, (eventType, filename) => {
2998
+ this.watcher = import_node_fs4.default.watch(fontsPath, (eventType, filename) => {
2850
2999
  if (filename && /\.(ttf|otf|woff2?|ttc|sfnt)$/.test(filename)) {
2851
3000
  if (this.debounceTimer) clearTimeout(this.debounceTimer);
2852
3001
  this.debounceTimer = setTimeout(() => {
@@ -2864,12 +3013,12 @@ var FontService = class extends import_koishi11.Service {
2864
3013
  });
2865
3014
  }
2866
3015
  async loadFonts(fontsPath) {
2867
- if (!import_node_fs3.default.existsSync(fontsPath)) return;
3016
+ if (!import_node_fs4.default.existsSync(fontsPath)) return;
2868
3017
  const newFonts = [];
2869
3018
  try {
2870
- import_node_fs3.default.readdirSync(fontsPath).forEach((file) => {
3019
+ import_node_fs4.default.readdirSync(fontsPath).forEach((file) => {
2871
3020
  if (!/\.(ttf|otf|woff2?|ttc|sfnt)$/.test(file)) return;
2872
- const fullPath = import_node_path3.default.join(fontsPath, file);
3021
+ const fullPath = import_node_path4.default.join(fontsPath, file);
2873
3022
  try {
2874
3023
  const fontOrCollection = import_fontkit.default.openSync(fullPath);
2875
3024
  const parsedFonts = "fonts" in fontOrCollection ? fontOrCollection.fonts : [fontOrCollection];
@@ -2889,7 +3038,7 @@ var FontService = class extends import_koishi11.Service {
2889
3038
  });
2890
3039
  this.fonts = newFonts;
2891
3040
  const fontFamilies = Array.from(new Set(newFonts.map((f) => f.family))).sort();
2892
- this.ctx.schema.set("dota2tracker.fonts", import_koishi11.Schema.union(fontFamilies));
3041
+ this.ctx.schema.set("dota2tracker.fonts", import_koishi13.Schema.union(fontFamilies));
2893
3042
  this.logger.info(`Loaded ${newFonts.length} fonts from ${fontsPath}`);
2894
3043
  } catch (e) {
2895
3044
  this.ctx.logger.warn(`Failed to load fonts: ${e.message}`);
@@ -2950,7 +3099,7 @@ var FontService = class extends import_koishi11.Service {
2950
3099
  return descriptors;
2951
3100
  }
2952
3101
  getFileFormat(fileName) {
2953
- return import_node_path3.default.extname(fileName).slice(1);
3102
+ return import_node_path4.default.extname(fileName).slice(1);
2954
3103
  }
2955
3104
  getFontFormat(format) {
2956
3105
  const formatMap = {
@@ -2966,9 +3115,9 @@ var FontService = class extends import_koishi11.Service {
2966
3115
  };
2967
3116
 
2968
3117
  // src/app/presentation/message.builder.ts
2969
- var import_koishi12 = require("koishi");
3118
+ var import_koishi14 = require("koishi");
2970
3119
  var import_luxon6 = require("luxon");
2971
- var MessageBuilder = class extends import_koishi12.Service {
3120
+ var MessageBuilder = class extends import_koishi14.Service {
2972
3121
  static {
2973
3122
  __name(this, "MessageBuilder");
2974
3123
  }
@@ -3092,7 +3241,7 @@ var MessageBuilder = class extends import_koishi12.Service {
3092
3241
  const playerIds = players.map((player) => player.steamId);
3093
3242
  let broadPlayers = match.players.filter((item) => playerIds.includes(item.steamAccountId));
3094
3243
  for (let player of broadPlayers) {
3095
- const random = new import_koishi12.Random(() => enhancedSimpleHashToSeed(`${match.id}-${player.steamAccountId}-${player.playerSlot}`));
3244
+ const random = new import_koishi14.Random(() => enhancedSimpleHashToSeed(`${match.id}-${player.steamAccountId}-${player.playerSlot}`));
3096
3245
  let comment;
3097
3246
  if (player.isRadiant == match.didRadiantWin) {
3098
3247
  if (player.deathContribution < 0.2 || player.killContribution > 0.75 || player.heroDamage / player.networth > 1.5 || player.towerDamage > 1e4 || player.imp > 0)
@@ -3254,9 +3403,9 @@ function customConvertArrayOfString(str) {
3254
3403
  __name(customConvertArrayOfString, "customConvertArrayOfString");
3255
3404
 
3256
3405
  // src/app/tasks/match-watcher.task.ts
3257
- var import_koishi13 = require("koishi");
3406
+ var import_koishi15 = require("koishi");
3258
3407
  var import_luxon7 = require("luxon");
3259
- var MatchWatcherTask = class extends import_koishi13.Service {
3408
+ var MatchWatcherTask = class extends import_koishi15.Service {
3260
3409
  static {
3261
3410
  __name(this, "MatchWatcherTask");
3262
3411
  }
@@ -3385,9 +3534,9 @@ var MatchWatcherTask = class extends import_koishi13.Service {
3385
3534
  };
3386
3535
 
3387
3536
  // src/app/tasks/parse-polling.task.ts
3388
- var import_koishi14 = require("koishi");
3537
+ var import_koishi16 = require("koishi");
3389
3538
  var import_luxon8 = require("luxon");
3390
- var ParsePollingTask = class extends import_koishi14.Service {
3539
+ var ParsePollingTask = class extends import_koishi16.Service {
3391
3540
  static {
3392
3541
  __name(this, "ParsePollingTask");
3393
3542
  }
@@ -3521,9 +3670,9 @@ var ParsePollingTask = class extends import_koishi14.Service {
3521
3670
  };
3522
3671
 
3523
3672
  // src/app/tasks/daily-report.task.ts
3524
- var import_koishi15 = require("koishi");
3673
+ var import_koishi17 = require("koishi");
3525
3674
  var import_luxon9 = require("luxon");
3526
- var DailyReportTask = class extends import_koishi15.Service {
3675
+ var DailyReportTask = class extends import_koishi17.Service {
3527
3676
  static {
3528
3677
  __name(this, "DailyReportTask");
3529
3678
  }
@@ -3695,6 +3844,8 @@ var TaskMessenger = class {
3695
3844
  this.session = session;
3696
3845
  this.options = options;
3697
3846
  }
3847
+ session;
3848
+ options;
3698
3849
  static {
3699
3850
  __name(this, "TaskMessenger");
3700
3851
  }
@@ -3775,7 +3926,9 @@ function registerQueryHeroCommand(ctx) {
3775
3926
  if (input_data || options.random) {
3776
3927
  await task.send(session.text(".querying_hero"));
3777
3928
  const languageTag = await ctx.dota2tracker.i18n.getLanguageTag({ session });
3778
- const heroData = await ctx.dota2tracker.hero.getHeroDetails(input_data, languageTag, options.random);
3929
+ const heroData = await ctx.dota2tracker.hero.getHeroDetails(input_data, languageTag, options.random, () => {
3930
+ task.send(session.text(".fetching_patch_notes"));
3931
+ });
3779
3932
  if (!heroData) {
3780
3933
  await task.finish();
3781
3934
  return session.text(".not_found");
@@ -4022,72 +4175,77 @@ function registerUserCommand(ctx) {
4022
4175
  __name(registerUserCommand, "registerUserCommand");
4023
4176
 
4024
4177
  // src/app/data/opendota.api.ts
4025
- var import_koishi16 = require("koishi");
4178
+ var import_koishi18 = require("koishi");
4026
4179
  var import_axios4 = __toESM(require("axios"));
4027
4180
  var import_https_proxy_agent3 = require("https-proxy-agent");
4028
4181
  var import_node_http = require("node:http");
4029
4182
  var import_node_https = require("node:https");
4030
- var OpenDotaAPI = class extends import_koishi16.Service {
4183
+ var OpenDotaAPI = class extends import_koishi18.Service {
4031
4184
  static {
4032
4185
  __name(this, "OpenDotaAPI");
4033
4186
  }
4034
4187
  BASE_URL = "https://api.opendota.com/api";
4188
+ queue;
4035
4189
  http;
4036
4190
  abortController = new AbortController();
4037
4191
  constructor(ctx) {
4038
4192
  super(ctx, "dota2tracker.opendota-api", true);
4039
4193
  this.config = ctx.config;
4194
+ this.queue = new MiniQueue(ctx, { interval: 200 });
4040
4195
  this.http = import_axios4.default.create({ timeout: 15e3, signal: this.abortController.signal, baseURL: this.BASE_URL });
4041
4196
  ctx.on("dispose", () => this.dispose());
4042
4197
  }
4043
4198
  dispose() {
4199
+ this.queue.dispose();
4044
4200
  this.abortController.abort();
4045
4201
  }
4046
4202
  async queryMatchInfo(matchId) {
4047
- const path6 = `/matches/${matchId}`;
4048
- const data = await this.fetchData("GET", path6);
4049
- this.ctx.dota2tracker.cache.addOpendotaAPIRequestLog(path6, 1);
4203
+ const path7 = `/matches/${matchId}`;
4204
+ const data = await this.fetchData("GET", path7);
4205
+ this.ctx.dota2tracker.cache.addOpendotaAPIRequestLog(path7, 1);
4050
4206
  return data;
4051
4207
  }
4052
4208
  async requestParseMatch(matchId) {
4053
- const path6 = `/request/${matchId}`;
4054
- const job = await this.fetchData("POST", path6);
4055
- this.ctx.dota2tracker.cache.addOpendotaAPIRequestLog(path6, 10);
4209
+ const path7 = `/request/${matchId}`;
4210
+ const job = await this.fetchData("POST", path7);
4211
+ this.ctx.dota2tracker.cache.addOpendotaAPIRequestLog(path7, 10);
4056
4212
  return job;
4057
4213
  }
4058
- async fetchData(type, path6, data) {
4059
- const config = {
4060
- headers: {},
4061
- httpAgent: void 0,
4062
- httpsAgent: void 0
4063
- };
4064
- if (this.config.proxyAddress) {
4065
- config.httpsAgent = new import_https_proxy_agent3.HttpsProxyAgent(this.config.proxyAddress);
4066
- config.httpAgent = new import_https_proxy_agent3.HttpsProxyAgent(this.config.proxyAddress);
4067
- } else if (this.config.OpenDotaIPStack === "ipv4") {
4068
- config.httpAgent = new import_node_http.Agent({ family: 4 });
4069
- config.httpsAgent = new import_node_https.Agent({ family: 4 });
4070
- }
4071
- if (this.config.OPENDOTA_API_KEY) {
4072
- config.headers["Authorization"] = `Bearer ${this.config.OPENDOTA_API_KEY}`;
4073
- }
4074
- try {
4075
- let response;
4076
- if (type === "GET") {
4077
- response = await this.http.get(path6, config);
4078
- } else {
4079
- response = await this.http.post(path6, data, config);
4214
+ async fetchData(type, path7, data) {
4215
+ return this.queue.add(async () => {
4216
+ const config = {
4217
+ headers: {},
4218
+ httpAgent: void 0,
4219
+ httpsAgent: void 0
4220
+ };
4221
+ if (this.config.proxyAddress) {
4222
+ config.httpsAgent = new import_https_proxy_agent3.HttpsProxyAgent(this.config.proxyAddress);
4223
+ config.httpAgent = new import_https_proxy_agent3.HttpsProxyAgent(this.config.proxyAddress);
4224
+ } else if (this.config.OpenDotaIPStack === "ipv4") {
4225
+ config.httpAgent = new import_node_http.Agent({ family: 4 });
4226
+ config.httpsAgent = new import_node_https.Agent({ family: 4 });
4080
4227
  }
4081
- return response.data;
4082
- } catch (error) {
4083
- processFetchError(error, "OpenDota", path6);
4084
- }
4228
+ if (this.config.OPENDOTA_API_KEY) {
4229
+ config.headers["Authorization"] = `Bearer ${this.config.OPENDOTA_API_KEY}`;
4230
+ }
4231
+ try {
4232
+ let response;
4233
+ if (type === "GET") {
4234
+ response = await this.http.get(path7, config);
4235
+ } else {
4236
+ response = await this.http.post(path7, data, config);
4237
+ }
4238
+ return response.data;
4239
+ } catch (error) {
4240
+ processFetchError(error, "OpenDota", path7);
4241
+ }
4242
+ });
4085
4243
  }
4086
4244
  };
4087
4245
 
4088
4246
  // src/app/core/opendota.adapter.ts
4089
- var import_koishi17 = require("koishi");
4090
- var OpenDotaAdapter = class extends import_koishi17.Service {
4247
+ var import_koishi19 = require("koishi");
4248
+ var OpenDotaAdapter = class extends import_koishi19.Service {
4091
4249
  static {
4092
4250
  __name(this, "OpenDotaAdapter");
4093
4251
  }
@@ -4542,9 +4700,9 @@ function convertBuildingEvents(objectives) {
4542
4700
  __name(convertBuildingEvents, "convertBuildingEvents");
4543
4701
 
4544
4702
  // src/app/core/daily-report.service.ts
4545
- var import_koishi18 = require("koishi");
4703
+ var import_koishi20 = require("koishi");
4546
4704
  var import_luxon11 = require("luxon");
4547
- var DailyReportService = class _DailyReportService extends import_koishi18.Service {
4705
+ var DailyReportService = class _DailyReportService extends import_koishi20.Service {
4548
4706
  static {
4549
4707
  __name(this, "DailyReportService");
4550
4708
  }
@@ -4845,9 +5003,9 @@ var DailyReportService = class _DailyReportService extends import_koishi18.Servi
4845
5003
  };
4846
5004
 
4847
5005
  // src/config.ts
4848
- var import_koishi19 = require("koishi");
4849
- var import_node_fs4 = __toESM(require("node:fs"));
4850
- var import_node_path4 = __toESM(require("node:path"));
5006
+ var import_koishi21 = require("koishi");
5007
+ var import_node_fs5 = __toESM(require("node:fs"));
5008
+ var import_node_path5 = __toESM(require("node:path"));
4851
5009
 
4852
5010
  // require("./locales/**/*.schema.yml") in src/config.ts
4853
5011
  var globRequire_locales_schema_yml = __glob({
@@ -4856,87 +5014,87 @@ var globRequire_locales_schema_yml = __glob({
4856
5014
  });
4857
5015
 
4858
5016
  // src/config.ts
4859
- var templateDir = import_node_path4.default.join(__dirname, "templates");
5017
+ var templateDir = import_node_path5.default.join(__dirname, "templates");
4860
5018
  var allI18nConfigs = Object.fromEntries(Object.keys(LanguageTags).map((lang) => [lang, globRequire_locales_schema_yml(`./locales/${lang}.schema.yml`)._config]));
4861
- var Config = import_koishi19.Schema.intersect([
4862
- import_koishi19.Schema.intersect([
4863
- import_koishi19.Schema.object({
4864
- STRATZ_API_TOKEN: import_koishi19.Schema.string().required().role("secret"),
4865
- dataParsingTimeoutMinutes: import_koishi19.Schema.number().default(60).min(0).max(1440),
4866
- proxyAddress: import_koishi19.Schema.string(),
4867
- suppressApiNetworkErrors: import_koishi19.Schema.boolean().default(false),
4868
- enableOpenDotaFallback: import_koishi19.Schema.boolean().default(false)
5019
+ var Config = import_koishi21.Schema.intersect([
5020
+ import_koishi21.Schema.intersect([
5021
+ import_koishi21.Schema.object({
5022
+ STRATZ_API_TOKEN: import_koishi21.Schema.string().required().role("secret"),
5023
+ dataParsingTimeoutMinutes: import_koishi21.Schema.number().default(60).min(0).max(1440),
5024
+ proxyAddress: import_koishi21.Schema.string(),
5025
+ suppressApiNetworkErrors: import_koishi21.Schema.boolean().default(false),
5026
+ enableOpenDotaFallback: import_koishi21.Schema.boolean().default(false)
4869
5027
  }).i18n(getI18n("base")),
4870
- import_koishi19.Schema.union([
4871
- import_koishi19.Schema.object({
4872
- enableOpenDotaFallback: import_koishi19.Schema.const(true).required(),
4873
- OPENDOTA_API_KEY: import_koishi19.Schema.string().role("secret"),
4874
- OpenDotaIPStack: import_koishi19.Schema.union(["auto", "ipv4"]).default("auto")
5028
+ import_koishi21.Schema.union([
5029
+ import_koishi21.Schema.object({
5030
+ enableOpenDotaFallback: import_koishi21.Schema.const(true).required(),
5031
+ OPENDOTA_API_KEY: import_koishi21.Schema.string().role("secret"),
5032
+ OpenDotaIPStack: import_koishi21.Schema.union(["auto", "ipv4"]).default("auto")
4875
5033
  }),
4876
- import_koishi19.Schema.object({})
5034
+ import_koishi21.Schema.object({})
4877
5035
  ]).i18n(getI18n("base"))
4878
5036
  ]),
4879
- import_koishi19.Schema.intersect([
4880
- import_koishi19.Schema.object({
4881
- useHeroNicknames: import_koishi19.Schema.boolean().default(true),
4882
- urlInMessageType: import_koishi19.Schema.array(import_koishi19.Schema.union([import_koishi19.Schema.const("match"), import_koishi19.Schema.const("player"), import_koishi19.Schema.const("hero")])).role("checkbox"),
4883
- maxSendItemCount: import_koishi19.Schema.number().default(5).min(1).max(10),
4884
- showItemListAtTooMuchItems: import_koishi19.Schema.boolean().default(true),
4885
- customItemAlias: import_koishi19.Schema.array(
4886
- import_koishi19.Schema.object({
4887
- keyword: import_koishi19.Schema.string().required(),
4888
- alias: import_koishi19.Schema.string().required()
5037
+ import_koishi21.Schema.intersect([
5038
+ import_koishi21.Schema.object({
5039
+ useHeroNicknames: import_koishi21.Schema.boolean().default(true),
5040
+ urlInMessageType: import_koishi21.Schema.array(import_koishi21.Schema.union([import_koishi21.Schema.const("match"), import_koishi21.Schema.const("player"), import_koishi21.Schema.const("hero")])).role("checkbox"),
5041
+ maxSendItemCount: import_koishi21.Schema.number().default(5).min(1).max(10),
5042
+ showItemListAtTooMuchItems: import_koishi21.Schema.boolean().default(true),
5043
+ customItemAlias: import_koishi21.Schema.array(
5044
+ import_koishi21.Schema.object({
5045
+ keyword: import_koishi21.Schema.string().required(),
5046
+ alias: import_koishi21.Schema.string().required()
4889
5047
  })
4890
5048
  ).default([]).role("table"),
4891
- autoRecallTips: import_koishi19.Schema.boolean().default(true),
4892
- rankBroadSwitch: import_koishi19.Schema.boolean().default(false)
5049
+ autoRecallTips: import_koishi21.Schema.boolean().default(true),
5050
+ rankBroadSwitch: import_koishi21.Schema.boolean().default(false)
4893
5051
  }).i18n(getI18n("message")),
4894
- import_koishi19.Schema.union([
4895
- import_koishi19.Schema.object({
4896
- rankBroadSwitch: import_koishi19.Schema.const(true).required(),
4897
- rankBroadStar: import_koishi19.Schema.boolean().default(true),
4898
- rankBroadLeader: import_koishi19.Schema.boolean().default(true),
4899
- rankBroadFun: import_koishi19.Schema.boolean().default(false)
5052
+ import_koishi21.Schema.union([
5053
+ import_koishi21.Schema.object({
5054
+ rankBroadSwitch: import_koishi21.Schema.const(true).required(),
5055
+ rankBroadStar: import_koishi21.Schema.boolean().default(true),
5056
+ rankBroadLeader: import_koishi21.Schema.boolean().default(true),
5057
+ rankBroadFun: import_koishi21.Schema.boolean().default(false)
4900
5058
  }),
4901
- import_koishi19.Schema.object({})
5059
+ import_koishi21.Schema.object({})
4902
5060
  ]).i18n(getI18n("message"))
4903
5061
  ]),
4904
- import_koishi19.Schema.intersect([
4905
- import_koishi19.Schema.object({
4906
- dailyReportSwitch: import_koishi19.Schema.boolean().default(false)
5062
+ import_koishi21.Schema.intersect([
5063
+ import_koishi21.Schema.object({
5064
+ dailyReportSwitch: import_koishi21.Schema.boolean().default(false)
4907
5065
  }).i18n(getI18n("report")),
4908
- import_koishi19.Schema.union([
4909
- import_koishi19.Schema.object({
4910
- dailyReportSwitch: import_koishi19.Schema.const(true).required(),
4911
- dailyReportHours: import_koishi19.Schema.number().min(0).max(23).default(6),
4912
- dailyReportShowCombi: import_koishi19.Schema.boolean().default(true).deprecated()
5066
+ import_koishi21.Schema.union([
5067
+ import_koishi21.Schema.object({
5068
+ dailyReportSwitch: import_koishi21.Schema.const(true).required(),
5069
+ dailyReportHours: import_koishi21.Schema.number().min(0).max(23).default(6),
5070
+ dailyReportShowCombi: import_koishi21.Schema.boolean().default(true).deprecated()
4913
5071
  }),
4914
- import_koishi19.Schema.object({})
5072
+ import_koishi21.Schema.object({})
4915
5073
  ]).i18n(getI18n("report")),
4916
- import_koishi19.Schema.object({
4917
- weeklyReportSwitch: import_koishi19.Schema.boolean().default(false)
5074
+ import_koishi21.Schema.object({
5075
+ weeklyReportSwitch: import_koishi21.Schema.boolean().default(false)
4918
5076
  }).i18n(getI18n("report")).description(void 0),
4919
- import_koishi19.Schema.union([
4920
- import_koishi19.Schema.object({
4921
- weeklyReportSwitch: import_koishi19.Schema.const(true).required(),
4922
- weeklyReportDayHours: import_koishi19.Schema.tuple([import_koishi19.Schema.number().min(1).max(7), import_koishi19.Schema.number().min(0).max(23)]).default([1, 10]),
4923
- weeklyReportShowCombi: import_koishi19.Schema.boolean().default(true)
5077
+ import_koishi21.Schema.union([
5078
+ import_koishi21.Schema.object({
5079
+ weeklyReportSwitch: import_koishi21.Schema.const(true).required(),
5080
+ weeklyReportDayHours: import_koishi21.Schema.tuple([import_koishi21.Schema.number().min(1).max(7), import_koishi21.Schema.number().min(0).max(23)]).default([1, 10]),
5081
+ weeklyReportShowCombi: import_koishi21.Schema.boolean().default(true)
4924
5082
  }),
4925
- import_koishi19.Schema.object({})
5083
+ import_koishi21.Schema.object({})
4926
5084
  ]).i18n(getI18n("report"))
4927
5085
  ]),
4928
- import_koishi19.Schema.object({
4929
- template_match: import_koishi19.Schema.union([...readDirectoryFilesSync(import_node_path4.default.join(templateDir, "match"))]).default("match_1"),
4930
- template_player: import_koishi19.Schema.union([...readDirectoryFilesSync(import_node_path4.default.join(templateDir, "player"))]).default("player_1"),
4931
- template_hero: import_koishi19.Schema.union([...readDirectoryFilesSync(import_node_path4.default.join(templateDir, "hero"))]).default("hero_1"),
4932
- playerRankEstimate: import_koishi19.Schema.boolean().default(true),
4933
- templateFonts: import_koishi19.Schema.array(String).default([]).role("table").deprecated(),
4934
- fontPath: import_koishi19.Schema.path({ filters: ["directory"] }).default("data/fonts/dota2tracker"),
4935
- fonts: import_koishi19.Schema.object({
4936
- description: import_koishi19.Schema.never(),
4937
- sans: import_koishi19.Schema.array(import_koishi19.Schema.dynamic("dota2tracker.fonts")).collapse(),
4938
- serif: import_koishi19.Schema.array(import_koishi19.Schema.dynamic("dota2tracker.fonts")).collapse(),
4939
- mono: import_koishi19.Schema.array(import_koishi19.Schema.dynamic("dota2tracker.fonts")).collapse()
5086
+ import_koishi21.Schema.object({
5087
+ template_match: import_koishi21.Schema.union([...readDirectoryFilesSync(import_node_path5.default.join(templateDir, "match"))]).default("match_1"),
5088
+ template_player: import_koishi21.Schema.union([...readDirectoryFilesSync(import_node_path5.default.join(templateDir, "player"))]).default("player_1"),
5089
+ template_hero: import_koishi21.Schema.union([...readDirectoryFilesSync(import_node_path5.default.join(templateDir, "hero"))]).default("hero_1"),
5090
+ playerRankEstimate: import_koishi21.Schema.boolean().default(true),
5091
+ heroPatchNotesRetrievalDepth: import_koishi21.Schema.number().min(1).max(5).default(2).description(""),
5092
+ fontPath: import_koishi21.Schema.path({ filters: ["directory"] }).default("data/dota2tracker/fonts"),
5093
+ fonts: import_koishi21.Schema.object({
5094
+ description: import_koishi21.Schema.never(),
5095
+ sans: import_koishi21.Schema.array(import_koishi21.Schema.dynamic("dota2tracker.fonts")).collapse(),
5096
+ serif: import_koishi21.Schema.array(import_koishi21.Schema.dynamic("dota2tracker.fonts")).collapse(),
5097
+ mono: import_koishi21.Schema.array(import_koishi21.Schema.dynamic("dota2tracker.fonts")).collapse()
4940
5098
  })
4941
5099
  }).i18n(getI18n("template"))
4942
5100
  ]);
@@ -4952,8 +5110,8 @@ function getI18n(key) {
4952
5110
  __name(getI18n, "getI18n");
4953
5111
  function readDirectoryFilesSync(directoryPath) {
4954
5112
  try {
4955
- const files = import_node_fs4.default.readdirSync(directoryPath);
4956
- const fileNames = files.filter((file) => import_node_path4.default.extname(file).toLowerCase() === ".ejs").map((file) => import_node_path4.default.basename(file, ".ejs"));
5113
+ const files = import_node_fs5.default.readdirSync(directoryPath);
5114
+ const fileNames = files.filter((file) => import_node_path5.default.extname(file).toLowerCase() === ".ejs").map((file) => import_node_path5.default.basename(file, ".ejs"));
4957
5115
  return fileNames;
4958
5116
  } catch (error) {
4959
5117
  console.error("Error reading directory:", error);
@@ -4973,8 +5131,8 @@ async function apply(ctx, config) {
4973
5131
  const lib = await import("dotaconstants");
4974
5132
  const dotaconstants = lib.default || lib;
4975
5133
  const logger = ctx.logger("dota2tracker");
4976
- const currentDir = import_node_path5.default.resolve(__dirname);
4977
- const pluginVersion = require(import_node_path5.default.join(currentDir, "..", "package.json")).version;
5134
+ const currentDir = import_node_path6.default.resolve(__dirname);
5135
+ const pluginVersion = require(import_node_path6.default.join(currentDir, "..", "package.json")).version;
4978
5136
  ctx.dota2tracker = {};
4979
5137
  ctx.dota2tracker.dotaconstants = dotaconstants;
4980
5138
  ctx.dota2tracker.i18n = new I18NService(ctx);
@@ -4998,8 +5156,10 @@ async function apply(ctx, config) {
4998
5156
  ctx.dota2tracker.dailyReport = new DailyReportService(ctx);
4999
5157
  ctx.dota2tracker.cache = new CacheService(ctx);
5000
5158
  ctx.dota2tracker.database = new DatabaseService(ctx);
5159
+ ctx.dota2tracker.file = new FileService(ctx);
5001
5160
  ctx.dota2tracker.valveAPI = new ValveAPI(ctx);
5002
5161
  ctx.dota2tracker.stratzAPI = new StratzAPI(ctx, currentDir);
5162
+ ctx.dota2tracker.staticData = new StaticDataService(ctx);
5003
5163
  if (config.enableOpenDotaFallback) {
5004
5164
  ctx.dota2tracker.opendotaAPI = new OpenDotaAPI(ctx);
5005
5165
  ctx.dota2tracker.opendotaAdapter = new OpenDotaAdapter(ctx);