feat: Python Plugins

This commit is contained in:
Ian Chua
2026-07-02 17:49:36 +08:00
parent 395e070a0e
commit ecddf3d18f
183 changed files with 49955 additions and 2120 deletions

View File

@@ -0,0 +1,222 @@
#pragma once
#include "PythonPluginInterface.hpp"
#include <algorithm>
#include <cctype>
#include <optional>
#include <string>
#include <utility>
#include <vector>
namespace Slic3r {
// Cloud overlay on a PluginDescriptor; presence means the descriptor is cloud-backed.
struct CloudPluginState
{
std::string uuid; // Cloud service UUID without the cloud: key prefix.
bool installed = false; // Cloud package exists locally and can be loaded.
bool update_available = false; // Cloud version > the local package version.
bool unauthorized = false; // Cloud plugin is valid locally, but cannot receive cloud updates.
bool is_mine = false; // Plugin was created (and uploaded) by the current user.
};
enum class PluginUpdateStatus
{
Normal,
UpdateAvailable,
Unauthorized,
};
struct PluginChangelog {
std::string changelog_id;
std::string plugin_uuid;
std::string version;
std::string changelog;
long long created_time = 0;
};
inline void sort_plugin_changelog(std::vector<PluginChangelog>& changelog)
{
std::sort(changelog.begin(), changelog.end(), [](const PluginChangelog& lhs, const PluginChangelog& rhs) {
if (lhs.created_time != rhs.created_time)
return lhs.created_time > rhs.created_time;
return lhs.version > rhs.version;
});
}
// Canonical plugin runtime/catalog representation used by Orca.
struct PluginDescriptor
{
std::string plugin_key; // OrcaSlicer-generated operational identity
std::string name; // Display name
std::string description; // Plugin description
std::string author; // Plugin author from manifest, if available
std::string version; // Selected plugin version
std::string latest_version; // Latest available cloud version fallback when changelog is unavailable.
std::string installed_version; // Locally installed package version. Preserved across cloud merges, which overwrite `version` with the latest cloud version. Empty when not installed.
std::vector<PluginCapabilityType> capability_types; // Capability types this package materializes (one package → N capabilities)
std::vector<std::string> display_types; // Display-only "compatibility" labels (cloud: raw service labels; local: from real capabilities). Never used for dispatch.
std::string plugin_root; // Installed plugin directory, even when entry_path is invalid or ambiguous.
std::string entry_path; // Full path to the installed plugin entry file
std::string entry_package; // Import package/module used for package-based loading
std::vector<std::string> dependencies; // Python dependency requirements declared by plugin package metadata
std::vector<PluginChangelog> changelog; // Cloud release changelog, sorted newest-first when available.
std::string error; // Blocking error message. Non-empty means the plugin is in an error state.
std::optional<CloudPluginState> cloud; // Extra cloud state layered on top of a normal plugin descriptor.
bool metadata_valid = false; // Manifest/package validity stays separate from the user-facing error field.
std::string sharing_token; // Use BASE_URL/p/SHARING_TOKEN to open relevant plugin in browser.
std::string thumbnail_url; // Cloud main_image pre-signed (access_url) thumbnail; empty for local plugins. Display-only.
bool is_cloud_plugin() const { return cloud.has_value(); }
std::string cloud_uuid() const { return cloud.has_value() ? cloud->uuid : std::string{}; }
bool has_local_package() const { return !is_cloud_plugin() || cloud->installed || !plugin_root.empty() || !entry_path.empty(); }
bool is_metadata_valid() const { return metadata_valid; }
// Capability-type helpers. A package may materialize several capability types;
// these accessors give callers that still reason about a single "type" (catalog
// display, cloud overlay, dispatch — finalized in later tasks) a stable view.
bool has_capability_type(PluginCapabilityType t) const
{
return std::find(capability_types.begin(), capability_types.end(), t) != capability_types.end();
}
PluginCapabilityType primary_capability_type() const
{
return capability_types.empty() ? PluginCapabilityType::Unknown : capability_types.front();
}
// Set the package to a single capability type (metadata/cloud sources currently
// declare one type; multi-type discovery is finalized in later tasks).
void set_capability_type(PluginCapabilityType t) { capability_types.assign(1, t); }
// Canonical label derived from the primary type for UI / config matching.
std::string type_label() const { return plugin_capability_type_to_string(primary_capability_type()); }
std::string normalized_error() const
{
auto begin = std::find_if_not(error.begin(), error.end(), [](unsigned char ch) { return std::isspace(ch) != 0; });
if (begin == error.end())
return {};
auto end = std::find_if_not(error.rbegin(), error.rend(), [](unsigned char ch) { return std::isspace(ch) != 0; }).base();
return std::string(begin, end);
}
bool has_error() const { return !normalized_error().empty(); }
PluginUpdateStatus get_update_status() const
{
if (!cloud.has_value())
return PluginUpdateStatus::Normal;
if (cloud->unauthorized)
return PluginUpdateStatus::Unauthorized;
if (cloud->update_available)
return PluginUpdateStatus::UpdateAvailable;
return PluginUpdateStatus::Normal;
}
bool has_update_available() const { return get_update_status() == PluginUpdateStatus::UpdateAvailable; }
bool is_unauthorized() const { return get_update_status() == PluginUpdateStatus::Unauthorized; }
std::string latest_available_version() const
{
for (const PluginChangelog& entry : changelog) {
if (!entry.version.empty())
return entry.version;
}
if (!latest_version.empty())
return latest_version;
return version;
}
void set_metadata_valid(bool is_valid)
{
metadata_valid = is_valid;
if (!is_valid && !has_error())
error = "Plugin metadata is invalid.";
}
void set_unauthorized(bool unauthorized)
{
if (!cloud.has_value())
return;
cloud->unauthorized = unauthorized;
if (unauthorized)
cloud->update_available = false;
}
void clear_error() { error.clear(); }
void set_error(std::string message) { error = std::move(message); }
};
inline void apply_plugin_metadata_fallbacks(PluginDescriptor& target, const PluginDescriptor& fallback)
{
if (target.name.empty())
target.name = fallback.name;
if (target.description.empty())
target.description = fallback.description;
if (target.author.empty())
target.author = fallback.author;
if (target.version.empty())
target.version = fallback.version;
if (target.capability_types.empty())
target.capability_types = fallback.capability_types;
if (target.entry_package.empty())
target.entry_package = fallback.entry_package;
if (target.dependencies.empty())
target.dependencies = fallback.dependencies;
}
// Sanitize a value for use as a filesystem name and as a local plugin_key:
// keeps [A-Za-z0-9_-.], collapses any other run into a single '_'.
inline std::string filesystem_safe_escape(const std::string& value)
{
std::string escaped;
escaped.reserve(value.size());
for (unsigned char ch : value) {
if (std::isalnum(ch) || ch == '_' || ch == '-' || ch == '.') {
escaped += static_cast<char>(ch);
continue;
}
if (escaped.empty() || escaped.back() != '_')
escaped += '_';
}
return escaped.empty() ? "path" : escaped;
}
// Plugin display names are serialized into ';'-delimited config/preset strings
// (see escape_strings_cstyle in libslic3r/Config.cpp), so a ';' in a name would
// corrupt that encoding. Replace any ';' with '_' and otherwise leave the name
// untouched. Capability names are validated more strictly elsewhere — a ';' there
// is treated as an error because those names drive preset dispatch, not display.
inline std::string sanitize_plugin_name(std::string name)
{
std::replace(name.begin(), name.end(), ';', '_');
return name;
}
// True when s is a canonical 8-4-4-12 hex UUID, e.g. 550e8400-e29b-41d4-a716-446655440000.
inline bool is_uuid(const std::string& s)
{
if (s.size() != 36)
return false;
for (size_t i = 0; i < s.size(); ++i) {
const char ch = s[i];
if (i == 8 || i == 13 || i == 18 || i == 23) {
if (ch != '-')
return false;
} else if (std::isxdigit(static_cast<unsigned char>(ch)) == 0) {
return false;
}
}
return true;
}
// Key generation helpers.
// Local key = filesystem-safe plugin file stem (filename without extension), e.g. "Slow_Load_Plugin".
// Cloud key = the cloud UUID. Local vs cloud is determined from the descriptor's cloud
// state, never by parsing the key. Plugin keys are matched by plain equality.
inline std::string make_local_plugin_key(const std::string& stem)
{
return filesystem_safe_escape(stem);
}
} // namespace Slic3r