@sjtdev/koishi-plugin-dota2tracker 2.2.1 → 2.2.3

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
@@ -773,21 +773,21 @@ var require_en_US_command = __commonJS({
773
773
  // src/locales/en-US.schema.yml
774
774
  var require_en_US_schema = __commonJS({
775
775
  "src/locales/en-US.schema.yml"(exports2, module2) {
776
- 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 to disable the proxy.", suppressStratzNetworkErrors: "When enabled, downgrades Stratz network errors (e.g. timeouts) to lower-priority output. Debug-level logs are hidden by default. To enable debug logging, see [📖 Documentation: Configuration](http://sjtdev.github.io/koishi-plugin-dota2tracker/en-US/configs.html#suppressstratznetworkerrors-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." }, 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" } }, 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 to generate match information images, see [📖 Template Display](https://sjtdev.github.io/koishi-plugin-dota2tracker/template-match.html) for template display.", template_player: "Template used to generate player information images. (Currently only one template available)", template_hero: "Template used to generate hero information images. (Currently only one template available)", playerRankEstimate: "Estimate the rank of players without a rank in the player template <br>Estimated rank will be displayed as a gray image", templateFonts: 'Font names used in the template. Requires font files installed on the koishi host machine. \nMultiple fonts can be added; the system will use the first available font from top to bottom. \nIf all fonts are unavailable, falls back to system defaults. \nImportant formatting rules: \n- Enclose font names in quotes (" ") if they contain spaces or special characters (recommended for all font names)\n- Do NOT enclose generic font family names (e.g. sans-serif, monospace) in quotes\nExamples:\n```\n"Microsoft YaHei"\nsans-serif\n```\nFor details on font-family syntax, see:\n[📖 MDN: font-family](https://developer.mozilla.org/en-US/docs/Web/CSS/font-family)' } } };
776
+ 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](http://sjtdev.github.io/koishi-plugin-dota2tracker/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" } }, 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 to generate match information images, see [📖 Template Display](https://sjtdev.github.io/koishi-plugin-dota2tracker/template-match.html) for template display.", template_player: "Template used to generate player information images. (Currently only one template available)", template_hero: "Template used to generate hero information images. (Currently only one template available)", playerRankEstimate: "Estimate the rank of players without a rank in the player template <br>Estimated rank will be displayed as a gray image", templateFonts: 'Font names used in the template. Requires font files installed on the koishi host machine. \nMultiple fonts can be added; the system will use the first available font from top to bottom. \nIf all fonts are unavailable, falls back to system defaults. \nImportant formatting rules: \n- Enclose font names in quotes (" ") if they contain spaces or special characters (recommended for all font names)\n- Do NOT enclose generic font family names (e.g. sans-serif, monospace) in quotes\nExamples:\n```\n"Microsoft YaHei"\nsans-serif\n```\nFor details on font-family syntax, see:\n[📖 MDN: font-family](https://developer.mozilla.org/en-US/docs/Web/CSS/font-family)' } } };
777
777
  }
778
778
  });
779
779
 
780
780
  // src/locales/en-US.template.yml
781
781
  var require_en_US_template = __commonJS({
782
782
  "src/locales/en-US.template.yml"(exports2, module2) {
783
- 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: "R-#FFD700", Wise: "W-#8888FF", Controller: "C-#FF00FF", Nuker: "N-#CC0088", Breaker: "B-#DD0000", Ghost: "G-#CCCCCC", Utility: "U-#20B2AA", Assister: "A-#006400", Demolisher: "D-#FEDCBA", Healer: "H-#00FF00", Tank: "T-#84A1C7", Idle: "I-#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" } } };
783
+ 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: "R-#FFD700", Wise: "W-#8888FF", Controller: "C-#FF00FF", Nuker: "N-#CC0088", Breaker: "B-#DD0000", Ghost: "G-#CCCCCC", Utility: "U-#20B2AA", Assister: "A-#006400", Demolisher: "D-#FEDCBA", Healer: "H-#00FF00", Tank: "T-#84A1C7", Idle: "I-#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" } } } };
784
784
  }
785
785
  });
786
786
 
787
787
  // src/locales/en-US.yml
788
788
  var require_en_US = __commonJS({
789
789
  "src/locales/en-US.yml"(exports2, module2) {
790
- module2.exports = { dota2tracker: { heroes_nicknames: { "0": 'Please strictly follow the format of "", "" when filling out, if the format is incorrect, only the default name of the hero will be used. The default name of the hero may be omitted.', "1": '"Anti-Mage"', "2": '"Axe"', "3": '"Bane"', "4": '"Bloodseeker"', "5": '"Crystal Maiden"', "6": '"Drow Ranger"', "7": '"Earthshaker"', "8": '"Juggernaut"', "9": '"Mirana"', "10": '"Morphling"', "11": '"Shadow Fiend"', "12": '"Phantom Lancer"', "13": '"Puck"', "14": '"Pudge"', "15": '"Razor"', "16": '"Sand King"', "17": '"Storm Spirit"', "18": '"Sven"', "19": '"Tiny"', "20": '"Vengeful Spirit"', "21": '"Windranger"', "22": '"Zeus"', "23": '"Kunkka"', "25": '"Lina"', "26": '"Lion"', "27": '"Shadow Shaman"', "28": '"Slardar"', "29": '"Tidehunter"', "30": '"Witch Doctor"', "31": '"Lich"', "32": '"Riki"', "33": '"Enigma"', "34": '"Tinker"', "35": '"Sniper"', "36": '"Necrophos"', "37": '"Warlock"', "38": '"Beastmaster"', "39": '"Queen of Pain"', "40": '"Venomancer"', "41": '"Faceless Void"', "42": '"Wraith King"', "43": '"Death Prophet"', "44": '"Phantom Assassin"', "45": '"Pugna"', "46": '"Templar Assassin"', "47": '"Viper"', "48": '"Luna"', "49": '"Dragon Knight"', "50": '"Dazzle"', "51": '"Clockwerk"', "52": '"Leshrac"', "53": `"Nature's Prophet"`, "54": '"Lifestealer"', "55": '"Dark Seer"', "56": '"Clinkz"', "57": '"Omniknight"', "58": '"Enchantress"', "59": '"Huskar"', "60": '"Night Stalker"', "61": '"Broodmother"', "62": '"Bounty Hunter"', "63": '"Weaver"', "64": '"Jakiro"', "65": '"Batrider"', "66": '"Chen"', "67": '"Spectre"', "68": '"Ancient Apparition"', "69": '"Doom"', "70": '"Ursa"', "71": '"Spirit Breaker"', "72": '"Gyrocopter"', "73": '"Alchemist"', "74": '"Invoker"', "75": '"Silencer"', "76": '"Outworld Devourer"', "77": '"Lycan"', "78": '"Brewmaster"', "79": '"Shadow Demon"', "80": '"Lone Druid"', "81": '"Chaos Knight"', "82": '"Meepo"', "83": '"Treant Protector"', "84": '"Ogre Magi"', "85": '"Undying"', "86": '"Rubick"', "87": '"Disruptor"', "88": '"Nyx Assassin"', "89": '"Naga Siren"', "90": '"Keeper of the Light"', "91": '"Io"', "92": '"Visage"', "93": '"Slark"', "94": '"Medusa"', "95": '"Troll Warlord"', "96": '"Centaur Warrunner"', "97": '"Magnus"', "98": '"Timbersaw"', "99": '"Bristleback"', "100": '"Tusk"', "101": '"Skywrath Mage"', "102": '"Abaddon"', "103": '"Elder Titan"', "104": '"Legion Commander"', "105": '"Techies"', "106": '"Ember Spirit"', "107": '"Earth Spirit"', "108": '"Underlord"', "109": '"Terrorblade"', "110": '"Phoenix"', "111": '"Oracle"', "112": '"Winter Wyvern"', "113": '"Arc Warden"', "114": '"Monkey King"', "119": '"Dark Willow"', "120": '"Pangolier"', "121": '"Grimstroke"', "123": '"Hoodwink"', "126": '"Void Spirit"', "128": '"Snapfire"', "129": '"Mars"', "131": '"Ring Master"', "135": '"Dawnbreaker"', "136": '"Marci"', "137": '"Primal Beast"', "138": '"Muerta"', "145": '"Kez"' }, broadcast: { WIN_NEGATIVE: `"Won the match by sheer luck", "Won the match by a stroke of bad luck", "Coasted to victory", "Didn't even show up for the team fight, but my teammates won 4v5"`, WIN_POSITIVE: '"Led the team to victory", "Dominated the opponents and secured the win", "Carried the game to victory", "Treated the opponents like pigs and won", "Won again; this game is just so monotonous and dull", "Simply achieved a win in the match"', LOSE_NEGATIVE: '"Got crushed and lost the match", "Lost the match miserably", "Got my head knocked sideways and lost the match with a blown mindset", "Went fishing but got eaten by the fish, lost the match", "Played terribly", "Simply suffered a loss in the match"', LOSE_POSITIVE: `"Lost the match with no way to turn it around", "Gave it my all, but still lost the match", "Though we lost, we still have honor", "Couldn't carry my teammates, lost the match", "Lost again, it hurts; I'd rather it be me losing"`, message: "{name}'s {hero_name} {comment}.\nKDA: {kda}, GPM/XPM: {gpm_xpm}, Last Hits/Denies: {lh_dn}, Damage/Tower Damage: {damage}, Kill/Death Contribution Rate: {kc_dc}", rank_changed: "Player {name} rank changed: {prev.medal} {prev.star} → {curr.medal} {curr.star}" }, logger: { fetch_guilds_failed: "Failed to fetch guild information.", match_tracked: "Tracked new match {match.id} from {#each messageToLogger as item}users in guild {item.platform}:{item.guildId} [{#each item.players as player}{player.nickname}({player.steamId}){#if player !== item.players[item.players.length - 1]}, {/if}{/each}]{#if item !== messageToLogger[messageToLogger.length - 1]}, {/if}{/each}.", parse_request_sent: "The parsing request for match {matchId} has been successfully sent to the STRATZ server.", parse_request_failed: "The parsing request for match {matchId} failed to send.", match_parsed: "Match {matchId} has been parsed{#if odParsed} by OpenDota{/if}, an image was generated and published to {#each guilds as guild}{guild.platform}:{guild.guildId}{#if guild !== guilds[guilds.length - 1]}, {/if}{/each}.", match_unparsed: "Match {matchId} exceeded the waiting time [{timeout}] and remains unparsed, an image was generated and published to {#each guilds as guild}{guild.platform}:{guild.guildId}{#if guild !== guilds[guilds.length - 1]}, {/if}{/each}.", waiting_for_parse: "Match {matchId} has not been parsed yet, has been waiting for {time} minutes.", report_sent: "Posted {title} on {platform}:{guildId}.", rank_sent: "Posted rank change information of {player.nickName} ({player.steamId}) on {platform}:{guildId}.", ejs_error: "Error rendering EJS template: {error}", cron_not_enabled: "Cron service is not enabled; match report tracking cannot run.", stratz_token_banned: "Stratz API request denied (403). This usually means your token or IP has been temporarily restricted due to unusual activity. For details, see the documentation: http://sjtdev.github.io/koishi-plugin-dota2tracker/en-US/api-403.html", stratz_api_query_error: "Stratz API returned partial data with errors: {cause}", opendota_parse_request_on_later: "OpenDota fallback is enabled. A parse request will be sent to OpenDota every {timeout} minutes, and the status will be checked every minute.", opendota_parse_request_sent: "Parse request for match {matchId} has been sent to OpenDota servers successfully.", opendota_parse_request_failed: "Failed to send parse request for match {matchId} to OpenDota servers." }, time: { years_months_ago: "{years} years and {months} months ago", years_ago: "{years} years ago" } } };
790
+ module2.exports = { dota2tracker: { heroes_nicknames: { "0": 'Please strictly follow the format of "", "" when filling out, if the format is incorrect, only the default name of the hero will be used. The default name of the hero may be omitted.', "1": '"Anti-Mage"', "2": '"Axe"', "3": '"Bane"', "4": '"Bloodseeker"', "5": '"Crystal Maiden"', "6": '"Drow Ranger"', "7": '"Earthshaker"', "8": '"Juggernaut"', "9": '"Mirana"', "10": '"Morphling"', "11": '"Shadow Fiend"', "12": '"Phantom Lancer"', "13": '"Puck"', "14": '"Pudge"', "15": '"Razor"', "16": '"Sand King"', "17": '"Storm Spirit"', "18": '"Sven"', "19": '"Tiny"', "20": '"Vengeful Spirit"', "21": '"Windranger"', "22": '"Zeus"', "23": '"Kunkka"', "25": '"Lina"', "26": '"Lion"', "27": '"Shadow Shaman"', "28": '"Slardar"', "29": '"Tidehunter"', "30": '"Witch Doctor"', "31": '"Lich"', "32": '"Riki"', "33": '"Enigma"', "34": '"Tinker"', "35": '"Sniper"', "36": '"Necrophos"', "37": '"Warlock"', "38": '"Beastmaster"', "39": '"Queen of Pain"', "40": '"Venomancer"', "41": '"Faceless Void"', "42": '"Wraith King"', "43": '"Death Prophet"', "44": '"Phantom Assassin"', "45": '"Pugna"', "46": '"Templar Assassin"', "47": '"Viper"', "48": '"Luna"', "49": '"Dragon Knight"', "50": '"Dazzle"', "51": '"Clockwerk"', "52": '"Leshrac"', "53": `"Nature's Prophet"`, "54": '"Lifestealer"', "55": '"Dark Seer"', "56": '"Clinkz"', "57": '"Omniknight"', "58": '"Enchantress"', "59": '"Huskar"', "60": '"Night Stalker"', "61": '"Broodmother"', "62": '"Bounty Hunter"', "63": '"Weaver"', "64": '"Jakiro"', "65": '"Batrider"', "66": '"Chen"', "67": '"Spectre"', "68": '"Ancient Apparition"', "69": '"Doom"', "70": '"Ursa"', "71": '"Spirit Breaker"', "72": '"Gyrocopter"', "73": '"Alchemist"', "74": '"Invoker"', "75": '"Silencer"', "76": '"Outworld Devourer"', "77": '"Lycan"', "78": '"Brewmaster"', "79": '"Shadow Demon"', "80": '"Lone Druid"', "81": '"Chaos Knight"', "82": '"Meepo"', "83": '"Treant Protector"', "84": '"Ogre Magi"', "85": '"Undying"', "86": '"Rubick"', "87": '"Disruptor"', "88": '"Nyx Assassin"', "89": '"Naga Siren"', "90": '"Keeper of the Light"', "91": '"Io"', "92": '"Visage"', "93": '"Slark"', "94": '"Medusa"', "95": '"Troll Warlord"', "96": '"Centaur Warrunner"', "97": '"Magnus"', "98": '"Timbersaw"', "99": '"Bristleback"', "100": '"Tusk"', "101": '"Skywrath Mage"', "102": '"Abaddon"', "103": '"Elder Titan"', "104": '"Legion Commander"', "105": '"Techies"', "106": '"Ember Spirit"', "107": '"Earth Spirit"', "108": '"Underlord"', "109": '"Terrorblade"', "110": '"Phoenix"', "111": '"Oracle"', "112": '"Winter Wyvern"', "113": '"Arc Warden"', "114": '"Monkey King"', "119": '"Dark Willow"', "120": '"Pangolier"', "121": '"Grimstroke"', "123": '"Hoodwink"', "126": '"Void Spirit"', "128": '"Snapfire"', "129": '"Mars"', "131": '"Ring Master"', "135": '"Dawnbreaker"', "136": '"Marci"', "137": '"Primal Beast"', "138": '"Muerta"', "145": '"Kez"' }, broadcast: { WIN_NEGATIVE: `"Won the match by sheer luck", "Won the match by a stroke of bad luck", "Coasted to victory", "Didn't even show up for the team fight, but my teammates won 4v5"`, WIN_POSITIVE: '"Led the team to victory", "Dominated the opponents and secured the win", "Carried the game to victory", "Treated the opponents like pigs and won", "Won again; this game is just so monotonous and dull", "Simply achieved a win in the match"', LOSE_NEGATIVE: '"Got crushed and lost the match", "Lost the match miserably", "Got my head knocked sideways and lost the match with a blown mindset", "Went fishing but got eaten by the fish, lost the match", "Played terribly", "Simply suffered a loss in the match"', LOSE_POSITIVE: `"Lost the match with no way to turn it around", "Gave it my all, but still lost the match", "Though we lost, we still have honor", "Couldn't carry my teammates, lost the match", "Lost again, it hurts; I'd rather it be me losing"`, message: "{name}'s {hero_name} {comment}.\nKDA: {kda}, GPM/XPM: {gpm_xpm}, Last Hits/Denies: {lh_dn}, Damage/Tower Damage: {damage}, Kill/Death Contribution Rate: {kc_dc}", rank_changed: "Player {name} rank changed: {prev.medal} {prev.star} → {curr.medal} {curr.star}" }, logger: { fetch_guilds_failed: "Failed to fetch guild information.", match_tracked: "Tracked new match {match.id} from {#each messageToLogger as item}users in guild {item.platform}:{item.guildId} [{#each item.players as player}{player.nickname}({player.steamId}){#if player !== item.players[item.players.length - 1]}, {/if}{/each}]{#if item !== messageToLogger[messageToLogger.length - 1]}, {/if}{/each}.", parse_request_sent: "The parsing request for match {matchId} has been successfully sent to the STRATZ server.", parse_request_failed: "The parsing request for match {matchId} failed to send.", match_parsed: "Match {matchId} has been parsed{#if odParsed} by OpenDota{/if}, an image was generated and published to {#each guilds as guild}{guild.platform}:{guild.guildId}{#if guild !== guilds[guilds.length - 1]}, {/if}{/each}.", match_unparsed: "Match {matchId} exceeded the waiting time [{timeout}] and remains unparsed, an image was generated and published to {#each guilds as guild}{guild.platform}:{guild.guildId}{#if guild !== guilds[guilds.length - 1]}, {/if}{/each}.", waiting_for_parse: "Match {matchId} has not been parsed yet, has been waiting for {time} minutes.", report_sent: "Posted {title} on {platform}:{guildId}.", rank_sent: "Posted rank change information of {player.nickName} ({player.steamId}) on {platform}:{guildId}.", ejs_error: "Error rendering EJS template: {error}", cron_not_enabled: "Cron service is not enabled; match report tracking cannot run.", stratz_token_banned: "Stratz API request rejected (403). This usually means your token or IP has been temporarily restricted due to unusual activity. For details, see the documentation: http://sjtdev.github.io/koishi-plugin-dota2tracker/en-US/api-403.html", opendota_token_banned: "OpenDota API request rejected (403). Please check if the access limit has been exceeded.", stratz_api_query_error: "Stratz API returned partial data with errors: {cause}", opendota_parse_request_sent: "Parse request for match {matchId} has been sent to OpenDota servers successfully.", opendota_parse_request_failed: "Failed to send parse request for match {matchId} to OpenDota servers." }, time: { years_months_ago: "{years} years and {months} months ago", years_ago: "{years} years ago" } } };
791
791
  }
792
792
  });
