gangvy 4 luni în urmă
părinte
comite
6d9ba6da4e

+ 7 - 0
channel-plugin/openclaw.plugin.json

@@ -17,6 +17,13 @@
                 "default": true,
                 "description": "Master switch for this channel."
               },
+              "appId": {
+                "type": "string",
+                "description": "GEWE device appId for this channel instance. When set, all API requests are scoped to this device — messages, conversations, and contacts are isolated. Leave empty to use the backend's default GEWE_APP_ID.",
+                "examples": [
+                  "wx_wqIihu3r_ZIGxrqv3Fd7u"
+                ]
+              },
               "apiBase": {
                 "type": "string",
                 "description": "Root URL of the wechat-agent HTTP bot, e.g. http://8.138.37.248/api/wechat-agent",

+ 1 - 1
channel-plugin/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@fmode/openclaw-wechat-agent",
-  "version": "0.1.3",
+  "version": "0.1.5",
   "description": "OpenClaw WeChat channel plugin backed by a third-party wechat-agent HTTP bot (xbot/wcf-class). Polls inbound, dispatches through OpenClaw agent pipeline, replies via the bot's send-text endpoint.",
   "type": "module",
   "author": "fmode",

+ 34 - 8
channel-plugin/src/client.ts

@@ -48,8 +48,9 @@ export type SendTextOptions = {
   timeoutMs?: number;
 };
 
-export function createClient(apiBase: string): WechatAgentClient {
+export function createClient(apiBase: string, appId?: string): WechatAgentClient {
   const base = apiBase.replace(/\/+$/, "");
+  const deviceAppId = appId?.trim() || undefined;
 
   async function request(
     path: string,
@@ -100,7 +101,7 @@ export function createClient(apiBase: string): WechatAgentClient {
         const body = (await request("/login/check-online", {
           method: "POST",
           headers: { "content-type": "application/json" },
-          body: "{}",
+          body: JSON.stringify(deviceAppId ? { appId: deviceAppId } : {}),
         })) as { ret?: number; data?: unknown } | null;
         if (!body) return false;
         if (body.ret !== undefined && body.ret !== 200) return false;
@@ -121,7 +122,7 @@ export function createClient(apiBase: string): WechatAgentClient {
         const body = (await request("/login/check-online", {
           method: "POST",
           headers: { "content-type": "application/json" },
-          body: "{}",
+          body: JSON.stringify(deviceAppId ? { appId: deviceAppId } : {}),
         })) as { ret?: number; data?: unknown } | null;
         const d = body?.data;
         if (d && typeof d === "object") {
@@ -139,6 +140,7 @@ export function createClient(apiBase: string): WechatAgentClient {
       const limit = opts.limit ?? 50;
       const direction = opts.direction ?? "received";
       const qs = new URLSearchParams({ limit: String(limit), direction });
+      if (deviceAppId) qs.set("appId", deviceAppId);
       const body = (await request(`/messages?${qs.toString()}`, {
         method: "GET",
         signal: opts.signal,
@@ -151,11 +153,12 @@ export function createClient(apiBase: string): WechatAgentClient {
     },
 
     async sendText(opts: SendTextOptions): Promise<SendTextResult> {
-      const payload = {
+      const payload: Record<string, unknown> = {
         toWxid: opts.toWxid,
         content: opts.content,
         ats: opts.ats ?? "",
       };
+      if (deviceAppId) payload.appId = deviceAppId;
       const syntheticMessageId = `wx-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
       try {
         const body = (await request("/message/send-text", {
@@ -205,19 +208,42 @@ function normalizeMessage(item: unknown): RawWechatMessage | null {
     | string
     | number;
 
-  const chatroomId = pickString(o, ["chatroomId", "roomId", "groupId"]);
-  const isGroup = Boolean(chatroomId) || Boolean(o.isGroup);
-  const chatroomMemberWxid = isGroup
+  let chatroomId = pickString(o, ["chatroomId", "roomId", "groupId"]);
+  let isGroup = Boolean(chatroomId) || Boolean(o.isGroup);
+
+  // Fallback: detect group by @chatroom suffix in fromWxid (GEWE format)
+  if (!isGroup && fromWxid.endsWith("@chatroom")) {
+    isGroup = true;
+    chatroomId = chatroomId || fromWxid;
+  }
+  if (!isGroup && toWxid.endsWith("@chatroom")) {
+    isGroup = true;
+    chatroomId = chatroomId || toWxid;
+  }
+
+  let chatroomMemberWxid = isGroup
     ? pickString(o, ["chatroomMemberWxid", "memberWxid", "groupMember"])
     : undefined;
 
+  // GEWE group messages embed the member wxid as a prefix in content: "memberWxid:\ncontent"
+  let finalContent = String(content);
+  if (isGroup && !chatroomMemberWxid && finalContent.includes(":\n")) {
+    const colonNl = finalContent.indexOf(":\n");
+    const prefix = finalContent.slice(0, colonNl);
+    // Validate prefix looks like a wxid (no spaces, reasonable length)
+    if (prefix.length > 0 && prefix.length < 60 && !prefix.includes(" ")) {
+      chatroomMemberWxid = prefix;
+      finalContent = finalContent.slice(colonNl + 2);
+    }
+  }
+
   return {
     msgId,
     fromWxid,
     toWxid,
     fromName,
     type: String(type),
-    content: String(content),
+    content: finalContent,
     timestamp,
     isGroup,
     chatroomId,

+ 2 - 2
channel-plugin/src/monitor.ts

@@ -81,7 +81,7 @@ export async function startMonitor(opts: MonitorOpts): Promise<MonitorResult> {
   }
 
   const apiBase = resolveApiBase(channelCfg);
-  const client = createClient(apiBase);
+  const client = createClient(apiBase, channelCfg.appId);
   const pollIntervalMs = Math.max(
     POLL_MIN_INTERVAL_MS,
     channelCfg.pollIntervalMs ?? 10000
@@ -101,7 +101,7 @@ export async function startMonitor(opts: MonitorOpts): Promise<MonitorResult> {
   }
 
   log.info(
-    `starting poller apiBase=${apiBase} interval=${pollIntervalMs}ms batch=${batchWindowMs}ms cooldown=${replyCooldownMs}ms selfWxid=${selfWxid ?? "<auto>"}`
+    `starting poller apiBase=${apiBase} appId=${channelCfg.appId ?? "<default>"} interval=${pollIntervalMs}ms batch=${batchWindowMs}ms cooldown=${replyCooldownMs}ms selfWxid=${selfWxid ?? "<auto>"}`
   );
   opts.patchStatus({
     running: true,

+ 2 - 0
channel-plugin/src/types.ts

@@ -10,6 +10,8 @@
 /** Channel config shape under channels.wechat-agent in openclaw config. */
 export type WechatAgentConfig = {
   enabled?: boolean;
+  /** GEWE device appId — scopes all requests to this device for message/user isolation. */
+  appId?: string;
   apiBase?: string;
   pollIntervalMs?: number;
   batchWindowMs?: number;

+ 1 - 1
cli/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@fmode/wechat-cli",
-  "version": "0.1.2",
+  "version": "0.1.3",
   "description": "Local CLI wrapping the wechat-agent backend — consumed by the wechat OpenClaw skill for out-of-band WeChat operations (contact search, history, send-to-third-party, etc.). Sibling to the @fmode/openclaw-wechat-agent channel plugin.",
   "type": "module",
   "private": true,

+ 12 - 3
cli/src/client.ts

@@ -74,8 +74,9 @@ export type SendTextOptions = {
   timeoutMs?: number;
 };
 
-export function createClient(apiBase: string): WechatAgentClient {
+export function createClient(apiBase: string, appId?: string): WechatAgentClient {
   const base = apiBase.replace(/\/+$/, "");
+  const deviceAppId = appId?.trim() || undefined;
 
   async function request(
     path: string,
@@ -105,10 +106,16 @@ export function createClient(apiBase: string): WechatAgentClient {
   }
 
   function postJson(path: string, body: unknown, timeoutMs?: number): Promise<unknown> {
+    // Inject appId into every POST body when configured
+    let merged = body ?? {};
+    if (deviceAppId && merged && typeof merged === "object" && !Array.isArray(merged)) {
+      const obj = merged as Record<string, unknown>;
+      if (!obj.appId) merged = { appId: deviceAppId, ...obj };
+    }
     return request(path, {
       method: "POST",
       headers: { "content-type": "application/json" },
-      body: JSON.stringify(body ?? {}),
+      body: JSON.stringify(merged),
       ...(timeoutMs !== undefined ? { timeoutMs } : {}),
     });
   }
@@ -276,6 +283,7 @@ export function createClient(apiBase: string): WechatAgentClient {
       const limit = opts.limit ?? 50;
       const direction = opts.direction ?? "received";
       const qs = new URLSearchParams({ limit: String(limit), direction });
+      if (deviceAppId) qs.set("appId", deviceAppId);
       if (opts.wxid) qs.set("wxid", opts.wxid);
       if (opts.since) qs.set("since", opts.since);
       const resp = (await request(`/messages?${qs.toString()}`, {
@@ -293,7 +301,8 @@ export function createClient(apiBase: string): WechatAgentClient {
     },
 
     async getConversations(): Promise<Conversation[]> {
-      const resp = (await request("/conversations", { method: "GET" })) as
+      const qsStr = deviceAppId ? `?appId=${encodeURIComponent(deviceAppId)}` : "";
+      const resp = (await request(`/conversations${qsStr}`, { method: "GET" })) as
         | { ret?: number; data?: unknown[] }
         | null;
       if (!resp || !Array.isArray(resp.data)) return [];

+ 5 - 2
cli/src/commands/check-online.ts

@@ -1,11 +1,12 @@
 import { createClient } from "../client.js";
-import { resolveConfig } from "../config.js";
+import { resolveConfig, resolveAppId } from "../config.js";
 import type { GlobalArgs } from "../index.js";
 import { printJson, printKv } from "../output.js";
 
 export async function checkOnlineCommand(ctx: GlobalArgs): Promise<void> {
   const cfg = await resolveConfig({ apiBaseFlag: ctx.apiBaseFlag });
-  const client = createClient(cfg.apiBase);
+  const { appId } = await resolveAppId({ appIdFlag: ctx.appIdFlag });
+  const client = createClient(cfg.apiBase, appId);
   const res = await client.checkOnline();
 
   if (ctx.json) {
@@ -15,6 +16,7 @@ export async function checkOnlineCommand(ctx: GlobalArgs): Promise<void> {
       selfWxid: res.selfWxid ?? null,
       apiBase: cfg.apiBase,
       apiBaseSource: cfg.source,
+      ...(appId ? { appId } : {}),
     });
     if (!res.online) process.exitCode = 3;
     return;
@@ -23,6 +25,7 @@ export async function checkOnlineCommand(ctx: GlobalArgs): Promise<void> {
   printKv([
     ["apiBase", cfg.apiBase],
     ["source", cfg.source],
+    ...(appId ? [["appId", appId] as [string, string]] : []),
     ["online", res.online ? "yes" : "NO (offline)"],
     ["selfWxid", res.selfWxid ?? "-"],
   ]);

+ 3 - 2
cli/src/commands/contacts.ts

@@ -1,5 +1,5 @@
 import { createClient, type Contact } from "../client.js";
-import { resolveConfig } from "../config.js";
+import { resolveConfig, resolveAppId } from "../config.js";
 import type { GlobalArgs } from "../index.js";
 import { printError, printJson, printKv, printTable } from "../output.js";
 
@@ -11,7 +11,8 @@ export async function contactsCommand(ctx: GlobalArgs): Promise<void> {
   }
 
   const cfg = await resolveConfig({ apiBaseFlag: ctx.apiBaseFlag });
-  const client = createClient(cfg.apiBase);
+  const { appId } = await resolveAppId({ appIdFlag: ctx.appIdFlag });
+  const client = createClient(cfg.apiBase, appId);
 
   switch (sub) {
     case "list": {

+ 3 - 2
cli/src/commands/conversations.ts

@@ -1,5 +1,5 @@
 import { createClient, type Conversation } from "../client.js";
-import { resolveConfig } from "../config.js";
+import { resolveConfig, resolveAppId } from "../config.js";
 import type { GlobalArgs } from "../index.js";
 import { printError, printJson, printTable } from "../output.js";
 
@@ -7,7 +7,8 @@ export async function conversationsCommand(ctx: GlobalArgs): Promise<void> {
   const [sub] = ctx.positionals;
   if (!sub || sub === "list") {
     const cfg = await resolveConfig({ apiBaseFlag: ctx.apiBaseFlag });
-    const client = createClient(cfg.apiBase);
+    const { appId } = await resolveAppId({ appIdFlag: ctx.appIdFlag });
+    const client = createClient(cfg.apiBase, appId);
     const convs = await client.getConversations();
 
     if (ctx.json) {

+ 3 - 2
cli/src/commands/messages.ts

@@ -1,5 +1,5 @@
 import { createClient, type GetMessagesOptions, type RawMessage } from "../client.js";
-import { resolveConfig } from "../config.js";
+import { resolveConfig, resolveAppId } from "../config.js";
 import type { GlobalArgs } from "../index.js";
 import { printError, printJson, printKv, printTable } from "../output.js";
 
@@ -11,7 +11,8 @@ export async function messagesCommand(ctx: GlobalArgs): Promise<void> {
   }
 
   const cfg = await resolveConfig({ apiBaseFlag: ctx.apiBaseFlag });
-  const client = createClient(cfg.apiBase);
+  const { appId } = await resolveAppId({ appIdFlag: ctx.appIdFlag });
+  const client = createClient(cfg.apiBase, appId);
 
   switch (sub) {
     case "send": {

+ 24 - 0
cli/src/config.ts

@@ -7,6 +7,11 @@
  *   3. ~/.wecli/config.json                 -> { apiBase: "..." }
  *   4. ~/.openclaw/wechat-credentials.json  -> { wechatApiBase: "..." }   (legacy v1.x skills)
  *
+ * appId resolution (for multi-device isolation):
+ *   1. --app-id <id>    CLI flag
+ *   2. WECHAT_APP_ID    env var
+ *   3. ~/.wecli/config.json                 -> { appId: "..." }
+ *
  * No side-effectful discovery is performed (no network probes). If none of
  * the above yield an apiBase, the command throws with a helpful message
  * pointing at the config file locations.
@@ -25,6 +30,8 @@ export type ConfigSource =
 export type ResolvedConfig = {
   apiBase: string;
   source: ConfigSource;
+  appId?: string;
+  appIdSource?: ConfigSource | "none";
 };
 
 async function readJson(path: string): Promise<unknown | null> {
@@ -89,3 +96,20 @@ export async function resolveConfig(opts: {
     ].join("\n")
   );
 }
+
+/** Resolve appId independently (optional — omit for default device). */
+export async function resolveAppId(opts: {
+  appIdFlag?: string | undefined;
+}): Promise<{ appId?: string; source: ConfigSource | "none" }> {
+  // 1. Flag
+  if (opts.appIdFlag) return { appId: opts.appIdFlag, source: "flag" };
+  // 2. Env
+  const env = process.env.WECHAT_APP_ID;
+  if (env && env.length > 0) return { appId: env, source: "env" };
+  // 3. ~/.wecli/config.json
+  const wecliPath = join(homedir(), ".wecli", "config.json");
+  const wecliJson = await readJson(wecliPath);
+  const fromCfg = getString(wecliJson, "appId");
+  if (fromCfg) return { appId: fromCfg, source: "wecli-config" };
+  return { source: "none" };
+}

+ 14 - 5
cli/src/index.ts

@@ -59,15 +59,21 @@ Subcommands:
 
 Global options:
   --api-base <url>              Override API base URL (else env/file lookup)
+  --app-id <id>                 GEWE device appId for multi-device isolation
   --json                        Machine-readable JSON on stdout
   -h, --help                    Show this help
 
 Config resolution (highest priority first):
-  1. --api-base flag
-  2. WECHAT_API_BASE env
-  3. ~/.wecli/config.json                 -> { "apiBase": "..." }
-  4. ~/.openclaw/wechat-credentials.json  -> { "wechatApiBase": "..." }
-     (legacy, pre-dates this CLI; shared with v1.x skills)
+  apiBase:
+    1. --api-base flag
+    2. WECHAT_API_BASE env
+    3. ~/.wecli/config.json                 -> { "apiBase": "..." }
+    4. ~/.openclaw/wechat-credentials.json  -> { "wechatApiBase": "..." }
+       (legacy, pre-dates this CLI; shared with v1.x skills)
+  appId (optional, for multi-device):
+    1. --app-id flag
+    2. WECHAT_APP_ID env
+    3. ~/.wecli/config.json                 -> { "appId": "..." }
 
 Examples:
   wecli check-online
@@ -81,6 +87,7 @@ Examples:
 
 const OPTIONS: ParseArgsConfig["options"] = {
   "api-base": { type: "string" },
+  "app-id": { type: "string" },
   json: { type: "boolean" },
   help: { type: "boolean", short: "h" },
   to: { type: "string" },
@@ -95,6 +102,7 @@ const OPTIONS: ParseArgsConfig["options"] = {
 
 export type GlobalArgs = {
   apiBaseFlag: string | undefined;
+  appIdFlag: string | undefined;
   json: boolean;
   values: Record<string, string | boolean | undefined>;
   positionals: string[];
@@ -126,6 +134,7 @@ async function main(): Promise<void> {
   const [subcommand, ...rest] = positionals;
   const ctx: GlobalArgs = {
     apiBaseFlag: typeof values["api-base"] === "string" ? values["api-base"] : undefined,
+    appIdFlag: typeof values["app-id"] === "string" ? values["app-id"] : undefined,
     json: Boolean(values["json"]),
     values: values as Record<string, string | boolean | undefined>,
     positionals: rest,

+ 9 - 2
dist/README-v2.md

@@ -4,8 +4,8 @@
 
 | 文件 | 作用 | 装法 |
 |---|---|---|
-| `fmode-openclaw-wechat-agent-0.1.3.tgz` | Channel plugin(常驻 gateway,inbound 自动回复)| `openclaw plugins install ./fmode-openclaw-wechat-agent-0.1.3.tgz && systemctl --user restart openclaw-gateway.service` |
-| `fmode-wechat-cli-0.1.2.tgz` | `wecli` CLI(out-of-band 微信操作,被 skill 按需调用) | `tar -xzf fmode-wechat-cli-0.1.2.tgz && cd package && npm link` _(或参考工作区根 README 的 `~/.local/bin/` 单文件部署)_ |
+| `fmode-openclaw-wechat-agent-0.1.4.tgz` | Channel plugin(常驻 gateway,inbound 自动回复)| `openclaw plugins install ./fmode-openclaw-wechat-agent-0.1.4.tgz && systemctl --user restart openclaw-gateway.service` |
+| `fmode-wechat-cli-0.1.3.tgz` | `wecli` CLI(out-of-band 微信操作,被 skill 按需调用) | `tar -xzf fmode-wechat-cli-0.1.3.tgz && cd package && npm link` _(或参考工作区根 README 的 `~/.local/bin/` 单文件部署)_ |
 | `wechat.SKILL.md` | OpenClaw skill 文件,教 LLM 何时 / 如何用 `wecli` | `mkdir -p ~/.openclaw/skills/wechat && cp wechat.SKILL.md ~/.openclaw/skills/wechat/SKILL.md && systemctl --user restart openclaw-gateway.service` |
 
 三件共用同一个 `apiBase`(`http://8.138.37.248/api/wechat-agent`),**职责正交**:
@@ -23,6 +23,13 @@
 
 ## 版本记录
 
+- **2026-04-28** v0.1.4 channel plugin + v0.1.3 CLI — **多设备 appId 隔离**。后端新增多设备注册表后,channel plugin 和 CLI 同步适配:
+  - `openclaw.plugin.json` configSchema 新增 `appId` 字段,可在 OpenClaw Control 网页或 `openclaw.json` 里配置。
+  - `createClient(apiBase, appId)` 自动把 `appId` 注入所有 POST body 和 GET query,消息、会话、联系人均按设备隔离。
+  - `monitor.ts` 从 `channelCfg.appId` 读取并传递给 client,启动日志打印 `appId=`。
+  - CLI 新增 `--app-id` 全局 flag + `WECHAT_APP_ID` 环境变量 + `~/.wecli/config.json` 中 `appId` 字段,三级优先。
+  - `SKILL.md` Requirements 段补充多设备配置说明。
+  - 单设备部署无需任何改动(appId 不填则使用后端默认设备)。
 - **2026-04-22** v0.1.3 channel plugin — **真正修好** crash-loop。0.1.2 删 `timer.unref()` 没够 —— 客户机装完 0.1.2 后仍每 10-90s 看到 `[wechat-agent] starting poller` + 4 ms 后 `[default] auto-restart attempt N/10 in Xs`。读 `node_modules/openclaw/dist/server.impl-*.js` 才发现 OpenClaw gateway supervisor 把 `startAccount(ctx)` 返回的 Promise **本身**当 channel lifecycle task:
   ```js
   runTask().finally(() => runtime.running = false)

BIN
dist/fmode-openclaw-wechat-agent-0.1.4.tgz


BIN
dist/fmode-openclaw-wechat-agent-0.1.5.tgz


BIN
dist/fmode-wechat-cli-0.1.3.tgz


+ 5 - 0
dist/wechat.SKILL.md

@@ -73,6 +73,11 @@ reach OUT of the current DM and do something on the WeChat account.
   - `WECHAT_API_BASE` env var
   - `~/.wecli/config.json` → `{"apiBase":"..."}`
   - `~/.openclaw/wechat-credentials.json` → `{"wechatApiBase":"..."}` (legacy, may already exist)
+- (Optional) `appId` for **multi-device isolation** — when multiple WeChat
+  accounts share the same backend, pass `--app-id <GEWE_APP_ID>` or set via:
+  - `WECHAT_APP_ID` env var
+  - `~/.wecli/config.json` → `{"apiBase":"...","appId":"wx_xxx"}`
+  - If omitted, the backend uses its default device (single-device setups need no change).
 
 Verify with `wecli check-online` — should print `online yes`.
 

+ 42 - 0
skills/custom-persona/SKILL.md

@@ -0,0 +1,42 @@
+---
+name: custom-persona
+description: >
+  Bot persona and response style rules. Auto-generated by persona-manager.
+  Last updated: 2026-04-28
+metadata:
+  { "openclaw": { "emoji": "🎨" } }
+---
+
+# 自定义人设
+
+## When to Use
+所有对话均适用。这是本 bot 的基础人设,所有回复都应遵循以下设定。
+
+## Persona
+- 你的名字叫**小贝**
+- 你的角色是**康复科自动化客服助手**
+- 你服务于一家专业的康复医疗机构
+- 语气风格:专业、亲切、耐心,用通俗易懂的语言解释医学概念
+- 回复字数上限:150字(简明扼要,避免长篇大论)
+- 语言:中文
+- Emoji:适量使用,保持专业感
+
+## Greeting
+当用户第一次对话或打招呼时:
+> 你好,我是小贝,康复科的智能客服助手。有什么关于康复方面的问题我可以帮您解答吗?
+
+## Rules
+1. 始终以患者/咨询者的健康为第一优先
+2. 对于具体的康复训练动作,提醒用户需在专业康复师指导下进行
+3. 回答要简洁专业,避免啰嗦
+4. 遇到紧急症状描述(剧痛、无法活动、突然肿胀等),立即建议就医,不要尝试远程诊断
+5. 不确定的问题诚实说"建议您咨询主治医生",不要编造医学建议
+6. 可以提供一般性的康复知识科普(术后注意事项、常见康复周期、日常保养建议等)
+7. 对用户的疼痛和焦虑表示理解和共情
+
+## Forbidden Topics
+- 不提供具体药物处方或用药剂量建议(告知"请遵医嘱")
+- 不做明确诊断("根据您的描述可能是…建议就医确认")
+- 不讨论其他医疗机构的评价或对比
+- 不承诺具体的治疗效果或康复时间
+- 不讨论与康复医疗无关的政治、宗教等话题

+ 5 - 0
skills/wechat/SKILL.md

@@ -73,6 +73,11 @@ reach OUT of the current DM and do something on the WeChat account.
   - `WECHAT_API_BASE` env var
   - `~/.wecli/config.json` → `{"apiBase":"..."}`
   - `~/.openclaw/wechat-credentials.json` → `{"wechatApiBase":"..."}` (legacy, may already exist)
+- (Optional) `appId` for **multi-device isolation** — when multiple WeChat
+  accounts share the same backend, pass `--app-id <GEWE_APP_ID>` or set via:
+  - `WECHAT_APP_ID` env var
+  - `~/.wecli/config.json` → `{"apiBase":"...","appId":"wx_xxx"}`
+  - If omitted, the backend uses its default device (single-device setups need no change).
 
 Verify with `wecli check-online` — should print `online yes`.