mcpField
F
frappe-impl-whitelisted

frappe-impl-whitelisted

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

关于 frappe-impl-whitelisted

面向 Frappe 开发者的 whitelisted API 实现指南,涵盖端点设计、权限、安全和错误处理,并提供 v14、v15、v16 的版本差异。

主要功能

  1. 01端点类型与权限模型选择
  2. 02认证及访客 API 实现流程
  3. 03控制器方法与文件上传端点
  4. 04后台任务和错误处理模式
  5. 05客户端调用及外部集成示例
  6. 06v14–v16 功能差异说明

适用场景

  1. 01规划公开或登录用户 API
  2. 02为 DocType 表单实现控制器方法
  3. 03审核访客端点的输入与限流
  4. 04将 Server Script API 迁移为 whitelisted 方法

安装

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

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

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

包含的文件

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

references/
anti-patterns.md
decision-tree.md
examples.md
workflows.md

详细介绍

Frappe 白名单方法实现工作流

用于构建 API 端点的分步工作流。装饰器语法请参阅 frappe-syntax-whitelisted。

版本:v14/v15/v16(已注明版本专属功能)


决策总览:端点类型是什么?

WHAT ARE YOU BUILDING?
│
├─► Public API (no login required)?
│   └─► allow_guest=True + STRICT input validation + rate limiting
│
├─► Authenticated API for logged-in users?
│   └─► Default @frappe.whitelist() + document permission checks
│
├─► Admin-only API?
│   └─► frappe.only_for("System Manager")
│
├─► Document-specific method (called from form)?
│   └─► Controller method + frm.call() from JS
│
├─► Standalone utility API?
│   └─► Separate api.py + frappe.call() from JS
│
├─► External webhook receiver?
│   └─► allow_guest=True + signature verification
│
└─► Background job trigger?
    └─► Authenticated API that calls frappe.enqueue()

工作流 1:设计端点

步骤 1:选择位置
WHERE SHOULD THE CODE LIVE?
│
├─► Related to a DocType, called from its form?
│   └─► doctype/xxx/xxx.py (controller method)
│       Client: frm.call('method_name', args)
│
├─► Related to a DocType, standalone?
│   └─► doctype/xxx/xxx_api.py or myapp/api/module.py
│       Client: frappe.call('myapp.api.module.method')
│
├─► General app utility?
│   └─► myapp/api.py (small app) or myapp/api/module.py (large app)
│
└─► External integration?
    └─► myapp/integrations/service_name.py
步骤 2:选择权限模型
WHO CAN CALL THIS API?
│
├─► Anyone (public) → allow_guest=True
│   ⚠️ MUST validate ALL input, sanitize for XSS, rate limit
│
├─► Any logged-in user → Default (no allow_guest)
│   Still check document permissions per record!
│
├─► Specific role(s) → frappe.only_for("Role")
│
└─► Document-level → frappe.has_permission(doctype, ptype, doc)
步骤 3:选择 HTTP 方法
WHAT DOES THE API DO?
│
├─► Read-only → methods=["GET"]
├─► Creates/modifies data → methods=["POST"]
└─► Both or default → omit methods parameter (all allowed)

工作流 2:实现经过身份验证的 API

分步指南

步骤 1:创建函数

# myapp/api.py
import frappe
from frappe import _

@frappe.whitelist()
def get_customer_balance(customer):
    """Get outstanding balance for a customer."""
    # 1. Permission check
    if not frappe.has_permission("Customer", "read", customer):
        frappe.throw(_("Not permitted"), frappe.PermissionError)

    # 2. Validate input
    if not customer or not frappe.db.exists("Customer", customer):
        frappe.throw(_("Customer not found"), frappe.DoesNotExistError)

    # 3. Fetch and return
    balance = frappe.db.sql("""
        SELECT COALESCE(SUM(outstanding_amount), 0)
        FROM `tabSales Invoice`
        WHERE customer = %s AND docstatus = 1
    """, customer)[0][0]

    return {"customer": customer, "balance": balance}

步骤 2:从客户端脚本调用

