diff --git a/package-lock.json b/package-lock.json index 3df7381..369f2e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "xlsx": "^0.18.5" }, "devDependencies": { + "@types/node": "^22.20.4", "@types/react": "^19.2.0", "@types/react-dom": "^19.2.0", "@vitejs/plugin-react": "^4.3.4", @@ -1292,6 +1293,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "22.20.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.4.tgz", + "integrity": "sha512-zJRE40jpHtKqE/C4fgHrAKQLJuSpzEnP9ff9Y7YtoR3Wd2pwqzlekDeEuUQXjRd+QCYnVnNwuJYmhdk9XV8gvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, "node_modules/@types/react": { "version": "19.3.0", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", @@ -2350,6 +2361,13 @@ "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", "license": "MIT" }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "node_modules/update-browserslist-db": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", diff --git a/package.json b/package.json index 5b01060..9cdf907 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "xlsx": "^0.18.5" }, "devDependencies": { + "@types/node": "^22.20.4", "@types/react": "^19.2.0", "@types/react-dom": "^19.2.0", "@vitejs/plugin-react": "^4.3.4", diff --git a/src/App.tsx b/src/App.tsx index 5f8b686..bea6c91 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,7 +2,8 @@ import { useEffect, useRef, useState } from 'react' import { auth } from './auth' import { api } from './api' import { LoginPage, ChangePasswordGate } from './components/LoginPage' -import { AdminUsersModal, ChangePasswordModal } from './components/AdminUsersModal' +import { ChangePasswordModal } from './components/AdminUsersModal' +import { AdminZone } from './components/AdminZone' import { Icon } from '@ui' import { store, useStore } from './store' import { TaskSidebar } from './components/TaskSidebar' @@ -148,7 +149,7 @@ export function App() { {trackerRunId && } {ui.settingsOpen && } {ui.projectSettingsId && } - {ui.adminUsersOpen && store.setUI({ adminUsersOpen: false })} />} + {ui.adminZoneOpen && } {ui.changePwOpen && store.setUI({ changePwOpen: false })} />} {viewingArtifact && } {toast &&
{toast}
} diff --git a/src/components/AdminUsersModal.tsx b/src/components/AdminUsersModal.tsx index da8c592..a195626 100644 --- a/src/components/AdminUsersModal.tsx +++ b/src/components/AdminUsersModal.tsx @@ -1,13 +1,9 @@ -// 管理员用户管理弹层 + 主动改密弹窗(R2 管理员发放制)。 -// AdminUsersModal:建号(用户名/显示名/初始密码/管理员位)、重置密码、列表状态。 -// ChangePasswordModal:登录者主动改密(与强制改密共用 API)。 -import { useEffect, useState } from 'react' +// 管理员用户管理已迁「系统管理」分区(components/adminSections.tsx UsersPanel, +// 重设计方案 阶段②)。本文件只保留登录者主动改密弹窗(与强制改密共用 API)。 +import { useState } from 'react' import { api } from '../api' import { store, useStore } from '../store' -import { Icon, Avatar } from '@ui' -import { randomPassword } from '../passwords' - -interface AdminUserRow { owner: string; name: string; isAdmin: boolean; mustChangePassword: boolean; hasPassword: boolean; passwordSetAt?: string; createdAt: string } +import { Icon } from '@ui' function Shell({ title, onClose, children }: { title: string; onClose: () => void; children: React.ReactNode }) { return ( @@ -24,100 +20,6 @@ function Shell({ title, onClose, children }: { title: string; onClose: () => voi ) } -export function AdminUsersModal({ onClose }: { onClose: () => void }) { - const me = useStore(s => s.me) - const [users, setUsers] = useState(null) - const [err, setErr] = useState('') - const [notice, setNotice] = useState('') - const [owner, setOwner] = useState('') - const [name, setName] = useState('') - const [pw, setPw] = useState('') - const [asAdmin, setAsAdmin] = useState(false) - const [busy, setBusy] = useState(false) - const [resetFor, setResetFor] = useState(null) - const [resetPw, setResetPw] = useState('') - - const refresh = async () => { - try { setUsers(await api.listAdminUsers()) } catch (e) { setErr(String(e)) } - } - useEffect(() => { refresh() }, []) - - const create = async () => { - setErr(''); setNotice('') - if (pw.length < 8) { setErr('初始密码至少 8 位'); return } - setBusy(true) - try { - await api.createAdminUser(owner.trim(), name.trim(), pw, asAdmin) - setNotice(`已创建 ${owner.trim()}:首次登录须改密`) - setOwner(''); setName(''); setPw('') - await refresh() - } catch (e) { setErr(String(e).includes('409') ? '用户名已存在' : String(e)) } finally { setBusy(false) } - } - - const reset = async () => { - if (!resetFor) return - setErr('') - if (resetPw.length < 8) { setErr('新密码至少 8 位'); return } - setBusy(true) - try { - await api.resetAdminUserPassword(resetFor, resetPw) - setNotice(`已重置 ${resetFor} 的密码:其下次登录须改密(现有会话已吊销)`) - setResetFor(null); setResetPw('') - await refresh() - } catch (e) { setErr(String(e)) } finally { setBusy(false) } - } - - return ( - - {me?.isAdmin && ( -
-
- setOwner(e.target.value)} /> - setName(e.target.value)} /> -
-
- setPw(e.target.value)} /> - - - -
-

