@lansenger-pm/openclaw-lansenger-channel 3.15.0 → 3.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,112 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [3.15.2] - 2026-06-18
8
+
9
+ > **Compatible with OpenClaw `^2026.6.1`** (tested against `2026.6.1`).
10
+
11
+ ### Bug Fixes
12
+
13
+ - **Slash commands produce no reply**: `ctxPayload.CommandAuthorized` was hardcoded to `undefined`, causing the agent core to skip command execution and treat `/reset`/`/new` as plain agent text. Now set to the actual `resolveChannelMessageIngress` authorization result.
14
+
15
+ ## [3.15.1] - 2026-06-18
16
+
17
+ > **Compatible with OpenClaw `^2026.6.1`** (tested against `2026.6.1`).
18
+ >
19
+ > Supersedes the deprecated `3.15.0` (published by mistake on 2026-06-04). Includes all fixes from `3.14.5` through `3.14.8`.
20
+
21
+ ### Bug Fixes (v3.14.5 → v3.14.8)
22
+
23
+ - **Group chat reply routing**: Outbound adapters created fresh client instances with empty `chatTypeMap`, causing group replies to route via private-message API.
24
+ - **`lansenger_send_*` tools multi-account routing**: Tools used `getRunningClient()` which always returned the first account's client. Changed to factory pattern with `ctx.agentAccountId`.
25
+ - **Command authorization ignores global `commands.ownerAllowFrom`**: Slash commands blocked when owner configured globally rather than per-account.
26
+ - **Multi-account audit**: Outbound adapters, status snapshot, reply fallback delivery, and approval card store all now use per-account lookup via `getRunningEntryByAccount()`.
27
+
28
+ ### Chore
29
+
30
+ - Removed dead imports (`getRunningClient`, `getLastInboundTime`) from channel.ts after multi-account refactor.
31
+
32
+ ## [3.14.8] - 2026-06-18
33
+
34
+ > **Compatible with OpenClaw `^2026.6.1`** (tested against `2026.6.1`).
35
+
36
+ ### Bug Fixes
37
+
38
+ - **Command authorization ignores global `commands.ownerAllowFrom`**: Slash commands (`/reset`, etc.) were blocked when `allowFrom` was only configured at the global `commands.ownerAllowFrom` level rather than per-account. Fixed by extracting `lansenger:`-prefixed entries from the global config and passing them as `commandOwnerAllowFrom` to `resolveChannelMessageIngress`.
39
+ - **Multi-account audit — outbound adapters**: 7 outbound adapter methods (`sendText`, `sendMedia`, `sendFormattedText`, etc.) used `getRunningClient()` which returns the first running account's client. In multi-bot setups, this could deliver messages using the wrong account's credentials. Changed to `getRunningEntryByAccount(account.accountId)` to match the correct running entry.
40
+ - **Multi-account audit — status snapshot**: `buildAccountSnapshot` used `getRunningClient()` for connected state and `getLastInboundTime()` for last activity, both returning data from the first running account instead of the account being probed. Added `getLastInboundTimeByAccount()` and switched to per-account lookup.
41
+ - **Multi-account audit — reply fallback delivery**: The `reply_payload_sending` fallback hook used `getRunningClient()` with no account resolution, potentially delivering replies via the wrong bot. Now prefers per-account resolution via `getRunningEntryByAccount()`.
42
+ - **Multi-account audit — approval card store collision**: `pendingApprovalCards` was keyed by chatId only, risking cross-account overwrites when different Lansenger instances shared chatIds. Now keyed by `accountId:chatId`.
43
+
44
+ ## [3.14.7] - 2026-06-18
45
+
46
+ > **Compatible with OpenClaw `^2026.6.1`** (tested against `2026.6.1`).
47
+
48
+ ### Bug Fixes
49
+
50
+ - **`lansenger_send_*` tools multi-account routing**: All `lansenger_send_*` tools used `getRunningClient()` which always returned the first entry from `runningAccounts` Map, causing cross-account message sending in multi-bot setups. Fixed by:
51
+ - Added `getRunningEntryByAccount(accountId)` in runtime.ts that matches by account key/ID, with single-account fallback.
52
+ - Changed tool registration to use OpenClaw's tool factory pattern (`api.registerTool((ctx) => {...})`), resolving the correct client via `ctx.agentAccountId` from the OpenAI-compatible tool context.
53
+
54
+ ## [3.14.6] - 2026-06-17
55
+
56
+ > **Hotfix**: v3.14.5 was published with a broken `dist/src/runtime.js` that imports `./persistent-store.js` but the file was missing.
57
+
58
+ ### Bug Fixes
59
+
60
+ - **Missing `persistent-store.js` in npm bundle**: `src/persistent-store.ts` was uncommitted but referenced by modified `src/runtime.ts`. `prepublishOnly` ran `tsc` against the working directory, producing `dist/src/runtime.js` with the import, but the compiled `dist/src/persistent-store.js` was excluded from the npm tarball. Now committed and included.
61
+
62
+ ## [3.14.5] - 2026-06-17
63
+
64
+ > **Compatible with OpenClaw `^2026.6.1`** (tested against `2026.6.1`).
65
+
66
+ ### Bug Fixes
67
+
68
+ - **Group chat reply routing**: Outbound adapters (`sendText`, `sendMedia`, `sendFormattedText`, `sendPayload`, `beforeDeliverPayload`) created fresh `LansengerClient` instances via `makeClient()`, which had an empty `chatTypeMap` cache. `isGroupChat()` then fell back to `chatId.startsWith("group:")` — which never matches Lansenger group IDs — causing all group replies to be sent via private-message API instead of group-message API. Fixed by using `getRunningClient() ?? makeClient()` so the cached client (with populated `chatTypeMap`) handles routing.
69
+
70
+ ## [3.14.4] - 2026-06-15
71
+
72
+ > **Compatible with OpenClaw `^2026.6.1`** (tested against `2026.6.1`).
73
+
74
+ ### Bug Fixes
75
+
76
+ - **`recoverPendingInboundContexts` duplicate restart notice**: Gateway restart-recovery sent multiple "system restart" notices to the same chat due to two issues:
77
+ 1. `startLansengerGateway` was called from both runtime extension and channel extension initialization paths, causing `recoverPendingInboundContexts` to execute twice. Added a module-level `recoveryGuard` flag for idempotency.
78
+ 2. Even within a single invocation, multiple pending contexts for the same `chatId` would each trigger a separate notice. Added `chatId` deduplication — only one notice per chat per recovery cycle.
79
+ - **`recoverPendingInboundContexts` duplicate ack revoke**: Same guard prevents the stale ack message from being revoked twice.
80
+ - **WS reconnection without exponential backoff**: `backoffIdx` was reset to 0 immediately after `new WebSocket()` instead of on actual connection (`ws.onopen`). This caused every reconnect attempt to use a fixed 2s delay and log `attempt 1` regardless of retry count. Moved `this.backoffIdx = 0` into `ws.onopen` so the `[2, 5, 10, 30, 60]` backoff array is properly utilized.
81
+
82
+ ## [3.14.3] - 2026-06-04
83
+
84
+ > **Compatible with OpenClaw `^2026.6.1`** (tested against `2026.6.1`).
85
+
86
+ ### Features
87
+
88
+ - **Gateway restart-recovery inbound context persistence**: Active inbound sessions are persisted to `~/.openclaw/lansenger-inbound-contexts.json` before `inbound.run`. On gateway restart/plugin upgrade, `recoverPendingInboundContexts` detects interrupted sessions, revokes stale ack messages, and sends a "system restart, reprocessing..." notification. Contexts older than 5 minutes are discarded.
89
+ - **User notification**: Sends a locale-aware restart notice (`zh`/`en`) to the affected chat after recovery.
90
+
91
+ ## [3.14.2] - 2026-06-04
92
+
93
+ > **Compatible with OpenClaw `^2026.6.1`** (tested against `2026.6.1`).
94
+
95
+ ### Features
96
+
97
+ - **`reply_payload_sending` fallback delivery**: Tracks active `inbound.run` sessions via `activeDeliverySessions` set. When `reply_payload_sending` fires for a session without an active `inbound.run` (e.g., after gateway restart/turn timeout), the reply is delivered directly via the Lansenger client instead of being silently dropped. Extracts `chatId` from `sessionKey` to resolve delivery target.
98
+
99
+ ## [3.14.1] - 2026-06-04
100
+
101
+ > **Compatible with OpenClaw `^2026.6.1`** (tested against `2026.6.1`).
102
+
103
+ ### Bug Fixes
104
+
105
+ - **`ackMessage` default**: Changed from `false` to `true`; `revokeAckMessage` from `true` to `false`, aligning with the `after_agent_dispatch` ack policy.
106
+ - **`pendingApprovalCards` persistence**: Migrated from in-memory `Map` to `~/.openclaw/lansenger-approval-cards.json` file-backed store, surviving gateway restarts.
107
+ - **Group ingress fallback**: Added `requireMention` + `isAtMe` check to prevent non-@ messages from triggering the bot.
108
+ - **`isPathAllowed` security hardening**: Empty `mediaLocalRoots` now defaults to `[cwd, tmpdir]` instead of allowing any path.
109
+ - **Dead code removal**: Removed unused `debounceMs` statement in `runtime.ts`.
110
+ - **`convertPxToPtDeep` → `convertPxToPtCard`**: Only transforms known style fields; skips URL/link fields to avoid corruption.
111
+ - **Documentation**: All 5 locale READMEs updated with new defaults and `mediaLocalRoots` docs.
112
+
7
113
  ## [3.14.0] - 2026-06-04
