S3文件存储

我可以把文件存到 S3、GCS 或 Google Drive,而不是服务器上吗?

不能直接这样做。Frappe 没有内置的设置可以把新上传的附件发送到
Amazon S3、Google Cloud Storage (GCS) 或 Google Drive。用户通过附件对话框上传的每个文件
都会保存到站点的本地文件存储中,而不是外部对象存储。

提出这个问题最多的,通常是附件增长很快的团队,比如扫描的收据、客户上传的视频或产品图片,
他们希望在站点磁盘空间用完之前把这些文件转移出去。

为什么没有这个功能

Frappe 的文件存储出于速度和简洁性的考虑,设计上就是直接写入本地磁盘。要把新上传的文件
发送到外部对象存储,需要改变 Frappe 读写文件的方式,而不仅仅是一个设置,所以目前没有
受支持的方法来实现这一点。

Frappe Framework 中的 Google Drive 集成经常被误认为是这个功能。其实不是一回事:
它只能让你浏览自己的 Google Drive,并为那里已经存在的文件附加一个链接。
它不会改变用户新上传文件的存储位置,也不会把站点已有的文件移到 Drive 中。

最接近的办法

如果文件已经存在于 Frappe 之外

如果文件已经托管在 S3、GCS、Google Drive 或任何其他服务上,可以附加一个链接,
而不是上传一份副本:

  1. 打开文档,在侧边栏中进入 附件。
  2. 点击 +,然后选择 链接。
  3. 把文件的 URL 粘贴到 附加网页链接 中,然后点击 上传。

Frappe 存储的是链接而不是文件,所以这个附件不占用站点的任何存储空间。

如果你希望新上传的文件自动存到 S3

有一个社区维护的应用市场应用 Frappe S3 Attachment,可以把新上传的文件重定向到 S3
存储桶。它不是由 Frappe 开发或维护的:

  • Frappe 支持团队无法排查该应用中的问题,也无法保证它能与你的站点版本兼容。
    安装前请查看该应用自己的页面,了解它目前支持哪些 Frappe 版本。
  • 它是针对 AWS S3 开发和测试的。像 Oracle Object Storage 或 MinIO 这样的 S3 兼容服务商
    可能可以用相同的存储桶名称、访问密钥、密钥、区域和自定义端点 URL 来运行,
    但这并未得到确认,出现的任何问题都需要你自己解决。

这个变通方法的局限

  • 两种方案都不能自动生成文件夹结构,比如按文档类型、客户或日期分类。
    这需要通过 Frappe 合作伙伴或你自己的团队进行定制开发。
  • 网页链接方案只适用于你已经托管在别处的文件。它不会减少已经上传到 Frappe 的文件
    所占用的空间。
  • 如果团队很在意把文件存储从服务器上移走,可以在 GitHub 上提交功能请求,
    或者在社区论坛中询问是否已有其他开发者构建了可以复用的东西。
  • 附件
  • Google Drive
  • 将备份上传到 Amazon S3

事件流

事件流是 Frappe 的一项功能,用于从一个 Frappe 站点发布文档变更,并在另一个站点上消费这些变更。当需要在站点之间同步选定的 DocType,而又不想为每种记录类型单独构建集成时,这个功能非常有用。

事件流的工作原理

在典型的配置中,一个站点充当事件生产者,另一个站点充当事件消费者。当选定的文档在生产者站点上被创建或更新时,事件会被记录下来,然后由另一个站点消费。这样就可以在多个 Frappe 站点之间复制主数据、交易数据或自定义 DocType。

版本 15 及之后的状态

从 Frappe/ERPNext 版本 15 开始,事件流不再作为受维护的核心功能提供支持。在 Frappe 版本 14 中,事件流存在于框架源码的 frappe/event_streaming 下。在版本 15 中,Frappe 添加了一个移除补丁,用于移除事件流模块及其 DocType,包括事件生产者、事件消费者、事件更新日志以及相关的映射 DocType。

独立应用

该功能被拆分成了一个独立的应用:frappe/event_streaming。然而,这个应用并没有得到积极维护。该 GitHub 仓库已于 2024 年 12 月 6 日被 Frappe 归档,目前为只读状态,且没有发布任何版本。除非你的团队准备好独立测试和维护它,否则在新的生产环境中应将其视为不受支持的功能。

  • REST API:使用 Frappe 的 REST API 在 ERPNext 和其他系统之间进行明确的集成。
  • Webhook:当文档创建或更新后需要通知另一个系统时,使用 Webhook。
  • 后台任务:对于需要重试、日志和错误处理的同步逻辑,使用定时任务或队列任务。
  • 自定义集成应用:当需要仔细控制数据映射、冲突处理或业务规则时,构建自定义应用。

需要注意的情况

避免在版本 15 或更高版本上围绕事件流规划新的实现。对于当前的 ERPNext 集成,建议优先使用 API、Webhook 和针对集成场景的队列,这样可以明确责任归属、重试行为和故障处理方式。

  • GitHub 上的 frappe/event_streaming
  • Frappe 版本 15 事件流移除补丁
  • Frappe REST API
  • Webhook
  • 后台任务

日志记录

记录事件可以大大改善调试体验。Frappe 的开发和生产环境都自带日志记录功能。

根据访问方式的不同,日志主要分为两大类。桌面日志(Desk Logs)存储在站点数据库中,可以作为文档进行访问和查询;服务器日志(Server Logs)则存储在由日志轮转系统管理的文件中。

桌面日志

可以通过桌面界面(Desk UI)访问的日志(通常是从 Awesomebar 中搜索查找)。这些日志一般用于追踪操作事件;但你也可以利用它们的 API 来追踪 Frappe 应用中的任何内容。

Core 模块中的部分日志包括:

  • 访问日志
  • 活动日志
  • 错误日志
  • 计划任务日志

你可以通过嵌入的链接了解更多信息。了解每种日志的最佳方式是直接在你的站点上查看它们。

服务器日志

与桌面日志相比,服务器日志通常包含更底层的、事务性的数据。从版本 13 开始,日志也可以在站点级别获取。这些站点日志由 Frappe 应用程序创建,而许多 bench 级别的日志文件则由支持 Frappe 环境的进程生成。在你的 bench 文件夹中,你可以在以下位置找到日志:

  • ./logs
  • ./sites/{site}/logs

在撰写本文时,只有 frappe.web.log 和 scheduler.log 会在站点和 bench 级别记录日志。在 bench 级别,一些最有用的文件可能是:

  • bench.log
  • scheduler.log
  • worker.log

在这些日志中,你可以追踪已执行的 bench 命令、调度器或后台任务运行的状态。

> 要在你的站点上启用 Frappe Web 日志记录,请使用 > enable_frappe_logger 更新站点配置:true

错误快照

除此之外,Frappe 还会为你的站点维护 HTTP 响应码 500 及以上的错误快照。这样做是为了在发生服务器错误时提供更多的故障上下文。例如,某个桌面操作因 HTTP 状态码 500 而失败,这会在 ./sites/{site}/frappe.log 文件中添加一条相应的日志,如下所示:

2021-05-06 14:08:12,177 ERROR frappe New Exception collected with snapshot id: 2021-05-06 14:08:11.940809-127.0.0.1-bde
Site: frappeframework.com
Form Dict: {'docs': '{"name":"assets.frappeframework.com","owner":"Administrator","creation":"2020-04-04 14:58:18.771247","modified":"2021-02-08 09:40:56.820300","modified_by":"gavin@frappe.io","idx":0,"docstatus":0,"status":"Active","hostname":"assets","domain":"frappeframework.com","cluster":"Default","provider":"Generic","is_server_setup":1,"ip":"192.168.29.100","private_ip":"10.120.120.120","agent_password":"*****","is_primary":1,"doctype":"Server","__last_sync_on":"2021-05-06T08:38:08.931Z"}', 'method': 'ping_agent', 'cmd': 'run_doc_method'}

日志的第一行提到了该错误的快照 ID,在本例中是 2021-05-06 14:08:11.940809-127.0.0.1-bde。在 ./sites/frappeframework.com/error-snapshots 下应该有一个以该快照 ID 标识的对应文件,文件名为 2021-05-06 14:08:11.940809-127.0.0.1-bde.json。文件内容可能是:

    {
    "etype": "UnboundLocalError",
    "evalue": "UnboundLocalError("local variable 'headers' referenced before assignment")",
    "exception": {
      "args": [...],
      "with_traceback": [...]
    },
    "frames": [
      {
      "call": "(request=<request 'https:="" frappeframework.com="" api="" method="" run_doc_method'="" [post]="">)",
      "dump": {
        "frappe. api": "<module 'frappe.api'="" from="" '="" home="" frappe="" frappe-bench="" apps="" api.py'="">",
        "frappe.api. handle": "<function handle="">",
        "global frappe": "<module 'frappe'="" from="" '="" home="" frappe="" frappe-bench="" apps="" __init__.py'="">",
        "response": "None"
      },
      "file": "/home/frappe/frappe-bench/apps/frappe/frappe/app.py",
      "func": "application",
      "lines": {
        "66": "",
        "67": "elif request.path.startswith("/api/"):",
        "68": "response = frappe.api.handle()",
        "69": "",
        "70": "elif request.path.startswith('/backups'):"
      },
      "lnum": 68
      },
    ...
    ],
    "locals": {
      "data": "None",
      "files": "None",
      "method": "GET",
      "path": "ping",
      "self": "<orchestrator.agent.agent object="">",
      "url": "https://assets.frappeframework.com:443/agent/ping"
    },
    "pyver": "Python 3.8.2: /home/frappe/frappe-bench/env/bin/python (prefix: /home/frappe/frappe-bench/env)",
    "timestamp": "2021-05-06 14:08:11.940971",
    "traceback": "Traceback (most recent call last):
 File "/home/frappe/frappe-bench/env/lib/python3.8/site-packages/cryptography/fernet.py", line 119, in _verify_signature
 h.verify(data[-32:])
 File "/home/frappe/frappe-bench/env/lib/python3.8/site-packages/cryptography/hazmat/primitives/hmac.py", line 74, in verify
 ctx.verify(signature)
 File "/home/frappe/frappe-bench/env/lib/python3.8/site-packages/cryptography/hazmat/backends/openssl/hmac.py", line 75, in verify
 raise InvalidSignature("Signature did not match digest.")
cryptography.exceptions.InvalidSignature: Signature did not match digest.

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
 File "/home/frappe/frappe-bench/apps/frappe/frappe/app.py", line 68, in application
 response = frappe.api.handle()
 File "/home/frappe/frappe-bench/apps/frappe/frappe/api.py", line 54, in handle
 return frappe.handler.handle()
 File "/home/frappe/frappe-bench/apps/orchestrator/orchestrator/agent.py", line 474, in request
 headers=headers,
UnboundLocalError: local variable 'headers' referenced before assignment
"
    }

这些快照会定期与站点数据库同步,并可以通过桌面端访问,记录会添加到 DocType Error Snapshot 下。它们从创建之日起保留一个月,之后每条记录会被删除。

> 错误快照默认启用追踪。要为你站点禁用此功能,请使用 > disable_error_snapshot 更新站点配置:true

进程

服务器日志可能由 bench CLI、你的进程管理器或 Frappe 应用程序直接生成。你的进程管理器负责启动和运行 Frappe 环境的所有组件,并将输出流和错误流定向到日志文件。

除了“始终运行”的默认日志记录外,你还可以使用 Monitor 来记录更多关于站点请求的信息。

维护

如果你不注意的话,进程管理器生成的日志文件可能会随着时间的推移变得非常大。要了解你需要追踪哪些文件,请查看你 bench 上的 supervisor.conf 或 Procfile,具体取决于你运行的是生产实例还是开发实例。

旧日志的自动清理

Frappe 中的大多数日志 DocType 都支持可配置的保留期限,并带有合理的默认值。

“Log Settings” DocType 让你可以为默认的日志 DocType 配置保留期限。

你也可以通过向 DocType 控制器添加一个静态方法来将自定义日志 DocType 注册到日志设置中,如下所示。实现了此方法的自定义日志 DocType 将自动显示在 Log Settings 中。

class CustomLoggingDoctype(Document):
 @staticmethod
    def clear_old_logs(days=180):
        from frappe.query_builder import Interval
        from frappe.query_builder.functions import Now

        table = frappe.qb.DocType("Custom Logging Doctype")
        frappe.db.delete(table, filters=(table.modified < (Now() - Interval(days=days))))

更多

在生产环境中,你可能希望为 Frappe 应用程序或构成 Frappe 环境的进程记录更多信息。以下是一些你可以查阅的资源,以了解更多。

Frappe 应用程序

  • Frappe 日志 API

服务器监控

  • MySQL
  • PostgreSQL
  • NGINX

翻译

Frappe 内置了翻译系统,可以从代码中捕获可翻译的字符串,
并将其提取到不同语言的 CSV 文件中。

工作原理

翻译分为三个步骤。

1. 提取可翻译的字符串

许多字符串,如字段 label 和 description,可以很容易地从
DocType 的 JSON 文件中解析出来,它们会被自动标记为可翻译,无需任何
显式提示。

其他字符串则从源代码文件中提取,例如 .json、.js、
.py。只有当作者在 _() 或 __() 方法中
显式标记时,这些字符串才会被提取。

例如:

message = _("You don't have permissions to access this file")

2. 翻译提取出的字符串

一旦我们有了想要翻译的字符串列表,它们会按语言被放入 CSV
文件中。翻译主要由 Frappe 和 ERPNext 的各类用户在翻译门户上贡献。

提交的翻译经过验证后,每周由机器人转换为 Pull Request
并合并到核心代码中。

3. 在应用中翻译字符串

翻译本质上就是由原始字符串及其在某种语言中的译文组成的键值对。
它可能看起来像这样:

{
 "1 hour ago": "एक घंटे पहले",
 "1 minute ago": "1 मिनट पहले",
 "1 month": "1 महीना",
 "1 week": "1 सप्ताह",
 "1 year": "1 साल",
 "3 months": "3 महीने",
 "6 months": "6 महीने",
 "About": "के बारे में",
 "About Us Settings": "हमारे बारे में सेटिंग्स",
 "About Us Team Member": "हमारे बारे में टीम के सदस्य",
 "Academic Term": "शैक्षणिक शब्द",
 "Academic Year": "शैक्षणिक वर्ष",
 "Academics User": "अकादमिक उपयोगकर्ता",
 ...
}

当代码中将字符串写为 __('1 hour ago') 时,__ 方法
会查找字典,看该字符串是否存在翻译。如果存在,
就会返回翻译后的字符串并显示出来。

这是一个 JavaScript 文件中翻译如何工作的示例,但同样的
机制在 Python 中也适用。

编写有效可翻译字符串的技巧

为了让翻译解析器能够正确地从代码中提取字符串,
需要遵循一些规则。

1. 字面量字符串

要翻译的字符串必须始终是字面量字符串,不能是变量或
表达式。

Python 示例

# This will work
message = _('Document submitted successfully')
frappe.msgprint(message)

# This will also work
frappe.msgprint(_('Document submitted successfully'))

# This won't work
message = 'Document submitted successfully'
frappe.msgprint(_(message))

JavaScript 示例

// This will work
message = __('Document submitted successfully')
frappe.msgprint(message)

// This won't work
message = 'Document submitted successfully'
frappe.msgprint(__(message))

2. 变量

如果字符串中包含变量,必须使用位置格式化符
{0},其他类型的格式化符都不起作用。

Python 示例

# This is fine
_('Welcome {0}, get started with ERPNext in just a few clicks.').format(full_name)

# These are not
_('Welcome %s, get started with <a href="https://erpnext.yuannext.com">ERPNext</a> in just a few clicks.' % full_name)

_('Welcome %(name)s, get started with ERPNext in just a few clicks.' % {'name': full_name})

# This one uses the positional formatter,
# but won't work because the string is formatted before it is passed to _()
_('Welcome {0}, get started with ERPNext in just a few clicks.'.format(full_name))

JavaScript 示例

// This is fine
__('Welcome {0}, get started with ERPNext in just a few clicks.', [full_name])

// This is not
__(`Welcome ${full_name}, get started with ERPNext in just a few clicks.`)

3. 代码块

不要将字符串拆分成多个独立的字符串块再进行拼接。
不要写多行字符串。始终将字符串写在一行中,
即使字符串非常长。

Python 示例

# This is fine
_('You have {0} subscribers in your mailing list.').format(len(subscribers))

# Don't split strings
_('You have ') + len(subscribers) + _(' subscribers in your mailing list.')

# Don't write multiline strings
_('You have {0} subscribers \
 in your mailing list').format(len(subscribers))

JavaScript 示例

// This is fine
__('You have {0} subscribers in your mailing list.', [subscribers.length])

// Don't split strings
__('You have ') + subscribers.length + __(' subscribers in your mailing list.')

// Don't write multiline strings
__('You have {0} subscribers' +
 'in your mailing list', [subscribers.length])

4. 复数

不要试图用逻辑来处理单词的复数形式。每种语言都有不同的复数
形式。

# Don't do this
msg = _("You have {0} pending invoice").format(invoice_count)
if invoice_count > 1:
 msg += _("s")

# Write separate strings
# Every language has different plural forms
if invoice_count > 1:
 msg = _("You have {0} pending invoices").format(invoice_count)
else:
 msg = _("You have {0} pending invoice").format(invoice_count)

5. 不要有尾部空格

不要在句子的开头或结尾添加空格。通过翻译引擎处理时,其他语言的尾部空格会被去除。

如果需要在文本周围添加空格,可以在翻译语法之外进行。

# Don't do this
msg = _(" You have {0} pending invoice ")

# Do this
msg = ' ' + _("You have {0} pending invoices") + ' '

6. 为字符串添加上下文

一个可翻译的字符串在不同上下文中可能有不同的含义。
例如,字符串 “Change” 可以表示 “使不同” 或 “硬币”。
为了解决这个问题,可以使用 context 变量来为
字符串设置上下文,这样在翻译工具中就可以根据不同
语言进行不同的翻译。

JavaScript 中


__("Change", null, "Coins")

// Here "Coins" is the context for text "Change"

Python 中


_("Change", context="Switch")

# Here "Switch" is the context for text "Change"

添加新语言

要添加新语言,请按照以下步骤操作:

第 1 步:导出到文件

$ bench --site sitename get-untranslated [lang] [path-to-file]

第 2 步:翻译

创建另一个包含更新翻译的文件(顺序与源文件
相同)。为此,你可以使用 Google 翻译或
Bing 翻译。

第 3 步:导入你的翻译

$ bench update-translations [lang] [source-path] [translated-path]

会在每个应用的 translations 文件夹中创建一个名为 [lang].csv
的新文件。

第 4 步:更新 languages.json

在 frappe/geo/languages.json 中添加你的语言

第 5 步:提交每个应用并推送

将包含 .csv 文件的更改提交到每个应用,并推送到它们的
代码仓库。

性能分析与监控

记录器 – SQL 性能分析器

Frappe 记录器是 Frappe 框架内置的一个性能分析工具,用于捕获所有请求和后台任务,以及执行的 SQL 查询和相应的堆栈跟踪。

使用示例:你发现某个 DocType 保存时耗时过长,怀疑 SQL 查询可能是瓶颈。在这种情况下,启动记录器然后提交你的文档,就能完整捕获保存文档时触发的所有查询。

  1. 以管理员身份登录。从 Awesomebar 打开记录器,点击“开始”。
  2. 系统会要求你配置记录选项。选择适当的选项并开始记录。
  3. 执行你想要分析的操​​作(最好在另一个标签页中进行)。
  4. 捕获到足够的信息后,再次进入记录器并停止记录。

现在你将看到所有已处理和记录的请求或任务的列表。你可以按各种列进行排序,以找出有问题的请求。

点击某一行可打开该请求以查看额外信息。捕获中包含以下信息:

  1. path – 请求的路径或任务名称,例如 /app
  2. cmd – 方法的点分路径(后台任务为空)。
  3. time – 请求创建的时间。
  4. duration – 完成请求的耗时(参见下方的实现说明)。
  5. number of queries – 为完成该请求执行的 SQL 查询数量。
  6. Time in queries – SQL 查询所花费的时间。
  7. Request headers – 随请求收到的 HTTP 头(后台任务为空)。
  8. Form Dict – 随请求收到的表单数据(后台任务为空)。
  9. SQL queries – 所有已运行的 SQL 查询的表格。

要了解某个特定查询的更多信息,点击该行可展开额外信息。这包括查询的耗时、堆栈跟踪以及该查询的 SQL EXPLAIN 输出。

实现说明:记录器在捕获详细信息时会增加相当大的开销,因此总耗时并不代表真实环境下的性能。不过查询时间非常接近真实环境下的性能。

导入和导出 Frappe 记录器捕获数据

你可以导出记录器捕获数据,并将其导入到另一个站点以进行进一步分析。

  1. 进入记录器页面。记录完成后,点击菜单(三个点)> 导出。
  2. 这将下载一个包含捕获数据的 JSON 文件。要在另一个站点上查看,你可以点击导入并选择该 JSON 文件来查看捕获的数据。

使用记录器进行 Python 性能分析

除了捕获 SQL 查询外,记录器还可以运行内置的 Python 性能分析器“cProfile”,为你提供运行 Python 函数的性能分析输出。你可以在启动记录器时启用此选项。

如果你在记录器中启用 cProfiler,它会在记录期间增加更高的开销。请确保在调试完成后立即将其禁用。你也可以过滤请求/任务,以避免影响生产流量。

使用 bench 对函数进行性能分析

Bench 的 execute 命令会运行指向方法的点分路径,并且它也支持性能分析。

▶ bench --site [sitename] --profile execute erpnext.projects.doctype.task.task.set_tasks_as_overdue

现在你应该能够通过 execute 运行大多数可以通过控制台运行的命令,包括 db 方法。

▶ bench --site [sitename] execute frappe.db.get_database_size
6784

Frappe 监控器

监控器会记录请求和任务的元数据。要启用此功能,请在你的站点配置中设置 "monitor": 1。

收集到的数据会缓存在 redis 缓存中,并通过一个计划任务 frappe.monitor.flush 定期移动到 logs 目录下的 monitor.json.log 文件中。

{
    "duration": 807142,
    "request": {
        "ip": "127.0.0.1",
        "method": "GET",
        "path": "/api/method/frappe.realtime.get_user_info",
        "response_length": 9687,
        "status_code": 500
    },
    "site": "frappe.local",
    "timestamp": "2020-03-05 09:37:17.397884",
    "transaction_type": "request",
    "uuid": "83be6a4c-27a1-497a-9ce6-c815bca4e420"
}
{
    "duration": 1364,
    "job": {
        "method": "frappe.ping",
        "scheduled": false,
        "wait": 90204
    },
    "site": "frappe.local",
    "timestamp": "2020-03-05 09:37:40.124682",
    "transaction_type": "job",
    "uuid": "8225ab76-8bee-462c-b9fc-a556406b1ee7"
}

后台任务监控

Frappe 使用 RQ(Redis 队列)在后台异步执行长时间任务。你可以使用以下内置虚拟 doctype 来监控 RQ:

1. RQ Worker – 监控后台工作进程

RQ worker doctype 显示你站点上所有正在消费后台任务队列的后台工作进程。它还包含关于该工作进程的基本统计信息,如名称、时间、成功和失败的任务数量以及当前状态。

2. RQ Job – 监控和控制后台任务。

RQ Job 是一个虚拟 doctype,提供关于所有后台任务的信息。你可以按队列和状态过滤任务。

RQ 任务的表单视图显示该任务的所有信息。

调试卡住的进程

如果你认为某个工作进程卡住了,可以向它发送 SIGUSR1 信号,它会将其所有线程的当前堆栈打印到 stderr。

示例:kill -SIGUSR1 <pid></pid>

在典型的部署中,你可以在日志文件中找到写入 stderr 的输出:

  • Web 工作进程 – bench/logs/web.error.log
  • 后台工作进程 – bench/logs/worker.error.log
  • 调度器 – bench/logs/schedule.error.log

监控服务的整体健康状况

“系统健康报告”可用于快速检查系统健康状况。这包括但不限于:

  • 后台任务
  • 调度器
  • 数据库
  • 缓存
  • 电子邮件
  • 错误日志
  • 存储
  • 备份
  • 用户

在 Awesomebar 中搜索“系统健康报告”即可查看此报告。

为Frappe应用文档做贡献

Frappe 应用的文档维护在 Frappe Wiki 中。这样每位社区成员都可以为新应用贡献文档,或改进现有文档。就像 Wiki 社区一样,这里有编辑者和审核者。作为个人,你可以通过编辑新功能和改进的文档来建立自己的声誉,直到晋升为审核者角色。

贡献文档的步骤

登录和访问页面

在编辑文档之前,请确保先注册。你可以从这里登录/注册。

登录后,你将能看到编辑现有文档页面的选项。

编辑页面

在页面底部,点击省略号菜单(…)并选择 edit。

Markdown 编辑器

点击编辑按钮后,你应该会看到这样的界面

现在你可以编辑 markdown 代码,并在 `预览标签页` 中查看预览

所见即所得编辑器

如果你不想编辑 markdown,而是想通过所见即所得编辑器来编辑,请点击切换按钮并选择富文本

在 Markdown 中上传媒体文件

在 Markdown 模式下上传图片的步骤。

  1. 点击“添加附件”按钮上传图片
  2. 使用复制链接按钮将链接复制下来!
  3. 然后将链接粘贴到编辑器中以显示图片

在所见即所得编辑器中

在所见即所得模式下上传图片的步骤

  1. 你可以粘贴已复制的图片
  2. 列表项:拖放图片
  3. 使用编辑器顶部的图片按钮

提交文档

编辑完文件后,点击提交按钮,等待你的更改被提交。提交后,你将看到此界面

在此界面上,你可以跟踪所有贡献,并等待审核者审核你的更改,然后批准、拒绝或请求修改。

你可以点击打开的贡献来继续你的工作,或使用评论系统与审核者沟通。

创建新的 Wiki 页面

如果你想创建新页面而不是编辑页面,可以在左侧边栏底部找到新建页面链接。之后的流程与编辑类似。

编辑侧边栏

你还可以在编辑/新建页面时拖放侧边栏来重新排列它们(仅在必要时使用)

期待看到大量的贡献!

调试

服务器

在开发过程中运行 bench start 命令时,Procfile 中每个进程的日志都会输出到终端窗口中。

