first commit

This commit is contained in:
douboer
2026-03-21 18:57:10 +08:00
commit c49aa1a5e9
570 changed files with 107167 additions and 0 deletions

View File

@@ -0,0 +1,196 @@
/* global Page, wx, require, getCurrentPages, console */
const pluginRuntime = require("../../utils/pluginRuntime");
const { onSessionEvent, getSessionState } = require("../../utils/sessionBus");
const { getSettings } = require("../../utils/storage");
const { buildThemeStyle, applyNavigationBarTheme } = require("../../utils/themeStyle");
const { buildPageCopy, formatTemplate, getRuntimeStateLabel, normalizeUiLanguage } = require("../../utils/i18n");
/**
* 插件页:
* 1. 对齐 Web 的“插件运行时管理”能力;
* 2. 支持启用/禁用/重载/移除、JSON 导入导出、命令执行与运行日志。
*/
Page({
data: {
themeStyle: "",
canGoBack: false,
pluginJson: "",
records: [],
commands: [],
runtimeLogs: [],
sessionState: "disconnected",
sessionStateLabel: "Disconnected",
copy: buildPageCopy("zh-Hans", "plugins")
},
async onShow() {
const pages = getCurrentPages();
const settings = getSettings();
const language = normalizeUiLanguage(settings.uiLanguage);
const copy = buildPageCopy(language, "plugins");
applyNavigationBarTheme(settings);
wx.setNavigationBarTitle({ title: copy.navTitle || "插件" });
this.setData({
canGoBack: pages.length > 1,
sessionState: getSessionState(),
sessionStateLabel: getRuntimeStateLabel(language, getSessionState()),
copy,
themeStyle: buildThemeStyle(settings)
});
if (!Array.isArray(this.sessionUnsubs) || this.sessionUnsubs.length === 0) {
this.sessionUnsubs = [
onSessionEvent("connected", () => {
const nextLanguage = normalizeUiLanguage(getSettings().uiLanguage);
this.setData(
{
sessionState: "connected",
sessionStateLabel: getRuntimeStateLabel(nextLanguage, "connected")
},
() => this.reloadRuntime()
);
}),
onSessionEvent("disconnected", () => {
const nextLanguage = normalizeUiLanguage(getSettings().uiLanguage);
this.setData(
{
sessionState: "disconnected",
sessionStateLabel: getRuntimeStateLabel(nextLanguage, "disconnected")
},
() => this.reloadRuntime()
);
})
];
}
await this.reloadRuntime();
},
onUnload() {
if (Array.isArray(this.sessionUnsubs)) {
this.sessionUnsubs.forEach((off) => {
try {
off();
} catch (error) {
console.warn("[plugins.sessionUnsubs]", error);
}
});
}
this.sessionUnsubs = null;
},
async reloadRuntime() {
try {
await pluginRuntime.ensureBootstrapped();
const records = pluginRuntime.listRecords();
const commands = pluginRuntime.listCommands(this.data.sessionState);
const runtimeLogs = pluginRuntime.listRuntimeLogs();
this.setData({ records, commands, runtimeLogs });
} catch (error) {
wx.showToast({ title: (error && error.message) || this.data.copy?.toast?.bootstrapFailed || "插件初始化失败", icon: "none" });
}
},
goBack() {
if (!this.data.canGoBack) return;
wx.navigateBack({ delta: 1 });
},
onPluginJsonInput(event) {
this.setData({ pluginJson: event.detail.value || "" });
},
async onImportJson() {
if (!String(this.data.pluginJson || "").trim()) {
wx.showToast({ title: this.data.copy?.toast?.pastePluginJsonFirst || "请先粘贴插件 JSON", icon: "none" });
return;
}
try {
await pluginRuntime.importJson(this.data.pluginJson);
this.setData({ pluginJson: "" });
await this.reloadRuntime();
wx.showToast({ title: this.data.copy?.toast?.importSuccess || "导入成功", icon: "success" });
} catch (error) {
wx.showToast({ title: (error && error.message) || this.data.copy?.toast?.importFailed || "导入失败", icon: "none" });
}
},
async onExportJson() {
try {
const raw = await pluginRuntime.exportJson();
wx.setClipboardData({
data: raw,
success: () => {
wx.showToast({ title: this.data.copy?.toast?.exportSuccess || "插件 JSON 已复制", icon: "success" });
}
});
} catch (error) {
wx.showToast({ title: (error && error.message) || this.data.copy?.toast?.exportFailed || "导出失败", icon: "none" });
}
},
async onEnable(event) {
const id = String(event.currentTarget.dataset.id || "");
if (!id) return;
try {
await pluginRuntime.enable(id);
await this.reloadRuntime();
wx.showToast({ title: this.data.copy?.toast?.enabled || "已启用", icon: "success" });
} catch (error) {
wx.showToast({ title: (error && error.message) || this.data.copy?.toast?.enableFailed || "启用失败", icon: "none" });
}
},
async onDisable(event) {
const id = String(event.currentTarget.dataset.id || "");
if (!id) return;
try {
await pluginRuntime.disable(id);
await this.reloadRuntime();
wx.showToast({ title: this.data.copy?.toast?.disabled || "已禁用", icon: "success" });
} catch (error) {
wx.showToast({ title: (error && error.message) || this.data.copy?.toast?.disableFailed || "禁用失败", icon: "none" });
}
},
async onReload(event) {
const id = String(event.currentTarget.dataset.id || "");
if (!id) return;
try {
await pluginRuntime.reload(id);
await this.reloadRuntime();
wx.showToast({ title: this.data.copy?.toast?.reloaded || "已重载", icon: "success" });
} catch (error) {
wx.showToast({ title: (error && error.message) || this.data.copy?.toast?.reloadFailed || "重载失败", icon: "none" });
}
},
async onRemove(event) {
const id = String(event.currentTarget.dataset.id || "");
if (!id) return;
wx.showModal({
title: this.data.copy?.modal?.removeTitle || "移除插件",
content: formatTemplate(this.data.copy?.modal?.removeContent, { id }),
success: async (res) => {
if (!res.confirm) return;
try {
await pluginRuntime.remove(id);
await this.reloadRuntime();
wx.showToast({ title: this.data.copy?.toast?.removed || "已移除", icon: "success" });
} catch (error) {
wx.showToast({ title: (error && error.message) || this.data.copy?.toast?.removeFailed || "移除失败", icon: "none" });
}
}
});
},
async onRunCommand(event) {
const commandId = String(event.currentTarget.dataset.commandId || "");
if (!commandId) return;
try {
await pluginRuntime.runCommand(commandId);
wx.showToast({ title: this.data.copy?.toast?.commandExecuted || "命令已执行", icon: "success" });
} catch (error) {
wx.showToast({ title: (error && error.message) || this.data.copy?.toast?.commandExecuteFailed || "命令执行失败", icon: "none" });
}
}
});

