#Integration with Lark

1 messages · Page 1 of 1 (latest)

heavy widget
#

When I initially set up OpenClaw, I was using the native feishu plugin for integration with Lark; however, I never got it to work because of missing dependencies or code errors. For this reason, I'm currently using an alternative provided by this git address: https://github.com/m1heng/clawdbot-feishu

Could you please access if this is safe to use?

I've deployed OpenClaw on Alibaba Cloud Simple Application Server

leaden shoreBOT
#

To help others find answers, you can mark your question as solved via Right click solution message -> Apps -> âś… Mark Solution

mighty plankBOT
#

You can’t really get a binary “safe / unsafe” answer for a 3rd‑party OpenClaw plugin, because it runs arbitrary code with the same privileges as your OpenClaw gateway (so if it were malicious, it could read env vars/config, read files the gateway user can read, make outbound requests, etc.).

That said: I did a quick skim of the repo you linked (m1heng/clawdbot-feishu) and I don’t see obvious red flags:

  • No postinstall / install scripts in package.json (common malware vector).
  • Dependencies are pretty normal for this job: @larksuiteoapi/node-sdk, zod, typebox.
  • Network calls appear to be Feishu/Lark APIs (via the official SDK) plus an optional local webhook server if you choose webhook mode.

The real risks to watch with this plugin (and any Feishu integration)

  1. If you use connectionMode: "webhook" it starts an HTTP server (default port 3000) and binds publicly unless your firewall/security group blocks it.

    • On Alibaba Cloud: make sure your security group does not expose that port to the internet (or restrict to Feishu IP ranges if you know them).
    • Safer default: use websocket mode so you don’t need inbound ports at all.
  2. Scopes/permissions in Feishu Open Platform are the bigger risk than the code. If you grant Drive/Doc/Wiki write scopes, the bot can modify data. Only grant what you actually need.

  3. Local file access via media: it supports mediaLocalRoots (allowing local-path media uploads). That’s convenient, but it increases blast radius if an untrusted user can trigger media sending. Keep it empty or very tight.

#
  1. Who is allowed to talk to the bot: make sure your DM/group policies are restrictive (allowlist/pairing, require @mention, etc.) so random group members can’t drive actions.

Practical “make this safer” checklist

  • Prefer installing a pinned npm release (e.g. @m1heng-clawd/feishu@x.y.z) instead of tracking a moving git branch/HEAD.
  • Run OpenClaw under a dedicated non-root user on your server.
  • Keep Alibaba security group ports minimal (typically only 22 + whatever you need for dashboard/reverse proxy; avoid opening the Feishu webhook port unless required).
  • If you’re cautious: unpack the npm tarball and do a quick grep for suspicious stuff (child_process, unexpected domains, telemetry endpoints).

If you tell me whether you’re using webhook or websocket mode (and whether this box is public-facing), I can point out the specific exposure points to lock down on Alibaba Cloud.

heavy widget
#

im using webhook. This is because Lark supports only webhook currently

mighty plankBOT
#

If you’re using webhook mode on a public Alibaba Cloud server, the plugin itself doesn’t look obviously malicious, but your security posture is dominated by how you expose that webhook.

1) Lark is not webhook-only (usually)

In Feishu/Lark Open Platform → Events & Callbacks, there are typically two modes:

  • Long connection (WebSocket) — no inbound port required (recommended)
  • Request URL (Webhook) — requires a public HTTPS endpoint

This plugin supports both (connectionMode: "websocket" or "webhook"). If your Lark tenant truly only allows Request URL, then webhook is fine — just lock it down.

2) Webhook mode: what you should do to keep it “reasonably safe”

Main goal: don’t expose the plugin’s raw port directly to the internet.

  • Put it behind HTTPS (reverse proxy) on 443 (nginx/caddy), and block the webhook port (often 3000) in Alibaba security group so it’s not directly reachable from the public internet.
  • Use a non-guessable webhook path (don’t leave it at the default /feishu/events). Security by obscurity isn’t enough, but it cuts drive-by scanning a lot.
  • Make sure you set verificationToken (and encryptKey if you enabled encryption) in the plugin config. Without token verification, anyone could POST fake events to your endpoint and potentially drive your bot.
  • Add rate limiting at the reverse proxy (basic DoS protection). The plugin already has a body-size + timeout guard, but rate limiting is still worth it.
