@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.
Files changed (3) hide show
  1. package/client.js +143 -28
  2. package/index.js +113 -23
  3. 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
- function findMcp(term) {
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
- setMcpResult(r)
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
- var mcpRows = mcpResult && mcpResult.items
650
- ? mcpResult.items.map(function (s) {
651
- var d = mcpDetail[s.id]
652
- return createElement(McpMarketItem, {
653
- key: s.id, item: s, detail: d,
654
- onOpen: function () { if (!d) openMcp(s.id) },
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 () { if (!d) openSkill(s.id) },
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 && !mcpResult.items.length
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 || '复制配置 Copy')
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 !== undefined && d.stars !== null ? badge('★ ' + fmtNum(d.stars), 'muted') : null,
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: '20' })
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
- return json(res, 200, { ok: true, items })
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 = { page_number: page, page_size: 20 }
547
- if (search) body.search = search
548
- const key = `mcp?${JSON.stringify(body)}`
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 marketFetch(key, '/mcp/servers', {
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 items = (data?.data?.mcp_server_list ?? []).map((s) => ({
573
- id: s.id,
574
- name: s.chinese_name || s.name,
575
- description: s.description,
576
- publisher: s.publisher,
577
- tags: s.tags,
578
- logo: s.logo_url,
579
- views: s.view_count,
580
- categories: s.categories,
581
- configured: localMatches.some((local) => local.includes(s.id.toLowerCase())),
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
- return json(res, 200, { ok: true, items })
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.1",
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
  }