概览与界面

SGS Framework 由下载器在 Laya 初始化前注入。Core 统一管理生命周期、运行时事件、可逆 Hook、插件安装与配置、LocalStorage 前缀、版本检测和 Laya 底层访问。

屏幕边缘的 SGS 悬浮窗自动吸附左侧或右侧。自动隐藏开启时,只保留边缘感应区,鼠标进入当前侧边缘才显示;拖动后在边缘松开也会按该设置隐藏。点击悬浮窗可选择“选项”、切换自动隐藏,并查看不可点击的 Core 版本号。

选项窗口默认占视口宽高各 50%,可拖动和从右下角缩放。最小宽高为视口的 20%,同时保留 260×180 像素的可用下限;移动、缩放、视口变化和浏览器缩放后,窗口都会被约束在屏幕内。

SGS Framework 插件配置窗口
插件管理、插件配置与 Core 管理三个 Tab;插件声明配置,Core 负责排版。
全局入口window.SgsFramework
官方源/script/plugin/index.json
网络超时5000 ms / 请求

安装与入口

  1. 在油猴脚本管理器中打开 download.mjs 并安装。
  2. 下载器在 document-start 安装 Laya 初始化屏障,优先请求本站 core.mjs,失败后请求 GitHub;两个来源各自最多等待 5 秒。
  3. 远程请求都失败时,下载器先尝试页面 LocalStorage 缓存;仍不可用时显示“继续游戏 / 刷新重试”。选择继续时使用可用缓存,没有缓存则不加载 Framework。
  4. Core 注入完成后释放 Laya 屏障,恢复已启用插件并执行静默版本检测。
用户手册:https://sgs.senrax.com/
安装脚本:https://sgs.senrax.com/script/download.mjs
公开目录:https://sgs.senrax.com/script/

https://web.sanguosha.com/login/index.html 登录页,Core 会显示只读版本标记,便于确认注入是否成功。

Core API

window.SgsFrameworkwindow.__SgsFramework 指向同一个实例。插件优先使用传入的 context;只有框架调试或插件接口不足时才直接使用顶层服务。

入口暴露方法与定义
Corestatus() 返回版本、插件、Laya、事件监视、更新和 UI 状态;addDisposer(fn,label) 注册清理;dispose(reason) 停止插件并释放全部资源。
eventson(type,handler)onceoffemitclearlistenerCounton/once 返回取消函数。
loggerscope(name) 创建作用域 Logger;infowarnerror 同时写控制台和 log 事件。
storagescope(name)get/set/removegetJson/setJsonkeys。Core 负责真实 LocalStorage 前缀与 JSON 序列化。
timerssetTimeout(fn,ms,label)setInterval(fn,ms,label),返回取消函数并参与 Core 热重载清理。
hookswrapMethod(owner,name,handler,{label})。Handler 接收 {original,args,thisArg,callOriginal};返回的取消函数恢复包装前方法。
domcreateOverlaycreatePanelcreateButtontoast。创建结果包含元素与移除函数。
browseropenUrlrequestrequestTextrequestJsonrequestHeaders。请求默认不带凭据、禁用缓存并在 5 秒后中止。
layagetLaya()getStage()status()whenReady()walk()find()snapshot()。节点引用保持为原始 Laya 对象。
runtimestart()stop()poll()status()getBattle(),封装战斗、窗口和公开日志生命周期。
pluginsinspect/addFromUrl/installenable/disable/removecheckOne/checkUpdates/updatesettings/config/invokeAction/list、插件源与孤儿配置管理方法。
updatecheck()reload()status()reload 执行当前页面 Core 热替换。
uimount()openWindow(tab)closeWindow()selectTab()refresh()confirmAction()toast()status()
const framework = window.SgsFramework;
const remove = framework.events.on("battle:start", ({ scene, manager }) => {
  framework.logger.info("battle started", scene, manager);
});

// 插件卸载时调用 remove,或交给 context.addDisposer(remove)。

插件定义

插件是可直接下载、无需构建的经典 JavaScript 文件。Core 在执行代码前解析 Header,用于安装确认和版本检测;正文必须调用 SgsFramework.plugins.define()

