乐于分享
好东西不私藏

分享一个自制blender AI助手

分享一个自制blender AI助手

最近用AI做了blender AI助手,很简单以下是他能干的事,
并不完美,但也能用
可以执行一些简单指令的效果,我也没尝试太多,但是有些简单的批量指令还是可以给工作带来一些效率提升。
代码我放下边了,有兴趣的,可以自己尝试,把代码放记事本里,保存成py格式就可以用了,需要自己添加个大模型API。
大家有什么建议,欢迎交流。
bl_info = {    "name""Blender AI 智能助手 (时光机版)",    "author""匹宙",    "version": (700),    "blender": (400),    "location""3D视图 > 侧边栏 (快捷键N) > AI助手",    "description""支持智能问答、代码执行,并带有带勾选框的可视化操作历史。",    "warning""",    "doc_url""",    "category""3D View",}import bpyimport jsonimport urllib.requestimport threadingimport sysimport ioimport reimport traceback# --- 全局状态与日志管理 ---class ConsoleState:    is_processing = False    logs = [        "🤖 欢迎!您可以下达指令或提问。",        "💡 提示: 操作历史会在下方生成,可随时勾选/取消。"    ]state = ConsoleState()def wrap_text_for_blender(text, max_chars=22):    wrapped_lines = []    for paragraph in str(text).split('\n'):        if not paragraph.strip():            wrapped_lines.append("")            continue        while len(paragraph) > max_chars:            wrapped_lines.append(paragraph[:max_chars])            paragraph = paragraph[max_chars:]        wrapped_lines.append(paragraph)    return wrapped_linesdef print_to_console(text, prefix=""):    lines = wrap_text_for_blender(text)    for line in lines:        state.logs.append(prefix + line)    if len(state.logs) > 30:        state.logs = state.logs[-30:]# --- 数据结构: AI 历史记录项 ---class AIHistoryItem(bpy.types.PropertyGroup):    prompt: bpy.props.StringProperty(name="指令")    is_active: bpy.props.BoolProperty(name="生效状态", default=True)# --- 核心通信功能 ---def call_llm_async(system_prompt, user_prompt, context, callback):    api_key = context.scene.ai_api_key    base_url = context.scene.ai_base_url    model_name = context.scene.ai_model_name    if not api_key:        callback(None"ERROR: 请先展开上方配置,填写 API Key!")        return    def task():        try:            url = base_url.rstrip("/") + "/chat/completions"            payload = {                "model": model_name,                "messages": [                    {"role""system""content": system_prompt},                    {"role""user""content": user_prompt}                ],                "temperature"0.1            }            data = json.dumps(payload).encode('utf-8')            headers = {                'Content-Type''application/json',                'Authorization'f'Bearer {api_key}'            }            req = urllib.request.Request(url, data=data, headers=headers)            with urllib.request.urlopen(req, timeout=40as response:                result = json.loads(response.read().decode('utf-8'))                reply_text = result["choices"][0]["message"]["content"]                match = re.search(r'```(?:python)?\s*(.*?)\s*```', reply_text, re.DOTALL | re.IGNORECASE)                if match:                    code = match.group(1).strip()                    bpy.app.timers.register(lambda: callback(code, Noneor None)                else:                    bpy.app.timers.register(lambda: callback(None, reply_text.strip()) or None)        except Exception as e:            bpy.app.timers.register(lambda: callback(Nonef"ERROR: 网络错误 - {str(e)}"or None)    thread = threading.Thread(target=task)    thread.daemon = True    thread.start()# --- 算子 (Operators) ---class BLENDER_AI_OT_execute(bpy.types.Operator):    """执行 AI 指令或回答问题"""    bl_idname = "blender_ai.execute_code"    bl_label = "发送 (回车)"    def execute(self, context):        scene = context.scene        prompt = scene.ai_prompt        if not prompt:            return {'CANCELLED'}        state.is_processing = True        print_to_console(f"👤 我: {prompt}")        print_to_console("🤖 思考中...")        active_obj = context.active_object.name if context.active_object else "无"        current_mode = context.mode        system_prompt = f"""你是一个专业的 Blender 3D 专家助手。【情况 1:执行操作】如要求创建、修改物体、材质等,请只输出可放入 exec() 运行的 Python 脚本,用 ```python 包裹。包含 import bpy。【情况 2:问答咨询】如提问快捷键、概念等,直接用简短纯中文回答,不要输出代码。[当前状态] 模式: {current_mode}, 选中: {active_obj}"""        def handle_response(code, chat_reply):            state.is_processing = False            if chat_reply:                if chat_reply.startswith("ERROR:"):                    print_to_console(f"❌ {chat_reply}")                else:                    print_to_console("🤖 回答:")                    print_to_console(chat_reply)                scene.ai_prompt = ""                 return            if code:                # 核心逻辑:如果在历史中间插入新操作,需清除后面的撤销记录                if scene.ai_history_index < len(scene.ai_history) - 1:                    # 删除当前指针之后的所有历史记录                    for _ in range(len(scene.ai_history) - 1, scene.ai_history_index, -1):                        scene.ai_history.remove(len(scene.ai_history) - 1)                print_to_console("🤖 执行操作中...")                old_stdout = sys.stdout                redirected_output = io.StringIO()                sys.stdout = redirected_output                try:                    # 执行代码前推入历史节点                    bpy.ops.ed.undo_push(message=f"AI 之前状态")                    exec(code, globals(), locals())                    bpy.ops.ed.undo_push(message=f"AI: {prompt}")                    # 记录到自定义的时间线列表                    new_item = scene.ai_history.add()                    new_item.prompt = prompt                    new_item.is_active = True                    scene.ai_history_index = len(scene.ai_history) - 1                    print_to_console("✅ 操作成功!")                    scene.ai_prompt = ""                 except Exception as e:                    exc_type, exc_value, exc_traceback = sys.exc_info()                    tb_lines = traceback.format_exception(exc_type, exc_value, exc_traceback)                    print_to_console(f"❌ 报错:\n{''.join(tb_lines[-2:]).strip()}")                finally:                    sys.stdout = old_stdout        call_llm_async(system_prompt, prompt, context, callback=handle_response)        return {'FINISHED'}class BLENDER_AI_OT_toggle_history(bpy.types.Operator):    """时光机:通过勾选框撤销或重做操作"""    bl_idname = "blender_ai.toggle_history"    bl_label = "切换历史状态"    target_idx: bpy.props.IntProperty()    def execute(self, context):        scene = context.scene        history = scene.ai_history        current_idx = scene.ai_history_index        target_idx = self.target_idx        target_item = history[target_idx]        is_currently_active = target_item.is_active        # 如果取消勾选当前项,意味着我们要回到当前项的【前一步】        # 如果勾选当前项,意味着我们要来到【当前项】        desired_state_idx = target_idx - 1 if is_currently_active else target_idx        # 时光穿梭逻辑:调用原生的 Undo / Redo        if desired_state_idx < current_idx:            # 需要后退 (撤销)            steps = current_idx - desired_state_idx            for _ in range(steps):                bpy.ops.ed.undo()        elif desired_state_idx > current_idx:            # 需要前进 (重做)            steps = desired_state_idx - current_idx            for _ in range(steps):                bpy.ops.ed.redo()        # 更新历史列表的 UI 显示状态        scene.ai_history_index = desired_state_idx        for i, item in enumerate(history):            item.is_active = (i <= desired_state_idx)        return {'FINISHED'}class BLENDER_AI_OT_clear_history(bpy.types.Operator):    """清空列表"""    bl_idname = "blender_ai.clear_history"    bl_label = "清空历史与日志"    def execute(self, context):        state.logs = ["🧹 面板已清空。"]        context.scene.ai_history.clear()        context.scene.ai_history_index = -1        return {'FINISHED'}# --- UI 面板 ---class BLENDER_AI_PT_main_panel(bpy.types.Panel):    bl_label = "Blender AI 助手"    bl_idname = "BLENDER_AI_PT_main_panel"    bl_space_type = 'VIEW_3D'    bl_region_type = 'UI'    bl_category = 'AI助手'    def draw(self, context):        layout = self.layout        scene = context.scene        # 1. 配置菜单 (折叠)        row = layout.row()        icon = 'TRIA_DOWN' if scene.ai_show_settings else 'TRIA_RIGHT'        row.prop(scene, "ai_show_settings", text="大模型 API 配置", icon=icon, emboss=False)        if scene.ai_show_settings:            box = layout.box()            box.prop(scene, "ai_base_url")            box.prop(scene, "ai_model_name")            box.prop(scene, "ai_api_key")        layout.separator()        # 2. 对话/指令输入        box_input = layout.box()        box_input.label(text="输入操作指令 或 提问咨询", icon='COMMUNITY')        box_input.prop(scene, "ai_prompt", text="")        row = box_input.row()        row.scale_y = 1.5        if state.is_processing:            row.label(text="AI 正在处理...", icon='TIME')        else:            row.operator(BLENDER_AI_OT_execute.bl_idname, text="发送指令/提问", icon='PLAY')        # 3. 创新点:AI 操作历史 (带勾选框)        if len(scene.ai_history) > 0:            layout.separator()            box_history = layout.box()            box_history.label(text="📦 操作时间线 (点击勾选框撤销)", icon='ACTION')            for i, item in enumerate(scene.ai_history):                row = box_history.row()                icon = 'CHECKBOX_HLT' if item.is_active else 'CHECKBOX_DEHLT'                # 点击图标即可触发撤销/重做                op = row.operator(BLENDER_AI_OT_toggle_history.bl_idname, text="", icon=icon, emboss=False)                op.target_idx = i                # 如果被撤销了,文字变灰 (在Blender UI中通过禁用此行模拟)                row_text = row.row()                row_text.enabled = item.is_active                row_text.label(text=f"{i+1}{item.prompt}")        layout.separator()        # 4. 对话文本日志区        box_console = layout.box()        row_hdr = box_console.row()        row_hdr.label(text="对话与执行日志", icon='TEXT')        row_hdr.operator(BLENDER_AI_OT_clear_history.bl_idname, text="", icon='TRASH')        col = box_console.column(align=True)        for line in state.logs:            col.label(text=line)# --- 注册与卸载 ---classes = (    AIHistoryItem,    BLENDER_AI_OT_execute,    BLENDER_AI_OT_toggle_history,    BLENDER_AI_OT_clear_history,    BLENDER_AI_PT_main_panel,)def register():    for cls in classes:        bpy.utils.register_class(cls)    bpy.types.Scene.ai_history = bpy.props.CollectionProperty(type=AIHistoryItem)    bpy.types.Scene.ai_history_index = bpy.props.IntProperty(default=-1)    bpy.types.Scene.ai_show_settings = bpy.props.BoolProperty(name="显示设置", default=True)    bpy.types.Scene.ai_base_url = bpy.props.StringProperty(name="API 地址", default="https://api.deepseek.com/v1")    bpy.types.Scene.ai_model_name = bpy.props.StringProperty(name="模型名称", default="deepseek-chat")    bpy.types.Scene.ai_api_key = bpy.props.StringProperty(name="API Key", default="", subtype='PASSWORD')    bpy.types.Scene.ai_prompt = bpy.props.StringProperty(name="发送内容", default="")def unregister():    for cls in reversed(classes):        bpy.utils.unregister_class(cls)    del bpy.types.Scene.ai_history    del bpy.types.Scene.ai_history_index    del bpy.types.Scene.ai_show_settings    del bpy.types.Scene.ai_base_url    del bpy.types.Scene.ai_model_name    del bpy.types.Scene.ai_api_key    del bpy.types.Scene.ai_promptif __name__ == "__main__":    register()