Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API 的插件框架 #413

Draft
wants to merge 14 commits into
base: master
Choose a base branch
from
Draft

API 的插件框架 #413

wants to merge 14 commits into from

Conversation

Cirn09
Copy link
Collaborator

@Cirn09 Cirn09 commented May 1, 2023

相关:#354

现在这个是第三版设计,废弃的第二版中已经实现了几个 API
https://github.com/Cirn09/qiandao/tree/api-v2

web/handlers/api/__init__.py Fixed Show fixed Hide fixed
web/handlers/api/example.py Fixed Show fixed Hide fixed
web/tpl/about.html Fixed Show fixed Hide fixed
web/tpl/about.html Fixed Show fixed Hide fixed
web/handlers/api/__init__.py Fixed Show fixed Hide fixed
web/handlers/api/example.py Fixed Show fixed Hide fixed
web/handlers/base.py Fixed Show fixed Hide fixed
@a76yyyy
Copy link
Contributor

a76yyyy commented May 6, 2023

尝试修复一些安全性的bug以及优化了Web Error的前端显示

TODO: 更新所有旧的API到v1中

web/handlers/api/example.py Outdated Show resolved Hide resolved
@Cirn09
Copy link
Collaborator Author

Cirn09 commented May 7, 2023

迁移旧 API 我来吧,编码和时间相关的都写好了

有一个想要讨论的点,现在注册 API 的方式是通过 handler: list[ApiBase],算是从旧版 API 设计上继承下来的。现在是可以做到继承 ApiBase 即注册 API,哪种比较好呢

@a76yyyy
Copy link
Contributor

a76yyyy commented May 7, 2023

迁移旧 API 我来吧,编码和时间相关的都写好了

有一个想要讨论的点,现在注册 API 的方式是通过 handler: list[ApiBase],算是从旧版 API 设计上继承下来的。现在是可以做到继承 ApiBase 即注册 API,哪种比较好呢

怎么简单怎么直观怎么来,不用太受旧版约束,未来大概率新旧版api会同时存在很长一段时间

if escape_html:
data = escape(json.dumps(data, ensure_ascii=ensure_ascii, indent=indent), quote=escape_quote)
else:
data = json.dumps(data, ensure_ascii=ensure_ascii, indent=indent).replace('</', '<\\/')
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里应该没必要进行转义,因为 Content-Type 已经设置为 application/json

更进一步,API 的返回内容默认为 text/plain 吧,避免 API 作者忘记转义(比如我);bytes 类型则 application/octet-stream;JSON application/json
应该没有 API 需要使用其他类型的需求,所以就不用允许 API 自己设置 Content-Type 了

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我的建议是默认返回JSON格式的code,message,data信息

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

既然是api就一步到位,规范一下

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这样设计的话,我觉得对应的系统对 JSON respond 的处理能力需要增强,目前系统对返回内容好像只支持正则匹配。

思考了一下好像不难实现,提供一个 respond content 的魔法变量和一个 JSON 函数应该就可以了,比如 {{ json_parse(content)['code'] }}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这样设计的话,我觉得对应的系统对 JSON respond 的处理能力需要增强,目前系统对返回内容好像只支持正则匹配。

思考了一下好像不难实现,提供一个 respond content 的魔法变量和一个 JSON 函数应该就可以了,比如 {{ json_parse(content)['code'] }}

现在的模式:
image

新UI: 类型(下拉: 简单, 正则(和旧版兼容), 代码), 来源(下拉: respond.[content, header, status]), 变量名, 规则, 结果
类型为代码时则不显示来源,可以使用 {{ respond.content }} 之类的

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

没有看到新UI的界面?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

在写了.jpg,先把方案写下来讨论一下