frappe.call({
    method: 'myapp.api.get_customer_balance',
    args: { customer: 'CUST-00001' },
    callback(r) {
        if (r.message) console.log(r.message.balance);
    }
});

步骤 3:使用 curl 测试

# Authenticate first
curl -X POST https://site.com/api/method/login \
  -d 'usr=admin&pwd=password'

# Call the API
curl -X POST https://site.com/api/method/myapp.api.get_customer_balance \
  -H "Content-Type: application/json" \
  -d '{"customer": "CUST-00001"}' \
  --cookie cookies.txt

# Or use token auth
curl -X POST https://site.com/api/method/myapp.api.get_customer_balance \
  -H "Authorization: token api_key:api_secret" \
  -H "Content-Type: application/json" \
  -d '{"customer": "CUST-00001"}'

工作流 3:实现公开(访客)API

分步指南

步骤 1:创建并进行严格验证

@frappe.whitelist(allow_guest=True, methods=["POST"])
def submit_inquiry(name, email, phone=None, message=None):
    """Public contact form — strict validation required."""
    # 1. Validate required fields
    if not all([name, email]):
        frappe.throw(_("Name and email are required"))

    # 2. Validate email format
    if not frappe.utils.validate_email_address(email):
        frappe.throw(_("Invalid email address"))

    # 3. Sanitize ALL input
    name = frappe.utils.strip_html(name)[:100]
    email = email.strip().lower()[:200]
    phone = frappe.utils.strip_html(phone)[:20] if phone else None
    message = frappe.utils.strip_html(message)[:2000] if message else None

    # 4. Create record with ignore_permissions
    lead = frappe.get_doc({
        "doctype": "Lead",
        "lead_name": name, "email_id": email,
        "phone": phone, "notes": message, "source": "Website"
    })
    lead.insert(ignore_permissions=True)

    return {"success": True, "message": _("Thank you")}

步骤 2:添加速率限制(v15+)

from frappe.rate_limiter import rate_limit

@frappe.whitelist(allow_guest=True, methods=["POST"])
@rate_limit(limit=5, seconds=60)  # 5 calls per minute
def submit_inquiry(name, email, phone=None, message=None):
    ...
访客 API 的关键规则
  • 始终验证并清理每个输入参数
  • 始终为写入数据的端点使用 methods=["POST"]
  • 始终添加速率限制(v15+ 使用装饰器;v14 使用基于缓存的手动限流)
  • 绝不暴露内部错误详情——使用 frappe.log_error() 记录日志,并显示通用消息
  • 绝不返回敏感数据(内部 ID、文件路径、堆栈跟踪)
  • 绝不将原始用户输入传递给 frappe.get_doc()——应使用显式字段映射

工作流 4:实现控制器方法

分步指南

步骤 1:将方法添加到 DocType 控制器

# myapp/doctype/sales_order/sales_order.py
class SalesOrder(Document):
    @frappe.whitelist()
    def calculate_shipping(self, carrier):
        """Called from form via frm.call()."""
        if not self.shipping_address:
            frappe.throw(_("Shipping address required"))
        rate = get_shipping_rate(self.shipping_address, carrier)
        return {"carrier": carrier, "rate": rate}

步骤 2:从表单 JS 调用

frm.call('calculate_shipping', {
    carrier: 'FedEx'
}).then(r => {
    frm.set_value('shipping_amount', r.message.rate);
});

关键区别:通过 frm.call() 调用控制器方法时,Frappe 会自动检查文档权限。无需手动检查文档本身的权限。


工作流 5:实现错误处理

标准模式
@frappe.whitelist()
def process_payment(invoice, amount):
    try:
        # Validate
        if not invoice:
            frappe.throw(_("Invoice required"), frappe.ValidationError)
        if not frappe.has_permission("Sales Invoice", "write", invoice):
            frappe.throw(_("Not permitted"), frappe.PermissionError)

        # Process
        result = do_payment(invoice, float(amount))
        return {"success": True, "data": result}

    except (frappe.ValidationError, frappe.PermissionError):
        raise  # Let Frappe handle (417 / 403)
    except frappe.DoesNotExistError:
        frappe.throw(_("Not found"), frappe.DoesNotExistError)  # 404
    except Exception:
        frappe.log_error(frappe.get_traceback(), "Payment Error")
        frappe.local.response["http_status_code"] = 500
        return {"success": False, "error": _("Internal error")}
