mcpField
F
frappe-errors-clientscripts

frappe-errors-clientscripts

github.com/impertio-studio开发198110 次安装30 周前更新
代码仓库

关于 frappe-errors-clientscripts

面向 Frappe 开发者的客户端脚本错误诊断指南,将常见错误与原因、修复方法对应起来。内容覆盖异步调用、子表访问、事件时机及调试工具。

主要功能

  1. 01按错误信息定位原因
  2. 02提供常见问题修复模式
  3. 03说明异步调用错误处理
  4. 04指导子表事件中的数据访问
  5. 05列出浏览器调试工具

适用场景

  1. 01排查表单字段未定义错误
  2. 02修复 frappe.call 静默失败
  3. 03诊断子表行访问问题
  4. 04处理事件循环或保存校验问题

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/impertio-studio/frappe_claude_skill_package --skill frappe-errors-clientscripts

安装后在 Claude Code 中输入/frappe-errors-clientscripts确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
anti-patterns.md
examples.md
patterns.md

详细介绍

客户端脚本错误 — 诊断与解决

交叉引用:frappe-syntax-clientscripts(语法)、frappe-impl-clientscripts(工作流)、frappe-errors-serverscripts(服务器端)。


错误诊断流程图

ERROR IN CLIENT SCRIPT
│
├─► TypeError: Cannot read properties of undefined
│   ├─► "frm.doc.fieldname" → Field does not exist on DocType
│   ├─► "r.message.value" → Server returned null/error
│   └─► "row.fieldname" in child table → Row not fetched correctly
│
├─► frappe.call fails silently
│   ├─► Missing error callback → Add error handler
│   ├─► 403 Forbidden → Method not whitelisted (@frappe.whitelist)
│   ├─► 417 Expectation Failed → Server-side frappe.throw()
│   └─► 401 Unauthorized → Session expired or CSRF token invalid
│
├─► Uncaught (in promise) → Missing try/catch on async frappe.call
│
├─► Field appears blank after set_value → Timing issue (setup vs refresh)
│
├─► cur_frm is undefined → Using cur_frm in list/report context
│
└─► frappe.throw() does not prevent save → Used outside validate event

错误消息 → 原因 → 修复方法表

错误消息原因修复方法
TypeError: Cannot read properties of undefined (reading 'fieldname')DocType 中不存在该字段,或文档尚未加载始终在访问字段前检查 frm.doc 是否存在
TypeError: frm.set_value is not a function使用了未定义的 cur_frm 快捷方式始终使用事件处理程序中的 frm 参数
Uncaught (in promise)frappe.call 的异步拒绝未处理始终使用 try/catch 包装异步调用
CSRFTokenError / 403 with CSRF会话超时后令牌不匹配始终使用 frappe.call()(会自动处理 CSRF)
Not permitted / frappe.call 返回 403服务器方法缺少 @frappe.whitelist()始终为 API 方法添加 @frappe.whitelist() 装饰器
frappe.throw() not preventing save在 validate 事件之外使用了 frappe.throw()仅在 validate 中使用 frappe.throw()
set_query 中出现 field not found: xyzfieldname 拼写错误,或字段不在子表中对照 DocType 定义确认准确的 fieldname
row.item_code is undefined子表行访问方式错误 — locals 尚未同步在子表事件中使用 frappe.get_doc(cdt, cdn)
frm.set_value not working在表单完全加载前的 setup 中调用将字段设置逻辑移到 refresh 事件
Maximum call stack exceeded触发循环 — 字段更改触发了自身处理程序使用 frm.flags 守卫打破递归

关键错误模式

1. cur_frm 与 frm:新手最常犯的错误
// ❌ WRONG — cur_frm is undefined in many contexts
frappe.ui.form.on('Sales Order', {
    customer(frm) {
        cur_frm.set_value('territory', 'Default');  // BREAKS in list view
    }
});

// ✅ CORRECT — ALWAYS use the frm parameter
frappe.ui.form.on('Sales Order', {
    customer(frm) {
        frm.set_value('territory', 'Default');
    }
});

规则:绝不要使用 cur_frm。始终使用传递给每个事件处理程序的 frm 参数。

2. Async/Await:没有 try/catch 会静默失败
// ❌ WRONG — Unhandled rejection crashes silently
frappe.ui.form.on('Sales Order', {
    async customer(frm) {
        let r = await frappe.call({
            method: 'myapp.api.get_data',
            args: { customer: frm.doc.customer }
        });
        frm.set_value('credit_limit', r.message.limit);  // r.message may be null
    }
});

// ✅ CORRECT — try/catch with null check
frappe.ui.form.on('Sales Order', {
    async customer(frm) {
        if (!frm.doc.customer) return;
        try {
            let r = await frappe.call({
                method: 'myapp.api.get_data',
                args: { customer: frm.doc.customer }
            });
            if (r.message) {
                frm.set_value('credit_limit', r.message.limit || 0);
            }
        } catch (error) {
            console.error('Customer fetch failed:', error);
            frappe.show_alert({
                message: __('Could not load customer details'),
                indicator: 'red'
            }, 5);
        }
    }
});
3. 子表访问:错误模式
// ❌ WRONG — frm.doc.items[0] may not reflect latest state
frappe.ui.form.on('Sales Order Item', {
    item_code(frm, cdt, cdn) {
        let row = frm.doc.items.find(r => r.name === cdn);  // fragile
        row.rate = 100;  // Does not trigger UI refresh
    }
});

