# 用户画像 AI 生成 + 重画功能 实施计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 用户画像由 AI 根据系统行为数据 + 五维指标自动生成,用户可编辑,提供「重画」功能(参考旧画像 + 考虑变化因素) **Architecture:** - Java 侧:新增 `POST /api/user/portrait/generate`、`POST /api/user/portrait/regenerate`,调用 LangGraph portrait 生成/重画 graph - Python 侧:新增 `portrait_generation_graph.py`(LLM 分析行为+五维指标生成画像),`app/api/portrait.py` FastAPI 端点 - 前端:`portrait-edit.vue` 增加「生成画像」「重画」按钮、加载态、结果展示 **Tech Stack:** Java 8 + Spring Boot 2.7.18 + MyBatis-Plus / Python 3.10 + FastAPI + LangGraph + OpenAI / uni-app Vue 2 --- ## Global Constraints - 统一 `@PostMapping`,禁止 `@GetMapping/@PutMapping/@DeleteMapping` - 响应包装 `Result` (code/message/data) - JWT Bearer Token 认证,`JwtInterceptor` 拦截 `/api/**` - DI 使用 `@Resource`,字段名必须与 Bean Name 一致 - MyBatis-Plus `@TableName` + `@TableId(type = IdType.AUTO)` - 小程序限制:禁止 `?.`、CSS Grid、`:key` 表达式、`new Date(string)`(用 `parseDate()`) - Python 侧统一走 LangGraph graph 形式实现 AI 能力 - 编译验证:`mvn clean compile` / `python3 -m py_compile` 为唯一验证方式 --- ## 文件结构映射 ### Java 后端新增/修改 | 文件 | 类型 | 说明 | |------|------|------| | `service/PortraitService.java` | 修改 | 新增 `generatePortrait`、`regeneratePortrait` 接口方法 | | `service/impl/PortraitServiceImpl.java` | 修改 | 实现调用 AiGateway / portrait 生成逻辑 | | `controller/user/PortraitController.java` | 修改 | 新增 `/generate`、`/regenerate` 端点 | | `service/AIService.java` | 修改 | 新增 `generateUserPortrait` 方法(走 AiGateway 调用 LangGraph) | ### Python LangGraph 新增/修改 | 文件 | 类型 | 说明 | |------|------|------| | `graphs/portrait_generation_graph.py` | 新增 | AI 生成/重画用户画像的 LangGraph | | `api/portrait.py` | 新增 | FastAPI 路由:`POST /api/portrait/generate`、`POST /api/portrait/regenerate` | | `main.py` | 修改 | 注册 `portrait.router` | | `tools/java_client.py` | 修改 | 新增 `get_behavior_summary(user_id, member_id)` 获取行为数据 | ### 小程序前端修改 | 文件 | 类型 | 说明 | |------|------|------| | `pages/profile/portrait-edit.vue` | 修改 | 增加「生成画像」「重画」按钮、加载态、错误处理 | | `utils/api.js` | 修改 | 新增 `generatePortrait`、`regeneratePortrait` 封装 | --- ## 任务分解 ### 任务 1:Java PortraitService 接口扩展 **Files:** - Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/PortraitService.java` **Interfaces:** - Produces: 新增 `generatePortrait(Long userId, Long memberId)`、`regeneratePortrait(Long userId, Long memberId)` 方法签名 - [ ] **Step 1: 扩展 PortraitService 接口** ```java package com.etotem.cfc.service; import java.util.Map; public interface PortraitService { /** 获取用户画像文本(自定义 prompt + 快照渲染) */ String buildPortraitPrompt(Long userId, Long memberId); /** 更新用户自定义画像 prompt */ void updatePortraitPrompt(Long userId, String portraitPrompt); /** 读取用户画像(含渲染快照,用于前端展示) */ Map getPortrait(Long userId, Long targetUserId, Long memberId); /** AI 生成用户画像(分析行为+五维指标) */ String generatePortrait(Long userId, Long memberId); /** AI 重画用户画像(参考旧画像+变化因素) */ String regeneratePortrait(Long userId, Long memberId); } ``` - [ ] **Step 2: 编译验证** ```bash cd cfc-backend && mvn clean compile -q ``` - [ ] **Step 3: Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/service/PortraitService.java git commit -m "feat: PortraitService 新增 generatePortrait/regeneratePortrait 接口" ``` --- ### 任务 2:Java PortraitServiceImpl 实现 **Files:** - Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/impl/PortraitServiceImpl.java` **Interfaces:** - Consumes: `AIService.generateUserPortrait(userId, memberId, previousPortrait)` 返回 AI 生成的画像文本 - Produces: `generatePortrait`、`regeneratePortrait` 实现 - [ ] **Step 1: 注入 AIService** ```java @Resource private AIService aiService; ``` - [ ] **Step 2: 实现 generatePortrait** ```java @Override public String generatePortrait(Long userId, Long memberId) { // 调用 AIService 生成画像 String generated = aiService.generateUserPortrait(userId, memberId, null); if (generated != null && !generated.trim().isEmpty()) { // 保存到用户表 updatePortraitPrompt(userId, generated.trim()); return generated.trim(); } // 失败时降级为快照渲染 return renderSnapshot(memberId); } ``` - [ ] **Step 3: 实现 regeneratePortrait** ```java @Override public String regeneratePortrait(Long userId, Long memberId) { // 1. 获取当前画像 User user = userMapper.selectById(userId); String currentPortrait = user != null ? user.getPortraitPrompt() : null; // 2. 调用 AIService 重画(传入旧画像作为参考) String regenerated = aiService.generateUserPortrait(userId, memberId, currentPortrait); if (regenerated != null && !regenerated.trim().isEmpty()) { updatePortraitPrompt(userId, regenerated.trim()); return regenerated.trim(); } // 失败保持原画像 return currentPortrait; } ``` - [ ] **Step 4: 编译验证** ```bash cd cfc-backend && mvn clean compile -q ``` - [ ] **Step 5: Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/service/impl/PortraitServiceImpl.java git commit -m "feat: PortraitServiceImpl 实现 generatePortrait/regeneratePortrait" ``` --- ### 任务 3:Java AIService 扩展 **Files:** - Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/AIService.java` (接口) - Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/impl/AIServiceImpl.java` (实现) **Interfaces:** - Produces: `generateUserPortrait(Long userId, Long memberId, String previousPortrait)` → `String` - [ ] **Step 1: 扩展 AIService 接口** ```java /** * AI 生成用户画像 * @param userId 用户ID * @param memberId 成员ID(可空,为空时仅基于用户自身行为) * @param previousPortrait 旧画像文本(重画时传入,首次生成为 null) * @return 生成的画像文本,失败返回 null */ String generateUserPortrait(Long userId, Long memberId, String previousPortrait); ``` - [ ] **Step 2: 实现 AIServiceImpl.generateUserPortrait** ```java @Override public String generateUserPortrait(Long userId, Long memberId, String previousPortrait) { try { // 1. 获取用户行为摘要 + 五维画像 Map context = familyContextService.buildContext(userId); if (memberId != null) { context.put("memberId", memberId); } // 2. 构建输入 Map inputs = new HashMap<>(); inputs.put("user_id", userId); inputs.put("member_id", memberId); inputs.put("previous_portrait", previousPortrait); inputs.put("behavior_summary", context.get("behaviorSummary")); inputs.put("profile_snapshot", context.get("profileSnapshot")); // 3. 调用 LangGraph portrait 生成 graph // 端点:POST /api/portrait/generate (Python 侧) Map resp = aiGateway.chat("", userId, null, inputs); // aiGateway.chat 会路由到 portrait graph // 这里需要专门的方法,或者扩展 aiGateway // 临时方案:直接 HTTP 调用 Python 服务 // TODO: 在 AiGateway 增加专用方法 String result = callPortraitGraph(userId, memberId, previousPortrait, context); return result; } catch (Exception e) { log.error("AI 生成画像失败: userId={}, memberId={}", userId, memberId, e); return null; } } ``` > 注:`AiGateway` 目前通过 `chat` 方法统一路由。需要在 `AiGateway` 增加 `generatePortrait` 专用方法,或扩展 `chat` 支持 `portrait` 类型路由。 - [ ] **Step 3: 在 AiGateway 增加 portrait 生成方法** **Files:** - Modify: `cfc-backend/src/main/java/com/etotem/cfc/gateway/AiGateway.java` - Modify: `cfc-backend/src/main/java/com/etotem/cfc/gateway/impl/AiGatewayImpl.java` ```java // AiGateway.java String generatePortrait(Long userId, Long memberId, String previousPortrait, Map context); // AiGatewayImpl.java @Override public String generatePortrait(Long userId, Long memberId, String previousPortrait, Map context) { Map inputs = new HashMap<>(); inputs.put("user_id", userId); inputs.put("member_id", memberId); inputs.put("previous_portrait", previousPortrait); inputs.put("behavior_summary", context.get("behaviorSummary")); inputs.put("profile_snapshot", context.get("profileSnapshot")); // 专用 graph 类型标识 inputs.put("__graph_type", "portrait_generation"); Map resp = webClient.post() .uri(pythonBaseUrl + "/api/portrait/generate") .bodyValue(inputs) .retrieve() .bodyToMono(Map.class) .block(); // 解析返回的 portrait_text return resp != null ? (String) resp.get("portrait_text") : null; } ``` - [ ] **Step 4: 编译验证** ```bash cd cfc-backend && mvn clean compile -q ``` - [ ] **Step 5: Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/service/AIService.java \ cfc-backend/src/main/java/com/etotem/cfc/service/impl/AIServiceImpl.java \ cfc-backend/src/main/java/com/etotem/cfc/gateway/AiGateway.java \ cfc-backend/src/main/java/com/etotem/cfc/gateway/impl/AiGatewayImpl.java git commit -m "feat: AIService + AiGateway 新增 portrait 生成调用" ``` --- ### 任务 4:Java PortraitController 新增端点 **Files:** - Modify: `cfc-backend/src/main/java/com/etotem/cfc/controller/user/PortraitController.java` **Interfaces:** - Consumes: `PortraitService.generatePortrait`、`regeneratePortrait` - [ ] **Step 1: 新增 /generate 端点** ```java @PostMapping("/generate") public Result> generatePortrait( @RequestAttribute("userId") Long userId, @RequestBody Map params) { Long memberId = params.get("memberId") != null ? Long.valueOf(params.get("memberId").toString()) : null; String portrait = portraitService.generatePortrait(userId, memberId); Map result = new HashMap<>(); result.put("portraitPrompt", portrait); // 同时返回快照供前端展示 if (memberId != null) { Map profile = profileReadService.getProfile(memberId); result.put("renderedSnapshot", renderSnapshotFull(profile)); } return Result.success(result); } ``` - [ ] **Step 2: 新增 /regenerate 端点** ```java @PostMapping("/regenerate") public Result> regeneratePortrait( @RequestAttribute("userId") Long userId, @RequestBody Map params) { Long memberId = params.get("memberId") != null ? Long.valueOf(params.get("memberId").toString()) : null; String portrait = portraitService.regeneratePortrait(userId, memberId); Map result = new HashMap<>(); result.put("portraitPrompt", portrait); if (memberId != null) { Map profile = profileReadService.getProfile(memberId); result.put("renderedSnapshot", renderSnapshotFull(profile)); } return Result.success(result); } ``` - [ ] **Step 3: 复用 renderSnapshotFull(已存在于 impl 中,需提取为工具方法或在 controller 内复制)** - [ ] **Step 4: 编译验证** ```bash cd cfc-backend && mvn clean compile -q ``` - [ ] **Step 5: Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/controller/user/PortraitController.java git commit -m "feat: PortraitController 新增 /generate /regenerate 端点" ``` --- ### 任务 5:Python JavaClient 新增 get_behavior_summary **Files:** - Modify: `cfc-langgraph/app/tools/java_client.py` - [ ] **Step 1: 新增 get_behavior_summary 方法** ```python async def get_behavior_summary(self, user_id: int, member_id: int | None) -> dict: """获取用户行为摘要(任务完成率、挑战参与、积分流水、健康报告等)""" client = await self._get_client() body = {"user_id": str(user_id)} if member_id: body["member_id"] = member_id resp = await client.post("/api/ai/behavior/summary", json=body) data = resp.json() if data.get("code") == 200: return data.get("data", {}) return {} ``` - [ ] **Step 2: py_compile 验证** ```bash cd cfc-langgraph && python3 -m py_compile app/tools/java_client.py ``` - [ ] **Step 3: Commit** ```bash git add cfc-langgraph/app/tools/java_client.py git commit -m "feat: JavaClient 新增 get_behavior_summary 获取行为数据" ``` --- ### 任务 6:Python 后端新增 /api/ai/behavior/summary 端点 **Files:** - Modify: `cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java` 或新增 `AIBehaviorController.java` > 复用 `AIChatController` 的 `familyContextService.buildContext()` 逻辑,提取行为摘要 - [ ] **Step 1: 新增 AIBehaviorController** ```java @RestController @RequestMapping("/api/ai/behavior") public class AIBehaviorController { @Resource private FamilyContextService familyContextService; @PostMapping("/summary") public Result> getBehaviorSummary( @RequestAttribute("userId") Long userId, @RequestBody Map params) { Long memberId = params.get("member_id") != null ? Long.valueOf(params.get("member_id").toString()) : null; Map context = familyContextService.buildContext(userId); if (memberId != null) { context.put("memberId", memberId); } // 提取行为相关字段 Map summary = new HashMap<>(); summary.put("behaviorSummary", context.get("behaviorSummary")); summary.put("profileSnapshot", context.get("profileSnapshot")); summary.put("recentTasks", context.get("recentTasks")); summary.put("recentChallenges", context.get("recentChallenges")); summary.put("pointsHistory", context.get("pointsHistory")); summary.put("healthReports", context.get("healthReports")); return Result.success(summary); } } ``` - [ ] **Step 2: 编译验证** ```bash cd cfc-backend && mvn clean compile -q ``` - [ ] **Step 3: Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIBehaviorController.java git commit -m "feat: 新增 AIBehaviorController /behavior/summary 端点" ``` --- ### 任务 7:Python LangGraph portrait_generation_graph.py **Files:** - Create: `cfc-langgraph/app/graphs/portrait_generation_graph.py` **Interfaces:** - State: `PortraitGenerationState` { `user_id`, `member_id`, `previous_portrait`, `behavior_summary`, `profile_snapshot`, `portrait_text` } - Returns: `{ "portrait_text": "生成的画像文本" }` - [ ] **Step 1: 创建 graph 文件** ```python """用户画像生成/重画 LangGraph 输入: - user_id: 用户ID - member_id: 成员ID(可空) - previous_portrait: 旧画像文本(重画时传入,首次生成为 None) - behavior_summary: 行为摘要(任务完成率、挑战、积分、健康报告等) - profile_snapshot: 五维画像快照(dimension_scores、body_metrics、mind_metrics、problem_domains) 输出: - portrait_text: 生成的画像文本(中文,自然语言描述) """ from typing import TypedDict from langgraph.graph import StateGraph, START, END from langchain_openai import ChatOpenAI from langchain_core.messages import SystemMessage, HumanMessage from app.tools.java_client import JavaClient from app.config import settings import logging logger = logging.getLogger(__name__) class PortraitGenerationState(TypedDict): user_id: int member_id: int | None previous_portrait: str | None behavior_summary: dict | None profile_snapshot: dict | None portrait_text: str | None SYSTEM_PROMPT = """你是「浠艾福」家庭成长平台的 AI 画像分析师。 任务:根据用户的行为数据、五维能量指标、健康画像,生成一段自然语言的「用户画像」文本。 画像用途:作为后续所有 AI 对话(健康教练、管家、营养师、通用聊天)的 SystemMessage 注入,帮助 AI 更懂用户,给出更个性化的建议。 生成要求: 1. 语言自然、温暖、具体,像一位了解家庭的成长顾问写的观察笔记 2. 必须包含:核心特质、行为模式、关注点、潜在需求 3. 长度:200-500 字,不要过长 4. 不要列举原始数据(如"身 7.2 智 6.5"),要转化为洞察(如"身体维度相对突出,但智力维度有提升空间") 5. 如有 previous_portrait,重画时要体现"变化"与"延续":哪些特质稳定,哪些有新变化,原因可能是什么 输出格式:纯文本,无 JSON,无标记。""" def create_portrait_generation_graph(): llm = ChatOpenAI( model=settings.llm_model, api_key=settings.llm_api_key, base_url=settings.llm_base_url, temperature=0.7, # 稍高温度增加多样性 ) java = JavaClient() builder = StateGraph(PortraitGenerationState) async def fetch_data(state: PortraitGenerationState) -> dict: """并行拉取行为摘要 + 画像快照""" user_id = state["user_id"] member_id = state.get("member_id") # 并行获取 behavior_task = java.get_behavior_summary(user_id, member_id) profile_task = java.get_member_profile(member_id) if member_id else None behavior = await behavior_task profile = await profile_task if profile_task else {} return { "behavior_summary": behavior, "profile_snapshot": profile, } async def generate_portrait(state: PortraitGenerationState) -> dict: """LLM 生成画像""" previous = state.get("previous_portrait") behavior = state.get("behavior_summary") or {} profile = state.get("profile_snapshot") or {} # 构建用户画像上下文 context_parts = [] if profile: member = profile.get("member") or {} name = member.get("name", "用户") age = member.get("age") gender = member.get("gender") context_parts.append(f"用户:{name},{age}岁,{gender}") dims = profile.get("dimension_scores") or {} if dims: context_parts.append( f"五维评分:身 {dims.get('body','?')} 智 {dims.get('wisdom','?')} " f"心 {dims.get('mind','?')} 行 {dims.get('action','?')} 富 {dims.get('wealth','?')}" ) body = profile.get("body_metrics") or {} if body.get("sleep_dur_avg"): context_parts.append(f"平均睡眠:{body['sleep_dur_avg']}h/天") if body.get("exercise_count_week"): context_parts.append(f"周运动:{body['exercise_count_week']}次") mind = profile.get("mind_metrics") or {} if mind.get("stress_avg"): context_parts.append(f"平均压力:{mind['stress_avg']}/10") problems = profile.get("problem_domains") or [] if problems: context_parts.append(f"关注问题:{', '.join(problems[:5])}") if behavior: context_parts.append("行为摘要:" + str(behavior)) context_text = "\n".join(context_parts) # 构建 HumanMessage if previous: human_content = f"""【重画任务】请基于以下最新数据,重新生成用户画像。 【原画像(参考)】: {previous} 【最新数据】: {context_text} 要求:保留原画像中仍有效的核心特质,更新已变化的部分,体现"变化与延续"。""" else: human_content = f"""【生成任务】请基于以下数据,生成用户画像。 【数据】: {context_text}""" messages = [ SystemMessage(content=SYSTEM_PROMPT), HumanMessage(content=human_content), ] response = await llm.ainvoke(messages) portrait_text = response.content.strip() logger.info("生成画像完成: user_id=%d, member_id=%s, len=%d", state["user_id"], state.get("member_id"), len(portrait_text)) return {"portrait_text": portrait_text} builder.add_node("fetch_data", fetch_data) builder.add_node("generate_portrait", generate_portrait) builder.add_edge(START, "fetch_data") builder.add_edge("fetch_data", "generate_portrait") builder.add_edge("generate_portrait", END) return builder.compile() # 单例 _portrait_graph = None def get_portrait_graph(): global _portrait_graph if _portrait_graph is None: _portrait_graph = create_portrait_generation_graph() return _portrait_graph ``` - [ ] **Step 2: py_compile 验证** ```bash cd cfc-langgraph && python3 -m py_compile app/graphs/portrait_generation_graph.py ``` - [ ] **Step 3: Commit** ```bash git add cfc-langgraph/app/graphs/portrait_generation_graph.py git commit -m "feat: 新增 portrait_generation_graph.py AI 生成/重画用户画像" ``` --- ### 任务 8:Python FastAPI portrait 路由 **Files:** - Create: `cfc-langgraph/app/api/portrait.py` - Modify: `cfc-langgraph/app/main.py` **Interfaces:** - `POST /api/portrait/generate` → `{ "portrait_text": "..." }` - `POST /api/portrait/regenerate` → `{ "portrait_text": "..." }`(同一实现,区别在于是否传 previous_portrait) - [ ] **Step 1: 创建 api/portrait.py** ```python from fastapi import APIRouter, HTTPException from pydantic import BaseModel from app.graphs.portrait_generation_graph import get_portrait_graph import logging logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/portrait", tags=["portrait"]) class PortraitGenerateRequest(BaseModel): user_id: int member_id: int | None = None previous_portrait: str | None = None behavior_summary: dict | None = None profile_snapshot: dict | None = None class PortraitGenerateResponse(BaseModel): portrait_text: str @router.post("/generate", response_model=PortraitGenerateResponse) async def generate_portrait(req: PortraitGenerateRequest): """AI 生成用户画像(首次生成)""" try: graph = get_portrait_graph() state = { "user_id": req.user_id, "member_id": req.member_id, "previous_portrait": None, "behavior_summary": req.behavior_summary, "profile_snapshot": req.profile_snapshot, "portrait_text": None, } result = await graph.ainvoke(state) return PortraitGenerateResponse(portrait_text=result["portrait_text"]) except Exception as e: logger.exception("画像生成失败") raise HTTPException(status_code=500, detail=str(e)) @router.post("/regenerate", response_model=PortraitGenerateResponse) async def regenerate_portrait(req: PortraitGenerateRequest): """AI 重画用户画像(参考旧画像)""" try: graph = get_portrait_graph() state = { "user_id": req.user_id, "member_id": req.member_id, "previous_portrait": req.previous_portrait, "behavior_summary": req.behavior_summary, "profile_snapshot": req.profile_snapshot, "portrait_text": None, } result = await graph.ainvoke(state) return PortraitGenerateResponse(portrait_text=result["portrait_text"]) except Exception as e: logger.exception("画像重画失败") raise HTTPException(status_code=500, detail=str(e)) ``` - [ ] **Step 2: 注册路由到 main.py** ```python # main.py 第 30 行附近 from app.api import portrait ... app.include_router(portrait.router) ``` - [ ] **Step 3: py_compile 验证** ```bash cd cfc-langgraph && python3 -m py_compile app/api/portrait.py app/graphs/portrait_generation_graph.py ``` - [ ] **Step 4: Commit** ```bash git add cfc-langgraph/app/api/portrait.py cfc-langgraph/app/main.py git commit -m "feat: Python 侧新增 portrait 生成/重画 API 端点" ``` --- ### 任务 9:小程序前端 API 封装 **Files:** - Modify: `cfc-frontend/utils/api.js` - [ ] **Step 1: 新增封装** ```javascript // 用户画像 export function getPortrait(params = {}) { return request('/api/user/portrait/get', 'POST', params) } export function editPortrait(data) { return request('/api/user/portrait/edit', 'POST', data) } export function generatePortrait(params = {}) { return request('/api/user/portrait/generate', 'POST', params) } export function regeneratePortrait(params = {}) { return request('/api/user/portrait/regenerate', 'POST', params) } ``` - [ ] **Step 2: 语法检查** ```bash node --check <(sed -n '/',c,re.DOTALL); open(r'D:\tmp\portrait_check.mjs','w',encoding='utf-8').write(m.group(1))" node --check D:\tmp\portrait_check.mjs ``` - [ ] **Step 5: Commit** ```bash git add cfc-frontend/pages/profile/portrait-edit.vue git commit -m "feat: portrait-edit.vue 新增 AI 生成/重画按钮与逻辑" ``` --- ### 任务 11:全量验收 - [ ] **Step 1: 后端编译** ```bash cd cfc-backend && mvn clean compile -q ``` - [ ] **Step 2: Python 编译** ```bash cd cfc-langgraph && python3 -m py_compile app/api/portrait.py app/graphs/portrait_generation_graph.py app/tools/java_client.py ``` - [ ] **Step 3: 前端语法检查** ```bash # portrait-edit.vue python -c "import re; f=open(r'D:\cfc\cfc-frontend\pages\profile\portrait-edit.vue','r',encoding='utf-8'); c=f.read(); f.close(); m=re.search(r'',c,re.DOTALL); open(r'D:\tmp\pe.mjs','w',encoding='utf-8').write(m.group(1))" node --check D:\tmp\pe.mjs # api.js node --check <(sed -n '/