Skip to main content

Create a Text Generation AI Skill

Scenario

Enable AI to write copy, translate, summarize, and generate code for users within a mini program. Users simply say "write a notice for me" or "translate this passage", and the AI will invoke your atomic interface to generate text and display the result.

This article demonstrates the core development pattern by building a text-gen-skill from scratch: mcp.json declaration + direct invocation of the AI gateway from the mini program. Text generation does not go through cloud functions.

Prerequisites

Implementation Steps

Step 1: Create the SKILL Scaffold

# Run in the project root directory
npx mp-skills create text-gen-skill

Expected output:

* 创建 Skill: text-gen-skill
ok 目录已创建: miniprogram/skills/text-gen-skill/
ok SKILL.md 已创建
ok mcp.json 已创建
ok index.js 已创建
ok apis/ 目录已创建
ok app.json 已更新(agent.skills)
ok project.config.json 已更新

[OK] Skill 创建完成

File structure generated by the scaffold:

miniprogram/skills/text-gen-skill/
├── SKILL.md # Skill description — the AI engine reads this to determine when to trigger
├── mcp.json # Atomic interface declaration — defines generateText parameters and return values
├── index.js # Entry point — registers atomic interfaces
└── apis/
└── generateText.js # Interface implementation — calls the CloudBase AI gateway

Step 2: Write SKILL.md

SKILL.md tells the AI engine when this skill should and should not be triggered. Write miniprogram/skills/text-gen-skill/SKILL.md:

---
name: text-gen-skill
description: AI text generation: AI writing, copy generation, code generation, translation, summarization, Q&A, supports cloudbase / deepseek / hunyuan models. Only handles pure text generation needs, does not handle image-related needs
version: "1.0.0"
tags: ["微信小程序", "AI开发模式", "平台能力"]
platform: ["wechat-miniprogram"]
---

# AI Text Generation

## Trigger Scenarios

Examples of user prompts:

- **Article writing**: "Write a WeChat public account article about coffee culture", "Write a Xiaohongshu post recommending spring drinks"
- **Copy generation**: "Write a tagline for a coffee shop", "Generate an ad copy for a new product launch"
- **Code generation**: "Write a bubble sort in Python", "Write a cloud function template for me"
- **Translation**: "Translate this passage into English", "Translate into Chinese"
- **Summarization**: "Summarize the key points of this article"
- **Q&A conversation**: "What is CloudBase", "Explain the mini program lifecycle"

## Out of Scope

- Image generation, image editing, and other visual requests → not within this skill's scope, handled by image-gen-skill / image-edit-skill
- Real-time information queries requiring web search → not within this skill's scope
- Operations requiring calls to specific business APIs (e.g., ordering, queuing) → not within this skill's scope

SKILL.md YAML frontmatter rules:

FieldDescriptionRequired
nameSkill name, must match the directory nameYes
descriptionOne-line description, used by the AI engine to determine trigger时机Yes
versionSemantic versioningRecommended
tagsClassification tagsRecommended
platformTarget platform, fixed as ["wechat-miniprogram"]Yes

Step 3: Configure mcp.json

mcp.json is the "contract file" for atomic interfaces, declaring the name, parameters, and return values of each interface. Write miniprogram/skills/text-gen-skill/mcp.json:

{
"apis": [
{
"name": "generateText",
"description": "AI text generation: generates text content based on user input prompt, supporting writing, translation, code generation, summarization, Q&A and other scenarios.\nPrecondition before calling: The user has clearly expressed the need to generate text content.\n【Prohibited scenarios】 Do NOT call when the user has not expressed specific content needs; Do NOT call when the user expresses image generation/editing needs, redirect to image-gen-skill or image-edit-skill; Do NOT call when the user expresses real-time search/query needs.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"prompt": {
"type": "string",
"description": "The user's main input prompt, i.e., the description of the content to be generated by AI. Source of value: the user's original words. For example, if the user says \"Write an introduction copy for a coffee shop\", then prompt is \"Write an introduction copy for a coffee shop\". 【Do NOT fabricate】 Must be extracted or reasonably paraphrased from the user's original words."
},
"systemPrompt": {
"type": "string",
"description": "System prompt, used to set the AI's role and output style. Optional parameter. Automatically inferred based on user intent: writing articles → \"You are a professional copywriter...\", translation → \"You are a professional translator...\", code → \"You are a programming expert...\", do not pass when the user has not explicitly specified a role."
}
},
"required": ["prompt"],
"additionalProperties": false
},
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "AI generated text content"
}
},
"required": ["text"],
"additionalProperties": false
},
"_meta": {
"ui": {
"componentPath": "components/text-result-card/index"
}
}
}
],
"components": [
{
"path": "components/text-result-card/index",
"relatedPage": "/pages/home/home"
}
]
}

mcp.json core field descriptions:

FieldDescription
apis[].nameAtomic interface name, must match the registration name in index.js
apis[].descriptionInterface description, the AI engine determines when to call based on this. Must clearly state preconditions and prohibited scenarios
apis[].inputSchemaJSON Schema defining input parameters, required marks mandatory fields
apis[].outputSchemaOutput parameter definition, the AI engine parses the response based on the schema
apis[]._meta.ui.componentPathPath to the result display component
componentsComponent registration, mapping component paths to their usage pages

Step 4: Implement apis/generateText.js

This is the core logic: receives a prompt, calls wx.cloud.extend.AI to generate text, and returns the result.

Write miniprogram/skills/text-gen-skill/apis/generateText.js:

const { isPreviewMode, ensureCloudInit, successResult, errorResult } = require('../utils/util')
const { seedData } = require('../data/seed')
const { translateError } = require('../../_shared/mp-skills-shared/utils/cloud-error-handler')

async function generateText(params = {}) {
const { prompt, systemPrompt, model = 'hy3-preview', temperature = 0.7, maxTokens = 2048 } = params

if (!prompt) {
return errorResult('Missing prompt parameter. Please provide a description of the text content to generate.')
}

// Preview mode: return mock data
if (isPreviewMode()) {
const data = seedData({ prompt, model, systemPrompt })
return successResult(
`Text generated (model: ${model}, preview mode)`,
{ text: data.text, model: data.model, usage: data.usage },
{ rawText: data.text }
)
}

// Production mode: directly call wx.cloud.extend.AI
try {
ensureCloudInit()
const aiModel = wx.cloud.extend.AI.createModel('cloudbase')

const messages = []
if (systemPrompt) {
messages.push({ role: 'system', content: systemPrompt })
}
messages.push({ role: 'user', content: prompt })

const res = await aiModel.generateText({
model: modelToApiName(model),
messages,
temperature,
max_tokens: maxTokens
})

const text = res.choices && res.choices[0] && res.choices[0].message
? res.choices[0].message.content
: (res.text || '')
const usage = res.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }

return successResult(
`Text generation complete (model: ${model})`,
{
text,
model,
usage: {
promptTokens: usage.prompt_tokens || 0,
completionTokens: usage.completion_tokens || 0,
totalTokens: usage.total_tokens || 0
}
},
{ rawText: text }
)
} catch (err) {
console.error('[generateText] error:', err)
const friendlyMsg = translateError(err, 'text-gen-handler')
return errorResult(friendlyMsg)
}
}

function modelToApiName(model) {
const map = {
cloudbase: 'deepseek-v4-flash',
'deepseek-v4': 'deepseek-v4-flash',
hunyuan: 'hunyuan-2.0-instruct-20251111'
}
return map[model] || model
}

module.exports = generateText

Code highlights:

AspectDescription
wx.cloud.extend.AI.createModel('cloudbase')The mini program SDK's built-in AI gateway, directly accessing CloudBase large models. Note: This is a wx API, not an npm package
model parameterDefault is hy3-preview (Tencent Hunyuan preview version), free to use with the Mini Program Growth Plan. Can be switched to deepseek-v4-flash, hunyuan-2.0-instruct-20251111, etc. via parameters
Preview modeisPreviewMode() is based on a local storage flag. When enabled, returns mock data for debugging without a backend connection
translateErrorShared utility that converts raw CloudBase API error codes into user-friendly Chinese messages (e.g., insufficient tokens, model not enabled)
ensureCloudInit()Ensures wx.cloud.init() is only called once, preventing duplicate initialization
successResult / errorResultStandard response format following the wx.modelContext specification: { isError, content, structuredContent, _meta }
modelToApiNameMaps user-friendly model names (cloudbase, deepseek-v4, hunyuan) to actual API model IDs

Step 5: Implement the Result Display Component

The component receives the atomic interface's structured result and renders it. The following files need to be created:

miniprogram/skills/text-gen-skill/components/text-result-card/index.wxml:

<view class="wecard">
<!-- Header -->
<view class="wecard-header">
<text class="tag tag-model">{{model || 'cloudbase'}}</text>
</view>

<!-- Content: text area -->
<view class="wecard-content">
<text class="text-body">{{displayText}}</text>
<button wx:if="{{truncated}}" class="btn-ghost btn-expand" bindtap="onTapExpand">
Expand full text
</button>
</view>

<!-- Footer: action area -->
<view class="wecard-footer">
<button class="btn-secondary" bindtap="onTapRegenerate">Regenerate</button>
</view>
</view>

miniprogram/skills/text-gen-skill/components/text-result-card/index.js:

Component({
data: {
text: '',
model: '',
usage: null,
truncated: false,
displayText: '',
maxPreviewLength: 300
},
lifetimes: {
created() {
const { NotificationType } = wx.modelContext
const modelCtx = wx.modelContext.getContext(this)
modelCtx.on(NotificationType.Result, (data) => {
const sc = (data && data.result && data.result.structuredContent) || {}
const text = sc.text || ''
const model = sc.model || ''
const usage = sc.usage || null
const truncated = text.length > this.data.maxPreviewLength

this.setData({
text,
model,
usage,
truncated,
displayText: truncated ? text.slice(0, this.data.maxPreviewLength) + '...' : text
})
})
}
},
methods: {
onTapRegenerate() {
wx.modelContext.getContext(this).sendFollowUpMessage({
content: [
{ type: 'text', text: 'Regenerate the same content' },
{
type: 'api/call',
data: {
name: 'generateText',
arguments: { prompt: this.data.text, model: this.data.model }
}
}
]
})
},
onTapExpand() {
this.setData({
truncated: false,
displayText: this.data.text
})
}
}
})

miniprogram/skills/text-gen-skill/components/text-result-card/index.wxss:

.wecard {
display: flex;
flex-direction: column;
padding: 16px;
background: #FFFFFF;
border-radius: 12px;
box-sizing: border-box;
width: 100%;
}

.wecard-header {
display: flex;
align-items: center;
height: 40px;
margin-bottom: 8px;
}

.tag {
display: flex;
align-items: center;
height: 20px;
padding: 0 8px;
font-size: 12px;
font-weight: 400;
border-radius: 10px;
background: #F5F5F7;
color: #6E6E73;
line-height: 20px;
}

.wecard-content {
flex: 1;
margin-bottom: 12px;
}

.text-body {
display: block;
font-size: 14px;
font-weight: 400;
color: #1D1D1F;
line-height: 1.6;
word-break: break-word;
}

.wecard-footer {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}

.btn-secondary {
height: 40px;
min-width: 96px;
padding: 0 16px;
font-size: 14px;
font-weight: 500;
color: #1D1D1F;
background: #F2F2F7;
border-radius: 999px;
border: none;
line-height: 40px;
text-align: center;
}

