如下图一轮对话鹈鹕骑车SVG动画,别说让它写了,改别人的都改不了
paste-20260924-184408.png

下面是免费版账户简单几轮就能完成的"回收站"插件,原本计划回来再开发或维护一下插件,现在不能了😭

paste-20260924-185145.png
paste-20260924-185234.png
paste-20260924-185305.png

我把这个免费版账户写的代码分享出来吧,谁有兴趣可以参考继续打磨打磨:

<?php
declare(strict_types=1);
if (!defined('APP_ROOT')) exit;

function recycle_bin_table(): string { return 'plugin_recycle_bin_items'; }
function recycle_bin_requests_table(): string { return 'plugin_recycle_bin_requests'; }

function recycle_bin_config(): array
{
    $raw = plugin_config('recycle_bin', []);
    return [
        'capture_topics' => (int)($raw['capture_topics'] ?? 1) === 1,
        'capture_replies' => (int)($raw['capture_replies'] ?? 1) === 1,
        'auto_purge' => (int)($raw['auto_purge'] ?? 0) === 1,
        'retention_days' => min(3650, max(1, (int)($raw['retention_days'] ?? 30))),
        'purge_batch_limit' => min(1000, max(1, (int)($raw['purge_batch_limit'] ?? 100))),
        'admin_tab' => (int)($raw['admin_tab'] ?? 1) === 1,
        'sidebar_link' => (int)($raw['sidebar_link'] ?? 0) === 1,
        'user_entry' => (int)($raw['user_entry'] ?? 1) === 1,
        'notify_cascade_authors' => (int)($raw['notify_cascade_authors'] ?? 1) === 1,
    ];
}

function recycle_bin_install(array $plugin): void
{
    $t = app_db_types();
    app_db_create_table(recycle_bin_table(),
        "id {$t['id']},batch_key {$t['key']} NOT NULL, item_type {$t['key']} NOT NULL, item_id {$t['uint']} NOT NULL, parent_id {$t['uint']} NOT NULL DEFAULT 0, owner_id {$t['uint']} NOT NULL DEFAULT 0, delete_source {$t['key']} NOT NULL DEFAULT 'user', delete_reason {$t['text']} NULL, payload {$t['text']} NOT NULL, deleted_by {$t['uint']} NOT NULL DEFAULT 0, deleted_at {$t['uint']} NOT NULL, status {$t['key']} NOT NULL DEFAULT 'archived', restored_at {$t['uint']} NOT NULL DEFAULT 0, UNIQUE(batch_key,item_type,item_id)"
    );
    app_db_ensure_columns(recycle_bin_table(), [
        'owner_id' => $t['uint'] . ' NOT NULL DEFAULT 0',
        'delete_source' => $t['key'] . " NOT NULL DEFAULT 'user'",
        'delete_reason' => $t['text'] . ' NULL',
        'restored_at' => $t['uint'] . ' NOT NULL DEFAULT 0',
    ]);
    app_db_create_index('plugin_recycle_bin_items_owner_status', recycle_bin_table() . '(owner_id,status,deleted_at)');
    app_db_create_index('plugin_recycle_bin_items_batch', recycle_bin_table() . '(batch_key,status)');
    app_db_create_index('plugin_recycle_bin_items_source', recycle_bin_table() . '(delete_source,status,deleted_at)');

    app_db_create_table(recycle_bin_requests_table(),
        "id {$t['id']},archive_id {$t['uint']} NOT NULL,user_id {$t['uint']} NOT NULL,item_type {$t['key']} NOT NULL,proposed_payload {$t['text']} NOT NULL,message {$t['text']} NULL,status {$t['key']} NOT NULL DEFAULT 'pending',created_at {$t['uint']} NOT NULL,reviewed_at {$t['uint']} NOT NULL DEFAULT 0,reviewer_id {$t['uint']} NOT NULL DEFAULT 0,review_note {$t['text']} NULL,UNIQUE(archive_id,status)"
    );
    app_db_create_index('plugin_recycle_bin_requests_user_status', recycle_bin_requests_table() . '(user_id,status,created_at)');
    app_db_create_index('plugin_recycle_bin_requests_status', recycle_bin_requests_table() . '(status,created_at)');

    // 兼容 v1.0.0:补齐 owner/source 后,把旧快照归属到原作者。
    $rows = q('SELECT id,payload,deleted_by FROM ' . recycle_bin_table() . ' WHERE owner_id=0 LIMIT 1000')->fetchAll();
    foreach ($rows as $row) {
        $payload = json_decode((string)($row['payload'] ?? ''), true);
        if (!is_array($payload)) continue;
        $owner_id = max(0, (int)($payload['user_id'] ?? 0));
        $deleted_by = max(0, (int)($row['deleted_by'] ?? 0));
        $source = ($owner_id > 0 && $deleted_by > 0 && $owner_id === $deleted_by) ? 'user' : 'admin';
        q('UPDATE ' . recycle_bin_table() . ' SET owner_id=?,delete_source=? WHERE id=?', [$owner_id, $source, (int)$row['id']]);
    }
}

function recycle_bin_uninstall(array $plugin, bool $keep_data = true): void
{
    if ($keep_data) return;
    app_db_drop_table(recycle_bin_requests_table());
    app_db_drop_table(recycle_bin_table());
}

function recycle_bin_batch_key(): string
{
    if (!isset($GLOBALS['__recycle_bin_batch_key'])) $GLOBALS['__recycle_bin_batch_key'] = bin2hex(random_bytes(16));
    return (string)$GLOBALS['__recycle_bin_batch_key'];
}

