19 Commits
Author SHA1 Message Date
lijing 64de9a0548 feat(theme): 接入云海子主题通道(A 类免插件,规范 v1.12 §2/§3)
build / build (push) Successful in 3m22s
- index.html 插 A 类 loader:唯一接入点,FOUC 门/manifest 预取/主题 link 追加与排序
  全由通道 yh-theme-init.js 承担,主题升级走 /static 通道,本应用无需重新构建
- vite.config 加 /static dev 代理(YH_THEME_PROXY_TARGET 可覆盖,默认线上通道)
- host.config 回写 __APP_CONFIG__.serviceHost(仅同源):init 的 appearance 请求有硬性
  同源限制,跨源会被直接跳过;本地 dev 保持空串走同源 /api 代理
- 顶栏头像接平台键 yh-avatar,监听 yh-appearance-updated,userInfo 兜底
- view-ui-plus 声明锁 1.3.24(对齐通道皮肤生成版本)
2026-10-21 17:35:24 +08:00
lijing 1ee6b0c702 feat(表单): 下拉支持「按父节点分组」,选中标签与摘要带年级
build / build (push) Successful in 2m40s
- utils/module-session.ts 新增 parseOptionGroups(body, valueKey, labelKey, grouped):
  分组时保留父节点名,叶子带 fullLabel(组名-项名);parseOptionItems 改为其薄封装,
  不分组时行为与原先完全一致
- components/FormCard.vue:multiselect / select 两个分支用 <OptionGroup> 渲染分组
  (「全部」哨兵不进分组,仍单独摆最前);选中标签与提交摘要统一走 optionText();
  alignToOptions 加松匹配(认「高一一班」=「高一-一班」,但裸「一班」多义仍不认)
- types/agent.ts 补 groupByParent 类型

注:view-ui-plus 的 Option 用 :label prop 作选中标签、slot 作下拉项文本,
故分组之外标签也必须带年级(与模块页 `:label="${年级}-${班级}"` 一致)。

验证:npm run build 通过;真机 activity 隐藏档 65/65、可见档 63/63,
实测下拉分组标题为「高一 / 高二」,选中标签为「高一-一班 / 高二-一班」。
2026-10-15 17:00:41 +08:00
lijing 41aa77531a feat(表单): 表单支持条件显示与「隐藏时提交固定值」
build / build (push) Successful in 2m17s
- types/agent.ts:FormField 新增 visibleWhen / requiredWhenVisible / hiddenValue。
- FormCard.vue:
  · 新增 isFieldVisible() —— 字段随上级字段取值增删(多个条件之间是「且」);
    只判断显示、**不清值**(与模块页 form.formType 保留的语义一致)。
  · renderFields 过滤不可见字段;missingCount 只算可见的必填项;必填星号含条件必填。
  · onSubmit:必填校验只看可见字段;隐藏字段不进提交体,但后端标了 hiddenValue 时按固定值提交
    (对齐 NewActivity.vue 的 `quota: receiptMode === "none" ? 0 : …`)。
2026-10-15 15:47:36 +08:00
lijing b0f31608d4 feat(表单): 正文接入模块页同款富文本,公告/宿舍的接收对象接入自绘控件
build / build (push) Successful in 2m43s
承接上一提交(通知公告的两个控件),把其余模块里同类控件一次搬到位。

新增组件(都照搬对应模块页的实现,只把「数据从哪来」换成后端下发的 pickerSources)
  · ScopePickerInput.vue ← 校园公告 components/ClassScopeInput.vue
    (全部班级 / 指定班级 + 只读回显 + 弹窗班级树;v-model 是 scope 对象数组)
  · RoomPickerInput.vue ← 宿舍通知 views/dorm/NewNotice.vue 的「选择接收宿舍」弹窗
    (左楼栋列表 / 右按楼层分组的房间勾选 / 底部已选间数与住宿生人数;v-model 是字符串)
  · utils/picker-tree.ts:只剥 {code,msg,data} 外壳、**不拍平**
    (弹窗要的是原始嵌套树;拍平是下拉字典的口径)

改造
  · RichTextEditor:后端没下发正文图片上传地址时,把工具栏的「图片」整组摘掉
    —— 专业发展的编辑器就是这样(该模块没有 UploadImage)
  · UserCheckInput:抽出公共 parseTree,三个自绘控件共用一份外壳剥离口径
  · FormCard:接入 scopepicker / roompicker(初值、序列化、isWide 独占整行)
  · types/agent.ts:widget 联合类型补 scopepicker / roompicker