8
114
 
9
115
  > **Compatible with OpenClaw `^2026.6.1`** (tested against `2026.6.1`).
@@ -25,60 +131,43 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
25
131
  - **Test coverage**: 4 new tests via `verifyChannelMessageReceiveAckPolicyAdapterProofs` and `listDeclaredReceiveAckPolicies`, verifying all declared policies pass SDK contract proofs.
26
132
  - This replaces the previously implicit ack timing embedded in the `runtime.ts` monitor-local state with a standardized, SDK-verifiable contract.
27
133
 
28
- ## [3.13.0] - 2026-06-03
134
+ ## [3.13.1] - 2026-06-03
29
135
 
30
136
  > **Compatible with OpenClaw `^2026.5.28`** (tested against `2026.5.28`).
31
137
 
32
- ### Bug Fix
138
+ - migrate `message_sending` from `api.registerHook` to `api.on("message_sending", ...)` (OpenClaw 2026.5.28 rejects `registerHook` for typed hooks that lack a name)
139
+
140
+ ## [3.13.0] - 2026-06-03
33
141
 
34
- - **Missing `dist/src/setup-i18n.*` in npm package**: `setup-wizard.js` imports `./setup-i18n.js`, but the `files` field in `package.json` did not include `dist/src/setup-i18n.*`, causing `Cannot find module './setup-i18n.js'` at gateway startup. Added `dist/src/setup-i18n.*` to the `files` array.
35
- - **`message_sending` hook registration missing name**: `api.registerHook("message_sending", ...)` now fails at gateway startup with "hook registration missing name" (OpenClaw 2026.5.28 rejects `registerHook` calls for typed hooks that lack a name). Migrated `message_sending` to `api.on("message_sending", ...)` — the typed lifecycle hook API — alongside the existing `reply_payload_sending` hook.
142
+ > **Compatible with OpenClaw `^2026.5.28`** (tested against `2026.5.28`).
36
143
 
37
- ### Outbound Hook Expansion
144
+ - Add `dist/src/setup-i18n.*` to `files` field in package.json (fixes `Cannot find module './setup-i18n.js'` at gateway startup)
38
145
 
39
- - **`reply_payload_sending` hook**: Register typed plugin hook via `api.on()` for the lansenger channel. Intercepts reply payloads before delivery — enables logging, approval context injection, and payload rewrite/cancel decisions.
40
- - **`message_sending` hook**: Register typed plugin hook via `api.on()` for early-stage reply interception.
41
- - **`normalizePayload` — code-block detection**: Payloads containing `\`\`\`` code fences are now marked `_lansengerFormatText: true` even when they also carry `mediaUrl` or `presentation`. Previously, code-rich agent replies with attachments fell back to plain-text `msgType`, losing Markdown formatting. Now they always render via `formatText`.
42
- - **`beforeDeliverPayload` — approval-resolved crash recovery**: When `hint.kind === "approval-resolved"`, the delivery pipeline now proactively updates the original appCard status via `updateDynamicCard`. Because the delivery pipeline replays on gateway restart, this provides crash-recovery safety.
43
- - **`shouldSuppressLocalPayloadPrompt`**: Suppresses the local text prompt when the native approval route is active (`hint.kind === "approval-pending"` with `nativeRouteActive === true`). Prevents duplicate approval prompts.
44
- - **`pendingApprovalCards` Map**: Tracks sent approval card `messageId`s keyed by `chatId`. Enables `beforeDeliverPayload` to correlate approval-resolved payloads with the original card for status updates.
45
- - **Text chunking**: Add `textChunkLimit: 4000`, `chunkerMode: "markdown"`, and `chunker` (using SDK `chunkMarkdownTextWithMode`). Long agent replies are automatically split into multiple Lansenger messages, preserving Markdown structure across chunks.
46
- - **`resolveEffectiveTextChunkLimit`**: Uses SDK `resolveTextChunkLimit` to allow config-level override of the chunk limit.
47
- - Add `normalizePayload` and `beforeDeliverPayload` callbacks on outbound base (structural preparation for outbound hooks).
48
- - Add session-scoped delivery dedup (`sessionDeliveryTracker`) to prevent duplicate sends across turns in the same session.
146
+ ## [3.12.2] - 2026-06-03
49
147
 
50
- ### OpenClaw 2026.5.27–28 Compatibility
148
+ > **Compatible with OpenClaw `^2026.5.28`** (tested against `2026.5.28`).
51
149
 
52
- - Migrate `api.runtime.channel.turn` `api.runtime.channel.inbound` (OpenClaw removed the old `turn` runtime alias; the new `inbound` namespace provides the same `run` function with identical parameters). This is a **required** migration — the old `turn` alias no longer exists in OpenClaw 2026.5.27.
53
- - Thread canonical `sessionKey` into outbound hooks (`sendText`, `sendMedia`, `sendFormattedText`) for multi-session/multi-agent routing and dedup.
54
- - Fallback `sessionKey` now includes `accountId` for multi-bot scenarios (`agent:main:lansenger:<accountId>:<chatType>:<chatId>`).
55
- - Pin npm install spec to exact version (`@lansenger-pm/openclaw-lansenger-channel@3.13.0`) to prevent supply-chain attacks and accidental upgrades.
56
- - Bump `openclaw` devDependency from `^2026.5.20` to `^2026.5.28`.
57
- - Split changelog out of READMEs into standalone `CHANGELOG.md` (all 5 locale READMEs now reference this file).
150
+ - **Outbound Hook Expansion**: `reply_payload_sending` hook, `message_sending` hook, `normalizePayload` code-block detection, `beforeDeliverPayload` approval-resolved crash recovery, `shouldSuppressLocalPayloadPrompt`, `pendingApprovalCards` Map, text chunking (`textChunkLimit: 4000`, `chunkerMode: "markdown"`), `resolveEffectiveTextChunkLimit`, session-scoped delivery dedup (`sessionDeliveryTracker`)
151
+ - **Approval Flow Enhancement**: `approvalCapability.transport.send` stores card `messageId` in `pendingApprovalCards`; card status updates via both `transport.update` and `beforeDeliverPayload` (crash-recovery safety)
152
+ - **Setup Wizard i18n**: `createSetupTranslator()` + plugin-owned dictionary, locales `en`/`zh-CN`/`zh-TW`, `src/setup-i18n.ts`
58
153
 
59
- ### Approval Flow Enhancement
154
+ ## [3.12.1] - 2026-06-03
60
155
 
61
- - `approvalCapability.transport.send` now stores card `messageId` in `pendingApprovalCards` for later correlation in `beforeDeliverPayload`.
62
- - Card status updates (pending → approved/denied) now happen via both `transport.update` (approval framework) and `beforeDeliverPayload` (delivery pipeline). The latter provides crash-recovery safety.
156
+ > **Compatible with OpenClaw `^2026.5.28`** (tested against `2026.5.28`).
63
157
 
64
- ### Setup Wizard i18n
158
+ - **Security**: SecretRef support for appSecret, plaintext appSecret warning, SSRF protection for `sendImageUrl`, `dangerouslyAllowPrivateNetwork` config, `mediaLocalRoots` config, path validation (`isPathAllowed`)
159
+ - Pin npm install spec to exact version (`@lansenger-pm/openclaw-lansenger-channel@3.12.1`)
160
+ - Thread canonical `sessionKey` into outbound hooks for multi-session/multi-agent routing
161
+ - Fallback `sessionKey` includes `accountId` for multi-bot scenarios
162
+ - Bump `openclaw` devDependency from `^2026.5.20` to `^2026.5.28`
65
163
 
