Inventory_Stack_Helper

Steam 物品堆叠工具

  1. // ==UserScript==
  2. // @name:zh-CN Steam 库存物品堆叠工具
  3. // @name Inventory_Stack_Helper
  4. // @namespace https://blog.chrxw.com
  5. // @supportURL https://blog.chrxw.com/scripts.html
  6. // @contributionURL https://afdian.com/@chr233
  7. // @version 2.5
  8. // @description Steam 物品堆叠工具
  9. // @description:zh-CN Steam 物品堆叠工具
  10. // @author Chr_
  11. // @match https://steamcommunity.com/profiles/*/inventory*
  12. // @match https://steamcommunity.com/id/*/inventory*
  13. // @license AGPL-3.0
  14. // @icon https://blog.chrxw.com/favicon.ico
  15. // @grant GM_addStyle
  16. // ==/UserScript==
  17.  
  18. // 初始化
  19. (() => {
  20. "use strict";
  21.  
  22. let GappId = 0;
  23. let GcontextId = 2;
  24.  
  25. const delay = 300;
  26. const amount = 5000;
  27.  
  28. let token = document.querySelector("#application_config")?.getAttribute("data-loyalty_webapi_token");
  29. if (token) {
  30. token = token.replace(/"/g, "");
  31. }
  32. else {
  33. ShowAlertDialog("提示", "读取Token失败, 可能需要重新登录(不可用)");
  34. return;
  35. }
  36.  
  37. const GObjs = addPanel();
  38. loadSetting();
  39. doFitInventory();
  40.  
  41. //==================================================================================================
  42.  
  43. function genBtn(text, title, onclick) {
  44. let btn = document.createElement("button");
  45. btn.textContent = text;
  46. btn.title = title;
  47. btn.className = "ish_button";
  48. btn.addEventListener("click", onclick);
  49. return btn;
  50. }
  51. function genSpan(text) {
  52. let span = document.createElement("span");
  53. span.textContent = text;
  54. return span;
  55. }
  56. function genNumber(value, placeholder, title) {
  57. const t = document.createElement("input");
  58. t.className = "ish_inputbox";
  59. t.placeholder = placeholder;
  60. t.title = title;
  61. t.type = "number";
  62. t.value = value;
  63. return t;
  64. }
  65.  
  66. function addPanel() {
  67. const btnArea = document.querySelector("div.inventory_links");
  68.  
  69. const container = document.createElement("div");
  70. container.className = "ish_container";
  71. btnArea.insertBefore(container, btnArea.firstChild);
  72.  
  73. const hiddenContainer = document.createElement("div");
  74. hiddenContainer.style.display = "none";
  75. container.appendChild(hiddenContainer);
  76.  
  77.  
  78. const btnStack = genBtn("堆叠", "堆叠库存中的物品", doStack);
  79. const btnUnstack = genBtn("反堆叠", "取消堆叠库存中的物品", doUnstack);
  80.  
  81. const iptStackMax = genNumber("0", "堆叠上限", "物品堆叠上限, 留空或者0表示不设置堆叠上限");
  82.  
  83. const btnHelp = genBtn("❓", "查看帮助", doHelp);
  84. const spStatus = genSpan("");
  85.  
  86. hiddenContainer.appendChild(genSpan("库存"));
  87.  
  88. container.appendChild(btnStack);
  89. container.appendChild(btnUnstack);
  90. container.appendChild(btnHelp);
  91. container.appendChild(iptStackMax);
  92. container.appendChild(spStatus);
  93.  
  94. document.querySelectorAll('div.games_list_tabs>a').forEach(tab => {
  95. tab.addEventListener("click", doFitInventory);
  96. });
  97.  
  98. document.querySelector("#responsive_inventory_select")?.addEventListener("change", doFitInventory);
  99.  
  100. return { btnStack, btnUnstack, iptStackMax, btnHelp, spStatus };
  101. }
  102.  
  103. function doHelp() {
  104. const { script: { version } } = GM_info;
  105.  
  106. ShowAlertDialog("帮助",
  107. [
  108. "<p>【堆叠】: 将指定库存中的同类物品堆叠到一起</p>",
  109. "<p>【反堆叠】: 将指定库存中的已堆叠物品拆分成单个物品</p>",
  110. `<p>【<a href="https://keylol.com/t954659-1-1" target="_blank">发布帖</a>】 【<a href="https://blog.chrxw.com/scripts.html" target="_blank">脚本反馈</a>】</p>`,
  111. `<p>【Developed by <a href="https://steamcommunity.com/id/Chr_" target="_blank">Chr_</a>】 【当前版本 ${version}】</p>`,
  112. ].join("<br>")
  113.  
  114. );
  115. }
  116.  
  117. function doFitInventory() {
  118. const { appid, contextid } = g_ActiveInventory;
  119.  
  120. GappId = appid ?? "0";
  121. GcontextId = contextid ?? "2";
  122.  
  123. if (GappId == 753) {
  124. GcontextId = "6";
  125. }
  126. }
  127.  
  128. function doStack() {
  129. const { btnStack, btnUnstack, iptStackMax, btnHelp, spStatus } = GObjs;
  130.  
  131. if (GappId !== GappId || GcontextId !== GcontextId) {
  132. ShowAlertDialog("提示", "库存状态无效");
  133. return;
  134. }
  135.  
  136. let stackMax = 0;
  137. if (iptStackMax.value) {
  138. stackMax = parseInt(iptStackMax.value);
  139. if (stackMax !== stackMax) {
  140. ShowAlertDialog("提示", "请检查 堆叠上限 是否填写正确");
  141. return;
  142. }
  143. }
  144.  
  145. saveSetting();
  146.  
  147. spStatus.textContent = "堆叠中 [正在加载库存]";
  148. btnStack.style.display = "none";
  149. btnUnstack.style.display = "none";
  150. btnHelp.style.display = "none";
  151. iptStackMax.style.display = "none";
  152.  
  153. loadInventory(GappId, GcontextId, amount)
  154. .then(async (inv) => {
  155. if (!inv) {
  156. ShowAlertDialog("提示", "库存读取失败, 请检查 AppId 和 ContextId 是否填写正确");
  157. return;
  158. }
  159.  
  160. const { assets } = inv;
  161. if (assets) {
  162. const itemGroup = {};
  163.  
  164. for (let item of assets) {
  165. const { classid } = item;
  166.  
  167. // 只处理宝珠和宝珠袋
  168. if (GappId === 753) {
  169. continue;
  170. }
  171.  
  172. if (!itemGroup[classid]) {
  173. itemGroup[classid] = [];
  174. }
  175. item.amount = parseInt(item.amount);
  176. itemGroup[classid].push(item);
  177. }
  178.  
  179. let totalReq = 0;
  180. const todoList = [];
  181. for (let classId in itemGroup) {
  182. const items = itemGroup[classId];
  183. if (stackMax === 0) {
  184. if (items.length > 1) {
  185. todoList.push(items);
  186. totalReq += items.length - 1;
  187. }
  188. } else {
  189. const stacks = [];
  190. while (items.length > 0) {
  191. const item = items.pop();
  192. if (item.amount > stackMax) {
  193. continue;
  194. }
  195.  
  196. let added = false;
  197. for (let stack of stacks) {
  198. if (stack.amount + item.amount <= stackMax) {
  199. stack.list.push(item);
  200. stack.amount += item.amount;
  201. added = true;
  202. break;
  203. }
  204. }
  205.  
  206. if (!added) {
  207. stacks.push({ list: [item,], amount: item.amount });
  208. }
  209. }
  210.  
  211. for (let stack of stacks) {
  212. if (stack.list.length >= 1) {
  213. todoList.push(stack.list);
  214. totalReq += stack.list.length - 1;
  215. }
  216. }
  217. }
  218. }
  219.  
  220. if (totalReq > 0) {
  221. const totalType = todoList.length;
  222. spStatus.textContent = `堆叠中 [种类 0/${totalType} 请求 0/${totalReq} 0.00%]`;
  223.  
  224. let type = 1;
  225. let req = 1;
  226. for (let items of todoList) {
  227. for (let i = 1; i < items.length; i++) {
  228. await stackItem(GappId, items[i].assetid, items[0].assetid, items[i].amount);
  229. await asyncDelay(delay);
  230. const percent = (100 * req / totalReq).toFixed(2);
  231. spStatus.textContent = `堆叠中 [种类 ${type}/${totalType} 请求 ${req++}/${totalReq} ${percent}%]`;
  232. }
  233. type++;
  234. }
  235. }
  236.  
  237. ShowAlertDialog("提示", totalReq > 0 ? "堆叠操作完成" : "无可堆叠物品");
  238. } else {
  239. ShowAlertDialog("提示", "读取库存失败, 请稍后重试");
  240. }
  241. })
  242. .catch((err) => {
  243. ShowAlertDialog("提示", "库存读取出错, 错误信息\r\n" + err);
  244. console.error(err);
  245. })
  246. .finally(() => {
  247. spStatus.textContent = "";
  248. btnStack.style.display = null;
  249. btnUnstack.style.display = null;
  250. btnHelp.style.display = null;
  251. iptStackMax.style.display = null;
  252. g_ActiveInventory.m_owner.ReloadInventory(appId, contextId);
  253. });
  254. }
  255.  
  256. function doUnstack() {
  257. const { btnStack, btnUnstack, iptStackMax, btnHelp, spStatus } = GObjs;
  258.  
  259. if (GappId !== GappId || GcontextId !== GcontextId) {
  260. ShowAlertDialog("提示", "请检查 AppId 和 ContextId 是否填写正确");
  261. return;
  262. }
  263.  
  264. saveSetting();
  265.  
  266. spStatus.textContent = "反堆叠中 [正在加载库存]";
  267. btnStack.style.display = "none";
  268. btnUnstack.style.display = "none";
  269. btnHelp.style.display = "none";
  270. iptStackMax.style.display = "none";
  271.  
  272. loadInventory(GappId, GcontextId, amount)
  273. .then(async (inv) => {
  274. if (!inv) {
  275. ShowAlertDialog("提示", "库存读取失败, 请检查 AppId 和 ContextId 是否填写正确");
  276. return;
  277. }
  278.  
  279. const { assets } = inv;
  280. if (assets) {
  281. const itemGroup = [];
  282. let totalReq = 0;
  283. for (let item of assets) {
  284. const { amount } = item;
  285.  
  286. if (GappId === 753) {
  287. continue;
  288. }
  289.  
  290. if (amount > 1) {
  291. item.amount = amount;
  292. itemGroup.push(item);
  293. totalReq += amount - 1;
  294. }
  295. }
  296.  
  297. if (totalReq > 0) {
  298. const totalType = itemGroup.length;
  299.  
  300. spStatus.textContent = `反堆叠中 [种类 0/${totalType} 请求 0/${totalReq} 0.00%]`;
  301.  
  302. let type = 1;
  303. let req = 1;
  304.  
  305. for (let item of itemGroup) {
  306. for (let i = 1; i < item.amount; i++) {
  307. await unStackItem(GappId, item.assetid, 1);
  308. await asyncDelay(delay);
  309. const percent = (100 * req / totalReq).toFixed(2);
  310. spStatus.textContent = `反堆叠中 [种类 ${type}/${totalType} 请求 ${req++}/${totalReq} ${percent}%]`;
  311. }
  312. type++;
  313. }
  314. }
  315.  
  316. ShowAlertDialog("提示", totalReq > 0 ? "反堆叠操作完成" : "无可反堆叠物品");
  317. } else {
  318. ShowAlertDialog("提示", "库存读取失败, 请检查 AppId 和 ContextId 是否填写正确");
  319. }
  320. })
  321. .catch((err) => {
  322. ShowAlertDialog("提示", "库存读取出错, 错误信息\r\n" + err);
  323. console.error(err);
  324. })
  325. .finally(() => {
  326. spStatus.textContent = "";
  327. btnStack.style.display = null;
  328. btnUnstack.style.display = null;
  329. btnHelp.style.display = null;
  330. iptStackMax.style.display = null;
  331. g_ActiveInventory.m_owner.ReloadInventory(appId, contextId);
  332. });
  333. }
  334.  
  335. function loadSetting() {
  336. const { iptStackMax } = GObjs;
  337. iptStackMax.value = localStorage.getItem("ish_limit") ?? "0";
  338. }
  339.  
  340. function saveSetting() {
  341. const { iptStackMax } = GObjs;
  342. localStorage.setItem("ish_limit", iptStackMax.value);
  343. }
  344.  
  345. //==================================================================================================
  346.  
  347. // 延时
  348. function asyncDelay(ms) {
  349. return new Promise(resolve => setTimeout(resolve, ms));
  350. }
  351.  
  352. // 读取库存
  353. function loadInventory(appId, contextId, count) {
  354. return new Promise((resolve, reject) => {
  355. fetch(`https://steamcommunity.com/inventory/${g_steamID}/${appId}/${contextId}?l=${g_strLanguage}&count=${count}`)
  356. .then(async (response) => {
  357. response.json().then(json => {
  358. if (json) {
  359. resolve(json);
  360. } else {
  361. fetch(`https://steamcommunity.com/inventory/${g_steamID}/${appId}/${contextId}?l=${g_strLanguage}&count=${count}`)
  362. .then(async (response) => {
  363. response.json().then(json => {
  364. if (json) {
  365. resolve(json);
  366. } else {
  367. fetch(`https://steamcommunity.com/inventory/${g_steamID}/${appId}/${contextId}?l=${g_strLanguage}&count=${count}`)
  368. .then(async (response) => {
  369. response.json().then(json => {
  370. if (json) {
  371. resolve(json);
  372. } else {
  373.  
  374. }
  375. });
  376. })
  377. .catch((err) => {
  378. console.error(err);
  379. reject(`读取库存失败 ${err}`);
  380. });
  381. }
  382. });
  383. })
  384. .catch((err) => {
  385. console.error(err);
  386. reject(`读取库存失败 ${err}`);
  387. });
  388. }
  389. });
  390. })
  391. .catch((err) => {
  392. console.error(err);
  393. reject(`读取库存失败 ${err}`);
  394. });
  395. });
  396. }
  397.  
  398. // 堆叠物品
  399. function stackItem(appId, fromAssetId, destAssetId, quantity) {
  400. return new Promise((resolve, reject) => {
  401. fetch(
  402. `https://api.steampowered.com/IInventoryService/CombineItemStacks/v1/`,
  403. {
  404. method: "POST",
  405. body: `access_token=${token}&appid=${appId}&fromitemid=${fromAssetId}&destitemid=${destAssetId}&quantity=${quantity}&steamid=${g_steamID}`,
  406. headers: {
  407. "content-type":
  408. "application/x-www-form-urlencoded; charset=UTF-8",
  409. },
  410. }
  411. )
  412. .then((response) => {
  413. response.json().then(json => {
  414. const { success } = json;
  415. resolve(success);
  416. });
  417. })
  418. .catch((err) => {
  419. console.error(err);
  420. reject(`堆叠物品失败 ${err}`);
  421. });
  422. });
  423. }
  424.  
  425. // 取消堆叠物品
  426. function unStackItem(appId, itemAssetId, quantity) {
  427. return new Promise((resolve, reject) => {
  428. fetch(
  429. `https://api.steampowered.com/IInventoryService/SplitItemStack/v1/`,
  430. {
  431. method: "POST",
  432. body: `access_token=${token}&appid=${appId}&itemid=${itemAssetId}&quantity=${quantity}&steamid=${g_steamID}`,
  433. headers: {
  434. "content-type":
  435. "application/x-www-form-urlencoded; charset=UTF-8",
  436. },
  437. }
  438. )
  439. .then((response) => {
  440. response.json().then(json => {
  441. const { success } = json;
  442. resolve(success);
  443. });
  444. })
  445. .catch((err) => {
  446. console.error(err);
  447. reject(`取消堆叠物品失败 ${err}`);
  448. });
  449. });
  450. }
  451. })();
  452.  
  453. GM_addStyle(`
  454. div.ish_container {
  455. display: inline;
  456. }
  457.  
  458. div.ish_container > * {
  459. margin-right: 5px;
  460. }
  461.  
  462. input.ish_inputbox {
  463. width: 70px;
  464. padding: 5px;
  465. }
  466.  
  467. input.ish_inputbox:nth-of-type(3),
  468. input.ish_inputbox:nth-of-type(4){
  469. width: 50px;
  470. }
  471. `);

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址