function recycle_bin_archive_row(string $type, array $row, bool $cascade = false): void
{
    $config = recycle_bin_config();
    if ($type === 'topics' && !$config['capture_topics']) return;
    if ($type === 'replies' && !$config['capture_replies']) return;
    if (!in_array($type, ['topics', 'replies'], true)) return;

    $id = (int)($row['id'] ?? 0);
    $owner_id = (int)($row['user_id'] ?? 0);
    if ($id < 1 || $owner_id < 1) return;

    try {
        $payload = json_encode($row, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
    } catch (Throwable) {
        throw new RuntimeException('回收站无法保存内容快照');
    }

    $deleted_by = max(0, uid());
    $source = ($deleted_by > 0 && $deleted_by === $owner_id) ? 'user' : 'admin';
    $reason = cut(trim((string)($_POST['recycle_bin_reason'] ?? '')), DB_TEXT_MAX_LENGTH);
    if ($source === 'admin' && $reason === '') $reason = '管理员删除';
    $batch = recycle_bin_batch_key();

    q('INSERT INTO ' . recycle_bin_table() . '(batch_key,item_type,item_id,parent_id,owner_id,delete_source,delete_reason,payload,deleted_by,deleted_at,status,restored_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)', [
        $batch,
        $type,
        $id,
        $type === 'replies' ? (int)($row['topic_id'] ?? 0) : 0,
        $owner_id,
        $source,
        $reason,
        $payload,
        $deleted_by,
        now(),
        'archived',
        0,
    ]);

    if ($source === 'admin') {
        if (!$cascade) recycle_bin_notify_deleted_author($owner_id, $type, $row, $reason);
        elseif (recycle_bin_config()['notify_cascade_authors']) recycle_bin_notify_cascade_author($owner_id, $type, $row);
    }
}

function recycle_bin_notify_cascade_author(int $owner_id, string $type, array $row): void
{
    if ($owner_id <= 0 || $owner_id === uid()) return;
    if (!isset($GLOBALS['__recycle_bin_notified_cascade'])) $GLOBALS['__recycle_bin_notified_cascade'] = [];
    $key = (string)$owner_id . ':' . recycle_bin_batch_key();
    if (isset($GLOBALS['__recycle_bin_notified_cascade'][$key])) return;
    $GLOBALS['__recycle_bin_notified_cascade'][$key] = true;
    create_notification($owner_id, uid(), 'recycle_bin', '你参与的内容所在主题已被管理员删除,其中你的回帖也已进入回收站。你可以在「我的回收站」查看,并在需要时提交恢复申请。');
}

function recycle_bin_notify_deleted_author(int $owner_id, string $type, array $row, string $reason): void
{
    if ($owner_id <= 0 || $owner_id === uid()) return;
    $title = trim((string)($row['title'] ?? ''));
    $label = $type === 'topics' ? '主题《' . ($title !== '' ? $title : '#' . (int)($row['id'] ?? 0)) . '》' : '一条回帖';
    $reason_text = $reason !== '' ? '原因:' . content_excerpt($reason, 160) . '。' : '';
    $content = '您的' . $label . '已被管理员移入回收站。' . $reason_text . '你可以在「我的回收站」查看原内容,并修改后重新提交恢复申请。';
    create_notification($owner_id, uid(), 'recycle_bin', $content);
}

function recycle_bin_before_delete($value, array $ctx): mixed
{
    $type = (string)($ctx['table'] ?? '');
    $row = is_array($ctx['row'] ?? null) ? $ctx['row'] : [];
    recycle_bin_archive_row($type, $row, !empty($ctx['cascade']));
    return $value;
}

function recycle_bin_topic_actions($value, array $ctx): mixed
{
    if (!is_string($value) || !can_manage()) return $value;
    $topic = is_array($ctx['topic'] ?? null) ? $ctx['topic'] : [];
    if (!$topic) return $value;
    return $value . '<label class="grid recycle-bin-reason-field"><span>删除原因</span><textarea name="recycle_bin_reason" maxlength="' . (int)DB_TEXT_MAX_LENGTH . '" placeholder="可选;填写后作者会在通知中看到">' . h((string)($_POST['recycle_bin_reason'] ?? '')) . '</textarea></label>';
}

function recycle_bin_reply_form_extra($value, array $ctx): mixed
{
    if (!is_string($value) || !can_manage()) return $value;
    $reply = is_array($ctx['reply'] ?? null) ? $ctx['reply'] : [];
    if (!$reply || (int)($reply['id'] ?? 0) < 1) return $value;
    return $value . '<label class="grid recycle-bin-reason-field"><span>删除原因</span><textarea name="recycle_bin_reason" maxlength="' . (int)DB_TEXT_MAX_LENGTH . '" placeholder="可选;填写后作者会在通知中看到">' . h((string)($_POST['recycle_bin_reason'] ?? '')) . '</textarea></label>';
}

function recycle_bin_admin_tabs($value, array $ctx): mixed
{
    if (!recycle_bin_config()['admin_tab'] || !is_array($value)) return $value;
    $value['recycle_bin'] = ['label' => '回收站', 'href' => admin_url(['tab' => 'recycle_bin'])];
    return $value;
}

function recycle_bin_user_menu_links($value, array $ctx): mixed
{
    if (!recycle_bin_config()['user_entry'] || !is_array($value) || empty($ctx['self'])) return $value;
    $value['recycle_bin'] = ['text' => '我的回收站', 'url' => route_url('recycle_bin')];
    return $value;
}

function recycle_bin_sidebar_feature_links($value, array $ctx): mixed
{
    if (!recycle_bin_config()['sidebar_link'] || !uid() || !is_array($value)) return $value;
    $value['recycle_bin'] = ['text' => '我的回收站', 'url' => route_url('recycle_bin')];
    return $value;
}

function recycle_bin_purge_interval(): int { return 3600; }

function recycle_bin_purge_cron(array $plugin, array $task): string
{
    $config = recycle_bin_config();
    if (!$config['auto_purge']) return '自动清理未启用';
    $cutoff = now() - $config['retention_days'] * 86400;
    $batches = q('SELECT batch_key,MAX(deleted_at) AS newest_deleted_at FROM ' . recycle_bin_table() . ' WHERE status=? GROUP BY batch_key HAVING MAX(deleted_at)<=? ORDER BY newest_deleted_at ASC LIMIT ?', ['archived', $cutoff, $config['purge_batch_limit']])->fetchAll();
    $purged = 0;
    foreach ($batches as $batch) {
        $key = (string)($batch['batch_key'] ?? '');
        if ($key === '') continue;
        $purged += (int)q('DELETE FROM ' . recycle_bin_table() . ' WHERE batch_key=? AND status=?', [$key, 'archived'])->rowCount();
        q('DELETE FROM ' . recycle_bin_requests_table() . ' WHERE archive_id NOT IN (SELECT id FROM ' . recycle_bin_table() . ') AND status<>?', ['pending']);
    }
    return '自动清理归档 ' . $purged . ' 条';
}

function recycle_bin_decode_payload(array $row): array
{
    $payload = json_decode((string)($row['payload'] ?? ''), true);
    if (!is_array($payload) || (int)($payload['id'] ?? 0) < 1) err('回收站快照已损坏,无法恢复');
    return $payload;
}

function recycle_bin_load_rows(array $ids, string $status = 'archived'): array
{
    $ids = array_values(array_unique(array_filter(array_map('intval', $ids), static fn(int $id): bool => $id > 0)));
    if (!$ids) return [];
    $rows = q('SELECT * FROM ' . recycle_bin_table() . ' WHERE id IN (' . sql_marks(count($ids)) . ') AND status=? ORDER BY id', [...$ids, $status])->fetchAll();
    $map = [];
    foreach ($rows as $row) $map[(int)$row['id']] = $row;
    return $map;
}

function recycle_bin_load_batches(array $batch_keys, string $status = 'archived'): array
{
    $batch_keys = array_values(array_unique(array_filter(array_map('strval', $batch_keys), static fn(string $key): bool => $key !== '')));
    if (!$batch_keys) return [];
    $rows = q('SELECT * FROM ' . recycle_bin_table() . ' WHERE batch_key IN (' . sql_marks(count($batch_keys)) . ') AND status=? ORDER BY id', [...$batch_keys, $status])->fetchAll();
    $grouped = [];
    foreach ($rows as $row) $grouped[(string)$row['batch_key']][] = $row;
    return $grouped;
}

function recycle_bin_restore_pg_sequence(string $table): void
{
    if (db_driver() !== 'pgsql') return;
    q("SELECT setval(pg_get_serial_sequence(?, 'id'), COALESCE((SELECT MAX(id) FROM " . app_db_identifier('pgsql', $table) . "), 1))", [$table]);
}

function recycle_bin_assert_reference(array $payload, string $type): void
{
    $user_id = (int)($payload['user_id'] ?? 0);
    if ($user_id < 1 || !row('app_users', 'id', $user_id)) err(($type === 'topics' ? '主题' : '回帖') . '的作者账号已不存在,暂时无法恢复');
    if ($type === 'topics') {
        $forum_id = (int)($payload['forum_id'] ?? 0);
        if ($forum_id < 1 || !row('app_forums', 'id', $forum_id)) err('主题所属版块已不存在,暂时无法恢复');
    } else {
        $topic_id = (int)($payload['topic_id'] ?? 0);
        if ($topic_id < 1 || !row('app_topics', 'id', $topic_id)) err('回帖所属主题不存在,请先恢复主题');
    }
}

function recycle_bin_restore_topic(array $topic_row, array $batch_rows, ?array $override = null): void
{
    $topic = $override ?: recycle_bin_decode_payload($topic_row);
    $topic_id = (int)$topic['id'];
    if (row('app_topics', 'id', $topic_id)) err('主题ID #' . $topic_id . ' 已被占用,无法按原ID恢复');
    recycle_bin_assert_reference($topic, 'topics');

    $reply_payloads = [];
    foreach ($batch_rows as $row) {
        if ((string)$row['item_type'] !== 'replies' || (string)$row['status'] !== 'archived') continue;
        $reply = recycle_bin_decode_payload($row);
        if ((int)($reply['topic_id'] ?? 0) !== $topic_id) continue;
        if (row('app_replies', 'id', (int)($reply['id'] ?? 0))) err('回帖ID #' . (int)$reply['id'] . ' 已被占用,无法恢复整个主题');
        recycle_bin_assert_reference($reply, 'replies');
        $reply_payloads[] = ['archive' => $row, 'payload' => $reply];
    }

    $has_floor_index = app_db_table_exists(db(), db_driver(), 'app_topics_del');
    q('INSERT INTO app_topics(id,forum_id,user_id,title,body,highlight_style,reply_order,reply_count,view_count,last_reply_at,last_reply_user_id,created_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)', [
        $topic_id, (int)$topic['forum_id'], (int)$topic['user_id'], (string)$topic['title'], (string)$topic['body'], (string)($topic['highlight_style'] ?? ''), (int)($topic['reply_order'] ?? 0), 0, (int)($topic['view_count'] ?? 0), 0, 0, (int)$topic['created_at']
    ]);
    topic_fts_sync($topic_id, (string)$topic['title'], (string)$topic['body']);

    $restored_archive_ids = [(int)$topic_row['id']];
    foreach ($reply_payloads as $item) {
        $reply = $item['payload'];
        q('INSERT INTO app_replies(id,topic_id,user_id,body,created_at,updated_at) VALUES(?,?,?,?,?,?)', [(int)$reply['id'], $topic_id, (int)$reply['user_id'], (string)$reply['body'], (int)$reply['created_at'], (int)($reply['updated_at'] ?? $reply['created_at'])]);
        reply_fts_sync((int)$reply['id'], (string)$reply['body']);
        if ($has_floor_index) q('DELETE FROM app_topics_del WHERE topic_id=? AND reply_id=?', [$topic_id, (int)$reply['id']]);
        $restored_archive_ids[] = (int)$item['archive']['id'];
    }
    if (function_exists('floor_index_clear')) floor_index_clear();
    refresh_topic_stats($topic_id);
    q('UPDATE ' . recycle_bin_table() . ' SET status=?,restored_at=? WHERE id IN (' . sql_marks(count($restored_archive_ids)) . ')', ['restored', now(), ...$restored_archive_ids]);
    recycle_bin_restore_pg_sequence('app_topics');
    recycle_bin_restore_pg_sequence('app_replies');

    recycle_bin_notify_restore_authors($topic_row, $batch_rows, $topic_id, 0);
}

function recycle_bin_restore_reply(array $row, ?array $override = null): void
{
    $reply = $override ?: recycle_bin_decode_payload($row);
    $reply_id = (int)$reply['id'];
    $topic_id = (int)($reply['topic_id'] ?? 0);
    if (row('app_replies', 'id', $reply_id)) err('回帖ID #' . $reply_id . ' 已被占用,无法恢复');
    if (!$topic_id || !row('app_topics', 'id', $topic_id)) err('所属主题不存在,请先恢复主题');
    recycle_bin_assert_reference($reply, 'replies');
    q('INSERT INTO app_replies(id,topic_id,user_id,body,created_at,updated_at) VALUES(?,?,?,?,?,?)', [$reply_id, $topic_id, (int)$reply['user_id'], (string)$reply['body'], (int)$reply['created_at'], (int)($reply['updated_at'] ?? $reply['created_at'])]);
    reply_fts_sync($reply_id, (string)$reply['body']);
    if (app_db_table_exists(db(), db_driver(), 'app_topics_del')) q('DELETE FROM app_topics_del WHERE topic_id=? AND reply_id=?', [$topic_id, $reply_id]);
    if (function_exists('floor_index_clear')) floor_index_clear();
    refresh_topic_stats($topic_id);
    q('UPDATE ' . recycle_bin_table() . ' SET status=?,restored_at=? WHERE id=?', ['restored', now(), (int)$row['id']]);
    recycle_bin_restore_pg_sequence('app_replies');
    recycle_bin_notify_restore_authors($row, [$row], $topic_id, $reply_id);
}

function recycle_bin_notify_restore_authors(array $primary, array $batch_rows, int $topic_id, int $reply_id): void
{
    if ((string)($primary['delete_source'] ?? '') !== 'admin') return;
    $owners = [];
    foreach ($batch_rows as $item) {
        $owner = (int)($item['owner_id'] ?? 0);
        if ($owner > 0) $owners[$owner] = true;
    }
    $owner = (int)($primary['owner_id'] ?? 0);
    if ($owner > 0) $owners[$owner] = true;
    foreach (array_keys($owners) as $owner_id) {
        if ((int)$owner_id === uid()) continue;
        $url = route_url('recycle_bin');
        $content = '管理员已恢复你被删除的' . ((string)($primary['item_type'] ?? '') === 'topics' ? '主题' : '回帖') . '。你可以查看恢复结果;如需继续修改,可直接编辑后重新发布。\n\n[打开我的回收站](' . $url . ')';
        create_notification((int)$owner_id, uid(), 'recycle_bin', $content, $topic_id, $reply_id);
    }
}

function recycle_bin_restore_selected(array $ids): int
{
    $rows = recycle_bin_load_rows($ids, 'archived');
    if (!$rows) return 0;
    $topic_batch_keys = [];
    foreach ($rows as $row) {
        if ((string)$row['item_type'] === 'topics') $topic_batch_keys[(string)$row['batch_key']] = true;
    }
    $batches = recycle_bin_load_batches(array_keys($topic_batch_keys), 'archived');
    $count = 0;
    foreach ($rows as $row) {
        $type = (string)$row['item_type'];
        if ($type === 'replies' && isset($topic_batch_keys[(string)$row['batch_key']])) continue;
        tx(function () use ($row, $batches, &$count): void {
            $type = (string)$row['item_type'];
            if ($type === 'topics') {
                recycle_bin_restore_topic($row, $batches[(string)$row['batch_key']] ?? []);
            } elseif ($type === 'replies') {
                recycle_bin_restore_reply($row);
            } else {
                err('未知回收类型');
            }
            $count++;
        });
    }
    return $count;
}

function recycle_bin_purge_selected(array $ids): int
{
    $rows = recycle_bin_load_rows($ids, 'archived');
    if (!$rows) return 0;
    $count = 0;
    foreach ($rows as $row) {
        $count += (int)q('DELETE FROM ' . recycle_bin_table() . ' WHERE id=? AND status=?', [(int)$row['id'], 'archived'])->rowCount();
    }
    return $count;
}

function recycle_bin_admin_handle_action(): never
{
    require_post();
    if ((string)($_POST['recycle_bin_action'] ?? '') === 'save_config') {
        $config = recycle_bin_config();
        foreach (['capture_topics','capture_replies','auto_purge','admin_tab','sidebar_link','user_entry','notify_cascade_authors'] as $key) $config[$key] = isset($_POST[$key]) ? 1 : 0;
        $config['retention_days'] = min(3650, max(1, (int)($_POST['retention_days'] ?? $config['retention_days'])));
        $config['purge_batch_limit'] = min(1000, max(1, (int)($_POST['purge_batch_limit'] ?? $config['purge_batch_limit'])));
        plugin_save_config('recycle_bin', $config);
        set_flash('回收站设置已保存');
        go(admin_url(['tab' => 'recycle_bin']));
    }

    $action = (string)($_POST['recycle_bin_action'] ?? '');
    $ids = array_values(array_unique(array_filter(array_map('intval', (array)($_POST['ids'] ?? [])), static fn(int $id): bool => $id > 0)));
    if (count($ids) > 200) err('一次最多处理200条归档');
    if ($action === 'restore') {
        set_flash('已恢复 ' . recycle_bin_restore_selected($ids) . ' 条内容');
        go(admin_url(['tab' => 'recycle_bin']));
    }
    if ($action === 'purge') {
        set_flash('已永久清除 ' . recycle_bin_purge_selected($ids) . ' 条回收站快照(不会删除当前论坛数据)');
        go(admin_url(['tab' => 'recycle_bin']));
    }
    if ($action === 'approve_request' || $action === 'reject_request') {
        $request_id = max(0, (int)($_POST['request_id'] ?? 0));
        recycle_bin_review_request($request_id, $action === 'approve_request');
        go(admin_url(['tab' => 'recycle_bin', 'view' => 'requests']));
    }
    if ($action === 'purge_all') {
        if ((string)($_POST['confirm_phrase'] ?? '') !== '永久清空') err('请输入“永久清空”确认');
        $count = (int)q('DELETE FROM ' . recycle_bin_table() . ' WHERE status=?', ['archived'])->rowCount();
        set_flash('已永久清空 ' . $count . ' 条回收站快照(不会删除当前论坛数据)');
        go(admin_url(['tab' => 'recycle_bin']));
    }
    err('参数错误');
}

function recycle_bin_submit_request(int $archive_id): void
{
    need_login();
    require_post();
    $row = one('SELECT * FROM ' . recycle_bin_table() . ' WHERE id=? AND status=?', [$archive_id, 'archived']);
    if (!$row || (int)$row['owner_id'] !== uid()) err('回收站内容不存在或无权操作');
    if ((string)$row['delete_source'] !== 'admin') err('你可以直接恢复自己删除的内容');
    $payload = recycle_bin_decode_payload($row);
    $type = (string)$row['item_type'];
    if ($type === 'topics') {
        $title = cut(trim((string)($_POST['title'] ?? '')), length_limit('title', 'max'));
        $body = cut(trim((string)($_POST['body'] ?? '')), length_limit('topic_body', 'max'));
        if ($title === '' || $body === '') err('标题和内容不能为空');
        require_length($title, length_limit('title', 'min'), '标题');
        require_length($body, length_limit('topic_body', 'min'), '主题内容');
        $payload['title'] = $title;
        $payload['body'] = $body;
    } elseif ($type === 'replies') {
        $body = cut(trim((string)($_POST['body'] ?? '')), length_limit('reply_body', 'max'));
        if ($body === '') err('回帖内容不能为空');
        require_length($body, length_limit('reply_body', 'min'), '回帖内容');
        $payload['body'] = $body;
    } else err('未知内容类型');

    $message = cut(trim((string)($_POST['message'] ?? '')), DB_TEXT_MAX_LENGTH);
    $existing = one('SELECT id FROM ' . recycle_bin_requests_table() . ' WHERE archive_id=? AND status=?', [$archive_id, 'pending']);
    if ($existing) err('该内容已有待处理的恢复申请,请等待管理员处理');
    app_db_insert_ignore(recycle_bin_requests_table(), [
        'archive_id' => $archive_id,
        'user_id' => uid(),
        'item_type' => $type,
        'proposed_payload' => json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR),
        'message' => $message,
        'status' => 'pending',
        'created_at' => now(),
        'reviewed_at' => 0,
        'reviewer_id' => 0,
        'review_note' => '',
    ], ['archive_id','status']);
    set_flash('恢复申请已提交,等待管理员处理');
    go(route_url('recycle_bin'));
}

