




















@@ -2,6 +2,8 @@ import fs from "node:fs";
22import path from "node:path";
33import type { OpenClawConfig } from "../config/types.openclaw.js";
44import { tryReadJsonSync } from "../infra/json-files.js";
5+import { normalizeOptionalString } from "../shared/string-coerce.js";
6+import { normalizeOptionalTrimmedStringList } from "../shared/string-normalization.js";
57import type { PluginCandidate } from "./discovery.js";
68import { hashJson } from "./installed-plugin-index-hash.js";
79import type { InstalledPluginIndex, InstalledPluginIndexRecord } from "./installed-plugin-index.js";
@@ -13,6 +15,7 @@ import {
1315getPackageManifestMetadata,
1416type OpenClawPackageManifest,
1517type PackageManifest,
18+type PluginPackageChannel,
1619} from "./manifest.js";
1720import { isPathInsideWithRealpath, safeRealpathSync } from "./path-safety.js";
1821import { tracePluginLifecyclePhase } from "./plugin-lifecycle-trace.js";
@@ -123,14 +126,248 @@ function resolveFallbackPluginSource(record: InstalledPluginIndexRecord): string
123126return path.join(rootDir, DEFAULT_PLUGIN_ENTRY_CANDIDATES[0]);
124127}
125128129+function isRecord(value: unknown): value is Record<string, unknown> {
130+return typeof value === "object" && value !== null && !Array.isArray(value);
131+}
132+133+function normalizePackageChannelCommands(
134+commands: unknown,
135+): PluginPackageChannel["commands"] | undefined {
136+if (!isRecord(commands)) {
137+return undefined;
138+}
139+const nativeCommandsAutoEnabled =
140+typeof commands.nativeCommandsAutoEnabled === "boolean"
141+ ? commands.nativeCommandsAutoEnabled
142+ : undefined;
143+const nativeSkillsAutoEnabled =
144+typeof commands.nativeSkillsAutoEnabled === "boolean"
145+ ? commands.nativeSkillsAutoEnabled
146+ : undefined;
147+return nativeCommandsAutoEnabled !== undefined || nativeSkillsAutoEnabled !== undefined
148+ ? {
149+ ...(nativeCommandsAutoEnabled !== undefined ? { nativeCommandsAutoEnabled } : {}),
150+ ...(nativeSkillsAutoEnabled !== undefined ? { nativeSkillsAutoEnabled } : {}),
151+}
152+ : undefined;
153+}
154+155+function normalizePackageChannelExposure(
156+exposure: unknown,
157+): PluginPackageChannel["exposure"] | undefined {
158+if (!isRecord(exposure)) {
159+return undefined;
160+}
161+const configured = typeof exposure.configured === "boolean" ? exposure.configured : undefined;
162+const setup = typeof exposure.setup === "boolean" ? exposure.setup : undefined;
163+const docs = typeof exposure.docs === "boolean" ? exposure.docs : undefined;
164+return configured !== undefined || setup !== undefined || docs !== undefined
165+ ? {
166+ ...(configured !== undefined ? { configured } : {}),
167+ ...(setup !== undefined ? { setup } : {}),
168+ ...(docs !== undefined ? { docs } : {}),
169+}
170+ : undefined;
171+}
172+173+function normalizePackageChannelConfiguredState(
174+configuredState: unknown,
175+): PluginPackageChannel["configuredState"] | undefined {
176+if (!isRecord(configuredState)) {
177+return undefined;
178+}
179+const env = isRecord(configuredState.env)
180+ ? {
181+ ...(normalizeOptionalTrimmedStringList(configuredState.env.allOf)?.length
182+ ? { allOf: normalizeOptionalTrimmedStringList(configuredState.env.allOf) }
183+ : {}),
184+ ...(normalizeOptionalTrimmedStringList(configuredState.env.anyOf)?.length
185+ ? { anyOf: normalizeOptionalTrimmedStringList(configuredState.env.anyOf) }
186+ : {}),
187+}
188+ : undefined;
189+const specifier = normalizeOptionalString(configuredState.specifier);
190+const exportName = normalizeOptionalString(configuredState.exportName);
191+return specifier || exportName || (env && Object.keys(env).length > 0)
192+ ? {
193+ ...(specifier ? { specifier } : {}),
194+ ...(exportName ? { exportName } : {}),
195+ ...(env && Object.keys(env).length > 0 ? { env } : {}),
196+}
197+ : undefined;
198+}
199+200+function normalizePackageChannelPersistedAuthState(
201+persistedAuthState: unknown,
202+): PluginPackageChannel["persistedAuthState"] | undefined {
203+if (!isRecord(persistedAuthState)) {
204+return undefined;
205+}
206+const specifier = normalizeOptionalString(persistedAuthState.specifier);
207+const exportName = normalizeOptionalString(persistedAuthState.exportName);
208+return specifier || exportName
209+ ? {
210+ ...(specifier ? { specifier } : {}),
211+ ...(exportName ? { exportName } : {}),
212+}
213+ : undefined;
214+}
215+216+function normalizePackageChannelDoctorCapabilities(
217+doctorCapabilities: unknown,
218+): PluginPackageChannel["doctorCapabilities"] | undefined {
219+if (!isRecord(doctorCapabilities)) {
220+return undefined;
221+}
222+const dmAllowFromMode =
223+doctorCapabilities.dmAllowFromMode === "topOnly" ||
224+doctorCapabilities.dmAllowFromMode === "topOrNested" ||
225+doctorCapabilities.dmAllowFromMode === "nestedOnly"
226+ ? doctorCapabilities.dmAllowFromMode
227+ : undefined;
228+const groupModel =
229+doctorCapabilities.groupModel === "sender" ||
230+doctorCapabilities.groupModel === "route" ||
231+doctorCapabilities.groupModel === "hybrid"
232+ ? doctorCapabilities.groupModel
233+ : undefined;
234+const groupAllowFromFallbackToAllowFrom =
235+typeof doctorCapabilities.groupAllowFromFallbackToAllowFrom === "boolean"
236+ ? doctorCapabilities.groupAllowFromFallbackToAllowFrom
237+ : undefined;
238+const warnOnEmptyGroupSenderAllowlist =
239+typeof doctorCapabilities.warnOnEmptyGroupSenderAllowlist === "boolean"
240+ ? doctorCapabilities.warnOnEmptyGroupSenderAllowlist
241+ : undefined;
242+return dmAllowFromMode ||
243+groupModel ||
244+groupAllowFromFallbackToAllowFrom !== undefined ||
245+warnOnEmptyGroupSenderAllowlist !== undefined
246+ ? {
247+ ...(dmAllowFromMode ? { dmAllowFromMode } : {}),
248+ ...(groupModel ? { groupModel } : {}),
249+ ...(groupAllowFromFallbackToAllowFrom !== undefined
250+ ? { groupAllowFromFallbackToAllowFrom }
251+ : {}),
252+ ...(warnOnEmptyGroupSenderAllowlist !== undefined
253+ ? { warnOnEmptyGroupSenderAllowlist }
254+ : {}),
255+}
256+ : undefined;
257+}
258+259+function normalizePackageChannelCliOptions(
260+cliAddOptions: unknown,
261+): PluginPackageChannel["cliAddOptions"] | undefined {
262+if (!Array.isArray(cliAddOptions)) {
263+return undefined;
264+}
265+const normalized = cliAddOptions.flatMap((option) => {
266+if (!isRecord(option)) {
267+return [];
268+}
269+const flags = normalizeOptionalString(option.flags);
270+const description = normalizeOptionalString(option.description);
271+if (!flags || !description) {
272+return [];
273+}
274+const defaultValue =
275+typeof option.defaultValue === "boolean" || typeof option.defaultValue === "string"
276+ ? option.defaultValue
277+ : undefined;
278+return [
279+{
280+ flags,
281+ description,
282+ ...(defaultValue !== undefined ? { defaultValue } : {}),
283+},
284+];
285+});
286+return normalized.length > 0 ? normalized : undefined;
287+}
288+289+function normalizePersistedPackageChannel(value: unknown): PluginPackageChannel | undefined {
290+if (!isRecord(value)) {
291+return undefined;
292+}
293+const id = normalizeOptionalString(value.id);
294+if (!id) {
295+return undefined;
296+}
297+const channel: PluginPackageChannel = { id };
298+for (const key of [
299+"label",
300+"selectionLabel",
301+"detailLabel",
302+"docsPath",
303+"docsLabel",
304+"blurb",
305+"systemImage",
306+"selectionDocsPrefix",
307+] as const) {
308+const normalized = normalizeOptionalString(value[key]);
309+if (normalized) {
310+channel[key] = normalized;
311+}
312+}
313+if (typeof value.order === "number" && Number.isFinite(value.order)) {
314+channel.order = value.order;
315+}
316+for (const key of ["aliases", "preferOver", "selectionExtras"] as const) {
317+const normalized = normalizeOptionalTrimmedStringList(value[key]);
318+if (normalized?.length) {
319+channel[key] = normalized;
320+}
321+}
322+for (const key of [
323+"selectionDocsOmitLabel",
324+"markdownCapable",
325+"showConfigured",
326+"showInSetup",
327+"quickstartAllowFrom",
328+"forceAccountBinding",
329+"preferSessionLookupForAnnounceTarget",
330+] as const) {
331+if (typeof value[key] === "boolean") {
332+channel[key] = value[key];
333+}
334+}
335+const exposure = normalizePackageChannelExposure(value.exposure);
336+if (exposure) {
337+channel.exposure = exposure;
338+}
339+const commands = normalizePackageChannelCommands(value.commands);
340+if (commands) {
341+channel.commands = commands;
342+}
343+const configuredState = normalizePackageChannelConfiguredState(value.configuredState);
344+if (configuredState) {
345+channel.configuredState = configuredState;
346+}
347+const persistedAuthState = normalizePackageChannelPersistedAuthState(value.persistedAuthState);
348+if (persistedAuthState) {
349+channel.persistedAuthState = persistedAuthState;
350+}
351+const doctorCapabilities = normalizePackageChannelDoctorCapabilities(value.doctorCapabilities);
352+if (doctorCapabilities) {
353+channel.doctorCapabilities = doctorCapabilities;
354+}
355+const cliAddOptions = normalizePackageChannelCliOptions(value.cliAddOptions);
356+if (cliAddOptions) {
357+channel.cliAddOptions = cliAddOptions;
358+}
359+return channel;
360+}
361+126362function resolveInstalledPackageMetadata(record: InstalledPluginIndexRecord): {
127363packageManifest?: OpenClawPackageManifest;
128364packageDependencies?: PluginDependencySpecMap;
129365packageOptionalDependencies?: PluginDependencySpecMap;
130366} {
131-const fallbackPackageManifest = record.packageChannel
367+const recordPackageChannel = normalizePersistedPackageChannel(record.packageChannel);
368+const fallbackPackageManifest = recordPackageChannel
132369 ? {
133-channel: record.packageChannel,
370+channel: recordPackageChannel,
134371}
135372 : undefined;
136373const packageJsonPath = record.packageJson?.path ? resolvePackageJsonPath(record) : undefined;
@@ -151,16 +388,18 @@ function resolveInstalledPackageMetadata(record: InstalledPluginIndexRecord): {
151388packageOptionalDependencies: dependencies.optionalDependencies,
152389};
153390}
391+const packageChannel = normalizePersistedPackageChannel(packageManifest.channel);
154392const channel =
155-record.packageChannel || packageManifest.channel
393+recordPackageChannel || packageChannel
156394 ? {
157- ...record.packageChannel,
158- ...packageManifest.channel,
395+ ...recordPackageChannel,
396+ ...packageChannel,
159397}
160398 : undefined;
399+const { channel: _ignoredChannel, ...packageManifestWithoutChannel } = packageManifest;
161400return {
162401packageManifest: {
163- ...packageManifest,
402+ ...packageManifestWithoutChannel,
164403 ...(channel ? { channel } : {}),
165404},
166405packageDependencies: dependencies.dependencies,
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。