🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@double-coding/pixel-print

Package Overview
Dependencies
Maintainers
2
Versions
10
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@double-coding/pixel-print - npm Package Compare versions

Comparing version
1.0.0
to
1.0.2
+28
templates/adapter-presets/rn.json
{
"id": "rn",
"name": "pure React Native / Expo",
"description": "6 大 RN 原生标签保留原名(View / Text / Image / Pressable / TextInput / ScrollView),全部从 react-native 导入。适合纯 RN / Expo 项目;想用 xtaro / taro / native-base 请选对应预设",
"helperTemplate": "rn.rpx.ts",
"referenceDoc": "rn.reference.md",
"adapter": {
"enabled": true,
"tagMap": {
"View": "View",
"Text": "Text",
"Image": "Image",
"Pressable": "Pressable",
"TextInput": "TextInput",
"ScrollView": "ScrollView"
},
"importMap": {
"View": "react-native",
"Text": "react-native",
"Image": "react-native",
"Pressable": "react-native",
"TextInput": "react-native",
"ScrollView": "react-native"
},
"propMap": {},
"reactImport": "react"
}
}
# pure RN / Expo adapter 参考手册
> **pure RN 预设不做标签替换**(tagMap 全部是 identity 映射,View→View / Text→Text / ...),所以**没有跨组件库差异**需要处理。本手册只列 RN 内核本身可能触发 QA warn 的边界情况。
>
> 如果你在用 native-base / react-native-paper / gluestack-ui 等 RN 生态组件库,请**另建预设**(参考 xtaro.json / taro.json 的结构),而不是继续用本预设。
## 使用约定
- 本手册**只影响 §5.5.3c 步骤**,但对 pure RN 来说 §5.5.3c 几乎是 no-op(propMap 空、无跨组件库映射)
- 主要作用:让 agent 在生成 pure RN 产物时**保持 SKILL 内核默认行为**,不做任何后处理
- 如果发现 pure RN 项目里出现了跨组件库差异,说明选错了预设
---
## 一、值域映射
**无**。pure RN 保留原生标签,无值域映射需求。
---
## 二、布尔/值取反
**无**。RN 内核就是 RN 内核,不需要转成别人。
---
## 三、事件签名转换
**无**。RN 内核就是 RN 内核,不需要事件签名转换。
- `TextInput.onChangeText(text: string) => void` 直接保留
- `Pressable.onPress(event: GestureResponderEvent) => void` 直接保留
- `ScrollView.onEndReached / onEndReachedThreshold` 直接保留
---
## 四、结构变化
**无**。RN 内核标签的 prop 结构本来就是 SKILL 内核描述的目标形态,不需要重塑。
- `ScrollView.horizontal={true}` 直接保留(RN 就是这个 API)
- `ScrollView.contentContainerStyle` 直接保留
---
## 五、无跨端支持
**无**。pure RN 生态本身就是 iOS / Android(+ Expo 场景)双端,SKILL 生成产物默认走 RN 官方 API,没有"不支持"这一说。
不过有几个**平台差异**需要提醒(不影响 agent 生成,只在 §7 QA 段做 info 提示):
| API | 差异 | agent 处理 |
|---|---|---|
| `TextInput.keyboardType='visible-password'` | 仅 Android | 静默保留 + QA info(iOS 上等效退化为 default) |
| `TextInput.textContentType` | 仅 iOS | 静默保留 + QA info(Android 无影响) |
| `Pressable.onLongPress delayLongPress` | RN 0.63+ | 静默保留(默认 500ms) |
---
## 六、agent 快速参考(§5.5.3c 执行 checklist)
**pure RN 预设下,§5.5.3c 直接跳过**(referenceDoc 存在但内容全部 no-op)。
agent 仍需要走一遍 §5.5.3c 流程验证 preset 完整性,但**不会命中任何改写规则**。§7 QA 段照常输出"参考手册命中:0 条(pure RN 预设)"。
---
## 为什么这个预设仍然需要?
pure RN 预设的意义**不在于映射规则**,而在于:
1. **让用户显式选择**:CLI init 时"选择预设 adapter"选项列表里有一个明确的"我不做替换"选项,而不是让用户困惑于"不选预设 = 什么行为?"
2. **保留 helper 一致性**:预设自带 `rn.rpx.ts` 使用 pure RN 的 `Dimensions.get('window')`,与 xtaro / taro 预设的 helper 走同一套接口(都从 `@/utils/rpx` 导出 `rpx()`),SKILL 生成产物无需关心底层实现
3. **未来扩展的对照基线**:如果社区加 native-base / paper 等预设,可以对照 pure RN 预设看"哪些差异需要处理"
import { Dimensions } from 'react-native'
/**
* 响应式尺寸包装(pure RN / Expo 版)— 把设计稿数值按屏幕宽度线性缩放。
*
* 由 pp-d2c-rn SKILL 生成的代码使用。SKILL 只对 layout / spacing / borderRadius / fontSize 类属性调用 rpx(),
* 对 opacity / flex / color / fontWeight 这类"非像素属性"保持原始数值。
*
* 命名说明: rpx 沿用小程序 / uni-app 的 responsive px 惯例。默认基准 375(iPhone Mini 类竖屏宽度),
* 与 config.unit.figmaBase 保持一致。改基准请同时改 config, 不要只改本文件。
*
* 无障碍备注: 本 helper **不**跟随系统字号(Dimensions 不感知 fontScale),
* 视觉一致优先。需要无障碍的团队可自行改本文件, SKILL 不会覆盖已存在的文件。
*
* 屏宽取值: Dimensions.get('window').width 是当前可用布局宽(排除状态栏);
* 静态导入时机是模块 load 时,横屏切换 / 分屏调整时 rpx() 返回值不会跟着变。
* 如需响应式切换,请改成 useWindowDimensions() hook 版本(需要在组件内调用)。
*/
const DESIGN_BASE = 375
const SCREEN_W = Dimensions.get('window').width || DESIGN_BASE
const SCALE = SCREEN_W / DESIGN_BASE
export function rpx(size: number): number {
return size * SCALE
}
export const screenWidth = SCREEN_W
export const scale = SCALE
{
"id": "taro",
"name": "Taro (@tarojs/components)",
"description": "6 大 RN 标签映射到 @tarojs/components;Pressable 归到 View(View 自身可点击);Image.source → src(taro Image 用 src);TextInput → Input;rpx helper 走 Taro.getSystemInfoSync from @tarojs/taro",
"helperTemplate": "taro.rpx.ts",
"referenceDoc": "taro.reference.md",
"adapter": {
"enabled": true,
"tagMap": {
"View": "View",
"Text": "Text",
"Image": "Image",
"Pressable": "View",
"TextInput": "Input",
"ScrollView": "ScrollView"
},
"importMap": {
"View": "@tarojs/components",
"Text": "@tarojs/components",
"Image": "@tarojs/components",
"Input": "@tarojs/components",
"ScrollView": "@tarojs/components"
},
"propMap": {
"Image": {
"source": "src"
},
"TextInput": {
"maxLength": "maxlength",
"secureTextEntry": "password",
"autoFocus": "focus",
"onSubmitEditing": "onConfirm"
},
"ScrollView": {
"onEndReached": "onScrollToLower"
}
},
"reactImport": "react"
}
}
# taro adapter 参考手册
> SKILL 在 §5.5.3c 步骤 Read 本文件。**声明式 propMap 已在 taro.json 处理的差异不在此重复**,本文只覆盖"prop 名机械改名之外"的复杂差异。
>
> 覆盖对象:@tarojs/components 组件(View / Text / Image / Input / ScrollView + Pressable→View 归并)与 RN 内核标签的语义差异。
>
> 数据来源:Taro 官方文档 https://taro-docs.jd.com/docs/components/viewContainer/view
## 使用约定
- 本手册**只影响 §5.5.3c 步骤**(propMap 声明式改名之后、import 重写之前),不改变主流程结构
- 每一节都是 agent 的 checklist:命中场景 → 按"改写规则"改;找不到对应关系 → 按"丢弃策略"处理并写入 §7 QA 段
- **绝不**在此手册里放"改名类"差异(否则与 taro.json 分工混乱) — 纯改名一律回 taro.json `propMap`
---
## 一、值域映射(prop 名一致或已改名,但取值域不同)
taro.json propMap 只能改 prop 名,不能改 prop 值。命中下表时,agent **在 §5.5.3c 时按 valueMap 改属性值**。
### 1.1 `Image.resizeMode` (RN) → `mode` (Taro)
**说明**:Taro Image 用 `mode` prop(与 xtaro 一致的 taro 语义),取值和 RN 的 resizeMode 不同,agent 必须同时改 prop 名和值。
| RN 值 | Taro 值 | 备注 |
|---|---|---|
| `'contain'` | `'aspectFit'` | 保持纵横比,长边完全显示 |
| `'cover'` | `'aspectFill'` | 保持纵横比,填满容器 |
| `'stretch'` | `'scaleToFill'` | 不保持比例,完全拉伸(Taro 默认值) |
| `'center'` | `'center'` | 不缩放,只显示中间区域 |
| `'repeat'` | *无对应* | Taro 无此值 → 退化 `'scaleToFill'` + 写入 §7 QA warn |
**注**:taro.json propMap 已把 `resizeMode` 视作字面改名(值域也需要同步改),此处补齐值改写规则。
### 1.2 `TextInput.keyboardType` (RN) → `type` (Taro Input)
**说明**:Taro Input 用 `type` 而不是 `keyboardType`,取值域也不同。
| RN keyboardType | Taro Input type | 备注 |
|---|---|---|
| `'default'` | `'text'` | 默认文本键盘 |
| `'numeric'` | `'number'` | 数字键盘 |
| `'email-address'` | `'text'` | Taro Input 无 email 类型,退化 text + QA warn |
| `'phone-pad'` | `'number'` | 退化 number |
| `'decimal-pad'` | `'digit'` | 带小数点数字 |
| `'number-pad'` | `'number'` | 同 numeric |
| `'ascii-capable'` / `'url'` / 其他 | `'text'` | 全部退化 text + QA warn |
### 1.3 `TextInput.returnKeyType` (RN) → `confirmType` (Taro Input)
**说明**:值域**基本一致**,但 RN 有 Taro 没有的 `'default'` / `'previous'` 等,退化处理。
| RN returnKeyType | Taro confirmType | 备注 |
|---|---|---|
| `'done'` | `'done'` | 一致 |
| `'search'` | `'search'` | 一致 |
| `'go'` | `'go'` | 一致 |
| `'next'` | `'next'` | 一致 |
| `'send'` | `'send'` | 一致 |
| `'default'` / `'previous'` / `'yahoo'` / `'google'` / `'route'` | `'done'` | 全部退化 + QA warn |
---
## 二、布尔/值取反
RN 与 Taro 语义相反的属性,agent 在 §5.5.3c 时同时改 prop 名和值。
### 2.1 `TextInput.editable` (RN, 默认 true) → `disabled` (Taro Input, 默认 false)
**改写规则**:
```tsx
// 改写前
<TextInput editable={false} />
<TextInput editable={true} />
<TextInput editable={someVar} />
// 改写后
<Input disabled={true} />
<Input disabled={false} />
<Input disabled={!someVar} />
```
**注**:变量引用场景要包一层 `!` 取反;字面 boolean 直接对换 true/false;省略 `editable` prop 无需处理(默认 editable=true → 默认 disabled=false,语义一致)。
---
## 三、事件签名转换
RN 事件 payload 与 Taro 不同,agent 生成时需**改回调函数体**,不只是改 prop 名。
### 3.1 `TextInput.onChangeText` (RN) → `onInput` (Taro Input)
**签名差异**:
- RN: `onChangeText: (text: string) => void`
- Taro: `onInput: (e: { detail: { value: string, cursor: number, keyCode: number } }) => void`
**改写规则**:
```tsx
// 改写前
<TextInput onChangeText={(text) => setValue(text)} />
<TextInput onChangeText={handleTextChange} />
// 改写后
<Input onInput={(e) => setValue(e.detail.value)} />
<Input onInput={(e) => handleTextChange(e.detail.value)} />
```
**注**:命中此项时 agent **必须查回调函数体**,把原来接收 `text` 的地方改成 `e.detail.value`;若原回调是命名函数(如 `handleTextChange`)则包一层箭头函数适配。
### 3.2 `Pressable.onPress` (RN) → `onClick` (Taro View)
**签名差异**:
- RN: `onPress: (event: GestureResponderEvent) => void`
- Taro: `onClick: (event: ITouchEvent) => void` — event 结构不同,但绝大多数业务代码不用 event → 直接改 prop 名即可
**改写规则**:
```tsx
// 改写前
<Pressable onPress={() => doSomething()} />
<Pressable onPress={handleTap} />
// 改写后(Pressable 已被 tagMap 映射到 View)
<View onClick={() => doSomething()} />
<View onClick={handleTap} />
```
**注**:若原回调**用了** `event.nativeEvent` / `event.locationX` 之类 RN 特有字段,则改写后需 QA warn 提示手工核对(极少见);仅 `() => doSomething()` 或不用 event 的 → 静默改。
### 3.3 `Pressable.onPressIn` / `onPressOut` (RN) → 无直接对应
**说明**:Taro View 没有 pressIn/pressOut,只有 touchStart / touchEnd。
**改写规则**:
```tsx
// 改写前
<Pressable onPressIn={onIn} onPressOut={onOut} />
// 改写后
<View onTouchStart={onIn} onTouchEnd={onOut} />
```
**注**:Taro 不做 tap 手势细分(不像 RN Pressable 有 delayPressIn),行为可能有微差,QA warn 记录。
### 3.4 `ScrollView.onEndReached` 阈值单位差异
**说明**:taro.json 已把 `onEndReached` 改名成 `onScrollToLower`,但**相关的 `onEndReachedThreshold` 单位也变了**。
- RN `onEndReachedThreshold`: `0-1 浮点数`(离底部剩余占屏幕比例)
- Taro `lowerThreshold`: `像素数`(离底部剩余像素)
**改写规则**:
```tsx
// 改写前
<ScrollView onEndReached={loadMore} onEndReachedThreshold={0.1} />
// 改写后
<ScrollView onScrollToLower={loadMore} lowerThreshold={50} />
// 注: 0.1 * 屏高(约 812) ≈ 81px,四舍五入取 50(Taro 默认值)
// 若原代码有具体屏高变量,可写 lowerThreshold={屏高变量 * 0.1};否则用 50 作 sensible default + QA warn
```
**注**:值需要业务侧手工核对,agent 直接给 sensible default(50)并写 §7 QA warn。
---
## 四、结构变化(一个 prop 拆多个 / 需要包一层)
改写涉及 JSX 结构,不是简单 prop 转换。agent 在 §5.5.3c 时按下述规则重塑 JSX。
### 4.1 `ScrollView.horizontal` (RN, boolean) → `scrollX + scrollY` (Taro, 双 boolean 二选一)
**说明**:Taro ScrollView 用两个独立 prop 表达方向,必须二选一,不允许同 true。
**改写规则**:
```tsx
// 改写前 A: horizontal=true → 横滚
<ScrollView horizontal={true} />
// 改写前 B: horizontal=false / 缺省 → 纵滚
<ScrollView horizontal={false} />
<ScrollView />
// 改写后 A
<ScrollView scrollX={true} />
// 改写后 B
<ScrollView scrollY={true} />
```
**注**:agent 必须**同时删除 horizontal prop** + **加对应方向的 scrollX/scrollY**。原来省略 horizontal 的场景需**主动补** `scrollY={true}`(RN 默认纵滚,Taro 必须显式声明)。
### 4.2 `ScrollView.contentContainerStyle` (RN) → 需包一层 View (Taro)
**说明**:Taro ScrollView 只有 `style`,没有 `contentContainerStyle`。RN 里 style 作用于外层滚动容器、contentContainerStyle 作用于内部内容区,Taro 需要**手工把内容区包一层 View**。
**改写规则**:
```tsx
// 改写前
<ScrollView style={styles.wrap} contentContainerStyle={styles.content}>
<Item />
<Item />
</ScrollView>
// 改写后
<ScrollView style={styles.wrap} scrollY={true}>
<View style={styles.content}>
<Item />
<Item />
</View>
</ScrollView>
```
**注**:若原代码没写 contentContainerStyle,不需要包 View,静默改;若有 contentContainerStyle,agent 必须包一层 View 并把该 style 挂上去 + QA info 提示"新增了 View 内容容器"。
---
## 五、无跨端支持(直接丢弃 + QA warn)
以下属性 Taro rn 端不支持,agent 在 §5.5.3c 时**删除属性** + 写入 §7 QA warn 段(列出文件名 + 行号 + 属性名)。
| 原属性(RN) | 原标签 | 丢弃原因 |
|---|---|---|
| `numberOfLines` | `Text` | Taro Text 只在 alipay 端支持;rn/h5/小程序端多行省略需在业务代码用 CSS `-webkit-line-clamp` 或 Text 手工截断 |
| `ellipsizeMode` | `Text` | 无对应 |
| `selectable` | `Text` | ✅ 一致,不丢(此项仅列为对照,agent 保留原属性) |
| `showsHorizontalScrollIndicator` | `ScrollView` | 无内置;Taro 靠 CSS `::-webkit-scrollbar` 隐藏,业务侧手工写 |
| `showsVerticalScrollIndicator` | `ScrollView` | 同上 |
| `pagingEnabled` | `ScrollView` | Taro 只 weapp 支持;rn/h5 端无 |
| `onMomentumScrollEnd` | `ScrollView` | Taro 只 weapp 支持 |
| `multiline` | `TextInput` | 无;若确需多行,改用 `Textarea`(不同标签,超出本预设覆盖范围,QA error) |
| `keyboardAppearance` | `TextInput` | 无对应 |
| `blurOnSubmit` | `TextInput` | 无对应 |
| `caretHidden` | `TextInput` | 无对应 |
**注**:`selectable` 列在这里只作对照(实际保留);其他项 agent 静默删 + §7 QA 段 warn。
---
## 六、agent 快速参考(§5.5.3c 执行 checklist)
按此顺序遍历每个 index.tsx 的 JSX:
1. **值域映射**(§一):`resizeMode`(Image)/ `keyboardType`(TextInput)/ `returnKeyType`(TextInput) 命中 → 按 valueMap 改值 + 必要时改 prop 名
2. **布尔取反**(§二):`editable`(TextInput) 命中 → 改成 `disabled` + 值取反
3. **事件签名**(§三):`onChangeText` / `onPress` / `onPressIn` / `onPressOut` / `onEndReachedThreshold` 命中 → 改 prop 名 + 改回调函数体(§3.1 需读回调体)
4. **结构变化**(§四):`ScrollView.horizontal` 命中 → 删原 prop + 加 scrollX/scrollY;`contentContainerStyle` 命中 → 包 View
5. **丢弃属性**(§五):表内属性命中 → 删属性 + 写入 §7 QA warn 段
**未在本手册出现的属性**一律**保留原样**;`style / key / ref / children / className` 永远不改。
import Taro from '@tarojs/taro'
/**
* 响应式尺寸包装(Taro 版)— 把设计稿数值按屏幕宽度线性缩放。
*
* 与 pure RN 版的区别: 用 @tarojs/taro 提供的 getSystemInfoSync 而不是 Dimensions.get('window'),
* 因为 taro 覆盖多端(小程序 / H5 / RN),不能依赖 react-native 的 Dimensions。
* taro 提供了统一的 getSystemInfoSync 屏蔽端差异。
*
* 由 pp-d2c-rn SKILL 生成的代码使用。SKILL 只对 layout / spacing / borderRadius / fontSize 类属性调用 rpx(),
* 对 opacity / flex / color / fontWeight 这类"非像素属性"保持原始数值。
*
* 命名说明: rpx 沿用小程序 / uni-app 的 responsive px 惯例。默认基准 375(iPhone Mini 类竖屏宽度),
* 与 config.unit.figmaBase 保持一致。改基准请同时改 config, 不要只改本文件。
*
* 无障碍备注: 本 helper **不**跟随系统字号(小程序端 taro 也无 fontScale 概念),
* 视觉一致优先。需要无障碍的团队可自行改本文件, SKILL 不会覆盖已存在的文件。
*
* windowWidth vs screenWidth: 优先 windowWidth(可用布局宽,排除状态栏/胶囊按钮等),
* 拿不到时降级 screenWidth, 再兜底 375(SSR / 编译期读取时无 window 全局)。
*/
const DESIGN_BASE = 375
function resolveWidth(): number {
try {
const info = Taro.getSystemInfoSync()
return info.windowWidth || info.screenWidth || DESIGN_BASE
} catch {
return DESIGN_BASE
}
}
const SCREEN_W = resolveWidth()
const SCALE = SCREEN_W / DESIGN_BASE
export function rpx(size: number): number {
return size * SCALE
}
export const screenWidth = SCREEN_W
export const scale = SCALE
+173
-47