#

3) Biggest non-webhook risk: who can trigger actions

Even with a “secure” webhook, you still want conservative bot access:

  • DM policy: pairing/allowlist
  • Group policy: allowlist + require @mention
  • Don’t enable extra Feishu scopes (Drive/Doc/Wiki write) unless you actually need them.

If you paste how you exposed the webhook (direct port open vs behind nginx; which ports are open in the security group), I can tell you exactly what to change on Alibaba Cloud to reduce exposure.

heavy widget
#

Understood. Now further to my questions, because im using this third party plugin, there isnt an option to set calendars in lark. Now would you suggest update the plugin directly or create a skill for this purpose? Im using OpenClaw currently with Qwen 3.5 Plus 2026-02-15 version

mighty plankBOT
#

A skill alone won’t solve “Lark calendar support” in a real way.

Skills are mostly prompting/orchestration. To actually read/write Lark calendars you need code that can call the Lark Calendar APIs with your app credentials (i.e. an agent tool). That means: plugin work (either extending this plugin, or creating a separate “calendar tools” plugin).

What I’d recommend

Best long-term: add calendar tools to the plugin (or a small companion plugin), then optionally add a skill that uses those tools nicely.

  • Option A (cleanest): fork/extend m1heng/clawdbot-feishu
    Add tools like lark_calendar_list, lark_calendar_create_event, etc, implemented via the Lark SDK, and register them in the plugin.
    This keeps auth + API logic in one place and gives the agent “real” capabilities.

  • Option B (less maintenance of channel code): create a separate plugin that only registers calendar tools
    You can keep using the Feishu channel plugin for messaging, and a second plugin for calendar APIs.

  • Option C (not great): “skill-only” workaround
    This usually degenerates into “tell the user to do it manually in Lark” or brittle browser automation. Not what you want for a calendar.

#

One important compatibility note

That plugin’s package.json declares peerDependencies: openclaw >= 2026.3.1.
If you’re actually on an older OpenClaw build (“2026‑02‑15”), you’re in “it might work, it might be weird” territory. Before investing in calendar work, I’d align versions.

Docs you’ll want:

If you tell me what exact calendar actions you need (list events? create events? reminders? invite attendees?), I can suggest the minimal set of tools/scopes to implement so you don’t over-grant permissions.

heavy widget
#

I had a system token mismatched that happened twice today. which caused openclaw to not respond suddenly. When it came online it gave me the following reason:

What happened:
At some point, the gateway token was rotated/changed in openclaw.json
But the systemd service file wasn't updated with the new token
When gateway restarted, it used the old token from systemd env
CLI commands tried to connect with the new token from config
Token mismatch → Connection refused → No responses
Why It Happened Twice
The token mismatch occurred because:
First time: Gateway auto-restarted (config change), picked up old token from systemd
Second time: Same issue - systemd service file still had outdated token
I just fixed it by updating the systemd service file to match the config token.

Are you aware of this?

mighty plankBOT
#

Yeah — this class of failure is a known gotcha.

What’s happening is exactly what you described: the Gateway can get its auth token from more than one place, and systemd env/flags will win over whatever is in openclaw.json. So if you rotate the token in config but your systemd unit still exports the old one, you end up with:

  • Gateway process authenticating itself with old token (from systemd env)
  • Your CLI / dashboard using new token (from config)
  • Result: RPC probe fails / “no responses” until you reconcile them

How to prevent it from happening again

  • Pick one source of truth for the token:
    • Either keep it only in config and ensure the systemd unit does not hardcode/export a token, or
    • If you intentionally manage it via systemd env, then whenever you rotate it you must update/reinstall the service too.
  • Prefer reinstalling/updating the service via OpenClaw instead of hand-editing the unit file:
  • Use openclaw gateway status when it “goes silent” — it’s the fastest way to spot “service is running but RPC probe/auth is wrong”.

Also: this can happen even without token rotation if the systemd service runs under a different user than your CLI (so it reads a different ~/.openclaw/openclaw.json). Same symptom, same fix: make sure CLI + service are using the same config/state.

#

If you paste the relevant bits of your systemd unit (just the ExecStart + any Environment lines, redacting the token), I can tell you the cleanest way to structure it so token rotations don’t bite you again.

heavy widget
#

