### 添加

- 引入了BS估算功能

### 修改

- 快捷功能【快速犯罪】去除了烦人的通知
This commit is contained in:
Liwanyi 2024-03-27 16:54:55 +08:00
parent 4d92efa48b
commit 2eb1fbf087
15 changed files with 514 additions and 42 deletions

View File

@ -1,5 +1,17 @@
# CHANGE # CHANGE
## 1.1.9
2024年03月27日
### 添加
- 引入了BS估算功能
### 修改
- 快捷功能【快速犯罪】去除了烦人的通知
## 1.1.8 ## 1.1.8
2024年03月20日 2024年03月20日

View File

@ -1,6 +1,6 @@
{ {
"name": "wuhu-torn-helper", "name": "wuhu-torn-helper",
"version": "1.1.8", "version": "1.1.9",
"description": "芜湖助手", "description": "芜湖助手",
"scripts": { "scripts": {
"release": "cross-env NODE_ENV=production rollup -c && node build.mjs", "release": "cross-env NODE_ENV=production rollup -c && node build.mjs",

File diff suppressed because one or more lines are too long

View File

@ -283,6 +283,10 @@ div#wh-popup::after {
user-select: none; user-select: none;
} }
.mt-4 {
margin-bottom: 4px;
}
/*.el-overlay {*/ /*.el-overlay {*/
/* backdrop-filter: blur(20px);*/ /* backdrop-filter: blur(20px);*/
/*}*/ /*}*/

View File

@ -5,6 +5,10 @@ import ResponseInject from "../../interface/ResponseInject";
import { Injectable } from "../../container/Injectable"; import { Injectable } from "../../container/Injectable";
import ClassName from "../../container/ClassName"; import ClassName from "../../container/ClassName";
import LocalConfigWrapper from "../LocalConfigWrapper"; import LocalConfigWrapper from "../LocalConfigWrapper";
import Logger from "../Logger";
import MsgWrapper from "../utils/MsgWrapper";
import { fetchYata } from "../../func/module/fetchYata";
import toThousands from "../../func/utils/toThousands";
/** /**
* fetch * fetch
@ -15,10 +19,13 @@ export default class FetchEventCallback extends Provider implements ResponseInje
className = "FetchEventCallback"; className = "FetchEventCallback";
newNode = document.createElement('div') newNode = document.createElement('div')
bsEstNode = document.createElement('div')
constructor( constructor(
private readonly localConfigWrapper: LocalConfigWrapper, private readonly localConfigWrapper: LocalConfigWrapper,
private readonly commonUtils: CommonUtils, private readonly commonUtils: CommonUtils,
private readonly logger: Logger,
private readonly msgWrapper: MsgWrapper,
) { ) {
super(); super();
} }
@ -30,14 +37,38 @@ export default class FetchEventCallback extends Provider implements ResponseInje
*/ */
public responseHandler(url: string, response) { public responseHandler(url: string, response) {
// mini profile 中添加上次动作 // mini profile 中添加上次动作
if (url.startsWith('/page.php?sid=UserMiniProfile&userID') && this.localConfigWrapper.config.ShowMiniProfLastAct) { if (url.startsWith('/page.php?sid=UserMiniProfile&userID')) {
window.setTimeout(async () => { window.setTimeout(async () => {
let cont = CommonUtils.querySelector('[class*=profile-mini-_userProfileWrapper___]'); let cont = CommonUtils.querySelector('[class*=profile-mini-_userProfileWrapper___]');
let resp: MiniProfile = response.json as MiniProfile; let resp: MiniProfile = response.json as MiniProfile;
let formatted = this.commonUtils.secondsFormat(resp.user.lastAction.seconds); if (this.localConfigWrapper.config.ShowMiniProfLastAct) {
this.logger.info({ resp })
let formatted = this.commonUtils.secondsFormat(resp.user.lastAction.seconds);
(await cont).append(this.newNode); (await cont).append(this.newNode);
this.newNode.innerText = '上次动作: ' + formatted; this.newNode.innerText = '上次动作: ' + formatted;
}
if (this.localConfigWrapper.config.isBSEstMiniProfOn) {
const id = resp.user.userID
const apikey = localStorage.getItem('APIKey')
this.bsEstNode.innerHTML = `[BS估算] [${ id }]获取中...`;
(await cont).append(this.bsEstNode)
if (!apikey) {
this.bsEstNode.innerHTML = '[BS估算] 未配置APIKey无法估算BS'
this.logger.error('MINI Profile bs估算失败: APIKey为空')
} else {
const bsData = fetchYata(id, apikey)
bsData.then(data => {
// 网速过慢时可能mini profile容器已更新新内容与上次请求的用户数据不同需要判断
if (this.bsEstNode.innerHTML.includes(resp.user.userID.toString())) {
this.bsEstNode.innerHTML = `[BS估算] ${ resp.user.playerName }[${ id }] ${ toThousands(data.total) }`
}
})
.catch(err => {
this.bsEstNode.innerHTML = `[BS估算] ${ err.message }`
})
}
}
}, 0); }, 0);
} }
} }

View File

@ -9,6 +9,10 @@ import { Injectable } from "../../container/Injectable";
import LocalConfigWrapper from "../LocalConfigWrapper"; import LocalConfigWrapper from "../LocalConfigWrapper";
import Logger from "../Logger"; import Logger from "../Logger";
import IFeature from "../../man/IFeature"; import IFeature from "../../man/IFeature";
import { fetchYata } from "../../func/module/fetchYata";
import MsgWrapper from "../utils/MsgWrapper";
import toThousands from "../../func/utils/toThousands";
import { timePastFormat } from "../../func/utils/timePastFormat";
@ClassName('ProfileHelper') @ClassName('ProfileHelper')
@Injectable() @Injectable()
@ -19,6 +23,7 @@ export default class ProfileHelper implements ResponseInject, IFeature {
private readonly localConfigWrapper: LocalConfigWrapper, private readonly localConfigWrapper: LocalConfigWrapper,
private readonly commonUtils: CommonUtils, private readonly commonUtils: CommonUtils,
private readonly logger: Logger, private readonly logger: Logger,
private readonly msgWrapper: MsgWrapper,
) { ) {
} }
@ -56,14 +61,46 @@ export default class ProfileHelper implements ResponseInject, IFeature {
} }
this.block = new TornStyleBlock('芜湖助手').insert2Dom(); this.block = new TornStyleBlock('芜湖助手').insert2Dom();
// 隐藏头像 // 隐藏头像
let hideImgSwitch = new TornStyleSwitch('隐藏头像', this.localConfigWrapper.config.HideProfileImg); try {
this.block.append(hideImgSwitch.getBase()); let hideImgSwitch = new TornStyleSwitch('隐藏头像', this.localConfigWrapper.config.HideProfileImg);
hideImgSwitch.getInput().addEventListener('change', () => { this.block.append(hideImgSwitch.getBase());
document.body.classList.toggle('wh-hide_profile_img'); hideImgSwitch.getInput().addEventListener('change', () => {
this.localConfigWrapper.config.HideProfileImg = hideImgSwitch.getInput().checked; document.body.classList.toggle('wh-hide_profile_img');
}); this.localConfigWrapper.config.HideProfileImg = hideImgSwitch.getInput().checked;
if (this.localConfigWrapper.config.ShowNameHistory) { });
globVars.responseHandlers.push((...args: any[]) => this.responseHandler.apply(this, args)); if (this.localConfigWrapper.config.ShowNameHistory) {
globVars.responseHandlers.push((...args: any[]) => this.responseHandler.apply(this, args));
}
} catch (e) {
this.logger.error('隐藏头像时出错了', e.stack)
}
// bs估算
if (this.localConfigWrapper.config.isBSEstProfOn) {
try {
const apikey = localStorage.getItem('APIKey')
if (!apikey) {
this.msgWrapper.create('BS估算失败: 尚未设定APIKey', null, 'error')
}
const promise = fetchYata(parseInt(id), apikey)
const domNode = document.createElement('div')
domNode.innerHTML = 'BS估算中...'
domNode.classList.add('mt-4')
domNode.style.border = '1px solid green'
domNode.style.padding = '2px'
this.block.append(domNode)
const buildType = { Offensive: '攻击型', Defensive: '防御型', Balanced: '平衡型' }
promise.then(data => {
domNode.innerHTML = `<b>BS估算</b><br/>
BS: ${ toThousands(data.total) }<br/>
评分: ${ toThousands(data.score) }<br/>
风格: ${ buildType[data.type] }<br/>
偏差: ${ data.skewness }%<br/>
估算时间: ${ timePastFormat(Date.now() - data.timestamp * 1000) }
`
}).catch(err => domNode.innerHTML = 'BS估算出错了: ' + err.message)
} catch (e) {
this.msgWrapper.create('BS估算失败', null, 'error')
}
} }
} }