@a76yyyy a76yyyy force-pushed the master branch 2 times, most recently from 2f56b2f to e208928 Compare June 1, 2023 06:45
# 使用原生 tornado API,详细参考 tornado 文档
text = self.get_argument("text", "")
self.write(
escape(text)

Check warning

Code scanning / CodeQL

Reflected server-side cross-site scripting

Cross-site scripting vulnerability due to a [user-provided value](1).
traceback.print_exception(*kwargs["exc_info"])
if len(kwargs["exc_info"]) > 1:
logger_Web_Handler.debug(str(kwargs["exc_info"][1]))
self.write(data)

Check warning

Code scanning / CodeQL

Information exposure through an exception

[Stack trace information](1) flows to this location and may be exposed to an external user.
@Cirn09
Copy link
Collaborator Author

Cirn09 commented Jun 10, 2023

有个问题,less 还在用吗
有直接修改 css 的记录: f1aaace

@a76yyyy
Copy link
Contributor

a76yyyy commented Jun 10, 2023

有个问题,less 还在用吗 有直接修改 css 的记录: f1aaace

我对前端学习并不多,没有学过less,

如果可以的话,你可以考虑一下用less或者直接用css哪个合适一些,并且将合适的方案重新创建一个PR

@Cirn09
Copy link
Collaborator Author

Cirn09 commented Jun 18, 2023

有个问题,less 还在用吗 有直接修改 css 的记录: f1aaace

我对前端学习并不多,没有学过less,

如果可以的话,你可以考虑一下用less或者直接用css哪个合适一些,并且将合适的方案重新创建一个PR

qd 的前端部分太抽象了,用了一些重复的、停止维护的包,结构也有点乱。
本来打算实现一个移除 Seajs、Grunt、Bower,用 Webpack 打包,暂时保留 coffee 的临时版本,最终过渡到一个前后端分离的版本。
临时版本的半成品: https://github.com/Cirn09/qiandao/tree/webpack/web npm install && npx webpack
接着写下去的话,这个过渡版本就成了模板套模板(先前端编译模板,再 jinja 运行时模板),感觉很笨
而且接着搞下去工作量还会比较大,感觉不如直接一步到位。

因为前后端分离的设计也需要新 API,所以下面有两种方案:

  1. 前端暂且不动,先把旧 API 都在新框架下实现出来,变量提取没有 JSON 功能也不是不能用。所有新 API 都完成后再重构前端。
  2. 前后端分离的前端需要后端 API 支持,所以可以 API 和 前端一起重构

@a76yyyy
Copy link
Contributor

a76yyyy commented Jun 18, 2023

其实如果可以的话, 我的想法是在保留框架所有功能的基础上采用第二种方案, 直接做一个全新的前端或者UI

旧的前端过于冗余、陈旧和复杂了

因为前后端分离的设计也需要新 API,所以下面有两种方案:

  1. 前端暂且不动,先把旧 API 都在新框架下实现出来,变量提取没有 JSON 功能也不是不能用。所有新 API 都完成后再重构前端。
  2. 前后端分离的前端需要后端 API 支持,所以可以 API 和 前端一起重构

@Cirn09
Copy link
Collaborator Author

Cirn09 commented Jun 25, 2023

3000 预算进图吧系列:本来我只想加个 API,现在我想直接来个 qd2(

YAML 可能是最适合表示流程格式,CI/CD 基本都是使用 YAML 格式。
YAML 比现在使用的 JSON 可读性更强
前后端分离 + 易于手动编写的配置文件 = 不要前端也可以(

task.yaml 草案

# task.yaml

# code 类型是我自己定义的,该类型的值会被解释为 python 表达式进行执行,比如 1+1 会被解释为 2, '1'+'1' 会被解释为 '11'
# 使用 YAML 在 code 类型声明纯字符串是有点麻烦的, "xxx" 外围的双引号在 YAML 中会被解释为字符串的定界符,解析之后会丢掉引号。
# 在 code 中声明纯字符串有三种方法:
# 1. 双重引号:"'xxx'"
# 2. 多行字符串:
#     key: |
#         "xxx"
#    这得到的结果是 {'key': '"xxx"\n' },虽然最后多了一个换行,但是对最终值没有影响
# 3. 在 vars 中预先定义,vars 的 default 是 string 类型。
#    当一个字符串需要多次使用时,推荐这种方法。
# 4. 让引号不在最外围就可以了,str('xxx')、u'xxx'、f'xxx'
#    推荐用 u'',因为在 Python3 中 '' 和 u'' 完全没有区别

require:
  - string # 依赖的模块/插件

'on': # 不加引号会被自动转换为 bool(True),这是 YAML 特性
  corn: string # cron 风格定时,min hour day month dayOfWeek
  timer: int # 倒计时定时
  retry: # 失败后重试
    delay: int # 选项1: 固定等待时间
    delay: # 选项2: 随机等待时间
      max: int # 最大等待时间
      min: int # 最小等待时间
    max: int # 最多重试次数
  delay: int # 执行延时区间。前后范围内延迟,比如设定为 60,那么范围就是 -60~60s
             # 为什么 retry 的 delay 有两种格式,此处只有一种?因为固定的执行延迟似乎没有必要,如果能找到合适的场景,再加上

vars:
  - name: string # 变量名
    type: string # 变量类型,可选 string, int, float, bool, list, dict。默认 string
    display: bool # 是否在前端设置界面显示。默认 True
    default: string # 默认值,可选
    description: string # 描述,可选

process:
  # type: statement 时
  - type: string('statement') # 类型,默认是 statement,下面会有 if 和 loop 的格式
    name: string # 任务名
    id: string # step id,可选。其他 step 可以通过 id 访问此 step 的输出
    if: code # 条件,可选。如果设置了 if,那么只有满足条件才会执行
    url: code # url。有此项时才会执行请求、断言(assert)和输出(output),未给出时则直接执行输出(output)
    method: string # 请求方法,默认 GET。这个应该没必要设为 code 吧
    assert-success: # 成功断言,满足一条即为成功
      - status: int # 检查状态码,完全相等为真
      - match: string # 检查 response body,包含为真
      - regex: string # 正则匹配 response body
      - code: code # 代码
    assert-failure: # 失败断言,满足一条即为失败
    headers: # 请求头
      - string: code
    output: # 输出
      key: code # 输出的 key
    log: # 日志输出
      debug: code
      info: code
      warning: code
      error: code
      summary: code # 概括性输出,会在前端突出显示
    delay: int # 延迟执行,单位毫秒

  # loop 
  - type: string('loop') 
    # 等价为
    # for `iter` in `in`:
    #     then()
    iter: string # 循环
    in: string # 迭代对象
    then: # 循环体
    if: code # 条件,可选。如果不满足条件,整个语句块都不会执行
  
  # if
  - type: string('if')
    # 等价为 
    # if `if`:
    #     then()
    # else:
    #     else()
    if: code # 条件
    then: # if 语句块
    else: # else 语句块

  # while
  - type: string('while')
    # 等价为
    # while `while`:
    #     then()
    if: code # 条件
    then: # while 语句块

一个简单的例子

# 场景:用户提供账号、密码。
#      首先判断 cookie 是否存在且有效,如果有效,直接跳过,否则执行登录;
#      访问 example.com/api/get-task 获取任务列表(格式:{"tasks": [1,2,3...]};
#      根据 task id 选择访问 url,完成任务。

'on': # 执行任务
  cron: '0 12 * * *' # 每天 12:00 执行
  timer: 86400 # 每过 24 小时执行一次
  retry:
    delay: # 重试前等待 1800~3600s
      max: 3600
      min: 1800
    max: 8 # 最多重试 8 次
  delay: 60 # 随机延迟 -60~60s 执行

vars: # 变量
  - name: username
    type: string
    display: True
    default: xyz
    description: 用户名
  - name: password
    type: string
    display: True
    default: "a very strong password"
    description: 密码

  # 隐藏变量,不会在前端显示
  - name: cookie
    type: string
    display: False
    default: ""
    description: Cookie 存储
  - name: taskMap
    type: dict
    display: False
    default:
      1: 'https://example.com/api/task/1'
      2: 'https://example.com/api/task/3'
      3: 'https://example.com/api/task/9'
  - name: success-sum
    type: int
    display: False
    default: 0
    description: 成功次数,用于生成报告
  - name: urlLogin
    type: string
    display: False
    default: 'https://example.com/api/login'

  # 一些系统内置变量
  - name: __taskid__
    type: int
    description: 本任务的 ID
  - name: __proxy__
    type: string
    display: True
    description: |
      本任务使用的代理,所有除了框架 API 访问之外的请求都会使用此代理。
      如果有复杂的代理策略,可以自行声明变量,然后在代码中使用。
  - name: __token__
    type: string
    description: 本任务的临时 token,用于访问框架 API,仅对本任务有完全读写权限。
    # “临时”是一次性还是短有效期呢?
    #   一次性需要额外的数据库
    #   短有效期的短是多短呢
    #   要不要保证 token 不泄露呢(a. 只能用于访问框架 API; b. 不能被输出到日志中),还是将安全责任交给作者和管理员呢
  - name: __retry__
    type: int
    description: 本次运行是第几次重试
  # - name: __log__
  #   type: string
  #   description: 本次运行的日志,支持有限的 HTML


_36541de69: # 并不在定义中的 key,可以在这随便写引用的模板
            # 数据引用是 YAML 的特性
  - headers: &UA
      user-agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) HEICORE/49.1.2623.213 Safari/537.36
      X-User-Agent: HEICORE/49.1.2623.213
  - x: &assert
      assert-success:
        - content: {"code": 0}
        - status: 200
        - header: {"Content-Type": "application/json"}
      assert-fail:
        - code: response.status != 200


process:
  - name: 检测 cookie 有效性
    type: statement # 类型,默认是 statement,下面会有 if 和 loop 的例子
    id: cookie-check
    if: cookie != ""
    url: u'https://example.com/api/user/me'
    method: GET
    <<: *assert # 引用模板
    assert-success: # 覆盖前面 assert 模板中的部分内容
      - code: respose.status == 200 and 'username' in response.json()
    headers:
      <<: *UA
      cookie: cookie
    output:
      valid: | # YAML 中,开头的引号不能再字符串中间闭合,两种解决方法:1. 外层加上引号;2. 使用 | 或 > 多行字符串;3. u'''
        'username' in response.json()
    log:
      debug: response.json()

  - name: 登录
    id: login
    if: cookie == "" or not cookie-check.valid
    url: u'https://example.com/api/login'
    method: POST
    headers:
      <<: *UA
      content-type: application/json
    body: |
      {
        "username": username,
        "password": password
      }
    assert-success:
      - code: response.status == 200 and 'success' in response.json()
    output:
      cookie: response.cookie() # 会覆盖全局的 cookie,也可以通过 login.cookie 访问

  - name: 保存 cookie
    id: cookie-save
    if: login.success
    url: u'api://v1/var/save'
    method: POST
    headers:
      Authorization: __token__ # 和所有 API 的认证方法保持一致
    body: >
      {
        "task": __taskid__,
        "name": "cookie",
        "value": json.dump(cookie)
      }
    assert-success:
      - status: 200

  - name: 获取任务
    id: get-task
    url: u'https://example.com/api/get-task'
    headers:
      <<: *UA
      cookie: cookie
    <<: *assert
    output:
      tasks: response.json()['tasks']
    log:
      info: >
        "获取到任务:" + response.json()['tasks']

  - type: loop
    iter: taskId
    in: get-task.tasks
    then:
      - name: 执行任务
        id: task
        url: taskMap[taskId]
        headers:
          <<: *UA
          cookie: cookie
        <<: *assert
        log:
          info: f"任务 { taskId } 执行结果:{ response.json() }"
      - if: task.success
        output:
          success-sum: success-sum + 1

  - name: 生成日志
    log:
      summary: f'共有 {len(get-task.tasks)} 个任务,成功执行 {success-sum} 个任务'

@a76yyyy
Copy link
Contributor

a76yyyy commented Jun 25, 2023

3000 预算进图吧系列:本来我只想加个 API,现在我想直接来个 qd2(

YAML 可能是最适合表示流程格式,CI/CD 基本都是使用 YAML 格式。 YAML 比现在使用的 JSON 可读性更强 前后端分离 + 易于手动编写的配置文件 = 不要前端也可以(

task.yaml 草案

如果是这样的话, 需要重写一个har2yaml的前端函数, 这个可以作为一个v3的长期任务, v2的任务在于前后端分离和api重构

@Cirn09
Copy link
Collaborator Author

Cirn09 commented Jul 1, 2023

rm -rf api api_v2 api_v3, namespace.symbol is All you Need.

  • namespace.symbol 的设计完全可以取代旧的 API 式处理数据的方法
    • 原先的时间戳 API,可以通过读 time.stamp 实现
    • ……
  • namespace.symbol 的写操作可以有可选的副作用

去掉了网络访问,速度更快,错误处理更加简单,同时免去了有副作用 API 的认证问题(虽然现在还有没有副作用的 API)


完整定义(非终稿)

## code 类型是我自己定义的,该类型的值会被解释为 python 表达式进行执行,比如 1+1 会被解释为 2, '1'+'1' 会被解释为 '11'
## 使用 YAML 在 code 类型声明纯字符串是有点麻烦的, "xxx" 外围的双引号在 YAML 中会被解释为字符串的定界符,解析之后会丢掉引号。
## 'xxx' in xxx,也会因为最开头的引号在字符串中间闭合而导致 YAML 报错。
## 所以定义了 meta.ingnorePrefix,如果字符串以此开头,则会忽略此前缀,将剩余部分作为 code。
## 例如 meta.ignorePrefix: '$',那么 $1+1 会被解释为 1+1,也就是 int(2);$'1'+'1' 会被解释为 '1'+'1',也就是 str('11')
## 也可以在在 variables 中预先定义需要用到的字符串,当一个字符串需要多次使用时,推荐这种方法。
# hint: YAML 关键字:
  #   c-sequence-entry    # '-'
  # | c-mapping-key       # '?'
  # | c-mapping-value     # ':'
  # | c-collect-entry     # ','
  # | c-sequence-start    # '['
  # | c-sequence-end      # ']'
  # | c-mapping-start     # '{'
  # | c-mapping-end       # '}'
  # | c-comment           # '#'
  # | c-anchor            # '&'
  # | c-alias             # '*'
  # | c-tag               # '!'
  # | c-literal           # '|'
  # | c-folded            # '>'
  # | c-single-quote      # "'"
  # | c-double-quote      # '"'
  # | c-directive         # '%'
  # | c-reserved          # '@' '`'
# 这么一看,同时不是 YAML 和 Python 关键字就没几个
# 最终定稿可能会把 meta.ignorePrefix 固定为 $

version: integer # 版本号,用于兼容性检查

meta: # 模块的 meta 信息
  name: string # 模板名(网站名)
  author: string # 作者
  version: string # 版本
  url:
    release: string # 发布页
    update: string # 更新地址
    homepage: string # 作者主页
  description: string # 备注、描述
  ignorePrefix: string # 忽略前缀,默认为 '~'

require:
  - string # 依赖的模块

schedule: # 什么时候应该执行
  interval: # 固定间隔执行
    seconds: integer
    minutes: integer
    hours: integer
    days: integer
    weeks: integer
    months: integer
    years: integer
  cron: # cron 风格
    second: string
    minute: string
    hour: string
    day: string
    dayOfWeek: string
    week: string
    month: string
    year: string

  retry: # 失败后重试
    delay: integer # 选项1: 固定等待时间
    delay: # 选项2: 随机等待时间
      max: integer # 最大等待时间(要使用和 interval 一样的类型吗?
      min: integer # 最小等待时间
    max: integer # 最多重试次数
  delayRange: integer # 执行延时区间。前后范围内延迟,比如设定为 60,那么范围就是 -60~60s
             # 为什么 retry 的 delay 有两种格式,此处只有一种?因为固定的执行延迟似乎没有必要,如果能找到合适的场景,再加上

variables:
  # 内置变量,替代旧的变量存储和 API
  # 格式为 namespace.symbol
  # process[].output 中创建的变量的完整访问路径是 process.id.name,也可以省略 process、通过 id.name 访问(注意,优先级是 namespace > id,例如:id=global,那么通过 process.global.name 是可以正常访问的,而通过 global.name 则会访问到全局变量)
  # global 变量是全局的,访问时可省略 namespace
  # 根据定义不同,每次读变量得到的值可能不同:
  #   如每个任务的 context.taskid 都不同,不同时间读取 time.stamp 得到的值不同
  # 向 namespace 写值会将 namespace 覆盖为 variable;
  # 根据定义不同,向 namespace.symbol 写值可能会有副作用:
  #   如向 time.stamp 写值不会有任何影响,下次读 time.stamp 得到的还是当前时间戳
  #   如向 global.varname 写值会更新全局变量,下次任务启动时,读取 global.varname 得到的是新值(省略 namespace,则只会覆盖本次运行时变量,下次运行时变量仍然是旧值)
  #   
  # context.taskid
  # context.proxy
  # context.retry
  # time.
  - name: string # 变量名
    # 命名空间,在template.yaml variables 下声明的变量的命名空间强制为 global
    # 在 process[].output 中创建的变量命名空间默认为空。
    # namespace: string
    type: string # 变量类型,可选 string, integer, float, boolean, array, map。默认 string
    display: boolean # 是否在前端设置界面显示。默认 True
    default: string # 默认值,可选
    description: string # 描述,可选



cookies:
  - name: string
    default: string
    domain: string
    path: string
    # expires: string


process:
  # type: fetch
  - type: string('fetch') # 类型。类型是 fetch、simple 时可省略,下面会有 if 和 loop 的格式
    name: string # 任务名
    id: string # step id,可选。其他 step 可以通过 id 访问此 step 的输出
    when: code # 条件,可选。如果设置了 when,那么只有满足条件才会执行
    url: code # url。有此项时才会执行请求、断言(assert)和输出(output),未给出时则直接执行输出(output)
    method: string # 请求方法,默认 GET。这个应该没必要设为 code 吧
    asserts:
      success: # 成功断言,满足一条即为成功
        - status: integer # 检查状态码,完全相等为真
        - match: string # 检查 response body,包含为真
        - regex: string # 正则匹配 response body
        - code: code # 代码
      failure: # 失败断言,满足一条即为失败
    headers: # 请求头
      string: code # HTTP Header 里应该没有同个 key 多次出现的情形吧
    cookies:
      name: string # 仅用于此条请求的 cookie,会被全局 cookie 覆盖
    body: code # 请求体
    output: # 输出
      key: code # 输出的 key
    log: # 日志输出
      debug: code
      info: code
      warning: code
      error: code
      summary: code # 概括性输出,会在前端突出显示
    delay: integer # 延迟执行,单位毫秒

  # simple
  - type: string('simple') # 可省略
    name: string
    id: string
    when: string
    assert-success: ...
    assert-failure: ...
    output: ...
    log: ...

  # loop 
  - type: string('loop') 
    # 等价为
    # for `iter` in `in`:
    #     then()
    iter: string # 循环
    of: string # 迭代对象
    then: # 循环体

  # while
  - type: string('while')
    # 等价为
    # while `while`:
    #     then()
    when: code # 条件
    then: # while 语句块

  # if
  - type: string('if')
    # 等价为 
    # if `if`:
    #     then()
    # else:
    #     else()
    _if: code # 条件。
    # when 的行为是不满足时不执行该条语句,包括 else 块
    # 加下划线是为了避开 Python 的关键字。不加也可以,但会多很多麻烦
    then: # if 语句块
    _else: # else 语句块。下划线同样是为了避开关键字(otherwise 有点长
    when: 


例子

# 例子:登录
# 场景:
#   网站使用 HTTP Header Authorization token 进行认证。
#   访问 example.com/api/get-task 获取任务列表(格式:{"tasks": [1,2,3...]};
#   根据 task id 选择访问 url,完成任务。
#
#   用户提供账号、密码。
#   首先判断是否已有 Authorization token 且是否有效,如果有效,直接跳过,否则执行登录,并保存;
#      首先判断 cookie 是否存在且有效,如果有效,直接跳过,否则执行登录;
#      
#      

version: 1

meta:
  name: 测试模板
  author: Cirn09
  version: canary#1
  url:
    release: https://github.com/cirn09/qd2
    update: https://github.com/
    homepage: https://github.com/cirn09
  description: |
    测试模板
  ignorePrefix: '$'


require:


schedule: # 执行任务
  interval:
    seconds: 1
    minutes: 0
    hours: 1
  cron:
    hour: "*/1"
  retry:
    delay: # 重试前等待 1800~3600s
      max: 3600
      min: 1800
    max: 8 # 最多重试 8 次
  delayRange: 60 # 随机延迟 -60~60s 执行


variables: # 变量
  - name: username
    # type: string
    # display: True
    default: xyz
    description: 用户名
  - name: password
    default: "aVeryStr0ngPassword!"
    description: 密码

  # 隐藏变量,不会在前端显示
  - name: token
    display: False
  - name: taskMap
    type: dict
    display: False
    default:
      1: 'https://example.com/api/task/1'
      2: 'https://example.com/api/task/3'
      3: 'https://example.com/api/task/9'

  - name: success_sum # 通过 variables 创建,下面有通过 simple 语句动态创建的例子
    type: int
    display: False
    default: 0
    description: 成功次数,用于生成报告
  - name: urlLogin
    type: string
    display: False
    default: 'https://example.com/api/login'

  # 一些系统内置变量
  - name: context.taskid
    type: int
    description: 本任务的 ID
  - name: context.proxy
    type: string
    display: True
    description: |
      本任务使用的代理,所有除了框架 API 访问之外的请求都会使用此代理。
      如果有复杂的代理策略,可以自行声明变量,然后在代码中使用。
  # - name: context.token 
  #   type: string
  #   description: 本任务的临时 token,用于访问框架 API,仅对本任务有完全读写权限。
  # 设计token的本意是为了方便访问 API 保存一些数据,后续设计了新的不使用 API fetch 保存方法
  # 所以取消了内置 token,也就不用考虑下面的“临时”问题了。
    # “临时”是一次性还是短有效期呢?
    #   一次性需要额外的数据库
    #   短有效期的短是多短呢
    #   要不要保证 token 不泄露呢(a. 只能用于访问框架 API; b. 不能被输出到日志中),还是将安全责任交给作者和管理员呢
  - name: context.retry
    type: int
    description: 本次运行是第几次重试


_36541de69: # 并不在定义中的 key,可以在这随便写引用的模板
            # 数据引用是 YAML 的特性
  - headers: &UA
      user-agent: $'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) HEICORE/49.1.2623.213 Safari/537.36'
      X-User-Agent: $'HEICORE/49.1.2623.213'
  - x: &assert
      asserts:
        success:
          - match: {"code": 0}
          - status: 200
        failure:
          - code: response.status != 200


process:
  - name: 检测 token 有效性
    type: fetch # 类型,默认是 statement,下面会有 if 和 loop 的例子
    id: token_check
    when: token != ""
    url: $'https://example.com/api/user/me'
    method: GET
    <<: *assert # 引用模板
    asserts:
      success: # 覆盖前面 assert 模板中的部分内容
        - code: respose.status == 200
    headers:
      <<: *UA
      Authorization: token
    output:
      valid: $'username' in response.json()
    log:
      debug: response.json()

  - name: 登录
    id: login
    when: token == "" or not valid # 完整路径为 process.token_check.valid
    url: $'https://example.com/api/login'
    method: POST
    headers:
      <<: *UA
      content-type: application/json
    body: |
      {
        "username": username,
        "password": password
      }
    asserts:
      success:
        - code: response.status == 200 and 'success' in response.json()
    output:
      global.token: response.json()['token'] # 向 global.token 输出,即会覆盖 token,也会将 token 保存到数据库,下次运行时的 token 也会是这个值

  - name: 获取任务
    id: get_task
    url: $'https://example.com/api/get-task'
    headers:
      <<: *UA
      Authorization: token
    <<: *assert
    output:
      tasks: response.json()['tasks']
    log:
      info: >
        "获取到任务:" + response.json()['tasks']

  - output:
      failure_sum: 0 # 动态创建,上面有静态创建的例子
    # type: simple # 未提供 url 时默认为 simple

  - type: loop
    iter: taskId
    of: get_task.tasks
    then:
      - name: 执行任务
        id: task
        url: taskMap[taskId]
        headers:
          <<: *UA
          cookie: cookie
        <<: *assert
        output:
          success: response.json()['success']
        log:
          info: f"任务 { taskId } 执行结果:{ response.json() }"
      - _if: process.task.success
        then:
          output:
            success_sum: success_sum + 1
        _else:
          output:
            failure_sum: failure_sum + 1

  - name: 生成日志
    log:
      summary: f'共有 {len(get_task.tasks)} 个任务,成功执行 {success-sum} 个任务,失败 {failure_sum} 个任务。'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants