// ==UserScript== // @name 易学·超星学习通助手[AI答题][一键启动][最小化运行] // @namespace 易学团队 // @version 4.1.11 // @description 易学学习助手:超星课程任务管理、智慧树视频学习辅助,查询并核对参考答案。 // @author 易学团队 // @homepageURL https://yx.zy1314520.eu.org/ // @updateURL https://raw.githubusercontent.com/zxkj2022/yixue-chaoxing-assistant/main/dist/%E6%98%93%E5%AD%A6%E8%B6%85%E6%98%9F%E5%AD%A6%E4%B9%A0%E9%80%9A%E5%8A%A9%E6%89%8B.user.js // @downloadURL https://raw.githubusercontent.com/zxkj2022/yixue-chaoxing-assistant/main/dist/%E6%98%93%E5%AD%A6%E8%B6%85%E6%98%9F%E5%AD%A6%E4%B9%A0%E9%80%9A%E5%8A%A9%E6%89%8B.user.js // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAACIISURBVHhe5Zt3dFzFvcfvrlwly6pWb9vUJduSu407tnEBTCghQCBADNjUhJKEQCBAIIFHAiEQID0k5xFC72CwsY1tuciS1XtZ9dVKK6202pW0+3ln5t5dFZuX9/+753zPzPym/b7fKXfuvbuKoihbFUX5xf9jKM8pisL/Y0gVphvPC2PILG5MjeKlRSl8vMrMobXpHFqbwdENmRzfmMXxTdkc35xN0YVauDmLos0iLkI1rqazZdljm7IC8eOijXUZHFuXwfH1GTJdJPNFuyqObcjiuMD6TI6tz+Tougy+FtiQycerLby4KIUbUqNIC551ju//C/6zAGsjQ3hnqQHXzoVw+VK4QoOI+3HlMvj28gl8ZwVcPSl9lQhXaJhsW67WFW1ctkRDIXxridrHlVrbVyxTbX6IcrtFXOtf5Gs+DW1fyBtL0lgREXIOl/PgmwWYq9fxUm4i7FwElxbi27WY0V2L8VxcgOcSDbsKZHrUn760UGJUg0wLu6gjysmyWv4lWp6/nZ2L8exYhGf7IjUUdXYtxr1zMe7ti3BvysSzLR/3RWoZtyijQZYR8Z2L8O1crPq7YxHPZicyS687h9t/FCBh1gyKVlrgkkLGti/GfdFCzbHFqqMBh1Wn3TLU8jSbGoq6i1Sn/dguyhWooWhbkJlUxh+X2LaIkS0LcV+Yj+fKjYysMuPenC/TbmHfqmGbKKvGPZptfNtC2FXI4eUWYmYGncPxGwWInTWDytUZsKtAbVhzZMLJxaqjwrZVCzV4RFo4pmHkwoWaw5rNH25ZpMYn2wUpjZioN7I5H9emfFwbcnEtSWHswzcZ/cVPcOXFMrI+l5ENeYxszGNkQ64absrDvTEP96Z8Cb+NHYWUrEwn6vwiTBUgSKfjq0IjbC+YIOEnpJGTzkkHNQgnN6oYCYTCMRUuiXxc6/NwrVPTopyEn8TGCbssK+qsy2X4glyGLshhOCeOsTf+jrjct3yHoawYmTe8JofhNdlaqELUC2BtjmyTbQV8vNAwnfy5AjyYsgC2Lsbtd3CKoxNEXes1CELC0bUCeZOgOu/H0JochlbnMrQ6h6E1wpbH8Lp8htdNDvNwiboiX5RblY1zRTbO5VkMpsfief5XUgDvkBPX1btwWmJwLlfzJVZmSwwtS2coL4mh3ASG1y2UPo6szYVNi7grMeqbBUiePRPn6hzG1/mJ+UPNMQ2uALk8NZTO+gnm4hQQaT9W5qhEVogwB6dIC0cn1RlaJaCWV/NzGFyWxcCSTBwFGTgMCxh55CdSAJ+AvZehnZtwGGMZLMxkIDeNAXMsg+lxONcsZGTv9Yy9/yYjD93L0CKDbHdsdS69K7KImTnj/AI8nhwDa/NxrcpheLUgJqaY5qAGv111UhDV4hq5QeG4gIgvy2ZwebYaLs1icGk2Axqk7ZsgyizJwlGQSf+iDPryM7CnxjB8122qAD6fFMHb0cHg+hX0JUYwsHElrvvvYvSDt/F2dcpy45XlDC5OZ7AgHecyMVOy4YJ8fpy04FwBZup0VOSZ8K3MZXhFDkMaoaGV2uiIUKSXi7xcnMtzJLmBZSr85EQ4uESMXJZKdkk2A4VaWoQFglgWDr9tqRqXNolMHIsy6F+YQX9+Bn156fTlZWBPjsF5zZXq6AsBfF6VZHMTo1/uxzs8FJgdcpn02nBesISBrBQGluZIP4RvvhW5nMwxoJ8uQO7c2XgKM3EVZuFcmi0Vk2QElucwIAgvm4AkJkZJQJCRyFbJLM7EsXiCVL+Ia7b+RQJiZDNVkosEYdUmRlqSFiOel449y0yvIRF7fCR9ceEM7fmeKoAUwatCIxywafGhm7+LIy0m0L/0oSCLocVZDC7MwDR75lQBro6YD4XZDCwSoyRGTR05R0G2rCjTIi4aWjQB2bg/7ie3MJO+aRA2adcI9uVn0pebgV1AjLAIcwTS6TUl0xMXRW9qIgNbNzL8xKOMHfsa7/CwStKrwS9CIK3K4Xrxt9jjIukXfcj+s+gXEOm8THz5WeyaP2+qAPdFR8LCbBz5amGHHBltJCcRFY3052fJhiREo36bZrf7kasRzRNTeRLZfJGXSa8gm52BLTuDHlMa3bHR2FIScFy4keEnn2D05Al8Y2MTIyymtp+416vGJ4khLs+BL6VwvRkmtb+AyJmqwNkZkJvFrRHhUwV4MDoK8rLoy9VGRkxDqWCWhD1fgyijlZMNitCPHBW92RpEPDcrkCfI9uZkYMvNxJaTSU9mOt1JCXRFR2FblM/gffcyevTrwLSeMrX/N0wa/cHbbqUnLFT2bcsWUPsMpDMzICebOyMjpwrw46goqYwkIRwWimmEJSQRDTlaOUFKIoveLLVxP3oyRToTm7BnZajw51nMdMXG0BUfh33nDlx/+yvewcEAY6/Virevb8qaDoz05Kk/HWJTbGrClpVFT2oqtowMejK0fqV/mTLty8rijoiIqQL8RAggiYh16B9VlagczaxJ0MiJuF2ms7BlZNKTnhGATYOaTldhMtMVvYDu5CQcN9yA5+DBwEgLx91vvIHnw4/wtrYGZsG4zYbX4Qjc+v7TjBCX67V/0BkaTo/JQo85nR5L+oQvlnR8md8kQFYWvZkZUgQ5qpog0iYIC2SosKVn0itIWibQYxYdqOgWHcvORdxCV7QY8QQcN3+f0VOnAsTHSkoYfvQRRn7za8aqqgLEx5qaGPrNb3B/8AG+Ie0W53TidYmNUL0NytmglVeFEXY13ffta+mcH0W3KV2iRwuFX76MbO4Iny5AZBRIYun0ZqiExRQSEGlJ0pxBr0WFiItGJWkZz6DbpIb+zkTYFZdAZ0Q0/VdehefYcc1dcf9uZvD2fQzu+T6jRRN23+goQ489Qv8lO/F8+WXAPvLRR/RfehlepzMwC2Q7vb14TpzA53YHykp7Wztdael0xCbTnWah25hOt9FCt8GC15zJHWHTBPhxRCReOYrqdJmYMhpZY7qETQuno9uQTpdBhGpnXckGOuaF07tyDSPvvDvFOeeTv6B32VJcf3h1it1z+BD2woU4rtiN124P2Ad/9QxW3Ux6d+2Waf+G5+3uxnbBetojouleuYa+2/Yx9No/GK2tk/mud96jPSKWzmQjnWkWOg0WOlMtjBkyuH3+tLvAj8IjGRcjZzSrU9esThc5bQzp9KRa6Ek1051qmUCKma5kDSKeYqYz1UxH2AI6Y5Jw/vxxfEPDASKjJSXYV67AvnkT442NAbu4hp58gu45M3A+cO+E0eulf9+dWGeESDgefiSQNd7RQc+qtbSHRtKRYqR9QSJtIRFYg8NpSzDQtWErjod+TtfCpbTHp9KRKnyz0JFiZjQ1ndtDpwnwQFgEY3IUzXQbzDLsElMnzaKSSzIFyHZrYUeSiY4EDYkmOuLSaJ81H9u6C/GcODlBRBD83e/onBfC4N13T7GLDa7/O1fTpSi4nv7lpAwv9utvxhoUQnu8AevscIb+9g+ZNd5qpXvJSkm4PdlER4qJjmSTjAu0JaZhjYzDOi+KjiSjJC3QKcJkM56UdPbNC5smwHwhQIYkLaeLgKikkRWhRJKKriSLSjreRHuCmbbwRNrmRDLwk4fxeTwBHj6Xi74bb6JDURh+4QXVpuWNt7XRu249nSLvqV9MybPffg+t+hDa4o1Y4420hcUyWlcv13Zn/lLaQqJoTzbTniRIm6dhkk3LF8Tbk1R4kr9BgFGxaaSIdaKplWKRFSXpRDMdk9AuEG+iLc6EdU407bFGRt5U13qAYGcXto2bJfmRv/9tal5rK12Fy2lXFAb33T4lb/DFV2lRQrDGpkny1uhkOhcuZ+RYER2LV9I6N4q2RBPWRKMMVZjVMEmEmn8CCWKAJspYE82MJKWzL2TaErg/NAJPsoV2MaqaUmoobGY6EtTG2gRpDdY4Ey1B4XTmLmP0bLlGQqUx3tKKrXC5HF3XK6+oef7Nq9dO9/I1WJWZ2FavxSfO+Bp5T00drZHJWKOSscapoy+E6MgupD2rgJbgBVhl30ZaRZggYFYRb5K2VlnPJBHwNV7km2W+KyGdvecIMC8Cd6KFtikkzSriTLTFqqMtR1xDi24+3Wu2MN7VrRLz35raO+hZvFyOvPPBhzRhtBH2+bBdchVW3Tzaw+PwHC+aIk7P9/bSrJuvCawJIEY7RqzrJJW0Jn6rBmucWY3HmmiJMUqIuESMFsaaaV1gomWBiaFYC7cFT1sC94VEMJKQrpKLnYBowCoaEaFMGyWalVB6Nl+MTzvCikOJFME5RPeazbQpCo4duwOHFT9Bx9PP0awEY50RTv8ebepreaNt7TTHmGkOT9ZIGGkRfWpEW/xE/WQlYY1cjIAgaKRZEBXxGJOMS0QbaYpS4Vxg4bY50wS4NziC4ThLQDXZgGgoWgu1dPMCI01KGN0bd+Fzaic0eQzVRvA7N9GizKY7xsB4XUMgXxKsb6QlPIXWkATaQhMZLdWWjZbvfPsDGnXhqrOiH62/AAlByE8sxizzm0TZaJNWVk03akSlXca1MNJIU4SRwSgLt55PgKEYs9a42qBoKNCYFm/QR9JeuA5vv0N1ftKjqOP5l2lQgmlV5uF8TH2BOVkc2x3306DMo2V2DD3rdqj5kwQQ9euV+dLR6X0HCEnCalzk10caaRB+RanpBpGWMEmbhGhPwkRjhAlHpIVbZ08XYG4EzgVmmmUnojGTrBRoMMJIw6xYmhOyGG1qlg57x714x8dl3FPXQMP8ZBqDomlPzWO8r18lpy2B8X4Hzcn5NMxNoFEJo/8HD04IpC2f/hf/RJ0yX+0rwqBCiqH5o5GR+ZFG6iMmQ+SZpCB1ms1fRm1voqwQ4JbpAvxwTgSD0Ra1kqxo0qBVnJdCw8xYXF98pZIfG5PwaQK0X34jtUooDUoYfT/4qUpufFyWEZfr8DHq9TE0zE+TJB1P/1Ytoz3iimv4i0PUKpHUh6ZRH26UqBOhJGCS8NuFb3VhRurCDNTON1AbZpBlBWq1UObLuCkAkdcXcR4BfjAnAkekmbpwg6xYOxnzjVQrodifeFZ12u3GNzKCd2REdfzoCap0UdTNSaZeF83wp+pDjNftxqsdipxvvEetEkHdfAM1ynz6Hn16QqTxcblMxoeHaTAvp1q/gNpQA7WhadSI8gKCaJiJmjCjZjNSI8r480NVCF9FXrUWyvJhpil17WFmbpk1XYDZEfRHmCcUDTRgolKJpGXdxdJZcc/2DgzgE9DuAK1X30K5EkrtrCSaIiyM1jfA2Kj6+Ko9yjrf/pAqJYKaeUKAcLq+vUcVQJtJ/pnS96d/claZS9XcVKpCDFTPM1A1z0BlSJq0Vc6Ip1yJolwJp1wJo0IJp0qJplqfQE2Ikep5RqpCjVQLzDdpQphUaHbbfDN7pgtw96xwqYxfcVFRFK6ck0xlcDIjRSfBMyKfwLw9KnAO4qmpoTTEQIVwQBdPS/Jixtvb8Q0O4O3txdffJ8VwnThNxYxEqmanUB0UT1NKgfpoK57hxUwRs2psVBX0utspVmZTNiuV8hnJlCkRlCmhVIkZlr6Cls2X037tXjr2/JCOG+7EuuMaGs0rqZmVQmWwkcoQIxXzjFQKMaYj1EhPqIXvz5wuwMxweqVSBqpDNdXnGTirhNF1z4MwNoLX2oq3pUlFazN4PXQ98zzHFYVSZR4Vyizq9QmMiufz9lZ8ti58zgEYduIddFCds46zSjSVc1KpUOYz8Md/qktlaAjfsMCwfB8grpZb7+OEEkRZSBrNF19H3+//grusEq/LJfOnX55mKzWhFipmJFM+10BZiDGAikmCCHG65pnPFeCemRH0hmrTRFNLjFh1fB5jFWX4rM14a6vx1lRJ0NGKp+gYtdsuo3L9Ftpu/xE9jz6D/ZfP43rjTUZe/xcjr/0D979eZ/SLz/H12eh54Q+cUOZTOjOVUiWGmrQl6nP/qBufox/cIzA+xuDpYrr+9Dc6fvkc7vqpj83+E6WcOf73g1peQ8E2SpVQSpU4zs42cHaukbK5RsrnGqkInkBHsJmbZkyfATMisIWYqAo2SIipdFYJp+eBn0G3Fe/ZMypKi6GxlqGXX6Hrxn2MtlinOCguv5PiPf5YUzOjhw7jfuttPO+8RWVqAceVuRTrUzilzKP56lvVSt4xhktKaHv6WTr+8Bfc1raJ9nw+uVF6tfOEbN/jmfSSVLXXXbuP8tyVNORtpEKJo0xgjpnSORNCCLQHm7kxaJoAdwWF0x1spnKuWPcGyoOSqYrKZvTwQXxni/GeOIa36Gu8RUehtoLODdtxvvm+5pA2Gtrt7HyXdLq/H88XB6jZuJsiZT6nlChOKeF03Pswtr/9ndYnn8ZVWz+lTiCujbj70Z8xvG0z3u4u1T7pIFb36FPU/PQx+bzhOvA1jTkbKNHHc3a2kdLZRsqEEHOMtM+xnCvAnUKAOSYq5xgpn22gRImm7YqboOI03kNf4jt6CN/hL/F+8A6eP75Kc/4axmx2daS1URDx0U8+wvP3v+J+711Gi4oYb7MGNrfJl/2Ndyk1FnJcmcnpaDP29z6cXkS+MB3Yvo3h+38o06MnT2LXKziWFuL+Yj9jNdWq+JoAPZ/s59SFlwTqu15/m0olnOIZRkpnCZgkrLPM3Kg/jwCds82UzzZydoaBM7oEnM+/AEe+wHfwMzwv/x7XQ4/i+etrdN//EFVbL5OdyOmpnfbEBmXPzsIWGY7jW5fRv2snfVu3Yt++g77rr8f561/jOXYs8LVntLePiiuuZbisMuD08Gv/oG/1KjyffwbDQ9gLFtMZFET/LbfQU1BIx+zZdKWk0D5rFr2bNweWm7jcPTaOF65lrKtLHFOhuhJXfDqlQamcnmHkzCwTZ2YaaZlpOVeAO/ThdMw0UzbTSImSRGXCErxvvw77P8L93HM47rif8Q512lXvu4emR5+UcUHeKzoTO3F1DW2x8XSIN8c33kzv9/dgv3UvfXtvx37rbVjjEmmeE4L71OkAYds7HzAuboeaKCOffII1aBZtsQl0bt6K1WCmY1EBtt2X4XzqKdz7P2fkwAGssQlYjRbGxQcUbRmK68Tai+j7+BNwD+G12/Cu3UprUALH9QZOzRAw0jTDwvd00wXQhdM+w0TZDCPFShzNGy6F/e8x/udX6N+yA/fBQyph5wCncpfhLC6ZEEA7Dg/+69806mfRvmY9tnvupfeOu+ndewc9N+6h6+rraEwUDy3xeCoqVJd9Xgbfeld+/BDrduTMGawrVtMiXq1feTWOp36F65NPcR0+gvOtd+gTG+RV36F9525GDh/BU1Ornh8m7T/lN91B00M/h+EBfEN9ePbchV1ZQFGQgSK9gZNBBhqCzFx/PgE6g0xUiPWixNNz/a0M3L4PW4oBx8ZN+Ozd4BrE8emnnMpfgc+r/UjBqx5lxTV88Cvq50dTH50onRRouXAnrTu/hfXSK6lQQqiONzHW043P45YY+ehjxsWhStQ/cIBKRaH7lr0M/PstOvbspSGngLoEcSSPlXkCtp88LMv7L3mc1gRoe/kvVOy4HPq78PW04Xn+RRxKDKUz0jimT+PENwlwpy6criATlUEGKoOMNC9ajzU6hZ7waIZ/8QQM9MJwPw1776Lmxr1qx0J5jbx42mvafillikJtkoXux5/C9tuX6Hv1j9iff5HuB3+G9cpr6LznARjz4HP04Rty4v7iC/UXHW4Xnuoqeu65l4FX/0h9WgYt2y6m/6VX6P/jX6hOz6E6xUTfq3/CXVuL499v0fngQ/Q+/7vAPiQu55mzlC+8AF9DFb7WekY//wxHsJG6oFSO6dI4oVcFuOF8AvQEmagWAugN1M5KoT0mTb4XHP3qC2hvgo5mivOW0vuOumPLaefzMVxcQnmqhRJlFl0/fpiqlAxOKgpno1MoTUynxJxH5QVbaL7rPnr/+3XGrK34+u0SnkNf4e1ow2e34XNMfAxhZEieDVzlZZRlFnBKCeZsnImSeCOnlSCKlSBKlNk0XXRJ4CwgZuT4yAj1+etxf7kfn7UBb30VfRkX0KyIfSCNIn0a9XrTuQLcrQvHHmSkNiiNGr2BpuA0WubE0rNlF76WWmiqwfX5J5xOy2VsQH0IEtNfdDzw2X7aH/4Zw0eO0vnkM/Jo3LBjN56qakoyF3FYUai5aDfFiRl8qSg03naXerTu6WRU/PDB2oyvpxNvbzfDx47S+atnqNp2CSdC4ziiKBxTQjg6M5Sy5etof/jn9L3+Bq7TxXK3l0+c2mO5/0DUueXbOF94GXraoK+Tvt030iLOHDqxDFJpOJ8A9+jC6NOLqZJGfVAazXPTaFSiGPzFk9BWC211dP3oJ9RtvVwdff/pbFzdvb2eUaquuo6DgvzV1zHW0cHpNRv5TKT37JNlSnd8i48VhfofPABuJ962ZsZOHMXb0gC2TkbrqimaG8VRRUdpTgEtd9zDwKefcWbdVj4Qwj2u3nkmXz6xB/kF8L9YuecRHN+/Gxzdci8YePxZmpRISqQAad8kQDj9mgANQWk06pNpCrMw+vmH+MpOQH0pTUvW0fvcy1rH6vqX4dgYjT99hCOJSTTc9UOcxcUcTEzlbUWh4oab5YMQA/0cSDZLW9vvfgeDdrxNtYydOoa3sQavtYnxxlrsr72Gu6QYhgbkncLrGebrdZt4TVE4efW1at+uIXwjLnUjFY/dfj/8n8f/8TaOtZfhs7XJZSu+EdYrSZxVUr95CfxQF86A3ijJ1+vTqFEW0LFmJ5Qdw3fyK8Y+f5+2hHzGu22T9D//NVBURPmdd9L4y6flhvfV4iV8mJzMG4rCfysKve++DbYOvHWVjAsBasvxyk2rAZx94OiFcTdNL7wgy/9TUXgvOZme/Z9P7+q8l7e7F0feJrxlZ/DVVjB2uoj6BYspVRKlAHV607l3gT26UJx6A41SoTQqlSj6H3gQzhyE0wdwPfY4NvMq+j7aT/Njv6L25jupvvwGKi++hprd36Xxhn1Yf/o4PX9+jRHtbbD/avnTH/lqyxbeSUrkixUrGK2vxtdch7e6jPHTx/BWnMFbU4a3oRpfayPeDqvcEAdPHKfqicfoP3Fcnux8I2569x+k8anfUHHD7ZzZcgWlKy6ionArDcu207rpKmw3/ADn07/HnrSMsffew1teDFUlNK66hGIlhpNSACO7ddN+JHWhMpcBvYFmIYCSRN0cE57X/4LvwHv4jn/K4KXX0GPMp/miS2m58Sa6H34Y+/PP0/eHP8hbXc+zz9F2/0+pv+q7VKzdQvWGi2i58z6cBw8HhBiT5FrxtYtH6wq8VaV4zxzHW3ZKjddX4Wupl3sD4twhLx/d/36X0suu5Uj6Eo7mrODMtsup3ftDrI8/je35l+h/6VUGfvM7+h96HPsNe7Gv2Yk9Lgf3s8/hO3sCKk/TdtNdnFCiKdYbqNIbWKLMnipAvBJEiz6FTr3Y/GJoL9iE78A7eD96He8nbzD655fwfvxvOPkFnP0aKk9CQxl0NYGzF3zq+0F5eYZxFRfT+ctnqNt+GQ2XfJv+f70lX6CI84S3qU5Of2/lGVWA0iJ8lSXSJpfBuBt3q5Wa+x/icM5STohb6JPPMCR+WSL3Bu3yjsBQH9ja5VrHWgfNVVBbyviBTxh7/228hz6HU4fpe+oZTimxVOkMHNenMF/RTxVA4C1dDMN6Ay1KNAO37IODb+N99zW8H76O7+B7cPhDKPoczh6BiiK8ZScYKy7CffQII4cOM3zoCK5jx3GXlQdOd+IaLj5D682303zlTYycPCUPVGK0vYJ0SRHe0hP4Ks9AZzO+oQFan/gvjmcvofKWOxiqrp4g7BvD29/LWEebPEuMtVvxdrZBVxt0t0JbA9SVQ8lxfEVf4T30GeOfvY/v4CeM/P2vVMw00aWk8rIuxk9+qgCXK8GgpNGtS2L0uafhyzfhwDtw5AO8n72J52+vMPizR7BdcxNta3bQbF5OXVQWNcFpVM9OomZuIvVhKTTGW2jJKsS6bhvdN+/D8dd/4m6yMvDpVzRdtYfel/8Mg73quj97Up2mtlY8Z8/SsPZi6q/bw0hjIz67naG338d2/yO0in1m+Vbqs1ZRm1ZIVepiKizLOZu3jso1u6i/7Hu03P0g3c+9iPP9dxk/cQRKj8OxL/Dt/4Dxj96iLXkpA0oSm3Vzzy/ATEVHkRKNN6EQ3+H38H76BsNPPob94ivoNCzGOiOeFiWMViWM9plRdEcmYE810J+VwWBBLsMrF+Fat4SRTcvwbFnO6NZluDcVMHzBQgY2rcZx370MvP8RTXseoPNXv5WHFF/ZKehoYOSz/ViXbKP/pT8z/NYHdO2+luZIAw3iS5ESSnNQJK3BsbRHJNIRl0J3ioEeoxGb2US30UBbYjLNUfHURiZSkZBOReE6mq67BdtzL+L5/GMoOYJv8VY+UsLQTZCfKoDAGkXPeO4Ghm65i/bEfFqVaFqVCFqVGFpnJ9M2P42OCAOdkUY6osy0RJhpDLdQH26hJsxCVXgG1ZGZ1MRm02DIpy1/CbZ1axjefSFj39nC2Pd2MvRfj1N90TX0vPJXcHYxeuQwHYUX0fuDh+jedAmt+jDaZkfTGZ1KV7xJ/iJF/FJF/kZJ/DotKxt7Xi79BfkMrCjAuWYJrvXLcG9Zydj21YxdtAr3xiU4lubRsWghLRu24LjzRwzlbmKhcs7/h6YKIHCfMh+UODqUJFr1RpqCjNQHGagTzwi6NGoVFdVKGhVKKuVKGmWKeItkoFgxcFoxcFIxcEJJo0hJ5biSygmRPz+D+uwl2HdswnH9FVSkr2LkdDE96y6nM62AnjgjvZFx2FMs9CRb6BDf86PFFyHxvcJEzTwT1cEmKueaKZ9jomy2mbLZJsrnmKkISaciLIOayEwa47JpS8ujN7eQwVVrcOQWMqQk8l3lvP8iO1cAgWd0UaBPp0dvpkVvpklvkicocQ+t0RupCjJRruGs3kRpkImSIBPFQSZO6Y2c1pso1ps4HaTilN4kRTiupHBMSaE8KofSWQbqTGtxRJsZSTXgSDHRHmOmLtRE+WwTZ/QGinXi4cfIGcVEqSIe1cULGyNndCaK/VAExHsMUU68yhcDIw5zaViVNIYUI/crgZ/FTcf5BRC4WwljUG/Grc+gXW+hRW+R5+g6vZkavZkqvZmKIDNleiGCmdIgMyVBZs7oTZQIUTSbardIu8BpvZlTiiBmokZJYkhsmuEWSmdMIqIzam2oAos+yvVmyrW4sKvtWzgbJOxmKvUmqqV/Jlr1Zlz6dLr05slvf86HbxZAQBwYPtAl4tKlM6rPwi7FSKdFn06TPp0GvYV6vYU6vYVaiXQJkfZDtVuo0VAtEJROtT6der2Zep2ZcsVEhc5MtV6FKOevJ9oQ/TTI/kQdf3v+vlRbs96CTZ+BW5+JQ2fhn7p4spT/+C/S/10APy5Q5vKsLobjujTadOn06TIY0GUyoM9iUJ+FQ59Fnz5zEjJk2K+lhXACIi3gkGEG/TqBdBm36zLoFdDK+us7dBk49AKiP9Ge6EuFXQtt+kwa9RYO6FJ5XBdN4cRJ7z/h/yaAH+IWkqrMZIUylw1KMBt1IRLrdMGsVoJZpcxltTJXC4NZpZvLKpE3BWqZ6RB1pmN6GRXBrNUFs14XwnolhA26EJbr5pKkTPkz1P8V/7//Pf4/wiq6Qj68WdAAAAAASUVORK5CYII= // @run-at document-end // @storageName 易学团队 // @match *://*/* // @grant unsafeWindow // @grant GM_xmlhttpRequest // @grant GM_info // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @grant GM_registerMenuCommand // @grant GM_notification // @connect mooc1.wljx.hfut.edu.cn // @connect mooc1.chaoxing.com // @connect mooc1-1.chaoxing.com // @connect mooc1-api.chaoxing.com // @connect zhibo.chaoxing.com // @connect pstore-rk.chaoxing.com // @connect kshlsplay-rk.chaoxing.com // @connect ksrtmpplay-rk.chaoxing.com // @connect mooc1.hnust.edu.cn // @connect stat2-ans.hnust.edu.cn // @connect stat2-ans.istudy.szpu.edu.cn // @connect stat2-ans.chaoxing.com // @connect stat2-ans.wht.zjelib.cn // @connect mooc1.wht.zjelib.cn // @connect passport2.wht.zjelib.cn // @connect mooc1.hnsyu.net // @connect passport2.xust.edu.cn // @connect stat2-ans.hnsyu.net // @connect passport2.hnsyu.net // @connect mooc1.gdhkmooc.com // @connect stat2-ans.gdhkmooc.com // @connect mooc1.zut.edu.cn // @connect passport2.hncj.edu.cn // @connect stat2-ans.zut.edu.cn // @connect passport2.jxjyzx.xust.edu.cn // @connect passport2.zut.edu.cn // @connect stat2-ans.wljx.hfut.edu.cn // @connect live-rk.chaoxing.com // @connect task.chaoxing.com // @connect passport2.wljx.hfut.edu.cn // @connect mooc1.hncj.edu.cn // @connect stat2-ans.hncj.edu.cn // @connect mooc1.qutjxjy.cn // @connect stat2-ans.qutjxjy.cn // @connect passport2.hnust.edu.cn // @connect passport2.cqrspx.cn // @connect passport2.gdhkmooc.com // @connect passport2.qutjxjy.cn // @connect mooc1.jxjyzx.xust.edu.cn // @connect mooc2-ans.chaoxing.com // @connect passport2.hnvist.cn // @connect passport2.cugbonline.cn // @connect passport2.chaoxing.com // @connect stat2-ans.jxjyzx.xust.edu.cn // @connect mooc1.xueyinonline.com // @connect passport2.ynny.cn // @connect passport2.ecnusole.com // @connect stat2-ans.xueyinonline.com // @connect static-rk.chaoxing.com // @connect passport2.xueyinonline.com // @connect lbapi-rk.chaoxing.com // @connect mooc1.cqrspx.cn // @connect stat2-ans.cqrspx.cn // @connect f1-rk.chaoxing.com // @connect mooc1-2.chaoxing.com // @connect mooc1.ynny.cn // @connect mqtt7-rk.chaoxing.com // @connect stat2-ans.ynny.cn // @connect mooc1.cugbonline.cn // @connect stat2-ans.cugbonline.cn // @connect mooc1.xust.edu.cn // @connect stat2-ans.xust.edu.cn // @connect mooc1.xynu.edu.cn // @connect mooc1.ntu.edu.cn // @connect stat2-ans.xynu.edu.cn // @connect mobilelearn.chaoxing.com // @connect k.chaoxing.com // @connect stat2-ans.ntu.edu.cn // @connect mooc1.hnvist.cn // @connect stat2-ans.hnvist.cn // @connect passport2.xynu.edu.cn // @connect passport2.ntu.edu.cn // @connect mooc1.ecnusole.com // @connect stat2-ans.ecnusole.com // @connect mooc.s.ecust.edu.cn // @connect webvpn.ahmu.edu.cn // @connect 127.0.0.1 // @connect tk.sanzwk.com // @license MIT // ==/UserScript== // Generated from src modules. Edit src/, then run the build. (function () { 'use strict'; const modules = { "app.js": function(module, exports, require) { 'use strict'; const createQuestionBankClient = require('./core/question-bank'); const createCourseAccess = require('./core/access'); const { createSettings } = require('./core/settings'); const { createTransport } = require('./core/transport'); const { classifyPage, readCourseContext, consoleUrl } = require('./core/routes'); const { claimCourseSurface, isManagedQuestionFrame } = require('./core/ownership'); const { createCollection } = require('./core/collection'); const { toText, abortError, disposableScope } = require('./core/primitives'); const { createUi } = require('./ui/index'); const { createCourseRunner } = require('./platform/course-engine'); const { createQuestionWorkflow, collectQuestions } = require('./questions/index'); const { isReviewDocument } = require('./questions/reader'); const { isReviewPath } = require('./questions/collection-answer'); const { classifyZhihuishu, createZhihuishuStudy } = require('./platform/zhihuishu'); const { createCourseActivity } = require('./core/course-activity'); const { createChapterVisits } = require('./platform/chapter-visits'); const { prepareVideoSession } = require('./platform/video-session'); async function bootstrap() { if (isManagedQuestionFrame(window)) return; if (document.documentElement.dataset.yixueBooted) return; document.documentElement.dataset.yixueBooted = '1'; const zhihuishu = classifyZhihuishu(location.href); const route = zhihuishu === 'questions' ? 'exam' : zhihuishu ? 'zhihuishu-' + zhihuishu : classifyPage(location.href); if (route === 'entry' && !await claimCourseSurface(window, document)) return; const store = { read: (key, fallback) => GM_getValue(key, fallback), write: (key, value) => GM_setValue(key, value) }; const settings = createSettings(store); const bank = createQuestionBankClient({ request: details => GM_xmlhttpRequest(details), getValue: store.read, setValue: store.write, toText }); const access = createCourseAccess({ questionBank: bank, getValue: store.read, setValue: store.write }); const context = readCourseContext(location.href, { title: document.title, cookie: document.cookie, document, serverHost: unsafeWindow.ServerHost?.mooc1Domain }); if (route === 'dashboard' && context.origin !== location.origin) { location.replace(consoleUrl(context)); return; } const transport = createTransport({ base: location.origin, pageUrl: location.href, send: details => GM_xmlhttpRequest(details), createXhr: () => new XMLHttpRequest() }); if (route === 'dashboard') { document.body.replaceChildren(); document.title = context.courseName + ' · 易学助手'; } const collection = createCollection({ bank, store, collect: collectQuestions, document, hostname: location.hostname }); const ui = createUi({ document, window, bank, access, settings, onTokenSaved: () => collection.send() }); const lifetime = disposableScope(); let activeController, questionFrame; lifetime.listen(window, 'pagehide', () => { activeController?.abort(); collection.dispose(); ui.destroy(); lifetime.dispose(); }); function menu(name, handler) { if (typeof GM_registerMenuCommand === 'function') GM_registerMenuCommand(name, handler); } menu('设置搜题 Token', () => ui.requestApiToken()); const reviewPage = isReviewPath(location.href) || (['work', 'exam', 'other'].includes(route) && isReviewDocument(document)); const questionPage = reviewPage || ['work', 'exam', 'zhihuishu-study'].includes(route) || /\/(?:work\/phone\/work|modules\/work\/index\.html)/.test(location.pathname); if (questionPage) { void collection.send().catch(() => {}); let timer; const observer = new MutationObserver(records => { if (!records.some(record => !record.target.parentElement?.closest('#yixue-ui-root') && record.target.id !== 'yixue-ui-root')) return; clearTimeout(timer); timer = setTimeout(() => { void collection.send().catch(() => {}); }, 500); }); observer.observe(document.documentElement, { subtree: true, childList: true, characterData: true, attributes: true, attributeFilter: ['class', 'style', 'hidden', 'aria-hidden', 'data-answer-status'] }); lifetime.add(() => { clearTimeout(timer); observer.disconnect(); }); } if (route === 'dashboard') { const favicon = document.createElement('link'); favicon.rel = 'icon'; favicon.href = require('./ui/logo'); (document.head || document.documentElement).append(favicon); } if (reviewPage) return; function openChapterQuestions(href, { signal } = {}) { if (questionFrame?.dataset.retained === 'true') { questionFrame.dataset.previous = 'true'; questionFrame.querySelector('iframe').style.minHeight = '360px'; } else questionFrame?.remove(); const section = document.createElement('section'); section.className = 'yixue-chapter-question-view'; section.style.cssText = 'max-width:1300px;margin:16px auto;padding:16px;background:white;border:1px solid #e9eaf0;border-radius:20px'; const frame = document.createElement('iframe'); frame.name = 'yixue-managed-chapter'; frame.title = '章节测验核对区'; frame.style.cssText = 'width:100%;min-height:640px;border:0'; section.append(frame); document.getElementById('yixue-ui-root').append(section); questionFrame = section; return new Promise((resolve, reject) => { if (signal?.aborted) { section.remove(); reject(abortError()); return; } let finished = false; const timer = setTimeout(() => finish(new Error('章节测验页面加载超时,请在学习通页面打开后核对。')), 15000); const cancel = () => { section.remove(); finish(abortError()); }; const finish = (error, result) => { if (finished) return; finished = true; clearTimeout(timer); signal?.removeEventListener('abort', cancel); error ? reject(error) : resolve(result); }; signal?.addEventListener('abort', cancel, { once: true }); frame.onload = () => { try { if (!frame.contentDocument) throw new Error('无法读取章节测验,请在原页面处理。'); if (frame.contentDocument.URL === 'about:blank') return; finish(null, { document: frame.contentDocument, window: frame.contentWindow, close: () => section.remove(), retain: () => { section.dataset.retained = 'true'; }, release: () => { delete section.dataset.retained; if (section !== questionFrame) section.remove(); }, }); } catch (error) { finish(error); } }; frame.onerror = () => finish(new Error('章节测验页面加载失败。')); const target = new URL(href, context.origin); if (context.prefix && !target.pathname.startsWith(context.prefix)) target.pathname = context.prefix + target.pathname; frame.src = target.href; }); } if (route === 'zhihuishu-study' || route === 'zhihuishu-entry') { const controller = new AbortController(); activeController = controller; let study; const panel = ui.showQuestionPanel({ title: '智慧树学习助手', onPause: paused => study?.pause(paused), onConnect: () => ui.requestApiToken() }); if (route === 'zhihuishu-entry') { panel.log('请打开具体课程的学习页面,再点击开始播放。'); return; } const actions = document.createElement('div'); actions.className = 'yixue-question-actions'; const workflow = createQuestionWorkflow({ bank, settings, transport, unsafeWindow, onLog: panel.log, waitIfPaused: panel.waitIfPaused, onCollect: collection.send }); for (const [text, callback] of [['开始播放', () => { void panel.setPaused(false, { notify: false }); study.start(); }], ['下一资源', () => study.next()], ['停止播放', () => { study.stop(); void panel.setPaused(false, { notify: false }); }], ['查询当前练习', async () => { try { panel.setSummary(await workflow.run(document, { signal: controller.signal })); } catch (error) { panel.log(error.message, 'warning'); } }]]) { const button = document.createElement('button'); button.type = 'button'; button.className = 'yixue-panel-button'; button.textContent = text; button.addEventListener('click', callback); actions.append(button); } panel.element.querySelector('.yixue-question-body').prepend(actions); const statuses = { stopped: '已停止', paused: '已暂停', blocked: '等待处理平台提示', waiting: '等待播放器', playing: '正在播放', ended: '播放结束', finished: '已遍历完成', switching: '正在切换资源' }; study = createZhihuishuStudy({ document, window, settings, signal: controller.signal, emit: event => { if (event.type === 'log') panel.log(event.message, event.tone); if (event.type === 'state') panel.element.querySelector('[role="status"]').textContent = statuses[event.status] || '等待开始'; } }); lifetime.add(() => study.destroy()); panel.log('已识别智慧树学习页面,点击“开始播放”运行;播放结束后切换目录资源,进度以平台记录为准。'); return; } if (route === 'entry') { if (!await ui.requestCourseAccess()) return; const activate = selectors => { const target = document.querySelector(selectors); if (target) target.click(); else alert('未找到课程入口,请从学习通页面打开对应功能。'); }; ui.showEntry({ onCourse: () => { window.top.location.href = consoleUrl(context); }, onHomework: () => activate('.zy, a[href*="getAllWork"], a[href*="work/list"]'), onExam: () => activate('.ks, a[href*="exam-list"], a[href*="examnotes"]'), onVisits: () => { store.write('doxxcs', true); const tab = document.querySelector('[dataname="zj"], a[href*="studentstudy"]'); if (tab) tab.click(); else alert('请打开课程的章节页面,学习次数辅助会在那里继续。'); }, }); return; } if (route === 'dashboard') { if (!access.hasAccess() && !await ui.requestCourseAccess()) return; let surface, running = false; const run = async ({ challenge }) => { if (running) return; running = true; surface.setRemaining(null); surface.setProgress(0); surface.setSummary({}); surface.setTask({ name: '正在读取课程目录', chapter: '', status: 'running' }); const scope = disposableScope(); const controller = new AbortController(); activeController = controller; try { if (!context.courseId || !context.classId) throw new Error('课程参数不全,请从课程首页重新进入控制台。'); if (challenge) { for (const key of ['doVideo', 'doDocument', 'doLive', 'doWork']) settings.set(key, true); surface.refreshSettings(); } let backgroundNotified = false; scope.listen(document, 'visibilitychange', () => { if (document.hidden && !backgroundNotified) { backgroundNotified = true; surface.log('页面已转入后台,实际运行速度受浏览器限制,请留意任务日志。', 'warning'); } }); const activity = createCourseActivity({ window, context, onConflict: event => { if (event.sameCourse) controller.abort(); surface.log(event.sameCourse ? '检测到同一课程重复运行,已停止当前任务,请仅保留一个运行页面。' : '检测到其他课程正在运行,请避免同账号并行执行。', 'warning'); } }); const started = activity.start(); scope.add(() => activity.destroy()); if (!started && activity.duplicate()) throw new Error('同一课程正在其他页面运行,请先停止另一个页面再试。'); if (document.getElementById('abc-helper-root')) surface.log('检测到 ABC 助手同时运行,请仅启用一份同类脚本后再测试,避免互相控制播放器或重复查询。', 'warning'); surface.log('开始查询任务'); const emit = event => { if (controller.signal.aborted) return; if (event.type === 'log') surface.log(event.message, event.tone); if (event.type === 'progress') surface.setProgress(event.percent); if (event.type === 'remaining') surface.setRemaining(event.seconds); if (event.type === 'counts') surface.setCounts(event); if (event.type === 'summary') surface.setSummary(event); if (event.type === 'task') surface.setTask(event); }; const questions = { openPage: openChapterQuestions, run: (page, options) => createQuestionWorkflow({ bank, settings, transport, unsafeWindow: page.defaultView || unsafeWindow, onLog: (message, tone) => surface.log(message, tone), onCollect: collection.send }).run(page, options), }; await activity.runExclusive(() => createCourseRunner({ context, transport, settings, bank, questions, emit, prepareVideoSession: course => prepareVideoSession(document, course) }).run({ challenge, signal: controller.signal })); } catch (error) { surface.log((error.name === 'AbortError' ? '任务已中止' : '任务运行失败:' + error.message) + '。处理后可点击启动重试。', 'danger'); } finally { scope.dispose(); if (controller.signal.aborted) surface.setRemaining(null); running = false; if (activeController === controller) activeController = null; } }; surface = ui.showDashboard({ courseName: context.courseName, onRun: run, onConnect: () => ui.requestApiToken() }); const installedVersion = typeof GM_info === 'object' ? GM_info?.script?.version : ''; surface.log('脚本初始化成功(版本 ' + (installedVersion || '未知') + '),点击启动按钮开始刷课', 'info'); if (!bank.getToken()) surface.log('本地随机 Token 未连接题库,视频、文档可先使用;章节答题将跳过,请填写题库 Token 后再启用。', 'warning'); return; } if (['work-list', 'exam-list', 'work', 'exam'].includes(route)) { const panel = ui.showQuestionPanel({ title: route.startsWith('exam') ? '考试参考答案' : '作业参考答案', onConnect: () => ui.requestApiToken(), onRefresh: () => bank.refreshBalance({ force: true }), onAiToggle: enabled => settings.set('doAI', enabled) }); if (route.endsWith('-list')) { panel.log(route === 'work-list' ? '脚本准备就绪,请进入作业' : '脚本准备就绪,请进入考试'); return; } if (!bank.getToken()) { panel.log('题库未连接,请保存题库 Token 后刷新页面。', 'warning'); return; } const controller = new AbortController(); activeController = controller; const workflow = createQuestionWorkflow({ bank, settings, transport, unsafeWindow, onLog: panel.log, waitIfPaused: panel.waitIfPaused, onCollect: collection.send }); const state = document.createElement('p'); state.dataset.yixueQuestionState = ''; state.style.cssText = 'margin:8px;font:13px sans-serif'; panel.element.append(state); try { const result = route === 'exam' ? await require('./questions/navigation').runQuestionPages(workflow, document, { signal: controller.signal, waitIfPaused: panel.waitIfPaused, onPage: pageResult => panel.setSummary(pageResult) }) : await workflow.run(document, { signal: controller.signal }); panel.setSummary(result); state.textContent = result.stopped ? '查询已停止,请查看日志核对题目加载、账户或额度。' : result.answered ? '已填写 ' + result.answered + ' 道参考答案,请人工核对。' : '没有确认写入参考答案,请查看题目日志。'; panel.log(result.answered ? route === 'work' ? '本次参考答案填写结束,请核对后保存或提交。' : '本次参考答案填写结束,请核对后继续,最终交卷由你确认。' : '本次没有确认填写答案,请检查未命中或未写入的具体原因。', result.answered ? 'info' : 'warning'); } catch (error) { panel.log('答题流程已停止:' + error.message, 'danger'); } finally { if (activeController === controller) activeController = null; } return; } if (route === 'study' && store.read('doxxcs', false)) { let visits; const panel = ui.showQuestionPanel({ title: '章节访问辅助', onPause: paused => visits.pause(paused) }); visits = createChapterVisits({ document, window, store, context, onLog: panel.log }); const stop = document.createElement('button'); stop.type = 'button'; stop.className = 'yixue-panel-button'; stop.textContent = '停止章节访问'; stop.addEventListener('click', () => { store.write('doxxcs', false); visits.stop(); panel.log('章节访问已停止。'); }); panel.element.querySelector('.yixue-question-body').prepend(stop); visits.start(); } } module.exports = { bootstrap }; }, "core/access.js": function(module, exports, require) { 'use strict'; function createCourseAccess({ questionBank, getValue, setValue, randomValues = bytes => crypto.getRandomValues(bytes) }) { const getLocalToken = () => { const value = getValue('yixueLocalToken', ''); return typeof value === 'string' && value.length === 32 && /^[a-f0-9]{32}$/.test(value) ? value : ''; }; const getRealToken = () => { const value = questionBank.getToken(); return questionBank.isValidToken(value) ? value : ''; }; const getDisplayToken = () => getRealToken() || getLocalToken(); const createLocalToken = () => { const token = Array.from(randomValues(new Uint8Array(16)), byte => byte.toString(16).padStart(2, '0')).join(''); setValue('yixueLocalToken', token); return token; }; const saveToken = value => { const token = String(value ?? '').trim(); if (questionBank.isValidToken(token)) { questionBank.setToken(token); return token; } if (token && token === getLocalToken() && !getRealToken()) return token; throw new Error('请填写题库提供的 Token;本地随机 Token 不能用于查题。'); }; return { getLocalToken, getDisplayToken, createLocalToken, saveToken, hasAccess: () => Boolean(getDisplayToken()) }; } module.exports = createCourseAccess; }, "core/collection.js": function(module, exports, require) { 'use strict'; const digest = require('./md5'); const { delay } = require('./primitives'); function createCollection({ bank, store, collect, document, hostname, now = () => Date.now(), wait = delay, onStatus }) { const keys = { token: 'yixueSubmissionToken', owner: 'yixueSubmissionOwner', seen: 'yixueSubmissionSeen', history: 'yixueSubmissionHistory', retry: 'yixueSubmissionRetry', provision: 'yixueSubmissionProvision', next: 'yixueSubmissionNext' }; const controller = new AbortController(), flights = new Map(); let tail = Promise.resolve(); const readTime = (key, owner) => { const value = store.read(key, null); return value?.owner === owner && Number.isFinite(value.until) ? value.until : 0; }; const recordTime = (key, owner, milliseconds) => store.write(key, { owner, until: now() + milliseconds }); const history = () => { const value = store.read(keys.history, []); return Array.isArray(value) ? value.filter(item => typeof item === 'string').slice(-200) : []; }; const report = event => { try { onStatus?.(event); } catch { /* A UI callback cannot invalidate an accepted batch. */ } }; function enqueue(body) { const origin = bank.base(), identity = bank.getToken(), owner = digest(origin + '\n' + identity); const fingerprint = digest(JSON.stringify(body)), marker = owner + ':' + fingerprint; const current = () => !controller.signal.aborted && bank.base() === origin && bank.getToken() === identity; if (flights.has(marker)) return flights.get(marker); const task = tail.then(async () => { if (!current() || history().includes(marker) || now() < readTime(keys.retry, owner)) return; if (store.read(keys.owner, '') !== owner) { store.write(keys.token, ''); store.write(keys.owner, owner); } let token = store.read(keys.token, ''); if (!/^sub_[\w-]+$/.test(token)) { // A short shared lease prevents several course frames provisioning at once. if (now() < readTime(keys.provision, owner)) return; recordTime(keys.provision, owner, 20000); const response = await bank.provisionSubmission(); if (!current()) return; store.write(keys.provision, null); if (response.status !== 200 || !/^sub_[\w-]+$/.test(response.body?.token || '')) { recordTime(keys.retry, owner, response.status === 429 ? 3600000 : 60000); return; } token = response.body.token; store.write(keys.token, token); store.write('yixueSubmissionConsent', true); } const launchAt = Math.max(now(), readTime(keys.next, owner)); store.write(keys.next, { owner, until: launchAt + 3500 }); const pause = launchAt - now(); if (pause > 0) await wait(pause, controller.signal); if (!current() || history().includes(marker) || store.read(keys.token, '') !== token) return; const response = await bank.submitQuestions(token, body, 'page-' + fingerprint); if (!current() || store.read(keys.token, '') !== token) return; const answerCount = body.questions.filter(item => item.answer).length; const confirmed = !answerCount || (response.body?.answer_collection_version === '1' && response.body?.accepted_answer_count === answerCount); if (response.status === 201 && confirmed) { store.write(keys.seen, marker); store.write(keys.history, [...history().filter(item => item !== marker), marker].slice(-200)); if (answerCount) report({ state: 'answers-accepted', answers: answerCount }); } else { if (response.status === 401) store.write(keys.token, ''); recordTime(keys.retry, owner, [403, 422].includes(response.status) && !answerCount ? 3600000 : 60000); if (answerCount) report({ state: response.status === 422 || (response.status === 201 && !confirmed) ? 'answers-unsupported' : 'upload-failed' }); } }).catch(() => { if (current()) recordTime(keys.retry, owner, 60000); }).finally(() => flights.delete(marker)); flights.set(marker, task); tail = task; return task; } function send(target = document) { if (controller.signal.aborted || !bank.getToken()) return Promise.resolve(); const sourceHost = target.location?.hostname || hostname; // Snapshot before queuing: a paginated exam may change its DOM while uploading. const questions = collect(target).filter(item => typeof item.title === 'string' && item.title.trim() && item.title.length <= 5000 && Array.isArray(item.options) && item.options.length <= 50).map(item => ({ title: item.title, options: item.options.map(String), type: String(item.type || 'unknown').slice(0, 40), ...(['published', 'graded_correct'].includes(item.answer_source) && item.answer_format === 'text' && typeof item.answer === 'string' && item.answer.trim() && item.answer.length <= 10000 ? { answer: item.answer.trim(), answer_format: 'text', answer_source: item.answer_source, analysis: typeof item.analysis === 'string' ? item.analysis.trim().slice(0, 20000) : '' } : {}), ...(item.assets?.length ? { assets: item.assets.slice(0, 20).flatMap(asset => { const alt = String(asset.alt || '').slice(0, 300); if (asset.kind === 'formula' && typeof asset.formula === 'string') return [{ kind: 'formula', formula: asset.formula.slice(0, 2000), alt }]; if (asset.kind !== 'image') return []; try { const url = new URL(asset.url); if (url.protocol !== 'https:' || url.username || url.password || !(url.hostname === sourceHost || /\.(?:chaoxing\.com|xueyinonline\.com)$/.test(url.hostname))) return []; url.search = ''; url.hash = ''; return url.href.length <= 2000 ? [{ kind: 'image', url: url.href, alt }] : []; } catch { return []; } }) } : {}), })); const tasks = []; for (let index = 0; index < questions.length; index += 100) { const batch = questions.slice(index, index + 100); tasks.push(enqueue({ consent: true, source_host: sourceHost, ...(batch.some(item => item.answer) ? { answer_collection_version: '1' } : {}), questions: batch })); } return Promise.all(tasks); } return { send, dispose: () => controller.abort() }; } module.exports = { createCollection }; }, "core/course-activity.js": function(module, exports, require) { 'use strict'; function createCourseActivity({ window, context, onConflict } = {}) { const uid = String(context?.uid || '').trim(); const courseId = String(context?.courseId || '').trim(); const classId = String(context?.classId || '').trim(); const key = 'yixue-active-course:' + encodeURIComponent(uid); const lockKey = 'yixue-course-run:' + JSON.stringify([uid, courseId, classId]); const owner = window?.crypto?.randomUUID?.() || Date.now().toString(36) + Math.random().toString(36).slice(2); const expiryMs = 35000; const now = () => window?.Date?.now?.() ?? Date.now(); let active = false, timer = null; const warned = new Set(); function decode(raw) { try { const state = JSON.parse(raw); if (state?.version !== 1 || !Array.isArray(state.leases)) return []; return state.leases.filter(lease => typeof lease?.owner === 'string' && typeof lease.courseId === 'string' && typeof lease.classId === 'string' && Number.isFinite(lease.at) && lease.at <= now() + 5000 && now() - lease.at < expiryMs).slice(-128); } catch (_) { return []; } } function report(leases) { const peers = leases.filter(lease => lease.owner !== owner); const present = new Set(peers.map(lease => lease.owner)); for (const peer of peers) { if (warned.has(peer.owner)) continue; warned.add(peer.owner); try { onConflict?.({ courseId: peer.courseId, classId: peer.classId, updatedAt: peer.at, sameCourse: sameCourse(peer) }); } catch (_) {} } for (const previous of warned) if (!present.has(previous)) warned.delete(previous); } function sameCourse(lease) { return lease.owner !== owner && lease.courseId === courseId && lease.classId === classId; } function duplicate() { if (!window || !uid || !courseId) return false; try { return decode(window.localStorage.getItem(key)).some(sameCourse); } catch (_) { return false; } } function duplicateError() { return Object.assign(new Error('同一课程已在其他标签页运行,请先停止该标签页的任务。'), { retryable: false }); } function heartbeat() { if (!active) return false; try { const leases = decode(window.localStorage.getItem(key)); report(leases); const others = leases.filter(lease => lease.owner !== owner); others.push({ owner, courseId, classId, at: now() }); window.localStorage.setItem(key, JSON.stringify({ version: 1, leases: others })); return true; } catch (_) { return false; } } function changed(event) { if (active && event.key === key) report(decode(event.newValue)); } function destroy() { if (!active) return; active = false; if (timer !== null) window.clearInterval(timer); timer = null; window.removeEventListener('storage', changed); window.removeEventListener('pagehide', destroy); window.removeEventListener('beforeunload', destroy); try { const leases = decode(window.localStorage.getItem(key)).filter(lease => lease.owner !== owner); if (leases.length) window.localStorage.setItem(key, JSON.stringify({ version: 1, leases })); else window.localStorage.removeItem(key); } catch (_) {} warned.clear(); } function start() { if (active) return true; if (!window || !uid || !courseId) return false; try { const leases = decode(window.localStorage.getItem(key)); report(leases); if (leases.some(sameCourse)) return false; } catch (_) { return false; } active = true; if (!heartbeat()) { active = false; return false; } window.addEventListener('storage', changed); window.addEventListener('pagehide', destroy); window.addEventListener('beforeunload', destroy); timer = window.setInterval(() => { if (!heartbeat()) destroy(); }, 10000); return true; } async function runExclusive(operation) { const execute = async () => { if (duplicate()) throw duplicateError(); // Also support callers that have not registered an activity lease. const startedHere = !active && start(); if (!startedHere && duplicate()) throw duplicateError(); try { return await operation(); } finally { if (startedHere) destroy(); } }; let locks; try { locks = window?.navigator?.locks; } catch (_) {} if (courseId && typeof locks?.request === 'function') { // The browser owns this lock until the entire operation settles, // including background timer delays and rejected task promises. return locks.request(lockKey, { ifAvailable: true }, lock => { if (!lock) throw duplicateError(); return execute(); }); } return execute(); } return { start, destroy, duplicate, runExclusive }; } module.exports = { createCourseActivity }; }, "core/md5.js": function(module, exports, require) { 'use strict'; // Independent implementation of the RFC 1321 algorithm, for host protocol compatibility. // Specification: https://www.rfc-editor.org/rfc/rfc1321 (not its reference C implementation). const rotations = [[7, 12, 17, 22], [5, 9, 14, 20], [4, 11, 16, 23], [6, 10, 15, 21]]; const constants = Array.from({ length: 64 }, (_, index) => Math.floor(Math.abs(Math.sin(index + 1)) * 0x100000000) >>> 0); function md5(value) { const input = new TextEncoder().encode(String(value)); const size = Math.ceil((input.length + 9) / 64) * 64; const bytes = new Uint8Array(size); bytes.set(input); bytes[input.length] = 128; const view = new DataView(bytes.buffer); const bits = BigInt(input.length) * 8n; view.setUint32(size - 8, Number(bits & 0xffffffffn), true); view.setUint32(size - 4, Number(bits >> 32n), true); const state = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476]; for (let offset = 0; offset < size; offset += 64) { let [a, b, c, d] = state; for (let step = 0; step < 64; step++) { const round = Math.floor(step / 16); let mixed, word; if (round === 0) { mixed = (b & c) | (~b & d); word = step; } else if (round === 1) { mixed = (d & b) | (~d & c); word = (5 * step + 1) % 16; } else if (round === 2) { mixed = b ^ c ^ d; word = (3 * step + 5) % 16; } else { mixed = c ^ (b | ~d); word = (7 * step) % 16; } const sum = (a + mixed + constants[step] + view.getUint32(offset + 4 * word, true)) >>> 0; const shift = rotations[round][step % 4]; const next = (b + ((sum << shift) | (sum >>> (32 - shift)))) >>> 0; [a, b, c, d] = [d, next, b, c]; } [a, b, c, d].forEach((word, index) => { state[index] = (state[index] + word) >>> 0; }); } return state.map(word => Array.from({ length: 4 }, (_, index) => ((word >>> (index * 8)) & 255).toString(16).padStart(2, '0')).join('')).join(''); } module.exports = md5; }, "core/ownership.js": function(module, exports, require) { 'use strict'; function isManagedQuestionFrame(windowObject) { try { for (let current = windowObject, depth = 0; current && depth < 12; depth++) { if (current.name === 'yixue-managed-chapter') return true; if (current.parent === current) break; current = current.parent; } } catch { /* Cross-origin ancestors cannot be inspected. */ } return false; } async function claimCourseSurface(windowObject, documentObject) { const tag = 'data-yixue-course-owner'; if (documentObject.documentElement.hasAttribute(tag)) return false; const bus = 'yixue-course-ui-owner-v1'; documentObject.documentElement.setAttribute(tag, 'pending'); if (windowObject.top === windowObject) { documentObject.documentElement.setAttribute(tag, 'top'); windowObject.addEventListener('message', event => { const data = event.data; if (data?.channel !== bus || data.type !== 'probe' || typeof data.id !== 'string' || data.id.length > 100) return; try { event.source?.postMessage({ channel: bus, type: 'owner', id: data.id }, event.origin === 'null' ? '*' : event.origin); } catch {} }); for (let index = 0; index < windowObject.frames.length; index++) { try { windowObject.frames[index].postMessage({ channel: bus, type: 'ready' }, '*'); } catch {} } return true; } const nonce = crypto.randomUUID(); let delegated = false; await new Promise(resolve => { const timer = setTimeout(resolve, 450); windowObject.addEventListener('message', event => { if (event.source !== windowObject.top || event.data?.channel !== bus) return; if (event.data.type !== 'ready' && !(event.data.type === 'owner' && event.data.id === nonce)) return; delegated = true; documentObject.documentElement.setAttribute(tag, 'delegated'); windowObject.dispatchEvent(new Event('yixue-course-ui-suppressed')); clearTimeout(timer); resolve(); }); try { windowObject.top.postMessage({ channel: bus, type: 'probe', id: nonce }, '*'); } catch { clearTimeout(timer); resolve(); } }); if (!delegated) documentObject.documentElement.setAttribute(tag, 'frame'); return !delegated; } module.exports = { claimCourseSurface, isManagedQuestionFrame }; }, "core/primitives.js": function(module, exports, require) { 'use strict'; const abortError = () => Object.assign(new Error('任务已中止'), { name: 'AbortError' }); function delay(milliseconds, signal) { return new Promise((resolve, reject) => { if (signal?.aborted) return reject(abortError()); let timer; const cancel = () => { clearTimeout(timer); signal?.removeEventListener('abort', cancel); reject(abortError()); }; signal?.addEventListener('abort', cancel, { once: true }); timer = setTimeout(() => { signal?.removeEventListener('abort', cancel); resolve(); }, Math.max(0, milliseconds)); }); } const escapeText = value => String(value ?? '').replace(/[&<>"']/g, symbol => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[symbol]); function toText(value, documentObject = globalThis.document) { if (typeof documentObject?.createElement !== 'function') documentObject = globalThis.document; if (!documentObject) return String(value ?? '').replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim(); const fragment = documentObject.createElement('template'); fragment.innerHTML = String(value ?? '').replace(/<(?:br|\/p|\/div|\/li)\b[^>]*>/gi, ' '); fragment.content.querySelectorAll('script,style').forEach(element => element.remove()); return (fragment.content.textContent || '').replace(/\s+/g, ' ').trim(); } function parseCookie(name, cookie = globalThis.document?.cookie || '') { const entry = cookie.split(';').map(part => part.trim()).find(part => part.startsWith(name + '=')); if (!entry) return ''; try { return decodeURIComponent(entry.slice(name.length + 1)); } catch { return entry.slice(name.length + 1); } } function disposableScope() { const cleanups = []; return { add(cleanup) { cleanups.push(cleanup); return cleanup; }, listen(target, event, callback, options) { target.addEventListener(event, callback, options); cleanups.push(() => target.removeEventListener(event, callback, options)); }, dispose() { for (const cleanup of cleanups.splice(0).reverse()) { try { cleanup(); } catch {} } }, }; } module.exports = { delay, escapeText, toText, parseCookie, abortError, disposableScope }; }, "core/question-bank.js": function(module, exports, require) { 'use strict'; // Standalone transport: the page code below consumes a normalized answer string. function createQuestionBankClient({ request, getValue, setValue, toText = value => String(value ?? ''), now = () => Date.now() }) { const defaultBase = 'https://tk.sanzwk.com'; const tokenKey = 'shenchanranToken'; const pendingKey = 'yixuePendingSearches'; let blocked = null; let usage = '尚未查询题目'; let balance = null, balanceError = '', balanceIdentity = '', balanceEpoch = 0; let balanceFlight = null, balanceAttempt = null, balanceRetryAt = 0, balanceUpdatedAt = 0; const balanceListeners = new Set(); let capability = null, capabilityError = '', capabilityIdentity = '', capabilityFlight = null, capabilityAttempt = null, capabilityRetryAt = 0, capabilityUpdatedAt = 0, capabilityEpoch = 0; const aiReasons = { account_disabled: '账户未开启 AI,请到题库账户设置开启', plan_disallowed: '当前套餐不允许 AI', service_unavailable: 'AI 服务未就绪', unsupported_model: '当前 AI 模型暂不支持', insufficient_points: 'AI 所需积分不足', not_requested: '本次未请求 AI', answer_found: '普通题源已命中,无需 AI', cache_hit: '使用 AI 缓存结果', interrupted: 'AI 查询已中断', query_error: '查询异常,未完成 AI 查询', none: 'AI 可用' }; const notifyBalance = () => { for (const listener of balanceListeners) { try { listener(); } catch (_) {} } }; const resetBalance = () => { capabilityEpoch++; capability = null; capabilityError = ''; capabilityIdentity = ''; capabilityFlight = null; capabilityAttempt = null; capabilityRetryAt = 0; capabilityUpdatedAt = 0; balanceEpoch++; balance = null; balanceError = ''; balanceIdentity = ''; balanceFlight = null; balanceAttempt = null; balanceRetryAt = 0; balanceUpdatedAt = 0; notifyBalance(); }; const uuid = () => Array.from(crypto.getRandomValues(new Uint8Array(16)), n => n.toString(16).padStart(2, '0')).join(''); const validBase = value => { const url = new URL(String(value).trim()); const local = ['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname); if ((url.protocol !== 'https:' && !(local && url.protocol === 'http:')) || url.username || url.password || url.search || url.hash || url.pathname !== '/') { throw new Error('请填写 HTTPS 网站根地址;本地测试可使用 HTTP。'); } return url.origin; }; const base = () => { try { return validBase(getValue('yixueQuestionBankBase', defaultBase)); } catch (_) { return defaultBase; } }; const isValidToken = value => typeof value === 'string' && value === value.trim() && /^tk_[A-Za-z0-9_-]{32,128}$/.test(value); const getToken = () => { const token = getValue(tokenKey, ''); const origin = getValue('yixueApiTokenOrigin', defaultBase); return origin === base() && isValidToken(token) ? token : ''; }; const clearCollection = () => { setValue('yixueSubmissionToken', ''); setValue('yixueSubmissionConsent', false); setValue('yixueSubmissionSeen', ''); for (const key of ['yixueSubmissionOwner', 'yixueSubmissionHistory', 'yixueSubmissionRetry', 'yixueSubmissionProvision', 'yixueSubmissionNext']) setValue(key, null); }; const setToken = value => { if (!isValidToken(value)) throw new Error('请填写题库「接口接入」生成的 tk_ 开头 API Token。'); if (value !== getToken()) { setValue(pendingKey, {}); setValue('yixueTokenRevision', uuid()); clearCollection(); usage = '尚未查询题目'; resetBalance(); } setValue(tokenKey, value); setValue('yixueApiTokenOrigin', base()); blocked = null; }; const setBase = value => { const next = validBase(value); if (next !== base()) { setValue(tokenKey, ''); setValue(pendingKey, {}); clearCollection(); blocked = null; usage = '尚未查询题目'; resetBalance(); } setValue('yixueQuestionBankBase', next); }; const jsonRequest = (path, { method = 'GET', body, token = '', idempotencyKey = '', timeout = 120000 } = {}) => new Promise(resolve => { let handle, settled = false; const requestId = idempotencyKey || uuid(); const finish = result => { if (!settled) { settled = true; clearTimeout(timer); resolve({ request_id: requestId, ...result }); } }; // anonymous/redirect may use fetch mode, where the extension's timeout is ignored. const timer = setTimeout(() => { finish({ status: 0 }); try { handle?.abort(); } catch (_) {} }, timeout); const headers = { Accept: 'application/json', 'X-Request-ID': requestId }; if (body !== undefined) headers['Content-Type'] = 'application/json'; if (token) headers.Authorization = 'Bearer ' + token; if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey; const origin = base(); try { handle = request({ method, url: origin + path, headers, anonymous: true, redirect: 'error', timeout, ...(body === undefined ? {} : { data: JSON.stringify(body) }), onload: response => { let responseId = requestId; try { if (response.finalUrl && new URL(response.finalUrl).origin !== origin) return finish({ status: 0 }); const ids = typeof response.responseHeaders === 'string' ? response.responseHeaders.split(/\r?\n/).filter(line => /^x-request-id:/i.test(line)).map(line => line.slice(line.indexOf(':') + 1).trim()) : []; if (ids.length === 1 && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$/.test(ids[0]) && !/^(?:tk_|sub_)/.test(ids[0])) responseId = ids[0]; const payload = JSON.parse(response.responseText); finish({ status: response.status, body: payload && typeof payload === 'object' ? payload : {}, request_id: responseId }); } catch (_) { finish({ status: response.status || 0, request_id: responseId }); } }, onerror: () => finish({ status: 0 }), ontimeout: () => finish({ status: 0 }), onabort: () => finish({ status: 0 }), }); } catch (_) { finish({ status: 0 }); } }); const failure = (msg, fatal = false) => ({ code: 0, data: '', msg, fatal }); const searchMetadata = response => { const body = response.body || {}; const metadata = { http_status: response.status, request_id: response.request_id }; if (['OK', 'NOT_FOUND', 'UPSTREAM_ERROR', 'ALREADY_DELIVERED', 'INTERRUPTED'].includes(body.code)) metadata.business_code = body.code; if (Number.isSafeInteger(body.record_id) && body.record_id > 0) metadata.record_id = body.record_id; if (body.protocol_version === '1') metadata.protocol_version = '1'; if (['local', 'upstream'].includes(body.source)) metadata.source = body.source; if (typeof body.is_ai === 'boolean') metadata.is_ai = body.is_ai; if (Number.isSafeInteger(body.cost) && body.cost >= 0) metadata.cost = body.cost; if (['points', 'member'].includes(body.bucket)) metadata.bucket = body.bucket; if (typeof body.saved === 'boolean') metadata.saved = body.saved; for (const key of ['ai_requested', 'ai_attempted']) if (typeof body[key] === 'boolean') metadata[key] = body[key]; if (Object.hasOwn(aiReasons, body.ai_skip_reason)) metadata.ai_skip_reason = body.ai_skip_reason; if (['none', 'source_unavailable', 'no_answer', 'no_authorized_source', 'sources_unconfigured', 'interrupted'].includes(body.failure_reason)) metadata.failure_reason = body.failure_reason; if (typeof body.idempotency_key === 'string' && /^[A-Za-z0-9_-]{16,100}$/.test(body.idempotency_key) && !/^(?:tk_|sub_)/.test(body.idempotency_key)) metadata.idempotency_key = body.idempotency_key; return metadata; }; const supportLabel = metadata => '(' + [metadata.record_id ? '查询记录:' + metadata.record_id : '', metadata.request_id ? '请求编号:' + metadata.request_id : ''].filter(Boolean).join(';') + ')'; const currentBalanceIdentity = () => base() + '\n' + getToken(); const capabilitiesState = () => capabilityIdentity === currentBalanceIdentity() ? { data: capability ? { ...capability } : null, error: capabilityError, updatedAt: capabilityUpdatedAt, loading: Boolean(capabilityFlight) } : { data: null, error: '', updatedAt: 0, loading: false }; const capabilitiesLabel = () => { const state = capabilitiesState(); if (!getToken()) return '连接题库后可查看 AI 可用状态'; if (!state.data) return state.error || (state.loading ? '正在获取 AI 可用状态…' : 'AI 可用状态尚未刷新'); const ai = state.data; return aiReasons[ai.disabled_reason] + (ai.effective_model ? ';默认模型:' + ai.effective_model : '') + (ai.price_points !== null ? ';AI 参考答案每次 ' + ai.price_points + ' 积分' : '') + (state.error ? '(刷新失败,请重试)' : ''); }; const refreshCapabilities = async ({ force = false } = {}) => { const token = getToken(), identity = currentBalanceIdentity(); if (capabilityIdentity !== identity) { capabilityEpoch++; capability = null; capabilityError = ''; capabilityFlight = null; capabilityAttempt = null; capabilityRetryAt = 0; capabilityUpdatedAt = 0; capabilityIdentity = identity; } if (!token) return capabilitiesState(); if (capabilityFlight) return capabilityFlight.promise; const time = now(); if (time < capabilityRetryAt || (capabilityAttempt !== null && time - capabilityAttempt < (force ? 1000 : 60000))) return capabilitiesState(); capabilityAttempt = time; capabilityError = ''; const flight = { epoch: capabilityEpoch, promise: null }; capabilityFlight = flight; notifyBalance(); flight.promise = jsonRequest('/api/v1/capabilities', { token, timeout: 10000 }).then(response => { if (flight.epoch !== capabilityEpoch || identity !== currentBalanceIdentity()) return; const body = response.body || {}, ai = body.data?.ai; const valid = response.status === 200 && body.code === 'OK' && body.cost === 0 && body.protocol_version === '1' && ai && ['enabled', 'allowed', 'ready'].every(key => typeof ai[key] === 'boolean') && [null, 'glm-5.2', 'deepseek-v4-flash'].includes(ai.effective_model) && (ai.price_points === null || (Number.isSafeInteger(ai.price_points) && ai.price_points >= 0)) && ['account_disabled', 'plan_disallowed', 'service_unavailable', 'unsupported_model', 'none'].includes(ai.disabled_reason) && (ai.disabled_reason !== 'none' || (ai.enabled && ai.allowed && ai.ready && ai.effective_model !== null && ai.price_points !== null)); if (valid) { capability = Object.fromEntries(['enabled', 'allowed', 'ready', 'effective_model', 'price_points', 'disabled_reason'].map(key => [key, ai[key]])); capabilityUpdatedAt = now(); } else { capabilityError = ({ 401: 'AI 状态查询失败:Token 无效', 403: 'AI 状态查询未获授权', 404: '题库暂未提供 AI 能力查询,仍可正常搜题', 429: 'AI 状态查询过于频繁,请稍后刷新' })[response.status] || 'AI 状态暂无法确认,请稍后刷新'; if ([401, 403].includes(response.status)) capability = null; if ([404, 429].includes(response.status)) capabilityRetryAt = now() + 60000; } }).finally(() => { if (capabilityFlight === flight) { capabilityFlight = null; notifyBalance(); } }).then(capabilitiesState); return flight.promise; }; const balanceState = () => balanceIdentity === currentBalanceIdentity() ? { data: balance ? { ...balance } : null, error: balanceError, updatedAt: balanceUpdatedAt, loading: !!balanceFlight } : { data: null, error: '', updatedAt: 0, loading: false }; const balanceLabel = () => { const state = balanceState(); if (!state.data) return state.error ? '额度查询失败:' + state.error : state.loading ? '正在获取账户额度…' : '账户额度尚未刷新'; const data = state.data; const expires = data.member_until > 0 ? new Date(data.member_until * 1000).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false }) + '(北京时间)' : ''; const member = data.member ? `会员今日剩余:${data.daily_remaining}/${data.daily_limit};到期:${expires}` : expires ? '会员已到期:' + expires : '非会员'; return `积分:${data.points};${member}` + (state.error ? `(刷新失败:${state.error})` : ''); }; const refreshBalance = async ({ force = false } = {}) => { const identity = currentBalanceIdentity(); if (balanceIdentity !== identity) { resetBalance(); balanceIdentity = identity; } const token = getToken(); if (!token) { balance = null; balanceError = '请先设置新版搜题 Token。'; notifyBalance(); return balanceState(); } if (balanceFlight) return balanceFlight.promise; const time = now(); if (time < balanceRetryAt || (balanceAttempt !== null && time - balanceAttempt < (force ? 1000 : 10000))) return balanceState(); balanceAttempt = time; balanceError = ''; const flight = { epoch: balanceEpoch, promise: null }; balanceFlight = flight; notifyBalance(); flight.promise = jsonRequest('/api/v1/balance', { token, timeout: 10000 }).then(response => { if (flight.epoch !== balanceEpoch || identity !== currentBalanceIdentity()) return; const data = response.body?.data; const valid = response.status === 200 && response.body?.code === 'OK' && response.body.cost === 0 && data && Number.isSafeInteger(data.points) && data.points >= 0 && typeof data.member === 'boolean' && Number.isFinite(data.member_until) && data.member_until >= 0 && data.member_until <= 8640000000000 && Number.isSafeInteger(data.daily_limit) && data.daily_limit >= 0 && Number.isSafeInteger(data.daily_remaining) && data.daily_remaining >= 0 && data.daily_remaining <= data.daily_limit; if (valid) { balance = { points: data.points, member: data.member, member_until: data.member_until, daily_limit: data.daily_limit, daily_remaining: data.daily_remaining }; balanceUpdatedAt = now(); balanceError = ''; } else { const errors = { 401: 'Token 无效或已撤销', 403: '账户停用或未获授权', 404: '题库尚未部署余额接口', 429: '查询过于频繁,请稍后刷新' }; balanceError = errors[response.status] || (response.status === 200 ? '余额响应格式不匹配' : '网络或题库服务暂不可用'); if ([401, 403].includes(response.status)) balance = null; if ([404, 429].includes(response.status)) balanceRetryAt = now() + 60000; } }).finally(() => { if (balanceFlight === flight) { balanceFlight = null; notifyBalance(); } }).then(balanceState); return flight.promise; }; const search = async input => { if (blocked) return blocked; const token = getToken(); if (!token) return failure('请先设置新版搜题 Token。', true); const aliases = { '0': 'single', '1': 'multiple', '2': 'fill', '3': 'judge', '4': 'answer', '单选题': 'single', '多选题': 'multiple', '填空题': 'fill', '判断题': 'judge', true_false: 'judge', judgement: 'judge', judgment: 'judge', completion: 'fill', 'fill-in-the-blank': 'fill' }; const type = aliases[input.type] || input.type; const options = Array.isArray(input.options) ? input.options : typeof input.options === 'string' ? input.options.split(/\r?\n/) : []; const query = { title: toText(input.title).trim(), options: options.map(toText).map(value => value.trim()).filter(Boolean), type: ['single', 'multiple', 'fill', 'judge', 'answer', 'unknown'].includes(type) ? type : 'unknown', use_ai: input.use_ai === true, }; if (!query.title || query.title.length > 5000 || query.options.length > 30 || query.options.some(value => value.length > 2000)) return failure('题干或选项超出题库接口限制。'); let revision = getValue('yixueTokenRevision', ''); if (!revision) { revision = uuid(); setValue('yixueTokenRevision', revision); } const origin = base(); const identity = JSON.stringify([origin, revision, query]); const pending = getValue(pendingKey, {}); if (!pending[identity] && Object.keys(pending).length >= 100) return failure('尚有较多未确认请求,请稍后重试原题,避免重复扣费。', true); const idempotencyKey = pending[identity] || uuid(); pending[identity] = idempotencyKey; setValue(pendingKey, pending); const response = await jsonRequest('/api/v1/search', { method: 'POST', body: query, token, idempotencyKey }); if (base() !== origin || getToken() !== token || getValue('yixueTokenRevision', '') !== revision) { return failure('题库账户已更换,已忽略旧账户的查询结果;请重新启动查询。', true); } const body = response.body || {}; const metadata = searchMetadata(response); const fail = (message, fatal = false, reason = 'search-failed') => ({ ...failure(message + supportLabel(metadata), fatal), reason, metadata }); const forget = () => { const latest = getValue(pendingKey, {}); delete latest[identity]; setValue(pendingKey, latest); }; const messages = { 401: '搜题 Token 无效或已撤销,请重新设置。', 402: '题库额度不足,请到题库网站充值后刷新页面。', 403: '账户停用或当前查询未获授权,请到题库网站查看。', 429: '题库查询过于频繁,请稍后刷新页面重试。' }; if (messages[response.status]) { if (response.status !== 429) forget(); blocked = fail(messages[response.status], true); return blocked; } if (response.status === 422) { forget(); return fail('题目字段或请求标识未通过题库校验。'); } if (response.status === 409) return fail('原查询仍在处理,请稍后重试;请求标识已保留。'); if (response.status !== 200 || !response.body) return fail('查询结果尚未确认;请重试原题,脚本会复用请求标识,避免重复扣费。'); if (Object.hasOwn(body, 'protocol_version') && body.protocol_version !== '1') { blocked = fail('题库协议版本不匹配,请更新脚本或联系题库管理员;请求标识已保留。', true, 'protocol-mismatch'); return blocked; } if (body.code !== 'OK') { if (['NOT_FOUND', 'UPSTREAM_ERROR', 'ALREADY_DELIVERED', 'INTERRUPTED'].includes(body.code)) forget(); const fee = metadata.cost === 0 ? '本次未扣额度' : '本次扣费状态未确认,请到题库网站核对'; const ai = metadata.ai_attempted === true ? '已尝试 AI,仍未获得参考答案。' : metadata.ai_skip_reason && metadata.ai_skip_reason !== 'none' ? aiReasons[metadata.ai_skip_reason] + '。' : query.use_ai ? '已请求 AI 兜底,其可用性取决于题库账户偏好、套餐权限和服务配置,请到题库网站核对。' : '脚本未开启 AI。'; const source = ({ no_authorized_source: '当前套餐没有获授权的普通题源。', sources_unconfigured: '普通题源尚未配置。' })[metadata.failure_reason] || ''; const errors = { NOT_FOUND: '未找到参考答案,' + fee + '。' + source + '请核对题干、题型和选项;' + ai, UPSTREAM_ERROR: '题源查询暂不可用,' + fee + ';请稍后重试或联系题库管理员。' + ai, ALREADY_DELIVERED: '此请求的答案已交付且未保存,请核对之前的结果;重新查询可能再次扣费。', INTERRUPTED: '查询中断,预扣额度已退回,可重新查询。' }; if (['NOT_FOUND', 'UPSTREAM_ERROR'].includes(body.code)) usage = fee; const reasons = { NOT_FOUND: 'not-found', UPSTREAM_ERROR: 'upstream-error', ALREADY_DELIVERED: 'already-delivered', INTERRUPTED: 'interrupted' }; return fail(errors[body.code] || '题库返回了无法识别的结果;请稍后重试原题。', false, reasons[body.code] || 'unknown-response'); } const answer = Array.isArray(body.data) ? body.data.find(item => typeof item?.answer === 'string' && item.answer.trim()) : null; if (!answer) return fail('题库响应缺少答案;请重试原题以核对结果。', false, 'missing-answer'); const answerFormat = answer.answer_format ?? 'text'; if (!['text', 'letters'].includes(answerFormat)) return fail('题库返回了暂不支持的答案格式,请更新脚本;请求标识已保留。', false, 'unsupported-answer-format'); forget(); usage = metadata.cost !== undefined ? `本次消耗:${metadata.cost} ${metadata.bucket === 'member' ? '次会员额度' : '积分'}` : '额度请到题库网站查看'; return { code: 1, data: answerFormat === 'letters' ? answer.answer : answer.answer.replaceAll('###', '#!#'), answer_format: answerFormat, msg: metadata.is_ai === true ? 'ai' : '', cost: metadata.cost, bucket: metadata.bucket, analysis: typeof answer.analysis === 'string' ? answer.analysis : '', metadata }; }; return { base, setBase, getToken, setToken, isValidToken, search, usageLabel: () => usage, resume: () => { blocked = null; }, refreshBalance, balanceState, balanceLabel, refreshCapabilities, capabilitiesState, capabilitiesLabel, onBalanceChange: listener => { balanceListeners.add(listener); return () => balanceListeners.delete(listener); }, health: async () => { const result = await jsonRequest('/api/health', { timeout: 10000 }); return result.status === 200 && result.body?.status === 'ok'; }, provisionSubmission: () => jsonRequest('/api/submission-tokens/provision', { method: 'POST', token: getToken(), body: {}, timeout: 15000 }), submitQuestions: (token, body, idempotencyKey) => jsonRequest('/api/question-submissions', { method: 'POST', token, body, idempotencyKey, timeout: 15000 }), }; } module.exports = createQuestionBankClient; }, "core/routes.js": function(module, exports, require) { 'use strict'; const { parseCookie } = require('./primitives'); function classifyPage(href) { const path = new URL(href).pathname; if (/\/mycourse\/(?:stu|studentcourse)\/?$/.test(path)) return 'entry'; if (/\/mooc-ans\/course\/(?:portal\/)?(?:yixue-console|999999999[\d.]+)(?:\.html)?\/?$/.test(path)) return 'dashboard'; if (/\/mycourse\/studentstudy\/?$/.test(path)) return 'study'; if (/\/work\/(?:getAllWork|list)|\/mooc2\/work\/list/.test(path)) return 'work-list'; if (/\/work\/(?:doHomeWorkNew|dowork)|\/mooc2\/work\/dowork/.test(path)) return 'work'; if (/\/(?:api\/work|work\/phone\/work)\/?$/.test(path)) return 'work'; if (/reVersionTestStartNew|\/exam\/preview|\/exam\/test\/reVersion|\/exam\/test\/start/.test(path)) return 'exam'; if (/exam-list|examnotes|\/exam\/test\/?$/.test(path)) return 'exam-list'; return 'other'; } function studyOrigin(url, serverHost, prefix) { if (prefix) return url.origin; if (serverHost) { try { const candidate = new URL(serverHost, url); const family = host => host.replace(/^[^.]+\./, ''); const studyHost = /^mooc1(?:-\d+|-api)?\./i.test(candidate.hostname); if (['https:', 'http:'].includes(candidate.protocol) && !candidate.username && !candidate.password && candidate.pathname === '/' && !candidate.search && !candidate.hash && (candidate.origin === url.origin || (studyHost && family(candidate.hostname) === family(url.hostname)))) { return candidate.origin; } } catch (_) { /* An invalid page setting must not replace the course server. */ } } // The modern portal serves the course shell; legacy study APIs live on mooc1. return url.hostname === 'mooc2-ans.chaoxing.com' ? 'https://mooc1.chaoxing.com' : url.origin; } function readCourseContext(href, { title = '', cookie = '', document: page, serverHost = '' } = {}) { const url = new URL(href); const param = (...keys) => keys.map(key => url.searchParams.get(key)).find(Boolean) || ''; const prefix = url.pathname.match(/^(.*\/https\/[a-z0-9]+)(?:\/|$)/i)?.[1] || ''; return { origin: studyOrigin(url, serverHost, prefix), prefix, courseId: param('courseid', 'courseId'), classId: param('classid', 'clazzid', 'classId'), cpi: param('cpi'), uid: param('uid', 'userId') || parseCookie('UID', cookie) || parseCookie('_uid', cookie) || page?.querySelector('input[name="userId"],#uploadUid')?.value || '', fid: parseCookie('fid', cookie), courseName: param('coursename') || title.replace(/-首页$/, '') || '章节学习控制台', }; } function consoleUrl(context) { const url = new URL((context.prefix || '') + '/mooc-ans/course/yixue-console.html', context.origin); for (const [key, value] of Object.entries({ ut: 's', classid: context.classId, courseid: context.courseId, cpi: context.cpi, coursename: context.courseName, uid: context.uid })) url.searchParams.set(key, value || ''); return url.href; } module.exports = { classifyPage, readCourseContext, consoleUrl }; }, "core/settings.js": function(module, exports, require) { 'use strict'; const defaults = Object.freeze({ doVideo: true, doDocument: true, doLive: true, doWork: true, doRenwu: false, doNoMission: false, doAI: false, autoSubmit: true, beisu: 1, vgqtlv: 85 }); function createSettings({ read, write }) { function normalize(key, value) { if (!(key in defaults)) return undefined; if (typeof defaults[key] === 'boolean') return [true, false, 0, 1].includes(value) ? Boolean(value) : defaults[key]; const numeric = Number(value); const valid = Number.isFinite(numeric) && (key === 'beisu' ? numeric >= 0.5 && numeric <= 16 : numeric >= 0 && numeric <= 100); return valid ? numeric : defaults[key]; } const get = (key, fallback) => key in defaults ? normalize(key, read(key, defaults[key])) : read(key, fallback); return { get, set(key, value) { if (!(key in defaults)) throw new Error('未知配置项'); const normalized = normalize(key, value); if (typeof defaults[key] === 'number' && normalized !== Number(value)) throw new RangeError(key === 'beisu' ? '倍速必须在0.5-16之间' : '正确率必须在0-100之间'); write(key, normalized); return normalized; }, snapshot: () => Object.fromEntries(Object.keys(defaults).map(key => [key, get(key)])), }; } module.exports = { createSettings, defaults }; }, "core/transport.js": function(module, exports, require) { 'use strict'; const { abortError } = require('./primitives'); const isLoginResponse = result => /