@clawrent/openclaw-channel 0.5.0 → 0.6.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/dist/group-routing.d.ts +10 -11
- package/dist/group-routing.js +10 -11
- package/dist/guardrails.d.ts +18 -0
- package/dist/guardrails.js +25 -0
- package/dist/index.js +5 -0
- package/dist/provider.d.ts +3 -0
- package/dist/provider.js +45 -10
- package/openclaw.plugin.json +27 -1
- package/package.json +1 -1
package/dist/group-routing.d.ts
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
/** @-
|
|
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
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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 →
|
|
14
|
-
export declare function
|
|
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: {
|
package/dist/group-routing.js
CHANGED
|
@@ -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
|
-
/** @-
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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 →
|
|
18
|
-
export function
|
|
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)
|
package/dist/guardrails.d.ts
CHANGED
|
@@ -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
|
+
};
|
package/dist/guardrails.js
CHANGED
|
@@ -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
|
@@ -64,6 +64,9 @@ const entry = defineChannelPluginEntry({
|
|
|
64
64
|
// fall back to legacy /ws/session (e.g. before the server auto-provisions the
|
|
65
65
|
// provider-agent participant — Plan 4b Part A — else new sessions reject 4013).
|
|
66
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;
|
|
67
70
|
// --- resolve the agents to serve (multi-agent array, else single token) ---
|
|
68
71
|
// multi: config.agents:[{agentId,token}] → one ProviderClient per agent.
|
|
69
72
|
// single: config.token (+ optional config.agentId), with ~/.clawrent/config.json
|
|
@@ -141,6 +144,8 @@ const entry = defineChannelPluginEntry({
|
|
|
141
144
|
},
|
|
142
145
|
onLog: (m) => ctx.logger?.info?.(`[clawrent] ${m}`),
|
|
143
146
|
useGroupChannel,
|
|
147
|
+
triagePrefilter,
|
|
148
|
+
triageTwoPhase,
|
|
144
149
|
}).catch((e) => {
|
|
145
150
|
ctx.logger?.error?.(`[clawrent] startProvider failed (agent=${agentKey}): ${String(e)}`);
|
|
146
151
|
return null;
|
package/dist/provider.d.ts
CHANGED
|
@@ -18,6 +18,9 @@ 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;
|
|
21
24
|
}
|
|
22
25
|
export interface ProviderHandle {
|
|
23
26
|
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 {
|
|
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
|
/**
|
|
@@ -119,11 +119,24 @@ export async function startProvider(opts) {
|
|
|
119
119
|
log(`skip empty content session=${sessionId}`);
|
|
120
120
|
return;
|
|
121
121
|
}
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
122
|
+
// D1: @-gate removed — agent self-decides whether to reply (prompt-based).
|
|
123
|
+
// @ is now CONTEXT (addressedToMe), threaded to the agent via UntrustedStructuredContext (D2).
|
|
124
|
+
const addressedToMe = isAddressedToMe(session?.participantId, message);
|
|
125
|
+
// D2 prefilter (D3: gated by triagePrefilter, default on) — silent-drop garbage.
|
|
126
|
+
if (opts.triagePrefilter !== false) {
|
|
127
|
+
const pre = prefilter(content);
|
|
128
|
+
if (pre.drop) {
|
|
129
|
+
log(`prefilter drop session=${sessionId}: ${pre.reason}`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// D3 phase-1 (default off via triageTwoPhase) — cheap pre-screen before full dispatch.
|
|
134
|
+
if (opts.triageTwoPhase) {
|
|
135
|
+
const p1 = phase1Triage(content);
|
|
136
|
+
if (p1.skip) {
|
|
137
|
+
log(`phase-1 skip session=${sessionId}: ${p1.reason}`);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
127
140
|
}
|
|
128
141
|
// 护栏:危险指令直接拦截,不驱动 agent。
|
|
129
142
|
const guard = checkGuardrails(content);
|
|
@@ -142,15 +155,37 @@ export async function startProvider(opts) {
|
|
|
142
155
|
// Windows 文件名不允许冒号, storePath 需做安全转义。
|
|
143
156
|
const storePath = routeSessionKey.replace(/[:]/g, "_");
|
|
144
157
|
const consumerId = session?.consumerUserId ?? "consumer";
|
|
158
|
+
// D2: UntrustedStructuredContext — sender / mentions / gist / @-addressing. With the
|
|
159
|
+
// @-gate gone (D1), this is what lets the agent self-decide whether + how to reply.
|
|
160
|
+
const mentions = Array.isArray(message.mentions) ? message.mentions : [];
|
|
161
|
+
const untrusted = {
|
|
162
|
+
addressedToMe,
|
|
163
|
+
sender: {
|
|
164
|
+
participantId: message.sender?.participantId,
|
|
165
|
+
side: message.sender?.side,
|
|
166
|
+
agentId: message.sender?.agentId,
|
|
167
|
+
consumerUserId: session?.consumerUserId,
|
|
168
|
+
},
|
|
169
|
+
mentions,
|
|
170
|
+
gist: session?.taskDescription,
|
|
171
|
+
participantId: session?.participantId,
|
|
172
|
+
};
|
|
173
|
+
// Inject the context as a preamble in the agent-facing text so the model literally
|
|
174
|
+
// sees it (also available structured on ctxPayload.UntrustedStructuredContext).
|
|
175
|
+
const textForAgentContent = `[被@: ${addressedToMe ? "是" : "否"}] ` +
|
|
176
|
+
`[发送方: ${untrusted.sender.side ?? "unknown"}] ` +
|
|
177
|
+
`[会话: ${untrusted.gist ?? ""}] ` +
|
|
178
|
+
`[mentions: ${mentions.length}]\n${content}`;
|
|
145
179
|
const ctxPayload = {
|
|
146
180
|
From: `${CHANNEL}:${consumerId}`,
|
|
147
181
|
Body: content,
|
|
148
182
|
RawBody: content,
|
|
149
|
-
BodyForAgent:
|
|
183
|
+
BodyForAgent: textForAgentContent,
|
|
150
184
|
channel: CHANNEL,
|
|
151
185
|
SessionKey: routeSessionKey,
|
|
186
|
+
UntrustedStructuredContext: untrusted,
|
|
152
187
|
};
|
|
153
|
-
const raw = { sessionId, consumerId, content, message };
|
|
188
|
+
const raw = { sessionId, consumerId, content, message, textForAgent: textForAgentContent };
|
|
154
189
|
try {
|
|
155
190
|
await opts.deps.runChannelInboundEvent({
|
|
156
191
|
channel: CHANNEL,
|
|
@@ -166,7 +201,7 @@ export async function startProvider(opts) {
|
|
|
166
201
|
id: `${r.sessionId}:${r.message?.id ?? Date.now()}`,
|
|
167
202
|
timestamp: Date.now(),
|
|
168
203
|
rawText: r.content,
|
|
169
|
-
textForAgent: r.content,
|
|
204
|
+
textForAgent: r.textForAgent ?? r.content,
|
|
170
205
|
textForCommands: r.content,
|
|
171
206
|
raw: r,
|
|
172
207
|
}),
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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.
|
|
5
|
+
"version": "0.6.0",
|
|
6
6
|
"channels": ["clawrent"],
|
|
7
7
|
"activation": {
|
|
8
8
|
"onStartup": false,
|
|
@@ -52,6 +52,16 @@
|
|
|
52
52
|
"title": "Use /ws/group participant-scoped channel (Plan 4b)",
|
|
53
53
|
"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
54
|
},
|
|
55
|
+
"triagePrefilter": {
|
|
56
|
+
"type": "boolean",
|
|
57
|
+
"title": "Triage: prefilter silent-drop",
|
|
58
|
+
"description": "true (default) = silent-drop obvious garbage (no alphanumeric/CJK) before driving the agent. false = pass everything through."
|
|
59
|
+
},
|
|
60
|
+
"triageTwoPhase": {
|
|
61
|
+
"type": "boolean",
|
|
62
|
+
"title": "Triage: two-phase pre-screen",
|
|
63
|
+
"description": "false (default). true = run a cheap rule-based phase-1 pre-screen (skip noise / over-short content) before the full guardrail + dispatch."
|
|
64
|
+
},
|
|
55
65
|
"agents": {
|
|
56
66
|
"type": "array",
|
|
57
67
|
"title": "Multiple provider agents (multi-agent mode)",
|
|
@@ -89,6 +99,14 @@
|
|
|
89
99
|
"guardrailsFile": {
|
|
90
100
|
"label": "Guardrails file",
|
|
91
101
|
"help": "外置护栏策略文件(每行 `/regex/ || 原因`,# 开头为注释)。规则追加在内置护栏之后,实现策略外置。"
|
|
102
|
+
},
|
|
103
|
+
"triagePrefilter": {
|
|
104
|
+
"label": "Triage: prefilter (silent-drop)",
|
|
105
|
+
"help": "默认开。静默丢弃明显垃圾内容(无字母/数字/中文,纯符号/空白)。关闭则全部放行。"
|
|
106
|
+
},
|
|
107
|
+
"triageTwoPhase": {
|
|
108
|
+
"label": "Triage: two-phase pre-screen",
|
|
109
|
+
"help": "默认关。开启后在完整护栏+派发前加一道廉价规则预筛(跳过噪音/过短内容)。"
|
|
92
110
|
}
|
|
93
111
|
}
|
|
94
112
|
}
|
|
@@ -129,6 +147,14 @@
|
|
|
129
147
|
"type": "boolean",
|
|
130
148
|
"title": "Use /ws/group participant-scoped channel (Plan 4b)"
|
|
131
149
|
},
|
|
150
|
+
"triagePrefilter": {
|
|
151
|
+
"type": "boolean",
|
|
152
|
+
"title": "Triage: prefilter silent-drop"
|
|
153
|
+
},
|
|
154
|
+
"triageTwoPhase": {
|
|
155
|
+
"type": "boolean",
|
|
156
|
+
"title": "Triage: two-phase pre-screen"
|
|
157
|
+
},
|
|
132
158
|
"agents": {
|
|
133
159
|
"type": "array",
|
|
134
160
|
"title": "Multiple provider agents (multi-agent mode)",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clawrent/openclaw-channel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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",
|