@weibaohui/skills-management 0.2.0 → 0.4.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/client/bundle.js CHANGED
@@ -8,6 +8,113 @@ window.__ModuleLoader__.load({
8
8
  var exports = module.exports
9
9
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" })
10
10
  var React = require("react")
11
+ /**
12
+ * @weibaohui/dsh-plugin-kit — client source(由消费者构建脚本内联进 bundle,
13
+ * 不经 loader 运行时加载)。对外暴露 PluginKit:
14
+ *
15
+ * PluginKit.substituteParams(template, params) — {{key}} 模板插值
16
+ * PluginKit.makeActionShareDialog(React, opts) — 返回 ActionShareDialog 组件
17
+ *
18
+ * ActionShareDialog props:
19
+ * title / hint / rows: [[label, value], ...] / initialPrompt
20
+ * run: async (prompt) => { jobId } — 发起执行
21
+ * poll: async (jobId) => { status, output, code }
22
+ * labels: { copy, copied, run, running, done, failed, outputLabel, openSession, close }
23
+ * onOpenSession: (sessionId) => void — 可选;job 出现 sessionId 时渲染「打开会话」
24
+ * onClose
25
+ *
26
+ * 全部样式内联(主题 token + 回退值),消费者无需自带 CSS。
27
+ */
28
+ var PluginKit = (function () {
29
+ function substituteParams(template, params) {
30
+ var out = String(template || '')
31
+ for (var key in (params || {})) out = out.split('{{' + key + '}}').join(String(params[key]))
32
+ return out
33
+ }
34
+
35
+ function makeActionShareDialog(React, options) {
36
+ options = options || {}
37
+ var h = React.createElement
38
+ var useState = React.useState
39
+ var useEffect = React.useEffect
40
+ var doFetch = options.fetch || (typeof fetch !== 'undefined' ? fetch : null)
41
+ 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' }
42
+ 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' }
43
+ var primaryStyle = Object.assign({}, btnStyle, { background: 'var(--dsw-alias-brand-primary,#4a7dff)', borderColor: 'var(--dsw-alias-brand-primary,#4a7dff)', color: '#fff' })
44
+
45
+ return function ActionShareDialog(props) {
46
+ var title = props.title
47
+ var hint = props.hint
48
+ var labels = props.labels || {}
49
+ var _p = useState(props.initialPrompt || '')
50
+ var prompt = _p[0]; var setPrompt = _p[1]
51
+ var _j = useState(null)
52
+ var job = _j[0]; var setJob = _j[1]
53
+ var _b = useState(false)
54
+ var busy = _b[0]; var setBusy = _b[1]
55
+ var _c = useState(false)
56
+ var copied = _c[0]; var setCopied = _c[1]
57
+ var _e = useState('')
58
+ var error = _e[0]; var setError = _e[1]
59
+ var _d = useState(false)
60
+ var dirty = _d[0]; var setDirty = _d[1]
61
+
62
+ // initialPrompt 异步到位(如宿主先要下发真实路径)时跟随刷新;用户编辑过则不打断
63
+ useEffect(function () {
64
+ if (!dirty) setPrompt(props.initialPrompt || '')
65
+ }, [props.initialPrompt])
66
+
67
+ useEffect(function () {
68
+ if (job === null || job.status !== 'running' || typeof props.poll !== 'function') return
69
+ var timer = setInterval(function () {
70
+ props.poll(job.jobId).then(function (d) {
71
+ setJob({ jobId: job.jobId, status: d.status, output: d.output || '', code: d.code !== undefined ? d.code : null, sessionId: d.sessionId })
72
+ }).catch(function () {})
73
+ }, 1500)
74
+ return function () { clearInterval(timer) }
75
+ }, [job !== null && job.jobId])
76
+
77
+ var doRun = function () {
78
+ if (typeof props.run !== 'function') return
79
+ setBusy(true); setError('')
80
+ props.run(prompt).then(function (r) {
81
+ setJob({ jobId: r.jobId, status: 'running', output: '', code: null })
82
+ }).catch(function (e) { setError(String(e && e.message)) }).finally(function () { setBusy(false) })
83
+ }
84
+ var canOpenSession = typeof props.onOpenSession === 'function' && job !== null && job.sessionId
85
+ var openSession = function () { props.onOpenSession(job.sessionId) }
86
+ var copy = function () {
87
+ if (typeof navigator !== 'undefined' && navigator.clipboard && navigator.clipboard.writeText) {
88
+ navigator.clipboard.writeText(prompt).then(function () { setCopied(true); setTimeout(function () { setCopied(false) }, 1500) }).catch(function () {})
89
+ }
90
+ }
91
+ var statusText = job === null ? '' : job.status === 'running' ? (labels.running || 'running') : job.status === 'done' ? (labels.done || 'done') : (labels.failed || 'failed') + (job.code != null ? ' (' + job.code + ')' : '')
92
+
93
+ 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' } },
94
+ 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)' } },
95
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 10 } },
96
+ h('div', { style: { fontSize: 17, fontWeight: 600 } }, title || ''),
97
+ h('button', { onClick: props.onClose, style: Object.assign({}, btnStyle, { marginLeft: 'auto', width: 28, height: 28, padding: 0, borderRadius: 28 }) }, '✕')),
98
+ hint ? h('div', { style: { fontSize: 12, opacity: .7 } }, hint) : null,
99
+ (props.rows || []).length > 0 ? h('div', { style: { display: 'flex', flexDirection: 'column', gap: 4, fontSize: 13 } },
100
+ props.rows.map(function (r, i) {
101
+ return r[1] ? h('div', { key: i }, h('b', null, r[0] + ':'), h('span', null, r[1])) : null
102
+ })) : null,
103
+ h('textarea', { value: prompt, onChange: function (e) { setDirty(true); setPrompt(e.target.value) }, spellCheck: false, style: inputStyle }),
104
+ error !== '' ? h('div', { style: { fontSize: 12, color: 'var(--dsw-alias-state-error,#c75050)' } }, error) : null,
105
+ job !== null ? h('div', null,
106
+ h('div', { style: { fontSize: 12, opacity: .7, margin: '4px 0' } }, (labels.outputLabel || 'Output') + ' · ' + statusText),
107
+ 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,
108
+ h('div', { style: { display: 'flex', gap: 8 } },
109
+ canOpenSession ? h('button', { onClick: openSession, style: btnStyle }, labels.openSession || 'Open chat') : null,
110
+ h('button', { onClick: copy, style: btnStyle }, copied ? (labels.copied || 'Copied') : (labels.copy || 'Copy')),
111
+ h('button', { onClick: doRun, disabled: busy || (job !== null && job.status === 'running'), style: primaryStyle }, job !== null && job.status === 'running' ? (labels.running || 'Running…') : (labels.run || 'Run')))))
112
+ }
113
+ }
114
+
115
+ return { substituteParams: substituteParams, makeActionShareDialog: makeActionShareDialog }
116
+ })()
117
+
11
118
  /**
12
119
  * dsh-plugin-skills-management - Browser half.
13
120
  *
@@ -467,24 +574,16 @@ window.__ModuleLoader__.load({
467
574
  // ── Pure helpers ────────────────────────────────────────────────────────
468
575
 
469
576
  /** ntd ActionButton 同款 {{key}} 替换:split/join 规避正则元字符 */