function recycle_bin_review_request(int $request_id, bool $approve): void
{
    need_admin();
    $request = one('SELECT * FROM ' . recycle_bin_requests_table() . ' WHERE id=? AND status=?', [$request_id, 'pending']);
    if (!$request) err('恢复申请不存在或已处理');
    $archive = one('SELECT * FROM ' . recycle_bin_table() . ' WHERE id=? AND status=?', [(int)$request['archive_id'], 'archived']);
    if (!$archive) err('对应回收站内容已不存在');

    $review_note = cut(trim((string)($_POST['review_note'] ?? '')), DB_TEXT_MAX_LENGTH);
    tx(function () use ($request, $archive, $approve, $review_note): void {
        if ($approve) {
            $payload = json_decode((string)$request['proposed_payload'], true);
            if (!is_array($payload)) err('申请内容已损坏');
            if ((string)$archive['item_type'] === 'topics') {
                $batch = recycle_bin_load_batches([(string)$archive['batch_key']], 'archived');
                recycle_bin_restore_topic($archive, $batch[(string)$archive['batch_key']] ?? [], $payload);
            } else {
                recycle_bin_restore_reply($archive, $payload);
            }
        }
        q('UPDATE ' . recycle_bin_requests_table() . ' SET status=?,reviewed_at=?,reviewer_id=?,review_note=? WHERE id=? AND status=?', [$approve ? 'approved' : 'rejected', now(), uid(), $review_note, (int)$request['id'], 'pending']);
        $recipient = (int)$request['user_id'];
        if ($recipient > 0 && $recipient !== uid()) {
            $message = $approve ? '管理员已通过你的恢复申请,修改后的内容已经恢复。' : '管理员暂未通过你的恢复申请。' . ($review_note !== '' ? '原因:' . content_excerpt($review_note, 160) : '');
            create_notification($recipient, uid(), 'recycle_bin', $message, (string)$archive['item_type'] === 'topics' ? (int)($payload['id'] ?? 0) : (int)($payload['topic_id'] ?? 0), (string)$archive['item_type'] === 'replies' ? (int)($payload['id'] ?? 0) : 0);
        }
    });
    set_flash($approve ? '恢复申请已通过' : '恢复申请已驳回');
}