2026-10-14 16:56:50 +08:00
lijing 6610623cf6 feat(通知公告): 发布表单接入模块页同款「通知对象」与「正文」控件
build / build (push) Successful in 3m28s
按用户要求,把 NoticeFront 里这两块 UI 原样搬到助手表单(不再自制近似控件):

- 正文 → wangEditor v5(@wangeditor/editor + @wangeditor/editor-for-vue)。
  提交给后端的是 **HTML 字符串**,与模块页 NoticeRequest.content 一致;
  插图上传走 /api/file/UploadImage、预览走 /api/file/Preview?id=(地址由后端下发,
  统一由编排层拼模块前缀)。异步 chunk 加载,避免把 800KB 编辑器塞进首屏。
- 通知对象 → 1:1 搬运模块页 components/UserCheckInput.vue + UserCheckModal.vue:
  radio「全部用户 / 指定用户」+ 只读回显 + +/清空 + 选人弹窗(左 zTree 组织树勾选级联 /
  右已选列表 / 全选 / 删除选中 / 按名字搜索回勾);v-model 值是字符串("all" 或 id 逗号串)。
- 全局依赖 jQuery + zTree 按模块页的方式由 index.html 全局加载(public/libs/),
  不用 npm 引入 —— 保证加载顺序(zTree 必须在 jQuery 之后)与样式/图标与模块页完全一致。
  已确认模块页的 lodash 实际未被使用,故不引入。

FormCard 新增两个 widget 分支,每一处都对齐模块页口径:

- richtext:初值取字符串、提交原 HTML,只读摘要用剥标签后的纯文本;
- userpicker:初值数组 join / 字符串原样(空则 allToken)、提交字符串;
  ★ 跳过下拉字典的「拍平 + 取值对齐」—— 那条路会把人员 id 串判成「认不出的值」清掉,
    等于把模型/用户已经选好的人悄悄抹掉;
- 两者都独占整行(自适应多列会把单选组挤到换行)。

验证:.scratch/verify-notice-publish-widgets.mjs 真机 38/38(含正文真实打字、
两个数据源页签、勾选回显、以及提交体 receiveList 必须是字符串 "101"、content 必须是 <p>…)。
npm run build 通过(RichTextEditor 单独成 chunk)。
2026-10-14 15:19:08 +08:00
lijing 86e3a10a06 fix(表单): 对话里定好的值要对齐选项取值,别再让用户选一遍
build / build (push) Successful in 1m53s
「所属栏目」这类下拉的选项值一律是字符串,而对话里定下来的值走的是另一条路:
integer 参数模型给的是数字 71,string 参数模型很可能直接给名称(年级段「初中」、
学科「数学」)。Select / Radio 的选中态是严格相等比较,于是预填了也选不上、
下拉照旧显示「请选择」—— 用户看到的就是「刚问过我,表单里又让我选一遍」。

在 FormCard 里补一步「对齐选项取值」:先按值匹配(String 后比),再按 label 反查
(唯一命中才认,避免同名歧义)。匹配不上时**不无条件清空**:带 optionCall 的字段
在下发时可能只有内置兜底项(如请假类型的内置「病假 / 事假及其他」,学校的自定义
「调休」要等实时字典),此时认不出只是「暂时」——若清空,实时选项回来后会被
autoPickFirst 顺手改成第一项,那是悄悄改坏数据。只有选项权威(无代取通道,或
实时选项已取回)才丢弃认不出的值,让必填校验老老实实要求用户重选。

同时把 mock 夹具的形态对齐真实后端:专业发展的 detect 早先给字符串 "71",
而工具 schema 声明的是 integer(模型给数字 71),于是「数字 71 与选项字符串 "71"
严格比较不相等」这个坑本地永远测不出来(第 5 次夹具失真)。
2026-10-14 11:35:00 +08:00
lijing 4e6725aa1b feat(profession): 发布专业发展记录表单对齐模块页,支持栏目动态字段
build / build (push) Successful in 1m53s
参照 ProfessionView/src/views/profession/RecordEdit.vue(权威契约)逐字段重写:

- 正文字段名由 content 改为 remark;按模块页补齐「年级段 / 学科 / 是否公开 / 栏目动态字段 / 添加附件」
- 新增「栏目动态字段」机制:这组字段由**栏目的后台配置**决定,后端构建期无法枚举 ⇒
  后端只下发「去哪儿取」(含 {{占位符}} 依赖映射),前端带用户登录态取回后就地渲染;
  提交时按模块页口径打包成 contents = JSON.stringify([{fieldId,value}])