470
- function substituteParams(template, params) {
471
- let out = template
472
- for (const [key, value] of Object.entries(params)) {
473
- out = out.split(`{{${key}}}`).join(String(value))
474
- }
475
- return out
476
- }
477
-
478
577
  const SHARE_PROMPT_ZH = [
479
578
  '请把本地技能「{{skillName}}」{{version}}打包提交到 GitCode 官方仓库,作为一个 PR 供维护者审核。',
480
579
  '',
481
580
  '## 关键信息',
482
581
  '- 技能目录:{{resourceDir}}(~ 表示当前用户家目录,执行前先展开为绝对路径)',
483
582
  '- 官方仓库:weibaohui/ntd-resource(GitCode,API base = https://api.gitcode.com)',
484
- '- PAT 位置:~/.dsh/settings.yaml 中 skills-management.market 段的 token 字段(由技能市场设置面板保存)。',
583
+ '- PAT 位置:{{settingsFile}} 中 skills-management.market 段的 token 字段(由技能市场设置面板保存)。',
485
584
  '',
486
585
  '## 执行步骤(严格按顺序)',
487
- '1. 读取 PAT:读取 ~/.dsh/settings.yaml,定位 skills-management.market 段下的 token 字段。token 是敏感凭据,读取后不要把明文打印到输出、日志或最终结果里。',
586
+ '1. 读取 PAT:读取 {{settingsFile}},定位 skills-management.market 段下的 token 字段。token 是敏感凭据,读取后不要把明文打印到输出、日志或最终结果里。',
488
587
  '2. 展开技能目录为绝对路径,遍历该目录,收集每个文件的「相对该目录的路径」与内容;跳过 .downloaded_at、.clawhub、.git 三类同步元数据。',
489
588
  '3. 把第 1 步读到的 token 作为 HTTP 认证令牌,附加到下面每个 GitCode API 请求的认证头里(bearer 认证方式),不要写成占位符:',
490
589
  ' a. 验证用户:`GET https://api.gitcode.com/api/v5/user`,拿到返回的 login 字段——这是 token 真实所属的账号,后续所有 URL 里的 {owner} 一律用它。',
@@ -871,60 +970,31 @@ window.__ModuleLoader__.load({
871
970
  return h('div', { className: 'sk-toast' }, text)
872
971
  }
873
972
 
874
- /** ntd ActionButton 同款分享抽屉:可编辑提示词 + 参数预览 + 复制到会话执行。 */
875
- function ShareSkillDialog({ t, params, onClose, onToast }) {
876
- const [prompt, setPrompt] = useState(substituteParams(SHARE_PROMPT_ZH, params))
877
- const [hasToken, setHasToken] = useState(null)
878
- const [job, setJob] = useState(null) // {jobId,status,output,code}
879
- const [busy, setBusy] = useState(false)
973
+ let _actionShareDialog = null
974
+ function getActionShareDialog() {
975
+ if (_actionShareDialog === null) _actionShareDialog = PluginKit.makeActionShareDialog(__React)
976
+ return _actionShareDialog
977
+ }
978
+
979
+ /** 分享抽屉:壳交给 PluginKit(ActionShareDialog),本插件只负责
980
+ * hasToken 提示、settingsFile 插值与 run/poll 的 API 映射。 */
981
+ function ShareSkillDialog({ t, params, onClose }) {
982
+ const _st = useState(null)
983
+ const status = _st[0]; const setStatus = _st[1]
880
984
  useEffect(() => {
881
- getJson(API + '/market/status').then(d => setHasToken(d.hasToken === true)).catch(() => setHasToken(false))
985
+ getJson(API + '/market/status').then((d) => setStatus(d)).catch(() => setStatus({ hasToken: false }))
882
986
  }, [])
883
- // 轮询执行输出,直到关闭/结束
884
- useEffect(() => {
885
- if (job === null || job.status !== 'running') return
886
- const timer = setInterval(() => {
887
- getJson(API + '/share/run?id=' + encodeURIComponent(job.jobId))
888
- .then(d => setJob(prev => prev && { ...prev, status: d.status, output: d.output || '', code: d.code, sessionId: d.sessionId || prev.sessionId }))
889
- .catch(() => {})
890
- }, 2000)
891
- if (typeof timer.unref === 'function') timer.unref()
892
- return () => clearInterval(timer)
893
- }, [job && job.status])
894
- const doRun = async () => {
895
- setBusy(true)
896
- try {
897
- const r = await fetch(API + '/share/run', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt, dir: params.resourceDir }) })
898
- const d = await r.json().catch(() => ({}))
899
- if (!r.ok) throw new Error(d.error || 'HTTP ' + r.status)
900
- setJob({ jobId: d.jobId, status: 'running', output: '', code: null })
901
- } catch (e) { onToast(t('runFailed') + ': ' + e.message) } finally { setBusy(false) }
902
- }
903
- const row = (label, value) => h('div', { style: { display: 'flex', justifyContent: 'space-between', gap: 12, padding: '3px 0' } },
904
- h('span', { className: 'sk-dir' }, label), h('span', { className: 'sk-hint', style: { wordBreak: 'break-all', textAlign: 'right' } }, value))
905
- const copy = () => {
906
- navigator.clipboard.writeText(prompt).then(() => onToast(t('promptCopied'))).catch(() => {})
907
- }
908
- return h(SkDialog, { title: t('shareTitle'), onClose, wide: true },
909
- h('div', { style: { display: 'flex', flexDirection: 'column', gap: 10, minWidth: 380 } },
910
- h('div', { className: 'sk-hint' }, hasToken === false ? t('shareHintPatMissing') : t('shareHint')),
911
- h('div', null,
912
- row(t('shareParamName'), params.skillName),
913
- row(t('shareParamVersion'), params.version || '-'),
914
- row(t('shareParamDir'), params.resourceDir),
915
- row(t('shareParamRemote'), params.remotePath)),
916
- h('textarea', { className: 'sk-input', value: prompt, onChange: e => setPrompt(e.target.value),
917
- style: { width: '100%', minHeight: 190, resize: 'vertical', fontFamily: 'var(--dsw-font-family)', lineHeight: 1.6 } }),
918
- job !== null && h('div', null,
919
- h('div', { className: 'sk-dir', style: { margin: '4px 0' } },
920
- t('outputLabel') + ' · ' + (job.status === 'running' ? t('running') : job.status === 'done' ? t('runDone') : t('runFailed') + (job.code != null ? ' (' + job.code + ')' : ''))),
921
- h('pre', { className: 'sk-preview', style: { maxHeight: 220, margin: 0, whiteSpace: 'pre-wrap', fontSize: 12 } },
922
- job.output || '…')),
923
- h('div', { className: 'sk-dlg-foot', style: { marginTop: 0 } },
924
- h(ButtonLite, { onClick: copy }, t('copyPrompt')),
925
- job !== null && job.sessionId && sessionsSvc() && h(ButtonLite, { onClick: () => { if (openRunSession(job.sessionId)) onClose() } }, t('openChat')),
926
- h(ButtonLite, { primary: true, disabled: busy || (job !== null && job.status === 'running'), onClick: doRun },
927
- job !== null && job.status === 'running' ? t('running') : t('runBtn')))))
987
+ const hasToken = status ? status.hasToken === true : null
988
+ const hint = hasToken === false ? t('shareHintPatMissing') : t('shareHint')
989
+ const initialPrompt = PluginKit.substituteParams(SHARE_PROMPT_ZH, { ...params, settingsFile: (status && status.settingsFile) || '' })
990
+ return h(getActionShareDialog(), {
991
+ title: t('shareTitle'), hint, initialPrompt,
992
+ rows: [[t('shareParamName'), params.skillName], [t('shareParamVersion'), params.version || '-'], [t('shareParamDir'), params.resourceDir]],
993
+ labels: { copy: t('copyPrompt'), copied: t('copied'), run: t('runBtn'), running: t('running'), done: t('runDone'), failed: t('runFailed'), outputLabel: t('outputLabel') },
994
+ run: (prompt) => fetch(API + '/share/run', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt, dir: params.resourceDir }) }).then((r) => r.json()),
995
+ poll: (id) => fetch(API + '/share/run?id=' + encodeURIComponent(id)).then((r) => r.json()),
996
+ onClose,
997
+ })
928
998
  }