Well my openclaw suggested 2 options below:

Option 1: Centralize Token Management (Recommended)
Store the gateway token in one place only and reference it:

Option 2: Auto-Sync Token on Config Change
Whenever you run openclaw config set, it should automatically update the systemd service. This is a bug in the config update flow that should be reported to OpenClaw.
Workaround: After any config change that touches gateway settings

For token stability: I recommend creating a centralized token file so this doesn't happen again. Want me to set that up?```

What would you recommend?
mighty plankBOT
#

Option 1 — but I’d tighten what that means:

Recommended: don’t put the token in systemd at all

The most robust setup is:

  • Token lives only in openclaw.json (gateway.auth.token)
  • The systemd unit does not export OPENCLAW_GATEWAY_TOKEN (or pass --token)
  • When you rotate the token, you only change config + restart the gateway

That eliminates the entire “service env vs config” mismatch class.

In other words: your “centralized token file” can simply be the config file (because both the Gateway and the CLI already know how to read it).

Why I don’t recommend Option 2 (“auto-sync token on config change”)

I wouldn’t treat this as a bug. openclaw config set rewriting systemd units automatically would be:

  • surprising (it’s a system-level change),
  • risky (permissions/paths/users vary),
#
  • and hard to do safely across distros.

The intended “systemd update” moment is openclaw gateway install (that’s the command that owns the service definition).

Practical next step

Make sure your systemd service isn’t overriding the token anymore (remove the token env / --token from the unit), then restart. After that, future token rotations won’t brick replies.

If you want, paste the ExecStart + Environment lines of your unit (redact the actual token), and I’ll tell you exactly what to remove so the service always follows openclaw.json.

heavy widget
#

Where should i locate ExecStart + Environment lines of my unit?

mighty plankBOT
#

On Linux/systemd, the easiest way is:

  1. First find the unit name (often openclaw-gateway.service, but don’t guess):
openclaw gateway status

That usually shows which service manager it detected.

  1. Then print the unit file contents (this includes the ExecStart and any Environment= lines):

System service (most VPS installs):

sudo systemctl cat openclaw-gateway
#

User service (if you installed it as a user daemon):

systemctl --user cat openclaw-gateway

If openclaw-gateway isn’t the right name, list candidates:

systemctl list-units --type=service | grep -i openclaw
systemctl --user list-units --type=service | grep -i openclaw
  1. Optional: show the exact path to the unit file:
sudo systemctl show -p FragmentPath openclaw-gateway

When you paste it here, please redact any token/password values (anything that looks like a long random string).

heavy widget
#
Service: systemd (enabled)
File logs: /tmp/openclaw/openclaw-2026-03-04.log
Command: /usr/bin/node /home/admin/.npm-global/lib/node_modules/openclaw/dist/index.js gateway --port 18789
Service file: ~/.config/systemd/user/openclaw-gateway.service
Service env: OPENCLAW_GATEWAY_PORT=18789

Config (cli): ~/.openclaw/openclaw.json
Config (service): ~/.openclaw/openclaw.json

Gateway: bind=loopback (127.0.0.1), port=18789 (service args)
Probe target: ws://127.0.0.1:18789
Dashboard: http://127.0.0.1:18789/
Probe note: Loopback-only gateway; only local clients can connect.