View File

@ -108,6 +108,13 @@ class DefaultConfigType {
monitorOn = ['drugCDMonitor'] monitorOn = ['drugCDMonitor']
drugCDMonitorInterval = 60000 drugCDMonitorInterval = 60000
// mini profile显示bs估算
@Notified()
isBSEstMiniProfOn = false
// profile页面显示bs估算
@Notified()
isBSEstProfOn = true
} }
export type Config = DefaultConfigType; export type Config = DefaultConfigType;

View File

@ -0,0 +1,98 @@
import CommonUtils from "../../class/utils/CommonUtils"
type YataBSEstData = {
"total": number,
"score": number,
"type": "Offensive" | "Defensive" | "Balanced",
"skewness": number,
"timestamp": number,
"version": 1,
}
type YataApiResponse = {
// key为请求id
[key: number]: YataBSEstData
error?: { 'error': string, 'code': 1 | 2 | 3 | 4 }
}
type YataApiDataWrap = YataBSEstData & {
id: string,
isCache: boolean,
}
const cacheExpireMs = 86400000 // 一天
const KEY = 'WHBSEstCache'
const getCacheObj = () => {
let obj: { [key: string]: YataApiDataWrap }
try {
obj = JSON.parse(localStorage.getItem(KEY)) ?? {}
} catch (e) {
obj = {}
}
return obj
}
const getCache = (id: number): YataApiDataWrap => {
let cache: YataApiDataWrap = getCacheObj()[id]
if (cache && (Date.now() - cache.timestamp * 1000 > cacheExpireMs)) {
cache.isCache = true
} else {
cache = null
}
return cache
}
const setCache = (data: YataApiDataWrap): void => {
const cache = getCacheObj()
cache[data.id] = data
localStorage.setItem(KEY, JSON.stringify(cache))
}
/**
*
*/
const purge = () => {
localStorage.removeItem(KEY)
}
const fetchYata = async (id: number, apikey: string): Promise<YataApiDataWrap> => {
if (!id || !apikey) {
throw new TypeError('请求yata接口时出错: id和apikey不能为空')
}
const cache = getCache(id)
if (cache) {
return cache
} else {
let responseString: string, response: YataApiResponse
try {
responseString = await CommonUtils.COFetch(`https://yata.yt/api/v1/bs/${ id }?key=${ apikey }`)
} catch (e) {
throw new TypeError('请求yata接口时出错 ' + e.message)
}
try {
response = JSON.parse(responseString)
} catch (e) {
throw new TypeError('解析yata接口响应时出错 ' + e.message)
}
if (response.error) {
switch (response.error.code) {
case 1:
throw new TypeError('请求yata接口时出错: yata服务端错误-' + response.error.error)
case 2:
throw new TypeError('请求yata接口时出错: 脚本逻辑错误-' + response.error.error)
case 3:
throw new TypeError('请求yata接口时出错: 已达到次数限制-' + response.error.error)
case 4:
throw new TypeError('请求yata接口时出错: apikey错误-' + response.error.error)
}
}
const wrapper = <YataApiDataWrap>response[id]
wrapper.id = String(id)
wrapper.isCache = false
setCache(wrapper)
return wrapper
}
}
export { fetchYata, YataApiDataWrap, purge }

