全部文档

动作与 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=scriptactions[].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,禁止例如:subprocessos.system、直接 socket/urllib/requestseval/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_iddownload_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. 在界面增加功能时,后端怎么加

步骤清单

  1. app.json · actions:增加动作 id、labelimpl: "script"params表单控件来自这里)。
  2. app.json · views:增加或修改 action_formaction 指向该 id。
  3. logic/handlers.py:增加 if action == "...": 分支;参数校验 → 业务 → 返回约定字段。
  4. 可选:复杂逻辑拆到 logic/xxx.py,handlers 只负责分发。
  5. 可选:需要历史记录 → 实体 + list;在成功/失败时用 sqlite 写 app_db
  6. 可选:多步取数 → 取数与流水线
  7. 保存后打开应用验证;若经「应用开发助手」发布,确认草案含 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 字段