// ✅ CORRECT — Use frappe.get_doc and frappe.model.set_value
frappe.ui.form.on('Sales Order Item', {
    item_code(frm, cdt, cdn) {
        let row = frappe.get_doc(cdt, cdn);
        if (!row.item_code) return;
        frappe.model.set_value(cdt, cdn, 'rate', 100);  // Triggers refresh
    }
});
4. 时机:setup 与 refresh
// ❌ WRONG — set_value in setup, form not ready
frappe.ui.form.on('Sales Order', {
    setup(frm) {
        frm.set_value('company', 'My Company');  // May not work
    }
});

// ✅ CORRECT — set_query in setup, set_value in refresh/onload
frappe.ui.form.on('Sales Order', {
    setup(frm) {
        // Filters belong in setup
        frm.set_query('customer', () => ({ filters: { disabled: 0 } }));
    },
    refresh(frm) {
        // Value changes belong in refresh (or onload for new docs)
        if (frm.is_new()) {
            frm.set_value('company', 'My Company');
        }
    }
});
5. frappe.throw() 的作用范围:仅在 validate 中有效
// ❌ WRONG — throw in customer change does NOT prevent save
frappe.ui.form.on('Sales Order', {
    customer(frm) {
        if (!frm.doc.customer) {
            frappe.throw(__('Customer required'));  // Stops script, NOT save
        }
    }
});

// ✅ CORRECT — throw in validate prevents save
frappe.ui.form.on('Sales Order', {
    customer(frm) {
        if (!frm.doc.customer) {
            frappe.msgprint({ message: __('Customer required'), indicator: 'orange' });
        }
    },
    validate(frm) {
        if (!frm.doc.customer) {
            frappe.throw(__('Customer is required'));  // Prevents save
        }
    }
});
6. 使用标志防止递归
// ❌ WRONG — discount change triggers amount recalc, which triggers discount...
frappe.ui.form.on('Sales Order', {
    discount_percent(frm) {
        frm.set_value('grand_total', calculate(frm));  // Fires on_change loop
    }
});

// ✅ CORRECT — Use flags to break the cycle
frappe.ui.form.on('Sales Order', {
    discount_percent(frm) {
        if (frm.flags.skip_recalc) return;
        frm.flags.skip_recalc = true;
        frm.set_value('grand_total', calculate(frm));
        frm.flags.skip_recalc = false;
    }
});

调试工具

工具使用方法适用场景
浏览器控制台(F12)console.log(frm.doc)检查表单状态
console.table()console.table(frm.doc.items)查看子表行
JSON.parse(JSON.stringify(frm.doc))深度克隆以创建快照避免控制台中的循环引用
frappe.boot.developer_mode检查是否开启开发者模式有条件地记录调试日志
frappe.ui.toolbar.clear_cache()清除客户端缓存部署脚本更改后
Network 标签页(F12)筛选 XHR 请求检查 frappe.call 负载
frappe.show_alert({message: 'debug', indicator: 'blue'}, 5)在 UI 中显示可视化调试信息无需控制台即可快速反馈

始终遵循 / 绝不要违反的规则

始终遵循
  1. 使用 frm 参数 — 绝不要使用 cur_frm [v14+]
  2. 用 try/catch 包装异步 frappe.call — 未处理的拒绝会静默失败
  3. 所有面向用户的字符串都使用 __() — 翻译所必需
  4. 先收集多个验证错误,再调用 frappe.throw()
  5. 使用 frappe.get_doc(cdt, cdn) 在事件中访问子表行
  6. 仅在 validate 中使用 frappe.throw() 以阻止保存
  7. 访问服务器响应属性前,检查 r.message 是否为 null
  8. 在子表事件中使用 frappe.model.set_value(cdt, cdn, field, value)
绝对不要
  1. 绝对不要使用 alert()、confirm() 或 prompt() — 使用 frappe.msgprint / frappe.confirm
  2. 绝对不要向用户暴露堆栈跟踪 — 记录到控制台,并显示友好的消息
  3. 绝对不要使用 cur_frm — 它不可靠,在许多上下文中未定义
  4. 绝对不要在生产环境中留下 console.log — 使用条件检查 frappe.boot.developer_mode
  5. 绝对不要在同一个函数中混用 .then() 和 await — 选择一种模式
  6. 绝对不要在 setup 中调用 frm.set_value — 表单尚未就绪;请使用 refresh 或 onload
  7. 使用回调模式时,绝对不要忽略 frappe.call 的 error 回调

参考文件

文件内容
references/examples.md真实错误场景及其诊断
references/anti-patterns.md常见错误及修复前后对比
references/patterns.md防御性错误处理模式

评论

支持 Markdown