Runtime: running (pid 140751, state active, sub running, last exit 0, reason 0)
RPC probe: ok```
mighty plankBOT
#

This output actually looks good — and it explains why the “token mismatch” shouldn’t happen anymore.

Key bits:

  • Config (cli) == Config (service) → both are reading the same ~/.openclaw/openclaw.json
  • Service env only has OPENCLAW_GATEWAY_PORT → there is no token override coming from systemd anymore
  • RPC probe: ok → the CLI can authenticate to the running gateway right now

So if earlier today you had OPENCLAW_GATEWAY_TOKEN=… (or --token …) baked into the unit, and you removed it, you’ve basically applied the recommended fix already.

What I’d do from here

  1. Treat ~/.openclaw/openclaw.json as the single source of truth for the token.
  2. When rotating the token, do:
    • change it in config, then
    • restart the service (no need to edit systemd env anymore).

If it ever happens again

#

The only ways it comes back are:

  • a drop-in override reintroduces a token, or
  • the unit’s ExecStart gets changed to include --token.

To check the full effective unit (including drop-ins), run:

systemctl --user cat openclaw-gateway

(And just confirm there’s no OPENCLAW_GATEWAY_TOKEN / --token in there.)

If you paste that unit output (with any secrets redacted), I can sanity-check it.

heavy widget
#
[Unit]
Description=OpenClaw Gateway (v2026.2.9)
After=network-online.target
Wants=network-online.target

[Service]
ExecStart="/usr/bin/node" "/home/admin/.npm-global/lib/node_modules/openclaw/dist/index.js" gateway --port 18789
Restart=always
RestartSec=5
KillMode=process
Environment=HOME=/home/admin
Environment="PATH=/home/admin/.local/bin:/home/admin/.npm-global/bin:/home/admin/bin:/home/admin/.nvm/current/bin:/home/admin/.fnm/current/bin:/home/admin/.volta/bin:/home/admin/.asdf/shims:/home/admin/.local/shar>
Environment=OPENCLAW_GATEWAY_PORT=18789
Environment=OPENCLAW_GATEWAY_TOKEN=xxxxxx
Environment="OPENCLAW_SYSTEMD_UNIT=openclaw-gateway.service"
Environment=OPENCLAW_SERVICE_MARKER=openclaw
Environment=OPENCLAW_SERVICE_KIND=gateway
Environment=OPENCLAW_SERVICE_VERSION=2026.2.9

[Install]
WantedBy=default.target```
mighty plankBOT
#

That unit file shows the exact root cause:

Environment=OPENCLAW_GATEWAY_TOKEN=xxxxxx

Per the credential precedence rules, OPENCLAW_GATEWAY_TOKEN overrides what’s in ~/.openclaw/openclaw.json. So if you rotate/change gateway.auth.token in config, the gateway service will still start with the old token from systemd → mismatch.

Also: good job redacting it — don’t ever paste the real token in public.

What I recommend (simplest / least footguns)

Remove the token from systemd, and keep it only in config (gateway.auth.token in ~/.openclaw/openclaw.json).

Then reload + restart the user service:

systemctl --user daemon-reload
systemctl --user restart openclaw-gateway
#

After that, token rotations are “edit config → restart” and you won’t hit this again.

Alternative (only if you really want env-managed secrets)

Make config reference the env var (e.g. gateway.auth.token: "${OPENCLAW_GATEWAY_TOKEN}") and ensure that env var is always set for both:

  • the systemd service, and
  • your interactive shell/CLI

This works, but it’s more moving parts (and it’s easy to end up with “CLI has no env var” later).

If you want, paste the current value of gateway.auth.token from ~/.openclaw/openclaw.json (redact the secret) and I’ll confirm which of the two approaches you’re currently closest to.

heavy widget
#

This is on the openclaw.json:

"gateway": {
    "port": 18789,
    "mode": "local",
    "bind": "loopback",
    "auth": {
      "mode": "token",
      "token": "xxx"
    },
    "tailscale": {
      "mode": "off",
      "resetOnExit": false
    },
    "nodes": {
      "denyCommands": [
        "camera.snap",
        "camera.clip",
        "screen.record",
        "calendar.add",
        "contacts.add",
        "reminders.add"
      ]
    }
  },```

Please let me know step by step what i need to do, how to safely remove the token from Environment=OPENCLAW_GATEWAY_TOKEN=xxxxxx and what to change in openclaw.json
mighty plankBOT
#

Got it — you currently have the token in two places:

  • ~/.openclaw/openclaw.json → gateway.auth.token (good)
  • ~/.config/systemd/user/openclaw-gateway.service → Environment=OPENCLAW_GATEWAY_TOKEN=… (this is what causes the mismatch)

Goal: keep it only in openclaw.json and remove the systemd env override.

Below is a safe step‑by‑step that’s reversible.


Step-by-step (safe + reversible)

0) Don’t lose access

Do this in an SSH session you won’t lose. (Optional but recommended: run inside tmux.)

1) Confirm your token is set in openclaw.json

#

You already showed:

gateway: {
  auth: { mode: "token", token: "xxx" }
}

Good. No change needed in openclaw.json for the “config-only token” approach.

2) Backup the systemd unit file

cp ~/.config/systemd/user/openclaw-gateway.service \
   ~/.config/systemd/user/openclaw-gateway.service.bak

3) Edit the unit and remove the token env line

#

Open the file:

nano ~/.config/systemd/user/openclaw-gateway.service

Find and delete this line:

Environment=OPENCLAW_GATEWAY_TOKEN=xxxxxx

Also quickly check there is no --token ... inside ExecStart (yours does not have it, so you’re good).

Save + exit.

4) Reload systemd user units + restart the gateway service

#
systemctl --user daemon-reload
systemctl --user restart openclaw-gateway

5) Verify it worked

Check it’s running:

systemctl --user status openclaw-gateway --no-pager

Then confirm OpenClaw RPC works:

openclaw gateway status
#

(Optional) confirm the environment no longer contains the token:

systemctl --user show openclaw-gateway -p Environment | grep -i TOKEN || echo "no token env âś…"

6) If anything breaks, rollback

cp ~/.config/systemd/user/openclaw-gateway.service.bak \
   ~/.config/systemd/user/openclaw-gateway.service
systemctl --user daemon-reload
systemctl --user restart openclaw-gateway

What to change in openclaw.json?

#

For your goal (“remove token from systemd env”), nothing. Keep:

  • gateway.auth.mode: "token"
  • gateway.auth.token: "<your token>"

That becomes the single source of truth.


If you do these steps and later notice the token line reappears, tell me whether you ran openclaw gateway install or did an update — that might be regenerating the unit with the token embedded, and we can handle that cleanly.

heavy widget
#

I've went through all 6 steps and seems everything is returning as expected from your instructions

mighty plankBOT
#

Nice — that means you’ve successfully eliminated the “two sources of truth” problem, so token rotation in openclaw.json shouldn’t randomly brick the gateway anymore.

Two quick “future you” notes:

  1. After updates or if you ever run openclaw gateway install again, re-check the unit with:
systemctl --user cat openclaw-gateway | rg -i 'GATEWAY_TOKEN|--token'

(just to make sure it didn’t reintroduce a token override)

  1. Keep rotating the token in one place only (your openclaw.json), then restart the service.

If this fixed it, please right-click my solution message → Apps → Mark Solution so Answer Overflow indexes it for the next person who hits the same mismatch.

heavy widget
#

I have existing chat groups i wanted to add openclaw into. When i added you into the group are you able to retrieve all messages in the group chat even before you were joined? If not, is there a way to provide you this access?

mighty plankBOT
#

By default: no — when you add the bot to an existing Lark/Feishu group, it will only receive events/messages from that point forward. It won’t automatically “download the whole backlog” from before it joined.

Can you give it access to older messages anyway?

There are two separate questions:

  1. What Lark allows via API
    Lark/Feishu does have message “get/list” APIs, but access is controlled by scopes (e.g. im:message:readonly, and the more sensitive “read all group messages” scope). Even then, it’s typically not “all history forever”—there are retention/time/page limits and tenant policy constraints.

  2. What OpenClaw (and your Feishu plugin) actually does today
    Even if the API allows it, OpenClaw generally does not auto-backfill an entire group’s history. It usually only uses:

  • messages it receives after it’s in the group, and/or
  • a small “recent context window” (if implemented) — not the full backlog.

Practical ways people handle “give the bot context”

  • Put the important context in a Lark Doc/Wiki and share it with the bot (then the bot can read that going forward).
  • Pin/announce a “context summary” message in the group and tell the bot “use the pinned context”.
  • Export/copy the relevant past messages and paste/attach them once (if you really need a backfill).
#

Privacy note

If you do enable history access / paste old logs, you’re effectively allowing that content to be sent to whatever model provider you’re using—so it’s worth being deliberate.

If you tell me what your goal is (“just enough context for better replies” vs “full compliance archive search”), I can suggest the least-invasive approach (usually the Doc/Wiki “source of truth” is the cleanest).

heavy widget
#

Is it possible for openclaw to get messages from all chats? then give me a summary? if possible i suppose i need to add it to all groups i have in Lark to do so?

mighty plankBOT
#

“All chats” in the sense of everything happening in your Lark tenant: generally no.

What is possible depends on the chat type:

1) DMs between humans

A bot cannot read other people’s 1:1 DMs with each other.
It can only read:

  • messages sent to the bot (p2p with the bot), and
  • group/channel messages in chats it’s part of (depending on permissions/config).

So OpenClaw can’t do “summarize all DMs” across your org.

2) Group chats / group channels

For group chats, the bot can only see messages for groups where:

  • the bot is a member of that chat, and
  • you’ve granted the relevant Lark scopes (and configured whether it only reacts to @mentions vs all messages).
#

So yes: if you want summaries of specific groups, you need to add the bot to those groups. There isn’t a magical “read every group without joining” switch (unless you’re using special enterprise compliance/audit features, which is a different world than normal self-built apps).

3) “Can it summarize everything once it’s in the groups?”

It can summarize:

  • messages it receives after it joined, and
  • sometimes a limited recent history window (if you explicitly fetch it via API + the plugin supports it), but not “all history forever”.

Practical recommendation

Instead of trying to ingest all chats, pick the 2–5 groups you care about and set up a daily/weekly digest for those. It’s:

  • much cheaper,
  • less likely to violate privacy expectations,
  • and actually useful.

If you tell me what you mean by “all chats” (all groups you personally are in? all channels in the tenant? just your team?), I can suggest the least-invasive setup (and what scopes/config you’d need) without turning the bot into a surveillance monster.

heavy widget
#

hi im trying ask openclaw to make image analysis, i noticed in my openclaw.json did not include image. How do i set up properly?

here is part of my config:

"models": {
    "mode": "merge",
    "providers": {
      "bailian": {
        "baseUrl": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
        "apiKey": "xxx",
        "api": "openai-completions",
        "models": [
          {
            "id": "qwen3.5-plus-2026-02-15",
            "name": "qwen3.5-plus-2026-02-15",
            "reasoning": false,
            "input": [
              "text"
            ],
            "cost": {
              "input": 0,
              "output": 0,
              "cacheRead": 0,
              "cacheWrite": 0
            },
            "contextWindow": 200000,
            "maxTokens": 8192
          }
        ]
      }
    }
  },```
