diff --git a/backend/app/routes/main_agent.py b/backend/app/routes/main_agent.py index 38794f4d..90931312 100644 --- a/backend/app/routes/main_agent.py +++ b/backend/app/routes/main_agent.py @@ -945,12 +945,15 @@ async def run_and_record() -> dict[str, Any]: assistant_message = str(result.get("assistant_message") or "").strip() completed_conversation = conversation if assistant_message: + # Re-read: steering messages and parked sibling Runs may have + # appended to this conversation while the Run was executing. + latest_conversation = get_conversation(conversation["id"]) or conversation completed_conversation = await _ui_operation_outputs( "save_harness_conversation", { "conversation_id": conversation["id"], "messages": [ - *conversation["messages"], + *(latest_conversation.get("messages") or []), {"role": "assistant", "content": assistant_message}, ], }, @@ -1245,6 +1248,42 @@ async def resume_runtime_run(run_id: str) -> dict[str, Any]: raise HTTPException(status_code=400, detail=safe_error_message(exc)) +class RuntimeSteerBody(BaseModel): + message: str = Field(min_length=1, max_length=4000) + + +@runtime_router.post("/runtime/runs/{run_id}/steer") +async def steer_runtime_run(run_id: str, body: RuntimeSteerBody) -> dict[str, Any]: + """Deliver a mid-run user message to the live Run (D-1: input never locks). + + ``not_running`` means the Run is not executing a prompt; the client then + sends the text as a normal follow-up turn. Nothing here approves anything. + """ + try: + provider = await _provider_for_run(run_id) + steer = getattr(provider, "steer_run", None) + if steer is None: + return {"ok": True, "disposition": "not_running"} + result = await steer(run_id, message=body.message) + except ValueError as exc: + raise HTTPException(status_code=404, detail=safe_error_message(exc)) + if result.get("disposition") == "steered": + conversation_id = str((result.get("run") or {}).get("conversation_id") or "") + conversation = get_conversation(conversation_id) if conversation_id else None + if conversation is not None: + await _ui_operation_outputs( + "save_harness_conversation", + { + "conversation_id": conversation_id, + "messages": [ + *(conversation.get("messages") or []), + {"role": "user", "content": body.message}, + ], + }, + ) + return {"ok": True, "disposition": result.get("disposition") or "not_running"} + + @runtime_router.post("/runtime/runs/{run_id}/abort") async def abort_runtime_run(run_id: str) -> dict[str, Any]: try: diff --git a/backend/app/services/agent_runtime.py b/backend/app/services/agent_runtime.py index f0898bd3..3ff0e63a 100644 --- a/backend/app/services/agent_runtime.py +++ b/backend/app/services/agent_runtime.py @@ -89,6 +89,7 @@ "run.waiting_confirmation": "approval.requested", "input.required": "approval.requested", "input.answered": "approval.resolved", + "input.steered": "reasoning.status", "run.waiting_input": "approval.requested", "continuation.requested": "reasoning.status", "approval.requested": "approval.requested", @@ -294,6 +295,11 @@ async def abort_run(self, run_id: str) -> dict[str, Any]: return await abort_embedded_agent_run(run_id) + async def steer_run(self, run_id: str, *, message: str) -> dict[str, Any]: + from app.services.embedded_agent_host import steer_embedded_agent_run + + return await steer_embedded_agent_run(run_id, message) + class ReplayAgentRunProvider: """Durable, deterministic Main Agent provider for local acceptance paths.""" diff --git a/backend/app/services/embedded_agent_host.py b/backend/app/services/embedded_agent_host.py index fd5e8e8d..f2636ca2 100644 --- a/backend/app/services/embedded_agent_host.py +++ b/backend/app/services/embedded_agent_host.py @@ -45,6 +45,7 @@ from app.services.embedded_agent_worker import ( PROTOCOL_VERSION, EmbeddedAgentWorker, + EmbeddedAgentWorkerError, get_embedded_agent_worker, ) from app.services.security_redaction import safe_error_message @@ -991,6 +992,16 @@ async def _request_user_input(args: dict[str, Any]) -> dict[str, Any]: await publish("run.completed", {"status": "completed"}) await active_worker.dispose_run(run_id) current = await load_agent_run(run_id) or current + if current["status"] in _PARKED_WAITING_STATUSES and active_worker.active_run_id == run_id: + # D-1: a Run that waits on the user must not hold the single + # embedded worker. Its session file stays on disk; approvals and + # answers already resume through continuations, which restart the + # worker from that file. Parking lets the user keep chatting. + try: + await active_worker.dispose_run(run_id) + except Exception: + pass + current = await load_agent_run(run_id) or current await persist_and_publish( "guardian.reviewed", { @@ -1339,6 +1350,43 @@ async def reject_embedded_agent_action( return result +_PARKED_WAITING_STATUSES = frozenset({"waiting_confirmation", "waiting_input", "waiting_decision"}) + + +async def steer_embedded_agent_run( + run_id: str, + message: str, + *, + worker: EmbeddedAgentWorker | None = None, +) -> dict[str, Any]: + """Inject a user message into a live Run at its next tool boundary. + + Steering is collaboration input, never authorization. When the Run is not + executing a prompt right now (finished, parked or waiting on the user) the + caller gets ``not_running`` and should start a follow-up turn instead. + """ + + text = str(message or "").strip() + if not text: + raise ValueError("引导消息不能为空。") + run = await load_agent_run(run_id) + if run is None: + raise ValueError(f"Agent Run {run_id} 不存在。") + active_worker = worker or get_embedded_agent_worker() + if active_worker.active_run_id != run_id or not active_worker.prompt_active: + return {"ok": True, "disposition": "not_running", "run": run} + try: + await active_worker.steer_run(run_id, text[:4000]) + except EmbeddedAgentWorkerError: + return {"ok": True, "disposition": "not_running", "run": run} + await append_agent_run_event( + run_id, + event_type="input.steered", + payload={"characters": len(text)}, + ) + return {"ok": True, "disposition": "steered", "run": run} + + async def abort_embedded_agent_run( run_id: str, *, diff --git a/backend/app/services/resume_workspace.py b/backend/app/services/resume_workspace.py index a57ed25a..b1a4a9cf 100644 --- a/backend/app/services/resume_workspace.py +++ b/backend/app/services/resume_workspace.py @@ -614,6 +614,18 @@ def _find_section(resume: Resume, row: Optional[dict[str, Any]]) -> Optional[Res ) +def _change_conflicts_with_manual_edit(resume: Resume, diff: dict[str, Any]) -> bool: + """True when the section this change targets no longer matches the proposal's ``before``.""" + change_type = str(diff.get("change_type") or "modified") + before = diff.get("before") if isinstance(diff.get("before"), dict) else None + if change_type == "added" or before is None: + return False + target = _find_section(resume, before) + if target is None: + return True + return _json_hash(target.content_json or []) != _json_hash(before.get("content_json") or []) + + async def review_resume_proposal_item( proposal_id: str, resume_id: int, change_id: str, action: str, edited_text: str = "", ) -> dict[str, Any]: @@ -723,11 +735,19 @@ async def _review_resume_proposal_items( await db.commit() raise ValueError(proposal.review_note) if expected_hash and workspace_content_hash(resume) != expected_hash: - proposal.status = "stale" - proposal.review_note = "用户手动修改后,原提案已过期,请重新生成或重新计算" - proposal.reviewed_at = _now() - await db.commit() - raise ValueError(proposal.review_note) + # 人工修改优先,冲突按段落判断(docs/05 §5):用户改了别处, + # 这条建议仍可采用;只有建议要改的那一段在生成后被用户改过, + # 才拒绝采用这一条,且绝不覆盖用户的版本。整份提案不因此失效。 + conflicted = [ + change_id + for change_id in pending_ids + if _change_conflicts_with_manual_edit(resume, diffs[change_id]) + ] + if conflicted: + raise ValueError( + "这一段在 AI 建议生成后被你改过,建议基于旧内容,不能直接采用;" + "可以跳过,或让 AI 基于你的新版本重新建议" + ) for clean_change_id in pending_ids: diff = diffs[clean_change_id] if clean_action == "accept": diff --git a/backend/tests/test_agent_never_locks.py b/backend/tests/test_agent_never_locks.py new file mode 100644 index 00000000..bc93b616 --- /dev/null +++ b/backend/tests/test_agent_never_locks.py @@ -0,0 +1,103 @@ +"""docs/02 D-1 / D-2: a Run waiting on the user never holds the Agent hostage. + +* A Run that reaches ``waiting_confirmation`` parks: it releases the single + embedded worker so the user can start another turn, while its session file + and plan stay durable for the later approval continuation. +* A mid-run user message is steered into the live Run, or reported as + ``not_running`` so the client sends it as a normal follow-up turn. +""" +from __future__ import annotations + +import asyncio +import os +import sys +import unittest +from pathlib import Path +from typing import Any + +BACKEND_DIR = Path(__file__).resolve().parents[1] +os.chdir(BACKEND_DIR) +if str(BACKEND_DIR) not in sys.path: + sys.path.insert(0, str(BACKEND_DIR)) +TESTS_DIR = Path(__file__).resolve().parent +if str(TESTS_DIR) not in sys.path: + sys.path.insert(0, str(TESTS_DIR)) + +from app.database import init_db +from app.services.agent_run_state import create_agent_run, list_agent_run_events, load_agent_run +from app.services.embedded_agent_host import start_embedded_agent_run, steer_embedded_agent_run +from test_embedded_agent_host import FakeEmbeddedWorker + +PROVIDER = {"name": "fixture", "model": "fixture"} +METADATA = {"runtime": "python_agent", "provider_id": "embedded"} + + +class SteerableWorker: + def __init__(self, run_id: str, *, prompt_active: bool) -> None: + self.active_run_id = run_id + self.prompt_active = prompt_active + self.steered: list[str] = [] + + async def steer_run(self, run_id: str, message: str) -> dict[str, Any]: + assert run_id == self.active_run_id + self.steered.append(message) + return {"run_id": run_id, "disposition": "queued"} + + +class AgentNeverLocksTests(unittest.TestCase): + def test_waiting_confirmation_parks_the_worker_but_keeps_the_run(self) -> None: + worker = FakeEmbeddedWorker() + + async def run() -> tuple[dict[str, Any], str | None, dict[str, Any] | None]: + await init_db() + from app.database import async_session + from app.models.models import Job + async with async_session() as db: + if await db.get(Job, 74291) is None: + db.add(Job(id=74291, title="Synthetic recovery role", company="Fixture", + raw_description="Public synthetic JD", hash_key="host-recovery-job")) + await db.commit() + started = await start_embedded_agent_run( + message="帮我调研这个岗位", + skill_id="company_research", + conversation_id="never-locks-park", + worker=worker, + provider_config=PROVIDER, + provider_metadata=METADATA, + ) + return started, worker.active_run_id, await load_agent_run(started["run"]["id"]) + + started, active_run_id, stored = asyncio.run(run()) + self.assertTrue(started["ok"], started) + self.assertEqual(stored["status"], "waiting_confirmation") + # Parked: the single worker is free for the user's next turn... + self.assertIsNone(active_run_id) + # ...while the Run itself is neither cancelled nor approved. + self.assertTrue(started["run"]["final_result"]["requires_confirmation"]) + + def test_steer_reaches_only_a_live_prompt(self) -> None: + async def run() -> tuple[dict[str, Any], dict[str, Any], list[str], list[str]]: + await init_db() + record = await create_agent_run(conversation_id="never-locks-steer", goal="steer fixture", mode="skill", skill_id="discovery", actions=[]) + run_id = record["id"] + live = SteerableWorker(run_id, prompt_active=True) + steered = await steer_embedded_agent_run(run_id, " 换成只看上海的岗位 ", worker=live) + idle = SteerableWorker(run_id, prompt_active=False) + not_running = await steer_embedded_agent_run(run_id, "这条应作为新一轮", worker=idle) + events = [event["type"] for event in await list_agent_run_events(run_id)] + return steered, not_running, live.steered + idle.steered, events + + steered, not_running, delivered, events = asyncio.run(run()) + self.assertEqual(steered["disposition"], "steered") + self.assertEqual(not_running["disposition"], "not_running") + self.assertEqual(delivered, ["换成只看上海的岗位"]) + self.assertEqual(events.count("input.steered"), 1) + + def test_empty_steer_is_rejected(self) -> None: + with self.assertRaises(ValueError): + asyncio.run(steer_embedded_agent_run("run_0000000000000000", " ")) + + +if __name__ == "__main__": + unittest.main() + diff --git a/backend/tests/test_resume_workspace.py b/backend/tests/test_resume_workspace.py index a4259904..b358cc16 100644 --- a/backend/tests/test_resume_workspace.py +++ b/backend/tests/test_resume_workspace.py @@ -115,13 +115,14 @@ async def run() -> dict: self.assertEqual(len(result["automation_events"]), 1) self.assertEqual(result["automation_events"][0].payload_json["resume_version_id"], result["version"]["id"]) - def test_manual_edit_makes_unreviewed_proposal_stale(self) -> None: - async def run() -> str: + def test_manual_edit_elsewhere_keeps_the_suggestion_acceptable(self) -> None: + """人工修改优先,冲突按段落:改了别处,这条建议仍可采用。""" + async def run() -> tuple[str, str, str]: engine = create_async_engine("sqlite+aiosqlite:///:memory:") sessions = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) async with engine.begin() as connection: await connection.run_sync(Base.metadata.create_all) - fixture = await _seed(sessions, "stale") + fixture = await _seed(sessions, "elsewhere") with patch.object(resume_workspace, "async_session", sessions), patch.object( resume_route_operations, "async_session", sessions ), patch.object( @@ -135,19 +136,68 @@ async def run() -> str: await resume_route_operations.update_resume_record( workspace["resume"]["id"], {"summary": "用户自己的最新修改"} ) - with self.assertRaisesRegex(ValueError, "过期"): + reviewed = await resume_workspace.review_resume_proposal_item( + proposal_id=fixture["proposal_id"], + resume_id=workspace["resume"]["id"], + change_id=fixture["change_id"], + action="accept", + ) + async with sessions() as db: + proposal = await db.get(ResumeOptimizationProposal, fixture["proposal_id"]) + await engine.dispose() + return ( + proposal.status if proposal else "missing", + reviewed["resume"]["summary"], + reviewed["resume"]["sections"][0]["content_json"][0]["description"], + ) + + status, summary, description = asyncio.run(run()) + self.assertNotEqual(status, "stale") + self.assertEqual(summary, "用户自己的最新修改") + self.assertEqual(description, "new evidence") + + def test_manual_edit_of_the_target_section_blocks_only_that_suggestion(self) -> None: + """建议要改的那一段被用户改过:不覆盖用户版本,提案整体也不失效。""" + async def run() -> tuple[str, str]: + engine = create_async_engine("sqlite+aiosqlite:///:memory:") + sessions = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) + async with engine.begin() as connection: + await connection.run_sync(Base.metadata.create_all) + fixture = await _seed(sessions, "target-edit") + with patch.object(resume_workspace, "async_session", sessions), patch.object( + resume_route_operations, "async_session", sessions + ), patch.object( + resume_workspace, + "get_pre_application_state", + new=AsyncMock(return_value={"stage": "resume_proposal_ready"}), + ): + workspace = await resume_workspace.ensure_resume_workspace( + job_id=fixture["job_id"], proposal_id=fixture["proposal_id"] + ) + sections = [dict(item) for item in workspace["resume"]["sections"]] + sections[0]["content_json"] = [{"company": "Example", "description": "用户亲手写的版本"}] + await resume_route_operations.update_resume_record( + workspace["resume"]["id"], {"sections": sections} + ) + with self.assertRaisesRegex(ValueError, "改过"): await resume_workspace.review_resume_proposal_item( proposal_id=fixture["proposal_id"], resume_id=workspace["resume"]["id"], change_id=fixture["change_id"], action="accept", ) + current = await resume_workspace.get_resume_workspace(workspace["resume"]["id"]) async with sessions() as db: proposal = await db.get(ResumeOptimizationProposal, fixture["proposal_id"]) await engine.dispose() - return proposal.status if proposal else "missing" + return ( + proposal.status if proposal else "missing", + current["resume"]["sections"][0]["content_json"][0]["description"], + ) - self.assertEqual(asyncio.run(run()), "stale") + status, description = asyncio.run(run()) + self.assertNotEqual(status, "stale") + self.assertEqual(description, "用户亲手写的版本") def test_edit_then_accept_replaces_only_suggested_text(self) -> None: async def run() -> str: @@ -351,11 +401,13 @@ async def run() -> None: async with sessions() as db: proposal = await db.get(ResumeOptimizationProposal, fixture["proposal_id"]) proposal.diff_json = [*proposal.diff_json, {**proposal.diff_json[0], "change_id": "other"}] - resume = await db.get(Resume, workspace["resume"]["id"]) - resume.summary = "manual edit" + from app.models.models import ResumeSection + section = (await db.execute(select(ResumeSection).where(ResumeSection.resume_id == workspace["resume"]["id"]))).scalars().first() + section.content_json = [{"company": "Example", "description": "manual edit"}] await db.commit() await resume_workspace.review_resume_proposal_items(fixture["proposal_id"], workspace["resume"]["id"], ["other"], "reject") - with self.assertRaisesRegex(ValueError, "过期"): + # A reject must not launder a manual edit of the target section. + with self.assertRaisesRegex(ValueError, "改过"): await resume_workspace.review_resume_proposal_items(fixture["proposal_id"], workspace["resume"]["id"], [fixture["change_id"]], "accept") await engine.dispose() asyncio.run(run()) diff --git a/docs/02-interaction-design.md b/docs/02-interaction-design.md index e67e3631..4db0cb3a 100644 --- a/docs/02-interaction-design.md +++ b/docs/02-interaction-design.md @@ -153,10 +153,10 @@ Ask 支持 `selection_mode: single | multiple`、推荐项、可跳过、自由 ## 11. 已知交互缺陷(待修复,按优先级) +> 2026-10 已修复:D-1(等待中的 Run 不再禁用输入、能力选择和快捷动作)与 D-2(新建 / 切换对话不再 abort Run,运行中的流只分离到后台)。实现见 §11.1。 + | 优先级 | 位置 | 问题 | 违反 | | --- | --- | --- | --- | -| P0 | `frontend/src/components/workbench/AgentPanel.tsx`,约 1161、1189 行 | 存在 pending action、plan review 或 interrupted run 时,能力选择器和全部快捷动作被 `disabled` | D-1 | -| P0 | 同文件 `startNewConversation` / `loadConversation`,约 551–600 行 | 有待办时调用 `agentRuntimeApi.abort(activeRunId)`,新建对话会杀掉任务 | D-2 | | P1 | `AgentAskPanel.tsx` | 单选问题被渲染成复选框 | §4.2 | | P1 | `AgentPanel.tsx` 中的 `QUICK_ACTIONS` | 写死了校招动作 | H-1 | | P1 | `components/layout/Sidebar.tsx` | 11 个入口、中英混排、三个简历入口 | N-1、N-2 | @@ -168,6 +168,19 @@ Ask 支持 `selection_mode: single | multiple`、推荐项、可跳过、自由 修复顺序:先修两个 P0 死锁,再修 P1 的策略和导航问题,最后做 P2 的拆分。每修一项都要补上对应规则的测试。 +### 11.1 D-1 / D-2 的实现方式(参考 Codex steer/queue 与 Claude Code agent view) + +| 用户动作 | 当前 Run 状态 | 行为 | +| --- | --- | --- | +| 发消息 | 正在执行 | `POST /api/agent/runtime/runs/{id}/steer`:在下一个工具边界注入当前 Run,气泡标注「已交给正在进行的任务」;返回 `not_running` 时进入待发送队列 | +| 发消息 | 等审核 / 等回答 / 已中断 | 立即开新一轮;原 Run 收进「停放」条(「之前的任务等你审核,状态已保存 · 回去处理」),不取消、不批准 | +| 发消息 | 有审核提交在途 / 后台流未结束 | 进入待发送队列;队列可见、可改、可删,空闲后按顺序发送 | +| 新建 / 切换对话 | 任意 | 只把当前流分离到后台,不调用 abort;等待中的 Run 在历史对话和统一审核入口继续可见 | + +后端配合:Run 进入 `waiting_confirmation / waiting_input / waiting_decision` 时释放(停放)唯一的内置 Worker,会话文件保留;审批和回答本来就经由 continuation 从会话文件恢复,所以停放不改变授权路径。测试:`backend/tests/test_agent_never_locks.py`、`frontend/src/components/workbench/AgentPanel.test.tsx`「Agent input never locks」。 + +已知限制:整页刷新或关闭应用仍会断开 SSE,后端会把执行中的 Run 标记为 `interrupted`(可恢复,不会丢失);彻底解决需要把 Run 执行与 SSE 连接解耦,留给 AG-UI interrupt 方向(§10)。 + ## 12. 验收指标(单个真实岗位的完整旅程) - 任务进行中,聊天输入始终可用;导航从不隐式中止 Run; diff --git a/docs/05-module-resume.md b/docs/05-module-resume.md index 9a117741..68f27d36 100644 --- a/docs/05-module-resume.md +++ b/docs/05-module-resume.md @@ -86,6 +86,30 @@ Career Truth + 目标岗位 + Role Intelligence + 当前简历 + Skill 方法 - 设计系统:Editorial / Classic / Modern 三套预设。原则是克制、打印效果与屏幕一致、清晰的字体层级、关注信息密度。模板与 HTML 简历(原 Studio)作为检查器里的「设计」面板。 - PDF 优先用 managed Chromium(Python Playwright),与预览共用同一个 React 渲染器;失败时回退到 ReportLab;仍然失败就显示可重试的错误。导出的 PDF 必须对应用户实际采用的版本。 +### 8.1 「纸」模板:成品即编辑器(2026-10 已实现,切片 ①) + +- 新模板 `paper`(「纸 · 可直接编辑」),组件 `frontend/src/app/resume/components/templates/ResumePaper.tsx`。同一个组件既是导出 PDF 的成品,也是工作区画布:`editable=true` 时页面上每段文字都能直接点击修改,所以画布所见就是导出所得。 +- 编辑直接写回原始字段(`content_json` 里的 `company / position / startDate / description …`),左侧表单、AI 建议和导出读的是同一份数据;自动保存、撤销、版本沿用现有机制,不调用模型。 +- 键盘:回车提交并新增下一条要点,空要点按退格删除,Esc 撤回本次输入;悬停一条经历可删除,段落末尾「+ 添加一条」。 +- 分页可见:画布量出成品高度,超过一页时在第一页底部画出分页线并标注超出多少毫米,由用户决定删哪条,而不是自动缩字号。 +- 排版约束参考 tw93/Kami(MIT)的 design.md:单一墨蓝强调色 `#1B365D`、暖灰文字层级、衬线两字重 400/500、行高 1.45、页边距 11/13mm、无装饰线与阴影;暖纸 `#f5f4ed` / 白纸可一键切换。中文字体只用 OFL 许可的思源宋体 / Noto Serif SC,**不使用**仅限个人免费的仓耳今楷。 +- 其他模板暂时仍是只读预览,画布顶部提供「换成『纸』模板,直接在页面上改」。 +- 限制:要点按纯文本编辑,带加粗等富文本格式的那一条在被修改后会变成纯文本;未修改的条目原样保留。 +- 测试:`ResumePaper.test.tsx`(直接编辑写回原字段、Esc 还原、未改不提交、增删条目、隐藏要点索引对齐、默认排版只作用于纸模板)。 +- 下一切片:③ 排版控件与「压到一页」;④ DOCX 导出。 + +### 8.2 画布上的 AI 协作(2026-10 已实现,切片 ②) + +- **行内 diff**:AI 建议显示在它要改的那一段标题下方,按字比较,新增的字用墨蓝底色,删除的字划掉;每块都可以「采用 / 修改 / 跳过」。画布顶部显示「AI 建议 n 条 · 全部采用 · 只看当前 / 显示对比」。代码在 `components/canvas/CanvasAssist.tsx`、`components/canvas/proposalDiff.ts`。 +- **修改后采用**:只改了一句的建议,可以先在原位置改成自己想要的样子再采用(沿用 `edited_text`,后端会对新文本重新做事实门检查);改了多处的建议,先采用,再直接在页面上改。 +- **人工修改优先,冲突按段落判断**(后端 `_change_conflicts_with_manual_edit`): + - 用户改了别处,比如简介、样式或其他段落,这条建议照样可以采用,整份提案**不再**因此作废; + - 只有建议要改的那一段在建议生成后被用户改过,这一条才不能采用:页面标注「以你的版本为准」,提供「跳过」和「让 AI 按我的版本重写」;「全部采用」只采用没有冲突的条目; + - 拒绝一条建议不会顺带让其他条目的冲突失效;JD 或档案事实变了,仍然让整份提案作废(这条规则不变)。 +- **选区浮条与修改意见**:在画布上选中文字,浮条提供「更精炼 / 对齐岗位 / 换个说法 / 留言给 AI」。意见先堆在右下角的托盘里,可以逐条删除,最后点「一次交给 AI」。 +- **交给 Agent 的通道**:`lib/agentCompose.ts` 提供邮箱加事件。Agent 面板挂载时取走邮箱里的消息,已挂载的面板也能立即收到;消息一律先进待发送队列,按 D-1 规则发送。提示词要求 Agent 先读取最新工作区、只改圈出的文字、没有证据的数字先问用户,并以可审核的建议提交,不直接覆盖。 +- 测试:`CanvasAssist.test.tsx`、`[id]/page.paper.test.tsx`、`AgentPanel.test.tsx`「Agent compose mailbox」、`backend/tests/test_resume_workspace.py` 的两条段落级冲突用例。 + ## 9. 验收 - 一次单岗位定制:最终采用决定 ≤ 1 次;需要审阅的独立 Proposal 达到 50–80 个即判为不合格。 diff --git a/docs/README.md b/docs/README.md index 56816287..77efd973 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,118 +1,67 @@ +# OfferU 文档 -# OfferU Documentation +> 状态:**当前文档入口** · 重构于 2026-10(旧文档见 [archive/](./archive/README.md)) -Status: **CURRENT NAVIGATION AUTHORITY** -Updated: 2026-10-05 +## 一句话 -OfferU has accumulated design documents, audits, eval reports and implementation snapshots across several architecture generations. This page defines which documents are authoritative **now**. +**给 OfferU 一个岗位,它会告诉你:这个岗位真正要什么,你能拿出什么证据,下一步该准备什么。** -## Current authority order +OfferU 是一个本地优先(local-first)的求职工作台。你的职业事实、岗位、简历版本、投递进展和面试复盘都保存在你自己的电脑上。AI 负责研究、判断和起草;需要你做决定的事情,由你来决定。 -When documents disagree, use this order: +```text +保存一个岗位 + → 岗位要求 × 你能证明的经历 + → 岗位工作区:研究 · 证据 · 定制简历 · 投递 · 面试 + → 「今天」告诉你下一步 + → 结果回流到职业档案,下一次更准 +``` -~~~ -GOAL.md - release goal and durable product constraints - ↓ -docs/product/current-product.md - current product North Star and interaction model - ↓ -CONTEXT.md - current domain language and invariants - ↓ -docs/adr/README.md - accepted decisions and explicit supersession history - ↓ -ARCHITECTURE.md + current architecture topic docs - ↓ -live code / Operation Registry / Host Registry / generated Skill projections - ↓ -current Eval evidence -~~~ +## 文档地图(共 10 篇) -Historical audits and dated reports are evidence of what was true when they were produced. They do **not** override current product or architecture authority. - -## Start here - -| Area | Current document | Purpose | +| # | 文档 | 回答的问题 | | --- | --- | --- | -| Product | [Current Product North Star](./product/current-product.md) | Desktop-first real Career OS, Web Showcase built-in Agent, runtime-bound Skill entry, Guided Today | -| First use / dogfood | [Entry, Onboarding & Dogfood Contract](./product/entry-onboarding-and-dogfood.md) | Installer/Skill boundary, beginner hosts, current Agent capabilities, owner-dogfood path and marketing evidence | -| Proactivity | [Proactive Career Director](./product/proactive-career-director.md) | Triggered career judgment, campus/experienced Strategy Packs, autonomy levels, proactive Today/interview/re-engagement and eval contract | -| Memory / context | [Career Memory & Context Contract](./product/career-memory-contract.md) | Career Truth vs curated memory vs episodic learning vs prospective state vs external Agent memory | -| Goal | [GOAL.md](../GOAL.md) | Public-release goal and durable release gates | -| Domain | [CONTEXT.md](../CONTEXT.md) | Career Truth, candidates, evidence, applications, memory and Agent vocabulary | -| Architecture | [ARCHITECTURE.md](../ARCHITECTURE.md) | Short current system boundary | -| Agent | [Agent system](./architecture/agent-system.md) | Agent hosts, Skills, tool surface, Registry and runtime responsibilities | -| Integrations | [Agent host integrations](./architecture/harness-integrations.md) | Local-host discovery, auth ownership and runtime capability contract | -| UX | [Workbench interaction](./architecture/workbench-interaction.md) | Guided Today, progressive onboarding and Power mode | -| Security | [Operation security](./architecture/operation-security.md) | permissions, proposals, confirmation and failure rules | -| Browser | [Browser extension](./architecture/browser-extension.md) | user-triggered job capture and safe Smart Fill | -| Tool surface | [Tool Surface V2](./architecture/2026-09-22-tool-surface-v2.md) | Registry vs Agent Tool Surface vs Active Skill Surface | -| Eval | [Live Eval](./evals/LIVE_EVAL.md) | current Agent eval contract and execution model | -| Status | [STATUS.md](../STATUS.md) | current implementation state and blockers | -| Handoff | [HANDOFF.md](../HANDOFF.md) | latest continuation context for Coding Agents | -| Merge review | [Main readiness review](./evals/reports/2026-10-05-main-readiness-review.md) | Bilingual summaries of ready, conditional and held changes; evidence is bound to the reviewed checkout | -| History | [Development History / 发展历史](./history/development-history.md) | Distilled evolution, superseded directions and links to original checkpoints; not current authority | - -## Product design rationale - -These explain why current product rules exist; they are references, not higher authority. - -- [Skill-first Agent Entry, Job Workspace & Product Story](./product/skill-first-job-workspace-story.md) — rationale and external reference patterns behind the now-adopted App-first / Skill-first dual entry, canonical Job Workspace and outcome-first product story. - -## Document status classes - -- **CURRENT AUTHORITY** — actively defines current product/architecture. -- **CURRENT EVIDENCE** — current reproducible validation or status; does not define product direction. -- **HISTORICAL SNAPSHOT** — accurate for a dated commit/experiment; not a current spec. -- **SUPERSEDED** — retained only because the decision history is useful. -- **PROPOSAL** — not accepted until promoted by the appropriate authority. - -A date in a filename is a strong signal that the file may be a snapshot rather than timeless authority. - -## Historical material - -Start with [Development History / 发展历史](./history/development-history.md). Open the [archive index](./archive/README.md) only when the original decision or dated evidence is needed. - -Treat these as historical unless a current authority page explicitly adopts their conclusion: - -- docs/archive/**; -- docs/evals/reports/**; -- dated operation inventories and audit reports; -- old provider-specific or DSH-specific implementation plans; -- old release scorecards tied to an earlier commit; -- deprecated Main-Agent/Pi architecture notes. - -Do not rewrite old eval numbers to make them look current. Preserve the result and mark its scope. - -## Rules for future docs - -1. Prefer updating an existing current authority page over adding another dated “vNext design”. -2. New long-lived architecture decisions go into docs/adr/README.md with explicit supersession. -3. Product interaction changes update docs/product/current-product.md. -4. Dynamic capability counts come from live Registry/Host/Skill code, not hand-maintained prose. -5. Eval reports state the commit/runtime/model and remain evidence, not architecture. -6. A new Coding Agent should be able to identify current authority without reading historical reports. -7. If a document is kept only for archaeology, move it under docs/archive/ or add a clear historical header. -8. Distill superseded checkpoints into the development history; preserve their original results and old-path links. A dated filename alone does not retire a still-current design such as Tool Surface V2. - -## Current product summary - -~~~ -OfferU Desktop = primary Career OS experience -Job Workspace = durable product object for one opportunity -Desktop-first = default real-data front door -Web Showcase = built-in demo Agent + fictional browser data only -Skill-first = Desktop-bound power-user Agent front door -External local Agent = Desktop optional/preferred reasoning host -Built-in OfferU Agent = Web default + Desktop fallback -Agent Skills = composable methodology / Agent entry layer -Operation Registry = execution / permission authority -Career Runtime = canonical truth -Today = guided next-best-action layer -Browser = user-triggered job capture / safe fill -Email = evidence → candidate → reviewed stage event -~~~ - -See [Current Product North Star](./product/current-product.md) for the complete product contract. +| 00 | 本页 | OfferU 是什么,文档怎么读 | +| 01 | [总体设计](./01-overall-design.md) | 给谁用、核心闭环、信息架构、系统边界、权威模型、术语 | +| 02 | [交互设计](./02-interaction-design.md) | 导航、打断规则、审批呈现、防死锁、不硬编码、文案规范、已知交互缺陷 | +| 03 | [模块:职业档案与记忆](./03-module-profile.md) | Career Truth、证据、候选审核、记忆分层、渐进建档 | +| 04 | [模块:岗位工作区](./04-module-job-workspace.md) | 岗位采集、排序、Role Intelligence、Evidence Map、投递协助 | +| 05 | [模块:简历](./05-module-resume.md) | 文档式编辑、按岗位定制、AI 改动呈现、版本与导出 | +| 06 | [模块:投递进展](./06-module-pipeline.md) | 投递阶段、邮件/日历/平台信号、进展候选、跟进 | +| 07 | [模块:面试](./07-module-interview.md) | 分轮准备、练习、逐题复盘、学习回流 | +| 08 | [模块:Agent 运行时与安全](./08-module-agent-runtime.md) | Agent 接入、Skill、Operation Registry、Run 生命周期、权限与审批 | +| 09 | [质量与发布](./09-quality-and-release.md) | Eval 体系、可靠性与安全不变量、发布门槛、证据规则 | + +推荐阅读顺序:**新用户或贡献者** 00 → 01 → 02;**做某个模块** 01 → 02 → 对应模块;**做 Agent / 安全** 01 → 08 → 09。 + +## 权威顺序(只有三层) + +```text +1. 本目录 00–09 ← 产品与架构的唯一设计权威 +2. 实时代码 / Operation Registry / 生成的 Skill 投影 ← 「现在实际做到了什么」 +3. docs/evidence/ 下的验收报告 ← 「某个版本被证明做到了什么」 +``` + +- 设计文档与代码不一致时:先判断是代码缺陷还是文档过时,然后**在同一个 PR 里**修正其中一方。 +- `docs/archive/**` 只用于考古,不覆盖以上三层。 + +## 根目录保留的文件 + +| 文件 | 为什么留在根目录 | +| --- | --- | +| `README.md` / `README_ZH.md` | 项目门面 | +| `QUICKSTART.md` | 安装与首次运行 | +| `DEVELOPMENT.md` | 开发环境与命令 | +| `AGENTS.md` | Coding Agent 的施工约束 | +| `RELEASE_CHECKLIST.md` / `KNOWN_ISSUES.md` / `RELEASE_NOTES.md` | 发布脚本(`backend/scripts/release/*`)和 CI 直接读取 | +| `SECURITY.md` | GitHub 安全策略约定位置 | +| `LICENSE` / `THIRD_PARTY_NOTICES.md` | 法律文件 | + +## 文档维护规则 + +1. **总数不超过 10 篇。** 不新增 dated 设计稿或「vNext 方案」,直接修改对应文档。 +2. **每篇不超过约 300 行。** 超过时先删掉重复和过时内容,不要拆出新文件。 +3. **不写会过期的数字**,比如测试通过数、Operation 数量、commit 号、模型名。这些以实时代码和 `docs/evidence/` 为准。 +4. **提案要标注。** 尚未被产品负责人确认的设计,用 `> PROPOSAL` 块标出;确认后去掉标记。 +5. **验收报告不是设计。** 新报告放在 `docs/evidence/reports/YYYY-MM-DD-.md`,必须写明 commit、运行时和模型;格式见 [09](./09-quality-and-release.md)。 +6. **归档方式。** 过时内容用 `git mv` 移到 `docs/archive/`,保留原有路径结构,在文件开头加一行归档说明,并更新 [archive/README.md](./archive/README.md) 的映射表。 +7. **改了用户可见的交互,就同步改 02。** 改了某个模块的规则,就同步改对应的模块文档。 diff --git a/docs/evals/reports/2026-09-03-codex-offeru-public-release-standalone-credential-redaction.md b/docs/archive/pre-2026-10/evals/reports/2026-09-03-codex-offeru-public-release-standalone-credential-redaction.md similarity index 86% rename from docs/evals/reports/2026-09-03-codex-offeru-public-release-standalone-credential-redaction.md rename to docs/archive/pre-2026-10/evals/reports/2026-09-03-codex-offeru-public-release-standalone-credential-redaction.md index 7ed864a6..3346dc7c 100644 --- a/docs/evals/reports/2026-09-03-codex-offeru-public-release-standalone-credential-redaction.md +++ b/docs/archive/pre-2026-10/evals/reports/2026-09-03-codex-offeru-public-release-standalone-credential-redaction.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:历史证据,见 09-quality-and-release.md(位于 `docs/`)。 + # OfferU Public Release — Standalone Credential Redaction 日期:2026-09-03 diff --git a/docs/evals/reports/2026-09-03-codex-offeru-public-release-tauri-runtime-identity.md b/docs/archive/pre-2026-10/evals/reports/2026-09-03-codex-offeru-public-release-tauri-runtime-identity.md similarity index 91% rename from docs/evals/reports/2026-09-03-codex-offeru-public-release-tauri-runtime-identity.md rename to docs/archive/pre-2026-10/evals/reports/2026-09-03-codex-offeru-public-release-tauri-runtime-identity.md index 1875d5b9..2ae15d30 100644 --- a/docs/evals/reports/2026-09-03-codex-offeru-public-release-tauri-runtime-identity.md +++ b/docs/archive/pre-2026-10/evals/reports/2026-09-03-codex-offeru-public-release-tauri-runtime-identity.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:历史证据,见 09-quality-and-release.md(位于 `docs/`)。 + # OfferU Public Release — desktop runtime identity guard 日期:2026-09-03 diff --git a/docs/evals/reports/2026-09-03-codex-offeru-public-release-version-health-source.md b/docs/archive/pre-2026-10/evals/reports/2026-09-03-codex-offeru-public-release-version-health-source.md similarity index 90% rename from docs/evals/reports/2026-09-03-codex-offeru-public-release-version-health-source.md rename to docs/archive/pre-2026-10/evals/reports/2026-09-03-codex-offeru-public-release-version-health-source.md index 84c603d1..9d0469b8 100644 --- a/docs/evals/reports/2026-09-03-codex-offeru-public-release-version-health-source.md +++ b/docs/archive/pre-2026-10/evals/reports/2026-09-03-codex-offeru-public-release-version-health-source.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:历史证据,见 09-quality-and-release.md(位于 `docs/`)。 + # OfferU Public Release — health version source audit 日期:2026-09-03 diff --git a/docs/evals/reports/2026-09-03-codex-offeru-public-release-web-runtime-identity.md b/docs/archive/pre-2026-10/evals/reports/2026-09-03-codex-offeru-public-release-web-runtime-identity.md similarity index 90% rename from docs/evals/reports/2026-09-03-codex-offeru-public-release-web-runtime-identity.md rename to docs/archive/pre-2026-10/evals/reports/2026-09-03-codex-offeru-public-release-web-runtime-identity.md index 3ecf21ad..5fc23d50 100644 --- a/docs/evals/reports/2026-09-03-codex-offeru-public-release-web-runtime-identity.md +++ b/docs/archive/pre-2026-10/evals/reports/2026-09-03-codex-offeru-public-release-web-runtime-identity.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:历史证据,见 09-quality-and-release.md(位于 `docs/`)。 + # OfferU Public Release — web/runtime identity guard 日期:2026-09-03 diff --git a/docs/evals/reports/2026-09-07-codex-offeru-public-release-real-data-acceptance.md b/docs/archive/pre-2026-10/evals/reports/2026-09-07-codex-offeru-public-release-real-data-acceptance.md similarity index 96% rename from docs/evals/reports/2026-09-07-codex-offeru-public-release-real-data-acceptance.md rename to docs/archive/pre-2026-10/evals/reports/2026-09-07-codex-offeru-public-release-real-data-acceptance.md index 01122a97..b133e6c3 100644 --- a/docs/evals/reports/2026-09-07-codex-offeru-public-release-real-data-acceptance.md +++ b/docs/archive/pre-2026-10/evals/reports/2026-09-07-codex-offeru-public-release-real-data-acceptance.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:历史证据,见 09-quality-and-release.md(位于 `docs/`)。 + # OfferU Public Release — 2026-09-07 真实数据验收 ## 结论 diff --git a/docs/evals/reports/2026-09-23-zero-setup-onboarding.md b/docs/archive/pre-2026-10/evals/reports/2026-09-23-zero-setup-onboarding.md similarity index 98% rename from docs/evals/reports/2026-09-23-zero-setup-onboarding.md rename to docs/archive/pre-2026-10/evals/reports/2026-09-23-zero-setup-onboarding.md index 7ea50b9e..825203cf 100644 --- a/docs/evals/reports/2026-09-23-zero-setup-onboarding.md +++ b/docs/archive/pre-2026-10/evals/reports/2026-09-23-zero-setup-onboarding.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:历史证据,见 09-quality-and-release.md(位于 `docs/`)。 + # Zero-Setup Beta implementation record — 2026-09-23 Status: onboarding and native packaging code implemented; runtime acceptance pending. No builds, tests, runtime probes, browser checks, or release publication have been executed in this task, per the project's development instructions. This is not a public-release acceptance report. diff --git a/docs/evals/reports/2026-09-29-dogfood-onboarding-profile-pipeline.md b/docs/archive/pre-2026-10/evals/reports/2026-09-29-dogfood-onboarding-profile-pipeline.md similarity index 99% rename from docs/evals/reports/2026-09-29-dogfood-onboarding-profile-pipeline.md rename to docs/archive/pre-2026-10/evals/reports/2026-09-29-dogfood-onboarding-profile-pipeline.md index 19edc7cd..ce81f782 100644 --- a/docs/evals/reports/2026-09-29-dogfood-onboarding-profile-pipeline.md +++ b/docs/archive/pre-2026-10/evals/reports/2026-09-29-dogfood-onboarding-profile-pipeline.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:历史证据,见 09-quality-and-release.md(位于 `docs/`)。 + # Dogfooding 会话记录 — 接入 → 简历导入 → 岗位 → 简历优化/复刻 — 2026-09-29(终态) **性质声明**:本报告是一次 dogfooding 会话(OMP/SWE-2 作为外部 Agent Harness 驱动 OfferU)的执行记录与评估,不是 `offeru-core-v1` 正式 suite 报告,不构成 Public Release 证据。状态枚举沿用 `docs/evals/README.md`(PASS/FAIL/BLOCKED/NOT_RUN/INVALID),岗位任务用 `PARTIAL` 标注"链路跑通但数据为种子"。 diff --git a/docs/evals/reports/2026-09-30-agent-access-implementation.md b/docs/archive/pre-2026-10/evals/reports/2026-09-30-agent-access-implementation.md similarity index 97% rename from docs/evals/reports/2026-09-30-agent-access-implementation.md rename to docs/archive/pre-2026-10/evals/reports/2026-09-30-agent-access-implementation.md index 5fde7038..faec3b6c 100644 --- a/docs/evals/reports/2026-09-30-agent-access-implementation.md +++ b/docs/archive/pre-2026-10/evals/reports/2026-09-30-agent-access-implementation.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:历史证据,见 09-quality-and-release.md(位于 `docs/`)。 + # 用户自带 Agent:接入实施方案与第一切片 日期:2026-09-30。覆盖已经使用通用 Agent、愿意投入求职质量的大学生、研究生和社招用户。覆盖目标用户不等于所有宿主自动直连已经实现。 diff --git a/docs/evals/reports/2026-09-30-agent-contract-audit.md b/docs/archive/pre-2026-10/evals/reports/2026-09-30-agent-contract-audit.md similarity index 98% rename from docs/evals/reports/2026-09-30-agent-contract-audit.md rename to docs/archive/pre-2026-10/evals/reports/2026-09-30-agent-contract-audit.md index f748ce7c..53bb6c83 100644 --- a/docs/evals/reports/2026-09-30-agent-contract-audit.md +++ b/docs/archive/pre-2026-10/evals/reports/2026-09-30-agent-contract-audit.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:历史证据,见 09-quality-and-release.md(位于 `docs/`)。 + # Unified Agent access: source audit and implementation plan Baseline: `e1dbbaf`; integration branch `feat/agent-contract-desktop-binding`. User instructions on 2026-09-30 supersede the copy-a-connection-prompt default and preserve a useful built-in Agent. This report records current evidence, not a claim of universal compatibility. diff --git a/docs/evals/reports/2026-09-30-automation-maturity-review.md b/docs/archive/pre-2026-10/evals/reports/2026-09-30-automation-maturity-review.md similarity index 98% rename from docs/evals/reports/2026-09-30-automation-maturity-review.md rename to docs/archive/pre-2026-10/evals/reports/2026-09-30-automation-maturity-review.md index 21e80ea2..682222d1 100644 --- a/docs/evals/reports/2026-09-30-automation-maturity-review.md +++ b/docs/archive/pre-2026-10/evals/reports/2026-09-30-automation-maturity-review.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:历史证据,见 09-quality-and-release.md(位于 `docs/`)。 + # OfferU 外部求职自动化成熟度评估 审计日期:2026-09-30。代码基线:本地 HEAD `67a1d9da49d81530a83d5fe40bb40f0f5c0d070d`,含现存未提交改动;不是已发布 main 或安装包验收。此次只审计、研究和运行隔离测试,没有更改业务实现,没有访问真实邮箱、BOSS 会话,也没有对外提交、发送或联系。 diff --git a/docs/evals/reports/2026-09-30-project-code-review.md b/docs/archive/pre-2026-10/evals/reports/2026-09-30-project-code-review.md similarity index 99% rename from docs/evals/reports/2026-09-30-project-code-review.md rename to docs/archive/pre-2026-10/evals/reports/2026-09-30-project-code-review.md index 8ae17f80..1502e895 100644 --- a/docs/evals/reports/2026-09-30-project-code-review.md +++ b/docs/archive/pre-2026-10/evals/reports/2026-09-30-project-code-review.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:历史证据,见 09-quality-and-release.md(位于 `docs/`)。 + # OfferU 全面代码评审与 Resident Agent 接入评估 日期:2026-09-30。评审基线:`e1dbbaf`,本次 fetch 后 `HEAD...origin/main` 为 `0 0`。这是风险导向的跨模块评审,不是逐行穷尽审计,也不是 Public Release E2E 验收。此次没有修复业务代码。 diff --git a/docs/evals/reports/2026-10-01-embedded-agent-source-migration.md b/docs/archive/pre-2026-10/evals/reports/2026-10-01-embedded-agent-source-migration.md similarity index 98% rename from docs/evals/reports/2026-10-01-embedded-agent-source-migration.md rename to docs/archive/pre-2026-10/evals/reports/2026-10-01-embedded-agent-source-migration.md index 9b4d05fb..a0490265 100644 --- a/docs/evals/reports/2026-10-01-embedded-agent-source-migration.md +++ b/docs/archive/pre-2026-10/evals/reports/2026-10-01-embedded-agent-source-migration.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:历史证据,见 09-quality-and-release.md(位于 `docs/`)。 + # Embedded Agent source migration Date: 2026-10-01. Status: source replacement and UI adaptation implemented; diff --git a/docs/evals/reports/2026-10-02-career-skill-reference-review.md b/docs/archive/pre-2026-10/evals/reports/2026-10-02-career-skill-reference-review.md similarity index 98% rename from docs/evals/reports/2026-10-02-career-skill-reference-review.md rename to docs/archive/pre-2026-10/evals/reports/2026-10-02-career-skill-reference-review.md index 3c1250d3..629c1c9c 100644 --- a/docs/evals/reports/2026-10-02-career-skill-reference-review.md +++ b/docs/archive/pre-2026-10/evals/reports/2026-10-02-career-skill-reference-review.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:历史证据,见 09-quality-and-release.md(位于 `docs/`)。 + # 阿酥与其他 Skill 仓库:原始文件核对及 OfferU 验收映射 日期:2026-10-02。状态:**PUBLIC SOURCE REVIEW / NOT INSTALLED / NOT RUNTIME ACCEPTED**。 diff --git a/docs/evals/reports/2026-10-02-owner-journey-capability-audit.md b/docs/archive/pre-2026-10/evals/reports/2026-10-02-owner-journey-capability-audit.md similarity index 99% rename from docs/evals/reports/2026-10-02-owner-journey-capability-audit.md rename to docs/archive/pre-2026-10/evals/reports/2026-10-02-owner-journey-capability-audit.md index 97bb9357..0c6b3473 100644 --- a/docs/evals/reports/2026-10-02-owner-journey-capability-audit.md +++ b/docs/archive/pre-2026-10/evals/reports/2026-10-02-owner-journey-capability-audit.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:历史证据,见 09-quality-and-release.md(位于 `docs/`)。 + # 用户完整求职旅程:当前能力核对 日期:2026-10-02。状态:**SOURCE AUDIT COMPLETE / LIVE OWNER JOURNEY NOT_RUN**。 diff --git a/docs/evals/reports/2026-10-02-pr51-assessment-and-refactor-plan.md b/docs/archive/pre-2026-10/evals/reports/2026-10-02-pr51-assessment-and-refactor-plan.md similarity index 99% rename from docs/evals/reports/2026-10-02-pr51-assessment-and-refactor-plan.md rename to docs/archive/pre-2026-10/evals/reports/2026-10-02-pr51-assessment-and-refactor-plan.md index 0c7dc823..9d5687f3 100644 --- a/docs/evals/reports/2026-10-02-pr51-assessment-and-refactor-plan.md +++ b/docs/archive/pre-2026-10/evals/reports/2026-10-02-pr51-assessment-and-refactor-plan.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:历史证据,见 09-quality-and-release.md(位于 `docs/`)。 + # PR #51 设计对照评估与下一轮重构方案 日期:2026-10-02。状态:**SOURCE ASSESSMENT / REFACTOR PLAN;独立测试结果另行记录**。 diff --git a/docs/evals/reports/2026-10-02-pr51-luna-verification.md b/docs/archive/pre-2026-10/evals/reports/2026-10-02-pr51-luna-verification.md similarity index 99% rename from docs/evals/reports/2026-10-02-pr51-luna-verification.md rename to docs/archive/pre-2026-10/evals/reports/2026-10-02-pr51-luna-verification.md index 51441437..2e67c2a5 100644 --- a/docs/evals/reports/2026-10-02-pr51-luna-verification.md +++ b/docs/archive/pre-2026-10/evals/reports/2026-10-02-pr51-luna-verification.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:历史证据,见 09-quality-and-release.md(位于 `docs/`)。 + # PR #51 Luna 独立验收报告 日期:2026-10-02。状态:**独立测试报告;不代表 Goal A/B 或 Public Release 通过。** diff --git a/docs/evals/reports/2026-10-04-proposal-v2-engineering-migration.md b/docs/archive/pre-2026-10/evals/reports/2026-10-04-proposal-v2-engineering-migration.md similarity index 99% rename from docs/evals/reports/2026-10-04-proposal-v2-engineering-migration.md rename to docs/archive/pre-2026-10/evals/reports/2026-10-04-proposal-v2-engineering-migration.md index 489e597d..6a32f1a8 100644 --- a/docs/evals/reports/2026-10-04-proposal-v2-engineering-migration.md +++ b/docs/archive/pre-2026-10/evals/reports/2026-10-04-proposal-v2-engineering-migration.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:历史证据,见 09-quality-and-release.md(位于 `docs/`)。 + # Proposal v2 engineering migration evidence Status: **IN_PROGRESS — NOT PRODUCT ACCEPTED**. Updated 2026-10-05. diff --git a/docs/evals/reports/2026-10-05-main-readiness-review.md b/docs/archive/pre-2026-10/evals/reports/2026-10-05-main-readiness-review.md similarity index 98% rename from docs/evals/reports/2026-10-05-main-readiness-review.md rename to docs/archive/pre-2026-10/evals/reports/2026-10-05-main-readiness-review.md index 513e12b7..dc2d421b 100644 --- a/docs/evals/reports/2026-10-05-main-readiness-review.md +++ b/docs/archive/pre-2026-10/evals/reports/2026-10-05-main-readiness-review.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:历史证据,见 09-quality-and-release.md(位于 `docs/`)。 + # Main Readiness Review / 可合入 main 的改动梗概 Date: 2026-10-05 diff --git a/docs/evals/reports/README.md b/docs/archive/pre-2026-10/evals/reports/README.md similarity index 92% rename from docs/evals/reports/README.md rename to docs/archive/pre-2026-10/evals/reports/README.md index 6b841505..3775b698 100644 --- a/docs/evals/reports/README.md +++ b/docs/archive/pre-2026-10/evals/reports/README.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:历史证据,见 09-quality-and-release.md(位于 `docs/`)。 + # Eval 报告 本目录只接收能够证明一个可定位 commit 当前行为的正式 Eval 报告。预评估、测试计划、聊天总结、只读审计和已失效运行记录不放在这里;需要追溯时使用 Git 历史。 diff --git a/docs/evals/reports/artifacts/data-safety-01/settings-restart.png b/docs/archive/pre-2026-10/evals/reports/artifacts/data-safety-01/settings-restart.png similarity index 100% rename from docs/evals/reports/artifacts/data-safety-01/settings-restart.png rename to docs/archive/pre-2026-10/evals/reports/artifacts/data-safety-01/settings-restart.png diff --git a/docs/evals/reports/artifacts/data-safety-01/settings-stage.png b/docs/archive/pre-2026-10/evals/reports/artifacts/data-safety-01/settings-stage.png similarity index 100% rename from docs/evals/reports/artifacts/data-safety-01/settings-stage.png rename to docs/archive/pre-2026-10/evals/reports/artifacts/data-safety-01/settings-stage.png diff --git a/docs/evals/reports/artifacts/data-safety-03/export-ui-summary.txt b/docs/archive/pre-2026-10/evals/reports/artifacts/data-safety-03/export-ui-summary.txt similarity index 100% rename from docs/evals/reports/artifacts/data-safety-03/export-ui-summary.txt rename to docs/archive/pre-2026-10/evals/reports/artifacts/data-safety-03/export-ui-summary.txt diff --git a/docs/evals/reports/artifacts/data-safety-03/settings-demo-reset.png b/docs/archive/pre-2026-10/evals/reports/artifacts/data-safety-03/settings-demo-reset.png similarity index 100% rename from docs/evals/reports/artifacts/data-safety-03/settings-demo-reset.png rename to docs/archive/pre-2026-10/evals/reports/artifacts/data-safety-03/settings-demo-reset.png diff --git a/docs/history/development-history.md b/docs/archive/pre-2026-10/history/development-history.md similarity index 98% rename from docs/history/development-history.md rename to docs/archive/pre-2026-10/history/development-history.md index 02f36b39..3556349f 100644 --- a/docs/history/development-history.md +++ b/docs/archive/pre-2026-10/history/development-history.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:README.md(位于 `docs/`)。 + # OfferU Development History / 发展历史 Status: **HISTORICAL SYNTHESIS — NOT CURRENT PRODUCT AUTHORITY** diff --git a/docs/product/agentic-interaction-policy.md b/docs/archive/pre-2026-10/product/agentic-interaction-policy.md similarity index 99% rename from docs/product/agentic-interaction-policy.md rename to docs/archive/pre-2026-10/product/agentic-interaction-policy.md index 43f60db1..b7456f98 100644 --- a/docs/product/agentic-interaction-policy.md +++ b/docs/archive/pre-2026-10/product/agentic-interaction-policy.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:02-interaction-design.md(位于 `docs/`)。 + # Agentic Interaction Policy Status: **CURRENT PRODUCT AUTHORITY** diff --git a/docs/product/career-memory-contract.md b/docs/archive/pre-2026-10/product/career-memory-contract.md similarity index 98% rename from docs/product/career-memory-contract.md rename to docs/archive/pre-2026-10/product/career-memory-contract.md index 1150beda..e62dc3f7 100644 --- a/docs/product/career-memory-contract.md +++ b/docs/archive/pre-2026-10/product/career-memory-contract.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:03-module-profile.md(位于 `docs/`)。 + # Career Memory & Context Contract Status: **CURRENT PRODUCT DETAIL / IMPLEMENTATION CONTRACT** diff --git a/docs/product/career-operations-loop.md b/docs/archive/pre-2026-10/product/career-operations-loop.md similarity index 99% rename from docs/product/career-operations-loop.md rename to docs/archive/pre-2026-10/product/career-operations-loop.md index 5e26b2b9..c5b809cd 100644 --- a/docs/product/career-operations-loop.md +++ b/docs/archive/pre-2026-10/product/career-operations-loop.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:04、06、07(位于 `docs/`)。 + # OfferU Career Operations Loop Status: CURRENT PRODUCT DETAIL / OWNER-DOGFOOD CONTRACT diff --git a/docs/product/current-product.md b/docs/archive/pre-2026-10/product/current-product.md similarity index 99% rename from docs/product/current-product.md rename to docs/archive/pre-2026-10/product/current-product.md index 084acf13..ba072f72 100644 --- a/docs/product/current-product.md +++ b/docs/archive/pre-2026-10/product/current-product.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:01-overall-design.md(位于 `docs/`)。 + # OfferU Current Product North Star diff --git a/docs/product/entry-onboarding-and-dogfood.md b/docs/archive/pre-2026-10/product/entry-onboarding-and-dogfood.md similarity index 98% rename from docs/product/entry-onboarding-and-dogfood.md rename to docs/archive/pre-2026-10/product/entry-onboarding-and-dogfood.md index 46c9c395..000d646f 100644 --- a/docs/product/entry-onboarding-and-dogfood.md +++ b/docs/archive/pre-2026-10/product/entry-onboarding-and-dogfood.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:01-overall-design.md、08-module-agent-runtime.md、09-quality-and-release.md(位于 `docs/`)。 + # OfferU Entry, Onboarding & Dogfood Contract Status: **CURRENT PRODUCT DETAIL** diff --git a/docs/product/offeru-career-os-blueprint.html b/docs/archive/pre-2026-10/product/offeru-career-os-blueprint.html similarity index 100% rename from docs/product/offeru-career-os-blueprint.html rename to docs/archive/pre-2026-10/product/offeru-career-os-blueprint.html diff --git a/docs/product/owner-career-journey.md b/docs/archive/pre-2026-10/product/owner-career-journey.md similarity index 99% rename from docs/product/owner-career-journey.md rename to docs/archive/pre-2026-10/product/owner-career-journey.md index 30960cbe..2a10bd27 100644 --- a/docs/product/owner-career-journey.md +++ b/docs/archive/pre-2026-10/product/owner-career-journey.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:01–07 各模块(位于 `docs/`)。 + # 真实求职用户的完整体验与验收契约 状态:**CURRENT PRODUCT DETAIL / OWNER ACCEPTANCE TARGET** diff --git a/docs/product/proactive-career-director.md b/docs/archive/pre-2026-10/product/proactive-career-director.md similarity index 99% rename from docs/product/proactive-career-director.md rename to docs/archive/pre-2026-10/product/proactive-career-director.md index 45e4dcd5..2a3f7400 100644 --- a/docs/product/proactive-career-director.md +++ b/docs/archive/pre-2026-10/product/proactive-career-director.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:08-module-agent-runtime.md(§8)、03、07(位于 `docs/`)。 + # Proactive Career Director Status: **CURRENT PRODUCT DETAIL / IMPLEMENTATION CONTRACT** diff --git a/docs/product/resume-canvas-design.md b/docs/archive/pre-2026-10/product/resume-canvas-design.md similarity index 99% rename from docs/product/resume-canvas-design.md rename to docs/archive/pre-2026-10/product/resume-canvas-design.md index 28701627..03873445 100644 --- a/docs/product/resume-canvas-design.md +++ b/docs/archive/pre-2026-10/product/resume-canvas-design.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:05-module-resume.md(位于 `docs/`)。 + # Resume Canvas — Document-first, AI-assisted Resume Experience Status: **CURRENT PRODUCT DESIGN / OWNER-DOGFOOD TARGET** diff --git a/docs/product/skill-first-job-workspace-story.md b/docs/archive/pre-2026-10/product/skill-first-job-workspace-story.md similarity index 98% rename from docs/product/skill-first-job-workspace-story.md rename to docs/archive/pre-2026-10/product/skill-first-job-workspace-story.md index 3321f436..cc464159 100644 --- a/docs/product/skill-first-job-workspace-story.md +++ b/docs/archive/pre-2026-10/product/skill-first-job-workspace-story.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:01-overall-design.md(位于 `docs/`)。 + # OfferU Skill-first Agent Entry, Job Workspace & Product Story Status: **DESIGN RATIONALE / DISTILLED REFERENCE** diff --git a/ARCHITECTURE.md b/docs/archive/pre-2026-10/root/ARCHITECTURE.md similarity index 97% rename from ARCHITECTURE.md rename to docs/archive/pre-2026-10/root/ARCHITECTURE.md index 7fb81bbe..d1118247 100644 --- a/ARCHITECTURE.md +++ b/docs/archive/pre-2026-10/root/ARCHITECTURE.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:01-overall-design.md、08-module-agent-runtime.md(位于 `docs/`)。 + # OfferU Architecture diff --git a/CONTEXT.md b/docs/archive/pre-2026-10/root/CONTEXT.md similarity index 97% rename from CONTEXT.md rename to docs/archive/pre-2026-10/root/CONTEXT.md index 694cfdde..660f6df8 100644 --- a/CONTEXT.md +++ b/docs/archive/pre-2026-10/root/CONTEXT.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:01-overall-design.md(§10 术语)(位于 `docs/`)。 + # OfferU Context Status: **CURRENT DOMAIN AUTHORITY** diff --git a/GOAL.md b/docs/archive/pre-2026-10/root/GOAL.md similarity index 99% rename from GOAL.md rename to docs/archive/pre-2026-10/root/GOAL.md index 56d07dfe..755cb062 100644 --- a/GOAL.md +++ b/docs/archive/pre-2026-10/root/GOAL.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:01-overall-design.md、09-quality-and-release.md(位于 `docs/`)。 + # OfferU Public Release Goal ## Autonomous Production Readiness Program diff --git a/HANDOFF.md b/docs/archive/pre-2026-10/root/HANDOFF.md similarity index 94% rename from HANDOFF.md rename to docs/archive/pre-2026-10/root/HANDOFF.md index 04eeb765..cc33e3e7 100644 --- a/HANDOFF.md +++ b/docs/archive/pre-2026-10/root/HANDOFF.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:RELEASE_CHECKLIST.md、KNOWN_ISSUES.md(实时状态)(位于 `docs/`)。 + # OfferU Handoff Status: **CURRENT CONTINUATION INDEX** diff --git a/INTERNAL_BETA.md b/docs/archive/pre-2026-10/root/INTERNAL_BETA.md similarity index 97% rename from INTERNAL_BETA.md rename to docs/archive/pre-2026-10/root/INTERNAL_BETA.md index 73fee74b..2ce4b680 100644 --- a/INTERNAL_BETA.md +++ b/docs/archive/pre-2026-10/root/INTERNAL_BETA.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:09-quality-and-release.md(位于 `docs/`)。 + > [!NOTE] > **HISTORICAL INTERNAL-BETA WALKTHROUGH.** Keep this for regression/demo archaeology. It is not the current onboarding or product authority; use `docs/product/current-product.md`, `STATUS.md`, and `HANDOFF.md` for current direction. diff --git a/QUALITY_SCORE.md b/docs/archive/pre-2026-10/root/QUALITY_SCORE.md similarity index 98% rename from QUALITY_SCORE.md rename to docs/archive/pre-2026-10/root/QUALITY_SCORE.md index 6c1e5105..33e47c4f 100644 --- a/QUALITY_SCORE.md +++ b/docs/archive/pre-2026-10/root/QUALITY_SCORE.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:09-quality-and-release.md(位于 `docs/`)。 + # OfferU Public Release Quality Score 更新时间:2026-09-09 diff --git a/RELIABILITY.md b/docs/archive/pre-2026-10/root/RELIABILITY.md similarity index 99% rename from RELIABILITY.md rename to docs/archive/pre-2026-10/root/RELIABILITY.md index b354e872..2a025179 100644 --- a/RELIABILITY.md +++ b/docs/archive/pre-2026-10/root/RELIABILITY.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:09-quality-and-release.md(位于 `docs/`)。 + # OfferU Public Release Reliability 更新时间:2026-09-02 diff --git a/STATUS.md b/docs/archive/pre-2026-10/root/STATUS.md similarity index 95% rename from STATUS.md rename to docs/archive/pre-2026-10/root/STATUS.md index d9ed1aac..1948f37e 100644 --- a/STATUS.md +++ b/docs/archive/pre-2026-10/root/STATUS.md @@ -1,3 +1,5 @@ +> **已归档(2026-10)**:本文不再是当前权威。当前文档:RELEASE_CHECKLIST.md、KNOWN_ISSUES.md(实时状态)(位于 `docs/`)。 + # OfferU Status Status: **CURRENT CHECKOUT REVIEW — NOT RELEASE ACCEPTANCE** diff --git a/docs/evidence/README.md b/docs/evidence/README.md new file mode 100644 index 00000000..31249fd3 --- /dev/null +++ b/docs/evidence/README.md @@ -0,0 +1,24 @@ +# 验收证据 + +本目录存放**证据,不存放设计**。设计权威见 [docs/README.md](../README.md),证据规则见 [09 · 质量与发布](../09-quality-and-release.md)。 + +| 路径 | 内容 | +| --- | --- | +| `report-schema.json` | 正式 Eval 报告中 `eval-summary` JSON 代码块的机器 schema | +| `evolve-bench-schema.json` | OfferU-EvolveBench 的 case schema | +| `reports/YYYY-MM-DD---.md` | 能证明某个可定位 commit 当前行为的正式报告 | +| `reports/artifacts///...` | 脱敏截图、trace、机器输出(默认只保留在本地,不提交) | + +## 报告最小结构 + +1. 运行身份:suite / version / run ID、commit、dirty 状态、执行宿主和模型。 +2. 环境:OS、Python、Node、CLI、宿主和 adapter、provider 和 model、fixture 隔离。 +3. 总结:各状态的数量、目标范围、明确的结论。 +4. 每个 Task 的 trials、命令或交互、退出码、耗时、轨迹证据、结果证据。 +5. 脱敏后的失败复现、限制、成本、下一步。 +6. 与正文一致、能被 JSON parser 读取的 `eval-summary` 代码块,符合 `report-schema.json`。 + +出现以下任一情况,报告即为 `INVALID`:把跳过、阻塞、模拟返回或模型自评记成 `PASS`;正文、JSON、命令摘要或附件互相矛盾;没有记录 commit 或 dirty 状态;缺少必需的轨迹或结果证据;证据里有未脱敏的凭据或个人信息。 + +**当前没有有效的正式 baseline。** 2026-10 之前的报告已归档到 `docs/archive/pre-2026-10/evals/reports/`,只作为对应 commit 当时的历史证据。 + diff --git a/docs/evals/evolve-bench-schema.json b/docs/evidence/evolve-bench-schema.json similarity index 100% rename from docs/evals/evolve-bench-schema.json rename to docs/evidence/evolve-bench-schema.json diff --git a/docs/evals/report-schema.json b/docs/evidence/report-schema.json similarity index 100% rename from docs/evals/report-schema.json rename to docs/evidence/report-schema.json diff --git a/docs/evidence/reports/.gitkeep b/docs/evidence/reports/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/extension/src/background.ts b/extension/src/background.ts index 324c880e..e81c4e1f 100644 --- a/extension/src/background.ts +++ b/extension/src/background.ts @@ -810,7 +810,7 @@ const BOSS_COOKIE_DOMAIN = "zhipin.com"; * * 三条硬约束: * 1. Cookie 只在本次调用内存在 —— 不写 `chrome.storage.local`,不写日志, - * 也不回传给 popup(见 docs/architecture/browser-extension.md); + * 也不回传给 popup(见 docs/04-module-job-workspace.md); * 2. 缺少 wt2 时直接失败,不把"写入了别的字段"当成登录成功; * 3. 以后端回读到的状态为准,不以写入请求返回 200 判定成功。 */ diff --git a/extension/src/rule-packs/contracts.ts b/extension/src/rule-packs/contracts.ts index ea05a8ec..1c451982 100644 --- a/extension/src/rule-packs/contracts.ts +++ b/extension/src/rule-packs/contracts.ts @@ -1,6 +1,6 @@ // ============================================= // SiteRulePack v1 — 类型契约 -// 事实源:docs/architecture/site-rule-pack-v1.md +// 事实源:本文件;产品边界见 docs/04-module-job-workspace.md,原始规格见 docs/archive/pre-2026-10/architecture/site-rule-pack-v1.md // 规则是数据;复杂控件行为是扩展内置 ControlDriver 代码。 // ============================================= diff --git a/extension/wxt.config.ts b/extension/wxt.config.ts index e02c890f..f95122c2 100644 --- a/extension/wxt.config.ts +++ b/extension/wxt.config.ts @@ -27,7 +27,7 @@ export default defineConfig({ // (src/background.ts 的 syncBossScraperSession),不读取其他任何站点。 permissions: ["storage", "activeTab", "tabs", "scripting", "clipboardWrite", "offscreen", "cookies"], host_permissions: ["http://127.0.0.1/*", "http://localhost/*"], - // 招聘站点权限按需申请(见 docs/architecture/browser-extension.md), + // 招聘站点权限按需申请(见 docs/04-module-job-workspace.md), // 仅限常见招聘站点;用户连接对应站点登录态时才逐站申请,不再开放通配所有 http/https。 optional_host_permissions: [ "https://*.zhipin.com/*", diff --git a/frontend/src/app/resume/[id]/page.paper.test.tsx b/frontend/src/app/resume/[id]/page.paper.test.tsx new file mode 100644 index 00000000..1daa1520 --- /dev/null +++ b/frontend/src/app/resume/[id]/page.paper.test.tsx @@ -0,0 +1,119 @@ +import { render, screen, waitFor, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { ResumeWorkspace } from "@/lib/api"; + +const api = vi.hoisted(() => ({ + workspace: vi.fn(), + update: vi.fn(), + reviewProposalItem: vi.fn(), + reviewProposalItems: vi.fn(), +})); + +vi.mock("@/lib/api", () => ({ + resumeApi: { + workspace: api.workspace, + update: api.update, + reviewProposalItem: api.reviewProposalItem, + reviewProposalItems: api.reviewProposalItems, + createVersion: vi.fn(), + exportPdf: vi.fn(), + restoreVersion: vi.fn(), + updateDesign: vi.fn(), + }, +})); +vi.mock("next/navigation", () => ({ + useParams: () => ({ id: "7" }), + useRouter: () => ({ push: vi.fn(), replace: vi.fn(), back: vi.fn(), forward: vi.fn(), refresh: vi.fn() }), + usePathname: () => "/resume/7", + useSearchParams: () => new URLSearchParams(""), +})); +vi.mock("@/lib/showcase/router", () => ({ SHOWCASE: false })); +vi.mock("../components/SectionEditor", () => ({ default: () =>
})); + +import ResumeEditorPage from "./page"; + +const before = { + section_type: "experience", title: "工作经历", sort_order: 0, visible: true, source_section_ids: [11], + content_json: [{ company: "示例科技", position: "后端", description: "
  • 负责对账服务

" }], +}; +const after = { ...before, content_json: [{ ...before.content_json[0], description: "
  • 负责日均千万笔的对账服务

" }] }; + +function workspace(sectionContent = before.content_json): ResumeWorkspace { + return { + resume: { + id: 7, workspace_revision: 3, user_name: "林一", title: "后端工程师", summary: "", contact_json: {}, + template_id: "paper", style_config: { template: "paper" }, language: "zh", + sections: [{ id: 5, resume_id: 7, section_type: "experience", title: "工作经历", sort_order: 0, visible: true, + content_json: sectionContent, source_section_ids: [11] }], + } as unknown as ResumeWorkspace["resume"], + job: { id: 42, title: "后端工程师", company: "示例公司" }, + workspace: { revision: 3, content_hash: "h", is_tailored: true }, + application_packet: { + job_id: 42, resume_id: 7, current_version_id: 1, current_version_number: 1, status: "ready", + application_id: null, application_attempt_id: null, artifacts: {}, + }, + proposals: [{ + proposal_id: "prop-1", status: "ready", job_id: 42, job_title: "后端工程师", company: "示例公司", profile_id: 1, + research_run_id: "run-1", change_count: 1, fact_gate_status: "passed", fact_gate_warnings_count: 0, review_note: "", + created_at: "", updated_at: "", source_section_ids: [11], source_snapshot_hash: "s", research_snapshot_hash: "r", + original_summary: "", proposed_summary: "", original_rows: [before], proposed_rows: [after], + diff: [{ change_id: "c1", change_type: "modified", title: "工作经历", section_type: "experience", before, after, source_section_ids: [11] }], + strategy: {}, presentation: {}, fact_gates: {}, trace: {}, item_reviews: {}, + }], + versions: [], + } as unknown as ResumeWorkspace; +} + +describe("纸画布上的 AI 建议", () => { + beforeEach(() => { + vi.clearAllMocks(); + HTMLElement.prototype.scrollTo = vi.fn(); + api.update.mockImplementation(async (_id: number, data: Record) => ({ id: 7, ...data })); + }); + + it("shows the suggestion inside the section it changes and accepts it from there", async () => { + api.workspace.mockResolvedValue(workspace()); + const reviewed = workspace(); + reviewed.proposals[0].item_reviews = { c1: { action: "accept" } }; + api.reviewProposalItem.mockResolvedValue(reviewed); + const user = userEvent.setup(); + render(); + + const inline = await screen.findByTestId("inline-proposal-c1"); + expect(within(inline).getByText("日均千万笔的")).toBeInTheDocument(); + expect(screen.getByTestId("resume-canvas-proposals")).toHaveTextContent("AI 建议 1 条"); + await user.click(within(inline).getByRole("button", { name: /^采用/ })); + await waitFor(() => expect(api.reviewProposalItem).toHaveBeenCalledWith("prop-1", { + resume_id: 7, change_id: "c1", action: "accept", edited_text: "", + })); + await waitFor(() => expect(screen.queryByTestId("inline-proposal-c1")).not.toBeInTheDocument()); + }); + + it("keeps the user's own edit and refuses to apply a suggestion built on the old text", async () => { + api.workspace.mockResolvedValue(workspace([{ ...before.content_json[0], description: "
  • 我自己改的版本

" }])); + const user = userEvent.setup(); + render(); + + const inline = await screen.findByTestId("inline-proposal-c1"); + expect(within(inline).getByRole("note")).toHaveTextContent("以你的版本为准"); + expect(within(inline).getByRole("button", { name: /^采用/ })).toBeDisabled(); + expect(screen.getByRole("button", { name: /全部采用/ })).toBeDisabled(); + + await user.click(within(inline).getByRole("button", { name: /跳过/ })); + await waitFor(() => expect(api.reviewProposalItem).toHaveBeenCalledWith("prop-1", { + resume_id: 7, change_id: "c1", action: "reject", edited_text: "", + })); + }); + + it("hides the comparison on demand without touching the content", async () => { + api.workspace.mockResolvedValue(workspace()); + const user = userEvent.setup(); + render(); + await screen.findByTestId("inline-proposal-c1"); + await user.click(screen.getByRole("button", { name: "只看当前" })); + expect(screen.queryByTestId("inline-proposal-c1")).not.toBeInTheDocument(); + expect(screen.getAllByText("负责对账服务").length).toBeGreaterThan(0); + }); +}); + diff --git a/frontend/src/app/resume/[id]/page.tsx b/frontend/src/app/resume/[id]/page.tsx index 0340bd66..d455344b 100644 --- a/frontend/src/app/resume/[id]/page.tsx +++ b/frontend/src/app/resume/[id]/page.tsx @@ -43,6 +43,10 @@ import type { ResumeDetail, ResumeSectionBlock } from "@/lib/hooks"; import SectionEditor from "../components/SectionEditor"; import ResumePreview from "../components/ResumePreview"; import ResumeDesignPanel from "../components/ResumeDesignPanel"; +import { normalizeTemplateSettings } from "../components/templates/templateSettings"; +import { buildCommentPrompt, CanvasSelectionBar, CommentTray, InlineProposal, type CanvasComment, type InlineChange } from "../components/canvas/CanvasAssist"; +import { conflictsWithManualEdit, findTargetSection } from "../components/canvas/proposalDiff"; +import { composeToAgent } from "@/lib/agentCompose"; import { safeClientErrorMessage } from "@/lib/safe-error"; type DraftResume = ResumeDetail & { sections: ResumeSectionBlock[] }; @@ -224,6 +228,27 @@ export default function ResumeEditorPage() { const [exporting, setExporting] = useState(false); const [restoring, setRestoring] = useState(null); const [canUndo, setCanUndo] = useState(false); + // 「纸」画布:量出成品高度,超过一页时在第一页底部画出分页线,让用户自己决定删哪条。 + const [canvasEl, setCanvasEl] = useState(null); + const [pageOverflowMm, setPageOverflowMm] = useState(0); + const canvasNodeRef = useRef(null); + const canvasRef = useCallback((node: HTMLDivElement | null) => { canvasNodeRef.current = node; setCanvasEl(node); }, []); + // 切片 ②:画布上堆积的修改意见,以及「对比 / 当前」视图。 + const [comments, setComments] = useState([]); + const [showProposals, setShowProposals] = useState(true); + const pageSizeKey = draft?.style_config?.pageSize === "LETTER" ? "LETTER" : "A4"; + const pageHeightPx = ((pageSizeKey === "LETTER" ? 279.4 : 297) * 96) / 25.4; + useEffect(() => { + if (!canvasEl || typeof ResizeObserver === "undefined") { setPageOverflowMm(0); return; } + const measure = () => { + const height = canvasEl.getBoundingClientRect().height; + setPageOverflowMm(Math.max(0, ((height - pageHeightPx) * 25.4) / 96)); + }; + const observer = new ResizeObserver(measure); + observer.observe(canvasEl); + measure(); + return () => observer.disconnect(); + }, [canvasEl, pageHeightPx]); const hydratedRef = useRef(false); const skipAutosaveRef = useRef(false); const lastSavedSignatureRef = useRef(""); @@ -432,15 +457,54 @@ export default function ResumeEditorPage() { if (loadError || !workspace || !draft) return

{loadError || "简历工作区不存在"}

; const style = draft.style_config || {}; + const paperMode = normalizeTemplateSettings(style).template === "paper"; const setStyle = (key: string, value: string) => updateDraft({ style_config: { ...style, [key]: value } }); const targetLabel = workspace.job ? `${workspace.job.company} · ${workspace.job.title}` : "未绑定目标岗位"; const activeProposal = workspace.proposals.find((item) => ["ready", "in_review", "blocked"].includes(item.status)); const staleProposal = workspace.proposals.find((item) => item.status === "stale"); + // AI 建议落到它要改的那一段里。冲突(建议生成后用户改过这段)以用户版本为准,不能直接采用。 + const pendingChanges: InlineChange[] = activeProposal + ? (activeProposal.diff || []).filter((change) => !activeProposal.item_reviews?.[String(change.change_id || "")]) as InlineChange[] + : []; + const changeTargets = pendingChanges.map((change) => ({ + change, + target: change.change_type === "added" ? undefined : findTargetSection(draft.sections, change), + })); + const isConflicted = (change: InlineChange, target?: ResumeSectionBlock) => conflictsWithManualEdit(target, change); + const safeChangeIds = changeTargets.filter(({ change, target }) => !isConflicted(change, target)).map(({ change }) => String(change.change_id)); + const factGateBlocked = activeProposal?.fact_gate_status === "blocked"; + const askRewrite = (target: ResumeSectionBlock | undefined) => composeToAgent({ + skillId: "tailor_resume", + message: buildCommentPrompt(draft.id, targetLabel, [{ + id: "rewrite", quote: target?.title || "这一段", section: target?.title || "这一段", + instruction: "我在你的建议生成后手动改过这一段。请基于我现在的版本重新给出建议,保留我改过的措辞。", + }]), + }); + const renderInline = (change: InlineChange, target?: ResumeSectionBlock) => ( + void handleProposalAction(String(change.change_id), "accept", edited || "")} + onSkip={() => void handleProposalAction(String(change.change_id), "reject")} onAskRewrite={() => askRewrite(target)} /> + ); + const renderSectionAside = showProposals && paperMode + ? (section: { id: number }) => { + const items = changeTargets.filter(({ target }) => target?.id === section.id); + return items.length ? <>{items.map(({ change, target }) => renderInline(change, target))} : null; + } + : undefined; + const orphanChanges = changeTargets.filter(({ target }) => !target); + const canvasFooter = showProposals && paperMode && orphanChanges.length + ?
{orphanChanges.map(({ change }) => renderInline(change))}
+ : undefined; + const submitComments = () => { + if (!comments.length) return; + composeToAgent({ skillId: "tailor_resume", message: buildCommentPrompt(draft.id, targetLabel, comments) }); + setComments([]); + }; return (
-

Resume Workspace

{workspace.application_packet.status === "ready" ? "已准备" : "草稿"}

{targetLabel}

+

岗位简历

{workspace.application_packet.status === "ready" ? "已准备" : "草稿"}

{targetLabel}

{saveState === "saving" && "正在保存…"}{saveState === "saved" && "已保存"}{saveState === "failed" && "保存失败"}
@@ -453,9 +517,9 @@ export default function ResumeEditorPage() {
添加内容段落
{EDITOR_SECTION_TYPES.map(([type, label]) => )}
-
Live Preview {style.pageSize === "LETTER" ? "Letter" : "A4"}
编辑即更新
+
{paperMode ? "成品" : "预览"} {style.pageSize === "LETTER" ? "Letter" : "A4"}
{paperMode ?
{pendingChanges.length > 0 && AI 建议 {pendingChanges.length} 条}选中文字可留言给 AI · 点任意文字直接修改 · 回车新增要点 · Esc 撤回
: }
updateDraft(patch)} onSectionChange={(section) => updateSection(section as ResumeSectionBlock)} renderSectionAside={renderSectionAside} canvasFooter={canvasFooter} />{paperMode && setComments((current) => [...current, { ...comment, id: `comment-${Date.now()}-${current.length}` }])} />}{paperMode && pageOverflowMm > 0 &&
第一页到此 · 超出约 {Math.round(pageOverflowMm)} mm
}
{paperMode && setComments((current) => current.filter((item) => item.id !== id))} onSubmit={submitComments} />}
-