From 59d11612448eb508435ccf7bdc1be95b76aa7e42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B7=91=E5=A9=B7=E9=99=88?= Date: Tue, 28 Jul 2026 00:45:45 +0800 Subject: [PATCH] =?UTF-8?q?V0.1=E4=BA=94=E6=A8=A1=E5=9D=97=E5=85=A8?= =?UTF-8?q?=E9=83=A8=E5=AE=8C=E6=88=90:=20=E9=A2=91=E9=81=93=E9=A6=96?= =?UTF-8?q?=E9=A1=B5+=E5=B7=A5=E5=8D=95=E9=9D=A2=E6=9D=BF+=E6=8E=88?= =?UTF-8?q?=E6=9D=83=E5=8D=A1=E7=89=87+=E5=9B=9E=E6=89=A7+=E8=AE=B0?= =?UTF-8?q?=E5=BF=86=E5=81=A5=E5=BA=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- V01-TASKLIST.md | 9 ++- src/App.tsx | 8 +- src/lib/api.ts | 148 +++++++++++++++++++++++++++--------- src/pages/HomePage.tsx | 83 ++++++++++++++++++-- src/pages/PlanPage.tsx | 2 - src/pages/SettingsPage.tsx | 38 ++++++++- src/pages/WorkOrderPage.tsx | 140 ++++++++++++++++++++++++++++++++++ 7 files changed, 375 insertions(+), 53 deletions(-) delete mode 100644 src/pages/PlanPage.tsx create mode 100644 src/pages/WorkOrderPage.tsx diff --git a/V01-TASKLIST.md b/V01-TASKLIST.md index 82bdf1e..f41954d 100644 --- a/V01-TASKLIST.md +++ b/V01-TASKLIST.md @@ -90,12 +90,13 @@ created: 2026-07-23 ## 不依赖 REPO-008 可先做的 - [x] 回执数据接入 (已完成) -- [ ] 频道首页实时面板 -- [ ] 工单创建表单(先用模板,不连模型) -- [ ] 授权卡片(跟工单绑定) -- [ ] 记忆健康面板 +- [x] 频道首页实时面板 (2026-07-28 完成) +- [x] 工单创建表单(先用模板,不连模型) (2026-07-28 完成) +- [x] 授权卡片(跟工单绑定) (2026-07-28 完成) +- [x] 记忆健康面板 (2026-07-28 完成) --- > 执行者:小线 · PER-YE-LN-001 > 授权:妈妈 · ICE-GL∞ · 2026-07-23 签发 +> 完成:5/5 模块 diff --git a/src/App.tsx b/src/App.tsx index 26871e4..ec1e214 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,14 +1,14 @@ import React, { useState } from "react"; import { Page } from "./lib/types"; import { HomePage } from "./pages/HomePage"; -import { PlanPage } from "./pages/PlanPage"; +import { WorkOrderPage } from "./pages/WorkOrderPage"; import { AuthorizePage } from "./pages/AuthorizePage"; import { ReceiptPage } from "./pages/ReceiptPage"; import { SettingsPage } from "./pages/SettingsPage"; const NAV: { id: Page; label: string; icon: string }[] = [ { id: "home", label: "任务首页", icon: "📋" }, - { id: "plan", label: "计划详情", icon: "📐" }, + { id: "plan", label: "工单面板", icon: "📐" }, { id: "authorize", label: "授权管理", icon: "🔑" }, { id: "receipt", label: "回执记录", icon: "📝" }, { id: "settings", label: "系统设置", icon: "⚙️" }, @@ -29,11 +29,11 @@ export function App() {
{page === "home" && } - {page === "plan" && } + {page === "plan" && } {page === "authorize" && } {page === "receipt" && } {page === "settings" && }
); -} +} \ No newline at end of file diff --git a/src/lib/api.ts b/src/lib/api.ts index f244a30..5c631dd 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,7 +1,4 @@ // HoloLake Era · V0.1 · Gitea API 数据服务 -// 连接 guanghutansuohe.com 的 yeye 仓库作为事实源 - -// 通过 Vite 代理访问 Gitea API(避免 CORS) const BASE = "/gitea-api/repos/bingshuo/yeye/contents"; async function fetchJson(url: string): Promise { @@ -10,41 +7,44 @@ async function fetchJson(url: string): Promise { return res.json(); } -// 读取文件内容(API返回base64编码) async function readFile(path: string): Promise { const data = await fetchJson(`${BASE}/${path}?ref=main`); const bytes = Uint8Array.from(atob(data.content), (c: string) => c.charCodeAt(0)); return new TextDecoder("utf-8").decode(bytes); } -// 列出目录 async function listDir(path: string): Promise<{ name: string; type: string }[]> { const data = await fetchJson(`${BASE}/${path}?ref=main`); return Array.isArray(data) ? data.map((d: any) => ({ name: d.name, type: d.type })) : []; } -// 解析简单 YAML 回执字段 function parseReceipt(yaml: string) { const lines = yaml.split("\n"); const get = (key: string) => { const line = lines.find(l => l.trim().startsWith(key + ":")); return line ? line.split(":")[1].trim().replace(/"/g, "") : ""; }; - return { - id: get("receipt_id"), - operation: get("operation"), - operator: get("operator"), - timestamp: get("timestamp"), - result: get("result"), - }; + return { 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; - operation: string; - operator: string; + summary: string; + status: "draft" | "planned" | "authorized" | "executing" | "completed"; timestamp: string; - result: string; + operator: string; + risk: "low" | "medium" | "high"; + scope: string[]; + source: "repo" | "draft"; } export async function fetchReceipts(): Promise { @@ -53,29 +53,109 @@ export async function fetchReceipts(): Promise { const ymlFiles = files.filter(f => f.name.endsWith(".yml")); const receipts: ApiReceipt[] = []; for (const f of ymlFiles) { - try { - const raw = await readFile(`receipts/${f.name}`); - receipts.push(parseReceipt(raw)); - } catch (e) { /* skip unreadable */ } + try { const raw = await readFile(`receipts/${f.name}`); receipts.push(parseReceipt(raw)); } catch {} } return receipts.sort((a, b) => b.timestamp.localeCompare(a.timestamp)); - } catch { - return []; - } + } catch { return []; } } export async function fetchRepoStatus() { try { - // 读取记忆包获取频道状态 const raw = await readFile("memory-package.yml"); 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: "?" }; - } + 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 { + 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(); + 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 { + pathCount, priorityLevels: priorities.size, topPriorityPath: topFile, + lastSync: new Date().toISOString().split("T")[0], + externalReadable: true, hasEvidencChain: true, lockable: true, + }; + } catch { return null; } +} + +export async function fetchDashboardSummary(): Promise { + 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 { + 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", + }; +} \ No newline at end of file diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 9dff765..7122b3a 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -1,10 +1,79 @@ -import React from "react"; import { Page } from "../lib/types"; import { sampleWorkOrders } from "../lib/sample-data"; -export function HomePage({ onNavigate }: { onNavigate: (p: Page) => void }) { - const t = sampleWorkOrders.length, c = sampleWorkOrders.filter(w => w.status === "completed").length, a = sampleWorkOrders.filter(w => w.status === "planned").length; +import React, { useEffect, useState } from "react"; +import { Page } from "../lib/types"; +import { fetchDashboardSummary, DashboardSummary } from "../lib/api"; + +interface Props { onNavigate: (page: Page) => void; } + +export function HomePage({ onNavigate }: Props) { + const [dash, setDash] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetchDashboardSummary().then(d => { setDash(d); setLoading(false); }).catch(() => setLoading(false)); + }, []); + + if (loading) return