66
- - **Locale-aware setup wizard**: Replace all bilingual hard-coded strings (`"Chinese / English"`) with `createSetupTranslator()` + plugin-owned dictionary. The wizard now displays in the user's locale (resolved from `OPENCLAW_LOCALE` → `LC_ALL` → `LC_MESSAGES` → `LANG`, fallback English).
67
- - Supported locales: **`en`**, **`zh-CN`**, **`zh-TW`**. French and other locales fall back to English for wizard prompts (READMEs remain 5-language).
68
- - Common strings (status labels, "Docs:" prefix, etc.) use the built-in OpenClaw catalog keys (`wizard.channels.*`). Plugin-specific strings use a local dictionary with `lt()`.
69
- - New file: `src/setup-i18n.ts` — locale resolver + 40+ entry dictionary.
164
+ ## [3.12.0] - 2026-06-03
70
165
 
71
- ### Security
166
+ > **Compatible with OpenClaw `^2026.5.27`** (tested against `2026.5.27`).
72
167
 
73
- - **SecretRef support for appSecret**: `resolveAccount()` now detects SecretRef objects via `coerceSecretRef()` and resolves from env vars. No need to store appSecret as plaintext in config. After running `openclaw secrets configure`, the config contains `__OPENCLAW_SECRET__({ref_id})` instead of the raw secret value, while the actual value is stored in the system credential store.
74
- - **Plaintext appSecret warning in `resolveAccount()`**: Emits `log.warn` on every gateway startup when `appSecret` is stored as a plaintext string in `openclaw.json`, advising migration to SecretRef via `openclaw secrets configure`.
75
- - **Plaintext appSecurity warning in setup wizard `finalize`**: Console output in the user's locale when plaintext appSecret is detected after config migration.
76
- - **`securityNote` in setup wizard**: A conditional note that appears only when a plaintext appSecret exists, with locale-appropriate migration instructions.
77
- - **README security advisory**: All 5 locale READMEs now include a prominent `⚠️ Security: Migrate appSecret to SecretRef storage` block.
78
- - **SSRF protection for sendImageUrl**: `assertHttpUrlTargetsPrivateNetwork()` blocks RFC1918/link-local/metadata-IP targets by default.
79
- - **`dangerouslyAllowPrivateNetwork` config**: Opt-in at top-level or per-account to allow private network image URLs. Audit finding (`lansenger/dangerously-allow-private-network`) warns when enabled.
80
- - **`mediaLocalRoots` config**: Restrict local file delivery to configured directories. Prevents agents from accessing arbitrary files outside allowed roots. Available at top-level and per-account.
81
- - **Path validation**: `isPathAllowed()` validates local file paths against `mediaLocalRoots` before delivery; blocked paths are logged and skipped.
168
+ - **Breaking**: Migrate `api.runtime.channel.turn` `api.runtime.channel.inbound` (OpenClaw removed the old `turn` runtime alias)
169
+ - Split changelog out of READMEs into standalone `CHANGELOG.md`
170
+ - All 5 locale READMEs now reference this file
82
171
 
83
172
  ## [3.11.0] - 2026-05-26
84
173
 
@@ -120,12 +209,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
120
209
 
121
210
  > **Compatible with OpenClaw `^2026.5.20`** (tested against `2026.5.20`).
122
211
 
123
- ### Features
212
+ - Fix apiGatewayUrl lint check to skip when accounts have gateway set.
124
213
 
125
- - Add `security.collectWarnings` and `security.collectAuditFindings` for `openclaw doctor --lint` integration. Checks: credentials missing/incomplete, dmPolicy not pairing, apiGatewayUrl not set, group config unused.
214
+ ## [3.8.1] - 2026-05-20
215
+
216
+ > **Compatible with OpenClaw `^2026.5.20`** (tested against `2026.5.20`).
217
+
218
+ - Add `security.collectWarnings` and `security.collectAuditFindings` for `openclaw doctor --lint` integration.
126
219
  - Add `doctor.repairConfig` to auto-fix dmPolicy to pairing.
127
220
  - Bump `openclaw` devDependency from `^2026.5.7` to `^2026.5.20`.
128
- - Fix apiGatewayUrl lint check to skip when accounts have gateway set.
129
221
 
130
222
  ## [3.7.9] - 2026-05-18
131
223
 
@@ -388,4 +480,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
388
480
  - Multi-language READMEs (en, zhHans, zhHant, zhHantHK, fr).
389
481
  - DM security: pairing mode (default), allowlist, open, disabled.
390
482
  - Approval workflow: appCard with `headStatusInfo` for pending/approved/denied states.
391
- - Auto-start WebSocket gateway on plugin activation.
483
+ - Auto-start WebSocket gateway on plugin activation.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Lansenger OpenClaw Channel
3
+ Copyright (c) 2026 Lanxin Mobile (Beijing) Technology Co., ltd.
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.fr.md CHANGED
@@ -12,7 +12,7 @@ Connecte OpenClaw à Lansenger — une plateforme de messagerie d'entreprise —
12
12
  ## Fonctionnalités
13
13
 
14
14
  - **Messagerie en temps réel** via connexion longue WebSocket
15
- - **Support multi-bot** — lier plusieurs bots Lansenger à différents agents OpenClaw
15
+ - **Support multi-robot** — lier plusieurs robots Lansenger à différents agents OpenClaw
16
16
  - **Support Markdown** utilisant le msgType `formatText` (par défaut)
17
17
  - **Fichiers/Images/Vocaux** via le msgType `text` avec upload de médias
18
18
  - **Cartes d'approbation** — workflow d'approbation interactif avec mises à jour de statut en place (en attente → approuvé/refusé)
@@ -20,8 +20,8 @@ Connecte OpenClaw à Lansenger — une plateforme de messagerie d'entreprise —
20
20
  - **Auto-routage via msgTarget** — toutes les méthodes d'envoi routent automatiquement vers les API groupe ou DM (privé) ; pas de méthodes groupe/privé séparées
21
21
  - **@Mentions** — support @tout et @utilisateurs spécifiques dans les chats de groupe
22
22
  - **Traitement des médias entrants** — téléchargement d'images/fichiers/vocaux, détection d'extension, chemins de fichiers pour l'agent
