Skill: yangguangzhou/quote0 (version 1.0.4)

--- name: quote0 description: Control MindReset Dot Quote/0 through the local quote0.js script and Dot Developer Platform APIs. Use when the user asks to configure Quote/0, push text/image content, query device status, switch next content, list tasks, or troubleshoot Dot Open API integration. Requires credentials DOT_API_KEY and (for device actions) DOT_DEVICE_ID. homepage: https://dot.mindreset.tech/docs/service/open metadata: { "openclaw": { "emoji": "🖼️", "requires": { "bins": ["node"], "env": ["DOT_API_KEY", "DOT_DEVICE_ID"] }, "primaryEnv": "DOT_API_KEY", }, } --- # Quote/0 技能（v1.0.4） 使用本地脚本调用 Dot Developer Platform API： ```bash node quote0.js <command> [options] ``` > 在技能目录内执行命令。 --- ## 1) 凭据与安全（必须遵守） ### 必需凭据 - `DOT_API_KEY`（必需） - `DOT_DEVICE_ID`（设备相关命令必需，如 `status|next|list|text|image`） ### 禁止事项 - 不要把 API Key / Device ID 硬编码到脚本、SKILL.md、Git 提交。 - 不要把敏感文件路径传给 `--imageFile`（脚本会读取该路径对应的本地文件并上传）。 ### 推荐注入方式（按安全优先级） 1. **单次命令传参（最推荐）** 2. **当前终端临时环境变量** 3. **长期写入 shell 配置（仅在用户明确同意后）** > 若要持久保存环境变量（如写入 `~/.zshrc` / `~/.bashrc` / PowerShell profile），必须先征得用户明确同意。 --- ## 2) 跨平台运行方式（macOS / Linux / Windows） 先进入技能目录： ```bash cd /path/to/quote0 ``` ### 方式 A：单次命令传参（推荐） ```bash node quote0.js devices --apiKey "..." node quote0.js status --apiKey "..." --deviceId "..." ``` ### 方式 B：临时环境变量 #### macOS / Linux（bash/zsh） ```bash export DOT_API_KEY="你的_key" export DOT_DEVICE_ID="你的_id" node quote0.js devices node quote0.js status ``` #### Windows PowerShell ```powershell $env:DOT_API_KEY="你的_key" $env:DOT_DEVICE_ID="你的_id" node .\quote0.js devices node .\quote0.js status ``` #### Windows CMD ```cmd set DOT_API_KEY=你的_key set DOT_DEVICE_ID=你的_id node .\quote0.js devices node .\quote0.js status ``` ### 方式 C：持久环境变量（需用户同意） 仅在用户明确授权后再执行。 --- ## 3) 首次配置（避免 404/403） 1. 在 Dot App 完成设备配对、联网、在线确认 2. 创建 API Key：`More → API Key → Create API Key` 3. 获取 Device Serial Number（作为 `DOT_DEVICE_ID`） 4. 在 Content Studio 至少添加： - 一个 **Text API** 内容项 - 一个 **Image API** 内容项 5. 运行： - `devices` 确认设备可见 - `status` 确认设备在线 --- ## 4) 命令总览 ```bash node quote0.js --help ``` - `devices`：获取设备列表 - `status`：获取设备状态 - `next`：切换下一条内容 - `list`：列出设备内容项 - `text`：推送文本内容 - `image`：推送图像内容 --- ## 5) 命令与 API 对应 ### devices - API: `GET /api/authV2/open/devices` ```bash node quote0.js devices ``` ### status - API: `GET /api/authV2/open/device/:id/status` ```bash node quote0.js status node quote0.js status --deviceId "ABCD1234ABCD" ``` ### next - API: `POST /api/authV2/open/device/:id/next` ```bash node quote0.js next ``` ### list - API: `GET /api/authV2/open/device/:deviceId/:taskType/list` ```bash node quote0.js list --taskType loop ``` ### text - API: `POST /api/authV2/open/device/:deviceId/text` ```bash node quote0.js text --message "你好，Quote/0" node quote0.js text --title "验证码助手" --message "你的验证码：205112" --signature "2026-02-25 00:40" --refresh true ``` 关键参数： - `--message` 必填 - `--title` / `--signature` / `--icon` / `--link` / `--taskKey` 可选 - `--refresh` 默认 `true` ### image - API: `POST /api/authV2/open/device/:deviceId/image` ```bash node quote0.js image --imageFile ./card.png node quote0.js image --imageFile ./text-card.png --border 0 --ditherType NONE --refresh true ``` 关键参数： - `--image` 或 `--imageFile` 二选一 - `--imageFile` 必须是 `.png` 普通文件，且大小不超过 5MB - 脚本会校验 PNG 文件头（不可绕过） - `--border`：0 白边 / 1 黑边 - `--ditherType`：`DIFFUSION | ORDERED | NONE` - `--ditherKernel`：`THRESHOLD | ATKINSON | BURKES | FLOYD_STEINBERG | ...` --- ## 6) 常见错误排查 - `401 Unauthorized`：API Key 错误/过期 - `403 Forbidden`：Key 无此设备权限 - `404 Not Found`：设备 ID 不存在，或未添加 Text/Image API 内容项 - `400 Invalid Params`：参数非法（如 border、image、dither） - `500 Device response failure`：设备离线/网络异常 - 限流：`10 requests/second` --- ## 7) 官方文档 - 总览：`https://dot.mindreset.tech/docs/service/open` - API Key：`https://dot.mindreset.tech/docs/service/open/get_api` - Device ID：`https://dot.mindreset.tech/docs/service/open/get_device_id` - Device List：`https://dot.mindreset.tech/docs/service/open/list_devices_api` - Device Status：`https://dot.mindreset.tech/docs/service/open/device_status_api` - Device Next：`https://dot.mindreset.tech/docs/service/open/device_next_api` - List Device Content：`https://dot.mindreset.tech/docs/service/open/list_device_tasks_api` - Text API：`https://dot.mindreset.tech/docs/service/open/text_api` - Image API：`https://dot.mindreset.tech/docs/service/open/image_api`