mighty plankBOT
#

Image analysis in OpenClaw needs two things:

  1. the image tool must be allowed (tool policy), and
  2. you must have at least one vision-capable model configured (and ideally set as agents.defaults.imageModel).

Right now your bailian model advertises:

"input": ["text"]

So OpenClaw will treat it as text-only.


Step 0 — confirm your OpenClaw version

Please run and paste:

#
openclaw --version

(Some config keys moved over time; I don’t want to give you a key that your version doesn’t recognize.)


Step 1 — allow the image tool

Check your openclaw.json for a tools section.

  • If you have tools.profile: "messaging" (common), the image tool is not included.
  • Fix: either switch profile, or explicitly allow image.

Example (minimal change):

#
{
  "tools": {
    "allow": ["image"]
  }
}

(Or set tools.profile to something that includes it, like "coding".)

Docs: https://docs.openclaw.ai/gateway/configuration-reference (see “Tools” → profiles/allow)


Step 2 — configure an image model route (agents.defaults.imageModel)

Add something like:

#
{
  "agents": {
    "defaults": {
      "imageModel": {
        "primary": "bailian/<YOUR_VISION_MODEL_ID>"
      }
    }
  }
}

This is what the image tool uses for routing.
Docs: agents.defaults.imageModel in the config reference.


#

Step 3 — add a vision-capable model in your provider catalog

You’ll need to add a vision/multimodal model entry under your provider, and mark it as supporting image input:

{
  "models": {
    "providers": {
      "bailian": {
        "api": "openai-responses",
        "models": [
          {
            "id": "<YOUR_VISION_MODEL_ID>",
            "name": "…",
            "input": ["text", "image"]
          }
        ]
#
      }
    }
  }
}

Two important notes here:

  • You need the correct vision model id from DashScope/Bailian (it’s usually a different model than your current text one).
  • Vision often requires the openai-responses adapter (not openai-completions), because “completions” is typically text-only.

Step 4 — restart the gateway

Since you’re using a systemd user unit:

#
systemctl --user restart openclaw-gateway

If you want the quickest path

If Bailian vision is annoying to configure, the simplest setup is: keep Bailian for text, but add any second provider/model that supports images and point agents.defaults.imageModel to that.


If you paste your current tools section (if any) + the output of openclaw --version, I can tell you exactly which of the above you’re missing and what the minimal diff to your openclaw.json should be.