793
793
 
@@ -801,21 +801,21 @@ var require_zh_CN_command = __commonJS({
801
801
  // src/locales/zh-CN.schema.yml
802
802
  var require_zh_CN_schema = __commonJS({
803
803
  "src/locales/zh-CN.schema.yml"(exports2, module2) {
804
- module2.exports = { _config: { base: { $desc: "基础设置", STRATZ_API_TOKEN: "※必须。stratz.com的API TOKEN,可在 https://stratz.com/api 获取。", dataParsingTimeoutMinutes: "等待比赛数据解析的时间(单位:分钟)。如果数据解析时间超过等待时间,将直接生成战报而不再等待解析完成。", proxyAddress: "代理地址,留空时不使用代理", suppressStratzNetworkErrors: "开启后将stratz网络错误日志(如超时、网络不通等,但403 Forbidden除外)使用debug级别输出,默认不显示debug级日志。 \n若需要开启debug日志显示,请见 [📖 配置项#suppressstratznetworkerrors](http://sjtdev.github.io/koishi-plugin-dota2tracker/configs.html#suppressstratznetworkerrors-boolean)", enableOpenDotaFallback: "启用 OpenDota 作为战报追踪与查询比赛功能的备用数据源。", OPENDOTA_API_KEY: "OpenDota 的订阅付费APIKEY, \n可在 https://www.opendota.com/api-keys 查看详情。 \nOpenDota 的免费用户此处请留空。" }, 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: "别名" } }, 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: '模板所使用的字体名。需要 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) ' } } };
804
+ 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: "别名" } }, 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: '模板所使用的字体名。需要 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) ' } } };
805
805
  }
806
806
  });
807
807
 
808
808
  // src/locales/zh-CN.template.yml
809
809
  var require_zh_CN_template = __commonJS({
810
810
  "src/locales/zh-CN.template.yml"(exports2, module2) {
811
- 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: "比赛未解析或信息缺失,无法展示更多数据。" } } };
811
+ 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: "累计获得金币" } } } };
812
812
  }
813
813
  });
814
814
 
815
815
  // src/locales/zh-CN.yml
