dsh-whaleling 0.0.0-stage → 0.1.0
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/cordis.patch.yml +4 -0
- package/icon.svg +6 -0
- package/index.js +154 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +36 -4
- package/README.md +0 -3
package/cordis.patch.yml
ADDED
package/icon.svg
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
|
|
2
|
+
<rect x="4" y="4" width="56" height="56" rx="14" fill="#1b2030"/>
|
|
3
|
+
<path d="M14 40c6-12 14-12 18 0" fill="none" stroke="#4d6bfe" stroke-width="4" stroke-linecap="round"/>
|
|
4
|
+
<path d="M32 40c6-12 14-12 18 0" fill="none" stroke="#8fa6ff" stroke-width="4" stroke-linecap="round"/>
|
|
5
|
+
<circle cx="32" cy="22" r="3.2" fill="#fff"/>
|
|
6
|
+
</svg>
|
package/index.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dshfloat-bridge —— 把「哪个会话正在跑」从 host 进程里递出来。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要它:悬浮窗是独立进程,拿不到 ctx.*;而读磁盘只能靠日志 mtime 猜,
|
|
5
|
+
* 猜不准(一轮里可能几十秒没有写入)。这个插件在 host 里用 ctx.sessions.list()
|
|
6
|
+
* 拿到真实的活会话列表,增量读事件判断 turn 是否结束,再把结果写成一个小 JSON。
|
|
7
|
+
*
|
|
8
|
+
* 输出:~/.dsh/dshfloat-bridge.json
|
|
9
|
+
* { v, at, running, sessions, ids }
|
|
10
|
+
* 写不进去就算了 —— 悬浮窗读不到会自动退回读盘。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync, writeFileSync, renameSync } from 'node:fs'
|
|
14
|
+
import { homedir } from 'node:os'
|
|
15
|
+
import { join } from 'node:path'
|
|
16
|
+
|
|
17
|
+
export const name = 'dshfloat-bridge'
|
|
18
|
+
|
|
19
|
+
export const inject = ['sessions']
|
|
20
|
+
|
|
21
|
+
const FILE = join(homedir(), '.dsh', 'dshfloat-bridge.json')
|
|
22
|
+
const REQ_FILE = join(homedir(), '.dsh', 'dshfloat-request.json')
|
|
23
|
+
const REPLY_FILE = join(homedir(), '.dsh', 'dshfloat-reply.json')
|
|
24
|
+
const KEEP_ENDS = 200 // 最近多少条 turn/end 递出去
|
|
25
|
+
const KEEP_MS = 24 * 3600e3 // 更早的丢掉
|
|
26
|
+
const TEXT_LIMIT = 20000 // 按需返回时用
|
|
27
|
+
const MAX_SESSIONS = 120
|
|
28
|
+
|
|
29
|
+
export function apply(ctx) {
|
|
30
|
+
/** sessionId -> { seq, open, title, lastAssistant, lastUser, at } */
|
|
31
|
+
const state = new Map()
|
|
32
|
+
/** 最近的 turn/end:widget 靠它按事件时间判断「当时我在不在 DSH」 */
|
|
33
|
+
const ends = []
|
|
34
|
+
let servedNonce = ''
|
|
35
|
+
|
|
36
|
+
const tick = () => {
|
|
37
|
+
let running = 0
|
|
38
|
+
let total = 0
|
|
39
|
+
const ids = []
|
|
40
|
+
|
|
41
|
+
for (const session of ctx.sessions.list()) {
|
|
42
|
+
total += 1
|
|
43
|
+
const id = String(session.id)
|
|
44
|
+
const prev = state.get(id) ?? {
|
|
45
|
+
seq: 0, open: false, title: '', lastAssistant: '', lastUser: '', at: 0,
|
|
46
|
+
cwd: String(session.header?.cwd ?? ''),
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (session.seq > prev.seq) {
|
|
50
|
+
// 只读上次之后的新事件,不整份重放
|
|
51
|
+
for (const event of session.snapshotEvents(prev.seq, session.seq)) {
|
|
52
|
+
if (event.type === 'turn/start') {
|
|
53
|
+
prev.open = true
|
|
54
|
+
prev.at = Number(event.time)
|
|
55
|
+
} else if (event.type === 'assistant/message') {
|
|
56
|
+
const text = (event.data?.message?.content ?? [])
|
|
57
|
+
.filter((block) => block?.type === 'text')
|
|
58
|
+
.map((block) => block.text)
|
|
59
|
+
.join('')
|
|
60
|
+
.trim()
|
|
61
|
+
if (text) {
|
|
62
|
+
prev.lastAssistant = text.slice(0, TEXT_LIMIT)
|
|
63
|
+
prev.at = Number(event.time)
|
|
64
|
+
}
|
|
65
|
+
} else if (event.type === 'user/message') {
|
|
66
|
+
const text = (event.data?.content ?? [])
|
|
67
|
+
.filter((block) => block?.type === 'text')
|
|
68
|
+
.map((block) => block.text)
|
|
69
|
+
.join('')
|
|
70
|
+
.trim()
|
|
71
|
+
if (text) prev.lastUser = text.slice(0, TEXT_LIMIT)
|
|
72
|
+
} else if (event.type === 'session/title') {
|
|
73
|
+
const title = event.data?.title
|
|
74
|
+
if (typeof title === 'string' && title) prev.title = title
|
|
75
|
+
} else if (event.type === 'turn/end') {
|
|
76
|
+
prev.open = false
|
|
77
|
+
prev.at = Number(event.time)
|
|
78
|
+
ends.push({
|
|
79
|
+
id,
|
|
80
|
+
seq: Number(event.seq),
|
|
81
|
+
time: Number(event.time),
|
|
82
|
+
reason: event.data?.reason?.kind ?? 'unknown',
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
prev.seq = session.seq
|
|
87
|
+
state.set(id, prev)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (prev.open) {
|
|
91
|
+
running += 1
|
|
92
|
+
ids.push(id)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 只留最近的一批;按时间戳裁剪,避免无限增长
|
|
97
|
+
const cutoff = Date.now() - KEEP_MS
|
|
98
|
+
while (ends.length > 0 && (ends[0].time < cutoff || ends.length > KEEP_ENDS)) ends.shift()
|
|
99
|
+
|
|
100
|
+
const list = []
|
|
101
|
+
for (const [id, value] of state) {
|
|
102
|
+
if (!value.at && !value.lastAssistant && !value.title) continue
|
|
103
|
+
list.push({
|
|
104
|
+
id,
|
|
105
|
+
cwd: value.cwd || '',
|
|
106
|
+
title: value.title || '',
|
|
107
|
+
running: value.open,
|
|
108
|
+
at: value.at,
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
list.sort((a, b) => b.at - a.at)
|
|
112
|
+
|
|
113
|
+
// 只在你点开某个会话时,才把它的正文写出来
|
|
114
|
+
try {
|
|
115
|
+
const req = JSON.parse(readFileSync(REQ_FILE, 'utf8'))
|
|
116
|
+
if (req && req.nonce && req.nonce !== servedNonce) {
|
|
117
|
+
servedNonce = req.nonce
|
|
118
|
+
const value = state.get(String(req.id)) || {}
|
|
119
|
+
writeFileSync(REPLY_FILE + '.tmp', JSON.stringify({
|
|
120
|
+
nonce: req.nonce,
|
|
121
|
+
id: req.id,
|
|
122
|
+
title: value.title || '',
|
|
123
|
+
assistant: value.lastAssistant || '',
|
|
124
|
+
user: value.lastUser || '',
|
|
125
|
+
}))
|
|
126
|
+
renameSync(REPLY_FILE + '.tmp', REPLY_FILE)
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
// 没有请求就跳过
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const payload = JSON.stringify({
|
|
133
|
+
v: 3,
|
|
134
|
+
at: Date.now(),
|
|
135
|
+
running,
|
|
136
|
+
sessions: total,
|
|
137
|
+
ids,
|
|
138
|
+
ends,
|
|
139
|
+
list: list.slice(0, MAX_SESSIONS),
|
|
140
|
+
})
|
|
141
|
+
try {
|
|
142
|
+
writeFileSync(`${FILE}.tmp`, payload)
|
|
143
|
+
renameSync(`${FILE}.tmp`, FILE)
|
|
144
|
+
} catch {
|
|
145
|
+
// 静默失败:widget 那边会退回读盘
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
ctx.effect(() => {
|
|
150
|
+
const timer = setInterval(tick, 1000)
|
|
151
|
+
tick()
|
|
152
|
+
return () => clearInterval(timer)
|
|
153
|
+
})
|
|
154
|
+
}
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-whaleling",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Bridge plugin that exposes live DeepSeek Harness session state to the whaleling floating widget.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js",
|
|
9
|
+
"./locale/*.json": "./locale/*.json",
|
|
10
|
+
"./icon": "./icon.svg"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"index.js",
|
|
14
|
+
"cordis.patch.yml",
|
|
15
|
+
"locale/*.json",
|
|
16
|
+
"icon.svg"
|
|
17
|
+
],
|
|
18
|
+
"keywords": [
|
|
19
|
+
"dsh",
|
|
20
|
+
"deepseek-harness",
|
|
21
|
+
"plugin",
|
|
22
|
+
"dsh-bundle"
|
|
23
|
+
],
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/rUrU516/dsh-whaleling.git",
|
|
27
|
+
"directory": "bundles/dsh-whaleling"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"dsh": ">=0.2.0 <0.3.0"
|
|
31
|
+
},
|
|
32
|
+
"dsh": {
|
|
33
|
+
"bundle": {
|
|
34
|
+
"patch": "./cordis.patch.yml"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"license": "MIT"
|
|
38
|
+
}
|
package/README.md
DELETED