diff --git a/mateclaw-server/src/main/java/vip/mate/skill/template/SkillTemplate.java b/mateclaw-server/src/main/java/vip/mate/skill/template/SkillTemplate.java
new file mode 100644
index 00000000..65188a4e
--- /dev/null
+++ b/mateclaw-server/src/main/java/vip/mate/skill/template/SkillTemplate.java
@@ -0,0 +1,82 @@
+package vip.mate.skill.template;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * RFC-091 — definition of a skill creation template.
+ *
+ *
Templates are loaded from
+ * {@code resources/skill-templates/{id}/template.json} at startup. The
+ * frontend wizard renders the {@link #fields} as a form, the user fills
+ * them in, and the resulting key/value map is substituted into the
+ * {@link #skillMd} jinja-style placeholders to produce a manifest skill.
+ *
+ *
This is the data model only — see {@link SkillTemplateRegistry} for
+ * loading and {@link SkillTemplateService#instantiate} for the actual
+ * substitution + install path.
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@JsonInclude(JsonInclude.Include.NON_EMPTY)
+public class SkillTemplate {
+
+ /** Unique slug, matches the resource directory name. */
+ private String id;
+
+ private String name;
+ private String nameZh;
+ private String nameEn;
+ private String description;
+ private String descriptionZh;
+
+ /** prompt | knowledge | code | mcp | acp — drives wizard branching. */
+ private String type;
+
+ private String category;
+ private String icon;
+
+ /** Form fields the wizard renders. */
+ @Builder.Default
+ private List fields = List.of();
+
+ /**
+ * Skill body with {@code {{placeholders}}} ready for substitution.
+ * Stored verbatim from {@code template.json}; line endings preserved.
+ */
+ private String skillMd;
+
+ /** Forward-compat catch-all for fields the wizard hasn't typed yet. */
+ @Builder.Default
+ private Map extras = Map.of();
+
+ @Data
+ @Builder
+ @NoArgsConstructor
+ @AllArgsConstructor
+ @JsonInclude(JsonInclude.Include.NON_EMPTY)
+ public static class TemplateField {
+ private String key;
+ private String label;
+ /** text | textarea | select | toggle | kb-picker */
+ private String type;
+ @Builder.Default
+ private boolean required = false;
+ private String placeholder;
+ private String hint;
+ /** JSON {@code "default"} maps here — {@code default} is a Java keyword. */
+ @JsonProperty("default")
+ private Object defaultValue;
+ @Builder.Default
+ private List> options = List.of();
+ }
+}
diff --git a/mateclaw-server/src/main/java/vip/mate/skill/template/SkillTemplateController.java b/mateclaw-server/src/main/java/vip/mate/skill/template/SkillTemplateController.java
new file mode 100644
index 00000000..5ed11ac0
--- /dev/null
+++ b/mateclaw-server/src/main/java/vip/mate/skill/template/SkillTemplateController.java
@@ -0,0 +1,55 @@
+package vip.mate.skill.template;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+import vip.mate.common.result.R;
+import vip.mate.skill.model.SkillEntity;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * RFC-091 — REST entry point for the wizard UI.
+ *
+ * Three operations:
+ *
+ * {@code GET /api/v1/skill-templates} — list gallery
+ * {@code GET /api/v1/skill-templates/{id}} — single template
+ * definition (for the wizard form)
+ * {@code POST /api/v1/skill-templates/{id}/instantiate} —
+ * create a skill from filled-in values
+ *
+ */
+@Tag(name = "Skill Template Wizard")
+@RestController
+@RequestMapping("/api/v1/skill-templates")
+@RequiredArgsConstructor
+public class SkillTemplateController {
+
+ private final SkillTemplateRegistry registry;
+ private final SkillTemplateService templateService;
+
+ @Operation(summary = "List skill templates (RFC-091)")
+ @GetMapping
+ public R> list() {
+ return R.ok(registry.all());
+ }
+
+ @Operation(summary = "Get a single skill template")
+ @GetMapping("/{id}")
+ public R get(@PathVariable String id) {
+ SkillTemplate t = registry.find(id);
+ if (t == null) return R.fail("Template not found: " + id);
+ return R.ok(t);
+ }
+
+ @Operation(summary = "Instantiate a template into a skill")
+ @PostMapping("/{id}/instantiate")
+ public R instantiate(
+ @PathVariable String id,
+ @RequestBody Map values) {
+ return R.ok(templateService.instantiate(id, values));
+ }
+}
diff --git a/mateclaw-server/src/main/java/vip/mate/skill/template/SkillTemplateRegistry.java b/mateclaw-server/src/main/java/vip/mate/skill/template/SkillTemplateRegistry.java
new file mode 100644
index 00000000..379e2945
--- /dev/null
+++ b/mateclaw-server/src/main/java/vip/mate/skill/template/SkillTemplateRegistry.java
@@ -0,0 +1,78 @@
+package vip.mate.skill.template;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.annotation.PostConstruct;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.stereotype.Component;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * RFC-091 — registry for built-in skill creation templates.
+ *
+ * Loads {@code skill-templates/*/template.json} from the
+ * classpath at startup and exposes them via {@link #all()} /
+ * {@link #find(String)}. We intentionally don't watch the filesystem —
+ * additions land via redeploys, mirroring how SKILL.md skills work.
+ *
+ *
Templates with malformed JSON are logged and skipped rather than
+ * failing startup, so a single bad template can't block the rest.
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class SkillTemplateRegistry {
+
+ private static final String TEMPLATE_PATTERN = "classpath:/skill-templates/*/template.json";
+
+ private final ObjectMapper objectMapper;
+
+ /**
+ * Insertion order matches the alphabetical scan, which keeps the
+ * gallery deterministic across deployments. Frontend can re-sort.
+ */
+ private final Map templates = new LinkedHashMap<>();
+
+ @PostConstruct
+ public void load() {
+ templates.clear();
+ try {
+ PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
+ Resource[] resources = resolver.getResources(TEMPLATE_PATTERN);
+ for (Resource r : resources) {
+ try (InputStream in = r.getInputStream()) {
+ SkillTemplate tpl = objectMapper.readValue(in, SkillTemplate.class);
+ if (tpl.getId() == null || tpl.getId().isBlank()) {
+ log.warn("Skipping skill template at {} — missing id", r.getDescription());
+ continue;
+ }
+ templates.put(tpl.getId(), tpl);
+ log.info("Loaded skill template: {} ({})", tpl.getId(), tpl.getType());
+ } catch (Exception e) {
+ log.warn("Failed to parse skill template at {}: {}",
+ r.getDescription(), e.getMessage());
+ }
+ }
+ log.info("SkillTemplateRegistry loaded {} template(s)", templates.size());
+ } catch (Exception e) {
+ log.warn("Skill template scan failed; gallery will be empty: {}", e.getMessage());
+ }
+ }
+
+ /** All registered templates, deterministic order. */
+ public List all() {
+ return new ArrayList<>(templates.values());
+ }
+
+ /** Lookup by id; null when missing. */
+ public SkillTemplate find(String id) {
+ return templates.get(id);
+ }
+}
diff --git a/mateclaw-server/src/main/java/vip/mate/skill/template/SkillTemplateService.java b/mateclaw-server/src/main/java/vip/mate/skill/template/SkillTemplateService.java
new file mode 100644
index 00000000..3e2239fc
--- /dev/null
+++ b/mateclaw-server/src/main/java/vip/mate/skill/template/SkillTemplateService.java
@@ -0,0 +1,147 @@
+package vip.mate.skill.template;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import vip.mate.exception.MateClawException;
+import vip.mate.skill.model.SkillEntity;
+import vip.mate.skill.service.SkillService;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * RFC-091 — instantiate a {@link SkillTemplate} into a real
+ * {@code mate_skill} row.
+ *
+ * Algorithm:
+ *
+ * Validate every required field has a non-blank value.
+ * Compute auxiliary placeholders (e.g. {@code citation_string}
+ * from a boolean toggle) so the SKILL.md doesn't need conditional
+ * logic.
+ * Substitute {@code {{key}}} occurrences in the template body.
+ * Build a {@link SkillEntity} and hand it to
+ * {@link SkillService#createSkill}, which persists the row,
+ * initializes the workspace directory, and refreshes the runtime
+ * cache. The resolver will then pick up the manifest.
+ *
+ *
+ * This intentionally goes through {@code SkillService.createSkill}
+ * rather than the install task pipeline — the wizard produces a local
+ * skill from in-process content, no bundle download involved.
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class SkillTemplateService {
+
+ private static final Pattern PLACEHOLDER = Pattern.compile("\\{\\{([a-zA-Z0-9_]+)}}");
+
+ private final SkillTemplateRegistry registry;
+ private final SkillService skillService;
+
+ /**
+ * Instantiate the template by id, substituting fields, and create
+ * the skill. Returns the created {@link SkillEntity}.
+ *
+ * @param templateId id from the registry (e.g. {@code tcm-qa})
+ * @param values user-supplied field values; missing required
+ * fields throw a translatable exception
+ */
+ public SkillEntity instantiate(String templateId, Map values) {
+ SkillTemplate template = registry.find(templateId);
+ if (template == null) {
+ throw new MateClawException("err.skill_template.not_found",
+ "Skill template not found: " + templateId);
+ }
+ if (values == null) values = Map.of();
+
+ // 1. validate + collect into a single substitution map
+ Map substitutions = collectSubstitutions(template, values);
+
+ // 2. render SKILL.md
+ String skillMd = render(template.getSkillMd(), substitutions);
+
+ // 3. build entity and create via SkillService (which already
+ // handles uniqueness, defaults, workspace init, runtime
+ // cache refresh).
+ SkillEntity entity = new SkillEntity();
+ entity.setName(substitutions.get("skill_name"));
+ String displayZh = substitutions.getOrDefault("display_name_zh", "");
+ String displayEn = substitutions.getOrDefault("display_name_en",
+ substitutions.getOrDefault("display_name", ""));
+ entity.setNameZh(displayZh.isBlank() ? null : displayZh);
+ entity.setNameEn(displayEn.isBlank() ? null : displayEn);
+ entity.setDescription(template.getDescription());
+ entity.setSkillType(mapType(template.getType()));
+ entity.setIcon(template.getIcon());
+ entity.setVersion("1.0.0");
+ entity.setAuthor("skill-template-wizard");
+ entity.setSkillContent(skillMd);
+ entity.setEnabled(true);
+
+ return skillService.createSkill(entity);
+ }
+
+ private String mapType(String type) {
+ if (type == null) return "dynamic";
+ // RFC-090 §5.1 introduced more types, but mate_skill.skill_type
+ // historically only knows builtin / mcp / dynamic. Map knowledge /
+ // prompt back to dynamic for legacy callers; the manifest_json
+ // column carries the real v3 type.
+ return switch (type) {
+ case "mcp" -> "mcp";
+ case "builtin" -> "builtin";
+ default -> "dynamic";
+ };
+ }
+
+ private Map collectSubstitutions(SkillTemplate template,
+ Map values) {
+ Map out = new LinkedHashMap<>();
+ for (SkillTemplate.TemplateField field : template.getFields()) {
+ Object raw = values.get(field.getKey());
+ String resolved = raw == null ? null : raw.toString().trim();
+ if ((resolved == null || resolved.isBlank())) {
+ if (field.getDefaultValue() != null) {
+ resolved = field.getDefaultValue().toString();
+ } else if (field.isRequired()) {
+ throw new MateClawException("err.skill_template.missing_field",
+ "Required field missing: " + field.getKey());
+ } else {
+ resolved = "";
+ }
+ }
+ out.put(field.getKey(), resolved);
+ }
+ // Auxiliary derived placeholders so SKILL.md templates stay simple.
+ if (out.containsKey("citation_required")) {
+ boolean req = Boolean.parseBoolean(out.get("citation_required"));
+ out.put("citation_string", req ? "required" : "optional");
+ out.put("citation_instruction", req
+ ? "**每条建议必须标明引用的 KB 出处** ({{citation}} 自动注入)。"
+ : "如有 KB 引用,按 {{citation}} 标注;否则可省略。");
+ }
+ if (out.containsKey("output_language")) {
+ String lang = out.get("output_language");
+ out.put("output_language_label", "zh".equalsIgnoreCase(lang) ? "中文" : "English");
+ }
+ return out;
+ }
+
+ private String render(String template, Map values) {
+ if (template == null) return "";
+ Matcher m = PLACEHOLDER.matcher(template);
+ StringBuilder sb = new StringBuilder();
+ while (m.find()) {
+ String key = m.group(1);
+ String replacement = values.getOrDefault(key, "");
+ m.appendReplacement(sb, Matcher.quoteReplacement(replacement));
+ }
+ m.appendTail(sb);
+ return sb.toString();
+ }
+}
diff --git a/mateclaw-server/src/main/resources/skill-templates/meeting-summarizer/template.json b/mateclaw-server/src/main/resources/skill-templates/meeting-summarizer/template.json
new file mode 100644
index 00000000..f67ef4e5
--- /dev/null
+++ b/mateclaw-server/src/main/resources/skill-templates/meeting-summarizer/template.json
@@ -0,0 +1,45 @@
+{
+ "id": "meeting-summarizer",
+ "name": "Meeting Summarizer",
+ "nameZh": "会议纪要助手",
+ "category": "content",
+ "type": "prompt",
+ "icon": "📝",
+ "description": "Pure-prompt skill that turns a meeting transcript into a structured summary (decisions, action items, owners).",
+ "descriptionZh": "把会议转录稿整理成结构化纪要(决策、行动项、负责人)。",
+ "fields": [
+ {
+ "key": "skill_name",
+ "label": "Skill 标识 (slug)",
+ "type": "text",
+ "required": true,
+ "placeholder": "team-meeting-notes"
+ },
+ {
+ "key": "display_name",
+ "label": "显示名",
+ "type": "text",
+ "required": false,
+ "placeholder": "Team Meeting Notes"
+ },
+ {
+ "key": "team_context",
+ "label": "团队上下文",
+ "type": "textarea",
+ "required": false,
+ "default": "产研团队,敏捷开发,迭代节奏 2 周。",
+ "hint": "帮助模型理解参会人角色与术语"
+ },
+ {
+ "key": "output_language",
+ "label": "输出语言",
+ "type": "select",
+ "default": "zh",
+ "options": [
+ { "value": "zh", "label": "中文" },
+ { "value": "en", "label": "English" }
+ ]
+ }
+ ],
+ "skillMd": "---\nname: {{skill_name}}\ndescription: {{display_name}} - 把会议转录整理为结构化纪要\ntype: prompt\nicon: \"📝\"\ncategory: content\nversion: 1.0.0\nauthor: skill-template-wizard\nself-evolution:\n lessons_enabled: true\n---\n\n# {{display_name}}\n\n你是会议纪要助手。**团队背景**:{{team_context}}\n\n输出格式({{output_language_label}}):\n\n1. **核心决策** — 列点,每条 1 句话\n2. **行动项 (Action Items)** — 表格:负责人 / 任务 / Due\n3. **风险与待办** — 列点\n4. **下次同步** — 时间 + 议题草案\n\n约束:\n- 不要把闲聊误判为决策\n- 没有明确负责人的任务标 `Owner: TBD`\n- 提到具体日期时统一为 `YYYY-MM-DD`\n"
+}
diff --git a/mateclaw-server/src/main/resources/skill-templates/tcm-qa/template.json b/mateclaw-server/src/main/resources/skill-templates/tcm-qa/template.json
new file mode 100644
index 00000000..9da9dc0f
--- /dev/null
+++ b/mateclaw-server/src/main/resources/skill-templates/tcm-qa/template.json
@@ -0,0 +1,49 @@
+{
+ "id": "tcm-qa",
+ "name": "中医药知识问答",
+ "nameEn": "TCM Knowledge Q&A",
+ "category": "data",
+ "type": "knowledge",
+ "icon": "🌿",
+ "description": "Domain Q&A skill backed by a Wiki KB (e.g. 古籍 / 临床指南). Bind to a KB and start asking — citations are required by default.",
+ "descriptionZh": "中医师 / 药师可绑定古籍 / 临床指南 KB,自动 RAG + 引用追踪。",
+ "fields": [
+ {
+ "key": "skill_name",
+ "label": "Skill 标识 (slug)",
+ "type": "text",
+ "required": true,
+ "placeholder": "tcm-classics-qa",
+ "hint": "小写英文 + 连字符;用作 SKILL.md 的 name 字段"
+ },
+ {
+ "key": "display_name_zh",
+ "label": "显示名 (中文)",
+ "type": "text",
+ "required": false,
+ "placeholder": "中医药问答助手"
+ },
+ {
+ "key": "kb_slug",
+ "label": "Wiki KB slug",
+ "type": "kb-picker",
+ "required": true,
+ "hint": "选择已存在的 Wiki 知识库;不存在请先到 Wiki 页面创建"
+ },
+ {
+ "key": "expert_persona",
+ "label": "专家人设",
+ "type": "textarea",
+ "required": true,
+ "default": "你是有 20 年临床经验的中医师,擅长辨证论治。",
+ "hint": "skill 的角色定位,会注入到 system prompt"
+ },
+ {
+ "key": "citation_required",
+ "label": "强制引用古籍出处",
+ "type": "toggle",
+ "default": true
+ }
+ ],
+ "skillMd": "---\nname: {{skill_name}}\ndescription: {{display_name_zh}} - 基于 Wiki KB 的领域问答\ntype: knowledge\nicon: \"🌿\"\ncategory: data\nversion: 1.0.0\nauthor: skill-template-wizard\nknowledge:\n bind_kb: {{kb_slug}}\n retrieval: hybrid\n top_k: 6\n citation: {{citation_string}}\n rerank: true\nself-evolution:\n lessons_enabled: true\n lessons_max_entries: 30\n---\n\n# {{display_name_zh}}\n\n{{expert_persona}}\n\n基于检索到的 KB 内容,结合用户描述给出专业建议。\n{{citation_instruction}}\n\n如果检索结果不足以支撑判断,告知用户「需要更多信息」,不要编造。\n"
+}
diff --git a/mateclaw-ui/src/api/index.ts b/mateclaw-ui/src/api/index.ts
index f5e3ba8a..94db8b4a 100644
--- a/mateclaw-ui/src/api/index.ts
+++ b/mateclaw-ui/src/api/index.ts
@@ -176,6 +176,14 @@ export const skillApi = {
clearLessons: (id: string | number) => http.post(`/skills/${id}/lessons/clear`),
}
+// ==================== Skill Templates (RFC-091) ====================
+export const skillTemplateApi = {
+ list: () => http.get('/skill-templates'),
+ get: (id: string) => http.get(`/skill-templates/${id}`),
+ instantiate: (id: string, values: Record) =>
+ http.post(`/skill-templates/${id}/instantiate`, values),
+}
+
// ==================== Skill Install ====================
export const skillInstallApi = {
searchHub: (q: string, limit = 20) =>
diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts
index be46f1d7..cea8c1e6 100644
--- a/mateclaw-ui/src/i18n/locales/en-US.ts
+++ b/mateclaw-ui/src/i18n/locales/en-US.ts
@@ -1195,6 +1195,30 @@ export default {
toggleFailed: 'Failed to toggle tool status',
},
},
+ skillTemplates: {
+ kicker: 'Wizard',
+ title: 'Skill Templates',
+ desc: 'Pick a starter template, fill in a few fields, and a manifest skill is created in your workspace.',
+ backToSkills: '← Skills',
+ backToGallery: 'Back to gallery',
+ fields: 'fields',
+ useTemplate: 'Use template',
+ empty: 'No skill templates available yet.',
+ step1: 'Fill parameters',
+ step2: 'Preview',
+ step3: 'Done',
+ next: 'Next',
+ back: 'Back',
+ selectKb: '— Select Knowledge Base —',
+ previewHint: 'Preview of the SKILL.md that will be created. Click Install to commit.',
+ installSkill: 'Install Skill',
+ installing: 'Installing...',
+ installFailed: 'Installation failed',
+ installed: 'Skill installed',
+ installedDesc: '{name} is ready in your workspace.',
+ installAnother: 'Install another',
+ viewInSkills: 'View in Skills',
+ },
plugins: {
title: 'Plugins',
desc: 'Manage external plugins loaded from JAR files',
@@ -1858,6 +1882,7 @@ export default {
desc: 'Manage tools and skills available to your agents',
newSkill: 'New Skill',
importSkill: 'Import Skill',
+ browseTemplates: 'Browse Templates',
refreshRuntime: 'Refresh Runtime',
refreshing: 'Refreshing...',
refreshSuccess: 'Active skills refreshed',
diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts
index 698dccc7..beb135c3 100644
--- a/mateclaw-ui/src/i18n/locales/zh-CN.ts
+++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts
@@ -1195,6 +1195,30 @@ export default {
toggleFailed: '切换工具状态失败',
},
},
+ skillTemplates: {
+ kicker: '创作向导',
+ title: 'Skill 模板库',
+ desc: '选一个起步模板,填几个字段,模板向导会在你的工作区里生成一个标准 manifest skill。',
+ backToSkills: '← 返回 Skills',
+ backToGallery: '返回模板列表',
+ fields: '个字段',
+ useTemplate: '使用模板',
+ empty: '暂无可用模板。',
+ step1: '填写参数',
+ step2: '预览',
+ step3: '完成',
+ next: '下一步',
+ back: '上一步',
+ selectKb: '— 选择知识库 —',
+ previewHint: '即将生成的 SKILL.md 预览。确认无误后点 Install Skill 创建。',
+ installSkill: '安装 Skill',
+ installing: '安装中...',
+ installFailed: '安装失败',
+ installed: 'Skill 已安装',
+ installedDesc: '{name} 已添加到你的工作区。',
+ installAnother: '再装一个',
+ viewInSkills: '到 Skills 查看',
+ },
plugins: {
title: '插件',
desc: '管理从 JAR 文件加载的外部插件',
@@ -1860,6 +1884,7 @@ export default {
desc: '管理 Agent 可用的工具和技能',
newSkill: '新建技能',
importSkill: '导入技能',
+ browseTemplates: '浏览模板',
refreshRuntime: '刷新运行时',
refreshing: '刷新中...',
refreshSuccess: '运行时技能已刷新',
diff --git a/mateclaw-ui/src/router/index.ts b/mateclaw-ui/src/router/index.ts
index 963011da..472e07a3 100644
--- a/mateclaw-ui/src/router/index.ts
+++ b/mateclaw-ui/src/router/index.ts
@@ -63,6 +63,13 @@ const router = createRouter({
component: () => import('@/views/Security/Activity/index.vue'),
meta: { title: 'Activity' },
},
+ // RFC-091: Skill 模板库 + 创作向导
+ {
+ path: 'skills/templates',
+ name: 'SkillTemplates',
+ component: () => import('@/views/SkillTemplates.vue'),
+ meta: { title: 'Skill Templates' },
+ },
{
path: 'plugins',
name: 'Plugins',
diff --git a/mateclaw-ui/src/views/SkillMarket.vue b/mateclaw-ui/src/views/SkillMarket.vue
index 422a6f61..c502328c 100644
--- a/mateclaw-ui/src/views/SkillMarket.vue
+++ b/mateclaw-ui/src/views/SkillMarket.vue
@@ -16,6 +16,13 @@
{{ refreshing ? t('skills.refreshing') : t('skills.refreshRuntime') }}
+
+
+
+
+
+ {{ t('skills.browseTemplates') }}
+
diff --git a/mateclaw-ui/src/views/SkillTemplates.vue b/mateclaw-ui/src/views/SkillTemplates.vue
new file mode 100644
index 00000000..f26f9e8c
--- /dev/null
+++ b/mateclaw-ui/src/views/SkillTemplates.vue
@@ -0,0 +1,327 @@
+
+
+
+
+
+
+
+
+
+
+
{{ tpl.icon || '🧩' }}
+
+
{{ tpl.type }}
+
+
{{ tpl.descriptionZh || tpl.description }}
+
+
+
+ {{ t('skillTemplates.empty') }}
+
+
+
+
+
+
+
← {{ t('skillTemplates.backToGallery') }}
+
{{ selectedTemplate.nameZh || selectedTemplate.name }}
+
{{ selectedTemplate.descriptionZh || selectedTemplate.description }}
+
+
+
+
+ 1
+ {{ t('skillTemplates.step1') }}
+
+
+ 2
+ {{ t('skillTemplates.step2') }}
+
+
+ 3
+ {{ t('skillTemplates.step3') }}
+
+
+
+
+
+
+
+ {{ t('common.cancel') }}
+
+ {{ t('skillTemplates.next') }} →
+
+
+
+
+
+
+
{{ t('skillTemplates.previewHint') }}
+
{{ renderedSkillMd }}
+
+ ← {{ t('skillTemplates.back') }}
+
+ {{ installing ? t('skillTemplates.installing') : t('skillTemplates.installSkill') }}
+
+
+
+
+
+
+
+
✅
+
{{ t('skillTemplates.installed') }}
+
{{ t('skillTemplates.installedDesc', { name: form.skill_name }) }}
+
+ {{ t('skillTemplates.installAnother') }}
+ {{ t('skillTemplates.viewInSkills') }} →
+
+
+
+
+
+
+
+
+
+
+
+