23
- - **Révocation de messages** — révoquer les messages précédemment envoyés (chatType : bot ou groupe uniquement)
24
- - **Démarrage automatique** — la passerelle connecte automatiquement tous les comptes de bots configurés au démarrage
23
+ - **Révocation de messages** — révoquer les messages précédemment envoyés (chatType : robot ou groupe uniquement)
24
+ - **Démarrage automatique** — la passerelle connecte automatiquement tous les comptes de robots configurés au démarrage
25
25
  - **Debounce entrant** — fusionner les messages consécutifs rapides du même émetteur via la config OpenClaw `messages.inbound.debounceMs`
26
26
  - **Message ack** — envoyer un message « reçu, en cours de traitement... » avant le traitement de l'agent ; auto-révoqué après la réponse ; langue auto-détectée
27
27
  - **Zéro modification du core** — mode plugin pur, `git diff HEAD` reste INTACT
@@ -102,7 +102,7 @@ openclaw gateway restart
102
102
 
103
103
  ### Premier message
104
104
 
105
- Après le redémarrage, le bot se connecte automatiquement via WebSocket. Envoyez un DM au bot — vous recevrez un code de pairage. Approuvez-le :
105
+ Après le redémarrage, le robot se connecte automatiquement via WebSocket. Envoyez un DM au robot — vous recevrez un code de pairage. Approuvez-le :
106
106
 
107
107
  ```bash
108
108
  openclaw pairing approve lansenger <code>
@@ -116,8 +116,8 @@ Ajoutez ces variables à `~/.openclaw/.env` ou à votre environnement :
116
116
 
117
117
  | Variable | Description | Exemple |
118
118
  |----------|-------------|---------|
119
- | `LANSENGER_APP_ID` | App ID du bot personnel | `your-appid` |
120
- | `LANSENGER_APP_SECRET` | App Secret du bot personnel | `ABCDEF123456...` |
119
+ | `LANSENGER_APP_ID` | App ID du robot personnel | `your-appid` |
120
+ | `LANSENGER_APP_SECRET` | App Secret du robot personnel | `ABCDEF123456...` |
121
121
  | `LANSENGER_API_GATEWAY_URL` | URL de la passerelle API Lansenger (remplacement) | `https://open.e.lanxin.cn/open/apigw` |
122
122
 
123
123
  Les identifiants peuvent aussi être fournis via la configuration `openclaw.json` (voir Configuration optionnelle ci-dessous). Les valeurs de configuration sont prioritaires ; les variables d'environnement sont utilisées comme repli lorsque la configuration n'est pas définie.
@@ -165,8 +165,8 @@ Les identifiants peuvent aussi être fournis via la configuration `openclaw.json
165
165
 
166
166
  | Champ | Description | Valeur par défaut |
167
167
  |-------|-------------|-------------------|
168
- | `appId` | App ID du bot personnel | — |
169
- | `appSecret` | App Secret du bot personnel | — |
168
+ | `appId` | App ID du robot personnel | — |
169
+ | `appSecret` | App Secret du robot personnel | — |
170
170
  | `apiGatewayUrl` | URL de la passerelle API | `https://open.e.lanxin.cn/open/apigw` |
171
171
  | `homeChannel` | Chat ID par défaut pour la livraison cron/notification | — |
172
172
  | `enabled` | Activer/désactiver le canal (défaut runtime : false sans identifiants) | `true` |
@@ -174,9 +174,9 @@ Les identifiants peuvent aussi être fournis via la configuration `openclaw.json
174
174
  | `dmPolicy` | Politique DM : `pairing`, `allowlist`, `open`, `disabled` | `pairing` |
175
175
  | `configWrites` | Autoriser Lansenger à écrire la config en réponse aux événements du canal | `true` |
176
176
  | `name` | Nom d'affichage pour ce compte | — |
177
- | `accounts` | Configuration multi-bot | — |
177
+ | `accounts` | Configuration multi-robot | — |
178
178
  | `groupPolicy` | Politique de groupe : `open` (tous les groupes), `allowlist` (groupes autorisés uniquement), `disabled` (messages de groupe désactivés) | `allowlist` |
179
- | `groupAllowFrom` | IDs de groupes autorisés à déclencher le bot | `[]` |
179
+ | `groupAllowFrom` | IDs de groupes autorisés à déclencher le robot | `[]` |
180
180
  | `groups` | Configuration par groupe (requireMention, enabled, allowFrom) | — |
181
181
  | `ackMessage` | Envoyer un message de confirmation avant le traitement de l'agent | `true` |
182
182
  | `revokeAckMessage` | Révoquer automatiquement le message ack après la réponse de l'agent. Mettre `false` pour garder le message visible (certains utilisateurs préfèrent voir le message plutôt qu'une notification « message révoqué ») | `false` |
@@ -208,12 +208,12 @@ Lorsqu'un utilisateur envoie plusieurs messages rapides consécutifs, le mécani
208
208
  - Les messages média et commandes de contrôle ne sont PAS debounceés — ils sont traités immédiatement
209
209
  - Quand le debounce est actif, les textes fusionnés sont joints par `\n` ; les chemins média sont concaténés ; les métadonnées du dernier message sont utilisées
210
210
 
211
- ### Configuration multi-bot
211
+ ### Configuration multi-robot
212
212
 
213
- Pour ajouter plusieurs bots, utilisez `openclaw config set` avec la structure `accounts` :
213
+ Pour ajouter plusieurs robots, utilisez `openclaw config set` avec la structure `accounts` :
214
214
 
215
215
  ```bash
216
- # Ajouter un deuxième bot (remplacez appid/appsecret/gateway par vos valeurs)
216
+ # Ajouter un deuxième robot (remplacez appid/appsecret/gateway par vos valeurs)
217
217
  openclaw config set channels.lansenger.accounts.your-appid-2.appId "your-appid-2"