function recycle_bin_user_handle_action(): never
{
    need_login();
    require_post();
    $action = (string)($_POST['recycle_bin_action'] ?? '');
    $id = max(0, (int)($_POST['archive_id'] ?? 0));
    if ($action === 'submit_request') recycle_bin_submit_request($id);
    if ($action === 'restore_self') {
        $row = one('SELECT * FROM ' . recycle_bin_table() . ' WHERE id=? AND status=? AND owner_id=?', [$id, 'archived', uid()]);
        if (!$row) err('回收站内容不存在');
        if ((string)$row['delete_source'] !== 'user') err('管理员删除的内容需要修改后提交恢复申请');
        recycle_bin_restore_selected([$id]);
        set_flash('内容已恢复');
        go(route_url('recycle_bin'));
    }
    err('参数错误');
}

function recycle_bin_user_page(array $plugin): void
{
    need_login();
    if (is_post_request()) recycle_bin_user_handle_action();

    $view = (string)($_GET['view'] ?? 'all');
    if (!in_array($view, ['all','archived','restored','requests'], true)) $view = 'all';
    $archive_id = max(0, (int)($_GET['id'] ?? 0));
    if ($archive_id > 0 && $view === 'edit') $view = 'edit';

    if ($archive_id > 0 && (string)($_GET['view'] ?? '') === 'edit') {
        $row = one('SELECT * FROM ' . recycle_bin_table() . ' WHERE id=? AND status=? AND owner_id=?', [$archive_id, 'archived', uid()]);
        if (!$row) err('回收站内容不存在');
        if ((string)$row['delete_source'] !== 'admin') err('自己删除的内容可以直接恢复,无需申请');
        $payload = recycle_bin_decode_payload($row);
        $body = '<div class="recycle-bin-page"><section class="form-panel"><h2>修改后重新提交</h2><p class="admin-muted">这是管理员删除前的原始内容。修改后会提交给管理员审核,不会直接重新公开。</p><form method="post">' . form_token() . '<input type="hidden" name="recycle_bin_action" value="submit_request"><input type="hidden" name="archive_id" value="' . (int)$row['id'] . '">';
        if ((string)$row['item_type'] === 'topics') $body .= input('标题', 'title', (string)($payload['title'] ?? ''), 'text', true) . textarea('内容', 'body', (string)($payload['body'] ?? ''), true);
        else $body .= textarea('内容', 'body', (string)($payload['body'] ?? ''), true);
        $body .= textarea('给管理员的说明', 'message', '', false, '例如:已删除违规内容中的链接,其他内容保持不变。') . '<button type="submit">提交恢复申请</button></form></section></div>';
        page('修改后重新提交', form_shell($body));
        return;
    }

    $where = ['owner_id=?']; $params = [uid()];
    if ($view === 'archived') { $where[] = 'status=?'; $params[] = 'archived'; }
    if ($view === 'restored') { $where[] = 'status=?'; $params[] = 'restored'; }
    $rows = q('SELECT * FROM ' . recycle_bin_table() . ' WHERE ' . implode(' AND ', $where) . ' ORDER BY deleted_at DESC,id DESC LIMIT 100', $params)->fetchAll();
    $requests = q('SELECT r.*,i.item_type,i.item_id,i.delete_reason FROM ' . recycle_bin_requests_table() . ' r LEFT JOIN ' . recycle_bin_table() . ' i ON i.id=r.archive_id WHERE r.user_id=? ORDER BY r.created_at DESC LIMIT 100', [uid()])->fetchAll();
    $pending_archive_ids = [];
    if ($rows) {
        $archive_ids = array_values(array_unique(array_map(static fn(array $r): int => (int)$r['id'], $rows)));
        $pending_rows = q('SELECT archive_id FROM ' . recycle_bin_requests_table() . ' WHERE archive_id IN (' . sql_marks(count($archive_ids)) . ') AND status=?', [...$archive_ids, 'pending'])->fetchAll();
        foreach ($pending_rows as $pending_row) $pending_archive_ids[(int)$pending_row['archive_id']] = true;
    }

    $nav = '<div class="recycle-bin-user-tabs"><a href="' . h(route_url('recycle_bin')) . '">全部</a><a href="' . h(route_url('recycle_bin', ['view'=>'archived'])) . '">待处理</a><a href="' . h(route_url('recycle_bin', ['view'=>'restored'])) . '">已恢复</a><a href="' . h(route_url('recycle_bin', ['view'=>'requests'])) . '">恢复申请</a></div>';
    $html = '<div class="recycle-bin-page"><section class="form-panel"><h2>我的回收站</h2><p class="admin-muted">这里只显示你自己的内容。管理员删除的内容不会直接恢复,需要修改后提交申请;你自己删除的内容可以直接恢复。</p>' . $nav;

    if ($view === 'requests') {
        $html .= '<ul class="recycle-bin-list">';
        foreach ($requests as $request) {
            $status = match ((string)$request['status']) { 'pending'=>'待审核','approved'=>'已通过','rejected'=>'已驳回', default=>'未知' };
            $html .= '<li><strong>' . h((string)$request['item_type'] === 'topics' ? '主题' : '回帖') . ' #' . (int)$request['item_id'] . '</strong><span>' . h($status) . ' · ' . human_time((int)$request['created_at']) . '</span>' . ($request['review_note'] !== '' ? '<small>' . h((string)$request['review_note']) . '</small>' : '') . '</li>';
        }
        if (!$requests) $html .= '<li class="empty-state">还没有恢复申请。</li>';
        $html .= '</ul></section></div>';
        page('我的回收站', form_shell($html));
        return;
    }

    $html .= '</section><section class="admin-list-panel"><ul class="recycle-bin-list">';
    foreach ($rows as $row) {
        $payload = json_decode((string)$row['payload'], true) ?: [];
        $type = (string)$row['item_type'];
        $title = $type === 'topics' ? trim((string)($payload['title'] ?? '')) : content_excerpt((string)($payload['body'] ?? ''), 120);
        if ($title === '') $title = ($type === 'topics' ? '主题' : '回帖') . ' #' . (int)$row['item_id'];
        $source = (string)$row['delete_source'] === 'admin' ? '管理员删除' : '自己删除';
        $status = (string)$row['status'];
        $html .= '<li><div><strong>' . h($title) . '</strong><small>' . h($type === 'topics' ? '主题' : '回帖') . ' #' . (int)$row['item_id'] . ' · ' . h($source) . ' · ' . h($status === 'archived' ? '待处理' : '已恢复') . ' · ' . human_time((int)$row['deleted_at']) . '</small>' . ((string)$row['delete_reason'] !== '' ? '<small>删除原因:' . h((string)$row['delete_reason']) . '</small>' : '') . '</div><div class="recycle-bin-actions">';
        if ($status === 'archived') {
            if ((string)$row['delete_source'] === 'user') {
                $html .= '<form method="post">' . form_token() . '<input type="hidden" name="recycle_bin_action" value="restore_self"><input type="hidden" name="archive_id" value="' . (int)$row['id'] . '"><button type="submit">恢复</button></form>';
            } else {
                $pending = isset($pending_archive_ids[(int)$row['id']]);
                $html .= $pending ? '<span class="admin-flag on">申请审核中</span>' : '<a class="btn" href="' . h(route_url('recycle_bin', ['view'=>'edit','id'=>(int)$row['id']])) . '">修改后重新提交</a>';
            }
        } else $html .= '<span class="admin-flag on">已恢复</span>';
        $html .= '</div></li>';
    }
    if (!$rows) $html .= '<li class="empty-state">你的回收站为空。</li>';
    $html .= '</ul></section></div>';
    page('我的回收站', form_shell($html));
}

