Ctrl K
ComunidadGuías…

electron/bot-move-main.mjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// Moving some of this computer's bots to My Cloud, while the rest go on here. This computer stays home, kept there by the
// switch that keeps its bots here, and never freezes: only the bots that go are held, each by its own hold on this
// computer's server (server/bot-moves.ts). Moving a bot from My Cloud into a team runs on the same phases (team-move.mjs):
// here one move carries a whole choice of bots to the "team" My Cloud, with the send kept to those bots (cloud-send.mjs).
import { randomUUID } from "node:crypto";

import { createTeamMove, parseTeamMoves } from "./team-move.mjs";
import { QUEUE_POLL_MS, switchOn } from "./one-home.mjs";

export const MY_CLOUD = Object.freeze({ id: "my-cloud", name: "My Cloud" });
const ID = /^[\w-]{1,128}$/u;
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
const list = (value) => (Array.isArray(value) ? value : []);
const nap = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); });

/** The bots a person can choose from: every bot of this computer's but the helper every place has, one that moved, and an
 * archived one. */
export const choosable = (bots) => list(bots).filter((bot) => record(bot) && ID.test(bot.id ?? "") && typeof bot.name === "string" && !bot.defaultBot && !bot.hidden && !bot.movedTo);

/**
 * @param {{
 *   journal: { read(): unknown, write(value: unknown): void },
 *   local: { call(method: string, route: string, body?: unknown): Promise<{ ok: boolean, status: number, body: any }> },
 *   send: { move(options: { final?: boolean, scope: string }): Promise<unknown>, check(options: { scope: string }): Promise<{ ok: boolean }> },
 *   session(): Promise<{ call(method: string, route: string, body?: unknown): Promise<{ ok: boolean, status: number, body: any }> }>,
 *   lease?: { take(): Promise<null | { name: string }>, release(): Promise<void> },
 *   ready(): boolean,
 *   lend?(projects: string[]): Promise<unknown>,
 *   onChange?(): void,
 *   sleep?(ms: number): Promise<void>,
 *   now?(): number,
 *   timers?: { set(work: () => void, ms: number): unknown, clear(timer: unknown): void },
 *   log?(line: string): void,
 * }} deps `ready`: My Cloud is there to move to. `lease`: one computer at a time runs the heavy part of a move there.
 */