- 支持七种栏目字段类型:text(多行转 textarea) / number / date / radio / checkbox / select / province_city(级联)
- 分区渲染在「正文内容」之前,与模块页一致;取不到字段定义时保留下拉并给「重新加载」
- 单行文本控件补 maxlength 与 placeholder(原先只有 textarea 认 placeholder)
- vite dev 补 /profession 代理,让「路径拼错」在本地就暴露
2026-10-13 18:11:03 +08:00
lijing e00f0db729 feat(welcome): 首屏按使用画像做「常用优先」,老用户回来先看见自己常办的事
build / build (push) Successful in 1m53s
原来首屏对每个人都长一个样:网格顺序写死,指标卡全是「能力 91 项 / 可提交 39 项」
这类覆盖数字。用过一百次请假的人,请假入口照样排在第 4 位。

新增 src/utils/usage.ts(本机使用画像)
- 用 localStorage 记「模块 / 能力的调用次数 + 最近时间」与「写操作成功件数」。
  后端 GET /api/agent/tools 只下发"你能用什么",不提供"你常用什么",
  所以第一步先落在本机:零后端改动、零数据出网;换设备重新累积。
- 归属按 userId + roleId 隔离(一个账号多重身份能力范围不同,混在一起会串推荐);
  目录未回来时退回平台写入的 currentRole,取不到才退化匿名。
- 模块按「轮」计数(一轮查 3 个接口只 +1,符合用户心里"我问过几次"),
  能力按「调用次数」计数;done 只统计 write && success —— 查询再多次也不算成果。
- rankFrequent / summarize 有意吃 state 而不是吃 scope:调用方手里那份快照带版本依赖,
  记录或清除后能立刻重算,避免"点了清除,页面上常用区块还在"的幽灵状态。

首屏(QuickActions.vue)改成双形态
- 无使用记录 → 完全保持原有引导形态(自我介绍 + 覆盖面指标 + 各模块一句成品请求)。
- 有使用记录 → 个性化问候「欢迎回来,X」,正文直接列出「你最近常办:…」;
  指标卡换成「本周已办成 / 常用模块 / 可操作能力」;
  新增「常用」区块(用过最多的模块,点一下就发);
  快速开始网格按频次重排,用过 2 次以上的卡片描边并打「常用 N 次」角标。
- 能力清单弹层叠加使用次数(模块级「你用过 N 次」+ 能力级次数),
  底部提供「清除本机使用记录」(两段式确认),并说明数据不出本机。

顺带把「常用」延伸到导航与顶栏
- AppSidebar:模块按频次置顶 + 常用项左缘一道色条;排序只在进页面时算一次,
  不在会话内实时跳动(否则"我刚点的那行跑哪去了"会打断操作)。
- App.vue:顶栏胶囊补「已帮你办成 N 件」——原来只回答"系统有多大",
  现在同时回答"它为我做过多少事"。

埋点(Chat.vue)
- 挂在每轮响应的 finally,只认回包里的 toolCalls(含浏览器代执行续跑后的最终结果),
  不认"用户点了哪张卡":用户完全可以自己在输入框里打出「我要请假」,
  只有后端的执行轨迹才是事实;失败时 traces 为空,自动不记。

铁律不变:所有入口仍由 /api/agent/tools 目录驱动,前端不自行判权。
「常用」只做排序不做增删;无权使用的能力即使局部画像里有记录,也不进清单。

验证:真机 46/46(.scratch/verify-usage-personalization.mjs,覆盖首次访问形态、
埋点计数口径、写操作才计件、常用前置与角标、侧栏顺序、清除记录、多账号隔离);
请假登记回归 17/17。
2026-10-13 17:11:43 +08:00
lijing 275a7c83af fix(dev): 本地补模块前缀代理,让「路径拼错」在本地就暴露
build / build (push) Successful in 1m52s
请假的模块 key 是 homeleave、真实路由前缀是 /home-leave,后端下发的
optionCall 与附件 uploadPath 都带真实前缀。本地只代理 /api 时,这些带前缀
的请求会直接 404 —— 于是「后端把模块前缀拼错」这类问题在本地测不出来,
请假佐证材料上传 404 就是这么漏到线上的。

这里把各模块前缀一并代理到本地 mock,让本地环境与线上同形。
2026-10-13 16:36:22 +08:00
lijing d1d76c6232 fix(form): 下拉取不到时不再退化成数字框,并让请假表单与模块页对齐
build / build (push) Successful in 2m58s
用户反馈「班级变成数字框了」的直接原因:选项加载失败(模块会话未建立时
接口回 302)后 widgetOf 把下拉降级成了数字输入框 —— 用户不可能知道
「一年级(2)班」的内部编号是 7,等于把表单搞坏。

