@clawrent/openclaw-channel 0.6.0 → 0.8.0
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 +50 -6
- package/dist/index.js +233 -126
- package/dist/provider.d.ts +36 -0
- package/dist/provider.js +71 -8
- package/dist/setup/setup.d.ts +0 -4
- package/dist/setup/setup.js +0 -1
- package/docs/openclaw-sdk-notes.md +27 -0
- package/openclaw.plugin.json +35 -9
- package/package.json +9 -7
package/README.md
CHANGED
|
@@ -135,11 +135,33 @@ See [guardrails.example.md](guardrails.example.md) for a full example. Recommend
|
|
|
135
135
|
|
|
136
136
|
1. `openclaw plugins install clawhub:@clawrent/openclaw-channel` (ClawHub, primary registry) — or `openclaw plugins install @clawrent/openclaw-channel` (npm). For development, `openclaw plugins install --link <this dir>`.
|
|
137
137
|
2. Configure two blocks in `~/.openclaw/openclaw.json`: `plugins.entries.clawrent.config` (read at runtime) + `channels.clawrent` (channel-instance detection), see the example above; put the token in `~/.clawrent/config.json`.
|
|
138
|
-
3. Restart the Gateway → the plugin loads via **channel-triggered loading** (`onStartup:false`) → provider WS comes online → auto-accepts sessions.
|
|
138
|
+
3. Restart the Gateway → the plugin loads via **channel-triggered loading** (`onStartup:false`) → the gateway autostarts the account via `startAccount` → provider WS comes online → auto-accepts sessions. With `channels.clawrent` configured, the provider is **gateway-managed**: the health-monitor sees it as healthy (no false restarts) and it auto-recovers from terminal death / startup failure.
|
|
139
139
|
|
|
140
140
|
1. `openclaw plugins install clawhub:@clawrent/openclaw-channel`(ClawHub,主渠道)—— 或 `openclaw plugins install @clawrent/openclaw-channel`(npm)。开发可用 `openclaw plugins install --link <本目录>`。
|
|
141
141
|
2. 在 `~/.openclaw/openclaw.json` 配置两块:`plugins.entries.clawrent.config`(plugin 运行时读)+ `channels.clawrent`(channel 实例检测),见上方示例;token 放 `~/.clawrent/config.json`。
|
|
142
|
-
3. 重启 Gateway → plugin 走 **channel 触发式加载**(`onStartup:false`)→ provider WS 上线 →
|
|
142
|
+
3. 重启 Gateway → plugin 走 **channel 触发式加载**(`onStartup:false`)→ gateway 经 `startAccount` 自启账户 → provider WS 上线 → 自动接单。配了 `channels.clawrent` 后,provider 由 **gateway 托管**:health-monitor 判其健康(无误判 restart),且终态死亡 / 启动失败时自动恢复。
|
|
143
|
+
|
|
144
|
+
> **OpenClaw 2026.9+ (capability consent):** after installing or upgrading, the plugin sits in an
|
|
145
|
+
> unauthorized state and the gateway log reports `requires capability consent`. Run:
|
|
146
|
+
>
|
|
147
|
+
> ```bash
|
|
148
|
+
> openclaw plugins enable clawrent --accept-capabilities
|
|
149
|
+
> openclaw gateway restart
|
|
150
|
+
> ```
|
|
151
|
+
>
|
|
152
|
+
> Afterwards `openclaw channels status` should show `clawrent ... running`.
|
|
153
|
+
> 2026.7.x/8.x hosts have no such mechanism — skip this step.
|
|
154
|
+
>
|
|
155
|
+
> **OpenClaw 2026.9+(capability consent):** 安装或升级后插件处于未授权状态,
|
|
156
|
+
> gateway 日志会报 `requires capability consent`。执行:
|
|
157
|
+
>
|
|
158
|
+
> ```bash
|
|
159
|
+
> openclaw plugins enable clawrent --accept-capabilities
|
|
160
|
+
> openclaw gateway restart
|
|
161
|
+
> ```
|
|
162
|
+
>
|
|
163
|
+
> 之后 `openclaw channels status` 应显示 `clawrent ... running`。
|
|
164
|
+
> 2026.7.x/8.x 宿主无此机制,跳过本步。
|
|
143
165
|
|
|
144
166
|
> ⚠️ **Do NOT add `plugins.allow: ["clawrent"]`.** In testing, adding `allow` switches the load path to strict mode and actually *blocks* `registerFull` from executing (hit in 0.3.1). Staying in auto-load (no `allow`) is the config that works in 0.3.2.
|
|
145
167
|
>
|
|
@@ -149,6 +171,10 @@ See [guardrails.example.md](guardrails.example.md) for a full example. Recommend
|
|
|
149
171
|
>
|
|
150
172
|
> ⚠️ **保持 `activation.onStartup` 为 `false`**(默认即如此,勿改)。这是跨版本安全的保守选择:channel 触发式加载路径(gateway 检测到 `channels.clawrent` 块即触发 `registerFull`)在所有 OpenClaw build 上都通。历史上 0.2.6–0.3.1 曾设为 `true`,撞上 OpenClaw startup validation 的 loader bug(不认 `defineChannelPluginEntry` 产出的导出形态,误报 `missing register/activate`),导致 plugin 不加载。**该 loader bug 已在 OpenClaw 2026.7.1 上游修复**(`resolvePluginModuleExport` 已 normalize default / module-wrapper 导出,`true` 现在也能干净加载)—— 但没必要换:`false` 全 build 通用,换 `true` 会让仍在修复前 build 的用户重新踩坑。改 manifest 的 onStartup 后**必须 reinstall**(install record 缓存了旧 onStartup 快照,`registry --refresh` 不够)。详见 [docs/openclaw-sdk-notes.md](docs/openclaw-sdk-notes.md)。
|
|
151
173
|
|
|
174
|
+
> ℹ️ **(0.7.0) Startup `missing register/activate` — the real cause was `setupEntry`.** Through 0.6.0 the gateway could log `clawrent missing register/activate export` at startup. This was **not** the default-export loader bug noted just above (that one is fixed upstream) — it was `package.json`'s `openclaw.setupEntry` pointing at `dist/setup/setup.js`: in setup-only plans the loader loaded `setup.js` as the plugin entry and ran the full register/activate check on it, which the setup module legitimately lacks. 0.7.0 removes `setupEntry`; the setup `testConnection` still works via the channel's `setup` adapter. With this fix the plugin registers as a normal plugin at startup and the gateway-managed `startAccount` autostart works.
|
|
175
|
+
>
|
|
176
|
+
> ℹ️ **(0.7.0)启动报 `missing register/activate` —— 真因是 `setupEntry`。** 直到 0.6.0,gateway 启动时可能报 `clawrent missing register/activate export`。这**不是**上一条说的 default-export loader bug(那条上游已修)—— 而是 `package.json` 的 `openclaw.setupEntry` 指向 `dist/setup/setup.js`:setup-only plan 下 loader 把 `setup.js` 当 plugin 入口加载并跑完整 register/activate 校验,而 setup 模块本就没有这些导出。0.7.0 删除 `setupEntry`;setup 的 `testConnection` 仍经 channel 的 `setup` adapter 可用。修复后 plugin 启动时正常注册,gateway 托管的 `startAccount` 自启才得以工作。
|
|
177
|
+
|
|
152
178
|
> ⚠️ **`channels status --deep` may falsely report `not-running` / `disabled` when there is no `plugins.allow`** (an OpenClaw CLI display-layer bug, not a plugin issue). Whether the plugin is actually running should be judged by the gateway log `[clawrent] provider started` + the platform `onlineStatus`, **not** the status display.
|
|
153
179
|
>
|
|
154
180
|
> ⚠️ **`channels status --deep` 在无 `plugins.allow` 模式下可能误报 `not-running` / `disabled`**(OpenClaw CLI 显示层 bug,非 plugin 问题)。实际是否运行以 gateway 日志 `[clawrent] provider started` + 平台 onlineStatus 为准,**不要依赖 status 显示**。
|
|
@@ -161,9 +187,9 @@ See [guardrails.example.md](guardrails.example.md) for a full example. Recommend
|
|
|
161
187
|
>
|
|
162
188
|
> manifest 的 `configSchema.required` 保持空数组:channel plugin 的 required 字段缺失会让 `openclaw` CLI 整体启动失败(config validation 阻断全局)。字段改为可选 + 运行时 warn。
|
|
163
189
|
|
|
164
|
-
> 💡 **
|
|
190
|
+
> 💡 **Gateway-managed provider lifecycle (≥ 0.7.0)**: since `@clawrent/openclaw-channel@0.7.0`, the plugin implements OpenClaw's `gateway.startAccount` / `stopAccount`, making `clawrent` a first-class gateway-managed account. While the `startAccount` task is alive the gateway marks the account `running:true` (no `setStatus` needed), so OpenClaw's per-channel health-monitor sees the channel as healthy and **no longer false-restarts it every ~10 min**. If the provider terminally dies (`agent:dead` / activation failure) or fails to start (e.g. a bad token → 401), `startAccount` exits and the gateway runs a **bounded auto-restart** (retries transient failures; gives up after N attempts on persistent config errors). The per-agent-id singleton still guarantees at most one ProviderClient per agent (no 4009 ping-pong). This requires the `channels.clawrent` block to be configured — without it the plugin falls back to legacy `registerFull` startup, which has no gateway health/restart integration. You do **not** need `gateway.channelHealthCheckMinutes: 0`. (Pre-0.7.0 the health-monitor restart was a silent no-op on this plugin: it could neither see real health nor recover a dead provider, and the only workaround was disabling the monitor.)
|
|
165
191
|
>
|
|
166
|
-
> 💡 **
|
|
192
|
+
> 💡 **gateway 托管的 provider 生命周期(≥ 0.7.0)**:`@clawrent/openclaw-channel@0.7.0` 起实现 OpenClaw 的 `gateway.startAccount` / `stopAccount`,`clawrent` 成为 gateway 托管的一等账户。`startAccount` 任务存活期间 gateway 自动把账户记 `running:true`(无需 `setStatus`),OpenClaw 的 per-channel health-monitor 据此判健康,**不再每 ~10min 误判 restart**。provider 终态死亡(`agent:dead` / 激活失败)或启动失败(如坏 token → 401)时 `startAccount` 退出,gateway 走 **bounded auto-restart**(瞬态故障重试;持久配置错误给足次数后放弃)。per-agent-id 单例仍保证每 agent 至多一个 ProviderClient(无 4009 乒乓)。**需配置 `channels.clawrent` 块** —— 缺失则走 legacy `registerFull` 启动,无 gateway 健康/重启集成。**无需 `gateway.channelHealthCheckMinutes: 0`**。(0.7.0 之前 health-monitor restart 对本插件是静默 no-op:既看不到真实健康、也无法恢复死亡 provider,唯一 workaround 是关掉监控。)
|
|
167
193
|
>
|
|
168
194
|
> ⚠️ **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
195
|
>
|
|
@@ -192,9 +218,27 @@ openclaw plugins install @clawrent/openclaw-channel@<version>
|
|
|
192
218
|
openclaw gateway restart
|
|
193
219
|
```
|
|
194
220
|
|
|
195
|
-
|
|
221
|
+
After the upgrade completes (OpenClaw 2026.9+ hosts):
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
openclaw plugins update clawrent --accept-capabilities # or enable --accept-capabilities after install
|
|
225
|
+
openclaw gateway restart
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
When the capability surface (declared channels / tools / hooks, etc.) changes, 9.x forces re-authorization — this is expected behavior.
|
|
229
|
+
|
|
230
|
+
升级完成后(OpenClaw 2026.9+ 宿主):
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
openclaw plugins update clawrent --accept-capabilities # 或 install 后 enable --accept-capabilities
|
|
234
|
+
openclaw gateway restart
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
能力面(channels/tools/hooks 等声明)有增减时,9.x 会强制重新授权,属预期行为。
|
|
238
|
+
|
|
239
|
+
> ⚠️ **`openclaw plugins uninstall --force` removes the `channels.clawrent` config block** from `~/.openclaw/openclaw.json` (including `agentId` / `autoApproveSessions` / `guardrailsFile` — **not the token**: the token always stays in `~/.clawrent/config.json`, credentials are unaffected). The subsequent `install` **does not recreate** that block. After upgrading, **verify `channels.clawrent` is still present**; if it was removed, restore it from the pre-uninstall backup (`openclaw.json.bak.*`) or re-fill it per the [Configuration](#configuration--配置) section (`agentId` can be left empty to auto-resolve from the token). As of plugin 0.7.0 (gateway-managed lifecycle), the `channels.clawrent` block is **required**: when missing, the plugin falls back to legacy `registerFull` start, which fires on every registerFull call → multiple provider instances → `system.roster` storms the session every few seconds (roster spam). The gateway-managed `startAccount` path (single instance, stable, health-monitor-friendly) only activates when `channels.clawrent` is present. **After `uninstall`, you MUST add back `channels: { clawrent: {} }` (or restore from backup) before restarting the gateway.** This behavior stems from the OpenClaw CLI's `uninstall` design (reported upstream); the plugin cannot intercept it.
|
|
196
240
|
>
|
|
197
|
-
> ⚠️ **`openclaw plugins uninstall --force` 会删除 `channels.clawrent` 配置块**(含 `agentId` / `autoApproveSessions` / `guardrailsFile`,**不含 token** —— token 始终在 `~/.clawrent/config.json`,凭据不受影响),随后的 `install` **不会重建**该块。升级后**请检查 `channels.clawrent` 是否还在**;若被删,从卸载前的备份(`openclaw.json.bak.*`)恢复,或按[配置](#configuration--配置)章节重新填写(`agentId` 可留空由 token
|
|
241
|
+
> ⚠️ **`openclaw plugins uninstall --force` 会删除 `channels.clawrent` 配置块**(含 `agentId` / `autoApproveSessions` / `guardrailsFile`,**不含 token** —— token 始终在 `~/.clawrent/config.json`,凭据不受影响),随后的 `install` **不会重建**该块。升级后**请检查 `channels.clawrent` 是否还在**;若被删,从卸载前的备份(`openclaw.json.bak.*`)恢复,或按[配置](#configuration--配置)章节重新填写(`agentId` 可留空由 token 自动解析)。plugin 0.7.0 起(gateway 托管生命周期)`channels.clawrent` 块**必需**:缺失时 plugin 退回 legacy `registerFull` 启动,每次 registerFull 都启动 → 多 provider 实例 → `system.roster` 每隔几秒刷屏(roster spam)。gateway 托管的 `startAccount` 路径(单实例、稳定、health-monitor 友好)只有在 `channels.clawrent` 存在时才激活。**`uninstall` 后、重启 gateway 前务必加回 `channels: { clawrent: {} }`(或从备份恢复)。**此行为源自 OpenClaw CLI 的 uninstall 设计(已向上游反馈),插件侧无法干预。
|
|
198
242
|
|
|
199
243
|
## Design notes / 设计要点
|
|
200
244
|
|
package/dist/index.js
CHANGED
|
@@ -12,18 +12,18 @@ const CHANNEL_ID = "clawrent";
|
|
|
12
12
|
* 模块级 provider 注册表:每个 agentId 一条串行化 chain + 一个 ProviderHandle,
|
|
13
13
|
* 确保同一 agent 任何时刻只有一个 ProviderClient 连 /ws/agent(或 /ws/group)。
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
* health-monitor restart 叠加)
|
|
17
|
-
* ProviderClient 用同一 agentToken 连。后端 registerAgentClient 的语义是
|
|
15
|
+
* 背景:若同一 agent 的 startProvider 被并发调用多次(OpenClaw 双加载 / startup 与
|
|
16
|
+
* channel 双触发 / health-monitor restart 叠加),每次都 `void startProvider(...)` 会
|
|
17
|
+
* 叠加多个 ProviderClient 用同一 agentToken 连。后端 registerAgentClient 的语义是
|
|
18
18
|
* 「新连接 4009 踢旧连接」,provider 侧 4009 走「非终态 → 无限重连」—— 两个实例互相
|
|
19
19
|
* 踢、互相重连,形成稳态乒乓振荡(后端日志指纹:connected → ~350ms disconnected →
|
|
20
20
|
* 2~3s connected 循环;provider 侧表现为 `presence reconnecting in 1000ms` 高频循环)。
|
|
21
21
|
* 后果:presence 立不住 + reconnect 间隙漏接会话消息。
|
|
22
22
|
*
|
|
23
23
|
* 解法:按 agentId 分链(chain Map),每条链把该 agent 的 startProvider 串行化 ——
|
|
24
|
-
* 先干净停掉该 agent 的上一个实例(若有)
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* 先干净停掉该 agent 的上一个实例(若有),再起新实例。无论谁发起启动(startAccount
|
|
25
|
+
* 或 legacy registerFull)、调几次,每个 agentId 任何时刻只有一个 ProviderClient 占着
|
|
26
|
+
* 连接,4009 互踢消失。不同 agent 持不同 agentToken,彼此不会 4009 互踢(spec §6.4)。
|
|
27
27
|
* 单实例下 provider 0.3.0+ 的自愈稳定(heartbeat 25s < 后端 40s 阈值)。
|
|
28
28
|
* 注意:这只能防「同进程内」同 agent 双实例;跨进程(同 token 跑 plugin + MCP
|
|
29
29
|
* start_serving / 第二个网关)仍会互踢 —— 那是部署规范(同一 agent 同时只一个 provider serve)。
|
|
@@ -39,6 +39,12 @@ const setupAdapter = {
|
|
|
39
39
|
};
|
|
40
40
|
const base = createChannelPluginBase({
|
|
41
41
|
id: CHANNEL_ID,
|
|
42
|
+
meta: {
|
|
43
|
+
label: "ClawRent",
|
|
44
|
+
selectionLabel: "ClawRent (rental sessions)",
|
|
45
|
+
docsPath: "https://clawrent.cloud/docs/openclaw",
|
|
46
|
+
blurb: "ClawRent rental sessions as native OpenClaw conversations, answered by your local provider agent.",
|
|
47
|
+
},
|
|
42
48
|
setup: setupAdapter,
|
|
43
49
|
config: {
|
|
44
50
|
listAccountIds: () => ["clawrent-provider"],
|
|
@@ -47,142 +53,243 @@ const base = createChannelPluginBase({
|
|
|
47
53
|
}),
|
|
48
54
|
},
|
|
49
55
|
});
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
56
|
+
/** Resolve the plugin config object from the gateway runtime cfg. */
|
|
57
|
+
function pluginConfigFromCfg(cfg) {
|
|
58
|
+
return cfg?.plugins?.entries?.[CHANNEL_ID]?.config ?? cfg?.plugins?.[CHANNEL_ID]?.config ?? {};
|
|
59
|
+
}
|
|
60
|
+
/** Read token from ~/.clawrent/config.json when not supplied inline. */
|
|
61
|
+
function readFallbackToken(log) {
|
|
62
|
+
try {
|
|
63
|
+
const crPath = path.join(homedir(), ".clawrent", "config.json");
|
|
64
|
+
let raw = readFileSync(crPath, "utf8");
|
|
65
|
+
if (raw.charCodeAt(0) === 0xfeff)
|
|
66
|
+
raw = raw.slice(1); // strip BOM
|
|
67
|
+
const cr = JSON.parse(raw);
|
|
68
|
+
return cr.token ?? cr.agentToken;
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
log?.(`[clawrent] token fallback read failed: ${e?.message ?? e}`);
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Resolve the agents to serve from plugin config. Returns null when no usable
|
|
77
|
+
* token is configured (channel should stay inactive).
|
|
78
|
+
* - multi: config.agents:[{agentId,token}] → one ProviderClient per agent
|
|
79
|
+
* - single: config.token (+ optional config.agentId), ~/.clawrent/config.json fallback
|
|
80
|
+
*/
|
|
81
|
+
function resolveEntries(config, log) {
|
|
82
|
+
if (Array.isArray(config.agents) && config.agents.length > 0) {
|
|
83
|
+
const entries = config.agents
|
|
84
|
+
.filter((a) => a && typeof a.token === "string" && a.token)
|
|
85
|
+
.map((a) => ({
|
|
86
|
+
agentId: typeof a.agentId === "string" ? a.agentId : undefined,
|
|
87
|
+
token: a.token,
|
|
88
|
+
}));
|
|
89
|
+
if (entries.length === 0) {
|
|
90
|
+
log?.("[clawrent] agents array configured but has no valid entries; channel inactive");
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
return entries;
|
|
94
|
+
}
|
|
95
|
+
let token = config.token;
|
|
96
|
+
if (!token)
|
|
97
|
+
token = readFallbackToken(log);
|
|
98
|
+
if (!token) {
|
|
99
|
+
log?.("[clawrent] no token configured; channel inactive");
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
return [{ agentId: config.agentId, token }];
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Start (or replace) one ProviderClient per resolved agent entry on its per-agentId
|
|
106
|
+
* chain. The chain guarantees ≤1 ProviderClient per agent at any instant (4009 guard).
|
|
107
|
+
* Handles are tracked in activeProviders.
|
|
108
|
+
*/
|
|
109
|
+
function serveAgents(opts) {
|
|
110
|
+
const entries = resolveEntries(opts.config, opts.onLog);
|
|
111
|
+
if (!entries)
|
|
112
|
+
return;
|
|
113
|
+
const multi = entries.length > 1;
|
|
114
|
+
const apiBaseUrl = opts.config.apiBaseUrl;
|
|
115
|
+
const wsUrl = opts.config.wsUrl;
|
|
116
|
+
const autoApprove = opts.config.autoApproveSessions ?? true;
|
|
117
|
+
const guardrailsFile = opts.config.guardrailsFile;
|
|
118
|
+
const useGroupChannel = opts.config.useGroupChannel ?? true;
|
|
119
|
+
const triagePrefilter = opts.config.triagePrefilter;
|
|
120
|
+
const triageTwoPhase = opts.config.triageTwoPhase;
|
|
121
|
+
const baseCursorPath = opts.config.cursorPath ?? path.join(homedir(), ".clawrent", "openclaw-provider-cursor.json");
|
|
122
|
+
for (const entry of entries) {
|
|
123
|
+
const agentKey = entry.agentId ?? "default";
|
|
124
|
+
const cursorPath = multi ? `${baseCursorPath}.${agentKey}` : baseCursorPath;
|
|
125
|
+
const accountId = entry.agentId ?? "clawrent-provider";
|
|
126
|
+
const start = () => startProvider({
|
|
127
|
+
agentToken: entry.token,
|
|
128
|
+
apiBaseUrl,
|
|
129
|
+
wsUrl,
|
|
130
|
+
cursorPath,
|
|
131
|
+
agentId: entry.agentId,
|
|
132
|
+
autoApprove,
|
|
133
|
+
guardrailsFile,
|
|
134
|
+
channelId: CHANNEL_ID,
|
|
135
|
+
accountId,
|
|
136
|
+
cfg: opts.cfg,
|
|
137
|
+
deps: opts.deps,
|
|
138
|
+
onLog: opts.onLog,
|
|
139
|
+
onTerminal: opts.onTerminal,
|
|
140
|
+
abortSignal: opts.abortSignal,
|
|
141
|
+
useGroupChannel,
|
|
142
|
+
triagePrefilter,
|
|
143
|
+
triageTwoPhase,
|
|
144
|
+
}).catch((e) => {
|
|
145
|
+
opts.onLog(`[clawrent] startProvider failed (agent=${agentKey}): ${String(e)}`);
|
|
146
|
+
return null;
|
|
147
|
+
});
|
|
148
|
+
const prevChain = providerChains.get(agentKey) ?? Promise.resolve();
|
|
149
|
+
providerChains.set(agentKey, prevChain.then(async () => {
|
|
150
|
+
const prev = activeProviders.get(agentKey);
|
|
151
|
+
if (prev) {
|
|
152
|
+
try {
|
|
153
|
+
await prev.stop();
|
|
154
|
+
}
|
|
155
|
+
catch { }
|
|
156
|
+
activeProviders.delete(agentKey);
|
|
157
|
+
}
|
|
158
|
+
const handle = await start();
|
|
159
|
+
if (handle)
|
|
160
|
+
activeProviders.set(agentKey, handle);
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Stop every active provider, each on its own chain so we stop the final live
|
|
166
|
+
* instance. Returns a promise that resolves once all current chains settle.
|
|
167
|
+
*/
|
|
168
|
+
function stopAllAgents() {
|
|
169
|
+
const waits = [];
|
|
170
|
+
for (const key of Array.from(providerChains.keys())) {
|
|
171
|
+
const prev = providerChains.get(key) ?? Promise.resolve();
|
|
172
|
+
const next = prev.then(async () => {
|
|
173
|
+
const h = activeProviders.get(key);
|
|
174
|
+
if (h) {
|
|
175
|
+
try {
|
|
176
|
+
await h.stop();
|
|
177
|
+
}
|
|
178
|
+
catch { }
|
|
179
|
+
activeProviders.delete(key);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
providerChains.set(key, next);
|
|
183
|
+
waits.push(next.catch(() => { }));
|
|
184
|
+
}
|
|
185
|
+
return Promise.all(waits).then(() => { });
|
|
186
|
+
}
|
|
187
|
+
const pluginDeps = {
|
|
188
|
+
runChannelInboundEvent,
|
|
189
|
+
recordInboundSession: recordInboundSession,
|
|
190
|
+
dispatchReplyWithBufferedBlockDispatcher: dispatchReplyWithBufferedBlockDispatcher,
|
|
191
|
+
};
|
|
192
|
+
/**
|
|
193
|
+
* Gateway-account lifecycle adapter. When `channels.clawrent` is configured, the
|
|
194
|
+
* gateway autostarts the account at boot and on every health-monitor restart by
|
|
195
|
+
* calling startAccount; stopAccount is called on stop/restart. This is what makes
|
|
196
|
+
* the provider recover after a terminal death WITHOUT disabling health-monitor.
|
|
197
|
+
*
|
|
198
|
+
* Why no setStatus: the gateway sets `running:true` while the startAccount task is
|
|
199
|
+
* alive and `running:false` (setStoppedRuntime) when it exits. evaluateChannelHealth
|
|
200
|
+
* keys off `running`, so simply keeping this task alive while the provider is up is
|
|
201
|
+
* enough to be seen as healthy (no false-positive restart). When the provider
|
|
202
|
+
* terminally dies (onTerminal), we exit; the gateway then auto-restarts us with a
|
|
203
|
+
* bounded backoff (server-channels restartAttempts). We must NOT set
|
|
204
|
+
* `terminalDisconnect` or the gateway would skip the restart.
|
|
205
|
+
*/
|
|
206
|
+
const gatewayAdapter = {
|
|
207
|
+
async startAccount(ctx) {
|
|
208
|
+
const config = pluginConfigFromCfg(ctx.cfg);
|
|
209
|
+
const log = (m) => ctx.log?.info?.(m);
|
|
210
|
+
const entries = resolveEntries(config, log);
|
|
211
|
+
if (!entries) {
|
|
212
|
+
ctx.log?.warn?.("[clawrent] startAccount: no token configured; staying stopped");
|
|
213
|
+
return; // exit cleanly → gateway marks not-running (not an error)
|
|
214
|
+
}
|
|
215
|
+
if (ctx.abortSignal?.aborted)
|
|
216
|
+
return;
|
|
217
|
+
// Hold this task alive until a provider terminally dies or the account is
|
|
218
|
+
// aborted. On exit (finally), stop everything; the gateway then restarts us.
|
|
219
|
+
let release;
|
|
220
|
+
const exited = new Promise((resolve) => { release = resolve; });
|
|
221
|
+
let exitedOnce = false;
|
|
222
|
+
const exitOnce = (reason) => {
|
|
223
|
+
if (exitedOnce)
|
|
224
|
+
return;
|
|
225
|
+
exitedOnce = true;
|
|
226
|
+
ctx.log?.error?.(`[clawrent] exiting account for restart: ${String(reason)}`);
|
|
227
|
+
try {
|
|
228
|
+
release();
|
|
229
|
+
}
|
|
230
|
+
catch { }
|
|
231
|
+
};
|
|
232
|
+
if (ctx.abortSignal) {
|
|
233
|
+
ctx.abortSignal.addEventListener("abort", () => exitOnce("account aborted"), { once: true });
|
|
234
|
+
}
|
|
235
|
+
serveAgents({
|
|
236
|
+
config,
|
|
237
|
+
cfg: ctx.cfg,
|
|
238
|
+
deps: pluginDeps,
|
|
239
|
+
onLog: log,
|
|
240
|
+
onTerminal: (agentKey, reason) => exitOnce(`provider terminally dead (agent=${agentKey}): ${String(reason)}`),
|
|
241
|
+
abortSignal: ctx.abortSignal,
|
|
242
|
+
});
|
|
243
|
+
log("[clawrent] startAccount: provider(s) started; holding account alive");
|
|
244
|
+
try {
|
|
245
|
+
await exited;
|
|
246
|
+
}
|
|
247
|
+
finally {
|
|
248
|
+
log("[clawrent] startAccount: stopping provider(s) before exit");
|
|
249
|
+
await stopAllAgents();
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
async stopAccount(_ctx) {
|
|
253
|
+
// The gateway also aborts startAccount's abortSignal (which stops clients via
|
|
254
|
+
// startProvider); this explicit stop is the idempotent belt-and-suspenders path
|
|
255
|
+
// for flows that call stopAccount directly.
|
|
256
|
+
await stopAllAgents();
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
// Attach the gateway-account adapter. createChannelPluginBase/createChatChannelPlugin
|
|
260
|
+
// don't expose a `gateway` option today, but ChannelPlugin carries it and the runtime
|
|
261
|
+
// reads `plugin.gateway.startAccount/stopAccount`, so we attach it post-construction.
|
|
262
|
+
const chatPlugin = createChatChannelPlugin({ base: base });
|
|
263
|
+
const plugin = { ...chatPlugin, gateway: gatewayAdapter };
|
|
53
264
|
const entry = defineChannelPluginEntry({
|
|
54
265
|
id: CHANNEL_ID,
|
|
55
266
|
name: "ClawRent",
|
|
56
267
|
description: "ClawRent rental-session channel: turns ClawRent rental sessions into native OpenClaw conversations so a local provider agent can answer tenants autonomously with its own model and identity. Uses @clawrent/provider for push-based inbound (no CLI daemon).",
|
|
57
268
|
plugin: plugin,
|
|
58
|
-
//
|
|
269
|
+
// 不在 entry 里注册 CLI command —— registerCliMetadata 阶段的 registerCommand
|
|
59
270
|
// 要求 command.name 字段, 多余且易错; channel 插件无需自定义 command。
|
|
60
271
|
registerFull(ctx) {
|
|
61
272
|
try {
|
|
62
|
-
const config = ctx.config ?? {};
|
|
63
|
-
// Plan 4b: /ws/group participant-scoped channel is the default. Set false to
|
|
64
|
-
// fall back to legacy /ws/session (e.g. before the server auto-provisions the
|
|
65
|
-
// provider-agent participant — Plan 4b Part A — else new sessions reject 4013).
|
|
66
|
-
const useGroupChannel = config.useGroupChannel ?? true;
|
|
67
|
-
// D3: triage switches (optional; prefilter defaults on at the provider, twoPhase off).
|
|
68
|
-
const triagePrefilter = config.triagePrefilter;
|
|
69
|
-
const triageTwoPhase = config.triageTwoPhase;
|
|
70
|
-
// --- resolve the agents to serve (multi-agent array, else single token) ---
|
|
71
|
-
// multi: config.agents:[{agentId,token}] → one ProviderClient per agent.
|
|
72
|
-
// single: config.token (+ optional config.agentId), with ~/.clawrent/config.json
|
|
73
|
-
// fallback so secrets need not live in openclaw.json.
|
|
74
|
-
const resolveSingleToken = () => {
|
|
75
|
-
let token = config.token;
|
|
76
|
-
if (!token) {
|
|
77
|
-
try {
|
|
78
|
-
const crPath = path.join(homedir(), ".clawrent", "config.json");
|
|
79
|
-
let raw = readFileSync(crPath, "utf8");
|
|
80
|
-
if (raw.charCodeAt(0) === 0xfeff)
|
|
81
|
-
raw = raw.slice(1); // strip BOM
|
|
82
|
-
const cr = JSON.parse(raw);
|
|
83
|
-
token = cr.token ?? cr.agentToken;
|
|
84
|
-
}
|
|
85
|
-
catch (e) {
|
|
86
|
-
ctx.logger?.warn?.(`[clawrent] token fallback read failed: ${e?.message ?? e}`);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return token;
|
|
90
|
-
};
|
|
91
|
-
let entries;
|
|
92
|
-
if (Array.isArray(config.agents) && config.agents.length > 0) {
|
|
93
|
-
entries = config.agents
|
|
94
|
-
.filter((a) => a && typeof a.token === "string" && a.token)
|
|
95
|
-
.map((a) => ({
|
|
96
|
-
agentId: typeof a.agentId === "string" ? a.agentId : undefined,
|
|
97
|
-
token: a.token,
|
|
98
|
-
}));
|
|
99
|
-
if (entries.length === 0) {
|
|
100
|
-
ctx.logger?.warn?.("[clawrent] agents array configured but has no valid entries; channel inactive");
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
else {
|
|
105
|
-
const token = resolveSingleToken();
|
|
106
|
-
if (!token) {
|
|
107
|
-
ctx.logger?.warn?.("[clawrent] no token configured; channel inactive");
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
entries = [{ agentId: config.agentId, token }];
|
|
111
|
-
}
|
|
112
|
-
const apiBaseUrl = config.apiBaseUrl;
|
|
113
|
-
const wsUrl = config.wsUrl;
|
|
114
|
-
const autoApprove = config.autoApproveSessions ?? true;
|
|
115
|
-
const guardrailsFile = config.guardrailsFile;
|
|
116
|
-
const baseCursorPath = config.cursorPath ?? path.join(homedir(), ".clawrent", "openclaw-provider-cursor.json");
|
|
117
273
|
const runtime = ctx.runtime;
|
|
118
274
|
const cfg = typeof runtime?.config?.current === "function"
|
|
119
275
|
? runtime.config.current()
|
|
120
276
|
: runtime?.cfg ?? {};
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const accountId = entry.agentId ?? "clawrent-provider";
|
|
129
|
-
const start = () => startProvider({
|
|
130
|
-
agentToken: entry.token,
|
|
131
|
-
apiBaseUrl,
|
|
132
|
-
wsUrl,
|
|
133
|
-
cursorPath,
|
|
134
|
-
agentId: entry.agentId,
|
|
135
|
-
autoApprove,
|
|
136
|
-
guardrailsFile,
|
|
137
|
-
channelId: CHANNEL_ID,
|
|
138
|
-
accountId,
|
|
139
|
-
cfg,
|
|
140
|
-
deps: {
|
|
141
|
-
runChannelInboundEvent,
|
|
142
|
-
recordInboundSession: recordInboundSession,
|
|
143
|
-
dispatchReplyWithBufferedBlockDispatcher: dispatchReplyWithBufferedBlockDispatcher,
|
|
144
|
-
},
|
|
145
|
-
onLog: (m) => ctx.logger?.info?.(`[clawrent] ${m}`),
|
|
146
|
-
useGroupChannel,
|
|
147
|
-
triagePrefilter,
|
|
148
|
-
triageTwoPhase,
|
|
149
|
-
}).catch((e) => {
|
|
150
|
-
ctx.logger?.error?.(`[clawrent] startProvider failed (agent=${agentKey}): ${String(e)}`);
|
|
151
|
-
return null;
|
|
152
|
-
});
|
|
153
|
-
const prevChain = providerChains.get(agentKey) ?? Promise.resolve();
|
|
154
|
-
providerChains.set(agentKey, prevChain.then(async () => {
|
|
155
|
-
const prev = activeProviders.get(agentKey);
|
|
156
|
-
if (prev) {
|
|
157
|
-
try {
|
|
158
|
-
await prev.stop();
|
|
159
|
-
}
|
|
160
|
-
catch { }
|
|
161
|
-
activeProviders.delete(agentKey);
|
|
162
|
-
}
|
|
163
|
-
const handle = await start();
|
|
164
|
-
if (handle)
|
|
165
|
-
activeProviders.set(agentKey, handle);
|
|
166
|
-
}));
|
|
277
|
+
// Adaptive start: when `channels.clawrent` is configured, the gateway owns the
|
|
278
|
+
// account lifecycle (autostart + health-monitor restart via startAccount). Don't
|
|
279
|
+
// start the provider here — startAccount will. Otherwise (no channels.clawrent)
|
|
280
|
+
// fall back to starting from registerFull so the channel still works.
|
|
281
|
+
if (cfg?.channels?.[CHANNEL_ID]) {
|
|
282
|
+
ctx.logger?.info?.("[clawrent] channels.clawrent configured — gateway owns account lifecycle; provider starts via startAccount");
|
|
283
|
+
return;
|
|
167
284
|
}
|
|
168
|
-
|
|
169
|
-
|
|
285
|
+
const config = ctx.config ?? pluginConfigFromCfg(cfg) ?? {};
|
|
286
|
+
ctx.logger?.info?.("[clawrent] no channels.clawrent — starting provider from registerFull (legacy)");
|
|
287
|
+
serveAgents({ config, cfg, deps: pluginDeps, onLog: (m) => ctx.logger?.info?.(m) });
|
|
288
|
+
// shutdown 只注册一次(多次 registerFull 不叠加);停掉所有 active provider。
|
|
170
289
|
if (!shutdownRegistered) {
|
|
171
290
|
shutdownRegistered = true;
|
|
172
291
|
ctx.registerShutdown?.(() => {
|
|
173
|
-
|
|
174
|
-
const prev = providerChains.get(key) ?? Promise.resolve();
|
|
175
|
-
providerChains.set(key, prev.then(async () => {
|
|
176
|
-
const h = activeProviders.get(key);
|
|
177
|
-
if (h) {
|
|
178
|
-
try {
|
|
179
|
-
await h.stop();
|
|
180
|
-
}
|
|
181
|
-
catch { }
|
|
182
|
-
activeProviders.delete(key);
|
|
183
|
-
}
|
|
184
|
-
}));
|
|
185
|
-
}
|
|
292
|
+
void stopAllAgents();
|
|
186
293
|
});
|
|
187
294
|
}
|
|
188
295
|
}
|
package/dist/provider.d.ts
CHANGED
|
@@ -21,8 +21,44 @@ export interface StartProviderOptions {
|
|
|
21
21
|
/** D3: triage switches. prefilter default on; twoPhase default off. */
|
|
22
22
|
triagePrefilter?: boolean;
|
|
23
23
|
triageTwoPhase?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Fired once when this provider terminally dies (ProviderClient emits
|
|
26
|
+
* `agent:dead` / `agent:activation:failed`). The gateway-account lifecycle
|
|
27
|
+
* (index.ts startAccount) uses this to exit the account task so the gateway
|
|
28
|
+
* auto-restarts it (bounded backoff). Optional — absent in legacy mode.
|
|
29
|
+
*/
|
|
30
|
+
onTerminal?: (agentKey: string, reason: unknown) => void;
|
|
31
|
+
/**
|
|
32
|
+
* Account abort signal. When aborted (gateway stopping the account), stop the
|
|
33
|
+
* ProviderClient. Removed again in the returned handle's stop() for cleanliness.
|
|
34
|
+
*/
|
|
35
|
+
abortSignal?: AbortSignal;
|
|
24
36
|
}
|
|
25
37
|
export interface ProviderHandle {
|
|
26
38
|
stop: () => Promise<void> | void;
|
|
27
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* D2 上下文 → OpenClaw ChannelStructuredContext entries(2026.9 channel contract)。
|
|
42
|
+
* 旧字段名 UntrustedStructuredContext 的移除窗口 2026-09-08 已过;9.3 运行时只认
|
|
43
|
+
* entries 数组形状。7.1 宿主不认识新字段 → 退化为 BodyForAgent preamble 通道,
|
|
44
|
+
* 与 9.3 修复前状态一致(两个宿主时代行为对齐,见 plan Task 6 差异表)。
|
|
45
|
+
*/
|
|
46
|
+
export interface ClawRentSessionContext {
|
|
47
|
+
addressedToMe: boolean;
|
|
48
|
+
sender: {
|
|
49
|
+
participantId?: string;
|
|
50
|
+
side?: string;
|
|
51
|
+
agentId?: string;
|
|
52
|
+
consumerUserId?: string;
|
|
53
|
+
};
|
|
54
|
+
mentions: unknown[];
|
|
55
|
+
gist?: string;
|
|
56
|
+
participantId?: string;
|
|
57
|
+
}
|
|
58
|
+
export declare function buildStructuredContextEntries(untrusted: ClawRentSessionContext): {
|
|
59
|
+
label: string;
|
|
60
|
+
source: string;
|
|
61
|
+
type: string;
|
|
62
|
+
payload: ClawRentSessionContext;
|
|
63
|
+
}[];
|
|
28
64
|
export declare function startProvider(opts: StartProviderOptions): Promise<ProviderHandle>;
|
package/dist/provider.js
CHANGED
|
@@ -31,6 +31,16 @@ function extractDialogue(message) {
|
|
|
31
31
|
"";
|
|
32
32
|
return { type, content: String(content ?? "") };
|
|
33
33
|
}
|
|
34
|
+
export function buildStructuredContextEntries(untrusted) {
|
|
35
|
+
return [
|
|
36
|
+
{
|
|
37
|
+
label: "ClawRent session context",
|
|
38
|
+
source: "clawrent",
|
|
39
|
+
type: "clawrent-session-context",
|
|
40
|
+
payload: untrusted,
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
}
|
|
34
44
|
export async function startProvider(opts) {
|
|
35
45
|
const log = (m) => (opts.onLog ? opts.onLog(m) : console.log(`[clawrent:provider] ${m}`));
|
|
36
46
|
// 动态 import,避免插件在未安装 @clawrent/provider 时整体加载失败。
|
|
@@ -56,10 +66,40 @@ export async function startProvider(opts) {
|
|
|
56
66
|
// Surface presence self-heal events (SDK EventEmitter). These replace the dead
|
|
57
67
|
// onDisconnect/onError args previously passed to client.start() — ProviderCallbacks
|
|
58
68
|
// never had those fields, so they were silently ignored.
|
|
69
|
+
const agentKey = opts.agentId ?? "default";
|
|
70
|
+
// Terminal death is fire-once: notifies the account lifecycle (onTerminal) so the
|
|
71
|
+
// gateway can auto-restart the account. Guarded so multiple terminal events don't
|
|
72
|
+
// re-fire.
|
|
73
|
+
let terminated = false;
|
|
74
|
+
const fireTerminal = (reason) => {
|
|
75
|
+
if (terminated)
|
|
76
|
+
return;
|
|
77
|
+
terminated = true;
|
|
78
|
+
opts.onTerminal?.(agentKey, reason);
|
|
79
|
+
};
|
|
59
80
|
client.on('agent:reconnecting', (delay) => log(`presence reconnecting in ${delay}ms`));
|
|
60
81
|
client.on('agent:activated', () => log(`presence activated (online)`));
|
|
61
|
-
client.on('agent:activation:failed', (_aid, err) =>
|
|
62
|
-
|
|
82
|
+
client.on('agent:activation:failed', (_aid, err) => {
|
|
83
|
+
log(`presence activation FAILED (terminal): ${err?.message ?? err}`);
|
|
84
|
+
fireTerminal(err);
|
|
85
|
+
});
|
|
86
|
+
client.on('agent:dead', (_aid, reason) => {
|
|
87
|
+
log(`presence DEAD — terminal close, will not reconnect: ${reason}`);
|
|
88
|
+
fireTerminal(reason);
|
|
89
|
+
});
|
|
90
|
+
// Stop the client when the gateway aborts the account (stopChannel/restart).
|
|
91
|
+
const onAbort = () => {
|
|
92
|
+
try {
|
|
93
|
+
client.stop();
|
|
94
|
+
}
|
|
95
|
+
catch { /* best-effort */ }
|
|
96
|
+
};
|
|
97
|
+
if (opts.abortSignal) {
|
|
98
|
+
if (opts.abortSignal.aborted)
|
|
99
|
+
onAbort();
|
|
100
|
+
else
|
|
101
|
+
opts.abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
102
|
+
}
|
|
63
103
|
// Phase 3: pause/resume observability (ProviderClient 0.4.0 emits these).
|
|
64
104
|
// Pause = consumer paused this agent (4020 close, no reconnect); resume = consumer
|
|
65
105
|
// resumed → ProviderClient reconnects /ws/group. The agent simply goes idle while
|
|
@@ -82,7 +122,13 @@ export async function startProvider(opts) {
|
|
|
82
122
|
if (total > 0)
|
|
83
123
|
usageBySession.set(sid, total);
|
|
84
124
|
});
|
|
85
|
-
|
|
125
|
+
// client.start() resolves once the ProviderClient is set up. If initial setup
|
|
126
|
+
// throws (e.g. 401 auth, unreachable API), treat it as terminal so the account
|
|
127
|
+
// lifecycle (startAccount) exits and the gateway runs its bounded auto-restart
|
|
128
|
+
// (retries transient failures; gives up on persistent config errors). Without
|
|
129
|
+
// this, a setup failure would leave startAccount holding the task alive with no
|
|
130
|
+
// live provider and no recovery.
|
|
131
|
+
const startError = await client.start({
|
|
86
132
|
agentId: opts.agentId,
|
|
87
133
|
onPendingApproval: async (session) => {
|
|
88
134
|
// 平台基线为 false(不自动批准);批准动作只在此端侧一道门发生。
|
|
@@ -120,7 +166,7 @@ export async function startProvider(opts) {
|
|
|
120
166
|
return;
|
|
121
167
|
}
|
|
122
168
|
// D1: @-gate removed — agent self-decides whether to reply (prompt-based).
|
|
123
|
-
// @ is now CONTEXT (addressedToMe), threaded to the agent via
|
|
169
|
+
// @ is now CONTEXT (addressedToMe), threaded to the agent via ChannelStructuredContext (D2).
|
|
124
170
|
const addressedToMe = isAddressedToMe(session?.participantId, message);
|
|
125
171
|
// D2 prefilter (D3: gated by triagePrefilter, default on) — silent-drop garbage.
|
|
126
172
|
if (opts.triagePrefilter !== false) {
|
|
@@ -155,7 +201,7 @@ export async function startProvider(opts) {
|
|
|
155
201
|
// Windows 文件名不允许冒号, storePath 需做安全转义。
|
|
156
202
|
const storePath = routeSessionKey.replace(/[:]/g, "_");
|
|
157
203
|
const consumerId = session?.consumerUserId ?? "consumer";
|
|
158
|
-
// D2:
|
|
204
|
+
// D2: ChannelStructuredContext — sender / mentions / gist / @-addressing. With the
|
|
159
205
|
// @-gate gone (D1), this is what lets the agent self-decide whether + how to reply.
|
|
160
206
|
const mentions = Array.isArray(message.mentions) ? message.mentions : [];
|
|
161
207
|
const untrusted = {
|
|
@@ -171,7 +217,7 @@ export async function startProvider(opts) {
|
|
|
171
217
|
participantId: session?.participantId,
|
|
172
218
|
};
|
|
173
219
|
// Inject the context as a preamble in the agent-facing text so the model literally
|
|
174
|
-
// sees it (also available structured on ctxPayload.
|
|
220
|
+
// sees it (also available structured on ctxPayload.ChannelStructuredContext).
|
|
175
221
|
const textForAgentContent = `[被@: ${addressedToMe ? "是" : "否"}] ` +
|
|
176
222
|
`[发送方: ${untrusted.sender.side ?? "unknown"}] ` +
|
|
177
223
|
`[会话: ${untrusted.gist ?? ""}] ` +
|
|
@@ -183,7 +229,7 @@ export async function startProvider(opts) {
|
|
|
183
229
|
BodyForAgent: textForAgentContent,
|
|
184
230
|
channel: CHANNEL,
|
|
185
231
|
SessionKey: routeSessionKey,
|
|
186
|
-
|
|
232
|
+
ChannelStructuredContext: buildStructuredContextEntries(untrusted),
|
|
187
233
|
};
|
|
188
234
|
const raw = { sessionId, consumerId, content, message, textForAgent: textForAgentContent };
|
|
189
235
|
try {
|
|
@@ -268,7 +314,22 @@ export async function startProvider(opts) {
|
|
|
268
314
|
log(`runChannelInboundEvent failed session=${sessionId}: ${String(e)}`);
|
|
269
315
|
}
|
|
270
316
|
},
|
|
271
|
-
});
|
|
317
|
+
}).then(() => undefined, (e) => e);
|
|
318
|
+
if (startError !== undefined) {
|
|
319
|
+
log(`provider failed to start (terminal): ${String(startError)}`);
|
|
320
|
+
fireTerminal(startError);
|
|
321
|
+
return {
|
|
322
|
+
stop: () => {
|
|
323
|
+
if (opts.abortSignal)
|
|
324
|
+
opts.abortSignal.removeEventListener("abort", onAbort);
|
|
325
|
+
// client.start() threw before setup completed (e.g. 401 auth, unreachable
|
|
326
|
+
// API). Do NOT call client.stop() here — the SDK has an internal pending
|
|
327
|
+
// firstActivation promise that stop() would reject with "Provider stopped",
|
|
328
|
+
// and that rejection is unhandled and crashes the host gateway. The client
|
|
329
|
+
// never finished starting, so there is nothing to stop.
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
}
|
|
272
333
|
log(`provider started (agentId=${opts.agentId ?? "auto"} autoApprove=${opts.autoApprove})`);
|
|
273
334
|
return {
|
|
274
335
|
stop: () => {
|
|
@@ -278,6 +339,8 @@ export async function startProvider(opts) {
|
|
|
278
339
|
catch {
|
|
279
340
|
/* diagnostic unsubscribe best-effort */
|
|
280
341
|
}
|
|
342
|
+
if (opts.abortSignal)
|
|
343
|
+
opts.abortSignal.removeEventListener("abort", onAbort);
|
|
281
344
|
try {
|
|
282
345
|
client.stop();
|
|
283
346
|
}
|
package/dist/setup/setup.d.ts
CHANGED
package/dist/setup/setup.js
CHANGED
|
@@ -6,6 +6,19 @@
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## 0. 7.1 vs 9.3 已知差异表(0.8.0 维护的地板=2026.7.1,2026-09-10 审计)
|
|
10
|
+
|
|
11
|
+
| 面 | 2026.7.1 | 2026.9.3 | 我们的策略 |
|
|
12
|
+
|---|---|---|---|
|
|
13
|
+
| 结构化上下文字段 | `UntrustedStructuredContext`,resolver 同样只收 entries 数组(对象形状在任何宿主上都从未生效) | `ChannelStructuredContext`(entries 数组),旧名已过移除窗口 | 只写新字段;0.8.0 是结构化上下文首次真正生效的版本(9.3),7.1 宿主仍走 BodyForAgent preamble |
|
|
14
|
+
| capability consent | 无 | install/enable/升级需 `--accept-capabilities`(第三方插件,能力面变化触发) | README 引导;manifest 面保持最小 |
|
|
15
|
+
| `plugins.installs` 落盘 | plugin 管理命令可能写入(legacy 残留) | 写盘自动剥除;strict schema 拒绝该 key | 宿主侧问题,见 memory `openclaw-8-2-installs-key-fix` |
|
|
16
|
+
| channel meta | 不要求 | label/selectionLabel/docsPath/blurb 缺失时 gateway 兜底并告警 | 0.8.0 显式提供 |
|
|
17
|
+
| Node 运行时 | 无硬性要求 | 宿主要求 Node ≥24.16(宿主侧约束,非插件约束) | 文档提示,不在 engines 强制 |
|
|
18
|
+
| update rehearsal | 无 | 宿主升级排练会核对插件兼容声明+channel readiness | 声明准确(Task 2),readiness 快速返回 |
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
9
22
|
## 1. 已确认的 OpenClaw Channel Plugin SDK 事实(2026.6.11)
|
|
10
23
|
|
|
11
24
|
| 项 | 文档说法 | 实际实现(dist 验证) |
|
|
@@ -45,6 +58,20 @@
|
|
|
45
58
|
normalize default / module-wrapper 导出,接受 register/activate;PinkBo 在 2026.7.1-2
|
|
46
59
|
复测 `onStartup:true` + `defineChannelPluginEntry`(含纯 default-export 变体)干净加载、
|
|
47
60
|
不复现(openclaw/openclaw#111344 关闭为 "already implemented")。
|
|
61
|
+
**⚠️ 但 clawrent 直到 0.6.0 仍在启动报 `missing register/activate`——真因是 #7(setupEntry),不是本条。** 本条(default-export loader bug)与 clawrent 实际踩到的validation 失败是**两条不同的路径**,别混。
|
|
62
|
+
7. **`openclaw.setupEntry` 被当 plugin 入口校验(2026-08-07 实测,0.7.0 已绕过)**:
|
|
63
|
+
`package.json` 的 `openclaw.setupEntry` / `runtimeSetupEntry` 指向 `./dist/setup/setup.js`
|
|
64
|
+
时,loader 在 setup-only / prefer-setup registration plan 下会把 setup.js 当作主入口加载
|
|
65
|
+
(`loader-svIpMF0d.js:1915` `loadEntry = loadSetupEntry && runtimeSetupEntry ? runtimeSetupEntry : runtimeCandidateEntry`),
|
|
66
|
+
再对它跑完整 plugin 的 register/activate 校验。而 setup.js 是 setup 接口(导出 `{testConnection}`),
|
|
67
|
+
合法地没有 register/activate → 误报 `clawrent missing register/activate export` → plugin
|
|
68
|
+
启动 validation 失败、不进注册 plugin 列表(只剩 channel 触发式加载的 registerFull 兜底,所以显得"非阻塞")。
|
|
69
|
+
诊断法:`OPENCLAW_PLUGIN_LOAD_DEBUG=1` 起 gateway,看 `module shape: export:object keys=__esModule,default,testConnection`
|
|
70
|
+
(= setup.js 形态,非 index.js 的 entry)即铁证。`describePluginModuleExportShape`(loader:392)输出。
|
|
71
|
+
**plugin 侧修法**:删 `package.json` 的 `setupEntry`/`runtimeSetupEntry`(setup 向导 testConnection
|
|
72
|
+
由 `createChannelPluginBase({setup})` adapter 覆盖;setup-only 模式 fallback 加载 index.js,registerFull
|
|
73
|
+
只 full 模式起 provider、setup-only 不起,安全)。**建议回馈上游**:loader 不应对 setupEntry 跑完整
|
|
74
|
+
register/activate 校验(setup entry 是 `defineSetupPluginEntry`/setup 契约,非 plugin entry)。
|
|
48
75
|
6. **`channelConfigs` manifest 字段文档不足**:install 警告
|
|
49
76
|
"channel plugin manifest declares clawrent without channelConfigs metadata",
|
|
50
77
|
但 manifest 已声明 `channelConfigs`。需明确该字段的精确期望结构。
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
"id": "clawrent",
|
|
3
3
|
"name": "ClawRent Channel",
|
|
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
|
-
"version": "0.
|
|
6
|
-
"channels": [
|
|
5
|
+
"version": "0.8.0",
|
|
6
|
+
"channels": [
|
|
7
|
+
"clawrent"
|
|
8
|
+
],
|
|
7
9
|
"activation": {
|
|
8
10
|
"onStartup": false,
|
|
9
|
-
"onChannels": [
|
|
11
|
+
"onChannels": [
|
|
12
|
+
"clawrent"
|
|
13
|
+
]
|
|
10
14
|
},
|
|
11
15
|
"channelConfigs": {
|
|
12
16
|
"clawrent": {
|
|
@@ -69,10 +73,17 @@
|
|
|
69
73
|
"items": {
|
|
70
74
|
"type": "object",
|
|
71
75
|
"properties": {
|
|
72
|
-
"agentId": {
|
|
73
|
-
|
|
76
|
+
"agentId": {
|
|
77
|
+
"type": "string"
|
|
78
|
+
},
|
|
79
|
+
"token": {
|
|
80
|
+
"type": "string"
|
|
81
|
+
}
|
|
74
82
|
},
|
|
75
|
-
"required": [
|
|
83
|
+
"required": [
|
|
84
|
+
"agentId",
|
|
85
|
+
"token"
|
|
86
|
+
]
|
|
76
87
|
}
|
|
77
88
|
}
|
|
78
89
|
}
|
|
@@ -161,13 +172,28 @@
|
|
|
161
172
|
"items": {
|
|
162
173
|
"type": "object",
|
|
163
174
|
"properties": {
|
|
164
|
-
"agentId": {
|
|
165
|
-
|
|
175
|
+
"agentId": {
|
|
176
|
+
"type": "string"
|
|
177
|
+
},
|
|
178
|
+
"token": {
|
|
179
|
+
"type": "string"
|
|
180
|
+
}
|
|
166
181
|
},
|
|
167
|
-
"required": [
|
|
182
|
+
"required": [
|
|
183
|
+
"agentId",
|
|
184
|
+
"token"
|
|
185
|
+
]
|
|
168
186
|
}
|
|
169
187
|
}
|
|
170
188
|
},
|
|
171
189
|
"required": []
|
|
190
|
+
},
|
|
191
|
+
"configContracts": {
|
|
192
|
+
"dangerousFlags": [
|
|
193
|
+
{
|
|
194
|
+
"path": "autoApproveSessions",
|
|
195
|
+
"equals": true
|
|
196
|
+
}
|
|
197
|
+
]
|
|
172
198
|
}
|
|
173
199
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clawrent/openclaw-channel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
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",
|
|
@@ -33,10 +33,9 @@
|
|
|
33
33
|
"runtimeExtensions": [
|
|
34
34
|
"./dist/index.js"
|
|
35
35
|
],
|
|
36
|
-
"setupEntry": "./dist/setup/setup.js",
|
|
37
|
-
"runtimeSetupEntry": "./dist/setup/setup.js",
|
|
38
36
|
"install": {
|
|
39
37
|
"clawhubSpec": "clawhub:@clawrent/openclaw-channel",
|
|
38
|
+
"npmSpec": "@clawrent/openclaw-channel",
|
|
40
39
|
"defaultChoice": "clawhub",
|
|
41
40
|
"minHostVersion": ">=2026.7.1"
|
|
42
41
|
},
|
|
@@ -44,7 +43,7 @@
|
|
|
44
43
|
"pluginApi": ">=2026.7.1"
|
|
45
44
|
},
|
|
46
45
|
"build": {
|
|
47
|
-
"openclawVersion": "2026.
|
|
46
|
+
"openclawVersion": "2026.9.3"
|
|
48
47
|
},
|
|
49
48
|
"release": {
|
|
50
49
|
"publishToClawHub": true
|
|
@@ -53,6 +52,9 @@
|
|
|
53
52
|
"scripts": {
|
|
54
53
|
"build": "tsc -p tsconfig.build.json",
|
|
55
54
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
55
|
+
"verify:compat:floor": "npm install --no-save openclaw@2026.7.1 && npm run typecheck",
|
|
56
|
+
"verify:compat:current": "npm install && npm run typecheck && npm test",
|
|
57
|
+
"verify:compat": "npm run verify:compat:floor && npm run verify:compat:current",
|
|
56
58
|
"test": "vitest run",
|
|
57
59
|
"prepublishOnly": "npm run build"
|
|
58
60
|
},
|
|
@@ -60,7 +62,7 @@
|
|
|
60
62
|
"@clawrent/provider": "^0.4.0"
|
|
61
63
|
},
|
|
62
64
|
"peerDependencies": {
|
|
63
|
-
"openclaw": ">=2026.
|
|
65
|
+
"openclaw": ">=2026.7.1"
|
|
64
66
|
},
|
|
65
67
|
"peerDependenciesMeta": {
|
|
66
68
|
"openclaw": {
|
|
@@ -68,8 +70,8 @@
|
|
|
68
70
|
}
|
|
69
71
|
},
|
|
70
72
|
"devDependencies": {
|
|
71
|
-
"openclaw": "2026.
|
|
72
|
-
"@types/node": "^
|
|
73
|
+
"openclaw": "2026.9.3",
|
|
74
|
+
"@types/node": "^24",
|
|
73
75
|
"typescript": "*",
|
|
74
76
|
"vitest": "^3.0.0"
|
|
75
77
|
}
|