path: package.json
content: {
  "name": "quote0",
  "version": "1.0.4",
  "description": "Control MindReset Dot Quote/0 via Dot Open API using local quote0.js CLI",
  "keywords": ["quote0", "dot", "mindreset", "api", "device"],
  "openclaw": {
    "type": "skill",
    "requiredEnv": ["DOT_API_KEY", "DOT_DEVICE_ID"],
    "primaryEnv": "DOT_API_KEY",
    "runtime": "instruction-only"
  }
}


path: quote0.js
content: #!/usr/bin/env node

const https = require('https');
const fs = require('fs');
const path = require('path');

const API_BASE = 'https://dot.mindreset.tech/api/authV2/open';
const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const MAX_IMAGE_BYTES = 5 * 1024 * 1024;

function parseArgs(argv) {
  const out = { _: [] };
  for (let i = 0; i < argv.length; i++) {
    const arg = argv[i];
    if (arg.startsWith('--')) {
      const key = arg.slice(2);
      if (i + 1 < argv.length && !argv[i + 1].startsWith('--')) {
        out[key] = argv[++i];
      } else {
        out[key] = 'true';
      }
    } else {
      out._.push(arg);
    }
  }
  return out;
}

function toBool(v, defaultValue = true) {
  if (v === undefined) return defaultValue;
  return !(String(v).toLowerCase() === 'false' || v === '0');
}

function nowSignature() {
  const now = new Date();
  return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
}

function printHelp() {
  console.log(`
Quote/0 API CLI (v1.0.4)

Usage:
  node quote0.js <command> [options]

Commands:
  text        Update Text API content
  image       Update Image API content
  devices     List my devices
  status      Get device status
  next        Switch to next content
  list        List device content by type

Global options:
  --apiKey <key>       API key (fallback: DOT_API_KEY env)
  --deviceId <id>      Device serial number (fallback: DOT_DEVICE_ID env)

text options:
  --title <text>       Title (default: Clawdbot)
  --message <text>     Message body (required)
  --signature <text>   Signature (default: current time)
  --icon <base64>      Base64 PNG icon (40x40)
  --link <url>         NFC redirect URL / scheme
  --taskKey <key>      Target specific Text API slot
  --refresh <bool>     Refresh immediately (default: true)

image options:
  --image <base64>     Base64 PNG image (296x152)
  --imageFile <path>   PNG file path <=5MB (auto convert to base64)
  --link <url>         NFC redirect URL / scheme
  --border <0|1>       Border color (0=white, 1=black; default 0)
  --ditherType <type>  DIFFUSION | ORDERED | NONE
  --ditherKernel <k>   THRESHOLD | ATKINSON | BURKES | FLOYD_STEINBERG | ...
  --taskKey <key>      Target specific Image API slot
  --refresh <bool>     Refresh immediately (default: true)

list options:
  --taskType <type>    Usually loop (default: loop)

Examples:
  node quote0.js text --message "Hello\nWorld"
  node quote0.js image --imageFile ./card.png --ditherType NONE
  node quote0.js devices
  node quote0.js status --deviceId ABCD1234ABCD
`);
}

function request({ method, path: requestPath, apiKey, body }) {
  return new Promise((resolve, reject) => {
    const options = {
      method,
      headers: {
        Authorization: `Bearer ${apiKey}`,
        'Content-Type': 'application/json',
      },
    };

    const req = https.request(`${API_BASE}${requestPath}`, options, (res) => {
      let data = '';
      res.on('data', (chunk) => (data += chunk));
      res.on('end', () => {
        const ok = res.statusCode >= 200 && res.statusCode < 300;
        if (ok) {
          try {
            resolve(JSON.parse(data));
          } catch {
            resolve(data);
          }
        } else {
          reject(new Error(`HTTP ${res.statusCode}: ${data}`));
        }
      });
    });

    req.on('error', reject);
    if (body !== undefined) req.write(JSON.stringify(body));
    req.end();
  });
}