218
218
  openclaw config set channels.lansenger.accounts.your-appid-2.appSecret "your-appsecret"
219
219
  openclaw config set channels.lansenger.accounts.your-appid-2.apiGatewayUrl "https://apigw.lx.qianxin.com"
@@ -348,7 +348,7 @@ Le plugin supporte les cartes d'approbation :
348
348
  ## Notes importantes
349
349
 
350
350
  - **Pas de chat staff** — Lansenger n'a que les chats de groupe et DM (privé) ; il n'existe pas de concept de « chat staff ».
351
- - **Révocation chatType** — uniquement `bot` ou `group` ; pas de chatType `staff`.
351
+ - **Révocation chatType** — uniquement `robot` ou `group` ; pas de chatType `staff`.
352
352
  - **Pas de sysMsg sur révocation** — l'API accepte `sysMsg` mais ne l'affiche pas.
353
353
  - **Pas de deleteMessage** — l'API retourne l'erreur 10000 ; la suppression n'est pas disponible.
354
354
  - **appArticles** — utilise le champ `summary` (pas `description`).
@@ -432,7 +432,7 @@ Voir [Documentation des appareils OpenClaw](https://docs.openclaw.ai/cli/devices
432
432
 
433
433
  ### "Le client mobile ne permet pas de voir les identifiants"
434
434
 
435
- Utilisez uniquement le **client Lansenger (desktop)**. L'application mobile n'affiche pas les identifiants du bot.
435
+ Utilisez uniquement le **client Lansenger (desktop)**. L'application mobile n'affiche pas les identifiants du robot.
436
436
 
437
437
  ### "No binding for botId"
438
438
 
package/README.zhHant.md CHANGED
@@ -22,7 +22,7 @@
22
22
  - **入站媒體處理**——下載圖片/檔案/語音,偵測副檔名,向代理提供檔案路徑
23
23
  - **訊息撤回**——撤回已傳送的訊息(chatType 僅支援 bot 與 group)
24
24
  - **自動啟動**——閘道啟動時自動連接所有已設定的機器人帳戶
25
- - **入站防抖合併**——利用 OpenClaw 的 `messages.inbound.debounceMs` 配置,合併同一發送者的連續快速訊息
25
+ - **入站防抖合併**——利用 OpenClaw 的 `messages.inbound.debounceMs` 配置,合併同一傳送者的連續快速訊息
26
26
  - **確認訊息**——在代理處理前傳送「收到,正在處理...」確認訊息,代理回覆後自動撤回,語言自動偵測
27
27
  - **零核心修改**——純插件模式,`git diff HEAD` 保持 PRISTINE
28
28
 
@@ -50,13 +50,13 @@
50
50
 
51
51
  | 工具 | 說明 |
52
52
  |------|------|
53
- | `lansenger_send_text` | 發送純文字訊息,不支援 Markdown |
54
- | `lansenger_send_format_text` | 發送 Markdown 格式文字,支援 @提及 |
55
- | `lansenger_send_file` | 發送檔案/圖片/影片/語音(工作區或外部路徑) |
56
- | `lansenger_send_image_url` | 透過 URL 發送圖片 |
57
- | `lansenger_send_link_card` | 發送富連結預覽卡片 |
58
- | `lansenger_send_app_card` | 發送互動/審批卡片 |
59
- | `lansenger_send_app_articles` | 發送多文章卡片 |
53
+ | `lansenger_send_text` | 傳送純文字訊息,不支援 Markdown |
54
+ | `lansenger_send_format_text` | 傳送 Markdown 格式文字,支援 @提及 |
55
+ | `lansenger_send_file` | 傳送檔案/圖片/影片/語音(工作區或外部路徑) |
56
+ | `lansenger_send_image_url` | 透過 URL 傳送圖片 |
57
+ | `lansenger_send_link_card` | 傳送富連結預覽卡片 |
58
+ | `lansenger_send_app_card` | 傳送互動/審批卡片 |
59
+ | `lansenger_send_app_articles` | 傳送多文章卡片 |
60
60
  | `lansenger_update_dynamic_card` | 原地更新動態卡片狀態 |
61
61
  | `lansenger_revoke_message` | 撤回已傳送的訊息 |
62
62
  | `lansenger_query_groups` | 查詢可用群組 |
@@ -201,7 +201,7 @@ openclaw pairing approve lansenger <配對碼>
201
201
 
202
202
  | 欄位 | 說明 | 預設值 |
203
203
  |------|------|--------|
204
- | `messages.inbound.debounceMs` | 全域防抖視窗(毫秒);同一發送者在視窗內的連續訊息會被合併 | `0`(停用) |
204
+ | `messages.inbound.debounceMs` | 全域防抖視窗(毫秒);同一傳送者在視窗內的連續訊息會被合併 | `0`(停用) |
205
205
  | `messages.inbound.byChannel.lansenger` | 藍信頻道專屬覆蓋(優先於全域) | — |
206
206
  | `messages.queue.mode` | 代理處理中的佇列模式:`steer`、`followup`、`collect`、`queue`、`interrupt` | `steer`(推薦) |
207
207
 
@@ -14,7 +14,7 @@
14
14
  - **即時訊息** — 透過 WebSocket 長連線實現
15
15
  - **多機械人支援** — 將多個藍信機械人綁定至不同的 OpenClaw 代理
16
16
  - **Markdown 支援** — 使用 `formatText` msgType(預設)
17
- - **檔案/圖片/語音附件** — 透過 `text` msgType 上傳媒體
17
+ - **檔案/圖片/語音附件** — 透過 `text` msgType 上載媒體
18
18
  - **審批卡片**——互動式審批流程,支援原地狀態更新(待審批 → 已通過/已拒絕)
19
19
  - **語言偵測** — 自動偵測使用者語言,提供本地化回應
20
20
  - **msgTarget 自動路由** — 所有發送方法自動路由至群組聊天或私聊(DM)API;無需分別的群組/私聊方法
@@ -83,7 +83,7 @@ openclaw gateway restart
83
83
 
84
84
  > **可選**:安裝 `lansenger-cli` 作為 CLI 替代方案:`pipx install lansenger-cli`。
85
85
 
86
- > **自訂閘道**:企業私有化部署(如奇安信)需在設定後透過 `openclaw.json` 或環境變數設定 `apiGatewayUrl` — 見[可選設定](#可選設定)。
86
+ > **自訂網關**:企業私有化部署(如奇安信)需在設定後透過 `openclaw.json` 或環境變數設定 `apiGatewayUrl` — 見[可選設定](#可選設定)。
87
87
 
88
88
  ### 開發安裝(本地連結)
89
89
 
@@ -180,7 +180,7 @@ openclaw pairing approve lansenger <配對碼>
180
180
  | `groups` | 羣級設定(requireMention、enabled、allowFrom) | — |
181
181
  | `ackMessage` | 在代理處理前發送確認訊息 | `true` |
182
182
  | `revokeAckMessage` | 代理回覆遞送後自動撤回確認訊息。設為 `false` 則保留確認訊息可見(有些使用者偏好看到確認訊息而非撤回的系統通知) | `false` |
183
- | `mediaLocalRoots` | 透過媒體傳送本地檔案時允許的根目錄列表;空陣列預設為工作區 + `/tmp` | `[cwd, /tmp]` |
183
+ | `mediaLocalRoots` | 透過媒體發送本地檔案時允許的根目錄列表;空陣列預設為工作區 + `/tmp` | `[cwd, /tmp]` |
184
184
  | `ackMessageTextZh` | 中文確認訊息文案 | `收到,正在處理...` |
185
185
  | `ackMessageTextEn` | 英文確認訊息文案 | `Received, processing...` |
186
186
 
@@ -8,59 +8,20 @@ import { coerceSecretRef } from "openclaw/plugin-sdk/secret-ref-runtime";
8
8
  import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-policy";
9
9
  import { chunkMarkdownTextWithMode, resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-chunking";
10
10
  import * as fs from "node:fs/promises";
11
- import * as fsSync from "node:fs";
12
11
  import * as os from "node:os";
13
12
  import * as path from "node:path";
14
13
  import * as crypto from "node:crypto";
15
14
  import { LansengerClient, DEFAULT_API_GATEWAY_URL } from "./client.js";
16
15
  import { createMessageReceiptFromOutboundResults, createChannelMessageAdapterFromOutbound, } from "openclaw/plugin-sdk/channel-outbound";
17
- import { getRunningClient, getLastInboundTime, stripOpenClawUuidSuffix, gatewayStartAccount, gatewayStopAccount } from "./runtime.js";
16
+ import { getRunningEntryByAccount, getLastInboundTimeByAccount, stripOpenClawUuidSuffix, gatewayStartAccount, gatewayStopAccount } from "./runtime.js";
18
17
  import { lansengerSetupWizard } from "./setup-wizard.js";
18
+ import { PersistentStore } from "./persistent-store.js";
19
19
  const log = createSubsystemLogger("lansenger");
20
20
  const LANSENGER_TEXT_CHUNK_LIMIT = 4000;
21
21
  const APPROVAL_CARD_FILE = path.join(os.homedir(), ".openclaw", "lansenger-approval-cards.json");
22
- class PersistentApprovalCardStore {
23
- data = new Map();
22
+ class PersistentApprovalCardStore extends PersistentStore {
24
23
  constructor() {
25
- this.load();
26
- }
27
- load() {
28
- try {
29
- const raw = fsSync.readFileSync(APPROVAL_CARD_FILE, "utf-8");
30
- const parsed = JSON.parse(raw);
31
- if (parsed && typeof parsed === "object") {
32
- for (const [k, v] of Object.entries(parsed)) {
33
- this.data.set(k, v);
34
- }
35
- }
36
- }
37
- catch { }
38
- }
39
- save() {
40
- try {
41
- const dir = path.dirname(APPROVAL_CARD_FILE);
42
- fsSync.mkdirSync(dir, { recursive: true });
43
- const obj = {};
44
- for (const [k, v] of this.data)
45
- obj[k] = v;
46
- fsSync.writeFileSync(APPROVAL_CARD_FILE, JSON.stringify(obj), "utf-8");
47
- }
48
- catch (e) {
49
- log.warn(`failed to persist approval cards: ${e instanceof Error ? e.message : String(e)}`);
50
- }
51
- }
52
- get(chatId) { return this.data.get(chatId); }
53
- set(chatId, info) {
54
- this.data.set(chatId, info);
55
- this.save();
56
- }
57
- delete(chatId) {
58
- this.data.delete(chatId);
59
- this.save();
60
- }
61
- clear() {
62
- this.data.clear();
63
- this.save();
24
+ super(APPROVAL_CARD_FILE, "approval cards");
64
25
  }
65
26
  }
66
27
  const pendingApprovalCards = new PersistentApprovalCardStore();
@@ -456,14 +417,14 @@ const chatPlugin = createChatChannelPlugin({
456
417
  sendText: async (ctx) => {
457
418
  const sessionKey = ctx.sessionKey;
458
419
  const account = resolveAccount(ctx.cfg, ctx.accountId ?? undefined);
459
- const client = makeClient(account);
420
+ const client = getRunningEntryByAccount(account.accountId ?? "")?.client ?? makeClient(account);
460
421
  const result = await client.sendFormatText(ctx.to, ctx.text);
461
422
  return toOutboundResult(result.messageId, sessionKey, ctx.to);
462
423
  },
463
424
  sendMedia: async (ctx) => {
464
425
  const sessionKey = ctx.sessionKey;
465
426
  const account = resolveAccount(ctx.cfg, ctx.accountId ?? undefined);
466
- const client = makeClient(account);
427
+ const client = getRunningEntryByAccount(account.accountId ?? "")?.client ?? makeClient(account);
467
428
  const caption = ctx.text ?? "";
468
429
  if (ctx.mediaUrl && /^https?:\/\//i.test(ctx.mediaUrl)) {
469
430
  const result = await client.sendImageUrl(ctx.to, ctx.mediaUrl, caption, account.dangerouslyAllowPrivateNetwork);
@@ -520,15 +481,17 @@ const chatPlugin = createChatChannelPlugin({
520
481
  const sessionKey = params.sessionKey ?? "";
521
482
  if (hint?.kind === "approval-resolved") {
522
483
  const chatId = target?.to;
523
- const cardInfo = pendingApprovalCards.get(chatId);
484
+ const acctId = target?.accountId ?? "";
485
+ const cardKey = acctId ? `${acctId}:${chatId}` : chatId;
486
+ const cardInfo = pendingApprovalCards.get(cardKey);
524
487
  if (cardInfo?.messageId) {
525
488
  const account = resolveAccount(params.cfg, target?.accountId ?? undefined);
526
- const client = makeClient(account);
489
+ const client = getRunningEntryByAccount(account.accountId ?? "")?.client ?? makeClient(account);
527
490
  const status = payload?.text?.includes("✅") ? "approved" : "denied";
528
491
  try {
529
492
  await client.updateCardStatus(cardInfo.messageId, status, cardInfo.lang);
530
493
  log.info(`beforeDeliverPayload: approval card updated — messageId=${cardInfo.messageId} status=${status}`);
531
- pendingApprovalCards.delete(chatId);
494
+ pendingApprovalCards.delete(cardKey);
532
495
  }
533
496
  catch (e) {
534
497
  log.error(`beforeDeliverPayload: card update failed — ${e instanceof Error ? e.message : String(e)}`);
@@ -546,7 +509,7 @@ const chatPlugin = createChatChannelPlugin({
546
509
  sendFormattedText: async (ctx) => {
547
510
  const sessionKey = ctx.sessionKey;
548
511
  const account = resolveAccount(ctx.cfg, ctx.accountId ?? undefined);
549
- const client = makeClient(account);
512
+ const client = getRunningEntryByAccount(account.accountId ?? "")?.client ?? makeClient(account);
550
513
  const result = await client.sendFormatText(ctx.to, ctx.text);
551
514
  return [{ channel: "lansenger", messageId: result.messageId ?? "", sessionKey }];
552
515
  },
@@ -572,7 +535,7 @@ const chatPlugin = createChatChannelPlugin({
572
535
  const lansengerOutboundBridge = {
573
536
  sendText: async (ctx) => {
574
537
  const account = resolveAccount(ctx.cfg, ctx.accountId ?? undefined);
575
- const client = makeClient(account);
538
+ const client = getRunningEntryByAccount(account.accountId ?? "")?.client ?? makeClient(account);
576
539
  const result = await client.sendFormatText(ctx.to, ctx.text);
577
540
  const receipt = result.messageId
578
541
  ? createMessageReceiptFromOutboundResults({ results: [{ messageId: result.messageId, chatId: ctx.to }], kind: "text", sentAt: Date.now() })
@@ -581,7 +544,7 @@ const lansengerOutboundBridge = {
581
544
  },
582
545
  sendMedia: async (ctx) => {
583
546
  const account = resolveAccount(ctx.cfg, ctx.accountId ?? undefined);
584
- const client = makeClient(account);
547
+ const client = getRunningEntryByAccount(account.accountId ?? "")?.client ?? makeClient(account);
585
548
  const caption = ctx.text ?? "";
586
549
  if (ctx.mediaUrl && /^https?:\/\//i.test(ctx.mediaUrl)) {
587
550
  const result = await client.sendImageUrl(ctx.to, ctx.mediaUrl, caption, account.dangerouslyAllowPrivateNetwork);
@@ -630,7 +593,7 @@ const lansengerOutboundBridge = {
630
593
  },
631
594
  sendPayload: async (ctx) => {
632
595
  const account = resolveAccount(ctx.cfg, ctx.accountId ?? undefined);
633
- const client = makeClient(account);
596
+ const client = getRunningEntryByAccount(account.accountId ?? "")?.client ?? makeClient(account);
634
597
  const payload = ctx.payload;
635
598
  const hasCodeBlock = payload?.text && /```/.test(payload.text);
636
599
  if (payload?.text && !payload?.mediaUrl && !payload?.presentation) {
@@ -901,9 +864,9 @@ export const lansengerPlugin = {
901
864
  return await probeLansengerAccount(account);
902
865
  },
903
866
  buildAccountSnapshot: ({ account, runtime, probe }) => {
904
- const liveClient = getRunningClient();
905
- const connected = liveClient?.isWsAlive() ?? false;
906
- const lastInboundAt = getLastInboundTime();
867
+ const entry = getRunningEntryByAccount(account.accountId ?? "");
868
+ const connected = entry?.client.isWsAlive() ?? false;
869
+ const lastInboundAt = getLastInboundTimeByAccount(account.accountId ?? "");
907
870
  return {
908
871
  accountId: account.accountId ?? account.appId ?? DEFAULT_ACCOUNT_ID,
909
872
  enabled: account.enabled,
@@ -1016,7 +979,7 @@ export const lansengerPlugin = {
1016
979
  const appCard = (lang === "zh" ? payload.zh : payload.en) ?? payload.en;
1017
980
  const result = await client.sendAppCard(target.to, appCard);
1018
981
  if (result.messageId) {
1019
- pendingApprovalCards.set(target.to, { messageId: result.messageId, lang: lang });
982
+ pendingApprovalCards.set(`${accountId}:${target.to}`, { messageId: result.messageId, lang: lang });
1020
983
  }
1021
984
  return { delivered: result.success, messageId: result.messageId ?? null };
1022
985
  }