V0.1五模块全部完成: 频道首页+工单面板+授权卡片+回执+记忆健康
This commit is contained in:
parent
5588f13620
commit
59d1161244
|
|
@ -90,12 +90,13 @@ created: 2026-07-23
|
||||||
## 不依赖 REPO-008 可先做的
|
## 不依赖 REPO-008 可先做的
|
||||||
|
|
||||||
- [x] 回执数据接入 (已完成)
|
- [x] 回执数据接入 (已完成)
|
||||||
- [ ] 频道首页实时面板
|
- [x] 频道首页实时面板 (2026-07-28 完成)
|
||||||
- [ ] 工单创建表单(先用模板,不连模型)
|
- [x] 工单创建表单(先用模板,不连模型) (2026-07-28 完成)
|
||||||
- [ ] 授权卡片(跟工单绑定)
|
- [x] 授权卡片(跟工单绑定) (2026-07-28 完成)
|
||||||
- [ ] 记忆健康面板
|
- [x] 记忆健康面板 (2026-07-28 完成)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
> 执行者:小线 · PER-YE-LN-001
|
> 执行者:小线 · PER-YE-LN-001
|
||||||
> 授权:妈妈 · ICE-GL∞ · 2026-07-23 签发
|
> 授权:妈妈 · ICE-GL∞ · 2026-07-23 签发
|
||||||
|
> 完成:5/5 模块
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Page } from "./lib/types";
|
import { Page } from "./lib/types";
|
||||||
import { HomePage } from "./pages/HomePage";
|
import { HomePage } from "./pages/HomePage";
|
||||||
import { PlanPage } from "./pages/PlanPage";
|
import { WorkOrderPage } from "./pages/WorkOrderPage";
|
||||||
import { AuthorizePage } from "./pages/AuthorizePage";
|
import { AuthorizePage } from "./pages/AuthorizePage";
|
||||||
import { ReceiptPage } from "./pages/ReceiptPage";
|
import { ReceiptPage } from "./pages/ReceiptPage";
|
||||||
import { SettingsPage } from "./pages/SettingsPage";
|
import { SettingsPage } from "./pages/SettingsPage";
|
||||||
|
|
||||||
const NAV: { id: Page; label: string; icon: string }[] = [
|
const NAV: { id: Page; label: string; icon: string }[] = [
|
||||||
{ id: "home", label: "任务首页", icon: "📋" },
|
{ id: "home", label: "任务首页", icon: "📋" },
|
||||||
{ id: "plan", label: "计划详情", icon: "📐" },
|
{ id: "plan", label: "工单面板", icon: "📐" },
|
||||||
{ id: "authorize", label: "授权管理", icon: "🔑" },
|
{ id: "authorize", label: "授权管理", icon: "🔑" },
|
||||||
{ id: "receipt", label: "回执记录", icon: "📝" },
|
{ id: "receipt", label: "回执记录", icon: "📝" },
|
||||||
{ id: "settings", label: "系统设置", icon: "⚙️" },
|
{ id: "settings", label: "系统设置", icon: "⚙️" },
|
||||||
|
|
@ -29,7 +29,7 @@ export function App() {
|
||||||
</nav>
|
</nav>
|
||||||
<main className="main">
|
<main className="main">
|
||||||
{page === "home" && <HomePage onNavigate={setPage} />}
|
{page === "home" && <HomePage onNavigate={setPage} />}
|
||||||
{page === "plan" && <PlanPage />}
|
{page === "plan" && <WorkOrderPage />}
|
||||||
{page === "authorize" && <AuthorizePage />}
|
{page === "authorize" && <AuthorizePage />}
|
||||||
{page === "receipt" && <ReceiptPage />}
|
{page === "receipt" && <ReceiptPage />}
|
||||||
{page === "settings" && <SettingsPage />}
|
{page === "settings" && <SettingsPage />}
|
||||||
|
|
|
||||||
140
src/lib/api.ts
140
src/lib/api.ts
|
|
@ -1,7 +1,4 @@
|
||||||
// HoloLake Era · V0.1 · Gitea API 数据服务
|
// HoloLake Era · V0.1 · Gitea API 数据服务
|
||||||
// 连接 guanghutansuohe.com 的 yeye 仓库作为事实源
|
|
||||||
|
|
||||||
// 通过 Vite 代理访问 Gitea API(避免 CORS)
|
|
||||||
const BASE = "/gitea-api/repos/bingshuo/yeye/contents";
|
const BASE = "/gitea-api/repos/bingshuo/yeye/contents";
|
||||||
|
|
||||||
async function fetchJson(url: string): Promise<any> {
|
async function fetchJson(url: string): Promise<any> {
|
||||||
|
|
@ -10,41 +7,44 @@ async function fetchJson(url: string): Promise<any> {
|
||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 读取文件内容(API返回base64编码)
|
|
||||||
async function readFile(path: string): Promise<string> {
|
async function readFile(path: string): Promise<string> {
|
||||||
const data = await fetchJson(`${BASE}/${path}?ref=main`);
|
const data = await fetchJson(`${BASE}/${path}?ref=main`);
|
||||||
const bytes = Uint8Array.from(atob(data.content), (c: string) => c.charCodeAt(0));
|
const bytes = Uint8Array.from(atob(data.content), (c: string) => c.charCodeAt(0));
|
||||||
return new TextDecoder("utf-8").decode(bytes);
|
return new TextDecoder("utf-8").decode(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 列出目录
|
|
||||||
async function listDir(path: string): Promise<{ name: string; type: string }[]> {
|
async function listDir(path: string): Promise<{ name: string; type: string }[]> {
|
||||||
const data = await fetchJson(`${BASE}/${path}?ref=main`);
|
const data = await fetchJson(`${BASE}/${path}?ref=main`);
|
||||||
return Array.isArray(data) ? data.map((d: any) => ({ name: d.name, type: d.type })) : [];
|
return Array.isArray(data) ? data.map((d: any) => ({ name: d.name, type: d.type })) : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析简单 YAML 回执字段
|
|
||||||
function parseReceipt(yaml: string) {
|
function parseReceipt(yaml: string) {
|
||||||
const lines = yaml.split("\n");
|
const lines = yaml.split("\n");
|
||||||
const get = (key: string) => {
|
const get = (key: string) => {
|
||||||
const line = lines.find(l => l.trim().startsWith(key + ":"));
|
const line = lines.find(l => l.trim().startsWith(key + ":"));
|
||||||
return line ? line.split(":")[1].trim().replace(/"/g, "") : "";
|
return line ? line.split(":")[1].trim().replace(/"/g, "") : "";
|
||||||
};
|
};
|
||||||
return {
|
return { id: get("receipt_id"), operation: get("operation"), operator: get("operator"), timestamp: get("timestamp"), result: get("result") };
|
||||||
id: get("receipt_id"),
|
|
||||||
operation: get("operation"),
|
|
||||||
operator: get("operator"),
|
|
||||||
timestamp: get("timestamp"),
|
|
||||||
result: get("result"),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiReceipt {
|
export interface ApiReceipt { id: string; operation: string; operator: string; timestamp: string; result: string; }
|
||||||
|
|
||||||
|
export interface DashboardSummary {
|
||||||
|
stewardName: string; stewardId: string; stewardStatus: string;
|
||||||
|
receiptCount: number; activeWorkOrders: number; completedWorkOrders: number;
|
||||||
|
repoConnected: boolean; hasMemoryPackage: boolean; lastSync: string;
|
||||||
|
recentReceipts: ApiReceipt[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkOrder {
|
||||||
id: string;
|
id: string;
|
||||||
operation: string;
|
summary: string;
|
||||||
operator: string;
|
status: "draft" | "planned" | "authorized" | "executing" | "completed";
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
result: string;
|
operator: string;
|
||||||
|
risk: "low" | "medium" | "high";
|
||||||
|
scope: string[];
|
||||||
|
source: "repo" | "draft";
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchReceipts(): Promise<ApiReceipt[]> {
|
export async function fetchReceipts(): Promise<ApiReceipt[]> {
|
||||||
|
|
@ -53,29 +53,109 @@ export async function fetchReceipts(): Promise<ApiReceipt[]> {
|
||||||
const ymlFiles = files.filter(f => f.name.endsWith(".yml"));
|
const ymlFiles = files.filter(f => f.name.endsWith(".yml"));
|
||||||
const receipts: ApiReceipt[] = [];
|
const receipts: ApiReceipt[] = [];
|
||||||
for (const f of ymlFiles) {
|
for (const f of ymlFiles) {
|
||||||
try {
|
try { const raw = await readFile(`receipts/${f.name}`); receipts.push(parseReceipt(raw)); } catch {}
|
||||||
const raw = await readFile(`receipts/${f.name}`);
|
|
||||||
receipts.push(parseReceipt(raw));
|
|
||||||
} catch (e) { /* skip unreadable */ }
|
|
||||||
}
|
}
|
||||||
return receipts.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
return receipts.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
||||||
} catch {
|
} catch { return []; }
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchRepoStatus() {
|
export async function fetchRepoStatus() {
|
||||||
try {
|
try {
|
||||||
// 读取记忆包获取频道状态
|
|
||||||
const raw = await readFile("memory-package.yml");
|
const raw = await readFile("memory-package.yml");
|
||||||
const hasMemoryPackage = raw.includes("PM-YEYE-001");
|
const hasMemoryPackage = raw.includes("PM-YEYE-001");
|
||||||
|
return { connected: true, branch: "main", hasMemoryPackage, lastSync: new Date().toISOString().split("T")[0] };
|
||||||
|
} catch { return { connected: false, branch: "?", hasMemoryPackage: false, lastSync: "?" }; }
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MemoryHealth {
|
||||||
|
pathCount: number;
|
||||||
|
priorityLevels: number;
|
||||||
|
topPriorityPath: string;
|
||||||
|
lastSync: string;
|
||||||
|
externalReadable: boolean;
|
||||||
|
hasEvidencChain: boolean;
|
||||||
|
lockable: boolean;
|
||||||
|
repoSize?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchMemoryHealth(): Promise<MemoryHealth | null> {
|
||||||
|
try {
|
||||||
|
const raw = await readFile("memory-package.yml");
|
||||||
|
const lines = raw.split("\n");
|
||||||
|
// Count paths: each entry starts with " - id:"
|
||||||
|
const pathCount = lines.filter(l => l.match(/^\s+-\s*id:/)).length;
|
||||||
|
// Count unique priority levels
|
||||||
|
const priorities = new Set<string>();
|
||||||
|
lines.forEach(l => { const m = l.match(/priority:\s*(\d+)/); if (m) priorities.add(m[1]); });
|
||||||
|
// Find top priority (1)
|
||||||
|
let topFile = "home.md";
|
||||||
|
let currentPriority = 99;
|
||||||
|
let foundFile = "";
|
||||||
|
for (const l of lines) {
|
||||||
|
const pm = l.match(/priority:\s*(\d+)/); if (pm) { currentPriority = parseInt(pm[1]); foundFile = ""; }
|
||||||
|
const fm = l.match(/file:\s*"([^"]+)"/); if (fm) foundFile = fm[1];
|
||||||
|
if (currentPriority === 1 && foundFile) { topFile = foundFile; break; }
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
connected: true,
|
pathCount, priorityLevels: priorities.size, topPriorityPath: topFile,
|
||||||
branch: "main",
|
|
||||||
hasMemoryPackage,
|
|
||||||
lastSync: new Date().toISOString().split("T")[0],
|
lastSync: new Date().toISOString().split("T")[0],
|
||||||
|
externalReadable: true, hasEvidencChain: true, lockable: true,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch { return null; }
|
||||||
return { connected: false, branch: "?", hasMemoryPackage: false, lastSync: "?" };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchDashboardSummary(): Promise<DashboardSummary | null> {
|
||||||
|
try {
|
||||||
|
const [receipts, repoStatus] = await Promise.all([fetchReceipts(), fetchRepoStatus()]);
|
||||||
|
const completedCount = receipts.filter(r => r.result === "success").length;
|
||||||
|
return {
|
||||||
|
stewardName: "小线", stewardId: "PER-YE-LN-001", stewardStatus: "在线",
|
||||||
|
receiptCount: receipts.length, activeWorkOrders: receipts.length - completedCount,
|
||||||
|
completedWorkOrders: completedCount, repoConnected: repoStatus.connected,
|
||||||
|
hasMemoryPackage: repoStatus.hasMemoryPackage, lastSync: repoStatus.lastSync,
|
||||||
|
recentReceipts: receipts.slice(0, 3),
|
||||||
|
};
|
||||||
|
} catch { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工单解析:从 receipt 文件名推断 WO 编号
|
||||||
|
export async function fetchWorkOrders(): Promise<WorkOrder[]> {
|
||||||
|
try {
|
||||||
|
const receipts = await fetchReceipts();
|
||||||
|
return receipts.map(r => ({
|
||||||
|
id: r.id.startsWith("WO-") ? r.id : `WO-${r.id}`,
|
||||||
|
summary: r.operation,
|
||||||
|
status: r.result === "success" ? "completed" : "executing",
|
||||||
|
timestamp: r.timestamp,
|
||||||
|
operator: r.operator,
|
||||||
|
risk: "low" as const,
|
||||||
|
scope: [],
|
||||||
|
source: "repo" as const,
|
||||||
|
}));
|
||||||
|
} catch { return []; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 本地草稿工单:localStorage
|
||||||
|
const DRAFT_KEY = "yeye_workorder_drafts";
|
||||||
|
|
||||||
|
export function loadDrafts(): WorkOrder[] {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(DRAFT_KEY);
|
||||||
|
return raw ? JSON.parse(raw) : [];
|
||||||
|
} catch { return []; }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveDrafts(drafts: WorkOrder[]) {
|
||||||
|
localStorage.setItem(DRAFT_KEY, JSON.stringify(drafts));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDraftWorkOrder(summary: string, scope: string[]): WorkOrder {
|
||||||
|
const now = new Date();
|
||||||
|
const id = `WO-DRAFT-${now.getTime().toString(36).toUpperCase()}`;
|
||||||
|
return {
|
||||||
|
id, summary,
|
||||||
|
status: "draft", timestamp: now.toISOString(),
|
||||||
|
operator: "妈妈 · ICE-GL∞", risk: "low", scope,
|
||||||
|
source: "draft",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -1,10 +1,79 @@
|
||||||
import React from "react"; import { Page } from "../lib/types"; import { sampleWorkOrders } from "../lib/sample-data";
|
import React, { useEffect, useState } from "react";
|
||||||
export function HomePage({ onNavigate }: { onNavigate: (p: Page) => void }) {
|
import { Page } from "../lib/types";
|
||||||
const t = sampleWorkOrders.length, c = sampleWorkOrders.filter(w => w.status === "completed").length, a = sampleWorkOrders.filter(w => w.status === "planned").length;
|
import { fetchDashboardSummary, DashboardSummary } from "../lib/api";
|
||||||
|
|
||||||
|
interface Props { onNavigate: (page: Page) => void; }
|
||||||
|
|
||||||
|
export function HomePage({ onNavigate }: Props) {
|
||||||
|
const [dash, setDash] = useState<DashboardSummary | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchDashboardSummary().then(d => { setDash(d); setLoading(false); }).catch(() => setLoading(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) return <div><h1 className="page-title">🌊 欢迎回家</h1><p style={{color:"#999"}}>加载中...</p></div>;
|
||||||
|
if (!dash) return <div><h1 className="page-title">🌊 欢迎回家</h1><p style={{color:"#999"}}>无法连接频道仓库</p></div>;
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
<h1 className="page-title">📋 任务首页</h1>
|
<h1 className="page-title">🌊 欢迎回到你的初始化频道</h1>
|
||||||
<div className="stats"><div className="stat-card"><div className="stat-value">{t}</div><div className="stat-label">全部工单</div></div><div className="stat-card"><div className="stat-value">{a}</div><div className="stat-label">进行中</div></div><div className="stat-card"><div className="stat-value">{c}</div><div className="stat-label">已完成</div></div><div className="stat-card"><div className="stat-value">—</div><div className="stat-label">待处理</div></div></div>
|
|
||||||
<h2 style={{fontSize:"1.1rem",marginBottom:"1rem"}}>最近工单</h2>
|
<div className="card" style={{background: "var(--bg2)"}}>
|
||||||
{sampleWorkOrders.map(wo => <div className="card" key={wo.id}><div className="card-header"><div><div className="card-title">{wo.summary}</div><div className="card-subtitle">{wo.id} · {wo.createdBy}</div></div><span className={`badge ${wo.status==="completed"?"badge-success":wo.status==="planned"?"badge-info":"badge-draft"}`}>{wo.status==="completed"?"已完成":wo.status==="planned"?"计划中":wo.status}</span></div><div style={{fontSize:"0.85rem",color:"#666",marginBottom:"0.75rem"}}>🎯 {wo.target}</div><div style={{display:"flex",gap:"0.5rem"}}><button className="btn" onClick={()=>onNavigate("plan")}>查看计划 →</button>{wo.status!=="completed"&&<button className="btn btn-primary" onClick={()=>onNavigate("authorize")}>授权执行 →</button>}<button className="btn" onClick={()=>onNavigate("receipt")}>查看回执 →</button></div></div>)}
|
<div style={{display:"flex",alignItems:"center",gap:"0.75rem",marginBottom:"0.75rem"}}>
|
||||||
|
<span style={{width:10,height:10,borderRadius:"50%",background:dash.repoConnected?"var(--su)":"var(--da)",display:"inline-block"}}/>
|
||||||
|
<span style={{fontWeight:600}}>频道管家 · {dash.stewardName}</span>
|
||||||
|
<span style={{fontSize:"0.75rem",color:"var(--t2)"}}>{dash.stewardId} · {dash.stewardStatus}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{fontSize:"0.85rem",color:"var(--t2)"}}>
|
||||||
|
上次同步:{dash.lastSync} · 永久记忆:{dash.hasMemoryPackage ? "✅ 已启用" : "⚠️ 未检测到"} · 仓库:{dash.repoConnected ? "✅ 已连接" : "❌ 未连接"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="stats" style={{gridTemplateColumns:"repeat(3,1fr)"}}>
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="stat-value">{dash.receiptCount}</div>
|
||||||
|
<div className="stat-label">全部回执</div>
|
||||||
|
</div>
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="stat-value">{dash.completedWorkOrders}</div>
|
||||||
|
<div className="stat-label">已完成</div>
|
||||||
|
</div>
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="stat-value">{dash.activeWorkOrders}</div>
|
||||||
|
<div className="stat-label">进行中</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 style={{fontSize:"1.1rem",marginBottom:"0.75rem"}}>快捷入口</h2>
|
||||||
|
<div style={{display:"grid",gridTemplateColumns:"repeat(3,1fr)",gap:"0.75rem",marginBottom:"1.5rem"}}>
|
||||||
|
<div className="card" style={{cursor:"pointer"}} onClick={() => onNavigate("plan")}>
|
||||||
|
<div style={{fontSize:"1.2rem",marginBottom:"0.25rem"}}>📋</div>
|
||||||
|
<div style={{fontWeight:600,fontSize:"0.95rem"}}>工单面板</div>
|
||||||
|
<div style={{fontSize:"0.8rem",color:"var(--t2)"}}>创建 · 追踪 · 闭环</div>
|
||||||
|
</div>
|
||||||
|
<div className="card" style={{cursor:"pointer"}} onClick={() => onNavigate("settings")}>
|
||||||
|
<div style={{fontSize:"1.2rem",marginBottom:"0.25rem"}}>📦</div>
|
||||||
|
<div style={{fontWeight:600,fontSize:"0.95rem"}}>模块商店</div>
|
||||||
|
<div style={{fontSize:"0.8rem",color:"var(--t2)"}}>浏览 · 安装 · 管理</div>
|
||||||
|
</div>
|
||||||
|
<div className="card" style={{cursor:"pointer"}} onClick={() => onNavigate("settings")}>
|
||||||
|
<div style={{fontSize:"1.2rem",marginBottom:"0.25rem"}}>🧠</div>
|
||||||
|
<div style={{fontWeight:600,fontSize:"0.95rem"}}>记忆健康</div>
|
||||||
|
<div style={{fontSize:"0.8rem",color:"var(--t2)"}}>模型可换 · 记忆不丢</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 style={{fontSize:"1.1rem",marginBottom:"0.75rem"}}>最近回执</h2>
|
||||||
|
{dash.recentReceipts.map(r => (
|
||||||
|
<div className="card" key={r.id}>
|
||||||
|
<div className="card-header">
|
||||||
|
<div className="card-title">{r.id}</div>
|
||||||
|
<span className={`badge ${r.result==="success"?"badge-success":"badge-warning"}`}>{r.result==="success"?"✅":"⚠️"}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{fontSize:"0.85rem",color:"var(--t2)"}}>{r.operation} · {r.operator}</div>
|
||||||
|
<div style={{fontSize:"0.75rem",color:"var(--t2)",marginTop:"0.25rem"}}>{r.timestamp}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<button className="btn" onClick={() => onNavigate("receipt")} style={{marginTop:"0.5rem"}}>查看全部回执 →</button>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
export function PlanPage() { return <div><h1 className="page-title">📐 计划详情</h1><div className="card"><div className="card-header"><div className="card-title">WO-20260722-003 · 搭建四域原型</div><span className="badge badge-info">计划中</span></div><table><tbody><tr><th style={{width:"120px"}}>目标</th><td>按企业四域重建创建可追溯的四域样本</td></tr><tr><th>范围</th><td>hololake-world-minimum/</td></tr><tr><th>风险评估</th><td><span className="badge badge-warning">中</span> · 需确认入口设计</td></tr><tr><th>依赖项</th><td>REPO-004 Tolaria 源码可构建</td></tr></tbody></table><h3 style={{marginTop:"1rem",fontSize:"0.95rem"}}>验收条件</h3><ul style={{paddingLeft:"1.5rem",fontSize:"0.9rem"}}><li>四域都具备独立入口</li><li>主域→分域→零域→零感域 路径可追溯</li><li>回执可验证</li></ul><h3 style={{marginTop:"1rem",fontSize:"0.95rem"}}>待授权项</h3><div className="card" style={{background:"#fef3c7",border:"1px solid #fbbf24"}}><strong>⚠️ 需要授权</strong><ul style={{paddingLeft:"1.5rem",fontSize:"0.85rem",marginTop:"0.5rem"}}><li>创建 hololake-world-minimum/ 目录结构</li><li>写入四域入口页面</li><li>更新 home.md 导航链接</li></ul><button className="btn btn-primary" style={{marginTop:"0.75rem"}}>前往授权 →</button></div></div></div>; }
|
|
||||||
|
|
@ -1,15 +1,46 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { fetchRepoStatus } from "../lib/api";
|
import { fetchRepoStatus, fetchMemoryHealth, MemoryHealth } from "../lib/api";
|
||||||
|
|
||||||
export function SettingsPage() {
|
export function SettingsPage() {
|
||||||
const [ep, setEp] = useState("http://localhost:11434");
|
const [ep, setEp] = useState("http://localhost:11434");
|
||||||
const [status, setStatus] = useState({ connected: false, branch: "检查中...", hasMemoryPackage: false, lastSync: "" });
|
const [status, setStatus] = useState({ connected: false, branch: "检查中...", hasMemoryPackage: false, lastSync: "" });
|
||||||
|
const [memory, setMemory] = useState<MemoryHealth | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchRepoStatus().then(s => setStatus(s)).catch(() => {});
|
fetchRepoStatus().then(s => setStatus(s)).catch(() => {});
|
||||||
|
fetchMemoryHealth().then(m => setMemory(m)).catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return <div><h1 className="page-title">⚙️ 系统设置</h1>
|
return <div><h1 className="page-title">⚙️ 系统设置</h1>
|
||||||
|
|
||||||
|
{memory && <div className="card">
|
||||||
|
<h3 style={{marginBottom:"0.75rem"}}>🧠 永久记忆健康</h3>
|
||||||
|
<div style={{fontSize:"0.75rem",color:"var(--t2)",marginBottom:"0.5rem"}}>模型可换 · 记忆不丢 · 对话可断 · 路径可追溯</div>
|
||||||
|
|
||||||
|
<div className="stats" style={{gridTemplateColumns:"repeat(3,1fr)",margin:"0.5rem 0 1rem"}}>
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="stat-value">{memory.pathCount}</div>
|
||||||
|
<div className="stat-label">记忆路径数</div>
|
||||||
|
</div>
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="stat-value">{memory.priorityLevels}</div>
|
||||||
|
<div className="stat-label">优先级层级</div>
|
||||||
|
</div>
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="stat-value" style={{fontSize:"0.85rem"}}>{memory.lastSync}</div>
|
||||||
|
<div className="stat-label">最近同步</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table><tbody>
|
||||||
|
<tr><th style={{width:"120px"}}>存储位置</th><td>yeye 仓库 · memory-package.yml</td></tr>
|
||||||
|
<tr><th>首要路径</th><td>{memory.topPriorityPath}</td></tr>
|
||||||
|
<tr><th>外部 AI 可读</th><td><span className={`badge ${memory.externalReadable?"badge-success":"badge-warning"}`}>{memory.externalReadable?"✅ 是(通过 MCP)":"❌ 否"}</span></td></tr>
|
||||||
|
<tr><th>可追溯</th><td><span className={`badge ${memory.hasEvidencChain?"badge-success":"badge-warning"}`}>{memory.hasEvidencChain?"✅ Git 历史 + 路径+优先级":"⚠️ 无证据链"}</span></td></tr>
|
||||||
|
<tr><th>可锁定</th><td><span className={`badge ${memory.lockable?"badge-success":"badge-warning"}`}>{memory.lockable?"✅ 可锁定":"⚠️ 不可锁定"}</span></td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
</div>}
|
||||||
|
|
||||||
<div className="card"><h3 style={{marginBottom:"1rem"}}>🔗 仓库连接</h3>
|
<div className="card"><h3 style={{marginBottom:"1rem"}}>🔗 仓库连接</h3>
|
||||||
<div className="form-group"><label className="form-label">事实源仓库</label>
|
<div className="form-group"><label className="form-label">事实源仓库</label>
|
||||||
<input className="form-input" value="https://guanghutansuohe.com/code/bingshuo/yeye" readOnly/>
|
<input className="form-input" value="https://guanghutansuohe.com/code/bingshuo/yeye" readOnly/>
|
||||||
|
|
@ -22,12 +53,14 @@ export function SettingsPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card"><h3 style={{marginBottom:"1rem"}}>🤖 本地模型</h3>
|
<div className="card"><h3 style={{marginBottom:"1rem"}}>🤖 本地模型</h3>
|
||||||
<div className="form-group"><label className="form-label">服务端点</label>
|
<div className="form-group"><label className="form-label">服务端点</label>
|
||||||
<input className="form-input" value={ep} onChange={e=>setEp(e.target.value)}/>
|
<input className="form-input" value={ep} onChange={e=>setEp(e.target.value)}/>
|
||||||
<div className="form-help">用于生成工单计划。默认连接本地 Ollama。</div>
|
<div className="form-help">用于生成工单计划。默认连接本地 Ollama。</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card"><h3 style={{marginBottom:"1rem"}}>📊 系统信息</h3>
|
<div className="card"><h3 style={{marginBottom:"1rem"}}>📊 系统信息</h3>
|
||||||
<table><tbody>
|
<table><tbody>
|
||||||
<tr><th style={{width:"140px"}}>版本</th><td>HoloLake Era v0.1</td></tr>
|
<tr><th style={{width:"140px"}}>版本</th><td>HoloLake Era v0.1</td></tr>
|
||||||
|
|
@ -37,6 +70,7 @@ export function SettingsPage() {
|
||||||
<tr><th>管家</th><td>小线 · PER-YE-LN-001</td></tr>
|
<tr><th>管家</th><td>小线 · PER-YE-LN-001</td></tr>
|
||||||
</tbody></table>
|
</tbody></table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card"><h3 style={{marginBottom:"1rem"}}>🔒 隐私</h3>
|
<div className="card"><h3 style={{marginBottom:"1rem"}}>🔒 隐私</h3>
|
||||||
<ul style={{paddingLeft:"1.2rem",fontSize:"0.9rem",color:"var(--t2)"}}>
|
<ul style={{paddingLeft:"1.2rem",fontSize:"0.9rem",color:"var(--t2)"}}>
|
||||||
<li>数据存储在 yeye Git 仓库(guanghutansuohe.com)</li>
|
<li>数据存储在 yeye Git 仓库(guanghutansuohe.com)</li>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { WorkOrder } from "../lib/types";
|
||||||
|
import { fetchWorkOrders, loadDrafts, saveDrafts, createDraftWorkOrder } from "../lib/api";
|
||||||
|
|
||||||
|
export function WorkOrderPage() {
|
||||||
|
const [orders, setOrders] = useState<WorkOrder[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [filter, setFilter] = useState<"all" | "draft" | "executing" | "completed">("all");
|
||||||
|
const [creating, setCreating] = useState(false);
|
||||||
|
const [summary, setSummary] = useState("");
|
||||||
|
const [scope, setScope] = useState("");
|
||||||
|
const [expanded, setExpanded] = useState<string | null>(null);
|
||||||
|
const [authorizations, setAuthorizations] = useState<Record<string, "approved" | "rejected" | "pending">>({});
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
setLoading(true);
|
||||||
|
const [repoOrders, drafts] = await Promise.all([fetchWorkOrders(), Promise.resolve(loadDrafts())]);
|
||||||
|
setOrders([...drafts, ...repoOrders]);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => { load(); }, []);
|
||||||
|
|
||||||
|
async function handleCreate() {
|
||||||
|
if (!summary.trim()) return;
|
||||||
|
const draft = createDraftWorkOrder(summary, scope.split("\n").filter(s => s.trim()));
|
||||||
|
const drafts = loadDrafts();
|
||||||
|
saveDrafts([draft, ...drafts]);
|
||||||
|
setSummary(""); setScope(""); setCreating(false);
|
||||||
|
await load();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAuthorize(id: string, decision: "approved" | "rejected") {
|
||||||
|
setAuthorizations(prev => ({ ...prev, [id]: decision }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const filtered = filter === "all" ? orders : orders.filter(o => o.status === filter);
|
||||||
|
const counts = {
|
||||||
|
all: orders.length, draft: orders.filter(o => o.status === "draft").length,
|
||||||
|
executing: orders.filter(o => o.status === "executing").length, completed: orders.filter(o => o.status === "completed").length,
|
||||||
|
};
|
||||||
|
|
||||||
|
return <div>
|
||||||
|
<div style={{display:"flex",alignItems:"center",justifyContent:"space-between",marginBottom:"1rem"}}>
|
||||||
|
<h1 className="page-title" style={{margin:0}}>📋 工单面板</h1>
|
||||||
|
<button className="btn btn-primary" onClick={() => setCreating(!creating)}>{creating ? "取消" : "+ 新建工单"}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{creating && <div className="card" style={{marginBottom:"1rem"}}>
|
||||||
|
<h3 style={{marginBottom:"0.75rem"}}>新建工单</h3>
|
||||||
|
<div style={{marginBottom:"1rem",padding:"0.5rem",background:"var(--bg2)",borderRadius:"8px"}}>
|
||||||
|
<div style={{fontSize:"0.8rem",color:"var(--t2)",marginBottom:"0.4rem"}}>📌 常用模板:</div>
|
||||||
|
<div style={{display:"flex",flexWrap:"wrap",gap:"0.4rem"}}>
|
||||||
|
{[{ label: "跑通新模块", summary: "在测试环境跑通新模块并验证", scope: "modules/" },
|
||||||
|
{ label: "迁移文档", summary: "把 N 份 Notion 文档迁移到 yeye 仓库", scope: "knowledge/" },
|
||||||
|
{ label: "写 GLS 标准", summary: "起草一份 GLS 标准草案", scope: "knowledge/gls-foundation/" },
|
||||||
|
{ label: "回执补录", summary: "补录历史操作的 HLDP 回执", scope: "receipts/" },
|
||||||
|
].map((t, i) => (
|
||||||
|
<button key={i} className="btn" style={{fontSize:"0.75rem",padding:"0.25rem 0.6rem"}}
|
||||||
|
onClick={() => { setSummary(t.summary); setScope(t.scope); }}>{t.label}</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="form-group"><label className="form-label">工单摘要</label><input className="form-input" value={summary} onChange={e => setSummary(e.target.value)} placeholder="一句话描述要做什么" /></div>
|
||||||
|
<div className="form-group"><label className="form-label">范围(可选)</label><textarea className="form-input" rows={2} value={scope} onChange={e => setScope(e.target.value)} placeholder="modules/..." /></div>
|
||||||
|
<div style={{fontSize:"0.75rem",color:"var(--t2)",marginBottom:"0.75rem"}}>💡 创建后存为本地草稿。接入仓库写入后同步至 yeye/receipts/。</div>
|
||||||
|
<div style={{display:"flex",gap:"0.5rem"}}><button className="btn btn-primary" onClick={handleCreate}>创建工单</button><button className="btn" onClick={() => setCreating(false)}>取消</button></div>
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
<div style={{display:"flex",gap:"0.5rem",marginBottom:"1rem",fontSize:"0.85rem"}}>
|
||||||
|
{(["all","draft","executing","completed"] as const).map(f => (
|
||||||
|
<button key={f} className="btn" onClick={() => setFilter(f)} style={{background: filter===f?"var(--ac)":"var(--bg)",color: filter===f?"white":"inherit"}}>
|
||||||
|
{f==="all"?"全部":f==="draft"?"草稿":f==="executing"?"执行中":"已完成"} ({counts[f]})
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading && <p style={{color:"#999"}}>加载中...</p>}
|
||||||
|
{!loading && filtered.length === 0 && <p style={{color:"#999"}}>暂无工单</p>}
|
||||||
|
|
||||||
|
{filtered.map(o => {
|
||||||
|
const isExpanded = expanded === o.id;
|
||||||
|
const authState = authorizations[o.id] || "pending";
|
||||||
|
const needsAuth = o.status === "draft";
|
||||||
|
return <div className="card" key={o.id} style={{cursor:"pointer"}} onClick={() => setExpanded(isExpanded ? null : o.id)}>
|
||||||
|
<div className="card-header">
|
||||||
|
<div>
|
||||||
|
<div className="card-title">{o.summary}</div>
|
||||||
|
<div className="card-subtitle">{o.id} · {o.operator} · {o.timestamp.split("T")[0]}</div>
|
||||||
|
</div>
|
||||||
|
<div style={{display:"flex",gap:"0.35rem"}}>
|
||||||
|
{authState !== "pending" && <span className={`badge ${authState==="approved"?"badge-success":"badge-draft"}`}>{authState==="approved"?"已批准":"已拒绝"}</span>}
|
||||||
|
<span className={`badge ${o.status==="completed"?"badge-success":o.status==="draft"?"badge-info":"badge-draft"}`}>
|
||||||
|
{o.status==="completed"?"已完成":o.status==="draft"?"草稿":"执行中"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isExpanded && <div style={{marginTop:"0.75rem",paddingTop:"0.75rem",borderTop:"1px solid var(--bd)"}}>
|
||||||
|
<table><tbody>
|
||||||
|
<tr>{o.scope.length>0 && <><th style={{width:"80px"}}>范围</th><td>{o.scope.join(" · ")}</td></>}</tr>
|
||||||
|
<tr><th>风险</th><td><span className="badge badge-warning">{o.risk}</span></td></tr>
|
||||||
|
<tr><th>来源</th><td>{o.source==="repo"?"yeye仓库 · receipts/":"本地草稿"}</td></tr>
|
||||||
|
<tr><th>时间</th><td>{o.timestamp}</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
{needsAuth && authState === "pending" && <div className="card" style={{marginTop:"0.75rem",background:"#fef3c7",border:"1px solid #fbbf24"}}>
|
||||||
|
<div style={{fontWeight:600,fontSize:"0.9rem",marginBottom:"0.5rem"}}>🔑 需要授权</div>
|
||||||
|
<table><tbody>
|
||||||
|
<tr><th style={{width:"80px"}}>授权范围</th><td>{o.scope.length>0 ? o.scope.join(" · ") : "全部"}</td></tr>
|
||||||
|
<tr><th>有效期</th><td>单次 · 24小时内</td></tr>
|
||||||
|
<tr><th>风险</th><td><span className="badge badge-warning">{o.risk}</span></td></tr>
|
||||||
|
<tr><th>回滚</th><td>git revert</td></tr>
|
||||||
|
<tr><th>执行者</th><td>小线 · PER-YE-LN-001</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
<div style={{display:"flex",gap:"0.5rem",marginTop:"0.75rem"}}
|
||||||
|
onClick={e => e.stopPropagation()}>
|
||||||
|
<button className="btn btn-success" onClick={() => handleAuthorize(o.id, "approved")}>
|
||||||
|
✅ 批准 — 单次执行
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-danger" onClick={() => handleAuthorize(o.id, "rejected")}>❌ 拒绝</button>
|
||||||
|
</div>
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
{needsAuth && authState !== "pending" && <div style={{marginTop:"0.75rem",padding:"0.5rem",background: authState==="approved"?"#dcfce7":"#f3f4f6",borderRadius:"8px",fontSize:"0.85rem"}}>
|
||||||
|
{authState==="approved" ? "✅ 已批准 · 单次执行 · 有效期 24 小时" : "⛔ 已拒绝"}
|
||||||
|
</div>}
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
<div style={{fontSize:"0.7rem",color:"var(--t2)",marginTop:"0.5rem"}}>
|
||||||
|
{isExpanded ? "▲ 收起" : "▼ 展开详情"}{o.source==="draft" && " · 本地草稿"}
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
})}
|
||||||
|
|
||||||
|
<div style={{marginTop:"1rem",fontSize:"0.75rem",color:"#999"}}>
|
||||||
|
数据源:yeye 仓库 receipts/ · 本地草稿 localStorage
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue