dsh-session-superagent 0.8.16 → 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 +61 -0
- package/README.md +3 -3
- package/client.js +11 -1
- package/lib/index.js +65 -11
- package/package.json +4 -7
- package/scripts/port-official-ui.mjs +0 -40
- package/scripts/preview-orbit.mjs +0 -136
- package/scripts/render-preview.mjs +0 -213
- package/scripts/sync-profile.mjs +0 -129
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,67 @@
|
|
|
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
|
+
|
|
5
66
|
## [0.8.16] - 2026-11-02 —— 第一次在真浏览器里看面板,抓到一个对比度问题
|
|
6
67
|
|
|
7
68
|
宿主的 web 界面有鉴权(`dsh web authentication required`),ego 浏览器过不去,所以搭了个
|
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
|
|
@@ -699,6 +699,8 @@ window.__ModuleLoader__.load({
|
|
|
699
699
|
isolated: !!draft.isolated,
|
|
700
700
|
workdir: draft.workdir || '',
|
|
701
701
|
observe: !!draft.observe,
|
|
702
|
+
batchLimit: Number(draft.batchLimit) || undefined,
|
|
703
|
+
maxWakesPerHour: Number(draft.maxWakesPerHour) || undefined,
|
|
702
704
|
}),
|
|
703
705
|
})
|
|
704
706
|
setEditing(null)
|
|
@@ -732,6 +734,8 @@ window.__ModuleLoader__.load({
|
|
|
732
734
|
isolated: !!form.isolated,
|
|
733
735
|
workdir: form.workdir || '',
|
|
734
736
|
observe: !!form.observe,
|
|
737
|
+
batchLimit: Number(form.batchLimit) || undefined,
|
|
738
|
+
maxWakesPerHour: Number(form.maxWakesPerHour) || undefined,
|
|
735
739
|
}),
|
|
736
740
|
})
|
|
737
741
|
setForm({ repo: '', intervalSec: form.intervalSec, goal: '', labels: '', onlyAssigned: false, activeHours: form.activeHours, isolated: form.isolated, workdir: form.workdir, observe: form.observe })
|
|
@@ -798,6 +802,8 @@ window.__ModuleLoader__.load({
|
|
|
798
802
|
isolated: !!watcher.isolated,
|
|
799
803
|
workdir: watcher.workdir || '',
|
|
800
804
|
observe: !!watcher.observe,
|
|
805
|
+
batchLimit: String(watcher.batchLimit ?? 5),
|
|
806
|
+
maxWakesPerHour: String(watcher.maxWakesPerHour ?? 6),
|
|
801
807
|
})
|
|
802
808
|
},
|
|
803
809
|
}, editing === key ? '收起' : '编辑'),
|
|
@@ -818,6 +824,8 @@ window.__ModuleLoader__.load({
|
|
|
818
824
|
field('活跃时段', h(Input, { value: draft.activeHours, placeholder: '09:00-23:00,留空 = 全天', onChange: (e) => setDraft({ ...draft, activeHours: e.target.value }) })),
|
|
819
825
|
field('目标/规则', h(Input, { value: draft.goal || '', placeholder: '可选,会附在唤醒消息里', onChange: (e) => setDraft({ ...draft, goal: e.target.value }) })),
|
|
820
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 }) })),
|
|
821
829
|
h('label', { className: 'dt-switch-row' }, h(Toggle, { checked: !!draft.observe, onChange: (v) => setDraft({ ...draft, observe: v }), label: '仅观察' }), '仅观察(只记录不唤醒)'),
|
|
822
830
|
h('label', { className: 'dt-switch-row' }, h(Toggle, { checked: !!draft.isolated, onChange: (v) => setDraft({ ...draft, isolated: v }), label: '隔离执行' }), '隔离执行(每次唤醒开一次性 worktree)'),
|
|
823
831
|
draft.isolated ? field('隔离工作目录', h(Input, { value: draft.workdir || '', placeholder: '绝对路径,留空 = 当前工作目录', onChange: (e) => setDraft({ ...draft, workdir: e.target.value }) })) : null,
|
|
@@ -888,6 +896,8 @@ window.__ModuleLoader__.load({
|
|
|
888
896
|
h('div', { className: 'dt-form' },
|
|
889
897
|
field('仓库', h(Input, { value: form.repo, placeholder: 'owner/name', onChange: (e) => setForm({ ...form, repo: e.target.value }) })),
|
|
890
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 }) })),
|
|
891
901
|
field('只处理这些标签', h(Input, { value: form.labels, placeholder: '逗号分隔,留空 = 全部', onChange: (e) => setForm({ ...form, labels: e.target.value }) })),
|
|
892
902
|
field('活跃时段', h(Input, { value: form.activeHours, placeholder: '09:00-23:00,留空 = 全天', onChange: (e) => setForm({ ...form, activeHours: e.target.value }) })),
|
|
893
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,11 +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",
|
|
24
|
-
"scripts/render-preview.mjs"
|
|
20
|
+
"client.js"
|
|
25
21
|
],
|
|
26
22
|
"engines": {
|
|
27
23
|
"node": ">=22.19.0",
|
|
@@ -35,7 +31,8 @@
|
|
|
35
31
|
"smoke:observe": "node scripts/smoke-observe.mjs",
|
|
36
32
|
"sync:profile:check": "node scripts/sync-profile.mjs --check",
|
|
37
33
|
"sync:profile:repair": "node scripts/sync-profile.mjs --repair",
|
|
38
|
-
"preview": "node scripts/render-preview.mjs"
|
|
34
|
+
"preview": "node scripts/render-preview.mjs",
|
|
35
|
+
"preflight": "node scripts/preflight.mjs"
|
|
39
36
|
},
|
|
40
37
|
"keywords": [
|
|
41
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')
|
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
// 把「主理鱼」面板渲染成静态 HTML,用于在真浏览器里看布局(不需要宿主、不需要 React)。
|
|
2
|
-
//
|
|
3
|
-
// 为什么:client.js 是给宿主用的模块(`window.__ModuleLoader__.load` + `require('react')`),
|
|
4
|
-
// 组件树用 h() 构造。这里提供一个极简的假 React,把组件渲染成虚拟节点树,
|
|
5
|
-
// 再序列化成 HTML,并内联移植来的官方 CSS —— 于是可以在浏览器里真实地看布局。
|
|
6
|
-
//
|
|
7
|
-
// 用法: node scripts/render-preview.mjs [输出文件]
|
|
8
|
-
// node scripts/render-preview.mjs preview-panel.html
|
|
9
|
-
import { readFileSync, writeFileSync } from 'node:fs'
|
|
10
|
-
import { dirname, join } from 'node:path'
|
|
11
|
-
import { fileURLToPath } from 'node:url'
|
|
12
|
-
import vm from 'node:vm'
|
|
13
|
-
|
|
14
|
-
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
15
|
-
const OUT = process.argv[2] || join(ROOT, 'preview-panel.html')
|
|
16
|
-
|
|
17
|
-
// ── 假 React:只需要 createElement/useState/useEffect/useCallback/useRef/memo/Fragment ──
|
|
18
|
-
let hookQueue = []
|
|
19
|
-
let hookIndex = 0
|
|
20
|
-
const React = {
|
|
21
|
-
Fragment: Symbol('Fragment'),
|
|
22
|
-
createElement(type, props, ...children) {
|
|
23
|
-
return { type, props: { ...(props || {}), children: children.flat(Infinity).filter((c) => c !== null && c !== undefined && c !== false) } }
|
|
24
|
-
},
|
|
25
|
-
memo: (fn) => fn,
|
|
26
|
-
useState(init) {
|
|
27
|
-
const value = hookIndex < hookQueue.length ? hookQueue[hookIndex] : (typeof init === 'function' ? init() : init)
|
|
28
|
-
hookIndex += 1
|
|
29
|
-
return [value, () => {}]
|
|
30
|
-
},
|
|
31
|
-
useEffect() {},
|
|
32
|
-
useCallback: (fn) => fn,
|
|
33
|
-
useRef: (value) => ({ current: value }),
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// 假宿主:只收集注册,不真的挂任何东西
|
|
37
|
-
const registrations = []
|
|
38
|
-
const sandbox = {
|
|
39
|
-
window: {
|
|
40
|
-
__ModuleLoader__: { load: (mod) => registrations.push(mod) },
|
|
41
|
-
innerWidth: 1280, innerHeight: 900,
|
|
42
|
-
addEventListener() {}, removeEventListener() {},
|
|
43
|
-
},
|
|
44
|
-
document: {
|
|
45
|
-
getElementById: () => null,
|
|
46
|
-
createElement: () => ({ id: '', textContent: '' }),
|
|
47
|
-
head: { appendChild() {} },
|
|
48
|
-
addEventListener() {}, removeEventListener() {},
|
|
49
|
-
},
|
|
50
|
-
fetch: () => Promise.resolve({ ok: true, status: 200, text: () => Promise.resolve('{}') }),
|
|
51
|
-
setInterval: () => 0, clearInterval: () => {}, setTimeout: () => 0, clearTimeout: () => {},
|
|
52
|
-
console,
|
|
53
|
-
}
|
|
54
|
-
vm.createContext(sandbox)
|
|
55
|
-
vm.runInContext(readFileSync(join(ROOT, 'client.js'), 'utf8'), sandbox)
|
|
56
|
-
|
|
57
|
-
const mod = registrations[0]
|
|
58
|
-
if (!mod) throw new Error('client.js 没有注册任何模块')
|
|
59
|
-
const slots = new Map()
|
|
60
|
-
const ctx = {
|
|
61
|
-
slots: {
|
|
62
|
-
inject: (_name, fn) => fn(),
|
|
63
|
-
register: (opts, Component) => { slots.set(opts.id, Component); return () => {} },
|
|
64
|
-
},
|
|
65
|
-
effect: (fn) => fn(),
|
|
66
|
-
}
|
|
67
|
-
mod.factory((name) => {
|
|
68
|
-
if (name === 'react') return React
|
|
69
|
-
throw new Error('unexpected require: ' + name)
|
|
70
|
-
}).apply(ctx)
|
|
71
|
-
|
|
72
|
-
const Desk = slots.get('session-superagent-desk')
|
|
73
|
-
if (!Desk) throw new Error('没有注册 session-superagent-desk,已注册:' + [...slots.keys()].join(','))
|
|
74
|
-
|
|
75
|
-
// ── 样本快照:覆盖各种状态(正常/暂停/异常/仅观察/隔离/退避/过滤记录/历史)──
|
|
76
|
-
const now = Date.now()
|
|
77
|
-
const iso = (ms) => new Date(now - ms).toISOString()
|
|
78
|
-
const snapshot = {
|
|
79
|
-
loading: false, error: null,
|
|
80
|
-
body: {
|
|
81
|
-
sessionTitle: 'README 收尾与发布',
|
|
82
|
-
sessionGoal: null,
|
|
83
|
-
gh: { ok: true, version: 'gh version 2.101.0' },
|
|
84
|
-
credentials: { token: 'configured', sshKeyPath: 'none' },
|
|
85
|
-
watchers: [
|
|
86
|
-
{
|
|
87
|
-
id: 'MYCF711/dsh-session-superagent#s1', repo: 'MYCF711/dsh-session-superagent', sessionId: 's1',
|
|
88
|
-
intervalSec: 60, enabled: true, observe: false, title: 'README 收尾与发布', note: '把 0.8.x 的文档补齐',
|
|
89
|
-
lastPoll: iso(15000), lastWake: iso(300000), lastWakeCount: 3, lastAbsorbed: 1,
|
|
90
|
-
filters: { labels: ['ai-fix'], ignoreLabels: [], skipBots: true, skipDrafts: true, skipClosed: true, onlyAssigned: false, assignee: null },
|
|
91
|
-
activeHours: '09:00-23:00', isolated: true, workdir: 'E:/temp/iso', lastWorktree: 'E:/temp/iso/.dsh-zhuli/a-b',
|
|
92
|
-
history: [
|
|
93
|
-
{ at: iso(300000), numbers: [12], ok: true, reason: null },
|
|
94
|
-
{ at: iso(600000), numbers: [11], ok: false, reason: 'observe' },
|
|
95
|
-
],
|
|
96
|
-
filteredLog: [{ number: 7, title: '机器人提的依赖升级', reason: '机器人', at: iso(120000) }],
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
id: 'MYCF711/dsh-GOkifu-compact#s2', repo: 'MYCF711/dsh-GOkifu-compact', sessionId: 's2',
|
|
100
|
-
intervalSec: 120, enabled: false, observe: true, title: '联调会话',
|
|
101
|
-
lastPoll: iso(900000), lastDeferred: '仅观察:本轮本会唤醒 2 条(#4 #3)',
|
|
102
|
-
filters: { labels: [], ignoreLabels: [], skipBots: true, skipDrafts: true, skipClosed: true, onlyAssigned: false, assignee: null },
|
|
103
|
-
history: [], filteredLog: [],
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
id: 'a/b#s3', repo: 'someone/broken-repo', sessionId: 's3', intervalSec: 30, enabled: true,
|
|
107
|
-
title: '出错示例', lastPoll: iso(5000), lastError: 'gh: Not Found (HTTP 404)(仓库不存在或没权限:检查 owner/name)',
|
|
108
|
-
consecutiveFailures: 2, nextAt: new Date(now + 180000).toISOString(),
|
|
109
|
-
filters: { labels: [], ignoreLabels: [], skipBots: true, skipDrafts: true, skipClosed: true, onlyAssigned: false, assignee: null },
|
|
110
|
-
history: [], filteredLog: [],
|
|
111
|
-
},
|
|
112
|
-
],
|
|
113
|
-
},
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// ── 渲染:把元素树序列化成 HTML ───────────────────────────────────────────────
|
|
117
|
-
function escapeHtml(text) {
|
|
118
|
-
return String(text).replace(/[&<>]/g, (c) => ({ '&': '&', '<': '<', '>': '>' }[c]))
|
|
119
|
-
}
|
|
120
|
-
function styleToCss(style) {
|
|
121
|
-
if (!style) return ''
|
|
122
|
-
return Object.entries(style).map(([k, v]) => {
|
|
123
|
-
const prop = k.startsWith('--') ? k : k.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase())
|
|
124
|
-
return prop + ':' + v
|
|
125
|
-
}).join(';')
|
|
126
|
-
}
|
|
127
|
-
function attr(name, value) {
|
|
128
|
-
if (value === undefined || value === null || value === false || value === true) {
|
|
129
|
-
return value === true ? ' ' + name : ''
|
|
130
|
-
}
|
|
131
|
-
return ' ' + name + '="' + escapeHtml(value) + '"'
|
|
132
|
-
}
|
|
133
|
-
function render(node) {
|
|
134
|
-
if (node === null || node === undefined || node === false) return ''
|
|
135
|
-
if (typeof node === 'string' || typeof node === 'number') return escapeHtml(node)
|
|
136
|
-
if (Array.isArray(node)) return node.map(render).join('')
|
|
137
|
-
if (typeof node !== 'object') return ''
|
|
138
|
-
const { type, props = {} } = node
|
|
139
|
-
if (typeof type === 'function') return render(type(props))
|
|
140
|
-
if (type === React.Fragment) return (props.children || []).map(render).join('')
|
|
141
|
-
const voidTags = new Set(['input', 'img', 'br', 'hr', 'meta', 'link'])
|
|
142
|
-
const tag = String(type)
|
|
143
|
-
const parts = []
|
|
144
|
-
if (props.className) parts.push(attr('class', props.className))
|
|
145
|
-
if (props.style) parts.push(attr('style', styleToCss(props.style)))
|
|
146
|
-
for (const key of ['data-tone', 'data-state', 'data-open', 'role', 'title', 'aria-label', 'aria-selected', 'aria-pressed', 'aria-modal', 'type', 'placeholder', 'value', 'disabled']) {
|
|
147
|
-
if (props[key] !== undefined && props[key] !== false) parts.push(attr(key, props[key]))
|
|
148
|
-
}
|
|
149
|
-
if (keyIsVoid(tag)) return '<' + tag + parts.join('') + '>'
|
|
150
|
-
return '<' + tag + parts.join('') + '>' + (props.children || []).map(render).join('') + '</' + tag + '>'
|
|
151
|
-
}
|
|
152
|
-
function keyIsVoid(tag) {
|
|
153
|
-
return ['input', 'img', 'br', 'hr', 'meta', 'link'].includes(tag)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// 取移植来的 CSS
|
|
157
|
-
const clientSrc = readFileSync(join(ROOT, 'client.js'), 'utf8')
|
|
158
|
-
const marker = 'const PORTED_CSS = "'
|
|
159
|
-
const head = clientSrc.indexOf(marker) + marker.length
|
|
160
|
-
let i = head
|
|
161
|
-
while (i < clientSrc.length && clientSrc[i] !== '"') i += clientSrc[i] === '\\' ? 2 : 1
|
|
162
|
-
const PORTED_CSS = JSON.parse('"' + clientSrc.slice(head, i) + '"')
|
|
163
|
-
|
|
164
|
-
const scenarios = [
|
|
165
|
-
{ name: '总览', hooks: [snapshot, 'overview', null, { repo: '', intervalSec: '60', goal: '', labels: '', onlyAssigned: false, activeHours: '', isolated: false, workdir: '' }, '', '', false, null, null, {}, '', false] },
|
|
166
|
-
{ name: '判定', hooks: [snapshot, 'judge', null, { repo: '', intervalSec: '60', goal: '', labels: '', onlyAssigned: false, activeHours: '', isolated: false, workdir: '' }, '', '', false, null, null, {}, '', false] },
|
|
167
|
-
{ name: '触发', hooks: [snapshot, 'trigger', null, { repo: '', intervalSec: '60', goal: '', labels: '', onlyAssigned: false, activeHours: '', isolated: false, workdir: '' }, '', '', false, null, null, {}, '', false] },
|
|
168
|
-
{ name: '设置', hooks: [snapshot, 'settings', null, { repo: '', intervalSec: '60', goal: '', labels: '', onlyAssigned: false, activeHours: '', isolated: false, workdir: '' }, '', '', false, null, null, {}, '', false] },
|
|
169
|
-
{ name: 'Toast(保存成功)', hooks: [snapshot, 'overview', null, { repo: '', intervalSec: '60', goal: '', labels: '', onlyAssigned: false, activeHours: '', isolated: false, workdir: '' }, '', '', false, { id: 't1', text: '已保存:MYCF711/dsh-session-superagent', tone: 'info' }, null, {}, '', false] },
|
|
170
|
-
{ name: 'Toast(错误)', hooks: [snapshot, 'overview', null, { repo: '', intervalSec: '60', goal: '', labels: '', onlyAssigned: false, activeHours: '', isolated: false, workdir: '' }, '', '', false, { id: 't2', text: '保存失败:gh: Not Found (HTTP 404)', tone: 'error' }, null, {}, '', false] },
|
|
171
|
-
{ name: '术语表', hooks: [snapshot, 'overview', null, { repo: '', intervalSec: '60', goal: '', labels: '', onlyAssigned: false, activeHours: '', isolated: false, workdir: '' }, '', '', false, null, null, {}, '', true] },
|
|
172
|
-
]
|
|
173
|
-
|
|
174
|
-
const sections = scenarios.map((scenario) => {
|
|
175
|
-
hookQueue = scenario.hooks
|
|
176
|
-
hookIndex = 0
|
|
177
|
-
const tree = Desk({ sessionId: 'preview-session' })
|
|
178
|
-
return '<h2 class="preview-title">' + scenario.name + '</h2>\n<div class="preview-frame">' + render(tree) + '</div>'
|
|
179
|
-
}).join('\n')
|
|
180
|
-
|
|
181
|
-
// 极简 token 兜底:宿主里这些由 --dsw-alias-* 提供,预览里给一组浅色近似值
|
|
182
|
-
const tokens = `:root{
|
|
183
|
-
--dsw-alias-label-primary:#1f2328; --dsw-alias-label-secondary:#59636e; --dsw-alias-label-tertiary:#818b98;
|
|
184
|
-
--dsw-alias-bg-layer-1:#ffffff; --dsw-alias-bg-layer-2:#f6f8fa; --dsw-alias-bg-layer-3:#eef1f4;
|
|
185
|
-
--dsw-alias-border-default:#d8dee4; --dsw-alias-border-muted:#e6eaef;
|
|
186
|
-
--dsw-alias-accent-primary:#0969da; --dsw-alias-success-500:#1a7f37; --dsw-alias-warning-500:#9a6700;
|
|
187
|
-
--dsw-alias-error-500:#cf222e; --dsw-static-neutral-bluish-00:#ffffff;
|
|
188
|
-
--dsw-shadow-lv3:0 8px 24px rgba(31,35,40,.12);
|
|
189
|
-
/* tag / 状态点真正引用的 token(官方 CSS 用 --dsw-alias-state-*) */
|
|
190
|
-
--dsw-alias-state-success-primary:#1a7f37; --dsw-alias-state-warn-primary:#9a6700;
|
|
191
|
-
--dsw-alias-state-error-primary:#cf222e; --dsw-alias-state-info-primary:#0969da;
|
|
192
|
-
--dsw-alias-toast-bg:#24292f;
|
|
193
|
-
}`
|
|
194
|
-
|
|
195
|
-
const html = `<!doctype html>
|
|
196
|
-
<meta charset="utf-8">
|
|
197
|
-
<title>主理鱼面板预览</title>
|
|
198
|
-
<style>
|
|
199
|
-
${tokens}
|
|
200
|
-
body{font:14px/1.45 -apple-system,"Segoe UI",system-ui,sans-serif;background:#eef1f4;margin:0;padding:24px;color:#1f2328}
|
|
201
|
-
.preview-title{margin:28px 0 8px;font-size:15px;color:#57606a}
|
|
202
|
-
.preview-frame{width:900px;background:#fff;border:1px solid #d8dee4;border-radius:12px;min-height:120px;max-height:820px;overflow:auto}
|
|
203
|
-
.dt-page{height:auto!important;max-height:none!important}
|
|
204
|
-
${PORTED_CSS}
|
|
205
|
-
</style>
|
|
206
|
-
<div style="font:13px/1.5 system-ui;color:#57606a">
|
|
207
|
-
主理鱼面板静态预览 —— 真实 client.js 渲染出的 DOM + 移植的官方 CSS(宽度 900px,token 为浅色近似值)
|
|
208
|
-
</div>
|
|
209
|
-
${sections}
|
|
210
|
-
`
|
|
211
|
-
writeFileSync(OUT, html)
|
|
212
|
-
console.log('已写出:' + OUT)
|
|
213
|
-
console.log('字节:' + html.length)
|
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
|
-
}
|