@clawrent/openclaw-channel 0.3.5 → 0.3.6
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/README.md +8 -0
- package/dist/index.js +55 -10
- package/package.json +19 -7
package/README.md
CHANGED
|
@@ -161,6 +161,14 @@ See [guardrails.example.md](guardrails.example.md) for a full example. Recommend
|
|
|
161
161
|
>
|
|
162
162
|
> manifest 的 `configSchema.required` 保持空数组:channel plugin 的 required 字段缺失会让 `openclaw` CLI 整体启动失败(config validation 阻断全局)。字段改为可选 + 运行时 warn。
|
|
163
163
|
|
|
164
|
+
> 💡 **Health-monitor restart no longer causes oscillation (≥ 0.3.6)**: since `@clawrent/openclaw-channel@0.3.6`, the plugin guards against OpenClaw's per-channel health-monitor restart causing presence oscillation — if the health-monitor restarts the `clawrent` channel, the plugin's idempotent singleton stops the previous provider before starting a new one, so at most one ProviderClient holds `/ws/agent` at any time (no 4009 "Replaced by new connection" ping-pong). You do **not** need to disable the health-monitor for this. (An earlier version of this note suggested `channels.clawrent.healthMonitor.enabled: false` — that was **wrong**: the path is rejected by the plugin's strict channel schema (`additionalProperties:false`); the health-monitor is an OpenClaw gateway-core knob, not a plugin channel-config field. If you ever want to disable it for other reasons, look for the exact key in the OpenClaw gateway config docs.)
|
|
165
|
+
>
|
|
166
|
+
> 💡 **health-monitor 重启不再造成振荡(≥ 0.3.6)**:`@clawrent/openclaw-channel@0.3.6` 起,插件已防 OpenClaw per-channel health-monitor 重启导致的 presence 振荡 —— health-monitor 重启 `clawrent` channel 时,插件的幂等单例会先停上一个 provider 再起新的,任何时刻只有一个 ProviderClient 连 `/ws/agent`(不会 4009「Replaced by new connection」乒乓)。**无需为此关闭 health-monitor**。(本说明早先版本建议设 `channels.clawrent.healthMonitor.enabled: false` —— **那是错的**:该路径被插件 strict channel schema(`additionalProperties:false`)拒绝;health-monitor 是 OpenClaw gateway 核心旋钮,不是插件 channel-config 字段。若因别的原因想关,请到 OpenClaw gateway 配置文档查准确的键名。)
|
|
167
|
+
>
|
|
168
|
+
> ⚠️ **One provider per agent token**: the idempotent guard above only prevents **in-process** double-load (OpenClaw calling `registerFull` twice, or a health-monitor restart overlap). It **cannot** prevent two **separate processes** from using the same agent token — e.g. running this OpenClaw plugin **and** an MCP `clawrent_start_serving` on the same token, or two OpenClaw gateways. Two processes are invisible to each other and will 4009-kick each other into a stable oscillation (presence reconnecting every 1–2s, messages dropped in the gaps). Run exactly one provider per agent token.
|
|
169
|
+
>
|
|
170
|
+
> ⚠️ **每个 agent token 只跑一个 provider**:上面的幂等保护只防**进程内**双加载(OpenClaw 两次调 `registerFull`、或 health-monitor 重启叠加)。它**防不了**两个**独立进程**用同一个 agent token —— 例如同一 token 既跑本 OpenClaw plugin、**又**跑 MCP `clawrent_start_serving`,或两个 OpenClaw 网关。两个进程互不可见,会互相 4009 踢成稳态振荡(presence 每 1–2s 重连一轮,间隙丢消息)。**每个 agent token 只跑一个 provider。**
|
|
171
|
+
|
|
164
172
|
### Migrate from a community fork / 从社区 fork 迁移
|
|
165
173
|
|
|
166
174
|
If you previously installed a community fork (same manifest `id` `clawrent` as the official package), make the old fork stop loading **before** installing the official package; otherwise the two coexist and trigger a `duplicate plugin id` warning, and config precedence may silence one of them:
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,27 @@ import { readFileSync } from "node:fs";
|
|
|
8
8
|
import { startProvider } from "./provider.js";
|
|
9
9
|
import { testConnection } from "./setup/setup.js";
|
|
10
10
|
const CHANNEL_ID = "clawrent";
|
|
11
|
+
/**
|
|
12
|
+
* 模块级 provider 单例:同一进程内确保任何时刻只有一个 ProviderClient 连 /ws/agent。
|
|
13
|
+
*
|
|
14
|
+
* 背景:若 registerFull 被调用多次(OpenClaw 双加载 / startup 与 channel 双触发 /
|
|
15
|
+
* health-monitor restart 叠加),而每次都 `void startProvider(...)`,会叠加多个
|
|
16
|
+
* ProviderClient 用同一 agentToken 连 /ws/agent。后端 registerAgentClient 的语义是
|
|
17
|
+
* 「新连接 4009 踢旧连接」,provider 侧 4009 走「非终态 → 无限重连」—— 两个实例互相
|
|
18
|
+
* 踢、互相重连,形成稳态乒乓振荡(后端日志指纹:connected → ~350ms disconnected →
|
|
19
|
+
* 2~3s connected 循环;provider 侧表现为 `presence reconnecting in 1000ms` 高频循环)。
|
|
20
|
+
* 后果:presence 立不住 + reconnect 间隙漏接会话消息。
|
|
21
|
+
*
|
|
22
|
+
* 解法:用 chain 把所有 startProvider 串行化 —— 先干净停掉上一个实例(若有),再起新
|
|
23
|
+
* 实例。无论 registerFull 被调几次,任何时刻只有一个 ProviderClient 占着 /ws/agent,
|
|
24
|
+
* 4009 互踢消失。单实例下 provider 0.2.1+ 的自愈本就稳定(heartbeat 25s < 后端 40s 阈值,
|
|
25
|
+
* 不会被 heartbeat 断)。注意:这只能防「同进程内」双实例;若用户在同 token 上同时跑
|
|
26
|
+
* plugin 进程 + 另一个 provider(MCP start_serving / 第二个 OpenClaw 网关),仍会跨进程
|
|
27
|
+
* 互踢 —— 那是部署规范(同一 agent 同时只能一个 provider serve)。
|
|
28
|
+
*/
|
|
29
|
+
let providerChain = Promise.resolve();
|
|
30
|
+
let activeProvider = null;
|
|
31
|
+
let shutdownRegistered = false;
|
|
11
32
|
// Minimal setup adapter required by createChannelPluginBase.
|
|
12
33
|
const setupAdapter = {
|
|
13
34
|
async testConnection(cfg) {
|
|
@@ -68,8 +89,11 @@ const entry = defineChannelPluginEntry({
|
|
|
68
89
|
const cfg = typeof runtime?.config?.current === "function"
|
|
69
90
|
? runtime.config.current()
|
|
70
91
|
: runtime?.cfg ?? {};
|
|
71
|
-
|
|
72
|
-
|
|
92
|
+
// 串行化:先停上一个 provider 实例(幂等保护,防 registerFull 多次调用叠加多个
|
|
93
|
+
// ProviderClient → 4009 乒乓),再起新实例。任何时刻只有一个 ProviderClient 连
|
|
94
|
+
// /ws/agent(见模块级 providerChain 注释)。chain 排队也消除了「startProvider 异步
|
|
95
|
+
// 启动期间第二次 registerClean 进入」的竞态(那时 activeProvider 尚未赋值)。
|
|
96
|
+
const start = () => startProvider({
|
|
73
97
|
agentToken: token,
|
|
74
98
|
apiBaseUrl,
|
|
75
99
|
wsUrl,
|
|
@@ -86,15 +110,36 @@ const entry = defineChannelPluginEntry({
|
|
|
86
110
|
dispatchReplyWithBufferedBlockDispatcher: dispatchReplyWithBufferedBlockDispatcher,
|
|
87
111
|
},
|
|
88
112
|
onLog: (m) => ctx.logger?.info?.(`[clawrent] ${m}`),
|
|
89
|
-
})
|
|
90
|
-
.
|
|
91
|
-
|
|
92
|
-
})
|
|
93
|
-
.catch((e) => ctx.logger?.error?.(`[clawrent] startProvider failed: ${String(e)}`));
|
|
94
|
-
ctx.registerShutdown?.(() => {
|
|
95
|
-
if (handle)
|
|
96
|
-
void handle.stop();
|
|
113
|
+
}).catch((e) => {
|
|
114
|
+
ctx.logger?.error?.(`[clawrent] startProvider failed: ${String(e)}`);
|
|
115
|
+
return null;
|
|
97
116
|
});
|
|
117
|
+
providerChain = providerChain.then(async () => {
|
|
118
|
+
if (activeProvider) {
|
|
119
|
+
try {
|
|
120
|
+
await activeProvider.stop();
|
|
121
|
+
}
|
|
122
|
+
catch { }
|
|
123
|
+
activeProvider = null;
|
|
124
|
+
}
|
|
125
|
+
activeProvider = await start();
|
|
126
|
+
});
|
|
127
|
+
// shutdown 只注册一次(多次 registerFull 不叠加 shutdown 回调);回调本身停当前
|
|
128
|
+
// activeProvider 并入队 chain,保证停的是最终存活的那个实例。
|
|
129
|
+
if (!shutdownRegistered) {
|
|
130
|
+
shutdownRegistered = true;
|
|
131
|
+
ctx.registerShutdown?.(() => {
|
|
132
|
+
providerChain = providerChain.then(async () => {
|
|
133
|
+
if (activeProvider) {
|
|
134
|
+
try {
|
|
135
|
+
await activeProvider.stop();
|
|
136
|
+
}
|
|
137
|
+
catch { }
|
|
138
|
+
activeProvider = null;
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}
|
|
98
143
|
}
|
|
99
144
|
catch (e) {
|
|
100
145
|
ctx.logger?.error?.(`[clawrent] registerFull threw: ${e?.stack ?? e}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clawrent/openclaw-channel",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "OpenClaw channel plugin that turns ClawRent rental sessions into native OpenClaw conversations, so a local ClawRent provider agent can answer tenants autonomously with its own model and identity. / OpenClaw 频道插件:把 ClawRent 租赁会话桥接成 OpenClaw 原生对话,让本地 ClawRent provider 智能体用自有模型与身份自动应答租户。",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"type": "module",
|
|
@@ -27,8 +27,12 @@
|
|
|
27
27
|
"access": "public"
|
|
28
28
|
},
|
|
29
29
|
"openclaw": {
|
|
30
|
-
"extensions": [
|
|
31
|
-
|
|
30
|
+
"extensions": [
|
|
31
|
+
"./dist/index.js"
|
|
32
|
+
],
|
|
33
|
+
"runtimeExtensions": [
|
|
34
|
+
"./dist/index.js"
|
|
35
|
+
],
|
|
32
36
|
"setupEntry": "./dist/setup/setup.js",
|
|
33
37
|
"runtimeSetupEntry": "./dist/setup/setup.js",
|
|
34
38
|
"install": {
|
|
@@ -36,9 +40,15 @@
|
|
|
36
40
|
"defaultChoice": "clawhub",
|
|
37
41
|
"minHostVersion": ">=2026.7.1"
|
|
38
42
|
},
|
|
39
|
-
"compat": {
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
"compat": {
|
|
44
|
+
"pluginApi": ">=2026.7.1"
|
|
45
|
+
},
|
|
46
|
+
"build": {
|
|
47
|
+
"openclawVersion": "2026.7.1"
|
|
48
|
+
},
|
|
49
|
+
"release": {
|
|
50
|
+
"publishToClawHub": true
|
|
51
|
+
}
|
|
42
52
|
},
|
|
43
53
|
"scripts": {
|
|
44
54
|
"build": "tsc -p tsconfig.json",
|
|
@@ -52,7 +62,9 @@
|
|
|
52
62
|
"openclaw": ">=2026.6.11"
|
|
53
63
|
},
|
|
54
64
|
"peerDependenciesMeta": {
|
|
55
|
-
"openclaw": {
|
|
65
|
+
"openclaw": {
|
|
66
|
+
"optional": true
|
|
67
|
+
}
|
|
56
68
|
},
|
|
57
69
|
"devDependencies": {
|
|
58
70
|
"openclaw": "2026.7.1",
|