Header含义
@id稳定且唯一的插件 ID;更新包必须保持一致。
@name / @version显示名称与可比较版本号。
@description安装确认框和插件列表中的简介。
@permissions逗号分隔的能力声明,仅披露,不构成沙箱。
@updateModedefaultheaderapi,不区分大小写。
@versionHeaderHeader 模式读取的响应头,默认 x-sgs-plugin-version
@versionApiAPI 模式 JSON 地址;相对地址按插件 URL 解析。
// ==SgsPlugin==
// @id           example.plugin
// @name         Example Plugin
// @version      1.0.0
// @description  Example description.
// @permissions  core.events,browser.dom-overlay
// @updateMode   default
// ==/SgsPlugin==

(() => {
  SgsFramework.plugins.define({
    id: "example.plugin",
    manifest: {
      name: "Example Plugin",
      version: "1.0.0",
      description: "Example description.",
      permissions: ["core.events", "browser.dom-overlay"]
    },
    defaults: {},
    settings: [],
    install(context) {
      const remove = context.events.on("battle:end", () => {});
      context.addDisposer(remove, "battle-end");
      return () => {};
    }
  });
})();

context 提供 frameworkidmanifestsourceUrleventsloggerstorageconfigtimershooksdombrowserlayaruntimeaddDisposerinstall 返回的函数也会在禁用、更新、删除或 Core 热替换时调用。

插件配置

插件只声明配置、默认值和动作,Core 负责 Group、行列、控件、错误提示与重置按钮。每个插件独立显示并可收起;没有配置项的插件仍显示“无可配置项”,且不显示重置按钮。

type声明与触发
toggle布尔开关;onChange 在成功保存后调用插件动作。
buttonaction / onClick 指向插件函数;插件未启动时禁用。
textTextBox;支持 inputTypemin/maxminLength/maxLength 和一个或多个 patterns
selectoptions: [{value,label}] 下拉框;可声明 onChange
customrender / action 接收 Core 创建的 host;插件仅负责 host 内部复杂 UI。
defaults: { enabled: true, name: "demo", mode: "safe" },
settings: [{
  id: "general",
  name: "General",
  items: [
    { key: "enabled", name: "Enabled", type: "toggle", onChange: "apply" },
    { key: "name", name: "Name", type: "text",
      minLength: 1, maxLength: 20,
      patterns: ["^[A-Za-z]", "^[A-Za-z0-9_-]+$"] },
    { key: "mode", name: "Mode", type: "select", options: [
      { value: "safe", label: "Safe" },
      { value: "full", label: "Full" }
    ], onChange: "apply" },
    { name: "Open list", type: "button", action: "openList" },
    { name: "Complex", type: "custom", render: "renderComplex" }
  ]
}],
actions: {
  apply(context, value, key) {},
  openList(context) {},
  renderComplex(context, host) {},
  onReset(context, values) {}
}

context.config.get/set/reset/defaults 与配置页共享同一状态。值先通过全部约束,再序列化到 LocalStorage,随后触发动作;动作读取 context.config 时得到的就是本次新值。首次安装且没有配置、或用户按下重置时,使用插件的 defaults

更新检测

模式请求与版本来源
defaultGET 插件 JavaScript,读取响应体并解析 Header 的 @version
header通过 Fetch 发送 HEAD,不读取响应体;读取 @versionHeader 指定的响应头。
apiGET @versionApi JSON;必须提供 version,可提供本次下载使用的 url

Core 恢复插件后自动检测插件版本,同时检测自身版本。插件管理页提供手动检测按钮;远程版本更高时,插件行显示更新按钮。Core 页显示当前/远程版本,并在有更新时显示热重载按钮。

插件更新会保存新源码、停止旧实例、执行新定义并恢复原启用状态。Core 热重载会释放旧 Core 的插件、Hook、定时器、监听器和 DOM,再由新 Core 从缓存源码恢复已启用插件。

跨域来源必须允许当前游戏页面读取响应。Header 模式若使用自定义响应头,还要通过 Access-Control-Expose-Headers 暴露该名称。

运行时事件

事件触发证据与 Payload
framework:readyCore 服务和 UI 已发布;Payload 为 status() 快照。
battle:start有效可见的 TableGameScene/RogueLikeGameScenemanager.seats 存在;提供 scenemanagersceneNamestartedAt
battle:endScene 离开、失去有效可见性、seats 消失或被替换;提供原 Scene/Manager、原因和结束时间。
battle:log来自公开 Laya HTML 战斗日志容器;提供 html、纯文本、节点和 Scene。
window:openStage 中有效可见的 Window/Dialog/Popup 节点出现;提供稳定对象 ID、原始节点和节点信息。
window:close此前可见窗口离开集合;提供同一对象 ID、原始节点和关闭时间。
插件事件plugin:installed/removed/started/stopped/updatedplugin:config-changedplugin:config-reset

有效可见性会检查节点及父链的 visiblealphascaleX/scaleYdestroyed。Core 不通过 console.log 判断战斗,也不把可能残留的窗口注册表当作事实。

高层事件不足时,可从 framework.laya.getLaya()getStage() 或事件 Payload 取得原始 Laya 对象并调用底层公开接口。

存储

sgs.framework.ui.dock
sgs.framework.ui.window
sgs.framework.plugins.registry
sgs.framework.plugins.sources
sgs.framework.plugins.catalog-cache
sgs.framework.plugins.<pluginId>.code
sgs.framework.plugins.<pluginId>.config
sgs.framework.plugins.<pluginId>.data.*

context.config 保存 Core 排版配置;context.storage 保存插件内部配置或数据。插件只传相对键,Core 自动添加 plugins.<pluginId> 前缀并负责序列化。

Core 启动时把历史双分隔符键(例如 ui..dockdata..runtime-source)迁移为单点键;如果单点键已经存在,以单点键为准。

删除插件会删除注册记录和源码缓存,但保留 .config.data.*。Core 页的“检查已删除插件配置”按插件 ID 汇总这些孤儿键、数量和字符数;用户确认删除时只移除该插件的配置/数据,不影响已安装或被禁用插件。

插件源

插件管理页按来源分组,每个官方/第三方/手动来源都可收起。官方源固定为 /script/plugin/index.json。第三方源可添加名称与 Catalog URL,并支持禁用、重命名和删除;删除来源不会删除已经安装的插件。

{
  "name": "Source name",
  "plugins": [
    { "id": "example.plugin", "url": "https://example/plugin.js" }
  ]
}

从 URL 安装插件时,Core 先下载并解析 Header,再显示名称、版本、简介、来源 URL 和权限列表。用户确认后才执行插件代码。

官方插件

官方源提供 SGS Card Tracker、SGS Blocker 和 SGS Automation。它们与普通插件使用同一套安装、启用、禁用、更新和删除流程;插件停止后,所属 Hook、DOM、定时器、离屏对象、等待任务和可调用服务都会释放。

SGS Card Tracker

无配置项。插件加载与 Scripts/dist/sgs-runtime.js 相同的运行时构建,只在有效可见的 TableGameSceneRogueLikeGameScene 中显示 UI。离开战斗后立即停止描画并清理临时状态。已知牌只来自自己的手牌、公开日志或窗口、公开区和明确揭示牌的协议记录;隐藏对手 handCards 不作为已知事实。

SGS Blocker

提供“屏蔽广告 / 屏蔽特效 / 屏蔽道具弹窗”三个独立开关。每项都使用当前 Laya 类与已经核对的具体路径,不按宽泛英文类名隐藏节点,也不全局关闭 Tween、Timer、Animation 或渲染。

开关作用范围
屏蔽广告阻止 ModeScene.showAdView、隐藏当前广告视图、过滤类型 0x27cb 的军典入口,并通过公开关闭方法处理 AdPushWindow
屏蔽特效XMLHttpRequest.open 中只替换已确认的特效资源模式;保留铁索/酒白名单,以及 Plot_tiesuolianhuanFX_SHT_SLCX 的结束回调语义。
屏蔽道具弹窗处理 GetPropSpecialWindowGeneralOpenResultWindowSkinOpenResultWindowNewOldbackOneClickDrawAwdWinTianShuWindow type=6

禁用、更新、删除 Blocker 或热替换 Core 时,每个 Hook 都恢复包装前的方法。

SGS Automation

无配置项。插件通过 plugins.invokeAction() 暴露九个供 Agent 或控制台调用的函数。模式入口使用当前有效可见的 Laya 节点;角色信息按固定白名单读取自己的角色对象;领奖、免费抽取与免费擂鼓使用当前运行时管理器的资格状态,不要求目标窗口已经显示。相同 Action 的并发调用会复用同一个进行中的 Promise,不会重复发送请求。

函数目标与结果
openRogueMap查找 modeId=151 并打开山河图;已经位于大地图、小地图或山河图战斗 Scene 时返回 already-open
openGeneralTrial查找 modeId=147 并打开武将试炼;已经位于试炼 Scene 或挑战窗口时返回 already-open
getCharacterInfo返回当前角色的 ID、昵称、等级与经验、元宝/绑定元宝/总元宝、银两、今日在线时长、VIP 与官阶、工会、信誉/人气、势力和主公等级等白名单字段,不打开个人资料窗口。
claimDailyTaskRewards循环领取状态为 CanAward 的每日任务,再按运行时数组处理累计次数银两奖励。任务数量可随跨日变化;函数不打开任务栏,也不把任务 View 加入 Stage。
claimMonthCardRewards先同步直购月卡状态;月卡有效且当日奖励可领时领取当日奖励,随后领取全部可领累计奖励。即使当日奖励已领取,也继续检查累计档位。
claimFreeGeneralDrawRewards读取武将抽取的免费标记、免费掉落 ID、免费类型 Free_MianFei_Draw 和请求占用状态。四项共同确认免费后只发送一次免费抽取;没有免费次数时返回 nothing-claimable
claimFreeBlessDrawRewards先同步祈福轮次数据,遍历所有运行时轮次;只有 freeOpen、活动期、抽取期和 CanFreeBless(roundId) 同时成立时,才以 count=1 抽取。每次等待免费状态消失后再处理下一轮次。
useFreeGuildDrums确认角色已加入公会、CanDrum=trueLeftFreeDrumTimes>0,随后循环调用常山战鼓的免费公会捐献请求,直到免费次数为零或一次请求未确认。
claimOfficerSalaryRewards分别检查 CanGetOfficerDayReward()CanGetOfficerWeekReward();每日俸禄存在时先领取每日并等待资格变为不可领,再检查、领取并确认每周俸禄。
const invoke = (action) => SgsFramework.plugins.invokeAction(
  "sgs.automation", action
);

const rogue = await invoke("openRogueMap");
const trial = await invoke("openGeneralTrial");
const character = await invoke("getCharacterInfo");
const dailyRewards = await invoke("claimDailyTaskRewards");
const monthCardRewards = await invoke("claimMonthCardRewards");
const generalDraw = await invoke("claimFreeGeneralDrawRewards");
const blessDraw = await invoke("claimFreeBlessDrawRewards");
const guildDrums = await invoke("useFreeGuildDrums");
const officerSalary = await invoke("claimOfficerSalaryRewards");

每个函数都返回 okactionstatus,并附带适合该操作的调用前后状态。没有可执行项时返回 nothing-claimable;月卡未激活返回 card-inactive;未加入公会返回 not-in-guild;请求占用、刷新超时、回包未确认或部分完成会返回相应结构化状态。插件未启用时返回 inactive。模式入口不会选择武将、开始挑战、确认弹窗或继续战斗;奖励、免费抽取和免费擂鼓函数不会购买、续费、消耗元宝或使用付费擂鼓道具。

权限与边界

插件权限用于安装前披露,不是能力沙箱,也不会按权限名拒绝执行。插件与普通页面脚本处于同一页面环境,可以读取对象、调用公开方法、发起网络请求或创建 DOM。只安装可信来源,并审查声明了网络、Hook、Laya、游戏请求或 DOM 能力的代码。

Framework 不替插件绕过付费、购买确认或服务器权限。Card Tracker 不得把隐藏对手手牌当作已知事实。Blocker 只能使用具体、可恢复且经过当前运行时核对的路径。Automation 的模式入口会改变当前游戏界面,但只调用活动场分页与模式入口方法;不选择武将、不开始挑战、不确认弹窗,也不调用购买路径。入口条件不成立时返回结构化失败。

getCharacterInfo 只读取当前 Stage 中 IsSelf=true 的角色对象,并按固定白名单构造新对象。它不返回原始运行时引用,也不返回 Session、重连 Token、二级密码、账号散列或登录 IP。当前 Scene 没有自己的角色对象时返回 character-unavailable,不打开个人资料窗口,也不发出资料查询请求。

claimDailyTaskRewards 会发出真实任务领奖请求。它只处理运行时标记为每日且状态为 CanAward 的任务,再循环点击运行时提供的累计银两档位,由游戏自身方法过滤未达标和已领取项目。任务数量不参与正确性判断,跨日增减只是本次循环输入不同。离屏任务对象不加入 Laya.stage,结束时销毁;taskUi.opened 用于确认调用没有新增任务界面。

claimMonthCardRewards 会发出真实特权卡同步与领奖请求。它先调用 SendClientPrivilegeDataReq() 同步服务器状态;仅当类型 3 月卡激活且 bReward=true 时调用 sendAwardReq()。累计奖励只对 AddAwardsCanAwardList() 为真的索引调用 addItemClick(index),并通过 AddAwardsHasAwardList() 确认。实现不调用含购买分支的 getBtnClick(),不会购买或续费。离屏月卡对象不加入 Stage,结束时解除监听并销毁。

claimFreeGeneralDrawRewards 会发出一次真实武将抽取请求。请求前必须同时确认免费掉落 ID 存在、客户端免费标记可见、免费按钮可用、FreeOpenGeneralPackTime/CanFreeOpenGeneral() 判定免费、请求池未占用,并使用运行时枚举 Free_MianFei_Draw。它不调用单抽、五连抽、购买道具或元宝补足路径;任一免费证据缺失时不发送请求。

claimFreeBlessDrawRewards 会先调用 SendAllListReq() 同步祈福数据。每个轮次都必须同时满足 freeOpen、活动时间、抽取时间和 CanFreeBless(roundId),才会发送 sendQifuDrawReq(roundId, 1)。函数不调用 blessBtnClick()confirmBuy() 或商店方法,因此不会落入灯数不足后的购买分支;同步失败或免费状态无法确认时不抽取。

useFreeGuildDrums 会发出真实公会擂鼓请求。它只在已加入公会、CanDrum=trueLeftFreeDrumTimes>0 时调用 SendGuildDonate(),并在每次免费次数减少后继续。它不调用亮银槌、鎏金锤、背包道具使用、SendGuildDrumUse() 或元宝确认路径。免费次数为零、加入公会时间不足或不在公会时不请求。

claimOfficerSalaryRewards 会发出真实官阶领奖请求。每日俸禄只在 CanGetOfficerDayReward() 为真时调用 sendGetDayReward(),并以资格状态变为 false 确认;随后每周俸禄只在 CanGetOfficerWeekReward() 为真时调用 sendGetWeekReward(),使用同一方式确认。函数不升级官阶、不领取其他官阶活动奖励,也不打开官阶窗口。

禁用、更新、删除插件或热替换 Core 时,插件释放 Hook、监听器、定时器、离屏对象、DOM、等待任务和内部可调用服务。Automation Action 只在插件启用期间工作;停用后的调用返回 inactive。相同 Action 的并发调用复用同一 Promise,停用时取消事件等待,循环在当前短轮询结束后停止继续请求。

浏览器 Fetch 受 CORS 约束。第三方插件与 Catalog 来源需要允许游戏页面跨域访问;Header 更新模式还需要显式暴露版本响应头。