@clawrent/openclaw-channel 0.5.0 → 0.7.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 CHANGED
@@ -135,11 +135,11 @@ 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
143
 
144
144
  > ⚠️ **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
145
  >
@@ -149,6 +149,10 @@ See [guardrails.example.md](guardrails.example.md) for a full example. Recommend
149
149
  >
150
150
  > ⚠️ **保持 `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
151
 
152
+ > ℹ️ **(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.
153
+ >
154
+ > ℹ️ **(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` 自启才得以工作。
155
+
152
156
  > ⚠️ **`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
157
  >
154
158
  > ⚠️ **`channels status --deep` 在无 `plugins.allow` 模式下可能误报 `not-running` / `disabled`**(OpenClaw CLI 显示层 bug,非 plugin 问题)。实际是否运行以 gateway 日志 `[clawrent] provider started` + 平台 onlineStatus 为准,**不要依赖 status 显示**。
@@ -161,9 +165,9 @@ See [guardrails.example.md](guardrails.example.md) for a full example. Recommend
161
165
  >
162
166
  > manifest 的 `configSchema.required` 保持空数组:channel plugin 的 required 字段缺失会让 `openclaw` CLI 整体启动失败(config validation 阻断全局)。字段改为可选 + 运行时 warn。
163
167
 
164
- > 💡 **Health-monitor restart no longer causes oscillation (≥ 0.3.7)**: since `@clawrent/openclaw-channel@0.3.7`, 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.)
168
+ > 💡 **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
169
  >
166
- > 💡 **health-monitor 重启不再造成振荡(≥ 0.3.7)**:`@clawrent/openclaw-channel@0.3.7` 起,插件已防 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 配置文档查准确的键名。)
170
+ > 💡 **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
171
  >
168
172
  > ⚠️ **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
173
  >
@@ -1,17 +1,16 @@
1
- /** @-routing policy (spec §6.3 + Plan 4b-2.2).
1
+ /** Is this provider agent @-addressed by the inbound group message? (D1: was a
2
+ * wake/sleep GATE — now CONTEXT only; the agent self-decides whether to reply.)
2
3
  *
3
- * Decides whether THIS provider agent should wake (drive the OpenClaw bot) for an
4
- * inbound group message:
5
- * - No `mentions` wake. Backward-compatible with the single-agent /ws/session
6
- * plugin (where every consumer message is answered). Also the sensible default
7
- * when a session has a single provider agent.
8
- * - `mentions` present wake ONLY if this agent's participantId is in the list.
9
- * Non-@-ed agents stay silent (spec §6.3). If our participantId is unknown
10
- * (handshake not yet cached), we cannot match → sleep (safer than guessing).
4
+ * Returns true (addressed / no @ in play) when:
5
+ * - No `mentions` → true (backward-compat single-agent /ws/session; sensible
6
+ * default when a session has a single provider agent).
7
+ * - `mentions` present + this agent's participantId is in the list → true.
8
+ * Returns false only when `mentions` names others and our participantId is known
9
+ * but absent (or participantId unknown conservatively false).
11
10
  *
12
11
  * Note: in legacy /ws/session mode there is no top-level `mentions` and
13
- * `myParticipantId` is undefined → always wakes (unchanged behavior). */
14
- export declare function shouldWakeAgent(myParticipantId: string | undefined, message: Record<string, unknown>): boolean;
12
+ * `myParticipantId` is undefined → true (unchanged). */
13
+ export declare function isAddressedToMe(myParticipantId: string | undefined, message: Record<string, unknown>): boolean;
15
14
  /** Reduce a `model.usage` diagnostic `usage` object to a single token total.
16
15
  * Preference: `total` → `promptTokens + output` → `input + output`. 0 if none. */
17
16
  export declare function computeUsageTotal(usage: {
@@ -2,20 +2,19 @@
2
2
  //
3
3
  // Kept dependency-free + side-effect-free so the @-routing and token-usage logic
4
4
  // can be unit-tested without standing up an OpenClaw runtime or a live WS server.
5
- /** @-routing policy (spec §6.3 + Plan 4b-2.2).
5
+ /** Is this provider agent @-addressed by the inbound group message? (D1: was a
6
+ * wake/sleep GATE — now CONTEXT only; the agent self-decides whether to reply.)
6
7
  *
7
- * Decides whether THIS provider agent should wake (drive the OpenClaw bot) for an
8
- * inbound group message:
9
- * - No `mentions` wake. Backward-compatible with the single-agent /ws/session
10
- * plugin (where every consumer message is answered). Also the sensible default
11
- * when a session has a single provider agent.
12
- * - `mentions` present wake ONLY if this agent's participantId is in the list.
13
- * Non-@-ed agents stay silent (spec §6.3). If our participantId is unknown
14
- * (handshake not yet cached), we cannot match → sleep (safer than guessing).
8
+ * Returns true (addressed / no @ in play) when:
9
+ * - No `mentions` → true (backward-compat single-agent /ws/session; sensible
10
+ * default when a session has a single provider agent).
11
+ * - `mentions` present + this agent's participantId is in the list → true.
12
+ * Returns false only when `mentions` names others and our participantId is known
13
+ * but absent (or participantId unknown conservatively false).
15
14
  *
16
15
  * Note: in legacy /ws/session mode there is no top-level `mentions` and
17
- * `myParticipantId` is undefined → always wakes (unchanged behavior). */
18
- export function shouldWakeAgent(myParticipantId, message) {
16
+ * `myParticipantId` is undefined → true (unchanged). */
17
+ export function isAddressedToMe(myParticipantId, message) {
19
18
  const raw = message["mentions"];
20
19
  const mentions = Array.isArray(raw) ? raw : [];
21
20
  if (mentions.length === 0)
@@ -1,5 +1,7 @@
1
1
  export interface GuardrailResult {
2
2
  blocked: boolean;
3
+ /** D2: silent drop (no reply, no warning) — vs `blocked` which replies with a warning. */
4
+ drop?: boolean;
3
5
  reason?: string;
4
6
  }
5
7
  export declare function loadGuardrails(path?: string): string;
@@ -12,3 +14,19 @@ export declare function parseGuardrailRules(content: string): {
12
14
  reason: string;
13
15
  }[];
14
16
  export declare function checkGuardrails(text: string, fileContent?: string): GuardrailResult;
17
+ /**
18
+ * D2 prefilter: silent-drop obvious garbage (no reply, no warning) BEFORE driving the
19
+ * agent. Conservative — only drops content with no alphanumeric/CJK at all (pure
20
+ * symbols/punctuation/whitespace beyond the empty-trim check). Legit messages pass.
21
+ */
22
+ export declare function prefilter(text: string): GuardrailResult;
23
+ /**
24
+ * D3 phase-1 triage (default OFF, gated by `triageTwoPhase` config): cheap rule-based
25
+ * pre-screen BEFORE the full guardrail + dispatch. Returns {skip} to silent-drop what's
26
+ * clearly not worth a full turn. Conservative; twoPhase is off by default so this only
27
+ * runs when explicitly enabled.
28
+ */
29
+ export declare function phase1Triage(text: string): {
30
+ skip: boolean;
31
+ reason?: string;
32
+ };
@@ -52,3 +52,28 @@ export function checkGuardrails(text, fileContent) {
52
52
  }
53
53
  return { blocked: false };
54
54
  }
55
+ /**
56
+ * D2 prefilter: silent-drop obvious garbage (no reply, no warning) BEFORE driving the
57
+ * agent. Conservative — only drops content with no alphanumeric/CJK at all (pure
58
+ * symbols/punctuation/whitespace beyond the empty-trim check). Legit messages pass.
59
+ */
60
+ export function prefilter(text) {
61
+ if (!/[A-Za-z0-9一-鿿]/.test(text)) {
62
+ return { blocked: false, drop: true, reason: "content has no alphanumeric/CJK — likely noise" };
63
+ }
64
+ return { blocked: false, drop: false };
65
+ }
66
+ /**
67
+ * D3 phase-1 triage (default OFF, gated by `triageTwoPhase` config): cheap rule-based
68
+ * pre-screen BEFORE the full guardrail + dispatch. Returns {skip} to silent-drop what's
69
+ * clearly not worth a full turn. Conservative; twoPhase is off by default so this only
70
+ * runs when explicitly enabled.
71
+ */
72
+ export function phase1Triage(text) {
73
+ const pre = prefilter(text);
74
+ if (pre.drop)
75
+ return { skip: true, reason: pre.reason };
76
+ if (text.trim().length < 2)
77
+ return { skip: true, reason: "content too short for a turn" };
78
+ return { skip: false };
79
+ }
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
- * 背景:若 registerFull 被调用多次(OpenClaw 双加载 / startup 与 channel 双触发 /
16
- * health-monitor restart 叠加),而每次都 `void startProvider(...)`,会叠加多个
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 的上一个实例(若有),再起新实例。无论 registerFull 调几次,
25
- * 每个 agentId 任何时刻只有一个 ProviderClient 占着连接,4009 互踢消失。
26
- * 不同 agent 持不同 agentToken,彼此不会 4009 互踢(可安全共驻,spec §6.4)。
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)。
@@ -47,137 +47,243 @@ const base = createChannelPluginBase({
47
47
  }),
48
48
  },
49
49
  });
50
- const plugin = createChatChannelPlugin({
51
- base: base,
52
- });
50
+ /** Resolve the plugin config object from the gateway runtime cfg. */
51
+ function pluginConfigFromCfg(cfg) {
52
+ return cfg?.plugins?.entries?.[CHANNEL_ID]?.config ?? cfg?.plugins?.[CHANNEL_ID]?.config ?? {};
53
+ }
54
+ /** Read token from ~/.clawrent/config.json when not supplied inline. */
55
+ function readFallbackToken(log) {
56
+ try {
57
+ const crPath = path.join(homedir(), ".clawrent", "config.json");
58
+ let raw = readFileSync(crPath, "utf8");
59
+ if (raw.charCodeAt(0) === 0xfeff)
60
+ raw = raw.slice(1); // strip BOM
61
+ const cr = JSON.parse(raw);
62
+ return cr.token ?? cr.agentToken;
63
+ }
64
+ catch (e) {
65
+ log?.(`[clawrent] token fallback read failed: ${e?.message ?? e}`);
66
+ return undefined;
67
+ }
68
+ }
69
+ /**
70
+ * Resolve the agents to serve from plugin config. Returns null when no usable
71
+ * token is configured (channel should stay inactive).
72
+ * - multi: config.agents:[{agentId,token}] → one ProviderClient per agent
73
+ * - single: config.token (+ optional config.agentId), ~/.clawrent/config.json fallback
74
+ */
75
+ function resolveEntries(config, log) {
76
+ if (Array.isArray(config.agents) && config.agents.length > 0) {
77
+ const entries = config.agents
78
+ .filter((a) => a && typeof a.token === "string" && a.token)
79
+ .map((a) => ({
80
+ agentId: typeof a.agentId === "string" ? a.agentId : undefined,
81
+ token: a.token,
82
+ }));
83
+ if (entries.length === 0) {
84
+ log?.("[clawrent] agents array configured but has no valid entries; channel inactive");
85
+ return null;
86
+ }
87
+ return entries;
88
+ }
89
+ let token = config.token;
90
+ if (!token)
91
+ token = readFallbackToken(log);
92
+ if (!token) {
93
+ log?.("[clawrent] no token configured; channel inactive");
94
+ return null;
95
+ }
96
+ return [{ agentId: config.agentId, token }];
97
+ }
98
+ /**
99
+ * Start (or replace) one ProviderClient per resolved agent entry on its per-agentId
100
+ * chain. The chain guarantees ≤1 ProviderClient per agent at any instant (4009 guard).
101
+ * Handles are tracked in activeProviders.
102
+ */
103
+ function serveAgents(opts) {
104
+ const entries = resolveEntries(opts.config, opts.onLog);
105
+ if (!entries)
106
+ return;
107
+ const multi = entries.length > 1;
108
+ const apiBaseUrl = opts.config.apiBaseUrl;
109
+ const wsUrl = opts.config.wsUrl;
110
+ const autoApprove = opts.config.autoApproveSessions ?? true;
111
+ const guardrailsFile = opts.config.guardrailsFile;
112
+ const useGroupChannel = opts.config.useGroupChannel ?? true;
113
+ const triagePrefilter = opts.config.triagePrefilter;
114
+ const triageTwoPhase = opts.config.triageTwoPhase;
115
+ const baseCursorPath = opts.config.cursorPath ?? path.join(homedir(), ".clawrent", "openclaw-provider-cursor.json");
116
+ for (const entry of entries) {
117
+ const agentKey = entry.agentId ?? "default";
118
+ const cursorPath = multi ? `${baseCursorPath}.${agentKey}` : baseCursorPath;
119
+ const accountId = entry.agentId ?? "clawrent-provider";
120
+ const start = () => startProvider({
121
+ agentToken: entry.token,
122
+ apiBaseUrl,
123
+ wsUrl,
124
+ cursorPath,
125
+ agentId: entry.agentId,
126
+ autoApprove,
127
+ guardrailsFile,
128
+ channelId: CHANNEL_ID,
129
+ accountId,
130
+ cfg: opts.cfg,
131
+ deps: opts.deps,
132
+ onLog: opts.onLog,
133
+ onTerminal: opts.onTerminal,
134
+ abortSignal: opts.abortSignal,
135
+ useGroupChannel,
136
+ triagePrefilter,
137
+ triageTwoPhase,
138
+ }).catch((e) => {
139
+ opts.onLog(`[clawrent] startProvider failed (agent=${agentKey}): ${String(e)}`);
140
+ return null;
141
+ });
142
+ const prevChain = providerChains.get(agentKey) ?? Promise.resolve();
143
+ providerChains.set(agentKey, prevChain.then(async () => {
144
+ const prev = activeProviders.get(agentKey);
145
+ if (prev) {
146
+ try {
147
+ await prev.stop();
148
+ }
149
+ catch { }
150
+ activeProviders.delete(agentKey);
151
+ }
152
+ const handle = await start();
153
+ if (handle)
154
+ activeProviders.set(agentKey, handle);
155
+ }));
156
+ }
157
+ }
158
+ /**
159
+ * Stop every active provider, each on its own chain so we stop the final live
160
+ * instance. Returns a promise that resolves once all current chains settle.
161
+ */
162
+ function stopAllAgents() {
163
+ const waits = [];
164
+ for (const key of Array.from(providerChains.keys())) {
165
+ const prev = providerChains.get(key) ?? Promise.resolve();
166
+ const next = prev.then(async () => {
167
+ const h = activeProviders.get(key);
168
+ if (h) {
169
+ try {
170
+ await h.stop();
171
+ }
172
+ catch { }
173
+ activeProviders.delete(key);
174
+ }
175
+ });
176
+ providerChains.set(key, next);
177
+ waits.push(next.catch(() => { }));
178
+ }
179
+ return Promise.all(waits).then(() => { });
180
+ }
181
+ const pluginDeps = {
182
+ runChannelInboundEvent,
183
+ recordInboundSession: recordInboundSession,
184
+ dispatchReplyWithBufferedBlockDispatcher: dispatchReplyWithBufferedBlockDispatcher,
185
+ };
186
+ /**
187
+ * Gateway-account lifecycle adapter. When `channels.clawrent` is configured, the
188
+ * gateway autostarts the account at boot and on every health-monitor restart by
189
+ * calling startAccount; stopAccount is called on stop/restart. This is what makes
190
+ * the provider recover after a terminal death WITHOUT disabling health-monitor.
191
+ *
192
+ * Why no setStatus: the gateway sets `running:true` while the startAccount task is
193
+ * alive and `running:false` (setStoppedRuntime) when it exits. evaluateChannelHealth
194
+ * keys off `running`, so simply keeping this task alive while the provider is up is
195
+ * enough to be seen as healthy (no false-positive restart). When the provider
196
+ * terminally dies (onTerminal), we exit; the gateway then auto-restarts us with a
197
+ * bounded backoff (server-channels restartAttempts). We must NOT set
198
+ * `terminalDisconnect` or the gateway would skip the restart.
199
+ */
200
+ const gatewayAdapter = {
201
+ async startAccount(ctx) {
202
+ const config = pluginConfigFromCfg(ctx.cfg);
203
+ const log = (m) => ctx.log?.info?.(m);
204
+ const entries = resolveEntries(config, log);
205
+ if (!entries) {
206
+ ctx.log?.warn?.("[clawrent] startAccount: no token configured; staying stopped");
207
+ return; // exit cleanly → gateway marks not-running (not an error)
208
+ }
209
+ if (ctx.abortSignal?.aborted)
210
+ return;
211
+ // Hold this task alive until a provider terminally dies or the account is
212
+ // aborted. On exit (finally), stop everything; the gateway then restarts us.
213
+ let release;
214
+ const exited = new Promise((resolve) => { release = resolve; });
215
+ let exitedOnce = false;
216
+ const exitOnce = (reason) => {
217
+ if (exitedOnce)
218
+ return;
219
+ exitedOnce = true;
220
+ ctx.log?.error?.(`[clawrent] exiting account for restart: ${String(reason)}`);
221
+ try {
222
+ release();
223
+ }
224
+ catch { }
225
+ };
226
+ if (ctx.abortSignal) {
227
+ ctx.abortSignal.addEventListener("abort", () => exitOnce("account aborted"), { once: true });
228
+ }
229
+ serveAgents({
230
+ config,
231
+ cfg: ctx.cfg,
232
+ deps: pluginDeps,
233
+ onLog: log,
234
+ onTerminal: (agentKey, reason) => exitOnce(`provider terminally dead (agent=${agentKey}): ${String(reason)}`),
235
+ abortSignal: ctx.abortSignal,
236
+ });
237
+ log("[clawrent] startAccount: provider(s) started; holding account alive");
238
+ try {
239
+ await exited;
240
+ }
241
+ finally {
242
+ log("[clawrent] startAccount: stopping provider(s) before exit");
243
+ await stopAllAgents();
244
+ }
245
+ },
246
+ async stopAccount(_ctx) {
247
+ // The gateway also aborts startAccount's abortSignal (which stops clients via
248
+ // startProvider); this explicit stop is the idempotent belt-and-suspenders path
249
+ // for flows that call stopAccount directly.
250
+ await stopAllAgents();
251
+ },
252
+ };
253
+ // Attach the gateway-account adapter. createChannelPluginBase/createChatChannelPlugin
254
+ // don't expose a `gateway` option today, but ChannelPlugin carries it and the runtime
255
+ // reads `plugin.gateway.startAccount/stopAccount`, so we attach it post-construction.
256
+ const chatPlugin = createChatChannelPlugin({ base: base });
257
+ const plugin = { ...chatPlugin, gateway: gatewayAdapter };
53
258
  const entry = defineChannelPluginEntry({
54
259
  id: CHANNEL_ID,
55
260
  name: "ClawRent",
56
261
  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
262
  plugin: plugin,
58
- // 注意: 不在 entry 里注册 CLI command —— registerCliMetadata 阶段的 registerCommand
263
+ // 不在 entry 里注册 CLI command —— registerCliMetadata 阶段的 registerCommand
59
264
  // 要求 command.name 字段, 多余且易错; channel 插件无需自定义 command。
60
265
  registerFull(ctx) {
61
266
  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
- // --- resolve the agents to serve (multi-agent array, else single token) ---
68
- // multi: config.agents:[{agentId,token}] → one ProviderClient per agent.
69
- // single: config.token (+ optional config.agentId), with ~/.clawrent/config.json
70
- // fallback so secrets need not live in openclaw.json.
71
- const resolveSingleToken = () => {
72
- let token = config.token;
73
- if (!token) {
74
- try {
75
- const crPath = path.join(homedir(), ".clawrent", "config.json");
76
- let raw = readFileSync(crPath, "utf8");
77
- if (raw.charCodeAt(0) === 0xfeff)
78
- raw = raw.slice(1); // strip BOM
79
- const cr = JSON.parse(raw);
80
- token = cr.token ?? cr.agentToken;
81
- }
82
- catch (e) {
83
- ctx.logger?.warn?.(`[clawrent] token fallback read failed: ${e?.message ?? e}`);
84
- }
85
- }
86
- return token;
87
- };
88
- let entries;
89
- if (Array.isArray(config.agents) && config.agents.length > 0) {
90
- entries = config.agents
91
- .filter((a) => a && typeof a.token === "string" && a.token)
92
- .map((a) => ({
93
- agentId: typeof a.agentId === "string" ? a.agentId : undefined,
94
- token: a.token,
95
- }));
96
- if (entries.length === 0) {
97
- ctx.logger?.warn?.("[clawrent] agents array configured but has no valid entries; channel inactive");
98
- return;
99
- }
100
- }
101
- else {
102
- const token = resolveSingleToken();
103
- if (!token) {
104
- ctx.logger?.warn?.("[clawrent] no token configured; channel inactive");
105
- return;
106
- }
107
- entries = [{ agentId: config.agentId, token }];
108
- }
109
- const apiBaseUrl = config.apiBaseUrl;
110
- const wsUrl = config.wsUrl;
111
- const autoApprove = config.autoApproveSessions ?? true;
112
- const guardrailsFile = config.guardrailsFile;
113
- const baseCursorPath = config.cursorPath ?? path.join(homedir(), ".clawrent", "openclaw-provider-cursor.json");
114
267
  const runtime = ctx.runtime;
115
268
  const cfg = typeof runtime?.config?.current === "function"
116
269
  ? runtime.config.current()
117
270
  : runtime?.cfg ?? {};
118
- // (Re)start one ProviderClient per agent, each on its OWN per-agentId chain so the
119
- // same agent never has two concurrent clients (4009 protection; see module doc).
120
- // Different agents use different tokens and never 4009 each other (spec §6.4).
121
- for (const entry of entries) {
122
- const agentKey = entry.agentId ?? "default";
123
- // per-agent cursor file when serving multiple agents (independent dedupe state)
124
- const cursorPath = entries.length > 1 ? `${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,
137
- deps: {
138
- runChannelInboundEvent,
139
- recordInboundSession: recordInboundSession,
140
- dispatchReplyWithBufferedBlockDispatcher: dispatchReplyWithBufferedBlockDispatcher,
141
- },
142
- onLog: (m) => ctx.logger?.info?.(`[clawrent] ${m}`),
143
- useGroupChannel,
144
- }).catch((e) => {
145
- ctx.logger?.error?.(`[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
- }));
271
+ // Adaptive start: when `channels.clawrent` is configured, the gateway owns the
272
+ // account lifecycle (autostart + health-monitor restart via startAccount). Don't
273
+ // start the provider here startAccount will. Otherwise (no channels.clawrent)
274
+ // fall back to starting from registerFull so the channel still works.
275
+ if (cfg?.channels?.[CHANNEL_ID]) {
276
+ ctx.logger?.info?.("[clawrent] channels.clawrent configured gateway owns account lifecycle; provider starts via startAccount");
277
+ return;
162
278
  }
163
- // shutdown 只注册一次(多次 registerFull 不叠加);停掉所有 active provider,每个走自己的 chain,
164
- // 保证停的是最终存活的实例。
279
+ const config = ctx.config ?? pluginConfigFromCfg(cfg) ?? {};
280
+ ctx.logger?.info?.("[clawrent] no channels.clawrent — starting provider from registerFull (legacy)");
281
+ serveAgents({ config, cfg, deps: pluginDeps, onLog: (m) => ctx.logger?.info?.(m) });
282
+ // shutdown 只注册一次(多次 registerFull 不叠加);停掉所有 active provider。
165
283
  if (!shutdownRegistered) {
166
284
  shutdownRegistered = true;
167
285
  ctx.registerShutdown?.(() => {
168
- for (const key of Array.from(providerChains.keys())) {
169
- const prev = providerChains.get(key) ?? Promise.resolve();
170
- providerChains.set(key, prev.then(async () => {
171
- const h = activeProviders.get(key);
172
- if (h) {
173
- try {
174
- await h.stop();
175
- }
176
- catch { }
177
- activeProviders.delete(key);
178
- }
179
- }));
180
- }
286
+ void stopAllAgents();
181
287
  });
182
288
  }
183
289
  }
@@ -18,6 +18,21 @@ export interface StartProviderOptions {
18
18
  onLog?: (msg: string) => void;
19
19
  /** Plan 4b: opt into /ws/group (participant-scoped) instead of /ws/session. */
20
20
  useGroupChannel: boolean;
21
+ /** D3: triage switches. prefilter default on; twoPhase default off. */
22
+ triagePrefilter?: boolean;
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;
21
36
  }
22
37
  export interface ProviderHandle {
23
38
  stop: () => Promise<void> | void;
package/dist/provider.js CHANGED
@@ -16,8 +16,8 @@
16
16
  // - recordInboundSession ← openclaw/plugin-sdk/conversation-runtime
17
17
  // - dispatchReplyWithBufferedBlockDispatcher ← openclaw/plugin-sdk/reply-dispatch-runtime
18
18
  // - runChannelInboundEvent ← openclaw/plugin-sdk/channel-inbound
19
- import { checkGuardrails, loadGuardrails } from "./guardrails.js";
20
- import { shouldWakeAgent, computeUsageTotal, sessionIdFromKey } from "./group-routing.js";
19
+ import { checkGuardrails, loadGuardrails, prefilter, phase1Triage } from "./guardrails.js";
20
+ import { isAddressedToMe, computeUsageTotal, sessionIdFromKey } from "./group-routing.js";
21
21
  import { onDiagnosticEvent } from "openclaw/plugin-sdk/diagnostic-runtime";
22
22
  const CHANNEL = "clawrent";
23
23
  /**
@@ -56,10 +56,40 @@ export async function startProvider(opts) {
56
56
  // Surface presence self-heal events (SDK EventEmitter). These replace the dead
57
57
  // onDisconnect/onError args previously passed to client.start() — ProviderCallbacks
58
58
  // never had those fields, so they were silently ignored.
59
+ const agentKey = opts.agentId ?? "default";
60
+ // Terminal death is fire-once: notifies the account lifecycle (onTerminal) so the
61
+ // gateway can auto-restart the account. Guarded so multiple terminal events don't
62
+ // re-fire.
63
+ let terminated = false;
64
+ const fireTerminal = (reason) => {
65
+ if (terminated)
66
+ return;
67
+ terminated = true;
68
+ opts.onTerminal?.(agentKey, reason);
69
+ };
59
70
  client.on('agent:reconnecting', (delay) => log(`presence reconnecting in ${delay}ms`));
60
71
  client.on('agent:activated', () => log(`presence activated (online)`));
61
- client.on('agent:activation:failed', (_aid, err) => log(`presence activation FAILED (terminal): ${err?.message ?? err}`));
62
- client.on('agent:dead', (_aid, reason) => log(`presence DEAD terminal close, will not reconnect: ${reason}`));
72
+ client.on('agent:activation:failed', (_aid, err) => {
73
+ log(`presence activation FAILED (terminal): ${err?.message ?? err}`);
74
+ fireTerminal(err);
75
+ });
76
+ client.on('agent:dead', (_aid, reason) => {
77
+ log(`presence DEAD — terminal close, will not reconnect: ${reason}`);
78
+ fireTerminal(reason);
79
+ });
80
+ // Stop the client when the gateway aborts the account (stopChannel/restart).
81
+ const onAbort = () => {
82
+ try {
83
+ client.stop();
84
+ }
85
+ catch { /* best-effort */ }
86
+ };
87
+ if (opts.abortSignal) {
88
+ if (opts.abortSignal.aborted)
89
+ onAbort();
90
+ else
91
+ opts.abortSignal.addEventListener("abort", onAbort, { once: true });
92
+ }
63
93
  // Phase 3: pause/resume observability (ProviderClient 0.4.0 emits these).
64
94
  // Pause = consumer paused this agent (4020 close, no reconnect); resume = consumer
65
95
  // resumed → ProviderClient reconnects /ws/group. The agent simply goes idle while
@@ -82,7 +112,13 @@ export async function startProvider(opts) {
82
112
  if (total > 0)
83
113
  usageBySession.set(sid, total);
84
114
  });
85
- await client.start({
115
+ // client.start() resolves once the ProviderClient is set up. If initial setup
116
+ // throws (e.g. 401 auth, unreachable API), treat it as terminal so the account
117
+ // lifecycle (startAccount) exits and the gateway runs its bounded auto-restart
118
+ // (retries transient failures; gives up on persistent config errors). Without
119
+ // this, a setup failure would leave startAccount holding the task alive with no
120
+ // live provider and no recovery.
121
+ const startError = await client.start({
86
122
  agentId: opts.agentId,
87
123
  onPendingApproval: async (session) => {
88
124
  // 平台基线为 false(不自动批准);批准动作只在此端侧一道门发生。
@@ -119,11 +155,24 @@ export async function startProvider(opts) {
119
155
  log(`skip empty content session=${sessionId}`);
120
156
  return;
121
157
  }
122
- // Plan 4b-2.2: @-routingonly wake when this agent is addressed (spec §6.3).
123
- // /ws/session mode (no mentions, no participantId) always wakes (unchanged).
124
- if (!shouldWakeAgent(session?.participantId, message)) {
125
- log(`skip not-@-ed session=${sessionId} (mentions do not include this agent)`);
126
- return;
158
+ // D1: @-gate removed agent self-decides whether to reply (prompt-based).
159
+ // @ is now CONTEXT (addressedToMe), threaded to the agent via UntrustedStructuredContext (D2).
160
+ const addressedToMe = isAddressedToMe(session?.participantId, message);
161
+ // D2 prefilter (D3: gated by triagePrefilter, default on) — silent-drop garbage.
162
+ if (opts.triagePrefilter !== false) {
163
+ const pre = prefilter(content);
164
+ if (pre.drop) {
165
+ log(`prefilter drop session=${sessionId}: ${pre.reason}`);
166
+ return;
167
+ }
168
+ }
169
+ // D3 phase-1 (default off via triageTwoPhase) — cheap pre-screen before full dispatch.
170
+ if (opts.triageTwoPhase) {
171
+ const p1 = phase1Triage(content);
172
+ if (p1.skip) {
173
+ log(`phase-1 skip session=${sessionId}: ${p1.reason}`);
174
+ return;
175
+ }
127
176
  }
128
177
  // 护栏:危险指令直接拦截,不驱动 agent。
129
178
  const guard = checkGuardrails(content);
@@ -142,15 +191,37 @@ export async function startProvider(opts) {
142
191
  // Windows 文件名不允许冒号, storePath 需做安全转义。
143
192
  const storePath = routeSessionKey.replace(/[:]/g, "_");
144
193
  const consumerId = session?.consumerUserId ?? "consumer";
194
+ // D2: UntrustedStructuredContext — sender / mentions / gist / @-addressing. With the
195
+ // @-gate gone (D1), this is what lets the agent self-decide whether + how to reply.
196
+ const mentions = Array.isArray(message.mentions) ? message.mentions : [];
197
+ const untrusted = {
198
+ addressedToMe,
199
+ sender: {
200
+ participantId: message.sender?.participantId,
201
+ side: message.sender?.side,
202
+ agentId: message.sender?.agentId,
203
+ consumerUserId: session?.consumerUserId,
204
+ },
205
+ mentions,
206
+ gist: session?.taskDescription,
207
+ participantId: session?.participantId,
208
+ };
209
+ // Inject the context as a preamble in the agent-facing text so the model literally
210
+ // sees it (also available structured on ctxPayload.UntrustedStructuredContext).
211
+ const textForAgentContent = `[被@: ${addressedToMe ? "是" : "否"}] ` +
212
+ `[发送方: ${untrusted.sender.side ?? "unknown"}] ` +
213
+ `[会话: ${untrusted.gist ?? ""}] ` +
214
+ `[mentions: ${mentions.length}]\n${content}`;
145
215
  const ctxPayload = {
146
216
  From: `${CHANNEL}:${consumerId}`,
147
217
  Body: content,
148
218
  RawBody: content,
149
- BodyForAgent: content,
219
+ BodyForAgent: textForAgentContent,
150
220
  channel: CHANNEL,
151
221
  SessionKey: routeSessionKey,
222
+ UntrustedStructuredContext: untrusted,
152
223
  };
153
- const raw = { sessionId, consumerId, content, message };
224
+ const raw = { sessionId, consumerId, content, message, textForAgent: textForAgentContent };
154
225
  try {
155
226
  await opts.deps.runChannelInboundEvent({
156
227
  channel: CHANNEL,
@@ -166,7 +237,7 @@ export async function startProvider(opts) {
166
237
  id: `${r.sessionId}:${r.message?.id ?? Date.now()}`,
167
238
  timestamp: Date.now(),
168
239
  rawText: r.content,
169
- textForAgent: r.content,
240
+ textForAgent: r.textForAgent ?? r.content,
170
241
  textForCommands: r.content,
171
242
  raw: r,
172
243
  }),
@@ -233,7 +304,22 @@ export async function startProvider(opts) {
233
304
  log(`runChannelInboundEvent failed session=${sessionId}: ${String(e)}`);
234
305
  }
235
306
  },
236
- });
307
+ }).then(() => undefined, (e) => e);
308
+ if (startError !== undefined) {
309
+ log(`provider failed to start (terminal): ${String(startError)}`);
310
+ fireTerminal(startError);
311
+ return {
312
+ stop: () => {
313
+ if (opts.abortSignal)
314
+ opts.abortSignal.removeEventListener("abort", onAbort);
315
+ // client.start() threw before setup completed (e.g. 401 auth, unreachable
316
+ // API). Do NOT call client.stop() here — the SDK has an internal pending
317
+ // firstActivation promise that stop() would reject with "Provider stopped",
318
+ // and that rejection is unhandled and crashes the host gateway. The client
319
+ // never finished starting, so there is nothing to stop.
320
+ },
321
+ };
322
+ }
237
323
  log(`provider started (agentId=${opts.agentId ?? "auto"} autoApprove=${opts.autoApprove})`);
238
324
  return {
239
325
  stop: () => {
@@ -243,6 +329,8 @@ export async function startProvider(opts) {
243
329
  catch {
244
330
  /* diagnostic unsubscribe best-effort */
245
331
  }
332
+ if (opts.abortSignal)
333
+ opts.abortSignal.removeEventListener("abort", onAbort);
246
334
  try {
247
335
  client.stop();
248
336
  }
@@ -7,7 +7,3 @@ export declare function testConnection(config: {
7
7
  token?: string;
8
8
  apiBaseUrl?: string;
9
9
  }): Promise<ClawRentConnectionResult>;
10
- declare const _default: {
11
- testConnection: typeof testConnection;
12
- };
13
- export default _default;
@@ -19,4 +19,3 @@ export async function testConnection(config) {
19
19
  return { ok: false, error: String(e?.message ?? e) };
20
20
  }
21
21
  }
22
- export default { testConnection };
@@ -45,6 +45,20 @@
45
45
  normalize default / module-wrapper 导出,接受 register/activate;PinkBo 在 2026.7.1-2
46
46
  复测 `onStartup:true` + `defineChannelPluginEntry`(含纯 default-export 变体)干净加载、
47
47
  不复现(openclaw/openclaw#111344 关闭为 "already implemented")。
48
+ **⚠️ 但 clawrent 直到 0.6.0 仍在启动报 `missing register/activate`——真因是 #7(setupEntry),不是本条。** 本条(default-export loader bug)与 clawrent 实际踩到的validation 失败是**两条不同的路径**,别混。
49
+ 7. **`openclaw.setupEntry` 被当 plugin 入口校验(2026-08-07 实测,0.7.0 已绕过)**:
50
+ `package.json` 的 `openclaw.setupEntry` / `runtimeSetupEntry` 指向 `./dist/setup/setup.js`
51
+ 时,loader 在 setup-only / prefer-setup registration plan 下会把 setup.js 当作主入口加载
52
+ (`loader-svIpMF0d.js:1915` `loadEntry = loadSetupEntry && runtimeSetupEntry ? runtimeSetupEntry : runtimeCandidateEntry`),
53
+ 再对它跑完整 plugin 的 register/activate 校验。而 setup.js 是 setup 接口(导出 `{testConnection}`),
54
+ 合法地没有 register/activate → 误报 `clawrent missing register/activate export` → plugin
55
+ 启动 validation 失败、不进注册 plugin 列表(只剩 channel 触发式加载的 registerFull 兜底,所以显得"非阻塞")。
56
+ 诊断法:`OPENCLAW_PLUGIN_LOAD_DEBUG=1` 起 gateway,看 `module shape: export:object keys=__esModule,default,testConnection`
57
+ (= setup.js 形态,非 index.js 的 entry)即铁证。`describePluginModuleExportShape`(loader:392)输出。
58
+ **plugin 侧修法**:删 `package.json` 的 `setupEntry`/`runtimeSetupEntry`(setup 向导 testConnection
59
+ 由 `createChannelPluginBase({setup})` adapter 覆盖;setup-only 模式 fallback 加载 index.js,registerFull
60
+ 只 full 模式起 provider、setup-only 不起,安全)。**建议回馈上游**:loader 不应对 setupEntry 跑完整
61
+ register/activate 校验(setup entry 是 `defineSetupPluginEntry`/setup 契约,非 plugin entry)。
48
62
  6. **`channelConfigs` manifest 字段文档不足**:install 警告
49
63
  "channel plugin manifest declares clawrent without channelConfigs metadata",
50
64
  但 manifest 已声明 `channelConfigs`。需明确该字段的精确期望结构。
@@ -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.4.0",
6
- "channels": ["clawrent"],
5
+ "version": "0.7.0",
6
+ "channels": [
7
+ "clawrent"
8
+ ],
7
9
  "activation": {
8
10
  "onStartup": false,
9
- "onChannels": ["clawrent"]
11
+ "onChannels": [
12
+ "clawrent"
13
+ ]
10
14
  },
11
15
  "channelConfigs": {
12
16
  "clawrent": {
@@ -52,6 +56,16 @@
52
56
  "title": "Use /ws/group participant-scoped channel (Plan 4b)",
53
57
  "description": "true (default) = connect /ws/group (participant-scoped, @-routing, per_token usage). false = legacy /ws/session (fallback). Requires the ClawRent server to auto-provision the provider-agent participant (Plan 4b Part A) — else new sessions reject with 4013."
54
58
  },
59
+ "triagePrefilter": {
60
+ "type": "boolean",
61
+ "title": "Triage: prefilter silent-drop",
62
+ "description": "true (default) = silent-drop obvious garbage (no alphanumeric/CJK) before driving the agent. false = pass everything through."
63
+ },
64
+ "triageTwoPhase": {
65
+ "type": "boolean",
66
+ "title": "Triage: two-phase pre-screen",
67
+ "description": "false (default). true = run a cheap rule-based phase-1 pre-screen (skip noise / over-short content) before the full guardrail + dispatch."
68
+ },
55
69
  "agents": {
56
70
  "type": "array",
57
71
  "title": "Multiple provider agents (multi-agent mode)",
@@ -59,10 +73,17 @@
59
73
  "items": {
60
74
  "type": "object",
61
75
  "properties": {
62
- "agentId": { "type": "string" },
63
- "token": { "type": "string" }
76
+ "agentId": {
77
+ "type": "string"
78
+ },
79
+ "token": {
80
+ "type": "string"
81
+ }
64
82
  },
65
- "required": ["agentId", "token"]
83
+ "required": [
84
+ "agentId",
85
+ "token"
86
+ ]
66
87
  }
67
88
  }
68
89
  }
@@ -89,6 +110,14 @@
89
110
  "guardrailsFile": {
90
111
  "label": "Guardrails file",
91
112
  "help": "外置护栏策略文件(每行 `/regex/ || 原因`,# 开头为注释)。规则追加在内置护栏之后,实现策略外置。"
113
+ },
114
+ "triagePrefilter": {
115
+ "label": "Triage: prefilter (silent-drop)",
116
+ "help": "默认开。静默丢弃明显垃圾内容(无字母/数字/中文,纯符号/空白)。关闭则全部放行。"
117
+ },
118
+ "triageTwoPhase": {
119
+ "label": "Triage: two-phase pre-screen",
120
+ "help": "默认关。开启后在完整护栏+派发前加一道廉价规则预筛(跳过噪音/过短内容)。"
92
121
  }
93
122
  }
94
123
  }
@@ -129,16 +158,31 @@
129
158
  "type": "boolean",
130
159
  "title": "Use /ws/group participant-scoped channel (Plan 4b)"
131
160
  },
161
+ "triagePrefilter": {
162
+ "type": "boolean",
163
+ "title": "Triage: prefilter silent-drop"
164
+ },
165
+ "triageTwoPhase": {
166
+ "type": "boolean",
167
+ "title": "Triage: two-phase pre-screen"
168
+ },
132
169
  "agents": {
133
170
  "type": "array",
134
171
  "title": "Multiple provider agents (multi-agent mode)",
135
172
  "items": {
136
173
  "type": "object",
137
174
  "properties": {
138
- "agentId": { "type": "string" },
139
- "token": { "type": "string" }
175
+ "agentId": {
176
+ "type": "string"
177
+ },
178
+ "token": {
179
+ "type": "string"
180
+ }
140
181
  },
141
- "required": ["agentId", "token"]
182
+ "required": [
183
+ "agentId",
184
+ "token"
185
+ ]
142
186
  }
143
187
  }
144
188
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawrent/openclaw-channel",
3
- "version": "0.5.0",
3
+ "version": "0.7.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,8 +33,6 @@
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",
40
38
  "defaultChoice": "clawhub",