▶ bench start
14:55:17 system | redis_cache.1 started (pid=4085)
14:55:17 system | redis_socketio.1 started (pid=4086)
14:55:17 system | redis_queue.1 started (pid=4088)
14:55:17 system | web.1 started (pid=4089)
14:55:17 system | socketio.1 started (pid=4090)
14:55:17 system | watch.1 started (pid=4094)
14:55:17 system | worker_short.1 started (pid=4096)
14:55:17 system | schedule.1 started (pid=4095)
14:55:17 redis_queue.1 | 4088:C 22 May 14:55:17.257 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
14:55:17 redis_queue.1 | 4088:C 22 May 14:55:17.264 # Redis version=4.0.11, bits=64, commit=00000000, modified=0, pid=4088, just started
14:55:17 redis_queue.1 | 4088:C 22 May 14:55:17.264 # Configuration loaded
14:55:17 redis_queue.1 | 4088:M 22 May 14:55:17.265 * Increased maximum number of open files to 10032 (it was originally set to 4864).
14:55:17 redis_cache.1 | 4085:C 22 May 14:55:17.262 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
14:55:17 redis_cache.1 | 4085:C 22 May 14:55:17.268 # Redis version=4.0.11, bits=64, commit=00000000, modified=0, pid=4085, just started
14:55:17 redis_cache.1 | 4085:C 22 May 14:55:17.268 # Configuration loaded
14:55:17 redis_cache.1 | 4085:M 22 May 14:55:17.269 * Increased maximum number of open files to 10032 (it was originally set to 4864).
14:55:17 redis_socketio.1 | 4086:C 22 May 14:55:17.262 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
14:55:17 redis_socketio.1 | 4086:C 22 May 14:55:17.270 # Redis version=4.0.11, bits=64, commit=00000000, modified=0, pid=4086, just started
14:55:17 redis_socketio.1 | 4086:C 22 May 14:55:17.270 # Configuration loaded
14:55:17 redis_socketio.1 | 4086:M 22 May 14:55:17.272 * Increased maximum number of open files to 10032 (it was originally set to 4864).
14:55:17 redis_queue.1 | 4088:M 22 May 14:55:17.285 * Running mode=standalone, port=11002.
14:55:17 redis_queue.1 | 4088:M 22 May 14:55:17.285 # Server initialized
14:55:17 redis_queue.1 | 4088:M 22 May 14:55:17.286 * Ready to accept connections
14:55:17 redis_cache.1 | 4085:M 22 May 14:55:17.287 * Running mode=standalone, port=13002.
14:55:17 redis_cache.1 | 4085:M 22 May 14:55:17.292 # Server initialized
14:55:17 redis_cache.1 | 4085:M 22 May 14:55:17.292 * Ready to accept connections
14:55:17 redis_socketio.1 | 4086:M 22 May 14:55:17.294 * Running mode=standalone, port=12002.
14:55:17 redis_socketio.1 | 4086:M 22 May 14:55:17.294 # Server initialized
14:55:17 redis_socketio.1 | 4086:M 22 May 14:55:17.295 * Ready to accept connections
14:55:17 system | worker_long.1 started (pid=4098)
14:55:17 system | worker_default.1 started (pid=4100)
14:55:18 socketio.1 | listening on *: 9002
14:55:20 socketio.1 | { Error: connect ECONNREFUSED 0.0.0.0:8002
14:55:20 socketio.1 | at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1191:14)
14:55:20 socketio.1 | errno: 'ECONNREFUSED',
14:55:20 socketio.1 | code: 'ECONNREFUSED',
14:55:20 socketio.1 | syscall: 'connect',
14:55:20 socketio.1 | address: '0.0.0.0',
14:55:20 socketio.1 | port: 8002,
14:55:20 socketio.1 | response: undefined }
14:55:24 web.1 | * Running on http://0.0.0.0:8002/ (Press CTRL+C to quit)
14:55:24 web.1 | * Restarting with fsevents reloader
14:55:24 watch.1 | yarn run v1.10.1
14:55:24 watch.1 | $ node rollup/watch.js
14:55:25 web.1 | * Debugger is active!
14:55:25 web.1 | * Debugger PIN: 321-355-865
14:55:26 watch.1 |
14:55:26 watch.1 | Rollup Watcher Started
14:55:26 watch.1 |
14:55:26 watch.1 | Watching...
14:55:26 watch.1 | Rebuilding frappe-web.css
14:55:27 watch.1 | Rebuilding frappe-web-b4.css
14:55:27 watch.1 | Rebuilding chat.js
14:55:28 web.1 |
14:55:28 web.1 | test print

当你在 Python 代码中编写任何 print 语句时,如果是请求/响应相关的,它会显示在 web: 进程日志中;如果代码在后台任务中运行,则会显示在某个 worker_ 进程中。

> 如果你是 VSCode 用户,可以直接在编辑器中通过设置断点来调试代码。按照这些[步骤](在 VS Code 中调试 / 调试适配器协议)进行设置。

日志

如果你运行的是生产环境,你就更需要日志来跟踪 Frappe 环境的尽可能多的信息。

开箱即用,日志存储在 bench 中的 ./logs 文件夹下。从 Frappe 13 版本开始,日志也可以在站点级别访问,位于 ./sites/{site}/logs 下。

这些站点日志由 Frappe 应用程序创建,而许多 bench 级别的日志文件则由支持你 Frappe 环境的进程生成。查看 bench 中的 Procfile 或 supervisor.conf 以获取更多信息。

> 在此处了解更多关于 Frappe 日志的信息,在此处了解 Frappe 日志 API。

控制台

要使用 Python API,bench 提供了一个 iPython shell。运行以下命令后,它会导入 frappe、初始化它并连接到数据库。

▶ bench --site [sitename] console

In [1]: frappe.get_doc('Task', 'TASK00004')
Out[1]: <erpnext.projects.doctype.task.task.task at="" 0x10825d710="">

客户端

客户端调试就像在你的 JS 文件中添加一个 debugger 语句一样简单。你必须在浏览器中打开开发者工具,它才会在该语句处暂停。

frappe.db.get_value('Task', 'TASK00004', 'status')
    .then(values => {
        debugger
        console.log(values);
    })

控制台

要使用客户端 API,你可以打开浏览器的控制台,使用全局可用的 frappe 对象来探索和运行方法以及访问属性。

浏览器控制台

> 在此处了解更多关于客户端 API 的信息

在 VS Code 中调试 / 调试适配器协议

正常运行的检查清单。

  • 更新 Procfile
  • 获取 Visual Studio Code(废话!)
  • 为 VS Code 安装 Python 扩展
  • 更新 launch.json
  • 开始调试

注意事项:

  1. 禁用自动重载功能(不过你可以通过手动重载(⌘⇧F5)达到相同效果)
  2. 禁用 Werkzeug 多线程

1. 更新 Procfile

注意:这会修改 bench start 的行为

从 Procfile(位于 bench 目录中)注释掉一行(在其前面加上 #),该行看起来像这样。

web: bench serve --port 8000

我们将从 VS Code 运行此进程,而不是用 bench start 来运行它。

2. 更新 launch.json 在 VS Code 中为你的 launch.json 添加一个配置,应该看起来像这样(这大致上就是被移除的 Procfile 行所做的事情)。

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Bench",
            "type": "python",
            "request": "launch",
            "justMyCode": false,
            "program": "${workspaceFolder}/frappe/frappe/utils/bench_helper.py",
            "args": [
             "frappe", "serve", "--port", "8000", "--noreload", "--nothreading"
            ],
            "python": "${workspaceFolder}/../env/bin/python",
            "cwd": "${workspaceFolder}/../sites",
            "env": {
             "DEV_SERVER": "1"
            }
        }
    ]
}

给定配置中提到的路径假设你将 apps 目录作为你的工作区目录(你用 code 打开的目录)。workspaceFolder 是一个 VS Code 变量,指向(如果从名字看不出来的话)工作区目录。

你并非必须使用 apps 作为你的工作区目录,但请记得相应地修改 workspaceFolder、pythonPath 和 cwd。

3. 执行 bench start

这应该像往常一样保持运行。

4. 开始调试

VS Code -> 调试面板(⌘⇧D)-> 开始调试,或使用键盘快捷键(⌘⇧F5)。

说明

1. program 和 args

"program": "${workspaceFolder}/frappe/frappe/utils/bench_helper.py",
"args": ["frappe", "serve", "--port", "8000", "--noreload", "--nothreading"],

做的事情与 bench serve --port 8000 --noreload --nothreading 完全相同,也就是等同于

cd sites
../env/bin/python ../apps/frappe/frappe/utils/bench_helper.py frappe serve  --port 8000 --noreload --nothreading

--noreload 禁用了 Werkzeug 的自动重载功能,--nothreading 禁用了多线程。

2. cwd

"cwd": "${workspaceFolder}/../sites",

上述命令必须从 sites 目录执行。

3. env

"env": {
    "DEV_SERVER": "1"
}

bench start 创建一个环境变量 DEV_SERVER 并将其设置为 1。没有这个,Socket.io 无法正常工作(说来话长)。

仅运行 bench serve 不会设置此变量,所以你需要显式设置它。

注意:

目前,frappe 使用 use_reloader=True 和 threaded=True 运行,VS Code 调试器由于某些原因与这些功能配合不佳,Django 和 Flask 也有这个问题。