From 736fc855c13f700ed61330cf013e3605aeab2584 Mon Sep 17 00:00:00 2001 From: zhao0335 <3797723137@qq.com> Date: Thu, 5 Nov 2026 02:56:31 +0800 Subject: [PATCH 1/7] feat: add bilingual anti-defensive writing task skills --- .gitattributes | 5 + apps/server/src/agent/skill-registry.test.ts | 12 ++ apps/server/src/agent/skill-registry.ts | 4 +- .../task-anti-defensive-writing-en/LICENSE | 21 +++ .../task-anti-defensive-writing-en/SKILL.md | 123 ++++++++++++++++++ .../task-anti-defensive-writing-en/TASK.md | 11 ++ .../task-anti-defensive-writing/LICENSE | 21 +++ .../task-anti-defensive-writing/SKILL.md | 94 +++++++++++++ .../task-anti-defensive-writing/TASK.md | 11 ++ docs/DEV.md | 2 + 10 files changed, 303 insertions(+), 1 deletion(-) create mode 100644 .gitattributes create mode 100644 apps/server/src/skills/task-anti-defensive-writing-en/LICENSE create mode 100644 apps/server/src/skills/task-anti-defensive-writing-en/SKILL.md create mode 100644 apps/server/src/skills/task-anti-defensive-writing-en/TASK.md create mode 100644 apps/server/src/skills/task-anti-defensive-writing/LICENSE create mode 100644 apps/server/src/skills/task-anti-defensive-writing/SKILL.md create mode 100644 apps/server/src/skills/task-anti-defensive-writing/TASK.md diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..037b612 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# Keep vendored originals byte-identical on Windows checkouts. +/apps/server/src/skills/task-anti-defensive-writing/SKILL.md text eol=lf +/apps/server/src/skills/task-anti-defensive-writing/LICENSE text eol=lf +/apps/server/src/skills/task-anti-defensive-writing-en/SKILL.md text eol=lf +/apps/server/src/skills/task-anti-defensive-writing-en/LICENSE text eol=lf diff --git a/apps/server/src/agent/skill-registry.test.ts b/apps/server/src/agent/skill-registry.test.ts index 59114d8..3af5c3f 100644 --- a/apps/server/src/agent/skill-registry.test.ts +++ b/apps/server/src/agent/skill-registry.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test"; import { WRITING_PROFILES, paperSkillForProfile } from "@fastwrite/shared"; +import { readFile } from "node:fs/promises"; import { join } from "node:path"; import { parseSkillManifest, SkillRegistry } from "./skill-registry"; @@ -63,3 +64,14 @@ describe("published Skill manifests", () => { expect(() => parseSkillManifest(JSON.stringify({ id: "review", version: "1.0.0", scope: "system", owner: "FastWrite", license: "Apache-2.0", workflows: [], requiredEvidence: [], capabilities: [], maxContextChars: 1000, riskLevel: "medium", requiresReview: true, eval: { status: "passed", score: 2 }, references: [] }), "review")).toThrow(); }); }); + +test("reads task metadata separately from the skill instructions", async () => { + const directory = join(import.meta.dir, "..", "skills"); + const registry = new SkillRegistry(directory); + for (const [id, language] of [["anti-defensive-writing", "中文"], ["anti-defensive-writing-en", "English"]] as const) { + const { descriptor, instructions } = await registry.loadTask(id); + expect(descriptor.description).toContain(language); + expect(instructions).toBe(await readFile(join(directory, `task-${id}`, "SKILL.md"), "utf8")); + } + expect((await registry.loadTask("claim-audit")).descriptor.riskLevel).toBe("high"); +}); diff --git a/apps/server/src/agent/skill-registry.ts b/apps/server/src/agent/skill-registry.ts index f8ac96e..f2e82e3 100644 --- a/apps/server/src/agent/skill-registry.ts +++ b/apps/server/src/agent/skill-registry.ts @@ -92,7 +92,9 @@ export class SkillRegistry { const result: AgentTaskSkillDescriptor[] = []; for (const entry of entries.filter((item) => item.isDirectory() && item.name.startsWith("task-"))) { const content = await readFile(join(this.skillsDirectory, entry.name, "SKILL.md"), "utf8").catch(() => ""); - const field = (name: string) => new RegExp(`^\\s*${name}:\\s*(.+)$`, "m").exec(content)?.[1]?.trim() ?? ""; + if (!content) continue; + const metadata = await readFile(join(this.skillsDirectory, entry.name, "TASK.md"), "utf8").catch(() => content); + const field = (name: string) => new RegExp(`^\\s*${name}:\\s*(.+)$`, "m").exec(metadata)?.[1]?.trim() ?? ""; const list = (name: string) => field(name).split(",").map((item) => item.trim()).filter(Boolean); const intents = list("supportedIntents").filter((item): item is AgentTaskIntent => ["draft", "continue", "revise"].includes(item)); if (!intents.length) continue; diff --git a/apps/server/src/skills/task-anti-defensive-writing-en/LICENSE b/apps/server/src/skills/task-anti-defensive-writing-en/LICENSE new file mode 100644 index 0000000..f2fddc5 --- /dev/null +++ b/apps/server/src/skills/task-anti-defensive-writing-en/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Adkid-Zephyr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apps/server/src/skills/task-anti-defensive-writing-en/SKILL.md b/apps/server/src/skills/task-anti-defensive-writing-en/SKILL.md new file mode 100644 index 0000000..87a1636 --- /dev/null +++ b/apps/server/src/skills/task-anti-defensive-writing-en/SKILL.md @@ -0,0 +1,123 @@ +--- +name: anti-defensive-writing-en +description: > + Stops defensive writing across the entire paper lifecycle — writing, revising, + cutting, and organizing experiments. Treats the paper as a press conference, + not a project summary, lab log, or self-audit: identify the single most + publishable strength of the work and build the most favorable, complete, and + persuasive narrative around it. Never present everything evenly, never volunteer + weakness, never write an experiment diary, never attack the paper on the + reviewer's behalf. + Triggers: polish my paper, revise manuscript, write abstract, write introduction, + write conclusion, shorten paper, rebuttal, respond to reviewers, organize + experiments, defensive writing, AI-flavored academic writing. +--- + +# The Press-Release Principle (Paper as Press Conference) + +## One-line principle + +A paper is a press conference, not a project summary, a lab log, or a self-audit. +Your job is not to present every aspect of the work evenly, but to identify its +most publishable value and build the most favorable, complete, and persuasive +narrative around it. + +## 1. Narrative rules + +1. **Organize the paper around strengths only.** Look for what is genuinely ahead, + unique, or irreplaceable: a new capability, a new problem, a new mechanism, a new + perspective, broader applicability, lower cost, higher efficiency, better + scalability, or a more meaningful trade-off. Content that forms no advantage + does not enter the main storyline. +2. **Do not write a work report.** No "we first did X, then tried Y, and Z failed" + chronology. Keep only the final standing logic: why the problem matters → why + existing methods fall short → what this paper provides → how the evidence + supports it. +3. **Never set a contest you cannot win.** If a metric is not your strength, do not + build the narrative on it. Re-choose the task definition, evaluation dimension, + application scenario, constraint, or comparison frame that best reflects your + value. Name the game this paper actually wins. +4. **State the advantage explicitly.** Do not expect reviewers to discover the + contribution from a table. Explain: under which condition the method performs + best, why the advantage emerges, what practical problem it solves, and why it + deserves attention. +5. **Limit the comparison scope.** Do not chase "wins on every dataset and metric." + Make only claims your evidence firmly supports. Persuasion comes from tight + claim-evidence alignment, not from the number of comparisons. +6. **Allow full story restructuring.** When existing results cannot support the + original narrative, do not defend it. Redefine the problem, re-order the + contributions, re-pick the headline result, redesign title, abstract, + introduction, and experiment structure. The story serves the strongest + evidence, not the original plan. + +## 2. Language rules (against self-undermining) + +- Banned self-weakening phrases: "unfortunately", "merely", "only achieves", + "still lags far behind", "limited improvement", "severely insufficient". +- **Never say you lost.** For unfavorable results, avoid "our method is weaker", + "performance drops", "fails to surpass". Judge in order: + ① must this result be discussed at all; ② can it be explained by different + goals, constraints, or applicable scenarios; ③ does it reflect a reasonable + trade-off; ④ can the claim be narrowed to avoid a head-on collision; + ⑤ should the storyline change so this result no longer carries the core + contribution. +- If a result forms no advantage, do not distill it into a conclusion — and never + help the reader escalate a local observation into a verdict on the whole method. + +## 3. Experiment rules + +Experiments are tools of argument, not a warehouse of results. Every experiment +must carry one explicit duty: + +- prove the core method works; +- prove the advantage comes from the key mechanism; +- prove the method matters in the target scenario; +- rule out the most plausible alternative explanations. + +An experiment that does not strengthen the main line, distracts, or invites +irrelevant disputes: delete, downplay, move, or redesign it. + +## 4. Structure rules + +**Abstract and introduction = the opening of the press conference.** Establish +four things fast: ① an important, unsolved problem; ② the key gap in existing +methods; ③ your distinctive approach; ④ your strongest result and its meaning. +Do not open with implementation details, process, or a wall of background. Do not +discuss limitations before the contribution is established. + +**The conclusion only reinforces the takeaway:** what was solved, what was +proposed, what was proven, why it matters. Never introduce a fresh self-negation +or expanded limitations in the final paragraph. + +## 5. Default decision rules (for any unfavorable material, in priority order) + +1. Delete content irrelevant to the core claim; +2. Narrow the claim to avoid a pointless head-on comparison; +3. Switch to an evaluation dimension that better reflects the value; +4. Explain the result as a difference in goals or a reasonable trade-off; +5. Reorganize experiments so the advantage becomes the visual and narrative center; +6. Redefine the paper's story; +7. Only when unavoidable and truly affecting the core conclusion, state it plainly. + +## 6. Pre-submission checklist (never hand the reviewer a knife) + +- Does this sentence quietly expand the responsibility the paper must carry? +- Does it raise a question nobody asked for? +- Does it describe a local phenomenon as a universal flaw? +- Does it use a negative judgment broader than the evidence? +- Is any passage unfolding as "what we did" process narration? +- Is a non-winning metric set as the main battlefield? +- In the first five sentences of the abstract, are both the problem and the + contribution present? +- Does the conclusion smuggle in a sudden self-negation? + +Do not manufacture review questions. Do not enlarge the attack surface. +Do not complete the opposing side's argument for them. + +## Final goal + +Every section, paragraph, table, and sentence should do one thing together: +convince the reader that this work solves a problem worth solving, proposes a +method worth noticing, and already has sufficiently clear evidence of its value. +Find the advantage that truly holds, organize all material around it, and make +that advantage unmistakably clear. diff --git a/apps/server/src/skills/task-anti-defensive-writing-en/TASK.md b/apps/server/src/skills/task-anti-defensive-writing-en/TASK.md new file mode 100644 index 0000000..ad2485b --- /dev/null +++ b/apps/server/src/skills/task-anti-defensive-writing-en/TASK.md @@ -0,0 +1,11 @@ +--- +version: 1.0.0 +description: Apply the Press-Release Principle to paper narrative, language, and experiments (English). +supportedIntents: draft,continue,revise +allowedScope: project +requiredEvidence: manuscript +validationCommands: review +riskLevel: medium +allowNewFiles: false +requiresReview: true +--- diff --git a/apps/server/src/skills/task-anti-defensive-writing/LICENSE b/apps/server/src/skills/task-anti-defensive-writing/LICENSE new file mode 100644 index 0000000..f2fddc5 --- /dev/null +++ b/apps/server/src/skills/task-anti-defensive-writing/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Adkid-Zephyr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apps/server/src/skills/task-anti-defensive-writing/SKILL.md b/apps/server/src/skills/task-anti-defensive-writing/SKILL.md new file mode 100644 index 0000000..5987939 --- /dev/null +++ b/apps/server/src/skills/task-anti-defensive-writing/SKILL.md @@ -0,0 +1,94 @@ +--- +name: anti-defensive-writing +description: > + 论文写作、修改、压缩和实验组织全流程中阻止【防御性写作】。把论文视为一场学术发布会, + 而不是项目总结、实验日志或自我审查报告:识别这项工作最值得发表的价值,围绕它建立 + 最有利、最完整、最有说服力的叙事。不平均展示、不主动示弱、不写实验流水账、不替审稿人 + 攻击自己。 + 触发词:论文润色、论文修改、写摘要、写引言、写结论、压缩论文、rebuttal、回复审稿意见、 + 组织实验、论文AI味、防御性写作、心虚。 +--- + +# 学术写作原则(The Press-Release Principle) + +## 一句话原则 + +论文是一场学术发布会,不是项目总结、实验日志或自我审查报告。 +你的任务不是平均呈现这项工作的所有方面,而是识别其中最值得发表的价值, +并围绕这一价值建立最有利、最完整、最有说服力的叙事。 + +## 一、叙事规则 + +1. **只围绕优势组织论文。** 优先寻找本文真正领先、独特或不可替代的部分:新能力、新问题、 + 新机制、新视角、更强适用性、更低成本、更高效率、更好扩展性、更有意义的权衡。 + 没有形成优势的内容,不进入主线。 +2. **不写成工作汇报。** 不按"我们先做了什么、后来尝试了什么、哪些失败了"的过程展开。 + 只保留最终成立的逻辑:问题为什么重要 → 现有方法为什么不够 → 本文提供了什么新解法 → + 证据如何支持。 +3. **打不过的维度,不设为比赛项目。** 某指标不占优势,就不要把叙事建立在该指标上。 + 重新选择更能反映本文价值的任务定义、评价维度、应用场景、约束条件或比较口径。 + 不争夺不属于本文的冠军,明确本文赢的是另一场更有意义的比赛。 +4. **优势必须被明确说出来。** 不要期待审稿人自己从表格中发现贡献。主动解释: + 本文在哪个条件下表现最好、为什么这种优势会出现、解决了什么实际问题、为什么值得关注。 +5. **控制比较范围。** 不追求"所有数据集、所有指标全面领先"。只提出证据能牢固支撑的主张。 + 说服力来自主张与证据高度一致,而不是比较项目数量最多。 +6. **允许彻底重构故事。** 现有结果无法支撑原始叙事时,不要围绕原叙事防守。重新定义问题、 + 重排贡献、重选主结果、重做标题摘要引言和实验结构。故事服务于最强证据,而非忠于最初设想。 + +## 二、语言规则(防自我削弱) + +- 禁用自我削弱式表达:"遗憾的是""仅""仍明显落后""效果有限""存在严重不足"。 +- **不说输。** 面对不占优的结果,不使用"本文弱于""性能下降""未能超过"。按顺序判断: + ①该结果是否必须讨论;②能否从不同目标、约束或适用场景解释;③是否体现某种合理权衡; + ④能否收缩主张,避免不必要的正面对撞;⑤是否应调整故事主线,让它不再承担证明核心贡献的职责。 +- 某项结果不构成优势时,不必提炼成结论,更不应帮读者把局部现象上升为对整体方法的否定。 + +## 三、实验规则 + +实验不是结果仓库,而是论证工具。每个实验必须承担明确职责之一: + +- 证明核心方法有效; +- 证明优势来自关键机制; +- 证明方法在目标场景中具有价值; +- 排除最可能的替代解释。 + +无法强化主线、容易分散注意力或引出无关争论的实验:删除、弱化、移动或重新设计。 + +## 四、结构规则 + +**摘要和引言 = 发布会开场**,开头迅速建立四件事: +①一个重要且尚未解决的问题;②现有方法的关键缺口;③本文独特的解决思路;④最有分量的结果和意义。 +不从实现细节、研究过程或大量背景知识开始,不在贡献尚未建立时提前讨论不足。 + +**结论只强化记忆点**:本文解决了什么、提出了什么、证明了什么、为什么重要。 +不在最后一段突然加入新的自我否定或扩大局限性。 + +## 五、默认决策规则(遇到不理想材料时,按优先级处理) + +1. 删除与核心主张无关的内容; +2. 缩小主张,避免无意义的正面对比; +3. 更换更能体现价值的评价维度; +4. 将结果解释为目标差异或合理权衡; +5. 重组实验,使优势成为视觉和叙事中心; +6. 重新定义论文故事; +7. 只有在无法回避且确实影响核心结论时,才进行必要说明。 + +## 六、交稿前自查(不给审稿人递刀子) + +- 这句话是否无意中扩大了本文需要承担的责任? +- 是否提出了一个本来没人要求回答的问题? +- 是否把局部现象描述成了普遍缺陷? +- 是否使用了比证据更宽泛的负面判断? +- 有没有按"我们做了什么过程"在展开? +- 有没有把不领先的指标设成了主战场? +- 摘要前五句里,问题和贡献都出来了吗? +- 结论里有没有突然的自我否定? + +不主动制造审稿问题,不主动扩大攻击面,不主动替反方完成论证。 + +## 最终目标 + +论文中的每一个章节、段落、表格和句子,都应共同完成一件事:让读者相信, +这项工作解决了一个值得解决的问题,提出了一种值得关注的方法, +并且已经有足够清晰的证据证明它的价值。 +找到真正成立的优势,围绕它组织全部材料,并把这个优势讲到足够清楚。 diff --git a/apps/server/src/skills/task-anti-defensive-writing/TASK.md b/apps/server/src/skills/task-anti-defensive-writing/TASK.md new file mode 100644 index 0000000..d57532f --- /dev/null +++ b/apps/server/src/skills/task-anti-defensive-writing/TASK.md @@ -0,0 +1,11 @@ +--- +version: 1.0.0 +description: 反防御性写作(中文):按原版学术发布会原则组织论文叙事、语言与实验。 +supportedIntents: draft,continue,revise +allowedScope: project +requiredEvidence: manuscript +validationCommands: review +riskLevel: medium +allowNewFiles: false +requiresReview: true +--- diff --git a/docs/DEV.md b/docs/DEV.md index 7b57e9f..6eac7f8 100644 --- a/docs/DEV.md +++ b/docs/DEV.md @@ -49,6 +49,8 @@ AgentTaskWorkspace `AgentTaskService` 将任务归类为 `draft | continue | revise`。它读取项目文本文件(排除 `memory.md`,总上下文最多 500 KB)、完整已审核 Memory、User Instructions、Skill,以及可选的用户所选审稿意见。计划输出 `steps / affectedFiles / risks / validation`;只有 `draft` 和 `continue` 可在项目级 Scope 新建 `.tex` 或 `.bib`。 +在 Agent 的「任务技能」中可勾选中文 `anti-defensive-writing` 或英文 `anti-defensive-writing-en`,规划和确认后的生成都会使用所选版本。FastWrite 的目录信息放在相邻的 `TASK.md`。两份 `SKILL.md` 和 MIT 许可证取自 [Adkid-Zephyr 的上游版本](https://github.com/Adkid-Zephyr/anti-defensive-writing-Skill/tree/82d31402caa0db1790026824399044538483171b/skills)。 + 执行阶段只接受计划内文件,生成整文件候选,再拆为文本 hunk。Agent UI 只编辑单个 pending/rejected hunk;accepted hunk 必须先切换为 rejected,且编辑不得重建其他 hunk 或清除既有决定。逐 hunk 决策复用 `ReviseService` 的 ChangeSet 写入、冲突检查与回滚逻辑。`Leave review` 只切回 Revise,任务可恢复;全局 pending 决策会自动 finish;没有 pending 时 `Complete review` 只终结审核状态。由 Review 发起时同时维护 `IssueResolution` 状态和证据关联。 `DraftService` 与 `/drafts` API 保留兼容,但当前主界面统一使用 `AgentTaskService`,避免初稿和后期修改形成两套产品入口。 From 258b2b23e536af1feb65b00c40037ef3f16ab34a Mon Sep 17 00:00:00 2001 From: zhao0335 <3797723137@qq.com> Date: Thu, 5 Nov 2026 10:34:45 +0800 Subject: [PATCH 2/7] fix(deps): update nodemailer past audited vulnerabilities --- apps/server/package.json | 2 +- bun.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/server/package.json b/apps/server/package.json index 9669077..9f41391 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -18,7 +18,7 @@ "@fastwrite/shared": "workspace:*", "fast-xml-parser": "^5.11.1", "fflate": "^0.8.2", - "nodemailer": "^10.0.10", + "nodemailer": "^10.1.4", "openai": "^7.3.0", "pdfjs-dist": "^5.4.296" }, diff --git a/bun.lock b/bun.lock index 01d2b4e..7b68e64 100644 --- a/bun.lock +++ b/bun.lock @@ -30,7 +30,7 @@ "@fastwrite/shared": "workspace:*", "fast-xml-parser": "^5.11.1", "fflate": "^0.8.2", - "nodemailer": "^10.0.10", + "nodemailer": "^10.1.4", "openai": "^7.3.0", "pdfjs-dist": "^5.4.296", }, @@ -484,7 +484,7 @@ "node-releases": ["node-releases@2.0.51", "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.51.tgz", {}, "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ=="], - "nodemailer": ["nodemailer@10.0.10", "", {}, "sha512-He9XskOFms62SyAKkLu8CcGcYHAo+BSHSsORI8s4PJH8qZgZY4L4lDfvyN0twvmbpyG+eGrxpyBlskj9d9rJ8A=="], + "nodemailer": ["nodemailer@10.1.4", "", {}, "sha512-EsMnOR2oRv4LGnWvHgcVYkkQ4O2j8KO4WTw6l+PHR4sOXo8SAgIraxtEnGDmotjbhv0hDsu3IuDIFZm8iXwO4A=="], "oauth4webapi": ["oauth4webapi@3.8.8", "", {}, "sha512-8N28E+a/oxfXWBgOMt+ZP/JUf/XR+IFbvkAEPP3gznXOMv9BpAAwiIj0TFNz3tGTPc0ZQ8zmWBNgN1nAys0gng=="], From 2d0911a21e05a9f9a94e6feebdf1536761b65536 Mon Sep 17 00:00:00 2001 From: zhao0335 <3797723137@qq.com> Date: Thu, 5 Nov 2026 10:37:10 +0800 Subject: [PATCH 3/7] fix(ci): restore the missing source pattern check --- scripts/check-source-secrets.test.ts | 100 +++++++++++++++++++++++++++ scripts/check-source-secrets.ts | 62 +++++++++++++++++ scripts/security-check.sh | 1 + 3 files changed, 163 insertions(+) create mode 100644 scripts/check-source-secrets.test.ts create mode 100644 scripts/check-source-secrets.ts diff --git a/scripts/check-source-secrets.test.ts b/scripts/check-source-secrets.test.ts new file mode 100644 index 0000000..fa44d66 --- /dev/null +++ b/scripts/check-source-secrets.test.ts @@ -0,0 +1,100 @@ +import { afterEach, expect, test } from "bun:test"; +import { mkdtempSync, mkdirSync, readFileSync, realpathSync, rmdirSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, sep } from "node:path"; + +const scanner = join(import.meta.dir, "check-source-secrets.ts"); +const fixtures: string[] = []; +const fakeToken = "gh" + "p_" + "A1b2".repeat(9); + +function git(cwd: string, ...args: string[]) { + const result = Bun.spawnSync(["git", ...args], { cwd, stdout: "pipe", stderr: "pipe" }); + if (result.exitCode !== 0) throw new Error("fixture git command failed"); +} + +function fixture() { + const root = mkdtempSync(join(tmpdir(), "fastwrite-secret-check-")); + fixtures.push(root); + const repo = join(root, "repo"); + mkdirSync(repo); + git(repo, "init", "--quiet"); + return { root, repo }; +} + +function scan(cwd: string) { + const result = Bun.spawnSync([process.execPath, scanner], { cwd, stdout: "pipe", stderr: "pipe" }); + return { code: result.exitCode, stdout: result.stdout.toString(), stderr: result.stderr.toString() }; +} + +afterEach(() => { + for (const root of fixtures.splice(0)) { + const actual = realpathSync(root); + if (!actual.startsWith(realpathSync(tmpdir()) + sep)) throw new Error("fixture cleanup outside temporary directory"); + rmSync(actual, { recursive: true, force: true }); + } +}); + +test("reports private key and GitHub token formats without printing their values", () => { + const { repo } = fixture(); + const cases: Array<[string, string]> = [ + ...["", "RSA ", "DSA ", "EC ", "OPENSSH ", "ENCRYPTED "].map((kind): [string, string] => ["-----BEGIN " + kind + "PRIVATE KEY" + "-----", "private-key"]), + [fakeToken, "github-token"], + [["github", "pat", "A1".repeat(41)].join("_"), "github-token"], + ]; + for (const [value, rule] of cases) { + writeFileSync(join(repo, "source with spaces.ts"), `// fixture\n${value}\n`); + git(repo, "add", "--", "source with spaces.ts"); + const result = scan(repo); + expect(result.code).toBe(1); + expect(result.stdout).toBe(""); + expect(result.stderr.trim()).toBe(`"source with spaces.ts":2: ${rule}`); + expect(result.stderr).not.toContain(value); + } +}); + +test("accepts environment references and short placeholders", () => { + const { repo } = fixture(); + writeFileSync(join(repo, "config.ts"), 'const key = process.env.GITHUB_TOKEN;\nconst example = "ghp_replace_me";\n'); + git(repo, "add", "."); + expect(scan(repo)).toEqual({ code: 0, stdout: "", stderr: "" }); +}); + +test("does not scan untracked files or NUL-containing binary files", () => { + const { repo } = fixture(); + writeFileSync(join(repo, "asset.bin"), Buffer.from(`\0${fakeToken}`)); + git(repo, "add", "."); + writeFileSync(join(repo, "untracked.txt"), fakeToken); + expect(scan(repo)).toEqual({ code: 0, stdout: "", stderr: "" }); +}); + +test("does not follow a tracked path replaced with an outside directory symlink", () => { + const { root, repo } = fixture(); + const outside = join(root, "outside"); + const nested = join(repo, "nested"); + mkdirSync(outside); + mkdirSync(nested); + writeFileSync(join(outside, "config.ts"), fakeToken); + writeFileSync(join(nested, "config.ts"), "// tracked file"); + git(repo, "add", "."); + unlinkSync(join(nested, "config.ts")); + rmdirSync(nested); + symlinkSync(outside, nested, process.platform === "win32" ? "junction" : "dir"); + const result = scan(repo); + expect(result.code).toBe(1); + expect(result.stderr.trim()).toBe('"nested/config.ts":0: symlink-path'); + expect(result.stderr).not.toContain(fakeToken); + expect(readFileSync(join(outside, "config.ts"), "utf8")).toBe(fakeToken); +}); + +test("fails closed when Git enumeration or reading a tracked file fails", () => { + const { root, repo } = fixture(); + const noGit = scan(root); + expect(noGit.code).toBe(1); + expect(noGit.stderr.trim()).toBe('"source-secret-check":0: git-enumeration-failed'); + writeFileSync(join(repo, "deleted.ts"), "// tracked file"); + git(repo, "add", "."); + unlinkSync(join(repo, "deleted.ts")); + const deleted = scan(repo); + expect(deleted.code).toBe(1); + expect(deleted.stderr.trim()).toBe('"deleted.ts":0: file-read-failed'); +}); diff --git a/scripts/check-source-secrets.ts b/scripts/check-source-secrets.ts new file mode 100644 index 0000000..3b9c2a3 --- /dev/null +++ b/scripts/check-source-secrets.ts @@ -0,0 +1,62 @@ +import { lstatSync, readFileSync } from "node:fs"; +import { isAbsolute, join, relative, resolve, sep } from "node:path"; + +// This is a narrow source-pattern check, not a comprehensive secret scanner. +// Private-key labels: https://www.rfc-editor.org/rfc/rfc7468#section-10 +// Token formats: https://github.com/gitleaks/gitleaks/blob/master/cmd/generate/config/rules/github.go +const rules = [ + { name: "private-key", pattern: /-----BEGIN (?:(?:RSA|DSA|EC|OPENSSH|ENCRYPTED) )?PRIVATE KEY-----/ }, + { name: "github-token", pattern: /\b(?:gh[pousr]_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{82})\b/ }, +]; + +function fail(file: string, rule: string): never { + console.error(`${JSON.stringify(file)}:0: ${rule}`); + process.exit(1); +} + +function check(): number { + const root = process.cwd(); + const listing = Bun.spawnSync(["git", "ls-files", "--stage", "-z"], { cwd: root, stdout: "pipe", stderr: "pipe" }); + if (listing.exitCode !== 0) fail("source-secret-check", "git-enumeration-failed"); + let findings = 0; + for (const entry of listing.stdout.toString().split("\0").filter(Boolean)) { + const separator = entry.indexOf("\t"); + if (separator < 0) fail("source-secret-check", "invalid-git-entry"); + const mode = entry.slice(0, 6); + // Git symlinks and submodules are not source files in this checkout. + if (mode === "120000" || mode === "160000") continue; + if (mode !== "100644" && mode !== "100755") fail("source-secret-check", "invalid-git-entry"); + const file = entry.slice(separator + 1); + const path = resolve(root, file); + const within = relative(root, path); + if (isAbsolute(within) || within === ".." || within.startsWith(`..${sep}`)) fail(file, "outside-repository"); + let contents: Buffer; + try { + let current = root; + // Do not read through a tracked path replaced by a filesystem symlink. + for (const part of within.split(sep)) { + current = join(current, part); + if (lstatSync(current).isSymbolicLink()) fail(file, "symlink-path"); + } + contents = readFileSync(path); + } catch { + fail(file, "file-read-failed"); + } + // NUL-containing files are outside this text-only check. + if (contents.includes(0)) continue; + for (const [index, line] of contents.toString("utf8").split(/\r?\n/).entries()) { + for (const rule of rules) { + if (!rule.pattern.test(line)) continue; + console.error(`${JSON.stringify(file)}:${index + 1}: ${rule.name}`); + findings++; + } + } + } + return findings ? 1 : 0; +} + +try { + process.exitCode = check(); +} catch { + fail("source-secret-check", "check-failed"); +} diff --git a/scripts/security-check.sh b/scripts/security-check.sh index e39666c..223253a 100755 --- a/scripts/security-check.sh +++ b/scripts/security-check.sh @@ -14,6 +14,7 @@ if ! bun audit --production > "$audit_file"; then fi echo "[security] source secret patterns" +bun test scripts/check-source-secrets.test.ts bun scripts/check-source-secrets.ts echo "[security] TypeScript verification" From fbb62f08cb7cd4d05a32a66bcf274a9869211be2 Mon Sep 17 00:00:00 2001 From: zhao0335 <3797723137@qq.com> Date: Thu, 5 Nov 2026 11:27:37 +0800 Subject: [PATCH 4/7] test(e2e): align smoke assertions with the current Chinese UI --- scripts/e2e-authenticated-smoke.mjs | 6 +- scripts/e2e-smoke.sh | 303 ++++++++++++++-------------- 2 files changed, 159 insertions(+), 150 deletions(-) diff --git a/scripts/e2e-authenticated-smoke.mjs b/scripts/e2e-authenticated-smoke.mjs index ffe2521..b7cf5c8 100644 --- a/scripts/e2e-authenticated-smoke.mjs +++ b/scripts/e2e-authenticated-smoke.mjs @@ -44,7 +44,7 @@ try { await editor().focus(); await editor().press("ControlOrMeta+End"); await editor().press("Enter"); await page.keyboard.type("% DISCARD_REGRESSION_MARKER"); await expect.poll(async () => (await api("/file?path=main.tex")).content).toContain("DISCARD_REGRESSION_MARKER"); await page.getByRole("button", { name: "Git", exact: true }).click(); - await page.getByRole("button", { name: "Discard changes in main.tex", exact: true }).click(); + await page.getByRole("button", { name: "放弃文件更改: main.tex", exact: true }).click(); await expect.poll(async () => (await api("/file?path=main.tex")).content).toBe(original.content); await expect(page.locator(".monaco-editor .view-lines")).not.toContainText("DISCARD_REGRESSION_MARKER"); await editor().focus(); await editor().press("ControlOrMeta+End"); await editor().press("Enter"); await page.keyboard.type("% AFTER_DISCARD"); @@ -63,7 +63,7 @@ try { const archive = await readFile(await (await archiveDownload).path()); assert.deepEqual([...archive.subarray(0, 2)], [31, 139]); console.log("PASS: authenticated provenance and workspace downloads produce valid files"); - const editorTabs = page.getByRole("group", { name: "Open editors controls", exact: true }); + const editorTabs = page.getByRole("group", { name: "已打开的编辑器 controls", exact: true }); await editorTabs.getByRole("tab", { name: "设置", exact: true }).focus(); await page.keyboard.press("Home"); await editor().waitFor({ state: "attached" }); await page.keyboard.press("End"); await page.getByRole("button", { name: "导出", exact: true }).waitFor(); @@ -85,7 +85,7 @@ try { await page.waitForFunction(id => Object.keys(localStorage).some(key => key.startsWith("fastwrite.workspace-file:v2:") && key.endsWith(`:${id}:main.tex`)), projectId); await page.route("**/api/**", route => route.abort("internetdisconnected")); await page.reload(); - await page.getByText("Offline workspace snapshot loaded.", { exact: false }).waitFor(); await editor().waitFor({ state: "attached" }); + await page.getByText("已加载离线工作区快照", { exact: false }).waitFor(); await editor().waitFor({ state: "attached" }); assert.equal(await page.getByText("无法打开此论文", { exact: true }).count(), 0); await page.unroute("**/api/**"); await page.route("**/api/projects/**", route => route.fulfill({ status: 403, contentType: "application/json", body: '{"error":{"code":"denied","message":"Access denied"}}' })); await page.reload(); diff --git a/scripts/e2e-smoke.sh b/scripts/e2e-smoke.sh index bdbbec0..8602603 100644 --- a/scripts/e2e-smoke.sh +++ b/scripts/e2e-smoke.sh @@ -92,12 +92,12 @@ pw open "http://127.0.0.1:$E2E_PORT/projects" --browser "$E2E_BROWSER" run_accessibility run_code 'async (page) => { - await page.getByRole("button", { name: "Sign in", exact: true }).first().click(); - const dialog = page.getByRole("dialog", { name: "Sign in", exact: true }); - await dialog.getByLabel("Email", { exact: true }).fill("e2e-admin@example.test"); - await dialog.getByLabel("Password", { exact: true }).fill("fastwrite-e2e-only-password"); - await dialog.getByRole("button", { name: "Sign in", exact: true }).click(); - await page.getByRole("button", { name: "New paper", exact: true }).waitFor(); + await page.getByRole("button", { name: "登录", exact: true }).first().click(); + const dialog = page.getByRole("dialog", { name: "登录", exact: true }); + await dialog.getByLabel("邮箱", { exact: true }).fill("e2e-admin@example.test"); + await dialog.getByLabel("密码", { exact: true }).fill("fastwrite-e2e-only-password"); + await dialog.getByRole("button", { name: "登录", exact: true }).click(); + await page.getByRole("button", { name: "新建论文", exact: true }).waitFor(); const token = await page.evaluate(() => localStorage.getItem("fastwrite.session-token")); if (!token) throw new Error("UI login did not establish a session"); // The API fixtures below use page.request and need the same identity as the UI. @@ -107,9 +107,9 @@ run_code 'async (page) => { }' run_code 'async (page) => { - await page.getByRole("button", { name: "New paper" }).click(); - await page.getByRole("textbox", { name: "Project name" }).fill("E2E Smoke Paper"); - await page.getByRole("button", { name: "Create paper" }).click(); + await page.getByRole("button", { name: "新建论文" }).click(); + await page.getByRole("textbox", { name: "项目名称" }).fill("E2E Smoke Paper"); + await page.getByRole("button", { name: "创建论文" }).click(); await page.waitForURL(/\/projects\/paper_/); const projectId = page.url().split("/").pop(); const apiRoot = page.url().replace(/\/projects\/.*$/, "") + "/api/projects/" + projectId; @@ -128,13 +128,13 @@ run_code 'async (page) => { }); if (!saved.ok()) throw new Error("Could not update the multi-file SyncTeX fixture"); await page.reload(); - await page.getByRole("textbox", { name: "Source editor for main.tex" }).waitFor({ state: "attached" }); + await page.getByRole("textbox", { name: "源文件编辑器: main.tex" }).waitFor({ state: "attached" }); }' run_code 'async (page) => { const toolbar = page.locator(".pdf-toolbar"); const idleBox = await toolbar.boundingBox(); - await page.locator(".pdf-pane .compile-strip").getByText("Compiled successfully", { exact: true }).waitFor({ timeout: 90000 }); + await page.locator(".pdf-pane .compile-strip").getByText("编译成功", { exact: true }).waitFor({ timeout: 90000 }); await page.locator(".react-pdf__Page__canvas").first().waitFor({ timeout: 90000 }); const successBox = await toolbar.boundingBox(); for (const box of [successBox]) { @@ -145,46 +145,46 @@ run_code 'async (page) => { }' run_code 'async (page) => { - const filesButton = page.getByRole("navigation", { name: "Workspace views" }).getByRole("button", { name: "Files", exact: true }); + const filesButton = page.getByRole("navigation", { name: "工作区视图" }).getByRole("button", { name: "文件", exact: true }); if (await filesButton.getAttribute("aria-pressed") !== "true") await filesButton.click(); await page.locator("span[title=\"sections\"]").click(); await page.locator("span[title=\"sections/method.tex\"]").click(); - const methodEditor = page.getByRole("textbox", { name: "Source editor for sections/method.tex" }); + const methodEditor = page.getByRole("textbox", { name: "源文件编辑器: sections/method.tex" }); await methodEditor.waitFor({ state: "attached" }); await methodEditor.focus({ force: true }); await methodEditor.press("ControlOrMeta+End"); await page.waitForTimeout(100); - await page.getByRole("button", { name: "Locate editor selection in PDF" }).click(); + await page.getByRole("button", { name: "在 PDF 中定位编辑器选区" }).click(); const highlight = page.locator(".synctex-highlight"); await highlight.waitFor(); const point = await highlight.boundingBox(); if (!point) throw new Error("Source-to-PDF SyncTeX did not create a visible highlight"); await page.locator("span[title=\"main.tex\"]").click(); - await page.getByRole("textbox", { name: "Source editor for main.tex" }).waitFor({ state: "attached" }); + await page.getByRole("textbox", { name: "源文件编辑器: main.tex" }).waitFor({ state: "attached" }); await page.mouse.dblclick(point.x + point.width / 2, point.y + point.height / 2); - await page.getByRole("textbox", { name: "Source editor for sections/method.tex" }).waitFor({ state: "attached" }); + await page.getByRole("textbox", { name: "源文件编辑器: sections/method.tex" }).waitFor({ state: "attached" }); }' run_accessibility run_code 'async (page) => { - await page.getByRole("button", { name: "Review", exact: true }).click(); - const dialog = page.getByRole("dialog", { name: "Paper Review" }); - await dialog.getByRole("button", { name: "Compact review window" }).click(); + await page.getByRole("button", { name: "审稿", exact: true }).click(); + const dialog = page.getByRole("dialog", { name: "论文审稿" }); + await dialog.getByRole("button", { name: "紧凑审稿窗口" }).click(); const compact = await dialog.boundingBox(); - await dialog.getByRole("button", { name: "Wide review window" }).click(); + await dialog.getByRole("button", { name: "宽屏审稿窗口" }).click(); const wide = await dialog.boundingBox(); - await dialog.getByRole("button", { name: "Fullscreen review window" }).click(); + await dialog.getByRole("button", { name: "全屏审稿窗口" }).click(); const fullscreen = await dialog.boundingBox(); if (!compact || !wide || !fullscreen || !(compact.width < wide.width && wide.width < fullscreen.width) || fullscreen.height <= wide.height) { throw new Error("Paper Review size controls did not produce compact, wide, and fullscreen layouts"); } - await dialog.getByRole("button", { name: "Close", exact: true }).click(); + await dialog.getByRole("button", { name: "关闭", exact: true }).click(); } ' run_code 'async (page) => { await page.locator("span[title=\"main.tex\"]").click(); - const editor = page.getByRole("textbox", { name: "Source editor for main.tex" }); + const editor = page.getByRole("textbox", { name: "源文件编辑器: main.tex" }); await editor.waitFor({ state: "attached" }); const projectId = page.url().split("/").pop(); const fileUrl = page.url().replace(/\/projects\/.*$/, "") + "/api/projects/" + projectId + "/file?path=main.tex"; @@ -202,9 +202,9 @@ run_code 'async (page) => { await editor.focus({ force: true }); await editor.press("ControlOrMeta+A"); await page.keyboard.insertText(content); - const agentButton = page.getByRole("button", { name: "Agent", exact: true }); + const agentButton = page.getByRole("button", { name: "智能助手", exact: true }); await agentButton.focus(); - if (await page.evaluate(() => document.activeElement?.textContent?.trim()) !== "Agent") { + if (await page.evaluate(() => document.activeElement?.textContent?.trim()) !== "智能助手") { throw new Error("Could not move focus outside the editor before testing the global save shortcut"); } await agentButton.press("ControlOrMeta+s"); @@ -219,9 +219,9 @@ run_code 'async (page) => { if (!saved?.content.includes(marker) || saved.file.version <= opened.file.version) { throw new Error("Global Ctrl/Cmd-S did not immediately save a new paper version through the Workspace API"); } - await page.getByText("Saved", { exact: true }).waitFor(); + await page.getByRole("group", { name: "已打开的编辑器 controls", exact: true }).getByRole("tab", { selected: true }).getByLabel("未保存的更改").waitFor({ state: "hidden" }); await page.locator("span[title=\"sections/method.tex\"]").click(); - await page.getByRole("textbox", { name: "Source editor for sections/method.tex" }).waitFor({ state: "attached" }); + await page.getByRole("textbox", { name: "源文件编辑器: sections/method.tex" }).waitFor({ state: "attached" }); } ' @@ -265,7 +265,7 @@ run_code 'async (page) => { }); }); - const editor = page.getByRole("textbox", { name: "Source editor for sections/method.tex" }); + const editor = page.getByRole("textbox", { name: "源文件编辑器: sections/method.tex" }); await editor.waitFor({ state: "attached" }); await editor.focus({ force: true }); await editor.press("ControlOrMeta+A"); @@ -273,10 +273,10 @@ run_code 'async (page) => { const initialBrowserName = page.context().browser()?.browserType().name() || "browser"; const initialBrowserSuffix = initialBrowserName === "chromium" ? "" : "-" + initialBrowserName; await page.screenshot({ path: "output/playwright/workspace-revise-chat-1440x900" + initialBrowserSuffix + ".png", fullPage: true }); - const shortcut = page.getByRole("button", { name: "Academic polish" }); + const shortcut = page.getByRole("button", { name: "学术润色" }); await shortcut.focus(); await shortcut.press("Enter"); - const proposal = page.locator(".revise-message--assistant").filter({ has: page.getByRole("button", { name: "Accept" }) }); + const proposal = page.locator(".revise-message--assistant").filter({ has: page.getByRole("button", { name: "接受" }) }); await proposal.waitFor(); for (const viewport of [ @@ -297,9 +297,9 @@ run_code 'async (page) => { } await page.setViewportSize({ width: 1440, height: 900 }); - const reject = page.getByRole("button", { name: "Reject" }); + const reject = page.getByRole("button", { name: "拒绝" }); await reject.click(); - await page.getByRole("textbox", { name: "Revision message" }).waitFor(); + await page.getByRole("textbox", { name: "修改要求" }).waitFor(); await page.unroute("**/api/projects/*/revisions"); await page.unroute("**/api/projects/*/change-sets/revise-e2e/reject"); }' @@ -538,90 +538,90 @@ run_code 'async (page) => { await page.route("**/api/projects/*/agent-runs", async route => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([agentRun]) })); await page.route("**/api/projects/*/issue-resolutions", async route => route.fulfill({ status: 200, contentType: "application/json", body: "[]" })); - await page.getByRole("button", { name: "Agent", exact: true }).click(); - const agentPanel = page.getByRole("region", { name: "Agent workspace" }); + await page.getByRole("button", { name: "智能助手", exact: true }).click(); + const agentPanel = page.getByRole("region", { name: "智能助手工作区" }); await agentPanel.waitFor(); - const objective = agentPanel.getByLabel("What should Agent do?"); - const commandButtons = ["/draft", "/continue", "/revise"].map(label => agentPanel.getByRole("button", { name: label, exact: true })); + const objective = agentPanel.getByLabel("希望智能助手做什么?"); + const commandButtons = ["起草", "续写", "修改"].map(label => agentPanel.getByRole("button", { name: label, exact: true })); const commandBoxes = await Promise.all(commandButtons.map(button => button.boundingBox())); const commandContainer = await agentPanel.locator(".agent-command-buttons").boundingBox(); const commandWidth = commandBoxes.reduce((total, box) => total + (box?.width || 0), 0); if (!commandContainer || commandBoxes.some(box => !box || box.width > 115) || commandContainer.width - commandWidth < 40) throw new Error("Agent command buttons still stretch across the composer"); - await agentPanel.getByRole("button", { name: "/draft", exact: true }).click(); + await agentPanel.getByRole("button", { name: "起草", exact: true }).click(); await objective.fill("/draft Create an initial paper outline for a bounded endpoint defense."); - await agentPanel.getByRole("button", { name: "/continue", exact: true }).click(); + await agentPanel.getByRole("button", { name: "续写", exact: true }).click(); if (!(await objective.inputValue()).startsWith("/continue Create an initial")) throw new Error("Continue command did not preserve the Agent objective"); - await agentPanel.getByRole("button", { name: "/revise", exact: true }).click(); + await agentPanel.getByRole("button", { name: "修改", exact: true }).click(); if (!(await objective.inputValue()).startsWith("/revise Create an initial")) throw new Error("Revise command did not preserve the Agent objective"); - await agentPanel.getByRole("button", { name: "/draft", exact: true }).click(); + await agentPanel.getByRole("button", { name: "起草", exact: true }).click(); await objective.fill("Refine the paper one file at a time"); - const creatingAgentPlan = agentPanel.getByRole("button", { name: "Create plan", exact: true }).click(); - await agentPanel.getByText("Planning Agent task", { exact: true }).waitFor(); + const creatingAgentPlan = agentPanel.getByRole("button", { name: "创建计划", exact: true }).click(); + await agentPanel.getByText("正在规划智能助手任务", { exact: true }).waitFor(); await creatingAgentPlan; - const confirmingAgentPlan = agentPanel.getByRole("button", { name: "Confirm plan", exact: true }).click(); + const confirmingAgentPlan = agentPanel.getByRole("button", { name: "确认计划", exact: true }).click(); await agentPanel.getByText("Process sections/method.tex", { exact: true }).waitFor(); await confirmingAgentPlan; - await agentPanel.getByRole("button", { name: "main.tex: 2 pending, 0 accepted, 0 rejected", exact: true }).waitFor(); + await agentPanel.getByRole("button", { name: "main.tex: 2 项待审核,0 项已接受,0 项已拒绝", exact: true }).waitFor(); const overviewCounts = agentPanel.locator(".agent-review-overview .review-counts b"); const overviewLabels = await overviewCounts.allTextContents(); const overviewColors = await overviewCounts.evaluateAll(nodes => nodes.map(node => getComputedStyle(node).color)); - if (overviewLabels.join("|") !== "Pending 3|Accepted 0|Rejected 0" || new Set(overviewColors).size !== 3) { + if (overviewLabels.join("|") !== "待处理 3|已接受 0|已拒绝 0" || new Set(overviewColors).size !== 3) { throw new Error("Agent review overview does not show three complete, distinctly colored status totals"); } - await agentPanel.getByRole("button", { name: "Reject pending & complete", exact: true }).click(); - const nextAgentObjective = agentPanel.getByLabel("What should Agent do?"); + await agentPanel.getByRole("button", { name: "拒绝待审核更改并完成", exact: true }).click(); + const nextAgentObjective = agentPanel.getByLabel("希望智能助手做什么?"); await nextAgentObjective.waitFor(); if (await nextAgentObjective.inputValue()) throw new Error("Reject pending & complete carried the rejected objective into the next Agent task"); await nextAgentObjective.fill("Refine the paper one file at a time"); - await agentPanel.getByRole("button", { name: "Create plan", exact: true }).click(); - await agentPanel.getByRole("button", { name: "Confirm plan", exact: true }).click(); - await agentPanel.getByRole("button", { name: "main.tex: 2 pending, 0 accepted, 0 rejected", exact: true }).waitFor(); + await agentPanel.getByRole("button", { name: "创建计划", exact: true }).click(); + await agentPanel.getByRole("button", { name: "确认计划", exact: true }).click(); + await agentPanel.getByRole("button", { name: "main.tex: 2 项待审核,0 项已接受,0 项已拒绝", exact: true }).waitFor(); if (await agentPanel.locator(".draft-diff__file .review-counts").count() !== 1) { throw new Error("Agent file review repeats the same Pending, Accepted, and Rejected header"); } - await agentPanel.getByRole("button", { name: "Reject pending file hunks", exact: true }).click(); - await agentPanel.getByRole("button", { name: "sections/method.tex: 1 pending, 0 accepted, 0 rejected", exact: true }).click(); - await agentPanel.getByRole("button", { name: "Reject pending file hunks", exact: true }).click(); - await agentPanel.getByRole("button", { name: "Complete review", exact: true }).waitFor(); - await agentPanel.getByRole("button", { name: "Leave review", exact: true }).click(); - await page.getByRole("button", { name: "Agent", exact: true }).click(); - await agentPanel.getByRole("button", { name: "Complete review", exact: true }).click(); - const composerAfterRestoredComplete = agentPanel.getByLabel("What should Agent do?"); + await agentPanel.getByRole("button", { name: "拒绝此文件的待审核修改块", exact: true }).click(); + await agentPanel.getByRole("button", { name: "sections/method.tex: 1 项待审核,0 项已接受,0 项已拒绝", exact: true }).click(); + await agentPanel.getByRole("button", { name: "拒绝此文件的待审核修改块", exact: true }).click(); + await agentPanel.getByRole("button", { name: "完成审核", exact: true }).waitFor(); + await agentPanel.getByRole("button", { name: "退出审核", exact: true }).click(); + await page.getByRole("button", { name: "智能助手", exact: true }).click(); + await agentPanel.getByRole("button", { name: "完成审核", exact: true }).click(); + const composerAfterRestoredComplete = agentPanel.getByLabel("希望智能助手做什么?"); await composerAfterRestoredComplete.waitFor(); if (await composerAfterRestoredComplete.inputValue()) throw new Error("Completing a restored Agent review did not open an empty next-task composer"); await composerAfterRestoredComplete.fill("Refine the paper one file at a time"); - await agentPanel.getByRole("button", { name: "Create plan", exact: true }).click(); - await agentPanel.getByRole("button", { name: "Confirm plan", exact: true }).click(); - await agentPanel.getByRole("button", { name: "main.tex: 2 pending, 0 accepted, 0 rejected", exact: true }).waitFor(); + await agentPanel.getByRole("button", { name: "创建计划", exact: true }).click(); + await agentPanel.getByRole("button", { name: "确认计划", exact: true }).click(); + await agentPanel.getByRole("button", { name: "main.tex: 2 项待审核,0 项已接受,0 项已拒绝", exact: true }).waitFor(); const fileCounts = agentPanel.locator(".draft-diff__file > header .review-counts b"); const fileLabels = await fileCounts.allTextContents(); const fileColors = await fileCounts.evaluateAll(nodes => nodes.map(node => getComputedStyle(node).color)); - if (fileLabels.join("|") !== "Pending 2/2|Accepted 0/2|Rejected 0/2" || new Set(fileColors).size !== 3) { + if (fileLabels.join("|") !== "待处理 2/2|已接受 0/2|已拒绝 0/2" || new Set(fileColors).size !== 3) { throw new Error("Agent file review does not show three complete, distinctly colored status totals"); } - await agentPanel.getByRole("button", { name: "Reject", exact: true }).first().click(); - await agentPanel.getByRole("button", { name: "main.tex: 1 pending, 0 accepted, 1 rejected", exact: true }).waitFor(); - const acceptPendingFile = agentPanel.getByRole("button", { name: "Accept pending file hunks", exact: true }); + await agentPanel.getByRole("button", { name: "拒绝", exact: true }).first().click(); + await agentPanel.getByRole("button", { name: "main.tex: 1 项待审核,0 项已接受,1 项已拒绝", exact: true }).waitFor(); + const acceptPendingFile = agentPanel.getByRole("button", { name: "接受此文件的待审核修改块", exact: true }); if (await acceptPendingFile.count() !== 1) { await page.screenshot({ path: "output/playwright/workspace-agent-review-after-reject.png", fullPage: true }); throw new Error("Accept pending file hunks is missing after rejecting one hunk: " + await agentPanel.locator(".hunk-review").innerText()); } await acceptPendingFile.click(); - await agentPanel.getByRole("button", { name: "main.tex: 0 pending, 1 accepted, 1 rejected", exact: true }).waitFor(); - await agentPanel.getByRole("button", { name: "Change to reject", exact: true }).click(); - await agentPanel.getByRole("button", { name: "main.tex: 0 pending, 0 accepted, 2 rejected", exact: true }).waitFor(); - await agentPanel.getByRole("button", { name: "Change to accept", exact: true }).first().click(); - await agentPanel.getByRole("button", { name: "main.tex: 0 pending, 1 accepted, 1 rejected", exact: true }).waitFor(); - await agentPanel.getByRole("button", { name: "sections/method.tex: 1 pending, 0 accepted, 0 rejected", exact: true }).click(); + await agentPanel.getByRole("button", { name: "main.tex: 0 项待审核,1 项已接受,1 项已拒绝", exact: true }).waitFor(); + await agentPanel.getByRole("button", { name: "改为拒绝", exact: true }).click(); + await agentPanel.getByRole("button", { name: "main.tex: 0 项待审核,0 项已接受,2 项已拒绝", exact: true }).waitFor(); + await agentPanel.getByRole("button", { name: "改为接受", exact: true }).first().click(); + await agentPanel.getByRole("button", { name: "main.tex: 0 项待审核,1 项已接受,1 项已拒绝", exact: true }).waitFor(); + await agentPanel.getByRole("button", { name: "sections/method.tex: 1 项待审核,0 项已接受,0 项已拒绝", exact: true }).click(); if (await agentPanel.getByRole("button", { name: "Edit proposal", exact: true }).count()) throw new Error("Agent review still exposes whole-file proposal editing"); - await agentPanel.getByRole("button", { name: "Edit hunk", exact: true }).click(); - const hunkEditor = agentPanel.getByLabel("Edit hunk 1 for sections/method.tex"); + await agentPanel.getByRole("button", { name: "编辑修改块", exact: true }).click(); + const hunkEditor = agentPanel.getByLabel("编辑修改块 1 for sections/method.tex"); await hunkEditor.fill("This sentence is grounded in bounded and unmanaged evidence."); - await agentPanel.getByRole("button", { name: "Save hunk", exact: true }).click(); + await agentPanel.getByRole("button", { name: "保存修改块", exact: true }).click(); await agentPanel.locator(".hunk-review").getByText("unmanaged", { exact: true }).waitFor(); - await agentPanel.getByRole("button", { name: "Leave review", exact: true }).click(); - await page.getByRole("button", { name: "Agent", exact: true }).click(); - await agentPanel.getByRole("button", { name: "sections/method.tex: 1 pending, 0 accepted, 0 rejected", exact: true }).waitFor(); + await agentPanel.getByRole("button", { name: "退出审核", exact: true }).click(); + await page.getByRole("button", { name: "智能助手", exact: true }).click(); + await agentPanel.getByRole("button", { name: "sections/method.tex: 1 项待审核,0 项已接受,0 项已拒绝", exact: true }).waitFor(); for (const viewport of [{ width: 1440, height: 900, name: "1440x900" }, { width: 720, height: 800, name: "720x800" }]) { await page.setViewportSize(viewport); const reviewBox = await agentPanel.locator(".draft-diff").boundingBox(); @@ -637,10 +637,10 @@ run_code 'async (page) => { const externalMarker = "% External edit while Agent review is open"; const externalSave = await page.request.put(apiRoot + "/file?path=sections%2Fmethod.tex", { data: { content: methodBeforeConflict.content + "\n" + externalMarker, baseVersion: methodBeforeConflict.file.version } }); if (!externalSave.ok()) throw new Error("Could not create the Agent conflict fixture"); - await agentPanel.getByRole("button", { name: "Accept all & complete", exact: true }).click(); - const conflictDialog = page.getByRole("dialog", { name: "File changed during review" }); + await agentPanel.getByRole("button", { name: "接受全部并完成", exact: true }).click(); + const conflictDialog = page.getByRole("dialog", { name: "审核期间文件已发生更改" }); await conflictDialog.waitFor(); - const overwriteDiff = conflictDialog.getByLabel("Overwrite diff for sections/method.tex"); + const overwriteDiff = conflictDialog.getByLabel("覆盖差异: sections/method.tex"); const removedText = (await overwriteDiff.locator("del").allTextContents()).join(""); const insertedText = (await overwriteDiff.locator("ins").allTextContents()).join(""); const compactRemoved = removedText.replace(/\s/g, ""); @@ -652,17 +652,17 @@ run_code 'async (page) => { await page.setViewportSize(viewport); const dialogBox = await conflictDialog.boundingBox(); const diffBox = await overwriteDiff.boundingBox(); - const confirmBox = await conflictDialog.getByRole("button", { name: "Overwrite with reviewed result", exact: true }).boundingBox(); + const confirmBox = await conflictDialog.getByRole("button", { name: "使用审核结果覆盖", exact: true }).boundingBox(); if (!dialogBox || !diffBox || !confirmBox || diffBox.x < dialogBox.x || diffBox.x + diffBox.width > dialogBox.x + dialogBox.width + 1 || confirmBox.y + confirmBox.height > dialogBox.y + dialogBox.height + 1) { throw new Error("Agent conflict dialog overlaps or overflows at " + viewport.name); } await page.screenshot({ path: "output/playwright/workspace-agent-conflict-" + viewport.name + ".png", fullPage: true }); } await page.setViewportSize({ width: 1440, height: 900 }); - await conflictDialog.getByRole("button", { name: "Overwrite with reviewed result", exact: true }).click(); - await agentPanel.getByRole("button", { name: "Rollback", exact: true }).waitFor(); - await agentPanel.getByRole("button", { name: "New task", exact: true }).click(); - const composerAfterAcceptedFinish = agentPanel.getByLabel("What should Agent do?"); + await conflictDialog.getByRole("button", { name: "使用审核结果覆盖", exact: true }).click(); + await agentPanel.getByRole("button", { name: "回滚", exact: true }).waitFor(); + await agentPanel.getByRole("button", { name: "新任务", exact: true }).click(); + const composerAfterAcceptedFinish = agentPanel.getByLabel("希望智能助手做什么?"); await composerAfterAcceptedFinish.waitFor(); if (await composerAfterAcceptedFinish.inputValue()) throw new Error("Finishing an accepted Agent review did not open an empty next-task composer"); const overwrittenMethod = await (await page.request.get(apiRoot + "/file?path=sections%2Fmethod.tex")).json(); @@ -670,7 +670,7 @@ run_code 'async (page) => { throw new Error("Confirmed Agent overwrite did not apply the reviewed hunk result"); } await page.screenshot({ path: "output/playwright/workspace-agent-composer.png", fullPage: true }); - await page.getByRole("button", { name: "Revise", exact: true }).click(); + await page.getByRole("button", { name: "修改", exact: true }).click(); await page.unroute("**/api/projects/*/agent-tasks"); await page.unroute("**/api/projects/*/agent-tasks/agent-plan-review-e2e/confirm"); await page.unroute("**/api/projects/*/change-sets/agent-change-review-e2e"); @@ -684,11 +684,11 @@ run_code 'async (page) => { }' run_code 'async (page) => { - await page.getByRole("button", { name: "Agent", exact: true }).click(); - const agentPanel = page.getByRole("region", { name: "Agent workspace" }); + await page.getByRole("button", { name: "智能助手", exact: true }).click(); + const agentPanel = page.getByRole("region", { name: "智能助手工作区" }); await agentPanel.waitFor(); - await agentPanel.getByLabel("What should Agent do?").fill("Inspect cancellation behavior without creating changes"); - await page.getByRole("button", { name: "Revise", exact: true }).click(); + await agentPanel.getByLabel("希望智能助手做什么?").fill("Inspect cancellation behavior without creating changes"); + await page.getByRole("button", { name: "修改", exact: true }).click(); }' run_code 'async (page) => { @@ -734,16 +734,16 @@ run_code 'async (page) => { await route.fallback(); }); - await page.getByRole("button", { name: "Memory" }).click(); - const memoryDialog = page.getByRole("dialog", { name: "Paper Memory" }); - await memoryDialog.getByRole("button", { name: "Generate Memory" }).click(); - await memoryDialog.getByText("threat-model: Trust boundary", { exact: true }).waitFor(); - await memoryDialog.getByRole("button", { name: "Apply reviewed memory" }).click(); - await memoryDialog.getByText("Reviewed memory is current", { exact: true }).waitFor(); + await page.getByRole("button", { name: "论文记忆" }).click(); + const memoryDialog = page.getByRole("dialog", { name: "论文记忆" }); + await memoryDialog.getByRole("button", { name: "生成记忆" }).click(); + await memoryDialog.getByText("威胁模型: Trust boundary", { exact: true }).waitFor(); + await memoryDialog.getByRole("button", { name: "应用已审核的记忆" }).click(); + await memoryDialog.getByText("已审核的记忆为最新版本", { exact: true }).waitFor(); const browserName = page.context().browser()?.browserType().name() || "browser"; const browserSuffix = browserName === "chromium" ? "" : "-" + browserName; await page.screenshot({ path: "output/playwright/workspace-memory-1440x900" + browserSuffix + ".png", fullPage: true }); - await memoryDialog.getByRole("button", { name: "Close", exact: true }).click(); + await memoryDialog.getByRole("button", { name: "关闭", exact: true }).click(); await page.route("**/api/projects/*/reviews", async route => { if (route.request().method() === "GET") { @@ -862,48 +862,48 @@ run_code 'async (page) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(resolution) }); }); - await page.getByRole("button", { name: "Review", exact: true }).click(); - const reviewDialog = page.getByRole("dialog", { name: "Paper Review" }); - await page.getByRole("button", { name: /Review paper|Continue source-only/ }).click(); + await page.getByRole("button", { name: "审稿", exact: true }).click(); + const reviewDialog = page.getByRole("dialog", { name: "论文审稿" }); + await page.getByRole("button", { name: /审查论文|继续仅审查源文件/ }).click(); await reviewDialog.locator(".review-run-progress li").filter({ hasText: "Collect section evidence" }).waitFor(); await reviewDialog.getByText("Threat-model boundary needs explicit evidence", { exact: true }).waitFor(); await page.screenshot({ path: "output/playwright/workspace-review-1440x900" + browserSuffix + ".png", fullPage: true }); - await reviewDialog.getByRole("article").getByRole("button", { name: "Revise locally", exact: true }).click(); - const localRevision = page.getByRole("textbox", { name: "Revision message" }); + await reviewDialog.getByRole("article").getByRole("button", { name: "局部修改", exact: true }).click(); + const localRevision = page.getByRole("textbox", { name: "修改要求" }); await reviewDialog.waitFor({ state: "hidden" }); - await page.waitForFunction(() => (document.querySelector("textarea[aria-label=\"Revision message\"]")?.value || "").includes("Threat-model boundary needs explicit evidence")); + await page.waitForFunction(() => (document.querySelector("textarea[aria-label=\"修改要求\"]")?.value || "").includes("Threat-model boundary needs explicit evidence")); await localRevision.focus(); await page.locator(".source-editor .fastwrite-monaco-selection").first().waitFor({ timeout: 5000 }); - await page.getByRole("button", { name: "Review", exact: true }).click(); - const reopenedReview = page.getByRole("dialog", { name: "Paper Review" }); - await reopenedReview.getByRole("article").getByRole("button", { name: "Fix with Agent", exact: true }).click(); + await page.getByRole("button", { name: "审稿", exact: true }).click(); + const reopenedReview = page.getByRole("dialog", { name: "论文审稿" }); + await reopenedReview.getByRole("article").getByRole("button", { name: "使用智能助手修复", exact: true }).click(); - const revisionPanel = page.getByRole("region", { name: "Agent workspace" }); + const revisionPanel = page.getByRole("region", { name: "智能助手工作区" }); await revisionPanel.waitFor(); - await revisionPanel.getByRole("button", { name: "Create plan" }).click(); + await revisionPanel.getByRole("button", { name: "创建计划" }).click(); await revisionPanel.getByText("Clarify the attacker boundary", { exact: true }).waitFor(); - await revisionPanel.getByRole("button", { name: "Confirm plan" }).click(); + await revisionPanel.getByRole("button", { name: "确认计划" }).click(); const hunkEvidence = revisionPanel.locator(".hunk-evidence"); await hunkEvidence.waitFor(); if (!(await hunkEvidence.textContent()).includes("Make the claim match the explicit attacker model.")) throw new Error("Revision Diff omitted its Issue rationale"); await page.screenshot({ path: "output/playwright/workspace-revision-evidence-1440x900" + browserSuffix + ".png", fullPage: true }); - await revisionPanel.getByRole("button", { name: "Edit hunk", exact: true }).click(); - const proposalEditor = revisionPanel.getByLabel("Edit hunk 1 for main.tex"); + await revisionPanel.getByRole("button", { name: "编辑修改块", exact: true }).click(); + const proposalEditor = revisionPanel.getByLabel("编辑修改块 1 for main.tex"); await proposalEditor.fill(revisionChangeSet.changes[0].after.replace("evaluate guarantees only within this boundary", "evaluate guarantees only within this explicitly stated boundary")); await page.screenshot({ path: "output/playwright/workspace-revision-manual-edit-1440x900" + browserSuffix + ".png", fullPage: true }); - await revisionPanel.getByRole("button", { name: "Save hunk", exact: true }).click(); + await revisionPanel.getByRole("button", { name: "保存修改块", exact: true }).click(); await revisionPanel.locator(".hunk-card").waitFor(); - await revisionPanel.getByRole("button", { name: "Accept all & complete", exact: true }).click(); - await revisionPanel.getByText("needs review", { exact: true }).waitFor(); - await revisionPanel.getByRole("button", { name: "Compile current version" }).click(); - await page.locator(".pdf-pane .compile-strip").getByText("Compiled successfully", { exact: true }).waitFor({ timeout: 90000 }); - const rereview = revisionPanel.getByRole("button", { name: "Targeted re-review" }); + await revisionPanel.getByRole("button", { name: "接受全部并完成", exact: true }).click(); + await revisionPanel.getByText("待审核", { exact: true }).waitFor(); + await revisionPanel.getByRole("button", { name: "编译当前版本" }).click(); + await page.locator(".pdf-pane .compile-strip").getByText("编译成功", { exact: true }).waitFor({ timeout: 90000 }); + const rereview = revisionPanel.getByRole("button", { name: "定向复审" }); await rereview.waitFor(); await rereview.click(); - await revisionPanel.getByText("resolved", { exact: true }).waitFor(); + await revisionPanel.getByText("已解决", { exact: true }).waitFor(); await page.screenshot({ path: "output/playwright/workspace-revision-resolved-1440x900" + browserSuffix + ".png", fullPage: true }); if (await revisionPanel.getByRole("button", { name: "Done", exact: true }).count()) throw new Error("Completed Agent review still exposes a Done action that conflicts with New task"); - await revisionPanel.getByRole("button", { name: "New task", exact: true }).click(); + await revisionPanel.getByRole("button", { name: "新任务", exact: true }).click(); await page.unroute("**/api/projects/*/memory**"); await page.unroute("**/api/projects/*/reviews"); @@ -929,7 +929,7 @@ run_code 'async (page) => { await route.fulfill({ status: 201, contentType: "application/json", body: JSON.stringify({ ...request, suggestion: " This claim is scoped to the stated attacker model." }) }); }); await page.locator("span[title=\"main.tex\"]").click(); - const editor = page.getByRole("textbox", { name: "Source editor for main.tex" }); + const editor = page.getByRole("textbox", { name: "源文件编辑器: main.tex" }); await editor.waitFor(); const current = (await (await page.request.get(fileUrl)).json()).content; const typingStarted = Date.now(); @@ -940,14 +940,13 @@ run_code 'async (page) => { if (typingLatency > 500) throw new Error("Completion integration blocked normal editor input for " + typingLatency + "ms"); await page.waitForTimeout(350); if (completionRequests !== 0) throw new Error("Completion request ignored the input debounce window"); - const preview = page.getByRole("status").filter({ hasText: "Writing suggestion available" }); + const preview = page.getByRole("status").filter({ hasText: "有可用的写作建议" }); await preview.waitFor({ timeout: 5000 }); const browserName = page.context().browser()?.browserType().name() || "browser"; const browserSuffix = browserName === "chromium" ? "" : "-" + browserName; await page.screenshot({ path: "output/playwright/workspace-completion-1440x900" + browserSuffix + ".png", fullPage: true }); await editor.press("Tab"); - await page.getByText("Undo completion", { exact: true }).waitFor(); - await page.getByText("Saved", { exact: true }).waitFor(); + await preview.waitFor({ state: "hidden" }); let acceptedContent = ""; for (let attempt = 0; attempt < 30; attempt += 1) { acceptedContent = (await (await page.request.get(fileUrl)).json()).content; @@ -958,9 +957,10 @@ run_code 'async (page) => { const metrics = await page.evaluate(() => JSON.parse(localStorage.getItem("fastwrite.completion.metrics.v1") || "{}")); if (!metrics.suggested || !metrics.accepted || Object.values(metrics).some(value => typeof value !== "number")) throw new Error("Completion metrics were not privacy-safe counters"); - const toggle = page.getByRole("checkbox", { name: "Complete" }); + const toggle = page.getByRole("button", { name: "补全", exact: true }); + if (await toggle.getAttribute("aria-pressed") !== "true") throw new Error("Completion toggle was not enabled before disabling it"); await toggle.press("Space"); - if (await toggle.isChecked()) throw new Error("Completion toggle did not switch off"); + if (await toggle.getAttribute("aria-pressed") !== "false") throw new Error("Completion toggle did not switch off"); const beforeDisabledEdit = completionRequests; const disabledValue = acceptedContent; await editor.focus(); @@ -974,7 +974,7 @@ run_code 'async (page) => { run_code 'async (page) => { const openStarted = Date.now(); await page.locator("span[title=\"large-notes.md\"]").click(); - const editor = page.getByRole("textbox", { name: "Source editor for large-notes.md" }); + const editor = page.getByRole("textbox", { name: "源文件编辑器: large-notes.md" }); await editor.waitFor({ timeout: 5000 }); if (Date.now() - openStarted > 2500) throw new Error("Large file blocked the workspace while opening"); await editor.focus(); @@ -987,17 +987,17 @@ run_code 'async (page) => { run_code 'async (page) => { await page.locator("span[title=\"main.tex\"]").click(); - const editor = page.getByRole("textbox", { name: "Source editor for main.tex" }); + const editor = page.getByRole("textbox", { name: "源文件编辑器: main.tex" }); await editor.waitFor(); await editor.focus(); await editor.press("ControlOrMeta+A"); await page.keyboard.insertText("\\documentclass{article}\n\\begin{document}\n\\undefinedFastWriteCommand\n\\end{document}"); const errorIcon = page.locator(".compile-status-icon--error"); await errorIcon.waitFor({ timeout: 90000 }); - if (await errorIcon.getAttribute("aria-label") !== "Compilation error") throw new Error("Compilation failure does not expose an unambiguous error icon"); - await page.getByRole("tab", { name: /^Problems/ }).click(); + if (await errorIcon.getAttribute("aria-label") !== "编译错误") throw new Error("Compilation failure does not expose an unambiguous error icon"); + await page.getByRole("tab", { name: /^问题/ }).click(); const diagnostics = page.locator(".bottom-panel-problems"); - await diagnostics.getByText("Compilation failed", { exact: true }).waitFor(); + await diagnostics.getByText("编译失败", { exact: true }).waitFor(); await diagnostics.getByText(/Undefined control sequence/).first().waitFor(); if (await page.locator(".react-pdf__Page__canvas").count() < 1) { throw new Error("Previous PDF disappeared after a failed compile"); @@ -1015,15 +1015,15 @@ run_code 'async (page) => { await page.screenshot({ path: "output/playwright/workspace-pdf-error-" + viewport.name + browserSuffix + ".png", fullPage: true }); } await page.setViewportSize({ width: 1280, height: 800 }); - const fixButton = page.locator(".pdf-pane").getByRole("button", { name: "Fix with Agent" }); + const fixButton = page.locator(".pdf-pane").getByRole("button", { name: "使用智能助手修复" }); await fixButton.waitFor(); const fixBox = await fixButton.boundingBox(); const pdfBox = await page.locator(".pdf-pane").boundingBox(); if (!fixBox || !pdfBox || fixBox.x < pdfBox.x || fixBox.x + fixBox.width > pdfBox.x + pdfBox.width) throw new Error("Fix with Agent is clipped in the PDF failure state"); - await page.getByRole("button", { name: "Hide panel", exact: true }).click(); + await page.getByRole("button", { name: "隐藏面板", exact: true }).click(); await fixButton.click(); - await page.getByRole("region", { name: "Agent workspace" }).waitFor(); - const agentObjective = page.getByRole("textbox", { name: "What should Agent do?" }); + await page.getByRole("region", { name: "智能助手工作区" }).waitFor(); + const agentObjective = page.getByRole("textbox", { name: "希望智能助手做什么?" }); await agentObjective.waitFor(); await page.waitForFunction(() => (document.querySelector(".agent-task-form textarea")?.value || "").startsWith("/revise ") @@ -1040,11 +1040,20 @@ run_code 'async (page) => { await page.screenshot({ path: "output/playwright/workspace-compile-repair-agent-720x800" + browserSuffix + ".png", fullPage: true }); await page.setViewportSize({ width: 1440, height: 900 }); - await page.getByRole("button", { name: "Revise", exact: true }).click(); + await page.getByRole("button", { name: "修改", exact: true }).click(); await editor.focus(); await editor.press("ControlOrMeta+A"); - await page.keyboard.insertText("\\documentclass{article}\n\\begin{document}\nCompilation repaired.\n\\end{document}"); - await page.getByText("Saved", { exact: true }).waitFor(); + const repairedContent = "\\documentclass{article}\n\\begin{document}\nCompilation repaired.\n\\end{document}"; + await page.keyboard.insertText(repairedContent); + const projectId = page.url().split("/").pop(); + const fileUrl = page.url().replace(/\/projects\/.*$/, "") + "/api/projects/" + projectId + "/file?path=main.tex"; + let savedContent; + for (let attempt = 0; attempt < 40; attempt += 1) { + savedContent = (await (await page.request.get(fileUrl)).json()).content; + if (savedContent === repairedContent) break; + await page.waitForTimeout(100); + } + if (savedContent !== repairedContent) throw new Error("Compilation repair was not saved through the Workspace API"); await page.locator(".compile-strip--success").waitFor({ timeout: 90000 }); await page.locator(".react-pdf__Page__canvas").first().waitFor({ timeout: 90000 }); }' @@ -1090,14 +1099,14 @@ run_code 'async (page) => { requestBody = route.request().postDataJSON(); await route.fulfill({ status: 201, contentType: "application/json", body: JSON.stringify(imported) }); }); - await page.getByRole("main").getByRole("button", { name: "Import paper", exact: true }).click(); - const dialog = page.getByRole("dialog", { name: "Import a paper" }); - await dialog.getByRole("tab", { name: "GitHub repository" }).click(); - await dialog.getByLabel("Repository URL").fill("https://github.com/example/security-paper"); - await dialog.getByLabel(/Branch, tag or commit/).fill("camera-ready"); - await dialog.getByLabel(/Project name/).fill("Imported Security Paper"); - await dialog.getByLabel(/Main document/).fill("paper.tex"); - await dialog.getByRole("combobox", { name: "Research domain" }).click(); + await page.getByRole("main").getByRole("button", { name: "导入论文", exact: true }).click(); + const dialog = page.getByRole("dialog", { name: "导入论文" }); + await dialog.getByRole("tab", { name: "GitHub 仓库" }).click(); + await dialog.getByLabel("仓库地址").fill("https://github.com/example/security-paper"); + await dialog.getByLabel(/分支、标签或提交/).fill("camera-ready"); + await dialog.getByLabel(/项目名称/).fill("Imported Security Paper"); + await dialog.getByLabel(/主文档/).fill("paper.tex"); + await dialog.getByRole("combobox", { name: "研究领域" }).click(); await dialog.getByRole("option", { name: "网络与信息安全", exact: true }).click(); await page.waitForTimeout(350); const axeUrl = await page.evaluate(() => location.origin + "/__e2e__/axe.min.js"); @@ -1111,9 +1120,9 @@ run_code 'async (page) => { if (violations.length) { throw new Error(violations.map(item => item.id + ": " + item.nodes.map(node => node.target.join(" ") + " " + node.html + " " + node.failureSummary).join(", ")).join("\n")); } - await dialog.getByRole("button", { name: "Import paper", exact: true }).click(); + await dialog.getByRole("button", { name: "导入论文", exact: true }).click(); await page.waitForURL(new RegExp("/projects/" + imported.id + "$")); - await page.getByRole("textbox", { name: "Source editor for main.tex" }).waitFor({ state: "attached" }); + await page.getByRole("textbox", { name: "源文件编辑器: main.tex" }).waitFor({ state: "attached" }); const expected = { repository: "https://github.com/example/security-paper", ref: "camera-ready", From 5d72c986b9d15e8bfe734c8a3a09d52fe36251ab Mon Sep 17 00:00:00 2001 From: zhao0335 <3797723137@qq.com> Date: Thu, 5 Nov 2026 11:33:43 +0800 Subject: [PATCH 5/7] test(e2e): retain the whole-file edit absence assertion --- scripts/e2e-smoke.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/e2e-smoke.sh b/scripts/e2e-smoke.sh index 8602603..ed58aa6 100644 --- a/scripts/e2e-smoke.sh +++ b/scripts/e2e-smoke.sh @@ -613,7 +613,7 @@ run_code 'async (page) => { await agentPanel.getByRole("button", { name: "改为接受", exact: true }).first().click(); await agentPanel.getByRole("button", { name: "main.tex: 0 项待审核,1 项已接受,1 项已拒绝", exact: true }).waitFor(); await agentPanel.getByRole("button", { name: "sections/method.tex: 1 项待审核,0 项已接受,0 项已拒绝", exact: true }).click(); - if (await agentPanel.getByRole("button", { name: "Edit proposal", exact: true }).count()) throw new Error("Agent review still exposes whole-file proposal editing"); + if (await agentPanel.getByRole("button", { name: "编辑建议内容", exact: true }).count()) throw new Error("Agent review still exposes whole-file proposal editing"); await agentPanel.getByRole("button", { name: "编辑修改块", exact: true }).click(); const hunkEditor = agentPanel.getByLabel("编辑修改块 1 for sections/method.tex"); await hunkEditor.fill("This sentence is grounded in bounded and unmanaged evidence."); From a245ed34c7ae23328b9ccfd43a73c17d8d3aff38 Mon Sep 17 00:00:00 2001 From: zhao0335 <3797723137@qq.com> Date: Thu, 5 Nov 2026 11:36:35 +0800 Subject: [PATCH 6/7] fix(a11y): preserve contrast for selected writing modes --- apps/web/src/styles.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index 28e70a4..baa6c9a 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -403,7 +403,7 @@ code { font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; } .ai-workspace__controls { min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; } .ai-workspace__mode { flex: 0 0 auto; } .ai-workspace__mode button { height: 30px; font-size: 12px; } -.ai-workspace__mode button[aria-pressed="true"] { color: var(--accent); background: var(--vscode-active); } +.ai-workspace__mode button[aria-pressed="true"] { color: var(--vscode-foreground); background: var(--vscode-active); } .ai-workspace__harness { display: flex; align-items: center; gap: 8px; } .ai-workspace__harness > div { width: 112px; } .ai-workspace__harness [role="combobox"] { height: 30px; } From 6797f5be2aa7436291e24b497202a84f261478d9 Mon Sep 17 00:00:00 2001 From: zhao0335 <3797723137@qq.com> Date: Thu, 5 Nov 2026 11:42:41 +0800 Subject: [PATCH 7/7] test(e2e): scope writing modes separately from Agent commands --- scripts/e2e-smoke.sh | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/scripts/e2e-smoke.sh b/scripts/e2e-smoke.sh index ed58aa6..df860a2 100644 --- a/scripts/e2e-smoke.sh +++ b/scripts/e2e-smoke.sh @@ -202,7 +202,7 @@ run_code 'async (page) => { await editor.focus({ force: true }); await editor.press("ControlOrMeta+A"); await page.keyboard.insertText(content); - const agentButton = page.getByRole("button", { name: "智能助手", exact: true }); + const agentButton = page.getByRole("group", { name: "AI 写作模式", exact: true }).getByRole("button", { name: "智能助手", exact: true }); await agentButton.focus(); if (await page.evaluate(() => document.activeElement?.textContent?.trim()) !== "智能助手") { throw new Error("Could not move focus outside the editor before testing the global save shortcut"); @@ -538,7 +538,7 @@ run_code 'async (page) => { await page.route("**/api/projects/*/agent-runs", async route => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([agentRun]) })); await page.route("**/api/projects/*/issue-resolutions", async route => route.fulfill({ status: 200, contentType: "application/json", body: "[]" })); - await page.getByRole("button", { name: "智能助手", exact: true }).click(); + await page.getByRole("group", { name: "AI 写作模式", exact: true }).getByRole("button", { name: "智能助手", exact: true }).click(); const agentPanel = page.getByRole("region", { name: "智能助手工作区" }); await agentPanel.waitFor(); const objective = agentPanel.getByLabel("希望智能助手做什么?"); @@ -584,7 +584,7 @@ run_code 'async (page) => { await agentPanel.getByRole("button", { name: "拒绝此文件的待审核修改块", exact: true }).click(); await agentPanel.getByRole("button", { name: "完成审核", exact: true }).waitFor(); await agentPanel.getByRole("button", { name: "退出审核", exact: true }).click(); - await page.getByRole("button", { name: "智能助手", exact: true }).click(); + await page.getByRole("group", { name: "AI 写作模式", exact: true }).getByRole("button", { name: "智能助手", exact: true }).click(); await agentPanel.getByRole("button", { name: "完成审核", exact: true }).click(); const composerAfterRestoredComplete = agentPanel.getByLabel("希望智能助手做什么?"); await composerAfterRestoredComplete.waitFor(); @@ -620,7 +620,7 @@ run_code 'async (page) => { await agentPanel.getByRole("button", { name: "保存修改块", exact: true }).click(); await agentPanel.locator(".hunk-review").getByText("unmanaged", { exact: true }).waitFor(); await agentPanel.getByRole("button", { name: "退出审核", exact: true }).click(); - await page.getByRole("button", { name: "智能助手", exact: true }).click(); + await page.getByRole("group", { name: "AI 写作模式", exact: true }).getByRole("button", { name: "智能助手", exact: true }).click(); await agentPanel.getByRole("button", { name: "sections/method.tex: 1 项待审核,0 项已接受,0 项已拒绝", exact: true }).waitFor(); for (const viewport of [{ width: 1440, height: 900, name: "1440x900" }, { width: 720, height: 800, name: "720x800" }]) { await page.setViewportSize(viewport); @@ -670,7 +670,7 @@ run_code 'async (page) => { throw new Error("Confirmed Agent overwrite did not apply the reviewed hunk result"); } await page.screenshot({ path: "output/playwright/workspace-agent-composer.png", fullPage: true }); - await page.getByRole("button", { name: "修改", exact: true }).click(); + await page.getByRole("group", { name: "AI 写作模式", exact: true }).getByRole("button", { name: "修改", exact: true }).click(); await page.unroute("**/api/projects/*/agent-tasks"); await page.unroute("**/api/projects/*/agent-tasks/agent-plan-review-e2e/confirm"); await page.unroute("**/api/projects/*/change-sets/agent-change-review-e2e"); @@ -684,11 +684,11 @@ run_code 'async (page) => { }' run_code 'async (page) => { - await page.getByRole("button", { name: "智能助手", exact: true }).click(); + await page.getByRole("group", { name: "AI 写作模式", exact: true }).getByRole("button", { name: "智能助手", exact: true }).click(); const agentPanel = page.getByRole("region", { name: "智能助手工作区" }); await agentPanel.waitFor(); await agentPanel.getByLabel("希望智能助手做什么?").fill("Inspect cancellation behavior without creating changes"); - await page.getByRole("button", { name: "修改", exact: true }).click(); + await page.getByRole("group", { name: "AI 写作模式", exact: true }).getByRole("button", { name: "修改", exact: true }).click(); }' run_code 'async (page) => { @@ -1040,7 +1040,7 @@ run_code 'async (page) => { await page.screenshot({ path: "output/playwright/workspace-compile-repair-agent-720x800" + browserSuffix + ".png", fullPage: true }); await page.setViewportSize({ width: 1440, height: 900 }); - await page.getByRole("button", { name: "修改", exact: true }).click(); + await page.getByRole("group", { name: "AI 写作模式", exact: true }).getByRole("button", { name: "修改", exact: true }).click(); await editor.focus(); await editor.press("ControlOrMeta+A"); const repairedContent = "\\documentclass{article}\n\\begin{document}\nCompilation repaired.\n\\end{document}";