.btn-ghost {
height: 36px;
padding: 0 12px;
font-size: 12px;
font-weight: 500;
color: #1D1D1F;
background: transparent;
border: 1px solid #E5E5EA;
border-radius: 999px;
line-height: 36px;
text-align: center;
}

@media (prefers-color-scheme: dark) {
.wecard {
background: #1C1C1E;
}
.text-body {
color: #FFFFFF;
}
.tag {
background: #2C2C2E;
color: rgba(255, 255, 255, 0.68);
}
.btn-secondary {
background: rgba(255, 255, 255, 0.12);
color: #FFFFFF;
}
.btn-ghost {
color: #FFFFFF;
border-color: rgba(255, 255, 255, 0.08);
}
}

Data flow in the component lifecycle:

wx.modelContext.on(NotificationType.Result)
→ structuredContent = { text, model, usage }
→ setData updates the view

Step 6: Write index.js to Register Interfaces

Write miniprogram/skills/text-gen-skill/index.js:

const cloudMw = require('../_shared/mp-skills-shared/utils/cloud-middleware')
const generateText = require('./apis/generateText')

function registerAPIs() {
const skill = wx.modelContext.createSkill('skills/text-gen-skill')
skill.use(cloudMw)
skill.registerAPI('generateText', generateText)
console.log('[text-gen-skill] APIs registered')
}

registerAPIs()

Responsibilities of index.js:

Code lineDescription
wx.modelContext.createSkill('skills/text-gen-skill')Creates a SKILL instance, the path corresponds to the subpackage directory
skill.use(cloudMw)Registers shared middleware (preview mode detection, ensureCloudInit, etc.)
skill.registerAPI('generateText', generateText)Associates the atomic interface name with the implementation function, the name must match the name in mcp.json

Step 7: Create Preview Mock Data

To facilitate local debugging, implement mock data for preview mode. Create miniprogram/skills/text-gen-skill/data/seed.js:

function mockGenerateText(prompt, model) {
const mocks = {
'hy3-preview': `This is a mock response from AI for "${prompt}". Mock data is used in preview mode; in production mode, the Hunyuan model will be called to generate real content.`,
cloudbase: `This is a mock response from AI for "${prompt}". Mock data is used in preview mode; in production mode, the cloud model will be called to generate real content.`,
'deepseek-v4': `(DeepSeek deep reasoning) Analysis of "${prompt}" is as follows:\n\n1. First, this is a mock response in preview mode\n2. In production mode, the DeepSeek model will provide more in-depth analysis\n3. The content includes detailed reasoning process and conclusions`,
hunyuan: `(Hunyuan model) Regarding "${prompt}":\n\nThis placeholder content is shown in preview mode. In production mode, the Hunyuan model will be called to generate high-quality Chinese content.`
}
return mocks[model] || mocks['hy3-preview']
}

function seedData(params) {
const { prompt, model = 'hy3-preview', systemPrompt = '' } = params
const text = mockGenerateText(prompt, model)
return {
text,
model,
systemPrompt,
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
}
}

module.exports = { seedData }

Step 8: Create Shared Utility Functions

Create miniprogram/skills/text-gen-skill/utils/util.js (preview mode detection, cloud initialization, standard response formatting):

const PREVIEW_MODE_KEY = 'mp_skills_preview_mode'

function isPreviewMode() {
return wx.getStorageSync(PREVIEW_MODE_KEY) === true
}

let _cloudInited = false

function ensureCloudInit() {
if (_cloudInited) return
if (!wx.cloud) throw new Error('Current environment does not support wx.cloud')
wx.cloud.init({ traceUser: true })
_cloudInited = true
}

function successResult(msg, structuredContent, meta) {
const result = { isError: false, content: [{ type: 'text', text: msg }] }
if (structuredContent !== undefined) result.structuredContent = structuredContent
if (meta !== undefined) result._meta = meta
return result
}

