feat: add Supermarket integration (MCP & Skill marketplace) (#309)

* feat: add Supermarket integration (MCP & Skill marketplace)

Backend:
- Add [supermarket] config section with base_url (default: supermarket.memoh.ai)
- Add SupermarketHandler with proxy endpoints for MCPs, Skills, and Tags
- Add install endpoints: POST /bots/:id/supermarket/install-mcp (creates MCP
  connection with env vars) and install-skill (downloads tar.gz, extracts to
  container via gRPC)
- Register handler in FX wiring, generate Swagger docs and TypeScript SDK

Frontend:
- Add /settings/supermarket route with Store icon in sidebar
- Create supermarket page with search, tag filtering, MCP and Skill sections
- Add MCP/Skill card components with tag badges and install buttons
- Add install dialogs: MCP (bot selector + env var form), Skill (bot selector)
- Add i18n entries for en.json and zh.json

* fix: improve supermarket install UX

- Create BotSelect component with avatar + name using UI Select
- Replace NativeSelect in install dialogs and usage page with BotSelect
- Change MCP install flow: navigate to bot detail MCP tab with pre-filled
  draft instead of direct install, letting users review before saving
- Move Supermarket sidebar entry between Browser and Usage

* web: remove supermarket page top tag selector bar

Drop the horizontal tag chips and getSupermarketTags fetch; keep
search and tag filter via card tag clicks with clearable badge.

* web: add homepage link to supermarket MCP and Skill cards

Show an external-link icon next to the card title when homepage is
available, opening in a new tab on click.
This commit is contained in:
Acbox Liu
2026-03-31 02:22:39 +08:00
committed by Acbox
parent 49e5f3d8ae
commit faaf13a0e9
25 changed files with 3168 additions and 24 deletions
+502
View File
@@ -4524,6 +4524,113 @@ const docTemplate = `{
}
}
},
"/bots/{bot_id}/supermarket/install-mcp": {
"post": {
"tags": [
"supermarket"
],
"summary": "Install MCP from supermarket to bot",
"parameters": [
{
"type": "string",
"description": "Bot ID",
"name": "bot_id",
"in": "path",
"required": true
},
{
"description": "Install MCP request",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.InstallMcpRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/github_com_memohai_memoh_internal_mcp.Connection"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"502": {
"description": "Bad Gateway",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/bots/{bot_id}/supermarket/install-skill": {
"post": {
"tags": [
"supermarket"
],
"summary": "Install skill from supermarket to bot container",
"parameters": [
{
"type": "string",
"description": "Bot ID",
"name": "bot_id",
"in": "path",
"required": true
},
{
"description": "Install skill request",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.InstallSkillRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": {
"type": "boolean"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"502": {
"description": "Bad Gateway",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/bots/{bot_id}/token-usage": {
"get": {
"description": "Get daily aggregated token usage for a bot, split by chat, heartbeat, and schedule session types, with optional model filter and per-model breakdown",
@@ -7756,6 +7863,204 @@ const docTemplate = `{
}
}
},
"/supermarket/mcps": {
"get": {
"tags": [
"supermarket"
],
"summary": "List MCPs from supermarket",
"parameters": [
{
"type": "string",
"description": "Search query",
"name": "q",
"in": "query"
},
{
"type": "string",
"description": "Filter by tag",
"name": "tag",
"in": "query"
},
{
"type": "string",
"description": "Filter by transport type",
"name": "transport",
"in": "query"
},
{
"type": "integer",
"description": "Page number",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "Items per page",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.SupermarketMcpListResponse"
}
},
"502": {
"description": "Bad Gateway",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/supermarket/mcps/{id}": {
"get": {
"tags": [
"supermarket"
],
"summary": "Get MCP detail from supermarket",
"parameters": [
{
"type": "string",
"description": "MCP ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.SupermarketMcpEntry"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"502": {
"description": "Bad Gateway",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/supermarket/skills": {
"get": {
"tags": [
"supermarket"
],
"summary": "List skills from supermarket",
"parameters": [
{
"type": "string",
"description": "Search query",
"name": "q",
"in": "query"
},
{
"type": "string",
"description": "Filter by tag",
"name": "tag",
"in": "query"
},
{
"type": "integer",
"description": "Page number",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "Items per page",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.SupermarketSkillListResponse"
}
},
"502": {
"description": "Bad Gateway",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/supermarket/skills/{id}": {
"get": {
"tags": [
"supermarket"
],
"summary": "Get skill detail from supermarket",
"parameters": [
{
"type": "string",
"description": "Skill ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.SupermarketSkillEntry"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"502": {
"description": "Bad Gateway",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/supermarket/tags": {
"get": {
"tags": [
"supermarket"
],
"summary": "List all tags from supermarket",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.SupermarketTagsResponse"
}
},
"502": {
"description": "Bad Gateway",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/tts-models": {
"get": {
"produces": [
@@ -10836,6 +11141,28 @@ const docTemplate = `{
}
}
},
"handlers.InstallMcpRequest": {
"type": "object",
"properties": {
"env": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"mcp_id": {
"type": "string"
}
}
},
"handlers.InstallSkillRequest": {
"type": "object",
"properties": {
"skill_id": {
"type": "string"
}
}
},
"handlers.ListSnapshotsResponse": {
"type": "object",
"properties": {
@@ -11130,6 +11457,181 @@ const docTemplate = `{
}
}
},
"handlers.SupermarketAuthor": {
"type": "object",
"properties": {
"email": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"handlers.SupermarketConfigVar": {
"type": "object",
"properties": {
"defaultValue": {
"type": "string"
},
"description": {
"type": "string"
},
"key": {
"type": "string"
}
}
},
"handlers.SupermarketMcpEntry": {
"type": "object",
"properties": {
"args": {
"type": "array",
"items": {
"type": "string"
}
},
"author": {
"$ref": "#/definitions/handlers.SupermarketAuthor"
},
"command": {
"type": "string"
},
"description": {
"type": "string"
},
"env": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.SupermarketConfigVar"
}
},
"headers": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.SupermarketConfigVar"
}
},
"homepage": {
"type": "string"
},
"icon": {
"type": "string"
},
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"tags": {
"type": "array",
"items": {
"type": "string"
}
},
"transport": {
"type": "string"
},
"url": {
"type": "string"
}
}
},
"handlers.SupermarketMcpListResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.SupermarketMcpEntry"
}
},
"limit": {
"type": "integer"
},
"page": {
"type": "integer"
},
"total": {
"type": "integer"
}
}
},
"handlers.SupermarketSkillEntry": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"description": {
"type": "string"
},
"files": {
"type": "array",
"items": {
"type": "string"
}
},
"id": {
"type": "string"
},
"metadata": {
"$ref": "#/definitions/handlers.SupermarketSkillMetadata"
},
"name": {
"type": "string"
}
}
},
"handlers.SupermarketSkillListResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.SupermarketSkillEntry"
}
},
"limit": {
"type": "integer"
},
"page": {
"type": "integer"
},
"total": {
"type": "integer"
}
}
},
"handlers.SupermarketSkillMetadata": {
"type": "object",
"properties": {
"author": {
"$ref": "#/definitions/handlers.SupermarketAuthor"
},
"homepage": {
"type": "string"
},
"tags": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"handlers.SupermarketTagsResponse": {
"type": "object",
"properties": {
"tags": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"handlers.TokenUsageResponse": {
"type": "object",
"properties": {