dsh-thesis 2.1.0 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,97 +1,47 @@
1
1
  # dsh-thesis
2
2
 
3
- **把一堆材料变成一篇经得起查重与答辩的论文,和一个讲得清自己系统的你。**
3
+ **把一堆材料变成一篇经得起查重与答辩的论文。**
4
4
 
5
- [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 的单一插件。给它学校模板、
6
- 论文要求、实验数据、代码和参考文献,它先追问、再动手,按九个阶段推进:
5
+ [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 的单一插件。
6
+ 给它学校模板、论文要求、实验数据、代码和参考文献,它先追问、再动手,按九个阶段推进:
7
7
 
8
8
  **选题 → 开题 → 文献 → 设计 → 实现 → 测试 → 论文 → 合规 → 答辩**
9
9
 
10
- **没有斜杠命令。** 在论文工作区里开口说话,插件问一道选择题确认意图,然后自己一轮轮跑下去,
11
- 只在 G1-G4 人工关卡停下来让你拍板。全程留痕(台账 / 决定日志 / 检索记录 / 检查报告)。
12
-
13
10
  纯 TypeScript,零运行时第三方依赖。对外只有三个命名空间:`thesis_*`、`fact_*`、`defense_code_*`。
14
11
 
15
12
  ## 安装
16
13
 
17
- 在 DSH 的**插件管理器里搜索 `dsh-thesis`** 装上,重启 dsh。
14
+ 在 DSH 的插件管理器里搜索 `dsh-thesis`即可 装上。
18
15
 
19
- 从源码装(想改代码,或本机没有 pnpm):
16
+ 从源码装:
20
17
 
21
18
  ```bash
22
19
  git clone https://github.com/booondenz/dsh-thesis.git && cd dsh-thesis
23
- npm install && npm run build # DSH 没有 TS 加载器,必须先编译
24
- dsh plugin --profile web add "$PWD" # 需要 pnpm
25
- node scripts/install-into-profile.ts --profile web # 没有 pnpm 时的兜底
20
+ npm install && npm run build
21
+ dsh plugin --profile web add "$PWD"
26
22
  ```
27
23
 
28
- 卸载:插件管理器里移除,或 `dsh plugin --profile web remove dsh-thesis`。
29
-
30
- ## 第一次用
24
+ ## 使用
31
25
 
32
- 把材料放进论文目录(或告诉它绝对路径),在**该目录下**开会话说:
26
+ 把材料放进论文目录,或告诉它绝对路径,在**该目录下**开会话说:
33
27
 
34
28
  ```
35
29
  这是我的论文材料,帮我做毕业设计
36
30
  ```
37
31
 
38
32
  它会弹一道选择题(从零走到答辩 / 只补合规与答辩 / 只做答辩准备 / 先别自动跑),
39
- 选完就自动推进;撞上 G1-G4 关卡时在审批界面点「允许」或「拒绝」。
40
- 没有材料也能用:「我要做毕业设计,还没定题」。
41
-
42
- ## 配置
43
-
44
- 默认值都在,通常不用写。需要时在 profile 的 `cordis.patch.yml` 那一行里覆盖:
45
-
46
- ```yaml
47
- - id: paper
48
- name: dsh-thesis
49
- config:
50
- workspace: D:/毕业论文/我的论文 # 缺省从会话 cwd 向上探测 00-管理/进度台账.md
51
- autopilot: { maxGoalRounds: 20 } # 自动续跑的轮数上限(默认 40)
52
- ```
53
-
54
- 完整字段见 [docs/config.md](./docs/config.md)。
55
-
56
- ## 红线(写在护栏里,不是写在文档里)
57
-
58
- 1. **零假文献**:引用只能来自真实检索缓存,`thesis_lit_save` 拒绝缓存外的 id。
59
- 2. **零假数据**:数据与结果必须来自真实运行;降重不许改数据与结论。
60
- 3. **人是作者**:G1-G4 关卡由工具强制,决定必须由你拍板。
61
- 4. **规范以学校文件为准**:没有模板时用内置过渡模板并**显式标注「非学校模板」**。
62
- 5. **降重不是洗稿**:不接收费查重,报告里是本地估算,与学校结果不可等同(可回填对照)。
63
- 6. **不静默销毁数据**:读不出来 ≠ 不存在;读失败就抛错,绝不用默认值覆盖。
64
-
65
- ## 文档
66
-
67
- | 文档 | 内容 |
68
- |---|---|
69
- | [docs/usage.md](./docs/usage.md) | 使用手册:第一次会话、九阶段、常见动作、产出物、中断续跑 |
70
- | [docs/autopilot.md](./docs/autopilot.md) | 自动驾驶:四档意图、关卡拍板、何时不介入、降级与关闭 |
71
- | [docs/tools.md](./docs/tools.md) | 24 个工具 + 11 个技能一览(名字即契约) |
72
- | [docs/config.md](./docs/config.md) | 全部配置字段、默认值与约束 |
73
- | [docs/troubleshooting.md](./docs/troubleshooting.md) | 装了没反应、没弹选择题、PDF/pptx 失败等 |
74
- | [docs/design.md](./docs/design.md) | 设计文档与 ADR(架构、分层、质量策略、已知边界) |
75
- | [docs/host-api.md](./docs/host-api.md) | 宿主契约:用到的 DSH 接口与实测行为 |
76
- | [docs/release.md](./docs/release.md) | 发版:tag → GitHub Actions → npm(含 provenance)|
77
-
78
- ## 开发
79
-
80
- ```bash
81
- npm install && npm run build # tsc → lib/
82
- npm test # node --test(Node 24 直接跑 .ts)
83
- npm run verify # build + 两个 typecheck + 全部单测 + 审计 + 端到端
84
- ```
33
+ 选完就自动推进;撞上 G1-G4 关卡时在审批界面点允许或拒绝。
85
34
 
86
- 仓库里**没有 JS 源文件**:`src/`、`scripts/`、`tests/` 全是 `.ts`,直接由 Node 执行(需要 Node ≥ 22.18);
87
- `lib/` 只是 `tsc` 产物(已 gitignore,发布时作为编译后的 JS 提供)。
35
+ ## 红线
88
36
 
89
- 质量分层:单测/契约、真实场景端到端、真实 cordis 装配、真实 dsh 引擎装载、对外表面审计
90
- (见 [docs/design.md](./docs/design.md) §9)。
37
+ - **零假文献**:引用只能来自真实检索缓存,`thesis_lit_save` 拒绝缓存外的 id。
38
+ - **零假数据**:数据与结果必须来自真实运行;降重不许改数据与结论。
39
+ - **人是作者**:G1-G4 关卡由工具强制,决定必须由你拍板。
40
+ - **规范以学校文件为准**:没有模板时用内置过渡模板并**显式标注「非学校模板」**。
41
+ - **降重不是洗稿**:不接收费查重,报告里是本地估算,与学校结果不可等同(可回填对照)。
42
+ - **不静默销毁数据**:读不出来 ≠ 不存在;读失败就抛错,绝不用默认值覆盖。
91
43
 
92
- **发版**:打 tag 即发布,由 GitHub Actions 跑构建、校验与 `npm publish --provenance`,
93
- 见 [docs/release.md](./docs/release.md)。
94
44
 
95
45
  ## 许可
96
46
 
97
- MIT。来源声明见 [NOTICE.md](./NOTICE.md)。
47
+ MIT
package/docs/design.md CHANGED
@@ -351,6 +351,28 @@ DSH 的 profile 预检(`dsh-app-boot` 的 `evaluatePluginCompatibility`,在
351
351
  **取舍**:内置过渡引擎只落实表格与页边距(A4、上 2.5/下 2.5/左 3.0/右 2.5 cm);
352
352
  分节页码、目录域、页眉依赖模板级排版,交给 `build_docx.py` 或 pandoc + 学校模板。
353
353
 
354
+ ### ADR-19 发布由「版本号」驱动,tag 是产物而不是开关
355
+
356
+ **背景**:原来发版要先打 tag 再推,tag 名还必须与 `package.json` 逐字一致——漏一步就是一次红 run,
357
+ 而 tag 本身没有携带任何信息,只是"我现在要发版了"的暗号。
358
+
359
+ **决定**:
360
+ 1. 触发条件改为 `push: branches: [main]`,同时保留 `tags: ['v*']` 兼容手动打 tag;
361
+ 2. 是否发布由「`package.json` 的版本在 npm 上是否已存在」决定——**版本号没变就不发**;
362
+ 3. 发布成功后由 workflow 自己 `git tag v<version>` 并推上去,再建 Release;
363
+ 4. 版本号只有一个来源:`package.json`。`npm run release:minor` 同时改
364
+ `package.json` 与 `package-lock.json`(顶层 + `packages[""]`)并提交,提交信息从版本号派生。
365
+
366
+ **为什么不从 commit message 解析版本**:发布包里写的是 `package.json` 的 version。
367
+ 让提交信息决定发什么版本,要么包与 tag 不一致,要么 CI 临时改包导致仓库与 npm 永久错位,
368
+ 要么机器人提交回 main 撞保护分支。三者都比"多跑一条 `npm run release:*`"更贵。
369
+
370
+ **取舍**:每次推 main 都会多跑一个 `npm ci` + `npm view`(约 40 秒)才判定跳过;
371
+ 换来的是"日常提交随便推、发版只需升版本号"。
372
+
373
+ **验证**:`npm run release:minor -- --dry-run` 只打印不落盘;`publish.yml` 用 PyYAML 解析校验结构与条件;
374
+ 真正的判定(已发布则跳过)由 npm 侧 `npm view` 保证,重复触发不会覆盖已发布版本。
375
+
354
376
  ---
355
377
 
356
378
  ## 7. 数据与产物
package/docs/release.md CHANGED
@@ -1,138 +1,147 @@
1
1
  # 发布
2
2
 
3
3
  发版由 GitHub Actions 完成([`.github/workflows/publish.yml`](../.github/workflows/publish.yml))。
4
- **打 tag 即发布**,本地不需要 `npm login`,也不需要任何 npm token。
5
4
 
6
- 认证走 **Trusted Publishing(OIDC 可信发布)**:npm 只接受「这个仓库的这个 workflow」发来的包,
7
- 凭据是每次运行临时签发的,不落盘、不过期、也没有 2FA 输 OTP 的问题;provenance 由 npm 自动附上。
5
+ **升版本号 + push 就发版,不用打 tag。** 推上去以后 workflow 会自己看 npm 上有没有这个版本,
6
+ 没有就发布,发完**自动补 tag 与 GitHub Release**——tag 是发布的产物,不再是发布的前提。
8
7
 
9
- ## 一次性设置(只做一次)
8
+ ```bash
9
+ npm run release:minor # 2.1.0 -> 2.2.0(改 package.json 与 package-lock.json 并提交)
10
+ git push origin main # 推完就自动发布
11
+ ```
10
12
 
11
- 1. 打开 <https://www.npmjs.com/package/dsh-thesis/access> → **Trusted Publishers** →
12
- **Select publisher: GitHub Actions**,填四个字段:
13
+ 就这两条。`npm run release:patch` / `release:major` 同理。
13
14
 
14
- | 字段 | 填什么 |
15
- |---|---|
16
- | Organization or user | `booondenz` |
17
- | Repository | `dsh-thesis` |
18
- | Workflow filename | `publish.yml`(只写文件名,不带路径)|
19
- | Environment name | **留空**(除非 workflow 里写了 `environment:`)|
15
+ 本地不需要 `npm login`,也不需要任何 npm token。
20
16
 
21
- 同一页下方的 **Allowed actions**:
17
+ ## 它怎么判断该不该发
22
18
 
23
- | 选项 | 勾不勾 | 为什么 |
24
- |---|---|---|
25
- | stage publish | 强制,改不了 | — |
26
- | **Allow npm publish** | **勾上** | 不勾的话 `npm publish` 会被拒(`403 OIDC permission denied for this action`),只接受 `npm stage publish`,每次发版还得你到网页上做 2FA 批准 |
27
- | Allow npm dist-tag | 不用勾 | `npm publish` 会自动把 `latest` 指过去;我们没有 `npm dist-tag` 操作 |
19
+ | 检查 | 结果 |
20
+ |---|---|
21
+ | push 到 `main` | 进入发布流程 |
22
+ | push 的 tag(`v*`) | 同样进入(兼容手动打 tag 的老做法) |
23
+ | `package.json` 的版本 npm 上**没有** | `npm pack --dry-run` → 守卫 → `npm publish` → 自动打 tag → 建 Release |
24
+ | `package.json` 的版本 npm 上**已有** | 只跑检查,跳过发布(不会红,也不会覆盖)|
25
+ | 手动触发(`workflow_dispatch`) | 只做打包预览,不发布、不打 tag |
28
26
 
29
- > 黄色提示条只说「provider 与必填字段建好后不能改」,`Allowed actions` 的两个勾选框随时可以改。
27
+ 所以:**版本号没变 = 这次 push 不发版**。日常提交随便推,只有 `release:*` 那次会发。
30
28
 
31
- 2. 确认仓库是 **public**:私有仓库发不出 provenance。
32
- 3. 不需要 `NPM_TOKEN`,**之前建过的话建议在 Settings → Secrets 里删掉**,免得以后误用。
29
+ ## 为什么版本号只认 `package.json`,不从 commit message 解析
33
30
 
34
- > 配置要先「验证」过一次才算生效:2026-10 起,长期没成功发布过的可信发布配置会自动过期。
35
- > 所以配完就立刻发一次版。
31
+ 发布包里真正写进去的是 `package.json` 的 `version`。如果让提交信息里的 `v2.2.0` 决定发什么版本,
32
+ 只有三条路,都不好:
36
33
 
37
- ## 备选:用 token(不推荐)
34
+ 1. **照抄提交信息 → 包里版本不对**:提交写 `v2.2.0`、`package.json` 还是 `2.1.0`,
35
+ npm 上会出现两个"2.1.0"(第二次直接被拒),或者发出去的包版本与 tag 不一致;
36
+ 2. **CI 里临时改 `package.json` 再发**:npm 上是 2.2.0,仓库里还是 2.1.0,
37
+ 下次谁从仓库装就是旧版本,provenance 里的 commit 也对不上;
38
+ 3. **CI 里改完再提交回 main**:机器人提交 + 可能撞保护分支 + 每次发版两个 commit。
38
39
 
39
- 如果哪天可信发布不可用,也可以在 npm 建 **Classic → Automation** 类型的 token
40
- (Granular token 在开了 2FA 的账号上会报 `EOTP`,除非它明确允许绕过 2FA),
41
- 存成仓库 secret `NPM_TOKEN`,然后在 workflow 的 publish 步骤加回:
40
+ 现在的做法是:**版本号只有一处真相(`package.json`),`npm run release:*` 负责同时改两处并提交**,
41
+ 提交信息与 tag 名都从它派生(`chore: release v2.2.0` → tag `v2.2.0`),三者不可能不一致。
42
42
 
43
- ```yaml
44
- env:
45
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
46
- ```
43
+ ## 一次性设置(只做一次,已完成)
47
44
 
48
- 并把 `setup-node` 的 `with:` 里加回 `registry-url: https://registry.npmjs.org`。
49
- Automation token 专为 CI 设计,不受 2FA 影响。
45
+ npm 侧认证走 **Trusted Publishing(OIDC)**:npm 只接受「这个仓库的这个 workflow」发来的包,
46
+ 凭据每次运行临时签发,不落盘、不过期、没有 2FA 输 OTP 的问题;provenance 自动附带。
50
47
 
51
- ## 发一次版
48
+ <https://www.npmjs.com/package/dsh-thesis/access> → **Trusted Publishers**:
52
49
 
53
- ```bash
54
- npm version patch --no-git-tag-version # 或 minor / major,或手动改 package.json
55
- git add package.json package-lock.json
56
- git commit -m "chore: release v2.0.1"
57
- git tag v2.0.1
58
- git push origin main --tags
59
- ```
50
+ | 字段 | 值 |
51
+ |---|---|
52
+ | Organization or user | `booondenz` |
53
+ | Repository | `dsh-thesis` |
54
+ | **Workflow filename** | **`publish.yml`**(只写文件名,不带路径)|
55
+ | Environment name | 留空 |
56
+
57
+ **`publish.yml` 这个文件名不能改**(也不能把发布逻辑挪进 `ci.yml`):npm 的配置绑的就是文件名,
58
+ 改名后第一次发布会 403。改了触发条件是安全的,改文件名不是。
59
+
60
+ 同一页 **Allowed actions** 里要勾 **Allow npm publish**,只勾 stage 的话每次发版还得去网页做 2FA 批准。
60
61
 
61
- Action 会依次做:
62
+ ## workflow 干了什么
62
63
 
63
64
  | 步骤 | 失败就停 |
64
65
  |---|---|
65
- | `npm ci` | 锁文件与 package.json 不同步会在这里红 |
66
- | tag 与 `package.json` 版本一致 | 防止「tag 写 v2.0.1、包里是 2.0.0」|
67
- | 该版本是否已存在于 npm | 已存在则**跳过发布**(不会红,也不会覆盖)|
68
- | `npm pack --dry-run` | 发布内容一目了然(日志里能看到文件清单)|
69
- | `npm publish` | 内部触发 `prepublishOnly` 守卫:构建 + 类型 + 单测 + 审计 + 端到端;provenance 由可信发布自动附带 |
70
- | 建 GitHub Release | 标题用 tag,正文自动生成 |
66
+ | `npm ci` | 锁文件与 `package.json` 不同步会红(所以别手改版本号,用 `npm run release:*`)|
67
+ | 读 `package.json` 版本 | — |
68
+ | 手动 tag 与版本一致(仅 tag 触发时)| 防止「tag 写 v2.2.0、包里是 2.1.0」|
69
+ | 该版本是否已发布 | 已存在则跳过发布 |
70
+ | `npm pack --dry-run` | 日志里能直接看到这次包里有什么 |
71
+ | `npm publish` | 内部跑 `prepublishOnly` 守卫:构建 + 类型 + 单测 + 审计 + 端到端;**main 红了在这里被拦下** |
72
+ | 自动打 tag | `git tag v<version>` + push;用 `GITHUB_TOKEN` 推的 tag 不会再触发 workflow,不会套娃 |
73
+ | 建 Release | 标题 `v<version>`,正文自动生成 |
71
74
 
72
- 发布成功后 npm 页面会带一个 **Provenance** 徽章,点进去能看到是哪次 commit、哪次 workflow 构建的。
75
+ ## 手动打 tag(仍然可以,但不再是发版的必要条件)
73
76
 
74
- ## 在网页上发版(完全不碰命令行)
77
+ ```bash
78
+ git tag v2.2.0
79
+ git push origin v2.2.0
80
+ ```
75
81
 
76
- 1. 打开 <https://github.com/booondenz/dsh-thesis/releases/new>;
77
- 2. **Choose a tag** 里输入 `v2.0.1`,点下拉第一条 **Create new tag: v2.0.1 on publish**;
78
- 3. **Target** 选 `main`,并确认它指向 bump 版本的那个 commit(否则 tag 会打在别的提交上,
79
- workflow 第一步的版本比对就会红);
80
- 4. 标题写 `v2.0.1`,正文可以留空——workflow 里是 `gh release view || gh release create`,
81
- 发现 Release 已存在会跳过;
82
- 5. **Publish release**。
82
+ 行为与以前完全一致:先校验 tag 与 `package.json` 一致,再发布。适合"版本号早就提交了、只是补发"的场景。
83
+
84
+ ## 备选:用 token(不推荐)
83
85
 
84
- 创建 tag 本身就是一次 `push`,所以 `publish.yml` 会被触发。两条路等价:也可以先
85
- `git push origin v2.0.1` 再建 Release,或者只推 tag 不建 Release(workflow 会替你建)。
86
+ 可信发布不可用时,在 npm 建 **Classic → Automation** token(Granular token 在开了 2FA 的账号上会报 `EOTP`),
87
+ 存成仓库 secret `NPM_TOKEN`,然后在 publish 步骤加回:
88
+
89
+ ```yaml
90
+ env:
91
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
92
+ ```
93
+
94
+ 并把 `setup-node` 的 `with:` 里加回 `registry-url: https://registry.npmjs.org`。
95
+ Automation token 专为 CI 设计,不受 2FA 影响。
86
96
 
87
97
  ## 确认发成功了
88
98
 
89
99
  | 看哪 | 期望 |
90
100
  |---|---|
91
- | Actions → **publish** | 这次 run 是绿的;Summary 里有一行 `https://www.npmjs.com/package/dsh-thesis/v/2.0.1` |
92
- | `npm view dsh-thesis version` | `2.0.1`(registry 立刻生效,网页可能还要一分钟)|
93
- | `npm view dsh-thesis dist-tags` | `{ latest: '2.0.1' }` |
94
- | <https://www.npmjs.com/package/dsh-thesis> | 版本号旁出现 **Provenance** 徽章 |
95
- | 仓库 **Releases** | 多了一条 `v2.0.1` |
96
-
97
- Actions 里**根本没出现 publish 这个 run**,说明 tag 没推上去(或名字不是 `v` 开头);
98
- 出现 run 但红在第一步,通常是 tag 与 `package.json` 版本对不上。
101
+ | Actions → **publish** | 绿;Summary 里有 `https://www.npmjs.com/package/dsh-thesis/v/2.2.0` |
102
+ | `npm view dsh-thesis version` | `2.2.0`(registry 立刻生效,网页可能慢一分钟)|
103
+ | `npm view dsh-thesis dist-tags` | `{ latest: '2.2.0' }` |
104
+ | 仓库 **Tags / Releases** | 自动多了 `v2.2.0` |
105
+ | <https://www.npmjs.com/package/dsh-thesis> | 版本号旁有 **Provenance** 徽章 |
99
106
 
100
107
  ## 发完之后:让 DSH 用上新版
101
108
 
102
- profile 把版本钉在 `~/.dsh/profiles/<profile>/package.json` 里(例如 `"dsh-thesis": "1.0.1"`),
103
- 所以 npm 上发了新版**不会自动生效**。到插件管理器里移除再装一次(会取 `latest`),或者把那条依赖
104
- 改成 `"^2.0.1"` 后重装,然后**重启 DSH**——2.0 删掉了全部斜杠命令,不是热更新能覆盖的。
109
+ profile 把版本钉在 `~/.dsh/profiles/<profile>/package.json` 里,npm 发了新版**不会自动生效**:
105
110
 
106
- ## 手动触发
111
+ ```bash
112
+ dsh plugin --profile web add dsh-thesis@2.2.0
113
+ dsh plugin --profile desktop add dsh-thesis@2.2.0
114
+ ```
107
115
 
108
- Actions → publish → Run workflow:只做打包预览,**不会发布**。用来确认「这次发出去的包里到底有什么」。
109
- 想真发版,还是打 tag。
116
+ 然后**重启 DSH**(2.0 删掉了全部斜杠命令,不是热更新能覆盖的)。
110
117
 
111
118
  ## 失败排查
112
119
 
113
120
  | 现象 | 原因 |
114
121
  |---|---|
115
- | `EOTP` / `This operation requires a one-time password` | 用的是需要 OTP 的 token(2FA 账号的 Granular / Publish token)。CI 里没人能输 OTP:改用可信发布,或换 Classic → Automation token |
116
- | 发布被拒,提示只能 `npm stage publish` | 可信发布里没勾 **Allow publish directly**(默认只允许 stage)|
117
- | `403 … OIDC permission denied for this action` | 同一条:OIDC 认证已经通过(组织/仓库/workflow 名字都对),只是没给「直接发布」权限。去包设置里编辑那条配置,勾上 **Allow npm publish** |
118
- | `401 Unauthorized` | token 过期(Automation token 有 90/180 天寿命),或可信发布配置里 workflow 文件名写错 |
119
- | `npm ci … not in sync` | 改了 `package.json` 的依赖/peer 却没跑 `npm install --package-lock-only` |
120
- | `403 … cannot publish over previously published version` | 版本号没升;这条在 workflow 里已被「已存在则跳过」挡住,手动发版时要注意 |
121
- | 守卫里 `单元/契约测试` 红 | 本地 `npm test` 先复现;守卫跑的就是它 |
122
+ | Actions 里**根本没有** publish run | push 的不是 `main`,也不是 `v*` tag |
123
+ | publish 绿但 Summary 写"已存在,未发布" | 版本号没升(`npm run release:*` 没跑)|
124
+ | `EOTP` / 要求一次性密码 | 用的是需要 OTP 的 token → 改用可信发布或 Automation token |
125
+ | 被拒、提示只能 `npm stage publish` | 可信发布里没勾 **Allow npm publish** |
126
+ | `403 … OIDC permission denied for this action` | 同上:认证过了,但没给「直接发布」权限 |
127
+ | `401 Unauthorized` | token 过期,或可信发布配置里 workflow 文件名写错 |
128
+ | `npm ci … not in sync` | 手改了 `package.json` 版本没同步锁文件 → 改用 `npm run release:*` |
129
+ | `403 … cannot publish over previously published version` | 版本号没升;workflow 里"已存在则跳过"会挡住,手动发版时要注意 |
130
+ | 守卫里单测红 / 端到端红 | 与 `npm run verify` 同一套;本地先复现 |
122
131
  | 守卫警告「未找到 DSH 安装」 | CI 里正常(没有宿主);**发版前请在本地跑一次 `npm run test:host-load` 与 `npm run test:host`** |
132
+ | 自动打 tag 失败但 npm 已发布 | tag 只是标记,事后手动 `git tag v2.2.0 && git push origin v2.2.0` 补上即可 |
123
133
 
124
134
  ## 撤回与补救
125
135
 
126
- - **已发布的版本不能改**,只能 `npm deprecate dsh-thesis@2.0.1 "原因与替代版本"`。
136
+ - **已发布的版本不能改**,只能 `npm deprecate dsh-thesis@2.2.0 "原因与替代版本"`。
127
137
  - 72 小时内理论上可以 `npm unpublish`,但那会让依赖它的人突然装不上,**不要用**。
128
- - 发错了就立刻发一个 patch:`2.0.2`。
138
+ - 发错了就立刻发一个 patch:`npm run release:patch` + `git push`。
129
139
 
130
- ## 仍然可以本地发布
140
+ ## 本地发布(应急)
131
141
 
132
142
  ```bash
133
143
  npm login
134
144
  npm publish --access public # 会跑同一个 prepublishOnly 守卫,会交互式要 OTP
135
145
  ```
136
146
 
137
- 本地发布用的是你登录后的凭据(可以输 OTP),不需要 token,也不需要可信发布配置。
138
- CI 只是把「构建 → 校验 → 发布」串起来,判据完全一样。
147
+ 本地发布用你登录后的凭据,不需要 token,也不需要可信发布配置。CI 只是把「构建 → 校验 → 发布」串起来,判据完全一样。
@@ -259,6 +259,9 @@ function buildFlow(model) {
259
259
  rows.set(row, bucket);
260
260
  }
261
261
  const rowRanks = [...rows.keys()].sort((a, b) => a - b);
262
+ // 层数一多,固定 48 的行距会把整张图拉成两三页高(8 层 ≈ 740 单位高)。
263
+ // 层数超过 4 就收紧行距:A4 正文宽 15.5cm 时,这张图仍能一页放下。
264
+ const gapY = rowRanks.length <= 4 ? cfg.gapY : Math.max(30, cfg.gapY - 14);
262
265
  const nodeWidthOf = (node) => (node.kind === 'decision' ? cfg.decisionWidth : cfg.nodeWidth);
263
266
  const nodeHeightOf = (node) => (node.kind === 'decision' ? cfg.decisionHeight : cfg.nodeHeight);
264
267
  // 先以「中轴 x=0」排布,最后整体平移:主干节点落在中轴上,其它节点摆在它右侧。
@@ -268,7 +271,7 @@ function buildFlow(model) {
268
271
  let maxX = 0;
269
272
  rowRanks.forEach((rankNo, rowIndex) => {
270
273
  const nodes = rows.get(rankNo) ?? [];
271
- const rowY = LAYOUT.margin + rowIndex * (cfg.nodeHeight + cfg.gapY);
274
+ const rowY = LAYOUT.margin + rowIndex * (cfg.nodeHeight + gapY);
272
275
  if (nodes.length > 1)
273
276
  notes.push(`第 ${rankNo + 1} 层有 ${nodes.length} 个并行节点(${nodes.map(n => n.text).join('、')})`);
274
277
  const main = nodes.find(node => mainPath.has(node.id));
@@ -303,6 +306,10 @@ function buildFlow(model) {
303
306
  for (const [id, rect] of positions) {
304
307
  positions.set(id, { ...rect, x: rect.x + shift });
305
308
  }
309
+ // 跳层连线的绕行通道:放在内容右侧。不绕行的话,draw.io 会把「判断 → 第 N 层」这类
310
+ // 跨层边画成一条竖线,正好从中间几层的节点身上穿过去(看上去像把框划开了)。
311
+ const laneX = maxX + shift + 26;
312
+ const contentWidth = Math.max(460, laneX + LAYOUT.margin - shift);
306
313
  for (const node of model.nodes) {
307
314
  const rect = positions.get(node.id);
308
315
  if (rect === undefined)
@@ -325,20 +332,29 @@ function buildFlow(model) {
325
332
  throw new FigureLayoutError(`连线端点「${edge.from} → ${edge.to}」在版面里找不到`);
326
333
  const fromRect = positions.get(edge.from);
327
334
  const toRect = positions.get(edge.to);
328
- const horizontal = fromRect !== undefined && toRect !== undefined && Math.abs(fromRect.y - toRect.y) < 1;
335
+ const fromRank = rank.get(edge.from) ?? 0;
336
+ const toRank = rank.get(edge.to) ?? 0;
337
+ const skipsRanks = toRank - fromRank > 1;
338
+ const horizontal = !skipsRanks && fromRect !== undefined && toRect !== undefined && Math.abs(fromRect.y - toRect.y) < 1;
339
+ const style = skipsRanks
340
+ ? `${STYLE.edge}exitX=1;exitY=0.5;entryX=1;entryY=0.5;`
341
+ : horizontal
342
+ ? `${STYLE.edge}exitX=1;exitY=0.5;entryX=0;entryY=0.5;`
343
+ : `${STYLE.edge}exitX=0.5;exitY=1;entryX=0.5;entryY=0;`;
329
344
  cells.push({
330
345
  kind: 'edge',
331
346
  id: nextEdge(),
332
347
  value: edge.label ?? '',
333
- style: horizontal
334
- ? `${STYLE.edge}exitX=1;exitY=0.5;entryX=0;entryY=0.5;`
335
- : `${STYLE.edge}exitX=0.5;exitY=1;entryX=0.5;entryY=0;`,
348
+ style,
336
349
  source,
337
350
  target,
351
+ ...(skipsRanks && fromRect !== undefined && toRect !== undefined
352
+ ? { points: [{ x: laneX, y: fromRect.y + fromRect.height / 2 }, { x: laneX, y: toRect.y + toRect.height / 2 }] }
353
+ : {}),
338
354
  });
339
355
  }
340
- const height = LAYOUT.margin + rowRanks.length * (cfg.nodeHeight + cfg.gapY) - cfg.gapY + LAYOUT.margin;
341
- return { cells, width, height, notes };
356
+ const height = LAYOUT.margin + rowRanks.length * (cfg.nodeHeight + gapY) - gapY + LAYOUT.margin;
357
+ return { cells, width: contentWidth, height, notes };
342
358
  }
343
359
  // ---------------------------------------------------------------------------
344
360
  // E-R 图
@@ -360,6 +376,7 @@ function buildEr(model) {
360
376
  const columns = model.entities.length > 1 ? 2 : 1;
361
377
  const width = LAYOUT.margin * 2 + maxWidth * columns + (columns - 1) * cfg.columnGap;
362
378
  const entityIds = new Map();
379
+ const entityRects = new Map();
363
380
  const columnY = new Array(columns).fill(LAYOUT.margin);
364
381
  model.entities.forEach((entity, index) => {
365
382
  const column = index % columns;
@@ -369,6 +386,7 @@ function buildEr(model) {
369
386
  const y = columnY[column] ?? LAYOUT.margin;
370
387
  const id = nextVertex('entity');
371
388
  entityIds.set(entity.name, id);
389
+ entityRects.set(entity.name, { x, y, width: widthForEntity, height });
372
390
  // 不可见的容器先画(在下层):关系线连到整张表,而不是某一行;
373
391
  // 先入队保证它垫在标题与字段下面,不遮挡文字与点击。
374
392
  cells.push({
@@ -405,11 +423,21 @@ function buildEr(model) {
405
423
  if (source === undefined || target === undefined)
406
424
  throw new FigureLayoutError(`关系端点「${relation.from} → ${relation.to}」在版面里找不到`);
407
425
  const label = [relation.cardinality, relation.label].filter(part => part !== undefined && part !== '').join(' ');
426
+ // 关系线必须锚在实体**外框**上:不给锚点时 draw.io 会从容器中心起笔,
427
+ // 线直接从字段行中间穿过去(三张表并排时尤其明显)。
428
+ const fromRect = entityRects.get(relation.from);
429
+ const toRect = entityRects.get(relation.to);
430
+ const sameColumn = fromRect !== undefined && toRect !== undefined && Math.abs(fromRect.x - toRect.x) < 1;
431
+ const ports = fromRect === undefined || toRect === undefined || sameColumn
432
+ ? 'exitX=0.5;exitY=1;entryX=0.5;entryY=0;'
433
+ : toRect.x > fromRect.x
434
+ ? 'exitX=1;exitY=0.5;entryX=0;entryY=0.5;'
435
+ : 'exitX=0;exitY=0.5;entryX=1;entryY=0.5;';
408
436
  cells.push({
409
437
  kind: 'edge',
410
438
  id: nextEdge(),
411
439
  value: label,
412
- style: `${STYLE.edge}endArrow=none;startArrow=none;`,
440
+ style: `${STYLE.edge}endArrow=none;startArrow=none;${ports}`,
413
441
  source,
414
442
  target,
415
443
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-thesis",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "description": "论文与答辩全流程助手(DeepSeek Harness 插件):材料摄取 → 逐题追问 → 文献检索 → 逐章写作与评审 → 原创性自查与合规 → Word/答辩幻灯生产,全流程留痕,并跨会话记住学校规范与导师要求。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -60,6 +60,9 @@
60
60
  "test:host": "node scripts/host-smoke.ts && node .host-smoke/app/run.ts",
61
61
  "test:host-load": "node scripts/host-verify.ts",
62
62
  "audit": "node scripts/audit-surface.ts",
63
+ "release:patch": "node scripts/release.ts patch",
64
+ "release:minor": "node scripts/release.ts minor",
65
+ "release:major": "node scripts/release.ts major",
63
66
  "verify": "npm run build && npm run typecheck && npm run typecheck:scripts && npm run test:sandbox && npm run audit && npm run test:e2e",
64
67
  "prepublishOnly": "node scripts/prepublish-guard.ts"
65
68
  },
package/NOTICE.md DELETED
@@ -1,10 +0,0 @@
1
- # 来源与许可声明(NOTICE)
2
-
3
- 本仓库的全部代码与文档均为作者本人(booondenz)的原创作品,其中一部分由作者此前
4
- 在其它宿主环境下的同题材项目整理、合并、重写而来。**没有使用任何第三方代码**,
5
- 因此这里没有需要致谢的外部贡献者;本文件存在的唯一目的,是如实说明这一点。
6
-
7
- ## 许可
8
-
9
- MIT,以本仓库根目录的 [`LICENSE`](./LICENSE) 为准。被合并整理的早期作品同为该作者所有、
10
- 同为 MIT 许可,合并这件事本身不改变许可。