动作与 handlers(界面按钮 → 代码)
读者:在界面加了动作表单后,要在包内写出对应实现的人。
来源 sdk/appsdk/动作与handlers.md
读者:在界面加了动作表单后,要在包内写出对应实现的人。 关联:应用包结构 · 取数与流水线 · 界面描述规范
1. 端到端对应关系
用户填写动作表单并提交
→ 前端 invoke(action_id, params)
→ 平台:先匹配内置动作(crud.* / ping / schema.tables)
→ 未命中:加载 logic/handlers.py,调用 handle(action, params, ctx)
→ 你的代码:取数 / 写 app.db / 生成附件
→ 返回 dict
→ 前端按约定字段展示成功、错误、打开/下载、重名候选等
原则:app.json 里出现的每个 impl=script 的 actions[].id,都应在 handle 里有分支;否则用户会看到「未知动作」或失败。
2. handlers.py 契约
2.1 必须提供的入口
def handle(action, params, ctx):
"""
action: str — 与 app.json 中 actions[].id 相同,如 "profile.generate"
params: dict — 表单提交的参数(键为 params[].name)
ctx: dict — 平台注入的上下文
返回: dict — 见 §2.3;若返回 None,平台视为 {"ok": True}
"""
...
缺少 handle → {"ok": False, "error": "missing_handle"}。
2.2 ctx 常用键
| 键 | 含义 |
|---|---|
app_db | 本应用 data/app.db 的绝对路径 |
app_dir / app_root | 本次 invoke 的包拷贝根目录(含 logic/scripts/assets/references) |
platform | 平台桥模块(取数、存附件);不要自己用 urllib/requests |
2.3 返回值:前端认识的字段
| 字段 | 用途 |
|---|---|
ok | 是否成功 |
error | 失败说明(展示给用户) |
message | 成功提示 |
upload_id | 生成的附件 id → 界面可「打开/下载」 |
html_filename | 下载文件名提示 |
warnings | 字符串列表:部分取数失败等非致命提示 |
code: "disambiguate" | 需用户消歧(如重名) |
candidates | 消歧候选项列表(结构由你定义,界面会尽量展示) |
其它业务字段可照常返回;标准渲染器可能忽略,但对话/调试有用。
2.4 安全限制(写脚本时)
沙箱会扫描 logic/、scripts/ 下的 Python,禁止例如:subprocess、os.system、直接 socket/urllib/requests、eval/exec 等。 外网与数据连接访问 只能 通过 ctx["platform"]。
3. 平台桥(ctx["platform"])
由 invoke 注入,不要放进应用包。
| 方法 | 作用 |
|---|---|
query_list(source_slug=None) | 列出当前可用的预定义查询 |
query_run(query_id, params=None, source_slug=None) | 执行预定义查询,返回含 rows 等结果 |
save_upload(filename, content, mime_type=None) | 保存附件,返回含 upload_id、download_url |
default_data_source_slug() | 默认数据连接标识(若有) |
示例:
def handle(action, params, ctx):
if action != "report.run":
return {"ok": False, "error": f"未知动作: {action}"}
name = (params.get("name") or "").strip()
if not name:
return {"ok": False, "error": "请填写名称"}
platform = ctx.get("platform")
if platform is None:
return {"ok": False, "error": "平台能力未注入,无法取数"}
raw = platform.query_run("employee_by_name", {"empName": name}) or {}
rows = (raw.get("rows") or []) if isinstance(raw, dict) else []
if not rows:
return {"ok": False, "error": f"未找到「{name}」"}
html = f"<html><body><h1>{name}</h1></body></html>"
up = platform.save_upload(f"报告-{name}.html", html, "text/html; charset=utf-8") or {}
return {
"ok": True,
"message": "已生成",
"upload_id": up.get("upload_id"),
"html_filename": up.get("filename"),
}
4. 在界面增加功能时,后端怎么加
步骤清单
app.json·actions:增加动作 id、label、impl: "script"、params(表单控件来自这里)。app.json·views:增加或修改action_form,action指向该 id。logic/handlers.py:增加if action == "...":分支;参数校验 → 业务 → 返回约定字段。- 可选:复杂逻辑拆到
logic/xxx.py,handlers 只负责分发。 - 可选:需要历史记录 → 实体 +
list;在成功/失败时用 sqlite 写app_db。 - 可选:多步取数 → 取数与流水线。
- 保存后打开应用验证;若经「应用开发助手」发布,确认草案含
handlers_py/files。
最小新增示例
app.json(节选)
{
"views": [
{ "type": "action_form", "id": "ping_box", "label": "试跑", "action": "demo.ping" }
],
"actions": [
{
"id": "demo.ping",
"label": "试跑",
"impl": "script",
"params": [
{ "name": "who", "label": "称呼", "type": "text", "required": true }
]
}
]
}
handlers.py(节选)
def handle(action, params, ctx):
params = params or {}
if action == "demo.ping":
who = (params.get("who") or "").strip() or "朋友"
return {"ok": True, "message": f"你好,{who}"}
return {"ok": False, "error": f"未知动作: {action}"}
实体仍至少保留一个(校验要求);可继续用空白记事实体 + 列表,或与动作无关的占位实体。
5. 内置动作(不必写进 handlers)
| action | 谁调用 | 说明 |
|---|---|---|
crud.list / create / update / delete | 列表/实体表单 | 标准 UI 已接好 |
ping | 探测 | 健康检查 |
schema.tables | 调试 | 列业务表 |
只有 自定义 id 才进 handlers.py。
6. 写库(历史记录)习惯写法
import sqlite3
from datetime import datetime, timezone, timedelta
def _db(ctx):
path = (ctx or {}).get("app_db") or ""
if not path:
raise RuntimeError("missing app_db")
conn = sqlite3.connect(path)
conn.row_factory = sqlite3.Row
return conn
def _now():
# 存盘一律 UTC ISO;列表展示由平台按账号显示时区换算
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def _beijing_wall(iso_utc: str) -> str:
"""报告 HTML 正文用:UTC → 北京时间墙钟。脚本读不到账号偏好。"""
dt = datetime.fromisoformat(iso_utc.replace("Z", "+00:00"))
return (dt + timedelta(hours=8)).strftime("%Y-%m-%d %H:%M:%S")
表名、列名须与 app.json 实体一致(平台生成 schema)。员工画像模板在生成成功/失败时都会登记一行,便于列表展示状态——可按业务仿照。
时区说明:
- 写库 /
created_at:用_now()(UTC)。 - 标准列表里的时间列:平台按用户「时间显示」偏好展示,脚本不必、也不该把库字段改成「假东八区字符串」。
- 报告封面/正文印刷时间:用类似
_beijing_wall写进 HTML;用户若要跟账号偏好走,须如实说明沙箱目前未注入该偏好,勿编造平台 API。 - 用户只想改全站界面时区:引导去偏好设置,不要改 handlers。
7. 常见失败原因
| 现象 | 排查 |
|---|---|
| 提交后「未知动作」 | handle 未处理该 id;或动作 id 与视图不一致 |
| 表单没有输入框 | 控件来自 actions[].params,不是 views[].fields |
| 无法取数 | 未注入 platform;或 query_id 不存在/无权限 |
| 脚本被拒 | 使用了禁止的网络/进程 API |
| 有结果但界面不能打开 | 未返回 upload_id;或列表实体无 upload_id 字段 |