- widgetOf 恒返回后端下发的控件形态,取不到选项时保留下拉 + 显示「重新加载」
- 请假表单对齐 HomeLeave 模块页:
  * 请假类型 = 单选按钮组 + 默认选中第一项(radio / autoPickFirst)
  * 请假时间 = 起止 datetime + 快捷时段(全天/上午/下午)+ 连续请假勾选框
    + 连续天数 + 逐天可微调时段(switch / quickPeriods / dailyExpand)
  * 事由 placeholder「请输入请假原因.....」、附件提示「可选,大小不超过 200MB」
  * 单选组 / 勾选框 / 多行文本 / 附件 / 审批流独占整行,避免挤在半列走形
- 会话预热:上限 6s→12s,且失败后总是重试两次(原「到点即弃且不重试」是
  「时不时出不来」的来源之一);callModuleApi 支持 forceWarm 供「重新加载」绕过 60s 限流
- alova 的 timeout 原先被写进 headers(fetch 不认),改为真正的客户端超时
- 处理中的转圈常驻并显示已等待秒数,不再因为气泡已有文案就静默
2026-10-13 16:15:42 +08:00
lijing 97cd25b0e0 feat(form): 表单卡支持审批流/部门名镜像/附件对象数组,并落地两步提交
build / build (push) Successful in 1m58s
配合后端把 5 个模块「新建/发布」页的字段完整搬进助手表单:

- FormCard 新增审批流控件(widget=flow):勾「走审批流程」后按步配置,
  每步可选或签/会签并多选审批人(来自教师组织树),提交时拼
  flow:[{name,vote,approverTeaIds}];发布方式选了「提交审批」却没配审批人则当场拦下
- 镜像字段(deptName)不渲染控件,提交时按源字段选中项反查显示名一起带上,
  与模块页「字典 id + 反查名称」的提交口径一致
- 附件支持对象数组形态(作业 attachments:[{fileId,fileUrl,fileType,fileName,sort}]);
  数组类字段在为空时也照常提交(模块页就是恒带 fileIds:"" / attachments:[])
- module-session:callModuleApi 支持 followUp —— 首步业务成功后用返回的资源 id 续跑第二步,
  并带上「第一步成功但没拿回编号」的如实报错
- types:BrowserToolCall 增加 followUp;widget 联合类型补 scopes / flow

验证:npm run build 通过;headless Chrome 真机跑「表单 → 浏览器代执行 → 模块接口」,
公告(镜像 + 审批流 + pushScopes)/ 作业(Create→Publish、Create→Schedule)/
宿舍(镜像 + 全部宿舍哨兵)/ 活动(分类存名称)共 50 项断言全绿
2026-10-13 11:29:12 +08:00
lijing da347f21b5 feat(form): 表单支持多选接收对象 / 附件上传 / 全字段渲染
build / build (push) Successful in 2m50s
配合后端「发布表单与模块页对齐」:前端补齐三类控件与序列化。

- FormCard: 新增 multiselect 与 scopes 多选控件(「全部 XX」哨兵恒为首项,
  选中后仍可叠加具体项;提交时若只有哨兵则按 scopeType 折成
  [{scopeType:1,scopeId:0,scopeName}]);新增 file 附件控件(带登录态 POST
  multipart 到 uploadPath,遇登录跳转先预热模块会话再重试)
- FormCard: allFields 的表单渲染全部字段(不再只渲染缺失必填);file 控件
  显示已传文件名并可移除
- types: widget 联合加 multiselect/file,FormField 补 joinSeparator/uploadPath/
  multiple/allToken/allTokenLabel/scopeType,FormRequest 补 allFields
- module-session: parseOptionItems 支持 labelKey 用「+」拼多字段
  (如 buildingName+roomNo →「1 号楼 101」),组织树自动拍平到叶子

验证:.scratch/verify-module-publish-form.mjs 真机断言三个发布表单
(notice 24/24、dorm 35/35、activity 33/33 通过),npm run build 通过
2026-10-12 22:05:16 +08:00
lijing 2b114829ab feat(chat): 例句与提示覆盖宿舍通知 / 家校活动
build / build (push) Successful in 2m47s
- EXAMPLES 新增「我要发布宿舍通知」「我要发布活动」两个裸句(按能力目录过滤,
  学生身份不会轮播),以及「这条宿舍通知还有谁没回执?」「秋季运动会的报名情况怎么样?」