816
816
  var require_zh_CN = __commonJS({
817
817
  "src/locales/zh-CN.yml"(exports2, module2) {
818
- module2.exports = { dota2tracker: { heroes_nicknames: { "0": '请严格遵循 "", "" 格式填写(如下方默认数据,注意是英文半角符号),如果格式有误将仅使用英雄默认名称。可以不包含英雄默认名称。', "1": '"敌法师", "敌法", "AM"', "2": '"斧王"', "3": '"祸乱之源", "祸乱", "水桶腰"', "4": '"血魔"', "5": '"水晶室女", "冰女", "CM"', "6": '"卓尔游侠", "小黑"', "7": '"撼地者", "小牛", "牛头"', "8": '"主宰", "剑圣", "jugg", "奶棒人"', "9": '"米拉娜", "白虎", "pom"', "10": '"变体精灵", "水人"', "11": '"影魔", "影魔王", "SF", "影儿魔儿"', "12": '"幻影长矛手", "PL"', "13": '"帕克"', "14": '"帕吉", "屠夫", "扒鸡", "啪唧"', "15": '"雷泽", "电魂", "电棍"', "16": '"沙王", "SK"', "17": '"风暴之灵", "蓝猫"', "18": '"斯温", "流浪剑客", "流浪"', "19": '"小小"', "20": '"复仇之魂", "复仇", "VS"', "21": '"风行者", "风行", "WR"', "22": '"宙斯"', "23": '"昆卡", "船长"', "25": '"莉娜", "火女"', "26": '"莱恩", "恶魔巫师", "Lion"', "27": '"暗影萨满", "小Y", "小歪"', "28": '"斯拉达", "大鱼", "大鱼人"', "29": '"潮汐猎人", "潮汐", "西瓜皮"', "30": '"巫医"', "31": '"巫妖"', "32": '"力丸", "隐形刺客", "隐刺"', "33": '"谜团"', "34": '"修补匠", "TK", "Tinker"', "35": '"狙击手", "矮人火枪手", "火枪", "传说哥"', "36": '"瘟疫法师", "死灵法", "NEC"', "37": '"术士", "Warlock"', "38": '"兽王"', "39": '"痛苦女王", "女王", "QOP"', "40": '"剧毒术士", "剧毒"', "41": '"虚空假面", "虚空", "JB脸"', "42": '"冥魂大帝", "骷髅王"', "43": '"死亡先知", "DP"', "44": '"幻影刺客", "幻刺", "PA"', "45": '"帕格纳", "骨法", "湮灭法师"', "46": '"圣堂刺客", "圣堂", "TA"', "47": '"冥界亚龙", "毒龙", "Viper"', "48": '"露娜", "月骑", "Luna"', "49": '"龙骑士", "龙骑"', "50": '"戴泽", "暗影牧师", "暗牧"', "51": '"发条技师", "发条"', "52": '"拉席克", "老鹿"', "53": '"先知"', "54": '"噬魂鬼", "小狗"', "55": '"黑暗贤者", "黑贤"', "56": '"克林克兹", "小骷髅"', "57": '"全能骑士", "全能"', "58": '"魅惑魔女", "小鹿"', "59": '"哈斯卡", "神灵", "神灵武士"', "60": '"暗夜魔王", "夜魔"', "61": '"育母蜘蛛", "蜘蛛"', "62": '"赏金猎人", "赏金"', "63": '"编织者", "蚂蚁"', "64": '"杰奇洛", "双头龙"', "65": '"蝙蝠骑士", "蝙蝠"', "66": '"陈", "老陈"', "67": '"幽鬼", "SPE", "UG"', "68": '"远古冰魄", "冰魂"', "69": '"末日使者", "末日", "Doom"', "70": '"熊战士", "拍拍", "拍拍熊"', "71": '"裂魂人", "白牛", "sb"', "72": '"矮人直升机", "飞机"', "73": '"炼金术士", "炼金"', "74": '"祈求者", "卡尔"', "75": '"沉默术士", "沉默"', "76": '"殁境神蚀者", "黑鸟"', "77": '"狼人"', "78": '"酒仙", "熊猫", "熊猫酒仙"', "79": '"暗影恶魔", "毒狗"', "80": '"德鲁伊", "熊德"', "81": '"混沌骑士", "混沌", "CK"', "82": '"米波"', "83": '"树精卫士", "大树", "树精"', "84": '"食人魔魔法师", "蓝胖"', "85": '"不朽尸王", "尸王"', "86": '"拉比克"', "87": '"干扰者", "萨尔"', "88": '"司夜刺客", "小强"', "89": '"娜迦海妖", "小娜迦"', "90": '"光之守卫", "光法"', "91": '"艾欧", "小精灵", "精灵", "IO"', "92": '"维萨吉", "死灵龙", "死灵飞龙"', "93": '"斯拉克", "小鱼", "小鱼人"', "94": '"美杜莎", "一姐", "美杜莎"', "95": '"巨魔战将", "巨魔", "巨馍蘸酱"', "96": '"半人马战行者", "人马", "半人马"', "97": '"马格纳斯", "猛犸"', "98": '"伐木机", "花母鸡"', "99": '"钢背兽", "钢背"', "100": '"巨牙海民", "海民"', "101": '"天怒法师", "天怒"', "102": '"亚巴顿"', "103": '"上古巨神", "大牛"', "104": '"军团指挥官", "军团"', "105": '"工程师", "炸弹", "炸弹人"', "106": '"灰烬之灵", "火猫"', "107": '"大地之灵", "土猫"', "108": '"孽主", "大屁股"', "109": '"恐怖利刃", "TB"', "110": '"凤凰", "烧鸡"', "111": '"神谕者", "神谕"', "112": '"寒冬飞龙", "冰龙"', "113": '"天穹守望者", "电狗"', "114": '"齐天大圣", "大圣"', "119": '"邪影芳灵", "小仙女", "花仙子"', "120": '"石鳞剑士", "滚滚"', "121": '"天涯墨客", "墨客"', "123": '"森海飞霞", "松鼠", "小松鼠", "小松许"', "126": '"虚无之灵", "紫猫"', "128": '"电炎绝手", "老奶奶"', "129": '"玛尔斯"', "131": '"百戏大王"', "135": '"破晓辰星", "大锤"', "136": '"玛西"', "137": '"獸", "畜"', "138": '"琼英碧灵", "奶绿", "绿奶奶"', "145": '"凯", "鸟人"' }, broadcast: { WIN_NEGATIVE: '"侥幸赢得了比赛", "走狗屎运赢得了比赛", "躺赢了比赛", "打团都没来, 队友4V5赢得了比赛"', WIN_POSITIVE: '"带领团队走向了胜利", "暴打对面后赢得了胜利", " CARRY全场赢得了胜利", "把对面当猪宰了, 赢得了胜利", "又赢了, 这游戏就是这么枯燥, 且乏味", "直接进行一个比赛的赢"', LOSE_NEGATIVE: '"被人按在地上摩擦, 输掉了这场比赛", "悲惨地输掉了比赛", "头都被打歪了, 心态爆炸地输掉了比赛", "捕鱼被鱼吃了, 输掉了比赛", "打的是个几把", "直接进行一个比赛的输"', LOSE_POSITIVE: '"无力回天输掉了比赛", "尽力了, 但还是输了比赛", "背靠世界树, 虽败犹荣", "带不动队友, 输了比赛", "又输了, 很难受, 宁愿输的是我"', message: "{name}的{hero_name}{comment}。\nKDA:{kda},GPM/XPM:{gpm_xpm},补刀/反补:{lh_dn},伤害/塔伤:{damage},参战/参葬率:{kc_dc}", rank_changed: "群友 {name} 段位变动:{prev.medal}{prev.star} → {curr.medal}{curr.star}" }, logger: { fetch_guilds_failed: "获取群组信息失败,将继续后续步骤。", match_tracked: "追踪到最新比赛 {match.id} 来自{#each messageToLogger as item}群组 {item.platform}:{item.guildId} 的玩家 [{#each item.players as player}{player.nickname}({player.steamId}){#if player !== item.players[item.players.length - 1]}、{/if}{/each}]{#if item !== messageToLogger[messageToLogger.length - 1]}、{/if}{/each}。", parse_request_sent: "比赛 {matchId} 解析请求已成功发送至STRATZ服务器。", parse_request_failed: "比赛 {matchId} 解析请求发送失败。", match_parsed: "比赛 {matchId} 已{#if odParsed}由 OpenDota {/if}解析,生成图片并发布于{#each guilds as guild}{guild.platform}:{guild.guildId}{#if guild !== guilds[guilds.length - 1]}、{/if}{/each}。", match_unparsed: "比赛 {matchId} 超过等待时间[{timeout}分钟]仍未解析,生成图片并发布于{#each guilds as guild}{guild.platform}:{guild.guildId}{#if guild !== guilds[guilds.length - 1]}、{/if}{/each}。", waiting_for_parse: "比赛 {matchId} 尚未解析完成,已等待{time}分钟。", report_sent: "发布{title}于{platform}:{guildId}。", rank_sent: "向{platform}:{guildId}发布{player.nickName}({player.steamId})的段位变动信息。", ejs_error: "EJS模板渲染错误:{error}", cron_not_enabled: "未启用cron服务,无法运行战报追踪等定时任务。", stratz_token_banned: "Stratz API 请求被拒绝(403),若频繁发生很有可能意味着您的Token或IP因异常使用被临时限制访问。有关此报错请见文档:http://sjtdev.github.io/koishi-plugin-dota2tracker/api-403.html", stratz_api_query_error: "Stratz API 返回了有效数据,但报错: {cause}", opendota_parse_request_on_later: "已启用 OpenDota 备用查询,将每{timeout}分钟发送一次 OpenDota 解析请求,并每分钟检测比赛是否已被 OpenDota 解析。", opendota_parse_request_sent: "比赛 {matchId} 解析请求已成功发送至 OpenDota 服务器。", opendota_parse_request_failed: "比赛 {matchId} 解析请求向 OpenDota 服务器发送失败。" }, time: { years_months_ago: "{years}年{months}个月前", years_ago: "{years}年前" } } };
818
+ module2.exports = { dota2tracker: { heroes_nicknames: { "0": '请严格遵循 "", "" 格式填写(如下方默认数据,注意是英文半角符号),如果格式有误将仅使用英雄默认名称。可以不包含英雄默认名称。', "1": '"敌法师", "敌法", "AM"', "2": '"斧王"', "3": '"祸乱之源", "祸乱", "水桶腰"', "4": '"血魔"', "5": '"水晶室女", "冰女", "CM"', "6": '"卓尔游侠", "小黑"', "7": '"撼地者", "小牛", "牛头"', "8": '"主宰", "剑圣", "jugg", "奶棒人"', "9": '"米拉娜", "白虎", "pom"', "10": '"变体精灵", "水人"', "11": '"影魔", "影魔王", "SF", "影儿魔儿"', "12": '"幻影长矛手", "PL"', "13": '"帕克"', "14": '"帕吉", "屠夫", "扒鸡", "啪唧"', "15": '"雷泽", "电魂", "电棍"', "16": '"沙王", "SK"', "17": '"风暴之灵", "蓝猫"', "18": '"斯温", "流浪剑客", "流浪"', "19": '"小小"', "20": '"复仇之魂", "复仇", "VS"', "21": '"风行者", "风行", "WR"', "22": '"宙斯"', "23": '"昆卡", "船长"', "25": '"莉娜", "火女"', "26": '"莱恩", "恶魔巫师", "Lion"', "27": '"暗影萨满", "小Y", "小歪"', "28": '"斯拉达", "大鱼", "大鱼人"', "29": '"潮汐猎人", "潮汐", "西瓜皮"', "30": '"巫医"', "31": '"巫妖"', "32": '"力丸", "隐形刺客", "隐刺"', "33": '"谜团"', "34": '"修补匠", "TK", "Tinker"', "35": '"狙击手", "矮人火枪手", "火枪", "传说哥"', "36": '"瘟疫法师", "死灵法", "NEC"', "37": '"术士", "Warlock"', "38": '"兽王"', "39": '"痛苦女王", "女王", "QOP"', "40": '"剧毒术士", "剧毒"', "41": '"虚空假面", "虚空", "JB脸"', "42": '"冥魂大帝", "骷髅王"', "43": '"死亡先知", "DP"', "44": '"幻影刺客", "幻刺", "PA"', "45": '"帕格纳", "骨法", "湮灭法师"', "46": '"圣堂刺客", "圣堂", "TA"', "47": '"冥界亚龙", "毒龙", "Viper"', "48": '"露娜", "月骑", "Luna"', "49": '"龙骑士", "龙骑"', "50": '"戴泽", "暗影牧师", "暗牧"', "51": '"发条技师", "发条"', "52": '"拉席克", "老鹿"', "53": '"先知"', "54": '"噬魂鬼", "小狗"', "55": '"黑暗贤者", "黑贤"', "56": '"克林克兹", "小骷髅"', "57": '"全能骑士", "全能"', "58": '"魅惑魔女", "小鹿"', "59": '"哈斯卡", "神灵", "神灵武士"', "60": '"暗夜魔王", "夜魔"', "61": '"育母蜘蛛", "蜘蛛"', "62": '"赏金猎人", "赏金"', "63": '"编织者", "蚂蚁"', "64": '"杰奇洛", "双头龙"', "65": '"蝙蝠骑士", "蝙蝠"', "66": '"陈", "老陈"', "67": '"幽鬼", "SPE", "UG"', "68": '"远古冰魄", "冰魂"', "69": '"末日使者", "末日", "Doom"', "70": '"熊战士", "拍拍", "拍拍熊"', "71": '"裂魂人", "白牛", "sb"', "72": '"矮人直升机", "飞机"', "73": '"炼金术士", "炼金"', "74": '"祈求者", "卡尔"', "75": '"沉默术士", "沉默"', "76": '"殁境神蚀者", "黑鸟"', "77": '"狼人"', "78": '"酒仙", "熊猫", "熊猫酒仙"', "79": '"暗影恶魔", "毒狗"', "80": '"德鲁伊", "熊德"', "81": '"混沌骑士", "混沌", "CK"', "82": '"米波"', "83": '"树精卫士", "大树", "树精"', "84": '"食人魔魔法师", "蓝胖"', "85": '"不朽尸王", "尸王"', "86": '"拉比克"', "87": '"干扰者", "萨尔"', "88": '"司夜刺客", "小强"', "89": '"娜迦海妖", "小娜迦"', "90": '"光之守卫", "光法"', "91": '"艾欧", "小精灵", "精灵", "IO"', "92": '"维萨吉", "死灵龙", "死灵飞龙"', "93": '"斯拉克", "小鱼", "小鱼人"', "94": '"美杜莎", "一姐", "美杜莎"', "95": '"巨魔战将", "巨魔", "巨馍蘸酱"', "96": '"半人马战行者", "人马", "半人马"', "97": '"马格纳斯", "猛犸"', "98": '"伐木机", "花母鸡"', "99": '"钢背兽", "钢背"', "100": '"巨牙海民", "海民"', "101": '"天怒法师", "天怒"', "102": '"亚巴顿"', "103": '"上古巨神", "大牛"', "104": '"军团指挥官", "军团"', "105": '"工程师", "炸弹", "炸弹人"', "106": '"灰烬之灵", "火猫"', "107": '"大地之灵", "土猫"', "108": '"孽主", "大屁股"', "109": '"恐怖利刃", "TB"', "110": '"凤凰", "烧鸡"', "111": '"神谕者", "神谕"', "112": '"寒冬飞龙", "冰龙"', "113": '"天穹守望者", "电狗"', "114": '"齐天大圣", "大圣"', "119": '"邪影芳灵", "小仙女", "花仙子"', "120": '"石鳞剑士", "滚滚"', "121": '"天涯墨客", "墨客"', "123": '"森海飞霞", "松鼠", "小松鼠", "小松许"', "126": '"虚无之灵", "紫猫"', "128": '"电炎绝手", "老奶奶"', "129": '"玛尔斯"', "131": '"百戏大王"', "135": '"破晓辰星", "大锤"', "136": '"玛西"', "137": '"獸", "畜"', "138": '"琼英碧灵", "奶绿", "绿奶奶"', "145": '"凯", "鸟人"' }, broadcast: { WIN_NEGATIVE: '"侥幸赢得了比赛", "走狗屎运赢得了比赛", "躺赢了比赛", "打团都没来, 队友4V5赢得了比赛"', WIN_POSITIVE: '"带领团队走向了胜利", "暴打对面后赢得了胜利", " CARRY全场赢得了胜利", "把对面当猪宰了, 赢得了胜利", "又赢了, 这游戏就是这么枯燥, 且乏味", "直接进行一个比赛的赢"', LOSE_NEGATIVE: '"被人按在地上摩擦, 输掉了这场比赛", "悲惨地输掉了比赛", "头都被打歪了, 心态爆炸地输掉了比赛", "捕鱼被鱼吃了, 输掉了比赛", "打的是个几把", "直接进行一个比赛的输"', LOSE_POSITIVE: '"无力回天输掉了比赛", "尽力了, 但还是输了比赛", "背靠世界树, 虽败犹荣", "带不动队友, 输了比赛", "又输了, 很难受, 宁愿输的是我"', message: "{name}的{hero_name}{comment}。\nKDA:{kda},GPM/XPM:{gpm_xpm},补刀/反补:{lh_dn},伤害/塔伤:{damage},参战/参葬率:{kc_dc}", rank_changed: "群友 {name} 段位变动:{prev.medal}{prev.star} → {curr.medal}{curr.star}" }, logger: { fetch_guilds_failed: "获取群组信息失败,将继续后续步骤。", match_tracked: "追踪到最新比赛 {match.id} 来自{#each messageToLogger as item}群组 {item.platform}:{item.guildId} 的玩家 [{#each item.players as player}{player.nickname}({player.steamId}){#if player !== item.players[item.players.length - 1]}、{/if}{/each}]{#if item !== messageToLogger[messageToLogger.length - 1]}、{/if}{/each}。", parse_request_sent: "比赛 {matchId} 解析请求已成功发送至STRATZ服务器。", parse_request_failed: "比赛 {matchId} 解析请求发送失败。", match_parsed: "比赛 {matchId} 已{#if odParsed}由 OpenDota {/if}解析,生成图片并发布于{#each guilds as guild}{guild.platform}:{guild.guildId}{#if guild !== guilds[guilds.length - 1]}、{/if}{/each}。", match_unparsed: "比赛 {matchId} 超过等待时间[{timeout}分钟]仍未解析,生成图片并发布于{#each guilds as guild}{guild.platform}:{guild.guildId}{#if guild !== guilds[guilds.length - 1]}、{/if}{/each}。", waiting_for_parse: "比赛 {matchId} 尚未解析完成,已等待{time}分钟。", report_sent: "发布{title}于{platform}:{guildId}。", rank_sent: "向{platform}:{guildId}发布{player.nickName}({player.steamId})的段位变动信息。", ejs_error: "EJS模板渲染错误:{error}", cron_not_enabled: "未启用cron服务,无法运行战报追踪等定时任务。", stratz_token_banned: "Stratz API 请求被拒绝(403),若频繁发生很有可能意味着您的Token或IP因异常使用被临时限制访问。有关此报错请见文档:http://sjtdev.github.io/koishi-plugin-dota2tracker/api-403.html", opendota_token_banned: "OpenDota API 请求被拒绝(403),请注意访问次数是否超限。", stratz_api_query_error: "Stratz API 返回了有效数据,但同时也返回了查询报错。错误信息: {cause}", opendota_parse_request_sent: "比赛 {matchId} 解析请求已成功发送至 OpenDota 服务器。", opendota_parse_request_failed: "比赛 {matchId} 解析请求向 OpenDota 服务器发送失败。" }, time: { years_months_ago: "{years}年{months}个月前", years_ago: "{years}年前" } } };
819
819
  }
820
820
  });
