dsh-session-superagent 0.8.14 → 0.8.16
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 +72 -0
- package/README.md +1 -1
- package/client.js +3 -2
- package/package.json +7 -3
- package/scripts/render-preview.mjs +213 -0
- package/scripts/sync-profile.mjs +81 -28
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,72 @@
|
|
|
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.16] - 2026-11-02 —— 第一次在真浏览器里看面板,抓到一个对比度问题
|
|
6
|
+
|
|
7
|
+
宿主的 web 界面有鉴权(`dsh web authentication required`),ego 浏览器过不去,所以搭了个
|
|
8
|
+
**静态预览**:`scripts/render-preview.mjs` 用极简假 React 把真实 `client.js` 的组件渲染成 DOM,
|
|
9
|
+
内联移植来的官方 CSS,六个场景(总览/判定/触发/设置/术语表/两种 Toast)一起看。
|
|
10
|
+
|
|
11
|
+
### 修复(真浏览器里发现的)
|
|
12
|
+
- **错误 Toast 白字看不清**:0.8.12 给错误态加了「浅红底」,而官方 toast 是深底白字——
|
|
13
|
+
白字压在浅粉上对比度极低。现在保留官方深底,改用**左侧红条**标记错误。
|
|
14
|
+
|
|
15
|
+
### 验证(在 Chromium 里读 computed style,不是猜)
|
|
16
|
+
```
|
|
17
|
+
模态框 position:fixed; inset:0,宽度 380 居中 ← 「弹层掉到页面底部」的 bug 确认已修
|
|
18
|
+
Toast position:fixed; top:40px; z-index:1100; 299×46 深底白字
|
|
19
|
+
错误态 背景 #24292f / 白字 / 左边框 #f85149
|
|
20
|
+
标签 success 绿底绿字、warning 琥珀底琥珀字(tone 与 CSS 对得上)
|
|
21
|
+
列表行 高 102–122px(三线结构:标题 / meta / 操作)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
顺带搞清了 `dt-tag` 的颜色依赖 `--dsw-alias-state-{success,warn,error,info}-primary` 这几个 token
|
|
25
|
+
(之前预览里缺,标签看着是裸文字——这和审核第二轮发现的「状态色全丢」是同一个根因)。
|
|
26
|
+
|
|
27
|
+
### 工具
|
|
28
|
+
- `pnpm run preview` 生成 `preview-panel.html`(已加进 .gitignore,不进仓库/不发包)。
|
|
29
|
+
|
|
30
|
+
### 测试
|
|
31
|
+
- 合计 **81 例**(本次是样式与渲染层修复,靠真浏览器核对)。
|
|
32
|
+
|
|
33
|
+
## [0.8.15] - 2026-11-02 —— sync-profile 支持修复断链(事故 1 的第二种形态)
|
|
34
|
+
|
|
35
|
+
### 修什么
|
|
36
|
+
`sync-profile.mjs` 之前只会「补缺失文件」,遇到**已存在但 inode 不同**(断链)只打印一句“注意”。
|
|
37
|
+
断链的来源很日常:任何用「写临时文件再 rename」保存的工具(含某些编辑器的原子写)都会让
|
|
38
|
+
profile 里那份留在旧 inode 上 —— 于是**宿主跑旧代码,仓库里看着是新代码**。
|
|
39
|
+
|
|
40
|
+
现在三种模式:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pnpm run sync:profile # 补齐缺失(不动断链,只警告)
|
|
44
|
+
pnpm run sync:profile:repair # 断链重建硬链接 + 清理 profile 里的陈旧文件
|
|
45
|
+
pnpm run sync:profile:check # 只检查:有缺失/断链/陈旧就 exit 1(CI/发布前用)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
实测:profile 里 `README.md` / `CHANGELOG.md` 当时确实是断链,`--repair` 重建后
|
|
49
|
+
`--check` 从 exit 1 变 exit 0。另外故意把 README 换成新 inode,验证 `--check` 会红(exit 1)。
|
|
50
|
+
|
|
51
|
+
### 守门
|
|
52
|
+
- 新增用例:`sync-profile` 的 `TARGETS` 必须覆盖 `lib/` 下所有 js 文件 ——
|
|
53
|
+
防止「新增 lib 文件忘了登记」再次导致插件不挂载。
|
|
54
|
+
|
|
55
|
+
### 发布
|
|
56
|
+
- 已发 `dsh-session-superagent@0.8.15`(registry `latest = 0.8.15`)。
|
|
57
|
+
- tarball 直装验证:`version 0.8.15`、**routes 6**、`client.js` 语法通过、包内 `scripts/` 三个脚本齐全。
|
|
58
|
+
|
|
59
|
+
### 发布前快照(每次发版都跑这一套)
|
|
60
|
+
```
|
|
61
|
+
node --test 81/81
|
|
62
|
+
node --check client.js lib/*.js scripts/*.mjs 全过
|
|
63
|
+
sync-profile --check 缺失 0 / 断链 0 / 陈旧 0,相对 import 全可解析
|
|
64
|
+
smoke-observe 两场景 12 项 OK
|
|
65
|
+
npm pack --dry-run 13 文件 / 75.4 kB
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 测试
|
|
69
|
+
- 合计 **81 例**。
|
|
70
|
+
|
|
5
71
|
## [0.8.14] - 2026-11-02 —— 第四轮审核:1 条文档 + 3 条代码建议
|
|
6
72
|
|
|
7
73
|
审核范围 0.8.9→0.8.13(上一轮门禁只覆盖到 0.8.8)。结论:代码侧无阻塞,文档有 1 处必须修。
|
|
@@ -21,6 +87,12 @@
|
|
|
21
87
|
- **`sync-profile --repair`**:断链(inode 不同)目前只警告。已在 HANDOFF 写明「新增文件后必须跑」,
|
|
22
88
|
留作后续(改起来简单,但此刻没有证据表明它造成过事故)。
|
|
23
89
|
|
|
90
|
+
### 发布
|
|
91
|
+
- 已发 `dsh-session-superagent@0.8.14`(registry `latest = 0.8.14`)。
|
|
92
|
+
- **从 tarball 真装验证**:`npm i https://registry.npmjs.org/dsh-session-superagent/-/dsh-session-superagent-0.8.14.tgz`
|
|
93
|
+
→ `apply()` 成功、**路由 6 条**(`/client-hello` 确实没了)、`client.js` 语法通过、`lib/` 四个文件齐全。
|
|
94
|
+
注:刚发布时 `npm i pkg@x.y.z` 可能因客户端元数据缓存报 ETARGET,用 tarball URL 或 `--prefer-online` 可绕过。
|
|
95
|
+
|
|
24
96
|
### 其他
|
|
25
97
|
- `scripts/smoke-observe.mjs` 加 `gh auth status` 预检:没登录时明确 SKIP 并退出 0,
|
|
26
98
|
不再把「凭证/网络问题」混报成逻辑失败(第四轮审核的建议)。
|
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
|
|
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.16'
|
|
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]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-session-superagent",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.16",
|
|
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",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"client.js",
|
|
21
21
|
"scripts/preview-orbit.mjs",
|
|
22
22
|
"scripts/port-official-ui.mjs",
|
|
23
|
-
"scripts/sync-profile.mjs"
|
|
23
|
+
"scripts/sync-profile.mjs",
|
|
24
|
+
"scripts/render-preview.mjs"
|
|
24
25
|
],
|
|
25
26
|
"engines": {
|
|
26
27
|
"node": ">=22.19.0",
|
|
@@ -31,7 +32,10 @@
|
|
|
31
32
|
"check": "node --test",
|
|
32
33
|
"preview:orbit": "node scripts/preview-orbit.mjs",
|
|
33
34
|
"sync:profile": "node scripts/sync-profile.mjs",
|
|
34
|
-
"smoke:observe": "node scripts/smoke-observe.mjs"
|
|
35
|
+
"smoke:observe": "node scripts/smoke-observe.mjs",
|
|
36
|
+
"sync:profile:check": "node scripts/sync-profile.mjs --check",
|
|
37
|
+
"sync:profile:repair": "node scripts/sync-profile.mjs --repair",
|
|
38
|
+
"preview": "node scripts/render-preview.mjs"
|
|
35
39
|
},
|
|
36
40
|
"keywords": [
|
|
37
41
|
"dsh",
|
|
@@ -0,0 +1,213 @@
|
|
|
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
CHANGED
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
// 把插件源码目录同步到 profile 的安装副本。
|
|
2
2
|
//
|
|
3
3
|
// 为什么需要:profile 里是 `file:` 安装 —— pnpm 为**已存在的文件**建了硬链接,
|
|
4
|
-
//
|
|
5
|
-
// 真实事故(2026-11-01 23:59 重启):新增的 lib/body.js 没进 profile,
|
|
6
|
-
// 宿主 import './body.js' 失败 → 整个插件不挂载 → 工具和 HTTP 路由全消失,
|
|
7
|
-
// 而客户端 bundle 照常加载,表面看起来"插件还在"。
|
|
4
|
+
// 改内容会自动生效;但**新增文件不会**自动出现,而**被原子写入替换的文件会断链**。
|
|
8
5
|
//
|
|
9
|
-
//
|
|
10
|
-
import
|
|
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'
|
|
11
18
|
import { dirname, join } from 'node:path'
|
|
12
19
|
import { fileURLToPath } from 'node:url'
|
|
13
20
|
|
|
14
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')
|
|
15
26
|
const DEFAULT_PROFILE = join(
|
|
16
27
|
process.env.DSH_HOME || join(process.env.USERPROFILE || '', '.dsh'),
|
|
17
28
|
'profiles',
|
|
@@ -19,7 +30,7 @@ const DEFAULT_PROFILE = join(
|
|
|
19
30
|
'node_modules',
|
|
20
31
|
'dsh-session-superagent',
|
|
21
32
|
)
|
|
22
|
-
const DEST =
|
|
33
|
+
const DEST = dirFlag >= 0 ? args[dirFlag + 1] : DEFAULT_PROFILE
|
|
23
34
|
|
|
24
35
|
/** 运行时要用的文件(不含 docs / test / scripts 这些开发件)。 */
|
|
25
36
|
const TARGETS = [
|
|
@@ -32,45 +43,87 @@ if (!existsSync(DEST)) {
|
|
|
32
43
|
process.exit(1)
|
|
33
44
|
}
|
|
34
45
|
|
|
46
|
+
const drift = { missing: [], broken: [], stale: [] }
|
|
35
47
|
let linked = 0
|
|
36
|
-
|
|
48
|
+
|
|
37
49
|
for (const rel of TARGETS) {
|
|
38
50
|
const src = join(ROOT, rel)
|
|
39
51
|
const dst = join(DEST, rel)
|
|
40
52
|
if (!existsSync(src)) continue
|
|
41
|
-
if (existsSync(dst)) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
+
}
|
|
45
61
|
continue
|
|
46
62
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
+
}
|
|
54
89
|
}
|
|
55
90
|
}
|
|
56
91
|
|
|
57
|
-
// lib/ 里每个相对 import
|
|
92
|
+
// lib/ 里每个相对 import 都必须能解析(这正是事故 1 的直接原因)
|
|
58
93
|
const libDir = join(ROOT, 'lib')
|
|
59
|
-
const
|
|
94
|
+
const missingImports = []
|
|
60
95
|
for (const name of readdirSync(libDir)) {
|
|
61
96
|
if (!name.endsWith('.js')) continue
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
if (!
|
|
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
|
+
}
|
|
66
111
|
}
|
|
67
112
|
}
|
|
68
113
|
|
|
114
|
+
const drifted = drift.missing.length + drift.broken.length + drift.stale.length
|
|
69
115
|
console.log('---')
|
|
70
|
-
console.log(`新增链接 ${linked} 个,跳过 ${skipped} 个`)
|
|
71
116
|
console.log(`目标:${DEST}`)
|
|
72
|
-
|
|
73
|
-
|
|
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(', '))
|
|
74
122
|
process.exit(2)
|
|
75
123
|
}
|
|
76
124
|
console.log('相对 import 全部可解析 ✓')
|
|
125
|
+
|
|
126
|
+
if (CHECK && drifted > 0) {
|
|
127
|
+
console.error('profile 与仓库不一致(跑 --repair 修)')
|
|
128
|
+
process.exit(1)
|
|
129
|
+
}
|