@weibaohui/dsh-plugin-kit 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/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # @weibaohui/dsh-plugin-kit
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@weibaohui/dsh-plugin-kit)](https://www.npmjs.com/package/@weibaohui/dsh-plugin-kit)
4
+
5
+ # @weibaohui/dsh-plugin-kit
6
+
7
+ [![npm version](https://img.shields.io/npm/v/@weibaohui/dsh-plugin-kit)](https://www.npmjs.com/package/@weibaohui/dsh-plugin-kit)
8
+
9
+ dsh 插件共享工具箱。把系列插件中重复的「AI 动作按钮」能力收拢为一处:
10
+
11
+ - **宿主**:`createShareRunJob` —— 把提示词交给真实 agent 会话执行(进程内 agents 服务优先流式,缺失降级 headless spawn;30 分钟超时 + 输出截断),任务状态由调用方轮询
12
+ - **client**:`PluginKit.ActionShareDialog` —— ntd ActionButton 同款交互(可编辑提示词 + 参数预览 + 复制 + 执行 + 实时输出),全内联样式,消费者无需自带 CSS
13
+
14
+ ## 消费方式
15
+
16
+ ```jsonc
17
+ // 插件 package.json
18
+ "dependencies": { "@weibaohui/dsh-plugin-kit": "github:weibaohui/dsh-plugin-kit" }
19
+ ```
20
+
21
+ `dsh plugin add` 安装插件时作为传递依赖一次性装上。
22
+
23
+ ### 宿主
24
+
25
+ ```js
26
+ const { createShareRunJob } = require('@weibaohui/dsh-plugin-kit')
27
+ const job = createShareRunJob({ binary: 'dsh', prompt, dir, jobs: shareRunJobs, logger: ctx.logger, services: shareServices })
28
+ ```
29
+
30
+ ### client(构建期内联)
31
+
32
+ `client/index.js` 顶部声明,构建脚本把 `client/source.js` 内联进 bundle:
33
+
34
+ ```js
35
+ const ShareDialog = PluginKit.makeActionShareDialog(__React)
36
+ // h(ShareDialog, { title, hint, rows, initialPrompt, run, poll, labels, onClose })
37
+ ```
38
+
39
+ ## 设计决策
40
+
41
+ - **构建期内联而非运行时插件**:kit 不进 profile 的 bundle 图,无降级逻辑;消费者构建时把 client 源码打进自己的 bundle,`dsh plugin add` 时宿主代码经 npm 传递依赖装齐——一次性装上,无挖坑
42
+ - **协议无关**:run/poll 以函数注入,提示词模板归各插件自有(业务层),kit 只收骨架
@@ -0,0 +1,102 @@
1
+ /**
2
+ * @weibaohui/dsh-plugin-kit — client source(由消费者构建脚本内联进 bundle,
3
+ * 不经 loader 运行时加载)。对外暴露 PluginKit:
4
+ *
5
+ * PluginKit.substituteParams(template, params) — {{key}} 模板插值
6
+ * PluginKit.makeActionShareDialog(React, opts) — 返回 ActionShareDialog 组件
7
+ *
8
+ * ActionShareDialog props:
9
+ * title / hint / rows: [[label, value], ...] / initialPrompt
10
+ * run: async (prompt) => { jobId } — 发起执行
11
+ * poll: async (jobId) => { status, output, code }
12
+ * labels: { copy, copied, run, running, done, failed, outputLabel, close }
13
+ * onClose
14
+ *
15
+ * 全部样式内联(主题 token + 回退值),消费者无需自带 CSS。
16
+ */
17
+ var PluginKit = (function () {
18
+ function substituteParams(template, params) {
19
+ var out = String(template || '')
20
+ for (var key in (params || {})) out = out.split('{{' + key + '}}').join(String(params[key]))
21
+ return out
22
+ }
23
+
24
+ function makeActionShareDialog(React, options) {
25
+ options = options || {}
26
+ var h = React.createElement
27
+ var useState = React.useState
28
+ var useEffect = React.useEffect
29
+ var doFetch = options.fetch || (typeof fetch !== 'undefined' ? fetch : null)
30
+ var inputStyle = { width: '100%', minHeight: 190, resize: 'vertical', fontFamily: 'var(--dsw-font-family)', lineHeight: 1.6, fontSize: 12, background: 'var(--dsw-alias-bg-layer-2,transparent)', color: 'inherit', border: '1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3))', borderRadius: '8px', padding: '10px', boxSizing: 'border-box' }
31
+ var btnStyle = { background: 'transparent', color: 'inherit', border: '1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3))', borderRadius: '8px', padding: '5px 12px', fontSize: 13, cursor: 'pointer', font: 'inherit' }
32
+ var primaryStyle = Object.assign({}, btnStyle, { background: 'var(--dsw-alias-brand-primary,#4a7dff)', borderColor: 'var(--dsw-alias-brand-primary,#4a7dff)', color: '#fff' })
33
+
34
+ return function ActionShareDialog(props) {
35
+ var title = props.title
36
+ var hint = props.hint
37
+ var labels = props.labels || {}
38
+ var _p = useState(props.initialPrompt || '')
39
+ var prompt = _p[0]; var setPrompt = _p[1]
40
+ var _j = useState(null)
41
+ var job = _j[0]; var setJob = _j[1]
42
+ var _b = useState(false)
43
+ var busy = _b[0]; var setBusy = _b[1]
44
+ var _c = useState(false)
45
+ var copied = _c[0]; var setCopied = _c[1]
46
+ var _e = useState('')
47
+ var error = _e[0]; var setError = _e[1]
48
+ var _d = useState(false)
49
+ var dirty = _d[0]; var setDirty = _d[1]
50
+
51
+ // initialPrompt 异步到位(如宿主先要下发真实路径)时跟随刷新;用户编辑过则不打断
52
+ useEffect(function () {
53
+ if (!dirty) setPrompt(props.initialPrompt || '')
54
+ }, [props.initialPrompt])
55
+
56
+ useEffect(function () {
57
+ if (job === null || job.status !== 'running' || typeof props.poll !== 'function') return
58
+ var timer = setInterval(function () {
59
+ props.poll(job.jobId).then(function (d) {
60
+ setJob({ jobId: job.jobId, status: d.status, output: d.output || '', code: d.code !== undefined ? d.code : null, sessionId: d.sessionId })
61
+ }).catch(function () {})
62
+ }, 1500)
63
+ return function () { clearInterval(timer) }
64
+ }, [job !== null && job.jobId])
65
+
66
+ var doRun = function () {
67
+ if (typeof props.run !== 'function') return
68
+ setBusy(true); setError('')
69
+ props.run(prompt).then(function (r) {
70
+ setJob({ jobId: r.jobId, status: 'running', output: '', code: null })
71
+ }).catch(function (e) { setError(String(e && e.message)) }).finally(function () { setBusy(false) })
72
+ }
73
+ var copy = function () {
74
+ if (typeof navigator !== 'undefined' && navigator.clipboard && navigator.clipboard.writeText) {
75
+ navigator.clipboard.writeText(prompt).then(function () { setCopied(true); setTimeout(function () { setCopied(false) }, 1500) }).catch(function () {})
76
+ }
77
+ }
78
+ var statusText = job === null ? '' : job.status === 'running' ? (labels.running || 'running') : job.status === 'done' ? (labels.done || 'done') : (labels.failed || 'failed') + (job.code != null ? ' (' + job.code + ')' : '')
79
+
80
+ return h('div', { onClick: function (e) { if (e.target === e.currentTarget && props.onClose) props.onClose() }, style: { position: 'fixed', inset: 0, zIndex: 2147483000, background: 'rgba(0,0,0,.45)', display: 'flex', alignItems: 'center', justifyContent: 'center' } },
81
+ h('div', { style: { width: 'min(640px,92vw)', maxHeight: '86vh', overflow: 'auto', background: 'var(--dsw-alias-bg-layer-1,#fff)', border: '1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3))', borderRadius: '16px', padding: '20px', display: 'flex', flexDirection: 'column', gap: 12, color: 'var(--dsw-alias-label-primary,inherit)', font: 'var(--dsw-font-family,inherit)' } },
82
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 10 } },
83
+ h('div', { style: { fontSize: 17, fontWeight: 600 } }, title || ''),
84
+ h('button', { onClick: props.onClose, style: Object.assign({}, btnStyle, { marginLeft: 'auto', width: 28, height: 28, padding: 0, borderRadius: 28 }) }, '✕')),
85
+ hint ? h('div', { style: { fontSize: 12, opacity: .7 } }, hint) : null,
86
+ (props.rows || []).length > 0 ? h('div', { style: { display: 'flex', flexDirection: 'column', gap: 4, fontSize: 13 } },
87
+ props.rows.map(function (r, i) {
88
+ return r[1] ? h('div', { key: i }, h('b', null, r[0] + ':'), h('span', null, r[1])) : null
89
+ })) : null,
90
+ h('textarea', { value: prompt, onChange: function (e) { setDirty(true); setPrompt(e.target.value) }, spellCheck: false, style: inputStyle }),
91
+ error !== '' ? h('div', { style: { fontSize: 12, color: 'var(--dsw-alias-state-error,#c75050)' } }, error) : null,
92
+ job !== null ? h('div', null,
93
+ h('div', { style: { fontSize: 12, opacity: .7, margin: '4px 0' } }, (labels.outputLabel || 'Output') + ' · ' + statusText),
94
+ h('pre', { style: { maxHeight: 220, margin: 0, overflow: 'auto', whiteSpace: 'pre-wrap', fontSize: 12, background: 'var(--dsw-alias-bg-layer-2,transparent)', border: '1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.2))', borderRadius: '8px', padding: '8px' } }, job.output || '…')) : null,
95
+ h('div', { style: { display: 'flex', gap: 8 } },
96
+ h('button', { onClick: copy, style: btnStyle }, copied ? (labels.copied || 'Copied') : (labels.copy || 'Copy')),
97
+ h('button', { onClick: doRun, disabled: busy || (job !== null && job.status === 'running'), style: primaryStyle }, job !== null && job.status === 'running' ? (labels.running || 'Running…') : (labels.run || 'Run')))))
98
+ }
99
+ }
100
+
101
+ return { substituteParams: substituteParams, makeActionShareDialog: makeActionShareDialog }
102
+ })()
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@weibaohui/dsh-plugin-kit",
3
+ "version": "0.1.0",
4
+ "description": "dsh 插件共享工具箱:AI 动作分享抽屉(ntd ActionButton 同款交互)+ 宿主任务执行器,供系列插件复用",
5
+ "license": "MIT",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./client/source.js": "./client/source.js"
10
+ },
11
+ "files": [
12
+ "src",
13
+ "client",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "check": "node --check src/index.js && node --check client/source.js",
18
+ "test": "node --test test/*.test.mjs",
19
+ "prepublishOnly": "npm run check && npm test"
20
+ },
21
+ "engines": {
22
+ "node": ">=22.5"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/weibaohui/dsh-plugin-kit.git"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "keywords": [
32
+ "dsh",
33
+ "deepseek-harness",
34
+ "cordis",
35
+ "plugin",
36
+ "dsh-plugin",
37
+ "ui",
38
+ "kit"
39
+ ],
40
+ "homepage": "https://github.com/weibaohui/dsh-plugin-kit#readme",
41
+ "bugs": {
42
+ "url": "https://github.com/weibaohui/dsh-plugin-kit/issues"
43
+ }
44
+ }
package/src/index.js ADDED
@@ -0,0 +1,101 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * @weibaohui/dsh-plugin-kit — host half.
5
+ *
6
+ * createShareRunJob:把一段提示词交给一个真实 agent 会话执行(分享/提交类
7
+ * 动作的通用执行器)。优先使用同进程 agents 服务(输出流式可见);服务缺失
8
+ * 的组合降级为 headless spawn(`<binary> --profile headless <prompt>`,cwd
9
+ * 为目标目录)。30 分钟超时 + 输出尾部截断,job 状态由调用方轮询。
10
+ */
11
+
12
+ const { spawn } = require('node:child_process')
13
+
14
+ const SHARE_RUN_TIMEOUT_MS = 30 * 60 * 1000
15
+ const SHARE_RUN_OUTPUT_CAP = 256 * 1024
16
+
17
+ function createShareRunJob({ binary = 'dsh', prompt, dir, jobs, logger, services }) {
18
+ const id = 'sr' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8)
19
+ const job = { id, status: 'running', startedAt: new Date().toISOString(), dir, promptHead: String(prompt || '').slice(0, 80), output: '', code: null }
20
+ jobs.set(id, job)
21
+ if (services && services.agents && services.agentDefaultModel) {
22
+ runShareInProcess(services, { prompt, dir, job, logger })
23
+ .catch(e => { job.status = 'error'; job.output = (job.output + '\n' + String(e && e.message)).slice(-SHARE_RUN_OUTPUT_CAP) })
24
+ return job
25
+ }
26
+ let child
27
+ try {
28
+ child = spawn(binary, ['--profile', 'headless', prompt], { cwd: dir })
29
+ } catch (e) {
30
+ job.status = 'error'
31
+ job.output = String(e && e.message)
32
+ return job
33
+ }
34
+ const append = (chunk) => {
35
+ job.output = (job.output + String(chunk)).slice(-SHARE_RUN_OUTPUT_CAP)
36
+ }
37
+ child.stdout && child.stdout.on('data', append)
38
+ child.stderr && child.stderr.on('data', append)
39
+ const timer = setTimeout(() => {
40
+ try { child.kill('SIGKILL') } catch {}
41
+ job.status = 'error'
42
+ job.output += '\n[killed: timeout]'
43
+ }, SHARE_RUN_TIMEOUT_MS)
44
+ if (typeof timer.unref === 'function') timer.unref()
45
+ child.on('error', (e) => { clearTimeout(timer); job.status = 'error'; append('\n' + String(e && e.message)) })
46
+ child.on('close', (code) => {
47
+ clearTimeout(timer)
48
+ job.status = code === 0 ? 'done' : 'failed'
49
+ job.code = code
50
+ logger.info && logger.info(`dsh-plugin-kit: share run ${id} ${job.status} (code ${code})`)
51
+ })
52
+ return job
53
+ }
54
+
55
+ async function runShareInProcess(services, { prompt, dir, job, logger }) {
56
+ const { randomUUID } = await import('node:crypto')
57
+ const selection = services.agentDefaultModel.currentSelection()
58
+ const sessionId = 'session-' + randomUUID()
59
+ job.sessionId = sessionId
60
+ const { agent } = await services.agents.create({
61
+ sessionId,
62
+ // 标准预设:不带显式选择会继承用户默认(如 Solo Thinking 只有 thinking/notify
63
+ // 工具),读文件/调 API 都做不了
64
+ meta: { cwd: dir, agentPreset: 'standard' },
65
+ agentOptions: { provider: selection.provider, model: selection.model },
66
+ })
67
+ await agent.whenIdle()
68
+ const firstSeq = agent.session.seq
69
+ const seen = new Set()
70
+ const liveLine = (text) => {
71
+ job.output = (job.output + text).slice(-SHARE_RUN_OUTPUT_CAP)
72
+ }
73
+ const pump = () => {
74
+ for (const ev of agent.session.events) {
75
+ if (ev.seq < firstSeq || seen.has(ev.seq)) continue
76
+ seen.add(ev.seq)
77
+ const d = ev.data || {}
78
+ if (ev.type === 'assistant/chunk' && d.chunk && d.chunk.type === 'text' && d.chunk.text) {
79
+ liveLine(d.chunk.text)
80
+ } else if (ev.type === 'tool/call') {
81
+ liveLine('\n[tool] ' + d.name + ' ')
82
+ } else if (ev.type === 'assistant/message') {
83
+ liveLine('\n')
84
+ }
85
+ }
86
+ }
87
+ const timer = setInterval(pump, 300)
88
+ if (typeof timer.unref === 'function') timer.unref()
89
+ try {
90
+ agent.followup({ content: [{ type: 'text', text: prompt }], source: { kind: 'user' } })
91
+ await agent.whenIdle()
92
+ } finally {
93
+ clearInterval(timer)
94
+ pump()
95
+ }
96
+ try { await services.sessions.flush(agent.session) } catch {}
97
+ job.status = 'done'
98
+ logger.info && logger.info(`dsh-plugin-kit: share run ${job.id} done`)
99
+ }
100
+
101
+ module.exports = { createShareRunJob, runShareInProcess, SHARE_RUN_TIMEOUT_MS, SHARE_RUN_OUTPUT_CAP }