function needApiKey(args) {
  const [REDACTED_SECRET] || process.env.DOT_API_KEY;
  if (!apiKey) {
    throw new Error('缺少 API Key：请传 --apiKey 或设置环境变量 DOT_API_KEY');
  }
  return apiKey;
}

function needDeviceId(args) {
  const deviceId = args.deviceId || process.env.DOT_DEVICE_ID;
  if (!deviceId) {
    throw new Error('缺少设备 ID：请传 --deviceId 或设置环境变量 DOT_DEVICE_ID');
  }
  return deviceId;
}

function isPngBuffer(buf) {
  if (!Buffer.isBuffer(buf) || buf.length < PNG_MAGIC.length) return false;
  return buf.subarray(0, PNG_MAGIC.length).equals(PNG_MAGIC);
}

function readImageBase64(args) {
  if (args.image) return args.image;

  if (args.imageFile) {
    const filePath = path.resolve(args.imageFile);
    const st = fs.statSync(filePath);
    if (!st.isFile()) {
      throw new Error(`imageFile 不是普通文件: ${filePath}`);
    }

    if (path.extname(filePath).toLowerCase() !== '.png') {
      throw new Error(`imageFile 必须是 .png 文件: ${filePath}`);
    }

    if (st.size > MAX_IMAGE_BYTES) {
      throw new Error(`imageFile 过大（>${MAX_IMAGE_BYTES} bytes）: ${filePath}`);
    }

    const buf = fs.readFileSync(filePath);
    if (!isPngBuffer(buf)) {
      throw new Error(`imageFile 不是有效 PNG 文件: ${filePath}`);
    }

    return buf.toString('base64');
  }

  throw new Error('image 命令需要 --image 或 --imageFile');
}

async function main() {
  const args = parseArgs(process.argv.slice(2));
  const cmd = args._[0];

  if (!cmd || args.help || args.h) {
    printHelp();
    process.exit(0);
  }

  const [REDACTED_SECRET]);

  switch (cmd) {
    case 'devices': {
      const result = await request({ method: 'GET', path: '/devices', apiKey });
      console.log(JSON.stringify(result, null, 2));
      return;
    }

    case 'status': {
      const deviceId = needDeviceId(args);
      const result = await request({ method: 'GET', path: `/device/${deviceId}/status`, apiKey });
      console.log(JSON.stringify(result, null, 2));
      return;
    }

    case 'next': {
      const deviceId = needDeviceId(args);
      const result = await request({ method: 'POST', path: `/device/${deviceId}/next`, apiKey });
      console.log(JSON.stringify(result, null, 2));
      return;
    }

    case 'list': {
      const deviceId = needDeviceId(args);
      const taskType = args.taskType || 'loop';
      const result = await request({ method: 'GET', path: `/device/${deviceId}/${taskType}/list`, apiKey });
      console.log(JSON.stringify(result, null, 2));
      return;
    }

    case 'text': {
      const deviceId = needDeviceId(args);
      if (!args.message) throw new Error('text 命令需要 --message');

      const payload = {
        refreshNow: toBool(args.refresh, true),
        title: args.title || 'Clawdbot',
        message: args.message,
        signature: args.signature || nowSignature(),
      };
      if (args.icon) payload.icon = args.icon;
      if (args.link) payload.link = args.link;
      if (args.taskKey) payload.taskKey = args.taskKey;

      const result = await request({
        method: 'POST',
        path: `/device/${deviceId}/text`,
        apiKey,
        body: payload,
      });
      console.log(JSON.stringify(result, null, 2));
      return;
    }

    case 'image': {
      const deviceId = needDeviceId(args);
      const payload = {
        refreshNow: toBool(args.refresh, true),
        image: readImageBase64(args),
        border: args.border !== undefined ? Number(args.border) : 0,
      };
      if (args.link) payload.link = args.link;
      if (args.ditherType) payload.ditherType = args.ditherType;
      if (args.ditherKernel) payload.ditherKernel = args.ditherKernel;
      if (args.taskKey) payload.taskKey = args.taskKey;

      const result = await request({
        method: 'POST',
        path: `/device/${deviceId}/image`,
        apiKey,
        body: payload,
      });
      console.log(JSON.stringify(result, null, 2));
      return;
    }

    default:
      throw new Error(`未知命令: ${cmd}`);
  }
}

main().catch((err) => {
  console.error('Error:', err.message || err);
  process.exit(1);
});


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: suspicious | SkillSpector: clean (LOW)