返回 DeepSeek-Reasonix
community.ts
根目录 / workers / crash-report / src / community.ts
1 // Moderation console for user-published skills/plugins/MCP servers. Gated on the unified
2 // dashboard admin role; reads and writes the registry database.
3 import { esc, page } from "./shell";
4 import { type User, userNav } from "./auth";
5 import type { PackageRow } from "./registry/types";
6
7 const STATUS_TABS = [
8 { key: "pending", label: "Pending" },
9 { key: "active", label: "Active" },
10 { key: "hidden", label: "Hidden" },
11 { key: "rejected", label: "Rejected" },
12 ];
13
14 function actionForm(pkg: PackageRow, action: string, label: string, cls: string, backStatus: string, confirm?: string): string {
15 const onsubmit = confirm ? ` onsubmit="return confirm('${esc(confirm)}')"` : "";
16 return `<form method="post" action="/community/${esc(pkg.scope_handle)}/${esc(pkg.name)}/${action}" class="inline"${onsubmit}>
17 <input type="hidden" name="status" value="${esc(backStatus)}">
18 <input type="hidden" name="expectedVersion" value="${esc(pkg.latest_version)}">
19 <input type="hidden" name="expectedUpdatedAt" value="${esc(pkg.updated_at)}">
20 <input type="hidden" name="expectedStatus" value="${esc(pkg.status)}">
21 <button class="btn ${cls} sm" type="submit">${esc(label)}</button></form>`;
22 }
23
24 function rowActions(pkg: PackageRow, backStatus: string): string {
25 if (pkg.status === "active") {
26 const verify = pkg.verified
27 ? actionForm(pkg, "unverify", "Unverify", "ghost", backStatus)
28 : actionForm(pkg, "verify", "Verify", "ghost", backStatus);
29 return `${verify}${actionForm(pkg, "hide", "Hide", "danger", backStatus, `Hide ${pkg.slug}?`)}`;
30 }
31 const approve = actionForm(pkg, "approve", "Approve", "", backStatus);
32 const reject = pkg.status === "rejected" ? "" : actionForm(pkg, "reject", "Reject", "danger", backStatus, `Reject ${pkg.slug}?`);
33 return `${approve}${reject}`;
34 }
35
36 function sourceLinks(pkg: PackageRow): string {
37 const links: string[] = [];
38 if (pkg.repo_url) links.push(`<a class="navlink" href="${esc(pkg.repo_url)}" target="_blank" rel="noopener">repo</a>`);
39 if (pkg.homepage) links.push(`<a class="navlink" href="${esc(pkg.homepage)}" target="_blank" rel="noopener">home</a>`);
40 return links.join(" · ");
41 }
42
43 // One paste-ready block of everything a reviewer needs: the install pointer,
44 // provenance links, and README. The manifest snapshot lives on package_versions,
45 // not here — source/repo_url are what actually carry the reviewable content.
46 function reviewBlob(pkg: PackageRow): string {
47 return [
48 `Registry submission — ${pkg.slug}`,
49 `kind: ${pkg.kind}`,
50 `status: ${pkg.status}`,
51 `publisher: @${pkg.scope_handle}`,
52 `version: ${pkg.latest_version || "—"}`,
53 `submitted: ${pkg.created_at}`,
54 `install_kind: ${pkg.install_kind}`,
55 `source: ${pkg.source || "—"}`,
56 `repo_url: ${pkg.repo_url || "—"}`,
57 `homepage: ${pkg.homepage || "—"}`,
58 `tags: ${pkg.tags || "—"}`,
59 `summary: ${pkg.summary || "—"}`,
60 ``,
61 `--- description (README) ---`,
62 pkg.description || "(none)",
63 ].join("\n");
64 }
65
66 function copyButton(pkg: PackageRow): string {
67 return `<button type="button" class="btn ghost sm copy-btn" data-copy="${esc(reviewBlob(pkg))}"><span class="copy-label">Copy for review</span></button>`;
68 }
69
70 export function renderCommunity(viewer: User, packages: PackageRow[], status: string): string {
71 const tabs = STATUS_TABS.map(
72 (t) => `<a class="filter-tab${t.key === status ? " active" : ""}" href="/community?status=${t.key}">${t.label}</a>`,
73 ).join("");
74 const rows = packages.length
75 ? packages
76 .map((p) => {
77 const verified = p.verified ? ` <span class="badge admin">verified</span>` : "";
78 const links = sourceLinks(p);
79 return `<tr>
80 <td><div class="crash-summary"><span>${esc(p.slug)}${verified}</span><small>${esc(p.summary || "—")}</small></div></td>
81 <td><span class="pill">${esc(p.kind)}</span></td>
82 <td>@${esc(p.scope_handle)}</td>
83 <td class="n">${esc(p.latest_version || "—")} · ${p.install_count} inst · ${p.star_count}★</td>
84 <td class="n">${esc(p.created_at.slice(0, 10))}</td>
85 <td><div class="actions">${rowActions(p, status)}</div><div class="rowlinks">${copyButton(p)}${links ? `<span class="muted">${links}</span>` : ""}</div></td>
86 </tr>`;
87 })
88 .join("")
89 : `<tr><td colspan="6"><div class="empty">No ${esc(status)} packages</div></td></tr>`;
90 return page(
91 "Reasonix · Community",
92 "community",
93 `<h1>Community</h1><p class="sub">Review user-published skills, plugins, and MCP servers — approve to publish, verify to badge, hide to take down</p>
94 <div class="filter-tabs">${tabs}</div>
95 <div class="card full"><table class="reg-table"><colgroup><col class="c-pkg"><col class="c-kind"><col class="c-pub"><col class="c-ver"><col class="c-sub"><col class="c-act"></colgroup><thead><tr><th>package</th><th>kind</th><th>publisher</th><th>version · installs · stars</th><th>submitted</th><th></th></tr></thead>
96 <tbody>${rows}</tbody></table></div>`,
97 userNav(viewer),
98 );
99 }
100
100 lines TYPESCRIPT