dsh-session-superagent 0.8.11 → 0.8.13

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 CHANGED
@@ -2,6 +2,49 @@
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.13] - 2026-11-02 —— 真机跑「只观察」闭环,抓出两个真 bug
6
+
7
+ 拿真仓库(MYCF711/dsh-GOkifu-compact,3 个真 issue)跑 observe 模式验证,抓到:
8
+
9
+ ### 修复
10
+ - **`filteredLog` 从没落过盘**:`filteredLog` 不在 `POLL_RESULT_FIELDS` 白名单里,
11
+ 内存里写得好好的,`persistEntry` 一过滤就没了 → 面板「最近被过滤」永远是空的。
12
+ 现在进白名单,并补了守门用例。
13
+ - **空轮询不清理 `lastDeferred`**:把过滤器改严之后一条都不该唤醒,但面板仍显示上一轮的
14
+ 「仅观察:本轮本会唤醒 4 条(#4 #2 #3 #1)」——那是过期信息。现在 `fresh.length === 0` 时清空。
15
+
16
+ ### 真机证据(修复前)
17
+ ```
18
+ observe: true
19
+ lastDeferred: 仅观察:本轮本会唤醒 4 条(#4 #2 #3 #1)
20
+ history: [{"numbers":[4,2,3,1],"ok":false,"reason":"observe"}]
21
+ filteredLog: [] ← 改成 labels:["ai-fix"] 后仍为空(3 个 issue 都没这个标签)
22
+ ```
23
+
24
+ ### 测试
25
+ - 合计 **79 例**。
26
+
27
+ ## [0.8.12] - 2026-11-02 —— 体验打磨四条
28
+
29
+ ### 改了什么
30
+ - **Toast 改成官方单条语义**:移植来的 `.dt-toast` 是 `position:fixed` 顶部居中、自带淡出动画,
31
+ 同时挂多条会**重叠成一团**。现在同一时刻只显示最新一条,停留时长用官方的 `--dt-toast-hold` 控制,
32
+ 错误态保留淡红底 + `role=status` / `aria-live=polite`。
33
+ - **页签焦点跟随**:方向键/Home/End 切页后,焦点跟到新页签 —— 之前旧页签变 `tabIndex=-1`、
34
+ 新页签变 0,但焦点留在原地,键盘用户会「掉出」tablist。
35
+ - **删掉 `/session-superagent/config`**:它和 `/state` 重复,是未鉴权的 GET,而且上一轮
36
+ `ReferenceError: gh is not defined` 就出在它身上。现在路由从 8 条减到 7 条,测试改为断言它已下线。
37
+
38
+ ### 发布与验证
39
+ - 已发 `dsh-session-superagent@0.8.12`(registry 确认 `latest=0.8.12`)。
40
+ - **从 registry 真装一遍验证**(不是只看 `npm pack`):
41
+ `npm i dsh-session-superagent@0.8.12` → `import(...)/lib/index.js` → `apply()` 成功、
42
+ 路由 **7** 条(`/config` 确实没了)、`node --check client.js` 通过。
43
+ - 新增守门:`lib/` 里每个相对 import 必须能解析到真实文件(发布前挡住 `body.js` 那类事故)。
44
+
45
+ ### 测试
46
+ - 合计 **78 例**(`/config` 用例改为「路由表里不应再有它」+ 相对 import 守门)。
47
+
5
48
  ## [0.8.11] - 2026-11-02 —— 真机验证隔离执行 + watch_doctor
6
49
 
7
50
  ### 验证(这轮真的在跑起来的宿主上做的)
@@ -26,6 +69,12 @@
26
69
  结果是 `lastError="会话已不在:请删除或重建这条主理鱼"`、`lastWorktree=null`、**没有** `.dsh-zhuli` 目录
27
70
  (修复前会先建一个永远用不上的 worktree)。
28
71
 
72
+ ### 发布
73
+ - **已发布到 npm**:`dsh-session-superagent@0.8.11`(`npm view` 确认,tarball 220.6 kB / 13 文件)。
74
+ - 顺手把 `latest` 指向 0.8.11,并把 npm 自动生成的 `0.0.0-stage` 占位版标为弃用
75
+ (否则 `npm i dsh-session-superagent` 会装到那个空占位)。
76
+ - GitHub Release:https://github.com/MYCF711/dsh-session-superagent/releases/tag/v0.8.11
77
+
29
78
  ### 测试
30
79
  - 合计 **77 例**(本修复属运行时守卫,靠真机 E2E 验证)。
31
80
 
package/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # dsh-session-superagent
2
2
 
3
3
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
4
- [![Version](https://img.shields.io/badge/version-0.8.11-green.svg)](https://github.com/MYCF711/dsh-session-superagent/releases)
4
+ [![Version](https://img.shields.io/badge/version-0.8.13-green.svg)](https://github.com/MYCF711/dsh-session-superagent/releases)
5
+ [![npm](https://img.shields.io/npm/v/dsh-session-superagent.svg)](https://www.npmjs.com/package/dsh-session-superagent)
5
6
  [![dsh](https://img.shields.io/badge/DeepSeek%20Harness-plugin-orange.svg)](https://github.com/topics/dsh-plugin)
6
7
 
7
8
  > **一句话**:给一个 DSH 会话装上一条 **主理鱼**——
@@ -50,18 +51,17 @@ GitHub 有新动态 → watcher 轮询 → Agent.followup(消息) → 会话跑
50
51
 
51
52
  ## 安装
52
53
 
53
- ```sh
54
- dsh plugin --profile <profile> add github:MYCF711/dsh-session-superagent
55
- ```
56
-
57
- 包会带 `cordis.patch.yml`(插入一个 tools-only 行),**不会**自动改你的 `bundles` 列表——那一步是操作者的动作。
54
+ ```bash
55
+ # 方式一:npm(已发布)
56
+ npm i dsh-session-superagent
58
57
 
59
- 装完重启一次宿主,然后在任意会话里:
60
-
61
- ```
62
- watch_start({ repo: "owner/name", intervalSec: 60 })
58
+ # 方式二:从 GitHub 源码装(会跟踪 main)
59
+ dsh plugin add https://github.com/MYCF711/dsh-session-superagent
63
60
  ```
64
61
 
62
+ 装进 profile 时是 `file:` 硬链接,改代码即改线上;**但新增文件不会被自动带过去**,
63
+ 加完文件要跑 `pnpm run sync:profile`(否则下次重启插件会整体不挂载,这坑踩过一次)。
64
+
65
65
  ## 一次真实运行长什么样
66
66
 
67
67
  1. `watch_start` 登记 watcher(状态存 `~/.dsh/state/session-superagent/watchers.json`);
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.11'
17
+ const CLIENT_VERSION = '0.8.13'
18
18
  const BASE = '/session-superagent'
19
19
  const ENDPOINT = BASE + '/state'
20
20
  const POLL_MS = 5000
@@ -125,6 +125,7 @@ window.__ModuleLoader__.load({
125
125
 
126
126
  /** 页级 tab 条:点当前页会再发一次 onChange(回调里可重置到根部)。 */
127
127
  function SegTabs({ items, value, onChange, label }) {
128
+ const buttons = useRef({})
128
129
  const walk = (event) => {
129
130
  if (items.length === 0) return
130
131
  const values = items.map((item) => item.value)
@@ -133,13 +134,19 @@ window.__ModuleLoader__.load({
133
134
  if (target === undefined) return
134
135
  event.preventDefault()
135
136
  const next = values[(target + values.length) % values.length]
136
- if (next !== value) onChange(next)
137
+ if (next !== value) {
138
+ onChange(next)
139
+ // 焦点跟随:旧页签变成 tabIndex=-1 后,键盘用户会掉出 tablist
140
+ const nextButton = buttons.current[next]
141
+ if (nextButton && typeof nextButton.focus === 'function') nextButton.focus()
142
+ }
137
143
  }
138
144
  return h('div', {
139
145
  role: 'tablist', 'aria-label': label, className: 'dt-tabs', onKeyDown: walk,
140
146
  style: { gridTemplateColumns: 'repeat(' + items.length + ', minmax(0, 1fr))' },
141
147
  }, items.map((item) => h('button', {
142
148
  key: item.value, type: 'button', role: 'tab',
149
+ ref: (node) => { buttons.current[item.value] = node },
143
150
  'aria-selected': item.value === value,
144
151
  tabIndex: item.value === value ? 0 : -1,
145
152
  className: 'dt-tab',
@@ -593,26 +600,24 @@ window.__ModuleLoader__.load({
593
600
 
594
601
  /** 轻量 Toast:右下角堆叠,4 秒自动消失。 */
595
602
  function useToast() {
596
- const [items, setItems] = useState([])
597
- const timers = useRef([])
598
- useEffect(() => () => { for (const timer of timers.current) clearTimeout(timer); timers.current = [] }, [])
603
+ // 官方 .dt-toast 是 position:fixed 顶部居中、且带自动淡出动画 —— 同时挂两条会重叠,
604
+ // 所以这里保持「同一时刻只有最新一条」,停留时长用官方的 --dt-toast-hold 控制。
605
+ const [item, setItem] = useState(null)
606
+ const timer = useRef(null)
607
+ useEffect(() => () => { if (timer.current) clearTimeout(timer.current) }, [])
599
608
  const notify = useCallback((text, tone = 'info') => {
600
- const id = Math.random().toString(36).slice(2)
601
- setItems((prev) => [...prev, { id, text, tone }])
602
- const timer = setTimeout(() => {
603
- // 用后即删:长会话里这个数组不能只增不减
604
- timers.current = timers.current.filter((handle) => handle !== timer)
605
- setItems((prev) => prev.filter((item) => item.id !== id))
606
- }, 4000)
607
- timers.current.push(timer)
609
+ if (timer.current) clearTimeout(timer.current)
610
+ setItem({ id: Math.random().toString(36).slice(2), text, tone })
611
+ timer.current = setTimeout(() => setItem(null), 4000)
608
612
  }, [])
609
- // .dt-toast 自己就是 position:fixed(顶部居中),这里只做分组,不再加定位样式
610
- const view = items.length === 0 ? null : h(React.Fragment, null,
611
- items.map((item) => h('div', {
612
- key: item.id,
613
- className: cx('dt-toast', item.tone === 'error' && 'dt-danger'),
614
- style: item.tone === 'error' ? { background: 'color-mix(in srgb, #f85149 14%, var(--dsw-alias-bg-layer-1, #fff))' } : undefined,
615
- }, item.text)))
613
+ const view = item === null ? null : h('div', {
614
+ className: cx('dt-toast', item.tone === 'error' && 'dt-danger'),
615
+ role: 'status',
616
+ 'aria-live': 'polite',
617
+ style: item.tone === 'error'
618
+ ? { '--dt-toast-hold': '3600ms', background: 'color-mix(in srgb, #f85149 14%, var(--dsw-alias-bg-layer-1, #fff))' }
619
+ : { '--dt-toast-hold': '3600ms' },
620
+ }, item.text)
616
621
  return [view, notify]
617
622
  }
618
623
 
package/lib/index.js CHANGED
@@ -422,7 +422,11 @@ async function pollOnce(ctx, entry, baseMs) {
422
422
  const newestAbsorbed = newestUpdatedAt(absorbed)
423
423
  if (!entry.cursor || Date.parse(newestAbsorbed) > Date.parse(entry.cursor)) entry.cursor = newestAbsorbed
424
424
  }
425
- if (fresh.length === 0) return
425
+ if (fresh.length === 0) {
426
+ // 这一轮没有新增:上一轮的说法(比如「仅观察:本会唤醒 4 条」)已经过期了
427
+ entry.lastDeferred = null
428
+ return
429
+ }
426
430
 
427
431
  // 仅观察模式(对标官方 observe):照常判定,但只记录「本会唤醒哪几条」,不真唤醒
428
432
  if (entry.observe === true) {
@@ -663,25 +667,6 @@ export function apply(ctx) {
663
667
  res.end(JSON.stringify(body))
664
668
  }
665
669
 
666
- // Operator-facing configuration: what is configured (never the values).
667
- routes.push(httpServer.register({
668
- kind: 'exact',
669
- path: '/session-superagent/config',
670
- handler: async (req, res) => {
671
- if (req.method !== 'GET') return json(res, 405, { error: 'method not allowed' })
672
- const state = readState()
673
- const gh = await ghHealth()
674
- json(res, 200, {
675
- gh,
676
- credentials: secretsSummary(),
677
- watchers: Object.values(state.watchers || {}).map((w) => ({
678
- repo: w.repo, sessionId: w.sessionId, intervalSec: w.intervalSec,
679
- enabled: w.enabled !== false, goal: w.goal ?? '',
680
- })),
681
- })
682
- },
683
- }))
684
-
685
670
  // Create or update a watcher from the panel. The panel is session-scoped,
686
671
  // so it must send the session id it is bound to.
687
672
  routes.push(httpServer.register({
package/lib/watch.js CHANGED
@@ -350,6 +350,8 @@ export const POLL_RESULT_FIELDS = [
350
350
  'lastPoll', 'lastError', 'nextAt', 'consecutiveFailures', 'seen', 'cursor', 'lastReported',
351
351
  'lastWake', 'lastWakeCount', 'lastBatch', 'lastAbsorbed', 'wakeTimes', 'lastDeferred',
352
352
  'history', 'lastWorktree', 'lastAcked', 'lastTrigger',
353
+ // 过滤记录也是轮询结果:漏了它面板上的「最近被过滤」永远是空的
354
+ 'filteredLog',
353
355
  ]
354
356
 
355
357
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-session-superagent",
3
- "version": "0.8.11",
3
+ "version": "0.8.13",
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",