export function createBotMove({ journal, local, send, session, lease = null, ready, lend, onChange = () => {}, sleep = nap, now, timers, log = () => {} }) {
  const moves = () => parseTeamMoves(journal.read()).moves;
  const note = (id, change) => {
    const kept = parseTeamMoves(journal.read());
    journal.write({ ...kept, moves: kept.moves.map((entry) => (entry.bot.id === id ? { ...entry, ...change } : entry)) });
  };
  /** My Cloud reaches the folders of the bots that moved through this computer; while lending can't take them yet, a later look does. */
  let lending = false;
  async function lendWaiting() {
    if (lending) return;
    lending = true;
    try {
      for (const move of moves().filter((entry) => Array.isArray(entry.lend) && entry.phase === "done")) {
        try { if (move.lend.length) await lend?.(move.lend); note(move.bot.id, { lend: undefined }); }
        catch (error) { log(`bot move: lending waits (${error?.message ?? error})`); }
      }
    } finally { lending = false; }
  }

  function open(move) {
    let token = null;
    const current = () => moves().find((entry) => entry.bot.id === move.bot.id) ?? move;
    const ids = () => list(current().bots).map((bot) => bot.id);
    /** The chosen bots, named to this computer's server for this run: it keeps them in memory only. */
    const scope = async () => {
      if (token) return token;
      const asked = await local.call("POST", "/api/cloud-send/scope", { v: 1, bots: ids() });
      if (!asked.ok || typeof asked.body?.scope !== "string") throw Object.assign(new Error("OpenMausBot on this computer did not answer."), { status: asked.status });
      token = asked.body.scope;
      return token;
    };
    return {
      engine: {
        move: async (options) => send.move({ final: options?.final === true, scope: await scope() }),
        check: async () => send.check({ scope: await scope() }),
      },
      /** A hold, or its end, for each bot of the choice; a bot still answering is what the move waits for. */
      myCloud: {
        async call(method, _route, body) {
          if (body?.state === "moving") await lease?.take().catch(() => null);
          const held = [];
          for (const id of ids()) {
            const answer = await local.call(method, `/api/bot-moves/${id}`, body);
            if (!answer.ok) return answer;
            held.push(...list(answer.body?.movedTo?.held));
          }
          return { ok: true, status: 200, body: { v: 1, released: true, movedTo: { held } } };
        },
      },
      /** Only bots a move would carry go: an untouched one has nothing to carry, and stays here. */
      async prepare() {
        const listed = await local.call("GET", `/api/cloud-send?move=1&scope=${await scope()}`);
        if (!listed.ok) throw Object.assign(new Error("OpenMausBot on this computer did not answer."), { status: listed.status });
        const carried = new Set(list(listed.body?.items).filter((item) => item?.kind === "bot").map((item) => item.id));
        const bots = list(current().bots).filter((bot) => carried.has(bot.id));
        if (!bots.length) throw Object.assign(new Error("These bots have nothing to move yet."), { final: true });
        return { bots };
      },
      team: {
        /** Waits its turn at My Cloud, behind another computer's move there. */
        async begin() {
          for (let other = await lease?.take() ?? null; other; other = await lease.take()) {
            log(`bot move: waiting for ${other.name || "another computer"} to finish moving to My Cloud`);
            await sleep(QUEUE_POLL_MS);
          }
          return { name: move.bot.name };
        },
        /** My Cloud switches on what was on here, and keeps which computer the bots came from. */
        async done() {
          const there = await session();
          const held = list(current().held).filter((key) => typeof key === "string");
          for (const key of held) {
            const change = switchOn(key);
            if (!change) continue;
            const answer = await there.call(change.method, change.route, change.body);
            if (!answer.ok && answer.status !== 404) throw Object.assign(new Error(answer.body?.error ?? "My Cloud did not switch on an automation."), { status: answer.status });
          }
          const computer = String((await local.call("GET", "/api/cloud-connections").catch(() => null))?.body?.computer?.label ?? "").trim() || "this computer";
          const webhooks = held.flatMap((key) => (key.startsWith("webhook:") ? [key.slice("webhook:".length)] : []));
          const arrived = await there.call("POST", "/api/cloud-send/arrived", { v: 1, computer, webhooks });
          if (!arrived.ok) throw Object.assign(new Error(arrived.body?.error ?? "My Cloud did not finish the move."), { status: arrived.status });
          const going = new Set(ids());
          const bots = list((await local.call("GET", "/api/bots?messages=0").catch(() => null))?.body?.bots);
          note(move.bot.id, { lend: [...new Set(bots.filter((bot) => going.has(bot?.id) && typeof bot.cwd === "string" && bot.cwd).map((bot) => bot.cwd))] });
        },
      },
      close: async () => { await lease?.release().catch(() => {}); },
    };
  }

  const engine = createTeamMove({ journal, open, inTeam: () => ready(), onChange: () => onChange(), log, ...(now ? { now } : {}), ...(timers ? { timers } : {}) });

  return {
    kick: () => engine.kick().finally(lendWaiting),
    /** Moves this choice of bots for this account; bots already on their way are left out. Null when none is left to move. */
    start(bots, account = null) {
      const moving = new Set(moves().filter((move) => move.phase !== "done" && !move.stopped).flatMap((move) => list(move.bots).map((bot) => bot.id)));
      const fresh = bots.filter((bot) => !moving.has(bot.id));
      if (!fresh.length) return null;
      const id = `bots-${randomUUID()}`;
      void engine.start({ bot: { id, name: fresh[0].name }, team: MY_CLOUD, bots: fresh, account }).finally(lendWaiting).catch((error) => log(`bot move: ${error?.message ?? error}`));
      return id;
    },
    view: () => engine.view(),
    seen: () => engine.seen(),
    retry: () => engine.retry(),
    /** The bots on their way now, by id. */
    moving: () => moves().filter((move) => move.phase !== "done").flatMap((move) => list(move.bots).map((bot) => bot.id)),
    /** Everything is moving now instead: a choice still on its way stops, and its bots go on here until that move takes them.
     * One at its switch is past the point of no return (My Cloud has switched its automations on), so it finishes: let go
     * here, its bots would run in both places. */
    async drop() {
      for (const move of moves().filter((entry) => entry.phase !== "done" && entry.phase !== "switch")) {
        for (const bot of list(move.bots)) await local.call("DELETE", `/api/bot-moves/${bot.id}`).catch(() => null);
        const left = parseTeamMoves(journal.read());
        journal.write({ ...left, moves: left.moves.filter((entry) => entry.bot.id !== move.bot.id) });
      }
      onChange();
    },
    /** This account's My Cloud ended: the bots that moved there are here again as they went. */
    async back(account) {
      for (const move of moves().filter((entry) => entry.phase === "done" && entry.account === account)) {
        for (const bot of list(move.bots)) {
          const answer = await local.call("POST", `/api/bot-moves/${bot.id}`, { v: 1, state: "back", team: MY_CLOUD }).catch(() => null);
          if (!answer?.ok) { log(`bot move: ${bot.name} comes back later`); return; }
        }
        const left = parseTeamMoves(journal.read());
        journal.write({ ...left, moves: left.moves.filter((entry) => entry.bot.id !== move.bot.id) });
      }
      onChange();
    },
    close: () => engine.close(),
  };
}