View File

@@ -0,0 +1,7 @@
{
"navigationBarTitleText": "插件",
"disableScroll": true,
"usingComponents": {
"bottom-nav": "/components/bottom-nav/index"
}
}

View File

@@ -0,0 +1,72 @@
<view class="page-root plugins-page" style="{{themeStyle}}">
<view class="page-content plugins-content">
<view class="surface-panel plugins-panel">
<view class="card plugin-summary">
<text>{{copy.runtimeStatePrefix}}{{sessionStateLabel}}</text>
<text class="muted">{{copy.summary}}</text>
</view>
<scroll-view class="surface-scroll plugins-scroll" scroll-y="true">
<view class="list-stack plugins-sections">
<view class="card plugin-block">
<view class="item-title">{{copy.sections.pluginList}}</view>
<view wx:for="{{records}}" wx:key="id" class="plugin-record">
<view class="plugin-record-head">
<text class="plugin-record-title">{{item.id}} · {{item.status}}</text>
<text class="plugin-record-sub">errorCount: {{item.errorCount}}</text>
</view>
<text class="plugin-record-sub">{{item.lastError || '-'}}</text>
<view class="actions plugin-actions">
<button class="btn" data-id="{{item.id}}" bindtap="onEnable">{{copy.buttons.enable}}</button>
<button class="btn" data-id="{{item.id}}" bindtap="onDisable">{{copy.buttons.disable}}</button>
<button class="btn" data-id="{{item.id}}" bindtap="onReload">{{copy.buttons.reload}}</button>
<button class="btn danger" data-id="{{item.id}}" bindtap="onRemove">{{copy.buttons.remove}}</button>
</view>
</view>
<text wx:if="{{records.length === 0}}" class="empty">{{copy.empty.noPlugins}}</text>
</view>
<view class="card plugin-block">
<view class="item-title">{{copy.sections.importJson}}</view>
<textarea
class="textarea plugin-json-input"
value="{{pluginJson}}"
placeholder="{{copy.placeholder.pluginJson}}"
bindinput="onPluginJsonInput"
/>
<view class="actions">
<button class="btn" bindtap="onImportJson">{{copy.buttons.importJson}}</button>
<button class="btn" bindtap="onExportJson">{{copy.buttons.exportJson}}</button>
</view>
</view>
<view class="card plugin-block">
<view class="item-title">{{copy.sections.runCommand}}</view>
<view class="plugin-command-list">
<button
wx:for="{{commands}}"
wx:key="id"
class="btn plugin-command-btn"
data-command-id="{{item.id}}"
bindtap="onRunCommand"
>
{{item.title}}
</button>
</view>
<text wx:if="{{commands.length === 0}}" class="muted">{{copy.empty.noCommands}}</text>
</view>
<view class="card plugin-block">
<view class="item-title">{{copy.sections.runtimeLogs}}</view>
<view class="plugin-log-box">
<text wx:for="{{runtimeLogs}}" wx:key="index" class="plugin-log-line">{{item}}</text>
<text wx:if="{{runtimeLogs.length === 0}}" class="muted">{{copy.empty.noLogs}}</text>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
<bottom-nav page="plugins" />
</view>

View File

@@ -0,0 +1,95 @@
.plugins-content {
padding-top: 16rpx;
}
.plugins-panel {
padding: 0 0 16rpx;
}
.plugin-summary {
margin-bottom: 16rpx;
gap: 8rpx;
display: flex;
flex-direction: column;
}
.plugins-scroll {
flex: 1;
min-height: 0;
}
.plugins-sections {
min-height: 0;
}
.plugin-block {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.plugin-record {
border: 1rpx solid var(--surface-border);
border-radius: 16rpx;
background: var(--surface);
padding: 12rpx;
display: flex;
flex-direction: column;
gap: 8rpx;
}
.plugin-record-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8rpx;
}
.plugin-record-title {
font-size: 26rpx;
font-weight: 600;
}
.plugin-record-sub {
font-size: 22rpx;
color: var(--muted);
word-break: break-all;
}
.plugin-actions {
gap: 8rpx;
}
.plugin-json-input {
min-height: 220rpx;
width: 100%;
}
.plugin-command-list {
display: flex;
flex-wrap: wrap;
gap: 8rpx;
}
.plugin-command-btn {
flex: 0 0 auto;
}
.plugin-log-box {
border: 1rpx solid var(--surface-border);
border-radius: 16rpx;
background: var(--surface);
padding: 12rpx;
max-height: 320rpx;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 6rpx;
}
.plugin-log-line {
color: var(--text);
font-size: 22rpx;
line-height: 1.4;
word-break: break-all;
}