929
999
 
930
1000
  /** Market sync settings: status card, sync action, editable url/branch. */
package/client/index.js CHANGED
@@ -457,24 +457,16 @@ const EN = {
457
457
  // ── Pure helpers ────────────────────────────────────────────────────────
458
458
 
459
459
  /** ntd ActionButton 同款 {{key}} 替换:split/join 规避正则元字符 */
460
- function substituteParams(template, params) {
461
- let out = template
462
- for (const [key, value] of Object.entries(params)) {
463
- out = out.split(`{{${key}}}`).join(String(value))
464
- }
465
- return out
466
- }
467
-
468
460
  const SHARE_PROMPT_ZH = [
469
461
  '请把本地技能「{{skillName}}」{{version}}打包提交到 GitCode 官方仓库,作为一个 PR 供维护者审核。',
470
462
  '',
471
463
  '## 关键信息',
472
464
  '- 技能目录:{{resourceDir}}(~ 表示当前用户家目录,执行前先展开为绝对路径)',
473
465
  '- 官方仓库:weibaohui/ntd-resource(GitCode,API base = https://api.gitcode.com)',
474
- '- PAT 位置:~/.dsh/settings.yaml 中 skills-management.market 段的 token 字段(由技能市场设置面板保存)。',
466
+ '- PAT 位置:{{settingsFile}} 中 skills-management.market 段的 token 字段(由技能市场设置面板保存)。',
475
467
  '',
476
468
  '## 执行步骤(严格按顺序)',
477
- '1. 读取 PAT:读取 ~/.dsh/settings.yaml,定位 skills-management.market 段下的 token 字段。token 是敏感凭据,读取后不要把明文打印到输出、日志或最终结果里。',
469
+ '1. 读取 PAT:读取 {{settingsFile}},定位 skills-management.market 段下的 token 字段。token 是敏感凭据,读取后不要把明文打印到输出、日志或最终结果里。',
478
470
  '2. 展开技能目录为绝对路径,遍历该目录,收集每个文件的「相对该目录的路径」与内容;跳过 .downloaded_at、.clawhub、.git 三类同步元数据。',
479
471
  '3. 把第 1 步读到的 token 作为 HTTP 认证令牌,附加到下面每个 GitCode API 请求的认证头里(bearer 认证方式),不要写成占位符:',
480
472
  ' a. 验证用户:`GET https://api.gitcode.com/api/v5/user`,拿到返回的 login 字段——这是 token 真实所属的账号,后续所有 URL 里的 {owner} 一律用它。',
@@ -861,60 +853,31 @@ function InToast({ text }) {
861
853
  return h('div', { className: 'sk-toast' }, text)
862
854
  }
863
855
 
864
- /** ntd ActionButton 同款分享抽屉:可编辑提示词 + 参数预览 + 复制到会话执行。 */
865
- function ShareSkillDialog({ t, params, onClose, onToast }) {
866
- const [prompt, setPrompt] = useState(substituteParams(SHARE_PROMPT_ZH, params))
867
- const [hasToken, setHasToken] = useState(null)
868
- const [job, setJob] = useState(null) // {jobId,status,output,code}
869
- const [busy, setBusy] = useState(false)
856
+ let _actionShareDialog = null
857
+ function getActionShareDialog() {
858
+ if (_actionShareDialog === null) _actionShareDialog = PluginKit.makeActionShareDialog(__React)
859
+ return _actionShareDialog
860
+ }
861
+
862
+ /** 分享抽屉:壳交给 PluginKit(ActionShareDialog),本插件只负责
863
+ * hasToken 提示、settingsFile 插值与 run/poll 的 API 映射。 */
864
+ function ShareSkillDialog({ t, params, onClose }) {
865
+ const _st = useState(null)
866
+ const status = _st[0]; const setStatus = _st[1]
870
867
  useEffect(() => {
871
- getJson(API + '/market/status').then(d => setHasToken(d.hasToken === true)).catch(() => setHasToken(false))
868
+ getJson(API + '/market/status').then((d) => setStatus(d)).catch(() => setStatus({ hasToken: false }))
872
869
  }, [])
873
- // 轮询执行输出,直到关闭/结束
874
- useEffect(() => {
875
- if (job === null || job.status !== 'running') return
876
- const timer = setInterval(() => {
877
- getJson(API + '/share/run?id=' + encodeURIComponent(job.jobId))
878
- .then(d => setJob(prev => prev && { ...prev, status: d.status, output: d.output || '', code: d.code, sessionId: d.sessionId || prev.sessionId }))
879
- .catch(() => {})
880
- }, 2000)
881
- if (typeof timer.unref === 'function') timer.unref()
882
- return () => clearInterval(timer)
883
- }, [job && job.status])
884
- const doRun = async () => {
885
- setBusy(true)
886
- try {
887
- const r = await fetch(API + '/share/run', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt, dir: params.resourceDir }) })
888
- const d = await r.json().catch(() => ({}))
889
- if (!r.ok) throw new Error(d.error || 'HTTP ' + r.status)
890
- setJob({ jobId: d.jobId, status: 'running', output: '', code: null })
891
- } catch (e) { onToast(t('runFailed') + ': ' + e.message) } finally { setBusy(false) }
892
- }
893
- const row = (label, value) => h('div', { style: { display: 'flex', justifyContent: 'space-between', gap: 12, padding: '3px 0' } },
894
- h('span', { className: 'sk-dir' }, label), h('span', { className: 'sk-hint', style: { wordBreak: 'break-all', textAlign: 'right' } }, value))
895
- const copy = () => {
896
- navigator.clipboard.writeText(prompt).then(() => onToast(t('promptCopied'))).catch(() => {})
897
- }
898
- return h(SkDialog, { title: t('shareTitle'), onClose, wide: true },
899
- h('div', { style: { display: 'flex', flexDirection: 'column', gap: 10, minWidth: 380 } },
900
- h('div', { className: 'sk-hint' }, hasToken === false ? t('shareHintPatMissing') : t('shareHint')),
901
- h('div', null,
902
- row(t('shareParamName'), params.skillName),
903
- row(t('shareParamVersion'), params.version || '-'),
904
- row(t('shareParamDir'), params.resourceDir),
905
- row(t('shareParamRemote'), params.remotePath)),
906
- h('textarea', { className: 'sk-input', value: prompt, onChange: e => setPrompt(e.target.value),
907
- style: { width: '100%', minHeight: 190, resize: 'vertical', fontFamily: 'var(--dsw-font-family)', lineHeight: 1.6 } }),
908
- job !== null && h('div', null,
909
- h('div', { className: 'sk-dir', style: { margin: '4px 0' } },
910
- t('outputLabel') + ' · ' + (job.status === 'running' ? t('running') : job.status === 'done' ? t('runDone') : t('runFailed') + (job.code != null ? ' (' + job.code + ')' : ''))),
911
- h('pre', { className: 'sk-preview', style: { maxHeight: 220, margin: 0, whiteSpace: 'pre-wrap', fontSize: 12 } },
912
- job.output || '…')),
913
- h('div', { className: 'sk-dlg-foot', style: { marginTop: 0 } },
914
- h(ButtonLite, { onClick: copy }, t('copyPrompt')),
915
- job !== null && job.sessionId && sessionsSvc() && h(ButtonLite, { onClick: () => { if (openRunSession(job.sessionId)) onClose() } }, t('openChat')),
916
- h(ButtonLite, { primary: true, disabled: busy || (job !== null && job.status === 'running'), onClick: doRun },
917
- job !== null && job.status === 'running' ? t('running') : t('runBtn')))))
870
+ const hasToken = status ? status.hasToken === true : null
871
+ const hint = hasToken === false ? t('shareHintPatMissing') : t('shareHint')
872
+ const initialPrompt = PluginKit.substituteParams(SHARE_PROMPT_ZH, { ...params, settingsFile: (status && status.settingsFile) || '' })
873
+ return h(getActionShareDialog(), {
874
+ title: t('shareTitle'), hint, initialPrompt,
875
+ rows: [[t('shareParamName'), params.skillName], [t('shareParamVersion'), params.version || '-'], [t('shareParamDir'), params.resourceDir]],
876
+ labels: { copy: t('copyPrompt'), copied: t('copied'), run: t('runBtn'), running: t('running'), done: t('runDone'), failed: t('runFailed'), outputLabel: t('outputLabel') },
877
+ run: (prompt) => fetch(API + '/share/run', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt, dir: params.resourceDir }) }).then((r) => r.json()),
878
+ poll: (id) => fetch(API + '/share/run?id=' + encodeURIComponent(id)).then((r) => r.json()),
879
+ onClose,
880
+ })
918
881
  }
