dsh-session-superagent 0.8.15 → 0.8.20
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/CHANGELOG.md +102 -0
- package/README.md +3 -3
- package/client.js +13 -2
- package/lib/index.js +65 -11
- package/package.json +5 -6
- package/scripts/port-official-ui.mjs +0 -40
- package/scripts/preview-orbit.mjs +0 -136
- package/scripts/sync-profile.mjs +0 -129
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,95 @@
|
|
|
2
2
|
|
|
3
3
|
格式参照 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
|
4
4
|
|
|
5
|
+
## [0.8.20] - 2026-11-02 —— 冷启动演练:工具返回要能证明「选项生效了」
|
|
6
|
+
|
|
7
|
+
照 HANDOFF + README 走一遍「加 watcher → 观察 → 确认」(不看代码,只看文档),暴露的摩擦点:
|
|
8
|
+
|
|
9
|
+
### 修复
|
|
10
|
+
- **`watch_start` 的返回看不出生效项**:只说 `watching … every 3600s`,`observe` / 过滤 / 时段 / 限流
|
|
11
|
+
到底有没有落上,只能再开面板或读状态文件。现在返回会带回显:
|
|
12
|
+
`[observe-only(只记录不唤醒) labels=ai-fix batch=5 perHour=6 goal="…"]`,更新已有 watcher 时标 `(updated existing watcher)`。
|
|
13
|
+
- **`watch_status` 同样不回显**:现在每行带 `observe / isolated / paused / hours= / labels= / onlyAssigned / deferred=`,
|
|
14
|
+
一眼能看出「为什么没动静」。
|
|
15
|
+
- HANDOFF 补「在会话里自查」小表:首选工具(`watch_status` / `watch_doctor`),其次直接读状态文件,
|
|
16
|
+
HTTP 只在面板/调试时用 —— 并写清端口每次重启都会变、怎么找(会话上下文里的 GUI URL 或按 PID 查 netstat)。
|
|
17
|
+
冷启动演练时就是卡在「端口找不到」这一步。
|
|
18
|
+
|
|
19
|
+
### 测试
|
|
20
|
+
- 合计 **85 例**。
|
|
21
|
+
|
|
22
|
+
## [0.8.19] - 2026-11-02 —— `pnpm run preflight`:把「发版必跑」变成一条命令
|
|
23
|
+
|
|
24
|
+
### 新增
|
|
25
|
+
- `scripts/preflight.mjs`:七项检查一次跑完并以退出码给结论 ——
|
|
26
|
+
单测、语法、profile 一致性(`--check`)、版本五处一致、路由面(6 条)、`npm pack` 内容、冒烟(gh 未登录自动 SKIP)。
|
|
27
|
+
HANDOFF 的验证清单从散文改成这一条命令(散文会过期,脚本不会)。
|
|
28
|
+
- 自测时抓到并修掉一个真问题:preflight 自己的 `DSH_SUPERAGENT_NO_POLL=1` 会**泄漏给子进程**,
|
|
29
|
+
导致冒烟「3 项 OK / 9 项 FAIL」。现在子进程环境会剔掉这三个开关。
|
|
30
|
+
|
|
31
|
+
### 变更
|
|
32
|
+
- **包内容精简**:`files` 去掉 `scripts/`,只发运行时需要的 6 项(`lib`、`client.js`、
|
|
33
|
+
`cordis.patch.yml`、README、CHANGELOG、LICENSE)。`npm pack` 从 14 个文件降到 10 个。
|
|
34
|
+
开发脚本仍在仓库里(`pnpm run preview` / `smoke:observe` / `preflight` 照旧可用)。
|
|
35
|
+
|
|
36
|
+
### 测试
|
|
37
|
+
- 合计 **85 例**(本轮无新增用例,改动是工具与打包)。
|
|
38
|
+
|
|
39
|
+
## [0.8.18] - 2026-11-02 —— watch_start 工具补齐参数(面板能配的,工具也要能配)
|
|
40
|
+
|
|
41
|
+
### 修复
|
|
42
|
+
- **`watch_start` 之前只接受 `repo` / `intervalSec`**:面板上能设的过滤、仅观察、隔离、时段、
|
|
43
|
+
限流、每 watcher 目标,用工具都设不了 —— 而 README 把它们都写成能力。现在工具参数补齐
|
|
44
|
+
(`labels` / `ignoreLabels` / `onlyAssigned` / `assignee` / `observe` / `isolated` / `workdir` /
|
|
45
|
+
`activeHours` / `batchLimit` / `maxWakesPerHour` / `goal`)。
|
|
46
|
+
- **`intervalSec` 没夹紧**:工具里只判断 `> 0`,能把 5 秒写进状态文件(调度时按 30 秒跑,
|
|
47
|
+
状态与实际不一致,面板也会显示「每 5s」)。现在夹到 30–3600。
|
|
48
|
+
- `workdir` 走工具时也要求绝对路径且存在。
|
|
49
|
+
|
|
50
|
+
### 测试
|
|
51
|
+
- 新增 2 例:工具参数落盘与夹紧(5→30、99→20、0→1)、相对 `workdir` 被拒。合计 **84 例**。
|
|
52
|
+
|
|
53
|
+
## [0.8.17] - 2026-11-02 —— README 声明与面板能力对齐(限流可改)
|
|
54
|
+
|
|
55
|
+
对着 README 逐条核代码时发现的落差:`batchLimit` / `maxWakesPerHour` 在服务端和工具里都支持,
|
|
56
|
+
**面板却没法设** —— 用户按 README 找这两个开关会找不到。
|
|
57
|
+
|
|
58
|
+
### 修复
|
|
59
|
+
- 「触发」页新增两个字段:**单次最多唤醒条数**(默认 5,夹到 1-20)、**每小时最多唤醒次数**(默认 6,夹到 1-60)。
|
|
60
|
+
- 行内编辑器同样能改这两项,保存走同一个 `POST /watch`。
|
|
61
|
+
- README 的限流一行补上「面板可改」并标注默认值与范围。
|
|
62
|
+
|
|
63
|
+
### 测试
|
|
64
|
+
- 新增用例:面板设置 `maxWakesPerHour: 99` 会被夹成 60 并回传。合计 **82 例**。
|
|
65
|
+
|
|
66
|
+
## [0.8.16] - 2026-11-02 —— 第一次在真浏览器里看面板,抓到一个对比度问题
|
|
67
|
+
|
|
68
|
+
宿主的 web 界面有鉴权(`dsh web authentication required`),ego 浏览器过不去,所以搭了个
|
|
69
|
+
**静态预览**:`scripts/render-preview.mjs` 用极简假 React 把真实 `client.js` 的组件渲染成 DOM,
|
|
70
|
+
内联移植来的官方 CSS,六个场景(总览/判定/触发/设置/术语表/两种 Toast)一起看。
|
|
71
|
+
|
|
72
|
+
### 修复(真浏览器里发现的)
|
|
73
|
+
- **错误 Toast 白字看不清**:0.8.12 给错误态加了「浅红底」,而官方 toast 是深底白字——
|
|
74
|
+
白字压在浅粉上对比度极低。现在保留官方深底,改用**左侧红条**标记错误。
|
|
75
|
+
|
|
76
|
+
### 验证(在 Chromium 里读 computed style,不是猜)
|
|
77
|
+
```
|
|
78
|
+
模态框 position:fixed; inset:0,宽度 380 居中 ← 「弹层掉到页面底部」的 bug 确认已修
|
|
79
|
+
Toast position:fixed; top:40px; z-index:1100; 299×46 深底白字
|
|
80
|
+
错误态 背景 #24292f / 白字 / 左边框 #f85149
|
|
81
|
+
标签 success 绿底绿字、warning 琥珀底琥珀字(tone 与 CSS 对得上)
|
|
82
|
+
列表行 高 102–122px(三线结构:标题 / meta / 操作)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
顺带搞清了 `dt-tag` 的颜色依赖 `--dsw-alias-state-{success,warn,error,info}-primary` 这几个 token
|
|
86
|
+
(之前预览里缺,标签看着是裸文字——这和审核第二轮发现的「状态色全丢」是同一个根因)。
|
|
87
|
+
|
|
88
|
+
### 工具
|
|
89
|
+
- `pnpm run preview` 生成 `preview-panel.html`(已加进 .gitignore,不进仓库/不发包)。
|
|
90
|
+
|
|
91
|
+
### 测试
|
|
92
|
+
- 合计 **81 例**(本次是样式与渲染层修复,靠真浏览器核对)。
|
|
93
|
+
|
|
5
94
|
## [0.8.15] - 2026-11-02 —— sync-profile 支持修复断链(事故 1 的第二种形态)
|
|
6
95
|
|
|
7
96
|
### 修什么
|
|
@@ -24,6 +113,19 @@ pnpm run sync:profile:check # 只检查:有缺失/断链/陈旧就 exit 1
|
|
|
24
113
|
- 新增用例:`sync-profile` 的 `TARGETS` 必须覆盖 `lib/` 下所有 js 文件 ——
|
|
25
114
|
防止「新增 lib 文件忘了登记」再次导致插件不挂载。
|
|
26
115
|
|
|
116
|
+
### 发布
|
|
117
|
+
- 已发 `dsh-session-superagent@0.8.15`(registry `latest = 0.8.15`)。
|
|
118
|
+
- tarball 直装验证:`version 0.8.15`、**routes 6**、`client.js` 语法通过、包内 `scripts/` 三个脚本齐全。
|
|
119
|
+
|
|
120
|
+
### 发布前快照(每次发版都跑这一套)
|
|
121
|
+
```
|
|
122
|
+
node --test 81/81
|
|
123
|
+
node --check client.js lib/*.js scripts/*.mjs 全过
|
|
124
|
+
sync-profile --check 缺失 0 / 断链 0 / 陈旧 0,相对 import 全可解析
|
|
125
|
+
smoke-observe 两场景 12 项 OK
|
|
126
|
+
npm pack --dry-run 13 文件 / 75.4 kB
|
|
127
|
+
```
|
|
128
|
+
|
|
27
129
|
### 测试
|
|
28
130
|
- 合计 **81 例**。
|
|
29
131
|
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# dsh-session-superagent
|
|
2
2
|
|
|
3
3
|
[](LICENSE)
|
|
4
|
-
[](https://github.com/MYCF711/dsh-session-superagent/releases)
|
|
5
5
|
[](https://www.npmjs.com/package/dsh-session-superagent)
|
|
6
6
|
[](https://github.com/topics/dsh-plugin)
|
|
7
7
|
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
| 盯仓库 | `watch_start` / `watch_status` / `watch_stop` | 30–3600 秒轮询;有变化时用 `Agent.followup()` 唤醒**本会话** |
|
|
22
22
|
| 传递规则 | 仓库里的 `AGENTS.md` | 唤醒消息只说“有新动态”,怎么处理由项目规则决定 |
|
|
23
23
|
| 收窄范围 | `filters` | 只处理带指定标签、或已指派给人的条目;默认跳过机器人、草稿 PR、已关闭条目 |
|
|
24
|
-
| 限流 | `batchLimit` / `maxWakesPerHour` | 单次最多铺开 N
|
|
24
|
+
| 限流 | `batchLimit` / `maxWakesPerHour` | 单次最多铺开 N 条(默认 5,1-20),每小时最多唤醒 N 次(默认 6,1-60),超了攒着;**面板「触发」页可改** |
|
|
25
25
|
| 幂等 | `watch_ack` 工具 | 处理完给条目打「主理鱼:已处理」标签(标签不存在会自动建),轮询直接跳过(换机器也不重报);支持 `numbers: "12,13"` 批量 |
|
|
26
26
|
| 时段 | `activeHours` | 只在 `09:00-23:00` 这类时段唤醒,夜里攒着;支持跨夜 `22:00-06:00` |
|
|
27
27
|
| 隔离 | `isolated` | 每次唤醒前开一次性 `git worktree`,改副本、推 `zhuli/*` 分支,不动主工作区;超过 3 天的旧副本自动回收 |
|
|
@@ -88,7 +88,7 @@ dsh plugin add https://github.com/MYCF711/dsh-session-superagent
|
|
|
88
88
|
## 开发
|
|
89
89
|
|
|
90
90
|
```sh
|
|
91
|
-
node --test #
|
|
91
|
+
node --test # 85 项单测
|
|
92
92
|
```
|
|
93
93
|
|
|
94
94
|
版本与发布流程见 [UPDATING.md](UPDATING.md),交付门见 [CHECKLIST.md](CHECKLIST.md),接手台账见 [MAINTENANCE.md](MAINTENANCE.md)。
|
package/client.js
CHANGED
|
@@ -14,7 +14,7 @@ window.__ModuleLoader__.load({
|
|
|
14
14
|
const { useCallback, useEffect, useRef, useState } = React
|
|
15
15
|
|
|
16
16
|
// 客户端版本探针:每次模块加载写一次,用来确认浏览器里跑的是哪一版
|
|
17
|
-
const CLIENT_VERSION = '0.8.
|
|
17
|
+
const CLIENT_VERSION = '0.8.20'
|
|
18
18
|
const BASE = '/session-superagent'
|
|
19
19
|
const ENDPOINT = BASE + '/state'
|
|
20
20
|
const POLL_MS = 5000
|
|
@@ -614,8 +614,9 @@ window.__ModuleLoader__.load({
|
|
|
614
614
|
className: cx('dt-toast', item.tone === 'error' && 'dt-danger'),
|
|
615
615
|
role: 'status',
|
|
616
616
|
'aria-live': 'polite',
|
|
617
|
+
// 官方 toast 是深底白字;错误态不要改成浅红底(白字会看不清),改用左侧红条标记
|
|
617
618
|
style: item.tone === 'error'
|
|
618
|
-
? { '--dt-toast-hold': '3600ms',
|
|
619
|
+
? { '--dt-toast-hold': '3600ms', borderLeft: '3px solid #f85149' }
|
|
619
620
|
: { '--dt-toast-hold': '3600ms' },
|
|
620
621
|
}, item.text)
|
|
621
622
|
return [view, notify]
|
|
@@ -698,6 +699,8 @@ window.__ModuleLoader__.load({
|
|
|
698
699
|
isolated: !!draft.isolated,
|
|
699
700
|
workdir: draft.workdir || '',
|
|
700
701
|
observe: !!draft.observe,
|
|
702
|
+
batchLimit: Number(draft.batchLimit) || undefined,
|
|
703
|
+
maxWakesPerHour: Number(draft.maxWakesPerHour) || undefined,
|
|
701
704
|
}),
|
|
702
705
|
})
|
|
703
706
|
setEditing(null)
|
|
@@ -731,6 +734,8 @@ window.__ModuleLoader__.load({
|
|
|
731
734
|
isolated: !!form.isolated,
|
|
732
735
|
workdir: form.workdir || '',
|
|
733
736
|
observe: !!form.observe,
|
|
737
|
+
batchLimit: Number(form.batchLimit) || undefined,
|
|
738
|
+
maxWakesPerHour: Number(form.maxWakesPerHour) || undefined,
|
|
734
739
|
}),
|
|
735
740
|
})
|
|
736
741
|
setForm({ repo: '', intervalSec: form.intervalSec, goal: '', labels: '', onlyAssigned: false, activeHours: form.activeHours, isolated: form.isolated, workdir: form.workdir, observe: form.observe })
|
|
@@ -797,6 +802,8 @@ window.__ModuleLoader__.load({
|
|
|
797
802
|
isolated: !!watcher.isolated,
|
|
798
803
|
workdir: watcher.workdir || '',
|
|
799
804
|
observe: !!watcher.observe,
|
|
805
|
+
batchLimit: String(watcher.batchLimit ?? 5),
|
|
806
|
+
maxWakesPerHour: String(watcher.maxWakesPerHour ?? 6),
|
|
800
807
|
})
|
|
801
808
|
},
|
|
802
809
|
}, editing === key ? '收起' : '编辑'),
|
|
@@ -817,6 +824,8 @@ window.__ModuleLoader__.load({
|
|
|
817
824
|
field('活跃时段', h(Input, { value: draft.activeHours, placeholder: '09:00-23:00,留空 = 全天', onChange: (e) => setDraft({ ...draft, activeHours: e.target.value }) })),
|
|
818
825
|
field('目标/规则', h(Input, { value: draft.goal || '', placeholder: '可选,会附在唤醒消息里', onChange: (e) => setDraft({ ...draft, goal: e.target.value }) })),
|
|
819
826
|
h('label', { className: 'dt-switch-row' }, h(Toggle, { checked: !!draft.onlyAssigned, onChange: (v) => setDraft({ ...draft, onlyAssigned: v }), label: '只看已指派' }), '只处理已指派给人的条目'),
|
|
827
|
+
field('单次最多唤醒条数', h(Input, { value: draft.batchLimit || '', placeholder: '5(1-20)', onChange: (e) => setDraft({ ...draft, batchLimit: e.target.value }) })),
|
|
828
|
+
field('每小时最多唤醒次数', h(Input, { value: draft.maxWakesPerHour || '', placeholder: '6(1-60)', onChange: (e) => setDraft({ ...draft, maxWakesPerHour: e.target.value }) })),
|
|
820
829
|
h('label', { className: 'dt-switch-row' }, h(Toggle, { checked: !!draft.observe, onChange: (v) => setDraft({ ...draft, observe: v }), label: '仅观察' }), '仅观察(只记录不唤醒)'),
|
|
821
830
|
h('label', { className: 'dt-switch-row' }, h(Toggle, { checked: !!draft.isolated, onChange: (v) => setDraft({ ...draft, isolated: v }), label: '隔离执行' }), '隔离执行(每次唤醒开一次性 worktree)'),
|
|
822
831
|
draft.isolated ? field('隔离工作目录', h(Input, { value: draft.workdir || '', placeholder: '绝对路径,留空 = 当前工作目录', onChange: (e) => setDraft({ ...draft, workdir: e.target.value }) })) : null,
|
|
@@ -887,6 +896,8 @@ window.__ModuleLoader__.load({
|
|
|
887
896
|
h('div', { className: 'dt-form' },
|
|
888
897
|
field('仓库', h(Input, { value: form.repo, placeholder: 'owner/name', onChange: (e) => setForm({ ...form, repo: e.target.value }) })),
|
|
889
898
|
field('间隔(秒)', h(Input, { value: form.intervalSec, onChange: (e) => setForm({ ...form, intervalSec: e.target.value }) })),
|
|
899
|
+
field('单次最多唤醒条数', h(Input, { value: form.batchLimit || '', placeholder: '5(1-20)', onChange: (e) => setForm({ ...form, batchLimit: e.target.value }) })),
|
|
900
|
+
field('每小时最多唤醒次数', h(Input, { value: form.maxWakesPerHour || '', placeholder: '6(1-60)', onChange: (e) => setForm({ ...form, maxWakesPerHour: e.target.value }) })),
|
|
890
901
|
field('只处理这些标签', h(Input, { value: form.labels, placeholder: '逗号分隔,留空 = 全部', onChange: (e) => setForm({ ...form, labels: e.target.value }) })),
|
|
891
902
|
field('活跃时段', h(Input, { value: form.activeHours, placeholder: '09:00-23:00,留空 = 全天', onChange: (e) => setForm({ ...form, activeHours: e.target.value }) })),
|
|
892
903
|
field('目标/规则', h(Input, { value: form.goal || '', placeholder: '可选,会附在唤醒消息里', onChange: (e) => setForm({ ...form, goal: e.target.value }) })),
|
package/lib/index.js
CHANGED
|
@@ -1018,7 +1018,18 @@ export function apply(ctx) {
|
|
|
1018
1018
|
description: 'Watch a GitHub repository and wake THIS session when its issues or pull requests change. Idempotent per (repo, session). Default interval 60 s, minimum 30 s.',
|
|
1019
1019
|
parameters: {
|
|
1020
1020
|
repo: { type: 'string', description: 'owner/name. Omit to infer from the current working directory.' },
|
|
1021
|
-
intervalSec: { type: 'number', description: 'Polling interval in seconds, default 60.' },
|
|
1021
|
+
intervalSec: { type: 'number', description: 'Polling interval in seconds, clamped to 30-3600, default 60.' },
|
|
1022
|
+
labels: { type: 'array', items: { type: 'string' }, description: 'Only handle items carrying one of these labels (whitelist).' },
|
|
1023
|
+
ignoreLabels: { type: 'array', items: { type: 'string' }, description: 'Skip items carrying any of these labels.' },
|
|
1024
|
+
onlyAssigned: { type: 'boolean', description: 'Only handle items assigned to someone (or to `assignee`).' },
|
|
1025
|
+
assignee: { type: 'string', description: 'With onlyAssigned: require this login.' },
|
|
1026
|
+
observe: { type: 'boolean', description: 'Observe only: record what WOULD be woken, never wake.' },
|
|
1027
|
+
isolated: { type: 'boolean', description: 'Run each wake in a throwaway git worktree.' },
|
|
1028
|
+
workdir: { type: 'string', description: 'Repository used for isolation worktrees (absolute path).' },
|
|
1029
|
+
activeHours: { type: 'string', description: 'Wake only inside this window, e.g. "09:00-23:00" (supports overnight).' },
|
|
1030
|
+
batchLimit: { type: 'number', description: 'Max items listed per wake (1-20, default 5).' },
|
|
1031
|
+
maxWakesPerHour: { type: 'number', description: 'Max wakes per hour (1-60, default 6).' },
|
|
1032
|
+
goal: { type: 'string', description: 'Optional per-watcher goal appended to the wake message.' },
|
|
1022
1033
|
},
|
|
1023
1034
|
async execute(args) {
|
|
1024
1035
|
const agent = ctx.agents && typeof ctx.agents.currentInitiator === 'function' ? ctx.agents.currentInitiator() : undefined
|
|
@@ -1044,14 +1055,46 @@ export function apply(ctx) {
|
|
|
1044
1055
|
enabled: true,
|
|
1045
1056
|
createdAt: new Date().toISOString(),
|
|
1046
1057
|
}
|
|
1047
|
-
if (typeof args.intervalSec === 'number' && args.intervalSec > 0)
|
|
1058
|
+
if (typeof args.intervalSec === 'number' && args.intervalSec > 0) {
|
|
1059
|
+
// 之前只判断 >0,能把 5 秒存进状态文件(调度时会按 30 秒跑,但状态与实际不一致)
|
|
1060
|
+
entry.intervalSec = Math.max(30, Math.min(3600, Math.round(args.intervalSec)))
|
|
1061
|
+
}
|
|
1062
|
+
const patch = {}
|
|
1063
|
+
if (args.labels !== undefined) patch.labels = args.labels
|
|
1064
|
+
if (args.ignoreLabels !== undefined) patch.ignoreLabels = args.ignoreLabels
|
|
1065
|
+
if (args.onlyAssigned !== undefined) patch.onlyAssigned = args.onlyAssigned
|
|
1066
|
+
if (args.assignee !== undefined) patch.assignee = args.assignee
|
|
1067
|
+
if (Object.keys(patch).length > 0) entry.filters = mergeFilters(entry.filters, patch)
|
|
1068
|
+
if (typeof args.goal === 'string') entry.goal = args.goal.slice(0, 500)
|
|
1069
|
+
if (typeof args.observe === 'boolean') entry.observe = args.observe
|
|
1070
|
+
if (typeof args.isolated === 'boolean') entry.isolated = args.isolated
|
|
1071
|
+
if (typeof args.workdir === 'string' && args.workdir.trim()) {
|
|
1072
|
+
const workdir = args.workdir.trim()
|
|
1073
|
+
if (!isAbsolute(workdir)) return '[error: workdir must be an absolute path]'
|
|
1074
|
+
if (!existsSync(workdir)) return '[error: workdir does not exist: ' + workdir + ']'
|
|
1075
|
+
entry.workdir = workdir
|
|
1076
|
+
}
|
|
1077
|
+
if (typeof args.activeHours === 'string') entry.activeHours = args.activeHours.trim() || null
|
|
1078
|
+
if (typeof args.batchLimit === 'number') entry.batchLimit = Math.max(1, Math.min(20, Math.round(args.batchLimit)))
|
|
1079
|
+
if (typeof args.maxWakesPerHour === 'number') entry.maxWakesPerHour = Math.max(1, Math.min(60, Math.round(args.maxWakesPerHour)))
|
|
1048
1080
|
entry.enabled = true
|
|
1049
1081
|
state.watchers = state.watchers || {}
|
|
1050
1082
|
state.watchers[id] = entry
|
|
1051
1083
|
writeState(state)
|
|
1052
1084
|
stopWatcher(id)
|
|
1053
1085
|
startWatcher(entry)
|
|
1054
|
-
|
|
1086
|
+
const flags = []
|
|
1087
|
+
if (entry.observe === true) flags.push('observe-only(只记录不唤醒)')
|
|
1088
|
+
if (entry.isolated === true) flags.push('isolated')
|
|
1089
|
+
if (entry.activeHours) flags.push('activeHours=' + entry.activeHours)
|
|
1090
|
+
if (entry.filters && entry.filters.labels && entry.filters.labels.length) flags.push('labels=' + entry.filters.labels.join(','))
|
|
1091
|
+
if (entry.filters && entry.filters.onlyAssigned) flags.push('onlyAssigned' + (entry.filters.assignee ? '(' + entry.filters.assignee + ')' : ''))
|
|
1092
|
+
if (entry.batchLimit) flags.push('batch=' + entry.batchLimit)
|
|
1093
|
+
if (entry.maxWakesPerHour) flags.push('perHour=' + entry.maxWakesPerHour)
|
|
1094
|
+
if (entry.goal) flags.push('goal=' + JSON.stringify(entry.goal.slice(0, 40)))
|
|
1095
|
+
return `watching ${repo} → session ${sessionId} every ${entry.intervalSec}s` +
|
|
1096
|
+
(flags.length ? ' [' + flags.join(' ') + ']' : '') +
|
|
1097
|
+
(existing ? ' (updated existing watcher)' : '')
|
|
1055
1098
|
},
|
|
1056
1099
|
}))
|
|
1057
1100
|
|
|
@@ -1064,14 +1107,25 @@ export function apply(ctx) {
|
|
|
1064
1107
|
const list = Object.values(state.watchers || {})
|
|
1065
1108
|
if (!list.length) return '(no watchers)'
|
|
1066
1109
|
return list
|
|
1067
|
-
.map((w) =>
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
'
|
|
1073
|
-
'
|
|
1074
|
-
|
|
1110
|
+
.map((w) => {
|
|
1111
|
+
const flags = []
|
|
1112
|
+
if (w.observe === true) flags.push('observe')
|
|
1113
|
+
if (w.isolated === true) flags.push('isolated')
|
|
1114
|
+
if (w.enabled === false) flags.push('paused')
|
|
1115
|
+
if (w.activeHours) flags.push('hours=' + w.activeHours)
|
|
1116
|
+
if (w.filters && w.filters.labels && w.filters.labels.length) flags.push('labels=' + w.filters.labels.join(','))
|
|
1117
|
+
if (w.filters && w.filters.onlyAssigned) flags.push('onlyAssigned')
|
|
1118
|
+
if (w.lastDeferred) flags.push('deferred=' + JSON.stringify(String(w.lastDeferred).slice(0, 40)))
|
|
1119
|
+
return [
|
|
1120
|
+
w.repo, '→', w.sessionId,
|
|
1121
|
+
'| every ' + w.intervalSec + 's',
|
|
1122
|
+
flags.length ? '| ' + flags.join(' ') : '',
|
|
1123
|
+
'| cursor=' + (w.cursor || '-'),
|
|
1124
|
+
'| lastPoll=' + (w.lastPoll || '-'),
|
|
1125
|
+
'| lastWake=' + (w.lastWake || '-'),
|
|
1126
|
+
'| err=' + (w.lastError || '-'),
|
|
1127
|
+
].filter(Boolean).join(' ')
|
|
1128
|
+
})
|
|
1075
1129
|
.join('\n')
|
|
1076
1130
|
},
|
|
1077
1131
|
}))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-session-superagent",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.20",
|
|
4
4
|
"description": "Session-level superagent for DeepSeek Harness: watch a GitHub repository and wake a session when something needs doing. Credentials never reach the model.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -17,10 +17,7 @@
|
|
|
17
17
|
"README.md",
|
|
18
18
|
"CHANGELOG.md",
|
|
19
19
|
"LICENSE",
|
|
20
|
-
"client.js"
|
|
21
|
-
"scripts/preview-orbit.mjs",
|
|
22
|
-
"scripts/port-official-ui.mjs",
|
|
23
|
-
"scripts/sync-profile.mjs"
|
|
20
|
+
"client.js"
|
|
24
21
|
],
|
|
25
22
|
"engines": {
|
|
26
23
|
"node": ">=22.19.0",
|
|
@@ -33,7 +30,9 @@
|
|
|
33
30
|
"sync:profile": "node scripts/sync-profile.mjs",
|
|
34
31
|
"smoke:observe": "node scripts/smoke-observe.mjs",
|
|
35
32
|
"sync:profile:check": "node scripts/sync-profile.mjs --check",
|
|
36
|
-
"sync:profile:repair": "node scripts/sync-profile.mjs --repair"
|
|
33
|
+
"sync:profile:repair": "node scripts/sync-profile.mjs --repair",
|
|
34
|
+
"preview": "node scripts/render-preview.mjs",
|
|
35
|
+
"preflight": "node scripts/preflight.mjs"
|
|
37
36
|
},
|
|
38
37
|
"keywords": [
|
|
39
38
|
"dsh",
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
// 把官方 dsh-superagent 的 GUI 样式表移植进 client.js(可重复执行,幂等)。
|
|
2
|
-
//
|
|
3
|
-
// 官方包的 CSS 在编译产物里是 `var client_default = "<JSON 转义的 CSS>";`,
|
|
4
|
-
// 这里用 JSONDecoder 的等价物(JSON.parse 的 raw 扫描)取出来,再以 JSON 字符串
|
|
5
|
-
// 塞进本仓库 client.js 的 `const PORTED_CSS = "…";` 行。
|
|
6
|
-
//
|
|
7
|
-
// 用法: node scripts/port-official-ui.mjs [官方包路径]
|
|
8
|
-
import { readFileSync, writeFileSync } from 'node:fs'
|
|
9
|
-
|
|
10
|
-
const OFFICIAL = process.argv[2] ?? 'D:/主工作区/projects/dsh-superagent-prototype/lib/client.js'
|
|
11
|
-
const TARGET = new URL('../client.js', import.meta.url)
|
|
12
|
-
|
|
13
|
-
const official = readFileSync(OFFICIAL, 'utf8')
|
|
14
|
-
const marker = 'var client_default = '
|
|
15
|
-
const at = official.indexOf(marker)
|
|
16
|
-
if (at < 0) throw new Error('官方产物里找不到 client_default(CSS)常量,路径对吗?' + OFFICIAL)
|
|
17
|
-
|
|
18
|
-
// 从引号开始扫到匹配的收尾引号(处理反斜杠转义),再 JSON.parse 还原
|
|
19
|
-
let i = at + marker.length
|
|
20
|
-
if (official[i] !== '"') throw new Error('CSS 常量不是字符串字面量')
|
|
21
|
-
let j = i + 1
|
|
22
|
-
while (j < official.length) {
|
|
23
|
-
if (official[j] === '\\') j += 2
|
|
24
|
-
else if (official[j] === '"') break
|
|
25
|
-
else j += 1
|
|
26
|
-
}
|
|
27
|
-
const css = JSON.parse(official.slice(i, j + 1))
|
|
28
|
-
|
|
29
|
-
const client = readFileSync(TARGET, 'utf8')
|
|
30
|
-
const linePrefix = ' const PORTED_CSS = '
|
|
31
|
-
const start = client.indexOf(linePrefix)
|
|
32
|
-
if (start < 0) throw new Error('client.js 里没有 PORTED_CSS 占位行')
|
|
33
|
-
const end = client.indexOf('\n', start)
|
|
34
|
-
if (end < 0) throw new Error('PORTED_CSS 行没有换行结尾')
|
|
35
|
-
|
|
36
|
-
const next = client.slice(0, start) + linePrefix + JSON.stringify(css) + ';' + client.slice(end)
|
|
37
|
-
writeFileSync(TARGET, next)
|
|
38
|
-
|
|
39
|
-
console.log(`官方 CSS: ${css.length} 字符 / ${css.split('\n').length} 行`)
|
|
40
|
-
console.log(`client.js: ${client.length} -> ${next.length} 字节`)
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
// 用【真实的 client.js】渲染出 SVG,再拼成 standalone 预览页 —— 预览与线上同一份代码
|
|
2
|
-
import { readFileSync, writeFileSync } from 'node:fs'
|
|
3
|
-
import vm from 'node:vm'
|
|
4
|
-
|
|
5
|
-
const SRC = readFileSync(new URL('../client.js', import.meta.url), 'utf8')
|
|
6
|
-
|
|
7
|
-
let mod = null
|
|
8
|
-
const sandbox = {
|
|
9
|
-
window: {
|
|
10
|
-
__ModuleLoader__: { load(m) { mod = m } },
|
|
11
|
-
innerWidth: 1440, innerHeight: 900,
|
|
12
|
-
addEventListener() {}, removeEventListener() {},
|
|
13
|
-
},
|
|
14
|
-
document: {
|
|
15
|
-
getElementById: () => null,
|
|
16
|
-
createElement: () => ({ id: '', textContent: '' }),
|
|
17
|
-
head: { appendChild() {} },
|
|
18
|
-
addEventListener() {}, removeEventListener() {},
|
|
19
|
-
},
|
|
20
|
-
fetch: () => Promise.resolve({ ok: true, status: 200, text: () => Promise.resolve('{}') }),
|
|
21
|
-
setInterval: () => 0, clearInterval: () => {},
|
|
22
|
-
console,
|
|
23
|
-
}
|
|
24
|
-
vm.createContext(sandbox)
|
|
25
|
-
vm.runInContext(SRC, sandbox)
|
|
26
|
-
|
|
27
|
-
let queue = [], index = 0
|
|
28
|
-
const React = {
|
|
29
|
-
Fragment: Symbol('Fragment'),
|
|
30
|
-
createElement(type, props, ...children) {
|
|
31
|
-
return { type, props: { ...(props || {}), children: children.flat(Infinity).filter((c) => c !== null && c !== undefined && c !== false) } }
|
|
32
|
-
},
|
|
33
|
-
memo: (fn) => fn,
|
|
34
|
-
useState(init) {
|
|
35
|
-
const v = index < queue.length ? queue[index] : (typeof init === 'function' ? init() : init)
|
|
36
|
-
index += 1
|
|
37
|
-
return [v, () => {}]
|
|
38
|
-
},
|
|
39
|
-
useEffect() {},
|
|
40
|
-
useCallback: (fn) => fn,
|
|
41
|
-
useRef: (v) => ({ current: v }),
|
|
42
|
-
}
|
|
43
|
-
const regs = []
|
|
44
|
-
mod.factory((n) => { if (n === 'react') return React; throw new Error(n) }).apply({
|
|
45
|
-
effect: (fn) => fn(),
|
|
46
|
-
slots: { inject: (_n, fn) => fn(), register: (o, C) => { regs.push({ o, C }); return () => {} } },
|
|
47
|
-
})
|
|
48
|
-
const orbit = regs.find((r) => r.o.id === 'session-superagent-orbit')
|
|
49
|
-
|
|
50
|
-
const render = (watchers) => { queue = [watchers]; index = 0; return orbit.C({}) }
|
|
51
|
-
|
|
52
|
-
function renderNode(node) {
|
|
53
|
-
if (node === null || node === undefined || node === false) return ''
|
|
54
|
-
if (typeof node === 'string' || typeof node === 'number') return String(node)
|
|
55
|
-
if (typeof node.type === 'function') return renderNode(node.type(node.props || {}))
|
|
56
|
-
const attrs = []
|
|
57
|
-
for (const [k, v] of Object.entries(node.props || {})) {
|
|
58
|
-
if (k === 'children' || v === null || v === undefined || v === false) continue
|
|
59
|
-
if (k === 'key' || k === 'ref') continue
|
|
60
|
-
if (k === 'style') {
|
|
61
|
-
const css = Object.entries(v).map(([sk, sv]) => sk.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase()) + ':' + sv).join(';')
|
|
62
|
-
attrs.push(`style="${css}"`)
|
|
63
|
-
continue
|
|
64
|
-
}
|
|
65
|
-
const KEEP = new Set(['viewBox', 'attributeName', 'attributeType', 'repeatCount', 'repeatDur', 'preserveAspectRatio', 'gradientUnits', 'patternUnits', 'spreadMethod', 'startOffset', 'markerWidth', 'markerHeight', 'refX', 'refY', 'baseFrequency', 'numOctaves', 'stdDeviation', 'textLength', 'lengthAdjust'])
|
|
66
|
-
const name = KEEP.has(k) ? k : k.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase())
|
|
67
|
-
attrs.push(`${name}="${String(v).replace(/"/g, '"')}"`)
|
|
68
|
-
}
|
|
69
|
-
const kids = (node.props?.children || []).map(renderNode).join('')
|
|
70
|
-
return `<${node.type}${attrs.length ? ' ' + attrs.join(' ') : ''}>${kids}</${node.type}>`
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const svgOf = (watchers) => {
|
|
74
|
-
const tree = render(watchers)
|
|
75
|
-
if (!tree) return ''
|
|
76
|
-
// 找到 svg 节点(button 的唯一子元素)
|
|
77
|
-
const find = (n) => {
|
|
78
|
-
if (!n || typeof n !== 'object') return null
|
|
79
|
-
if (typeof n.type === 'function') return find(n.type(n.props || {}))
|
|
80
|
-
if (n.type === 'svg') return n
|
|
81
|
-
for (const c of n.props?.children || []) { const hit = find(c); if (hit) return hit }
|
|
82
|
-
return null
|
|
83
|
-
}
|
|
84
|
-
return renderNode(find(tree))
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const w = (repo, extra = {}) => ({ repo, sessionId: 'session-' + repo, intervalSec: 30, enabled: true, lastPoll: null, lastWake: null, lastError: null, title: repo, ...extra })
|
|
88
|
-
const cases = [
|
|
89
|
-
['0 个 · 空态(灰环占位,无卫星)', []],
|
|
90
|
-
['1 个 · 值守中', [w('MYCF711/dsh-GOkifu-compact')]],
|
|
91
|
-
['3 个 · 全部值守中', [w('a/b'), w('c/d'), w('e/f')]],
|
|
92
|
-
['3 个 · 混合(绿=值守中 / 琥珀=已暂停 / 红=轮询失败)', [w('a/b'), w('c/d', { enabled: false }), w('e/f', { lastError: 'boom' })]],
|
|
93
|
-
['2 个 · 全部已暂停', [w('a/b', { enabled: false }), w('c/d', { enabled: false })]],
|
|
94
|
-
['6 个 · 上限', [w('a/b'), w('c/d'), w('e/f'), w('g/h'), w('i/j'), w('k/l')]],
|
|
95
|
-
]
|
|
96
|
-
|
|
97
|
-
const rows = cases.map(([label, list]) => {
|
|
98
|
-
const svg = svgOf(list)
|
|
99
|
-
return `<tr><td class="lb">${label}</td>
|
|
100
|
-
<td class="big">${svg}</td>
|
|
101
|
-
<td class="real">${svg}</td></tr>`
|
|
102
|
-
}).join('\n')
|
|
103
|
-
|
|
104
|
-
const OUT = process.argv[2] || 'E:/Documents/deepseek-harness/default-workspace/orbit-preview.html'
|
|
105
|
-
writeFileSync(OUT, `<!doctype html>
|
|
106
|
-
<html lang="zh-CN"><head><meta charset="utf-8"><title>轨道环预览 · dsh-session-superagent</title>
|
|
107
|
-
<style>
|
|
108
|
-
:root { color-scheme: dark }
|
|
109
|
-
body { margin:0; padding:28px 34px; background:#1b1b1f; color:#e6e6e6;
|
|
110
|
-
font:13px/1.6 system-ui,-apple-system,"Segoe UI",sans-serif }
|
|
111
|
-
h1 { font-size:15px; font-weight:600; margin:0 0 4px }
|
|
112
|
-
p.sub { margin:0 0 22px; opacity:.55 }
|
|
113
|
-
table { border-collapse:collapse }
|
|
114
|
-
td { padding:10px 22px 10px 0; vertical-align:middle; border-bottom:1px solid #ffffff14 }
|
|
115
|
-
td.lb { opacity:.8; white-space:nowrap; padding-right:34px }
|
|
116
|
-
td.big svg { width:44px; height:44px } /* 放大看形状 */
|
|
117
|
-
td.real svg { width:16px; height:16px } /* 线上就是这个大小 */
|
|
118
|
-
th { text-align:left; font-weight:500; opacity:.45; font-size:12px; padding-bottom:6px }
|
|
119
|
-
.note { margin-top:22px; opacity:.5; font-size:12px }
|
|
120
|
-
code { background:#ffffff12; padding:1px 5px; border-radius:4px }
|
|
121
|
-
</style></head>
|
|
122
|
-
<body>
|
|
123
|
-
<h1>轨道环 · 真实组件渲染预览</h1>
|
|
124
|
-
<p class="sub">这份 HTML 由 <code>client.js</code> 本身渲染生成(不是另画一份),所以线上长什么样这里就是什么样。</p>
|
|
125
|
-
<table>
|
|
126
|
-
<tr><th>状态</th><th>放大 44px</th><th>实际 16px</th></tr>
|
|
127
|
-
${rows}
|
|
128
|
-
</table>
|
|
129
|
-
<div class="note">
|
|
130
|
-
值守中 = 卫星沿环公转(6s/圈)+ 环本身呼吸(2.6s);已暂停 = 虚线环、不转;轮询失败 = 红点呼吸(1.6s)。<br>
|
|
131
|
-
已验证动画走 SVG 自带的 SMIL,不依赖外部样式表 —— 上一版引用了不存在的 <code>.sa-sat-spin</code> 规则,所以是死的。
|
|
132
|
-
</div>
|
|
133
|
-
</body></html>
|
|
134
|
-
`, 'utf8')
|
|
135
|
-
console.log('wrote', OUT)
|
|
136
|
-
console.log('SMIL in preview:', (readFileSync(OUT, 'utf8').match(/animateTransform/g) || []).length, 'animateTransform tags')
|
package/scripts/sync-profile.mjs
DELETED
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
// 把插件源码目录同步到 profile 的安装副本。
|
|
2
|
-
//
|
|
3
|
-
// 为什么需要:profile 里是 `file:` 安装 —— pnpm 为**已存在的文件**建了硬链接,
|
|
4
|
-
// 改内容会自动生效;但**新增文件不会**自动出现,而**被原子写入替换的文件会断链**。
|
|
5
|
-
//
|
|
6
|
-
// 真实事故 1(2026-11-01 23:59 重启):新增的 lib/body.js 没进 profile,
|
|
7
|
-
// 宿主 import './body.js' 失败 → 整个插件不挂载 → 工具和 HTTP 路由全消失,
|
|
8
|
-
// 客户端 bundle 却照常加载,表面看起来"插件还在"。
|
|
9
|
-
// 真实事故 2(潜伏):某些工具用「写临时文件再 rename」保存,profile 里那份仍是旧 inode,
|
|
10
|
-
// 于是宿主跑的是旧代码,而仓库里看着已经是新代码。
|
|
11
|
-
//
|
|
12
|
-
// 用法:
|
|
13
|
-
// node scripts/sync-profile.mjs 补齐缺失文件(不动断链,只警告)
|
|
14
|
-
// node scripts/sync-profile.mjs --repair 断链也重建硬链接,并清理 profile 里的陈旧文件
|
|
15
|
-
// node scripts/sync-profile.mjs --check 只检查:有缺失/断链/陈旧就 exit 1(给 CI 用)
|
|
16
|
-
// node scripts/sync-profile.mjs --dir <path> 指定 profile 安装目录
|
|
17
|
-
import { existsSync, linkSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync } from 'node:fs'
|
|
18
|
-
import { dirname, join } from 'node:path'
|
|
19
|
-
import { fileURLToPath } from 'node:url'
|
|
20
|
-
|
|
21
|
-
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
22
|
-
const args = process.argv.slice(2)
|
|
23
|
-
const REPAIR = args.includes('--repair')
|
|
24
|
-
const CHECK = args.includes('--check')
|
|
25
|
-
const dirFlag = args.indexOf('--dir')
|
|
26
|
-
const DEFAULT_PROFILE = join(
|
|
27
|
-
process.env.DSH_HOME || join(process.env.USERPROFILE || '', '.dsh'),
|
|
28
|
-
'profiles',
|
|
29
|
-
'desktop',
|
|
30
|
-
'node_modules',
|
|
31
|
-
'dsh-session-superagent',
|
|
32
|
-
)
|
|
33
|
-
const DEST = dirFlag >= 0 ? args[dirFlag + 1] : DEFAULT_PROFILE
|
|
34
|
-
|
|
35
|
-
/** 运行时要用的文件(不含 docs / test / scripts 这些开发件)。 */
|
|
36
|
-
const TARGETS = [
|
|
37
|
-
'client.js', 'package.json', 'cordis.patch.yml', 'README.md', 'CHANGELOG.md', 'LICENSE',
|
|
38
|
-
'lib/index.js', 'lib/watch.js', 'lib/redact.js', 'lib/body.js',
|
|
39
|
-
]
|
|
40
|
-
|
|
41
|
-
if (!existsSync(DEST)) {
|
|
42
|
-
console.error('找不到 profile 安装目录:' + DEST)
|
|
43
|
-
process.exit(1)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const drift = { missing: [], broken: [], stale: [] }
|
|
47
|
-
let linked = 0
|
|
48
|
-
|
|
49
|
-
for (const rel of TARGETS) {
|
|
50
|
-
const src = join(ROOT, rel)
|
|
51
|
-
const dst = join(DEST, rel)
|
|
52
|
-
if (!existsSync(src)) continue
|
|
53
|
-
if (!existsSync(dst)) {
|
|
54
|
-
drift.missing.push(rel)
|
|
55
|
-
if (!CHECK) {
|
|
56
|
-
mkdirSync(dirname(dst), { recursive: true })
|
|
57
|
-
try { linkSync(src, dst); linked += 1; console.log('linked ' + rel) } catch (error) {
|
|
58
|
-
console.log('link 失败:' + rel + ' — ' + (error && error.message))
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
continue
|
|
62
|
-
}
|
|
63
|
-
const same = statSync(src).ino === statSync(dst).ino
|
|
64
|
-
if (same) continue
|
|
65
|
-
drift.broken.push(rel)
|
|
66
|
-
if (REPAIR && !CHECK) {
|
|
67
|
-
try {
|
|
68
|
-
unlinkSync(dst)
|
|
69
|
-
linkSync(src, dst)
|
|
70
|
-
linked += 1
|
|
71
|
-
console.log('relinked ' + rel + '(原来断了)')
|
|
72
|
-
} catch (error) {
|
|
73
|
-
console.log('重建失败:' + rel + ' — ' + (error && error.message))
|
|
74
|
-
}
|
|
75
|
-
} else {
|
|
76
|
-
console.log('注意 ' + rel + '(inode 不同:内容改动不会同步到 profile,用 --repair 重建)')
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// 陈旧文件:profile 里有、仓库里已经没有(例如删掉的模块)
|
|
81
|
-
for (const name of readdirSync(join(DEST, 'lib')).filter((f) => f.endsWith('.js'))) {
|
|
82
|
-
if (!existsSync(join(ROOT, 'lib', name))) {
|
|
83
|
-
drift.stale.push('lib/' + name)
|
|
84
|
-
if (REPAIR && !CHECK) {
|
|
85
|
-
try { unlinkSync(join(DEST, 'lib', name)); console.log('pruned lib/' + name) } catch {}
|
|
86
|
-
} else {
|
|
87
|
-
console.log('陈旧 lib/' + name + '(仓库已删除,profile 里还在)')
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// lib/ 里每个相对 import 都必须能解析(这正是事故 1 的直接原因)
|
|
93
|
-
const libDir = join(ROOT, 'lib')
|
|
94
|
-
const missingImports = []
|
|
95
|
-
for (const name of readdirSync(libDir)) {
|
|
96
|
-
if (!name.endsWith('.js')) continue
|
|
97
|
-
const code = readFileSync(join(libDir, name), 'utf8')
|
|
98
|
-
for (const line of code.split(String.fromCharCode(10))) {
|
|
99
|
-
const trimmed = line.trim()
|
|
100
|
-
if (!trimmed.startsWith('import ')) continue
|
|
101
|
-
const at = trimmed.lastIndexOf("from '")
|
|
102
|
-
let spec = null
|
|
103
|
-
if (at >= 0) {
|
|
104
|
-
spec = trimmed.slice(at + 6).split("'")[0]
|
|
105
|
-
} else if (trimmed.slice('import '.length).startsWith("'")) {
|
|
106
|
-
spec = trimmed.slice('import '.length + 1).split("'")[0]
|
|
107
|
-
}
|
|
108
|
-
if (spec && spec.startsWith('.') && !existsSync(join(libDir, spec))) {
|
|
109
|
-
missingImports.push(name + ' -> ' + spec)
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const drifted = drift.missing.length + drift.broken.length + drift.stale.length
|
|
115
|
-
console.log('---')
|
|
116
|
-
console.log(`目标:${DEST}`)
|
|
117
|
-
console.log(`缺失 ${drift.missing.length} / 断链 ${drift.broken.length} / 陈旧 ${drift.stale.length}` +
|
|
118
|
-
(linked ? `,本次处理 ${linked} 个` : ''))
|
|
119
|
-
|
|
120
|
-
if (missingImports.length > 0) {
|
|
121
|
-
console.error('相对 import 解析失败:' + missingImports.join(', '))
|
|
122
|
-
process.exit(2)
|
|
123
|
-
}
|
|
124
|
-
console.log('相对 import 全部可解析 ✓')
|
|
125
|
-
|
|
126
|
-
if (CHECK && drifted > 0) {
|
|
127
|
-
console.error('profile 与仓库不一致(跑 --repair 修)')
|
|
128
|
-
process.exit(1)
|
|
129
|
-
}
|