@monkey-mini-app/dsh-mini-app 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cordis.patch.yml +6 -0
- package/lib/client.d.cts +35 -0
- package/lib/client.js +5462 -0
- package/lib/index.d.ts +140 -0
- package/lib/index.js +1094 -0
- package/package.json +90 -0
- package/skills/monkey-mini-app/SKILL.md +209 -0
- package/skills/monkey-mini-app/references/catalog.md +99 -0
- package/skills/monkey-mini-app/references/contracts/activity-feed.md +24 -0
- package/skills/monkey-mini-app/references/contracts/app-shell.md +28 -0
- package/skills/monkey-mini-app/references/contracts/attachment-gallery.md +22 -0
- package/skills/monkey-mini-app/references/contracts/autocomplete.md +16 -0
- package/skills/monkey-mini-app/references/contracts/cascader.md +25 -0
- package/skills/monkey-mini-app/references/contracts/code-block.md +15 -0
- package/skills/monkey-mini-app/references/contracts/code-editor.md +17 -0
- package/skills/monkey-mini-app/references/contracts/color-picker.md +14 -0
- package/skills/monkey-mini-app/references/contracts/comment-thread.md +24 -0
- package/skills/monkey-mini-app/references/contracts/confirm-dialog.md +19 -0
- package/skills/monkey-mini-app/references/contracts/copyable.md +13 -0
- package/skills/monkey-mini-app/references/contracts/currency-input.md +15 -0
- package/skills/monkey-mini-app/references/contracts/data-grid.md +87 -0
- package/skills/monkey-mini-app/references/contracts/date-picker.md +17 -0
- package/skills/monkey-mini-app/references/contracts/date-range-picker.md +26 -0
- package/skills/monkey-mini-app/references/contracts/date-time-picker.md +18 -0
- package/skills/monkey-mini-app/references/contracts/date-time-range-picker.md +24 -0
- package/skills/monkey-mini-app/references/contracts/description-list.md +13 -0
- package/skills/monkey-mini-app/references/contracts/detail-panel.md +27 -0
- package/skills/monkey-mini-app/references/contracts/diff-viewer.md +16 -0
- package/skills/monkey-mini-app/references/contracts/donut-chart.md +31 -0
- package/skills/monkey-mini-app/references/contracts/duration-input.md +14 -0
- package/skills/monkey-mini-app/references/contracts/env-badge.md +13 -0
- package/skills/monkey-mini-app/references/contracts/event-calendar.md +50 -0
- package/skills/monkey-mini-app/references/contracts/file-dropzone.md +14 -0
- package/skills/monkey-mini-app/references/contracts/file-tree.md +23 -0
- package/skills/monkey-mini-app/references/contracts/filter-bar.md +26 -0
- package/skills/monkey-mini-app/references/contracts/gantt.md +24 -0
- package/skills/monkey-mini-app/references/contracts/gauge.md +14 -0
- package/skills/monkey-mini-app/references/contracts/jira-wiki.md +53 -0
- package/skills/monkey-mini-app/references/contracts/jql-input.md +37 -0
- package/skills/monkey-mini-app/references/contracts/json-viewer.md +13 -0
- package/skills/monkey-mini-app/references/contracts/kanban-issue-panel.md +45 -0
- package/skills/monkey-mini-app/references/contracts/kanban.md +64 -0
- package/skills/monkey-mini-app/references/contracts/log-viewer.md +13 -0
- package/skills/monkey-mini-app/references/contracts/markdown-editor.md +24 -0
- package/skills/monkey-mini-app/references/contracts/markdown.md +14 -0
- package/skills/monkey-mini-app/references/contracts/mini-calendar.md +15 -0
- package/skills/monkey-mini-app/references/contracts/notification-center.md +23 -0
- package/skills/monkey-mini-app/references/contracts/number-field.md +17 -0
- package/skills/monkey-mini-app/references/contracts/page-header.md +25 -0
- package/skills/monkey-mini-app/references/contracts/password-field.md +14 -0
- package/skills/monkey-mini-app/references/contracts/phone-input.md +14 -0
- package/skills/monkey-mini-app/references/contracts/progress-ring.md +13 -0
- package/skills/monkey-mini-app/references/contracts/radar-chart.md +23 -0
- package/skills/monkey-mini-app/references/contracts/rating.md +15 -0
- package/skills/monkey-mini-app/references/contracts/relative-date-picker.md +24 -0
- package/skills/monkey-mini-app/references/contracts/request-inspector.md +16 -0
- package/skills/monkey-mini-app/references/contracts/rich-text-editor.md +15 -0
- package/skills/monkey-mini-app/references/contracts/run-timeline.md +24 -0
- package/skills/monkey-mini-app/references/contracts/scrollspy.md +13 -0
- package/skills/monkey-mini-app/references/contracts/search-input.md +17 -0
- package/skills/monkey-mini-app/references/contracts/severity-chip.md +13 -0
- package/skills/monkey-mini-app/references/contracts/slider-range.md +16 -0
- package/skills/monkey-mini-app/references/contracts/sortable-list.md +23 -0
- package/skills/monkey-mini-app/references/contracts/sparkline.md +13 -0
- package/skills/monkey-mini-app/references/contracts/stacked-bar-chart.md +23 -0
- package/skills/monkey-mini-app/references/contracts/stat-card.md +27 -0
- package/skills/monkey-mini-app/references/contracts/status-badge.md +24 -0
- package/skills/monkey-mini-app/references/contracts/stepper.md +26 -0
- package/skills/monkey-mini-app/references/contracts/tag-input.md +15 -0
- package/skills/monkey-mini-app/references/contracts/terminal.md +13 -0
- package/skills/monkey-mini-app/references/contracts/test-step-list.md +23 -0
- package/skills/monkey-mini-app/references/contracts/time-picker.md +18 -0
- package/skills/monkey-mini-app/references/contracts/time-range-picker.md +23 -0
- package/skills/monkey-mini-app/references/contracts/time-wheel.md +29 -0
- package/skills/monkey-mini-app/references/contracts/timeline.md +24 -0
- package/skills/monkey-mini-app/references/contracts/timezone-select.md +27 -0
- package/skills/monkey-mini-app/references/contracts/transfer.md +24 -0
- package/skills/monkey-mini-app/references/contracts/tree-view.md +23 -0
- package/skills/monkey-mini-app/references/contracts/trend-card.md +26 -0
- package/skills/monkey-mini-app/references/contracts/user-picker.md +24 -0
- package/skills/monkey-mini-app/references/ctx.md +137 -0
- package/skills/monkey-mini-app/references/history.md +10 -0
- package/skills/monkey-mini-app/references/icons.md +83 -0
- package/skills/monkey-mini-app/references/llm-json.md +69 -0
- package/skills/monkey-mini-app/references/loader.md +30 -0
- package/skills/monkey-mini-app/references/test.md +30 -0
- package/skills/monkey-mini-app/references/tools.md +21 -0
- package/skills/monkey-mini-app/references/troubleshoot.md +12 -0
- package/skills/monkey-mini-app/templates/README.md +24 -0
- package/skills/monkey-mini-app/templates/agentrun/main.api.ts +78 -0
- package/skills/monkey-mini-app/templates/agentrun/manifest.json +9 -0
- package/skills/monkey-mini-app/templates/agentrun/ui.tsx +125 -0
- package/skills/monkey-mini-app/templates/insights/lib/sample.ts +17 -0
- package/skills/monkey-mini-app/templates/insights/main.api.ts +101 -0
- package/skills/monkey-mini-app/templates/insights/manifest.json +9 -0
- package/skills/monkey-mini-app/templates/insights/ui.tsx +138 -0
- package/skills/monkey-mini-app/templates/jira/main.api.ts +115 -0
- package/skills/monkey-mini-app/templates/jira/manifest.json +9 -0
- package/skills/monkey-mini-app/templates/jira/ui.tsx +318 -0
- package/skills/monkey-mini-app/templates/minimal/main.api.ts +16 -0
- package/skills/monkey-mini-app/templates/minimal/manifest.json +8 -0
- package/skills/monkey-mini-app/templates/minimal/ui.tsx +74 -0
- package/skills/monkey-mini-app/templates/monitor/main.api.ts +91 -0
- package/skills/monkey-mini-app/templates/monitor/manifest.json +9 -0
- package/skills/monkey-mini-app/templates/monitor/ui.tsx +176 -0
- package/skills/monkey-mini-app/templates/review/main.api.ts +68 -0
- package/skills/monkey-mini-app/templates/review/manifest.json +9 -0
- package/skills/monkey-mini-app/templates/review/ui.tsx +159 -0
- package/skills/monkey-mini-app/templates/todo/main.api.ts +83 -0
- package/skills/monkey-mini-app/templates/todo/manifest.json +8 -0
- package/skills/monkey-mini-app/templates/todo/ui.tsx +177 -0
- package/tsup.config.ts +68 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
AppShell,
|
|
5
|
+
Badge,
|
|
6
|
+
Button,
|
|
7
|
+
Card,
|
|
8
|
+
CardContent,
|
|
9
|
+
CardDescription,
|
|
10
|
+
CardHeader,
|
|
11
|
+
CardTitle,
|
|
12
|
+
Icon,
|
|
13
|
+
PageHeader,
|
|
14
|
+
Progress,
|
|
15
|
+
Sparkline,
|
|
16
|
+
StatCard,
|
|
17
|
+
Table,
|
|
18
|
+
TableBody,
|
|
19
|
+
TableCell,
|
|
20
|
+
TableHead,
|
|
21
|
+
TableHeader,
|
|
22
|
+
TableRow,
|
|
23
|
+
} from "@monkey-mini-app/ui";
|
|
24
|
+
import { useDashboardApi } from "@monkeyagent/host";
|
|
25
|
+
|
|
26
|
+
type Snapshot = {
|
|
27
|
+
hostname?: string;
|
|
28
|
+
platform?: string;
|
|
29
|
+
arch?: string;
|
|
30
|
+
loadavg?: Record<string, number>;
|
|
31
|
+
uptimeSec?: number;
|
|
32
|
+
collectedAt: number;
|
|
33
|
+
cpu: { model: string; count: number };
|
|
34
|
+
processes: { pid: string; cpu: number; mem: number; name: string }[];
|
|
35
|
+
memory: { total: number; used: number; free: number; usedPct: number };
|
|
36
|
+
disk: { total: string; used: string; free: string; usedPct: number } | null;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default function Ui() {
|
|
40
|
+
const { call } = useDashboardApi();
|
|
41
|
+
const [snap, setSnap] = useState<Snapshot | null>(null);
|
|
42
|
+
const [error, setError] = useState<string | null>(null);
|
|
43
|
+
const [memHist, setMemHist] = useState<number[]>([]);
|
|
44
|
+
const [loadHist, setLoadHist] = useState<number[]>([]);
|
|
45
|
+
|
|
46
|
+
// ⭐ 关键:轮询 —— 每 2s 拉一次;页面隐藏(或在后台)就停止,避免空跑。
|
|
47
|
+
// busy 防止上一帧未回再发下一帧;alive 防组件卸载后 setState。
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
let alive = true;
|
|
50
|
+
let busy = false;
|
|
51
|
+
async function tick() {
|
|
52
|
+
if (busy) return;
|
|
53
|
+
busy = true;
|
|
54
|
+
try {
|
|
55
|
+
const next = (await call("getSnapshot", {})) as Snapshot;
|
|
56
|
+
if (!alive) return;
|
|
57
|
+
setSnap(next);
|
|
58
|
+
setError(null);
|
|
59
|
+
setMemHist((prev) => prev.concat(next.memory.usedPct).slice(-48));
|
|
60
|
+
const load = Number(next.loadavg?.["1m"] ?? 0);
|
|
61
|
+
setLoadHist((prev) => prev.concat(load).slice(-48));
|
|
62
|
+
} catch (e) {
|
|
63
|
+
if (alive) setError(String((e as Error)?.message || e));
|
|
64
|
+
} finally {
|
|
65
|
+
busy = false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
void tick();
|
|
69
|
+
const id = setInterval(() => void tick(), 2000);
|
|
70
|
+
const stop = () => {
|
|
71
|
+
if (document.hidden) clearInterval(id);
|
|
72
|
+
};
|
|
73
|
+
document.addEventListener("visibilitychange", stop);
|
|
74
|
+
return () => {
|
|
75
|
+
alive = false;
|
|
76
|
+
clearInterval(id);
|
|
77
|
+
document.removeEventListener("visibilitychange", stop);
|
|
78
|
+
};
|
|
79
|
+
}, [call]);
|
|
80
|
+
|
|
81
|
+
const procs = snap?.processes ?? [];
|
|
82
|
+
const load = snap?.loadavg;
|
|
83
|
+
const loadMax = Math.max(4, ...(loadHist.length ? loadHist : [1]));
|
|
84
|
+
const memPct = snap?.memory.usedPct ?? 0;
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<AppShell header={<PageHeader title="系统监控" description={snap ? `${snap.hostname ?? "本机"} · 每 2 秒刷新` : "正在读取本机指标…"} />}>
|
|
88
|
+
<div className="flex h-full min-h-0 flex-col gap-3 overflow-y-auto">
|
|
89
|
+
{error && <p className="text-sm" style={{ color: "var(--destructive)" }}>{error}</p>}
|
|
90
|
+
|
|
91
|
+
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
|
92
|
+
<StatCard title="CPU" value={snap ? `${snap.cpu.count} 核` : "—"}>
|
|
93
|
+
<span className="text-xs text-muted-foreground">{snap?.cpu.model || ""}</span>
|
|
94
|
+
</StatCard>
|
|
95
|
+
<StatCard title="平均负载" value={load ? Number(load["1m"]).toFixed(2) : "—"} delta={snap ? "1 / 5 / 15 分钟" : ""}>
|
|
96
|
+
<span className="text-xs text-muted-foreground">已运行 {fmtUptime(snap?.uptimeSec ?? 0)}</span>
|
|
97
|
+
</StatCard>
|
|
98
|
+
<StatCard title="内存" value={`${memPct}%`} delta={snap ? `${fmtBytes(snap.memory.used)} / ${fmtBytes(snap.memory.total)}` : ""} />
|
|
99
|
+
<StatCard title="磁盘 /" value={snap?.disk ? `${snap.disk.usedPct}%` : "—"} delta={snap?.disk ? `${snap.disk.used} / ${snap.disk.total}` : ""} />
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div className="grid gap-3 md:grid-cols-2">
|
|
103
|
+
<Card>
|
|
104
|
+
<CardHeader>
|
|
105
|
+
<CardTitle>内存压力 (%)</CardTitle>
|
|
106
|
+
<CardDescription>最近 {memHist.length} 次 · 每 2 秒</CardDescription>
|
|
107
|
+
</CardHeader>
|
|
108
|
+
<CardContent className="flex flex-col gap-3">
|
|
109
|
+
<Sparkline data={memHist.map((v) => ({ value: v }))} />
|
|
110
|
+
<Progress value={memPct} />
|
|
111
|
+
</CardContent>
|
|
112
|
+
</Card>
|
|
113
|
+
<Card>
|
|
114
|
+
<CardHeader>
|
|
115
|
+
<CardTitle>负载(1 分钟)</CardTitle>
|
|
116
|
+
<CardDescription>峰值 {loadMax.toFixed(2)}</CardDescription>
|
|
117
|
+
</CardHeader>
|
|
118
|
+
<CardContent>
|
|
119
|
+
<Sparkline data={loadHist.map((v) => ({ value: v }))} />
|
|
120
|
+
</CardContent>
|
|
121
|
+
</Card>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<Card>
|
|
125
|
+
<CardHeader>
|
|
126
|
+
<CardTitle>进程</CardTitle>
|
|
127
|
+
<CardDescription>按 CPU 排序 · 跟随刷新</CardDescription>
|
|
128
|
+
</CardHeader>
|
|
129
|
+
<CardContent>
|
|
130
|
+
{!procs.length ? (
|
|
131
|
+
<span className="text-sm text-muted-foreground">还没有进程列表。</span>
|
|
132
|
+
) : (
|
|
133
|
+
<Table>
|
|
134
|
+
<TableHeader>
|
|
135
|
+
<TableRow>
|
|
136
|
+
<TableHead className="w-20">PID</TableHead>
|
|
137
|
+
<TableHead className="w-24">CPU%</TableHead>
|
|
138
|
+
<TableHead className="w-24">内存%</TableHead>
|
|
139
|
+
<TableHead>进程</TableHead>
|
|
140
|
+
</TableRow>
|
|
141
|
+
</TableHeader>
|
|
142
|
+
<TableBody>
|
|
143
|
+
{procs.map((p) => (
|
|
144
|
+
<TableRow key={p.pid}>
|
|
145
|
+
<TableCell className="font-mono">{p.pid}</TableCell>
|
|
146
|
+
<TableCell>{p.cpu.toFixed(1)}</TableCell>
|
|
147
|
+
<TableCell>{p.mem.toFixed(1)}</TableCell>
|
|
148
|
+
<TableCell className="max-w-[420px] truncate">{p.name}</TableCell>
|
|
149
|
+
</TableRow>
|
|
150
|
+
))}
|
|
151
|
+
</TableBody>
|
|
152
|
+
</Table>
|
|
153
|
+
)}
|
|
154
|
+
</CardContent>
|
|
155
|
+
</Card>
|
|
156
|
+
</div>
|
|
157
|
+
</AppShell>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function fmtBytes(n: number): string {
|
|
162
|
+
if (!Number.isFinite(n) || n <= 0) return "—";
|
|
163
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
164
|
+
let i = 0;
|
|
165
|
+
while (n >= 1024 && i < units.length - 1) {
|
|
166
|
+
n /= 1024;
|
|
167
|
+
i++;
|
|
168
|
+
}
|
|
169
|
+
return n.toFixed(i ? 1 : 0) + units[i];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function fmtUptime(sec: number): string {
|
|
173
|
+
const h = Math.floor(sec / 3600);
|
|
174
|
+
const m = Math.floor((sec % 3600) / 60);
|
|
175
|
+
return h ? `${h}h ${m}m` : `${m}m`;
|
|
176
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { defineDashboard } from "@monkeyagent/dashboard";
|
|
2
|
+
|
|
3
|
+
// ⭐ 关键:StatusBadge / health 类组件认的都是小写状态词(pass|fail|blocked|flaky|running|pending),
|
|
4
|
+
// 不要用 "PASS"/"FAIL" 之类大写 —— 会落到默认 pending 灰色、颜色全错。
|
|
5
|
+
type CaseStatus = "pass" | "fail" | "blocked";
|
|
6
|
+
type TestCase = { id: string; name: string; status: CaseStatus; duration: number };
|
|
7
|
+
type LogLine = { level: "info" | "debug" | "warn" | "error"; text: string };
|
|
8
|
+
|
|
9
|
+
const DEFAULT_BEFORE = `function fetchWithRetry(url) {
|
|
10
|
+
const res = fetch(url); // 网络错误直接抛,无重试
|
|
11
|
+
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
12
|
+
return res;
|
|
13
|
+
}`;
|
|
14
|
+
|
|
15
|
+
const DEFAULT_AFTER = `function fetchWithRetry(url, retries = 2) {
|
|
16
|
+
let lastErr;
|
|
17
|
+
for (let i = 0; i <= retries; i++) {
|
|
18
|
+
try {
|
|
19
|
+
const res = fetch(url);
|
|
20
|
+
if (res.ok) return res;
|
|
21
|
+
lastErr = new Error("HTTP " + res.status);
|
|
22
|
+
} catch (e) {
|
|
23
|
+
lastErr = e;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
throw lastErr;
|
|
27
|
+
}`;
|
|
28
|
+
|
|
29
|
+
const CASES: TestCase[] = [
|
|
30
|
+
{ id: "TC-01", name: "正常请求返回", status: "pass", duration: 42 },
|
|
31
|
+
{ id: "TC-02", name: "一次失败后重试成功", status: "pass", duration: 310 },
|
|
32
|
+
{ id: "TC-03", name: "重试耗尽抛错", status: "fail", duration: 1204 },
|
|
33
|
+
{ id: "TC-04", name: "非 2xx 状态码处理", status: "blocked", duration: 0 },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export default defineDashboard({
|
|
37
|
+
name: "修复基准",
|
|
38
|
+
description: "编辑 → diff 对比 → 用例表格,改完写回",
|
|
39
|
+
api: {
|
|
40
|
+
async get(ctx) {
|
|
41
|
+
const saved = await ctx.storage.get("after");
|
|
42
|
+
return {
|
|
43
|
+
before: DEFAULT_BEFORE,
|
|
44
|
+
after: typeof saved === "string" && saved ? saved : DEFAULT_AFTER,
|
|
45
|
+
cases: CASES,
|
|
46
|
+
logs: [
|
|
47
|
+
{ level: "info", text: "[10:00:01] 复现:无网络时 fetchWithRetry 直接抛错" },
|
|
48
|
+
{ level: "info", text: "[10:00:02] 分析:缺少异常捕获与重试逻辑" },
|
|
49
|
+
{ level: "debug", text: "[10:00:03] 方案:增加 retries 参数 + try/catch 循环" },
|
|
50
|
+
{ level: "warn", text: "[10:00:04] 回归结果:2 通过 / 1 失败 / 1 阻塞" },
|
|
51
|
+
] as LogLine[],
|
|
52
|
+
savedAt: (await ctx.storage.get("savedAt")) || 0,
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
async save(ctx, args?: { after?: string }) {
|
|
56
|
+
const code = String(args?.after ?? "").trim();
|
|
57
|
+
if (!code) throw new Error("代码不能为空");
|
|
58
|
+
await ctx.storage.set("after", code);
|
|
59
|
+
await ctx.storage.set("savedAt", Date.now());
|
|
60
|
+
return { ok: true, savedAt: Date.now() };
|
|
61
|
+
},
|
|
62
|
+
async reset(ctx) {
|
|
63
|
+
await ctx.storage.set("after", DEFAULT_AFTER);
|
|
64
|
+
await ctx.storage.set("savedAt", 0);
|
|
65
|
+
return { ok: true, savedAt: 0 };
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
AppShell,
|
|
5
|
+
Badge,
|
|
6
|
+
Button,
|
|
7
|
+
Card,
|
|
8
|
+
CardContent,
|
|
9
|
+
CardDescription,
|
|
10
|
+
CardHeader,
|
|
11
|
+
CardTitle,
|
|
12
|
+
CodeEditor,
|
|
13
|
+
type ColumnDef,
|
|
14
|
+
DataGrid,
|
|
15
|
+
DiffViewer,
|
|
16
|
+
FilterBar,
|
|
17
|
+
Icon,
|
|
18
|
+
LogViewer,
|
|
19
|
+
PageHeader,
|
|
20
|
+
StatusBadge,
|
|
21
|
+
} from "@monkey-mini-app/ui";
|
|
22
|
+
import { useDashboardApi } from "@monkeyagent/host";
|
|
23
|
+
|
|
24
|
+
type LogLine = { level: string; text: string };
|
|
25
|
+
type TestCase = { id: string; name: string; status: "pass" | "fail" | "blocked"; duration: number };
|
|
26
|
+
|
|
27
|
+
function fmtTime(ts: number) {
|
|
28
|
+
if (!ts) return "尚未保存";
|
|
29
|
+
const d = new Date(ts);
|
|
30
|
+
const pad = (n: number) => (n < 10 ? "0" + n : String(n));
|
|
31
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default function Ui() {
|
|
35
|
+
const { call } = useDashboardApi();
|
|
36
|
+
const [before, setBefore] = useState("");
|
|
37
|
+
const [after, setAfter] = useState("");
|
|
38
|
+
const [cases, setCases] = useState<TestCase[]>([]);
|
|
39
|
+
const [logs, setLogs] = useState<LogLine[]>([]);
|
|
40
|
+
const [savedAt, setSavedAt] = useState(0);
|
|
41
|
+
const [status, setStatus] = useState<"all" | TestCase["status"]>("all");
|
|
42
|
+
const [dirty, setDirty] = useState(false);
|
|
43
|
+
const [error, setError] = useState<string | null>(null);
|
|
44
|
+
|
|
45
|
+
const refresh = useCallback(async () => {
|
|
46
|
+
setError(null);
|
|
47
|
+
try {
|
|
48
|
+
const pack = (await call("get", {})) as { before: string; after: string; cases: TestCase[]; logs: LogLine[]; savedAt: number };
|
|
49
|
+
setBefore(pack?.before ?? "");
|
|
50
|
+
setAfter(pack?.after ?? "");
|
|
51
|
+
setCases(Array.isArray(pack?.cases) ? pack.cases : []);
|
|
52
|
+
setLogs(Array.isArray(pack?.logs) ? pack.logs : []);
|
|
53
|
+
setSavedAt(pack?.savedAt ?? 0);
|
|
54
|
+
setDirty(false);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
setError(String((e as Error)?.message || e));
|
|
57
|
+
}
|
|
58
|
+
}, [call]);
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
void refresh();
|
|
62
|
+
}, [refresh]);
|
|
63
|
+
|
|
64
|
+
async function act(job: () => Promise<unknown>) {
|
|
65
|
+
setError(null);
|
|
66
|
+
try {
|
|
67
|
+
await job();
|
|
68
|
+
await refresh();
|
|
69
|
+
} catch (e) {
|
|
70
|
+
setError(String((e as Error)?.message || e));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const filtered = status === "all" ? cases : cases.filter((c) => c.status === status);
|
|
75
|
+
|
|
76
|
+
// ⭐ 关键:DataGrid 的列用 meta 声明排序/列搜索;StatusBadge 认小写状态
|
|
77
|
+
const columns: ColumnDef<TestCase>[] = [
|
|
78
|
+
{ accessorKey: "id", header: "ID", meta: { sort: true, search: "text" } },
|
|
79
|
+
{ accessorKey: "name", header: "用例", meta: { sort: true, search: "text" } },
|
|
80
|
+
{
|
|
81
|
+
accessorKey: "status",
|
|
82
|
+
header: "状态",
|
|
83
|
+
meta: { sort: true, search: "select" },
|
|
84
|
+
cell: ({ getValue }) => <StatusBadge status={String(getValue())} />,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
accessorKey: "duration",
|
|
88
|
+
header: "耗时 (ms)",
|
|
89
|
+
meta: { sort: true, search: "number" },
|
|
90
|
+
cell: ({ getValue }) => String(getValue()),
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<AppShell header={<PageHeader title="修复基准" description="编辑 → diff → 用例表格,改完写回" />}>
|
|
96
|
+
<div className="flex h-full min-h-0 flex-col gap-3 overflow-y-auto">
|
|
97
|
+
{error && <p className="text-sm" style={{ color: "var(--destructive)" }}>{error}</p>}
|
|
98
|
+
|
|
99
|
+
<Card>
|
|
100
|
+
<CardHeader>
|
|
101
|
+
<CardTitle>编辑</CardTitle>
|
|
102
|
+
<CardDescription>修改后保存,diff 实时刷新 · {fmtTime(savedAt)}{dirty ? " · 未保存" : ""}</CardDescription>
|
|
103
|
+
</CardHeader>
|
|
104
|
+
<CardContent>
|
|
105
|
+
<CodeEditor value={after} onChange={(v) => { setAfter(v); setDirty(true); }} language="ts" height="240px" />
|
|
106
|
+
</CardContent>
|
|
107
|
+
</Card>
|
|
108
|
+
|
|
109
|
+
<Card>
|
|
110
|
+
<CardHeader>
|
|
111
|
+
<CardTitle>Diff 对比</CardTitle>
|
|
112
|
+
<CardDescription>before → after</CardDescription>
|
|
113
|
+
</CardHeader>
|
|
114
|
+
<CardContent>
|
|
115
|
+
{/* ⭐ 关键:DiffViewer 用 original/modified(不是老版的 oldText/newText) */}
|
|
116
|
+
<DiffViewer original={before} modified={after} mode="unified" fileName="fetchWithRetry.ts" />
|
|
117
|
+
</CardContent>
|
|
118
|
+
</Card>
|
|
119
|
+
|
|
120
|
+
<Card>
|
|
121
|
+
<CardHeader>
|
|
122
|
+
<CardTitle>用例结果</CardTitle>
|
|
123
|
+
<CardDescription>排序 / 列搜索 / 分页自带</CardDescription>
|
|
124
|
+
</CardHeader>
|
|
125
|
+
<CardContent>
|
|
126
|
+
<FilterBar>
|
|
127
|
+
{( ["all", "pass", "fail", "blocked"] as const ).map((s) => (
|
|
128
|
+
<Button key={s} size="sm" variant={status === s ? "default" : "outline"} onClick={() => setStatus(s)}>
|
|
129
|
+
{s === "all" ? "全部" : s}
|
|
130
|
+
</Button>
|
|
131
|
+
))}
|
|
132
|
+
<div className="ml-auto flex gap-2">
|
|
133
|
+
<Button size="sm" variant="outline" onClick={() => void act(() => call("reset", {}))}>
|
|
134
|
+
<Icon.RotateCcw size={16} strokeWidth={2} /> 重置
|
|
135
|
+
</Button>
|
|
136
|
+
<Button size="sm" onClick={() => void act(() => call("save", { after }))}>
|
|
137
|
+
<Icon.Save size={16} strokeWidth={2} /> 保存修复
|
|
138
|
+
</Button>
|
|
139
|
+
</div>
|
|
140
|
+
</FilterBar>
|
|
141
|
+
<div className="mt-3">
|
|
142
|
+
<DataGrid columns={columns} data={filtered} pageSize={8} />
|
|
143
|
+
</div>
|
|
144
|
+
</CardContent>
|
|
145
|
+
</Card>
|
|
146
|
+
|
|
147
|
+
<Card>
|
|
148
|
+
<CardHeader>
|
|
149
|
+
<CardTitle>日志</CardTitle>
|
|
150
|
+
<CardDescription>级别着色</CardDescription>
|
|
151
|
+
</CardHeader>
|
|
152
|
+
<CardContent>
|
|
153
|
+
<LogViewer lines={logs.map((l) => `[${l.level}] ${l.text}`)} />
|
|
154
|
+
</CardContent>
|
|
155
|
+
</Card>
|
|
156
|
+
</div>
|
|
157
|
+
</AppShell>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { defineDashboard } from "@monkeyagent/dashboard";
|
|
2
|
+
|
|
3
|
+
type TodoItem = { id: string; title: string; done: boolean; createdAt: number };
|
|
4
|
+
type Filter = "all" | "active" | "done";
|
|
5
|
+
|
|
6
|
+
function uid() {
|
|
7
|
+
return "t_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// ⭐ 关键:本地数据用 ctx.storage(单表 get/set);多表用 ctx.storage.table(name)。
|
|
11
|
+
// todo 用主表存"进行中的任务",把已完成的归档到第二张表体现分表。
|
|
12
|
+
async function loadItems(ctx): Promise<TodoItem[]> {
|
|
13
|
+
const items = await ctx.storage.get("items");
|
|
14
|
+
return Array.isArray(items) ? items : [];
|
|
15
|
+
}
|
|
16
|
+
async function saveItems(ctx, items: TodoItem[]) {
|
|
17
|
+
await ctx.storage.set("items", items);
|
|
18
|
+
}
|
|
19
|
+
function statsOf(items: TodoItem[]) {
|
|
20
|
+
const done = items.filter((i) => i.done).length;
|
|
21
|
+
return { total: items.length, active: items.length - done, done };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default defineDashboard({
|
|
25
|
+
name: "待办",
|
|
26
|
+
description: "本地待办:CRUD + 状态筛选 + 归档到第二张表",
|
|
27
|
+
api: {
|
|
28
|
+
async list(ctx, args?: { filter?: Filter }) {
|
|
29
|
+
const all = await loadItems(ctx);
|
|
30
|
+
return {
|
|
31
|
+
items: args?.filter === "active" ? all.filter((i) => !i.done) : args?.filter === "done" ? all.filter((i) => i.done) : all,
|
|
32
|
+
stats: statsOf(all),
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
async add(ctx, args?: { title?: string }) {
|
|
36
|
+
const title = String(args?.title ?? "").trim();
|
|
37
|
+
if (!title) throw new Error("请填写标题");
|
|
38
|
+
const items = await loadItems(ctx);
|
|
39
|
+
const item: TodoItem = { id: uid(), title, done: false, createdAt: Date.now() };
|
|
40
|
+
items.unshift(item);
|
|
41
|
+
await saveItems(ctx, items);
|
|
42
|
+
return item;
|
|
43
|
+
},
|
|
44
|
+
async toggle(ctx, args?: { id?: string }) {
|
|
45
|
+
const id = args?.id;
|
|
46
|
+
if (!id) throw new Error("缺少 id");
|
|
47
|
+
const items = await loadItems(ctx);
|
|
48
|
+
let found = false;
|
|
49
|
+
const next = items.map((it) => {
|
|
50
|
+
if (it.id !== id) return it;
|
|
51
|
+
found = true;
|
|
52
|
+
return { ...it, done: !it.done };
|
|
53
|
+
});
|
|
54
|
+
if (!found) throw new Error("任务不存在");
|
|
55
|
+
await saveItems(ctx, next);
|
|
56
|
+
return next.find((it) => it.id === id);
|
|
57
|
+
},
|
|
58
|
+
async remove(ctx, args?: { id?: string }) {
|
|
59
|
+
const items = await loadItems(ctx);
|
|
60
|
+
const next = items.filter((it) => it.id !== args?.id);
|
|
61
|
+
if (next.length === items.length) throw new Error("任务不存在");
|
|
62
|
+
await saveItems(ctx, next);
|
|
63
|
+
return { ok: true };
|
|
64
|
+
},
|
|
65
|
+
async clearDone(ctx) {
|
|
66
|
+
const items = await loadItems(ctx);
|
|
67
|
+
await saveItems(ctx, items.filter((i) => !i.done));
|
|
68
|
+
return { ok: true };
|
|
69
|
+
},
|
|
70
|
+
// ⭐ 关键:ctx.storage.table("archive") 是另一张独立表 —— 演示多表。
|
|
71
|
+
// table() 返回与 storage 同样的 get/set/delete/clear。
|
|
72
|
+
async archive(ctx) {
|
|
73
|
+
const items = await loadItems(ctx);
|
|
74
|
+
const done = items.filter((i) => i.done);
|
|
75
|
+
if (!done.length) return { ok: true, archived: 0 };
|
|
76
|
+
const archive = ctx.storage.table("archive");
|
|
77
|
+
const prev = (await archive.get("done")) ?? [];
|
|
78
|
+
await archive.set("done", [...prev, ...done]);
|
|
79
|
+
await saveItems(ctx, items.filter((i) => !i.done));
|
|
80
|
+
return { ok: true, archived: done.length };
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
});
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
AppShell,
|
|
5
|
+
Badge,
|
|
6
|
+
Button,
|
|
7
|
+
Card,
|
|
8
|
+
CardContent,
|
|
9
|
+
CardHeader,
|
|
10
|
+
CardTitle,
|
|
11
|
+
Checkbox,
|
|
12
|
+
Empty,
|
|
13
|
+
EmptyDescription,
|
|
14
|
+
EmptyHeader,
|
|
15
|
+
EmptyTitle,
|
|
16
|
+
FilterBar,
|
|
17
|
+
Icon,
|
|
18
|
+
Input,
|
|
19
|
+
PageHeader,
|
|
20
|
+
Table,
|
|
21
|
+
TableBody,
|
|
22
|
+
TableCell,
|
|
23
|
+
TableHead,
|
|
24
|
+
TableHeader,
|
|
25
|
+
TableRow,
|
|
26
|
+
} from "@monkey-mini-app/ui";
|
|
27
|
+
import { useDashboardApi } from "@monkeyagent/host";
|
|
28
|
+
|
|
29
|
+
type Filter = "all" | "active" | "done";
|
|
30
|
+
type TodoItem = { id: string; title: string; done: boolean; createdAt: number };
|
|
31
|
+
type Stats = { total: number; active: number; done: number };
|
|
32
|
+
|
|
33
|
+
const FILTERS: { id: Filter; label: string }[] = [
|
|
34
|
+
{ id: "all", label: "全部" },
|
|
35
|
+
{ id: "active", label: "进行中" },
|
|
36
|
+
{ id: "done", label: "已完成" },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
export default function Ui() {
|
|
40
|
+
const { call } = useDashboardApi();
|
|
41
|
+
const [filter, setFilter] = useState<Filter>("all");
|
|
42
|
+
const [draft, setDraft] = useState("");
|
|
43
|
+
const [items, setItems] = useState<TodoItem[]>([]);
|
|
44
|
+
const [stats, setStats] = useState<Stats>({ total: 0, active: 0, done: 0 });
|
|
45
|
+
const [loading, setLoading] = useState(true);
|
|
46
|
+
const [error, setError] = useState<string | null>(null);
|
|
47
|
+
|
|
48
|
+
// ⭐ 关键:refresh 依赖 filter,filter 变就重拉;用 useCallback 避免无限循环
|
|
49
|
+
const refresh = useCallback(async () => {
|
|
50
|
+
setLoading(true);
|
|
51
|
+
setError(null);
|
|
52
|
+
try {
|
|
53
|
+
const pack = (await call("list", { filter })) as { items: TodoItem[]; stats: Stats };
|
|
54
|
+
setItems(Array.isArray(pack?.items) ? pack.items : []);
|
|
55
|
+
setStats(pack?.stats ?? { total: 0, active: 0, done: 0 });
|
|
56
|
+
} catch (e) {
|
|
57
|
+
setError(String((e as Error)?.message || e));
|
|
58
|
+
} finally {
|
|
59
|
+
setLoading(false);
|
|
60
|
+
}
|
|
61
|
+
}, [call, filter]);
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
void refresh();
|
|
65
|
+
}, [refresh]);
|
|
66
|
+
|
|
67
|
+
async function act(job: () => Promise<unknown>) {
|
|
68
|
+
setError(null);
|
|
69
|
+
try {
|
|
70
|
+
await job();
|
|
71
|
+
await refresh();
|
|
72
|
+
} catch (e) {
|
|
73
|
+
setError(String((e as Error)?.message || e));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function add() {
|
|
78
|
+
const title = draft.trim();
|
|
79
|
+
if (!title) return;
|
|
80
|
+
await act(async () => {
|
|
81
|
+
await call("add", { title });
|
|
82
|
+
setDraft("");
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
// ⭐ 关键:一屏固定 —— AppShell 定高,main 内部滚动,页面不整体滚。
|
|
88
|
+
// 让「内容不被裁成一条缝」+ 侧栏折叠/钉到右侧后仍撑满。
|
|
89
|
+
<AppShell header={<PageHeader title="待办" description="CRUD · 状态筛选 · 归档" />}>
|
|
90
|
+
<div className="flex h-full min-h-0 flex-col gap-3">
|
|
91
|
+
<FilterBar>
|
|
92
|
+
{FILTERS.map((f) => (
|
|
93
|
+
<Button key={f.id} size="sm" variant={filter === f.id ? "default" : "outline"} onClick={() => setFilter(f.id)}>
|
|
94
|
+
{f.label}
|
|
95
|
+
</Button>
|
|
96
|
+
))}
|
|
97
|
+
<div className="ml-auto flex items-center gap-2">
|
|
98
|
+
<Badge variant="secondary">{stats.active} 进行中</Badge>
|
|
99
|
+
<Badge variant="outline">{stats.done} 已完成</Badge>
|
|
100
|
+
<Button size="sm" variant="ghost" onClick={() => void act(() => call("archive", {}))}>
|
|
101
|
+
<Icon.Archive size={16} strokeWidth={2} /> 归档
|
|
102
|
+
</Button>
|
|
103
|
+
</div>
|
|
104
|
+
</FilterBar>
|
|
105
|
+
|
|
106
|
+
<Card className="min-h-0 flex-1 overflow-hidden">
|
|
107
|
+
<CardHeader>
|
|
108
|
+
<CardTitle>添加任务</CardTitle>
|
|
109
|
+
<div className="flex gap-2">
|
|
110
|
+
<Input
|
|
111
|
+
value={draft}
|
|
112
|
+
placeholder="要做什么?回车提交"
|
|
113
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
114
|
+
onKeyDown={(e) => {
|
|
115
|
+
if (e.key === "Enter") void add();
|
|
116
|
+
}}
|
|
117
|
+
className="flex-1"
|
|
118
|
+
/>
|
|
119
|
+
<Button onClick={() => void add()} disabled={!draft.trim()}>
|
|
120
|
+
<Icon.Plus size={16} strokeWidth={2} /> 添加
|
|
121
|
+
</Button>
|
|
122
|
+
</div>
|
|
123
|
+
</CardHeader>
|
|
124
|
+
<CardContent className="flex flex-col min-h-0 flex-1 gap-2 overflow-y-auto">
|
|
125
|
+
{error && <p className="text-sm" style={{ color: "var(--destructive)" }}>{error}</p>}
|
|
126
|
+
{loading && !items.length ? <p className="text-sm text-muted-foreground">加载中…</p> : null}
|
|
127
|
+
{!loading && !items.length ? (
|
|
128
|
+
<Empty>
|
|
129
|
+
<EmptyHeader>
|
|
130
|
+
<EmptyTitle>还没有任务</EmptyTitle>
|
|
131
|
+
<EmptyDescription>上面加一条就开始。</EmptyDescription>
|
|
132
|
+
</EmptyHeader>
|
|
133
|
+
</Empty>
|
|
134
|
+
) : null}
|
|
135
|
+
{items.length ? (
|
|
136
|
+
<Table>
|
|
137
|
+
<TableHeader>
|
|
138
|
+
<TableRow>
|
|
139
|
+
<TableHead className="w-10"></TableHead>
|
|
140
|
+
<TableHead>标题</TableHead>
|
|
141
|
+
<TableHead className="w-20">状态</TableHead>
|
|
142
|
+
<TableHead className="w-20"></TableHead>
|
|
143
|
+
</TableRow>
|
|
144
|
+
</TableHeader>
|
|
145
|
+
<TableBody>
|
|
146
|
+
{items.map((item) => (
|
|
147
|
+
<TableRow key={item.id}>
|
|
148
|
+
<TableCell>
|
|
149
|
+
<Checkbox
|
|
150
|
+
checked={item.done}
|
|
151
|
+
onCheckedChange={() => void act(() => call("toggle", { id: item.id }))}
|
|
152
|
+
/>
|
|
153
|
+
</TableCell>
|
|
154
|
+
<TableCell
|
|
155
|
+
style={{ textDecoration: item.done ? "line-through" : "none", opacity: item.done ? 0.55 : 1 }}
|
|
156
|
+
>
|
|
157
|
+
{item.title}
|
|
158
|
+
</TableCell>
|
|
159
|
+
<TableCell>
|
|
160
|
+
<Badge variant={item.done ? "secondary" : "default"}>{item.done ? "已完成" : "进行中"}</Badge>
|
|
161
|
+
</TableCell>
|
|
162
|
+
<TableCell>
|
|
163
|
+
<Button size="sm" variant="ghost" onClick={() => void act(() => call("remove", { id: item.id }))}>
|
|
164
|
+
<Icon.Trash2 size={16} strokeWidth={2} />
|
|
165
|
+
</Button>
|
|
166
|
+
</TableCell>
|
|
167
|
+
</TableRow>
|
|
168
|
+
))}
|
|
169
|
+
</TableBody>
|
|
170
|
+
</Table>
|
|
171
|
+
) : null}
|
|
172
|
+
</CardContent>
|
|
173
|
+
</Card>
|
|
174
|
+
</div>
|
|
175
|
+
</AppShell>
|
|
176
|
+
);
|
|
177
|
+
}
|