- TIPS 的宿舍/活动提示改写:说明教师可直接说「我要发布宿舍通知」「我要发布活动」弹表单,
  也能问回执/名单情况。

(.env.dev 是本地配置,按约定不入库。)
2026-10-12 19:49:58 +08:00
lijing aaa08ddfd3 feat(chat): 例句与提示支持「我要发布校园公告」直达
build / build (push) Successful in 1m55s
- EXAMPLES 新增裸句「我要发布校园公告」(tool=notice.publish):
  按能力目录过滤,学生身份不会轮播这句,避免给出"照着说必被拒"的示范。
  保留带细节的「发一条关于社团招新的通知」。
- TIPS 的公告提示改为「直接说『我要发布校园公告』就会弹出发布表单
  (标题 + 正文 + 发文部门)」。

(.env.dev 的改动是本地配置,按约定不入库。)
2026-10-12 18:54:22 +08:00
lijing abd97adf9a feat(leave): 裸意图直达表单 + 按身份分离请假表单 + 前端代取下拉
build / build (push) Successful in 1m52s
- 说「我要请假」这类只有意图的短句时,直接弹出表单不再过模型:
  后端 IntentShortcut 在编排器入口做「去空白标点 + 剥前后缀后正好等于标准说法
  且原句 ≤12 字」的窄匹配,命中即跳过 LLM 直接下发表单。
- 按身份分离:学生 → leave.submit(提交申请),教师/管理员 → leave.register
  (帮学生登记,必填 classId+stuId);RolePolicy 把两者分别归入 STUDENT_ONLY
  与 STAFF_ONLY,模型视野里天然只有一个,不可能选错。
- 表单字段由各自必填参数推导;下拉选项改为后端下发 optionCall(含 {{占位符}}),
  由前端带用户登录态代取后按 valueKey/labelKey 映射(模块 /api/* 服务端必然 302)。
  stuId 依赖 classId,选完班级才解锁学生名单。
- 抽出 utils/module-session.ts(会话预热 / 带登录态调模块 / 字典项解析),
  Chat.vue 与 FormCard.vue 复用。
- 例句与提示改为「我要请假」「帮学生登记请假」。
2026-10-12 17:49:34 +08:00
lijing 854550c019 feat(sidebar): 身份名称直接绑定平台 currentRole.roleName
build / build (push) Successful in 1m58s
- 左下角身份标签改为直接展示平台下发的 roleName(与 GetUserInfo / 门户同源),
  平台叫什么就显示什么,不再用归一化的「教师/学生/管理员」桶
- 取值优先级:GetUserInfo.currentRole.roleName → /api/agent/tools identity.roleName → catalog.roles 兜底
- 实测 byLoginName(lijing) 返回 currentRole={roleId:1, roleName:"老师"},改后左下角显示「老师」
- UserInfoVo 补充 currentRole 字段类型
2026-10-12 16:34:20 +08:00
lijing 946d953103 refactor(modules): 移除资源库模块对接
build / build (push) Successful in 1m56s
- modules.ts / QuickActions.vue / Chat.vue 去掉 resourcelibrary 的标签、落地页、快捷卡与例句
- 模块清单仍完全由后端 /api/agent/tools 下发驱动,前端不硬编码、不自行判权
2026-10-12 16:10:45 +08:00
lijing ab87a4b560 fix(chat): 模块会话缺失时自动预热一次 CAS 会话再重试
build / build (push) Successful in 2m50s
模块接口只认模块自己容器里的 CAS 会话(_const_cas_assertion_)。同源部署只能
保证 Cookie 送得到,送不到「还没建出来的会话」:模块 SPA 建会话靠整页跳 SSO
签票回跳,而代执行走 fetch —— 平台对 AJAX 只回 JSON code=302、不真跳转,
于是首次查询某模块必然失败,且每次重问都同样失败。

现在首次收到 code=302 时:用同源隐藏 iframe 对刚失败的接口地址做一次真实导航
(不带 yschapirequest),走完「模块 302 → SSO 凭已有 TGT 签票回跳 → 模块落会话」,
随后重试原请求;每模块 60s 内只尝试一次,用户无感。会话建好后同浏览器后续查询
直接命中,无额外开销。
2026-10-12 14:57:30 +08:00
lijing 37d292c5d2 feat: 提交校园智能助手前端工程源码(Vue3 + Vite + view-ui-plus)
build / build (push) Successful in 3m14s
2026-10-12 14:45:00 +08:00