@kesike/reminder-dsh 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +62 -2
- package/README.zh-CN.md +138 -0
- package/assets/DEEPSEEK-LICENSE.txt +21 -0
- package/assets/README.md +9 -0
- package/assets/deepseek-harness.png +0 -0
- package/cordis.patch.yml +3 -0
- package/example.patch.yml +8 -0
- package/lib/client.js +138 -0
- package/lib/index.js +1 -0
- package/package.json +75 -4
- package/scripts/test-popup.js +19 -0
- package/scripts/test-taskbar.js +11 -0
- package/src/index.js +38 -0
- package/src/modules/popup/ToastActivator.cs +125 -0
- package/src/modules/popup/activate.ps1 +5 -0
- package/src/modules/popup/index.js +34 -0
- package/src/modules/popup/queue.js +93 -0
- package/src/modules/popup/reply.js +65 -0
- package/src/modules/popup/summary.js +67 -0
- package/src/modules/popup/toast.js +90 -0
- package/src/modules/popup/toast.ps1 +51 -0
- package/src/modules/taskbar/flash.ps1 +107 -0
- package/src/modules/taskbar/index.js +22 -0
- package/src/modules/taskbar/windows.js +77 -0
- package/src/reminder.js +81 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
|
|
4
|
+
const script = fileURLToPath(new URL('./flash.ps1', import.meta.url))
|
|
5
|
+
|
|
6
|
+
/** Coalesce taskbar attention for the same app; stop gracefully to clear native flashing. */
|
|
7
|
+
export function createTaskbarNotifier({ isEnabled, getWindowTitle }, launch = spawn) {
|
|
8
|
+
let active, disposed = false
|
|
9
|
+
|
|
10
|
+
function send(state, message) {
|
|
11
|
+
if (!state.child.stdin.destroyed) state.child.stdin.write(JSON.stringify(message) + '\n', 'utf8')
|
|
12
|
+
}
|
|
13
|
+
function stop(state = active) {
|
|
14
|
+
if (!state || state.stopping) return
|
|
15
|
+
state.stopping = true
|
|
16
|
+
send(state, { type: 'stop' })
|
|
17
|
+
state.killTimer = setTimeout(() => state.child.kill(), 2000)
|
|
18
|
+
}
|
|
19
|
+
function start() {
|
|
20
|
+
const child = launch('pwsh.exe', ['-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass',
|
|
21
|
+
'-File', script], { windowsHide: true, shell: false, stdio: ['pipe', 'ignore', 'pipe'] })
|
|
22
|
+
const state = { child, subscribers: new Set(), stopping: false }
|
|
23
|
+
let resolve, reject, stderr = '', error, finished = false
|
|
24
|
+
state.done = new Promise((yes, no) => { resolve = yes; reject = no })
|
|
25
|
+
state.done.catch(() => {})
|
|
26
|
+
const finish = failure => {
|
|
27
|
+
if (finished) return
|
|
28
|
+
finished = true
|
|
29
|
+
clearTimeout(state.timeout)
|
|
30
|
+
clearTimeout(state.killTimer)
|
|
31
|
+
if (active === state) active = undefined
|
|
32
|
+
if (failure && !state.stopping && !disposed) reject(failure)
|
|
33
|
+
else resolve()
|
|
34
|
+
}
|
|
35
|
+
child.stderr.setEncoding('utf8')
|
|
36
|
+
child.stderr.on('data', value => { stderr = (stderr + value).slice(-2048) })
|
|
37
|
+
child.on('error', failure => finish(failure.code === 'ENOENT'
|
|
38
|
+
? new Error('找不到 pwsh.exe,请安装 PowerShell 7 并重启 DSH。') : failure))
|
|
39
|
+
child.on('close', code => finish(error ?? (code === 0 ? undefined : new Error(`任务栏提醒失败:${stderr.trim() || code}`))))
|
|
40
|
+
child.stdin.on('error', failure => { error = failure; child.kill() })
|
|
41
|
+
state.armDeadline = () => {
|
|
42
|
+
clearTimeout(state.timeout)
|
|
43
|
+
state.timeout = setTimeout(() => { error = new Error('任务栏提醒进程超时'); child.kill() }, 30_000)
|
|
44
|
+
}
|
|
45
|
+
state.armDeadline()
|
|
46
|
+
send(state, { hostPid: process.pid, windowTitle: getWindowTitle(), durationSeconds: 10 })
|
|
47
|
+
return state
|
|
48
|
+
}
|
|
49
|
+
function subscribe(state, signal) {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const subscriber = {}
|
|
52
|
+
state.subscribers.add(subscriber)
|
|
53
|
+
const abort = () => {
|
|
54
|
+
state.subscribers.delete(subscriber)
|
|
55
|
+
resolve()
|
|
56
|
+
if (!state.subscribers.size) stop(state)
|
|
57
|
+
}
|
|
58
|
+
signal?.addEventListener('abort', abort, { once: true })
|
|
59
|
+
state.done.then(resolve, reject).finally(() => {
|
|
60
|
+
state.subscribers.delete(subscriber)
|
|
61
|
+
signal?.removeEventListener('abort', abort)
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
function notify({ signal } = {}) {
|
|
66
|
+
if (disposed || !isEnabled() || signal?.aborted) return Promise.resolve()
|
|
67
|
+
if (active?.stopping) return active.done.catch(() => {}).then(() => notify({ signal }))
|
|
68
|
+
if (active) { active.armDeadline(); send(active, { type: 'refresh' }) }
|
|
69
|
+
else active = start()
|
|
70
|
+
return subscribe(active, signal)
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
notify,
|
|
74
|
+
stop: () => stop(),
|
|
75
|
+
dispose() { disposed = true; stop() },
|
|
76
|
+
}
|
|
77
|
+
}
|
package/src/reminder.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createUserMessage } from '@deepseek-ai/dsh-llm/message'
|
|
2
|
+
|
|
3
|
+
/** Register live events only: restored history and initially idle agents do not notify. */
|
|
4
|
+
export function attachReminder(ctx, notify, reportError) {
|
|
5
|
+
const activities = new WeakMap()
|
|
6
|
+
const closedAgents = new WeakSet()
|
|
7
|
+
const notifications = new Map()
|
|
8
|
+
let disposed = false
|
|
9
|
+
const isMain = session => session.header.origin !== 'subagent'
|
|
10
|
+
&& !(session.header.delegationDepth > 0)
|
|
11
|
+
|
|
12
|
+
ctx.on('agent/status', ({ agent, status }) => {
|
|
13
|
+
const session = agent.session
|
|
14
|
+
if (disposed || !isMain(session)) return
|
|
15
|
+
if (status === 'running') {
|
|
16
|
+
if (!activities.has(session)) activities.set(session, { completed: false, text: '' })
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
if (status !== 'idle') return
|
|
20
|
+
const activity = activities.get(session)
|
|
21
|
+
activities.delete(session)
|
|
22
|
+
if (!activity?.completed) return
|
|
23
|
+
const controller = new AbortController()
|
|
24
|
+
const pending = notifications.get(agent) ?? new Set()
|
|
25
|
+
pending.add(controller)
|
|
26
|
+
notifications.set(agent, pending)
|
|
27
|
+
// Consume before launching the subprocess so repeated idle events cannot duplicate it.
|
|
28
|
+
try {
|
|
29
|
+
Promise.resolve(notify({
|
|
30
|
+
sessionId: session.id, completedText: activity.text,
|
|
31
|
+
route: agent.model?.current(), signal: controller.signal,
|
|
32
|
+
onReply(text) {
|
|
33
|
+
if (disposed || closedAgents.has(agent) || agent.session !== session) {
|
|
34
|
+
throw new Error('原会话已关闭或切换,请回到 DSH 回复。')
|
|
35
|
+
}
|
|
36
|
+
return agent.followup(createUserMessage({
|
|
37
|
+
content: [{ type: 'text', text }], source: { kind: 'user' },
|
|
38
|
+
}))
|
|
39
|
+
},
|
|
40
|
+
})).catch(error => {
|
|
41
|
+
if (!disposed && !controller.signal.aborted) reportError(error)
|
|
42
|
+
}).finally(() => {
|
|
43
|
+
pending.delete(controller)
|
|
44
|
+
if (!pending.size) notifications.delete(agent)
|
|
45
|
+
})
|
|
46
|
+
} catch (error) {
|
|
47
|
+
pending.delete(controller)
|
|
48
|
+
if (!pending.size) notifications.delete(agent)
|
|
49
|
+
reportError(error)
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
ctx.on('session/event', (session, event) => {
|
|
54
|
+
if (disposed) return
|
|
55
|
+
const activity = activities.get(session)
|
|
56
|
+
if (!activity) return
|
|
57
|
+
if (event.type === 'turn/start') {
|
|
58
|
+
activity.completed = false
|
|
59
|
+
activity.text = ''
|
|
60
|
+
}
|
|
61
|
+
if (event.type === 'assistant/message') {
|
|
62
|
+
const content = event.data.message.content
|
|
63
|
+
// Keep only the final, uninterrupted user-visible answer; never reasoning/tool payloads.
|
|
64
|
+
activity.text = event.data.interrupted || content.some(block => block.type === 'tool-call')
|
|
65
|
+
? '' : content.filter(block => block.type === 'text').map(block => block.text).join('\n')
|
|
66
|
+
}
|
|
67
|
+
if (event.type === 'turn/end') activity.completed = event.data.reason.kind === 'completed'
|
|
68
|
+
})
|
|
69
|
+
ctx.on('agent/error', ({ agent }) => activities.delete(agent.session))
|
|
70
|
+
ctx.on('agent/disposed', ({ agent }) => {
|
|
71
|
+
activities.delete(agent.session)
|
|
72
|
+
closedAgents.add(agent)
|
|
73
|
+
for (const controller of notifications.get(agent) ?? []) controller.abort()
|
|
74
|
+
notifications.delete(agent)
|
|
75
|
+
})
|
|
76
|
+
ctx.effect(() => () => {
|
|
77
|
+
disposed = true
|
|
78
|
+
for (const pending of notifications.values()) for (const controller of pending) controller.abort()
|
|
79
|
+
notifications.clear()
|
|
80
|
+
})
|
|
81
|
+
}
|