View File

@ -0,0 +1,14 @@
const convertToCSV = (data: any[]) => {
let csv = ''
for (let i = 0; i < data.length; i++) {
let row = ''
for (const key in data[i]) {
row += `"${ data[i][key] }",`
}
row = row.slice(0, -1) // 删除最后一个逗号
csv += row + '\r\n' // 添加换行符
}
return csv
}
export { convertToCSV }

View File

@ -0,0 +1,20 @@
const timePastFormat = (ts: number): string => {
// 毫秒
if (ts < 1000) {
return ts + 'ms'
}
// 秒
else if (ts < 60000) {
return (ts / 1000 | 0) + 's'
}
// 分
else if (ts < 3600000) {
return (ts / 60000 | 0) + 'm'
}
// 时
else {
return (ts / 3600000 | 0) + 'h'
}
}
export { timePastFormat }

View File

@ -32,6 +32,7 @@ export interface MiniProfile {
}, },
sendMoneyWarning: string, sendMoneyWarning: string,
playerName: string, playerName: string,
userID: number,
}; };
userStatus: { userStatus: {
status: { status: {

262
src/vue/BSEstView.vue Normal file
View File

@ -0,0 +1,262 @@
<script lang="ts" setup>
import { inject, onMounted, ref } from "vue"
import { LocalConfigWrapperKey } from "../ts/class/LocalConfigWrapper"
import { ElMessage } from "element-plus"
import { fetchYata, purge, YataApiDataWrap } from "../ts/func/module/fetchYata"
import { LoggerKey } from "../ts/class/Logger"
import toThousands from "../ts/func/utils/toThousands"
import { Download, QuestionFilled, Refresh, Search } from "@element-plus/icons-vue"
import { timePastFormat } from "../ts/func/utils/timePastFormat"
import { convertToCSV } from "../ts/func/utils/convert2Csv"
const isMiniProfOn = ref<boolean>(false)
const isProfOn = ref<boolean>(false)
const apikey = ref<string>('')
const targetId = ref<string>('')
const tableData = ref<Partial<YataApiDataWrap>[]>([])
const isLoading = ref(false)
const idKey: { [key: string]: number } = {}
const buildType = { Offensive: '攻击型', Defensive: '防御型', Balanced: '平衡型' }
const localConfigWrapper = inject(LocalConfigWrapperKey)
const logger = inject(LoggerKey)
const saveApikey = () => {
if (!apikey.value) {
ElMessage({
message: 'apikey保存时输入为空',
type: 'error',
showClose: true
})
throw new TypeError('apikey保存时输入为空')
}
localStorage.setItem('APIKey', apikey.value)
ElMessage.success({
message: 'APIKey设置成功',
showClose: true
})
}
const onMiniProfSwitchChange = () => localConfigWrapper.config.isBSEstMiniProfOn = isMiniProfOn.value
const onProfSwitchChange = () => localConfigWrapper.config.isBSEstProfOn = isProfOn.value
const doRequest = async () => {
if (!apikey.value) {
ElMessage.warning({
message: '未设置APIKey',
showClose: true
})
return
}
isLoading.value = true
for (let i = 0; i < tableData.value.length; i++) {
const item = tableData.value[i]
if (!item.total) {
let est: YataApiDataWrap
try {
est = await fetchYata(parseInt(item.id), apikey.value)
} catch (e) {
ElMessage.error({
message: e.message,
showClose: true
})
logger.error(e.stack)
continue
}
item.total = est.total
item.score = est.score
item.type = est.type
item.skewness = est.skewness
item.timestamp = est.timestamp
item.version = est.version
item.isCache = est.isCache
}
}
isLoading.value = false
}
/**
* 查询列表中添加新的id
* @param _id
*/
const add = (_id: string) => {
const id = parseInt(_id)
if (id) {
// id
if (!idKey[_id]) {
tableData.value.push({ id: _id })
idKey[_id] = 1
targetId.value = ''
} else {
ElMessage.warning({
message: '重复的id',
showClose: true
})
}
} else {
ElMessage.error({
message: 'id有误',
showClose: true
})
}
}
/**
* 清空查询列表和唯一id表
*/
const emptyList = () => {
tableData.value = []
const keys = Object.keys(idKey)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
idKey[key] = 0
}
}
const purgeCache = () => {
purge()
ElMessage.warning({
message: '本地缓存已清除',
showClose: true
})
}
const exportCsv = () => {
const exportList: any[] = [{
id: "ID", total: "BS", score: "评分", type: "风格", skewness: "偏差", timestamp: "时间戳"
}]
for (let i = 0; i < tableData.value.length; i++) {
const item = tableData.value[i]
const newItem = {
id: item.id ?? '-1',
total: item.total ?? null,
score: item.score ?? null,
type: buildType[item.type] ?? null,
skewness: item.skewness ?? null,
timestamp: item.timestamp ?? null,
// isCache: item.isCache ?? null,
// version: item.version ?? null
}
exportList.push(newItem)
}
const csvContent = convertToCSV(exportList)
let url: string
let isBlob = false
const UTF8 = "\uFEFF"
if (window.Blob && window.URL && window.URL.createObjectURL) {
const csvData = new window.Blob([UTF8 + csvContent], {
type: 'text/csv'
})
url = window.URL.createObjectURL(csvData)
isBlob = true
} else {
url = window.encodeURI(csvContent)
}
window.location.href = isBlob ? url : 'data:text/csv;charset=utf-8,' + UTF8 + url
}
onMounted(() => {
// APIKey
const key = localStorage.getItem('APIKey')
if (!key) {
ElMessage.error({
message: '尚未配置APIKey',
showClose: true
})
} else {
apikey.value = key
}
//
isMiniProfOn.value = localConfigWrapper.config.isBSEstMiniProfOn
isProfOn.value = localConfigWrapper.config.isBSEstProfOn
})
</script>
<template>
<el-space :fill="true">
<el-card shadow="never">
<template #header>设置</template>
<el-form label-width="auto" @submit.prevent>
<el-form-item label="MINI资料卡中显示">
<el-switch v-model="isMiniProfOn" @change="onMiniProfSwitchChange"/>
</el-form-item>
<el-form-item label="个人资料中显示">
<el-switch v-model="isProfOn" @change="onProfSwitchChange"/>
</el-form-item>
<el-form-item label="API KEY">
<el-input v-model="apikey">
<template #append>
<el-button :loading="isLoading" @click="saveApikey">保存</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-popconfirm cancel-button-text="取消" confirm-button-text="确定" title="确定操作" @confirm="purgeCache">
<template #reference>
<el-button type="danger">清空缓存</el-button>
</template>
</el-popconfirm>
</el-form-item>
</el-form>
</el-card>
<el-card shadow="never">
<template #header>使用</template>
<p class="mt-4">BS估算功能使用了
<el-link href="https://yata.yt/api/v1/" target="_blank">yata</el-link>
神经网络模型接口需要提供torn apikey (上面输入)
</p>
<p class="mt-4">本插件缓存机制1</p>
<el-form label-width="auto" @submit="add(targetId)" @submit.prevent>
<el-form-item label="目标数字ID">
<el-input v-model="targetId" clearable/>
</el-form-item>
<el-form-item>
<el-button :loading="isLoading" type="primary" @click="add(targetId)">添加</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card shadow="never">
<template #header>结果</template>
<el-button-group>
<el-button :icon="Search" :loading="isLoading" type="primary" @click="doRequest">估算</el-button>
<el-button :icon="Refresh" :loading="isLoading" @click="emptyList">清空</el-button>
<el-button :icon="Download" :loading="isLoading" @click="exportCsv">导出CSV</el-button>
</el-button-group>
<el-row>
<el-col :span="23">
<el-table :data="tableData" empty-text=" ">
<el-table-column label="ID" prop="id"/>
<el-table-column label="BS">
<template #default="scope">
{{ toThousands(scope.row.total) }}
</template>
</el-table-column>
<el-table-column label="评分" prop="score"/>
<el-table-column label="风格">
<template #default="scope">
{{ buildType[scope.row.type] }}
</template>
</el-table-column>
<el-table-column>
<template #header>
<el-tooltip content="越低越准" effect="light" placement="top">
<span>偏差<el-icon><QuestionFilled/></el-icon></span>
</el-tooltip>
</template>
<template #default="scope">
{{ scope.row.skewness }}%
</template>
</el-table-column>
<el-table-column label="时间">
<template #default="scope">
{{ timePastFormat(Date.now() - scope.row.timestamp * 1000) }}
</template>
</el-table-column>
</el-table>
</el-col>
</el-row>
</el-card>
</el-space>
</template>
<style scoped>
</style>

View File

@ -159,6 +159,7 @@ import QuickCrime from "./QuickCrime.vue"
import UpdateDate from "./UpdateScript.vue" import UpdateDate from "./UpdateScript.vue"
import VirusProgramming from "./VirusProgramming.vue" import VirusProgramming from "./VirusProgramming.vue"
import PropertyVault from "./PropertyVault.vue"; import PropertyVault from "./PropertyVault.vue";
import BSEstView from "./BSEstView.vue";
const logger = inject(LoggerKey) const logger = inject(LoggerKey)
const quickGymTrain = inject(QuickGymTrainKey) const quickGymTrain = inject(QuickGymTrainKey)
@ -207,6 +208,10 @@ const menuItemList: MenuItem[] = [
title: '📦 物品', title: '📦 物品',
template: InventoryView, template: InventoryView,
}, },
{
title: '🎯 BS估算',
template: BSEstView,
},
{ {
title: '🫵 关闭店铺(双击开启)', title: '🫵 关闭店铺(双击开启)',
handler: () => bazaarControl.method(), handler: () => bazaarControl.method(),

View File

@ -67,6 +67,7 @@ import { itemNameDict } from "../ts/dictionary/translation";
import toThousands from "../ts/func/utils/toThousands"; import toThousands from "../ts/func/utils/toThousands";
import Sleep from "../ts/func/utils/Sleep"; import Sleep from "../ts/func/utils/Sleep";
import { MathUtilsKey } from "../ts/class/utils/MathUtils"; import { MathUtilsKey } from "../ts/class/utils/MathUtils";
import { timePastFormat } from "../ts/func/utils/timePastFormat";
const logger = inject(LoggerKey); const logger = inject(LoggerKey);
const itemHelper = inject(ItemHelperKey); const itemHelper = inject(ItemHelperKey);
@ -90,25 +91,6 @@ const itemOnList = ref<ItemInfo[]>([]);
const listLoading = ref<boolean>(false); const listLoading = ref<boolean>(false);
const currentTs = ref<number>(Date.now()); const currentTs = ref<number>(Date.now());
const timePastFormat = (ts: number): string => {
//
if (ts < 1000) {
return ts + 'ms';
}
//
else if (ts < 60000) {
return (ts / 1000 | 0) + 's';
}
//
else if (ts < 3600000) {
return (ts / 60000 | 0) + 'm';
}
//
else {
return (ts / 3600000 | 0) + 'h';
}
};
const doGetIMarketList = async (id: number) => { const doGetIMarketList = async (id: number) => {
itemOnList.value = []; itemOnList.value = [];
listLoading.value = true; listLoading.value = true;

View File

@ -30,7 +30,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { Coffee } from "@element-plus/icons-vue"; import { Coffee } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
import { inject, onBeforeUnmount, onMounted, ref } from 'vue'; import { inject, onBeforeUnmount, onMounted, ref } from 'vue';
import { LoggerKey } from "../ts/class/Logger"; import { LoggerKey } from "../ts/class/Logger";
import { CrimeData } from "./data/CrimeData"; import { CrimeData } from "./data/CrimeData";
@ -70,14 +69,14 @@ const doCrime = async (nerve, crime: "hackbank" | "warehouse" | 'napcop') => {
logger.error(e.stack); logger.error(e.stack);
results.value = e.message; results.value = e.message;
} }
let err; let err: string;
try { try {
err = JSON.parse(results.value).error; err = JSON.parse(results.value).error;
} catch (e) { } catch (e) {
} }
if (err) { if (err) {
results.value = '出错了'; results.value = '错误: ' + err;
ElMessage.error('错误: ' + err); // ElMessage.error(': ' + err);
logger.error(err); logger.error(err);
} }
loading.value = false; loading.value = false;