@omdp/dsh-connector 0.2.2 → 0.2.4
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 +145 -31
- package/index.js +113 -23
- package/package.json +3 -2
package/client.js
CHANGED
|
@@ -102,7 +102,8 @@ window.__ModuleLoader__.load({
|
|
|
102
102
|
'.pm_chips{display:flex;gap:6px;flex-wrap:wrap}' +
|
|
103
103
|
'.pm_chip{cursor:pointer;font:inherit;font-size:11.5px;padding:4px 12px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.35));background:transparent;color:var(--dsw-alias-label-secondary,#9aa);transition:all .12s}' +
|
|
104
104
|
'.pm_chip:hover{color:var(--dsw-alias-label-primary,#ddd);border-color:var(--dsw-alias-brand-primary,#5b8cff)}' +
|
|
105
|
-
'.pm_chip.active{background:var(--dsw-alias-brand-primary,#5b8cff);border-color:transparent;color:#fff}'
|
|
105
|
+
'.pm_chip.active{background:var(--dsw-alias-brand-primary,#5b8cff);border-color:transparent;color:#fff}' +
|
|
106
|
+
'.pm_chipToggle{color:var(--dsw-alias-brand-primary,#5b8cff);border-style:dashed}'
|
|
106
107
|
|
|
107
108
|
if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css="@omdp/dsh-connector/section"]') === null) {
|
|
108
109
|
var tag = document.createElement('style')
|
|
@@ -566,6 +567,48 @@ window.__ModuleLoader__.load({
|
|
|
566
567
|
return createElement('div', { className: 'pm_avatar' }, (props.label || '?').charAt(0))
|
|
567
568
|
}
|
|
568
569
|
|
|
570
|
+
// Chinese labels for the official ModelScope MCP plaza taxonomy (the
|
|
571
|
+
// dolphin API returns ~90 category ids with exact counts via FiledAgg;
|
|
572
|
+
// labels below are the sidebar names where they exist, sensible
|
|
573
|
+
// translations elsewhere, and the raw id as a fallback).
|
|
574
|
+
var MCP_CAT_LABEL = {
|
|
575
|
+
'developer-tools': '开发者工具', search: '搜索工具', 'calendar-management': '日程管理',
|
|
576
|
+
other: '其他', 'browser-automation': '浏览器自动化', 'knowledge-and-memory': '知识管理',
|
|
577
|
+
communication: '交流协作', 'research-and-data': '学术研究', databases: '数据库',
|
|
578
|
+
'app-automation': '应用自动化', finance: '金融', 'file-systems': '文件系统',
|
|
579
|
+
'entertainment-and-media': '娱乐与多媒体', 'cloud-platforms': '云平台', 'os-automation': '系统自动化',
|
|
580
|
+
'rag-systems': 'RAG 系统', 'image-and-video-processing': '图像视频', 'autonomous-agents': '自主智能体',
|
|
581
|
+
'note-taking': '笔记记录', 'version-control': '版本控制', 'location-services': '位置服务',
|
|
582
|
+
monitoring: '监控', 'agent-orchestration': 'Agent 编排', 'security-and-iam': '安全与身份',
|
|
583
|
+
'code-execution': '代码执行', 'web-scraping': '网页抓取', 'content-management-systems': '内容管理',
|
|
584
|
+
'documentation-access': '文档访问', 'art-and-culture': '文化与艺术', 'social-media': '社交媒体',
|
|
585
|
+
virtualization: '虚拟化', 'project-management': '项目管理', 'code-analysis': '代码分析',
|
|
586
|
+
'ecommerce-and-retail': '电商零售', 'multimedia-processing': '多媒体处理',
|
|
587
|
+
'travel-and-transportation': '出行交通', 'customer-data-platforms': '客户数据平台',
|
|
588
|
+
marketing: '营销', 'cloud-storage': '云存储', 'education-and-learning-tools': '教育学习',
|
|
589
|
+
'testing-and-qa-tools': '测试质检', 'home-automation-and-iot': '家居物联网', 'shell-access': 'Shell 访问',
|
|
590
|
+
'command-line': '命令行', 'api-testing': 'API 测试', 'ci-cd': 'CI/CD', 'vector-databases': '向量数据库',
|
|
591
|
+
'games-and-gamification': '游戏化', 'speech-processing': '语音处理', 'health-and-wellness': '健康',
|
|
592
|
+
'text-summarization': '文本摘要', 'customer-and-marketing': '客户营销', 'weather-services': '天气服务',
|
|
593
|
+
observability: '可观测性', 'data-platforms': '数据平台', 'customer-support': '客户支持',
|
|
594
|
+
'open-data': '开放数据', blockchain: '区块链', 'government-data': '政务数据',
|
|
595
|
+
'coding-agents': '编程智能体', 'audio-processing': '音频处理', 'penetration-testing': '渗透测试',
|
|
596
|
+
cryptocurrency: '加密货币', 'language-translation': '语言翻译', 'text-to-speech': '语音合成',
|
|
597
|
+
'erp-systems': '企业 ERP', 'fitness-tracking': '健身追踪', 'legal-and-compliance': '法律合规',
|
|
598
|
+
'biology-and-medicine': '生物医学', 'software-architecture': '软件架构', AIGC: 'AIGC',
|
|
599
|
+
bioinformatics: '生物信息', 'home-automation': '家居自动化', security: '安全', sports: '体育',
|
|
600
|
+
transportation: '交通运输', productivity: '生产力工具', 'feature-flags': '特性开关', education: '教育',
|
|
601
|
+
'weather-and-climate': '天气气候', 'health-and-fitness': '健康健身', 'real-estate': '房地产',
|
|
602
|
+
'fitness-and-sports': '健身运动', 'network-services': '网络服务', healthcare: '医疗保健',
|
|
603
|
+
'sports-and-recreation': '运动休闲', 'Knowledge&Memory': '知识管理', weather: '天气', travel: '出行',
|
|
604
|
+
DeveloperTools: '开发者工具', 'network-monitoring': '网络监控', fitness: '健身',
|
|
605
|
+
'Research&Data': '学术研究', 'travel-services': '出行服务', health: '健康',
|
|
606
|
+
'research-an-data': '学术研究', 'aerospace-and-astrodynamics': '航空航天', data: '数据',
|
|
607
|
+
'security-and-compliance': '安全合规', gaming: '游戏', 'workplace-and-productivity': '工作生产力',
|
|
608
|
+
web3: 'Web3',
|
|
609
|
+
}
|
|
610
|
+
var MCP_CAT_LABEL_OF = function (id) { return MCP_CAT_LABEL[id] || id }
|
|
611
|
+
|
|
569
612
|
function MarketPane(props) {
|
|
570
613
|
var _a = react.useState('')
|
|
571
614
|
var mcpSearch = _a[0]
|
|
@@ -600,26 +643,51 @@ window.__ModuleLoader__.load({
|
|
|
600
643
|
var _k = react.useState('mcp')
|
|
601
644
|
var sub = _k[0]
|
|
602
645
|
var setSub = _k[1]
|
|
646
|
+
var _l = react.useState('')
|
|
647
|
+
var mcpCat = _l[0]
|
|
648
|
+
var setMcpCat = _l[1]
|
|
649
|
+
var _m = react.useState(1)
|
|
650
|
+
var mcpPage = _m[0]
|
|
651
|
+
var setMcpPage = _m[1]
|
|
652
|
+
var _n = react.useState(1)
|
|
653
|
+
var skillPage = _n[0]
|
|
654
|
+
var setSkillPage = _n[1]
|
|
655
|
+
var _o = react.useState([])
|
|
656
|
+
var mcpCats = _o[0]
|
|
657
|
+
var setMcpCats = _o[1]
|
|
658
|
+
var _p = react.useState(false)
|
|
659
|
+
var mcpCatsOpen = _p[0]
|
|
660
|
+
var setMcpCatsOpen = _p[1]
|
|
661
|
+
// Collapsed state shows "all" + the top popular categories (the dolphin
|
|
662
|
+
// category table arrives in count-descending order); the toggle reveals
|
|
663
|
+
// the full taxonomy.
|
|
664
|
+
var visibleMcpCats = mcpCatsOpen || !mcpCats.length ? mcpCats : mcpCats.slice(0, 12)
|
|
603
665
|
|
|
604
|
-
function findMcp(term) {
|
|
666
|
+
function findMcp(term, cat, page, append) {
|
|
605
667
|
var q = (term !== undefined ? term : mcpSearch).trim()
|
|
668
|
+
var c = cat !== undefined ? cat : mcpCat
|
|
669
|
+
var p = page || 1
|
|
606
670
|
setMcpBusy(true)
|
|
607
671
|
setErr('')
|
|
608
|
-
api('/market/mcp?search=' + encodeURIComponent(q), { method: 'GET' }).then(function (r) {
|
|
672
|
+
api('/market/mcp?search=' + encodeURIComponent(q) + '&page=' + p + (c ? '&category=' + encodeURIComponent(c) : ''), { method: 'GET' }).then(function (r) {
|
|
609
673
|
if (r && r.error) { setErr(r.error); setMcpResult(null); return }
|
|
610
|
-
|
|
674
|
+
if (!c) setMcpCats(r.categories || []) // unfiltered responses carry the global category table
|
|
675
|
+
setMcpResult(append && mcpResult ? Object.assign({}, r, { items: mcpResult.items.concat(r.items) }) : r)
|
|
676
|
+
setMcpPage(p)
|
|
611
677
|
}).finally(function () { setMcpBusy(false) })
|
|
612
678
|
}
|
|
613
|
-
function findSkills(term, cat) {
|
|
679
|
+
function findSkills(term, cat, page, append) {
|
|
614
680
|
var q = (term !== undefined ? term : skillSearch).trim()
|
|
615
681
|
var c = cat !== undefined ? cat : category
|
|
682
|
+
var p = page || 1
|
|
616
683
|
setSkillBusy(true)
|
|
617
684
|
setErr('')
|
|
618
|
-
var qs = '/market/skills?search=' + encodeURIComponent(q)
|
|
685
|
+
var qs = '/market/skills?search=' + encodeURIComponent(q) + '&page=' + p
|
|
619
686
|
if (c) qs += '&category=' + encodeURIComponent(c)
|
|
620
687
|
api(qs, { method: 'GET' }).then(function (r) {
|
|
621
688
|
if (r && r.error) { setErr(r.error); setSkillResult(null); return }
|
|
622
|
-
setSkillResult(r)
|
|
689
|
+
setSkillResult(append && skillResult ? Object.assign({}, r, { items: skillResult.items.concat(r.items) }) : r)
|
|
690
|
+
setSkillPage(p)
|
|
623
691
|
}).finally(function () { setSkillBusy(false) })
|
|
624
692
|
}
|
|
625
693
|
function openMcp(id) {
|
|
@@ -641,28 +709,31 @@ window.__ModuleLoader__.load({
|
|
|
641
709
|
|
|
642
710
|
// Load the default (hot) lists right away so the tab never opens empty.
|
|
643
711
|
react.useEffect(function () {
|
|
644
|
-
findMcp('')
|
|
645
|
-
findSkills('', '')
|
|
712
|
+
findMcp('', '', 1, false)
|
|
713
|
+
findSkills('', '', 1, false)
|
|
646
714
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
647
715
|
}, [])
|
|
648
716
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
717
|
+
// Category filtering is server-side (dolphin Criterion); the response
|
|
718
|
+
// total already reflects the chosen category and the item list arrives
|
|
719
|
+
// pre-filtered with exact counts (FiledAgg) mirrored on the chips.
|
|
720
|
+
var loadedMcp = mcpResult && mcpResult.items ? mcpResult.items : []
|
|
721
|
+
var mcpRows = loadedMcp
|
|
722
|
+
.map(function (s) {
|
|
723
|
+
var d = mcpDetail[s.id]
|
|
724
|
+
return createElement(McpMarketItem, {
|
|
725
|
+
key: s.id, item: s, detail: d,
|
|
726
|
+
onOpen: function () {
|
|
727
|
+
if (d) {
|
|
728
|
+
var next = Object.assign({}, mcpDetail)
|
|
729
|
+
delete next[s.id]
|
|
730
|
+
setMcpDetail(next)
|
|
731
|
+
} else {
|
|
732
|
+
openMcp(s.id)
|
|
733
|
+
}
|
|
734
|
+
},
|
|
664
735
|
})
|
|
665
|
-
|
|
736
|
+
})
|
|
666
737
|
|
|
667
738
|
var skillRows = skillResult && skillResult.items
|
|
668
739
|
? skillResult.items.map(function (s) {
|
|
@@ -701,14 +772,47 @@ window.__ModuleLoader__.load({
|
|
|
701
772
|
placeholder: '搜索 MCP 服务,如 map / github / fetch Search…',
|
|
702
773
|
value: mcpSearch,
|
|
703
774
|
onChange: function (e) { setMcpSearch(e.target.value) },
|
|
704
|
-
onKeyDown: function (e) { if (e.key === 'Enter') findMcp() },
|
|
775
|
+
onKeyDown: function (e) { if (e.key === 'Enter') findMcp(undefined, undefined, 1, false) },
|
|
776
|
+
}),
|
|
777
|
+
createElement('button', { className: 'pm_btn primary', disabled: mcpBusy, onClick: function () { findMcp(undefined, undefined, 1, false) } }, mcpBusy ? '搜索中…' : '搜索'),
|
|
778
|
+
),
|
|
779
|
+
createElement('div', { className: 'pm_chips' },
|
|
780
|
+
createElement('button', {
|
|
781
|
+
key: 'all',
|
|
782
|
+
className: 'pm_chip' + (!mcpCat ? ' active' : ''),
|
|
783
|
+
onClick: function () { setMcpCat(''); findMcp(undefined, '', 1, false) },
|
|
784
|
+
}, '全部 ' + (mcpResult && mcpResult.total ? mcpResult.total.toLocaleString() : '')),
|
|
785
|
+
visibleMcpCats.map(function (c) {
|
|
786
|
+
return createElement('button', {
|
|
787
|
+
key: c.id,
|
|
788
|
+
className: 'pm_chip' + (mcpCat === c.id ? ' active' : ''),
|
|
789
|
+
onClick: function () { setMcpCat(c.id); findMcp(undefined, c.id, 1, false) },
|
|
790
|
+
}, MCP_CAT_LABEL_OF(c.id) + ' ' + c.count.toLocaleString())
|
|
705
791
|
}),
|
|
706
|
-
|
|
792
|
+
mcpCats.length > 12
|
|
793
|
+
? createElement('button', {
|
|
794
|
+
key: 'toggle',
|
|
795
|
+
className: 'pm_chip pm_chipToggle',
|
|
796
|
+
onClick: function () { setMcpCatsOpen(!mcpCatsOpen) },
|
|
797
|
+
}, mcpCatsOpen ? '收起 ▴' : '展开全部 ' + mcpCats.length + ' ▾')
|
|
798
|
+
: null,
|
|
707
799
|
),
|
|
800
|
+
mcpResult && mcpResult.total
|
|
801
|
+
? createElement('div', { className: 'pm_meta' },
|
|
802
|
+
(mcpCat
|
|
803
|
+
? '分类 ' + MCP_CAT_LABEL_OF(mcpCat) + ':共 ' + mcpResult.total.toLocaleString() + ' 个 MCP 服务'
|
|
804
|
+
: '共 ' + mcpResult.total.toLocaleString() + ' 个 MCP 服务,已加载 ' + loadedMcp.length + ' 条'),
|
|
805
|
+
)
|
|
806
|
+
: null,
|
|
708
807
|
createElement('div', { className: 'pm_grid' }, mcpRows),
|
|
709
|
-
mcpResult && mcpResult.items && !
|
|
808
|
+
mcpResult && mcpResult.items && !loadedMcp.length
|
|
710
809
|
? createElement('div', { className: 'pm_empty' }, '没有匹配的 MCP 服务。No matching MCP servers.')
|
|
711
810
|
: null,
|
|
811
|
+
mcpResult && mcpResult.hasMore
|
|
812
|
+
? createElement('div', { className: 'pm_actions', style: { justifyContent: 'center' } },
|
|
813
|
+
createElement('button', { className: 'pm_btn', disabled: mcpBusy, onClick: function () { findMcp(mcpSearch, mcpCat, mcpPage + 1, true) } }, '加载更多 ' + mcpResult.total.toLocaleString() + ' 中已看 ' + loadedMcp.length),
|
|
814
|
+
)
|
|
815
|
+
: null,
|
|
712
816
|
)
|
|
713
817
|
: createElement('div', { className: 'pm_section' },
|
|
714
818
|
createElement('div', { className: 'pm_search' },
|
|
@@ -716,23 +820,33 @@ window.__ModuleLoader__.load({
|
|
|
716
820
|
placeholder: '搜索技能,如 code-review / deploy Search…',
|
|
717
821
|
value: skillSearch,
|
|
718
822
|
onChange: function (e) { setSkillSearch(e.target.value) },
|
|
719
|
-
onKeyDown: function (e) { if (e.key === 'Enter') findSkills() },
|
|
823
|
+
onKeyDown: function (e) { if (e.key === 'Enter') findSkills(undefined, undefined, 1, false) },
|
|
720
824
|
}),
|
|
721
|
-
createElement('button', { className: 'pm_btn primary', disabled: skillBusy, onClick: function () { findSkills() } }, skillBusy ? '搜索中…' : '搜索'),
|
|
825
|
+
createElement('button', { className: 'pm_btn primary', disabled: skillBusy, onClick: function () { findSkills(undefined, undefined, 1, false) } }, skillBusy ? '搜索中…' : '搜索'),
|
|
722
826
|
),
|
|
723
827
|
createElement('div', { className: 'pm_chips' },
|
|
724
828
|
SKILL_CATEGORIES.map(function (c) {
|
|
725
829
|
return createElement('button', {
|
|
726
830
|
key: c.id || 'all',
|
|
727
831
|
className: 'pm_chip' + (category === c.id ? ' active' : ''),
|
|
728
|
-
onClick: function () { setCategory(c.id); findSkills(skillSearch, c.id) },
|
|
832
|
+
onClick: function () { setCategory(c.id); setSkillPage(1); findSkills(skillSearch, c.id, 1, false) },
|
|
729
833
|
}, c.label)
|
|
730
834
|
}),
|
|
731
835
|
),
|
|
836
|
+
skillResult && skillResult.total
|
|
837
|
+
? createElement('div', { className: 'pm_meta' },
|
|
838
|
+
'共 ' + skillResult.total.toLocaleString() + ' 个技能,已加载 ' + (skillResult.items ? skillResult.items.length : 0) + ' 条',
|
|
839
|
+
)
|
|
840
|
+
: null,
|
|
732
841
|
createElement('div', { className: 'pm_grid' }, skillRows),
|
|
733
842
|
skillResult && skillResult.items && !skillResult.items.length
|
|
734
843
|
? createElement('div', { className: 'pm_empty' }, '没有匹配的技能。No matching skills.')
|
|
735
844
|
: null,
|
|
845
|
+
skillResult && skillResult.hasMore
|
|
846
|
+
? createElement('div', { className: 'pm_actions', style: { justifyContent: 'center' } },
|
|
847
|
+
createElement('button', { className: 'pm_btn', disabled: skillBusy, onClick: function () { findSkills(skillSearch, category, skillPage + 1, true) } }, '加载更多 ' + skillResult.total.toLocaleString() + ' 中已看 ' + skillResult.items.length),
|
|
848
|
+
)
|
|
849
|
+
: null,
|
|
736
850
|
),
|
|
737
851
|
)
|
|
738
852
|
}
|
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.4",
|
|
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
|
}
|