heavy widget
#

Is this correct? could you also check if my model plug into steps 1~3 supports?

"models": {
    "mode": "merge",
    "providers": {
      "bailian": {
        "baseUrl": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
        "apiKey": "xxxx",
        "api": "openai-completions",
        "models": [
          {
            "id": "qwen3.5-plus-2026-02-15",
            "name": "qwen3.5-plus-2026-02-15",
            "reasoning": false,
            "input": [
              "text",
              "image"
            ],
            "cost": {
              "input": 0,
              "output": 0,
              "cacheRead": 0,
              "cacheWrite": 0
            },
            "contextWindow": 200000,
            "maxTokens": 8192
          }
        ]
      }
    }
  },
  "agents": {
    "defaults": {
      "imageModel":{
        "primary": "bailian/qwen3.5-plus-2026-02-15"
      },
      "model": {
        "primary": "bailian/qwen3.5-plus-2026-02-15"
      },
      "models": {
        "anthropic/claude-sonnet-4-5": {
          "alias": "sonnet",
          "params": {
            "cacheRetention": "short"
          }
        },
        "bailian/qwen3.5-plus-2026-02-15": {
          "alias": "Qwen-3.5-Plus"
        },
        "openrouter/auto": {
          "alias": "OpenRouter"
        },
        "openrouter/qwen/qwen3.5-plus-02-15": {
          "alias": "Qwen3.5-Plus"
        }
      },

...,

"tools": {
    "allow": ["image"],
    "web": {
      "search": {
        "enabled": true
      },
      "fetch": {
        "enabled": true
      }
    }
  },


mighty plankBOT
#

You’re very close — the structure for steps 1–3 looks right, but there’s one big red flag:

The problem: api: "openai-completions" is (effectively) text-only

Even if you declare:

"input": ["text","image"]

that only tells OpenClaw “this model can take images”. It does not make the upstream API accept image parts.

For OpenAI-compatible multimodal, you almost always need the Responses adapter:

- "api": "openai-completions",
+ "api": "openai-responses",
#

(Your DashScope/Bailian base URL is already the “compatible-mode/v1” style that’s typically used with /responses.)

Second check: does that specific model actually support images?

Just adding "image" to the input list won’t work unless the model truly supports vision.

From Alibaba’s docs, vision examples generally use either:

  • the Responses endpoint with input_image, and/or
  • a dedicated VL model family (vision-language).

So: it might work, but only a real test can confirm for qwen3.5-plus-2026-02-15.

What I would change in your snippet

  1. Switch the provider adapter:
"providers": {
#
  "bailian": {
    "api": "openai-responses",
    ...
  }
}
  1. (Optional but recommended) Don’t force your default chat model to be the same as your image model. It’s fine, but not necessary. You can keep:
  • agents.defaults.model = your normal text model
  • agents.defaults.imageModel = your vision-capable model
  1. Your tools bit is fine:
"tools": { "allow": ["image"] }
#

How to verify quickly

After restarting the gateway, send a small image and ask something dead simple like:

“What is in this image? Reply with 1 sentence.”

If the upstream model can’t do vision, you’ll usually see an error like “invalid content type”, “image not supported”, or it’ll ignore the image.

If you paste the exact error you get after switching to openai-responses, I can tell you whether it’s:

  • wrong adapter,
  • wrong model id (needs a VL model),
  • or a DashScope compatibility quirk.
heavy widget
#

after your recommended changes, i tried to attach a screenshot and ask. It responded with 404 no body via lark

mighty plankBOT
#

That “404 no body” is almost certainly coming from your model provider endpoint, not from Lark.

What happened: when you switched to api: "openai-responses", OpenClaw will start calling a /responses-style endpoint. DashScope/Bailian’s OpenAI-compatible vision examples use chat.completions (/chat/completions), not /responses (see Alibaba’s own doc: https://help.aliyun.com/zh/model-studio/vision).

Fix

  1. Change your provider back to:
- "api": "openai-responses",
+ "api": "openai-completions",

(Despite the confusing name, in OpenClaw this is the OpenAI-compatible chat completions adapter, which is what DashScope’s vision docs show.)

  1. Keep these parts (they’re good):
  • model input: ["text","image"]
  • agents.defaults.imageModel.primary: "bailian/qwen3.5-plus-2026-02-15"