function recycle_bin_admin_title(array $row): string
{
    $payload = json_decode((string)($row['payload'] ?? ''), true);
    if (!is_array($payload)) return '损坏的快照 #' . (int)$row['id'];
    if ((string)$row['item_type'] === 'topics') {
        $title = trim((string)($payload['title'] ?? ''));
        return $title !== '' ? $title : '主题 #' . (int)$row['item_id'];
    }
    return content_excerpt((string)($payload['body'] ?? ''), 100) ?: '回帖 #' . (int)$row['item_id'];
}

function recycle_bin_admin_page(array $plugin): string
{
    need_admin();
    if (is_post_request()) recycle_bin_admin_handle_action();
    $config = recycle_bin_config();
    $view = (string)($_GET['view'] ?? 'all');
    if (!in_array($view, ['all','requests'], true)) $view = 'all';

    $stats = [];
    foreach (['archived','restored'] as $status) $stats[$status] = (int)val('SELECT COUNT(*) FROM ' . recycle_bin_table() . ' WHERE status=?', [$status]);
    $pending_requests = (int)val('SELECT COUNT(*) FROM ' . recycle_bin_requests_table() . ' WHERE status=?', ['pending']);

    $settings = '<section class="form-panel"><h2>回收规则</h2><p class="admin-muted">这是管理员专用的全站回收站。这里可以看到所有用户的回收状态;普通用户只能看到自己的内容。</p><form method="post">' . form_token() . '<input type="hidden" name="recycle_bin_action" value="save_config">'
        . checkbox('回收主题','capture_topics',$config['capture_topics']) . checkbox('回收回帖','capture_replies',$config['capture_replies']) . checkbox('启用自动永久清理','auto_purge',$config['auto_purge'])
        . number_input('保留天数','retention_days',$config['retention_days'],1,3650) . number_input('每次自动清理最多处理批次','purge_batch_limit',$config['purge_batch_limit'],1,1000)
        . checkbox('在后台主导航显示“回收站”','admin_tab',$config['admin_tab']) . checkbox('在侧栏快捷功能显示“我的回收站”','sidebar_link',$config['sidebar_link']) . checkbox('在用户菜单显示“我的回收站”','user_entry',$config['user_entry']) . checkbox('删除主题时通知受影响的回帖作者','notify_cascade_authors',$config['notify_cascade_authors'])
        . '<p class="admin-muted">自动永久清理默认关闭。管理员删除会通知作者;作者可以修改后提交恢复申请,管理员审核后才恢复。</p><button type="submit">保存设置</button></form></section>';

    $tabs = '<div class="recycle-bin-admin-tabs"><a href="' . h(admin_url(['tab'=>'recycle_bin'])) . '">全部内容</a><a href="' . h(admin_url(['tab'=>'recycle_bin','view'=>'requests'])) . '">恢复申请' . ($pending_requests ? ' (' . $pending_requests . ')' : '') . '</a></div>';

    if ($view === 'requests') {
        $rows = q('SELECT r.*,i.item_type,i.item_id,i.owner_id,i.delete_reason,i.payload FROM ' . recycle_bin_requests_table() . ' r LEFT JOIN ' . recycle_bin_table() . ' i ON i.id=r.archive_id ORDER BY CASE r.status WHEN \'pending\' THEN 0 ELSE 1 END,r.created_at DESC LIMIT 200')->fetchAll();
        $body = '<section class="admin-list-panel"><div class="admin-list-head"><strong>恢复申请</strong><span class="admin-muted">待处理 ' . $pending_requests . '</span></div><ul class="admin-manage-list">';
        $request_users = rows_by_ids('app_users', array_column($rows, 'user_id'), 'id,username,avatar_style,avatar_seed,group_id');
        foreach ($rows as $row) {
            $user = $request_users[(int)$row['user_id']] ?? null;
            $payload = json_decode((string)$row['proposed_payload'], true) ?: [];
            $title = (string)$row['item_type'] === 'topics' ? (string)($payload['title'] ?? '') : content_excerpt((string)($payload['body'] ?? ''), 100);
            $body .= '<li class="admin-list-item"><div><strong>' . h($title ?: ('#' . (int)$row['item_id'])) . '</strong><small>用户:' . h((string)($user['username'] ?? ('#' . (int)$row['user_id']))) . ' · ' . h((string)$row['item_type'] === 'topics' ? '主题' : '回帖') . ' #' . (int)$row['item_id'] . ' · ' . h((string)$row['status']) . ' · ' . human_time((int)$row['created_at']) . '</small>' . ((string)$row['message'] !== '' ? '<small>用户说明:' . h((string)$row['message']) . '</small>' : '') . '</div>';
            if ((string)$row['status'] === 'pending') {
                $body .= '<div class="admin-inline-ops"><form method="post">' . form_token() . '<input type="hidden" name="recycle_bin_action" value="approve_request"><input type="hidden" name="request_id" value="' . (int)$row['id'] . '"><button type="submit">通过并恢复</button></form><form method="post">' . form_token() . '<input type="hidden" name="recycle_bin_action" value="reject_request"><input type="hidden" name="request_id" value="' . (int)$row['id'] . '"><input name="review_note" placeholder="驳回原因"><button type="submit" class="alt">驳回</button></form></div>';
            }
            $body .= '</li>';
        }
        if (!$rows) $body .= '<li class="empty-state">暂无恢复申请。</li>';
        $body .= '</ul></section>';
        return '<div class="recycle-bin-page">' . $settings . $tabs . $body . '</div>';
    }

    $status = (string)($_GET['status'] ?? 'archived');
    if (!in_array($status, ['archived','restored','all'], true)) $status = 'archived';
    $type = (string)($_GET['type'] ?? 'all');
    if (!in_array($type, ['topics','replies','all'], true)) $type = 'all';
    $source = (string)($_GET['source'] ?? 'all');
    if (!in_array($source, ['admin','user','all'], true)) $source = 'all';
    $search = cut(trim((string)($_GET['q'] ?? '')), 100);
    $page_no = min(max_pagination_pages(), max(1, (int)($_GET['p'] ?? 1)));
    $size = 50;
    $where=[]; $params=[];
    if ($status !== 'all') { $where[]='status=?'; $params[]=$status; }
    if ($type !== 'all') { $where[]='item_type=?'; $params[]=$type; }
    if ($source !== 'all') { $where[]='delete_source=?'; $params[]=$source; }
    if ($search !== '') { $where[]='payload LIKE ?'; $params[]='%' . $search . '%'; }
    $where_sql = $where ? ' WHERE ' . implode(' AND ', $where) : '';
    $total=(int)val('SELECT COUNT(*) FROM ' . recycle_bin_table() . $where_sql,$params);
    $offset=($page_no-1)*$size;
    $rows=q('SELECT * FROM ' . recycle_bin_table() . $where_sql . ' ORDER BY deleted_at DESC,id DESC LIMIT ? OFFSET ?', [...$params,$size+1,$offset])->fetchAll();
    $has_next=count($rows)>$size; if($has_next)array_pop($rows);
    $base=['tab'=>'recycle_bin','status'=>$status,'type'=>$type,'source'=>$source]; if($search!=='')$base['q']=$search;

    $toolbar='<div class="admin-list-head"><div class="admin-head-inline"><div><strong>全站回收状态</strong> <span class="admin-muted">待处理 ' . $stats['archived'] . ' · 已恢复 ' . $stats['restored'] . ' · 待审核 ' . $pending_requests . '</span></div></div></div>';
    $filters='<form method="get" class="admin-filter-form"><input type="hidden" name="a" value="admin"><input type="hidden" name="tab" value="recycle_bin"><select name="status"><option value="archived"' . ($status==='archived'?' selected':'') . '>待处理</option><option value="restored"' . ($status==='restored'?' selected':'') . '>已恢复</option><option value="all"' . ($status==='all'?' selected':'') . '>全部</option></select><select name="type"><option value="all">全部类型</option><option value="topics"' . ($type==='topics'?' selected':'') . '>主题</option><option value="replies"' . ($type==='replies'?' selected':'') . '>回帖</option></select><select name="source"><option value="all">全部来源</option><option value="admin"' . ($source==='admin'?' selected':'') . '>管理员删除</option><option value="user"' . ($source==='user'?' selected':'') . '>用户自己删除</option></select><input type="search" name="q" value="' . h($search) . '" placeholder="搜索标题/内容"><button type="submit">筛选</button></form>';
    $list='<form method="post">' . form_token() . '<input type="hidden" name="recycle_bin_action" value="restore"><div class="admin-list-panel">' . $toolbar . $filters . '<ul class="admin-manage-list">';
    $owner_users = rows_by_ids('app_users', array_column($rows, 'owner_id'), 'id,username,avatar_style,avatar_seed,group_id');
    foreach($rows as $row){
        $arch=(string)$row['status']==='archived'; $title=recycle_bin_admin_title($row); $user=$owner_users[(int)$row['owner_id']] ?? null;
        $meta=((string)$row['item_type']==='topics'?'主题':'回帖') . ' #' . (int)$row['item_id'] . ' · 作者 ' . (string)($user['username'] ?? ('#'.(int)$row['owner_id'])) . ' · ' . ((string)$row['delete_source']==='admin'?'管理员删除':'用户删除') . ' · ' . human_time((int)$row['deleted_at']);
        $list.='<li class="admin-list-item"><label class="check"><input type="checkbox" name="ids[]" value="' . (int)$row['id'] . '"' . ($arch?'':' disabled') . '><span><strong>' . h($title) . '</strong><small>' . h($meta) . '</small>' . ((string)$row['delete_reason']!==''?'<small>原因:'.h((string)$row['delete_reason']).'</small>':'') . '</span></label>' . ($arch?'<div class="admin-inline-ops"><button type="submit" name="recycle_bin_action" value="restore">恢复</button><button type="submit" name="recycle_bin_action" value="purge" class="alt" onclick="return confirm(\'永久清除所选快照?\')">永久清除</button></div>':'<span class="admin-flag on">已恢复</span>') . '</li>';
    }
    if(!$rows)$list.='<li class="empty-state">没有符合条件的回收站记录。</li>';
    $list.='</ul>';
    if($has_next||$page_no>1)$list.='<div class="pagination-bar">'.simple_paginate($page_no>1,$has_next,$page_no,admin_url($base)).'</div>';
    $list.='</div></form>';
    $danger='<section class="form-panel"><h2>危险操作</h2><p class="admin-muted">永久清除只删除回收站快照,不会再次删除当前论坛数据。</p><form method="post" onsubmit="return confirm(\'确定永久清空全部已归档数据?\')">'.form_token().'<input type="hidden" name="recycle_bin_action" value="purge_all">'.input('确认文字','confirm_phrase','','text',true).'<button type="submit">永久清空已归档</button></form></section>';
    return '<div class="recycle-bin-page">' . $settings . $tabs . $list . $danger . '</div>';
}

