@omdp/dsh-connector 0.2.1 → 0.2.3
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.js +143 -28
- package/index.js +113 -23
- package/package.json +3 -2
package/client.js
CHANGED
|
@@ -57,7 +57,7 @@ window.__ModuleLoader__.load({
|
|
|
57
57
|
'.pm_badge.err{color:var(--dsw-alias-state-error-primary,#f87171)}' +
|
|
58
58
|
/* buttons */
|
|
59
59
|
'.pm_actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap}' +
|
|
60
|
-
'.pm_btn{cursor:pointer;font:inherit;font-size:12px;padding:5px 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.4));background:transparent;color:inherit;transition:background .12s}' +
|
|
60
|
+
'.pm_btn{cursor:pointer;font:inherit;font-size:12px;padding:5px 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.4));background:transparent;color:inherit;transition:background .12s;flex:none}' +
|
|
61
61
|
'.pm_btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2,rgba(128,128,128,.14))}' +
|
|
62
62
|
'.pm_btn:disabled{opacity:.45;cursor:default}' +
|
|
63
63
|
'.pm_btn.primary{border-color:transparent;background:var(--dsw-alias-brand-primary,#5b8cff);color:#fff}' +
|
|
@@ -86,7 +86,7 @@ window.__ModuleLoader__.load({
|
|
|
86
86
|
'.pm_empty{font-size:12px;color:var(--dsw-alias-label-secondary,#888);padding:10px 2px}' +
|
|
87
87
|
'.pm_btnrow{display:flex;gap:8px;align-items:center;flex-wrap:wrap}' +
|
|
88
88
|
/* market grid & icons */
|
|
89
|
-
'.pm_grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-top:2px}' +
|
|
89
|
+
'.pm_grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-top:2px;align-items:start}' +
|
|
90
90
|
'@media(max-width:760px){.pm_grid{grid-template-columns:1fr}}' +
|
|
91
91
|
'.pm_mcard{background:var(--dsw-alias-bg-layer-1,rgba(255,255,255,.035));border:1px solid var(--dsw-alias-border-l1,rgba(128,128,128,.22));border-radius:12px;padding:12px 14px;display:flex;flex-direction:column;gap:8px;transition:background .15s,border-color .15s}' +
|
|
92
92
|
'.pm_mcard:hover{background:var(--dsw-alias-bg-layer-2,rgba(255,255,255,.06));border-color:var(--dsw-alias-border-l2,rgba(128,128,128,.4))}' +
|
|
@@ -566,6 +566,48 @@ window.__ModuleLoader__.load({
|
|
|
566
566
|
return createElement('div', { className: 'pm_avatar' }, (props.label || '?').charAt(0))
|
|
567
567
|
}
|
|
568
568
|
|
|
569
|
+
// Chinese labels for the official ModelScope MCP plaza taxonomy (the
|
|
570
|
+
// dolphin API returns ~90 category ids with exact counts via FiledAgg;
|
|
571
|
+
// labels below are the sidebar names where they exist, sensible
|
|
572
|
+
// translations elsewhere, and the raw id as a fallback).
|
|
573
|
+
var MCP_CAT_LABEL = {
|
|
574
|
+
'developer-tools': '开发者工具', search: '搜索工具', 'calendar-management': '日程管理',
|
|
575
|
+
other: '其他', 'browser-automation': '浏览器自动化', 'knowledge-and-memory': '知识管理',
|
|
576
|
+
communication: '交流协作', 'research-and-data': '学术研究', databases: '数据库',
|
|
577
|
+
'app-automation': '应用自动化', finance: '金融', 'file-systems': '文件系统',
|
|
578
|
+
'entertainment-and-media': '娱乐与多媒体', 'cloud-platforms': '云平台', 'os-automation': '系统自动化',
|
|
579
|
+
'rag-systems': 'RAG 系统', 'image-and-video-processing': '图像视频', 'autonomous-agents': '自主智能体',
|
|
580
|
+
'note-taking': '笔记记录', 'version-control': '版本控制', 'location-services': '位置服务',
|
|
581
|
+
monitoring: '监控', 'agent-orchestration': 'Agent 编排', 'security-and-iam': '安全与身份',
|
|
582
|
+
'code-execution': '代码执行', 'web-scraping': '网页抓取', 'content-management-systems': '内容管理',
|
|
583
|
+
'documentation-access': '文档访问', 'art-and-culture': '文化与艺术', 'social-media': '社交媒体',
|
|
584
|
+
virtualization: '虚拟化', 'project-management': '项目管理', 'code-analysis': '代码分析',
|
|
585
|
+
'ecommerce-and-retail': '电商零售', 'multimedia-processing': '多媒体处理',
|
|
586
|
+
'travel-and-transportation': '出行交通', 'customer-data-platforms': '客户数据平台',
|
|
587
|
+
marketing: '营销', 'cloud-storage': '云存储', 'education-and-learning-tools': '教育学习',
|
|
588
|
+
'testing-and-qa-tools': '测试质检', 'home-automation-and-iot': '家居物联网', 'shell-access': 'Shell 访问',
|
|
589
|
+
'command-line': '命令行', 'api-testing': 'API 测试', 'ci-cd': 'CI/CD', 'vector-databases': '向量数据库',
|
|
590
|
+
'games-and-gamification': '游戏化', 'speech-processing': '语音处理', 'health-and-wellness': '健康',
|
|
591
|
+
'text-summarization': '文本摘要', 'customer-and-marketing': '客户营销', 'weather-services': '天气服务',
|
|
592
|
+
observability: '可观测性', 'data-platforms': '数据平台', 'customer-support': '客户支持',
|
|
593
|
+
'open-data': '开放数据', blockchain: '区块链', 'government-data': '政务数据',
|
|
594
|
+
'coding-agents': '编程智能体', 'audio-processing': '音频处理', 'penetration-testing': '渗透测试',
|
|
595
|
+
cryptocurrency: '加密货币', 'language-translation': '语言翻译', 'text-to-speech': '语音合成',
|
|
596
|
+
'erp-systems': '企业 ERP', 'fitness-tracking': '健身追踪', 'legal-and-compliance': '法律合规',
|
|
597
|
+
'biology-and-medicine': '生物医学', 'software-architecture': '软件架构', AIGC: 'AIGC',
|
|
598
|
+
bioinformatics: '生物信息', 'home-automation': '家居自动化', security: '安全', sports: '体育',
|
|
599
|
+
transportation: '交通运输', productivity: '生产力工具', 'feature-flags': '特性开关', education: '教育',
|
|
600
|
+
'weather-and-climate': '天气气候', 'health-and-fitness': '健康健身', 'real-estate': '房地产',
|
|
601
|
+
'fitness-and-sports': '健身运动', 'network-services': '网络服务', healthcare: '医疗保健',
|
|
602
|
+
'sports-and-recreation': '运动休闲', 'Knowledge&Memory': '知识管理', weather: '天气', travel: '出行',
|
|
603
|
+
DeveloperTools: '开发者工具', 'network-monitoring': '网络监控', fitness: '健身',
|
|
604
|
+
'Research&Data': '学术研究', 'travel-services': '出行服务', health: '健康',
|
|
605
|
+
'research-an-data': '学术研究', 'aerospace-and-astrodynamics': '航空航天', data: '数据',
|
|
606
|
+
'security-and-compliance': '安全合规', gaming: '游戏', 'workplace-and-productivity': '工作生产力',
|
|
607
|
+
web3: 'Web3',
|
|
608
|
+
}
|
|
609
|
+
var MCP_CAT_LABEL_OF = function (id) { return MCP_CAT_LABEL[id] || id }
|
|
610
|
+
|
|
569
611
|
function MarketPane(props) {
|
|
570
612
|
var _a = react.useState('')
|
|
571
613
|
var mcpSearch = _a[0]
|
|
@@ -600,26 +642,44 @@ window.__ModuleLoader__.load({
|
|
|
600
642
|
var _k = react.useState('mcp')
|
|
601
643
|
var sub = _k[0]
|
|
602
644
|
var setSub = _k[1]
|
|
603
|
-
|
|
604
|
-
|
|
645
|
+
var _l = react.useState('')
|
|
646
|
+
var mcpCat = _l[0]
|
|
647
|
+
var setMcpCat = _l[1]
|
|
648
|
+
var _m = react.useState(1)
|
|
649
|
+
var mcpPage = _m[0]
|
|
650
|
+
var setMcpPage = _m[1]
|
|
651
|
+
var _n = react.useState(1)
|
|
652
|
+
var skillPage = _n[0]
|
|
653
|
+
var setSkillPage = _n[1]
|
|
654
|
+
var _o = react.useState([])
|
|
655
|
+
var mcpCats = _o[0]
|
|
656
|
+
var setMcpCats = _o[1]
|
|
657
|
+
|
|
658
|
+
function findMcp(term, cat, page, append) {
|
|
605
659
|
var q = (term !== undefined ? term : mcpSearch).trim()
|
|
660
|
+
var c = cat !== undefined ? cat : mcpCat
|
|
661
|
+
var p = page || 1
|
|
606
662
|
setMcpBusy(true)
|
|
607
663
|
setErr('')
|
|
608
|
-
api('/market/mcp?search=' + encodeURIComponent(q), { method: 'GET' }).then(function (r) {
|
|
664
|
+
api('/market/mcp?search=' + encodeURIComponent(q) + '&page=' + p + (c ? '&category=' + encodeURIComponent(c) : ''), { method: 'GET' }).then(function (r) {
|
|
609
665
|
if (r && r.error) { setErr(r.error); setMcpResult(null); return }
|
|
610
|
-
|
|
666
|
+
if (!c) setMcpCats(r.categories || []) // unfiltered responses carry the global category table
|
|
667
|
+
setMcpResult(append && mcpResult ? Object.assign({}, r, { items: mcpResult.items.concat(r.items) }) : r)
|
|
668
|
+
setMcpPage(p)
|
|
611
669
|
}).finally(function () { setMcpBusy(false) })
|
|
612
670
|
}
|
|
613
|
-
function findSkills(term, cat) {
|
|
671
|
+
function findSkills(term, cat, page, append) {
|
|
614
672
|
var q = (term !== undefined ? term : skillSearch).trim()
|
|
615
673
|
var c = cat !== undefined ? cat : category
|
|
674
|
+
var p = page || 1
|
|
616
675
|
setSkillBusy(true)
|
|
617
676
|
setErr('')
|
|
618
|
-
var qs = '/market/skills?search=' + encodeURIComponent(q)
|
|
677
|
+
var qs = '/market/skills?search=' + encodeURIComponent(q) + '&page=' + p
|
|
619
678
|
if (c) qs += '&category=' + encodeURIComponent(c)
|
|
620
679
|
api(qs, { method: 'GET' }).then(function (r) {
|
|
621
680
|
if (r && r.error) { setErr(r.error); setSkillResult(null); return }
|
|
622
|
-
setSkillResult(r)
|
|
681
|
+
setSkillResult(append && skillResult ? Object.assign({}, r, { items: skillResult.items.concat(r.items) }) : r)
|
|
682
|
+
setSkillPage(p)
|
|
623
683
|
}).finally(function () { setSkillBusy(false) })
|
|
624
684
|
}
|
|
625
685
|
function openMcp(id) {
|
|
@@ -641,27 +701,46 @@ window.__ModuleLoader__.load({
|
|
|
641
701
|
|
|
642
702
|
// Load the default (hot) lists right away so the tab never opens empty.
|
|
643
703
|
react.useEffect(function () {
|
|
644
|
-
findMcp('')
|
|
645
|
-
findSkills('', '')
|
|
704
|
+
findMcp('', '', 1, false)
|
|
705
|
+
findSkills('', '', 1, false)
|
|
646
706
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
647
707
|
}, [])
|
|
648
708
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
709
|
+
// Category filtering is server-side (dolphin Criterion); the response
|
|
710
|
+
// total already reflects the chosen category and the item list arrives
|
|
711
|
+
// pre-filtered with exact counts (FiledAgg) mirrored on the chips.
|
|
712
|
+
var loadedMcp = mcpResult && mcpResult.items ? mcpResult.items : []
|
|
713
|
+
var mcpRows = loadedMcp
|
|
714
|
+
.map(function (s) {
|
|
715
|
+
var d = mcpDetail[s.id]
|
|
716
|
+
return createElement(McpMarketItem, {
|
|
717
|
+
key: s.id, item: s, detail: d,
|
|
718
|
+
onOpen: function () {
|
|
719
|
+
if (d) {
|
|
720
|
+
var next = Object.assign({}, mcpDetail)
|
|
721
|
+
delete next[s.id]
|
|
722
|
+
setMcpDetail(next)
|
|
723
|
+
} else {
|
|
724
|
+
openMcp(s.id)
|
|
725
|
+
}
|
|
726
|
+
},
|
|
656
727
|
})
|
|
657
|
-
|
|
728
|
+
})
|
|
658
729
|
|
|
659
730
|
var skillRows = skillResult && skillResult.items
|
|
660
731
|
? skillResult.items.map(function (s) {
|
|
661
732
|
var d = skillDetail[s.id]
|
|
662
733
|
return createElement(SkillMarketItem, {
|
|
663
734
|
key: s.id, item: s, detail: d, skills: props.skills,
|
|
664
|
-
onOpen: function () {
|
|
735
|
+
onOpen: function () {
|
|
736
|
+
if (d) {
|
|
737
|
+
var next = Object.assign({}, skillDetail)
|
|
738
|
+
delete next[s.id]
|
|
739
|
+
setSkillDetail(next)
|
|
740
|
+
} else {
|
|
741
|
+
openSkill(s.id)
|
|
742
|
+
}
|
|
743
|
+
},
|
|
665
744
|
onChanged: props.onChanged,
|
|
666
745
|
})
|
|
667
746
|
})
|
|
@@ -685,14 +764,40 @@ window.__ModuleLoader__.load({
|
|
|
685
764
|
placeholder: '搜索 MCP 服务,如 map / github / fetch Search…',
|
|
686
765
|
value: mcpSearch,
|
|
687
766
|
onChange: function (e) { setMcpSearch(e.target.value) },
|
|
688
|
-
onKeyDown: function (e) { if (e.key === 'Enter') findMcp() },
|
|
767
|
+
onKeyDown: function (e) { if (e.key === 'Enter') findMcp(undefined, undefined, 1, false) },
|
|
768
|
+
}),
|
|
769
|
+
createElement('button', { className: 'pm_btn primary', disabled: mcpBusy, onClick: function () { findMcp(undefined, undefined, 1, false) } }, mcpBusy ? '搜索中…' : '搜索'),
|
|
770
|
+
),
|
|
771
|
+
createElement('div', { className: 'pm_chips' },
|
|
772
|
+
createElement('button', {
|
|
773
|
+
key: 'all',
|
|
774
|
+
className: 'pm_chip' + (!mcpCat ? ' active' : ''),
|
|
775
|
+
onClick: function () { setMcpCat(''); findMcp(undefined, '', 1, false) },
|
|
776
|
+
}, '全部 ' + (mcpResult && mcpResult.total ? mcpResult.total.toLocaleString() : '')),
|
|
777
|
+
mcpCats.map(function (c) {
|
|
778
|
+
return createElement('button', {
|
|
779
|
+
key: c.id,
|
|
780
|
+
className: 'pm_chip' + (mcpCat === c.id ? ' active' : ''),
|
|
781
|
+
onClick: function () { setMcpCat(c.id); findMcp(undefined, c.id, 1, false) },
|
|
782
|
+
}, MCP_CAT_LABEL_OF(c.id) + ' ' + c.count.toLocaleString())
|
|
689
783
|
}),
|
|
690
|
-
createElement('button', { className: 'pm_btn primary', disabled: mcpBusy, onClick: function () { findMcp() } }, mcpBusy ? '搜索中…' : '搜索'),
|
|
691
784
|
),
|
|
785
|
+
mcpResult && mcpResult.total
|
|
786
|
+
? createElement('div', { className: 'pm_meta' },
|
|
787
|
+
(mcpCat
|
|
788
|
+
? '分类 ' + MCP_CAT_LABEL_OF(mcpCat) + ':共 ' + mcpResult.total.toLocaleString() + ' 个 MCP 服务'
|
|
789
|
+
: '共 ' + mcpResult.total.toLocaleString() + ' 个 MCP 服务,已加载 ' + loadedMcp.length + ' 条'),
|
|
790
|
+
)
|
|
791
|
+
: null,
|
|
692
792
|
createElement('div', { className: 'pm_grid' }, mcpRows),
|
|
693
|
-
mcpResult && mcpResult.items && !
|
|
793
|
+
mcpResult && mcpResult.items && !loadedMcp.length
|
|
694
794
|
? createElement('div', { className: 'pm_empty' }, '没有匹配的 MCP 服务。No matching MCP servers.')
|
|
695
795
|
: null,
|
|
796
|
+
mcpResult && mcpResult.hasMore
|
|
797
|
+
? createElement('div', { className: 'pm_actions', style: { justifyContent: 'center' } },
|
|
798
|
+
createElement('button', { className: 'pm_btn', disabled: mcpBusy, onClick: function () { findMcp(mcpSearch, mcpCat, mcpPage + 1, true) } }, '加载更多 ' + mcpResult.total.toLocaleString() + ' 中已看 ' + loadedMcp.length),
|
|
799
|
+
)
|
|
800
|
+
: null,
|
|
696
801
|
)
|
|
697
802
|
: createElement('div', { className: 'pm_section' },
|
|
698
803
|
createElement('div', { className: 'pm_search' },
|
|
@@ -700,23 +805,33 @@ window.__ModuleLoader__.load({
|
|
|
700
805
|
placeholder: '搜索技能,如 code-review / deploy Search…',
|
|
701
806
|
value: skillSearch,
|
|
702
807
|
onChange: function (e) { setSkillSearch(e.target.value) },
|
|
703
|
-
onKeyDown: function (e) { if (e.key === 'Enter') findSkills() },
|
|
808
|
+
onKeyDown: function (e) { if (e.key === 'Enter') findSkills(undefined, undefined, 1, false) },
|
|
704
809
|
}),
|
|
705
|
-
createElement('button', { className: 'pm_btn primary', disabled: skillBusy, onClick: function () { findSkills() } }, skillBusy ? '搜索中…' : '搜索'),
|
|
810
|
+
createElement('button', { className: 'pm_btn primary', disabled: skillBusy, onClick: function () { findSkills(undefined, undefined, 1, false) } }, skillBusy ? '搜索中…' : '搜索'),
|
|
706
811
|
),
|
|
707
812
|
createElement('div', { className: 'pm_chips' },
|
|
708
813
|
SKILL_CATEGORIES.map(function (c) {
|
|
709
814
|
return createElement('button', {
|
|
710
815
|
key: c.id || 'all',
|
|
711
816
|
className: 'pm_chip' + (category === c.id ? ' active' : ''),
|
|
712
|
-
onClick: function () { setCategory(c.id); findSkills(skillSearch, c.id) },
|
|
817
|
+
onClick: function () { setCategory(c.id); setSkillPage(1); findSkills(skillSearch, c.id, 1, false) },
|
|
713
818
|
}, c.label)
|
|
714
819
|
}),
|
|
715
820
|
),
|
|
821
|
+
skillResult && skillResult.total
|
|
822
|
+
? createElement('div', { className: 'pm_meta' },
|
|
823
|
+
'共 ' + skillResult.total.toLocaleString() + ' 个技能,已加载 ' + (skillResult.items ? skillResult.items.length : 0) + ' 条',
|
|
824
|
+
)
|
|
825
|
+
: null,
|
|
716
826
|
createElement('div', { className: 'pm_grid' }, skillRows),
|
|
717
827
|
skillResult && skillResult.items && !skillResult.items.length
|
|
718
828
|
? createElement('div', { className: 'pm_empty' }, '没有匹配的技能。No matching skills.')
|
|
719
829
|
: null,
|
|
830
|
+
skillResult && skillResult.hasMore
|
|
831
|
+
? createElement('div', { className: 'pm_actions', style: { justifyContent: 'center' } },
|
|
832
|
+
createElement('button', { className: 'pm_btn', disabled: skillBusy, onClick: function () { findSkills(skillSearch, category, skillPage + 1, true) } }, '加载更多 ' + skillResult.total.toLocaleString() + ' 中已看 ' + skillResult.items.length),
|
|
833
|
+
)
|
|
834
|
+
: null,
|
|
720
835
|
),
|
|
721
836
|
)
|
|
722
837
|
}
|
|
@@ -761,14 +876,14 @@ window.__ModuleLoader__.load({
|
|
|
761
876
|
createElement('button', { className: 'pm_btn', onClick: props.onOpen }, d ? '收起' : '详情'),
|
|
762
877
|
d && configs.length
|
|
763
878
|
? createElement('button', { className: 'pm_btn primary', onClick: function () { copied.copy(cfgText) } },
|
|
764
|
-
copied.copied || '复制配置
|
|
879
|
+
copied.copied || '复制配置')
|
|
765
880
|
: null,
|
|
766
881
|
),
|
|
767
882
|
d ? createElement('div', { className: 'pm_section', style: { marginTop: '4px' } },
|
|
768
883
|
createElement('div', { className: 'pm_actions' },
|
|
769
884
|
d.hosted ? badge('Hosted 官方托管', 'brand') : null,
|
|
770
885
|
d.verified ? badge('已认证 Verified', 'brand') : null,
|
|
771
|
-
d.stars
|
|
886
|
+
d.stars > 0 ? badge('★ ' + fmtNum(d.stars), 'muted') : null,
|
|
772
887
|
),
|
|
773
888
|
(d.envSchema && d.envSchema.length)
|
|
774
889
|
? createElement('div', { className: 'pm_meta' },
|
package/index.js
CHANGED
|
@@ -26,6 +26,7 @@ import { spawnSync } from 'node:child_process'
|
|
|
26
26
|
import { homedir } from 'node:os'
|
|
27
27
|
import { join, dirname } from 'node:path'
|
|
28
28
|
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'
|
|
29
|
+
import { JSDOM, VirtualConsole } from 'jsdom'
|
|
29
30
|
|
|
30
31
|
const API_PREFIX = '/connector/api'
|
|
31
32
|
const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
|
|
@@ -444,6 +445,7 @@ async function removeUserSkill(home, name) {
|
|
|
444
445
|
* ───────────────────────────────────────────────────────────────────────── */
|
|
445
446
|
|
|
446
447
|
const MARKET_BASE = process.env.DSH_CONNECTOR_MARKET ?? 'https://modelscope.cn/openapi/v1'
|
|
448
|
+
const MARKET_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36'
|
|
447
449
|
const MARKET_TTL = 30 * 60 * 1000 // 30 minutes in-process cache
|
|
448
450
|
const marketCache = new Map()
|
|
449
451
|
|
|
@@ -465,6 +467,84 @@ function marketError(res, error) {
|
|
|
465
467
|
return json(res, 502, { error: `market unavailable: ${String(error?.message ?? error)}` })
|
|
466
468
|
}
|
|
467
469
|
|
|
470
|
+
/* ── ModelScope dolphin MCP API (the engine behind modelscope.cn/mcp) ──────
|
|
471
|
+
*
|
|
472
|
+
* The official plaza is a SPA whose data endpoint is
|
|
473
|
+
* PUT https://www.modelscope.cn/api/v1/dolphin/mcpServers
|
|
474
|
+
* with body { PageSize, PageNumber, Query, Criterion } where category
|
|
475
|
+
* filtering is Criterion: [{ Category:'Category', Predicate:'contains',
|
|
476
|
+
* StringValues:[categoryId] }]. The response carries
|
|
477
|
+
* Data.McpServer.{McpServers[], TotalCount} — paged list + exact total
|
|
478
|
+
* Data.FiledAgg.Category — every category + exact count
|
|
479
|
+
* (this is what the sidebar numbers on the official site are).
|
|
480
|
+
*
|
|
481
|
+
* The endpoint sits behind an Alibaba WAF JS challenge (acw_sc__v2). A plain
|
|
482
|
+
* server request gets a challenge page back; the cookie is produced by
|
|
483
|
+
* executing that page. We run it in jsdom (deterministic, no browser needed)
|
|
484
|
+
* and cache the cookie for 30 minutes (the WAF's own max-age is 3600s).
|
|
485
|
+
*/
|
|
486
|
+
const DOLPHIN_URL = 'https://www.modelscope.cn/api/v1/dolphin/mcpServers'
|
|
487
|
+
const WAF_TTL = 30 * 60 * 1000
|
|
488
|
+
const dolphinCache = new Map()
|
|
489
|
+
let wafCookie = null // { value, at }
|
|
490
|
+
|
|
491
|
+
// Solve the acw_sc__v2 cookie by executing the challenge page in jsdom. The
|
|
492
|
+
// challenge HTML arrives as the body of the WAF-blocked request itself (the
|
|
493
|
+
// SPA shell on /mcp is NOT a challenge page and yields no cookie).
|
|
494
|
+
async function solveWafFromChallenge(html) {
|
|
495
|
+
if (wafCookie && Date.now() - wafCookie.at < WAF_TTL) return wafCookie.value
|
|
496
|
+
const vc = new VirtualConsole()
|
|
497
|
+
vc.on('jsdomError', () => {}) // jsdom navigation is not implemented — that is expected here
|
|
498
|
+
const dom = new JSDOM(html, {
|
|
499
|
+
runScripts: 'dangerously',
|
|
500
|
+
pretendToBeVisual: true,
|
|
501
|
+
virtualConsole: vc,
|
|
502
|
+
url: 'https://www.modelscope.cn/',
|
|
503
|
+
beforeParse(window) {
|
|
504
|
+
for (const k of ['reload', 'replace', 'assign']) {
|
|
505
|
+
try { Object.defineProperty(window.location, k, { configurable: true, writable: true, value: () => {} }) } catch { /* noop */ }
|
|
506
|
+
}
|
|
507
|
+
window.open = () => null
|
|
508
|
+
},
|
|
509
|
+
})
|
|
510
|
+
await new Promise((resolve) => setTimeout(resolve, 1500))
|
|
511
|
+
const cookie = dom.window.document.cookie.match(/acw_sc__v2=([^;]+)/)
|
|
512
|
+
dom.window.close()
|
|
513
|
+
if (!cookie) throw new Error('WAF challenge did not yield a cookie')
|
|
514
|
+
wafCookie = { value: cookie[1], at: Date.now() }
|
|
515
|
+
return wafCookie.value
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async function dolphinPut(body) {
|
|
519
|
+
const key = 'dolphin?' + JSON.stringify(body)
|
|
520
|
+
const hit = dolphinCache.get(key)
|
|
521
|
+
if (hit !== undefined && Date.now() - hit.at < MARKET_TTL) return hit.value
|
|
522
|
+
const attempt = async (cookie) => {
|
|
523
|
+
const res = await fetch(DOLPHIN_URL, {
|
|
524
|
+
method: 'PUT',
|
|
525
|
+
headers: { 'content-type': 'application/json', 'user-agent': MARKET_UA, ...(cookie ? { cookie: `acw_sc__v2=${cookie}` } : {}) },
|
|
526
|
+
body: JSON.stringify(body),
|
|
527
|
+
signal: AbortSignal.timeout(15000),
|
|
528
|
+
})
|
|
529
|
+
const text = await res.text()
|
|
530
|
+
if (text.includes('acw_sc__v2') || text.includes('aliyunwaf')) return { challenge: text }
|
|
531
|
+
if (!res.ok) throw new Error(`dolphin returned HTTP ${res.status}`)
|
|
532
|
+
return JSON.parse(text)
|
|
533
|
+
}
|
|
534
|
+
// First try with the cached cookie; on a challenge page, solve from its HTML
|
|
535
|
+
// and retry exactly once with the fresh cookie.
|
|
536
|
+
let data = await attempt(wafCookie && Date.now() - wafCookie.at < WAF_TTL ? wafCookie.value : undefined)
|
|
537
|
+
if (data && data.challenge) {
|
|
538
|
+
wafCookie = null
|
|
539
|
+
const fresh = await solveWafFromChallenge(data.challenge)
|
|
540
|
+
data = await attempt(fresh)
|
|
541
|
+
if (data && data.challenge) throw new Error('WAF challenge loop (every attempt blocked)')
|
|
542
|
+
}
|
|
543
|
+
if (!data) throw new Error('dolphin request failed')
|
|
544
|
+
dolphinCache.set(key, { at: Date.now(), value: data })
|
|
545
|
+
return data
|
|
546
|
+
}
|
|
547
|
+
|
|
468
548
|
// Skill ids come back as `@author/name` and appear in URLs verbatim. Only
|
|
469
549
|
// accept the safe alphabet so a crafted id can never escape the path.
|
|
470
550
|
const MARKET_ID = /^@?[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+$/
|
|
@@ -474,7 +554,7 @@ async function marketSkillsList(req, res, params) {
|
|
|
474
554
|
const search = params.get('search')?.trim()
|
|
475
555
|
const category = params.get('category')?.trim()
|
|
476
556
|
const page = Math.max(1, Number(params.get('page')) || 1)
|
|
477
|
-
const qs = new URLSearchParams({ page_number: String(page), page_size: '
|
|
557
|
+
const qs = new URLSearchParams({ page_number: String(page), page_size: '50' })
|
|
478
558
|
if (search) qs.set('search', search)
|
|
479
559
|
if (category) qs.set('filter.category', category)
|
|
480
560
|
let data
|
|
@@ -497,7 +577,8 @@ async function marketSkillsList(req, res, params) {
|
|
|
497
577
|
logo: s.logo_url,
|
|
498
578
|
installed: local.some((l) => l.source === s.id),
|
|
499
579
|
}))
|
|
500
|
-
|
|
580
|
+
const total = Number(data?.data?.total) || items.length
|
|
581
|
+
return json(res, 200, { ok: true, items, total, page, hasMore: page * 50 < total })
|
|
501
582
|
}
|
|
502
583
|
|
|
503
584
|
// GET /api/market/skills/:id (id is `@author/name`, may contain a slash)
|
|
@@ -539,20 +620,20 @@ async function marketSkillDetail(res, id) {
|
|
|
539
620
|
})
|
|
540
621
|
}
|
|
541
622
|
|
|
542
|
-
// GET /api/market/mcp?search=&page=
|
|
623
|
+
// GET /api/market/mcp?search=&category=&page=
|
|
543
624
|
async function marketMcpList(req, res, params) {
|
|
544
|
-
const search = params.get('search')?.trim()
|
|
625
|
+
const search = params.get('search')?.trim() ?? ''
|
|
626
|
+
const category = params.get('category')?.trim()
|
|
545
627
|
const page = Math.max(1, Number(params.get('page')) || 1)
|
|
546
|
-
const body = {
|
|
547
|
-
|
|
548
|
-
|
|
628
|
+
const body = {
|
|
629
|
+
PageSize: 30,
|
|
630
|
+
PageNumber: page,
|
|
631
|
+
Query: search,
|
|
632
|
+
Criterion: category ? [{ Category: 'Category', Predicate: 'contains', StringValues: [category] }] : [],
|
|
633
|
+
}
|
|
549
634
|
let data
|
|
550
635
|
try {
|
|
551
|
-
data = await
|
|
552
|
-
method: 'PUT',
|
|
553
|
-
headers: { 'content-type': 'application/json' },
|
|
554
|
-
body: JSON.stringify(body),
|
|
555
|
-
})
|
|
636
|
+
data = await dolphinPut(body)
|
|
556
637
|
} catch (error) {
|
|
557
638
|
return marketError(res, error)
|
|
558
639
|
}
|
|
@@ -569,18 +650,27 @@ async function marketMcpList(req, res, params) {
|
|
|
569
650
|
} catch {
|
|
570
651
|
// patch missing/unreadable: no configured markers, market still works
|
|
571
652
|
}
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
653
|
+
const servers = data?.Data?.McpServer?.McpServers ?? []
|
|
654
|
+
const items = servers.map((s) => ({
|
|
655
|
+
id: s.Publisher, // `@author/name` — same shape the detail endpoint expects
|
|
656
|
+
name: s.ChineseName || s.Name || s.Publisher,
|
|
657
|
+
description: s.AbstractCN || s.Abstract || '',
|
|
658
|
+
publisher: s.Publisher,
|
|
659
|
+
tags: Array.isArray(s.Tags) ? s.Tags : [],
|
|
660
|
+
logo: s.FromSiteIcon || '',
|
|
661
|
+
views: s.ViewCount,
|
|
662
|
+
stars: s.Stars,
|
|
663
|
+
license: s.License || '',
|
|
664
|
+
hosted: !!s.Hosted,
|
|
665
|
+
verified: !!s.Verifed,
|
|
666
|
+
categories: Array.isArray(s.Category) ? s.Category : [],
|
|
667
|
+
configured: localMatches.some((local) => typeof s.Publisher === 'string' && local.includes(s.Publisher.toLowerCase())),
|
|
582
668
|
}))
|
|
583
|
-
|
|
669
|
+
const total = Number(data?.Data?.McpServer?.TotalCount) || items.length
|
|
670
|
+
const categories = (data?.Data?.FiledAgg?.Category ?? [])
|
|
671
|
+
.map((c) => ({ id: String(c.Value), count: Number(c.Count) || 0 }))
|
|
672
|
+
.filter((c) => c.id)
|
|
673
|
+
return json(res, 200, { ok: true, items, total, page, pageSize: 30, hasMore: page * 30 < total, categories })
|
|
584
674
|
}
|
|
585
675
|
|
|
586
676
|
// Reduce a JSONSchema env block to its variable list (never ship the raw
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omdp/dsh-connector",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Unified DeepSeek Harness connector: edit MCP servers (cordis.patch.yml), user skills (~/.dsh/skills) and browse the ModelScope MCP/Skills marketplaces from one Web UI settings page.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"directory": "dsh-connector"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"yaml": "^2.9.0"
|
|
45
|
+
"yaml": "^2.9.0",
|
|
46
|
+
"jsdom": "^24.1.3"
|
|
46
47
|
}
|
|
47
48
|
}
|