🌊 欢迎回家

加载中...

; + if (!dash) return

🌊 欢迎回家

无法连接频道仓库

; + return
-

📋 任务首页

-
{t}
全部工单
{a}
进行中
{c}
已完成
—
待处理
-

最近工单

- {sampleWorkOrders.map(wo =>
{wo.summary}
{wo.id} · {wo.createdBy}
{wo.status==="completed"?"已完成":wo.status==="planned"?"计划中":wo.status}
🎯 {wo.target}
{wo.status!=="completed"&&}
)} +

🌊 欢迎回到你的初始化频道

+ +
+
+ + 频道管家 · {dash.stewardName} + {dash.stewardId} · {dash.stewardStatus} +
+
+ 上次同步:{dash.lastSync} · 永久记忆:{dash.hasMemoryPackage ? "✅ 已启用" : "⚠️ 未检测到"} · 仓库:{dash.repoConnected ? "✅ 已连接" : "❌ 未连接"} +
+
+ +
+
+
{dash.receiptCount}
+
全部回执
+
+
+
{dash.completedWorkOrders}
+
已完成
+
+
+
{dash.activeWorkOrders}
+
进行中
+
+
+ +

快捷入口

+
+
onNavigate("plan")}> +
📋
+
工单面板
+
创建 · 追踪 · 闭环
+
+
onNavigate("settings")}> +
📦
+
模块商店
+
浏览 · 安装 · 管理
+
+
onNavigate("settings")}> +
🧠
+
记忆健康
+
模型可换 · 记忆不丢
+
+
+ +

