# AgentFront · 校园智能助手(前端) > 一句话说需求,助手直接替你操作 —— 请假、报名、发公告、查作业、找资源… > 信息说不全也没关系:助手会给你一张**填写表单**,填完直接提交执行。 与平台其它前端同族:**Vue3 + Vite + alova + view-ui-plus**,同源 `/api` 代理、运行时配置下发、SSO 登录态。 ![首屏:Hero 横幅 + 能力指标 + 8 张快捷卡](docs/new-1-welcome.png) ![补全表单 · 请假(深色导航侧栏 + 浅色对话画布)](docs/new-2-form.png) ![下拉浮层:选项由业务字典实时下发](docs/new-2b-select.png) ![提交后的回执与执行轨迹(写操作给「去系统里查看」)](docs/new-3-done.png) ![补全表单 · 发布公告(标题已从对话识别并回显)](docs/new-6-notice.png) ![查询类直接给结果(我的作业列表)](docs/new-7-query.png) ![提交闭环:报名活动成功回执](docs/new-8-activity-done.png) ![侧栏点模块 = 直接发一句完整请求](docs/new-4-module.png) ![全部 54 项能力清单(写操作高亮)](docs/new-5-caps.png) ## 1. 已接入模块 对话能力**不是只做了请假**——7 个业务模块共 **54 项能力**全部注册,其中 **15 项写操作**可直接提交: | 模块 | 标识 | 能力数 | 写操作(可直接提交) | |---|---|---|---| | 学生请假 | `homeleave` | 11 | 提交申请 / 撤销 / 销假 / 审批 / 批量通过 | | 校园公告 | `campusnotice` | 9 | 发布 / 通过审批 / 驳回审批 | | 宿舍通知 | `dormnotice` | 9 | 提交回执 / 取消回执 | | 家校活动 | `familyactivity` | 7 | 报名 / 取消报名 | | 课程作业 | `coursehomework` | 4 | 提交作业 | | 资源库 | `resourcelibrary` | 5 | 收藏资源 | | 教师专业发展 | `profession` | 9 | 发布记录 | - **写操作**:缺必填参数 → 下发补全表单;信息齐全 → 直接执行。 - **查询**:直接返回结果(作业列表 / 请假记录 / 公告 / 宿舍通知 / 活动广场 / 资源检索 / 积分)。 以上数量是**全量注册**能力,由后端 `ToolRegistry` 实时注册。前端首屏的能力指标卡、侧栏模块导航与「全部能力」弹层都来自 `GET /api/agent/tools`,不是写死的数字。 > **注意:这 54 项不是每个登录用户都能看到。** > `/api/agent/tools` 会按「当前登录身份 + 已授权模块」过滤后再下发:学生看不到「教师专业发展」的积分 / 发布类能力,教师也看不到学生专属的作业提交、活动报名。详见下一节。 ## 2. 登录与权限 ### 2.1 登录(SSO)—— 前端不做登录页 智能助手是平台的一个模块,**登录态由平台 CAS 统一管理**。前端不写登录页,也不自己存 token: 1. 浏览器打开前端,发起 `/api/base/GetUserInfo`; 2. 未登录时后端 CAS 过滤器返回 `{ code: 302, urlToRedirectTo: "…" }`; 3. `src/utils/alova/alova-core.ts` 的 `errorHandle` 收到 302 后**直接整页跳转**到平台登录页,并返回一个永不 resolve 的 Promise(避免后续业务流程继续往下跑); 4. 登录成功后 CAS 带票据跳回原页面,同一请求正常返回当前用户(`BoaUser`),页面直接可用。 所以这条链路只有两件事要维护:**后端负责下发跳转地址,前端负责见到 302 就跳**。 > 本地开发若后端以 `--yh.sso.url-pattern=/__local_noauth__/*` 放行,`/api/base/GetUserInfo` 会返回 `data: null`, > 页面显示「未登录」,能力目录按**访客**身份下发(21 项)—— 这是正常的本地联调形态,不是故障。 ### 2.2 退出登录(换账号) 侧栏底部用户区右侧有一个退出按钮。点击后前端**按顺序做两件事**,缺一不可: ``` ① POST /api/base/Logout ← 清本应用的服务端会话(后端 session.invalidate()) ② {ssoHost}/logout?service=<当前页地址> ← 再跳 CAS 全局登出,销毁 SSO 的 TGT ``` `ssoHost` 取自 `src/assets/config/host.config.ts`(本地 dev 读 `.env.dev` 的 `VITE_SSO_HOST`, 线上读运行时 `/api/env-config`);未配置时不乱跳,只提示。 **为什么两步都要(2026-10-10 修)**:本应用的「登录态」就是服务端 session 里的 `_const_cas_assertion_`(平台 `CasSsoApiRedirectFilter` 正是拿它判断登录与否), - 只做 ②:SSO 的票据没了,**本应用的 JSESSIONID 还活着** → 回到页面依旧是登录态, 用户看到的现象就是「点了退出没用」; - 只做 ①:本地会话清了,但 SSO 侧会话还在 → 一刷新就被重新认证成同一个账号, 现象是「点了退出又自动登回来 / 换不了人」。 顺序不能颠倒:跳走之后页面就卸载了,再没有机会回头调 ①。也不能指望 CAS 的单点登出 (SLO)回推来清会话 —— 平台只注册了 `SingleSignOutFilter`、没有配套的会话监听,且要求 本服务地址在 CAS 侧登记过,本地联调(`http://localhost:8093`)根本不成立。 平台自带模块(`HomeSchLeave` 的 `LoginUserController#logout`)同样是自己在服务端销毁会话。 登出后 CAS 会按 `service` 参数把浏览器送回本页,随后再走一次 「未登录 → 302 → 登录页」的流程,即可用另一个账号登录。 > 这是**唯一**需要前端主动跳 SSO 的地方;其余场景前端只被动响应后端的 302。 > 清会话期间由 `src/utils/logout-guard.ts` 压住「未登录自动跳登录页」—— > 此刻 TGT 还在,被 302 拽去 CAS 会被静默登回原账号。 > > 真机回归:`node .scratch/verify-logout.mjs`(断言"先清会话、再跳 CAS"的顺序与目标)。 ### 2.3 身份与权限 —— 三道处理,越往后越严格 | 层 | 位置 | 作用 | |---|---|---| | ① 角色解析 | 后端 `RoleResolver` | 读平台 `BoaUser`(`type` / `roleIds`)→ 归一为学生 / 教师 / 管理员 / 访客 | | ② 模型侧过滤 | 后端 `ToolRegistry` | 只把**本身份可见**的工具写进模型 schema —— 越权工具模型"根本看不见" | | ③ 服务端强校验 | 后端 `ToolExecutor` | 每次调用前再校验一次,越权直接拒绝并打 `[越权拦截]` 日志留痕 | 前端在这里的定位是**忠实呈现、绝不自行判权**: - 侧栏模块列表、首屏快捷卡、能力数全部由 `/api/agent/tools` 的返回驱动(`orderedModuleKeys()`), **不在前端写死模块名单** —— 否则会出现「后端已过滤、前端又把它显示出来」的越权入口; - 侧栏底部展示 `/api/agent/tools` 返回的 `roles`(如「学生」),让用户理解自己能力范围的由来; - 前端**不复刻角色判定逻辑**:角色号与判定规则属后端,前端抄一份迟早会不一致。 > 具体哪个 `roleId` 算学生 / 教师,由 `AgentService` 的 `application.yml` → `agent.roles.*` 配置, > 按本校 UserBase 角色体系校准;调整它**不需要动前端**。 ## 3. 本地开发 ```bash npm install npm run dev # http://localhost:8080 ``` `/api` 由 vite 代理到本地后端 **AgentService(8093)**(见 `vite.config.ts`)。 所以本地要同时起两个服务: ```bash # 1) 后端(BackendProject_java/AgentService) mvn spring-boot:run # 或 java -jar target/agent.jar # 2) 前端 npm run dev ``` > `VITE_SERVICE_HOST` 必须留空(`.env.dev` 已留空):dev 模式走同源代理, > 否则浏览器直连远端会被后端 CAS 过滤器拦截(不回 CORS 头)。 ### 没有后端也能看界面(本地演示 mock) `.scratch/mock-agent-server.mjs` 是一个与真实后端协议一致、覆盖全部 7 个模块的本地假后端, 用于纯前端验证(**它不是产品代码,不参与构建**): ```bash node .scratch/mock-agent-server.mjs # 监听 8093,与 vite 代理目标一致 npm run dev # 打开 8080 即可对话 ``` 它可以演示:7 个模块的写操作表单、查询结果、表单提交回执,以及「已识别参数中文回显」。 换成真实后端(AgentService)时无需改前端,只要让 8093 由真服务占用即可。 > mock 覆盖的是**全量 7 个模块**(它不模拟身份过滤)。若要在本地验证"学生 / 教师看到的能力不同", > 请连真实后端 8093,并用 `--agent.roles.*` 或真实账号切身份。 ## 4. 页面结构与视觉规范 版式基调:**深色导航侧栏 + 浅色对话画布**——左边管"有什么能力、我是谁",右边管"把事办完"。 ``` src/ ├── App.vue 外壳:左栏 + 右区(顶栏状态/能力概览);统一拉取用户与能力目录 ├── views/Chat.vue ★ 对话页(消息流 / 悬浮输入卡 / 会话管理 / 表单提交链路;轮播例句按身份过滤) ├── components/ │ ├── AppSidebar.vue 左栏:品牌 + 新建对话 + 模块导航(按身份过滤,点一下=发一句完整请求)+ 身份标签 + 使用提示 │ ├── QuickActions.vue 首屏:Hero 横幅 + 能力指标卡 + 快捷卡网格(按身份过滤)+ 全部能力弹层 │ ├── MessageBubble.vue 消息气泡(含等待动画、错误态、挂载表单卡) │ ├── FormCard.vue ★ 补全表单卡(下拉 / 日期时间 / 多行文本 + 必填校验) │ └── ToolTraceCard.vue ★ 工具执行轨迹卡(可展开看接口与参数、写操作给「去查看」) ├── api/module/agent.ts 后端接口封装(chat / submitForm / tools / clearSession) ├── assets/styles/agent.css 设计系统:色彩/圆角/阴影令牌、模块色调、外壳骨架、组件库微调 ├── assets/config/ │ ├── host.config.ts 平台运行时配置(与其他模块一致) │ └── modules.ts 模块 → 上下文路径映射 + MODULE_META(图标/色调/示例句)+ orderedModuleKeys(按能力目录排序) └── types/agent.ts 类型契约(与后端 model 包对齐) ``` 视觉约定(改样式前先看这里,避免各改一套): - **颜色**:只用 `--agent-*` 令牌(见 `agent.css` `:root`),不要在组件里写新色值。 - **模块色**:`MODULE_META[key].tone` → `.tone-xx` 类,侧栏/快捷卡/轨迹卡三处**同一模块必然同色**。 - **色值变体**:`.tone-xx` 提供 `--tone`(浅底上用)、`--tone-soft`(浅底)、`--tone-light`(深色侧栏上用)。 - **阴影**:`--agent-shadow-xs/sm/md/lg` 四档,统一"贴地短影 + 大扩散长影",不要自造 box-shadow。 - **内容宽度**:正文与输入区统一 `--agent-readw`(880px)居中。 - **view-ui-plus**:模板里组件必须 PascalCase(`Select` / `DatePicker`),写 `i-xxx` 会被静默忽略。 ## 5. 与后端的数据契约 `POST /api/agent/chat` ```jsonc // 请求:普通对话 { "sessionId": "可选,首次不传", "message": "我要请假" } // 响应 { "sessionId": "92f0…", // 后续轮次带上它保持上下文 "reply": "要完成「提交请假申请」,还差这几项:请假类型、开始时间、结束时间……", "toolCalls": [ // 本轮实际执行过的接口调用(可审计) { "name": "leave.types", "title": "请假类型", "module": "homeleave", "success": true, "write": false, "costMs": 42 } ], "wroteSomething": false, "landingPath": "/my", // 写操作后「去查看」的落地路由 "form": { // ★ 需要补全信息时才有 "tool": "leave.submit", "title": "提交请假申请", "write": true, "missing": ["请假类型", "开始时间"], "preset": { "remarks": "发烧就医" }, "recognized": [{ "name": "remarks", "label": "请假事由", "text": "发烧就医" }], "fields": [ /* widget: select | datetime | date | textarea | text | number */ ] } } ``` `POST /api/agent/chat`(表单提交) ```jsonc { "sessionId": "92f0…", "formSubmit": { "tool": "leave.submit", "args": { "leaveType": 1, "startDate": "…", "endDate": "…" } } } ``` ## 6. 表单化补参(核心交互) 用户说「我要请假」时,前端**不做口头追问**,而是把后端的 `form` 渲染成一张卡: - **控件按字段类型自动选**:`select` → 下拉(选项可来自业务字典接口)、 `datetime/date` → 日期时间选择器(不用手输格式)、`textarea` → 多行输入。 - **已识别的信息以中文标签回显**(「请假类型:病假」而不是 `leaveType=1`),用户核对后再补缺的。 - **必填校验在前端拦截**,缺失时字段下方标红提示,不会带着残缺参数打到后端。 - **提交即执行**:走 `formSubmit` 直接调用目标工具,服务端不再让模型二次猜测 —— 填什么就提交什么。 - 提交成功后卡片转为**只读摘要**;失败则恢复可编辑,用户改完可重试。 输入侧还有两处引导: - 输入框 **placeholder 轮播成品例句**(每 4.2s 换一条),例如「我要请 10月12日 上午的病假,发烧就医」。 **例句按当前身份过滤**:每条标注它依赖的模块与工具,学生不会轮播到「发通知」「专业发展积分」这类教师专属句子, 避免给出一个"照着说必被拒"的示范。 - 边打字边提示:命中「请假 / 公告 / 作业 / 宿舍 / 报名 / 审批 / 资源 / 积分」等关键词时, 下方显示该场景「怎么说更准」(如请假要说清 类型 + 起止时间 + 事由)。 > 后端识别到**信息已齐全**时不会弹表单,而是直接执行。 > 例:「我要请 10月12日 上午的病假,发烧就医」→ 类型、起止时间、事由全部解析成功 → 直接提交并回执。 ## 7. 怎么改「去处理」跳转 `src/assets/config/modules.ts` 里维护 `模块标识 → 前端上下文路径`: ```ts export const MODULE_CONTEXT = { homeleave: "home-leave", // → http://base.yunhaixingtong.com/home-leave/front/#/my coursehomework: "coursehomework", // … }; ``` key 与后端 `ToolDef.module` 一致;新模块上线时这里补一行即可。 ## 8. 构建与发布 ```bash npm run build # 产物直出 dist/ 根(CI 打 zip 取 dist 根) ``` 推送 `main` 分支后由内网 Gitea Actions 自动构建(`.gitea/workflows/build.yml`): `npm ci + npm run build → 打包 dist 为 zip → 上传 Nexus raw → 通知 projman 建档`, 最后由人工在 projman「前端发布」Tab 点部署。 - 产物名:`agent-front-.zip` - webhook app:`agent`,front:`front` > ⚠️ **首次上线的前置条件(必须由 projman / 平台管理员完成,否则流水线必红)** > 1. 在 Gitea 建组织 `agent`,并预生成两个仓库:`agent/agent-front`、`agent/agent-service` > (与其它 app 同构:`/-front` + `/-service`)。 > 仓库需带好 projman 脚手架(`README.md` + `.gitea/workflows/build.yml`)。 > 2. 给 `agent/agent-front` 配仓库级 Secrets:`DEPLOY_TOKEN`、`NEXUS_USER`、`NEXUS_PASSWORD`。 > 3. 在 projman 侧登记 app `agent`(`app=agent`、`front=front`),否则 webhook 建档那一步会失败。 > 4. 后端 `agent-service` 的部署方式与其它 Java 服务一致(打 jar 运行,端口 8093), > 并确保其 `application.yml` 的 `agent.roles.*`(身份判定)与 `agent.modules.*` 指向正确的模块地址。 > > 以上就绪后:本地 `git remote add origin <仓库地址>` → push main 即可触发构建。 ## 9. 浏览器代执行(方案 A)—— 打通模块 302 模块的 `/<模块>/api/*` 只认**浏览器 CAS 会话**,服务端代调必然 `code=302`(根因见后端 README 第 11 节)。 方案 A 换掉调用方:**服务端不直调,而是把接口描述挂起下发给前端,由浏览器用用户自己的登录态执行**, 再把原始响应回传服务端续跑。模块侧零改动。 **两段协议**(`POST /api/agent/chat`): ```jsonc // ① 服务端挂起 → 前端 { "sessionId": "92f0…", "browserTool": { "notice": "…正在通过你的浏览器执行…", "calls": [{ "name": "notice.received", "title": "我收到的公告", "method": "GET", "pathAndQuery": "/campusnotice/api/notice/List?pageNo=1", "arguments": { "pageNo": 1 }, "write": false }] } } // ② 前端执行后回传 → 服务端续跑 { "sessionId": "92f0…", "toolResult": [{ "name": "notice.received", "arguments": { "pageNo": 1 }, "status": 200, "body": "{\"code\":0,\"data\":{…}}" }] } ``` 实现位置: - `src/views/Chat.vue` 的 `execBrowserCall()`:拼 `location.origin + pathAndQuery`, `credentials: "same-origin"`(同源 ⇒ 自动带会话)、带 `yschapirequest: 1`、15s 超时; `runBrowserTool()` 负责批量执行 + 回传 + **链式挂起的递归续跑**。 - ★ **回传前必须先把挂起响应里的 `sessionId` 落地**,否则服务端找不到会话历史(会当成新会话)。 - 服务端只接受「工具名 + 参数 + 状态码 + 响应体」,路径/方法/模块一律自行重新解析,**不信任前端**。 **生效前提:必须与模块同站部署**(如 `base.yunhaixingtong.com/agent/`)。 浏览器只给同源请求带 Cookie,本地 `localhost` 拿不到 base 域的会话 —— 这是浏览器安全模型,绕不过; 本地只能验证"两段协议跑通",真实生效要等部署。 **降级**:浏览器这条也走不通时(跨域 / 会话过期 / 404),结果回传后由服务端判为失败, 自动落回第 7 节的深链兜底(「去系统里自己查看」),UI 不会卡死。 **真机断言**: ```bash MOCK_BROWSER_EXEC=1 node .scratch/mock-agent-server.mjs # mock 以代执行模式跑在 8093 node .scratch/verify-browser-exec.mjs http://127.0.0.1:8081 shot-browser-exec.png # 成功分支 13 项 STUB_FAIL=1 node .scratch/verify-browser-exec.mjs http://127.0.0.1:8081 shot-fail.png # 降级分支 13 项 ```