HTTP 状态码参考
代码Frappe 异常适用情况
200—成功
403PermissionError拒绝访问
404DoesNotExistError未找到
409DuplicateEntryError重复
417ValidationError验证失败
429—超出速率限制(v15+)
500Exception服务器错误

工作流 6:文件上传端点

@frappe.whitelist()
def upload_attachment(doctype, docname):
    """Handle file upload attached to a document."""
    if not frappe.has_permission(doctype, "write", docname):
        frappe.throw(_("Not permitted"), frappe.PermissionError)

    file = frappe.request.files.get('file')
    if not file:
        frappe.throw(_("No file provided"))

    file_doc = frappe.get_doc({
        "doctype": "File",
        "file_name": file.filename,
        "attached_to_doctype": doctype,
        "attached_to_name": docname,
        "content": file.read(),
        "is_private": 1
    })
    file_doc.insert(ignore_permissions=True)
    return {"file_url": file_doc.file_url}

工作流 7:后台任务端点

@frappe.whitelist()
def start_heavy_export(doctype, filters=None):
    """Trigger a background job — returns immediately."""
    frappe.only_for("System Manager")

    frappe.enqueue(
        "myapp.tasks.export_data",
        queue="long",
        timeout=1500,
        doctype=doctype,
        filters=filters,
        user=frappe.session.user
    )
    return {"status": "queued", "message": _("Export started")}

客户端集成模式

带选项的 frappe.call()
frappe.call({
    method: 'myapp.api.my_method',
    args: { param: 'value' },
    freeze: true,
    freeze_message: __('Processing...'),
    callback(r) { console.log(r.message); },
    error(r) { frappe.msgprint(__('Error')); }
});
Async/Await 模式
const r = await frappe.call({
    method: 'myapp.api.get_data',
    args: { id: 123 }
});
console.log(r.message);
来自外部系统的 REST API
# Token auth (recommended for integrations)
curl -H "Authorization: token api_key:api_secret" \
  https://site.com/api/method/myapp.api.method

# Bearer auth (OAuth)
curl -H "Authorization: Bearer access_token" \
  https://site.com/api/method/myapp.api.method

安全规则(始终/绝不)

  1. 始终在访问任何文档前检查权限
  2. 始终使用参数化查询——绝不在 SQL 中使用 f 字符串
  3. 始终验证并清理访客 API 的输入
  4. 始终在使用 ignore_permissions=True 前通过 frappe.only_for() 检查角色
  5. 绝不暴露内部错误——记录详情并返回通用消息
  6. 绝不对会修改数据的端点使用 methods=["GET"]
  7. 绝不信任访客 API 提供的 data 字典——应使用显式参数名
  8. 始终在浏览器发出的 fetch() 调用中包含 X-Frappe-CSRF-Token 标头

迁移:从 Server Script API 迁移到白名单方法

步骤操作
1将 Server Script 逻辑复制到 myapp/api/module.py
2使用相同的权限模型添加 @frappe.whitelist() 装饰器
3将所有 frappe.call() 引用更新为新的点分路径
4禁用或删除 Server Script
5运行 bench --site sitename migrate

版本差异

功能v14v15v16
@frappe.whitelist()是是是
allow_guest是是是
methods 参数是是是
类型注解验证否是是
@rate_limit 装饰器否是是
API v2 端点否是是
v15+ 类型验证
@frappe.whitelist()
def typed_api(customer: str, limit: int = 10) -> dict:
    """v15+ validates types from annotations automatically."""
    return {"customer": customer, "limit": limit}

参考文件

文件内容
decision-tree.md完整的 API 类型与权限选择指南
workflows.md分步实现模式(10 多种工作流)
examples.md可用于生产环境的代码示例

评论

支持 Markdown