function recycle_bin_plugin_css(): string
{
    return '.recycle-bin-page{display:grid;gap:16px}.recycle-bin-page .form-panel{display:grid;gap:12px}.recycle-bin-page .admin-muted{color:var(--text-muted);font-size:var(--font-size-sm)}.recycle-bin-user-tabs,.recycle-bin-admin-tabs{display:flex;gap:8px;flex-wrap:wrap}.recycle-bin-user-tabs a,.recycle-bin-admin-tabs a{padding:7px 10px;border:1px solid var(--line);border-radius:8px}.recycle-bin-list{list-style:none;margin:0;padding:0}.recycle-bin-list>li{display:flex;gap:12px;align-items:center;justify-content:space-between;padding:14px 0;border-bottom:1px solid var(--line-soft)}.recycle-bin-list small{display:block;color:var(--text-muted);font-size:var(--font-size-sm);margin-top:4px}.recycle-bin-actions{display:flex;gap:8px;flex-wrap:wrap}.recycle-bin-reason-field{margin-top:8px}.recycle-bin-page .admin-list-item{display:flex;gap:12px;align-items:center;justify-content:space-between}.recycle-bin-page .admin-list-item .check{flex:1}.recycle-bin-page .admin-list-item small{display:block;color:var(--text-muted);font-size:var(--font-size-sm)}.recycle-bin-page .admin-inline-ops{display:flex;gap:8px;flex-wrap:wrap}.recycle-bin-page .admin-filter-form{display:flex;gap:8px;flex-wrap:wrap;padding:12px;border-bottom:1px solid var(--line-soft)}.recycle-bin-page .admin-filter-form input,.recycle-bin-page .admin-filter-form select{min-width:120px}.recycle-bin-page .admin-filter-form input[type=search]{flex:1;min-width:180px}@media(max-width:700px){.recycle-bin-list>li,.recycle-bin-page .admin-list-item{align-items:flex-start;flex-direction:column}.recycle-bin-actions,.recycle-bin-page .admin-inline-ops{width:100%}}';
}

