@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,318 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
AppShell,
|
|
5
|
+
Badge,
|
|
6
|
+
Button,
|
|
7
|
+
Card,
|
|
8
|
+
CardContent,
|
|
9
|
+
CardDescription,
|
|
10
|
+
CardHeader,
|
|
11
|
+
CardTitle,
|
|
12
|
+
type ColumnDef,
|
|
13
|
+
DataGrid,
|
|
14
|
+
Dialog,
|
|
15
|
+
DialogContent,
|
|
16
|
+
DialogDescription,
|
|
17
|
+
DialogFooter,
|
|
18
|
+
DialogHeader,
|
|
19
|
+
DialogTitle,
|
|
20
|
+
FilterBar,
|
|
21
|
+
Icon,
|
|
22
|
+
Input,
|
|
23
|
+
Kanban,
|
|
24
|
+
type KanbanCard,
|
|
25
|
+
NativeSelect,
|
|
26
|
+
PageHeader,
|
|
27
|
+
Select,
|
|
28
|
+
SelectContent,
|
|
29
|
+
SelectItem,
|
|
30
|
+
SelectTrigger,
|
|
31
|
+
SelectValue,
|
|
32
|
+
Sheet,
|
|
33
|
+
SheetContent,
|
|
34
|
+
SheetDescription,
|
|
35
|
+
SheetHeader,
|
|
36
|
+
SheetTitle,
|
|
37
|
+
StatCard,
|
|
38
|
+
Tabs,
|
|
39
|
+
TabsList,
|
|
40
|
+
TabsTrigger,
|
|
41
|
+
Textarea,
|
|
42
|
+
} from "@monkey-mini-app/ui";
|
|
43
|
+
import { useDashboardApi } from "@monkeyagent/host";
|
|
44
|
+
|
|
45
|
+
type Issue = {
|
|
46
|
+
key: string;
|
|
47
|
+
type: "Story" | "Task" | "Bug" | "Epic";
|
|
48
|
+
summary: string;
|
|
49
|
+
status: "To Do" | "In Progress" | "Done" | "Blocked";
|
|
50
|
+
priority: "P0" | "P1" | "P2" | "P3";
|
|
51
|
+
assignee: string;
|
|
52
|
+
reporter: string;
|
|
53
|
+
points: number;
|
|
54
|
+
comments: { author: string; text: string; at: number }[];
|
|
55
|
+
createdAt: number;
|
|
56
|
+
updatedAt: number;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const STATUSES: Issue["status"][] = ["To Do", "In Progress", "Done", "Blocked"];
|
|
60
|
+
const ASSIGNEES = ["陈", "李", "王"];
|
|
61
|
+
const STATUS_TONE: Record<Issue["status"], string> = {
|
|
62
|
+
"To Do": "default",
|
|
63
|
+
"In Progress": "secondary",
|
|
64
|
+
Done: "secondary",
|
|
65
|
+
Blocked: "destructive",
|
|
66
|
+
};
|
|
67
|
+
const STATUS_COLOR: Record<Issue["status"], string> = {
|
|
68
|
+
"To Do": "var(--muted-foreground)",
|
|
69
|
+
"In Progress": "var(--primary)",
|
|
70
|
+
Done: "oklch(0.627 0.17 149.2)",
|
|
71
|
+
Blocked: "oklch(0.704 0.191 22.216)",
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export default function Ui() {
|
|
75
|
+
const { call } = useDashboardApi();
|
|
76
|
+
const [issues, setIssues] = useState<Issue[]>([]);
|
|
77
|
+
const [view, setView] = useState<"kanban" | "table">("kanban");
|
|
78
|
+
const [sel, setSel] = useState<Issue | null>(null);
|
|
79
|
+
const [assignee, setAssignee] = useState("all");
|
|
80
|
+
const [summary, setSummary] = useState<{ byStatus: Record<string, number>; total: number } | null>(null);
|
|
81
|
+
const [wlOpen, setWlOpen] = useState(false);
|
|
82
|
+
const [wlNote, setWlNote] = useState("");
|
|
83
|
+
const [wlMinutes, setWlMinutes] = useState(30);
|
|
84
|
+
const [comment, setComment] = useState("");
|
|
85
|
+
const [error, setError] = useState<string | null>(null);
|
|
86
|
+
// ⭐ 关键:主题切换 —— 存 ctx.storage(刷新恢复),live 改 document.documentElement
|
|
87
|
+
const load = useCallback(async () => {
|
|
88
|
+
try {
|
|
89
|
+
const data = (await call("list", {})) as { issues: Issue[] };
|
|
90
|
+
const s = (await call("summary", {})) as { byStatus: Record<string, number>; total: number };
|
|
91
|
+
let list = data.issues || [];
|
|
92
|
+
if (assignee !== "all") list = list.filter((i) => i.assignee === assignee);
|
|
93
|
+
setIssues(list);
|
|
94
|
+
setSummary(s);
|
|
95
|
+
} catch (e) {
|
|
96
|
+
setError(String((e as Error)?.message || e));
|
|
97
|
+
}
|
|
98
|
+
}, [call, assignee]);
|
|
99
|
+
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
void load();
|
|
102
|
+
}, [load]);
|
|
103
|
+
|
|
104
|
+
// 乐观更新 + 落库(后端 update 已持久化)
|
|
105
|
+
async function patch(key: string, p: Partial<Issue>) {
|
|
106
|
+
setSel((prev) => (prev?.key === key ? { ...prev, ...p } : prev));
|
|
107
|
+
await call("update", { key, patch: p });
|
|
108
|
+
void load();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ⭐ 关键:Kanban 拖拽 → onCardsChange 拿到新 cards,按 status 映射回 patch
|
|
112
|
+
const onKanban = (cards: KanbanCard[]) => {
|
|
113
|
+
for (const c of cards) {
|
|
114
|
+
const cur = issues.find((i) => i.key === c.key);
|
|
115
|
+
if (cur && cur.status !== c.columnId) void patch(c.key!, { status: c.columnId as Issue["status"] });
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const kanbanColumns = STATUSES.map((status) => ({ id: status, title: status, limit: undefined as number | undefined }));
|
|
120
|
+
const kanbanCards: KanbanCard[] = issues.map((i) => ({
|
|
121
|
+
id: i.key,
|
|
122
|
+
key: i.key,
|
|
123
|
+
title: i.summary,
|
|
124
|
+
columnId: i.status,
|
|
125
|
+
type: i.type,
|
|
126
|
+
status: i.status,
|
|
127
|
+
assignee: i.assignee,
|
|
128
|
+
tags: [i.priority],
|
|
129
|
+
updated: new Date(i.updatedAt).toLocaleDateString("zh-CN"),
|
|
130
|
+
}));
|
|
131
|
+
|
|
132
|
+
const columns: ColumnDef<Issue>[] = [
|
|
133
|
+
{ accessorKey: "key", header: "键", meta: { sort: true, search: "text" } },
|
|
134
|
+
{ accessorKey: "type", header: "类型", meta: { sort: true, search: "select" } },
|
|
135
|
+
{ accessorKey: "summary", header: "摘要", meta: { sort: true, search: "text" } },
|
|
136
|
+
{ accessorKey: "assignee", header: "负责人", meta: { sort: true, search: "text" } },
|
|
137
|
+
{
|
|
138
|
+
accessorKey: "status",
|
|
139
|
+
header: "状态",
|
|
140
|
+
meta: { sort: true, search: "select" },
|
|
141
|
+
cell: ({ getValue }) => <StatusBadgeMini status={String(getValue())} />,
|
|
142
|
+
},
|
|
143
|
+
{ accessorKey: "priority", header: "优先级", meta: { sort: true, search: "select" } },
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
async function openWorklog() {
|
|
147
|
+
if (!sel) return;
|
|
148
|
+
setWlOpen(true);
|
|
149
|
+
setWlNote("生成中…");
|
|
150
|
+
try {
|
|
151
|
+
const d = (await call("worklogDraft", { key: sel.key })) as { note: string };
|
|
152
|
+
setWlNote(d.note);
|
|
153
|
+
} catch (e) {
|
|
154
|
+
setWlNote("(AI 生成失败,手动填写)" + String((e as Error)?.message || e));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async function saveWorklog() {
|
|
158
|
+
if (!sel) return;
|
|
159
|
+
await call("logWork", { key: sel.key, note: wlNote, minutes: wlMinutes });
|
|
160
|
+
setWlOpen(false);
|
|
161
|
+
void load();
|
|
162
|
+
}
|
|
163
|
+
async function addComment() {
|
|
164
|
+
if (!sel || !comment.trim()) return;
|
|
165
|
+
await call("comment", { key: sel.key, text: comment });
|
|
166
|
+
setComment("");
|
|
167
|
+
void load();
|
|
168
|
+
setSel((prev) => (prev ? { ...prev } : prev));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const byStatus = summary?.byStatus ?? {};
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<AppShell
|
|
175
|
+
header={
|
|
176
|
+
<PageHeader
|
|
177
|
+
title="Jira 看板"
|
|
178
|
+
description={`${summary?.total ?? 0} 个 Issue`}
|
|
179
|
+
actions={
|
|
180
|
+
<>
|
|
181
|
+
<Button size="sm" variant="outline" onClick={() => { setSel(issues[0] ?? null); setView("kanban"); }}>
|
|
182
|
+
<Icon.Sparkles size={16} strokeWidth={2} /> AI 记工时
|
|
183
|
+
</Button>
|
|
184
|
+
</>
|
|
185
|
+
}
|
|
186
|
+
/>
|
|
187
|
+
}
|
|
188
|
+
>
|
|
189
|
+
<div className="flex h-full min-h-0 flex-col gap-3">
|
|
190
|
+
{/* 状态汇总(简约):复用 StatCard,改成一次遍历的色点条 */}
|
|
191
|
+
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
|
192
|
+
{STATUSES.map((s) => (
|
|
193
|
+
<StatCard key={s} title={s} value={byStatus[s] ?? 0}>
|
|
194
|
+
<span className="flex items-center gap-1.5">
|
|
195
|
+
<span className="size-2 rounded-full" style={{ background: STATUS_COLOR[s] }} />
|
|
196
|
+
<span className="text-xs text-muted-foreground">{s}</span>
|
|
197
|
+
</span>
|
|
198
|
+
</StatCard>
|
|
199
|
+
))}
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<FilterBar>
|
|
203
|
+
<Tabs value={view} onValueChange={(v) => setView(v as "kanban" | "table")}>
|
|
204
|
+
<TabsList>
|
|
205
|
+
<TabsTrigger value="kanban"><Icon.LayoutGrid size={14} strokeWidth={2} /> 看板</TabsTrigger>
|
|
206
|
+
<TabsTrigger value="table"><Icon.Table size={14} strokeWidth={2} /> 表格</TabsTrigger>
|
|
207
|
+
</TabsList>
|
|
208
|
+
</Tabs>
|
|
209
|
+
<NativeSelect value={assignee} onChange={(e) => setAssignee(e.target.value)}>
|
|
210
|
+
{["all", ...ASSIGNEES].map((a) => (
|
|
211
|
+
<option key={a} value={a}>{a === "all" ? "全部负责人" : a}</option>
|
|
212
|
+
))}
|
|
213
|
+
</NativeSelect>
|
|
214
|
+
{error && <span className="text-sm" style={{ color: "var(--destructive)" }}>{error}</span>}
|
|
215
|
+
</FilterBar>
|
|
216
|
+
|
|
217
|
+
<Card className="min-h-0 flex-1 overflow-y-auto">
|
|
218
|
+
<CardContent>
|
|
219
|
+
{view === "kanban" ? (
|
|
220
|
+
<div className="flex gap-3 overflow-x-auto pb-2">
|
|
221
|
+
<Kanban columns={kanbanColumns} cards={kanbanCards} onCardsChange={onKanban}
|
|
222
|
+
onCardClick={(c) => setSel(issues.find((i) => i.key === c.key) ?? null)} />
|
|
223
|
+
</div>
|
|
224
|
+
) : (
|
|
225
|
+
<DataGrid
|
|
226
|
+
columns={columns}
|
|
227
|
+
data={issues}
|
|
228
|
+
pageSize={10}
|
|
229
|
+
onRowClick={(issue) => setSel(issue)}
|
|
230
|
+
/>
|
|
231
|
+
)}
|
|
232
|
+
</CardContent>
|
|
233
|
+
</Card>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
{/* 详情编辑 Sheet */}
|
|
237
|
+
<Sheet open={Boolean(sel)} onOpenChange={(o) => { if (!o) setSel(null); }}>
|
|
238
|
+
<SheetContent className="sm:max-w-lg">
|
|
239
|
+
{sel && (
|
|
240
|
+
<>
|
|
241
|
+
<SheetHeader>
|
|
242
|
+
<SheetTitle className="font-mono flex items-center gap-2">{sel.key} <StatusBadgeMini status={sel.status} /></SheetTitle>
|
|
243
|
+
<SheetDescription>{sel.summary}</SheetDescription>
|
|
244
|
+
</SheetHeader>
|
|
245
|
+
<div className="flex flex-col gap-4 px-4 pb-4">
|
|
246
|
+
<div className="grid grid-cols-2 gap-2">
|
|
247
|
+
<Field label="状态">
|
|
248
|
+
<MapSelect value={sel.status} onChange={(v) => void patch(sel.key, { status: v })} options={STATUSES.map((s) => ({ v: s, label: s }))} />
|
|
249
|
+
</Field>
|
|
250
|
+
<Field label="负责人">
|
|
251
|
+
<MapSelect value={sel.assignee} onChange={(v) => void patch(sel.key, { assignee: v })} options={ASSIGNEES.map((a) => ({ v: a, label: a }))} />
|
|
252
|
+
</Field>
|
|
253
|
+
<Field label="优先级">
|
|
254
|
+
<MapSelect value={sel.priority} onChange={(v) => void patch(sel.key, { priority: v })} options={(["P0", "P1", "P2", "P3"] as const).map((p) => ({ v: p, label: p }))} />
|
|
255
|
+
</Field>
|
|
256
|
+
<Field label="工时点">
|
|
257
|
+
<MapSelect value={String(sel.points) as "1" | "2" | "3" | "5" | "8"} onChange={(v) => void patch(sel.key, { points: Number(v) })} options={["1", "2", "3", "5", "8"].map((p) => ({ v: p as "1" | "2" | "3" | "5" | "8", label: p }))} />
|
|
258
|
+
</Field>
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
<div className="flex flex-col gap-2">
|
|
262
|
+
<div className="text-xs text-muted-foreground">评论 · {sel.comments.length}</div>
|
|
263
|
+
<div className="flex flex-col gap-2">
|
|
264
|
+
{sel.comments.map((c, i) => (
|
|
265
|
+
<div key={i} className="rounded-lg border bg-muted/30 px-3 py-2 text-sm">
|
|
266
|
+
<div className="flex items-center justify-between"><span className="font-medium">{c.author}</span><span className="text-[11px] text-muted-foreground">{new Date(c.at).toLocaleString("zh-CN")}</span></div>
|
|
267
|
+
<div className="mt-0.5">{c.text}</div>
|
|
268
|
+
</div>
|
|
269
|
+
))}
|
|
270
|
+
</div>
|
|
271
|
+
<div className="flex gap-2">
|
|
272
|
+
<Textarea className="flex-1" value={comment} placeholder="写评论" onChange={(e) => setComment(e.target.value)} />
|
|
273
|
+
<Button onClick={() => void addComment()}>发送</Button>
|
|
274
|
+
</div>
|
|
275
|
+
<Button variant="outline" onClick={() => void openWorklog()}><Icon.Sparkles size={16} strokeWidth={2} /> AI 记工时</Button>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
</>
|
|
279
|
+
)}
|
|
280
|
+
</SheetContent>
|
|
281
|
+
</Sheet>
|
|
282
|
+
|
|
283
|
+
{/* AI 工时确认 Dialog */}
|
|
284
|
+
<Dialog open={wlOpen} onOpenChange={setWlOpen}>
|
|
285
|
+
<DialogContent className="sm:max-w-md">
|
|
286
|
+
<DialogHeader><DialogTitle>记工时</DialogTitle><DialogDescription>{sel?.key} · {sel?.summary}</DialogDescription></DialogHeader>
|
|
287
|
+
<Textarea value={wlNote} onChange={(e) => setWlNote(e.target.value)} rows={4} />
|
|
288
|
+
<div className="flex items-center gap-2">
|
|
289
|
+
<span className="text-sm text-muted-foreground">工时</span>
|
|
290
|
+
<Input value={wlMinutes} onChange={(e) => setWlMinutes(Number(e.target.value))} type="number" className="w-24" /> <span className="text-sm">分钟</span>
|
|
291
|
+
</div>
|
|
292
|
+
<DialogFooter>
|
|
293
|
+
<Button variant="outline" onClick={() => setWlOpen(false)}>取消</Button>
|
|
294
|
+
<Button onClick={() => void saveWorklog()}>确认记录</Button>
|
|
295
|
+
</DialogFooter>
|
|
296
|
+
</DialogContent>
|
|
297
|
+
</Dialog>
|
|
298
|
+
</AppShell>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function Field({ label, children }: { label: string; children: React.ReactNode }) {
|
|
303
|
+
return <div className="flex flex-col gap-1"><span className="text-xs text-muted-foreground">{label}</span>{children}</div>;
|
|
304
|
+
}
|
|
305
|
+
// ⭐ 关键:ui 的 Select 是 Base-UI 组合(Trigger/Value/Content/Item),别当原生 <select> 用
|
|
306
|
+
function MapSelect<T extends string>({ value, onChange, options }: { value: T; onChange: (v: T) => void; options: { v: T; label: string }[] }) {
|
|
307
|
+
return (
|
|
308
|
+
<Select value={value} onValueChange={(v) => onChange(v as T)}>
|
|
309
|
+
<SelectTrigger className="w-full"><SelectValue /></SelectTrigger>
|
|
310
|
+
<SelectContent>
|
|
311
|
+
{options.map((o) => <SelectItem key={o.v} value={o.v}>{o.label}</SelectItem>)}
|
|
312
|
+
</SelectContent>
|
|
313
|
+
</Select>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
function StatusBadgeMini({ status }: { status: string }) {
|
|
317
|
+
return <Badge variant={(STATUS_TONE as Record<string, never>)[status] ?? "default"}>{status}</Badge>;
|
|
318
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineDashboard } from "@monkeyagent/dashboard";
|
|
2
|
+
|
|
3
|
+
export default defineDashboard({
|
|
4
|
+
name: "骨架示例",
|
|
5
|
+
description: "最小可运行:一次 call 往返 + 读 Host 配置",
|
|
6
|
+
api: {
|
|
7
|
+
// ⭐ 关键:每个方法都拿到同一个 ctx —— appId / appDir / storage / config / log / 能力(这页不用)
|
|
8
|
+
async ping(ctx) {
|
|
9
|
+
return {
|
|
10
|
+
appId: ctx.appId, // 当前小程序 reverse-DNS id
|
|
11
|
+
theme: ctx.config?.theme ?? "light", // 跟随 Host 设置(设置页 / 顶栏可改)
|
|
12
|
+
now: Date.now(),
|
|
13
|
+
};
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import { AppShell, Badge, Card, CardContent, CardHeader, CardTitle, Icon, PageHeader } from "@monkey-mini-app/ui";
|
|
4
|
+
import { useDashboardApi } from "@monkeyagent/host";
|
|
5
|
+
|
|
6
|
+
type Ping = { appId: string; theme: string; now: number };
|
|
7
|
+
|
|
8
|
+
export default function Ui() {
|
|
9
|
+
// ⭐ 关键:访问后端**只有**一个入口 —— useDashboardApi().call(method, args)
|
|
10
|
+
// method 必须是 defineDashboard({ api }) 的键;UI 永不 import main.api.ts。
|
|
11
|
+
const { call } = useDashboardApi();
|
|
12
|
+
const [data, setData] = useState<Ping | null>(null);
|
|
13
|
+
const [loading, setLoading] = useState(true);
|
|
14
|
+
const [error, setError] = useState<string | null>(null);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
// ⭐ 关键:组件卸载后别 setState(长/慢任务与取消配套见 insights / agentrun)
|
|
18
|
+
let alive = true;
|
|
19
|
+
(async () => {
|
|
20
|
+
try {
|
|
21
|
+
const d = await call("ping", {});
|
|
22
|
+
if (alive) setData(d as Ping);
|
|
23
|
+
} catch (e) {
|
|
24
|
+
if (alive) setError(String((e as Error)?.message || e));
|
|
25
|
+
} finally {
|
|
26
|
+
if (alive) setLoading(false);
|
|
27
|
+
}
|
|
28
|
+
})();
|
|
29
|
+
return () => {
|
|
30
|
+
alive = false;
|
|
31
|
+
};
|
|
32
|
+
}, [call]);
|
|
33
|
+
|
|
34
|
+
const badge = error ? "destructive" : loading ? "secondary" : "default";
|
|
35
|
+
const label = error ? "出错" : loading ? "连接中" : "正常";
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
// ⭐ 关键:页面用 AppShell 包一层(可选 sidebar/header),页头用 PageHeader。
|
|
39
|
+
// 这里是「骨架基线」;别的模板会在 main 里放列表/看板/图表。
|
|
40
|
+
<AppShell header={<PageHeader title="骨架示例" description="最小可运行结构" />}>
|
|
41
|
+
<div className="max-w-xl">
|
|
42
|
+
<Card>
|
|
43
|
+
<CardHeader>
|
|
44
|
+
<CardTitle>连通状态</CardTitle>
|
|
45
|
+
<div className="flex items-center gap-2">
|
|
46
|
+
<Icon.Check size={16} strokeWidth={2} className="text-primary" />
|
|
47
|
+
<Badge variant={badge}>{label}</Badge>
|
|
48
|
+
</div>
|
|
49
|
+
</CardHeader>
|
|
50
|
+
<CardContent className="flex flex-col gap-2 text-sm">
|
|
51
|
+
{error && <p style={{ color: "var(--destructive)" }}>{error}</p>}
|
|
52
|
+
{data && (
|
|
53
|
+
<>
|
|
54
|
+
<div className="flex items-center justify-between">
|
|
55
|
+
<span className="text-muted-foreground">App ID</span>
|
|
56
|
+
<span className="font-mono">{data.appId}</span>
|
|
57
|
+
</div>
|
|
58
|
+
<div className="flex items-center justify-between">
|
|
59
|
+
<span className="text-muted-foreground">主题</span>
|
|
60
|
+
<span>{data.theme}</span>
|
|
61
|
+
</div>
|
|
62
|
+
<div className="flex items-center justify-between">
|
|
63
|
+
<span className="text-muted-foreground">Host 往返</span>
|
|
64
|
+
<span>{new Date(data.now).toLocaleTimeString("zh-CN")}</span>
|
|
65
|
+
</div>
|
|
66
|
+
</>
|
|
67
|
+
)}
|
|
68
|
+
{!data && !error && <span className="text-muted-foreground">正在检测…</span>}
|
|
69
|
+
</CardContent>
|
|
70
|
+
</Card>
|
|
71
|
+
</div>
|
|
72
|
+
</AppShell>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { defineDashboard } from "@monkeyagent/dashboard";
|
|
2
|
+
|
|
3
|
+
// ⭐ 关键:host 已经把 OS 基础指标给到 ctx.system.metrics()(内存/CPU/负载/uptime 都有),
|
|
4
|
+
// 不要再用 bash 重算。ctx.bash 只用于拿 host 没有的:ps(进程)、df(磁盘)。
|
|
5
|
+
type Proc = { pid: string; cpu: number; mem: number; name: string };
|
|
6
|
+
|
|
7
|
+
function fmtBytes(n: number): string {
|
|
8
|
+
if (!Number.isFinite(n) || n <= 0) return "—";
|
|
9
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
10
|
+
let i = 0;
|
|
11
|
+
while (n >= 1024 && i < units.length - 1) {
|
|
12
|
+
n /= 1024;
|
|
13
|
+
i++;
|
|
14
|
+
}
|
|
15
|
+
return n.toFixed(i ? 1 : 0) + units[i];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function sh(ctx, cmd: string): Promise<string> {
|
|
19
|
+
try {
|
|
20
|
+
const r = await ctx.bash(cmd);
|
|
21
|
+
if (r.exitCode !== 0) return "";
|
|
22
|
+
return String(r.stdout || "").trim();
|
|
23
|
+
} catch {
|
|
24
|
+
return "";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseProcesses(text: string): Proc[] {
|
|
29
|
+
// 最后一列是 comm,可能含空格;用最后一段当 name,前面 4 段是 pid/cpu/mem
|
|
30
|
+
if (!text) return [];
|
|
31
|
+
return text
|
|
32
|
+
.split("\n")
|
|
33
|
+
.map((line) => {
|
|
34
|
+
const cols = line.trim().split(/\s+/);
|
|
35
|
+
if (cols.length < 5) return null;
|
|
36
|
+
const cpu = Number(cols[1]);
|
|
37
|
+
const mem = Number(cols[2]);
|
|
38
|
+
if (Number.isNaN(cpu) || Number.isNaN(mem)) return null;
|
|
39
|
+
return { pid: cols[0], cpu, mem, name: cols.slice(3).join(" ") || "?" };
|
|
40
|
+
})
|
|
41
|
+
.filter((p): p is Proc => p !== null)
|
|
42
|
+
.slice(0, 8);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseDisk(text: string): { total: number; used: number; avail: number; usedPct: number; mount: string } | null {
|
|
46
|
+
const cols = text.trim().split(/\s+/);
|
|
47
|
+
if (cols.length < 5) return null;
|
|
48
|
+
const total = Number(cols[1]) * 1024;
|
|
49
|
+
const used = Number(cols[2]) * 1024;
|
|
50
|
+
const avail = Number(cols[3]) * 1024;
|
|
51
|
+
const usedPct = Number(cols[4]?.replace("%", ""));
|
|
52
|
+
if (Number.isNaN(total) || Number.isNaN(usedPct)) return null;
|
|
53
|
+
return { total, used, avail, usedPct, mount: cols[5] || "/" };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default defineDashboard({
|
|
57
|
+
name: "系统监控",
|
|
58
|
+
description: "CPU / 内存 / 磁盘 / 进程实时指标",
|
|
59
|
+
api: {
|
|
60
|
+
async getSnapshot(ctx) {
|
|
61
|
+
const base = await ctx.system.metrics();
|
|
62
|
+
const platform = base?.platform || "";
|
|
63
|
+
// 内存/CPU/负载由 metrics 提供,这里只补 host 没有的:进程 + 磁盘
|
|
64
|
+
const [psText, dfLine] = await Promise.all([
|
|
65
|
+
sh(ctx, platform === "darwin" ? "ps -axo pid,pcpu,pmem,comm -r | head -n 9" : "ps -axo pid,pcpu,pmem,comm --sort=-pcpu | head -n 9"),
|
|
66
|
+
sh(ctx, "df -k / | tail -1"),
|
|
67
|
+
]);
|
|
68
|
+
const mem = base?.memory || { total: 0, free: 0, used: 0, usedRatio: 0 };
|
|
69
|
+
const disk = parseDisk(dfLine);
|
|
70
|
+
return {
|
|
71
|
+
hostname: base?.hostname,
|
|
72
|
+
platform,
|
|
73
|
+
arch: base?.arch,
|
|
74
|
+
loadavg: base?.loadavg,
|
|
75
|
+
uptimeSec: base?.uptimeSec,
|
|
76
|
+
collectedAt: Date.now(),
|
|
77
|
+
cpu: { model: base?.cpu?.model || "—", count: base?.cpu?.count || 0 },
|
|
78
|
+
processes: parseProcesses(psText),
|
|
79
|
+
memory: {
|
|
80
|
+
total: mem.total,
|
|
81
|
+
used: mem.used,
|
|
82
|
+
free: mem.free,
|
|
83
|
+
usedPct: Math.round((mem.usedRatio || 0) * 100),
|
|
84
|
+
},
|
|
85
|
+
disk: disk
|
|
86
|
+
? { total: fmtBytes(disk.total), used: fmtBytes(disk.used), free: fmtBytes(disk.avail), usedPct: disk.usedPct }
|
|
87
|
+
: null,
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
});
|