页面加载中...
分析小程序项目源代码(含压缩/混淆),识别核心业务步骤,提取网络接口与 JSAPI 调用,生成符合 wx.modelContext 规范的技能分包(含原子接口 + 原子组件),并完成 app.json / project.config.json 配置集成。在以下场景触发:把小程序页面能力改造为小程序 AI 原子接口、生成 skills/ 分包代码、从源项目派生 MCP 工具、小程序 AI 的开发模式代码生成。仅负责静态生成,生成完成后必须交棒 wxa-skills-validate 做校验。
从小程序源码生成符合 wx.modelContext 规范的技能分包(skills/):分析源码 → 识别业务 → 提取接口与 JSAPI → 设计原子接口 → 生成代码 → 集成配置 → 交棒校验。
app.json / project.config.json 集成wxa-skills-validate 负责)skills/{skill-name}/(含 mcp.json、SKILL.md、index.js、原子接口实现文件、工具模块;组件目录仅在用户明确要求生成原子组件时才有)+ 配置文件更新scripts/probe.mjs + scripts/probe-lib.mjs(与本 skill 同目录)miniprogram-automator:阶段 3.6 环境检查时由模型安装到 skill 的 scripts/ 目录(cd <skill-path>/scripts && npm install miniprogram-automator),禁止安装到小程序源项目WX_CLI_PATH 环境变量 / 自动检测)requiresRuntimeProbe: true 时必须执行、禁止跳过;非必探接口在环境不可用时可回退静态结果(详见阶段 3.6 与 references/RUNTIME_PROBE.md)。通过 evaluate 覆写 wx.request 捕获请求参数与响应;支持点击触发、进页面自动发、request/evaluate 非 UI 直发三类,并支持「一个能力 = 多请求」的有序捕获skills/{skill}/apis/{name}.js(validator 也兼容 tools/services/ / tools/)skills/{skill}/components/{name}/(与 mcp.json._meta.ui.componentPath 严格相等)| 文件 | 用途 | 加载时机 |
|---|---|---|
references/ANALYSIS_PATTERNS.md | 业务流程识别、接口调用、JSAPI 匹配的正则模式 | 阶段 1/2/3 扫描源码时 |
references/JSAPI_WHITELIST.md | wx API 白名单完整清单(接口侧 / 组件侧 / 不可迁移),SKILL.md C 节只列高频项 | 阶段 1 鉴权扫描 / 阶段 3 JSAPI 提取 / 阶段 5 代码生成时(C 节高频清单未覆盖目标 API 时必查) |
references/CODE_TEMPLATES.md | index.js / 工具模块 / 接口实现 / mcp.json / SKILL.md / 配置的代码模板 | 阶段 5 代码生成 |
references/COMPONENT_TEMPLATES.md | 原子组件模板(列表/详情/状态) | 阶段 5 组件生成 |
references/ATOMIC_COMPONENT_DESIGN.md | 原子组件设计规范(尺寸档位 / 主题 / 边距 / 字体 / 布局 / 操作区) | 阶段 5 组件生成(强制前置,优先级最高) |
references/ATOMIC_COMPONENT_CSS.md | 原子组件 WXSS 实现规范(容器约束、单位换算、省略规范、禁用清单) | 阶段 5 样式编写 |
references/STYLE_MIGRATION.md | 源样式提取 + 字段映射的完整工作流 | 阶段 5 组件生成前(强制前置) |
references/HALF_SCREEN.md | 半屏页面(viewCtx.openDetailPage)API、上行消息、禁用接口/组件清单 | 按需——仅当业务确有"详情 / 补充信息"语义时(默认不生成) |
references/RUNTIME_PROBE.md | 运行时探测(automator probe)触发条件、SOP、失败兜底、结果接入。probe-first 验证 + 静态分析兜底 | 阶段 3.6——选中接口默认探测,命中 T1~T6 必探 |
references/API_EXTRACTION.md | 接口可请求性分析 + 签名模块可拷贝性判断 + 接口功能匹配 + 鉴权代码生成详细流程 + request.js 源码核对清单 | 阶段 3.4.1 / 阶段 5 鉴权代码生成 |
| 禁止项 | 正确做法 |
|---|---|
getApp() | 分包内自行管理状态(模块变量 / wx.storage) |
require('../../xxx') 引用主包/兄弟分包 / import ... from '@/' | 把依赖完整拷贝到当前分包:单 skill 私有放 {skill}/utils/,多 skill 复用放 skills/_shared/ |
依赖主包 wx.cloud.init() | utils/util.js 中 ensureCloudInit() 自行初始化 |
依赖主包 app.js 初始化 storage | utils/util.js 中 ensureStorageInit() 自行初始化 |
从 getApp().globalData 读配置 | baseUrl / env 硬编码在分包 utils/util.js |
| 依赖主包登录态 | 每次执行接口前 ensureLogin() 主动走一遍登录流程 |
| 使用主包注册的全局组件 | 在分包 JSON 中重新声明 usingComponents |
出现以下任一情况,立即终止生成并告知用户:
| 阻断情况 | 检测时机 | 告知文案 |
|---|---|---|
依赖小程序插件(plugin:// / requirePlugin / app.json 的 plugins) | 阶段 1/3 | "该功能依赖小程序插件,当前暂不支持自动生成,需手动接入" |
| 用户声明的能力在源码中找不到任何对应接口或页面 | 阶段 3 | "未能在源码中定位到 <能力名>,无法生成,请确认能力名称或补充源码" |
| 未提供可读的源码目录(只给 appid / URL / 截图) | 阶段 1 前 | "请提供小程序完整源码目录,当前无法基于非源码资产生成" |
| 所有候选实现都依赖非白名单 JSAPI 且无替代方案 | 阶段 3 | "该能力依赖非白名单 JSAPI(如 <api>),无法自动生成" |
app.json 缺 "lazyCodeLoading": "requiredComponents" 配置 | 阶段 1 | "项目 app.json 顶层缺少 \"lazyCodeLoading\": \"requiredComponents\",否则独立分包内的原子接口被小程序 AI 路由调用时无法正确加载执行。请在 app.json 顶层添加该字段后重新触发生成" |
| 静态分析 + 运行时探测 + 离线兜底三者全部失败 | 阶段 3 | "接口 <api> 无法通过静态分析、运行时探测、离线抓包任何一种方式获取真实接口信息,无法生成" |
阶段 1 鉴权扫描、阶段 3 JSAPI 提取、阶段 5 代码生成时必须对照白名单。源码用到清单之外的 JSAPI → 按"不可迁移 JSAPI"处理。
完整清单(接口侧 / 组件侧 / 不可迁移)见
references/JSAPI_WHITELIST.md。下文 C.1 / C.2 / C.4 仅列高频条目,覆盖业务时必查 reference 完整列表,不要凭印象。
references/JSAPI_WHITELIST.md §1)"接口侧"指通过
wx.modelContext.registerAPI()注册的处理函数及其依赖的纯 JS 模块——常规放在<skill>/apis/(也可放tools/services//tools/,validator 会按这三个候选目录解析),引用的工具模块目录名(如utils//services//helpers// 自定义名)不限。作用域以"是否在原子接口处理函数链路上"判定,不以目录名判定。
| 分类 | 高频接口 |
|---|---|
| 小程序 AI | wx.modelContext.registerAPI、wx.modelContext.createSkill(返回 { use, registerAPI })、wx.modelContext.expireAllCards、wx.modelContext.getSessionId(获取会话 ID) |
| 登录 | wx.login、wx.checkSession |
| 网络 | wx.request、网络状态 getNetworkType / on*NetworkStatusChange |
| 云开发 | wx.cloud.init / callFunction / database |
| 位置 | wx.getLocation / getFuzzyLocation(不含 chooseLocation / openLocation) |
| 系统 | wx.getDeviceInfo、wx.getAppBaseInfo、wx.getWindowInfo |
| 数据缓存 | wx.{get,set,remove,clear,batchGet,batchSet}Storage(含 Sync)、wx.getStorageInfo |
| 上传下载 | wx.uploadFile、wx.downloadFile |
| 订阅消息 | wx.requestSubscribeMessage |
| 授权设置 | wx.authorize、wx.getSetting(不含 openSetting) |
| 图片 | wx.getImageInfo |
| 手机号 | wx.getPhoneNumber、wx.getRealtimePhoneNumber |
| 账号 | wx.getAccountInfoSync(接口与组件均可调) |
支付类、系统选择器/采集(
choose*/scanCode/saveImageToPhotosAlbum)、主动打开原生页/面板(openLocation/makePhoneCall/openDocument/shareAppMessage/openSetting/openPrivacyContract)不在接口侧——见 C.2 /references/JSAPI_WHITELIST.md §2.1(动态组件)。其他场景(人脸核身、微信运动、加密、WiFi、蓝牙/BLE、WebSocket、TCP/UDP、mDNS、传感器等)查references/JSAPI_WHITELIST.md §1完整表。
references/JSAPI_WHITELIST.md §2)"组件侧"指原子组件
Component({})内的代码及其引用的纯 JS 模块。组件目录路径强约束为<skill>/components/<name>/index.{js,json,wxml,wxss}(与mcp.json中接口的_meta.ui.componentPath严格相等),但组件index.js引用的工具模块目录名不限。
| 分类 | 高频接口 |
|---|---|
| 小程序 AI | getContext(this)(支持 reapplyApiCall 等)、getViewContext(this)(支持 preloadDetailPage 及 on 事件等)、expireAllCards / expirePreviousCards |
| 网络请求 | wx.request(普通组件不支持,须声明 scope.dynamic 后在动态组件用) |
| 动态组件专属 | 声明 scope.dynamic 后可用(tap 回调触发):支付类 requestPayment / openBusinessView 等、系统选择器 chooseLocation / chooseAddress / chooseInvoice / chooseInvoiceTitle、媒体采集 chooseMedia / chooseMessageFile / saveImageToPhotosAlbum、scanCode(详见 references/JSAPI_WHITELIST.md §2.1) |
| 系统 | wx.getDeviceInfo、wx.getAppBaseInfo、wx.getWindowInfo |
| 数据缓存 | wx.getStorage / setStorage 全套(含 Sync) |
| 媒体/交互 | wx.previewMedia、wx.showToast、wx.hideToast |
| 文件/上传下载 | wx.openDocument、wx.downloadFile |
| 账号 | wx.getAccountInfoSync |
| 其它支持 | 位置 openLocation、设备 makePhoneCall、设置 openSetting、分享 shareAppMessage、振动、隐私授权 |
| 地图 | this.createSelectorQuery().select('#mapId').context() 获取 MapContext 后调 MapContext.*(openMapApp 除外),完整方法清单见 references/JSAPI_WHITELIST.md §2 |
组件侧禁用:wx.cloud.* / 位置 / 登录 / 支付 / 其它任何业务接口(除上表已列出的能力)。组件只能收数据(接口返回的 structuredContent / _meta)、做预览、读系统信息、读写本地缓存、读账号信息、操作 MapContext、发声明过能力的网络请求。组件与接口处于不同 JS 上下文,全局变量不共享。在 methods / tap handler / 异步回调里主动调 sendFollowUpMessage / getDimensions 时必须现取 wx.modelContext.getContext(this) / getViewContext(this),不要通过 this._modelCtx 等缓存引用调(详见 references/COMPONENT_TEMPLATES.md)。
默认不生成原子组件。原子接口只返回文本 + structuredContent + handoff(进接力页,见 C.3.3)。仅当用户明确要求生成原子组件(GUI 卡片)时才生成:对应接口声明 _meta.ui.componentPath,并在 mcp.json 顶层 components[] 声明一条记录,path 必须与该接口 _meta.ui.componentPath 字符串完全相等(含末尾 /index,严格相等比对)。网络能力(permissions.scope.dynamic)按需声明。
{ "components": [ { "path": "components/order-list/index" }, { "path": "components/weather-card/index", "permissions": { "scope.dynamic": { "desc": "声明使用场景" } } } ] }
默认不生成。仅当源业务上存在"卡片到某时刻作废、不应再被点"语义(成交、关店、活动结束、超时)时,在 components[] 记录上加 expirable: true + 业务化 expiredText(默认文案"服务已过期")。纯展示卡片不要写。代码示例与"过期触发"模板见 references/COMPONENT_TEMPLATES.md "卡片过期"节。
触发 API(按粒度二选一,不要同时调):
wx.modelContext.expireAllCards():原子接口/组件均可调,过期所有 expirable: true 卡片含自身wx.modelContext.getViewContext(this).expirePreviousCards():仅原子组件可调用(依赖组件实例 this),只过期此前已渲染的卡片不含自身精细过滤(两个 API 都支持):可传 { componentPaths: [...] } 只过期匹配 componentPath 的卡片;加 match: 'latest' 只过期最近一张。componentPath 用绝对路径(含分包前缀,如 packageA/weather-skill/components/weather-card/index),多条 path 取并集。
声明与调用必须配对。
半屏页面是原子组件内容的延伸——仅当源业务确有"详情 / 用户补充信息"语义时挂上。
methods 内,wx.modelContext.getViewContext(this).openDetailPage({ url }),承载页面用项目内已有的小程序页面(原子接口里没有 this,不可调)wx.modelContext.getContext().sendFollowUpMessage(...)(不传 this);web-view h5 走 WeixinJSBridge.invoke('invokeMiniProgramAPI', { name: 'sendFollowUpMessage', arg })。上行后半屏自动关闭回小程序 AI 对话wx.getDetailPageCloseButtonBoundingClientRect 适配navigateToMiniProgram / 公众号 / 视频号 / 表情 / 客服)、页面路由(navigateTo / redirectTo / switchTab / reLaunch / wx.router.*)、聊天工具(shareXxxToGroup)、地图 MapContext.openMapApp、广告(createInterstitialAd 等 + <ad> <ad-custom> 组件)、导航组件(<navigator> / <functional-page-navigator>)—— 完整清单与示例代码见 references/HALF_SCREEN.md进小程序统一走 handoff。默认流程:原子接口返回文本 + 小程序卡片,用户点卡片后由平台 handoff 进入小程序内的接力业务页继续操作。
何时必须配:若某原子接口执行完会停下等用户确认(展示小程序卡片、等用户点击进小程序),必须为它配置 pagePath,否则用户无法进入业务页。纯数据、无停顿接续的接口可不配。
四项适配(详见 references/CODE_TEMPLATES.md "handoff 接力页" 节):
mcp.json:在该接口 apis[]._meta.ui 加 pagePath(接力页 path,不含 query;与 componentPath 同级,componentPath 仅在生成组件时才有)。content / structuredContent 同级)增加 handoff,兼容两种形态:
handoff: { query, payload?, card? }——模型无需筛选数据时直接返回对象,平台即时生成 handoff,链路更短、更快。handoff: ({ result }) => ({ query, payload?, card? })——需要用模型修改后的 result 时返回函数,入参对象的 result 即模型修改后的完整 result。
字段:query 为对象(页面 query 键值对,如 { drugId });payload 可选(接力页首屏加速数据);card 可选(卡片展示信息,如 { title })。app.js:onLaunch 内注册 wx.onAgentHandoff(cb)(须早于 handoff 触发的 onBeforeAppRoute),把 { path, query, payload } 按 pageId 暂存。onLoad(query) 中 query 为对象(键值对,同 handoff.query);若 wx.onAgentHandoff 投递了 payload 则先 setData 加速首屏。平台代做(无需自己实现路由):按 pagePath 打开目标页 → 把 handoff.query 原样注入 onLoad(query) → 通过 wx.onAgentHandoff 回调投递 path / query / payload。
禁用:
wx.openAgent/wx.navigateBackAgent当前基础库侧未打通,调用会失败——接力页内不要依赖"打开 Agent / 返回 Agent 对话",后续流程由业务页自行完成。
references/JSAPI_WHITELIST.md §3)| 不可用 API | 替代策略 |
|---|---|
wx.showToast / showModal / showLoading / showActionSheet 等 UI 反馈 | 结果通过 content / structuredContent 回馈,小程序 AI 无 loading/modal 概念 |
wx.navigateTo / redirectTo / switchTab / reLaunch / navigateBack | 删除,小程序 AI 不在页面栈内导航 |
wx.chooseImage / wx.chooseVideo / wx.previewImage(老接口) | 改用 wx.chooseMedia(接口侧)/ wx.previewMedia(组件侧) |
wx.setClipboardData / getClipboardData | 跳过 |
wx.getUserInfo / getUserProfile | 改用登录 + 后端资料接口 |
wx.createSelectorQuery / createCanvasContext | 接口侧不适用;组件侧仅允许通过 this.createSelectorQuery().select('#mapId').context() 获取 MapContext(详见 C.2) |
wx.pageScrollTo / wx.createAnimation | 容器不支持滚动;动画用 CSS transition/animation(限 opacity/transform) |
其它老接口、Taro 特有不可迁移项(Hook、Pinia/Vuex、Vue setup 等)见
references/JSAPI_WHITELIST.md §3。
button 的 open-type 改写组件内 button 禁用 open-type(share / getPhoneNumber / getRealtimePhoneNumber)→ 去掉 open-type,改 bindtap,在 tap handler 内调对应白名单 JSAPI(wx.shareAppMessage / wx.getPhoneNumber / wx.getRealtimePhoneNumber)。
chooseImage → chooseMedia、previewImage → previewMedia)→ 自动替换tap 事件view(含 hover-class)/ text(不含 user-select)/ image(仅网络地址)/ map / button(不含 open-type)/ canvas / scroll-view(仅横向滚动 scroll-x,禁纵向 scroll-y)swiper / swiper-item / input / textarea / picker / picker-view / checkbox / radio / form / label / slider / switch / editor / rich-text / icon / progress / navigator / web-view / movable-area / movable-view / root-portal / match-media 等button 用 open-type → 按 C.5 改写为 bindtap + 白名单 JSAPI<scroll-view scroll-x="true"> 包裹)bindtap,tap handler 上行 content 数组(① 单 text 或 ② text + api/call 组合,推荐 ②)。详见阶段 5.0.1 + references/COMPONENT_TEMPLATES.md "上行消息"节阶段 0 — 业务需求澄清(强制前置) - [ ] 判定用户场景是否明确(两项判定) - [ ] 不明确 → 最小扫描 + 引导澄清 + 等待确认 - [ ] 确认是否生成原子组件(用户未明确要求 → 默认不生成,只做原子接口 + handoff) - [ ] 产出"目标业务场景 + 期望原子能力"清单 阶段 1 — 项目扫描 - [ ] 提取 app.json / app.js / project.config.json 关键字段 - [ ] 产出云开发标记 + 云环境 ID + appid + 插件使用情况 - [ ] 产出鉴权迁移清单(token key / header 方式 / 登录流程) - [ ] 产出 storage 初始化清单 阶段 2 — 业务功能识别(用户已明确时跳过) - [ ] 产出结构化功能清单 JSON - [ ] 用户二次确认 阶段 3 — 接口与 JSAPI 提取 + 可行性校验 - [ ] 每个能力对应的接口/JSAPI 清单 - [ ] 完整依赖链路 - [ ] 鉴权依赖确认 - [ ] 可行性三级评定(高/中/无置信) - [ ] 选中的业务接口**默认进 plan 探测**;逐条检查 T1~T6,命中任一即标记 `requiresRuntimeProbe: true`(必探硬底线) - [ ] 执行 probe(一次性批量):环境检查(自动安装 automator)→ 通知用户 → 生成 plan.json → 执行 `scripts/probe.mjs` → 结果写入 `<源项目>/.ai-mode-skills/probe/` → 合并写入 `merged-result.json` → 接入阶段 4。命中 T1~T6 **禁止只标记不执行**;非必探接口在环境不可用时可回退静态结果 阶段 4 — 原子接口设计 - [ ] 原子接口清单(含 name / description / inputSchema / outputSchema;进小程序的接口配 _meta.ui.pagePath + 返回 handoff;_meta.ui.componentPath 仅当用户要求生成原子组件时才有) - [ ] API 依赖图 - [ ] storage key 清单 阶段 5 — 代码生成 阶段 5 — 代码生成 - [ ] 进小程序的接口已配 _meta.ui.pagePath + 返回值顶层 handoff(见 C.3.3) - [ ] `utils/request.js` 鉴权代码完整复刻源码 request 封装(通用 header/query/签名/预处理,模块级鉴权变量不留空) - [ ] (仅当用户要求生成原子组件时)每个原子组件符合 `ATOMIC_COMPONENT_DESIGN.md`(尺寸档位 / 背景 / 边距 / 字号 + 透明度 / 布局 / 操作区)并走完 STYLE_MIGRATION.md 的 7 步;可交互元素绑 bindtap,tap handler 优先上行 `{ content: [{ type: 'text', text }, { type: 'api/call', data: { name, arguments } }] }` 组合,text 是用户视角简短中文、`name` 在 mcp.json 中存在、`arguments` 与 inputSchema 对齐;无法映射时退回单 `text` - [ ] skills/{skill-name}/ 目录完整(mcp.json / SKILL.md / index.js / apis/* / utils/*;仅生成组件时含 components/*) - [ ] SKILL.md 已按 `references/CODE_TEMPLATES.md` 第五节的 5 节结构与禁止项写完(路由说明,非接口手册) 阶段 6 — 配置集成 - [ ] app.json 加 agent.skills + subPackages - [ ] project.config.json 的 packOptions.include 加 skills 收尾 — 交棒给 wxa-skills-validate - [ ] 明确告知用户:"请使用 wxa-skills-validate 做校验" - [ ] 提示 skills 路径与 project-path
| 场景 | 流向 |
|---|---|
| 正常主干 | 0 → 1 → (2) → 3 → 4 → 5 → 6 → 交棒 wxa-skills-validate |
| 用户已明确能力 | 跳过 2,0 → 1 → 3 |
| 阶段 3(选中接口默认探测) | 3.5 → 3.6(probe) → 4。命中 T1~T6 时 ⚠️ 禁止 3.5 → 4 跳过 3.6;非必探接口环境不可用可回退静态结果 |
| probe 失败,离线兜底成功 | 3.6 → 4(使用离线兜底数据) |
| probe + 离线兜底均失败 | 3.6 → 阻断规则 B |
| validator 反馈 T1~T6 / A/B/C/D 类错误 | 回本 skill 阶段 5 改代码 |
| validator 反馈 T7/T8(接口划分 / 依赖链路) | 回本 skill 阶段 4 重设计 |
| 任一阶段触发阻断规则 B | 立即终止,输出阻断原因 |
核心原则:
wxa-skills-validate 负责工作区已存在 skills/ 产物时:
| 用户意图 | 入口阶段 | 说明 |
|---|---|---|
| 新增一个原子能力 | 阶段 0(轻量)→ 阶段 3 | 先澄清新能力,扫描接口并入增量清单 |
| 修改已有原子接口的行为 | 阶段 4 | 更新接口清单 → 5 → 6 → 交棒 |
| 修改组件样式/模板 | 阶段 5 | 仅改 components/{x}/,重新走 5 → 6 → 交棒 |
| validator T1~T6 / A/B/C/D 反馈 | 阶段 5 | 按报告定位文件,改完交棒 |
| validator T7/T8 反馈 | 阶段 4 | 重设计后 5 → 6 → 交棒 |
| 仅做验证 | 不进入本 skill,直接给 wxa-skills-validate | — |
重入时已生成且未触及的文件保持不变,只更新受影响的文件。
契约:
| 项 | 内容 |
|---|---|
| 入口条件 | 用户发起生成请求(任何请求都必须从本阶段开始) |
| 产出物 | 判定结果 + 必要时的澄清清单 |
| 下一步 | "明确"或澄清确认完毕 → 阶段 1 |
判定规则(必须同时满足 2 项才算"明确"):
| # | 判定项 | 示例 |
|---|---|---|
| ① | 指明具体业务名词 | "商品检索""订单管理""地址管理""签到";非"核心功能""主要能力" |
| ② | 可推断至少 2-3 个原子能力的粒度 | "检索商品 + 展示列表 + 查看详情";非"业务相关" |
任一不满足 → 进入下方澄清流程。
app.json 的 tabBar.list、pages(一级路径)、subPackages.root。禁止读 JS/WXML/WXSS,禁止做依赖分析。references/ANALYSIS_PATTERNS.md 页面功能识别表)归纳 3~6 个候选场景。澄清输出清单模板:
目标业务场景: - 场景 A:<名称> → 期望原子能力:<能力 1>、<能力 2> - 场景 B:<名称> → 期望原子能力:<能力 3> 技术约束: - 是否涉及支付/登录/位置:是/否 - 是否使用云开发:待阶段 1 扫描确认 - 是否生成原子组件(GUI 卡片):是/否(用户未明确 → 默认否,只生成原子接口 + handoff)
契约:
| 项 | 内容 |
|---|---|
| 入口条件 | 阶段 0 产出明确的业务场景与原子能力清单 |
| 产出物 | ① 配置字段(appid / pages / subPackages / tabBar / agent / packOptions);② 云开发标记 + 云环境 ID;③ 插件使用情况;④ 鉴权迁移清单;⑤ storage 初始化清单 |
| 下一步 | 用户已明确所有原子能力 → 阶段 3;否则 → 阶段 2 |
| 阻断条件 | 未提供源码目录 / 目标能力依赖插件 → 阻断规则 B |
读 app.json / app.js / project.config.json,提取 pages / subPackages / tabBar / 已有 agent / appid / packOptions;扫云开发(wx.cloud 调用 / cloudfunctions/ 目录)与云环境 ID(wx.cloud.init({ env }))。lazyCodeLoading 必检:缺 "lazyCodeLoading": "requiredComponents" → 阻断规则 B(不要"代为补全")。云开发项目同时扫 cloudfunctionRoot/<fn>/index.js 的入参/返回结构。
扫 app.js + 主包 request 封装(utils/request.js / http.js / api.js) + 登录文件,提取四项:①token/session 存取 key(关键词 getStorageSync + token/session/openid/cookie)②请求 header 鉴权方式(Authorization / Bearer / 自定义)③登录入口(wx.login / wx.checkSession)④换 token 接口(wx.login 后的 wx.request / 云函数)。
迁移策略(形成鉴权迁移清单,阶段 5 写入分包工具模块):分包自包含完整登录能力,每次执行接口前 ensureLogin() 主动登录;token 存模块级变量(不写 storage——分包与主包隔离,登录态不可靠);无鉴权接口跳过。
扫 app.js 与主包 .js 中的 wx.{set,get,clear}Storage*,提取 key / defaultValue / initCondition / sourceFile。迁移:① setStorageSync 初始化值 → 分包 ensureStorageInit() 重建;② getApp().globalData 运行时缓存 → 模块级变量或按需写 storage;③ onLaunch 异步获取后写 storage → 分包首次调用时自行重发请求并缓存。形成 storage 初始化清单(与阶段 4 内部"接口间数据传递的 storage key 清单"不是同一张表)。
识别:单行 >500 字符 / 单双字符变量名 / 缺注释空行。处理顺序:① 优先问用户要未压缩源码;② 否则尝试 prettier 格式化后再提取;③ 格式化后关键字段仍全是 a.b.c.d → 阻断规则 B。禁止盲目猜变量名——猜出来的代码会在 validator 大量失败。
扫 app.json 的 plugins 字段、页面/组件 JSON 的 usingComponents 中的 plugin:// 引用。目标能力依赖插件 → 阻断规则 B。
契约:
| 项 | 内容 |
|---|---|
| 入口条件 | 阶段 1 完成 且 用户仅给源码未明确原子能力 |
| 产出物 | 结构化功能清单(JSON)且已获得用户二次确认 |
| 下一步 | 用户确认 → 阶段 3 |
| 阻断条件 | 用户始终无法确认 → 停留本阶段 |
动作:
references/ANALYSIS_PATTERNS.md 的模式分析页面用途、交互事件、数据流向产出物 JSON(字段统一 camelCase):
[ { "functionName": "检索商品", "pages": ["pages/items/list", "pages/search/index"], "sourceApis": ["GET /api/items/search"], "suggestedAtomicInterfaces": ["searchItems"], "needsComponent": true } ]
必须将清单发给用户二次确认才能进入阶段 3。
契约:
| 项 | 内容 |
|---|---|
| 入口条件 | 已有用户确认的目标原子能力清单 |
| 产出物 | 每个能力的接口/JSAPI 清单 + 完整依赖链路 + 可行性校验结果 |
| 下一步 | 所有能力均找到对应实现 → 阶段 4 |
| 阻断条件 | 任一能力找不到对应实现 / 依赖链路含插件 → 阻断规则 B |
详细匹配模式见 references/ANALYSIS_PATTERNS.md。
3.1 提取范围:仅扫用户已确认能力对应的页面/模块,搜索网络调用(wx.request / wx.cloud.{callFunction,database,callContainer})+ 白名单内 JSAPI(高频列表见"硬性约束 C",完整清单见 references/JSAPI_WHITELIST.md)。
3.2 依赖追踪:对相关页面/模块的所有 require / import 递归追踪,识别完整依赖链路。阶段 5 把依赖完整内联拷贝到分包工具模块(utils/util.js 或 utils/request.js 等),不要放到 apis/ 下——apis/ 仅放 mcp.json 注册的接口。
3.3 鉴权依赖确认(必做):结合阶段 1 的鉴权迁移清单,对每个目标接口确认 ① 是否需要登录态 ② token 来源(storage 直读 / 需先登录) ③ 登录方式(wx.login + 换 token / 其他);标注后阶段 5 据此实现 ensureLogin()。
3.4 插件依赖阻断:依赖链路含 requirePlugin / require('../plugin/') / plugin:// → 阻断规则 B。
3.5 可行性三级校验(必做):
| 级别 | 识别特征 | 处理 |
|---|---|---|
| ✅ 高置信 | 唯一接口/云函数,参数返回路径清晰 | 直接采用,进阶段 4 |
| ⚠️ 中置信 | 多个候选 / 参数模糊 / 依赖非白名单 JSAPI 需替代 | 列候选 + 差异 + 询问用户后再进阶段 4(不可直接终止) |
| ❌ 无置信 | 遍历全部涉及页面仍找不到任何实现 | 阻断规则 B |
中置信询问模板:
以下原子能力在源码中存在多个候选实现,请确认选择: 能力:<能力名> 候选 1:<接口路径/云函数名> — 参数 <x>、返回 <y>(来自 pages/xxx.js 第 N 行) 候选 2:<接口路径/云函数名> — 参数 <x>、返回 <y>(来自 pages/yyy.js 第 M 行) 请回复序号(如"1")或说明选择理由。
3.6 运行时探测(probe):
策略:probe-first 验证 + 静态分析兜底——选中的业务接口默认进 plan 探测;静态分析负责选接口、生成 plan、给出 method/鉴权/URL 路径,probe 负责验证真实 URL + 抓真实响应结构。完整 SOP 见
references/RUNTIME_PROBE.md。
必探硬底线(命中任一即必探,仅环境不可用/用户拒绝时降级;命中后禁止只标记不执行):
| # | 条件 | 原因 |
|---|---|---|
| T1 | URL 动态拼接 / 压缩不可读 | 无法确定真实 URL |
| T2 | 请求含签名/加密字段 | 无法离线复现 |
| T3 | 响应结构不可推断(T3a 透传无字段访问 / T3b 模板隐式消费) | 无法推断 outputSchema |
| T4 | 必须登录才返回业务数据 | 无法确认正常态字段 |
| T5 | 中置信且用户也不确定 | 静态匹配不足 |
| T6 | 参数传递链 >3 跳 + globalData | 静态追溯不可靠 |
仅验证类(URL 常量 + 响应结构 JS 字段访问清晰):probe 成功则覆盖,环境不可用时直接采用静态结果、不阻断。
触发方式(plan 的 trigger):点击/输入(tap/input/callMethod)、进页面自动发(trigger 留空)、非 UI 直发(request 直调 wx.request / evaluate 执行取数函数)。一个能力串多请求时 matchUrlIncludes 用数组按序捕获。
执行流程:
<源项目>/.ai-mode-skills/static-analysis.json,标记各维度 confidence: "high"/"partial"/"low""high" 维度或命中 T1~T6 → 需要miniprogram-automator 已安装(装在 skill 的 scripts/ 目录)、CLI 可用<源项目>/.ai-mode-skills/probe/plan.json → 执行 scripts/probe.mjsreferences/RUNTIME_PROBE.md §5)→ 写入 <源项目>/.ai-mode-skills/merged-result.json代码注释溯源(apis/<name>.js 顶部):
// [ai-mode:static] URL /api/items/search 来自 utils/request.js:42 // [ai-mode:probe] 2026-06-02 验证完整 URL https://shop.example.com/api/items/search // [ai-mode:probe] 实际响应字段:list[].{id, name, price, img}, total(number)
契约:
| 项 | 内容 |
|---|---|
| 入口条件 | <源项目>/.ai-mode-skills/merged-result.json 已生成(阶段 3 完成静态分析 + probe 合并后的最终结果)。⚠️ 如果 static-analysis.json 中存在 requiresProbe: true 的接口,则必须先完成 3.6 probe 执行(成功或降级兜底)后才能进入本阶段,否则禁止进入 |
| 产出物 | ① 原子接口清单;② API 依赖图;③ storage key 清单 |
| 下一步 | 三份产出物齐全 → 阶段 5 |
4.1 技能划分:同业务域(商品/订单/地址)原子接口聚合到同一 skill;共享 storage 上下文的接口必须在同一 skill 内;每 skill 推荐 3-8 个原子接口(更多则按子业务拆分)。
4.2 接口字段:每条接口含 name(驼峰、全局唯一)/ description(含内部串联操作,帮助小程序 AI 决策)/ inputSchema(仅小程序 AI 需从用户获取的参数;无参用 {"type":"object","properties":{}})/ outputSchema(对应 structuredContent)/ _meta.ui.pagePath(按需,接力页 path、不含 query;"执行完停下等用户确认"类接口需配,配合返回值 handoff,详见 C.3.3)/ _meta.ui.componentPath(仅当用户明确要求生成原子组件时才声明,格式 components/xxx/index;声明则组件目录必须 4 文件齐全)。
多模态入参:当接口需要用户上传图片(如 P 图、图像识别)时,对应
inputSchema.properties.<field>加"format": "image",类型为string(运行时填本地图片路径)。小程序 AI 输入框会据此识别为多模态字段、引导用户上传图片。
4.3 进小程序方式(默认 handoff,不默认生成组件):默认不生成原子组件——需接续操作/查看详情的接口,配 _meta.ui.pagePath + 返回 handoff(见 C.3.3),用户点小程序卡片进接力页。仅当用户明确要求生成原子组件(GUI 卡片)时,才按返回值类型对照组件模板(详见 references/COMPONENT_TEMPLATES.md):列表/卡片项 → 通用列表;详情/单对象 → 详情卡片;购物车/带数量总价 → 购物车;下单成功/支付结果/操作确认 → 状态结果。
4.4 产出物示例(默认形态:无组件,配 handoff):
[{ "skill": "business", "name": "searchItems", "title": "检索商品", "description": "根据关键词检索商品,返回商品列表", "inputSchema": { "type": "object", "properties": {} }, "outputSchema": { "type": "object", "properties": { "items": { "type": "array" } } }, "_meta": { "ui": { "pagePath": "/pages/goods/list" } } }]
用户明确要求生成原子组件时,才在
_meta.ui追加componentPath: "components/item-list/index"并生成组件目录。
API 依赖图(仅在通过 storage 传上下文时必备):
searchProducts ──(storage: skills_shopping_lastSearchResult)──▶ addToCart └─(storage: skills_shopping_lastSearchResult)──▶ getProductDetail
storage key 命名统一 skills_{skillName}_{dataName},列表含 key / 写入方 / 读取方 / 数据结构。
契约:
| 项 | 内容 |
|---|---|
| 入口条件 | 阶段 4 三份产出物齐全 |
| 产出物 | 完整的 skills/{skill-name}/(mcp.json / SKILL.md / index.js / apis/* / utils/* / components/*) |
| 下一步 | 代码生成完成 → 阶段 6 |
| 阻断条件 | 产出物缺失 → 停留本阶段补齐 |
代码模板见 references/CODE_TEMPLATES.md、组件模板见 references/COMPONENT_TEMPLATES.md、设计规范见 references/ATOMIC_COMPONENT_DESIGN.md(最高优先级)、CSS 实现规范见 references/ATOMIC_COMPONENT_CSS.md。
默认不生成原子组件 → 本节整节跳过,只生成原子接口 + handoff。仅当用户明确要求生成 GUI 卡片时才执行。
| 编号 | 主题 | 关键要点 | 详见 |
|---|---|---|---|
| 5.0.0 设计规范(最高优先级) | 尺寸/主题/边距/字体/布局/操作区 | ① 5 档宽高比 + 圆角 4px;② 主题色按 §2.1 流程从主包 app.json/app.wxss 抽(浅 + 暗都抽,wxss 顶部注释"色源=…"链路;主包 6 步都查不到才走 §2.3 兜底);③ 边距 屏幕 16 / 卡片 12 / 元素 8·16;④ 字号 17/15/12 三档 + 同一基色 0.9/0.45/0.3 透明度分层;⑤ 主轴上下/左右布局;横向超长可用 <scroll-view scroll-x>,禁纵向滚动、禁 >2 列网格;⑥ ≤3 控件、主动作 ≤1、动宾文案、主按钮居右 | references/ATOMIC_COMPONENT_DESIGN.md |
| 5.0 源样式提取 + 字段映射 | 7 步工作流 | 与设计规范冲突时以设计规范为准,仅迁移源项目品牌色与字段映射结果。自检:wxss 主色是 #07c160 / #ff4d4f 且源页面未用,或 wxml 出现 item.imageUrl 但源 API 字段是 cover/pic/thumb — 视为"照抄模板",必须回炉重做 | references/STYLE_MIGRATION.md |
| 5.0.1 组件交互行为 | 组件是小程序 AI 的"回合出口",不是"页面入口" | 每个组件都要同时考虑"展示什么"+"用户下一步做什么"——按 mcp.json.apis[].description + API 依赖图列出下一步,映射到 mcp.json.apis[].name 已存在的接口;不存在则去掉按钮,不要上行不存在的 name。每个可交互元素绑 bindtap + hover-class,关键实体用 data-* 携带 | references/COMPONENT_TEMPLATES.md "上行消息"节 |
tap handler 优先形态 2(text + api/call 组合):
// 使用点必须现取 ctx;不要用 this._modelCtx 之类的缓存引用 wx.modelContext.getContext(this).sendFollowUpMessage({ content: [ { type: 'text', text: '<用户视角的简短中文,例如:选择拿铁>' }, { type: 'api/call', data: { name: '<mcp.json 已声明的 api name>', arguments: { /* 对齐该接口 inputSchema */ } } }, ], })
只有当点击动作无法映射到原子接口时才退回形态 1(单 text)。每次上行 api/call 前打一行 [ai-mode] {componentName} send api/call name=... args=... console.info。禁止:组件内直调业务接口、单独发 api/call 不带前导 text、arguments 用占位值、name 不在 mcp.json 中、只展示不响应的"死"按钮、用 this._modelCtx.sendFollowUpMessage(...) 缓存引用调方法。
{项目根目录}/ ├── app.json # 含 agent.skills 注册 └── skills/ # 独立分包(多 skill 共用) ├── _shared/ # 可选:≥2 个 skill 共用的工具函数才放这里 └── {skill-name}/ ├── mcp.json # 原子接口 Schema 定义 ├── SKILL.md # skill 路由说明 ├── index.js # 接口注册入口 ├── apis/ # 原子接口实现(推荐目录;validator 兼容 tools/services/、tools/) ├── utils/ # 工具模块(目录名不强制,常见 utils/services/helpers) └── components/{component-name}/ # index.js/json/wxml/wxss(路径强约束,与 mcp.json _meta.ui.componentPath 严格相等)
目录分层:跨 skill 禁止
require('../../{otherSkill}/...');多 skill 复用走skills/_shared/(不在mcp.json注册、不调registerAPI)。
mcp.json:顶层 { "apis": [...] },每项必含 name / description / inputSchema / outputSchema;进小程序的接口按需加 _meta.ui.pagePath(配合返回值 handoff);_meta.ui.componentPath 与 components[] 仅在用户明确要求生成原子组件时才有(components[] 声明组件网络能力,详见 C.3)。完整字段示例见 references/CODE_TEMPLATES.md 第四节SKILL.md(文件名严格全大写)定位"路由说明",只允许 5 节按序:能力域定位 → 触发场景(用户原话 few-shot)→ 不适用范围 → 前置条件 → 使用顺序。通篇禁止:驼峰 apiName / inputSchema / outputSchema / 参数表 / 返回值表 / componentPath / storage key / 接口依赖图 / 安装 CLI 运维。完整模板见 references/CODE_TEMPLATES.md 第五节{ isError?, content: [{type:'text', text}], structuredContent?, _meta?, handoff? }——content 给 LLM 文本,structuredContent 对应 outputSchema,_meta 对 LLM 不可见可传 UI 组件;handoff(进小程序按需,顶层与上述字段同级)为对象 { query, payload?, card? }(立即模式)或函数 ({ result }) => ({ query, payload?, card? })(延迟模式,result 为模型修改后的完整 result),query 为页面 query 键值对对象,详见 C.3.3created/attached / 收到 Result / setData / NotificationType.Overflow(必监听,用于校验裁剪)。统一前缀 [ai-mode]。日志不打够等于没日志——真机失败看不到关键节点 → 回阶段 5 补齐重跑契约:
| 项 | 内容 |
|---|---|
| 入口条件 | 阶段 5 生成完整 skills/{skill-name}/ |
| 产出物 | app.json 含 agent.skills + subPackages;project.config.json 的 packOptions.include 含 skills |
| 下一步 | 两份配置均已更新 → 交棒 wxa-skills-validate |
| 阻断条件 | 未更新配置直接交棒 → 必定失败,停留本阶段 |
配置格式见 references/CODE_TEMPLATES.md 第六节。关键要点:
agent.skills[].path 指向 skills/{skill-name} 目录subPackages 中 skills 整体作为 independent: true 的独立分包;多 skill 共用同一个分包——新增 skill 只在 agent.skills[] 里追加,不要为每个 skill 加一条 subPackages 条目project.config.json 的 packOptions.include 需含 { "type": "folder", "value": "skills" }_meta.ui.pagePath 并返回 handoff,在主包 app.js 的 onLaunch 内注册 wx.onAgentHandoff(详见 C.3.3 与 references/CODE_TEMPLATES.md "handoff 接力页" 节)阶段 6 完成后,必须在回复中明确告知用户:
代码生成与配置集成已完成。下一步请使用 `wxa-skills-validate` skill 对产物进行校验与真机验证: - skills 路径:<abs-path>/skills - project-path:<abs-path>(含 project.config.json 的 appid 为 <appid>) wxa-skills-validate 会依次执行:静态校验 → cli agent tool execute → cli agent render → 交付文档。
交棒步骤不可省略。仅输出代码不算完成,必须在对话中显式提示用户切换到校验 skill。
#!/usr/bin/env node
// scripts/probe.mjs
// 用法:node probe.mjs --project <path> --plan <plan.json> [--output <path>] [--auto-port 9420] [--cli-path <path>] [--mode launch|connect] [--ws-endpoint <url>]
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
import { parseArgs, detectDefaultCliPath, runProbePlan, summarize, DEFAULT_AUTO_PORT } from "./probe-lib.mjs";
async function main() {
const opts = parseArgs(process.argv.slice(2));
if (opts.help) {
console.log(`用法: node probe.mjs --project <path> --plan <plan.json> [--output] [--auto-port ${DEFAULT_AUTO_PORT}] [--cli-path] [--mode launch|connect] [--ws-endpoint]`);
process.exit(0);
}
if (!opts.project || !opts.plan) {
console.error("错误:必须提供 --project 和 --plan");
process.exit(2);
}
let plan;
try {
plan = JSON.parse(await readFile(resolve(opts.plan), "utf8"));
} catch (err) {
console.error(`错误:读取 plan 失败:${err.message}`);
process.exit(2);
}
if (!Array.isArray(plan) || !plan.length) {
console.error("错误:plan 必须是非空数组");
process.exit(2);
}
const cliPath = opts["cli-path"] || detectDefaultCliPath();
if (!cliPath && opts.mode !== "connect") {
console.error("错误:未找到微信开发者工具 CLI,请通过 --cli-path 指定或设置 WX_CLI_PATH 环境变量");
process.exit(2);
}
if (cliPath) console.log(`[probe] CLI: ${cliPath}`);
let payload;
try {
payload = await runProbePlan({
projectPath: resolve(opts.project),
plan,
autoPort: Number(opts["auto-port"]) || DEFAULT_AUTO_PORT,
cliPath,
launchTimeoutMs: Number(opts["launch-timeout"]) || undefined,
interactionTimeoutMs: Number(opts["interaction-timeout"]) || undefined,
outputPath: opts.output ? resolve(opts.output) : null,
mode: opts.mode === "connect" ? "connect" : "launch",
wsEndpoint: opts["ws-endpoint"],
});
} catch (err) {
console.error(`[probe] 执行失败:${err.message}`);
process.exit(2);
}
const sum = summarize(payload);
if (opts.output) {
console.log(`[probe] 结果已写入 ${opts.output}`);
} else {
console.log(JSON.stringify(payload, null, 2));
}
console.log(`[probe] 汇总:成功 ${sum.ok}/${sum.total},失败 ${sum.failed}`);
if (sum.failures.length) {
for (const f of sum.failures) {
console.error(` - ${f.api_name}: ${f.status} (${f.error || "未知"})`);
}
process.exit(1);
}
}
main().catch((err) => {
console.error(`[probe] 未处理异常:${err?.stack || err}`);
process.exit(2);
});// probe-lib.mjs — Automator 探针核心库
// 通过 evaluate 覆写 wx.request 捕获请求/响应,按 plan 执行交互
import { existsSync } from "node:fs";
import { mkdir, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { setTimeout as delay } from "node:timers/promises";
import { createConnection } from "node:net";
export const DEFAULT_AUTO_PORT = 9420;
export const DEFAULT_LAUNCH_TIMEOUT = 60_000;
export const DEFAULT_INTERACTION_TIMEOUT = 10_000;
// macOS 默认 CLI 路径
export const DEFAULT_CLI_PATH_MAC = "/Applications/wechatwebdevtools.app/Contents/MacOS/cli";
// Windows 默认 CLI 路径
export const DEFAULT_CLI_PATH_WIN = "C:/Program Files (x86)/Tencent/微信web开发者工具/cli.bat";
// 全局收集器变量名前缀(小程序端 window 上的 key)
const PROBE_COLLECTOR_PREFIX = "__ai_mode_probe_results__";
/**
* 加载 miniprogram-automator。
*/
async function loadAutomator() {
const { createRequire } = await import("node:module");
const { fileURLToPath } = await import("node:url");
const { dirname, join } = await import("node:path");
const __filename = fileURLToPath(import.meta.url);
const scriptsDir = dirname(__filename);
const scriptsRequire = createRequire(join(scriptsDir, "_entrypoint.js"));
let automator;
try {
automator = scriptsRequire("miniprogram-automator");
} catch (e) {
throw new Error(
`miniprogram-automator 未安装。请在 ${scriptsDir} 目录执行:\n` +
` cd ${scriptsDir} && npm install miniprogram-automator\n` +
`原始错误: ${e.message}`
);
}
// Patch: 新版开发者工具 Tool.getInfo 返回的 SDKVersion 可能为 undefined,
try {
const MiniProgram = scriptsRequire("miniprogram-automator/out/MiniProgram");
const MP = MiniProgram.default || MiniProgram;
if (MP?.prototype?.checkVersion) {
const orig = MP.prototype.checkVersion;
MP.prototype.checkVersion = async function () {
try { await orig.call(this); }
catch (err) { console.warn(`[probe] checkVersion 跳过: ${err.message}(不影响探测)`); }
};
}
} catch { /* patch 失败不阻断 */ }
return automator;
}
// ─── 工具函数 ────────────────────────────────────────────
/**
* 检测端口是否已被占用(即开发者工具是否已在 auto 模式运行)
*/
export function isPortInUse(port) {
return new Promise((resolve) => {
const client = createConnection(port, "127.0.0.1", () => { client.destroy(); resolve(true); });
client.on("error", () => { client.destroy(); resolve(false); });
client.setTimeout(2000, () => { client.destroy(); resolve(false); });
});
}
/**
* 获取默认 CLI 路径(按当前平台返回默认值)。
* 不做文件系统搜索——如果默认路径不存在,由调用方(LLM)通过 --cli-path 填充真实路径。
*/
export function detectDefaultCliPath() {
// 优先使用环境变量
const envCliPath = process.env.WX_CLI_PATH;
if (envCliPath && existsSync(envCliPath)) return envCliPath;
// 返回平台默认路径(存在则用,不存在则返回 null 让调用方指定)
const defaultPath = process.platform === "darwin" ? DEFAULT_CLI_PATH_MAC
: process.platform === "win32" ? DEFAULT_CLI_PATH_WIN
: null;
if (defaultPath && existsSync(defaultPath)) return defaultPath;
return null;
}
/**
* 解析命令行参数(--key value 格式)
*/
export function parseArgs(argv) {
const result = {};
for (let i = 0; i < argv.length; i++) {
if (!argv[i].startsWith("--")) continue;
const key = argv[i].slice(2);
const nextArg = argv[i + 1];
if (!nextArg || nextArg.startsWith("--")) {
result[key] = true;
} else {
result[key] = nextArg;
i++;
}
}
return result;
}
// ─── evaluate 注入:覆写 wx.request ─────────────────────
/**
* 通过 evaluate 在小程序运行时内覆写 wx.request,
* 在 success/fail 回调中记录请求参数 + 响应数据到全局变量。
* 原始请求仍然正常发出,业务行为不受影响。
*/
async function injectRequestCapture(miniProgram, collectorKey) {
await miniProgram.evaluate((key) => {
var global = window;
global[key] = [];
var originalRequest = wx.request;
wx.request = function (options) {
var requestInfo = {
url: options.url,
method: options.method || "GET",
data: options.data,
header: options.header,
};
return originalRequest({
url: options.url,
method: options.method,
data: options.data,
header: options.header,
success: function (response) {
global[key].push({
request: requestInfo,
response: { statusCode: response.statusCode, header: response.header, data: response.data },
});
if (options.success) options.success(response);
},
fail: function (error) {
global[key].push({
request: requestInfo,
response: { error: error && error.errMsg },
});
if (options.fail) options.fail(error);
},
});
};
}, collectorKey);
}
/**
* 从全局变量中读取已捕获的请求/响应,并清空收集器
*/
async function readCapturedRequests(miniProgram, collectorKey) {
const json = await miniProgram.evaluate((key) => {
var results = window[key] || [];
window[key] = [];
return JSON.stringify(results);
}, collectorKey);
return json ? JSON.parse(json) : [];
}
// ─── 交互步骤执行 ────────────────────────────────────────
async function executeStep(miniProgram, page, step) {
switch (step.kind) {
case "tap":
case "longpress": {
const element = await page.$(step.selector);
if (!element) throw new Error(`未找到元素:${step.selector}`);
await element[step.kind]();
break;
}
case "input": {
const element = await page.$(step.selector);
if (!element) throw new Error(`未找到元素:${step.selector}`);
await element.input(step.value);
break;
}
case "callMethod":
await page.callMethod(step.method, ...(step.args || []));
break;
case "request":
// 非 UI 直发:在运行时直接调用 wx.request(url/method/data/header 由静态分析提供)
// 适用于「不靠点击、需直接触发」的请求,或串联链路中的中间请求
await miniProgram.evaluate((opts) => { wx.request(opts); }, step.options || {});
break;
case "evaluate":
// 非 UI 通用:在运行时执行任意函数体字符串(如手动调用页面/全局取数函数)
await miniProgram.evaluate(new Function(step.code));
break;
case "wait":
await delay(step.ms || 500);
break;
default:
throw new Error(`未知 trigger.kind=${step.kind}`);
}
}
// ─── 导航辅助 ────────────────────────────────────────────
async function navigateToPage(miniProgram, pagePath) {
try { return await miniProgram.reLaunch(pagePath); }
catch { return await miniProgram.navigateTo(pagePath); }
}
// ─── 单接口探测 ──────────────────────────────────────────
async function probeOneApi({ miniProgram, planItem, interactionTimeoutMs }) {
const result = {
api_name: planItem.api_name,
target_page: planItem.target_page,
status: "pending",
request: null,
response: null,
requests: null, // 多请求链路(matchUrlIncludes 为数组时)的有序结果
duration_ms: 0,
error: null,
};
const startTime = Date.now();
const collectorKey = `${PROBE_COLLECTOR_PREFIX}${startTime}`;
try {
await injectRequestCapture(miniProgram, collectorKey);
// 执行前置步骤(如登录)
if (planItem.preSteps?.length) {
console.log(`[probe] ${planItem.api_name}: ${planItem.preSteps.length} 个前置步骤`);
const preStepPage = await navigateToPage(miniProgram, planItem.preSteps[0].target_page || planItem.target_page);
await delay(1000);
for (const preStep of planItem.preSteps) {
if (preStep.trigger) {
for (const triggerStep of preStep.trigger) {
await executeStep(miniProgram, preStepPage, triggerStep);
if (triggerStep.delayAfterMs) await delay(triggerStep.delayAfterMs);
}
}
if (preStep.waitMs) await delay(preStep.waitMs);
}
// 清空前置步骤产生的请求记录
await readCapturedRequests(miniProgram, collectorKey);
}
// 导航到目标页面(onLoad/onShow 自动发的请求此时已被捕获)+ 执行触发操作
// trigger 可为空:纯靠进页面自动发请求;也可用 request/evaluate 非 UI 直发
const targetPage = await navigateToPage(miniProgram, planItem.target_page);
await delay(800);
for (const triggerStep of planItem.trigger || []) {
await executeStep(miniProgram, targetPage, triggerStep);
if (triggerStep.delayAfterMs) await delay(triggerStep.delayAfterMs);
}
// 匹配关键词:支持单个(string)或多个有序(array,对应「一个能力 = 多请求」)
const patterns = Array.isArray(planItem.matchUrlIncludes)
? planItem.matchUrlIncludes
: planItem.matchUrlIncludes ? [planItem.matchUrlIncludes] : [];
const isMulti = Array.isArray(planItem.matchUrlIncludes);
// 轮询收集**全部**捕获请求(不再命中一条就停,确保多请求链路抓全)
const deadline = Date.now() + (planItem.captureWaitMs || interactionTimeoutMs);
const captured = [];
const allHit = () => patterns.length > 0 && patterns.every((p) => captured.some((c) => c.request?.url?.includes(p)));
while (Date.now() < deadline) {
await delay(500);
const batch = await readCapturedRequests(miniProgram, collectorKey);
if (batch.length) captured.push(...batch);
if (patterns.length ? allHit() : captured.length) break;
}
// 处理探测结果
if (!captured.length) {
result.status = "no_request";
result.error = "未捕获到 wx.request 调用";
} else if (!patterns.length) {
// 无匹配关键词:取第一条,其余进 extras
result.status = "ok";
result.request = captured[0].request;
result.response = captured[0].response;
if (captured.length > 1) result.extras = captured.slice(1).map((c) => ({ request: c.request, response: c.response }));
} else {
// 按 pattern 顺序逐个匹配
const matched = patterns.map((p) => {
const hit = captured.find((c) => c.request?.url?.includes(p));
return { matchUrlIncludes: p, request: hit?.request || null, response: hit?.response || null, matched: !!hit };
});
const hitCount = matched.filter((m) => m.matched).length;
if (hitCount === 0) {
result.status = "url_unmatched";
result.error = `${captured.length} 条请求均不匹配 ${JSON.stringify(patterns)}`;
result.request = captured.map((c) => c.request);
} else {
result.status = hitCount === patterns.length ? "ok" : "partial";
if (isMulti) {
result.requests = matched; // 多请求:返回完整有序列表
} else {
result.request = matched[0].request;
result.response = matched[0].response;
}
if (hitCount < patterns.length) result.error = `部分命中:${hitCount}/${patterns.length}`;
const usedUrls = new Set(matched.filter((m) => m.matched).map((m) => m.request?.url));
const others = captured.filter((c) => !usedUrls.has(c.request?.url));
if (others.length) result.extras = others.map((c) => ({ request: c.request, response: c.response }));
}
}
} catch (error) {
result.status = "error";
result.error = error?.stack || error?.message || String(error);
} finally {
result.duration_ms = Date.now() - startTime;
}
return result;
}
// ─── 主流程 ──────────────────────────────────────────────
export async function runProbePlan({
projectPath,
plan,
autoPort = DEFAULT_AUTO_PORT,
cliPath,
launchTimeoutMs = DEFAULT_LAUNCH_TIMEOUT,
interactionTimeoutMs = DEFAULT_INTERACTION_TIMEOUT,
outputPath,
mode = "launch",
wsEndpoint,
}) {
if (cliPath && !existsSync(cliPath)) throw new Error(`cliPath 不存在:${cliPath}`);
if (!existsSync(projectPath)) throw new Error(`projectPath 不存在:${projectPath}`);
const automator = await loadAutomator();
// 端口已占用时自动切换 connect 模式
let effectiveMode = mode;
let effectiveWsEndpoint = wsEndpoint;
if (mode === "launch" && await isPortInUse(autoPort)) {
console.log(`[probe] 端口 ${autoPort} 已占用,切换 connect 模式`);
effectiveMode = "connect";
effectiveWsEndpoint = `ws://127.0.0.1:${autoPort}`;
}
let miniProgram;
const results = [];
try {
if (effectiveMode === "connect") {
const endpoint = effectiveWsEndpoint || `ws://127.0.0.1:${autoPort}`;
console.log(`[probe] 连接 ${endpoint} ...`);
miniProgram = await automator.connect({ wsEndpoint: endpoint });
} else {
console.log(`[probe] Launch 端口 ${autoPort} ...`);
miniProgram = await automator.launch({ cliPath, projectPath, port: autoPort, timeout: launchTimeoutMs });
}
console.log("[probe] 已就绪");
for (const planItem of plan) {
console.log(`[probe] 探测: ${planItem.api_name} → ${planItem.target_page}`);
const probeResult = await probeOneApi({ miniProgram, planItem, interactionTimeoutMs });
results.push(probeResult);
console.log(`[probe] ${probeResult.status}${probeResult.error ? ` (${probeResult.error})` : ""}`);
}
} finally {
if (miniProgram && effectiveMode !== "connect") {
try { await miniProgram.close(); } catch {}
}
}
const payload = {
runId: new Date().toISOString().replace(/[-:T]/g, "").slice(0, 13),
project: projectPath,
autoPort,
mode: effectiveMode,
results,
};
if (outputPath) {
const absolutePath = resolve(outputPath);
await mkdir(dirname(absolutePath), { recursive: true });
await writeFile(absolutePath, JSON.stringify(payload, null, 2), "utf8");
}
return payload;
}
export function summarize(payload) {
const total = payload.results.length;
const okCount = payload.results.filter((result) => result.status === "ok").length;
const failures = payload.results.filter((result) => result.status !== "ok");
return { total, ok: okCount, failed: failures.length, failures };
}来源:用户上传
用户上传标准 Skill 文件夹。