cc 1 週間 前
親
コミット
5e4de566d4

+ 642 - 0
ai-electron/docs/design/ui-modes-v1.html

@@ -0,0 +1,642 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>清鉴 · 主界面双模式设计 v1(传统 / 智能)</title>
+<style>
+:root{
+  --primary:#2a50ec; --primary-deep:#1e3fd1; --primary-soft:rgba(42,80,236,.07);
+  --canvas:#f0f2f5; --card:#fff; --border:#e3e7ef; --border-strong:#d0d7de;
+  --text-1:#24292f; --text-2:#57606a; --text-3:#8b919c;
+  --ok:#1a7f37; --warn:#b58a05; --err:#d93025;
+  --sider:#273352; --sider-2:#1f2942;
+  --r-ctl:6px; --r-block:8px; --r-page:12px;
+  --mono:'SFMono-Regular',Consolas,Menlo,monospace;
+  --shadow-pop:0 6px 24px rgba(24,39,88,.10);
+}
+*{margin:0;padding:0;box-sizing:border-box}
+body{background:#dde1e8;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',Roboto,sans-serif;color:var(--text-1);font-size:14px;line-height:1.6}
+svg.i{width:1em;height:1em;fill:currentColor;vertical-align:-0.14em}
+.mono{font-family:var(--mono)}
+
+.doc-head{max-width:1500px;margin:28px auto 14px;padding:0 24px;display:flex;align-items:baseline;gap:16px}
+.doc-head h1{font-size:20px;font-weight:700}
+.doc-head .tag{font-size:12px;color:#fff;background:var(--primary);border-radius:99px;padding:2px 12px;font-weight:600}
+.doc-head .sub{color:var(--text-3);font-size:13px}
+.doc-head .num{width:22px;height:22px;border-radius:6px;background:var(--primary);color:#fff;display:inline-flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;align-self:center}
+.frame{max-width:1500px;margin:0 auto 16px;padding:0 24px}
+.window{position:relative;background:var(--canvas);border-radius:var(--r-page);border:1px solid var(--border-strong);overflow:hidden;box-shadow:var(--shadow-pop);display:flex;flex-direction:column}
+
+/* ── 传统模式壳 ── */
+.shell-top{height:46px;background:#fff;border-bottom:1px solid var(--border);display:flex;align-items:center;padding:0 14px;gap:14px}
+.logo{display:flex;align-items:center;gap:8px;font-weight:700;font-size:14px}
+.logo .mark{width:24px;height:24px;border-radius:6px;background:var(--primary);color:#fff;display:flex;align-items:center;justify-content:center;font-size:13px}
+.top-menu{display:flex;gap:2px}
+.top-menu span{padding:5px 12px;border-radius:6px;font-size:13px;color:#d8def5;cursor:default;white-space:nowrap}
+.top-menu span.on{background:rgba(255,255,255,.12);color:#fff;font-weight:600}
+.case-chip{display:inline-flex;align-items:center;gap:6px;font-size:12.5px;color:var(--text-2);background:#f2f4f8;border-radius:99px;padding:3px 11px}
+.case-chip b{color:var(--text-1);font-weight:600}
+.shell-body{display:flex;height:560px}
+.mix-sider{width:210px;flex:none;background:var(--sider);padding:10px 8px;display:flex;flex-direction:column;gap:2px}
+.ms-item{display:flex;align-items:center;gap:9px;padding:8px 10px;border-radius:7px;color:#aeb7d6;font-size:13px;cursor:default}
+.ms-item .i{font-size:14px;opacity:.85}
+.ms-item.on{background:var(--primary);color:#fff;font-weight:600}
+.ms-item.disabled{opacity:.32;text-decoration:line-through}
+.ms-sec{font-size:10.5px;color:#6b7899;letter-spacing:.06em;margin:10px 8px 4px;font-weight:600}
+.tabs{height:34px;background:#fff;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:6px;padding:0 12px}
+.tab{height:24px;padding:0 12px;border-radius:6px;font-size:12px;color:var(--text-2);display:inline-flex;align-items:center;gap:5px;border:1px solid var(--border)}
+.tab.on{color:var(--primary);border-color:var(--primary);background:var(--primary-soft);font-weight:600}
+.shell-content{flex:1;display:flex;align-items:center;justify-content:center;background:var(--canvas);position:relative}
+.content-hint{color:var(--text-3);font-size:13px;text-align:center}
+.content-hint b{color:var(--text-2)}
+/* 顶栏右侧 action */
+.actions{margin-left:auto;display:flex;align-items:center;gap:10px}
+/* 模式切换(核心设计件) */
+.mode-switch{display:inline-flex;background:#eef0f4;border-radius:99px;padding:2px;gap:2px;border:1px solid var(--border)}
+.mode-switch button{height:26px;padding:0 12px;border:none;background:transparent;border-radius:99px;font-size:12.5px;color:var(--text-2);display:inline-flex;align-items:center;gap:5px;cursor:pointer}
+.mode-switch button.on{background:#fff;color:var(--primary);font-weight:650;box-shadow:0 1px 4px rgba(24,39,88,.14)}
+.mode-switch button .i{font-size:13px}
+.avatar{width:26px;height:26px;border-radius:50%;background:var(--primary);color:#fff;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700}
+.iconbtn{width:28px;height:28px;border-radius:7px;border:none;background:transparent;color:var(--text-2);display:inline-flex;align-items:center;justify-content:center;cursor:pointer}
+.iconbtn:hover{background:#f2f4f8}
+.callout{position:absolute;background:var(--primary);color:#fff;font-size:11.5px;padding:5px 10px;border-radius:7px;box-shadow:var(--shadow-pop);max-width:210px;line-height:1.5;z-index:5}
+.callout::after{content:'';position:absolute;border:6px solid transparent}
+.callout.tr::after{border-bottom-color:var(--primary);right:24px;top:-12px}
+
+/* ── 智能模式工作台 ── */
+.wb{height:760px}
+.wb-top{height:50px;background:#fff;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:12px;padding:0 16px}
+.wb-body{display:flex;height:calc(100% - 50px)}
+.rail{width:264px;flex:none;background:#f7f8fa;border-right:1px solid var(--border);display:flex;flex-direction:column}
+.rail__menu{display:flex;flex-direction:column;gap:2px;padding:10px 8px;border-bottom:1px solid var(--border)}
+.rm{display:flex;align-items:center;gap:9px;padding:8px 10px;border-radius:7px;color:var(--text-2);font-size:13px;cursor:pointer;border:none;background:transparent;text-align:left}
+.rm:hover{background:#eef0f4;color:var(--primary)}
+.rm.on{background:var(--primary-soft);color:var(--primary);font-weight:600}
+.rm .i{font-size:15px;flex:none}
+.rail__new{margin:12px 12px 4px;height:36px;border-radius:9px;border:none;background:var(--primary);color:#fff;font-size:13.5px;font-weight:650;display:flex;align-items:center;justify-content:center;gap:7px;cursor:pointer}
+.rail__new:hover{background:var(--primary-deep)}
+.rail__sec{font-size:10.5px;color:var(--text-3);letter-spacing:.06em;margin:14px 14px 5px;font-weight:600}
+.sess{position:relative}
+.sess__more{position:absolute;right:6px;top:8px;width:22px;height:22px;border-radius:6px;display:none;align-items:center;justify-content:center;color:var(--text-3);cursor:pointer}
+.sess:hover .sess__more,.sess.on .sess__more{display:flex}
+.sess__more:hover{background:#e3e7ef;color:var(--primary)}
+.sess.is-renaming{background:#fff;border:1px solid var(--primary);padding:7px 8px}
+.sess__rename{width:100%;height:26px;border:1px solid var(--border);border-radius:5px;font:inherit;font-size:12.5px;padding:0 7px;outline:none}
+.sess__rename:focus{border-color:var(--primary)}
+.sess__rename-acts{display:flex;gap:5px;margin-top:6px}
+.rk{height:22px;padding:0 10px;border-radius:5px;border:none;background:var(--primary);color:#fff;font-size:11px;cursor:pointer}
+.rk--cancel{background:#f2f4f8;color:var(--text-2)}
+.sess-menu{position:fixed;background:#fff;border:1px solid var(--border);border-radius:8px;box-shadow:var(--shadow-pop);padding:4px;z-index:20;min-width:110px}
+.sess-menu button{display:flex;align-items:center;gap:7px;width:100%;padding:6px 10px;border:none;background:transparent;border-radius:6px;font-size:12.5px;color:var(--text-1);cursor:pointer;text-align:left}
+.sess-menu button:hover{background:#f2f4f8}
+.sess-menu button.danger{color:var(--err)}
+.sess-menu button.danger:hover{background:var(--err-soft)}
+.sess{margin:0 8px;padding:8px 10px;border-radius:8px;cursor:default}
+.sess:hover{background:#eef0f4}
+.sess.on{background:var(--primary-soft)}
+.sess__t{font-size:13px;color:var(--text-1);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:flex;align-items:center;gap:6px}
+.sess__t time{margin-left:auto;font-size:10.5px;color:var(--text-3);font-weight:400}
+.sess.on .sess__t{color:var(--primary);font-weight:600}
+.sess__m{font-size:11px;color:var(--text-3);margin-top:1px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+.eng{font-size:9.5px;padding:0 6px;border-radius:4px;font-weight:700;flex:none}
+.eng--local{color:#0e7c86;background:rgba(14,124,134,.1)}
+.eng--cloud{color:#7b46ec;background:rgba(123,70,236,.1)}
+.rail__foot{margin-top:auto;border-top:1px solid var(--border);padding:8px;display:grid;grid-template-columns:repeat(4,1fr);gap:4px}
+.rf{display:flex;flex-direction:column;align-items:center;gap:3px;padding:7px 2px;border-radius:8px;color:var(--text-2);font-size:10.5px;cursor:pointer;border:none;background:transparent}
+.rf:hover{background:#eef0f4;color:var(--primary)}
+.rf .i{font-size:16px}
+.wb-main{flex:1;min-width:0;display:flex;flex-direction:column;position:relative}
+.wb-msgs{flex:1;overflow:hidden;padding:22px 26px 8px;background:var(--canvas)}
+.wb-col{max-width:860px;margin:0 auto;display:flex;flex-direction:column;gap:22px}
+.wb-composer{flex:none;padding:8px 26px 16px}
+/* 右栏 */
+.wb-aside{width:328px;flex:none;border-left:1px solid var(--border);background:#f7f8fa;display:flex;flex-direction:column;padding:12px;gap:12px}
+.panel{background:var(--card);border:1px solid var(--border);border-radius:var(--r-block)}
+.panel__head{display:flex;align-items:center;gap:7px;padding:9px 12px;border-bottom:1px solid var(--border);font-size:12.5px;font-weight:650;color:var(--text-2)}
+.panel__head .i{color:var(--primary);font-size:14px}
+.panel__head .n{margin-left:auto;font-size:11px;color:var(--text-3)}
+.pin{margin:10px;border:1px solid var(--border);border-radius:8px;overflow:hidden}
+.pin__t{display:flex;align-items:center;gap:6px;padding:6px 10px;font-size:11.5px;color:var(--text-2);background:#fafbfd;border-bottom:1px solid var(--border)}
+.pin .bars{height:64px;display:flex;align-items:flex-end;gap:5px;padding:8px 14px}
+.pin .bars b{flex:1;background:var(--primary);border-radius:3px 3px 0 0;opacity:.85}
+/* 折叠右栏按钮 */
+.collapse-rail{position:absolute;right:0;top:50%;transform:translateY(-50%);width:18px;height:56px;border:1px solid var(--border);border-right:none;border-radius:9px 0 0 9px;background:#fff;color:var(--text-3);display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:4}
+/* 弹层:模型管理(智能模式里做成抽屉/弹层,不再占菜单) */
+.pop{position:absolute;background:#fff;border:1px solid var(--border);border-radius:12px;box-shadow:var(--shadow-pop);z-index:9}
+.pop__head{display:flex;align-items:center;gap:8px;padding:12px 16px;border-bottom:1px solid var(--border);font-weight:700;font-size:13.5px}
+.pop__head .i{color:var(--primary)}
+.pop__head .x{margin-left:auto;color:var(--text-3);cursor:pointer}
+.model-cards{display:grid;grid-template-columns:1fr 1fr;gap:10px;padding:14px 16px}
+.mc{border:1px solid var(--border);border-radius:10px;padding:10px 12px;position:relative}
+.mc.on{border-color:var(--primary);background:var(--primary-soft)}
+.mc__n{font-size:13px;font-weight:650;display:flex;align-items:center;gap:6px}
+.mc__n .dot{width:6px;height:6px;border-radius:50%;background:var(--ok)}
+.mc__u{font-size:11px;color:var(--text-3);margin-top:3px;font-family:var(--mono)}
+.mc__tag{position:absolute;top:8px;right:8px;font-size:9.5px;padding:1px 7px;border-radius:99px;background:var(--primary);color:#fff;font-weight:600}
+.pop__foot{padding:10px 16px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;gap:8px}
+.pbtn{height:28px;padding:0 14px;border-radius:7px;border:1px solid var(--border);background:#fff;font-size:12.5px;color:var(--text-2);cursor:pointer}
+.pbtn--p{background:var(--primary);color:#fff;border:none;font-weight:600}
+
+/* ── 设计说明 ── */
+.notes{max-width:1500px;margin:24px auto 60px;padding:0 24px;display:grid;grid-template-columns:repeat(3,1fr);gap:14px}
+.note{background:#fff;border:1px solid var(--border);border-radius:10px;padding:15px 17px}
+.note h3{font-size:13.5px;margin-bottom:7px;display:flex;align-items:center;gap:7px}
+.note h3 .n{width:18px;height:18px;border-radius:5px;background:var(--primary);color:#fff;font-size:11px;display:inline-flex;align-items:center;justify-content:center;flex:none}
+.note p{font-size:12.5px;color:var(--text-2);line-height:1.75}
+.note code{font-family:var(--mono);font-size:11.5px;background:#f2f4f8;border-radius:4px;padding:1px 5px;color:#1f3a8f}
+
+/* ══════════ 工作台内页面(模型/插件/看板) ══════════ */
+.page{flex:1;min-width:0;padding:22px 30px;overflow:auto}
+.page__head{display:flex;align-items:flex-start;gap:14px;max-width:1060px;margin:0 auto 18px}
+.page__head h3{font-size:17px;font-weight:700;margin:0}
+.page__head p{font-size:12.5px;color:var(--text-3);margin:3px 0 0}
+.page__head .sp{flex:1}
+.page__body{max-width:1060px;margin:0 auto}
+.mtabs{display:flex;gap:6px;margin-bottom:14px}
+.mtabs span{height:30px;padding:0 14px;border-radius:99px;border:1px solid var(--border);background:#fff;font-size:12.5px;color:var(--text-2);display:inline-flex;align-items:center;cursor:pointer}
+.mtabs span.on{color:var(--primary);border-color:var(--primary);background:var(--primary-soft);font-weight:600}
+.mgrid{display:grid;grid-template-columns:1fr 1fr;gap:12px}
+.mcard{background:#fff;border:1px solid var(--border);border-radius:10px;padding:13px 15px;position:relative}
+.mcard.on{border-color:var(--primary);background:var(--primary-soft)}
+.mcard__n{font-size:14px;font-weight:650;display:flex;align-items:center;gap:7px}
+.mcard__n .dot{width:7px;height:7px;border-radius:50%;background:var(--ok)}
+.mcard__u{font-family:var(--mono);font-size:11.5px;color:var(--text-3);margin-top:4px}
+.mcard__k{font-size:11.5px;color:var(--text-2);margin-top:2px}
+.mcard__tag{position:absolute;top:10px;right:11px;font-size:9.5px;padding:1px 8px;border-radius:99px;background:var(--primary);color:#fff;font-weight:700}
+.mcard__acts{display:flex;gap:5px;margin-top:9px;opacity:.85}
+.mcard__acts .ab{height:24px;padding:0 9px;border-radius:6px;display:inline-flex;align-items:center;font-size:11.5px;color:var(--text-2);cursor:pointer;border:1px solid var(--border);background:transparent}
+.mcard__acts .ab:hover{background:#eef0f4;color:var(--primary);border-color:var(--primary)}
+.mcard__acts .ab--danger{color:var(--err)}
+.mcard__acts .ab--danger:hover{background:var(--err-soft);color:var(--err);border-color:var(--err)}
+.psec{background:#fff;border:1px solid var(--border);border-radius:10px;margin-bottom:14px;overflow:hidden}
+.psec__head{display:flex;align-items:center;gap:8px;padding:10px 14px;border-bottom:1px solid var(--border);font-size:13px;font-weight:650}
+.psec__head .n{margin-left:auto;font-size:11px;color:var(--text-3);font-weight:400}
+.prow{display:grid;grid-template-columns:16px 1fr auto auto;gap:12px;align-items:center;padding:10px 14px;border-bottom:1px dashed var(--border)}
+.prow:last-child{border-bottom:none}
+.prow .i{color:var(--text-3);font-size:14px}
+.prow__n{font-size:13px;font-weight:600;color:var(--text-1)}
+.prow__d{font-size:11.5px;color:var(--text-3);font-family:var(--mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:420px}
+.prow__meta{font-size:11px;color:var(--text-3)}
+.sw{width:34px;height:19px;border-radius:99px;background:var(--primary);position:relative;flex:none}
+.sw::after{content:'';position:absolute;top:2px;right:2px;width:15px;height:15px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.2)}
+.sw.off{background:#cfd4dd}.sw.off::after{right:auto;left:2px}
+.tchip{font-size:10px;padding:1px 7px;border-radius:4px;font-weight:600}
+.tchip--http{color:#0e7c86;background:rgba(14,124,134,.09)}
+.tchip--cmd{color:#7b46ec;background:rgba(123,70,236,.09)}
+.tchip--scope{color:var(--text-2);background:#f2f4f8}
+.bgrid{display:grid;grid-template-columns:1fr 1fr;gap:14px}
+.bslot{border:1.5px dashed var(--border);border-radius:10px;min-height:150px;display:flex;align-items:center;justify-content:center;color:var(--text-3);font-size:12.5px;flex-direction:column;gap:6px}
+.page__empty-hint{display:flex;align-items:center;gap:8px;margin:0 0 14px;padding:9px 14px;background:var(--primary-soft);border-radius:8px;font-size:12.5px;color:var(--text-2)}
+</style>
+</head>
+<body>
+
+<svg style="display:none" xmlns="http://www.w3.org/2000/svg">
+  <symbol id="i-robot" viewBox="0 0 24 24"><path d="M12 2a2 2 0 0 1 2 2 2 2 0 0 1-1 1.73V7h3a4 4 0 0 1 4 4v1h1v4h-1v2a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-2H2v-4h1v-1a4 4 0 0 1 4-4h3V5.73A2 2 0 0 1 12 2m-4 8a2 2 0 1 0 0 4 2 2 0 0 0 0-4m8 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4"/></symbol>
+  <symbol id="i-send" viewBox="0 0 24 24"><path d="M2 21 23 12 2 3v7l15 2-15 2z"/></symbol>
+  <symbol id="i-check" viewBox="0 0 24 24"><path d="M21 7 9 19l-5.5-5.5 1.4-1.4L9 16.2 19.6 5.6z"/></symbol>
+  <symbol id="i-chev" viewBox="0 0 24 24"><path d="M7.4 8.6 12 13.2l4.6-4.6L18 10l-6 6-6-6z"/></symbol>
+  <symbol id="i-brain" viewBox="0 0 24 24"><path d="M12 2a3 3 0 0 1 3 3v.34A4.5 4.5 0 0 1 18.5 9c1.3.5 2.5 1.7 2.5 3.5 0 1.6-.9 3-2.3 3.7.2.4.3.9.3 1.3a3.5 3.5 0 0 1-3.5 3.5c-.7 0-1.4-.2-2-.6-.6.9-1.6 1.6-3 1.6s-2.4-.7-3-1.6c-.6.4-1.3.6-2 .6A3.5 3.5 0 0 1 2 17.5c0-.4.1-.9.3-1.3A4.1 4.1 0 0 1 0 12.5C0 10.7 1.2 9.5 2.5 9A4.5 4.5 0 0 1 6 5.34V5a3 3 0 0 1 3-3c1.2 0 2.3.7 3 1.7z"/></symbol>
+  <symbol id="i-terminal" viewBox="0 0 24 24"><path d="M20 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2M6.6 16.4 5.2 15l3-3-3-3 1.4-1.4L10.8 12zm7.4 1.6h-4v-2h4z"/></symbol>
+  <symbol id="i-chart" viewBox="0 0 24 24"><path d="M3 3h2v16h16v2H3zm4 12v-6h3v6zm5 0V7h3v8zm5 0V4h3v11z"/></symbol>
+  <symbol id="i-folder" viewBox="0 0 24 24"><path d="M10 4l2 2h8a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z"/></symbol>
+  <symbol id="i-doc" viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8zm-1 7V3.5L18.5 9zM8 12h8v2H8zm0 4h8v2H8z"/></symbol>
+  <symbol id="i-board" viewBox="0 0 24 24"><path d="M3 3h8v10H3zm10 0h8v6h-8zM3 15h8v6H3zm10-4h8v10h-8z"/></symbol>
+  <symbol id="i-slug" viewBox="0 0 24 24"><path d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20m0 4 6 4-6 4z"/></symbol>
+  <symbol id="i-idea" viewBox="0 0 24 24"><path d="M12 2a7 7 0 0 1 4 12.7c-.6.5-1 1.2-1 2V18H9v-1.3c0-.8-.4-1.5-1-2A7 7 0 0 1 12 2m-3 18h6v1a2 2 0 0 1-2 2h-2a2 2 0 0 1-2-2z"/></symbol>
+  <symbol id="i-clock" viewBox="0 0 24 24"><path d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20m1 5h-2v6l5 3 1-1.7-4-2.3z"/></symbol>
+  <symbol id="i-token" viewBox="0 0 24 24"><path d="M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2m3 5v2h3v6h2v-6h3V8z"/></symbol>
+  <symbol id="i-model" viewBox="0 0 24 24"><path d="M7 2h10l5 10-5 10H7L2 12zm1.6 2L4.6 12l4 8h8.8l4-8-4-8z"/></symbol>
+  <symbol id="i-spark" viewBox="0 0 24 24"><path d="M12 1l2.4 6.6L21 10l-6.6 2.4L12 19l-2.4-6.6L3 10l6.6-2.4zm7 12l1.2 3.3L23.5 17.5l-3.3 1.2L19 22l-1.2-3.3L14.5 17.5l3.3-1.2zM5 2l.9 2.6L8.5 5.5 5.9 6.4 5 9l-.9-2.6L1.5 5.5 4.1 4.6z"/></symbol>
+  <symbol id="i-trad" viewBox="0 0 24 24"><path d="M4 4h7v7H4zm9 0h7v7h-7zM4 13h7v7H4zm9 0h7v7h-7z"/></symbol>
+  <symbol id="i-case" viewBox="0 0 24 24"><path d="M9 4h6a1 1 0 0 1 1 1v2h4a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h4V5a1 1 0 0 1 1-1m1 3h4V6h-4z"/></symbol>
+  <symbol id="i-plugin" viewBox="0 0 24 24"><path d="M20 11V9h-2V7a2 2 0 0 0-2-2h-2V3h-4v2H8a2 2 0 0 0-2 2v2H4v4h2v2a2 2 0 0 0 2 2h2v2h4v-2h2a2 2 0 0 0 2-2v-2h2v-4z"/></symbol>
+  <symbol id="i-user" viewBox="0 0 24 24"><path d="M12 4a4 4 0 1 1 0 8 4 4 0 0 1 0-8m0 10c4.4 0 8 1.8 8 4v2H4v-2c0-2.2 3.6-4 8-4"/></symbol>
+  <symbol id="i-close" viewBox="0 0 24 24"><path d="M18.3 5.7 12 12l6.3 6.3-1.4 1.4L10.6 13.4 4.3 19.7 2.9 18.3 9.2 12 2.9 5.7l1.4-1.4L10.6 10.6l6.3-6.3z" transform="translate(1.4)"/></symbol>
+  <symbol id="i-more" viewBox="0 0 24 24"><path d="M12 8a2 2 0 1 0 0-4 2 2 0 0 0 0 4m0 6a2 2 0 1 0 0-4 2 2 0 0 0 0 4m0 6a2 2 0 1 0 0-4 2 2 0 0 0 0 4"/></symbol>
+  <symbol id="i-panel" viewBox="0 0 24 24"><path d="M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2m10 2v12h6V6z"/></symbol>
+</svg>
+
+<!-- ════════ ① 传统模式 + 切换入口 ══════════ -->
+<div class="doc-head">
+  <span class="num">①</span>
+  <h1>传统模式(现状)</h1>
+  <span class="tag">默认保留</span>
+  <span class="sub">布局与逻辑完全不变 · 仅隐藏「AI 数据分析」菜单组 · 顶栏新增模式切换入口(右上角)</span>
+</div>
+<div class="frame">
+<div class="window">
+  <div class="shell-top">
+    <div class="logo"><span class="mark">清</span>清鉴</div>
+    <div class="top-menu" style="background:var(--sider);border-radius:8px;padding:3px;margin-left:8px">
+      <span>案件数据</span><span>数据画像</span><span>通信分析</span><span>交易分析</span><span>活动轨迹</span><span>基站查询</span>
+    </div>
+    <span class="case-chip"><svg class="i"><use href="#i-case"/></svg>当前案件 <b>客户投诉案(9)</b></span>
+    <div class="actions">
+      <!-- 模式切换:设计核心件 -->
+      <div class="mode-switch">
+        <button class="on"><svg class="i"><use href="#i-trad"/></svg>传统</button>
+        <button><svg class="i"><use href="#i-spark"/></svg>智能</button>
+      </div>
+      <button class="iconbtn"><svg class="i"><use href="#i-slug"/></svg></button>
+      <span class="avatar">C</span>
+    </div>
+  </div>
+  <div class="shell-body">
+    <div class="mix-sider">
+      <div class="ms-sec">业务分析</div>
+      <div class="ms-item on"><svg class="i"><use href="#i-folder"/></svg>案件数据</div>
+      <div class="ms-item"><svg class="i"><use href="#i-user"/></svg>数据画像</div>
+      <div class="ms-item"><svg class="i"><use href="#i-chart"/></svg>通信分析</div>
+      <div class="ms-item"><svg class="i"><use href="#i-chart"/></svg>交易分析</div>
+      <div class="ms-item"><svg class="i"><use href="#i-chart"/></svg>活动轨迹分析</div>
+      <div class="ms-item"><svg class="i"><use href="#i-chart"/></svg>行为时空分析</div>
+      <div class="ms-item"><svg class="i"><use href="#i-chart"/></svg>对象关系分析</div>
+      <div class="ms-sec">AI 数据分析(已隐藏)</div>
+      <div class="ms-item disabled"><svg class="i"><use href="#i-robot"/></svg>AI 数据分析</div>
+      <div class="ms-item disabled"><svg class="i"><use href="#i-robot"/></svg>模型管理</div>
+      <div class="ms-item disabled"><svg class="i"><use href="#i-robot"/></svg>插件</div>
+      <div class="ms-item disabled"><svg class="i"><use href="#i-robot"/></svg>Codex 分析</div>
+      <div style="margin-top:auto;padding:8px 10px;font-size:11px;color:#6b7899;line-height:1.6">传统模式下整个「AI 数据分析」菜单组隐藏,其余菜单、多标签、案件逻辑零改动</div>
+    </div>
+    <div style="flex:1;display:flex;flex-direction:column;min-width:0">
+      <div class="tabs">
+        <span class="tab on">案件数据</span>
+        <span class="tab">通信分析</span>
+        <span class="tab">交易分析</span>
+        <span class="tab">+</span>
+      </div>
+      <div class="shell-content">
+        <div class="content-hint">传统模式内容区 = <b>现有页面原样渲染</b>(多标签 + 各业务分析页)<br>AI 能力整体让位给「智能模式」</div>
+        <div class="callout tr" style="right:14px;top:8px;width:190px">↑ 模式切换放顶栏右上角:两模式共用同一入口,点击即切,偏好按用户记住(localStorage)</div>
+      </div>
+    </div>
+  </div>
+</div>
+</div>
+
+<!-- ════════ ② 智能模式工作台 ══════════ -->
+<div class="doc-head">
+  <span class="num">②</span>
+  <h1>智能模式 · AI 工作台</h1>
+  <span class="tag">全新布局</span>
+  <span class="sub">不套传统壳(全屏路由)· 以 Codex 分析为内核 · 模型/插件收进弹层与抽屉 · aiAnalysis 备份页不并入 · 主流 AI 工具布局</span>
+</div>
+<div class="frame">
+<div class="window wb">
+  <!-- 工作台顶栏 -->
+  <div class="wb-top">
+    <div class="logo"><span class="mark">清</span>清鉴 · 智能工作台</div>
+    <span class="case-chip" title="切换案件请退出当前案件后,从案件管理重新进入"><svg class="i"><use href="#i-case"/></svg>案件:<b>客户投诉案(9)</b><svg class="i" style="color:var(--text-3)"><use href="#i-close"/></svg><span style="color:var(--text-3)">退出</span></span>
+    <span class="case-chip" style="color:var(--text-3)">数据就绪 · 18 个文件</span>
+    <div class="actions">
+      <span class="chip--route case-chip"><svg class="i"><use href="#i-slug"/></svg>路由层 · 10.66.66.66</span>
+      <span class="case-chip" style="color:var(--primary);border-color:transparent;background:var(--primary-soft);font-weight:600"><svg class="i"><use href="#i-model"/></svg>qwen · 本地模型</span>
+      <div class="mode-switch">
+        <button><svg class="i"><use href="#i-trad"/></svg>传统</button>
+        <button class="on"><svg class="i"><use href="#i-spark"/></svg>智能</button>
+      </div>
+      <button class="iconbtn"><svg class="i"><use href="#i-slug"/></svg></button>
+      <span class="avatar">C</span>
+    </div>
+  </div>
+
+  <div class="wb-body">
+    <!-- 左栏:会话 + 入口 -->
+    <div class="rail">
+      <!-- 左栏顶部菜单:模型 / 插件 / 案件 / 看板 -->
+      <div class="rail__menu">
+        <button class="rm on"><svg class="i"><use href="#i-robot"/></svg>对话</button>
+        <button class="rm"><svg class="i"><use href="#i-model"/></svg>模型管理</button>
+        <button class="rm"><svg class="i"><use href="#i-plugin"/></svg>插件与技能</button>
+              </div>
+      <button class="rail__new"><svg class="i"><use href="#i-send"/></svg>新建分析会话</button>
+      <div class="rail__sec">会话</div>
+      <div class="sess on">
+        <div class="sess__t">客户投诉激增分析<time>2m</time></div>
+        <div class="sess__m">qwen · 已生成分析报告与趋势图</div>
+      </div>
+      <div class="sess">
+        <div class="sess__t">退费渠道拆解<time>14:06</time></div>
+        <div class="sess__m">qwen · 3 轮对话</div>
+      </div>
+
+    </div>
+
+    <!-- 中栏:对话 -->
+    <div class="wb-main">
+      <div class="wb-msgs">
+      <div class="wb-col">
+        <!-- 用户 -->
+        <div class="turn--user" style="display:flex;justify-content:flex-end">
+          <div style="max-width:76%;background:var(--primary);color:#fff;border-radius:12px 12px 3px 12px;padding:10px 14px;box-shadow:0 2px 8px rgba(42,80,236,.22)">
+            分析近 30 天客户投诉数据,找出激增原因并生成报告。
+            <div style="margin-top:5px;font-size:11px;color:rgba(255,255,255,.62);text-align:right">14:02</div>
+          </div>
+        </div>
+        <!-- AI 回合 -->
+        <div class="turn--ai" style="display:grid;grid-template-columns:28px 1fr;gap:12px">
+          <div class="ai-avatar" style="width:28px;height:28px;border-radius:8px;background:var(--primary);color:#fff;display:flex;align-items:center;justify-content:center"><svg class="i"><use href="#i-robot"/></svg></div>
+          <div style="display:flex;flex-direction:column;gap:10px;min-width:0">
+            <div class="think" style="background:rgba(123,70,236,.045);border:1px solid rgba(123,70,236,.14);border-radius:8px;padding:8px 12px;font-size:12.5px;color:#6a3fd8;display:flex;gap:8px;align-items:center">
+              <svg class="i" style="color:#7b46ec"><use href="#i-brain"/></svg><b>思考过程</b><span style="color:var(--text-3)">已完成 · 8.2s</span>
+              <svg class="i" style="margin-left:auto;color:var(--text-3)"><use href="#i-chev"/></svg>
+            </div>
+            <div style="border:1px solid var(--border);border-radius:8px;background:#fff;overflow:hidden">
+              <div style="display:flex;align-items:center;gap:8px;padding:8px 12px;border-bottom:1px solid var(--border);font-size:12.5px">
+                <svg class="i" style="color:var(--primary)"><use href="#i-terminal"/></svg><b>执行过程</b><span style="color:var(--text-3)">3 个步骤</span>
+                <span style="margin-left:auto;color:var(--ok);font-size:11.5px;display:flex;align-items:center;gap:4px"><svg class="i"><use href="#i-check"/></svg>已完成</span>
+              </div>
+              <div style="display:grid;grid-template-columns:34px 1fr auto;gap:10px;padding:8px 12px;align-items:start">
+                <span style="width:22px;height:22px;border-radius:6px;background:#2a50ec;color:#fff;display:flex;align-items:center;justify-content:center;font-size:12px"><svg class="i"><use href="#i-terminal"/></svg></span>
+                <div><div style="font-size:13px">读取投诉明细并按日聚合</div>
+                <div class="mono" style="font-size:11.5px;color:var(--text-3)">SELECT date, count(*) FROM complaints …</div></div>
+                <span style="font-size:11px;color:var(--text-3)">2.6s <svg class="i" style="color:var(--ok)"><use href="#i-check"/></svg></span>
+              </div>
+              <div style="display:grid;grid-template-columns:34px 1fr auto;gap:10px;padding:8px 12px;border-top:1px dashed var(--border);align-items:start">
+                <span style="width:22px;height:22px;border-radius:6px;background:#7b46ec;color:#fff;display:flex;align-items:center;justify-content:center;font-size:12px"><svg class="i"><use href="#i-chart"/></svg></span>
+                <div><div style="font-size:13px">生成 30 天趋势图</div><div style="font-size:11.5px;color:var(--text-3)">已钉到看板</div></div>
+                <span style="font-size:11px;color:var(--text-3)">0.4s <svg class="i" style="color:var(--ok)"><use href="#i-check"/></svg></span>
+              </div>
+            </div>
+            <div style="font-size:14px;line-height:1.8">
+              <b style="color:var(--primary-deep)">投诉激增的直接原因:9/03 v2.6 版本上线后退费流程批量超时</b>,占新增投诉 62.4%。
+            </div>
+            <div style="display:flex;gap:10px;align-items:flex-start;background:var(--primary-soft);border-left:3px solid var(--primary);border-radius:8px;padding:10px 14px;font-size:13.5px">
+              <svg class="i" style="color:var(--primary);margin-top:2px"><use href="#i-idea"/></svg>
+              <span><b style="color:var(--primary-deep)">关键结论</b>:拐点与 v2.6 发版完全吻合;退费工单平均处理时长 6h → 41h。建议优先回滚退费网关。</span>
+            </div>
+            <div style="display:flex;align-items:center;gap:12px;font-size:11.5px;color:var(--text-3)">
+              <span style="display:flex;align-items:center;gap:4px"><svg class="i" style="color:var(--ok)"><use href="#i-check"/></svg>已完成</span>
+              <span style="width:1px;height:10px;background:var(--border)"></span>
+              <span style="display:flex;align-items:center;gap:4px"><svg class="i"><use href="#i-clock"/></svg>2m 14s</span>
+              <span style="display:flex;align-items:center;gap:4px"><svg class="i"><use href="#i-token"/></svg>48.2k / 1.9k</span>
+              <span style="margin-left:auto;display:flex;gap:2px;opacity:.6">
+                <span style="width:24px;height:24px;border-radius:6px;display:flex;align-items:center;justify-content:center"><svg class="i"><use href="#i-doc"/></svg></span>
+                <span style="width:24px;height:24px;border-radius:6px;display:flex;align-items:center;justify-content:center"><svg class="i"><use href="#i-terminal"/></svg></span>
+              </span>
+            </div>
+          </div>
+        </div>
+      </div>
+      </div>
+      <span class="collapse-rail"><svg class="i"><use href="#i-chev"/></svg></span>
+      <div class="wb-composer">
+        <div style="max-width:860px;margin:0 auto 8px;display:flex;gap:8px">
+          <span style="height:28px;padding:0 12px;border-radius:99px;border:1px solid var(--border);background:rgba(255,255,255,.75);font-size:12px;color:var(--text-2);display:inline-flex;align-items:center">按赔付金额再排一遍 TOP 渠道</span>
+          <span style="height:28px;padding:0 12px;border-radius:99px;border:1px solid var(--border);background:rgba(255,255,255,.75);font-size:12px;color:var(--text-2);display:inline-flex;align-items:center">生成管理层简要版</span>
+        </div>
+        <div style="max-width:860px;margin:0 auto;background:#fff;border:1px solid var(--border-strong);border-radius:12px;box-shadow:var(--shadow-pop);padding:12px 14px 10px">
+          <div style="min-height:40px;color:var(--text-3);font-size:13.5px">继续追问,或描述新的分析任务…</div>
+          <div style="display:flex;align-items:center;gap:8px;margin-top:8px">
+            <span style="display:inline-flex;align-items:center;gap:6px;height:28px;padding:0 10px;border-radius:6px;border:1px solid var(--border);font-size:12.5px;color:var(--text-1)"><svg class="i" style="color:var(--text-3)"><use href="#i-model"/></svg>qwen · 本地模型<svg class="i" style="color:var(--text-3)"><use href="#i-chev"/></svg></span>
+            <span style="display:inline-flex;align-items:center;gap:5px;height:26px;padding:0 9px;border-radius:6px;border:1px solid var(--border);font-size:12px;color:var(--text-2)"><svg class="i"><use href="#i-slug"/></svg>权限:默认</span>
+            <span style="flex:1"></span>
+            <span style="width:32px;height:32px;border-radius:9px;background:var(--primary);color:#fff;display:flex;align-items:center;justify-content:center"><svg class="i"><use href="#i-send"/></svg></span>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 右栏 -->
+    <div class="wb-aside">
+      <div class="panel">
+        <div class="panel__head"><svg class="i"><use href="#i-board"/></svg>看板<span class="n">已钉 2 / 6</span></div>
+        <div class="pin">
+          <div class="pin__t"><svg class="i" style="color:#7b46ec"><use href="#i-chart"/></svg>近 30 天投诉量趋势</div>
+          <div class="bars"><b style="height:24%"></b><b style="height:26%"></b><b style="height:92%"></b><b style="height:70%"></b><b style="height:46%"></b><b style="height:32%"></b><b style="height:26%"></b></div>
+        </div>
+        <div class="pin">
+          <div class="pin__t"><svg class="i" style="color:#0e7c86"><use href="#i-doc"/></svg>投诉类型 TOP 5</div>
+          <div class="bars" style="align-items:center"><b style="height:16px"></b><b style="height:10px;opacity:.6"></b><b style="height:7px;opacity:.4"></b></div>
+        </div>
+      </div>
+      <div class="panel">
+        <div class="panel__head"><svg class="i"><use href="#i-folder"/></svg>本会话产物<span class="n">本轮变更 2</span></div>
+        <div style="padding:4px 0">
+          <div style="display:flex;align-items:center;gap:9px;padding:7px 12px;font-size:12.5px;color:var(--text-2)"><svg class="i" style="color:var(--ok)"><use href="#i-doc"/></svg>投诉激增分析报告.md<span style="margin-left:auto;font-size:11px;color:var(--text-3)">18 KB</span></div>
+          <div style="display:flex;align-items:center;gap:9px;padding:7px 12px;font-size:12.5px;color:var(--text-2)"><svg class="i" style="color:var(--ok)"><use href="#i-chart"/></svg>trend_30d.echarts.json<span style="margin-left:auto;font-size:11px;color:var(--text-3)">4 KB</span></div>
+          <div style="display:flex;align-items:center;gap:9px;padding:7px 12px;font-size:12.5px;color:var(--text-2)"><svg class="i"><use href="#i-folder"/></svg>complaints_202608.csv<span style="margin-left:auto;font-size:11px;color:var(--text-3)">2.1 MB</span></div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+</div>
+
+
+<!-- ══════════ ③ 工作台 · 模型管理页 ══════════ -->
+<div class="doc-head">
+  <span class="num">③</span>
+  <h1>工作台内页面 · 模型管理</h1>
+  <span class="tag">工作台内视图</span>
+  <span class="sub">左栏菜单切换视图 · 数据与现有「模型管理」页同源(后端模型管理)· 默认模型用于新会话</span>
+</div>
+<div class="frame">
+<div class="window wb">
+    <div class="wb-top">
+    <div class="logo"><span class="mark">清</span>清鉴 · 智能工作台</div>
+    <span class="case-chip" title="切换案件请退出当前案件后,从案件管理重新进入"><svg class="i"><use href="#i-case"/></svg>案件:<b>客户投诉案(9)</b><svg class="i" style="color:var(--text-3)"><use href="#i-close"/></svg><span style="color:var(--text-3)">退出</span></span>
+    <span class="case-chip" style="color:var(--text-3)">数据就绪 · 18 个文件</span>
+    <div class="actions">
+      <span class="chip--route case-chip"><svg class="i"><use href="#i-slug"/></svg>路由层 · 10.66.66.66</span>
+      <span class="case-chip" style="color:var(--primary);border-color:transparent;background:var(--primary-soft);font-weight:600"><svg class="i"><use href="#i-model"/></svg>qwen · 本地模型</span>
+      <div class="mode-switch">
+        <button><svg class="i"><use href="#i-trad"/></svg>传统</button>
+        <button class="on"><svg class="i"><use href="#i-spark"/></svg>智能</button>
+      </div>
+      <button class="iconbtn"><svg class="i"><use href="#i-slug"/></svg></button>
+      <span class="avatar">C</span>
+    </div>
+  </div>
+  <div class="wb-body">
+        <div class="rail">
+      <div class="rail__menu">
+        <button class="rm"><svg class="i"><use href="#i-robot"/></svg>对话</button>
+        <button class="rm on"><svg class="i"><use href="#i-model"/></svg>模型管理</button>
+        <button class="rm"><svg class="i"><use href="#i-plugin"/></svg>插件与技能</button>
+              </div>
+            <button class="rail__new"><svg class="i"><use href="#i-send"/></svg>新建分析会话</button>
+      <div class="rail__sec">会话</div>
+      <div class="sess on">
+        <div class="sess__t">客户投诉激增分析<time>2m</time></div>
+        <div class="sess__m">qwen · 已生成分析报告与趋势图</div>
+        <span class="sess__more"><svg class="i"><use href="#i-more"/></svg></span>
+        <div class="sess-menu" style="position:absolute;right:2px;top:30px">
+          <button><svg class="i"><use href="#i-doc"/></svg>重命名</button>
+          <button class="danger"><svg class="i"><use href="#i-close"/></svg>删除会话</button>
+        </div>
+      </div>
+      <div class="sess is-renaming">
+        <input class="sess__rename" value="退费渠道拆解">
+        <div class="sess__rename-acts"><button class="rk">确认</button><button class="rk rk--cancel">取消</button></div>
+      </div>
+
+    </div>
+    <div class="wb-main">
+      <div class="page">
+        <div class="page__head">
+          <div>
+            <h3>模型管理</h3>
+            <p>聊天用模型与嵌入模型统一维护;模型同时供输入区下拉选择</p>
+          </div>
+          <span class="sp"></span>
+          <button class="pbtn pbtn--p">新增模型</button>
+        </div>
+        <div class="page__body">
+          <div class="mtabs"><span class="on">对话模型(LLM)· 3</span><span>嵌入模型 · 1</span></div>
+          <div class="mgrid">
+            <div class="mcard on">
+              <span class="mcard__tag">默认</span>
+              <div class="mcard__n"><span class="dot"></span>本地模型</div>
+              <div class="mcard__u">http://127.0.0.1:7987/v1 · qwen</div>
+              <div class="mcard__k">经路由层桥接 · 支持工具调用</div>
+              <div class="mcard__acts"><button class="ab" title="设为默认模型">设为默认</button><button class="ab">编辑</button><button class="ab ab--danger">删除</button></div>
+            </div>
+            <div class="mcard">
+              <div class="mcard__n"><span class="dot" style="background:var(--err)"></span>DeepSeek</div>
+              <div class="mcard__u">https://api.deepseek.com · deepseek-chat</div>
+              <div class="mcard__k">直连 · Chat 协议经路由层转 Responses</div>
+              <div class="mcard__acts"><button class="ab" title="设为默认模型">设为默认</button><button class="ab">编辑</button><button class="ab ab--danger">删除</button></div>
+            </div>
+            <div class="mcard">
+              <div class="mcard__n"><span class="dot" style="background:var(--err)"></span>通义千问</div>
+              <div class="mcard__u">dashscope.aliyuncs.com/compatible-mode/v1 · deepseek-v4.1-flash</div>
+              <div class="mcard__k">直连 · Chat 协议经路由层转 Responses</div>
+              <div class="mcard__acts"><button class="ab" title="设为默认模型">设为默认</button><button class="ab">编辑</button><button class="ab ab--danger">删除</button></div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+</div>
+
+<!-- ══════════ ④ 工作台 · 插件与技能页 ══════════ -->
+<div class="doc-head">
+  <span class="num">④</span>
+  <h1>工作台内页面 · 插件与技能</h1>
+  <span class="tag">工作台内视图</span>
+  <span class="sub">MCP 服务 + 本机 Codex 技能 · 写入本机 CODEX_HOME,重启运行时生效</span>
+</div>
+<div class="frame">
+<div class="window wb">
+  <div class="wb-top">
+    <div class="logo"><span class="mark">清</span>清鉴 · 智能工作台</div>
+    <span class="case-chip" title="切换案件请退出当前案件后,从案件管理重新进入"><svg class="i"><use href="#i-case"/></svg>案件:<b>客户投诉案(9)</b><svg class="i" style="color:var(--text-3)"><use href="#i-close"/></svg><span style="color:var(--text-3)">退出</span></span>
+    <span class="case-chip" style="color:var(--text-3)">数据就绪 · 18 个文件</span>
+    <div class="actions">
+      <span class="chip--route case-chip"><svg class="i"><use href="#i-slug"/></svg>路由层 · 10.66.66.66</span>
+      <span class="case-chip" style="color:var(--primary);border-color:transparent;background:var(--primary-soft);font-weight:600"><svg class="i"><use href="#i-model"/></svg>qwen · 本地模型</span>
+      <div class="mode-switch">
+        <button><svg class="i"><use href="#i-trad"/></svg>传统</button>
+        <button class="on"><svg class="i"><use href="#i-spark"/></svg>智能</button>
+      </div>
+      <button class="iconbtn"><svg class="i"><use href="#i-slug"/></svg></button>
+      <span class="avatar">C</span>
+    </div>
+  </div>
+  <div class="wb-body">
+    <div class="rail">
+      <div class="rail__menu">
+        <button class="rm"><svg class="i"><use href="#i-robot"/></svg>对话</button>
+        <button class="rm"><svg class="i"><use href="#i-model"/></svg>模型管理</button>
+        <button class="rm on"><svg class="i"><use href="#i-plugin"/></svg>插件与技能</button>
+      </div>
+      <button class="rail__new"><svg class="i"><use href="#i-send"/></svg>新建分析会话</button>
+      <div class="rail__sec">会话</div>
+      <div class="sess">
+        <div class="sess__t">客户投诉激增分析<time>2m</time></div>
+        <div class="sess__m">qwen · 已生成分析报告与趋势图</div>
+      </div>
+      <div class="sess">
+        <div class="sess__t">退费渠道拆解<time>14:06</time></div>
+        <div class="sess__m">qwen · 3 轮对话</div>
+      </div>
+    </div>
+    <div class="wb-main">
+      <div class="page">
+        <div class="page__head">
+          <div>
+            <h3>插件与技能</h3>
+            <p>MCP 服务与本机 Codex 技能的启用 / 停用 · 仅桌面客户端可用</p>
+          </div>
+          <span class="sp"></span>
+          <button class="pbtn pbtn--p">新增 MCP 服务</button>
+        </div>
+        <div class="page__body">
+          <div class="mtabs"><span class="on">MCP 服务 · 2</span><span>技能(Skills)· 6</span></div>
+          <div class="mgrid">
+            <div class="mcard">
+              <div class="mcard__n"><span class="dot" style="background:var(--ok)"></span>清鉴后端 MCP</div>
+              <div class="mcard__u">http://127.0.0.1:8980/js/a/mcpstreamable</div>
+              <div class="mcard__k"><span class="tchip tchip--http">streamable-http</span>&nbsp; 已启用 · 下一回合生效</div>
+              <div class="mcard__acts"><button class="ab">编辑</button><button class="ab ab--danger">删除</button></div>
+            </div>
+            <div class="mcard">
+              <div class="mcard__n"><span class="dot" style="background:var(--text-3)"></span>数据库直查</div>
+              <div class="mcard__u">node db-mcp.js --readonly</div>
+              <div class="mcard__k"><span class="tchip tchip--cmd">stdio</span>&nbsp; 已停用</div>
+              <div class="mcard__acts"><button class="ab">编辑</button><button class="ab ab--danger">删除</button></div>
+            </div>
+            <div class="mcard">
+              <div class="mcard__n"><span class="dot" style="background:var(--ok)"></span>报表生成</div>
+              <div class="mcard__k"><span class="tchip tchip--scope">技能 · 用户安装</span>&nbsp; 已启用</div>
+              <div class="mcard__u">按模板把会话结果整理成 Markdown 报告</div>
+              <div class="mcard__acts"><button class="ab">停用</button></div>
+            </div>
+            <div class="mcard">
+              <div class="mcard__n"><span class="dot" style="background:var(--text-3)"></span>SQL 审查</div>
+              <div class="mcard__k"><span class="tchip tchip--scope">技能 · 内置</span>&nbsp; 已启用</div>
+              <div class="mcard__u">危险语句拦截与只读校验</div>
+              <div class="mcard__acts"><button class="ab">停用</button></div>
+            </div>
+          </div>
+          <div class="page__empty-hint">
+            <svg class="i" style="color:var(--primary)"><use href="#i-idea"/></svg>
+            停用 / 启用会写入本机 Codex 配置,下一回合生效;新增 MCP 后需要重启运行时。
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+</div>
+
+
+<!-- ════════ 设计说明 ══════════ -->
+<div class="notes">
+  <div class="note">
+    <h3><span class="n">1</span>切换入口:顶栏右上角分段控件</h3>
+    <p><code>传统 ⇄ 智能</code> 分段控件放顶栏 action 区(搜索框旁、模板库之前)——两个模式的顶栏都有它,单击即切、偏好按用户持久化(localStorage <code>APP_WORKBENCH_MODE__</code>,仿暗色模式惯例)。业务人员日常停在自己常用的模式,不被打扰。</p>
+  </div>
+  <div class="note">
+    <h3><span class="n">2</span>传统模式 = 现状减 AI 菜单</h3>
+    <p>布局、多标签、案件逻辑零改动;唯一变化是「AI 数据分析」菜单组(AI 数据分析/模型管理/插件/Codex 分析)从混合侧边栏隐藏(菜单过滤 + 路由仍注册,直接敲 URL 会引导切模式)。</p>
+  </div>
+  <div class="note">
+    <h3><span class="n">3</span>智能模式 = 全屏工作台路由</h3>
+    <p>注册不套 LAYOUT 的全屏路由 <code>/workbench</code>(照 /case 案件管理页先例),覆盖整个视口。实现上新增一个 Workbench.vue 装配页 + 顶部/侧栏两个薄组件,复用 codexAnalysis 已重做的全部消息组件。</p>
+  </div>
+  <div class="note">
+    <h3><span class="n">4</span>智能模式的内核 = Codex 分析</h3>
+    <p>工作台即现有 Codex 分析页的组件整体复用(回合化排版、思考卡、步骤栈、内联审批、权限下拉、看板/产物)。<code>aiAnalysis</code> 为无效备份页不并入;其菜单在传统模式下同样隐藏。</p>
+  </div>
+  <div class="note">
+    <h3><span class="n">5</span>菜单功能去向</h3>
+    <p>左栏顶部菜单 = <b>视图切换</b>:对话 / 模型管理 / 插件与技能 / 看板,三页都在工作台内呈现(②③④);看板只保留在对话右栏。模型管理数据与现有页同源;<b>案件管理不做进工作台</b>——退出案件后走传统模式案件页。聊天选模型 = 输入区普通下拉框。顶栏案件 chip <b>只读</b>——不允许在工作台直接切换案件,必须退出案件后从案件管理重新进入(与业务口径一致)。</p>
+  </div>
+  <div class="note">
+    <h3><span class="n">6</span>会话重命名与删除</h3>
+    <p>会话行悬停出现 <code>⋯</code> 菜单:重命名(行内输入,Enter 确认 / Esc 取消)与删除(需确认;调用 Codex 归档接口并清理本地记录)。删除只影响该会话自身,案件产物目录不清理。</p>
+  </div>
+  <div class="note">
+    <h3><span class="n">7</span>不动的东西</h3>
+    <p>案件管理整套页面、业务分析各页面、后端与主进程 IPC 全部不变;codex 聊天页已完成的回合化排版/内联审批/权限下拉组件原样搬进工作台(组件级复用,不是复制)。</p>
+  </div>
+</div>
+
+</body>
+</html>

+ 256 - 0
ai-electron/frontend/src/codex/composables/useCodexChatPage.ts

@@ -0,0 +1,256 @@
+import { computed, onMounted, ref, watch } from 'vue';
+import { useMessage } from '@/core/hooks/web/useMessage';
+import { useUserStore } from '@/core/store/modules/case';
+import { useCodexChatStore } from '../store/codexChatStore';
+import type { CodexApprovalDecision, CodexPermMode, CodexReportFormat } from '../types/codexTypes';
+import {
+  exportCodexConversationExcel,
+  exportCodexConversationHtml,
+  exportCodexConversationMarkdown,
+  exportCodexElementPng,
+} from '../utils/codexReportExport';
+
+/**
+ * Codex 对话页的共享装配逻辑:状态与全部交互 handler。
+ *
+ * codexAnalysis(传统模式页)与 Workbench(智能模式工作台)共用同一套粘合层,
+ * 两边只保留各自的页面骨架模板。刻意不做 onBeforeUnmount dispose:
+ * 运行时是应用级单例(退出清理由 lifecycle beforeClose → disposeCodex 负责),
+ * 模式切换 / 页面销毁都不应该杀掉正在工作的 Codex 子进程。
+ */
+export function useCodexChatPage() {
+  const store = useCodexChatStore();
+  const userStore = useUserStore();
+  const { showMessage } = useMessage();
+
+  const composerRef = ref();
+  const listRef = ref();
+  const diagnosticsOpen = ref(false);
+  const templatesOpen = ref(false);
+  const starting = ref(false);
+  /** 输入框草稿:模板抽屉要拿它做「存为模板」 */
+  const composerDraft = ref('');
+  const modelApplying = ref(false);
+
+  /**
+   * 模型选择器(数据源 = 后端「模型管理」):
+   * 选项 value 用后端记录 id(防同 modelId 不同端点重名);会话存的是 modelId,反查记录得选中值。
+   */
+  const modelOptions = computed(() =>
+    store.models.map((item) => ({
+      value: item.id,
+      label: `${item.name || item.modelId || item.id}(${item.modelId || '—'})`,
+    })),
+  );
+  /** 选中值由 store 统一给:会话记过的模型 → 反查记录;新会话 → 后端默认模型 */
+  const selectedRecordId = computed(() => store.sessionModelRecordId || null);
+  /**
+   * 有消息即锁:会话进行中不可切换模型(空会话可换)。
+   * 记录被删除时不锁——否则选不了模型也发不出消息,会话就废在那里了。
+   */
+  const modelLocked = computed(() => !!selectedRecordId.value && !store.isSessionEmpty(store.activeThreadId));
+
+  // 多会话并行:输入区只看「运行时是否就绪」,当前会话在跑时按钮会变成「停止」
+  const composerDisabled = computed(() => !store.bridgeAvailable || !store.ready);
+
+  const reportTitle = computed(() => store.activeSession?.title || 'Codex 会话');
+
+  const gateHint = computed(() => {
+    if (store.runtimeError) return store.runtimeError;
+    const status = store.runtime;
+    if (!status) return '尚未检测到运行时状态,点击启动试试。';
+    if (status.state === 'error') return status.error || '运行时启动失败,请查看诊断日志。';
+    if (status.state === 'starting') return '运行时正在启动,稍候即可提问。';
+    if (!status.applied) {
+      return '尚未应用模型:在下方选择模型即可,首次提问时会自动应用(默认取「模型管理」里的默认模型)。';
+    }
+    return '点击启动 Codex 运行时后即可开始提问。';
+  });
+
+  onMounted(() => {
+    store.bootstrap();
+  });
+
+  /**
+   * 切换案件时重新对齐工作空间:新会话的 cwd 会跟着案件走,
+   * 已存在的会话保留各自的 cwd(Codex 在 threadStart 时就固定了,改不得)。
+   */
+  watch(
+    () => userStore.getCaseInfo?.id,
+    () => {
+      store.syncCaseContext();
+      store.reloadArtifacts();
+    },
+  );
+
+  async function onStartRuntime() {
+    starting.value = true;
+    try {
+      await store.startRuntime();
+      showMessage('Codex 运行时已启动', 'success');
+    } catch (error: any) {
+      showMessage(error?.message || '启动失败', 'error');
+    } finally {
+      starting.value = false;
+    }
+  }
+
+  /** 未选模型(也没有后端默认模型)就不发:未应用 provider 时 Codex 只会打它自己的默认端点 */
+  async function onSend(text: string) {
+    if (!store.sessionModelRecordId) {
+      showMessage(store.modelGuardMessage, 'warning');
+      return;
+    }
+    // 首次提问要先应用模型(重启运行时 + 探端点),等待期间给出可见反馈
+    const applying = !store.modelApplied;
+    modelApplying.value = applying;
+    try {
+      await store.send(text);
+    } catch (error: any) {
+      showMessage(error?.message || '发送失败', 'error');
+    } finally {
+      if (applying) modelApplying.value = false;
+    }
+  }
+
+  async function onStop() {
+    try {
+      await store.stop();
+    } catch (error: any) {
+      showMessage(error?.message || '中断失败', 'error');
+    }
+  }
+
+  /** 重新生成:找到该回复之前最近的一条提问重发 */
+  function onRegenerate(index: number) {
+    if (store.streaming) return;
+    const messages = store.messages;
+    for (let i = index - 1; i >= 0; i -= 1) {
+      const message = messages[i];
+      if (message.role === 'user') {
+        onSend(message.text);
+        return;
+      }
+    }
+  }
+
+  function usePrompt(text: string) {
+    composerRef.value?.fill(text);
+  }
+
+  /** 模板插入:只填进输入框(模板里常带占位符,需要用户改完再发) */
+  function onPickTemplate(prompt: string) {
+    composerRef.value?.fill(prompt);
+  }
+
+  /** 新建会话:滚动到底 + 聚焦输入框(真正的会话创建懒到首次发送) */
+  async function onCreate() {
+    await Promise.resolve();
+    listRef.value?.toBottom(true);
+    composerRef.value?.focus();
+  }
+
+  function onSessionSelect() {
+    listRef.value?.toBottom(true);
+  }
+
+  function onModelChange(recordId: string | null) {
+    const record = store.models.find((item) => item.id === recordId);
+    if (!record || !store.activeThreadId) return;
+    store.setSessionModel(store.activeThreadId, record.modelId || null);
+    showMessage('下次提问时生效', 'success');
+  }
+
+  /** 内联审批卡的决定:store 落库 + 留痕 */
+  async function onResolveApproval(
+    token: string,
+    decision: CodexApprovalDecision,
+    answers?: Record<string, string[]>,
+  ) {
+    try {
+      await store.resolveApproval(token, decision, answers);
+    } catch (error: any) {
+      showMessage(error?.message || '审批提交失败', 'error');
+    }
+  }
+
+  /** 权限档位切换(只读 / 默认 / 完全访问) */
+  function onPermChange(mode: CodexPermMode) {
+    store.setPermMode(mode);
+    showMessage(
+      mode === 'full' ? '已切换到完全访问:后续操作不再请求确认' : `已切换到「${mode === 'readonly' ? '只读' : '默认'}」,下一回合生效`,
+      mode === 'full' ? 'warning' : 'success',
+    );
+  }
+
+  /** 等待横条的「回到确认卡」 */
+  function onGotoApproval() {
+    const el = document.querySelector('[data-codex-approval]');
+    el?.scrollIntoView({ behavior: 'smooth', block: 'center' });
+  }
+
+  /** 会话级导出(md / html / xlsx / png) */
+  async function onExport(format: CodexReportFormat) {
+    const messages = store.messages;
+    if (!messages.length) {
+      showMessage('还没有可导出的内容', 'warning');
+      return;
+    }
+    const meta = {
+      title: reportTitle.value,
+      cwd: store.activeCwd,
+      model: store.activeSession?.model,
+      exportedAt: new Date().toLocaleString(),
+    };
+    try {
+      if (format === 'md') {
+        showMessage(`已导出 ${exportCodexConversationMarkdown(messages, meta)}`, 'success');
+        return;
+      }
+      if (format === 'html') {
+        showMessage(`已导出 ${exportCodexConversationHtml(messages, meta)}`, 'success');
+        return;
+      }
+      if (format === 'xlsx') {
+        const name = exportCodexConversationExcel(messages, meta);
+        showMessage(name ? `已导出 ${name}` : '会话里没有可导出的表格', name ? 'success' : 'warning');
+        return;
+      }
+      const element = listRef.value?.getCaptureElement?.() ?? undefined;
+      const name = await exportCodexElementPng(element, reportTitle.value);
+      showMessage(name ? `已导出 ${name}` : '导出长图失败:找不到内容区域', name ? 'success' : 'error');
+    } catch (error: any) {
+      showMessage(error?.message || '导出失败', 'error');
+    }
+  }
+
+  return {
+    store,
+    composerRef,
+    listRef,
+    diagnosticsOpen,
+    templatesOpen,
+    starting,
+    composerDraft,
+    modelApplying,
+    modelOptions,
+    selectedRecordId,
+    modelLocked,
+    composerDisabled,
+    reportTitle,
+    gateHint,
+    onStartRuntime,
+    onSend,
+    onStop,
+    onRegenerate,
+    onResolveApproval,
+    onPermChange,
+    onGotoApproval,
+    onModelChange,
+    usePrompt,
+    onPickTemplate,
+    onCreate,
+    onSessionSelect,
+    onExport,
+  };
+}

+ 293 - 0
ai-electron/frontend/src/codex/styles/codexTheme.less

@@ -726,3 +726,296 @@
   }
 }
 
+
+/* ================================================================== *
+ * 智能工作台 · 内页基元(模型管理 / 插件与技能)
+ *
+ * 版式对齐设计稿:页头与内容列统一 1060px 居中,动作按钮跟随内容列右缘
+ * (不贴窗口边缘——窗口越宽留白越多,按钮不与内容列脱节)。
+ * ================================================================== */
+.page {
+  display: flex;
+  flex: 1 1 auto;
+  flex-direction: column;
+  min-width: 0;
+  padding: 16px 10px;
+  overflow: auto;
+
+  &__head {
+    display: flex;
+    align-items: flex-start;
+    gap: var(--codex-sp-3);
+    flex: 0 0 auto;
+    width: 100%;
+    margin: 0 0 14px;
+
+    h3 {
+      margin: 0;
+      font-size: 17px;
+      font-weight: 700;
+      color: var(--codex-text);
+    }
+
+    p {
+      margin: 3px 0 0;
+      font-size: 12.5px;
+      color: var(--codex-text-3);
+    }
+  }
+
+  &__head-sp {
+    flex: 1 1 auto;
+  }
+
+  &__primary {
+    display: inline-flex;
+    align-items: center;
+    flex: 0 0 auto;
+    height: 30px;
+    padding: 0 14px;
+    font-size: 12.5px;
+    font-weight: 600;
+    color: #fff;
+    background: var(--codex-primary);
+    border: none;
+    border-radius: 7px;
+    cursor: pointer;
+
+    &:hover {
+      background: var(--codex-primary-deep);
+    }
+  }
+
+  &__body {
+    flex: 1 1 auto;
+    width: 100%;
+  }
+
+  /* 胶囊页签 */
+  &__tabs {
+    display: flex;
+    gap: 6px;
+    margin-bottom: 14px;
+  }
+
+  &__tab {
+    display: inline-flex;
+    align-items: center;
+    height: 30px;
+    padding: 0 14px;
+    font-size: 12.5px;
+    color: var(--codex-text-2);
+    background: var(--codex-bg);
+    border: 1px solid var(--codex-border);
+    border-radius: 99px;
+    cursor: pointer;
+
+    &:hover {
+      color: var(--codex-primary);
+      border-color: var(--codex-primary);
+    }
+
+    &.is-on {
+      color: var(--codex-primary);
+      background: var(--codex-primary-soft);
+      border-color: var(--codex-primary);
+      font-weight: 600;
+    }
+  }
+
+  /* 卡片网格:固定 320px 卡片宽 + 自适应列数(窄屏自动减列) */
+  &__grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fill, 320px);
+    gap: 12px 12px;
+    align-items: stretch;
+    justify-content: start;
+  }
+
+  &__card {
+    position: relative;
+    display: flex;
+    flex-direction: column;
+    min-height: 200px;
+    padding: 16px 18px;
+    background: var(--codex-bg);
+    border: 1px solid var(--codex-border);
+    border-radius: 10px;
+
+    &.is-default {
+      background: var(--codex-primary-soft);
+      border-color: var(--codex-primary);
+    }
+  }
+
+  &__tag {
+    position: absolute;
+    top: 10px;
+    right: 11px;
+    padding: 1px 8px;
+    font-size: 9.5px;
+    font-weight: 700;
+    color: #fff;
+    background: var(--codex-primary);
+    border-radius: 99px;
+  }
+
+  &__card-name {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    padding-right: 52px;
+    font-size: 15px;
+    font-weight: 650;
+    color: var(--codex-text);
+  }
+
+  &__dot {
+    flex: 0 0 auto;
+    width: 8px;
+    height: 8px;
+    background: var(--codex-text-3);
+    border-radius: 50%;
+
+    &.is-ok {
+      background: var(--codex-success);
+    }
+
+    &.is-off {
+      background: var(--codex-text-3);
+    }
+  }
+
+  &__conn {
+    margin-left: auto;
+    font-size: 10.5px;
+    font-weight: 400;
+    color: var(--codex-success);
+  }
+
+  &__card-mono {
+    margin-top: 6px;
+    overflow: hidden;
+    font-family: var(--codex-font-mono);
+    font-size: 12px;
+    color: var(--codex-text-3);
+    white-space: nowrap;
+    text-overflow: ellipsis;
+  }
+
+  &__card-mono-s {
+    font-size: 11px;
+  }
+
+  &__card-desc {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    margin-top: 5px;
+    overflow: hidden;
+    font-size: 12px;
+    color: var(--codex-text-2);
+  }
+
+  &__chip {
+    flex: 0 0 auto;
+    padding: 1px 7px;
+    font-size: 10px;
+    font-weight: 600;
+    color: var(--codex-text-2);
+    background: var(--codex-bg-subtle);
+    border-radius: 4px;
+
+    &.is-http {
+      color: #0e7c86;
+      background: fade(#0e7c86, 9%);
+    }
+
+    &.is-cmd {
+      color: #7b46ec;
+      background: fade(#7b46ec, 9%);
+    }
+
+    &.is-scope {
+      color: var(--codex-text-2);
+      background: var(--codex-bg-hover);
+    }
+  }
+
+  &__chip-text {
+    overflow: hidden;
+    font-size: 11px;
+    color: var(--codex-text-3);
+    white-space: nowrap;
+    text-overflow: ellipsis;
+  }
+
+  /* 操作行贴底:卡片定高 200px 时不留中段空白 */
+  &__card-acts {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    margin-top: auto;
+    padding-top: 12px;
+  }
+
+  &__acts-sp {
+    flex: 1 1 auto;
+  }
+
+  &__act {
+    display: inline-flex;
+    align-items: center;
+    height: 26px;
+    padding: 0 9px;
+    font-size: 11.5px;
+    color: var(--codex-text-2);
+    background: transparent;
+    border: 1px solid var(--codex-border);
+    border-radius: 6px;
+    cursor: pointer;
+
+    &:hover:not(:disabled) {
+      color: var(--codex-primary);
+      background: var(--codex-bg-hover);
+      border-color: var(--codex-primary);
+    }
+
+    &:disabled {
+      opacity: 0.55;
+      cursor: default;
+    }
+
+    &.is-danger {
+      color: var(--codex-error);
+    }
+
+    &.is-danger:hover {
+      color: var(--codex-error);
+      background: fade(@error-color, 8%);
+      border-color: var(--codex-error);
+    }
+  }
+
+  &__empty {
+    grid-column: 1 / -1;
+    padding: 34px 0;
+    font-size: 12.5px;
+    color: var(--codex-text-3);
+    text-align: center;
+    border: 1.5px dashed var(--codex-border);
+    border-radius: 10px;
+  }
+
+  &__empty-hint {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    margin-top: 14px;
+    padding: 9px 14px;
+    font-size: 12.5px;
+    color: var(--codex-text-2);
+    background: var(--codex-primary-soft);
+    border-radius: 8px;
+  }
+}

+ 33 - 223
ai-electron/frontend/src/codex/views/codexAnalysis/codexAnalysis.vue

@@ -101,25 +101,14 @@
 
 <script lang="ts" setup name="CodexAnalysisView">
   /**
-   * Codex 数据分析 · 对话页。
+   * Codex 数据分析 · 对话页(传统模式)。
    *
-   * 页面只做装配:状态与调用全部在 store,块渲染全部在 components。
-   * 这里保留的只有三类逻辑:导出(需要 DOM 句柄)、空态引导、以及对 store 的一层错误兜底。
+   * 页面只做装配:状态、交互 handler 在 useCodexChatPage(与智能工作台共用),
+   * 块渲染在 components;本文件额外保留的只有空态引导文案与示例问题。
    */
-  import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
   import { Button } from 'ant-design-vue';
   import '@/codex/codexIndex';
   import Icon from '@/core/components/Icon/src/Icon.vue';
-  import { useMessage } from '@/core/hooks/web/useMessage';
-  import { useUserStore } from '@/core/store/modules/case';
-  import { useCodexChatStore } from '../../store/codexChatStore';
-  import type { CodexApprovalDecision, CodexPermMode, CodexReportFormat } from '../../types/codexTypes';
-  import {
-    exportCodexConversationExcel,
-    exportCodexConversationHtml,
-    exportCodexConversationMarkdown,
-    exportCodexElementPng,
-  } from '../../utils/codexReportExport';
   import CodexChatToolbar from '../../components/codexChatToolbar.vue';
   import CodexSessionSidebar from '../../components/codexSessionSidebar.vue';
   import CodexMessageList from '../../components/codexMessageList.vue';
@@ -128,20 +117,39 @@
   import CodexDiagnosticDrawer from '../../components/codexDiagnosticDrawer.vue';
   import CodexTemplatePanel from '../../components/codexTemplatePanel.vue';
   import CodexBoardPanel from '../../components/codexBoardPanel.vue';
+  import { useCodexChatPage } from '../../composables/useCodexChatPage';
 
   const AButton = Button;
 
-  const store = useCodexChatStore();
-  const userStore = useUserStore();
-  const { showMessage } = useMessage();
-
-  const composerRef = ref<InstanceType<typeof CodexComposer>>();
-  const listRef = ref<InstanceType<typeof CodexMessageList>>();
-  const diagnosticsOpen = ref(false);
-  const templatesOpen = ref(false);
-  const starting = ref(false);
-  /** 输入框草稿:模板抽屉要拿它做「存为模板」 */
-  const composerDraft = ref('');
+  const {
+    store,
+    composerRef,
+    listRef,
+    diagnosticsOpen,
+    templatesOpen,
+    starting,
+    composerDraft,
+    modelApplying,
+    modelOptions,
+    selectedRecordId,
+    modelLocked,
+    composerDisabled,
+    reportTitle,
+    gateHint,
+    onStartRuntime,
+    onSend,
+    onStop,
+    onRegenerate,
+    onResolveApproval,
+    onPermChange,
+    onGotoApproval,
+    onModelChange,
+    usePrompt,
+    onPickTemplate,
+    onCreate,
+    onSessionSelect,
+    onExport,
+  } = useCodexChatPage();
 
   const EXAMPLES = [
     '当前工作目录里有哪些数据文件?分别是什么格式、多少行?',
@@ -149,204 +157,6 @@
     '统计出现次数最多的前 10 个对象,用图表展示,并列出明细',
     '把上面两个结果整理成一份带结论的报告',
   ];
-
-  /**
-   * 模型选择器(数据源 = 后端「模型管理」):
-   * 选项 value 用后端记录 id(防同 modelId 不同端点重名);会话存的是 modelId,反查记录得选中值。
-   */
-  const modelOptions = computed(() =>
-    store.models.map((item) => ({
-      value: item.id,
-      label: `${item.name || item.modelId || item.id}(${item.modelId || '—'})`,
-    })),
-  );
-  /** 选中值由 store 统一给:会话记过的模型 → 反查记录;新会话 → 后端默认模型 */
-  const selectedRecordId = computed(() => store.sessionModelRecordId || null);
-  /**
-   * 有消息即锁:会话进行中不可切换模型(空会话可换)。
-   * 记录被删除时不锁——否则选不了模型也发不出消息,会话就废在那里了。
-   */
-  const modelLocked = computed(() => !!selectedRecordId.value && !store.isSessionEmpty(store.activeThreadId));
-
-  const modelApplying = ref(false);
-
-  /** 选择只记到会话;真正应用(重启运行时 + 探端点)留到首次发送 */
-  function onModelChange(recordId: string | null) {
-    const record = store.models.find((item) => item.id === recordId);
-    if (!record || !store.activeThreadId) return;
-    store.setSessionModel(store.activeThreadId, record.modelId || null);
-    showMessage('下次提问时生效', 'success');
-  }
-
-  // 多会话并行:输入区只看「运行时是否就绪」,当前会话在跑时按钮会变成「停止」
-  const composerDisabled = computed(() => !store.bridgeAvailable || !store.ready);
-
-  const reportTitle = computed(() => store.activeSession?.title || 'Codex 会话');
-
-
-  const gateHint = computed(() => {
-    if (store.runtimeError) return store.runtimeError;
-    const status = store.runtime;
-    if (!status) return '尚未检测到运行时状态,点击启动试试。';
-    if (status.state === 'error') return status.error || '运行时启动失败,请查看诊断日志。';
-    if (status.state === 'starting') return '运行时正在启动,稍候即可提问。';
-    if (!status.applied) {
-      return '尚未应用模型:在下方选择模型即可,首次提问时会自动应用(默认取「模型管理」里的默认模型)。';
-    }
-    return '点击启动 Codex 运行时后即可开始提问。';
-  });
-
-  onMounted(() => {
-    store.bootstrap();
-  });
-
-  /**
-   * 切换案件时重新对齐工作空间:新会话的 cwd 会跟着案件走,
-   * 已存在的会话保留各自的 cwd(Codex 在 threadStart 时就固定了,改不得)。
-   */
-  watch(
-    () => userStore.getCaseInfo?.id,
-    () => {
-      store.syncCaseContext();
-      store.reloadArtifacts();
-    },
-  );
-
-  onBeforeUnmount(() => {
-    store.dispose();
-  });
-
-  async function onStartRuntime() {
-    starting.value = true;
-    try {
-      await store.startRuntime();
-      showMessage('Codex 运行时已启动', 'success');
-    } catch (error: any) {
-      showMessage(error?.message || '启动失败', 'error');
-    } finally {
-      starting.value = false;
-    }
-  }
-
-  /** 未选模型(也没有后端默认模型)就不发:未应用 provider 时 Codex 只会打它自己的默认端点 */
-  async function onSend(text: string) {
-    if (!store.sessionModelRecordId) {
-      showMessage(store.modelGuardMessage, 'warning');
-      return;
-    }
-    // 首次提问要先应用模型(重启运行时 + 探端点),等待期间给出可见反馈
-    const applying = !store.modelApplied;
-    modelApplying.value = applying;
-    try {
-      await store.send(text);
-    } catch (error: any) {
-      showMessage(error?.message || '发送失败', 'error');
-    } finally {
-      if (applying) modelApplying.value = false;
-    }
-  }
-
-  async function onStop() {
-    try {
-      await store.stop();
-    } catch (error: any) {
-      showMessage(error?.message || '中断失败', 'error');
-    }
-  }
-
-  /** 内联审批卡的决定:store 落库 + 留痕 */
-  async function onResolveApproval(
-    token: string,
-    decision: CodexApprovalDecision,
-    answers?: Record<string, string[]>,
-  ) {
-    try {
-      await store.resolveApproval(token, decision, answers);
-    } catch (error: any) {
-      showMessage(error?.message || '审批提交失败', 'error');
-    }
-  }
-
-  /** 权限档位切换(只读 / 默认 / 完全访问) */
-  function onPermChange(mode: CodexPermMode) {
-    store.setPermMode(mode);
-    showMessage(
-      mode === 'full' ? '已切换到完全访问:后续操作不再请求确认' : `已切换到「${mode === 'readonly' ? '只读' : '默认'}」,下一回合生效`,
-      mode === 'full' ? 'warning' : 'success',
-    );
-  }
-
-  /** 等待横条的「回到确认卡」 */
-  function onGotoApproval() {
-    const el = document.querySelector('[data-codex-approval]');
-    el?.scrollIntoView({ behavior: 'smooth', block: 'center' });
-  }
-
-  /** 重新生成:找到该回复之前最近的一条提问重发 */
-  function onRegenerate(index: number) {
-    if (store.streaming) return;
-    const messages = store.messages;
-    for (let i = index - 1; i >= 0; i -= 1) {
-      const message = messages[i];
-      if (message.role === 'user') {
-        onSend(message.text);
-        return;
-      }
-    }
-  }
-
-  function usePrompt(text: string) {
-    composerRef.value?.fill(text);
-  }
-
-  /** 模板插入:只填进输入框(模板里常带占位符,需要用户改完再发) */
-  function onPickTemplate(prompt: string) {
-    composerRef.value?.fill(prompt);
-  }
-
-  async function onCreate() {
-    await nextTick();
-    listRef.value?.toBottom(true);
-    composerRef.value?.focus();
-  }
-
-  function onSessionSelect() {
-    listRef.value?.toBottom(true);
-  }
-
-  async function onExport(format: CodexReportFormat) {
-    const messages = store.messages;
-    if (!messages.length) {
-      showMessage('还没有可导出的内容', 'warning');
-      return;
-    }
-    const meta = {
-      title: reportTitle.value,
-      cwd: store.activeCwd,
-      model: store.activeSession?.model,
-      exportedAt: new Date().toLocaleString(),
-    };
-    try {
-      if (format === 'md') {
-        showMessage(`已导出 ${exportCodexConversationMarkdown(messages, meta)}`, 'success');
-        return;
-      }
-      if (format === 'html') {
-        showMessage(`已导出 ${exportCodexConversationHtml(messages, meta)}`, 'success');
-        return;
-      }
-      if (format === 'xlsx') {
-        const name = exportCodexConversationExcel(messages, meta);
-        showMessage(name ? `已导出 ${name}` : '会话里没有可导出的表格', name ? 'success' : 'warning');
-        return;
-      }
-      const element = listRef.value?.getCaptureElement?.() ?? undefined;
-      const name = await exportCodexElementPng(element, reportTitle.value);
-      showMessage(name ? `已导出 ${name}` : '导出长图失败:找不到内容区域', name ? 'success' : 'error');
-    } catch (error: any) {
-      showMessage(error?.message || '导出失败', 'error');
-    }
-  }
 </script>
 
 <style lang="less">

+ 189 - 0
ai-electron/frontend/src/codex/views/workbench/WbModelPage.vue

@@ -0,0 +1,189 @@
+<template>
+  <div class="page">
+    <div class="page__head">
+      <div>
+        <h3>模型管理</h3>
+        <p>聊天用模型与嵌入模型统一维护;模型同时供输入区下拉选择</p>
+      </div>
+      <span class="page__head-sp"></span>
+      <button class="page__primary" type="button" @click="openCreate">新增模型</button>
+    </div>
+
+    <div class="page__body">
+      <div class="page__tabs">
+        <button
+          v-for="tab in TABS"
+          :key="tab.key"
+          type="button"
+          class="page__tab"
+          :class="{ 'is-on': activeTab === tab.key }"
+          @click="activeTab = tab.key"
+        >
+          {{ tab.label }} · {{ listOf(tab.key).length }}
+        </button>
+      </div>
+
+      <a-spin :spinning="loading">
+        <div class="page__grid">
+          <div
+            v-for="record in listOf(activeTab)"
+            :key="record.id"
+            class="page__card"
+            :class="{ 'is-default': record.defaultModel }"
+          >
+            <span v-if="record.defaultModel" class="page__tag">默认</span>
+            <div class="page__card-name">
+              <span
+                class="page__dot"
+                :class="record.status === 'ACTIVE' ? 'is-ok' : 'is-off'"
+              ></span>
+              {{ record.name || record.modelId }}
+            </div>
+            <div class="page__card-mono" :title="`${record.baseUrl || '—'} · ${record.modelId}`">
+              {{ record.baseUrl || '—' }} · {{ record.modelId }}
+            </div>
+            <div class="page__card-desc">
+              {{ describe(record) }}
+            </div>
+            <div class="page__card-acts">
+              <button
+                type="button"
+                class="page__act"
+                :disabled="record.defaultModel"
+                @click="onSetDefault(record)"
+              >
+                {{ record.defaultModel ? '已是默认' : '设为默认' }}
+              </button>
+              <button type="button" class="page__act" @click="openEdit(record)">编辑</button>
+              <button type="button" class="page__act is-danger" @click="onDelete(record)">删除</button>
+            </div>
+          </div>
+
+          <div v-if="!loading && !listOf(activeTab).length" class="page__empty">
+            还没有{{ activeMeta.label }},点右上角「新增模型」添加
+          </div>
+        </div>
+      </a-spin>
+    </div>
+
+    <ModelFormModal
+      v-model:open="formOpen"
+      :record="editing"
+      :providers="providers"
+      :default-type="activeTab"
+      @saved="onSaved"
+    />
+  </div>
+</template>
+
+<script lang="ts" setup name="WbModelPage">
+  /**
+   * 工作台内页面 · 模型管理。
+   *
+   * 严格按设计稿实现:页头 + 胶囊页签 + 双列模型卡片(状态点 / 端点等宽字体 /
+   * 设为默认·编辑·删除 文字操作)。数据与逻辑和「AI 数据分析」组的模型管理页同源
+   * (modelApi + ModelFormModal),只换排版。
+   */
+  import { computed, onMounted, ref } from 'vue';
+  import { Modal, Spin } from 'ant-design-vue';
+  import { useMessage } from '@/core/hooks/web/useMessage';
+  import * as modelApi from '@/ai/api/modelApi';
+  import type { AiModel, ModelProvider } from '@/ai/api/types';
+  import ModelFormModal from '@/ai/components/ModelFormModal.vue';
+
+  const ASpin = Spin;
+
+  const { showMessage } = useMessage();
+
+  type ModelTypeKey = 'llm' | 'embedding';
+
+  const TABS: Array<{ key: ModelTypeKey; label: string }> = [
+    { key: 'llm', label: '对话模型(LLM)' },
+    { key: 'embedding', label: '嵌入模型' },
+  ];
+
+  const models = ref<AiModel[]>([]);
+  const providers = ref<ModelProvider[]>([]);
+  const loading = ref(false);
+  const formOpen = ref(false);
+  const editing = ref<AiModel | null>(null);
+  const activeTab = ref<ModelTypeKey>('llm');
+
+  const activeMeta = computed(() => TABS.find((t) => t.key === activeTab.value) || TABS[0]);
+
+  /** type 为空的历史数据视为对话模型(与后端 ModelTypeEnum.fromCode 一致) */
+  const llmModels = computed(() => models.value.filter((m) => !m.type || m.type === 'llm'));
+  const embeddingModels = computed(() => models.value.filter((m) => m.type === 'embedding'));
+
+  function listOf(key: ModelTypeKey) {
+    return key === 'embedding' ? embeddingModels.value : llmModels.value;
+  }
+
+  /** 卡片说明行:provider / 路由口径 */
+  function describe(record: AiModel) {
+    const provider = record.provider ? `${record.provider} · ` : '';
+    const routed = /127\.0\.0\.1(:|\/)/u.test(record.baseUrl || '') || /localhost/u.test(record.baseUrl || '');
+    return `${provider}${routed ? '经路由层桥接' : '直连'}`;
+  }
+
+  onMounted(load);
+
+  async function load() {
+    loading.value = true;
+    try {
+      const [list, providerList] = await Promise.all([
+        modelApi.listModels(),
+        modelApi.listProviders().catch(() => [] as ModelProvider[]),
+      ]);
+      models.value = list || [];
+      providers.value = providerList || [];
+    } catch (e: any) {
+      showMessage(e?.message || '加载模型列表失败', 'error');
+    } finally {
+      loading.value = false;
+    }
+  }
+
+  function openCreate() {
+    editing.value = null;
+    formOpen.value = true;
+  }
+
+  function openEdit(record: AiModel) {
+    editing.value = record;
+    formOpen.value = true;
+  }
+
+  async function onSaved() {
+    showMessage('已保存', 'success');
+    await load();
+  }
+
+  async function onSetDefault(record: AiModel) {
+    try {
+      await modelApi.setDefaultModel(record.id);
+      showMessage(`已将「${record.name || record.modelId}」设为默认${activeMeta.value.label}`, 'success');
+      await load();
+    } catch (e: any) {
+      showMessage(e?.message || '设置失败', 'error');
+    }
+  }
+
+  function onDelete(record: AiModel) {
+    const isEmbedding = record.type === 'embedding';
+    Modal.confirm({
+      title: '删除模型',
+      content: `确认删除「${record.name || record.modelId}」?已绑定该模型的会话将回退到默认${
+        isEmbedding ? '向量' : '对话'
+      }模型。`,
+      okText: '删除',
+      okType: 'danger',
+      cancelText: '取消',
+      onOk: async () => {
+        await modelApi.deleteModel(record.id);
+        showMessage('已删除', 'success');
+        await load();
+      },
+    });
+  }
+</script>

+ 365 - 0
ai-electron/frontend/src/codex/views/workbench/WbPluginPage.vue

@@ -0,0 +1,365 @@
+<template>
+  <div class="page">
+    <div class="page__head">
+      <div>
+        <h3>插件与技能</h3>
+        <p>MCP 服务与本机 Codex 技能的启用 / 停用 · 仅桌面客户端可用</p>
+      </div>
+      <span class="page__head-sp"></span>
+      <button class="page__primary" type="button" @click="openMcpCreate">新增 MCP 服务</button>
+    </div>
+
+    <div v-if="!store.bridgeAvailable" class="page__gate">
+      本页由本机 Codex 运行时管理(主进程通道读写),请在桌面客户端内使用。
+    </div>
+
+    <div v-else class="page__body">
+      <div class="page__tabs">
+        <button
+          type="button"
+          class="page__tab"
+          :class="{ 'is-on': activeTab === 'mcp' }"
+          @click="activeTab = 'mcp'"
+        >
+          MCP 服务 · {{ mcpServers.length }}
+        </button>
+        <button
+          type="button"
+          class="page__tab"
+          :class="{ 'is-on': activeTab === 'skill' }"
+          @click="activeTab = 'skill'"
+        >
+          技能(Skills)· {{ skills.length }}
+        </button>
+      </div>
+
+      <!-- MCP 服务卡片 -->
+      <div v-if="activeTab === 'mcp'" class="page__grid">
+        <div v-for="record in mcpServers" :key="record.id" class="page__card">
+          <div class="page__card-name">
+            <span class="page__dot" :class="record.enabled ? 'is-ok' : 'is-off'"></span>
+            {{ record.id }}
+            <span v-if="record.connected" class="page__conn">已连接 · {{ record.toolCount }} 工具</span>
+          </div>
+          <div
+            class="page__card-mono"
+            :title="record.transport === 'http' ? record.url || '—' : [record.command, ...record.args].join(' ')"
+          >
+            {{ record.transport === 'http' ? record.url || '—' : [record.command, ...record.args].join(' ') }}
+          </div>
+          <div class="page__card-desc">
+            <span class="page__chip" :class="record.transport === 'http' ? 'is-http' : 'is-cmd'">
+              {{ record.transport === 'http' ? 'streamable-http' : 'stdio' }}
+            </span>
+            <span class="page__chip-text">{{ record.enabled ? '已启用 · 下一回合生效' : '已停用' }}</span>
+          </div>
+          <div class="page__card-acts">
+            <button type="button" class="page__act" @click="openMcpEdit(record)">编辑</button>
+            <button type="button" class="page__act" @click="toggleMcp(record)">
+              {{ record.enabled ? '停用' : '启用' }}
+            </button>
+            <button type="button" class="page__act is-danger" @click="onDeleteMcp(record)">删除</button>
+          </div>
+        </div>
+
+        <div v-if="!mcpServers.length" class="page__empty">
+          还没有 MCP 服务,点右上角「新增 MCP 服务」添加
+        </div>
+      </div>
+
+      <!-- 技能卡片 -->
+      <div v-else class="page__grid">
+        <div v-for="skill in skills" :key="skill.path" class="page__card">
+          <div class="page__card-name">
+            <span class="page__dot" :class="skill.enabled ? 'is-ok' : 'is-off'"></span>
+            {{ skill.name }}
+          </div>
+          <div class="page__card-desc">{{ skill.description || '—' }}</div>
+          <div class="page__card-desc page__card-mono-s" :title="skill.path">{{ skill.path }}</div>
+          <div class="page__card-acts">
+            <span class="page__chip is-scope">{{ skill.scope === 'system' ? '内置' : '用户安装' }}</span>
+            <span class="page__chip-text">{{ skill.enabled ? '已启用' : '已停用' }}</span>
+            <button type="button" class="page__act" @click="toggleSkill(skill)">
+              {{ skill.enabled ? '停用' : '启用' }}
+            </button>
+            <button
+              v-if="skill.managed"
+              type="button"
+              class="page__act is-danger"
+              @click="onDeleteSkill(skill)"
+            >
+              删除
+            </button>
+          </div>
+        </div>
+
+        <div v-if="!skills.length" class="page__empty">
+          还没有可用技能
+        </div>
+      </div>
+
+      <div class="page__empty-hint">
+        <svg class="i" style="width: 14px; height: 14px; vertical-align: -2px; color: var(--codex-primary)" viewBox="0 0 24 24"><path d="M12 2a7 7 0 0 1 4 12.7c-.6.5-1 1.2-1 2V18H9v-1.3c0-.8-.4-1.5-1-2A7 7 0 0 1 12 2m-3 18h6v1a2 2 0 0 1-2 2h-2a2 2 0 0 1-2-2z" fill="currentColor"/></svg>
+        停用 / 启用会写入本机 Codex 配置,下一回合生效;新增 MCP 后需要重启运行时。
+      </div>
+    </div>
+
+    <!-- MCP 新增 / 编辑表单(设计稿口径:工作台内完成,不跳页) -->
+    <a-modal
+      v-model:open="mcpFormOpen"
+      :title="mcpEditing ? '编辑 MCP 服务' : '新增 MCP 服务'"
+      :width="480"
+      destroy-on-close
+      @ok="submitMcp"
+    >
+      <div class="wb-mcp-form">
+        <label>服务 ID<span class="req">*</span></label>
+        <a-input v-model:value="mcpForm.id" :disabled="!!mcpEditing" placeholder="例如:qingjian-backend" />
+        <label>传输方式</label>
+        <a-radio-group v-model:value="mcpForm.transport">
+          <a-radio value="http">streamable-http</a-radio>
+          <a-radio value="stdio">stdio</a-radio>
+        </a-radio-group>
+        <label v-if="mcpForm.transport === 'http'">服务地址<span class="req">*</span></label>
+        <a-input
+          v-if="mcpForm.transport === 'http'"
+          v-model:value="mcpForm.url"
+          placeholder="http://127.0.0.1:8980/js/a/mcpstreamable"
+        />
+        <template v-if="mcpForm.transport === 'stdio'">
+          <label>命令<span class="req">*</span></label>
+          <a-input v-model:value="mcpForm.command" placeholder="node" />
+          <label>参数(空格分隔)</label>
+          <a-input v-model:value="mcpForm.argsText" placeholder="--readonly --db main" />
+        </template>
+        <label>Bearer Token 环境变量名(可选)</label>
+        <a-input v-model:value="mcpForm.bearerTokenEnvVar" placeholder="ZSJZ_MCP_TOKEN" />
+        <label>
+          <a-checkbox v-model:checked="mcpForm.enabled">启用该服务</a-checkbox>
+        </label>
+      </div>
+    </a-modal>
+  </div>
+</template>
+
+<script lang="ts" setup name="WbPluginPage">
+  /**
+   * 工作台内页面 · 插件与技能。
+   *
+   * 严格按设计稿实现:页头 + 胶囊页签(MCP 服务 / 技能)+ 双列卡片(状态点 /
+   * 地址命令等宽字体 / 协议与来源 chip / 文字操作)。数据经桌面 IPC
+   * (codexMcp* / codexSkill*),写入本机 CODEX_HOME 的 config.toml。
+   */
+  import { computed, onMounted, reactive, ref } from 'vue';
+  import {
+    Checkbox,
+    Input,
+    Modal,
+    Radio,
+    RadioGroup,
+  } from 'ant-design-vue';
+  import Icon from '@/core/components/Icon/src/Icon.vue';
+  import { useMessage } from '@/core/hooks/web/useMessage';
+  import { useCodexChatStore } from '../../store/codexChatStore';
+  import * as codexApi from '../../api/codexApi';
+  import type { CodexMcpServer, CodexSkill } from '../../api/codexApi';
+
+  const AModal = Modal;
+  const AInput = Input;
+  const ARadio = Radio;
+  const ARadioGroup = RadioGroup;
+  const ACheckbox = Checkbox;
+
+  const store = useCodexChatStore();
+  const { showMessage } = useMessage();
+
+  const activeTab = ref<'mcp' | 'skill'>('mcp');
+  const mcpServers = ref<CodexMcpServer[]>([]);
+  const skills = ref<CodexSkill[]>([]);
+  const loading = ref(false);
+
+  const mcpFormOpen = ref(false);
+  const mcpEditing = ref<CodexMcpServer | null>(null);
+  const mcpForm = reactive({
+    id: '',
+    transport: 'http' as 'http' | 'stdio',
+    url: '',
+    command: '',
+    argsText: '',
+    bearerTokenEnvVar: '',
+    enabled: true,
+  });
+
+  /** 会话在跑时不允许改 MCP 配置(改了也要重启运行时才生效) */
+  const busyTurn = computed(() => store.runningThreads.length > 0);
+
+  onMounted(load);
+
+  async function load() {
+    if (!store.bridgeAvailable) return;
+    loading.value = true;
+    try {
+      const [servers, skillList] = await Promise.all([
+        codexApi.codexMcpList().catch(() => [] as CodexMcpServer[]),
+        codexApi.codexSkillList().catch(() => [] as CodexSkill[]),
+      ]);
+      mcpServers.value = servers;
+      skills.value = skillList;
+    } finally {
+      loading.value = false;
+    }
+  }
+
+  function openMcpCreate() {
+    if (busyTurn.value) {
+      showMessage('有回合正在执行,结束后再修改 MCP 配置', 'warning');
+      return;
+    }
+    mcpEditing.value = null;
+    Object.assign(mcpForm, {
+      id: '',
+      transport: 'http',
+      url: '',
+      command: '',
+      argsText: '',
+      bearerTokenEnvVar: '',
+      enabled: true,
+    });
+    mcpFormOpen.value = true;
+  }
+
+  function openMcpEdit(record: CodexMcpServer) {
+    mcpEditing.value = record;
+    Object.assign(mcpForm, {
+      id: record.id,
+      transport: record.transport,
+      url: record.url ?? '',
+      command: record.command ?? '',
+      argsText: record.args.join(' '),
+      bearerTokenEnvVar: '',
+      enabled: record.enabled,
+    });
+    mcpFormOpen.value = true;
+  }
+
+  async function submitMcp() {
+    const id = mcpForm.id.trim();
+    if (!id) {
+      showMessage('请填写服务 ID', 'warning');
+      return;
+    }
+    if (mcpForm.transport === 'http' && !mcpForm.url.trim()) {
+      showMessage('请填写服务地址', 'warning');
+      return;
+    }
+    if (mcpForm.transport === 'stdio' && !mcpForm.command.trim()) {
+      showMessage('请填写启动命令', 'warning');
+      return;
+    }
+    try {
+      await codexApi.codexMcpSave({
+        id,
+        transport: mcpForm.transport,
+        ...(mcpForm.transport === 'http'
+          ? { url: mcpForm.url.trim() }
+          : {
+              command: mcpForm.command.trim(),
+              args: mcpForm.argsText.trim() ? mcpForm.argsText.trim().split(/\s+/u) : [],
+            }),
+        ...(mcpForm.bearerTokenEnvVar.trim()
+          ? { bearerTokenEnvVar: mcpForm.bearerTokenEnvVar.trim() }
+          : {}),
+        enabled: mcpForm.enabled,
+      } as codexApi.CodexMcpServerInput);
+      showMessage('已保存,重启运行时后生效', 'success');
+      mcpFormOpen.value = false;
+      mcpServers.value = await codexApi.codexMcpList();
+    } catch (error: any) {
+      showMessage(error?.message || '保存失败', 'error');
+    }
+  }
+
+  async function toggleMcp(record: CodexMcpServer) {
+    try {
+      mcpServers.value = await codexApi.codexMcpSave({
+        id: record.id,
+        transport: record.transport,
+        ...(record.transport === 'http'
+          ? { url: record.url ?? '' }
+          : { command: record.command ?? '', args: record.args }),
+        ...(record.bearerTokenEnvVar ? { bearerTokenEnvVar: record.bearerTokenEnvVar } : {}),
+        enabled: !record.enabled,
+      } as codexApi.CodexMcpServerInput);
+      showMessage(record.enabled ? '已停用,下一回合生效' : '已启用,下一回合生效', 'success');
+    } catch (error: any) {
+      showMessage(error?.message || '操作失败', 'error');
+    }
+  }
+
+  function onDeleteMcp(record: CodexMcpServer) {
+    Modal.confirm({
+      title: '删除 MCP 服务',
+      content: `确认删除「${record.id}」?`,
+      okText: '删除',
+      okType: 'danger',
+      cancelText: '取消',
+      onOk: async () => {
+        mcpServers.value = await codexApi.codexMcpRemove(record.id);
+        showMessage('已删除,重启运行时后生效', 'success');
+      },
+    });
+  }
+
+  async function toggleSkill(skill: CodexSkill) {
+    try {
+      // skillSetEnabled 只回 effectiveEnabled,列表需重新拉取
+      await codexApi.codexSkillSetEnabled({ path: skill.path, name: skill.name }, !skill.enabled);
+      skills.value = await codexApi.codexSkillList();
+      showMessage(skill.enabled ? '已停用,下一回合生效' : '已启用,下一回合生效', 'success');
+    } catch (error: any) {
+      showMessage(error?.message || '操作失败', 'error');
+    }
+  }
+
+  function onDeleteSkill(skill: CodexSkill) {
+    Modal.confirm({
+      title: '删除技能',
+      content: `确认删除技能「${skill.name}」?`,
+      okText: '删除',
+      okType: 'danger',
+      cancelText: '取消',
+      onOk: async () => {
+        skills.value = await codexApi.codexSkillRemove({ path: skill.path, name: skill.name });
+        showMessage('已删除', 'success');
+      },
+    });
+  }
+</script>
+
+<style lang="less" scoped>
+  .page__gate {
+    margin: 22px 30px;
+    padding: var(--codex-sp-3);
+    font-size: 13px;
+    color: var(--codex-text-2);
+    background: fade(@warning-color, 8%);
+    border: 1px solid fade(@warning-color, 30%);
+    border-radius: var(--codex-radius);
+  }
+
+  .wb-mcp-form {
+    display: flex;
+    flex-direction: column;
+    gap: 6px;
+
+    label {
+      margin-top: 8px;
+      font-size: 12px;
+      color: var(--codex-text-2);
+
+      .req {
+        color: var(--codex-error);
+      }
+    }
+  }
+</style>

+ 430 - 0
ai-electron/frontend/src/codex/views/workbench/WbSessionList.vue

@@ -0,0 +1,430 @@
+<template>
+  <div class="wb-sess">
+    <button class="wb-sess__new" type="button" :disabled="creating" @click="onCreate">
+      <Icon icon="mdi:send" :size="15" />
+      {{ creating ? '创建中…' : '新建分析会话' }}
+    </button>
+
+    <div class="wb-sess__sec">会话</div>
+
+    <div v-if="!sessions.length" class="wb-sess__empty">
+      还没有会话。<br />点上方「新建分析会话」开始一次分析。
+    </div>
+
+    <ul v-else class="wb-sess__list">
+      <li
+        v-for="session in sessions"
+        :key="session.threadId"
+        class="wb-sess__item"
+        :class="{ 'is-on': session.threadId === activeThreadId }"
+      >
+        <!-- 重命名态:行内输入 + 确认 / 取消 -->
+        <template v-if="renamingId === session.threadId">
+          <input
+            ref="renameInputRef"
+            v-model="renameDraft"
+            class="wb-sess__rename"
+            @keydown.enter="confirmRename(session.threadId)"
+            @keydown.esc="cancelRename"
+          />
+          <div class="wb-sess__rename-acts">
+            <button class="wb-sess__rk" type="button" @click="confirmRename(session.threadId)">确认</button>
+            <button class="wb-sess__rk is-plain" type="button" @click="cancelRename">取消</button>
+          </div>
+        </template>
+
+        <template v-else>
+          <button class="wb-sess__main" type="button" @click="onSelect(session.threadId)">
+            <div class="wb-sess__title">
+              {{ session.title }}
+              <time>{{ formatCodexRelative(session.updatedAt) }}</time>
+            </div>
+            <div class="wb-sess__meta">
+              <span v-if="store.isThreadRunning(session.threadId)" class="wb-sess__running" title="该会话正在执行"></span>
+              {{ describe(session) }}
+            </div>
+          </button>
+
+          <span class="wb-sess__more" role="button" title="更多操作" @click.stop="toggleMenu(session.threadId)">
+            <Icon icon="mdi:dots-horizontal" :size="14" />
+          </span>
+
+          <div v-if="menuId === session.threadId" class="wb-sess__menu" @click.stop>
+            <button type="button" @click="startRename(session)">
+              <Icon icon="mdi:pencil-outline" :size="13" />
+              重命名
+            </button>
+            <button type="button" class="is-danger" @click="onRemove(session)">
+              <Icon icon="mdi:trash-can-outline" :size="13" />
+              删除会话
+            </button>
+          </div>
+        </template>
+      </li>
+    </ul>
+  </div>
+</template>
+
+<script lang="ts" setup name="WbSessionList">
+  /**
+   * 工作台会话列表(严格按设计稿)。
+   *
+   * 与 codexAnalysis 的会话侧栏同一份 store 数据,但交互按设计:
+   * 「新建分析会话」实心主按钮 → 会话行(标题 + 相对时间 / 元信息行)→ 悬停「⋯」
+   * 弹出菜单(重命名跑行内输入、删除会话带确认)。会话按案件隔离(store.caseSessions)。
+   */
+  import { computed, nextTick, ref } from 'vue';
+  import { Modal } from 'ant-design-vue';
+  import Icon from '@/core/components/Icon/src/Icon.vue';
+  import { useMessage } from '@/core/hooks/web/useMessage';
+  import { useCodexChatStore } from '../../store/codexChatStore';
+  import { formatCodexRelative } from '../../utils/codexFormat';
+  import type { CodexSessionMeta } from '../../types/codexTypes';
+
+  const emit = defineEmits<{
+    (e: 'create'): void;
+    (e: 'select', threadId: string): void;
+  }>();
+
+  const store = useCodexChatStore();
+  const { showMessage } = useMessage();
+
+  const creating = ref(false);
+  const menuId = ref('');
+  const renamingId = ref('');
+  const renameDraft = ref('');
+  const renameInputRef = ref();
+
+  const sessions = computed(() => store.caseSessions);
+  const activeThreadId = computed(() => store.activeThreadId);
+
+  /** 元信息行:模型 + 轮次数(数据都来自会话自身,不编造) */
+  function describe(session: CodexSessionMeta): string {
+    const parts: string[] = [];
+    if (session.model) parts.push(session.model);
+    if (session.turnCount > 0) parts.push(`${session.turnCount} 轮对话`);
+    else parts.push('尚未开始');
+    if (session.external) parts.push('外部导入');
+    return parts.join(' · ');
+  }
+
+  async function onCreate() {
+    if (creating.value) return;
+    creating.value = true;
+    try {
+      const result = await store.startNewSession();
+      emit('create');
+      showMessage(
+        result.created ? '已创建会话' : '已有空白会话,直接输入问题即可',
+        result.created ? 'success' : 'info',
+      );
+    } catch (error: any) {
+      showMessage(error?.message || '创建会话失败', 'error');
+    } finally {
+      creating.value = false;
+    }
+  }
+
+  async function onSelect(threadId: string) {
+    menuId.value = '';
+    if (threadId === activeThreadId.value) return;
+    await store.selectSession(threadId);
+    emit('select', threadId);
+    const session = store.sessions.find((item) => item.threadId === threadId);
+    if (session?.external && !store.messages.length) {
+      showMessage('该会话来自外部导入:本机没有它的执行记录,只能看到标题与目录', 'warning');
+    }
+  }
+
+  function toggleMenu(threadId: string) {
+    menuId.value = menuId.value === threadId ? '' : threadId;
+  }
+
+  function startRename(session: CodexSessionMeta) {
+    menuId.value = '';
+    renamingId.value = session.threadId;
+    renameDraft.value = session.title;
+    nextTick(() => renameInputRef.value?.focus?.());
+  }
+
+  function cancelRename() {
+    renamingId.value = '';
+    renameDraft.value = '';
+  }
+
+  function confirmRename(threadId: string) {
+    if (renamingId.value !== threadId) return;
+    const title = renameDraft.value.trim();
+    if (title) store.renameSession(threadId, title);
+    cancelRename();
+  }
+
+  /** 删除是破坏性操作:先确认;只从本机列表移除,Codex 侧已落盘历史不删 */
+  function onRemove(session: CodexSessionMeta) {
+    menuId.value = '';
+    Modal.confirm({
+      title: '删除会话',
+      content: `确认删除「${session.title}」?将把它从本机列表移除,Codex 侧已落盘的历史不受影响。`,
+      okText: '删除',
+      okType: 'danger',
+      cancelText: '取消',
+      onOk: () => {
+        store.removeSession(session.threadId);
+        showMessage('已删除会话', 'success');
+      },
+    });
+  }
+</script>
+
+<style lang="less" scoped>
+  .wb-sess {
+    display: flex;
+    flex-direction: column;
+    flex: 1 1 auto;
+    min-height: 0;
+    padding-bottom: 8px;
+  }
+
+  /* 新建分析会话:实心主按钮(设计稿) */
+  .wb-sess__new {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 7px;
+    flex: 0 0 auto;
+    height: 36px;
+    margin: 12px 12px 4px;
+    font-size: 13.5px;
+    font-weight: 650;
+    color: #fff;
+    background: var(--codex-primary);
+    border: none;
+    border-radius: 9px;
+    cursor: pointer;
+
+    &:hover:not(:disabled) {
+      background: var(--codex-primary-deep);
+    }
+
+    &:disabled {
+      opacity: 0.65;
+      cursor: not-allowed;
+    }
+  }
+
+  .wb-sess__sec {
+    flex: 0 0 auto;
+    margin: 14px 14px 5px;
+    font-size: 10.5px;
+    font-weight: 600;
+    letter-spacing: 0.06em;
+    color: var(--codex-text-3);
+  }
+
+  .wb-sess__empty {
+    padding: 10px 16px;
+    font-size: 12px;
+    line-height: 1.8;
+    color: var(--codex-text-3);
+  }
+
+  .wb-sess__list {
+    flex: 1 1 auto;
+    min-height: 0;
+    margin: 0;
+    padding: 0 8px 8px;
+    overflow-y: auto;
+    list-style: none;
+  }
+
+  .wb-sess__item {
+    position: relative;
+    padding: 8px 10px;
+    border-radius: 8px;
+
+    &:hover {
+      background: var(--codex-bg-hover);
+    }
+
+    &.is-on {
+      background: var(--codex-primary-soft);
+    }
+  }
+
+  .wb-sess__main {
+    display: block;
+    width: 100%;
+    padding: 0;
+    text-align: left;
+    background: none;
+    border: 0;
+    cursor: pointer;
+  }
+
+  .wb-sess__title {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    overflow: hidden;
+    font-size: 13px;
+    color: var(--codex-text);
+    white-space: nowrap;
+    text-overflow: ellipsis;
+
+    time {
+      flex: 0 0 auto;
+      margin-left: auto;
+      font-size: 10.5px;
+      font-weight: 400;
+      color: var(--codex-text-3);
+    }
+
+    .wb-sess__item.is-on & {
+      color: var(--codex-primary);
+      font-weight: 600;
+    }
+  }
+
+  .wb-sess__meta {
+    display: flex;
+    align-items: center;
+    gap: 5px;
+    margin-top: 1px;
+    overflow: hidden;
+    font-size: 11px;
+    color: var(--codex-text-3);
+    white-space: nowrap;
+    text-overflow: ellipsis;
+  }
+
+  .wb-sess__running {
+    flex: 0 0 auto;
+    width: 6px;
+    height: 6px;
+    background: var(--codex-primary);
+    border-radius: 50%;
+    animation: wb-sess-pulse 1.2s ease-in-out infinite;
+  }
+
+  @keyframes wb-sess-pulse {
+    0%,
+    100% {
+      opacity: 1;
+    }
+
+    50% {
+      opacity: 0.25;
+    }
+  }
+
+  /* 悬停显示的「⋯」 */
+  .wb-sess__more {
+    position: absolute;
+    top: 7px;
+    right: 6px;
+    display: none;
+    align-items: center;
+    justify-content: center;
+    width: 22px;
+    height: 22px;
+    color: var(--codex-text-3);
+    background: var(--codex-bg);
+    border-radius: 6px;
+    cursor: pointer;
+
+    &:hover {
+      color: var(--codex-primary);
+      background: var(--codex-bg-hover);
+    }
+  }
+
+  /* 操作按钮只在鼠标悬停时出现(避免列表看着杂乱) */
+  .wb-sess__item:hover .wb-sess__more {
+    display: flex;
+  }
+
+  .wb-sess__menu {
+    position: absolute;
+    top: 30px;
+    right: 6px;
+    z-index: var(--codex-z-dropdown);
+    min-width: 122px;
+    padding: 4px;
+    background: var(--codex-bg);
+    border: 1px solid var(--codex-border);
+    border-radius: 8px;
+    box-shadow: var(--codex-shadow-lg);
+
+    button {
+      display: flex;
+      align-items: center;
+      gap: 7px;
+      width: 100%;
+      padding: 6px 10px;
+      font-size: 12.5px;
+      color: var(--codex-text);
+      text-align: left;
+      background: transparent;
+      border: none;
+      border-radius: 6px;
+      cursor: pointer;
+
+      &:hover {
+        background: var(--codex-bg-hover);
+      }
+
+      &.is-danger {
+        color: var(--codex-error);
+
+        &:hover {
+          background: fade(@error-color, 8%);
+        }
+      }
+    }
+  }
+
+  /* 行内重命名 */
+  .wb-sess__item:has(.wb-sess__rename) {
+    background: var(--codex-bg);
+    border: 1px solid var(--codex-primary);
+  }
+
+  .wb-sess__rename {
+    width: 100%;
+    height: 26px;
+    padding: 0 7px;
+    font: inherit;
+    font-size: 12.5px;
+    color: var(--codex-text);
+    background: var(--codex-bg);
+    border: 1px solid var(--codex-border);
+    border-radius: 5px;
+    outline: none;
+
+    &:focus {
+      border-color: var(--codex-primary);
+    }
+  }
+
+  .wb-sess__rename-acts {
+    display: flex;
+    gap: 5px;
+    margin-top: 6px;
+  }
+
+  .wb-sess__rk {
+    height: 22px;
+    padding: 0 10px;
+    font-size: 11px;
+    color: #fff;
+    background: var(--codex-primary);
+    border: none;
+    border-radius: 5px;
+    cursor: pointer;
+
+    &.is-plain {
+      color: var(--codex-text-2);
+      background: var(--codex-bg-hover);
+    }
+  }
+</style>

+ 613 - 0
ai-electron/frontend/src/codex/views/workbench/Workbench.vue

@@ -0,0 +1,613 @@
+<template>
+  <div class="wb-page">
+    <!-- 工作台顶栏 -->
+    <header class="wb-top">
+      <div class="wb-logo"><span class="wb-logo__mark">清</span>清鉴 · 智能工作台</div>
+
+      <span
+        class="wb-chip"
+        title="切换案件请退出当前案件后,从案件管理重新进入"
+      >
+        <Icon icon="mdi:briefcase-outline" :size="13" />
+        案件:<b>{{ caseName }}</b>
+        <button class="wb-chip__exit" type="button" @click="onExitCase">退出</button>
+      </span>
+
+      <span v-if="fileCount > 0" class="wb-chip wb-chip--muted">数据就绪 · {{ fileCount }} 个文件</span>
+
+      <span class="wb-spacer"></span>
+
+      <span v-if="routerInfo" class="wb-chip" title="端点只会 Chat 协议,经内置路由层桥接">
+        <Icon icon="mdi:swap-horizontal" :size="13" />
+        路由层
+      </span>
+      <span v-if="modelLabel" class="wb-chip wb-chip--model">
+        <Icon icon="mdi:robot-outline" :size="13" />
+        {{ modelLabel }}
+      </span>
+
+      <AppWorkbenchToggle :current="'smart'" @change="onModeChange" />
+
+      <Tooltip title="诊断日志" placement="bottom">
+        <button class="wb-iconbtn" type="button" @click="diagnosticsOpen = true">
+          <Icon icon="mdi:bug-outline" :size="15" />
+        </button>
+      </Tooltip>
+      <span class="wb-avatar">C</span>
+    </header>
+
+    <div class="wb-body">
+      <!-- 左栏:视图菜单 + 会话 -->
+      <aside class="wb-rail">
+        <nav class="wb-rail__menu">
+          <button
+            v-for="item in RAIL_ITEMS"
+            :key="item.key"
+            type="button"
+            class="wb-rm"
+            :class="{ 'is-on': view === item.key }"
+            @click="view = item.key"
+          >
+            <Icon :icon="item.icon" :size="15" />
+            {{ item.label }}
+          </button>
+        </nav>
+
+        <WbSessionList @create="onCreate" @select="onSessionSelect" />
+      </aside>
+
+      <!-- 中栏:视图 -->
+      <main class="wb-view">
+        <!-- ① 对话 -->
+        <template v-if="view === 'chat'">
+          <CodexMessageList
+            ref="listRef"
+            :messages="store.messages"
+            :streaming="store.streaming"
+            :report-title="reportTitle"
+            :approvals="store.pendingApprovals"
+            @regenerate="onRegenerate"
+            @resend="onSend"
+            @resolve="onResolveApproval"
+          >
+            <template #empty>
+              <div class="wb-empty">
+                <h2 class="wb-empty__title">用自然语言完成一次数据分析</h2>
+                <p class="wb-empty__desc">
+                  Codex 会在案件工作空间中检索数据、执行命令与脚本,把结果整理成数据表、图表与报告,
+                  生成的文件会出现在右侧产物栏。
+                </p>
+
+                <!-- 浏览器环境如实降级;桌面端未就绪时给出唯一下一步 -->
+                <div v-if="!store.bridgeAvailable" class="wb-gate">
+                  <div class="wb-gate__head">
+                    <Icon icon="mdi:laptop-off" :size="15" />
+                    <span>请在桌面客户端内使用</span>
+                  </div>
+                  <p class="wb-gate__desc">
+                    对话依赖本机 Codex 运行时(子进程 + 本地文件读写),浏览器环境无法工作;
+                    左侧「模型管理 / 插件与技能」等管理能力不受影响。
+                  </p>
+                </div>
+                <div v-else-if="!store.ready" class="wb-gate">
+                  <div class="wb-gate__head">
+                    <Icon icon="mdi:alert-circle-outline" :size="15" />
+                    <span>Codex 运行时未就绪</span>
+                  </div>
+                  <p class="wb-gate__desc">{{ gateHint }}</p>
+                  <a-button size="small" type="primary" :loading="starting" @click="onStartRuntime">
+                    启动运行时
+                  </a-button>
+                </div>
+              </div>
+            </template>
+          </CodexMessageList>
+
+          <!-- 追问建议 chips(设计稿:输入区上方) -->
+          <div v-if="suggestions.length" class="wb-sugs">
+            <button
+              v-for="item in suggestions"
+              :key="item"
+              type="button"
+              class="wb-sug"
+              :disabled="composerDisabled"
+              @click="onSuggest(item)"
+            >
+              {{ item }}
+            </button>
+          </div>
+
+          <CodexComposer
+            ref="composerRef"
+            :model-value="selectedRecordId ?? null"
+            :models="modelOptions"
+            :model-locked="modelLocked"
+            :model-loading="modelApplying"
+            :disabled="composerDisabled"
+            :streaming="store.streaming"
+            :waiting="store.pendingApprovals.length > 0"
+            :perm-mode="store.permMode"
+            :cwd="store.activeCwd"
+            @update:model-value="onModelChange"
+            @send="onSend"
+            @stop="onStop"
+            @templates="templatesOpen = true"
+            @update:draft="composerDraft = $event"
+            @perm-change="onPermChange"
+            @goto-approval="onGotoApproval"
+          />
+        </template>
+
+        <!-- ② 模型管理 -->
+        <template v-else-if="view === 'model'">
+          <WbModelPage />
+        </template>
+
+        <!-- ③ 插件与技能 -->
+        <template v-else-if="view === 'plugin'">
+          <WbPluginPage />
+        </template>
+      </main>
+
+      <!-- 右栏:看板 + 产物(设计稿:对话视图常驻) -->
+      <aside v-if="view === 'chat'" class="wb-aside">
+        <CodexBoardPanel />
+        <CodexArtifactPanel />
+      </aside>
+    </div>
+
+    <CodexDiagnosticDrawer v-model:open="diagnosticsOpen" />
+    <CodexTemplatePanel v-model:open="templatesOpen" :draft="composerDraft" @pick="onPickTemplate" />
+  </div>
+</template>
+
+<script lang="ts" setup name="CodexWorkbench">
+  /**
+   * 智能工作台(智能模式的主界面)。
+   *
+   * 不套传统 LAYOUT 的全屏路由:AI 能力集中呈现——对话(本机 Codex)/ 模型管理 /
+   * 插件与技能三个视图经左栏菜单切换。案件上下文与 codexAnalysis 完全同源
+   * (useUserStore().getCaseInfo → syncCaseContext);切换案件必须退出后从案件管理重进。
+   *
+   * 组件级复用:消息组件 / 内联审批 / 权限下拉 / 会话侧栏 / 看板产物面板全部来自
+   * codex 模块现有实现,本文件只做装配,不含协议逻辑。
+   */
+  import { computed, onMounted, ref } from 'vue';
+  import { Button, Tooltip } from 'ant-design-vue';
+  import { useRouter } from 'vue-router';
+  // 必须先加载模块主题(--codex-* 设计 token 全部定义在 codexTheme.less),
+  // 否则工作台路由上拿不到任何 token,样式整体塌掉
+  import '@/codex/codexIndex';
+  import Icon from '@/core/components/Icon/src/Icon.vue';
+  import { AppWorkbenchToggle } from '@/core/components/Application';
+  import { PageEnum } from '@/core/enums/pageEnum';
+  import { useMessage } from '@/core/hooks/web/useMessage';
+  import { useUserStore } from '@/core/store/modules/case';
+import { useAppStoreWithOut } from '@/core/store/modules/app';
+  import WbModelPage from './WbModelPage.vue';
+  import WbPluginPage from './WbPluginPage.vue';
+  import WbSessionList from './WbSessionList.vue';
+  import CodexMessageList from '../../components/codexMessageList.vue';
+  import CodexComposer from '../../components/codexComposer.vue';
+  import CodexArtifactPanel from '../../components/codexArtifactPanel.vue';
+  import CodexBoardPanel from '../../components/codexBoardPanel.vue';
+  import CodexDiagnosticDrawer from '../../components/codexDiagnosticDrawer.vue';
+  import CodexTemplatePanel from '../../components/codexTemplatePanel.vue';
+  import { useCodexChatPage } from '../../composables/useCodexChatPage';
+
+  const AButton = Button;
+
+  const router = useRouter();
+  const userStore = useUserStore();
+  const appStore = useAppStoreWithOut();
+  const { showMessage } = useMessage();
+
+  type WorkbenchView = 'chat' | 'model' | 'plugin';
+  const view = ref<WorkbenchView>('chat');
+
+  const RAIL_ITEMS: Array<{ key: WorkbenchView; icon: string; label: string }> = [
+    { key: 'chat', icon: 'mdi:robot-outline', label: '对话' },
+    { key: 'model', icon: 'mdi:brain', label: '模型管理' },
+    { key: 'plugin', icon: 'mdi:puzzle-outline', label: '插件与技能' },
+  ];
+
+  const {
+    store,
+    composerRef,
+    listRef,
+    diagnosticsOpen,
+    templatesOpen,
+    starting,
+    composerDraft,
+    modelApplying,
+    modelOptions,
+    selectedRecordId,
+    modelLocked,
+    composerDisabled,
+    reportTitle,
+    gateHint,
+    onStartRuntime,
+    onSend,
+    onStop,
+    onRegenerate,
+    onResolveApproval,
+    onPermChange,
+    onGotoApproval,
+    onModelChange,
+    onPickTemplate,
+    onCreate,
+    onSessionSelect,
+  } = useCodexChatPage();
+
+  /** 输入区上方的建议 chips:空会话给示例问题,问过之后给追问方向(均只填空、不自动发送) */
+  const EXAMPLES = [
+    '当前工作目录里有哪些数据文件?分别是什么格式、多少行?',
+    '把每张表的记录数与字段列出来,并标出可能为空的字段',
+    '统计出现次数最多的前 10 个对象,用图表展示,并列出明细',
+    '把上面两个结果整理成一份带结论的报告',
+  ];
+  const FOLLOW_UPS = ['按赔付金额再排一遍 TOP 渠道', '生成给管理层的简要版', '把结果导出成报告'];
+  const suggestions = computed(() => (store.hasMessages ? FOLLOW_UPS : EXAMPLES).slice(0, 3));
+
+  function onSuggest(text: string) {
+    composerRef.value?.fill(text);
+  }
+
+  const caseName = computed(() => userStore.getCaseInfo?.name || '未打开案件');
+  const fileCount = computed(() => store.sessionFiles.entries.length);
+
+  /** 顶栏模型 chip:只显示「模型管理」里已应用的模型 */
+  const modelLabel = computed(() => {
+    const applied = store.runtime?.applied;
+    return applied?.name || applied?.model || '';
+  });
+
+  /** 路由层状态:端点只会 Chat 协议时经内置路由层桥接 */
+  const routerInfo = computed(() => store.runtime?.router ?? null);
+
+  onMounted(() => {
+    // 从传统模式切进来时运行时可能还没起过:拉一次状态即可,不强制启动
+    store.loadRuntime();
+  });
+
+  /** 模式切回传统:回首页(业务页面在传统模式里照常使用) */
+  function onModeChange(mode: 'traditional' | 'smart') {
+    if (mode === 'traditional') {
+      appStore.setWorkbenchMode('traditional');
+      router.replace(PageEnum.BASE_HOME);
+    }
+  }
+
+  /**
+   * 退出案件:回传统模式的案件管理页重新选择。
+   * 案件切换不允许在工作台内直接完成(业务口径)。
+   */
+  function onExitCase() {
+    showMessage('已返回案件管理,重新打开案件后再进入工作台', 'info');
+    appStore.setWorkbenchMode('traditional');
+    router.replace(PageEnum.BASE_CASE);
+  }
+</script>
+
+<style lang="less">
+  .wb-page {
+    display: flex;
+    flex-direction: column;
+    width: 100vw;
+    height: 100vh;
+    overflow: hidden;
+    background: var(--codex-bg-page, #f0f2f5);
+  }
+
+  .wb-top {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    flex: 0 0 auto;
+    height: 50px;
+    padding: 0 16px;
+    background: var(--codex-bg);
+    border-bottom: 1px solid var(--codex-border);
+  }
+
+  .wb-logo {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    font-size: 14px;
+    font-weight: 700;
+    color: var(--codex-text);
+    white-space: nowrap;
+  }
+
+  .wb-logo__mark {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 24px;
+    height: 24px;
+    font-size: 13px;
+    color: #fff;
+    background: var(--codex-primary);
+    border-radius: 6px;
+  }
+
+  .wb-chip {
+    display: inline-flex;
+    align-items: center;
+    gap: 6px;
+    flex: 0 0 auto;
+    padding: 3px 11px;
+    font-size: 12.5px;
+    color: var(--codex-text-2);
+    white-space: nowrap;
+    background: var(--codex-bg-subtle);
+    border-radius: 99px;
+
+    b {
+      font-weight: 600;
+      color: var(--codex-text);
+    }
+
+    &.wb-chip--muted {
+      color: var(--codex-text-3);
+    }
+
+    &.wb-chip--model {
+      color: var(--codex-primary);
+      background: var(--codex-primary-soft);
+      font-weight: 600;
+    }
+  }
+
+  .wb-chip__exit {
+    display: inline-flex;
+    align-items: center;
+    padding: 0;
+    font-size: 11.5px;
+    color: var(--codex-text-3);
+    background: transparent;
+    border: none;
+    cursor: pointer;
+
+    &:hover {
+      color: var(--codex-error);
+    }
+  }
+
+  .wb-spacer {
+    flex: 1 1 auto;
+  }
+
+  .wb-iconbtn {
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    width: 28px;
+    height: 28px;
+    color: var(--codex-text-2);
+    background: transparent;
+    border: none;
+    border-radius: 7px;
+    cursor: pointer;
+
+    &:hover {
+      color: var(--codex-primary);
+      background: var(--codex-bg-hover);
+    }
+  }
+
+  .wb-avatar {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex: 0 0 auto;
+    width: 26px;
+    height: 26px;
+    font-size: 12px;
+    font-weight: 700;
+    color: #fff;
+    background: var(--codex-primary);
+    border-radius: 50%;
+  }
+
+  .wb-body {
+    display: flex;
+    flex: 1;
+    min-height: 0;
+  }
+
+  /* ── 左栏:菜单 + 会话 ── */
+  .wb-rail {
+    display: flex;
+    flex-direction: column;
+    flex: 0 0 auto;
+    width: 264px;
+    min-height: 0;
+    background: #f7f8fa;
+    border-right: 1px solid var(--codex-border);
+  }
+
+  .wb-rail__menu {
+    display: flex;
+    flex-direction: column;
+    gap: 2px;
+    padding: 10px 8px;
+    border-bottom: 1px solid var(--codex-border);
+  }
+
+  .wb-rm {
+    display: flex;
+    align-items: center;
+    gap: 9px;
+    padding: 8px 10px;
+    font-size: 13px;
+    color: var(--codex-text-2);
+    text-align: left;
+    background: transparent;
+    border: none;
+    border-radius: 7px;
+    cursor: pointer;
+
+    &:hover {
+      color: var(--codex-primary);
+      background: var(--codex-bg-hover);
+    }
+
+    &.is-on {
+      color: var(--codex-primary);
+      background: var(--codex-primary-soft);
+      font-weight: 600;
+    }
+  }
+
+  /* ── 中栏 ── */
+  .wb-view {
+    display: flex;
+    flex: 1;
+    flex-direction: column;
+    min-width: 0;
+
+    /* 对话视图:滚动容器占满,composer 贴底(与 codex-main 同构) */
+    > .codex-scroll {
+      flex: 1 1 auto;
+      min-height: 0;
+      background: var(--codex-bg-page);
+    }
+
+  }
+
+  /* ── 右栏:看板 + 产物,白卡面板(设计稿) ── */
+  .wb-aside {
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+    flex: 0 0 auto;
+    width: 340px;
+    padding: 12px;
+    overflow: hidden auto;
+    background: #f7f8fa;
+    border-left: 1px solid var(--codex-border);
+
+    .codex-board,
+    .codex-artifact-panel {
+      flex: 0 0 auto;
+      overflow: hidden;
+      background: var(--codex-bg);
+      border: 1px solid var(--codex-border);
+      border-radius: var(--codex-radius);
+    }
+  }
+
+  /* ── 追问 / 示例建议 chips(设计稿:输入区上方) ── */
+  .wb-sugs {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
+    flex: 0 0 auto;
+    width: 100%;
+    max-width: var(--codex-column-max);
+    margin: 0 auto 8px;
+    padding: 0 var(--codex-sp-2);
+  }
+
+  .wb-sug {
+    display: inline-flex;
+    align-items: center;
+    max-width: 100%;
+    height: 28px;
+    padding: 0 12px;
+    overflow: hidden;
+    font-size: 12px;
+    color: var(--codex-text-2);
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    background: var(--codex-bg);
+    border: 1px solid var(--codex-border);
+    border-radius: 99px;
+    cursor: pointer;
+
+    &:hover:not(:disabled) {
+      color: var(--codex-primary);
+      border-color: var(--codex-primary);
+    }
+
+    &:disabled {
+      opacity: 0.55;
+      cursor: not-allowed;
+    }
+  }
+
+  /* ── 空态 / 门禁 ── */
+  .wb-empty {
+    padding: var(--codex-sp-4) 0 var(--codex-sp-6);
+
+    &__title {
+      margin: 0 0 6px;
+      font-size: 17px;
+      font-weight: 700;
+      color: var(--codex-text);
+    }
+
+    &__desc {
+      margin: 0 0 var(--codex-sp-4);
+      font-size: 13px;
+      line-height: 1.8;
+      color: var(--codex-text-2);
+    }
+  }
+
+  .wb-gate {
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+    gap: var(--codex-sp-2);
+    margin: 0 0 var(--codex-sp-4);
+    padding: var(--codex-sp-3);
+    background: fade(@warning-color, 8%);
+    border: 1px solid fade(@warning-color, 30%);
+    border-radius: var(--codex-radius);
+
+    &__head {
+      display: flex;
+      align-items: center;
+      gap: 6px;
+      font-size: 13px;
+      font-weight: 600;
+      color: var(--codex-text);
+    }
+
+    &__desc {
+      margin: 0;
+      font-size: 12.5px;
+      line-height: 1.7;
+      color: var(--codex-text-2);
+    }
+  }
+
+  .wb-fallback {
+    display: flex;
+    flex: 1;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    gap: var(--codex-sp-2);
+    padding: var(--codex-sp-6);
+    color: var(--codex-text-2);
+    text-align: center;
+
+    h2 {
+      margin: 0;
+      font-size: 17px;
+      font-weight: 600;
+      color: var(--codex-text);
+    }
+
+    p {
+      max-width: 460px;
+      margin: 0;
+      font-size: 13px;
+      line-height: 1.8;
+    }
+  }
+</style>

+ 2 - 0
ai-electron/frontend/src/core/components/Application/index.ts

@@ -7,6 +7,7 @@ import appSearch from './src/search/AppSearch.vue';
 import windowControls from './src/WindowControls.vue';
 import appLocalePicker from './src/AppLocalePicker.vue';
 import appDarkModeToggle from './src/AppDarkModeToggle.vue';
+import appWorkbenchToggle from './src/AppWorkbenchToggle.vue';
 
 export { useAppProviderContext } from './src/useAppContext';
 
@@ -15,6 +16,7 @@ export const AppProvider = withInstall(appProvider);
 export const AppSearch = withInstall(appSearch);
 export const AppLocalePicker = withInstall(appLocalePicker);
 export const AppDarkModeToggle = withInstall(appDarkModeToggle);
+export const AppWorkbenchToggle = withInstall(appWorkbenchToggle);
 
 // 无边框窗口的标题栏:与主界面 LayoutHeader 同款外观(logo + 操作区 + 窗口控制)
 export const AppHeader = withInstall(appHeader);

+ 112 - 0
ai-electron/frontend/src/core/components/Application/src/AppWorkbenchToggle.vue

@@ -0,0 +1,112 @@
+<template>
+  <div class="jeesite-workbench-switch" role="group" aria-label="界面模式切换">
+    <button
+      type="button"
+      :class="{ 'is-on': mode === 'traditional' }"
+      :title="mode === 'traditional' ? '当前:传统模式' : '切换到传统模式(现有布局)'"
+      @click="setMode('traditional')"
+    >
+      <Icon icon="i-mdi:view-dashboard-outline" :size="13" />
+      传统
+    </button>
+    <button
+      type="button"
+      :class="{ 'is-on': mode === 'smart' }"
+      :title="mode === 'smart' ? '当前:智能模式' : '切换到智能模式(AI 工作台)'"
+      @click="setMode('smart')"
+    >
+      <Icon icon="i-mdi:star-four-points-outline" :size="13" />
+      智能
+    </button>
+  </div>
+</template>
+
+<script lang="ts" setup>
+  /**
+   * 主界面模式切换:传统(现有布局)⇄ 智能(全屏 AI 工作台)。
+   *
+   * 状态持久化在 app store(APP_WORKBENCH_MODE__,按用户记住);
+   * 路由跳转由使用方决定(传统壳里切智能 → /workbench;工作台里切传统 → 首页),
+   * 这里只负责发出意图事件,保持组件无路由依赖。
+   */
+  import { computed } from 'vue';
+  import Icon from '@/core/components/Icon/src/Icon.vue';
+  import { useAppStoreWithOut } from '@/core/store/modules/app';
+  import type { WorkbenchMode } from '@/core/store/modules/app';
+
+  const props = withDefaults(defineProps<{ current?: WorkbenchMode }>(), {
+    current: undefined,
+  });
+
+  const emit = defineEmits<{
+    (e: 'change', mode: WorkbenchMode): void;
+  }>();
+
+  const appStore = useAppStoreWithOut();
+
+  /** current 未传时直接读全局状态(工作台场景);传了则以受控值为准 */
+  const mode = computed<WorkbenchMode>(() => props.current ?? appStore.getWorkbenchMode);
+
+  function setMode(value: WorkbenchMode) {
+    if (mode.value === value) return;
+    emit('change', value);
+  }
+</script>
+
+<style lang="less" scoped>
+  @prefix-cls: ~'jeesite-workbench-switch';
+
+  .jeesite-workbench-switch {
+    display: inline-flex;
+    gap: 2px;
+    padding: 2px;
+    background: rgba(0, 0, 0, 0.06);
+    border: 1px solid rgba(0, 0, 0, 0.08);
+    border-radius: 99px;
+
+    button {
+      display: inline-flex;
+      align-items: center;
+      gap: 5px;
+      height: 26px;
+      padding: 0 12px;
+      font-size: 12.5px;
+      color: inherit;
+      white-space: nowrap;
+      opacity: 0.75;
+      background: transparent;
+      border: none;
+      border-radius: 99px;
+      cursor: pointer;
+      transition:
+        background 0.15s ease-out,
+        color 0.15s ease-out;
+
+      &:hover {
+        opacity: 1;
+      }
+
+      &.is-on {
+        color: @primary-color;
+        background: #fff;
+        font-weight: 650;
+        opacity: 1;
+        box-shadow: 0 1px 4px rgba(24, 39, 88, 0.14);
+      }
+    }
+  }
+
+  html[data-theme='dark'] .jeesite-workbench-switch {
+    background: rgba(255, 255, 255, 0.08);
+    border-color: rgba(255, 255, 255, 0.12);
+
+    button {
+      color: rgba(255, 255, 255, 0.72);
+    }
+
+    button.is-on {
+      color: #fff;
+      background: rgba(255, 255, 255, 0.18);
+    }
+  }
+</style>

+ 1 - 0
ai-electron/frontend/src/core/enums/cacheEnum.ts

@@ -9,6 +9,7 @@ export const MULTIPLE_TABS_KEY = 'MULTIPLE_TABS__';
 
 export const LOCALE_KEY = 'LOCALE__';
 export const APP_DARK_MODE_KEY = 'APP_DARK_MODE__';
+export const APP_WORKBENCH_MODE_KEY = 'APP_WORKBENCH_MODE__';
 export const APP_LOCAL_CACHE_KEY = 'COMMON_LOCAL__';
 export const APP_SESSION_CACHE_KEY = 'COMMON_SESSION__';
 

+ 1 - 0
ai-electron/frontend/src/core/enums/pageEnum.ts

@@ -4,6 +4,7 @@ export enum PageEnum {
   // 已废弃:登录页已删除,这里仅用于识别老书签 / 老回跳地址,不再注册为路由
   LEGACY_LOGIN = '/login',
   BASE_CASE = '/case',
+  BASE_WORKBENCH = '/workbench',
   // basic home path
   BASE_HOME = '/data/index',
   BASE_UPLOAD = '/data/upload',

+ 14 - 1
ai-electron/frontend/src/core/layouts/default/header/index.vue

@@ -34,6 +34,7 @@
         <AppSearch mode="bar" placeholder="搜索系统内容" />
       </div>
       <span :class="`${prefixCls}-search-divider`"></span>
+      <AppWorkbenchToggle @change="handleWorkbenchModeChange" />
       <Tooltip v-if="showTemplateLibraryAction" title="模板库" placement="bottom" :mouseEnterDelay="0.5">
         <Button
           type="text"
@@ -65,7 +66,7 @@
   import { useRoute } from 'vue-router';
   import { propTypes } from '@/core/utils/propTypes';
   import { Button, Layout, Tooltip } from 'ant-design-vue';
-  import { AppLogo } from '@/core/components/Application';
+  import { AppLogo, AppWorkbenchToggle } from '@/core/components/Application';
   import { AppSearch, WindowControls } from '@/core/components/Application';
   import { Icon } from '@/core/components/Icon';
   import { PageEnum } from '@/core/enums/pageEnum';
@@ -80,6 +81,8 @@
   import { useLocale } from '@/core/locales/useLocale';
   import { onMountedOrActivated } from '@/core/hooks/core/onMountedOrActivated';
   import { useUserStore } from '@/core/store/modules/case';
+  import { useAppStoreWithOut } from '@/core/store/modules/app';
+  import type { WorkbenchMode } from '@/core/store/modules/app';
   import { useEmitter } from '@/core/store/modules/case';
   import { getAuthCache } from '@/core/utils/auth';
   import { CASE_INFO_KEY } from '@/core/enums/cacheEnum';
@@ -95,6 +98,7 @@
       Button,
       ALayoutHeader: Layout.Header,
       AppLogo,
+      AppWorkbenchToggle,
       LayoutTrigger,
       LayoutBreadcrumb,
       LayoutMenu,
@@ -194,6 +198,14 @@
 
       const showTemplateLibraryAction = computed(() => true);
 
+      /** 主界面模式切换:智能 = 进入全屏 AI 工作台 */
+      const handleWorkbenchModeChange = (mode: WorkbenchMode) => {
+        useAppStoreWithOut().setWorkbenchMode(mode);
+        if (mode === 'smart') {
+          go(PageEnum.BASE_WORKBENCH, true);
+        }
+      };
+
       const handleTemplateLibraryClick = async () => {
         if (route.name !== 'ViewsDataIndex') {
           await go(PageEnum.BASE_HOME);
@@ -226,6 +238,7 @@
         currentCaseName,
         showTemplateLibraryAction,
         handleTemplateLibraryClick,
+        handleWorkbenchModeChange,
       };
     },
   });

+ 28 - 0
ai-electron/frontend/src/core/router/guard/index.ts

@@ -20,6 +20,7 @@ import projectSetting from '@/core/settings/projectSetting';
 
 // Don't change the order of creation
 export function setupRouterGuard(router: Router) {
+  createWorkbenchGuard(router);
   createPageGuard(router);
   createPageLoadingGuard(router);
   createHttpGuard(router);
@@ -31,6 +32,33 @@ export function setupRouterGuard(router: Router) {
   createStateGuard(router);
 }
 
+/**
+ * 主界面模式守卫:
+ * - 传统模式敲 /workbench → 回首页;
+ * - 智能模式敲 AI 菜单组页面 → 引导回工作台(AI 能力集中在工作台呈现)。
+ * 业务分析各页面两种模式都可直接访问(直接敲 URL 不拦)。
+ */
+function createWorkbenchGuard(router: Router) {
+  const AI_GROUP_PATHS = [
+    '/aiAnalysis/index',
+    '/aiModel/index',
+    '/aiPlugin/index',
+    '/codexAnalysis/codexAnalysis',
+  ];
+  router.beforeEach((to) => {
+    const mode = useAppStoreWithOut().getWorkbenchMode;
+    if (mode === 'smart') {
+      if (AI_GROUP_PATHS.some((p) => to.path === p || to.path.startsWith(`${p}/`))) {
+        return { path: '/workbench', replace: true };
+      }
+      return;
+    }
+    if (to.path === '/workbench') {
+      return { path: '/data/index', replace: true };
+    }
+  });
+}
+
 /**
  * Hooks for handling page state
  */

+ 26 - 1
ai-electron/frontend/src/core/router/menus/index.ts

@@ -37,9 +37,34 @@ const staticMenus: Menu[] = [];
   }
 })();
 
+/**
+ * 「AI 数据分析」菜单组(传统模式):含父级与全部子项的路径。
+ * 传统模式下整组隐藏——AI 能力集中在「智能模式」工作台里呈现;
+ * 路由仍注册(直接敲 URL 由 workbench 守卫引导切模式)。
+ */
+const AI_WORKBENCH_PATHS = [
+  '/10263',
+  '/aiAnalysis/index',
+  '/aiModel/index',
+  '/aiPlugin/index',
+  '/codexAnalysis/codexAnalysis',
+];
+
+function filterByWorkbenchMode(menus: Menu[]): Menu[] {
+  const mode = useAppStoreWithOut().getWorkbenchMode;
+  if (mode !== 'traditional') return menus;
+  return menus
+    .filter((item) => !AI_WORKBENCH_PATHS.includes(item.path))
+    .map((item) =>
+      item.children?.length
+        ? { ...item, children: item.children.filter((child) => !AI_WORKBENCH_PATHS.includes(child.path)) }
+        : item,
+    );
+}
+
 async function getAsyncMenus() {
   const permissionStore = usePermissionStore();
-  return permissionStore.getBackMenuList;
+  return filterByWorkbenchMode(permissionStore.getBackMenuList);
 }
 
 export const getMenus = async (): Promise<Menu[]> => {

+ 16 - 0
ai-electron/frontend/src/core/router/routes/index.ts

@@ -127,10 +127,26 @@ export const CaseCleanProgressRoute: AppRouteRecordRaw = {
   },
 };
 
+/**
+ * 智能工作台:独立于布局的全屏路由(不套 LAYOUT,覆盖包括菜单在内的整个视口)。
+ * 「智能模式」的主界面;「传统 ⇄ 智能」切换即在本路由与 BASE_HOME 间跳转。
+ */
+export const WorkbenchRoute: AppRouteRecordRaw = {
+  path: PageEnum.BASE_WORKBENCH,
+  name: 'Workbench',
+  component: () => import('@/codex/views/workbench/Workbench.vue'),
+  meta: {
+    title: '智能工作台',
+    hideMenu: true,
+    hideTab: true,
+  },
+};
+
 // Basic routing without permission
 export const basicRoutes = [
   AuthorizationRoute,
   CaseRoute,
+  WorkbenchRoute,
   CaseUploadRoute,
   CaseCleaningRoute,
   CaseCleanProgressRoute,

+ 16 - 1
ai-electron/frontend/src/core/store/modules/app.ts

@@ -11,15 +11,19 @@ import { defineStore } from 'pinia';
 import { store } from '@/core/store';
 
 import { ThemeEnum } from '@/core/enums/appEnum';
-import { APP_DARK_MODE_KEY, PROJ_CFG_KEY } from '@/core/enums/cacheEnum';
+import { APP_DARK_MODE_KEY, APP_WORKBENCH_MODE_KEY, PROJ_CFG_KEY } from '@/core/enums/cacheEnum';
 import { Persistent } from '@/core/utils/cache/persistent';
 import { darkMode } from '@/core/settings/designSetting';
 import { resetRouter } from '@/core/router';
 import { deepMerge } from '@/core/utils';
 import { switchSkin } from '@/core/api/sys/login';
 
+/** 主界面布局模式:traditional = 现有布局;smart = 全屏 AI 工作台 */
+export type WorkbenchMode = 'traditional' | 'smart';
+
 interface AppState {
   darkMode?: ThemeEnum;
+  workbenchMode: WorkbenchMode;
   // Page loading status
   pageLoading: boolean;
   // project config
@@ -32,6 +36,7 @@ let timeId: TimeoutHandle;
 export const useAppStore = defineStore('app', {
   state: (): AppState => ({
     darkMode: undefined,
+    workbenchMode: (localStorage.getItem(APP_WORKBENCH_MODE_KEY) as WorkbenchMode) || 'traditional',
     pageLoading: false,
     projectConfig: Persistent.getLocal(PROJ_CFG_KEY),
     beforeMiniInfo: {},
@@ -44,6 +49,10 @@ export const useAppStore = defineStore('app', {
       return this.darkMode || localStorage.getItem(APP_DARK_MODE_KEY) || darkMode;
     },
 
+    getWorkbenchMode(): WorkbenchMode {
+      return this.workbenchMode;
+    },
+
     getBeforeMiniInfo(): BeforeMiniState {
       return this.beforeMiniInfo;
     },
@@ -76,6 +85,12 @@ export const useAppStore = defineStore('app', {
       switchSkin();
     },
 
+    /** 主界面模式切换(持久化按用户);路由跳转由调用方负责 */
+    setWorkbenchMode(mode: WorkbenchMode): void {
+      this.workbenchMode = mode;
+      localStorage.setItem(APP_WORKBENCH_MODE_KEY, mode);
+    },
+
     setBeforeMiniInfo(state: BeforeMiniState): void {
       this.beforeMiniInfo = state;
     },

+ 39 - 0
ai-electron/frontend/uno.config.ts

@@ -86,6 +86,45 @@ export default defineConfig({
     'i-mdi:folder-open-outline',
     'i-mdi:eye-outline',
     'i-mdi:check-circle-outline',
+
+    // Codex 模块(Icon 运行时加 i- 前缀,动态拼接的图标必须在此白名单)
+    'i-mdi:star-four-points-outline',
+    'i-mdi:view-dashboard-outline',
+
+    // Codex 模块(Icon 组件运行时加 i- 前缀,动态图标必须在此白名单)
+    'i-mdi:bookmark-multiple-outline',
+    'i-mdi:briefcase-outline',
+    'i-mdi:bug-outline',
+    'i-mdi:clipboard-text-outline',
+    'i-mdi:code-braces',
+    'i-mdi:code-json',
+    'i-mdi:console-line',
+    'i-mdi:download-outline',
+    'i-mdi:file-document-edit-outline',
+    'i-mdi:file-document-outline',
+    'i-mdi:file-outline',
+    'i-mdi:file-pdf-box',
+    'i-mdi:folder-outline',
+    'i-mdi:head-snowflake-outline',
+    'i-mdi:image-outline',
+    'i-mdi:information-outline',
+    'i-mdi:language-html5',
+    'i-mdi:language-markdown-outline',
+    'i-mdi:laptop-off',
+    'i-mdi:loading',
+    'i-mdi:message-outline',
+    'i-mdi:message-text',
+    'i-mdi:progress-clock',
+    'i-mdi:restart',
+    'i-mdi:robot-outline',
+    'i-mdi:shield-check-outline',
+    'i-mdi:shield-lock-outline',
+    'i-mdi:stop-circle-outline',
+    'i-mdi:table-arrow-right',
+    'i-mdi:table-large',
+
+    // Codex 工作台新增页面/组件用到的图标
+    'i-mdi:dots-horizontal',
   ],
   presets: [
     presetWind3(),