Compare commits

...
2 Commits
Author SHA1 Message Date
brother7 4103754b80 docs(teacher): 记录一次发布与配置刷新集成
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled
2026-09-29 12:06:59 +08:00
brother7 913ccad577 fix(teacher): 独立刷新老师目录以缩短配置更新延迟 2026-09-29 12:03:06 +08:00
15 changed files with 243 additions and 11 deletions

No files matched your search

@@ -10,11 +10,14 @@
## Decision
- 2026-09-29 用户确认一次显式发布和及时刷新并授权合并。Yuxi 管理员在可管理的原生智能体编辑页一次“保存并发布”即可更新 Makelore;普通保存只改草稿。Yuxi 固定不可变快照并携同一操作身份交给 WS,只有 WS 登记提交后才报成功,失败重试不发布后来编辑的草稿。WS 按来源绑定稳定老师 ID,首次复用唯一启用旧项,歧义需显式选择;保留默认、停用和历史,不自动合并旧身份,重新启用需要显式动作。
- 前台每五秒读取小型目录 revision,进入模块及回到前台补查,变化才重新读取完整配置;后台停止轮询。刷新独立于聊天、输入和正在回答,不触发模型。Main 每次接受新问题仍读取当前版本,运行中的问题固定原版本,同项目可见聊天保持连续。 五秒是检查间隔,实际可见时间还包含网络请求,不承诺离线或故障时延迟。
- 2026-09-28 用户要求移除老师对话栏的结构化组件并确认合并;远程回复清理和恢复工作一并集成。新咨询由 Main 统一声明 `{reply, quickReplies}`,正文和 0–3 条可选快捷回复按所选智能体云端配置生成,客户端只提供入口事实、上下文与输出格式。超额新卡片保留正文及原文诊断,不截断或展示;历史卡片数量不改写。旧组件状态和快照作为不透明档案保留,不恢复交互、主动导入或进入模型上下文,旧组件修改路由已移除。 结果不确定的旧请求保留请求 ID、问题、引用与来源,客户端重试移除退役组件字段,Main 按已有请求身份返回结果,避免重复执行。
- 2026-09-28 用户确认像微信联系人一样持续聊天,随后明确纠正:不同项目必须有不同会话,并授权修复及合并。唯一可见聊天身份为账号+项目+稳定 config_id;同一项目不按名称、默认标记、Pi 会话或发布版本创建第二个可见聊天。项目隔离历史、草稿、已读和模型上下文;本次范围为本机持久化,不含跨设备同步。
- 新项目创建内部默认编程 Agent 并保存 defaultAgentId,会话直接列在项目下。打开项目不自动创建空会话,首次发送或显式新建才创建;历史 Agent ID、配置和 Pi 会话绑定保留。
- Yuxi 拥有老师提示词、模型和所选 Skills、知识库、MCP、子智能体。Works Square 运营菜单首先展示下发列表,添加时从 Yuxi 选择并发布当前配置,支持多位、默认选择与启停,不维护第二份提示词编辑器或试聊界面。
- 运营显式同步 Yuxi 配置产生不可变下发版本。每次接受新问题使用当前已下发版本,运行中的问题保持原版本;版本变化不另建可见聊天。配置与资源绑定固定,Skill 按问题建立运行快照,知识内容和远端工具仍由各自服务拥有;资源撤销可能阻止执行,版本不表示复制全部外部资源。
- Yuxi 显式发布或运营同步产生不可变下发版本。每次接受新问题使用当前已下发版本,运行中的问题保持原版本;版本变化不另建可见聊天。配置与资源绑定固定,Skill 按问题建立运行快照,知识内容和远端工具仍由各自服务拥有;资源撤销可能阻止执行,版本不表示复制全部外部资源。
- Yuxi 原生 Request/Run、PostgreSQL checkpoint 和 Redis 事件拥有云执行。Main 冻结本次账号、项目、来源 Pi 已完成分支的公开消息及老师话题,通过三个只读工具提供目录、UTF-8 文件行段和会话原文。文件按读取时内容提供,会话按本轮快照读取,不上传完整工程、Pi 原始日志或思考。
- 本地工具只交给老师主线程:云端持久中断完整批次,Main 主动回传配对结果,关联 Run 续接同一问题。用户于 2026-09-27 授权改造读取工具:声明 read_protocol=2 的客户端支持按层目录与连续原文分页,每问题最多 12 批、成功结果累计 64 KiB、单页 8 KiB,剩余不足 512 字节时收尾;旧客户端保持六批。最后结果消费后显式禁止继续选工具并要求基于已有证据回答。保留原付款人、版本、上下文和截止时间;读取限当前项目,允许 .makelore/project.json,排除其余内部记录与 Git 数据,不提供本地写入或命令。
- Works Square 验证学生编程资格,使用专用短期老师凭据;聊天模型费用记入学生 ai_programming / coding_teacher 账本。老师资源仍归 Yuxi 创建者,个人 Agents 模块继续创建者付费,老师问题不占创建者个人智能体金额上限。
@@ -35,6 +38,8 @@
## Evidence And Release Boundary
- 2026-09-29 目录刷新源通过 208 项相关单测、类型/lint/Vite 构建及独立审查;合并保持产品与测试源一致。WS 先升级 `20260929_0099` 和 revision/发布接口,再更新 Yuxi 与客户端;无本次客户端数据迁移。以下旧发布记录为历史,不代表当前 WS migration head。见[本次集成](../30-worklog/tasks/20260929-integrate-teacher-ml-a19f72c4.md)。
- 2026-09-28 组件移除源 317def79129503062d15076585881ceae753055c 无冲突快进合入本地 main。47 项相关单测、12 项浏览器布局、1 项 Electron 咨询场景及类型/lint/构建通过;产品与测试保持源字节,沿用其验证。没有服务端或历史数据迁移;未推送、打包或更新安装版。见[源任务](../30-worklog/tasks/20260928-remove-teacher-cards-ef1cf59c.md)和[集成记录](../30-worklog/tasks/20260928-merge-teacher-cards-a09c07f3.md)。
- 2026-09-28 项目隔离修复源 `4b41c23a3a5e9da1a329b1b69930787a670163de` 已合入本地 main,纠正上述原跨项目假设。382 项相关测试、标准 typecheck、变更文件 lint、Vite 构建和 3 项 Electron 场景通过,覆盖同一智能体的双项目历史/草稿切换、升级导入及目录刷新;扩展 Main 仍有 66 项既有诊断。合并保持产品/测试与已验证源一致,沿用源证据;没有更新安装版或验收线上模型。见[修复源记录](../30-worklog/tasks/20260928-agent-project-chat-6644c06d.md)与[续接集成记录](../30-worklog/tasks/20260928-merge-agent-single-chat-9911c6df.md#project-isolation-correction-resume)。
@@ -1,5 +1,7 @@
# Decision Index
- 2026-09-29 修订[下发智能体咨询决定](ADR-2026-09-22-coding-teacher.md):Yuxi 一次发布保持稳定老师身份;客户端五秒前台 revision 检查、入场/焦点刷新独立于聊天状态,保留项目连续聊天和运行快照。
- 2026-09-26 已确认[老师 Agent 产品定位](../00-brief/project-positioning.md#老师-agent-的产品定位):某一领域杰出人物的能力转化而成的智能体,供平台学生使用;蒸馏属于实现方法,以真实案例验证领域能力;补充下方既有咨询技术决定。
- [下发智能体咨询与项目会话导航](ADR-2026-09-22-coding-teacher.md):2026-09-28 amended / implemented;按用户纠正,唯一持续聊天身份为账号+项目+下发智能体,不同项目隔离历史、草稿、已读与模型上下文,保留同项目无新话题入口的交互。Pi 来源/发布版本在每轮接受时冻结,同项目变化只轮转内部线程。旧全局聊天按每轮可证明的项目分入对应会话,保留原文件并开启新云端执行段;不明归属仅保留原档案。学生计费、统一只读工具和主线程正文保持;老师对话使用 reply-v1 正文与 0–3 条可选快捷回复,退役组件仅留原档案;本地恢复完整旧正文,无法完整恢复时显式重试且保留草稿;主动观察建议接入由独立任务继续并绑定对应项目。
@@ -5,9 +5,9 @@
智能体图标跟随发布定义:Yuxi 将原生 `icon` 固定到发布快照,Works Square 转为公开地址 `icon_url` 下发。Makelore 顶栏、邀请、面板标题、欢迎区和消息行统一读取该字段;缺失或加载失败时回退到既有 `avatar_id`。每个新问题取当前发布定义,已接受问题保持原版本;自定义图标使用正常图像渲染,内置像素头像保持像素样式。
详见[图标集成记录](../30-worklog/tasks/20260926-merge-agent-avatar-ml-92dc837e.md)。
Code 咨询由 Works Square 运营下发的 Yuxi 智能体执行,不内置老师/朋友分类。Main 按账号/项目/稳定智能体 ID 保存唯一持续聊天,Renderer 按同一身份保存文字草稿、分页缓存及阅读位置。顶栏直接选择智能体,恢复当前项目的历史/空态与未读;没有新话题加号或话题下拉框。每次接受问题固定当前项目、Pi 公开消息及发布版本;同项目 Pi 来源或版本变化只轮转内部 Yuxi 线程,带入该项目有预算的公开近期交流。切换项目恢复另一个聊天,历史与云端 checkpoint 不跨项目复用。Main 执行三个只读工具并回传结果;学生支付咨询模型费用,个人 Agents 保持创建者付费。正文只接收云端主线程文本,其他线程事件仍推进游标。Code 默认 Agent、项目下操作会话及 Pi 执行保持项目边界。
Code 咨询由经 Works Square 下发的 Yuxi 智能体执行,不内置老师/朋友分类。Main 按账号/项目/稳定智能体 ID 保存唯一持续聊天,Renderer 按同一身份保存文字草稿、分页缓存及阅读位置。顶栏直接选择智能体,恢复当前项目的历史/空态与未读;没有新话题加号或话题下拉框。每次接受问题固定当前项目、Pi 公开消息及发布版本;同项目 Pi 来源或版本变化只轮转内部 Yuxi 线程,带入该项目有预算的公开近期交流。切换项目恢复另一个聊天,历史与云端 checkpoint 不跨项目复用。Main 执行三个只读工具并回传结果;学生支付咨询模型费用,个人 Agents 保持创建者付费。正文只接收云端主线程文本,其他线程事件仍推进游标。Code 默认 Agent、项目下操作会话及 Pi 执行保持项目边界。
顶栏与打开的咨询栏共享 useTeacherCompanion 提供的只读目录/配置刷新结果;刷新入口独立于旧跟进条件,保持账号/项目/智能体聊天及草稿身份。当前联系人资料可更新,执行版本仍由 Main 在每轮接受时冻结;介绍卡和真实气泡共同参与原生作品预览避让。
Yuxi 一次显式发布经 WS 登记更新稳定老师身份,运营保留默认与启停。顶栏与打开的咨询栏共享 useTeacherCompanion 提供的只读目录/配置刷新结果;前台每五秒读取小型目录 revision,进入模块及回到前台补查,变化才重新读取完整配置;后台停止轮询。刷新独立于聊天、输入和正在回答,不触发模型。Main 每次接受新问题仍读取当前版本,运行中的问题固定原版本,同项目可见聊天保持连续。 刷新入口独立于旧跟进条件,保持账号/项目/智能体聊天及草稿身份。当前联系人资料可更新,执行版本仍由 Main 在每轮接受时冻结;介绍卡和真实气泡共同参与原生作品预览避让。
TeacherConversationStore 在 userData/agent-conversations/<account>/<agent>/projects/<projectId> 保存轻量索引和独立轮文件,每页50轮。Host API /api/coding/projects/:projectId/agent-conversations/:agentId 绑定历史、发送、SSE、取消、讨论、保存和已读;接受锁、缓存与订阅也按项目隔离。SSE只发送当前轮与元数据,Renderer 按请求身份合并分页和流,排除另一项目的迟到响应。Main 持久保存已读,阅读锚点只缓存在当前窗口。旧全局轮按 request.projectId 或 origin.projectId 分入项目,保留原请求身份、映射已读、复制对应讨论,强制新云端执行段;中断后幂等续接且不修改原文件,无项目归属的轮只保留原档案。更早项目话题仍按账号、config_id和来源身份导入去重。旧全局草稿仅由记录的项目接纳,原记录保留。引用与带回操作核对原项目/Pi 来源。新下发聊天停止旧Renderer定时跟进;独立主动观察任务尚未纳入,未来建议须幂等投递相应项目的同一聊天。详见[已接受决定](../10-decisions/ADR-2026-09-22-coding-teacher.md)。
@@ -4,6 +4,8 @@ This file is the integrated default-branch snapshot. Feature tasks record progre
## Integrated Through
- 2026-09-29:用户确认“一次发布,自动更新 Makelore”并要求合并。源 `913ccad5774df9a8e1f8259394658a524f8255d4` 无冲突快进合入本地 `main`。Yuxi 管理员在可管理的原生智能体编辑页一次“保存并发布”即可更新 Makelore;普通保存只改草稿。Yuxi 固定不可变快照并携同一操作身份交给 WS,只有 WS 登记提交后才报成功,失败重试不发布后来编辑的草稿。WS 按来源绑定稳定老师 ID,首次复用唯一启用旧项,歧义需显式选择;保留默认、停用和历史,不自动合并旧身份,重新启用需要显式动作。 前台每五秒读取小型目录 revision,进入模块及回到前台补查,变化才重新读取完整配置;后台停止轮询。刷新独立于聊天、输入和正在回答,不触发模型。Main 每次接受新问题仍读取当前版本,运行中的问题固定原版本,同项目可见聊天保持连续。 独立 Reviewer 发现的历史字符串 ID `coding-teacher` 被 UUID 约束拒绝问题已修复并复核通过。WS 33 项(含真实 PostgreSQL 迁移/并发)、Yuxi 2260 后端单测/1 跳过及发布 HTTP/PG、worker/Redis、浏览器和前端构建,MakeLore 208 项及类型/lint/构建通过;具体分批证据见源记录。必须先部署 WS `20260929_0099`/API,再更新 Yuxi 和客户端;本次仅本地合并,未推送、部署或测量生产延迟。原有跨仓共 7 份外来文档保留未提交。此条更新旧运营必经和旧话题固定版本的发布说明,下方日期条目保留为历史。见[源记录](tasks/20260929-teacher-publish-ml-e6b32168.md)、[集成记录](tasks/20260929-integrate-teacher-ml-a19f72c4.md)。
- 2026-09-29:用户明确选择与其他官方插件相同的“客户端内置,默认安装”。已合入源 `97c3e45`,合并提交 `7a42de875bacb79fe6d660194b58f22c7d081830`。MakeLore 版本更新为 `2.0.2`,内置“对孩子说清楚”`1.0.1`,原 Skill 文件字节不变;登录/刷新时默认领取,无插件 ZIP 下载或单独签名。手动移除保持,项目启用后对操作 Agent 生效,无需另行分配 Skill。配套服务端 `f15cc46f` / 迁移 0098 创建新 bundled Release,保留旧 Artifact 和账号记录,并阻止不含 Skill 的旧客户端使用新版。113 项相关单测、类型检查、lint、构建和 1 项 Electron 场景通过;源产品与主目录一致,3 份外来文档原样保留。未推送、打包、发布或更新安装版,需两端发布。本条取代下方该插件从服务端下载并验签的历史指导;其他 Artifact 插件验签不变。见[源任务](tasks/20260928-default-child-replies-0108dd01.md)及[集成记录](tasks/20260928-merge-default-child-b4d62e81.md#bundled-delivery-resume)。
- 2026-09-28:经用户确认主目录交接,将源 `e70b6eab91b71c6149a96fa9cd5974d8706417ba` 从 `efff238a` 快进合入本地 `main`。“对孩子说清楚”在会话激活和插件列表刷新时通过既有签名市场流程自动领取并安装,已装复用、失败后续重试、账号隔离;明确移除或卸载后保留退出意愿。安装完成显示在已安装列表,项目启用与 Agent 绑定仍由原控制管理。配套服务端源 `c9ff8a4f` 修复线上已上架却未签名的 Release,并禁止未就绪包上架。产品/测试与已验证源一致,沿用 70 项相关单测、标准类型检查、lint、Vite 构建和 1 项 Electron 场景。三份外来未跟踪文档原样保留;未推送、打包、部署或更新安装版,需要同时发布客户端与服务端才能生效。见[源记录](tasks/20260928-default-child-replies-0108dd01.md)和[集成记录](tasks/20260928-merge-default-child-b4d62e81.md)。
@@ -0,0 +1,44 @@
# Task: Integrate foreground teacher catalog freshness
## Identity
- Task ID: 20260929-integrate-teacher-ml-a19f72c4
- Mode: Integration
- Branch: main
- Worktree: D:\Datas\OthersProjects\makelore
- Base commit: 2767a1501759735684d1509a34c43eb2303784cf
- Owner: codex
- Status: Ready for Integration
## Scope
- Integrate the approved direct teacher publication and catalog freshness source into local main; reconcile the teacher publication contract in canonical memory.
## Intent And Constraints
- User explicitly authorized merging and the three primary-checkout ownership handoffs. Official release --force released the previous completed owner; start --mode integration --adopt-existing and status confirm this Identity, exclusive integration lock and the exact seven preserved foreign document states across repositories.
- Concurrent and Planning Gates passed. Read own/source records, entry and required canonical context; reuse peer Scope/Intent/Promotion assessments from the same unchanged baselines. Historical placeholder records remain unknown coordination state, with no concrete dependency blocking this merge. Adjacent observer work is not imported.
- User-approved explicit publish supersedes operations-only publication. Stable teacher identity, paused/default state, historical versions and per-question execution remain. Source task records remain read-only after import; no push, deployment, production data mutation or cleanup authorization inferred.
- One fresh independent Reviewer Agent is authorized solely for this pre-merge review. Commit only after addressing its findings. Merge exact reviewed source, verify ancestry/product equality, promote accepted facts and pass documentation gate.
## Outcome
- Fast-forwarded local main without conflicts to reviewed source 913ccad5774df9a8e1f8259394658a524f8255d4. Canonical teacher decision/index, architecture/domain and current-state now record explicit native publication, stable identity and independent catalog revision refresh. Source task/supporting records remain unchanged.
- One fresh authorized Reviewer found the real legacy coding-teacher ID/UUID mismatch; both service contracts were corrected to the stored string-ID format. Negative HTTP regressions reproduced 422 before repair, then passed; the same Reviewer confirmed the blocker closed with no remaining actionable issue.
- Existing seven foreign documents across the primary checkouts remain adopted and excluded from commits. No push, deployment, production data operation or installer update.
## Verification
- Source evidence: WS 33 tests including actual PostgreSQL migration/concurrency; Yuxi 2260 full non-slow unit passed / 1 skipped in WSL Python 3.13.13, actual HTTP/PostgreSQL/Redis integration, browser editor, web 319 unit/lint/build, docs and engineering guards; MakeLore 208 focused unit/type/lint/build.
- The prior Python 3.12 system test-package collision is resolved by the actual Python 3.13 full-suite rerun; no unrelated product edits.
## Follow-ups
- Deployment order after separate authorization: WS migration 0099/API, Yuxi, MakeLore client. Local tests do not establish production latency or installed-client behavior.
- Post-merge cleanup offered for 20260929-teacher-publish-ml-e6b32168 at D:/Datas/OthersProjects/.codex-worktrees/makelore/20260929-teacher-publish-ml-e6b32168, branch codex/20260929-teacher-publish-ml-e6b32168-teacher-publish. Source checkout is clean/managed/ready and its latest commit is included in the actual main branch. Awaiting explicit confirmation; retain the checkout, branch and commits in the meantime. Primary checkout is not a cleanup candidate.
## Promotion Candidates
- Promoted user-approved explicit native publication, downstream commit success boundary, stable source identity and independent five-second foreground revision refresh to canonical teacher memory. Integrated Through identifies the exact source commit; old dated entries remain historical. This resolves the known operations-only and old-topic wording, with no new conflicting product choice.
- Merge verification: actual main/source ancestry and equality of product, tests, public documentation and source task records are checked against the reviewed commit. No repeated product regression is needed for a byte-identical merge plus canonical notes. Task-aware drift verifies imported-source provenance and preservation of all adopted foreign document states; git diff --check validates whitespace.
@@ -0,0 +1,47 @@
# Task: Investigate teacher catalog refresh and pinned topic configuration
## Identity
- Task ID: 20260929-teacher-publish-ml-e6b32168
- Mode: Feature
- Branch: codex/20260929-teacher-publish-ml-e6b32168-teacher-publish
- Worktree: D:\Datas\OthersProjects\.codex-worktrees\makelore\20260929-teacher-publish-ml-e6b32168
- Base commit: 2767a1501759735684d1509a34c43eb2303784cf
- Owner: codex
- Status: Ready for Integration
## Scope
- Implemented independent teacher catalog refresh, small revision read through Renderer/Main/WS and normal passing regressions. Converted the earlier opt-in failing diagnostic into tests/unit/teacher-catalog-refresh.test.tsx; removed its diagnostic config.
## Intent And Constraints
- User approved implementation after choosing one explicit publish. The maintain-project-docs Concurrent and Planning Gates passed on resume at the recorded unchanged base/branch/worktree; original context and peer scope assessments remain applicable. All changes are isolated to this owned feature worktree. Preserve history, default/pause state, student billing and running-question versions. One fresh read-only Reviewer Agent is explicitly authorized for this pre-merge review only. No destructive data merge, production writes or canonical project-memory edits.
## Outcome
- Entry still loads the full catalog. Focus/visibility and five-second foreground checks read only the catalog revision, fetching full definitions when it changes. Reads are deduplicated and detached from chat, composing, running requests and legacy check-in conditions; hidden/unmounted scopes do not poll. The revision request uses the current account binding without an extra profile read and rejects responses after account changes. Current Main per-question authoritative catalog lookup and stored/running question snapshots remain intact.
## Verification
- Vitest focused six-file set (teacher-catalog-revision, teacher-catalog-refresh, teacher-companion, teacher-conversation, coding-teacher, coding-teacher-ui): 208 passed. Covered empty/never-started/running/completed states, five-second update, hidden pause, focus, unchanged catalogs, offline retry, unmount, account switch and existing conversation/history/project isolation. TypeScript noEmit, changed-file ESLint and build:vite passed. Existing shared node_modules was reused through a worktree-local junction; dependencies were not changed. Paired WS revision/registration tests passed and Yuxi actual editor verified. git diff --check passed.
## Follow-ups
- No commit/merge/client distribution. Deploy WS revision endpoint before installing this client, following the server/Yuxi/client order. Actual installed client and proxy latency remain unmeasured; five seconds is a foreground check interval, not a hard network SLA. Paired Yuxi full backend unit passed in the Python 3.13 runtime; the authorized fresh independent pre-commit review completed.
## Promotion Candidates
- Target: canonical teacher freshness/current-state and frontend flow documentation. Propose independent foreground revision polling with entry/focus refresh, preserving per-project continuous visible chat and per-question fixed execution. Evidence: previously failing actual-hook diagnosis now green plus 208 focused tests/build. Impact: newly delivered or updated teachers appear without waiting for chat lifecycle. Conflict: diagnostic-only status is superseded; no canonical files changed. Human confirmation: user-approved, promote only via Integration Gate.
## Merge Preparation
- User requested local main-branch integration and explicitly approved the three primary-checkout ownership handoffs, preserving seven foreign documents. Same-task feature check/start/status and Planning Gate passed at unchanged bases. Independent review and its bounded repair recheck passed; ready for the source commit.
- Yuxi full non-slow backend suite in existing WSL Python 3.13.13 passed: 2260 passed / 1 skipped. The previous Python 3.12 test-package shadowing limitation is superseded by this actual rerun; no unrelated code changed.
## Independent Review Repair
- The user-authorized fresh Reviewer found one actual compatibility defect: historic teacher ID coding-teacher was rejected by UUID-only publication schemas. WS native registration and legacy operations publication, plus Yuxi native publish, now accept string IDs of length 1..40 matching the database.
- Negative regressions reproduced HTTP 422 uuid_parsing in both repositories before the repair. WS actual HTTP now verifies choosing the legacy ID preserves default/pause, binding and historical versions; the complete relevant set including real PostgreSQL passed 33 cases. Yuxi actual HTTP/PostgreSQL UUID and legacy-ID cases passed both parameterized cases. Changed-file Ruff and Yuxi format checks passed. The same authorized Reviewer independently rechecked the bounded repair and confirmed the blocker closed, with no remaining actionable issue.
- Final pre-commit review: fresh agent /root/teacher_merge_review inspected all three source diffs and untracked code/tests, found the legacy-ID defect, and confirmed its repair on 2026-09-29. Yuxi engineering verifier and all 62 verifier tests passed again after repair. Task documentation and whitespace checks complete the durable source handoff.
+3 -1
View File
@@ -2,11 +2,13 @@
## Project conversations and distributed agents
- 前台每五秒读取小型目录 revision,进入模块及回到前台补查,变化才重新读取完整配置;后台停止轮询。刷新独立于聊天、输入和正在回答,不触发模型。Main 每次接受新问题仍读取当前版本,运行中的问题固定原版本,同项目可见聊天保持连续。
- 每个新项目创建内部默认 Agent,普通界面无 Agent 创建步骤或分组列。defaultAgentId 选取已存默认、置顶或首个可用 Agent;历史 Agent 与会话绑定不重写,不重新启用停用/归档 Agent。
- 项目直接显示会话并保留归档、改名、未读、运行状态;打开项目不创建空会话。首次发送或显式新建才创建。项目设置编辑默认配置,历史会话仍能定位原绑定配置。
- 每个账号/项目/稳定下发智能体ID只有一个可见聊天,不同项目隔离历史、草稿、已读及模型上下文。每个问题使用接受时的当前发布版本,并冻结项目、Pi来源及公开消息;同项目相同作用域复用内部线程,来源或版本变化只开启内部执行段。切换项目恢复独立聊天,不复用其他项目checkpoint或历史。Main三个只读工具仍限本轮项目,允许 `.makelore/project.json`,无本机写入或命令;每次读取与续接检查绑定及取消状态。完整工程不全量上传。
- 同requestId相同输入读取当前项目聊天的已有结果(包括更早历史页),不同输入冲突;同一账号/项目/智能体一次接受一轮人工问答,各项目接受锁独立。SSE、取消、保存和已读均绑定项目,其他项目不能取消本轮。重启将未完成问题标为中断,账号退出中止。带回回答核对原项目/Pi来源,只追加草稿且不自动发送;已归档来源必须先恢复。
- 运营只管理下发、同步、默认与启停,无重复提示词编辑器或试聊。老师聊天模型由学生账号付费,个人 Agents 保持创建者付费。正文、历史及带回草稿只接收云端主线程文本;忽略子线程文本仍推进游标。
- Yuxi 管理员编辑后一次显式发布,经 WS 更新稳定老师身份,普通保存不发布;运营保留下发、同步、默认与启停,无重复提示词编辑器或试聊。老师聊天模型由学生账号付费,个人 Agents 保持创建者付费。正文、历史及带回草稿只接收云端主线程文本;忽略子线程文本仍推进游标。
- 客户端不内置老师/朋友角色:名称、头像、简介、欢迎语、推荐问题及人格由服务端发布定义决定,回复格式协议不定义人格。任意名称的下发智能体都遵守同一只读工具边界;不再按角色限制项目读取。
- 顶栏点击直接恢复当前项目的唯一聊天,没有新话题加号和话题下拉框。历史分页,已读由Main持久保存;草稿与阅读缓存按账号/项目/智能体隔离,引用仍绑定原项目/Pi,迟到响应不得进入另一项目。旧全局聊天依据每轮可证明的项目归属分入对应聊天,映射已读并开启新云端执行段;保留原文件与请求身份,不明归属仅留原档案。旧全局草稿只导入记录的项目,优先于更早话题草稿且保留原值。更早项目话题仍按当前账号、config_id与项目/话题/请求来源去重导入,不按名称猜归属。历史组件状态仅保留在原档案,既不渲染,也不主动导入或发送给模型。旧未确认请求重试保持原请求身份、问题、引用及来源,去除退役组件字段。旧Renderer定时跟进不再向现代聊天派发;独立主动观察后续也须投递到对应项目的同一聊天。
- 输入预算区分近似模型 Token 与精确云端 JSON 字节,编译和提交使用同一完整请求计量。保留当前问题、明确引用和固定指令,按需缩减来源节选;固定配置/讨论超限不误报为用户短问题过长,不静默提升预算或迁移旧话题。
+7 -2
View File
@@ -32,10 +32,11 @@ export async function handleCodingTeacherRoutes(
const role = projectTopics?.[2] === 'friend' ? 'friend' : undefined;
const config = url.pathname === '/api/coding/teacher/config' || url.pathname === '/api/coding/friend/config';
const catalog = url.pathname === '/api/coding/teacher/teachers';
const catalogRevision = url.pathname === '/api/coding/teacher/teachers/revision';
const draft = url.pathname === '/api/coding/teacher-preview';
const pending = url.pathname === '/api/coding/teacher-preview/pending-link';
if (!legacy && !conversation && !source && !preview && !projectTopics && !checkIn && !config && !catalog && !draft && !pending) return false;
if ((config || catalog || draft || pending) && req.method !== 'GET') {
if (!legacy && !conversation && !source && !preview && !projectTopics && !checkIn && !config && !catalog && !catalogRevision && !draft && !pending) return false;
if ((config || catalog || catalogRevision || draft || pending) && req.method !== 'GET') {
sendJson(res, 405, { error: '不支持此操作。' });
return true;
}
@@ -82,6 +83,10 @@ export async function handleCodingTeacherRoutes(
));
return true;
}
if (catalogRevision) {
sendJson(res, 200, await service.catalogRevision());
return true;
}
if (catalog) {
sendJson(res, 200, await service.catalog());
return true;
+5
View File
@@ -73,6 +73,11 @@ export const teacherAvailability = (account: TeacherAccount) =>
teacherCloudRequest<TeacherAvailability>(account, '/api/coding-teacher/config');
export const teacherCatalog = (account: TeacherAccount) =>
teacherCloudRequest<TeacherCatalog>(account, '/api/coding-teacher/teachers');
export async function teacherCatalogRevision(): Promise<{ revision: number }> {
const binding = getWorksSquareAccountBinding();
if (!binding) throw new TeacherError(401, 'teacher_auth_required', '请先登录。');
return teacherCloudRequest({ id: '', binding }, '/api/coding-teacher/teachers/revision');
}
export const teacherVersion = (account: TeacherAccount, version: number) =>
teacherCloudRequest<{ version: number; payload: TeacherDefinition }>(
account,
+5
View File
@@ -24,6 +24,7 @@ import {
teacherVersion,
teacherPreview,
teacherCatalog,
teacherCatalogRevision,
TeacherError,
type TeacherAccount,
} from './config-client';
@@ -103,6 +104,10 @@ export class CodingTeacherService {
async catalog() {
return (this.options.catalog ?? teacherCatalog)(await this.account());
}
async catalogRevision() {
return teacherCatalogRevision();
}
async legacyHistory(projectId: string, selection?: { id: string; sourceId: string; friend: boolean }) {
const account = await this.account();
const project = await this.options.projects.getProject(projectId);
+1
View File
@@ -40,6 +40,7 @@ export interface TeacherAvailability {
revision: number;
}
export interface TeacherCatalog {
revision?: number;
items: Array<{ teacher_id: string; version: number; definition: TeacherDefinition; is_default: boolean }>;
}
export interface TeacherReference {
+1
View File
@@ -37,6 +37,7 @@ export const teacherApi = {
sendConversation: (projectId: string, agentId: string, input: TeacherSend) => hostApiFetch<TeacherTopic>(
teacherConversationsPath(projectId) + '/' + encodeURIComponent(agentId) + '/messages', { method: 'POST', body: JSON.stringify(input) }),
catalog: () => hostApiFetch<TeacherCatalog>('/api/coding/teacher/teachers'),
catalogRevision: () => hostApiFetch<{ revision: number }>('/api/coding/teacher/teachers/revision'),
checkIn: (projectId: string, input: TeacherCheckInInput) =>
hostApiFetch<TeacherCheckInResult>(`/api/coding/projects/${encodeURIComponent(projectId)}/teacher-check-in`, {
method: 'POST',
+17 -5
View File
@@ -141,11 +141,14 @@ export function useTeacherCompanion(options: Options) {
const base = projectId ? teacherTopicsPath(projectId) : '';
type LoadedCatalog = { enabled: boolean; selected?: TeacherCatalog['items'][number]; selectedId?: string };
let configFlight: Promise<LoadedCatalog> | null = null;
let catalogRevision: number | undefined;
let checkingCatalog = false;
const loadConfig = (): Promise<LoadedCatalog> => {
if (configFlight) return configFlight;
configFlight = (async () => {
const [config, catalog] = await Promise.all([teacherApi.config(), teacherApi.catalog()]);
if (!alive || scopeRef.current !== scope) return { enabled: false };
catalogRevision = catalog.revision;
const selection = (current: CompanionState) => {
const selectedId = current.selectedAgent?.teacher_id ?? current.topic?.definition.config_id
?? readLocal<string | undefined>(storageKey + ':selected-agent', undefined);
@@ -215,10 +218,7 @@ export function useTeacherCompanion(options: Options) {
}
// Delivered agents own one chat per project. Project observations
// have their own Main scheduler; this legacy timer must not create topics.
if (currentState.selectedAgent || currentState.agents.length) {
try { await loadConfig(); } catch { /* Keep last known delivered contacts. */ }
return;
}
if (currentState.selectedAgent || currentState.agents.length) return;
if (!projectId || current.sourceBusy || current.sourceArchived || !current.sourceId || (current.teacherOpen && current.teacherComposing)) return;
if (Date.now() - lastAttempt < TEACHER_CHECK_IN_INTERVAL_MS) return;
if (unreadInvitation(currentState)) return;
@@ -252,13 +252,25 @@ export function useTeacherCompanion(options: Options) {
}
};
const timer = window.setInterval(() => { void check(); }, 15_000);
const onVisible = () => { void check(); };
// Catalog freshness is independent of chat/check-in state and never dispatches a model.
const refreshCatalog = async () => {
if (!alive || checkingCatalog || document.visibilityState !== 'visible') return;
checkingCatalog = true;
try {
const current = await teacherApi.catalogRevision();
if (alive && scopeRef.current === scope && current.revision !== catalogRevision) await loadConfig();
} catch { /* Keep the last usable catalog; the next visible tick retries. */ }
finally { checkingCatalog = false; }
};
const catalogTimer = window.setInterval(() => { void refreshCatalog(); }, 5_000);
const onVisible = () => { void refreshCatalog(); void check(); };
window.addEventListener('focus', onVisible);
document.addEventListener('visibilitychange', onVisible);
return () => {
alive = false;
if (configLoader.current === loadConfig) configLoader.current = null;
window.clearInterval(timer);
window.clearInterval(catalogTimer);
window.removeEventListener('focus', onVisible);
document.removeEventListener('visibilitychange', onVisible);
};
@@ -0,0 +1,67 @@
// Actual hook against a changed server catalog, independently of chat activity.
import { act, cleanup, renderHook } from '@testing-library/react';
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
import { useTeacherCompanion } from '@/pages/Chat/use-teacher-companion';
import type { TeacherDefinition, TeacherTopic } from '../../shared/coding-teacher';
const api = vi.hoisted(() => ({ config: vi.fn(), catalog: vi.fn(), catalogRevision: vi.fn(), conversation: vi.fn(), list: vi.fn(), read: vi.fn(), events: vi.fn() }));
vi.mock('@/lib/coding-teacher', () => ({ teacherApi: api, teacherTopicsPath: (id: string) => id, teacherConversationsPath: (id: string) => id }));
vi.mock('@/stores/auth', () => ({ useAuthStore: (select: (state: unknown) => unknown) => select({ user: { userId: 'diagnostic-user' } }) }));
const definition: TeacherDefinition = { schema_version: 1, teacher_id: 'coding-teacher', config_id: 'agent-a', name: 'Old name', description: '', avatar_id: 'avatar-06', welcome_message: '', suggested_questions: [], system_prompt: '', skills: [], model: { model_id: 'model', reasoning_choice: { mode: 'default' } }, limits: { max_input_tokens: 8000, max_output_tokens: 1500 } };
const original = { teacher_id: 'agent-a', version: 1, is_default: true, definition };
const updated = { ...original, version: 2, definition: { ...definition, name: 'New name' } };
beforeEach(() => {
vi.resetAllMocks(); vi.useFakeTimers(); localStorage.clear();
Object.defineProperty(document, 'visibilityState', { configurable: true, value: 'visible' });
api.config.mockResolvedValue({ enabled: true, definition });
api.catalog.mockResolvedValue({ revision: 1, items: [original] });
api.catalogRevision.mockResolvedValue({ revision: 1 });
api.conversation.mockResolvedValue({ topic: null, before: null });
api.list.mockResolvedValue({ items: [], lastSelectedTopicId: null });
api.events.mockResolvedValue(Object.assign(new EventTarget(), { close: vi.fn() }));
});
afterEach(() => { cleanup(); vi.useRealTimers(); });
it.each(['empty-catalog', 'never-started', 'running', 'completed'] as const)('refreshes changed catalog within five seconds with %s chat', async (state) => {
if (state === 'empty-catalog') api.catalog.mockResolvedValue({ revision: 1, items: [] });
if (state === 'running' || state === 'completed') {
const topic: TeacherTopic = { schemaVersion: 1, revision: 1, id: 'topic', accountId: 'diagnostic-user', projectId: 'project', sourceConversationId: 'project', version: 1, definition, createdAt: 'now', updatedAt: 'now', conversation: { agentId: 'agent-a', segmentTurns: 1, discussions: {} }, requests: [{ id: 'request', text: 'Question', references: [], createdAt: 'now', sourceCursor: { workerGeneration: 1, seq: 1 }, sourceCapturedAt: 'now', includedSourceMessageIds: [], omittedMessages: 0, status: state, response: '' }] };
api.conversation.mockResolvedValue({ topic, before: null });
}
let view!: ReturnType<typeof renderHook<ReturnType<typeof useTeacherCompanion>, unknown>>;
await act(async () => { view = renderHook(() => useTeacherCompanion({ projectId: 'project', sourceId: 'source', sourceBusy: false, sourceArchived: false, teacherOpen: false, teacherComposing: false })); });
expect(view.result.current.selectedAgent?.version).toBe(state === 'empty-catalog' ? undefined : 1);
api.catalog.mockResolvedValue({ revision: 2, items: [updated] });
api.catalogRevision.mockResolvedValue({ revision: 2 });
await act(async () => { await vi.advanceTimersByTimeAsync(5_000); });
expect(view.result.current.selectedAgent?.version).toBe(2);
if (state === 'running') {
expect(view.result.current.topic?.requests[0].status).toBe('running');
expect(view.result.current.topic?.version).toBe(1);
}
});
it('keeps unchanged catalogs, pauses while hidden, retries failures and refreshes immediately on focus', async () => {
let view!: ReturnType<typeof renderHook<ReturnType<typeof useTeacherCompanion>, unknown>>;
await act(async () => {
view = renderHook(() => useTeacherCompanion({ projectId: 'project', sourceId: 'source', sourceBusy: true, sourceArchived: false, teacherOpen: true, teacherComposing: true }));
});
await act(async () => { await vi.advanceTimersByTimeAsync(10_000); });
expect(api.catalog).toHaveBeenCalledTimes(1);
expect(api.catalogRevision).toHaveBeenCalledTimes(2);
Object.defineProperty(document, 'visibilityState', { configurable: true, value: 'hidden' });
await act(async () => { await vi.advanceTimersByTimeAsync(10_000); });
expect(api.catalogRevision).toHaveBeenCalledTimes(2);
Object.defineProperty(document, 'visibilityState', { configurable: true, value: 'visible' });
api.catalogRevision.mockRejectedValueOnce(new Error('offline'));
await act(async () => { window.dispatchEvent(new Event('focus')); });
expect(view.result.current.selectedAgent?.version).toBe(1);
api.catalogRevision.mockResolvedValue({ revision: 2 });
api.catalog.mockResolvedValue({ revision: 2, items: [updated] });
await act(async () => { window.dispatchEvent(new Event('focus')); });
expect(view.result.current.selectedAgent?.version).toBe(2);
view.unmount();
const calls = api.catalogRevision.mock.calls.length;
await act(async () => { await vi.advanceTimersByTimeAsync(5_000); window.dispatchEvent(new Event('focus')); });
expect(api.catalogRevision).toHaveBeenCalledTimes(calls);
});
@@ -0,0 +1,34 @@
import { beforeEach, expect, it, vi } from 'vitest';
const mock = vi.hoisted(() => ({
binding: vi.fn(), current: vi.fn(), token: vi.fn(), fetch: vi.fn(),
}));
vi.mock('../../electron/services/works-square-session', () => ({
getWorksSquareAccountBinding: mock.binding,
isCurrentWorksSquareAccountBinding: mock.current,
getValidWorksSquareAccessToken: mock.token,
}));
vi.mock('../../electron/utils/proxy-fetch', () => ({ proxyAwareFetch: mock.fetch }));
import { teacherCatalogRevision } from '../../electron/coding-teacher/config-client';
beforeEach(() => {
vi.resetAllMocks();
mock.binding.mockReturnValue({ accountId: 'student', generation: 1 });
mock.current.mockReturnValue(true);
mock.token.mockResolvedValue('student-token');
mock.fetch.mockResolvedValue(new Response(JSON.stringify({ revision: 12 })));
});
it('reads only the small directory revision with current account authentication', async () => {
expect(await teacherCatalogRevision()).toEqual({ revision: 12 });
expect(mock.fetch).toHaveBeenCalledTimes(1);
expect(mock.fetch.mock.calls[0][0]).toMatch(/\/api\/coding-teacher\/teachers\/revision$/);
expect(mock.fetch.mock.calls[0][1].headers.Authorization).toBe('Bearer student-token');
});
it('discards a response when the account changes while waiting', async () => {
mock.fetch.mockImplementation(async () => {
mock.current.mockReturnValue(false);
return new Response(JSON.stringify({ revision: 99 }));
});
await expect(teacherCatalogRevision()).rejects.toMatchObject({ code: 'teacher_account_changed' });
});