function errorResult(msg, structuredContent) {
const result = { isError: true, content: [{ type: 'text', text: msg }] }
if (structuredContent !== undefined) result.structuredContent = structuredContent
return result
}

module.exports = {
PREVIEW_MODE_KEY,
isPreviewMode,
ensureCloudInit,
successResult,
errorResult
}

Step 9: Validate

After creation, run the static validation to confirm all files are correctly configured:

npx mp-skills validate

Expected output:

[OK] 项目配置检查通过
[OK] 接口定义与实现一致
[OK] 原子组件定义完整

If validation fails, check these common issues:

  • The interface name in mcp.json doesn't match the registration name in index.js
  • The component file pointed to by componentPath doesn't exist
  • JSON format error in mcp.json

Step 10: Verify in Developer Tools

# macOS
/Applications/wechatwebdevtools.app/Contents/MacOS/cli open --project /your/path/to/my-ai-app

# Windows
"C:\Program Files (x86)\Tencent\微信web开发者工具\cli.bat" open --project "D:\...\my-ai-app"

In Developer Tools:

  1. Switch the base library version to 3.16.1 or above
  2. Switch the compilation mode to "Mini Program AI Compilation"
  3. text-gen-skill should appear in the SKILL list on the left
  4. After selecting it, the generateText atomic interface should be displayed on the right
  5. Enter {"prompt": "Write a tagline for a coffee shop"} and execute
  6. Check the returned text content and the card component rendering

Verification Checklist

  • npx mp-skills validate passes all checks
  • The generateText interface can be called normally in Developer Tools and returns text
  • The text result is rendered through the text-result-card component
  • The "Regenerate" button can trigger another call with the same prompt
  • Content exceeding 300 characters can be "expanded to full text"
  • The declaration in mcp.json matches the registration name in index.js
  • The componentPath in mcp.json points to an existing component file

Text Generation Architecture

User input → AI Engine → wx.modelContext → generateText.js

┌──────────┴──────────┐
▼ ▼
wx.cloud.extend.AI Preview Mode Mock
.createModel('cloudbase')
.generateText({...})


AI Model (hy3-preview)


structuredContent:
{ text, model, usage }


text-result-card component rendering

Key design decisions:

DecisionChoiceRationale
Whether text generation goes through cloud functionsNo, directly call wx.cloud.extend.AIThe mini program SDK has a built-in AI gateway, no additional backend needed. Only go through cloud functions when custom prompt pre-processing/post-processing is required
Model parameter exposureNot exposed to the frontend UIKeep the atomic interface simple. The AI engine automatically selects the appropriate model based on the scenario
Is systemPrompt requiredNo, AI infers automaticallyThe AI engine infers the role based on user intent, developers don't need to manually specify systemPrompt for each scenario
Result displayCustom componentMore expressive than plain text: can display model tags, token usage, "Regenerate" action entry

FAQ

wx.cloud.extend.AI.createModel('cloudbase') returns undefined?

Confirm that the mini program has enabled AI development mode and the Developer Tools base library version is ≥ 3.16.1. Try switching to another version and back in the tools to trigger a base library re-download.

Call returns a "Insufficient tokens" error?

Purchase a Token resource pack in CloudBase Console → AI → Token Management. Mini Program Growth Plan users have free credits, which can be viewed on the Growth Plan page.

How do I switch preview mode?

The SKILL preview mode reads wx.getStorageSync('mp_skills_preview_mode'). Provide usePreview() / useProduction() methods in app.js to switch, then re-execute the atomic interface for the change to take effect.

How long should the description in mcp.json be?

It is recommended to write 200-300 characters of description in Chinese, including:

  1. What the interface does
  2. Preconditions for calling (user has clearly expressed the need)
  3. Prohibited scenarios (when absolutely NOT to call) The AI engine relies on this description for intent matching; writing it too short may cause false triggers.

References