return [
    'id' => 'recycle_bin',
    'name' => '回收站',
    'version' => '1.1.0',
    'description' => '管理员可管理全站回收内容,用户可查看自己的删除记录;管理员删除后,作者可以修改内容并提交恢复申请。',
    'author' => 'bbs1org plugin',
    'hooks' => [
        'content.before_delete' => 'recycle_bin_before_delete',
        'topic.actions' => 'recycle_bin_topic_actions',
        'reply.form_extra' => 'recycle_bin_reply_form_extra',
        'admin.tabs' => 'recycle_bin_admin_tabs',
        'user.menu_links' => 'recycle_bin_user_menu_links',
        'sidebar.feature_links' => 'recycle_bin_sidebar_feature_links',
    ],
    'routes' => [
        'recycle_bin' => 'recycle_bin_user_page',
    ],
    'admin_tabs' => [
        'recycle_bin' => 'recycle_bin_admin_page',
    ],
    'assets' => [
        'css' => 'recycle_bin_plugin_css',
    ],
    'cron' => [
        'purge' => [
            'callback' => 'recycle_bin_purge_cron',
            'interval' => 'recycle_bin_purge_interval',
        ],
    ],
    'entries' => [
        'admin_tabs' => true,
        'feature_links' => false,
    ],
    'install' => 'recycle_bin_install',
    'uninstall' => 'recycle_bin_uninstall',
];
主楼