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
- Completed Build an AI Mini Program from Scratch, the project runs normally in the developer tools
- The mini program has enabled AI development mode (WeChat Official Platform → Basic Features → AI Capabilities → Access Mode → Select "Development Mode")
- WeChat Developer Tools Nightly version installed
- Node.js ≥ 18
-
npx mp-skills --versionoutputs the version number correctly - Enable the desired model (default
hy3-previewcan be used for free trial) in CloudBase Console → AI → Text Generation Models - Purchase a Token resource pack (the Mini Program Growth Plan provides free credits)
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:
| Field | Description | Required |
|---|---|---|
name | Skill name, must match the directory name | Yes |
description | One-line description, used by the AI engine to determine trigger时机 | Yes |
version | Semantic versioning | Recommended |
tags | Classification tags | Recommended |
platform | Target 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:
| Field | Description |
|---|---|
apis[].name | Atomic interface name, must match the registration name in index.js |
apis[].description | Interface description, the AI engine determines when to call based on this. Must clearly state preconditions and prohibited scenarios |
apis[].inputSchema | JSON Schema defining input parameters, required marks mandatory fields |
apis[].outputSchema | Output parameter definition, the AI engine parses the response based on the schema |
apis[]._meta.ui.componentPath | Path to the result display component |
components | Component 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:
| Aspect | Description |
|---|---|
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 parameter | Default 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 mode | isPreviewMode() is based on a local storage flag. When enabled, returns mock data for debugging without a backend connection |
translateError | Shared 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 / errorResult | Standard response format following the wx.modelContext specification: { isError, content, structuredContent, _meta } |
modelToApiName | Maps 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 line | Description |
|---|---|
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.jsondoesn't match the registration name inindex.js - The component file pointed to by
componentPathdoesn'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:
- Switch the base library version to 3.16.1 or above
- Switch the compilation mode to "Mini Program AI Compilation"
text-gen-skillshould appear in the SKILL list on the left- After selecting it, the
generateTextatomic interface should be displayed on the right - Enter
{"prompt": "Write a tagline for a coffee shop"}and execute - Check the returned text content and the card component rendering
Verification Checklist
-
npx mp-skills validatepasses all checks - The
generateTextinterface can be called normally in Developer Tools and returns text - The text result is rendered through the
text-result-cardcomponent - 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.jsonmatches the registration name inindex.js - The
componentPathinmcp.jsonpoints 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:
| Decision | Choice | Rationale |
|---|---|---|
| Whether text generation goes through cloud functions | No, directly call wx.cloud.extend.AI | The 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 exposure | Not exposed to the frontend UI | Keep the atomic interface simple. The AI engine automatically selects the appropriate model based on the scenario |
Is systemPrompt required | No, AI infers automatically | The AI engine infers the role based on user intent, developers don't need to manually specify systemPrompt for each scenario |
| Result display | Custom component | More 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:
- What the interface does
- Preconditions for calling (user has clearly expressed the need)
- 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.