最近回执

+ {dash.recentReceipts.map(r => ( +
+
+
{r.id}
+ {r.result==="success"?"✅":"⚠️"} +
+
{r.operation} · {r.operator}
+
{r.timestamp}
+
+ ))} +
; } diff --git a/src/pages/PlanPage.tsx b/src/pages/PlanPage.tsx deleted file mode 100644 index 8689e3d..0000000 --- a/src/pages/PlanPage.tsx +++ /dev/null @@ -1,2 +0,0 @@ -import React from "react"; -export function PlanPage() { return

📐 计划详情

WO-20260722-003 · 搭建四域原型
计划中
目标按企业四域重建创建可追溯的四域样本
范围hololake-world-minimum/
风险评估中 · 需确认入口设计
依赖项REPO-004 Tolaria 源码可构建

验收条件

  • 四域都具备独立入口
  • 主域→分域→零域→零感域 路径可追溯
  • 回执可验证

待授权项

⚠️ 需要授权
  • 创建 hololake-world-minimum/ 目录结构
  • 写入四域入口页面
  • 更新 home.md 导航链接
; } diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 898ef7c..b7b8bd8 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -1,15 +1,46 @@ import React, { useState, useEffect } from "react"; -import { fetchRepoStatus } from "../lib/api"; +import { fetchRepoStatus, fetchMemoryHealth, MemoryHealth } from "../lib/api"; export function SettingsPage() { const [ep, setEp] = useState("http://localhost:11434"); const [status, setStatus] = useState({ connected: false, branch: "检查中...", hasMemoryPackage: false, lastSync: "" }); + const [memory, setMemory] = useState(null); useEffect(() => { fetchRepoStatus().then(s => setStatus(s)).catch(() => {}); + fetchMemoryHealth().then(m => setMemory(m)).catch(() => {}); }, []); return

⚙️ 系统设置

+ + {memory &&
+

🧠 永久记忆健康

+
模型可换 · 记忆不丢 · 对话可断 · 路径可追溯
+ +
+
+
{memory.pathCount}
+
记忆路径数
+
+
+
{memory.priorityLevels}
+
优先级层级
+
+
+
{memory.lastSync}
+
最近同步
+
+
+ + + + + + + +
存储位置yeye 仓库 · memory-package.yml
首要路径{memory.topPriorityPath}
外部 AI 可读{memory.externalReadable?"✅ 是(通过 MCP)":"❌ 否"}
可追溯{memory.hasEvidencChain?"✅ Git 历史 + 路径+优先级":"⚠️ 无证据链"}
可锁定{memory.lockable?"✅ 可锁定":"⚠️ 不可锁定"}
+
} +

🔗 仓库连接

@@ -22,12 +53,14 @@ export function SettingsPage() {
+

🤖 本地模型

setEp(e.target.value)}/>
用于生成工单计划。默认连接本地 Ollama。
+

📊 系统信息

@@ -37,6 +70,7 @@ export function SettingsPage() {
版本HoloLake Era v0.1
管家小线 · PER-YE-LN-001
+

🔒 隐私

  • 数据存储在 yeye Git 仓库(guanghutansuohe.com)
  • @@ -45,4 +79,4 @@ export function SettingsPage() {
; -} +} \ No newline at end of file diff --git a/src/pages/WorkOrderPage.tsx b/src/pages/WorkOrderPage.tsx new file mode 100644 index 0000000..1d244ea --- /dev/null +++ b/src/pages/WorkOrderPage.tsx @@ -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([]); + 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(null); + const [authorizations, setAuthorizations] = useState>({}); + + 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
+
+

📋 工单面板

+ +
+ + {creating &&
+

新建工单

+
+
📌 常用模板:
+
+ {[{ 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) => ( + + ))} +
+
+
setSummary(e.target.value)} placeholder="一句话描述要做什么" />
+