821
821
 
@@ -829,7 +829,7 @@ __export(src_exports, {
829
829
  usage: () => usage
830
830
  });
831
831
  module.exports = __toCommonJS(src_exports);
832
- var import_path4 = __toESM(require("path"));
832
+ var import_path5 = __toESM(require("path"));
833
833
 
834
834
  // src/app/common/i18n.ts
835
835
  var import_koishi = require("koishi");
@@ -1392,6 +1392,133 @@ var ItemService = class _ItemService extends import_koishi3.Service {
1392
1392
  // src/app/core/match.service.ts
1393
1393
  var import_koishi4 = require("koishi");
1394
1394
  var dotaconstants3 = __toESM(require("dotaconstants"));
1395
+
1396
+ // src/app/common/error.ts
1397
+ var import_util = require("util");
1398
+ var import_axios = __toESM(require("axios"));
1399
+ var NetworkError = class extends Error {
1400
+ static {
1401
+ __name(this, "NetworkError");
1402
+ }
1403
+ constructor(message, options) {
1404
+ super(message, options);
1405
+ }
1406
+ };
1407
+ var ForbiddenError = class extends Error {
1408
+ static {
1409
+ __name(this, "ForbiddenError");
1410
+ }
1411
+ constructor(message, options) {
1412
+ super(message, options);
1413
+ }
1414
+ };
1415
+ var GraphQLQueryError = class extends Error {
1416
+ static {
1417
+ __name(this, "GraphQLQueryError");
1418
+ }
1419
+ errors;
1420
+ // 可以把原始 errors 存起来
1421
+ constructor(message, errors, options) {
1422
+ super(message, options);
1423
+ this.errors = errors;
1424
+ }
1425
+ };
1426
+ var FetchMatchDataFailError = class extends Error {
1427
+ static {
1428
+ __name(this, "FetchMatchDataFailError");
1429
+ }
1430
+ stratzError;
1431
+ opendotaError;
1432
+ constructor(message, options) {
1433
+ super(message, { cause: options?.stratzError || options?.opendotaError });
1434
+ this.name = "FetchMatchDataFailError";
1435
+ this.stratzError = options?.stratzError;
1436
+ this.opendotaError = options?.opendotaError;
1437
+ }
1438
+ };
1439
+ function classifyAxiosError(error) {
1440
+ if (import_axios.default.isCancel(error)) {
1441
+ return "Cancel";
1442
+ }
1443
+ if (import_axios.default.isAxiosError(error)) {
1444
+ if (error.code === "ECONNABORTED" && error.message.includes("timeout")) {
1445
+ return "Timeout";
1446
+ }
1447
+ if (error.response) {
1448
+ if (error.response.status === 403) return "Forbidden";
1449
+ if (error.response.status === 404) return "NotFound";
1450
+ if (error.response.status >= 400 && error.response.status < 500) return "ClientError";
1451
+ if (error.response.status >= 500) return "ServerError";
1452
+ } else if (error.request) {
1453
+ return "Network";
1454
+ }
1455
+ }
1456
+ if (error instanceof GraphQLQueryError) {
1457
+ return "GraphQLQueryError";
1458
+ }
1459
+ return "Unknown";
1460
+ }
1461
+ __name(classifyAxiosError, "classifyAxiosError");
1462
+ function processFetchError(error, serviceName, queryName) {
1463
+ const errorType = classifyAxiosError(error);
1464
+ const message = `${serviceName} query (${queryName}) failed. Type: ${errorType}`;
1465
+ if (errorType === "Forbidden") {
1466
+ throw new ForbiddenError(message, { cause: error });
1467
+ } else if (["Timeout", "Network", "ServerError", "ClientError", "NotFound"].includes(errorType)) {
1468
+ throw new NetworkError(message, { cause: error });
1469
+ }
1470
+ throw error;
1471
+ }
1472
+ __name(processFetchError, "processFetchError");
1473
+ function handleError(error, logger, i18n, config) {
1474
+ if (error instanceof FetchMatchDataFailError) {
1475
+ if (error.stratzError && error.opendotaError) {
1476
+ logger.error("Failed to fetch match data from all available sources. See details below:");
1477
+ } else {
1478
+ logger.error("A data source failed during the match fetch process. See details below:");
1479
+ }
1480
+ if (error.stratzError) {
1481
+ logger.error("--- Stratz API Error ---");
1482
+ handleError(error.stratzError, logger, i18n, config);
1483
+ }
1484
+ if (error.opendotaError) {
1485
+ logger.error("--- OpenDota API Error ---");
1486
+ handleError(error.opendotaError, logger, i18n, config);
1487
+ }
1488
+ } else if (error instanceof ForbiddenError) {
1489
+ let output = i18n.gt(`dota2tracker.logger.${error.message.includes("Stratz") ? "stratz" : "opendota"}_token_banned`) + "\n";
1490
+ if (import_axios.default.isAxiosError(error.cause)) {
1491
+ output += `Details: ${error.cause.message}. Response: ${JSON.stringify(error.cause.response?.data)}
1492
+ `;
1493
+ }
1494
+ output += error.stack || error.message;
1495
+ logger.error(output);
1496
+ } else if (error instanceof GraphQLQueryError) {
1497
+ logger.error(`Stratz GraphQL Query Error: ${error.message}`);
1498
+ logger.error("Details:", error.errors);
1499
+ } else if (error instanceof NetworkError) {
1500
+ let output = `A network error occurred: ${error.message}
1501
+ `;
1502
+ if (error.cause && import_axios.default.isAxiosError(error.cause)) {
1503
+ output += `Details: ${error.cause.message}. Response: ${JSON.stringify(error.cause.response?.data)}
1504
+ `;
1505
+ }
1506
+ output += error.stack;
1507
+ const logLevel = config.suppressApiNetworkErrors || config.suppressStratzNetworkErrors ? "debug" : "error";
1508
+ logger[logLevel](output);
1509
+ } else {
1510
+ let output = "An unexpected error was thrown:\n";
1511
+ if (error instanceof Error && error.stack) {
1512
+ output += error.stack;
1513
+ } else {
1514
+ output += (0, import_util.inspect)(error, { depth: null });
1515
+ }
1516
+ logger.error(output);
1517
+ }
1518
+ }
1519
+ __name(handleError, "handleError");
1520
+
1521
+ // src/app/core/match.service.ts
1395
1522
  var MatchService = class _MatchService extends import_koishi4.Service {
1396
1523
  constructor(ctx, pluginVersion2) {
1397
1524
  super(ctx, "dota2tracker.match", true);
@@ -1402,34 +1529,56 @@ var MatchService = class _MatchService extends import_koishi4.Service {
1402
1529
  }
1403
1530
  async getMatchResult({
1404
1531
  matchId,
1405
- requestParse,
1406
- requsetOpenDota
1532
+ waitForParse,
1533
+ allowFallback
1407
1534
  }) {
1408
- const matchQuery = await this.getMatchData(matchId);
1409
- if (matchQuery) {
1410
- if (!_MatchService.isMatchParsed(matchQuery) && requestParse && this.ctx.cron) {
1411
- if (requsetOpenDota) {
1412
- const odMatchQuery = await this.getOpenDotaMatchData(matchId);
1413
- if (odMatchQuery) {
1414
- return {
1415
- status: "READY",
1416
- matchData: odMatchQuery
1417
- };
1418
- }
1419
- }
1420
- return {
1421
- status: "PENDING",
1422
- matchId
1423
- };
1535
+ let stratzData = void 0;
1536
+ let stratzError;
1537
+ try {
1538
+ stratzData = await this.getMatchData(matchId);
1539
+ } catch (error) {
1540
+ stratzError = error;
1541
+ }
1542
+ if (stratzData) {
1543
+ const isParsed = _MatchService.isMatchParsed(stratzData);
1544
+ if (isParsed) {
1545
+ return { status: "READY", matchData: stratzData };
1546
+ }
1547
+ if (!waitForParse) {
1548
+ return { status: "READY", matchData: stratzData };
1549
+ }
1550
+ if (!allowFallback) {
1551
+ return { status: "PENDING", matchId };
1552
+ }
1553
+ } else if (stratzError) {
1554
+ if (!allowFallback) {
1555
+ throw stratzError;
1556
+ }
1557
+ } else {
1558
+ if (!allowFallback) {
1559
+ return { status: "NOT_FOUND" };
1424
1560
  }
1425
- return {
1426
- status: "READY",
1427
- matchData: matchQuery
1428
- };
1429
1561
  }
1430
- return {
1431
- status: "NOT_FOUND"
1432
- };
1562
+ let opendotaData = void 0;
1563
+ let opendotaError;
1564
+ try {
1565
+ opendotaData = await this.getOpenDotaMatchData(matchId);
1566
+ } catch (error) {
1567
+ opendotaError = error;
1568
+ }
1569
+ if (opendotaData) {
1570
+ if (stratzError) {
1571
+ handleError(stratzError, this.logger, this.ctx.dota2tracker.i18n, this.ctx.config);
1572
+ }
1573
+ return { status: "READY", matchData: opendotaData };
1574
+ }
1575
+ if (stratzError || opendotaError) {
1576
+ throw new FetchMatchDataFailError("Failed to fetch match data from all sources", { stratzError, opendotaError });
1577
+ }
1578
+ if (waitForParse) {
1579
+ return { status: "PENDING", matchId };
1580
+ }
1581
+ return { status: "NOT_FOUND" };
1433
1582
  }
1434
1583
  async generateMatchData(rawMatchData, languageTag) {
1435
1584
  return await this.formatMatchData(rawMatchData, languageTag);
@@ -1487,7 +1636,7 @@ var MatchService = class _MatchService extends import_koishi4.Service {
1487
1636
  }
1488
1637
  static async constantsInjectFacetData(constantsQuery, matchQuery, languageTag, heroService) {
1489
1638
  const facetData = {};
1490
- for (let player of matchQuery.match.players) {
1639
+ for (let player of matchQuery.match?.players) {
1491
1640
  if (player.variant != null) {
1492
1641
  const constantsFacet = constantsQuery.constants.facets.find((facet) => facet.id === player.hero.facets[player.variant - 1]?.facetId || facet.name === player.hero.facets[player.variant - 1]?.name);
1493
1642
  let displayName = constantsFacet?.language?.displayName;
@@ -2072,6 +2221,7 @@ var CacheService = class extends import_koishi6.Service {
2072
2221
  }
2073
2222
  constructor(ctx) {
2074
2223
  super(ctx, "dota2tracker.cache", true);
2224
+ this.config = ctx.config;
2075
2225
  }
2076
2226
  get msUntilUTCEndOfDay() {
2077
2227
  const now = import_luxon4.DateTime.utc();
@@ -2081,6 +2231,7 @@ var CacheService = class extends import_koishi6.Service {
2081
2231
  }
2082
2232
  addOpendotaAPIRequestLog(request, count = 1) {
2083
2233
  this.ctx.cache.set("dt_opendota_api_request_log", String(Date.now()), { count, request }, this.msUntilUTCEndOfDay);
2234
+ if (this.config.enableConsole) this.ctx.console.services["apiCount"].refresh();
2084
2235
  }
2085
2236
  async getTodayOpendotaAPIRequestCount() {
2086
2237
  let count = 0;
@@ -2234,63 +2385,27 @@ var DatabaseService = class extends import_koishi7.Service {
2234
2385
  var import_fs = __toESM(require("fs"));
2235
2386
  var import_koishi8 = require("koishi");
2236
2387
  var import_path = __toESM(require("path"));
2237
-
2238
- // src/app/common/error.ts
2239
- var import_util = require("util");
2240
- var NetworkError = class extends Error {
2241
- static {
2242
- __name(this, "NetworkError");
2243
- }
2244
- constructor(message, options) {
2245
- super(message, options);
2246
- }
2247
- };
2248
- var ForbiddenError = class extends Error {
2249
- static {
2250
- __name(this, "ForbiddenError");
2251
- }
2252
- constructor(message, options) {
2253
- super(message, options);
2254
- }
2255
- };
2256
- function handleError(error, logger, i18n, config) {
2257
- if (error instanceof ForbiddenError) {
2258
- let output = i18n.gt("dota2tracker.logger.stratz_token_banned") + "\n";
2259
- output += error.stack || error.message;
2260
- logger.error(output);
2261
- } else if (error instanceof NetworkError) {
2262
- if (config.suppressStratzNetworkErrors) {
2263
- logger.debug(error);
2264
- } else {
2265
- logger.error(error);
2266
- }
2267
- } else {
2268
- let output = "An unexpected error was thrown:\n";
2269
- if (error instanceof Error && error.stack) {
2270
- output += error.stack;
2271
- } else {
2272
- output += (0, import_util.inspect)(error, { depth: null });
2273
- }
2274
- logger.error(output);
2275
- }
2276
- }
2277
- __name(handleError, "handleError");
2278
-
2279
- // src/app/data/stratz.api.ts
2388
+ var import_axios2 = __toESM(require("axios"));
2389
+ var import_https_proxy_agent = require("https-proxy-agent");
2280
2390
  var StratzAPI = class extends import_koishi8.Service {
2281
2391
  constructor(ctx, pluginDir3) {
2282
2392
  super(ctx, "dota2tracker.stratz-api", true);
2283
2393
  this.pluginDir = pluginDir3;
2284
2394
  this.config = ctx.config;
2285
2395
  this.queue = new MiniQueue(ctx, { interval: 200 });
2396
+ this.http = import_axios2.default.create({ timeout: 1e4, signal: this.abortController.signal });
2397
+ ctx.on("dispose", () => this.dispose());
2286
2398
  }
2287
2399
  static {
2288
2400
  __name(this, "StratzAPI");
2289
2401
  }
2290
2402
  BASE_URL = "https://api.stratz.com/graphql";
2291
2403
  queue;
2404
+ http;
2405
+ abortController = new AbortController();
2292
2406
  dispose() {
2293
2407
  this.queue.dispose();
2408
+ this.abortController.abort();
2294
2409
  }
2295
2410
  async queryGetWeeklyMetaByPosition({ bracketIds }) {
2296
2411
  return this.query("GetWeeklyMetaByPosition", { bracketIds }, (data) => !!data.heroStats);
@@ -2366,7 +2481,7 @@ var StratzAPI = class extends import_koishi8.Service {
2366
2481
  const chunk = playerIds.slice(i, i + chunkSize);
2367
2482
  variables.steamAccountIds = chunk;
2368
2483
  const query_str = this.loadGraphqlFile(queryName);
2369
- const result = await this.fetchData({ query: query_str, variables }, isValid);
2484
+ const result = await this.fetchData({ query: query_str, variables }, isValid, queryName);
2370
2485
  if (result && result.players) {
2371
2486
  allPlayers = allPlayers.concat(result.players);
2372
2487
  }
@@ -2374,44 +2489,38 @@ var StratzAPI = class extends import_koishi8.Service {
2374
2489
  return { players: allPlayers };
2375
2490
  } else {
2376
2491
  const query_str = this.loadGraphqlFile(queryName);
2377
- const result = await this.fetchData({ query: query_str, variables }, isValid);
2492
+ const result = await this.fetchData({ query: query_str, variables }, isValid, queryName);
2378
2493
  return result;
2379
2494
  }
2380
2495
  }
2381
- async fetchData(query, isValid) {
2496
+ async fetchData(query, isValid, queryName) {
2382
2497
  return this.queue.add(async () => {
2383
2498
  try {
2384
- const result = await this.ctx.http.post(this.BASE_URL, JSON.stringify(query), {
2499
+ const config = {
2385
2500
  responseType: "json",
2386
- headers: {
2387
- "User-Agent": "STRATZ_API",
2388
- "Content-Type": "application/json",
2389
- Authorization: `Bearer ${this.config.STRATZ_API_TOKEN}`
2390
- },
2391
- proxyAgent: this.config.proxyAddress || void 0
2392
- });
2501
+ headers: { "User-Agent": "STRATZ_API", "Content-Type": "application/json", Authorization: `Bearer ${this.config.STRATZ_API_TOKEN}` },
2502
+ httpAgent: void 0,
2503
+ httpsAgent: void 0
2504
+ };
2505
+ if (this.config.proxyAddress) {
2506
+ config.httpsAgent = new import_https_proxy_agent.HttpsProxyAgent(this.config.proxyAddress);
2507
+ config.httpAgent = new import_https_proxy_agent.HttpsProxyAgent(this.config.proxyAddress);
2508
+ }
2509
+ const result = (await this.http.post(this.BASE_URL, JSON.stringify(query), config)).data;
2393
2510
  const isDataValid = isValid(result.data);
2394
2511
  if (result.errors) {
2395
- const error = result.errors.map((e) => e.message).join("\n");
2512
+ const errors = result.errors.map((e) => e.message);
2513
+ const details = errors.join("\n");
2396
2514
  if (isDataValid) {
2397
- this.logger.warn(this.ctx.dota2tracker.i18n.gt("dota2tracker.logger.stratz_api_query_error", { cause: error }));
2515
+ this.logger.warn(this.ctx.dota2tracker.i18n.gt("dota2tracker.logger.stratz_api_query_error", { cause: details }));
2398
2516
  return result.data;
2399
2517
  } else {
2400
- throw new Error("Stratz API Error", { cause: error });
2518
+ throw new GraphQLQueryError("Stratz API query failed and returned invalid data", errors);
2401
2519
  }
2402
2520
  }
2403
2521
  return result.data;
2404
2522
  } catch (error) {
2405
- if (error.response) {
2406
- if (error.response.status === 403) {
2407
- throw new ForbiddenError("Stratz API Forbidden", { cause: error });
2408
- }
2409
- throw new NetworkError("Stratz API HTTP Error", { cause: error });
2410
- } else if (error.code) {
2411
- throw new NetworkError("Stratz API Connection Error", { cause: error });
2412
- } else {
2413
- throw error;
2414
- }
2523
+ processFetchError(error, "Stratz", queryName);
2415
2524
  }
2416
2525
  });
2417
2526
  }
@@ -2437,11 +2546,11 @@ var MiniQueue = class {
2437
2546
  if (this.stopped) {
2438
2547
  return Promise.reject(new Error("Queue has been disposed."));
2439
2548
  }
2440
- return new Promise((resolve, reject) => {
2549
+ return new Promise((resolve2, reject) => {
2441
2550
  this.queue.push(async () => {
2442
2551
  try {
2443
2552
  const result = await task();
2444
- resolve(result);
2553
+ resolve2(result);
2445
2554
  } catch (error) {
2446
2555
  reject(error);
2447
2556
  }
@@ -2462,7 +2571,7 @@ var MiniQueue = class {
2462
2571
  const task = this.queue.shift();
2463
2572
  if (task) {
2464
2573
  await task();
2465
- await new Promise((resolve) => this.ctx.setTimeout(resolve, this.interval));
2574
+ await new Promise((resolve2) => this.ctx.setTimeout(resolve2, this.interval));
2466
2575
  }
2467
2576
  this.isProcessing = false;
2468
2577
  this._process();
@@ -2470,25 +2579,62 @@ var MiniQueue = class {
2470
2579
  };
2471
2580
 
2472
2581
  // src/app/data/valve.api.ts
2582
+ var import_axios3 = __toESM(require("axios"));
2473
2583
  var import_koishi9 = require("koishi");
2584
+ var import_https_proxy_agent2 = require("https-proxy-agent");
2474
2585
  var ValveAPI = class extends import_koishi9.Service {
2475
2586
  static {
2476
2587
  __name(this, "ValveAPI");
2477
2588
  }
2589
+ baseURL = "https://www.dota2.com/datafeed";
2590
+ http;
2591
+ abortController = new AbortController();
2478
2592
  constructor(ctx) {
2479
2593
  super(ctx, "dota2tracker.valve-api", true);
2594
+ this.config = ctx.config;
2595
+ this.http = import_axios3.default.create({ timeout: 1e4, signal: this.abortController.signal, baseURL: this.baseURL });
2596
+ ctx.on("dispose", () => this.dispose());
2597
+ }
2598
+ dispose() {
2599
+ this.abortController.abort();
2600
+ }
2601
+ // 7. 提取通用的 fetchData
2602
+ async fetchData(path5, languageTag) {
2603
+ const config = {
2604
+ headers: {},
2605
+ httpAgent: void 0,
2606
+ httpsAgent: void 0,
2607
+ params: {}
2608
+ };
2609
+ if (this.config.proxyAddress) {
2610
+ config.httpsAgent = new import_https_proxy_agent2.HttpsProxyAgent(this.config.proxyAddress);
2611
+ config.httpAgent = new import_https_proxy_agent2.HttpsProxyAgent(this.config.proxyAddress);
2612
+ }
2613
+ if (languageTag) {
2614
+ config.params.language = this.ctx.dota2tracker.i18n.getValveLanguageTag(languageTag);
2615
+ }
2616
+ try {
2617
+ const response = await this.http.get(path5, config);
2618
+ return response.data;
2619
+ } catch (error) {
2620
+ processFetchError(error, this.name, path5);
2621
+ }
2480
2622
  }
2481
2623
  async queryHeroDetailsFromValve(heroId, languageTag = "zh-CN") {
2482
- return (await this.ctx.http.get(`https://www.dota2.com/datafeed/herodata?language=${this.ctx.dota2tracker.i18n.getValveLanguageTag(languageTag)}&hero_id=${heroId}`)).result.data.heroes[0];
2624
+ const data = await this.fetchData(`/herodata?hero_id=${heroId}`, languageTag);
2625
+ return data.result.data.heroes[0];
2483
2626
  }
2484
2627
  async queryItemListFromValve(languageTag = "zh-CN") {
2485
- return (await this.ctx.http.get(`https://www.dota2.com/datafeed/itemlist?language=${this.ctx.dota2tracker.i18n.getValveLanguageTag(languageTag)}`)).result.data.itemabilities;
2628
+ const data = await this.fetchData(`/itemlist`, languageTag);
2629
+ return data.result.data.itemabilities;
2486
2630
  }
2487
2631
  async queryItemDetailsFromValve(itemId, languageTag = "zh-CN") {
2488
- return (await this.ctx.http.get(`https://www.dota2.com/datafeed/itemdata?language=${this.ctx.dota2tracker.i18n.getValveLanguageTag(languageTag)}&item_id=${itemId}`)).result.data.items[0];
2632
+ const data = await this.fetchData(`/itemdata?item_id=${itemId}`, languageTag);
2633
+ return data.result.data.items[0];
2489
2634
  }
2490
2635
  async queryLastPatchNumber() {
2491
- return (await this.ctx.http.get("https://www.dota2.com/datafeed/patchnoteslist")).patches.at(-1).patch_number;
2636
+ const data = await this.fetchData("/patchnoteslist", void 0);
2637
+ return data.patches.at(-1).patch_number;
2492
2638
  }
2493
2639
  };
2494
2640
 
@@ -2763,6 +2909,7 @@ var MessageBuilder = class extends import_koishi11.Service {
2763
2909
  body {
2764
2910
  padding: 16px;
2765
2911
  width: fit-content;
2912
+ height: fit-content;
2766
2913
  background-color: #f7f7f7;
2767
2914
  font-family:<%-fontFamily%>;
2768
2915
  }
@@ -3046,9 +3193,6 @@ var ParsePollingTask = class extends import_koishi13.Service {
3046
3193
  if (isNewEntry) {
3047
3194
  this.ctx.dota2tracker.stratzAPI.requestParseMatch(matchId).then((value) => {
3048
3195
  this.logger.info(this.ctx.dota2tracker.i18n.gt(`dota2tracker.logger.parse_request_${value ? "sent" : "failed"}`, { matchId }));
3049
- if (this.config.enableOpenDotaFallback) {
3050
- this.logger.info(this.ctx.dota2tracker.i18n.gt("dota2tracker.logger.opendota_parse_request_on_later", { timeout: 5 }));
3051
- }
3052
3196
  });
3053
3197
  }
3054
3198
  }
@@ -3062,18 +3206,13 @@ var ParsePollingTask = class extends import_koishi13.Service {
3062
3206
  const requestTime = import_luxon8.DateTime.fromJSDate(pendingMatch.requestTime);
3063
3207
  const timeout = requestTime.plus({ minutes: this.config.dataParsingTimeoutMinutes });
3064
3208
  const needToWait = import_luxon8.DateTime.now() < timeout;
3065
- const result = await this.ctx.dota2tracker.match.getMatchResult({ matchId: pendingMatch.matchId, requestParse: needToWait, requsetOpenDota: this.config.enableOpenDotaFallback });
3066
- if (result.status === "PENDING") {
3067
- const waitingTime = import_luxon8.DateTime.now().diff(requestTime, "minutes");
3068
- const waitingTimeMinutes = Math.floor(waitingTime.minutes);
3069
- if (waitingTimeMinutes > 0 && waitingTimeMinutes % 5 === 0) {
3070
- this.logger.info(this.ctx.dota2tracker.i18n.gt("dota2tracker.logger.waiting_for_parse", { matchId: pendingMatch.matchId, time: waitingTimeMinutes }));
3071
- if (this.config.enableOpenDotaFallback)
3072
- this.ctx.dota2tracker.opendotaAPI.requestParseMatch(pendingMatch.matchId).then((value) => this.logger.info(this.ctx.dota2tracker.i18n.gt(`dota2tracker.logger.opendota_parse_request_${value ? "sent" : "failed"}`, { matchId: pendingMatch.matchId })));
3073
- }
3074
- return;
3209
+ let result;
3210
+ try {
3211
+ result = await this.ctx.dota2tracker.match.getMatchResult({ matchId: pendingMatch.matchId, waitForParse: needToWait, allowFallback: this.config.enableOpenDotaFallback });
3212
+ } catch (error) {
3213
+ handleError(error, this.logger, this.ctx.dota2tracker.i18n, this.ctx.config);
3075
3214
  }
3076
- if (result.status === "READY") {
3215
+ if (result && result.status === "READY") {
3077
3216
  const subscribersByLang = /* @__PURE__ */ new Map();
3078
3217
  for (const subscriber of pendingMatch.subscribers) {
3079
3218
  if (!subscribersByLang.has(subscriber.languageTag)) subscribersByLang.set(subscriber.languageTag, []);
@@ -3109,6 +3248,17 @@ var ParsePollingTask = class extends import_koishi13.Service {
3109
3248
  );
3110
3249
  this.ctx.dota2tracker.cache.markMatchAsSended(pendingMatch.matchId);
3111
3250
  this.pendingMatches.delete(pendingMatch.matchId);
3251
+ return;
3252
+ }
3253
+ if (!result || result.status === "PENDING") {
3254
+ const waitingTime = import_luxon8.DateTime.now().diff(requestTime, "minutes");
3255
+ const waitingTimeMinutes = Math.floor(waitingTime.minutes);
3256
+ if (waitingTimeMinutes > 0 && waitingTimeMinutes % 5 === 0) {
3257
+ this.logger.info(this.ctx.dota2tracker.i18n.gt("dota2tracker.logger.waiting_for_parse", { matchId: pendingMatch.matchId, time: waitingTimeMinutes }));
3258
+ if (this.config.enableOpenDotaFallback)
3259
+ this.ctx.dota2tracker.opendotaAPI.requestParseMatch(pendingMatch.matchId).then((value) => this.logger.info(this.ctx.dota2tracker.i18n.gt(`dota2tracker.logger.opendota_parse_request_${value ? "sent" : "failed"}`, { matchId: pendingMatch.matchId })));
3260
+ }
3261
+ return;
3112
3262
  }
3113
3263
  } catch (error) {
3114
3264
  handleError(error, this.logger, this.ctx.dota2tracker.i18n, this.ctx.config);
@@ -3396,17 +3546,13 @@ function registerQueryMatchCommand(ctx) {
3396
3546
  return await handleQueryMatchCommand(ctx, ctx.config, session, options, match_id);
3397
3547
  });
3398
3548
  ctx.command("dota2tracker.query-recent-match [input_data]").alias("查询最近比赛").option("parse", "-p").option("template", "-t <value:string>").action(async ({ session, options }, input_data) => {
3399
- if (session.guild || !session.guild && input_data) {
3400
- const steamId = await resolvePlayerAndHandleErrors(ctx, session, input_data);
3401
- if (steamId === null) return;
3402
- session.send(session.text(".querying_match"));
3403
- const lastMatchId = await ctx.dota2tracker.player.getLastMatchId(Number(steamId));
3404
- if (!lastMatchId?.matchId) return session.text(".query_failed");
3405
- if (lastMatchId.isAnonymous) return session.text(".is_anonymous");
3406
- return await handleQueryMatchCommand(ctx, ctx.config, session, options, lastMatchId.matchId);
3407
- } else {
3408
- session.send(session.text(".user_not_in_group"));
3409
- }
3549
+ const steamId = await resolvePlayerAndHandleErrors(ctx, session, input_data);
3550
+ if (steamId === null) return;
3551
+ session.send(session.text(".querying_match"));
3552
+ const lastMatchId = await ctx.dota2tracker.player.getLastMatchId(Number(steamId));
3553
+ if (!lastMatchId?.matchId) return session.text(".query_failed");
3554
+ if (lastMatchId.isAnonymous) return session.text(".is_anonymous");
3555
+ return await handleQueryMatchCommand(ctx, ctx.config, session, options, lastMatchId.matchId);
3410
3556
  });
3411
3557
  }
3412
3558
  __name(registerQueryMatchCommand, "registerQueryMatchCommand");
@@ -3526,46 +3672,64 @@ __name(registerUserCommand, "registerUserCommand");
3526
3672
 
3527
3673
  // src/app/data/opendota.api.ts
3528
3674
  var import_koishi15 = require("koishi");
3675
+ var import_axios4 = __toESM(require("axios"));
3676
+ var import_https_proxy_agent3 = require("https-proxy-agent");
3677
+ var import_http = require("http");
3678
+ var import_https = require("https");
3529
3679
  var OpenDotaAPI = class extends import_koishi15.Service {
3530
3680
  static {
3531
3681
  __name(this, "OpenDotaAPI");
3532
3682
  }
3533
3683
  BASE_URL = "https://api.opendota.com/api";
3684
+ http;
3685
+ abortController = new AbortController();
3534
3686
  constructor(ctx) {
3535
- super(ctx, "opendota-api", true);
3687
+ super(ctx, "dota2tracker.opendota-api", true);
3536
3688
  this.config = ctx.config;
3689
+ this.http = import_axios4.default.create({ timeout: 1e4, signal: this.abortController.signal, baseURL: this.BASE_URL });
3690
+ ctx.on("dispose", () => this.dispose());
3691
+ }
3692
+ dispose() {
3693
+ this.abortController.abort();
3537
3694
  }
3538
3695
  async queryMatchInfo(matchId) {
3539
- const path5 = `${this.BASE_URL}/matches/${matchId}`;
3696
+ const path5 = `/matches/${matchId}`;
3540
3697
  const data = await this.fetchData("GET", path5);
3541
3698
  this.ctx.dota2tracker.cache.addOpendotaAPIRequestLog(path5, 1);
3542
3699
  return data;
3543
3700
  }
3544
3701
  async requestParseMatch(matchId) {
3545
- const path5 = `${this.BASE_URL}/request/${matchId}`;
3702
+ const path5 = `/request/${matchId}`;
3546
3703
  const job = await this.fetchData("POST", path5);
3547
3704
  this.ctx.dota2tracker.cache.addOpendotaAPIRequestLog(path5, 10);
3548
3705
  return job;
3549
3706
  }
3550
3707
  async fetchData(type, path5, data) {
3551
3708
  const config = {
3552
- responseType: "json",
3553
- proxyAgent: this.config.proxyAddress || void 0
3709
+ headers: {},
3710
+ httpAgent: void 0,
3711
+ httpsAgent: void 0
3554
3712
  };
3713
+ if (this.config.proxyAddress) {
3714
+ config.httpsAgent = new import_https_proxy_agent3.HttpsProxyAgent(this.config.proxyAddress);
3715
+ config.httpAgent = new import_https_proxy_agent3.HttpsProxyAgent(this.config.proxyAddress);
3716
+ } else if (this.config.OpenDotaIPStack === "ipv4") {
3717
+ config.httpAgent = new import_http.Agent({ family: 4 });
3718
+ config.httpsAgent = new import_https.Agent({ family: 4 });
3719
+ }
3555
3720
  if (this.config.OPENDOTA_API_KEY) {
3556
- config.headers = {
3557
- ...config.headers,
3558
- Authorization: `Bearer ${this.config.OPENDOTA_API_KEY}`
3559
- };
3721
+ config.headers["Authorization"] = `Bearer ${this.config.OPENDOTA_API_KEY}`;
3560
3722
  }
3561
- switch (type) {
3562
- case "GET":
3563
- return await this.ctx.http.get(path5, config);
3564
- case "POST":
3565
- return await this.ctx.http.post(path5, data, config);
3566
- // POST需要空数据占位
3567
- default:
3568
- throw new Error(`Unsupported HTTP method: ${type}`);
3723
+ try {
3724
+ let response;
3725
+ if (type === "GET") {
3726
+ response = await this.http.get(path5, config);
3727
+ } else {
3728
+ response = await this.http.post(path5, data, config);
3729
+ }
3730
+ return response.data;
3731
+ } catch (error) {
3732
+ processFetchError(error, "OpenDota", path5);
3569
3733
  }
3570
3734
  }
3571
3735
  };
@@ -3578,7 +3742,7 @@ var OpenDotaAdapter = class extends import_koishi16.Service {
3578
3742
  __name(this, "OpenDotaAdapter");
3579
3743
  }
3580
3744
  constructor(ctx) {
3581
- super(ctx, "opendota-adapter", true);
3745
+ super(ctx, "dota2tracker.opendota-adapter", true);
3582
3746
  }
3583
3747
  transform(_match) {
3584
3748
  determinePlayerPositions(_match);
@@ -3633,8 +3797,10 @@ var OpenDotaAdapter = class extends import_koishi16.Service {
3633
3797
  },
3634
3798
  dotaPlus: null,
3635
3799
  stats: {
3636
- networthPerMinute: _player.gold_t,
3637
- experiencePerMinute: _player.xp_t,
3800
+ networthPerMinute: _player.gold_t.map((g) => g + 600),
3801
+ // OpenDota一大缺陷,没有networth per minute类似的字段,只能通过gold_t累计获取金币曲线+600初始金币模拟,并且因为缺少扣除部分,随着时间延长偏差会逐渐增大。
3802
+ experiencePerMinute: _player.xp_t.slice(1).map((x, i, arr) => x - (arr?.[i - 1] || 0)),
3803
+ // OpenDota: xp_t为累计获取经验曲线,赋值给stratz的每分钟经验增量需要转换。
3638
3804
  campStack: [_player.camps_stacked],
3639
3805
  matchPlayerBuffEvent: [],
3640
3806
  killEvents: [],
@@ -3693,7 +3859,10 @@ var OpenDotaAdapter = class extends import_koishi16.Service {
3693
3859
  parsedDateTime: _match.start_time + _match.duration,
3694
3860
  startDateTime: _match.start_time,
3695
3861
  endDateTime: _match.start_time + _match.duration,
3696
- rank: (({ sum, count }) => count ? sum / count : 0)(_match.players.reduce((acc, player) => player.rank_tier != null ? { sum: acc.sum + player.rank_tier, count: acc.count + 1 } : acc, { sum: 0, count: 0 })),
3862
+ rank: ((avg) => {
3863
+ const num = Math.round(avg);
3864
+ return clamp(Math.floor(num / 10), 0, 8) * 10 + clamp(num % 10, 0, 5);
3865
+ })((({ sum, count }) => count ? sum / count : 0)(_match.players.reduce((acc, player) => player.rank_tier != null ? { sum: acc.sum + player.rank_tier, count: acc.count + 1 } : acc, { sum: 0, count: 0 }))),
3697
3866
  actualRank: 0,
3698
3867
  averageRank: 0,
3699
3868
  durationSeconds: _match.duration,
@@ -3703,11 +3872,13 @@ var OpenDotaAdapter = class extends import_koishi16.Service {
3703
3872
  ...determineLaneOutcome(_match),
3704
3873
  radiantKills: [_match.radiant_score],
3705
3874
  direKills: [_match.dire_score],
3706
- radiantNetworthLeads: _match.radiant_gold_adv,
3707
- radiantExperienceLeads: _match.radiant_xp_adv,
3875
+ radiantNetworthLeads: [0, ..._match.radiant_gold_adv],
3876
+ // 同样,此处的radiant_gold_adv也是基于累计获取金币的差值而非经济差。
3877
+ radiantExperienceLeads: [0, ..._match.radiant_xp_adv],
3878
+ // opendota两组数据都需要补充-1分钟时的数据对齐stratz格式。
3708
3879
  winRates: null,
3709
3880
  players,
3710
- pickBans: _match.picks_bans.map((pb) => ({ isPick: pb.is_pick, ...pb.is_pick ? { heroId: pb.hero_id, bannedHeroId: null } : { bannedHeroId: pb.hero_id, heroId: null }, order: pb.order })),
3881
+ pickBans: _match.picks_bans?.map((pb) => ({ isPick: pb.is_pick, ...pb.is_pick ? { heroId: pb.hero_id, bannedHeroId: null } : { bannedHeroId: pb.hero_id, heroId: null }, order: pb.order })),
3711
3882
  odParsed: true
3712
3883
  };
3713
3884
  return match;
@@ -3843,28 +4014,60 @@ function convertGameMode(openDotaGameModeId) {
3843
4014
  }
3844
4015
  }
3845
4016
  __name(convertGameMode, "convertGameMode");
4017
+ function assignSideLanePositions(sideLanePlayers) {
4018
+ sideLanePlayers.sort((a, b) => b.last_hits - a.last_hits);
4019
+ const pos1 = sideLanePlayers[0];
4020
+ const pos3 = sideLanePlayers[1];
4021
+ pos1.calculatedPosition = 1;
4022
+ pos3.calculatedPosition = 3;
4023
+ const supA = sideLanePlayers[2];
4024
+ const supB = sideLanePlayers[3];
4025
+ let pos4 = supA;
4026
+ let pos5 = supB;
4027
+ if (supA.lane === pos1.lane) {
4028
+ pos5 = supA;
4029
+ pos4 = supB;
4030
+ }
4031
+ pos4.calculatedPosition = 4;
4032
+ pos5.calculatedPosition = 5;
4033
+ }
4034
+ __name(assignSideLanePositions, "assignSideLanePositions");
3846
4035
  function determinePlayerPositions(match) {
3847
4036
  const players = match.players;
3848
- players.filter((p) => p.lane === 2).forEach((p) => {
3849
- p.calculatedPosition = 2;
3850
- });
3851
- const sideLanePlayers = players.filter((p) => p.lane !== 2);
3852
- const radiant = sideLanePlayers.filter((p) => p.isRadiant).sort((a, b) => b.last_hits - a.last_hits);
3853
- const dire = sideLanePlayers.filter((p) => !p.isRadiant).sort((a, b) => b.last_hits - a.last_hits);
3854
- for (const team of [radiant, dire]) {
3855
- team[0].calculatedPosition = 1;
3856
- team[1].calculatedPosition = 3;
3857
- const supA = team[2];
3858
- const supB = team[3];
3859
- const pos1 = team[0];
3860
- let pos4 = supA;
3861
- let pos5 = supB;
3862
- if (supA.lane === pos1.lane) {
3863
- pos5 = supA;
3864
- pos4 = supB;
3865
- }
3866
- pos4.calculatedPosition = 4;
3867
- pos5.calculatedPosition = 5;
4037
+ const radiantTeam = players.filter((p) => p.isRadiant);
4038
+ const direTeam = players.filter((p) => !p.isRadiant);
4039
+ for (const team of [radiantTeam, direTeam]) {
4040
+ if (team.length !== 5) {
4041
+ continue;
4042
+ }
4043
+ const midPlayers = team.filter((p) => p.lane === 2).sort((a, b) => b.last_hits - a.last_hits);
4044
+ let pos2Player = void 0;
4045
+ if (midPlayers.length > 0) {
4046
+ pos2Player = midPlayers[0];
4047
+ pos2Player.calculatedPosition = 2;
4048
+ }
4049
+ const sideLanePlayers = team.filter((p) => p !== pos2Player);
4050
+ if (sideLanePlayers.length === 4) {
4051
+ assignSideLanePositions(sideLanePlayers);
4052
+ } else if (sideLanePlayers.length === 5) {
4053
+ sideLanePlayers.sort((a, b) => b.net_worth - a.net_worth);
4054
+ const pos1 = sideLanePlayers[0];
4055
+ const pos2 = sideLanePlayers[1];
4056
+ const pos3 = sideLanePlayers[2];
4057
+ const supA = sideLanePlayers[3];
4058
+ const supB = sideLanePlayers[4];
4059
+ pos1.calculatedPosition = 1;
4060
+ pos2.calculatedPosition = 2;
4061
+ pos3.calculatedPosition = 3;
4062
+ let pos4 = supA;
4063
+ let pos5 = supB;
4064
+ if (supA.lane === pos1.lane) {
4065
+ pos5 = supA;
4066
+ pos4 = supB;
4067
+ }
4068
+ pos4.calculatedPosition = 4;
4069
+ pos5.calculatedPosition = 5;
4070
+ }
3868
4071
  }
3869
4072
  }
3870
4073
  __name(determinePlayerPositions, "determinePlayerPositions");
@@ -3928,10 +4131,45 @@ function determineIMP(player) {
3928
4131
  }
3929
4132
  __name(determineIMP, "determineIMP");
3930
4133
 
4134
+ // src/console.ts
4135
+ var import_path3 = require("path");
4136
+
4137
+ // src/app/console/data.ts
4138
+ var import_plugin_console = require("@koishijs/plugin-console");
4139
+ var CustomProvider = class extends import_plugin_console.DataService {
4140
+ // 临时存储dota2tracker对象
4141
+ constructor(ctx, tracker) {
4142
+ super(ctx, "apiCount");
4143
+ this.tracker = tracker;
4144
+ this.tracker = tracker;
4145
+ }
4146
+ static {
4147
+ __name(this, "CustomProvider");
4148
+ }
4149
+ async get() {
4150
+ return { opendota: await this.tracker.cache.getTodayOpendotaAPIRequestCount() };
4151
+ }
4152
+ };
4153
+
4154
+ // src/console.ts
4155
+ function registerConsolePage(ctx) {
4156
+ const tracker = ctx.dota2tracker;
4157
+ if (!tracker) {
4158
+ ctx.logger("dota2tracker").warn("dota2tracker service is missing during console registration!");
4159
+ return;
4160
+ }
4161
+ new CustomProvider(ctx, tracker);
4162
+ ctx.console.addEntry({
4163
+ dev: (0, import_path3.resolve)(__dirname, "../client/index.ts"),
4164
+ prod: (0, import_path3.resolve)(__dirname, "../dist")
4165
+ });
4166
+ }
4167
+ __name(registerConsolePage, "registerConsolePage");
4168
+
3931
4169
  // src/config.ts
3932
4170
  var import_koishi17 = require("koishi");
3933
4171
  var import_fs3 = __toESM(require("fs"));
3934
- var import_path3 = __toESM(require("path"));
4172
+ var import_path4 = __toESM(require("path"));
3935
4173
 
3936
4174
  // require("./locales/**/*.schema.yml") in src/config.ts
3937
4175
  var globRequire_locales_schema_yml = __glob({
@@ -3940,23 +4178,27 @@ var globRequire_locales_schema_yml = __glob({
3940
4178
  });
3941
4179
 
3942
4180
  // src/config.ts
3943
- var pluginDir = import_path3.default.resolve(__dirname, "..");
4181
+ var pluginDir = import_path4.default.resolve(__dirname, "..");
4182
+ var allI18nConfigs = Object.fromEntries(Object.keys(LanguageTags).map((lang) => [lang, globRequire_locales_schema_yml(`./locales/${lang}.schema.yml`)._config]));
3944
4183
  var Config = import_koishi17.Schema.intersect([
3945
4184
  import_koishi17.Schema.intersect([
3946
4185
  import_koishi17.Schema.object({
3947
4186
  STRATZ_API_TOKEN: import_koishi17.Schema.string().required().role("secret"),
3948
4187
  dataParsingTimeoutMinutes: import_koishi17.Schema.number().default(60).min(0).max(1440),
3949
4188
  proxyAddress: import_koishi17.Schema.string(),
3950
- suppressStratzNetworkErrors: import_koishi17.Schema.boolean().default(false),
3951
- enableOpenDotaFallback: import_koishi17.Schema.boolean().default(false)
3952
- }).i18n(Object.keys(LanguageTags).reduce((acc, cur) => (acc[cur] = globRequire_locales_schema_yml(`./locales/${cur}.schema.yml`)._config.base, acc), {})),
4189
+ suppressStratzNetworkErrors: import_koishi17.Schema.boolean().default(false).deprecated(),
4190
+ suppressApiNetworkErrors: import_koishi17.Schema.boolean().default(false),
4191
+ enableOpenDotaFallback: import_koishi17.Schema.boolean().default(false),
4192
+ enableConsole: import_koishi17.Schema.boolean().default(false).experimental()
4193
+ }).i18n(getI18n("base")),
3953
4194
  import_koishi17.Schema.union([
3954
4195
  import_koishi17.Schema.object({
3955
4196
  enableOpenDotaFallback: import_koishi17.Schema.const(true).required(),
3956
- OPENDOTA_API_KEY: import_koishi17.Schema.string().role("secret")
4197
+ OPENDOTA_API_KEY: import_koishi17.Schema.string().role("secret"),
4198
+ OpenDotaIPStack: import_koishi17.Schema.union(["auto", "ipv4"]).default("auto")
3957
4199
  }),
3958
4200
  import_koishi17.Schema.object({})
3959
- ]).i18n(Object.keys(LanguageTags).reduce((acc, cur) => (acc[cur] = globRequire_locales_schema_yml(`./locales/${cur}.schema.yml`)._config.base, acc), {}))
4201
+ ]).i18n(getI18n("base"))
3960
4202
  ]),
3961
4203
  import_koishi17.Schema.intersect([
3962
4204
  import_koishi17.Schema.object({
@@ -3971,7 +4213,7 @@ var Config = import_koishi17.Schema.intersect([
3971
4213
  })
3972
4214
  ).default([]).role("table"),
3973
4215
  rankBroadSwitch: import_koishi17.Schema.boolean().default(false)
3974
- }).i18n(Object.keys(LanguageTags).reduce((acc, cur) => (acc[cur] = globRequire_locales_schema_yml(`./locales/${cur}.schema.yml`)._config.message, acc), {})),
4216
+ }).i18n(getI18n("message")),
3975
4217
  import_koishi17.Schema.union([
3976
4218
  import_koishi17.Schema.object({
3977
4219
  rankBroadSwitch: import_koishi17.Schema.const(true).required(),
@@ -3980,12 +4222,12 @@ var Config = import_koishi17.Schema.intersect([
3980
4222
  rankBroadFun: import_koishi17.Schema.boolean().default(false)
3981
4223
  }),
3982
4224
  import_koishi17.Schema.object({})
3983
- ]).i18n(Object.keys(LanguageTags).reduce((acc, cur) => (acc[cur] = globRequire_locales_schema_yml(`./locales/${cur}.schema.yml`)._config.message, acc), {}))
4225
+ ]).i18n(getI18n("message"))
3984
4226
  ]),
3985
4227
  import_koishi17.Schema.intersect([
3986
4228
  import_koishi17.Schema.object({
3987
4229
  dailyReportSwitch: import_koishi17.Schema.boolean().default(false)
3988
- }).i18n(Object.keys(LanguageTags).reduce((acc, cur) => (acc[cur] = globRequire_locales_schema_yml(`./locales/${cur}.schema.yml`)._config.report, acc), {})),
4230
+ }).i18n(getI18n("report")),
3989
4231
  import_koishi17.Schema.union([
3990
4232
  import_koishi17.Schema.object({
3991
4233
  dailyReportSwitch: import_koishi17.Schema.const(true).required(),
@@ -3993,10 +4235,10 @@ var Config = import_koishi17.Schema.intersect([
3993
4235
  dailyReportShowCombi: import_koishi17.Schema.boolean().default(true)
3994
4236
  }),
3995
4237
  import_koishi17.Schema.object({})
3996
- ]).i18n(Object.keys(LanguageTags).reduce((acc, cur) => (acc[cur] = globRequire_locales_schema_yml(`./locales/${cur}.schema.yml`)._config.report, acc), {})),
4238
+ ]).i18n(getI18n("report")),
3997
4239
  import_koishi17.Schema.object({
3998
4240
  weeklyReportSwitch: import_koishi17.Schema.boolean().default(false)
3999
- }).i18n(Object.keys(LanguageTags).reduce((acc, cur) => (acc[cur] = globRequire_locales_schema_yml(`./locales/${cur}.schema.yml`)._config.report, acc), {})).description(void 0),
4241
+ }).i18n(getI18n("report")).description(void 0),
4000
4242
  import_koishi17.Schema.union([
4001
4243
  import_koishi17.Schema.object({
4002
4244
  weeklyReportSwitch: import_koishi17.Schema.const(true).required(),
@@ -4004,20 +4246,30 @@ var Config = import_koishi17.Schema.intersect([
4004
4246
  weeklyReportShowCombi: import_koishi17.Schema.boolean().default(true)
4005
4247
  }),
4006
4248
  import_koishi17.Schema.object({})
4007
- ]).i18n(Object.keys(LanguageTags).reduce((acc, cur) => (acc[cur] = globRequire_locales_schema_yml(`./locales/${cur}.schema.yml`)._config.report, acc), {}))
4249
+ ]).i18n(getI18n("report"))
4008
4250
  ]),
4009
4251
  import_koishi17.Schema.object({
4010
- template_match: import_koishi17.Schema.union([...readDirectoryFilesSync(import_path3.default.join(pluginDir, "template", "match"))]).default("match_1"),
4011
- template_player: import_koishi17.Schema.union([...readDirectoryFilesSync(import_path3.default.join(pluginDir, "template", "player"))]).default("player_1"),
4012
- template_hero: import_koishi17.Schema.union([...readDirectoryFilesSync(import_path3.default.join(pluginDir, "template", "hero"))]).default("hero_1"),
4252
+ template_match: import_koishi17.Schema.union([...readDirectoryFilesSync(import_path4.default.join(pluginDir, "template", "match"))]).default("match_1"),
4253
+ template_player: import_koishi17.Schema.union([...readDirectoryFilesSync(import_path4.default.join(pluginDir, "template", "player"))]).default("player_1"),
4254
+ template_hero: import_koishi17.Schema.union([...readDirectoryFilesSync(import_path4.default.join(pluginDir, "template", "hero"))]).default("hero_1"),
4013
4255
  playerRankEstimate: import_koishi17.Schema.boolean().default(true),
4014
4256
  templateFonts: import_koishi17.Schema.array(String).default([]).role("table")
4015
- }).i18n(Object.keys(LanguageTags).reduce((acc, cur) => (acc[cur] = globRequire_locales_schema_yml(`./locales/${cur}.schema.yml`)._config.template, acc), {}))
4257
+ }).i18n(getI18n("template"))
4016
4258
  ]);
4259
+ function getI18n(key) {
4260
+ return Object.fromEntries(
4261
+ Object.entries(allI18nConfigs).map(([lang, config]) => [
4262
+ lang,
4263
+ config[key]
4264
+ // 提取对应语言的对应类别
4265
+ ])
4266
+ );
4267
+ }
4268
+ __name(getI18n, "getI18n");
4017
4269
  function readDirectoryFilesSync(directoryPath) {
4018
4270
  try {
4019
4271
  const files = import_fs3.default.readdirSync(directoryPath);
4020
- const fileNames = files.filter((file) => import_path3.default.extname(file).toLowerCase() === ".ejs").map((file) => import_path3.default.basename(file, ".ejs"));
4272
+ const fileNames = files.filter((file) => import_path4.default.extname(file).toLowerCase() === ".ejs").map((file) => import_path4.default.basename(file, ".ejs"));
4021
4273
  return fileNames;
4022
4274
  } catch (error) {
4023
4275
  console.error("Error reading directory:", error);
@@ -4030,12 +4282,13 @@ __name(readDirectoryFilesSync, "readDirectoryFilesSync");
4030
4282
  var name = "dota2tracker";
4031
4283
  var usage = "";
4032
4284
  var inject = {
4033
- required: ["http", "database", "puppeteer", "cache"],
4034
- optional: ["cron"]
4285
+ required: ["database", "puppeteer", "cache"],
4286
+ optional: ["cron", "console"]
4035
4287
  };
4036
- var pluginDir2 = import_path4.default.resolve(__dirname, "..");
4037
- var pluginVersion = require(import_path4.default.join(pluginDir2, "package.json")).version;
4288
+ var pluginDir2 = import_path5.default.resolve(__dirname, "..");
4289
+ var pluginVersion = require(import_path5.default.join(pluginDir2, "package.json")).version;
4038
4290
  async function apply(ctx, config) {
4291
+ const logger = ctx.logger("dota2tracker");
4039
4292
  ctx.dota2tracker = {};
4040
4293
  ctx.dota2tracker.i18n = new I18NService(ctx);
4041
4294
  ctx.dota2tracker.image = new ImageRenderer(ctx, pluginDir2);
@@ -4051,7 +4304,7 @@ async function apply(ctx, config) {
4051
4304
  await ctx.dota2tracker.parsePolling.polling();
4052
4305
  });
4053
4306
  } else {
4054
- ctx.logger("dota2tracker").info(ctx.dota2tracker.i18n.gt("dota2tracker.logger.cron_not_enabled"));
4307
+ logger.info(ctx.dota2tracker.i18n.gt("dota2tracker.logger.cron_not_enabled"));
4055
4308
  }
4056
4309
  ctx.dota2tracker.hero = new HeroService(ctx);
4057
4310
  ctx.dota2tracker.item = new ItemService(ctx);
@@ -4074,6 +4327,7 @@ async function apply(ctx, config) {
4074
4327
  registerQueryHeroCommand(ctx);
4075
4328
  registerQueryItemCommand(ctx);
4076
4329
  registerHeroOfTheDayCommand(ctx);
4330
+ if (config.enableConsole) registerConsolePage(ctx);
4077
4331
  }
4078
4332
  __name(apply, "apply");
4079
4333
  // Annotate the CommonJS export names for ESM import in node: