UPDATE: Support RobinSR
All checks were successful
Gitea Auto Deploy / Deploy-Container (push) Successful in 1m9s

This commit is contained in:
2026-04-12 15:53:51 +07:00
parent 16ba852029
commit 9003b06b0e
15 changed files with 153 additions and 392 deletions

View File

@@ -1,4 +1,12 @@
[
{
"version": "4.1.6",
"date": "12/04/2026",
"type": "update",
"items": [
"Support RobinSR"
]
},
{
"version": "4.1.5",
"date": "17/03/2026",

View File

@@ -1,281 +0,0 @@
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Firefly tools by Firefly Shelter"
},
"DataPage": {
"skillType": "技能类型",
"skillName": "技能名称",
"character": "角色",
"id": "ID",
"path": "命运",
"rarity": "稀有度",
"element": "元素",
"technique": "秘技",
"talent": "天赋",
"basic": "普通攻击",
"skill": "技能",
"ultimate": "终结技",
"servant": "忆灵",
"damage": "伤害",
"type": "类型",
"warrior": "毁灭",
"knight": "守护",
"mage": "博学",
"priest": "丰饶",
"rogue": "狩猎",
"shaman": "同谐",
"warlock": "虚无",
"elation": "欢愉",
"memory": "记忆",
"fire": "火",
"ice": "冰",
"imaginary": "虚数",
"physical": "物理",
"quantum": "量子",
"thunder": "雷",
"wind": "风",
"hp": "生命值",
"atk": "攻击",
"speed": "速度",
"critRate": "暴击率",
"critDmg": "暴击伤害",
"breakEffect": "击破伤害",
"effectRes": "效果抗性",
"energyRegenerationRate": "能量恢复速率",
"effectHitRate": "效果命中率",
"outgoingHealingBoost": "治疗增强",
"fireDmgBoost": "火元素伤害增强",
"iceDmgBoost": "冰元素伤害增强",
"imaginaryDmgBoost": "虚数伤害增强",
"physicalDmgBoost": "物理伤害增强",
"quantumDmgBoost": "量子伤害增强",
"thunderDmgBoost": "雷元素伤害增强",
"windDmgBoost": "风元素伤害增强",
"pursued": "附加伤害",
"true damage": "真实伤害",
"elationdamage": "欢愉伤害",
"follow-up": "后续伤害",
"elemental damage": "击破与超击破伤害",
"dot": "持续伤害",
"qte": "QTE 技能",
"level": "等级",
"relics": "遗器",
"eidolons": "星魂",
"lightcones": "光锥",
"loadData": "加载数据",
"exportData": "导出数据",
"connectSetting": "连接设置",
"connected": "已连接",
"unconnected": "未连接",
"psConnection": "PS 连接",
"connectionType": "连接类型",
"status": "状态",
"connectPs": "连接 PS",
"other": "其他",
"freeSr": "FreeSR",
"database": "数据库",
"enka": "Enka",
"monsterSetting": "怪物设置",
"serverUrl": "服务器 URL",
"privateType": "私有类型",
"local": "本地",
"server": "服务器",
"username": "用户名",
"password": "密码",
"placeholderServerUrl": "输入服务器 URL",
"placeholderUsername": "输入用户名",
"placeholderPassword": "输入密码",
"connectedSuccess": "成功连接 PS",
"connectedFailed": "连接 PS 失败",
"syncSuccess": "已成功与 PS 同步数据",
"syncFailed": "与 PS 同步数据失败",
"sync": "同步",
"importSetting": "导入设置",
"profile": "配置档",
"default": "默认",
"copyProfiles": "复制配置档",
"addNewProfile": "添加新配置档",
"createNewProfile": "创建新配置档",
"editProfile": "编辑配置档",
"placeholderProfileName": "输入配置档名称",
"profileName": "配置档名称",
"create": "创建",
"update": "更新",
"characterInformation": "角色信息",
"skills": "技能",
"showcaseCard": "展示卡",
"comingSoon": "敬请期待",
"characterName": "角色名称",
"placeholderCharacter": "输入角色名称",
"characterSettings": "角色设置",
"levelConfiguration": "等级配置",
"characterLevel": "角色等级",
"max": "最大",
"ultimateEnergy": "终极能量",
"currentEnergy": "当前能量",
"setTo50": "设为50%",
"battleConfiguration": "战斗配置",
"useTechnique": "使用秘术",
"techniqueNote": "启用战前秘术效果",
"enhancement": "强化",
"enhancementLevel": "强化等级",
"origin": "来源",
"enhancedNote": "强化更高可解锁额外技能",
"lightconeEquipment": "光锥装备",
"lightconeSettings": "光锥设置",
"placeholderLevel": "输入等级",
"superimpositionRank": "叠加等级",
"ranksNote": "更高等级提供更强效果",
"changeLightcone": "更换光锥",
"removeLightcone": "移除光锥",
"equipLightcone": "装备光锥",
"noLightconeEquipped": "未装备光锥",
"equipLightconeNote": "装备光锥以增强角色能力",
"filter": "筛选",
"selectedCharacters": "已选角色",
"selectedProfiles": "已选配置档",
"clearAll": "清除全部",
"selectAll": "全选",
"copy": "复制",
"copied": "已复制",
"noAvatarSelected": "未选择角色",
"noAvatarToCopySelected": "未选择要复制的角色",
"pleaseSelectAtLeastOneProfile": "请至少选择一个配置档",
"pleaseEnterUid": "请输入 UID",
"failedToFetchEnkaData": "获取 Enka 数据失败",
"pleaseSelectAtLeastOneCharacter": "请至少选择一个角色",
"noDataToImport": "无可导入数据",
"pleaseSelectAFile": "请选择一个文件",
"fileMustBeAValidJsonFile": "文件必须为有效 JSON",
"importEnkaDataSuccess": "导入 Enka 数据成功",
"importFreeSRDataSuccess": "导入 FreeSR 数据成功",
"importDatabaseSuccess": "导入数据库成功",
"getData": "获取数据",
"import": "导入",
"freeSRImport": "导入 FreeSR",
"onlySupportFreeSRJsonFile": "仅支持 FreeSR JSON 文件",
"pickAFile": "选择文件",
"lightConeSetting": "光锥设置",
"relicMaker": "遗器制作",
"pleaseSelectAllOptions": "请选择所有选项",
"relicSavedSuccessfully": "遗器已成功保存",
"mainSettings": "主设置",
"mainStat": "主属性",
"set": "套装",
"pleaseSelectASet": "请选择一个套装",
"effectBonus": "效果加成",
"totalRoll": "总次数",
"randomizeStats": "随机属性",
"randomizeRolls": "随机次数",
"selectASubStat": "选择副属性",
"selectASet": "选择套装",
"selectAMainStat": "选择主属性",
"save": "保存",
"reset": "重置",
"roll": "滚动",
"step": "步数",
"memoryOfChaos": "混沌之忆",
"pureFiction": "虚构叙事",
"apocalypticShadow": "末日幻影",
"customEnemy": "自定义敌人",
"simulatedUniverse": "模拟宇宙",
"floor": "层数",
"side": "上/下半场",
"wave": "波次",
"stage": "关卡",
"useCycleCount": "使用轮次数?",
"useTurbulenceBuff": "使用紊乱增益?",
"firstHalfEnemies": "上半场敌人",
"secondHalfEnemies": "下半场敌人",
"listEnemies": "敌人列表",
"turbulenceBuff": "紊乱增益",
"noEventSelected": "未选择事件",
"noTurbulenceBuff": "未选择紊乱增益",
"upper": "上半",
"lower": "下半",
"upperToLower": "上半 -> 下半",
"lowerToUpper": "下半 -> 上半",
"selectMOCEvent": "选择 MOC 事件",
"selectPFEvent": "选择 PF 事件",
"selectASEvent": "选择 AS 事件",
"selectCEEvent": "选择 CE 事件",
"selectEvent": "选择事件",
"selectFloor": "选择层数",
"selectSide": "选择上/下半场",
"selectBuff": "选择 buff",
"selectStage": "选择关卡",
"previous": "上一页",
"next": "下一页",
"noMonstersFound": "未找到怪物",
"addNewWave": "添加新波次",
"searchStage": "搜索关卡...",
"noStageFound": "未找到关卡",
"searchMonster": "搜索怪物...",
"changeRelic": "更换遗物",
"deleteRelic": "删除遗物",
"deleteRelicConfirm": "确定要删除插槽中的遗物吗",
"setEffects": "设置效果",
"details": "详情",
"normal": "普通攻击",
"bpskill": "技能",
"maze": "技巧",
"ultra": "终结技",
"servantskill": "记灵技能",
"severaltalent": "记灵天赋",
"singleattack": "单体攻击",
"enhance": "强化",
"summon": "召唤",
"blast": "爆裂",
"restore": "恢复",
"support": "支援",
"aoeattack": "范围攻击",
"mazeattack": "迷宫秘技",
"impair": "削弱",
"active": "活跃",
"inactive": "不活跃",
"maxAll": "全部最大化",
"defence": "防御",
"maxAllSuccess": "技能等级已成功设置为最大。",
"maxAllFailed": "设置技能等级为最大失败。",
"noRelicEquipped": "未装备圣遗物",
"anomalyArbitration": "异相仲裁",
"normalMode": "普通模式",
"hardMode": "困难模式",
"selectPEAKEvent": "选择 PEAK 事件",
"mode": "模式",
"selectMode": "选择模式",
"rollBack": "回到之前的状态",
"upRoll": "增加副属性",
"downRoll": "减少副属性",
"actions": "操作",
"avatars": "头像",
"quickView": "快速预览",
"extraSetting": "额外设置",
"disableCensorship": "禁用审查",
"hideUI": "隐藏界面",
"theoryCraftMode": "Theory Craft 模式",
"cycleCount": "循环次数",
"pleaseSelectAllSubStats": "请选取所有副属性",
"subStatRollCountCannotBeZero": "副属性的行数不能为0",
"theoryCraft": "Theory Craft",
"multipathCharacter": "多命途角色",
"mainPath": "主角命途",
"march7Path": "三月七命途",
"challenge": "挑战",
"skipNode": "跳过节点",
"disableSkip": "禁用跳过",
"skipNode1": "跳过节点1",
"skipNode2": "跳过节点2",
"extraFeatures": "附加功能",
"detailTheoryCraft": "开启后可自定义循环数,并在敌人设置中调整生命值。",
"detailSkipNode": "开启后可跳过混沌回忆或虚构叙事的节点1/节点2。",
"detailChallengePeak": "允许更改当前异相中的「巅峰」赛季。",
"detailHiddenUi": "开启后将隐藏游戏界面。",
"detailDisableCensorship": "开启后将关闭游戏内的审查。",
"detailMultipathCharacter": "允许更改部分角色的命途。",
"trailblazer": "开拓者",
"listExtraEffect": "额外效果列表",
"extra": "额外"
}
}

View File

@@ -72,6 +72,7 @@
"connectionType": "Connection Type",
"status": "Status",
"connectPs": "Connect PS",
"disconnect": "Disconnect",
"other": "Other",
"freeSr": "FreeSR",
"database": "Database",

View File

@@ -72,6 +72,7 @@
"connectionType": "接続タイプ",
"status": "ステータス",
"connectPs": "PS接続",
"disconnect": "切断",
"other": "その他",
"freeSr": "FreeSR",
"database": "データベース",

View File

@@ -72,6 +72,7 @@
"connectionType": "연결 타입",
"status": "상태",
"connectPs": "PS 연결",
"disconnect": "연결 끊기",
"other": "기타",
"freeSr": "FreeSR",
"database": "데이터베이스",

View File

@@ -72,6 +72,7 @@
"connectionType": "Loại kết nối",
"status": "Trạng thái",
"connectPs": "Kết nối PS",
"disconnect": "Ngắt kết nối",
"other": "Khác",
"freeSr": "FreeSR",
"database": "Database",

View File

@@ -72,6 +72,7 @@
"connectionType": "连接类型",
"status": "状态",
"connectPs": "连接 PS",
"disconnect": "断开连接",
"other": "其他",
"freeSr": "FreeSR",
"database": "数据库",

View File

@@ -3,6 +3,7 @@
import { connectToPS, syncDataToPS } from "@/helper"
import useConnectStore from "@/stores/connectStore"
import useGlobalStore from "@/stores/globalStore"
import { PSConnectType } from "@/types"
import { useTranslations } from "next-intl"
import { useState } from "react"
@@ -21,11 +22,10 @@ export default function ConnectBar() {
setUsername,
setPassword
} = useConnectStore()
const { isConnectPS } = useGlobalStore()
const { isConnectPS, setIsConnectPS } = useGlobalStore()
return (
<div className="px-6 py-4">
{/* Select connection type */}
<div className="form-control grid grid-cols-1 w-full mb-6">
<label className="label">
<span className="label-text font-semibold text-purple-300">{transI18n("connectionType")}</span>
@@ -33,15 +33,18 @@ export default function ConnectBar() {
<select
className="select w-full select-bordered border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
value={connectionType}
onChange={(e) => setConnectionType(e.target.value)}
onChange={(e) => {
setIsConnectPS(false)
setConnectionType(e.target.value)
}}
>
<option value="FireflyGo">FireflyGo</option>
<option value="Other">{transI18n("other")}</option>
<option value={PSConnectType.FireflyGo}>FireflyGo</option>
<option value={PSConnectType.RobinSR}>RobinSR</option>
<option value={PSConnectType.Other}>{transI18n("other")}</option>
</select>
</div>
{/* Show host/port if Other */}
{connectionType === "Other" && (
{connectionType === PSConnectType.Other && (
<div className="flex flex-col md:space-x-4 mb-6 gap-2">
<div className="form-control w-full mb-4 md:mb-0">
<label className="label">
@@ -105,7 +108,6 @@ export default function ConnectBar() {
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6 mb-2">
{/* Status */}
<div className="flex items-center justify-center md:justify-start">
<span className="text-md mr-2">{transI18n("status")}:</span>
<span
@@ -114,6 +116,15 @@ export default function ConnectBar() {
>
{isConnectPS ? transI18n("connected") : transI18n("unconnected")}
</span>
{isConnectPS && (
<span
className={`badge ${isConnectPS ? "badge-success" : "badge-error"
} badge-lg`}
>
{isConnectPS ? transI18n("connected") : transI18n("unconnected")}
</span>
)}
</div>
{/* Buttons */}

View File

@@ -1,7 +1,7 @@
export const listCurrentLanguage = {
ja: "JP",
ko: "KR",
en: "US",
en: "EN",
vi: "VN",
zh: "CN"
};

View File

@@ -5,111 +5,101 @@ import useUserDataStore from "@/stores/userDataStore"
import { converterToFreeSRJson } from "./converterToFreeSRJson"
import { psResponseSchema } from "@/zod"
import useGlobalStore from "@/stores/globalStore"
import { ActionResult, ExtraData, ProxyPayload, ProxyResponse, PSConnectType, PSResponse } from "@/types"
export const connectToPS = async (): Promise<{ success: boolean, message: string }> => {
const {
connectionType,
privateType,
serverUrl,
username,
password
} = useConnectStore.getState()
const { setExtraData, setIsConnectPS } = useGlobalStore.getState()
let urlQuery = serverUrl
if (!urlQuery.startsWith("http://") && !urlQuery.startsWith("https://")) {
urlQuery = `http://${urlQuery}`
}
if (connectionType === "FireflyGo") {
urlQuery = "http://localhost:21000/sync"
} else if (connectionType === "Other" && privateType === "Server") {
const response = await SendDataThroughProxy({data: {username, password, serverUrl, data: null, method: "POST"}})
if (response instanceof Error) {
return { success: false, message: response.message }
} else if (response.error) {
return { success: false, message: response.error }
} else {
const parsed = psResponseSchema.safeParse(response.data)
if (!parsed.success) {
return { success: false, message: "Invalid response schema" }
}
return { success: true, message: "" }
}
}
const response = await SendDataToServer(username, password, urlQuery, null)
if (typeof response === "string") {
setIsConnectPS(false)
return { success: false, message: response }
} else if (response.status != 200) {
setIsConnectPS(false)
return { success: false, message: response.message }
} else {
setIsConnectPS(true)
const getUrlQuery = (connectionType: PSConnectType | string, serverUrl: string): string => {
if (connectionType === PSConnectType.FireflyGo) return "http://localhost:21000/sync"
if (connectionType === PSConnectType.RobinSR) return "http://localhost:21000/srtools"
setExtraData(response?.extra_data)
return { success: true, message: "" }
if (!serverUrl.startsWith("http://") && !serverUrl.startsWith("https://")) {
return `http://${serverUrl}`
}
return serverUrl
}
export const syncDataToPS = async (): Promise<{ success: boolean, message: string }> => {
const {
connectionType,
privateType,
serverUrl,
username,
password
} = useConnectStore.getState()
const handleProxyRequest = async (payload: ProxyPayload): Promise<ActionResult> => {
const response = await SendDataThroughProxy({
data: { ...payload, method: "POST" }
}) as ProxyResponse | Error
const {extraData, setIsConnectPS, setExtraData, isEnableChangePath, isEnableLua} = useGlobalStore.getState()
const {avatars, battle_type, moc_config, pf_config, as_config, ce_config, peak_config} = useUserDataStore.getState()
const data = converterToFreeSRJson(avatars, battle_type, moc_config, pf_config, as_config, ce_config, peak_config)
let urlQuery = serverUrl
if (!urlQuery.startsWith("http://") && !urlQuery.startsWith("https://")) {
urlQuery = `http://${urlQuery}`
if (response instanceof Error) {
return { success: false, message: response.message }
}
if (connectionType === "FireflyGo") {
urlQuery = "http://localhost:21000/sync"
} else if (connectionType === "Other" && privateType === "Server") {
const response = await SendDataThroughProxy({data: {username, password, serverUrl, data, method: "POST"}})
if (response instanceof Error) {
return { success: false, message: response.message }
} else if (response.error) {
return { success: false, message: response.error }
} else {
const parsed = psResponseSchema.safeParse(response.data)
if (!parsed.success) {
return { success: false, message: "Invalid response schema" }
}
return { success: true, message: "" }
}
}
const newExtra = structuredClone(extraData)
if (newExtra && !isEnableChangePath) {
newExtra.multi_path = undefined
if (response.error) {
return { success: false, message: response.error }
}
if (newExtra && !isEnableLua) {
newExtra.lua = null
const parsed = psResponseSchema.safeParse(response.data)
if (!parsed.success) {
return { success: false, message: "Invalid response schema" }
}
const response = await SendDataToServer(username, password, urlQuery, data, newExtra)
return { success: true, message: "" }
}
const handleDirectServerResponse = (
response: PSResponse | string,
setIsConnectPS: (val: boolean) => void,
onSuccess: (extraData?: ExtraData) => void
): ActionResult => {
if (typeof response === "string") {
setIsConnectPS(false)
return { success: false, message: response }
} else if (response.status != 200) {
}
if (response.status !== 200) {
setIsConnectPS(false)
return { success: false, message: response.message }
} else {
setIsConnectPS(true)
const newData = structuredClone(response?.extra_data)
}
setIsConnectPS(true)
onSuccess(response?.extra_data)
return { success: true, message: "" }
}
export const connectToPS = async (): Promise<ActionResult> => {
const { connectionType, privateType, serverUrl, username, password } = useConnectStore.getState()
const { setExtraData, setIsConnectPS } = useGlobalStore.getState()
if (connectionType === "Other" && privateType === "Server") {
return handleProxyRequest({ username, password, serverUrl, data: undefined })
}
const urlQuery = getUrlQuery(connectionType, serverUrl)
const response = await SendDataToServer(username, password, urlQuery, undefined)
return handleDirectServerResponse(response, setIsConnectPS, (extraData) => {
setExtraData(extraData)
})
}
export const syncDataToPS = async (): Promise<ActionResult> => {
const { connectionType, privateType, serverUrl, username, password } = useConnectStore.getState()
const { extraData, setIsConnectPS, setExtraData, isEnableChangePath, isEnableLua } = useGlobalStore.getState()
const { avatars, battle_type, moc_config, pf_config, as_config, ce_config, peak_config } = useUserDataStore.getState()
const data = converterToFreeSRJson(avatars, battle_type, moc_config, pf_config, as_config, ce_config, peak_config)
if (connectionType === "Other" && privateType === "Server") {
return handleProxyRequest({ username, password, serverUrl, data })
}
const urlQuery = getUrlQuery(connectionType, serverUrl)
const payloadExtra: PSResponse['extra_data'] = structuredClone(extraData)
if (payloadExtra) {
if (!isEnableChangePath) payloadExtra.multi_path = undefined
if (!isEnableLua) payloadExtra.lua = null
}
const response = await SendDataToServer(username, password, urlQuery, data, payloadExtra)
return handleDirectServerResponse(response, setIsConnectPS, (responseExtraData) => {
const newData = structuredClone(responseExtraData)
if (newData) {
newData.lua = extraData?.lua || null
}
setExtraData(newData)
return { success: true, message: "" }
}
})
}

View File

@@ -1,6 +1,7 @@
import useConnectStore from "@/stores/connectStore";
import useDetailDataStore from "@/stores/detailDataStore";
import { ASConfigStore, AvatarJson, AvatarStore, BattleConfigJson, CEConfigStore, FreeSRJson, LightconeJson, MOCConfigStore, PEAKConfigStore, PFConfigStore, RelicJson } from "@/types";
import { ASConfigStore, AvatarJson, AvatarStore, BattleConfigJson, CEConfigStore, FreeSRJson, LightconeJson, MOCConfigStore, PEAKConfigStore, PFConfigStore, PSConnectType, RelicJson } from "@/types";
export function converterToFreeSRJson(
@@ -13,6 +14,7 @@ export function converterToFreeSRJson(
peak_config: PEAKConfigStore,
): FreeSRJson {
const { skillConfig } = useDetailDataStore.getState()
const { connectionType } = useConnectStore.getState()
const lightcones: LightconeJson[] = []
const relics: RelicJson[] = []
let battleJson: BattleConfigJson
@@ -48,7 +50,7 @@ export function converterToFreeSRJson(
}
} else if (battle_type === "CE") {
battleJson = {
battle_type: battle_type,
battle_type: connectionType === PSConnectType.FireflyGo ? battle_type : "DEFAULT",
blessings: ce_config.blessings,
custom_stats: [],
cycle_count: ce_config.cycle_count,
@@ -58,7 +60,7 @@ export function converterToFreeSRJson(
}
} else if (battle_type === "PEAK") {
battleJson = {
battle_type: battle_type,
battle_type: connectionType === PSConnectType.FireflyGo ? battle_type : "DEFAULT",
blessings: peak_config.blessings,
custom_stats: [],
cycle_count: peak_config.cycle_count,

View File

@@ -117,7 +117,7 @@ export async function SendDataToServer(
username: string,
password: string,
serverUrl: string,
data: FreeSRJson | null,
data?: FreeSRJson,
extraData?: ExtraData
): Promise<PSResponse | string> {
try {

View File

@@ -18,3 +18,4 @@ export * from "./metaData"
export * from "./monsterDetail"
export * from "./filter"
export * from "./metaData"
export * from "./psConnect"

32
src/types/psConnect.ts Normal file
View File

@@ -0,0 +1,32 @@
import { ExtraData } from "./extraData";
import { FreeSRJson } from "./srtools";
export enum PSConnectType {
FireflyGo = "FireflyGo",
RobinSR = "RobinSR",
Other = "Other",
}
export interface ProxyPayload {
username?: string;
password?: string;
serverUrl: string;
data?: FreeSRJson;
}
export interface ProxyResponse {
error?: string;
message?: string;
data?: unknown;
}
export interface PSResponse {
status: number;
message: string;
extra_data?: ExtraData
}
export interface ActionResult {
success: boolean;
message: string;
}

View File

@@ -1,5 +1,3 @@
import { ExtraData } from "./extraData";
export interface SubAffix {
sub_affix_id: number;
count: number;
@@ -81,9 +79,4 @@ export interface FreeSRJson {
loadout?: LoadoutJson[];
}
export interface PSResponse {
status: number;
message: string;
extra_data?: ExtraData
}