gewe-openclaw 2026.3.13 → 2026.3.23
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 +455 -3
- package/index.ts +39 -1
- package/package.json +12 -1
- package/skills/gewe-agent-tools/SKILL.md +113 -0
- package/skills/gewe-channel-rules/SKILL.md +7 -0
- package/src/accounts.ts +51 -5
- package/src/api-tools.ts +1264 -0
- package/src/api.ts +37 -2
- package/src/binary-command.ts +65 -0
- package/src/channel-actions.ts +536 -0
- package/src/channel-allowlist.ts +150 -0
- package/src/channel-directory.ts +419 -0
- package/src/channel-status.ts +186 -0
- package/src/channel.ts +155 -58
- package/src/config-edit.ts +94 -0
- package/src/config-schema.ts +78 -3
- package/src/contacts-api.ts +113 -0
- package/src/delivery.ts +502 -62
- package/src/directory-cache.ts +164 -0
- package/src/gewe-account-api.ts +27 -0
- package/src/group-allowlist-tool.ts +242 -0
- package/src/group-binding-tool.ts +154 -0
- package/src/group-binding.ts +405 -0
- package/src/groups-api.ts +146 -0
- package/src/inbound-batch.ts +5 -2
- package/src/inbound.ts +248 -41
- package/src/media-server.ts +73 -93
- package/src/moments-api.ts +138 -0
- package/src/monitor.ts +81 -24
- package/src/onboarding.ts +9 -4
- package/src/openclaw-compat.ts +1070 -0
- package/src/pairing-store.ts +478 -0
- package/src/personal-api.ts +45 -0
- package/src/policy.ts +130 -22
- package/src/quote-context-cache.ts +97 -0
- package/src/reply-options.ts +101 -2
- package/src/s3.ts +1 -1
- package/src/send.ts +235 -16
- package/src/setup-wizard-types.ts +162 -0
- package/src/setup-wizard.ts +464 -0
- package/src/silk.ts +2 -1
- package/src/state-paths.ts +55 -14
- package/src/types.ts +66 -7
- package/src/xml.ts +158 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { ChannelStatusAdapter, ChannelStatusIssue } from "openclaw/plugin-sdk/channel-runtime";
|
|
2
|
+
|
|
3
|
+
import type { ResolvedGeweAccount } from "./accounts.js";
|
|
4
|
+
import { collectKnownGeweGroupEntries, collectKnownGewePeerEntries } from "./channel-directory.js";
|
|
5
|
+
import { getGeweDirectoryCacheCounts } from "./directory-cache.js";
|
|
6
|
+
import { CHANNEL_CONFIG_KEY } from "./constants.js";
|
|
7
|
+
import { getGeweProfile } from "./group-binding.js";
|
|
8
|
+
import { normalizeGeweBindingConversationId } from "./group-binding.js";
|
|
9
|
+
import { normalizeAccountId, type OpenClawConfig } from "./openclaw-compat.js";
|
|
10
|
+
import { readGeweAllowFromStore } from "./pairing-store.js";
|
|
11
|
+
|
|
12
|
+
type GeweStatusProbe = {
|
|
13
|
+
ok: boolean;
|
|
14
|
+
latencyMs?: number;
|
|
15
|
+
self?: {
|
|
16
|
+
wxid: string;
|
|
17
|
+
nickName?: string;
|
|
18
|
+
};
|
|
19
|
+
error?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type BindingLike = {
|
|
23
|
+
match?: {
|
|
24
|
+
channel?: string;
|
|
25
|
+
accountId?: string;
|
|
26
|
+
peer?: {
|
|
27
|
+
kind?: string;
|
|
28
|
+
id?: string;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const CHANNEL_ALIASES = new Set(["gewe-openclaw", "gewe", "wechat", "wx"]);
|
|
34
|
+
|
|
35
|
+
function listConfigBindings(cfg: OpenClawConfig): BindingLike[] {
|
|
36
|
+
return Array.isArray(cfg.bindings) ? (cfg.bindings as BindingLike[]) : [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeBindingChannel(value?: string): string {
|
|
40
|
+
const trimmed = value?.trim().toLowerCase() ?? "";
|
|
41
|
+
return CHANNEL_ALIASES.has(trimmed) ? CHANNEL_CONFIG_KEY : trimmed;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function bindingMatchesAccount(bindingAccountId: string | undefined, accountId: string): boolean {
|
|
45
|
+
const trimmed = bindingAccountId?.trim();
|
|
46
|
+
if (!trimmed) {
|
|
47
|
+
return accountId === "default";
|
|
48
|
+
}
|
|
49
|
+
if (trimmed === "*") {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return normalizeAccountId(trimmed) === accountId;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function countExplicitBindings(cfg: OpenClawConfig, accountId: string): number {
|
|
56
|
+
let count = 0;
|
|
57
|
+
for (const binding of listConfigBindings(cfg)) {
|
|
58
|
+
if (normalizeBindingChannel(binding.match?.channel) !== CHANNEL_CONFIG_KEY) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (!bindingMatchesAccount(binding.match?.accountId, accountId)) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const normalizedId = normalizeGeweBindingConversationId(binding.match?.peer?.id);
|
|
65
|
+
if (!normalizedId) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
count += 1;
|
|
69
|
+
}
|
|
70
|
+
return count;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function countGroupOverrides(account: ResolvedGeweAccount): number {
|
|
74
|
+
return Object.entries(account.config.groups ?? {}).filter(
|
|
75
|
+
([, group]) => Array.isArray(group?.allowFrom) && group.allowFrom.length > 0,
|
|
76
|
+
).length;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const geweStatus: ChannelStatusAdapter<ResolvedGeweAccount, GeweStatusProbe> = {
|
|
80
|
+
probeAccount: async ({ account }) => {
|
|
81
|
+
const startedAt = Date.now();
|
|
82
|
+
try {
|
|
83
|
+
const profile = await getGeweProfile({ account });
|
|
84
|
+
return {
|
|
85
|
+
ok: true,
|
|
86
|
+
latencyMs: Date.now() - startedAt,
|
|
87
|
+
self: {
|
|
88
|
+
wxid: profile.wxid,
|
|
89
|
+
nickName: profile.nickName,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
} catch (err) {
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
latencyMs: Date.now() - startedAt,
|
|
96
|
+
error: String(err),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
buildChannelSummary: async ({ snapshot }) => ({
|
|
101
|
+
configured: snapshot.configured ?? false,
|
|
102
|
+
tokenSource: snapshot.tokenSource ?? "none",
|
|
103
|
+
running: snapshot.running ?? false,
|
|
104
|
+
mode: snapshot.mode ?? null,
|
|
105
|
+
lastStartAt: snapshot.lastStartAt ?? null,
|
|
106
|
+
lastStopAt: snapshot.lastStopAt ?? null,
|
|
107
|
+
lastError: snapshot.lastError ?? null,
|
|
108
|
+
lastInboundAt: snapshot.lastInboundAt ?? null,
|
|
109
|
+
lastOutboundAt: snapshot.lastOutboundAt ?? null,
|
|
110
|
+
apiReachable: snapshot.apiReachable ?? false,
|
|
111
|
+
apiLatencyMs: snapshot.apiLatencyMs ?? null,
|
|
112
|
+
self: snapshot.self ?? null,
|
|
113
|
+
knownPeersCount: snapshot.knownPeersCount ?? 0,
|
|
114
|
+
knownGroupsCount: snapshot.knownGroupsCount ?? 0,
|
|
115
|
+
cachedGroupMemberCount: snapshot.cachedGroupMemberCount ?? 0,
|
|
116
|
+
explicitBindingCount: snapshot.explicitBindingCount ?? 0,
|
|
117
|
+
groupOverrideCount: snapshot.groupOverrideCount ?? 0,
|
|
118
|
+
pairingAllowFromCount: snapshot.pairingAllowFromCount ?? 0,
|
|
119
|
+
}),
|
|
120
|
+
buildAccountSnapshot: async ({ account, runtime, cfg, probe }) => {
|
|
121
|
+
const configured = Boolean(account.token?.trim() && account.appId?.trim());
|
|
122
|
+
const pairingEntries = await readGeweAllowFromStore({
|
|
123
|
+
accountId: account.accountId,
|
|
124
|
+
}).catch(() => []);
|
|
125
|
+
const peerEntries = collectKnownGewePeerEntries({
|
|
126
|
+
cfg,
|
|
127
|
+
accountId: account.accountId,
|
|
128
|
+
});
|
|
129
|
+
const groupEntries = collectKnownGeweGroupEntries({
|
|
130
|
+
cfg,
|
|
131
|
+
accountId: account.accountId,
|
|
132
|
+
});
|
|
133
|
+
const cacheCounts = getGeweDirectoryCacheCounts(account.accountId);
|
|
134
|
+
return {
|
|
135
|
+
accountId: account.accountId,
|
|
136
|
+
name: account.name,
|
|
137
|
+
enabled: account.enabled,
|
|
138
|
+
configured,
|
|
139
|
+
tokenSource: account.tokenSource,
|
|
140
|
+
baseUrl: account.config.apiBaseUrl ? "[set]" : "[missing]",
|
|
141
|
+
running: runtime?.running ?? false,
|
|
142
|
+
lastStartAt: runtime?.lastStartAt ?? null,
|
|
143
|
+
lastStopAt: runtime?.lastStopAt ?? null,
|
|
144
|
+
lastError: runtime?.lastError ?? null,
|
|
145
|
+
mode: "webhook",
|
|
146
|
+
lastInboundAt: runtime?.lastInboundAt ?? null,
|
|
147
|
+
lastOutboundAt: runtime?.lastOutboundAt ?? null,
|
|
148
|
+
dmPolicy: account.config.dmPolicy ?? "pairing",
|
|
149
|
+
groupPolicy: account.config.groupPolicy ?? "allowlist",
|
|
150
|
+
apiReachable: probe?.ok ?? false,
|
|
151
|
+
apiLatencyMs: probe?.latencyMs ?? null,
|
|
152
|
+
self: probe?.self ?? null,
|
|
153
|
+
knownPeersCount: peerEntries.length,
|
|
154
|
+
knownGroupsCount: groupEntries.length,
|
|
155
|
+
cachedGroupMemberCount: cacheCounts.cachedGroupMemberCount,
|
|
156
|
+
explicitBindingCount: countExplicitBindings(cfg, account.accountId),
|
|
157
|
+
groupOverrideCount: countGroupOverrides(account),
|
|
158
|
+
pairingAllowFromCount: pairingEntries.length,
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
collectStatusIssues: (accounts) => {
|
|
162
|
+
const issues: ChannelStatusIssue[] = [];
|
|
163
|
+
for (const account of accounts) {
|
|
164
|
+
if (account.configured && account.apiReachable === false) {
|
|
165
|
+
issues.push({
|
|
166
|
+
channel: CHANNEL_CONFIG_KEY,
|
|
167
|
+
accountId: account.accountId,
|
|
168
|
+
kind: "runtime",
|
|
169
|
+
message: `GeWe API probe failed for account "${account.accountId}".`,
|
|
170
|
+
fix: "Check token/appId, API base URL, and GeWe service availability.",
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
if (account.groupPolicy === "open" && (account.groupOverrideCount ?? 0) === 0) {
|
|
174
|
+
issues.push({
|
|
175
|
+
channel: CHANNEL_CONFIG_KEY,
|
|
176
|
+
accountId: account.accountId,
|
|
177
|
+
kind: "config",
|
|
178
|
+
message:
|
|
179
|
+
'GeWe groupPolicy="open" is active without any per-group allowFrom override.',
|
|
180
|
+
fix: `Set channels.${CHANNEL_CONFIG_KEY}.groupAllowFrom or groups.<room>.allowFrom to narrow group access.`,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return issues;
|
|
185
|
+
},
|
|
186
|
+
};
|
package/src/channel.ts
CHANGED
|
@@ -3,17 +3,20 @@ import {
|
|
|
3
3
|
buildChannelConfigSchema,
|
|
4
4
|
DEFAULT_ACCOUNT_ID,
|
|
5
5
|
deleteAccountFromConfigSection,
|
|
6
|
-
formatPairingApproveHint,
|
|
7
6
|
missingTargetError,
|
|
8
7
|
normalizeAccountId,
|
|
9
8
|
PAIRING_APPROVED_MESSAGE,
|
|
10
9
|
setAccountEnabledInConfigSection,
|
|
11
|
-
type ChannelPlugin,
|
|
12
10
|
type OpenClawConfig,
|
|
13
11
|
type ChannelSetupInput,
|
|
14
|
-
|
|
12
|
+
type ReplyPayload,
|
|
13
|
+
} from "./openclaw-compat.js";
|
|
15
14
|
|
|
16
15
|
import { resolveGeweAccount, resolveDefaultGeweAccountId, listGeweAccountIds } from "./accounts.js";
|
|
16
|
+
import { geweMessageActions } from "./channel-actions.js";
|
|
17
|
+
import { geweAllowlist } from "./channel-allowlist.js";
|
|
18
|
+
import { geweDirectory } from "./channel-directory.js";
|
|
19
|
+
import { geweStatus } from "./channel-status.js";
|
|
17
20
|
import { GeweConfigSchema } from "./config-schema.js";
|
|
18
21
|
import {
|
|
19
22
|
CHANNEL_ALIASES,
|
|
@@ -29,8 +32,10 @@ import { looksLikeGeweTargetId, normalizeGeweMessagingTarget } from "./normalize
|
|
|
29
32
|
import { resolveGeweGroupToolPolicy, resolveGeweRequireMention } from "./policy.js";
|
|
30
33
|
import { getGeweRuntime } from "./runtime.js";
|
|
31
34
|
import { sendTextGewe } from "./send.js";
|
|
35
|
+
import { geweSetupWizard } from "./setup-wizard.js";
|
|
36
|
+
import type { GeweChannelPlugin } from "./setup-wizard-types.js";
|
|
37
|
+
import { normalizeGeweBindingConversationId } from "./group-binding.js";
|
|
32
38
|
import type { CoreConfig, ResolvedGeweAccount } from "./types.js";
|
|
33
|
-
import { geweOnboarding } from "./onboarding.js";
|
|
34
39
|
|
|
35
40
|
const meta = {
|
|
36
41
|
id: CHANNEL_ID,
|
|
@@ -53,25 +58,119 @@ type GeweSetupInput = ChannelSetupInput & {
|
|
|
53
58
|
apiBaseUrl?: string;
|
|
54
59
|
};
|
|
55
60
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
61
|
+
const GEWE_QUOTE_PARTIAL_DIRECTIVE_RE = /(?:\r?\n)?\s*\[\[GEWE_QUOTE_PARTIAL:([\s\S]*?)\]\]\s*$/;
|
|
62
|
+
|
|
63
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
64
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return value as Record<string, unknown>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function withGeweScopedChannelData(
|
|
71
|
+
payload: ReplyPayload,
|
|
72
|
+
updater: (scoped: Record<string, unknown>) => Record<string, unknown>,
|
|
73
|
+
): ReplyPayload {
|
|
74
|
+
const channelData = asRecord(payload.channelData) ?? {};
|
|
75
|
+
const existingChannelScoped = asRecord(channelData[CHANNEL_ID]);
|
|
76
|
+
const existingLegacyScoped = asRecord(channelData.gewe);
|
|
77
|
+
const targetKey = existingChannelScoped ? CHANNEL_ID : existingLegacyScoped ? "gewe" : CHANNEL_ID;
|
|
78
|
+
const scoped = existingChannelScoped ?? existingLegacyScoped ?? {};
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
...payload,
|
|
82
|
+
channelData: {
|
|
83
|
+
...channelData,
|
|
84
|
+
[targetKey]: updater(scoped),
|
|
73
85
|
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function parseGeweQuotePartialDirective(text: string | undefined): {
|
|
90
|
+
cleanedText: string;
|
|
91
|
+
partialText: string;
|
|
92
|
+
} | null {
|
|
93
|
+
if (typeof text !== "string") return null;
|
|
94
|
+
const match = GEWE_QUOTE_PARTIAL_DIRECTIVE_RE.exec(text);
|
|
95
|
+
if (!match) return null;
|
|
96
|
+
const partialText = match[1]?.trim();
|
|
97
|
+
if (!partialText) return null;
|
|
98
|
+
const cleanedText = text.slice(0, match.index).replace(/\s+$/, "");
|
|
99
|
+
return {
|
|
100
|
+
cleanedText,
|
|
101
|
+
partialText,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function resolvePayloadMediaEntries(payload: ReplyPayload): string[] {
|
|
106
|
+
const mediaUrls = payload.mediaUrls?.map((entry) => entry?.trim()).filter(Boolean);
|
|
107
|
+
if (mediaUrls?.length) {
|
|
108
|
+
return mediaUrls;
|
|
109
|
+
}
|
|
110
|
+
const mediaUrl = payload.mediaUrl?.trim();
|
|
111
|
+
return mediaUrl ? [mediaUrl] : [];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function normalizeGeweOutboundPayload(payload: ReplyPayload): ReplyPayload {
|
|
115
|
+
let normalized = payload;
|
|
116
|
+
|
|
117
|
+
const partialDirective = parseGeweQuotePartialDirective(payload.text);
|
|
118
|
+
if (partialDirective) {
|
|
119
|
+
normalized = {
|
|
120
|
+
...normalized,
|
|
121
|
+
text: partialDirective.cleanedText,
|
|
122
|
+
};
|
|
123
|
+
normalized = withGeweScopedChannelData(normalized, (scoped) => {
|
|
124
|
+
const quoteReply = asRecord(scoped.quoteReply);
|
|
125
|
+
const existingPartialText = asRecord(quoteReply?.partialText);
|
|
126
|
+
if (existingPartialText?.text) {
|
|
127
|
+
return scoped;
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
...scoped,
|
|
131
|
+
quoteReply: {
|
|
132
|
+
...(quoteReply ?? {}),
|
|
133
|
+
partialText: {
|
|
134
|
+
...existingPartialText,
|
|
135
|
+
text: partialDirective.partialText,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (normalized.audioAsVoice !== true || resolvePayloadMediaEntries(normalized).length !== 1) {
|
|
143
|
+
return normalized;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return withGeweScopedChannelData(normalized, (scoped) => ({
|
|
147
|
+
...scoped,
|
|
148
|
+
audioAsVoice: true,
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const gewePairing = {
|
|
153
|
+
idLabel: "wechatUserId",
|
|
154
|
+
mode: "code" as const,
|
|
155
|
+
normalizeAllowEntry: (entry: string) => stripChannelPrefix(entry),
|
|
156
|
+
notifyApproval: async ({ cfg, id }: { cfg: OpenClawConfig; id: string }) => {
|
|
157
|
+
const account = resolveGeweAccount({ cfg: cfg as CoreConfig });
|
|
158
|
+
if (!account.token || !account.appId) {
|
|
159
|
+
throw new Error("GeWe token/appId not configured");
|
|
160
|
+
}
|
|
161
|
+
await sendTextGewe({
|
|
162
|
+
account,
|
|
163
|
+
toWxid: id,
|
|
164
|
+
content: PAIRING_APPROVED_MESSAGE,
|
|
165
|
+
});
|
|
74
166
|
},
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export const gewePlugin: GeweChannelPlugin<ResolvedGeweAccount> = {
|
|
170
|
+
id: CHANNEL_ID,
|
|
171
|
+
meta,
|
|
172
|
+
setupWizard: geweSetupWizard,
|
|
173
|
+
pairing: gewePairing as GeweChannelPlugin<ResolvedGeweAccount>["pairing"],
|
|
75
174
|
capabilities: {
|
|
76
175
|
chatTypes: ["direct", "group"],
|
|
77
176
|
reactions: false,
|
|
@@ -120,6 +219,7 @@ export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
|
|
|
120
219
|
.filter(Boolean)
|
|
121
220
|
.map((entry) => stripChannelPrefix(entry)),
|
|
122
221
|
},
|
|
222
|
+
allowlist: geweAllowlist,
|
|
123
223
|
security: {
|
|
124
224
|
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
|
125
225
|
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
|
|
@@ -134,7 +234,7 @@ export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
|
|
|
134
234
|
allowFrom: account.config.allowFrom ?? [],
|
|
135
235
|
policyPath: `${basePath}dmPolicy`,
|
|
136
236
|
allowFromPath: basePath,
|
|
137
|
-
approveHint:
|
|
237
|
+
approveHint: "Issue a pair code with: openclaw pairing code create gewe-openclaw",
|
|
138
238
|
normalizeEntry: (raw) => stripChannelPrefix(raw),
|
|
139
239
|
};
|
|
140
240
|
},
|
|
@@ -146,14 +246,34 @@ export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
|
|
|
146
246
|
account.config.groups && Object.keys(account.config.groups).length > 0;
|
|
147
247
|
if (groupAllowlistConfigured) {
|
|
148
248
|
return [
|
|
149
|
-
`- GeWe groups: groupPolicy="open" allows any member in allowed groups to trigger (
|
|
249
|
+
`- GeWe groups: groupPolicy="open" allows any member in allowed groups to trigger (at-gated by default). Set channels.${CHANNEL_CONFIG_KEY}.groupPolicy="allowlist" + channels.${CHANNEL_CONFIG_KEY}.groupAllowFrom to restrict senders.`,
|
|
150
250
|
];
|
|
151
251
|
}
|
|
152
252
|
return [
|
|
153
|
-
`- GeWe groups: groupPolicy="open" with no channels.${CHANNEL_CONFIG_KEY}.groups allowlist; any group can add +
|
|
253
|
+
`- GeWe groups: groupPolicy="open" with no channels.${CHANNEL_CONFIG_KEY}.groups allowlist; any group can add + at (at-gated by default). Set channels.${CHANNEL_CONFIG_KEY}.groupPolicy="allowlist" + channels.${CHANNEL_CONFIG_KEY}.groupAllowFrom or configure channels.${CHANNEL_CONFIG_KEY}.groups.`,
|
|
154
254
|
];
|
|
155
255
|
},
|
|
156
256
|
},
|
|
257
|
+
bindings: {
|
|
258
|
+
compileConfiguredBinding: ({ conversationId }) => {
|
|
259
|
+
const normalized = normalizeGeweBindingConversationId(conversationId);
|
|
260
|
+
return normalized
|
|
261
|
+
? {
|
|
262
|
+
conversationId: normalized,
|
|
263
|
+
}
|
|
264
|
+
: null;
|
|
265
|
+
},
|
|
266
|
+
matchInboundConversation: ({ compiledBinding, conversationId }) => {
|
|
267
|
+
const normalized = normalizeGeweBindingConversationId(conversationId);
|
|
268
|
+
if (!normalized || normalized !== compiledBinding.conversationId) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
conversationId: normalized,
|
|
273
|
+
matchPriority: 2,
|
|
274
|
+
};
|
|
275
|
+
},
|
|
276
|
+
},
|
|
157
277
|
groups: {
|
|
158
278
|
resolveRequireMention: ({ cfg, groupId, accountId }) => {
|
|
159
279
|
const account = resolveGeweAccount({ cfg: cfg as CoreConfig, accountId });
|
|
@@ -168,19 +288,23 @@ export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
|
|
|
168
288
|
resolveToolPolicy: resolveGeweGroupToolPolicy,
|
|
169
289
|
},
|
|
170
290
|
messaging: {
|
|
171
|
-
normalizeTarget: normalizeGeweMessagingTarget,
|
|
291
|
+
normalizeTarget: (raw) => normalizeGeweMessagingTarget(raw) ?? undefined,
|
|
172
292
|
targetResolver: {
|
|
173
293
|
looksLikeId: looksLikeGeweTargetId,
|
|
174
294
|
hint: "<wxid|@chatroom>",
|
|
175
295
|
},
|
|
176
296
|
},
|
|
297
|
+
directory: geweDirectory,
|
|
298
|
+
actions: geweMessageActions,
|
|
177
299
|
outbound: {
|
|
178
300
|
deliveryMode: "direct",
|
|
301
|
+
normalizePayload: ({ payload }) => normalizeGeweOutboundPayload(payload),
|
|
179
302
|
chunker: (text, limit) => {
|
|
180
303
|
const core = getGeweRuntime();
|
|
181
304
|
return core.channel.text.chunkMarkdownText(text, limit);
|
|
182
305
|
},
|
|
183
306
|
chunkerMode: "markdown",
|
|
307
|
+
textChunkLimit: 4000,
|
|
184
308
|
resolveTarget: ({ to, allowFrom, mode }) => {
|
|
185
309
|
const trimmed = to?.trim() ?? "";
|
|
186
310
|
const allowListRaw = (allowFrom ?? []).map((entry) => String(entry).trim()).filter(Boolean);
|
|
@@ -271,36 +395,7 @@ export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
|
|
|
271
395
|
lastStopAt: null,
|
|
272
396
|
lastError: null,
|
|
273
397
|
},
|
|
274
|
-
|
|
275
|
-
configured: snapshot.configured ?? false,
|
|
276
|
-
tokenSource: snapshot.tokenSource ?? "none",
|
|
277
|
-
running: snapshot.running ?? false,
|
|
278
|
-
mode: snapshot.mode ?? null,
|
|
279
|
-
lastStartAt: snapshot.lastStartAt ?? null,
|
|
280
|
-
lastStopAt: snapshot.lastStopAt ?? null,
|
|
281
|
-
lastError: snapshot.lastError ?? null,
|
|
282
|
-
lastInboundAt: snapshot.lastInboundAt ?? null,
|
|
283
|
-
lastOutboundAt: snapshot.lastOutboundAt ?? null,
|
|
284
|
-
}),
|
|
285
|
-
buildAccountSnapshot: ({ account, runtime }) => {
|
|
286
|
-
const configured = Boolean(account.token?.trim() && account.appId?.trim());
|
|
287
|
-
return {
|
|
288
|
-
accountId: account.accountId,
|
|
289
|
-
name: account.name,
|
|
290
|
-
enabled: account.enabled,
|
|
291
|
-
configured,
|
|
292
|
-
tokenSource: account.tokenSource,
|
|
293
|
-
baseUrl: account.config.apiBaseUrl ? "[set]" : "[missing]",
|
|
294
|
-
running: runtime?.running ?? false,
|
|
295
|
-
lastStartAt: runtime?.lastStartAt ?? null,
|
|
296
|
-
lastStopAt: runtime?.lastStopAt ?? null,
|
|
297
|
-
lastError: runtime?.lastError ?? null,
|
|
298
|
-
mode: "webhook",
|
|
299
|
-
lastInboundAt: runtime?.lastInboundAt ?? null,
|
|
300
|
-
lastOutboundAt: runtime?.lastOutboundAt ?? null,
|
|
301
|
-
dmPolicy: account.config.dmPolicy ?? "pairing",
|
|
302
|
-
};
|
|
303
|
-
},
|
|
398
|
+
...geweStatus,
|
|
304
399
|
},
|
|
305
400
|
gateway: {
|
|
306
401
|
startAccount: async (ctx) => {
|
|
@@ -355,7 +450,7 @@ export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
|
|
|
355
450
|
|
|
356
451
|
const accounts =
|
|
357
452
|
nextSection.accounts && typeof nextSection.accounts === "object"
|
|
358
|
-
? { ...nextSection.accounts }
|
|
453
|
+
? ({ ...nextSection.accounts } as Record<string, Record<string, unknown>>)
|
|
359
454
|
: undefined;
|
|
360
455
|
if (accounts && accountId in accounts) {
|
|
361
456
|
const entry = accounts[accountId];
|
|
@@ -436,10 +531,12 @@ export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
|
|
|
436
531
|
const section = (namedConfig.channels?.[CHANNEL_CONFIG_KEY] ?? {}) as Record<
|
|
437
532
|
string,
|
|
438
533
|
unknown
|
|
439
|
-
|
|
534
|
+
> & {
|
|
535
|
+
accounts?: Record<string, Record<string, unknown>>;
|
|
536
|
+
};
|
|
440
537
|
const useAccountPath = accountId !== DEFAULT_ACCOUNT_ID;
|
|
441
538
|
const base = useAccountPath
|
|
442
|
-
?
|
|
539
|
+
? section.accounts?.[accountId] ?? {}
|
|
443
540
|
: section;
|
|
444
541
|
const nextEntry = {
|
|
445
542
|
...base,
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { CHANNEL_CONFIG_KEY } from "./constants.js";
|
|
2
|
+
import { DEFAULT_ACCOUNT_ID, normalizeAccountId, type OpenClawConfig } from "./openclaw-compat.js";
|
|
3
|
+
|
|
4
|
+
type ConfigWriteTarget =
|
|
5
|
+
| { kind: "channel"; scope: { channelId: string } }
|
|
6
|
+
| { kind: "account"; scope: { channelId: string; accountId: string } };
|
|
7
|
+
|
|
8
|
+
type GeweWriteSection = {
|
|
9
|
+
nextCfg: OpenClawConfig;
|
|
10
|
+
channelSection: Record<string, unknown>;
|
|
11
|
+
target: Record<string, unknown>;
|
|
12
|
+
pathPrefix: string;
|
|
13
|
+
writeTarget: ConfigWriteTarget;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
17
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
return value as Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function cloneOpenClawConfig<T>(value: T): T {
|
|
24
|
+
return structuredClone(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function ensureGeweWriteSectionOnConfig(params: {
|
|
28
|
+
cfg: OpenClawConfig;
|
|
29
|
+
accountId?: string | null;
|
|
30
|
+
}): GeweWriteSection {
|
|
31
|
+
const nextCfg = params.cfg;
|
|
32
|
+
const channels = ((nextCfg.channels ??= {}) as Record<string, unknown>);
|
|
33
|
+
const channelSection = ((asRecord(channels[CHANNEL_CONFIG_KEY]) ?? {}) as Record<string, unknown>);
|
|
34
|
+
channels[CHANNEL_CONFIG_KEY] = channelSection;
|
|
35
|
+
|
|
36
|
+
const normalizedAccountId = normalizeAccountId(params.accountId);
|
|
37
|
+
const hasAccounts =
|
|
38
|
+
channelSection.accounts && typeof channelSection.accounts === "object" && !Array.isArray(channelSection.accounts);
|
|
39
|
+
const useAccount = normalizedAccountId !== DEFAULT_ACCOUNT_ID || Boolean(hasAccounts);
|
|
40
|
+
if (!useAccount) {
|
|
41
|
+
return {
|
|
42
|
+
nextCfg,
|
|
43
|
+
channelSection,
|
|
44
|
+
target: channelSection,
|
|
45
|
+
pathPrefix: `channels.${CHANNEL_CONFIG_KEY}`,
|
|
46
|
+
writeTarget: {
|
|
47
|
+
kind: "channel",
|
|
48
|
+
scope: { channelId: CHANNEL_CONFIG_KEY },
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const accounts = ((asRecord(channelSection.accounts) ?? {}) as Record<string, unknown>);
|
|
54
|
+
channelSection.accounts = accounts;
|
|
55
|
+
const accountSection = ((asRecord(accounts[normalizedAccountId]) ?? {}) as Record<string, unknown>);
|
|
56
|
+
accounts[normalizedAccountId] = accountSection;
|
|
57
|
+
return {
|
|
58
|
+
nextCfg,
|
|
59
|
+
channelSection,
|
|
60
|
+
target: accountSection,
|
|
61
|
+
pathPrefix: `channels.${CHANNEL_CONFIG_KEY}.accounts.${normalizedAccountId}`,
|
|
62
|
+
writeTarget: {
|
|
63
|
+
kind: "account",
|
|
64
|
+
scope: {
|
|
65
|
+
channelId: CHANNEL_CONFIG_KEY,
|
|
66
|
+
accountId: normalizedAccountId,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function ensureGeweWriteSection(params: {
|
|
73
|
+
cfg: OpenClawConfig;
|
|
74
|
+
accountId?: string | null;
|
|
75
|
+
}) {
|
|
76
|
+
return ensureGeweWriteSectionOnConfig({
|
|
77
|
+
cfg: cloneOpenClawConfig(params.cfg),
|
|
78
|
+
accountId: params.accountId,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function ensureGeweWriteSectionInPlace(params: {
|
|
83
|
+
cfg: OpenClawConfig;
|
|
84
|
+
accountId?: string | null;
|
|
85
|
+
}) {
|
|
86
|
+
return ensureGeweWriteSectionOnConfig(params);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function cleanupEmptyObject(parent: Record<string, unknown>, key: string) {
|
|
90
|
+
const value = asRecord(parent[key]);
|
|
91
|
+
if (value && Object.keys(value).length === 0) {
|
|
92
|
+
delete parent[key];
|
|
93
|
+
}
|
|
94
|
+
}
|
package/src/config-schema.ts
CHANGED
|
@@ -6,20 +6,94 @@ import {
|
|
|
6
6
|
MarkdownConfigSchema,
|
|
7
7
|
ToolPolicySchema,
|
|
8
8
|
requireOpenAllowFrom,
|
|
9
|
-
} from "openclaw
|
|
9
|
+
} from "./openclaw-compat.js";
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
|
|
12
|
+
const GeweGroupTriggerSchema = z
|
|
13
|
+
.object({
|
|
14
|
+
mode: z.enum(["at", "quote", "at_or_quote", "any_message"]).optional(),
|
|
15
|
+
})
|
|
16
|
+
.strict();
|
|
17
|
+
|
|
18
|
+
const GeweDmTriggerSchema = z
|
|
19
|
+
.object({
|
|
20
|
+
mode: z.enum(["any_message", "quote"]).optional(),
|
|
21
|
+
})
|
|
22
|
+
.strict();
|
|
23
|
+
|
|
24
|
+
const GeweGroupReplySchema = z
|
|
25
|
+
.object({
|
|
26
|
+
mode: z.enum(["plain", "quote_source", "at_sender", "quote_and_at"]).optional(),
|
|
27
|
+
})
|
|
28
|
+
.strict();
|
|
29
|
+
|
|
30
|
+
const GeweDmReplySchema = z
|
|
31
|
+
.object({
|
|
32
|
+
mode: z.enum(["plain", "quote_source"]).optional(),
|
|
33
|
+
})
|
|
34
|
+
.strict();
|
|
35
|
+
|
|
36
|
+
const GeweBindingIdentitySelfSchema = z
|
|
37
|
+
.object({
|
|
38
|
+
source: z.enum(["agent_name", "agent_id", "literal"]).optional(),
|
|
39
|
+
value: z.string().optional(),
|
|
40
|
+
})
|
|
41
|
+
.strict()
|
|
42
|
+
.superRefine((value, ctx) => {
|
|
43
|
+
if (value.source === "literal" && !value.value?.trim()) {
|
|
44
|
+
ctx.addIssue({
|
|
45
|
+
code: z.ZodIssueCode.custom,
|
|
46
|
+
path: ["value"],
|
|
47
|
+
message: "value is required when source=literal",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const GeweBindingIdentityRemarkSchema = z
|
|
53
|
+
.object({
|
|
54
|
+
source: z.enum(["agent_id", "agent_name", "name_and_id", "literal"]).optional(),
|
|
55
|
+
value: z.string().optional(),
|
|
56
|
+
})
|
|
57
|
+
.strict()
|
|
58
|
+
.superRefine((value, ctx) => {
|
|
59
|
+
if (value.source === "literal" && !value.value?.trim()) {
|
|
60
|
+
ctx.addIssue({
|
|
61
|
+
code: z.ZodIssueCode.custom,
|
|
62
|
+
path: ["value"],
|
|
63
|
+
message: "value is required when source=literal",
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const GeweBindingIdentitySchema = z
|
|
69
|
+
.object({
|
|
70
|
+
enabled: z.boolean().optional(),
|
|
71
|
+
selfNickname: GeweBindingIdentitySelfSchema.optional(),
|
|
72
|
+
remark: GeweBindingIdentityRemarkSchema.optional(),
|
|
73
|
+
})
|
|
74
|
+
.strict();
|
|
75
|
+
|
|
12
76
|
export const GeweGroupSchema = z
|
|
13
77
|
.object({
|
|
14
78
|
requireMention: z.boolean().optional(),
|
|
15
|
-
tools: ToolPolicySchema,
|
|
79
|
+
tools: ToolPolicySchema.optional(),
|
|
16
80
|
skills: z.array(z.string()).optional(),
|
|
17
81
|
enabled: z.boolean().optional(),
|
|
18
82
|
allowFrom: z.array(z.string()).optional(),
|
|
19
83
|
systemPrompt: z.string().optional(),
|
|
84
|
+
trigger: GeweGroupTriggerSchema.optional(),
|
|
85
|
+
reply: GeweGroupReplySchema.optional(),
|
|
86
|
+
bindingIdentity: GeweBindingIdentitySchema.optional(),
|
|
20
87
|
})
|
|
21
88
|
.strict();
|
|
22
89
|
|
|
90
|
+
export const GeweDmSchema = DmConfigSchema.extend({
|
|
91
|
+
skills: z.array(z.string()).optional(),
|
|
92
|
+
systemPrompt: z.string().optional(),
|
|
93
|
+
trigger: GeweDmTriggerSchema.optional(),
|
|
94
|
+
reply: GeweDmReplySchema.optional(),
|
|
95
|
+
}).strict();
|
|
96
|
+
|
|
23
97
|
export const GeweAccountSchemaBase = z
|
|
24
98
|
.object({
|
|
25
99
|
name: z.string().optional(),
|
|
@@ -80,9 +154,10 @@ export const GeweAccountSchemaBase = z
|
|
|
80
154
|
groups: z.record(z.string(), GeweGroupSchema.optional()).optional(),
|
|
81
155
|
historyLimit: z.number().int().min(0).optional(),
|
|
82
156
|
dmHistoryLimit: z.number().int().min(0).optional(),
|
|
83
|
-
dms: z.record(z.string(),
|
|
157
|
+
dms: z.record(z.string(), GeweDmSchema.optional()).optional(),
|
|
84
158
|
textChunkLimit: z.number().int().positive().optional(),
|
|
85
159
|
chunkMode: z.enum(["length", "newline"]).optional(),
|
|
160
|
+
autoQuoteReply: z.boolean().optional(),
|
|
86
161
|
blockStreaming: z.boolean().optional(),
|
|
87
162
|
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
|
88
163
|
})
|