@@ -32,2 +32,74 @@ #!/usr/bin/env node

// ─── .env 读写(极简,不引 dotenv) ─────────────────────────────
// 只处理 KEY=VALUE,支持 # 注释和引号;不做变量插值。与 figma.mjs::parseEnvFile 语义对齐
const ENV_PATH = path.join(CWD, '.env')
function readEnvFile() {
if (!fs.existsSync(ENV_PATH)) return { lines: [], map: {} }
const text = fs.readFileSync(ENV_PATH, 'utf8')
const lines = text.split(/\r?\n/)
const map = {}
for (const raw of lines) {
const line = raw.trim()
if (!line || line.startsWith('#')) continue
const m = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/)
if (!m) continue
let val = m[2]
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
val = val.slice(1, -1)
}
map[m[1]] = val
}
return { lines, map }
}
// 追加或更新一个 key,保留其他行不动;value 含空格/引号时自动包双引号
// 原则:.env 已存在就往里写,不覆盖整份;同名 key 就地替换(如果新旧值不同才写 .env.bak,且 bak 不覆盖已有的老 bak)
function upsertEnvVar(key, value) {
const exists = fs.existsSync(ENV_PATH)
const needsQuote = /[\s"'#]/.test(value)
const rendered = needsQuote ? `"${value.replace(/"/g, '\\"')}"` : value
const targetLine = `${key}=${rendered}`
if (!exists) {
fs.writeFileSync(ENV_PATH, targetLine + '\n')
return { action: 'create' }
}
const text = fs.readFileSync(ENV_PATH, 'utf8')
const lineRe = new RegExp(`^${key}\\s*=.*$`, 'm')
if (lineRe.test(text)) {
// 判断新旧值是否真变了,没变就完全不动
const oldLine = text.match(lineRe)[0]
if (oldLine === targetLine) return { action: 'unchanged' }
// 变了才做原地替换;bak 只在不存在时才创建,避免连续跑 init 冲掉真正想恢复的老备份
const bakPath = ENV_PATH + '.bak'
if (!fs.existsSync(bakPath)) fs.writeFileSync(bakPath, text)
const next = text.replace(lineRe, targetLine)
fs.writeFileSync(ENV_PATH, next)
return { action: 'replace', backup: fs.existsSync(bakPath) ? bakPath : null }
}
// 追加(确保前面有换行)
const next = text.endsWith('\n') || text.length === 0 ? text + targetLine + '\n' : text + '\n' + targetLine + '\n'
fs.writeFileSync(ENV_PATH, next)
return { action: 'append' }
}
// 项目根 .gitignore 保证有 .env 行(否则 token 会被 git 追踪)
function ensureGitignoreHasEnv() {
const gitignorePath = path.join(CWD, '.gitignore')
const line = '.env'
if (!fs.existsSync(gitignorePath)) {
fs.writeFileSync(gitignorePath, `# Local env (contains FIGMA_TOKEN — never commit)\n${line}\n`)
console.log(' → 创建 .gitignore 并加入 .env')
return
}
const text = fs.readFileSync(gitignorePath, 'utf8')
const hasLine = text.split(/\r?\n/).some(l => l.trim() === line || l.trim() === line + '/')
if (hasLine) return
const next = text.endsWith('\n') ? text + `\n# Local env (contains FIGMA_TOKEN — never commit)\n${line}\n` : text + `\n\n# Local env (contains FIGMA_TOKEN — never commit)\n${line}\n`
fs.writeFileSync(gitignorePath, next)
console.log(' → .gitignore 追加 .env 行')
}
function copyDir(srcDir, destDir, force = false) {

@@ -96,8 +168,18 @@ for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {

let idx = Math.max(0, choices.indexOf(defaultVal))
let rendered = false
// 多行渲染:label 一行 + 每个选项一行。上一版单行平铺遇到长选项会终端硬 wrap,
// 只清 \r\x1b[K 清不到 wrap 出来的行 → 方向键切换时堆多行。改成回到起点用 \x1b[0J 清到底。
const totalLines = choices.length + 1
function render() {
// 清除已渲染的行
process.stdout.write(`\r\x1b[K`)
const parts = choices.map((c, i) => i === idx ? `\x1b[36m● ${c}\x1b[0m` : ` ${c}`)
process.stdout.write(` ${label}: ${parts.join(' ')}\x1b[0K`)
if (rendered) {
// 光标上移 totalLines 行,再清到屏幕底部
process.stdout.write(`\x1b[${totalLines}A\x1b[0J`)
}
process.stdout.write(` ${label}:\n`)
for (let i = 0; i < choices.length; i++) {
if (i === idx) process.stdout.write(` \x1b[36m● ${choices[i]}\x1b[0m\n`)
else process.stdout.write(` ${choices[i]}\n`)
}
rendered = true
}

@@ -119,3 +201,5 @@

process.stdin.pause()
process.stdout.write(`\r\x1b[K ${label}: \x1b[36m${choices[idx]}\x1b[0m\n`)
// 确认后擦掉多行菜单,换成单行 "label: 选中值"
process.stdout.write(`\x1b[${totalLines}A\x1b[0J`)
process.stdout.write(` ${label}: \x1b[36m${choices[idx]}\x1b[0m\n`)
resolve(choices[idx])

@@ -244,42 +328,62 @@ }

// ─── 【新增】adapter 引导 ─────────────────────────
// ─── 【新增】组件框架映射引导 ─────────────────────────
// adapter 把 RN 原生标签映射到 xtaro / taro / 其他框架
// 已有完整 adapter 配置 → 直接沿用,不再询问
// 没配置过 → 一层选择:不启用 / xtaro / taro / pure RN / 自定义
const existingAdapter = existing.adapter || {}
const existingEnabledYn = existingAdapter.enabled === true ? 'Yes' : existingAdapter.enabled === false ? 'No' : null
const enableAdapterYn = await pickOrUse('[2.1/8] 是否启用 adapter 映射(把 RN 标签映射到 xtaro/taro 等)',
existingEnabledYn, ['No', 'Yes'], 'No')
const enableAdapter = enableAdapterYn === 'Yes'
if (!enableAdapter) {
adapterCfg = { enabled: false, tagMap: {}, importMap: {}, propMap: {}, reactImport: 'react' }
} else {
// 判断是否已存在完整 adapter 配置,已有则直接沿用
const hasExistingMap = existingAdapter.tagMap && Object.keys(existingAdapter.tagMap).length > 0
if (hasExistingMap) {
logUseExisting('[2.2/8] adapter 映射', `${Object.keys(existingAdapter.tagMap).length} 条(沿用)`)
adapterCfg = {
enabled: true,
tagMap: existingAdapter.tagMap || {},
importMap: existingAdapter.importMap || {},
propMap: existingAdapter.propMap || {},
reactImport: existingAdapter.reactImport || 'react'
}
} else {
// 扫 templates/adapter-presets/ 目录,把每个 preset 的 name 列成选项,末尾追加"自定义"兜底
const hasExistingMap = existingAdapter.tagMap && Object.keys(existingAdapter.tagMap).length > 0
if (hasExistingMap) {
logUseExisting('[2.1/8] 组件框架映射', `${Object.keys(existingAdapter.tagMap).length} 条(沿用)`)
adapterCfg = {
enabled: true,
tagMap: existingAdapter.tagMap || {},
importMap: existingAdapter.importMap || {},
propMap: existingAdapter.propMap || {},
reactImport: existingAdapter.reactImport || 'react'
}
// 沿用旧 tagMap/propMap,但 referenceDoc + _presetSource 需要以最新 preset 为准
// 老 config 里可能根本没这两字段(v1.0.1 之前 install.js 漏写),必须现在补上,
// 否则 SKILL §5.5.3c 拿不到手册路径 → 复杂差异处理全部 no-op
if (existingAdapter.referenceDoc) adapterCfg.referenceDoc = existingAdapter.referenceDoc
if (existingAdapter._presetSource) adapterCfg._presetSource = existingAdapter._presetSource
// 兜底回填:按 tagMap 反查匹配的 preset(用 View→? 这条最能定位框架)
if (!adapterCfg.referenceDoc || !adapterCfg._presetSource) {
const presets = loadAdapterPresets()
const CUSTOM_LABEL = '自定义'
const choices = [...presets.map(p => p.name), CUSTOM_LABEL]
const defaultChoice = presets[0]?.name || CUSTOM_LABEL
const picked = await select('[2.2/8] 选择预设 adapter', choices, defaultChoice)
if (picked === CUSTOM_LABEL) {
adapterCfg = { enabled: true, tagMap: {}, importMap: {}, propMap: {}, reactImport: 'react' }
console.log(' → adapter.enabled=true,请后续在 pp-d2c.config.json 手动填 tagMap / importMap / propMap')
const viewTarget = adapterCfg.tagMap.View
const hit = viewTarget
? presets.find(p => p.adapter && p.adapter.tagMap && p.adapter.tagMap.View === viewTarget)
: null
if (hit) {
if (!adapterCfg.referenceDoc && hit.referenceDoc) adapterCfg.referenceDoc = hit.referenceDoc
if (!adapterCfg._presetSource) adapterCfg._presetSource = PRESETS_DIR
console.log(` → 补齐 adapter.referenceDoc / _presetSource(反查匹配 ${hit.name} 预设)`)
} else {
const hit = presets.find(p => p.name === picked)
adapterCfg = { ...hit.adapter }
pickedPreset = hit
console.log(` → 已写入 ${hit.name} 预设(${hit.description || '见 templates/adapter-presets/README.md'})`)
console.log(' ⚠️ 未能反查到匹配的 preset,adapter.referenceDoc / _presetSource 保持缺失;SKILL §5.5.3c 复杂差异处理将 no-op')
}
}
} else {
// 扫 templates/adapter-presets/ 目录,把 preset.name 与"不启用"/"自定义"平铺在同一层选择
const presets = loadAdapterPresets()
const OFF_LABEL = '不启用(保留 RN 原写法)'
const CUSTOM_LABEL = '自定义(后续手填 tagMap/importMap/propMap)'
const choices = [OFF_LABEL, ...presets.map(p => p.name), CUSTOM_LABEL]
// 默认值:优先取第一个 preset(通常是 xtaro),没预设时用 OFF
const defaultChoice = presets[0]?.name || OFF_LABEL
const picked = await select('[2.1/8] 选择组件框架映射', choices, defaultChoice)
if (picked === OFF_LABEL) {
adapterCfg = { enabled: false, tagMap: {}, importMap: {}, propMap: {}, reactImport: 'react' }
} else if (picked === CUSTOM_LABEL) {
adapterCfg = { enabled: true, tagMap: {}, importMap: {}, propMap: {}, reactImport: 'react' }
console.log(' → adapter.enabled=true,请后续在 pp-d2c.config.json 手动填 tagMap / importMap / propMap')
} else {
const hit = presets.find(p => p.name === picked)
adapterCfg = { ...hit.adapter }
// 把 preset 顶层的 referenceDoc + preset 目录绝对路径写入 adapter 段
// SKILL §5.5.3c 靠这两字段拼参考手册路径;缺任一 → §5.5.3c 直接 no-op 跳过复杂差异处理
if (hit.referenceDoc) adapterCfg.referenceDoc = hit.referenceDoc
adapterCfg._presetSource = PRESETS_DIR
pickedPreset = hit
console.log(` → 已写入 ${hit.name} 预设(${hit.description || '见 templates/adapter-presets/README.md'})`)
}
}

@@ -295,3 +399,3 @@

const enableRespYn = await pickOrUse(
'[2.3/8] 是否启用响应式 rpx() 包装(按屏宽线性缩放尺寸)',
'[2.2/8] 是否启用响应式 rpx() 包装(按屏宽线性缩放尺寸)',
existingRespYn, ['Yes', 'No'], 'Yes'

@@ -302,7 +406,7 @@ )

const helperImport = await inputOrUse(
'[2.4/8] rpx helper import 路径',
'[2.3/8] rpx helper import 路径',
existingResp.helperImport, '@/utils/rpx'
)
const helperName = await inputOrUse(
'[2.5/8] rpx helper 导出函数名',
'[2.4/8] rpx helper 导出函数名',
existingResp.helperName, 'rpx'

@@ -408,12 +512,34 @@ )

// Figma Token 现在存 .env FIGMA_TOKEN,不再写入 pp-d2c.config.json
// 默认值优先级:process.env > 项目根 .env > 旧 config.figma.token(迁移场景)
const envCurrent = readEnvFile().map.FIGMA_TOKEN || ''
const legacyTokenInConfig = fig.token || ''
const defaultToken = process.env.FIGMA_TOKEN || envCurrent || legacyTokenInConfig
const figmaToken = await inputOrUse(
isRn ? '[单位2/2] Figma Personal Access Token(用于导出透明图片,回车跳过)'
: '[单位4/4] Figma Personal Access Token(用于导出透明图片,回车跳过)',
fig.token, ''
isRn ? '[单位2/2] Figma Personal Access Token(存到项目根 .env,回车跳过)'
: '[单位4/4] Figma Personal Access Token(存到项目根 .env,回车跳过)',
defaultToken, ''
)
// 落盘 .env + 保证 .gitignore 屏蔽 .env
if (figmaToken) {
const r = upsertEnvVar('FIGMA_TOKEN', figmaToken)
if (r.action === 'create') console.log(' ✓ 已创建 .env 并写入 FIGMA_TOKEN=<hidden>')
else if (r.action === 'append') console.log(' ✓ 已向现有 .env 追加 FIGMA_TOKEN=<hidden>')
else if (r.action === 'unchanged') console.log(' ✓ .env 中 FIGMA_TOKEN 已是最新值,未改动')
else {
const bakHint = r.backup ? `(原值备份到 ${path.relative(CWD, r.backup)})` : '(存在 .env.bak,保留旧备份)'
console.log(` ✓ 已更新现有 .env 中的 FIGMA_TOKEN=<hidden> ${bakHint}`)
}
ensureGitignoreHasEnv()
if (legacyTokenInConfig) {
console.log(' → 检测到旧 pp-d2c.config.json 里的 figma.token,已迁移到 .env,config 中将移除 figma 段')
}
} else {
console.log(' ⚠️ 未填 FIGMA_TOKEN,后续切图会失败;可手动编辑项目根 .env 补上')
}
const config = {
version: '2.0.0',
project: { name: path.basename(CWD), framework, styleFormat },
figma: { token: figmaToken },
merge: { mode: mergeMode },

@@ -556,3 +682,3 @@ unit: framework === 'rn'

console.log('\n─────────────────────────────────────────────────────')
console.log(' ✓ v0.3 起完全走 Figma REST API,无需 MCP;确保 figma.token 已配置即可。')
console.log(' ✓ v0.3 起完全走 Figma REST API,无需 MCP;确保项目根 .env 里 FIGMA_TOKEN 已配置即可。')
console.log(' ✓ pp-d2c.config.json 已配置')

@@ -559,0 +685,0 @@ console.log(' ✓ code-connect/mappings.json 已就绪')

{
"name": "@double-coding/pixel-print",
"version": "1.0.0",
"version": "1.0.2",
"description": "PixelPrint —— Figma D2C 工具,一键安装 Claude Code Skill,自动将 Figma 设计稿转换为前端代码(H5 / React Native / xtaro)",

@@ -5,0 +5,0 @@ "bin": {

+20
-18

@@ -59,3 +59,3 @@ # PixelPrint

[2.1/8] 启用 adapter(把 6 大 RN 标签映射到目标框架)? ● Yes No # rn 分支才问
[2.2/8] 选择预设 adapter: ● 携程 xtaro 自定义 # rn 分支才问
[2.2/8] 选择预设 adapter: ● pure RN xtaro taro 自定义 # rn 分支才问
[2.3/8] rpx 响应式包装启用? ● Yes No # rn 分支才问

@@ -254,14 +254,16 @@ [3/8] 合并模式: ● component flat

## RN / xtaro adapter(v0.3+)
## RN 分支 adapter(v0.3+)
RN 分支的核心机制:**内核以 6 大 RN 原生标签描述一切,再通过 config 映射到具体框架标签**。这样一套 SKILL 同时覆盖 pure React Native / Expo / xtaro / taro / 组织内部 RN 组件库。
**内置 xtaro 预设**(`templates/adapter-presets/xtaro.json`):
**内置 3 个预设**(`templates/adapter-presets/`):
```
View / Text / Image / Pressable / TextInput / ScrollView
↓ ↓ ↓
XView / XText / XImage / XView / XInput / XScrollView from '@ctrip/xtaro'
```
| 预设 | 目标 | 映射示意 |
|---|---|---|
| `rn` | pure React Native / Expo | 保留原名(identity),`from 'react-native'`;适合"我不做替换"场景 |
| `xtaro` | 携程 `@ctrip/xtaro` | `View→XView / Text→XText / Image→XImage / Pressable→XView / TextInput→XInput / ScrollView→XScrollView`,`from '@ctrip/xtaro'` |
| `taro` | Taro `@tarojs/components` | `View→View / Text→Text / Image→Image / Pressable→View / TextInput→Input / ScrollView→ScrollView`,`from '@tarojs/components'` |
每个预设 3 件套:`<id>.json`(映射规则)+ `<id>.rpx.ts`(专属屏宽 helper)+ `<id>.reference.md`(超改名的复杂差异手册)。
**adapter 分工**:

@@ -271,8 +273,8 @@

|---|---|---|
| prop 名不同、值和语义一样(如 `Image.source → src`) | `xtaro.json` `propMap` | §5.5.3b 声明式改名 |
| 值域映射(如 `resizeMode='contain' → mode='aspectFit'`) | `xtaro.reference.md` §一 | §5.5.3c 查手册 |
| 布尔取反(如 `editable → disabled` 取反) | `xtaro.reference.md` §二 | §5.5.3c |
| 事件签名转换(如 `onChangeText(text) → onInput(e.detail.value)`) | `xtaro.reference.md` §三 | §5.5.3c |
| 结构变化(如 `ScrollView.horizontal → scrollX + scrollY`) | `xtaro.reference.md` §四 | §5.5.3c |
| 无跨端支持,需删属性 + warn | `xtaro.reference.md` §五 | §5.5.3c |
| prop 名不同、值和语义一样(如 `Image.source → src`) | `<id>.json` `propMap` | §5.5.3b 声明式改名 |
| 值域映射(如 `resizeMode='contain' → mode='aspectFit'`) | `<id>.reference.md` §一 | §5.5.3c 查手册 |
| 布尔取反(如 `editable → disabled` 取反) | `<id>.reference.md` §二 | §5.5.3c |
| 事件签名转换(如 `onChangeText(text) → onInput(e.detail.value)`) | `<id>.reference.md` §三 | §5.5.3c |
| 结构变化(如 `ScrollView.horizontal → scrollX + scrollY`) | `<id>.reference.md` §四 | §5.5.3c |
| 无跨端支持,需删属性 + warn | `<id>.reference.md` §五 | §5.5.3c |

@@ -294,6 +296,6 @@ **加自己的预设**:见 [`templates/adapter-presets/README.md`](./templates/adapter-presets/README.md)。

│ │ ├── README.md ← 加预设的说明
│ │ ├── xtaro.json ← 携程 xtaro 预设映射
│ │ ├── xtaro.rpx.ts ← xtaro 版 rpx helper
│ │ └── xtaro.reference.md ← xtaro 复杂差异手册
│ ├── rn-helpers/rpx.ts ← pure RN 版 rpx helper(默认兜底)
│ │ ├── xtaro.{json,rpx.ts,reference.md} ← 携程 xtaro 预设(3 件套)
│ │ ├── taro.{json,rpx.ts,reference.md} ← Taro (@tarojs/components) 预设
│ │ └── rn.{json,rpx.ts,reference.md} ← pure React Native / Expo 预设
│ ├── rn-helpers/rpx.ts ← 兜底 rpx helper(用户选"自定义"无预设时用)
│ └── skills/

@@ -300,0 +302,0 @@ │ ├── pp-d2c/SKILL.md ← 主 D2C 流程(h5 分支,~1200 行)

@@ -7,8 +7,16 @@ # adapter-presets

| 文件 | 目标框架 | 说明 |
|-----|---------|------|
| `xtaro.json` + `xtaro.rpx.ts` + `xtaro.reference.md` | 携程 xtaro | 6 大 RN 标签映射到 `@ctrip/xtaro`;`Pressable → XView`(XView 自身可点击,不引 XClickableSimplified);`Image.source → src`(xtaro 走 taro 语义);自带 rpx helper 走 `xGetSystemInfoSync from @ctrip/xtaro`(xtaro H5 端 webpack 不解析 react-native Flow 语法);**xtaro.reference.md** 承载"prop 名机械改名之外"的复杂差异(值域映射 / 布尔取反 / 事件签名 / 结构变化 / 丢弃属性),SKILL 在 §5.5.3c 时读取应用 |
| 预设 id | 名称 | 目标框架 | 说明 |
|---|---|---|---|
| `xtaro` | 携程 xtaro | `@ctrip/xtaro` | 6 大 RN 标签映射到 xtaro 组件;`Pressable → XView`(XView 自身可点击);`Image.source → src`;rpx helper 走 `xGetSystemInfoSync from @ctrip/xtaro`(xtaro H5 端 webpack 不解析 react-native Flow 语法) |
| `taro` | Taro (@tarojs/components) | `@tarojs/components` | 6 大 RN 标签映射到 taro 组件;`Pressable → View`;`TextInput → Input`;`Image.source → src`;rpx helper 走 `Taro.getSystemInfoSync from @tarojs/taro`(taro 覆盖多端时统一屏蔽) |
| `rn` | pure React Native / Expo | `react-native` | 6 大 RN 原生标签保留原名(identity 映射);全部从 `react-native` 导入;rpx helper 走 `Dimensions.get('window').width`。适合纯 RN / Expo,不做跨组件库替换 |
CLI 里始终有个 `自定义` 兜底选项 — 选它写空 adapter,用户后续在 `pp-d2c.config.json` 手改 tagMap / importMap / propMap 即可,不必先建 preset 文件。
每个预设由 3 个文件组成:
- `<id>.json` — 映射规则(tagMap / importMap / propMap)
- `<id>.rpx.ts` — 该预设专属的 rpx helper(不同框架屏宽 API 不同)
- `<id>.reference.md` — 超出 propMap 声明式改名的复杂差异手册(SKILL §5.5.3c 读取)
CLI 里始终有一个 `自定义` 兜底选项 — 选它写空 adapter,用户后续在 `pp-d2c.config.json` 手改 tagMap / importMap / propMap 即可,不必先建 preset 文件。
## 加自己的预设

@@ -69,16 +77,16 @@

### 举例:接 `taro` 的预设
### 举例:接 `native-base` 的预设(假想,示意结构)
```json
{
"id": "taro",
"name": "Taro",
"description": "映射到 @tarojs/components,复用 taro Image 语义(source → src)",
"id": "native-base",
"name": "NativeBase (@native-base)",
"description": "映射到 native-base 组件库,提供 Box / VStack / HStack / Image / Pressable / Input / ScrollView 等",
"adapter": {
"enabled": true,
"tagMap": {
"View": "View",
"View": "Box",
"Text": "Text",
"Image": "Image",
"Pressable": "View",
"Pressable": "Pressable",
"TextInput": "Input",

@@ -88,10 +96,11 @@ "ScrollView": "ScrollView"

"importMap": {
"View": "@tarojs/components",
"Text": "@tarojs/components",
"Image": "@tarojs/components",
"Input": "@tarojs/components",
"ScrollView": "@tarojs/components"
"Box": "native-base",
"Text": "native-base",
"Image": "native-base",
"Pressable": "native-base",
"Input": "native-base",
"ScrollView": "native-base"
},
"propMap": {
"Image": { "source": "src" }
"Image": { "source": "source" }
}

@@ -102,2 +111,4 @@ }

> **想接的框架已有预设**?先看 `现有预设` 表,直接选就行。上面示例只演示"如何加一个新框架"。真实 taro / xtaro / pure RN 见目录里现成的 3 个预设。
## 加了预设之后的生效路径

@@ -104,0 +115,0 @@

@@ -26,3 +26,4 @@ {

"Image": {
"source": "src"
"source": "src",
"resizeMode": "mode"
},

@@ -33,3 +34,5 @@ "TextInput": {

"autoFocus": "focus",
"onSubmitEditing": "onConfirm"
"onSubmitEditing": "onConfirm",
"keyboardType": "type",
"returnKeyType": "confirmType"
},

@@ -36,0 +39,0 @@ "ScrollView": {

@@ -17,3 +17,3 @@ # xtaro adapter 参考手册

## 一、值域映射(prop 名一致或已改名,但取值域不同)
## 一、值域映射(prop 名已在 xtaro.json 改名,取值域不同)

@@ -24,18 +24,31 @@ xtaro.json propMap 只能改 prop 名,不能改 prop 值。命中下表时,agent **在 §5.5.3c 时按 valueMap 改属性值**。

**说明**:xtaro 已经通过 propMap 把 `resizeMode` 改名成 `mode`(见 xtaro.json),但**值域也变了**,agent 必须同步改属性值。
**说明**:xtaro.json propMap 已把 `resizeMode` 改名成 `mode`,agent 在 §5.5.3c 时**只需改属性值**(prop 名已由 §5.5.3b 处理)。
| RN 值 | xtaro 值 | 备注 |
xtaro `XImage.mode` 完整取值来自 `@ctrip/xtaro-types/types/component/XImage.d.ts` 的 `XImageProps.Mode`,共 15 个值(4 类缩放 + 9 类裁剪);其中类型注释明确 **`@rn 部分支持 scaleToFill, aspectFit, aspectFill, widthFix`**,其余值 rn 端会退化。
| RN `resizeMode` 值 | xtaro `mode` 值 | rn 端行为 |
|---|---|---|
| `'contain'` | `'aspectFit'` | 保持纵横比,长边完全显示 |
| `'cover'` | `'aspectFill'` | 保持纵横比,填满容器 |
| `'stretch'` | `'scaleToFill'` | 不保持比例,完全拉伸(xtaro 默认值) |
| `'center'` | `'center'` | 不缩放,只显示中间区域 |
| `'repeat'` | *无对应* | xtaro rn 端无此值 → 退化 `'scaleToFill'` + 写入 §7 QA warn |
| `'contain'` | `'aspectFit'` | ✅ rn 支持,保持纵横比,长边完全显示 |
| `'cover'` | `'aspectFill'` | ✅ rn 支持,保持纵横比,填满容器 |
| `'stretch'` | `'scaleToFill'` | ✅ rn 支持,不保持比例,完全拉伸(xtaro 默认值) |
| `'center'` | `'scaleToFill'` | ⚠️ rn 端不支持 `center`,退化 `scaleToFill` + QA warn |
| `'repeat'` | `'scaleToFill'` | ⚠️ rn 端不支持,退化 `scaleToFill` + QA warn |
**注**:若 xtaro.json 尚未把 `resizeMode` 改名成 `mode`,agent 需**同时**改 prop 名和值(不能只改值)。建议保持 xtaro.json 与本手册联动。
**改写示例**:
```tsx
// §5.5.3b propMap 处理后(prop 名已改)
<XImage src={require('./bg.png')} style={styles.bg} resizeMode="cover" />
// §5.5.3c valueMap 处理后(prop 值同步改)
<XImage src={require('./bg.png')} style={styles.bg} mode="aspectFill" />
```
**注**:遗留场景兜底 — 若某天回滚了 propMap,agent 需**同时**改 prop 名和值(等价于本节 + §5.5.3b 的 `resizeMode → mode`)。
### 1.2 `TextInput.keyboardType` (RN) → `type` (xtaro)
**说明**:xtaro rn 端支持的 type 是子集,超出的一律退化 + QA warn。
**说明**:xtaro.json propMap 已把 `keyboardType` 改名成 `type`,agent **只改属性值**。xtaro rn 端支持的 type 是子集,超出的一律退化 + QA warn。
xtaro `XInput.type` 完整取值(来自 `XInput.d.ts` `XInputProps.Type`):`text` / `number` / `idcard` / `digit` / `safe-password` / `nickname` / `numberpad` / `digitpad` / `idcardpad` / `email`;其中 `@supported rn` 标注的有 `text` / `number` / `idcard` / `digit` / `email`。
| RN keyboardType | xtaro type | 备注 |

@@ -49,8 +62,10 @@ |---|---|---|

| `'number-pad'` | `'number'` | 同 numeric |
| `'ascii-capable'` / `'url'` / 其他 | `'text'` | 全部退化 text + QA warn |
| `'ascii-capable'` / `'url'` / `'name-phone-pad'` / `'twitter'` / `'web-search'` / 其他 | `'text'` | 全部退化 text + QA warn |
### 1.3 `TextInput.returnKeyType` (RN) → `confirmType` (xtaro)
**说明**:值域**基本一致**,但 RN 有 xtaro 没有的 `'default'` / `'previous'` 等,退化处理。
**说明**:xtaro.json propMap 已把 `returnKeyType` 改名成 `confirmType`,agent **只改属性值**。值域**基本一致**,但 RN 有 xtaro 没有的 `'default'` / `'previous'` 等,退化处理。
xtaro `XInput.confirmType` 完整取值(来自 `XInput.d.ts` `XInputProps.ConfirmType`):`send` / `search` / `next` / `go` / `done`。
| RN returnKeyType | xtaro confirmType | 备注 |

@@ -63,5 +78,5 @@ |---|---|---|

| `'send'` | `'send'` | 一致 |
| `'default'` / `'previous'` / `'yahoo'` / `'google'` / `'route'` | `'done'` | 全部退化 + QA warn |
| `'default'` / `'previous'` / `'yahoo'` / `'google'` / `'route'` / `'join'` / `'emergency-call'` | `'done'` | 全部退化 + QA warn |
**注**:值域大部分重合,建议 agent 命中不在上表的值时才写 §7 QA warn;重合值静默改。**prop 名需要改**(returnKeyType → confirmType),这一步可以放 xtaro.json propMap,也可以在此手册里改;当前 xtaro.json 未声明,agent 在 §5.5.3c 时改名 + 改值。
**注**:值域大部分重合,建议 agent 命中不在上表的值时才写 §7 QA warn;重合值静默改。

@@ -223,20 +238,110 @@ ---

以下属性 xtaro rn 端不支持,agent 在 §5.5.3c 时**删除属性** + 写入 §7 QA warn 段(列出文件名 + 行号 + 属性名)。
以下属性 xtaro rn 端不支持,agent 在 §5.5.3c 时**删除属性** + 写入 §7 QA warn 段(列出文件名 + 行号 + 属性名)。清单依据 `@ctrip/xtaro-types/types/component/*.d.ts` 每个 prop 的 `@supported` 注释,凡是**明确不含 `rn`** 的属性都在此表。
| 原属性(RN) | 原标签 | 丢弃原因 |
### 5.1 `Text` (RN) → `XText`
| RN 属性 | xtaro XText 是否支持 rn | 处理策略 |
|---|---|---|
| `numberOfLines` | `Text` | xtaro XText 只在 alipay 端支持;rn 端多行省略需在业务代码用 CSS `-webkit-line-clamp` 或 Text 手工截断 |
| `ellipsizeMode` | `Text` | 无对应 |
| `selectable` | `Text` | ✅ 一致,不丢(此项仅列为对照,agent 保留原属性) |
| `showsHorizontalScrollIndicator` | `ScrollView` | 无内置;xtaro 靠 CSS `::-webkit-scrollbar` 隐藏,业务侧手工写 |
| `showsVerticalScrollIndicator` | `ScrollView` | 同上 |
| `pagingEnabled` | `ScrollView` | xtaro 只 weapp 支持;rn 端无 |
| `onMomentumScrollEnd` | `ScrollView` | xtaro 只 weapp 支持 |
| `multiline` | `TextInput` | 无;若确需多行,改用 `XTextarea`(不同标签,超出本预设覆盖范围,QA error) |
| `keyboardAppearance` | `TextInput` | 无对应 |
| `blurOnSubmit` | `TextInput` | 无对应 |
| `caretHidden` | `TextInput` | 无对应 |
| `numberOfLines` | ❌ 只支持 `alipay` | 静默删 + QA warn(rn 端多行省略需 CSS `-webkit-line-clamp` 或代码截断) |
| `ellipsizeMode` | ❌ 无 | 静默删 + QA warn |
| `adjustsFontSizeToFit` | ❌ 无 | 静默删 + QA warn |
| `minimumFontScale` | ❌ 无 | 静默删 + QA warn |
| `allowFontScaling` | ❌ 无 | 静默删 + QA warn |
| `maxFontSizeMultiplier` | ❌ 无 | 静默删 + QA warn |
| `selectable` | ✅ 保留 | 静默改(**不删**,列此仅作对照) |
| `onPress`(Text 上的) | ❌ RN Text 有 onPress;XText 无对应事件 | 静默删 + QA error(需要点击的文字通常应外包 XView 处理 onClick,超出 preset 覆盖) |
| `onLongPress`(Text 上的) | ❌ 同上 | 静默删 + QA warn |
**注**:`selectable` 列在这里只作对照(实际保留);其他项 agent 静默删 + §7 QA 段 warn。
### 5.2 `View` / `Pressable` (RN) → `XView`
| RN 属性 | xtaro XView 是否支持 rn | 处理策略 |
|---|---|---|
| `pointerEvents` | ❌ 无对应 prop(RN 直接支持 `pointerEvents`,xtaro 需在 style 里写 `pointerEvents: 'none'`) | 静默改进 style 内(注:该 style 属性 RN 端可用);无法搬迁则删 + QA warn |
| `needsOffscreenAlphaCompositing` | ❌ 无 | 静默删 + QA warn |
| `renderToHardwareTextureAndroid` | ❌ 无 | 静默删 + QA warn |
| `shouldRasterizeIOS` | ❌ 无 | 静默删 + QA warn |
| `collapsable` | ❌ 无 | 静默删 + QA warn |
| `Pressable.android_ripple` | ❌ 无 | 静默删 + QA warn |
| `Pressable.hitSlop` | ❌ 无(XView 用 style/CSS 实现类似效果) | 静默删 + QA warn |
| `Pressable.delayLongPress` | ❌ 无 | 静默删 + QA warn |
| `Pressable.disabled` | ❌ XView 无 disabled prop | 静默删 + QA warn(如需禁用点击,业务侧在 onClick 里判 return) |
**注**:XView 的 `onTouchStart/onTouchMove/onTouchEnd/onTouchCancel/onLongPress` 与 RN 语义基本一致,静默保留即可(见 `XView.d.ts` `EventProps`)。
### 5.3 `Image` (RN) → `XImage`
| RN 属性 | xtaro XImage 是否支持 rn | 处理策略 |
|---|---|---|
| `defaultSource` | ❌ 只支持 `alipay` | 静默删 + QA warn(可考虑外层套 XView 做占位背景) |
| `blurRadius` | ❌ 无对应 prop | 静默删 + QA warn |
| `fadeDuration` | ❌ 无 | 静默删 + QA warn |
| `progressiveRenderingEnabled` | ❌ 无 | 静默删 + QA warn |
| `capInsets` | ❌ 无 | 静默删 + QA warn |
| `loadingIndicatorSource` | ❌ 无 | 静默删 + QA warn |
| `onProgress` | ❌ 无 | 静默删 + QA warn |
| `onPartialLoad` | ❌ 无 | 静默删 + QA warn |
| `onLoadStart` / `onLoadEnd` | ❌ 无(XImage 只有 `onLoad` / `onError`) | 静默删 + QA warn |
| `resizeMethod` | ❌ 无 | 静默删 + QA warn |
### 5.4 `ScrollView` (RN) → `XScrollView`
| RN 属性 | xtaro XScrollView 是否支持 rn | 处理策略 |
|---|---|---|
| `showsHorizontalScrollIndicator` | ❌ 无(靠 CSS `::-webkit-scrollbar` 隐藏) | 静默删 + QA warn |
| `showsVerticalScrollIndicator` | ❌ 同上 | 静默删 + QA warn |
| `pagingEnabled` | ❌ 只 `weapp/swan` 支持 | 静默删 + QA warn |
| `onMomentumScrollBegin` | ❌ 只 `weapp` 支持 | 静默删 + QA warn |
| `onMomentumScrollEnd` | ❌ 只 `weapp` 支持 | 静默删 + QA warn |
| `onScrollBeginDrag` | ❌ 无(XScrollView `onDragStart` 只 `weapp` 支持) | 静默删 + QA warn |
| `onScrollEndDrag` | ❌ 同上 | 静默删 + QA warn |
| `refreshControl`(RefreshControl 组件) | ❌ 无对应 prop(XScrollView 的 refresher* 系列只 `weapp` 支持) | 静默删 + QA error(下拉刷新需业务侧改造,超出 preset 覆盖) |
| `stickyHeaderIndices` | ❌ 无 | 静默删 + QA warn |
| `keyboardShouldPersistTaps` | ❌ 无 | 静默删 + QA warn |
| `keyboardDismissMode` | ❌ 无 | 静默删 + QA warn |
| `bounces` | ❌ 只 `weapp/swan` 支持 | 静默删 + QA warn |
| `decelerationRate` | ❌ 无(xtaro `fastDeceleration` 只 `weapp`) | 静默删 + QA warn |
| `snapToInterval` / `snapToAlignment` / `snapToOffsets` / `snapToStart` / `snapToEnd` | ❌ 无 | 静默删 + QA warn |
| `overScrollMode` | ❌ 无 | 静默删 + QA warn |
| `nestedScrollEnabled` | ❌ 无 | 静默删 + QA warn |
| `contentInset` / `contentInsetAdjustmentBehavior` / `contentOffset` | ❌ 无对应 prop(XScrollView 用 `scrollTop/scrollLeft` 表达位置,`padding` 只 `weapp`) | 静默删 + QA warn(初始滚动位置可用 `scrollTop`/`scrollLeft` 替代,业务侧手工核对) |
| `automaticallyAdjustContentInsets` | ❌ 无 | 静默删 + QA warn |
| `alwaysBounceHorizontal` / `alwaysBounceVertical` | ❌ 无 | 静默删 + QA warn |
| `directionalLockEnabled` | ❌ 无 | 静默删 + QA warn |
| `maintainVisibleContentPosition` | ❌ 无 | 静默删 + QA warn |
| `scrollEnabled` | ❌ 无对应 prop(禁用滚动可写 `scrollX={false} scrollY={false}` 但语义不同) | 静默删 + QA warn |
| `scrollEventThrottle` | ✅ 保留(XScrollView 支持 `scrollEventThrottle`) | 静默改(**不删**,列此仅作对照) |
| `onEndReachedThreshold` | ⚠️ 单位不同 | 见 §3.4,不在本表 |
### 5.5 `TextInput` (RN) → `XInput`
| RN 属性 | xtaro XInput 是否支持 rn | 处理策略 |
|---|---|---|
| `multiline` | ❌ 无;若确需多行须改用 `XTextarea`(不同标签) | 静默删 + QA error(超出 preset 覆盖范围) |
| `numberOfLines` | ❌ 无 | 静默删 + QA warn |
| `keyboardAppearance` | ❌ 无 | 静默删 + QA warn |
| `blurOnSubmit` | ❌ 无 | 静默删 + QA warn |
| `caretHidden` | ❌ 无 | 静默删 + QA warn |
| `contextMenuHidden` | ❌ 无 | 静默删 + QA warn |
| `selectionColor` | ❌ 无(xtaro 用 `placeholderTextColor` 只控 placeholder 颜色) | 静默删 + QA warn |
| `underlineColorAndroid` | ❌ 无 | 静默删 + QA warn |
| `textContentType` | ❌ 无 | 静默删 + QA warn |
| `autoCorrect` | ❌ 无 | 静默删 + QA warn |
| `autoCapitalize` | ❌ 无 | 静默删 + QA warn |
| `autoComplete` | ❌ 无 | 静默删 + QA warn |
| `spellCheck` | ❌ 无 | 静默删 + QA warn |
| `clearButtonMode` / `clearTextOnFocus` | ❌ 无 | 静默删 + QA warn |
| `enablesReturnKeyAutomatically` | ❌ 无 | 静默删 + QA warn |
| `passwordRules` | ❌ 无 | 静默删 + QA warn |
| `rejectResponderTermination` | ❌ 无 | 静默删 + QA warn |
| `scrollEnabled`(TextInput 上的) | ❌ 无 | 静默删 + QA warn |
| `selectTextOnFocus` | ❌ 无 | 静默删 + QA warn |
| `showSoftInputOnFocus` | ❌ 无 | 静默删 + QA warn |
| `onKeyPress` | ❌ 无(XInput 只有 `onInput/onFocus/onBlur/onConfirm`) | 静默删 + QA warn |
| `onSelectionChange` | ❌ 无 | 静默删 + QA warn |
| `onContentSizeChange` | ❌ 无 | 静默删 + QA warn |
| `onEndEditing` | ❌ 无 | 静默删 + QA warn |
| `onScroll`(TextInput 上的) | ❌ 无 | 静默删 + QA warn |
**注**:表中标 ✅ 的行(`Text.selectable` / `ScrollView.scrollEventThrottle`)只作对照,agent **保留原属性**。其他项静默删 + §7 QA 段 warn。
---

@@ -248,8 +353,8 @@

1. **值域映射**(§一):`resizeMode`(Image)/ `keyboardType`(TextInput)/ `returnKeyType`(TextInput) 命中 → 按 valueMap 改值 + 必要时改 prop 名
1. **值域映射**(§一):`Image.mode`(propMap 已改名,值需按 §1.1 映射)/ `TextInput.type`(propMap 已改名,值按 §1.2)/ `TextInput.confirmType`(propMap 已改名,值按 §1.3) 命中 → 只改属性值
2. **布尔取反**(§二):`editable`(TextInput) 命中 → 改成 `disabled` + 值取反
3. **事件签名**(§三):`onChangeText` / `onPress` / `onPressIn` / `onPressOut` / `onEndReachedThreshold` 命中 → 改 prop 名 + 改回调函数体(§3.1 需读回调体)
4. **结构变化**(§四):`ScrollView.horizontal` 命中 → 删原 prop + 加 scrollX/scrollY;`contentContainerStyle` 命中 → 包 XView
5. **丢弃属性**(§五):表内属性命中 → 删属性 + 写入 §7 QA warn 段
5. **丢弃属性**(§五):表内属性命中 → 删属性 + 写入 §7 QA warn 段(逐组件 5.1~5.5,共 60+ 项)
**未在本手册出现的属性**一律**保留原样**;`style / key / ref / children / className` 永远不改。

@@ -8,5 +8,2 @@ {

},
"figma": {
"token": ""
},
"merge": {

@@ -13,0 +10,0 @@ "mode": "component"

@@ -8,5 +8,2 @@ {

},
"figma": {
"token": ""
},
"merge": {

@@ -13,0 +10,0 @@ "mode": "component"

@@ -50,2 +50,37 @@ #!/usr/bin/env node

// 极简 .env 解析:只处理 KEY=VALUE 行,支持引号包裹和 # 注释,不做变量插值
// 之所以自己写而不引入 dotenv:pp 仓库承诺零 npm 依赖,figma.mjs 现在只用 Node 18+ 内置能力
function parseEnvFile(text) {
const out = {}
for (const rawLine of text.split(/\r?\n/)) {
const line = rawLine.trim()
if (!line || line.startsWith('#')) continue
const m = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/)
if (!m) continue
let val = m[2]
// 剥单/双引号(常见于 KEY="xxx" 写法)
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
val = val.slice(1, -1)
}
out[m[1]] = val
}
return out
}
// token 读取优先级:process.env > 项目根 .env FIGMA_TOKEN > config.figma.token(向后兼容)
// v1.0.2 起主推 .env,老项目 config 里的 token 由 install.js 迁移;此处保留兼容读取避免旧项目立即断
function loadFigmaToken(config, projectRoot) {
if (process.env.FIGMA_TOKEN) return process.env.FIGMA_TOKEN
const envPath = path.join(projectRoot, '.env')
if (fs.existsSync(envPath)) {
try {
const parsed = parseEnvFile(fs.readFileSync(envPath, 'utf8'))
if (parsed.FIGMA_TOKEN) return parsed.FIGMA_TOKEN
} catch (_) {
// .env 读失败不阻塞,继续走 config 兜底
}
}
return config?.figma?.token || ''
}
function parseFlags(argv) {

@@ -126,5 +161,5 @@ const positional = []

async function cmdVerifyToken() {
const { config } = loadConfig()
const token = config.figma?.token
if (!token) return fail('figma.token 未在 pp-d2c.config.json 配置')
const { config, projectRoot } = loadConfig()
const token = loadFigmaToken(config, projectRoot)
if (!token) return fail('FIGMA_TOKEN 未配置 (请在项目根 .env 写 FIGMA_TOKEN=xxx)')
try {

@@ -145,4 +180,4 @@ const me = await figmaFetch('/v1/me', token)

const { config, projectRoot } = loadConfig()
const token = config.figma?.token
if (!token) return fail('figma.token 未配置')
const token = loadFigmaToken(config, projectRoot)
if (!token) return fail('FIGMA_TOKEN 未配置 (请在项目根 .env 写 FIGMA_TOKEN=xxx)')

@@ -197,4 +232,4 @@ const paths = cachePaths(projectRoot, fileKey)

const { config, projectRoot } = loadConfig()
const token = config.figma?.token
if (!token) return fail('figma.token 未配置')
const token = loadFigmaToken(config, projectRoot)
if (!token) return fail('FIGMA_TOKEN 未配置 (请在项目根 .env 写 FIGMA_TOKEN=xxx)')

@@ -241,4 +276,4 @@ const paths = cachePaths(projectRoot, fileKey)

const { config, projectRoot } = loadConfig()
const token = config.figma?.token
if (!token) return fail('figma.token 未配置')
const token = loadFigmaToken(config, projectRoot)
if (!token) return fail('FIGMA_TOKEN 未配置 (请在项目根 .env 写 FIGMA_TOKEN=xxx)')

@@ -294,4 +329,4 @@ const assetsDir = config.images?.assetsDir || 'static/'

const { config, projectRoot } = loadConfig()
const token = config.figma?.token
if (!token) return fail('figma.token 未配置')
const token = loadFigmaToken(config, projectRoot)
if (!token) return fail('FIGMA_TOKEN 未配置 (请在项目根 .env 写 FIGMA_TOKEN=xxx)')

@@ -353,3 +388,4 @@ const dir = tmpScreenshotsDir(projectRoot)

所有命令都从 cwd 向上查找 pp-d2c.config.json 拿 figma.token 和 assetsDir。
所有命令都从 cwd 向上查找 pp-d2c.config.json 拿 assetsDir;
FIGMA_TOKEN 从项目根 .env 读(优先级 process.env > .env > config.figma.token 兜底)。
输出统一为 stdout 一行 JSON: {ok: true, data: {...}} 或 {ok: false, error: "..."}。

@@ -356,0 +392,0 @@ 退出码 0 表示成功,非零表示失败。

@@ -50,2 +50,37 @@ #!/usr/bin/env node

// 极简 .env 解析:只处理 KEY=VALUE 行,支持引号包裹和 # 注释,不做变量插值
// 之所以自己写而不引入 dotenv:pp 仓库承诺零 npm 依赖,figma.mjs 现在只用 Node 18+ 内置能力
function parseEnvFile(text) {
const out = {}
for (const rawLine of text.split(/\r?\n/)) {
const line = rawLine.trim()
if (!line || line.startsWith('#')) continue
const m = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/)
if (!m) continue
let val = m[2]
// 剥单/双引号(常见于 KEY="xxx" 写法)
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
val = val.slice(1, -1)
}
out[m[1]] = val
}
return out
}
// token 读取优先级:process.env > 项目根 .env FIGMA_TOKEN > config.figma.token(向后兼容)
// v1.0.2 起主推 .env,老项目 config 里的 token 由 install.js 迁移;此处保留兼容读取避免旧项目立即断
function loadFigmaToken(config, projectRoot) {
if (process.env.FIGMA_TOKEN) return process.env.FIGMA_TOKEN
const envPath = path.join(projectRoot, '.env')
if (fs.existsSync(envPath)) {
try {
const parsed = parseEnvFile(fs.readFileSync(envPath, 'utf8'))
if (parsed.FIGMA_TOKEN) return parsed.FIGMA_TOKEN
} catch (_) {
// .env 读失败不阻塞,继续走 config 兜底
}
}
return config?.figma?.token || ''
}
function parseFlags(argv) {

@@ -126,5 +161,5 @@ const positional = []

async function cmdVerifyToken() {
const { config } = loadConfig()
const token = config.figma?.token
if (!token) return fail('figma.token 未在 pp-d2c.config.json 配置')
const { config, projectRoot } = loadConfig()
const token = loadFigmaToken(config, projectRoot)
if (!token) return fail('FIGMA_TOKEN 未配置 (请在项目根 .env 写 FIGMA_TOKEN=xxx)')
try {

@@ -145,4 +180,4 @@ const me = await figmaFetch('/v1/me', token)

const { config, projectRoot } = loadConfig()
const token = config.figma?.token
if (!token) return fail('figma.token 未配置')
const token = loadFigmaToken(config, projectRoot)
if (!token) return fail('FIGMA_TOKEN 未配置 (请在项目根 .env 写 FIGMA_TOKEN=xxx)')

@@ -197,4 +232,4 @@ const paths = cachePaths(projectRoot, fileKey)

const { config, projectRoot } = loadConfig()
const token = config.figma?.token
if (!token) return fail('figma.token 未配置')
const token = loadFigmaToken(config, projectRoot)
if (!token) return fail('FIGMA_TOKEN 未配置 (请在项目根 .env 写 FIGMA_TOKEN=xxx)')

@@ -241,4 +276,4 @@ const paths = cachePaths(projectRoot, fileKey)

const { config, projectRoot } = loadConfig()
const token = config.figma?.token
if (!token) return fail('figma.token 未配置')
const token = loadFigmaToken(config, projectRoot)
if (!token) return fail('FIGMA_TOKEN 未配置 (请在项目根 .env 写 FIGMA_TOKEN=xxx)')

@@ -294,4 +329,4 @@ const assetsDir = config.images?.assetsDir || 'static/'

const { config, projectRoot } = loadConfig()
const token = config.figma?.token
if (!token) return fail('figma.token 未配置')
const token = loadFigmaToken(config, projectRoot)
if (!token) return fail('FIGMA_TOKEN 未配置 (请在项目根 .env 写 FIGMA_TOKEN=xxx)')

@@ -353,3 +388,4 @@ const dir = tmpScreenshotsDir(projectRoot)

所有命令都从 cwd 向上查找 pp-d2c.config.json 拿 figma.token 和 assetsDir。
所有命令都从 cwd 向上查找 pp-d2c.config.json 拿 assetsDir;
FIGMA_TOKEN 从项目根 .env 读(优先级 process.env > .env > config.figma.token 兜底)。
输出统一为 stdout 一行 JSON: {ok: true, data: {...}} 或 {ok: false, error: "..."}。

@@ -356,0 +392,0 @@ 退出码 0 表示成功,非零表示失败。

@@ -35,3 +35,3 @@ # pp-doctor Skill

请检查 `pp-d2c.config.json` 里的 `figma.token`:
请检查项目根 `.env` 里的 `FIGMA_TOKEN`:
1. 是否已配置且未过期(Figma 网页版右上角头像 → Settings → Security → Personal access tokens)

@@ -38,0 +38,0 @@ 2. Token 权限是否包含 File content: Read-only

@@ -16,3 +16,3 @@ # pp-d2c 命名与实现规则

| `project.styleFormat` | 样式方案(见下表) |
| `figma.token` | Figma Personal Access Token(REST API 切图用) |
| `FIGMA_TOKEN` (项目根 `.env`) | Figma Personal Access Token(REST API 切图用;v1.0.2 起从 config 迁到 .env) |
| `images.assetsDir` | 图片下载目录 |

@@ -217,7 +217,7 @@ | `images.imageBaseUrl` | 代码中图片 src 前缀 |

### L0 主路径(figma.token 非空时,必须走此路径)
### L0 主路径(`FIGMA_TOKEN` 非空时,必须走此路径)
```bash
# PNG 2倍图,严格按 bbox(不含 effect / 父背景色)
curl -H "X-Figma-Token: {figma.token}" \
curl -H "X-Figma-Token: {FIGMA_TOKEN}" \
"https://api.figma.com/v1/images/{fileKey}?ids={nodeId}&format=png&scale=2&use_absolute_bounds=true" \

@@ -227,3 +227,3 @@ -o {projectRoot}/{assetsDir}/{filename}.png

# SVG(矢量图层优先)
curl -H "X-Figma-Token: {figma.token}" \
curl -H "X-Figma-Token: {FIGMA_TOKEN}" \
"https://api.figma.com/v1/images/{fileKey}?ids={nodeId}&format=svg&use_absolute_bounds=true" \

@@ -380,3 +380,3 @@ -o {projectRoot}/{assetsDir}/{filename}.svg

- 禁止父容器同时有 `bgc-` 和 `bg-` 时只写 `background-image` 不写 bgc- 的其他属性
- 禁止 `figma.token` 存在且非空时使用 MCP `download_assets` 导出图片(token 有效必须走 L0)
- 禁止 `FIGMA_TOKEN` 存在且非空时使用 MCP `download_assets` 导出图片(token 有效必须走 L0)
- 禁止调用 `/v1/images` 时省略 `use_absolute_bounds=true`(除非 nodeId 在 `preserveEffectIds` 中)

@@ -383,0 +383,0 @@ - 禁止把 `bg-` 节点的父容器当成切图源(切图源 nodeId 必须是 `bg-` 节点自己)

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display