919
882
 
920
883
  /** Market sync settings: status card, sync action, editable url/branch. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weibaohui/skills-management",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "dsh 插件 · 技能市场:一个页面管理本机所有 coding agent 的技能(Claude、ZCode、Codex 等十余个执行器),一键收编进 DSH 用户库供 skill 工具调用;内置 6600+ ntd 技能市场可浏览搜索安装,支持模型可见性治理与市场每日自动同步。",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -43,6 +43,7 @@
43
43
  "node": ">=22.5"
44
44
  },
45
45
  "dependencies": {
46
+ "@weibaohui/dsh-plugin-kit": "^0.2.0",
46
47
  "yaml": "^2.9.0"
47
48
  },
48
49
  "devDependencies": {
package/src/index.js CHANGED
@@ -14,7 +14,8 @@
14
14
  */
15
15
 
16
16
  const { createReadStream } = require('node:fs')
17
- const { execFile, spawn } = require('node:child_process')
17
+ const { execFile } = require('node:child_process')
18
+ const { createShareRunJob } = require('@weibaohui/dsh-plugin-kit')
18
19
  const { randomUUID } = require('node:crypto')
19
20
  const fsP = require('node:fs/promises')
20
21
  const { basename, join, relative, resolve, sep } = require('node:path')
@@ -477,99 +478,6 @@ function mergeMarketSync(config, overrides) {
477
478
  // (`dsh --profile headless "<task>"`, cwd = the skill directory — the
478
479
  // workspace, session and model loop are owned by that one-shot process). ──
479
480
 
480
- const SHARE_RUN_TIMEOUT_MS = 30 * 60 * 1000
481
- const SHARE_RUN_OUTPUT_CAP = 256 * 1024
482
-
483
- /** In-process run: drive the same Agent services the web app uses and
484
- * stream assistant/chunk tokens + tool calls into the job's output as they
485
- * happen (headless prints only the final message — no live channel there).
486
- * Mirrors packages/bundle/headless/src/index.ts run(). */
487
- async function runShareInProcess(services, { prompt, dir, job, logger }) {
488
- const selection = services.agentDefaultModel.currentSelection()
489
- const sessionId = 'session-' + randomUUID()
490
- job.sessionId = sessionId
491
- const { agent } = await services.agents.create({
492
- sessionId,
493
- // 标准预设:不带显式选择会继承用户默认(如 Solo Thinking 只有
494
- // thinking/notify 工具),读文件/调 API 都做不了
495
- meta: { cwd: dir, agentPreset: 'standard' },
496
- agentOptions: { provider: selection.provider, model: selection.model },
497
- })
498
- await agent.whenIdle()
499
- const firstSeq = agent.session.seq
500
- const seen = new Set()
501
- const liveLine = (text) => {
502
- job.output = (job.output + text).slice(-SHARE_RUN_OUTPUT_CAP)
503
- }
504
- const pump = () => {
505
- for (const ev of agent.session.events) {
506
- if (ev.seq < firstSeq || seen.has(ev.seq)) continue
507
- seen.add(ev.seq)
508
- const d = ev.data || {}
509
- if (ev.type === 'assistant/chunk' && d.chunk && d.chunk.type === 'text' && d.chunk.text) {
510
- liveLine(d.chunk.text)
511
- } else if (ev.type === 'tool/call') {
512
- liveLine('\n[tool] ' + d.name + ' ')
513
- } else if (ev.type === 'assistant/message') {
514
- liveLine('\n')
515
- }
516
- }
517
- }
518
- const timer = setInterval(pump, 300)
519
- if (typeof timer.unref === 'function') timer.unref()
520
- try {
521
- agent.followup({ content: [{ type: 'text', text: prompt }], source: { kind: 'user' } })
522
- await agent.whenIdle()
523
- } finally {
524
- clearInterval(timer)
525
- pump()
526
- }
527
- try { await services.sessions.flush(agent.session) } catch {}
528
- job.status = 'done'
529
- job.code = 0
530
- return job
531
- }
532
-
533
- function createShareRunJob({ binary, prompt, dir, jobs, logger, services }) {
534
- const id = 'sr' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8)
535
- const job = { id, status: 'running', startedAt: new Date().toISOString(), dir, promptHead: prompt.slice(0, 80), output: '', code: null }
536
- jobs.set(id, job)
537
- if (services && services.agents && services.agentDefaultModel) {
538
- runShareInProcess(services, { prompt, dir, job, logger })
539
- .catch(e => { job.status = 'error'; job.output = (job.output + '\n' + String(e && e.message)).slice(-SHARE_RUN_OUTPUT_CAP) })
540
- return job
541
- }
542
- let child
543
- try {
544
- child = spawn(binary, ['--profile', 'headless', prompt], { cwd: dir })
545
- } catch (e) {
546
- job.status = 'error'
547
- job.output = String(e && e.message)
548
- return job
549
- }
550
- const append = (chunk) => {
551
- job.output = (job.output + String(chunk)).slice(-SHARE_RUN_OUTPUT_CAP)
552
- }
553
- child.stdout && child.stdout.on('data', append)
554
- child.stderr && child.stderr.on('data', append)
555
- const timer = setTimeout(() => {
556
- try { child.kill('SIGKILL') } catch {}
557
- job.status = 'error'
558
- job.output += '\n[killed: timeout]'
559
- }, SHARE_RUN_TIMEOUT_MS)
560
- if (typeof timer.unref === 'function') timer.unref()
561
- child.on('error', (e) => { clearTimeout(timer); job.status = 'error'; append('\n' + String(e && e.message)) })
562
- child.on('close', (code) => {
563
- clearTimeout(timer)
564
- if (job.status === 'running') {
565
- job.status = code === 0 ? 'done' : 'error'
566
- job.code = code
567
- }
568
- logger.info && logger.info(`skills-management: share run ${id} ${job.status} (code ${code})`)
569
- })
570
- return job
571
- }
572
-
573
481
  function contentTypeFor(p) {
574
482
  const ext = p.slice(p.lastIndexOf('.') + 1).toLowerCase()
575
483
  const map = { md: 'text/markdown; charset=utf-8', txt: 'text/plain; charset=utf-8', json: 'application/json; charset=utf-8', js: 'text/javascript', mjs: 'text/javascript', ts: 'text/typescript', tsx: 'text/typescript', css: 'text/css', html: 'text/html', svg: 'image/svg+xml', png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp', yaml: 'text/yaml', yml: 'text/yaml' }
@@ -578,7 +486,7 @@ function contentTypeFor(p) {
578
486
 
579
487
  module.exports = {
580
488
  name: 'skills-management',
581
- inject: ['skills', 'webServer', 'settings'],
489
+ inject: ['skills', 'webServer', 'settings', 'agents', 'agentDefaultModel', 'sessions'],
582
490
  __internals: { extractFrontmatter, parseSkillMd, invocationPolicy, installDirName, EXECUTOR_DEFS },
583
491
 
584
492
  apply(ctx, config = {}) {
@@ -851,14 +759,9 @@ module.exports = {
851
759
  }, 'skills-management: market auto-sync')
852
760
 
853
761
  const shareRunJobs = new Map()
854
- // Same-process Agent services (the web app's own): when available the
855
- // share run streams live; absent compositions fall back to headless spawn.
856
- let shareServices = null
857
- try {
858
- if (ctx.inject && typeof ctx.inject === 'function') {
859
- ctx.inject(['agents', 'agentDefaultModel', 'sessions'], (svcs) => { shareServices = svcs })
860
- }
861
- } catch {}
762
+ // Same-process Agent services(静态注入:apply 时已就绪;动态 ctx.inject
763
+ // apply 内不触发是平台 gotcha)。
764
+ const shareServices = { agents: ctx.agents, agentDefaultModel: ctx.agentDefaultModel, sessions: ctx.sessions }
862
765
  let providerControl
863
766
  const invalidate = () => { if (providerControl !== undefined) providerControl.invalidate() }
864
767
 
@@ -949,6 +852,7 @@ module.exports = {
949
852
  hasToken: typeof eff.token === 'string' && eff.token !== '',
950
853
  syncing: marketSyncRun !== null,
951
854
  sparsePaths: marketSparsePaths() ?? null,
855
+ settingsFile: join(dshHome(), 'settings.yaml'),
952
856
  })
953
857
  return
954
858
  }
@@ -988,7 +892,7 @@ module.exports = {
988
892
  }
989
893
  const eff = marketSettings()
990
894
  const { token, ...safe } = eff // token 只写不回读
991
- sendJson(res, 200, { settings: safe, hasToken: typeof token === 'string' && token !== '' })
895
+ sendJson(res, 200, { settings: safe, hasToken: typeof token === 'string' && token !== '', settingsFile: join(dshHome(), 'settings.yaml') })
992
896
  return
993
897
  }
994
898