创建后该用户首次登录必须修改此初始密码。

-
- )} - {notice &&
{notice}
} - {err &&
{err}
} - {resetFor ? ( -
- 重置 {resetFor} 的密码: - setResetPw(e.target.value)} /> - - - -
- ) : ( - - - - {(users ?? []).map(u => ( - - - - - - - - - ))} - {users !== null && users.length === 0 && } - -
用户名显示名角色状态密码设置于
{u.owner}{u.name || '—'}{u.isAdmin ? ADMIN : USER}{u.mustChangePassword ? 待改密 : u.hasPassword ? '正常' : '未设密码'}{u.passwordSetAt ? new Date(u.passwordSetAt).toLocaleString() : '—'}{me?.isAdmin && ( - - )}
暂无账号
- )} -
- ) -} - /** 主动改密(账户菜单入口):当前密码 + 新密码 + 确认。 */ export function ChangePasswordModal({ onClose }: { onClose: () => void }) { const me = useStore(s => s.me) diff --git a/src/components/AdminZone.tsx b/src/components/AdminZone.tsx new file mode 100644 index 0000000..bc83811 --- /dev/null +++ b/src/components/AdminZone.tsx @@ -0,0 +1,66 @@ +// 系统管理(重设计方案 阶段②):管理员专属的全屏独立视图(带返回,非弹窗)。 +// 用户决策:系统管理 = 全屏独立视图;普通用户界面不暴露任何系统能力。 +// 分区注册表:新增管理页只改 SECTIONS,不改 AdminZone 本体(声明式注册)。 +import { useState } from 'react' +import { store, useStore } from '../store' +import { Icon, type IconName } from '@ui' +import { UsersPanel, ProfilesSection, PluginsSection, PluginWorkspacesPanel } from './adminSections' + +interface AdminSectionDef { + key: string + label: string + icon: IconName + desc: string + /** 懒渲染:进入分区才挂载(每个面板自带数据加载)。 */ + render: () => React.ReactNode +} + +function ProfilesPanelWrapper() { + const profiles = useStore(s => s.profiles) + return +} + +const SECTIONS: AdminSectionDef[] = [ + { key: 'users', label: '用户管理', icon: 'users', desc: '建号、发放初始密码、重置密码(管理员发放制)', render: () => }, + { key: 'profiles', label: '智能体画像', icon: 'bot', desc: '系统级画像:系统提示、工具允许/确认/拒绝清单', render: () => }, + { key: 'plugins', label: '插件管理', icon: 'puzzle', desc: '已装载插件清单与热重载(装卸不影响核心)', render: () => }, + { key: 'workspaces', label: '插件工作区', icon: 'grid', desc: '插件声明面板与服务端声明工作区', render: () => }, +] + +export function AdminZone() { + const me = useStore(s => s.me) + const [sectionKey, setSectionKey] = useState('users') + // 双保险:入口已在菜单层门控(me?.isAdmin),此处再拦一次(纵深防御)。 + if (!me?.isAdmin) return null + const section = SECTIONS.find(s => s.key === sectionKey) ?? SECTIONS[0] + return ( +
+
+ + 系统管理 + 仅管理员可见 · 变更即时生效 + + {me.owner} +
+
+ +
+
+

{section.label}

+

{section.desc}

+
+ {section.render()} +
+
+
+ ) +} diff --git a/src/components/AgentTracker.tsx b/src/components/AgentTracker.tsx index 1052836..55e2b3c 100644 --- a/src/components/AgentTracker.tsx +++ b/src/components/AgentTracker.tsx @@ -25,56 +25,50 @@ export function AgentTracker() { const focusMsgs = focus === runId ? messagesAll.filter(m => m.runId === runId) : subMessages[focus] ?? [] return ( + // 阶段③ 收敛:只保留 OverlayHost 一层壳(此前在其内又嵌 .modal 双层窗框)。 store.setUI({ trackerRunId: null })}> -
-
- 🤖 Sub-agent 追踪 - - -
-
- {!root &&
加载中…
} - {root && ( -
-
-
- - {root.kind === 'subagent' ? root.agentName || '子代理' : '主任务'} - {root.status}{root.phase ? ` · ${root.phase}` : ''} - - +
+ {!root &&
加载中…
} + {root && ( + <> +
+
+ + {root.kind === 'subagent' ? root.agentName || '子代理' : '主任务'} + {root.status}{root.phase ? ` · ${root.phase}` : ''} + + +
+ {children.length > 0 && ( +
+ {children.map(c => ( +
setSelected(c.id)}> + + 🤖 {c.agentName || c.id} + {c.status} +
+ ))}
- {children.length > 0 && ( -
- {children.map(c => ( -
setSelected(c.id)}> - - 🤖 {c.agentName || c.id} - {c.status} -
- ))} -
- )} -
-
中间过程({focus === runId ? '主任务' : '子代理'})
-
- {focusMsgs.length === 0 &&
(无中间过程消息)
} - {focusMsgs.map(m => ( -
- {m.parts.map((p, i) => ( -
- {p.type === 'text' && p.text} - {p.type === 'thinking' && {p.text}} - {p.type === 'tool_call' && 🛠 {p.toolCall?.name}} -
- ))} -
- ))} -
+ )}
- )} -
+
中间过程({focus === runId ? '主任务' : '子代理'})
+
+ {focusMsgs.length === 0 &&
(无中间过程消息)
} + {focusMsgs.map(m => ( +
+ {m.parts.map((p, i) => ( +
+ {p.type === 'text' && p.text} + {p.type === 'thinking' && {p.text}} + {p.type === 'tool_call' && 🛠 {p.toolCall?.name}} +
+ ))} +
+ ))} +
+ + )}
) diff --git a/src/components/Conversation.tsx b/src/components/Conversation.tsx index 26da6ec..9429a7c 100644 --- a/src/components/Conversation.tsx +++ b/src/components/Conversation.tsx @@ -1,5 +1,7 @@ // 中栏:消息流(Part 渲染器注册表)+ 流式渲染 + 过程折叠 + HITL 卡片 + 输入区(4.4)。 -import { Icon, Avatar, BrandMark } from '@ui' +// 重设计方案 阶段④:折叠区全部迁 @ui Collapsible(
已禁);工具调用块走 +// ToolBlock 契约(运行态扫光、完成边沿收起、展开态记忆);live 行扫光 + 已工作计时。 +import { Icon, Avatar, BrandMark, Collapsible, ToolBlock } from '@ui' import { useEffect, useMemo, useRef, useState } from 'react' import { marked } from 'marked' import { api } from '../api' @@ -21,12 +23,8 @@ function PartView({ part, live }: { part: Part; live?: boolean | null }) { dangerouslySetInnerHTML={{ __html: marked.parse(part.text ?? '') }} /> case 'thinking': // 流式期间展开可实时看到思考;run 终态后收进过程折叠区,默认收起。 - return ( -
- 思考过程 -
{part.text}
-
- ) + //(此单件形态用于过程组之外散落的思考;组内思考由 ProcessParts 渲染。) + return case 'tool_call': return 🛠 {part.toolCall?.name} case 'tool_result': { @@ -40,13 +38,14 @@ function PartView({ part, live }: { part: Part; live?: boolean | null }) { const art = a ? findArtifact(a.artifactId) : undefined const name = a?.name ?? '工件' const icon = artifactIcon(name, a?.kind ?? art?.kind) + // 阶段③ 收敛:标题点击 = 右栏预览(唯一落位);动作只剩 全屏预览 + 下载。 return ( - - + e.stopPropagation()}> @@ -93,40 +92,108 @@ function findCard(id: string) { return cardCache[id] } -// 过程折叠:run 终态后把连续 process 部件折叠为摘要行(可展开回看)。 -function ProcessCollapse({ parts }: { parts: Part[] }) { +// 思考块(Collapsible 契约):左线弱色,流式默认展开、终态默认收起;展开态记忆。 +function ThinkingBlock({ part, idKey, defaultOpen }: { part: Part; idKey: string; defaultOpen?: boolean }) { + return ( +
+ ▸思考过程}> +
{part.text}
+
+
+ ) +} + +// 过程段渲染(配对增强):tool_call 吸收紧随其后的同名 tool_result 组成 ToolBlock +//(运行态扫光、完成边沿收起、展开看结果摘要);其余部件原位 PartView。 +function ProcessParts({ parts, live, idKey }: { parts: Part[]; live: boolean; idKey: string }) { + const out: React.ReactNode[] = [] + for (let i = 0; i < parts.length; i++) { + const p = parts[i] + if (p.type === 'tool_call') { + const res = parts[i + 1]?.type === 'tool_result' ? parts[i + 1] : null + if (res) i++ + const st = res?.toolResult?.status + out.push( + + {res &&
{res.toolResult?.summary}
} +
) + continue + } + out.push(
) + } + return <>{out} +} + +// 过程折叠:run 终态后把连续 process 部件折叠为摘要行(可展开回看;Collapsible 契约)。 +function ProcessCollapse({ parts, idKey }: { parts: Part[]; idKey: string }) { const toolCount = parts.filter(p => p.type === 'tool_call').length const hasThinking = parts.some(p => p.type === 'thinking' && p.text) return ( -
- - 过程输出 - {hasThinking ? '含思考' : ''}{toolCount > 0 ? ` · ${toolCount} 次工具调用` : ''}(点击展开) - -
- {parts.map((p, i) =>
)} -
-
+
+ + ▸ + 过程输出 + + {hasThinking ? '含思考' : ''}{toolCount > 0 ? ` · ${toolCount} 次工具调用` : ''} + + }> +
+ +
+
+
) } // 长回复折叠阈值(CF0):终态后超长正文收成摘要行,点击展开全文;live 期间全文可见。 const LONG_REPLY_CHARS = 800 -function LongReplyCollapse({ text }: { text: string }) { +function LongReplyCollapse({ text, idKey }: { text: string; idKey: string }) { // 摘句取压缩空白后的前 100 字;字数按 Markdown 源码长度计。 const excerpt = text.replace(/\s+/g, ' ').trim().slice(0, 100) return ( -
- - 📄 长回复 - {text.length} 字(按 Markdown 源码计) - {excerpt && {excerpt}…} - -
-
-
-
+
+ + ▸ + 📄 长回复 + {text.length} 字(按 Markdown 源码计) + {excerpt && {excerpt}…} + }> +
+
+
+ +
+ ) +} + +// 已工作计时(ZCode「已工作 X 分 X 秒」摘要行;1s 步进,仅 live 期间挂载)。 +function useElapsed(startedAt: number): string { + const [, tick] = useState(0) + useEffect(() => { + const t = window.setInterval(() => tick(n => n + 1), 1000) + return () => window.clearInterval(t) + }, []) + const s = Math.max(0, Math.floor((Date.now() - startedAt) / 1000)) + const m = Math.floor(s / 60) + return m > 0 ? `${m} 分 ${s % 60} 秒` : `${s} 秒` +} + +// 流式状态行(独立组件:计时 hook 只在 live 行挂载期间运转)。 +function LiveBar({ live }: { live: LiveState }) { + const elapsed = useElapsed(live.startedAt) + return ( + + + {live.phase} + 已工作 {elapsed} + ) } @@ -151,16 +218,16 @@ function AssistantMessage({ msg, live }: { msg: Message; live?: LiveState | null
助手 - {live && {live.phase}} + {live && }
{blocks.map((b, i) => b.kind === 'process' ? (showProcess - ?
{b.parts.map((p, j) =>
)}
- : ) + ?
+ : ) : (() => { // CF0:非 live 的超长文本部件折叠为摘要行;live 与短文本不受影响。 if (!live && b.part.type === 'text' && (b.part.text?.length ?? 0) > LONG_REPLY_CHARS) - return + return return
@@ -174,22 +241,25 @@ function AssistantMessage({ msg, live }: { msg: Message; live?: LiveState | null // 上下文组装观测(4.10/4.17):模型实际看到什么、裁了什么——可解释性入口。 function ContextObsView({ obs }: { obs: ContextObs }) { return ( -
- - 🧩 上下文构成 - {obs.items.join(' · ')}(约 {obs.estChars} 字符) - -
-
    - {obs.items.map((it, i) =>
  • {it}
  • )} -
- {obs.truncations && obs.truncations.length > 0 && ( -
- {obs.truncations.map((t, i) =>
⚠ {t}
)} -
- )} -
-
+
+ + ▸ + 🧩 上下文构成 + {obs.items.join(' · ')}(约 {obs.estChars} 字符) + }> +
+
    + {obs.items.map((it, i) =>
  • {it}
  • )} +
+ {obs.truncations && obs.truncations.length > 0 && ( +
+ {obs.truncations.map((t, i) =>
⚠ {t}
)} +
+ )} +
+
+
) } @@ -298,7 +368,7 @@ export function Conversation() {
⌘
选择或创建一个会话开始
-
mock 演示模式可在 设置 → runtime 开启,无需模型密钥
+
mock 演示模式可在 设置 → runtime 开启,无需模型密钥
) @@ -365,7 +435,7 @@ export function Conversation() { {messages.length === 0 &&
发送消息开始对话
-
mock 模式演示指令:#hitl · #agents · #doc · #error · 命令:/help
+
mock 模式演示指令:#hitl · #agents · #doc · #error · 命令:/help
}
@@ -388,7 +458,9 @@ export function Conversation() { e.target.value = '' }} /> -