Character Selector

Summon any character in a normal chat

  1. // ==UserScript==
  2. // @name Character Selector
  3. // @description Summon any character in a normal chat
  4. // @version 1.0.3
  5. // @match https://old.character.ai/*
  6. // @match https://plus.character.ai/chat2*
  7. // @icon https://www.google.com/s2/favicons?sz=64&domain=character.ai
  8. // @grant none
  9. // @namespace https://gf.qytechs.cn/users/1077492
  10. // @run-at document-start
  11. // ==/UserScript==
  12. (function() {
  13. var func = window.WebSocket.prototype.send;
  14. var neo_socket = null;
  15. var initialized = false;
  16. var charDataCache = [];
  17. var chatCharDataCache = [];
  18. var selectedCharExternalId = "";
  19. var dropdown = null;
  20. var modal = null;
  21. var watchdog = null;
  22.  
  23. class ProfilePhotoWatchdog {
  24. constructor() {
  25. this.dom = null;
  26. this.observer = null;
  27. this.initialized = false;
  28. this.firstcheck = false;
  29. this.tryInit();
  30.  
  31. setTimeout(this.tryInit, 3000);
  32. }
  33.  
  34. tryInit() {
  35. try {
  36. var self = this;
  37. this.dom = document.querySelector(".chat2");
  38. if (this.initialized || this.dom == undefined || this.dom == null) {
  39. setTimeout(this.tryInit, 3000);
  40. return;
  41. }
  42.  
  43. let thisElement = this.dom.childNodes[1];
  44.  
  45. if (thisElement.className.indexOf("react-scroll") == -1) {
  46. setTimeout(this.tryInit, 3000);
  47. return;
  48. }
  49.  
  50. thisElement = thisElement.childNodes[0];
  51. this.dom = thisElement;
  52.  
  53. this.observer = new MutationObserver(function (e) {
  54. e.forEach(function(record) {
  55. if (record.addedNodes.length > 0) {
  56. for (let i = 0; i < record.addedNodes.length; i++) {
  57. let item = record.addedNodes[i];
  58. console.log(item);
  59. watchdog.analyzeNode(item);
  60. }
  61. }
  62. });
  63. });
  64.  
  65. this.observer.observe(thisElement, { childList: true });
  66. this.initialized = true;
  67. this.firstTreatment();
  68. } catch (ex) {
  69. setTimeout(this.tryInit, 3000);
  70. }
  71. }
  72.  
  73. firstTreatment() {
  74. if (!this.firstcheck) {
  75. let nodes = this.dom.childNodes;
  76. for (let i = 0; i < nodes.addedNodes.length; i++) {
  77. let node = nodes[i];
  78. this.analyzeNode(node);
  79. }
  80. this.firstcheck = true;
  81. }
  82. }
  83.  
  84. analyzeNode(node) {
  85. let img = node.querySelector("img");
  86. let p = node.querySelector("p");
  87.  
  88. if (img !== null && p !== null) {
  89. //so maybe this is a message, idk
  90. try {
  91. let element = node.querySelector(".rounded");
  92.  
  93. if (element !== null) { //hmm
  94. //lazy method to find out who the message is from
  95. //console.log("element", element, "elementParent", element.parentElement);
  96.  
  97. chatCharDataCache.forEach(function(charData) {
  98. if (element.parentElement.innerHTML.indexOf(charData.participant__name) != -1) {
  99. img.src = "https://characterai.io/i/80/static/avatars/" + charData.avatar_file_name;
  100. }
  101. });
  102. }
  103. } catch (ex) {
  104. //nope, it was not.
  105. }
  106. }
  107. }
  108. }
  109.  
  110. class FakeDropdownController {
  111. constructor() {
  112. this.dom = document.createElement("div");
  113. this.dom.innerHTML = '<div class="col-auto ps-2 dropdown dropup show"><span data-toggle="dropdown" aria-haspopup="listbox" class="" aria-expanded="true"> <div data-tag="currentChar" style="cursor: pointer;display: flex;justify-content: center;align-items: center;"><b data-tag="currentCharName">Switch</b><svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 24 24" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"> <path fill="none" d="M0 0h24v24H0z"></path> <path d="M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z"></path> </svg></div> </span> <div data-tag="dropDownMenu" tabindex="-1" role="listbox" aria-hidden="false" class="dropdown-menu show" style="position: absolute;inset: auto 0px 0px auto;transform: translate(0px, -24px);display: none;"> <h6 tabindex="-1" class="dropdown-header">Select character...</h6> <div tabindex="-1" class="dropdown-divider"></div><button type="button" data-tag="charBtn" tabindex="0" role="option" class="dropdown-item" style="display: flex; align-items: center;"> <div>Char</div> </button><button type="button" data-tag="newCharBtn" tabindex="0" role="option" class="dropdown-item" style="display: flex; align-items: center;"> <div><em>new character...</em></div> </button> </div> </div>';
  114. this.dom.style = "display: flex; justify-content: center; align-items: center; margin: 10px; user-select:none";
  115. this.charbtn = this.dom.querySelector("[data-tag=charBtn]");
  116. this.newcharbtn = this.dom.querySelector("[data-tag=newCharBtn]");
  117. this.dropdownmenu = this.dom.querySelector("[data-tag=dropDownMenu]");
  118. this.currentchar = this.dom.querySelector("[data-tag=currentChar]");
  119. this.isvisible = false;
  120.  
  121. this.dom.addEventListener("click", this.onClick.bind(this));
  122. this.newcharbtn.addEventListener("click", this.onAddNewChar.bind(this));
  123. document.getElementById("user-input").parentElement.insertBefore(this.dom, null);
  124. }
  125.  
  126. onClick(e) {
  127. var self = this;
  128. this.isvisible = !this.isvisible;
  129. this.dropdownmenu.style.display = this.isvisible ? "block" : "none";
  130.  
  131. let buttons = this.dropdownmenu.querySelectorAll("button");
  132.  
  133. for(var i = 0; i < buttons.length; i++) {
  134. let button = buttons[i];
  135. if (button.getAttribute("data-tag") === "newCharBtn") {
  136. continue;
  137. }
  138.  
  139. button.parentNode.removeChild(button);
  140. }
  141.  
  142. chatCharDataCache.forEach(function(charData) {
  143. let newUiElement = self.charbtn.cloneNode(true);
  144. newUiElement.innerText = charData.participant__name;
  145. newUiElement.setAttribute("data-externalid", charData.external_id);
  146. newUiElement.addEventListener("click", function(e) {
  147. selectCharacter(this.getAttribute("data-externalid"));
  148. });
  149.  
  150. self.dropdownmenu.querySelector(".dropdown-divider").parentElement.insertBefore(newUiElement, self.newcharbtn);
  151. });
  152. }
  153.  
  154. onAddNewChar(e) {
  155. modal = new FakeModalController();
  156. }
  157. }
  158.  
  159. class FakeModalController {
  160. constructor() {
  161. this.dom = document.createElement("div");
  162. this.dom.innerHTML = '<div class=""> <div class="modal fade show" role="dialog" tabindex="-1" style="display: block;"> <div class="modal-dialog modal-dialog-centered" role="document" style="margin-top: 0px;"> <div class="modal-content"> <div class="modal-body"> <div data-tag="charList" style="user-select:none;max-height: 300px;overflow-y: scroll;overflow-x: hidden;display: flex;flex-direction: column;"> <div data-tag="charOption" style="width:100%"> <img src="https://characterai.io/i/80/static/avatars/uploaded/2023/3/22/WOUx3xnZRql_j1TsQfS1TcNCI30D6uoPQvlGlKdYxHg.webp" style="height: 45px;width: 45px;margin-right: 10px;border-radius: 45px;object-fit: contain;"><b style="pointer-events:none">charname</b> <span style="pointer-events:none">@charowner</span> </div> </div> <input data-tag="searchInput" placeholder="Search..." style="width: 100%;"> </div> <div class="modal-footer"><button data-tag="cancelButton" type="button" class="btn btn-secondary">Cancel</button><button data-tag="addButton" type="button" disabled="" class="btn btn-primary disabled">Add</button></div> </div> </div> </div> <div class="modal-backdrop fade show"></div> </div>';
  163. this.dom.style = "position: relative; z-index: 1050; display: block;";
  164. this.chartemplate = this.dom.querySelector('[data-tag="charOption"]');
  165. this.charlist = this.dom.querySelector('[data-tag="charList"]');
  166. this.addbtn = this.dom.querySelector('[data-tag="addButton"]');
  167. this.charlist.removeChild(this.chartemplate);
  168. this.selectedid = "";
  169. this.selected = null;
  170.  
  171. this.dom.querySelector('[data-tag="cancelButton"]').addEventListener("click", this.onCancel.bind(this));
  172. this.addbtn.addEventListener("click", this.onAdd.bind(this));
  173. this.dom.querySelector('[data-tag="searchInput"]').addEventListener("keyup", this.onSearchInputKey.bind(this));
  174.  
  175. document.body.appendChild(this.dom);
  176. this.onData(charDataCache.slice(0, 100));
  177. }
  178.  
  179. onCancel(e) {
  180. this.dom.parentNode.removeChild(this.dom);
  181. }
  182.  
  183. onSearchInputKey(e) {
  184. let value = e.target.value.toLowerCase();
  185.  
  186. let results = charDataCache.filter(function (charData) {
  187. return charData.participant__name.toLowerCase().indexOf(value) != -1;
  188. });
  189.  
  190. this.onData(results.slice(0, 100));
  191. }
  192.  
  193. onData(data) {
  194. var self = this;
  195. this.charlist.innerHTML = "";
  196. data.forEach(function(each) {
  197.  
  198. let newUiElement = self.chartemplate.cloneNode(true);
  199. newUiElement.querySelector("b").innerText = each.participant__name;
  200. newUiElement.querySelector("span").innerText = "@" + each.user__username;
  201. newUiElement.setAttribute("data-externalid", each.external_id);
  202. newUiElement.querySelector("img").src = "https://characterai.io/i/80/static/avatars/" + each.avatar_file_name;
  203.  
  204. //No css injected, so i need use this lol
  205. newUiElement.addEventListener("click", function(e) {
  206. if (self.selected !== null) {
  207. self.selected.style.backgroundColor = "";
  208. }
  209.  
  210. self.selected = e.target;
  211. self.selectedid = e.target.getAttribute("data-externalid");
  212. e.target.style.backgroundColor = "rgb(68 114 175 / 58%)";
  213.  
  214. self.addbtn.classList.remove("disabled");
  215. self.addbtn.removeAttribute("disabled");
  216. });
  217.  
  218. self.charlist.appendChild(newUiElement);
  219. });
  220. }
  221.  
  222. onAdd(e) {
  223. this.dom.parentNode.removeChild(this.dom);
  224. selectCharacter(this.selectedid);
  225. }
  226. }
  227.  
  228. function tryGetCurrentCharacter() {
  229. let external_id = new URLSearchParams(document.location.search).get("char");
  230. if (external_id != null) {
  231. getCharacterInfo(external_id, function() {
  232. selectCharacter(external_id);
  233. });
  234. }
  235. }
  236.  
  237. async function getCharacterInfo(external_id, callback) {
  238. let token = localStorage.getItem("char_token");
  239. let response = await fetch("https://" + document.location.hostname + "/chat/character/info/", {
  240. mode: "cors",
  241. cache: "no-cache",
  242. credentials: "include",
  243. headers: {
  244. "Content-Type": "application/json",
  245. "Authorization": "Token " + JSON.parse(token).value,
  246. },
  247. method: "POST",
  248. body : JSON.stringify({ "external_id" : external_id })
  249. });
  250.  
  251. if (response.ok) {
  252. let json = await response.json();
  253.  
  254. if (!charDataCache.some(function(charData) {
  255. return charData.external_id === external_id;
  256. })) {
  257. charDataCache.push(json.character);
  258. }
  259.  
  260. if (callback) {
  261. callback();
  262. }
  263. } else {
  264. console.log("not ok");
  265. }
  266. }
  267.  
  268. function selectCharacter(external_id) {
  269. let results = charDataCache.filter(function (charData) {
  270. return charData.external_id == external_id;
  271. });
  272.  
  273. if (results.length != 0) {
  274. let result = results[0];
  275.  
  276. if (!chatCharDataCache.some(function(charData) {
  277. return charData.external_id === external_id;
  278. })) {
  279. chatCharDataCache.push(result);
  280. }
  281.  
  282. selectedCharExternalId = external_id;
  283. dropdown.currentchar.querySelector('[data-tag="currentCharName"]').innerText = result.participant__name;
  284. } else {
  285. selectedCharExternalId = "";
  286. alert("Error: No character data for: " + external_id);
  287. }
  288. }
  289.  
  290. async function fetchInitialData() {
  291.  
  292. let token = localStorage.getItem("char_token");
  293. let response = null;
  294.  
  295. if (token !== null) {
  296. response = await fetch("https://" + document.location.hostname + "/chat/characters/recent/", {
  297. mode: "cors",
  298. cache: "no-cache",
  299. credentials: "include",
  300. headers: {
  301. "Authorization": "Token " + JSON.parse(token).value,
  302. }
  303. });
  304.  
  305. if (response.ok) {
  306. let json = await response.json();
  307. charDataCache = charDataCache.concat(json.characters);
  308. } else {
  309. alert("Error fetching recent character data...");
  310. }
  311. }
  312.  
  313. response = await fetch("https://" + document.location.hostname + "/chat/characters/public/", {
  314. mode: "cors",
  315. cache: "no-cache",
  316. credentials: "include",
  317. headers: {
  318. "Content-Type": "application/json",
  319. }
  320. });
  321.  
  322. if (response.ok) {
  323. let json = await response.json();
  324. charDataCache = charDataCache.concat(json.characters);
  325. } else {
  326. alert("Error fetching character data...");
  327. }
  328.  
  329. tryGetCurrentCharacter();
  330. }
  331.  
  332. function addToPane() {
  333. dropdown = new FakeDropdownController();
  334. watchdog = new ProfilePhotoWatchdog();
  335. }
  336.  
  337. window.addEventListener("load", function() {
  338.  
  339. fetchInitialData();
  340.  
  341. var checkInit = function() {
  342. if (document.querySelector(".chat2") === null) {
  343. return false;
  344. }
  345.  
  346. addToPane();
  347.  
  348. var x = new MutationObserver(function (e) {
  349. e.forEach(function(record) {
  350. if (record.addedNodes.length > 0) {
  351. for (let i = 0; i < record.addedNodes.length; i++) {
  352. let item = record.addedNodes[i];
  353. if (item.className == "container-fluid chatbottom") {
  354. addToPane();
  355. }
  356. }
  357. }
  358. });
  359. });
  360.  
  361. x.observe(document.querySelector(".chat2"), { childList: true });
  362. return true;
  363. }
  364.  
  365. var infinitecheck = function() {
  366. if (!checkInit()) {
  367. setTimeout(infinitecheck, 10);
  368. }
  369. }
  370.  
  371. infinitecheck();
  372. });
  373.  
  374. function onMessage(message) {
  375. var json = JSON.parse(message.data);
  376.  
  377. if (json.hasOwnProperty("command")) {
  378. switch(json.command) {
  379. case "add_turn": {
  380.  
  381. break;
  382. }
  383. }
  384. }
  385. }
  386.  
  387. window.WebSocket.prototype.send = function(...args) {
  388. try {
  389. var json = JSON.parse(args[0]);
  390.  
  391. if (json.command == "create_and_generate_turn" || json.command == "generate_turn_candidate")
  392. {
  393. if (selectedCharExternalId != "") {
  394. json.payload.character_id = selectedCharExternalId;
  395. }
  396. args[0] = JSON.stringify(json);
  397. }
  398. } catch (ex) {
  399. alert(ex);
  400. }
  401.  
  402. if (neo_socket != this) {
  403. neo_socket = this;
  404. neo_socket.addEventListener("message", onMessage);
  405. }
  406.  
  407. func.call(this, ...args);
  408. }
  409. })();

QingJ © 2025

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