Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
+10
@@ -0,0 +1,10 @@
|
||||
/target/
|
||||
/rust/target/
|
||||
core.[0-9]*
|
||||
/build/documentation-*/
|
||||
/config/setting.json
|
||||
/.release-key.*/
|
||||
/.agents/
|
||||
/.codex/
|
||||
**/__pycache__/
|
||||
**/*.pyc
|
||||
@@ -0,0 +1,36 @@
|
||||
# Dever 核心开发
|
||||
|
||||
适用于整个仓库。这里维护编译器、运行库和标准库;Dever 应用开发使用 [随包 Skill](skills/SKILL.md)。
|
||||
|
||||
## 工作规则
|
||||
|
||||
- 首条回复和最终回复称呼用户为“主人”。先读适用规则、相关源码和测试,再修改;已读且未变的文件不重复读取。
|
||||
- 落实用户已授权的工作,不用方案或记录代替实施。讨论方案时只读;安装、部署、重启、提交、推送和重写 Git 历史须在授权范围内。
|
||||
- 在 `/dm` 工作时遵守根 AGENTS。构建和程序运行使用已有开发容器,不往宿主机安装环境;保留用户改动、数据和凭据。
|
||||
- 开发阶段统一使用 `0.0.1`,没有生产数据,不做旧语法、接口、配置或数据的兼容层。公开行为改动须说明,并同步调用方、测试和文档。
|
||||
|
||||
## 实现规则
|
||||
|
||||
- 保留 Dever 语法与约束。固定路线:Dever → 具体类型 Rust → 官方 Cargo/rustc → 原生程序。不开新后端,不 fork rustc。
|
||||
- 优先复用 Rust 标准库和已有成熟库。只实现 Dever 自身规则与必要适配,不重复造解析器、调度器、网络栈或数值算法。
|
||||
- 不预建扩展点、占位层或没有调用方的抽象。函数用于消除真实重复或表达清楚的职责,不包装每次库调用。
|
||||
- 不添加无依据的判空、类型猜测、备用实现或静默兜底。外部输入在所属入口验证,内部不重复检查已保证的条件;鉴权、数据隔离、协议和资源释放必须正确。
|
||||
- 错误沿现有路径传播,保留原因,不用空值或默认成功掩盖失败。重试、锁、缓存和补偿只用于当前真实需求。
|
||||
- 每次复制、扫描、分配、加锁或调度交接都要有用途。只读优先借用,能移动就移动,不用 clone 代替所有权分析。
|
||||
- 普通调用生成直接函数调用,只有需要挂起的函数才生成异步调用。具体类型直接传递,集合直接遍历,不套动态值或中间容器。
|
||||
- 环境地址、端口、路径和凭据走既有配置;身份及数据归属来自可信上下文。不按账号、租户、测试名称或固定答案写特例。协议常量和局部算法常量可留在代码中。
|
||||
- Rust 代码写必要的中文注释,说明原因和语义差异。Dever 源码为 `.dever` 纯代码;函数用 lowerCamelCase,字段、局部值和目录用 snake_case。
|
||||
- 沿用目录结构,只保留工具包和运行库两份 Cargo 清单。每目录最多八个手写 `.rs` 文件,通常每文件 100–300 行,接近 400 行审查,超过 500 行拆分;小模块可直接放在 mod.rs。
|
||||
|
||||
## 验证与交付
|
||||
|
||||
- 先比较生成代码、运行库调用和独立 Rust 实现,找到多余操作再改。保留已有优化,不为某个性能样例添加特殊路径。
|
||||
- 性能对照使用相同语义、输入、精度、依赖和编译选项。共享 Dever 运行库的对照只检验生成层,不能证明运行库追平。
|
||||
- 提前固定轮次,保留全部原始结果及源码、二进制对应关系。报告绝对耗时、修改前后变化和对 Rust 的差距,不挑选样本或隐瞒退化。
|
||||
- 每个独立改动先做相关功能和性能验证,再继续下一项;每批结束检查相关矩阵。稳定慢 2% 及以上继续定位,约 1% 不为凑数字增加代码。
|
||||
- 新增复杂度没有稳定收益就撤回。删除等价冗余且无退化可以保留,但不冒充提速。
|
||||
- 复用现有检查和测试,不造自证逻辑、不削弱断言。已通过的检查仅在相关内容变化或证据不足时补验。
|
||||
- 说明只写当前用法和必要规则,不写对话、开发日志或占位描述。修改代码同步相关说明;调用图由工具生成,见 [说明写法](DOCUMENTATION.md)。
|
||||
- 交付前检查差异、错误处理、冗余、引用和临时文件。未执行、失败或未测项目如实说明,不以构建成功代替业务通过。
|
||||
|
||||
源码布局、构建和测试命令见 [rust/README.md](rust/README.md)。语言行为见 [LANGUAGE.md](LANGUAGE.md)。
|
||||
@@ -0,0 +1,64 @@
|
||||
# 源码与说明
|
||||
|
||||
`.dever` 放代码,每个领域使用以下阅读入口:
|
||||
|
||||
```text
|
||||
module/manage/account/
|
||||
├── README.md
|
||||
├── doc/
|
||||
│ ├── session.md
|
||||
│ └── members.md
|
||||
├── app/
|
||||
│ ├── session.dever
|
||||
│ └── members.dever
|
||||
├── domain.dever
|
||||
├── model.dever
|
||||
└── link.md
|
||||
```
|
||||
|
||||
README 用一段话介绍职责,并链接主题说明。doc 按功能组织;业务角色只创建实际需要的文件,小领域可以使用 `app.dever`。
|
||||
|
||||
## 函数说明
|
||||
|
||||
二级标题写完整符号名,正文说明代码不容易表达的业务含义:
|
||||
|
||||
```markdown
|
||||
# 登录
|
||||
|
||||
## manage.account.login
|
||||
|
||||
校验登录名和密码,成功后创建会话并设置 Cookie。凭据错误返回未授权。
|
||||
```
|
||||
|
||||
- 简单函数通常一两句即可;复杂功能再补规则、失败条件和副作用。
|
||||
- 参数含义不清楚时解释参数,不重复列出已展示的类型和签名。
|
||||
- 不写“操作参数”“业务值”等占位文字,不把同一段介绍复制给每个函数。
|
||||
- 一个逻辑函数只写一处说明,多分句共用。类型也可用完整符号名绑定说明。
|
||||
- 调用方、行号和调用关系由工具读取,不手工维护。
|
||||
|
||||
不确定符号名时执行 `dever inspect . --json`。说明标题失效或重复时,检查器会报错;文字是否符合实现仍须对照源码。
|
||||
|
||||
## 调用流程
|
||||
|
||||
```sh
|
||||
dever inspect . --links
|
||||
dever inspect . --links --check
|
||||
```
|
||||
|
||||
第一条命令生成各领域的 `link.md`,第二条检查是否过期。流程按源码求值顺序展示入口、分支、调用和任务调度,点击节点可打开源码。
|
||||
|
||||
图展示代码可能执行的路径,不是一次请求的运行记录。Port 的实际 Adapter 由配置选择。完整调用关系保留在 link 中,README 和 doc 不再复制。
|
||||
|
||||
## 修改与阅读
|
||||
|
||||
先读领域 README、相关主题和调用流程,再读源码。修改实现时同步受影响的说明与测试,重新生成调用图。
|
||||
|
||||
```sh
|
||||
dever fmt . --check
|
||||
dever check .
|
||||
dever test . --filter manage.account
|
||||
dever inspect . --links
|
||||
dever inspect . --links --check
|
||||
```
|
||||
|
||||
VS Code 插件可在悬浮中显示签名、说明和调用摘要,并跳转源码及上下游调用,见 [插件说明](editor/vscode/README.md)。源码和说明保存后刷新索引,不需要在 Dever 源码里加说明注释。
|
||||
+448
@@ -0,0 +1,448 @@
|
||||
# Dever 语言规范
|
||||
|
||||
Dever 使用静态类型、命名输出和互斥分句。应用由 API、CMD、Job 声明入口,编译为原生程序。
|
||||
|
||||
- 开始项目:[开发约定](skills/references/development.md)
|
||||
- 源码格式:[源码与说明](DOCUMENTATION.md)
|
||||
- AI 开发:[随包 Skill](skills/SKILL.md)
|
||||
- 安装:[安装指引](skills/references/install.md)
|
||||
|
||||
本文代码块是语法片段,放入对应角色的 `.dever` 文件。完整项目见 [示例](skills/references/examples.md)。
|
||||
|
||||
## 项目与命名
|
||||
|
||||
```text
|
||||
config/setting.json
|
||||
module/<组件>/<领域>/<角色>.dever
|
||||
test/<组件>/<领域>/<主题>.dever
|
||||
data/
|
||||
```
|
||||
|
||||
配置来自 `config/setting.json`。run 读取项目配置,独立程序读取可执行文件旁的 `config/setting.json`。
|
||||
|
||||
| 名称 | 格式 |
|
||||
| --- | --- |
|
||||
| 组件、领域、主题、字段、局部值 | ASCII lower_snake_case |
|
||||
| 函数、SQL 方法、标准库方法 | ASCII lowerCamelCase |
|
||||
| 类型、choice 分支 | UpperCamelCase |
|
||||
|
||||
源码使用 UTF-8 和 `.dever` 后缀,不写分号。说明放领域 README 和 doc,Markdown 不参与编译。词法支持 `#` 行注释,项目源码不写说明注释;`//` 表示整数除法,不支持块注释。
|
||||
|
||||
| 角色 | 职责与可见性 |
|
||||
| --- | --- |
|
||||
| app | 公开领域能力;可调用本领域 Domain、Model、Port 和其他领域 App |
|
||||
| domain | 本领域业务规则;不能调用其他领域或执行外部 I/O |
|
||||
| model | 字段、查询和迁移,只由本领域 App 操作 |
|
||||
| api | 绑定本领域 App,或声明自动 REST |
|
||||
| port | 外部操作的输入、输出和错误 |
|
||||
| adapter | 实现 Port,可调用本文件函数和标准能力 |
|
||||
| job | 绑定本领域 App 的持久任务 |
|
||||
|
||||
应用不声明 package、import、exposes、internal 或 main。App 函数按 `组件.领域.函数` 调用;同组件可省略组件,同领域使用 `app.函数`。跨领域只访问 App,公开参数和输出不能暴露私有类型。领域之间不得循环依赖。
|
||||
|
||||
角色可使用 `app/publish.dever` 等主题文件,主题名不进入调用名,目录允许一个文件。App/Domain/Port/Adapter/Job 的主文件与目录二选一;Model/API 主文件可与目录共存。只有 API 目录可递归,其路径参与 URL。目录不能命名为 `common/shared/utils/helper/base/service`。
|
||||
|
||||
## 类型与值
|
||||
|
||||
| 类型 | 语义 |
|
||||
| --- | --- |
|
||||
| Bool | true / false |
|
||||
| Int | 有符号 64 位整数,检查溢出 |
|
||||
| Decimal | decimal128,最多 34 位有效数字,银行家舍入 |
|
||||
| Float | IEEE 754 binary64 |
|
||||
| Text / Bytes | UTF-8 文本 / 字节序列 |
|
||||
| Id / Uuid | 不透明标识 / UUID |
|
||||
| DateTime / Date / Time / Duration | UTC 时间点、日期、日内时间、时长;毫秒精度 |
|
||||
| Json | 不透明 JSON 值,保留数字文本 |
|
||||
| Secret | 仅在规定的输入、密码和摘要操作中使用的敏感值 |
|
||||
| T? | T 或 null |
|
||||
| List<T> / Map<K,V> | 同类型列表 / 保留插入顺序的映射 |
|
||||
|
||||
整数字面量默认 Int,小数字面量默认 Decimal;明确 Float 上下文可接收小数。科学计数形式为 Float。Decimal 与 Float 不隐式混合。Text 支持反斜杠、引号、换行、回车、Tab 和 `\u{HEX}` 转义。
|
||||
|
||||
字段型 type:
|
||||
|
||||
```dever
|
||||
type User {
|
||||
name: Text
|
||||
phone: Text?
|
||||
}
|
||||
|
||||
rename(user: User, name: Text) (result: User) {
|
||||
result = user
|
||||
result.name = name
|
||||
}
|
||||
```
|
||||
|
||||
构造使用 `User { name = "Lin" phone = null }`,必须一次给全字段。字段型、choice、List、Map 使用值语义,修改局部副本不改变原值。
|
||||
|
||||
选择型 type:
|
||||
|
||||
```dever
|
||||
type State {
|
||||
Draft
|
||||
Published
|
||||
Rejected(reason: Text)
|
||||
}
|
||||
```
|
||||
|
||||
构造为 `State.Published` 或 `State.Rejected("原因")`,载荷按位置传入。
|
||||
|
||||
List 写为 `[1, 2]`,Map 写为 `{"name" = "Lin"}`。空集合需由上下文确定类型。Map 键允许 Text、Int、Bool、Id 和无载荷 choice。Map 相等比较包含插入顺序;List 按位置比较。Float 的 NaN 与自身不相等。
|
||||
|
||||
## 函数与分句
|
||||
|
||||
```dever
|
||||
greet(name: Text) (message: Text) {
|
||||
message = "Hello, " + name
|
||||
}
|
||||
```
|
||||
|
||||
函数格式为 `名称(输入) (命名输出)`,空输入或输出仍写 `()`。调用按位置传参;单输出直接得到值,多输出按名称访问。输出必须在正常结束前赋值。
|
||||
|
||||
`=` 可初始化或修改当前函数的参数、局部值和输出;变量类型保持不变,读取前须初始化。字段只能在完整值上读取或修改。
|
||||
|
||||
局部值首次声明可写 `input: CreateInput = dever.json.decode(payload)`,为解码、空集合或 null 提供具体类型;类型省略时仍由表达式推断。后续赋值只写 `input = ...`,不重复声明类型,也不改变原类型。
|
||||
|
||||
同名、同参数数量的分句须相邻,输出类型和错误声明一致。分句互不重叠并覆盖全部输入,没有先后优先级:
|
||||
|
||||
```dever
|
||||
label(state: State.Published) (text: Text) {
|
||||
text = "published"
|
||||
}
|
||||
|
||||
label(state: State.Rejected(reason)) (text: Text) {
|
||||
text = reason
|
||||
}
|
||||
|
||||
label(state: other) (text: Text) {
|
||||
text = "draft"
|
||||
}
|
||||
```
|
||||
|
||||
模式支持类型、choice 分支、null、常量、Int/Decimal 范围和 other。other 是该参数位置未被明确模式覆盖的补集;多个参数按全部组合检查。choice 载荷按位置绑定,不使用的载荷可写 `_`。
|
||||
|
||||
可空值用 `value: Text` 与 `value: null` 两个分句处理。范围可写 `score: Int >= 60 and < 90`。没有 if/else、switch/match、return、条件表达式、用户递归、lambda、运行时闭包或用户泛型。
|
||||
|
||||
## 运算与范围
|
||||
|
||||
| 运算 | 规则 |
|
||||
| --- | --- |
|
||||
| + - * | 同类数值;Int 可提升为 Decimal;Text 支持 + |
|
||||
| / | Int/Int 和 Decimal 运算返回 Decimal;Float/Float 返回 Float |
|
||||
| // % | 仅 Int,整数除法向零截断 |
|
||||
| == != | 同类型且可比较的值 |
|
||||
| < <= > >= | Int、Decimal、Float |
|
||||
| and or not | Bool;and/or 短路 |
|
||||
|
||||
优先级依次为调用/字段、一元运算、乘除取余、加减、比较、and、or。同级从左到右。
|
||||
|
||||
字段、choice 载荷、函数及 handler 参数都可声明 Int、Decimal 范围:
|
||||
|
||||
```dever
|
||||
type Port {
|
||||
value: Int >= 1 and <= 65535
|
||||
}
|
||||
```
|
||||
|
||||
构造、赋值和返回都须满足范围。未知 Int 先经范围分句确认;不支持任意用户谓词。静态可知的数值故障在检查时拒绝,运行时故障报告源码位置并终止当前执行。
|
||||
|
||||
## 集合与 handler
|
||||
|
||||
使用具名函数处理集合,不提供 for/while、break/continue。
|
||||
|
||||
| 操作 | 行为 |
|
||||
| --- | --- |
|
||||
| each(handler, values[, context]) | 零输出时遍历;单输出时生成 List |
|
||||
| filter(handler, values[, context]) | Bool handler 筛选 List |
|
||||
| find(handler, values[, context]) | 返回 List 的首个匹配项或 null |
|
||||
| sum(values) / sum(handler, values[, context]) | 数值 List 求和 |
|
||||
| reduce(handler, values, initial) | handler 输入为元素、当前状态,返回下一状态 |
|
||||
| reduceUntil(handler, values, initial) | 另输出 stop: Bool,处理当前项后停止 |
|
||||
| parallelEach(handler, values, limit[, context]) | 并行执行零输出 handler,返回前收尾全部工作 |
|
||||
| contains(values, target) | 判断 List 中是否存在相等项,直接返回 Bool |
|
||||
|
||||
List/Bytes 支持 each、reduce、reduceUntil;Bytes 元素是 0–255 的 Int。Stream/AsyncStream 的 each 只接受零输出 handler。filter/find/sum 只处理 List。除 parallelEach 外按顺序处理,空集合求和为零,reduce 返回初始状态。
|
||||
|
||||
context 最多一个,需要多项数据时用 record;reduce 的共享数据放 State。parallelEach 的非挂起并发数为 1–256,挂起任务容量为 1–65536,并受运行时容量限制。同步 Stream 不接受挂起 handler。
|
||||
|
||||
handler 是签名匹配的具名函数或 handler 参数,不是可存储、比较或返回的运行时值:
|
||||
|
||||
```dever
|
||||
transformAll(
|
||||
transform: handler(value: Int) (result: Text),
|
||||
values: List<Int>
|
||||
) (results: List<Text>) {
|
||||
results = each(transform, values)
|
||||
}
|
||||
```
|
||||
|
||||
`append`、`put`、`remove` 返回新集合;`get`、`first`、`find` 返回可空值。entries 返回有序 MapEntry 列表。length 接受 List、Map、Text、Bytes,Text 按 Unicode 码点计数。
|
||||
|
||||
## 失败、纯函数与敏感值
|
||||
|
||||
choice 的错误分支显式标记 error:
|
||||
|
||||
```dever
|
||||
type DeliveryError {
|
||||
error Unavailable(message: Text)
|
||||
}
|
||||
```
|
||||
|
||||
普通调用返回成功值,失败自动向调用方传播。`fail(DeliveryError.Unavailable("原因"))` 主动失败;`result(call)` 捕获后交给覆盖所有结果的分句处理。
|
||||
|
||||
捕获按错误类型及变体身份匹配,不按消息或载荷形状猜测。结果 Choice 的 error 分支可包装原错误 Choice,实际构造的仍是原失败变体;原错误 Choice 含有成功分支不妨碍捕获。参数求值的失败继续向外传播。
|
||||
|
||||
错误必须传播、返回或明确恢复。读取消息、记录日志、覆盖变量不算处理;恢复函数写 `recover("具体原因")`,全部分句理由一致。失败义务跟随 record、choice 和集合,局部查找不能代表处理了其余错误。程序故障不属于可捕获业务失败。
|
||||
|
||||
`pure` 写在输出声明后,禁止 I/O、时钟、随机源、并发等效果;检查沿调用链和 handler 传播。pure 函数仍可能有数值故障。
|
||||
|
||||
private 字段只能由所属源码边界访问,含私有字段的值不能直接用于公开 wire 输出。Model 私有字段仅供同领域 App 操作,跨边界返回显式 View。
|
||||
|
||||
Secret 不可比较、打印、拼接、转为普通文本、作为 Model 列或输出。密码使用 dever.crypto;哈希和验证属于阻塞操作。测试源可用 `secret(Text)` 构造固定输入,生产源码没有此能力。
|
||||
|
||||
## 并发与资源
|
||||
|
||||
挂起由调用图推断,不写 async/await。普通调用等待完成,显式 run 才启动子任务。
|
||||
|
||||
| 操作 | 合同 |
|
||||
| --- | --- |
|
||||
| run(call) | 启动 Task |
|
||||
| wait(task) | 等待并消费 Task |
|
||||
| stop(task) | 请求取消并等待退出;仅接受零输出 Task |
|
||||
| timeout(task, ms, fallback) | 到期先清理任务,再调用输出签名相同的零输入 fallback |
|
||||
| race(task, task, ...) | 取首个完成结果,清理其余任务;输出签名须相同 |
|
||||
| group(limit) / run(group, call) | 有界零输出任务组,满时等待;最终 wait/stop |
|
||||
| parallel(pure_call) | 在线程池执行非挂起纯计算 |
|
||||
| blocking(call) | 在线程池执行明确的非挂起阻塞调用 |
|
||||
| channel(T, capacity) | 有界通道;send/receive 挂起,close 唤醒等待者 |
|
||||
|
||||
Task/Group 不能复制、嵌套、传参或逃逸,必须消费一次。异常路径也会收尾子任务;取消不撤销已经发生的外部副作用。程序故障不会变成 timeout 的正常 fallback。
|
||||
|
||||
Group、Channel 容量为 1–65536。Channel 关闭后仍可读完缓存,之后 receive 返回 null。Channel 元素不能为可空类型;需要传递空值时,用含可空字段的消息 Choice 区分消息与关闭。
|
||||
|
||||
跨线程或挂起点存活的普通值须可传递。挂起调用链中的同步文件、睡眠、密码计算等操作用 blocking;网络等待和 dever.task.sleep 自动挂起。
|
||||
|
||||
File、Socket、Listener、Stream、AsyncStream 等资源的副本共享状态。普通数据的值语义不适用于这些句柄。
|
||||
|
||||
- Stream 按需单次消费,别名共享读取位置;reduceUntil 停止后可继续读取同步 Stream。
|
||||
- AsyncStream 的消费结束、提前停止、故障或取消会关闭流;别名不能继续消费。
|
||||
- `stream(channel/list/bytes)` 创建 AsyncStream;`dever.task.ticks(ms)` 创建周期流。
|
||||
- 关闭流释放其持有的资源别名,不关闭调用者另外持有的 File、Socket 或 Channel。
|
||||
|
||||
## API、身份与权限
|
||||
|
||||
API 文件只声明绑定:
|
||||
|
||||
```dever
|
||||
get profile = app.profile
|
||||
public post login = app.login
|
||||
authenticated get current = app.current
|
||||
long post generate = app.generate
|
||||
cmd export = app.export
|
||||
rest model
|
||||
```
|
||||
|
||||
HTTP 支持 get/post/put/delete。URL 为组件、领域、API 主题路径和 action;CMD 为 `组件.领域.命令`。API 绑定本领域 App 的一个可序列化输出。
|
||||
|
||||
GET/DELETE 从 query 读取标量;POST/PUT 从 JSON 对象读取;CMD 接受单个 JSON 对象。未知、重复和缺少必填字段失败。成功包裹为 `{"code":0,"message":"ok","data":...}`,失败的 data 为 null。dever.api.Error 映射 400/401/403/404/409/429,其他内部失败返回安全的 500。
|
||||
|
||||
| 声明 | 访问要求 |
|
||||
| --- | --- |
|
||||
| 默认 HTTP | 可信身份与精确角色权限 |
|
||||
| public HTTP | 匿名入口 |
|
||||
| authenticated HTTP | 可信身份;对象所有权与业务权限由 App 检查 |
|
||||
|
||||
public/authenticated 不用于 CMD 或 REST。权限键为 `component.domain.site.action`,REST 的 action 为 read/create/replace/delete。HTTP 方法不进入权限键。
|
||||
|
||||
`sites.<key>.path` 匹配 API 父目录,每条接口须恰好匹配一个站点。例如 admin 匹配 api/admin/**,不匹配 api/admin.dever,也不改写 URL。
|
||||
|
||||
认证 Provider 绑定项目的 verify App。它接收 dever.auth.Claims,返回仅含 `id: Text`、`user_id: UserModelId?`、`tenant_id: TenantModelId?` 的身份 record。verify 校验账号、会话、租户和成员关系,只读 global Model,不返回权限列表。
|
||||
|
||||
身份读取使用 dever.auth 和 dever.site,ID 类型须匹配 verify。客户端参数不能建立身份。public、CMD、Job 和普通测试不能直接读取 HTTP 身份。
|
||||
|
||||
角色与权限由核心管理。授权读取允许受保护或 authenticated 入口;修改只允许普通受保护写入口。Owner 为受保护角色,不按特定用户 ID 判断。业务 capability 在可信租户和入口命名空间内注册,撤销同时清理 grants,重新注册不恢复授权。
|
||||
|
||||
HTTP 元数据用 dever.api 读取和设置。敏感头不能读作普通 Text;Cookie 登录/退出用 issueCookie/clearCookie。Cookie 写入及 Cookie 认证的写请求要求 Origin、Host 精确匹配站点 origin。响应元数据仅在处理与编码成功后发布。
|
||||
|
||||
Upload 仅用于 POST 的顶层 multipart 参数,不能复制、嵌套、输出或进入 CMD。显式关闭,或一次性传给本领域 Port,由 Adapter 使用 storage;失败和取消清理临时文件。
|
||||
|
||||
完整配置与调用边界见 [应用指引](skills/references/application.md)和 [签名索引](skills/references/signatures.md)。
|
||||
|
||||
## Model 与查询
|
||||
|
||||
主文件的记录名对应领域,主题文件对应主题名:
|
||||
|
||||
```dever
|
||||
type Article {
|
||||
title: Text(1, 200)
|
||||
body: Text
|
||||
published: Bool default false index
|
||||
}
|
||||
```
|
||||
|
||||
自动生成只读 id 和 created_at。每个 Model 的 id 是不同的名义类型,不能用 Int 或其他 Model 的 ID 替代。
|
||||
|
||||
字段支持存储标量、无载荷 choice、Model ID 及可空形式。Text/Bytes 可指定长度,Decimal(P,S) 指定精度;generated 用于非空 Uuid。default 为静态值,index/unique 可用于单字段或顶层复合索引。外键自动建立索引,不自动加载关联。结构化数据显式使用 Json。
|
||||
|
||||
Model 的 `Text` 字段写入时默认去除首尾 Unicode 空白,再检查长度;不处理内部空白。`raw` 保留原文,`lower`/`upper` 在去除首尾空白后转换大小写,三者互斥。可空字段的 null 保持不变,空串不转为 null。普通函数中的 Text、查询条件、读取结果和原生 SQL 不自动转换。
|
||||
|
||||
```dever
|
||||
type Category {
|
||||
name: Text(1, 80)
|
||||
code: Text(1, 40) lower unique
|
||||
description: Text?
|
||||
content: Text raw
|
||||
active: Bool default true
|
||||
}
|
||||
```
|
||||
|
||||
create、update、条件更新、createMany、upsert 和 REST 使用同一写入规则。default、Seed 在编译期按该规则处理。富文本、代码、签名原文等需要逐字保留的字段使用 raw;不会依据字段名称猜测策略。去除空白不负责 HTML 安全过滤。
|
||||
|
||||
同领域 App 使用 model.create/createMany/get/first/list/cursor/count/exists/stream/update/delete/upsert。多 Model 使用 model.topic.operation。查询块只出现在调用位置,不是可保存的动态 Map:
|
||||
|
||||
```dever
|
||||
page = model.list({
|
||||
where = published == true
|
||||
order = [created_at.desc, id.desc]
|
||||
page = 1
|
||||
size = 20
|
||||
})
|
||||
```
|
||||
|
||||
where 支持布尔组合、比较、null、in、between、contains、startsWith、endsWith;结构静态确定,值参数绑定。list 返回 items/page/size/total/pages;cursor 返回 items/next/has_more,不做 count。max_page_size 限制结果规模。
|
||||
|
||||
update(id, values) 返回记录,不存在时失败;条件 update/delete 返回影响行数,必须明确 where。delete(id) 返回影响行数。关系用 with 显式加载,返回 Related 的 Unloaded/Loaded 状态;仅 first/list/cursor 支持一层关系。
|
||||
|
||||
### REST 字段
|
||||
|
||||
create/replace/search 表达式写在 Model 字段上:引用 input 时转换客户端值,否则由服务端提供并拒绝客户端同名输入。表达式只允许纯计算及指定的只读请求能力。search 最多一个,不能叫 page/size。
|
||||
|
||||
`private owner user_id: ... = dever.auth.userId()` 创建时绑定可信用户,全部读写 SQL 均包含 owner 条件。private 字段不出现在 REST 输出;没有 replace 来源时 PUT 保留其原值。
|
||||
|
||||
普通 CRUD 直接使用 rest model;关联编排、状态转换或不同输出合同由 App 实现。
|
||||
|
||||
### 事务
|
||||
|
||||
```dever
|
||||
transaction publish(id: model.id) (article: model.Article) {
|
||||
article = model.update(id, { published = true })
|
||||
}
|
||||
```
|
||||
|
||||
普通写 HTTP、CMD、Job 建立整体事务,成功编码后提交。嵌套 transaction 复用同一连接的事务;失败或取消回滚后传播原错误。GET 不写库或入队。
|
||||
|
||||
long 入口不建立整体事务,写库和入队必须进入显式 transaction。外部 I/O、Worker、等待、并发和密码计算放在事务外,写入依赖的状态在事务内重新确认。long 已提交阶段不会因后续失败撤销。
|
||||
|
||||
事务不能跨物理数据库或混用 global/tenant 存储,不暴露 begin/commit/rollback。数据库失败保留 dever.database.Error。
|
||||
|
||||
### 数据库、迁移与租户
|
||||
|
||||
Model 优先使用组件同名连接,否则 database.default;显式 `database report` 绑定固定连接,缺失时报错。SQLite/PostgreSQL 配置在 setting.json;PostgreSQL 明确选择 TLS system/disabled。
|
||||
|
||||
当前 Model 是目标 schema。空库建最终表;安全变更自动应用,收紧约束先检查数据。字段重命名用 `from old_name`;破坏性变化用命名 migrate。已执行迁移的内容与顺序不可修改。
|
||||
|
||||
migrate 可含 before/after SQL,每块提供 sqlite、postgres 和参数列表,只允许单条参数化 INSERT/UPDATE/DELETE。全新数据库不执行历史数据变换。seed 按唯一键补缺失记录,不覆盖既有数据;迁移原子性限于所属 Model。
|
||||
|
||||
复杂查询在 Model 声明 `sql name(inputs) (output)`,两种方言均为静态字符串;结果是具体 Model、私有 SQL record 或本领域 App View。只有可证明只读的 SQL 能用于 GET。
|
||||
|
||||
启用 tenant 后,global type 使用控制库,其余 Model/Job 使用租户库。只支持 database 隔离。租户库须先显式迁移;缺失、未就绪或 schema 过期时报错,不回退控制库。
|
||||
|
||||
运维入口为 `dever tenant migrate`、`dever tenant owner`、`dever tenant component enable|disable`。数据库权限和真实数据操作由部署侧管理。
|
||||
|
||||
## Job
|
||||
|
||||
```dever
|
||||
database default
|
||||
|
||||
job store(input: app.Request) () retry(3) timeout(30000) {
|
||||
app.store(input)
|
||||
}
|
||||
```
|
||||
|
||||
Job 输入为零个或一个本领域 App record,无输出,只绑定本领域 App。retry 为 1–100 次,包含首次执行;timeout 为 1–3600000 毫秒。定时声明为 `schedule name = "0 * * * *"`,使用 UTC。
|
||||
|
||||
App 使用 dever.job.enqueue/enqueueAt,传入任务、可选 payload 和幂等 key。key 为 1–1024 字节;同租户、任务身份和 key 的活跃任务复用 Id。入队与业务写入可共享事务。
|
||||
|
||||
任务至少执行一次,外部副作用须业务幂等。受保护 HTTP 入队保存可信身份与精确入口权限键,执行时重验;public/authenticated 不能入队角色权限绑定的任务。CMD、schedule、测试使用 System 身份。身份失效、坏 payload 或不兼容 schema 进入 blocked。
|
||||
|
||||
runtime.mode 为 api/worker/all。任务配置包括 workers、poll_ms、lease_ms 和重试间隔;SQLite 的 lease 必须长于 Job timeout,worker 连接池需留出监管连接。停止时先停止接收和 claim,再按 shutdown_ms 排空。
|
||||
|
||||
测试使用临时数据库和用例独立时钟,可用 dever.test.advanceClock、drainJobs 验证队列。
|
||||
|
||||
## Port、Adapter 与依赖
|
||||
|
||||
Port 声明输入、命名输出和明确失败集合:
|
||||
|
||||
```dever
|
||||
send(message: Text) () fails app.DeliveryError
|
||||
```
|
||||
|
||||
Adapter 用 `port.send(...) (...) { ... }` 实现。每个文件完整实现一个 Port;单实现自动绑定,多实现由 setting.json 的 adapter 配置选择。实现只能抛出 Port 声明的错误。Port 不接受 handler 或数值范围参数。
|
||||
|
||||
Adapter 可声明 setting,选中的实现从配置读取同类型字段。外部程序用 external command/exec/pip/npm/go;代码可信性、网络、文件和设备权限由运行环境负责,allow 声明不是 OS 沙箱。
|
||||
|
||||
| 方式 | 用途 |
|
||||
| --- | --- |
|
||||
| external command | 普通 ELF;argv/stdin 输入,code/stdout/stderr 输出,无 shell |
|
||||
| external exec | 遵循 Port Worker 协议的程序 |
|
||||
| external pip/npm/go | 由相应生态实现 Port,使用锁定资源 |
|
||||
|
||||
外部调用和 fake 用法见 [依赖](skills/references/libraries.md)、[外部库示例](skills/references/external-lib.md)。协议与运行资源见 [SDK](sdk/README.md)。
|
||||
|
||||
Dever Package 是 module 下的可复用组件,使用 `dever package add/update/remove/list/doctor/export` 管理。dever.lock 固定版本、来源和摘要,不能覆盖本地同名组件。已有锁通过 `dever lib install` 恢复资源;run/build 消费锁定资产。当前 CLI 不提供 Lib add/update 或 target add。
|
||||
|
||||
## 标准库与数据格式
|
||||
|
||||
标准库按包名直接调用,无需 import。功能目录见 [标准库](lib/README.md),参数和输出见 [签名索引](skills/references/signatures.md)。
|
||||
|
||||
### JSON 数据
|
||||
|
||||
API/CMD 根据参数和输出类型编解码。Decimal/Id/Uuid 使用字符串,ModelId 为整数,日期时间使用规范字符串,Duration 为整数毫秒。支持 List、可空、可公开传输的 record 和无载荷、无 error 分支的 Choice。此类 Choice 按声明的分支名编码为 JSON 字符串,也可作 query 或 multipart 标量;未知分支报错。带载荷或 error 分支的 Choice、Map、资源不用于普通 wire。缺少可空字段得到 null,未知、重复和缺少必填字段失败。
|
||||
|
||||
已有 Json 需要转换成具体类型时,使用 `dever.json.decode(value)`;目标类型由局部声明、命名输出、字段或参数类型确定。直接从已有原文解码,不转为 Text 或 Document。解码失败传播 `dever.json.JsonResult.Failed`。Text 不在此处清洗。
|
||||
|
||||
需要处理解码失败时,直接写 `decoded: DecodeResult = result(dever.json.decode(payload))`。DecodeResult 必须有一个含单个目标类型字段的成功分支,以及包装 dever.json.JsonResult 的 error 分支,例如 `Parsed(value: CreateInput)` 和 `error Invalid(cause: dever.json.JsonResult)`。不需要另写解码函数;无类型上下文时不会猜测目标类型。
|
||||
|
||||
单个布尔条件失败使用 `dever.api.require(condition, dever.api.Error.Invalid)`;false 传播指定错误,true 继续。该函数不代替业务状态分支和执行时的权限校验。
|
||||
|
||||
日期时间使用 RFC3339,输出 UTC,最多毫秒精度;Date 为 YYYY-MM-DD,Time 为 HH:MM:SS[.sss]。无效日期、非有限 Float、越界整数失败。
|
||||
|
||||
Json 提供 fromText/toText、object/array、propertyValue/setProperty 等操作;encode 按实参具体类型输出 Json。原始数字不经过 Float。Json 限制为 16 MiB、64 层、65536 个值。
|
||||
|
||||
需要遍历节点时使用 parse/stringify 和 Document:root 为根 ID,nodes 为连续节点列表,子 ID 小于父 ID。Number 保留原文,Object 保留键顺序。node/property/element 缺失返回 null;JSON null 是实际节点。Document 的解析深度上限为 256,stringify 检查手工节点引用。
|
||||
|
||||
### 文件、HTTP 与流
|
||||
|
||||
文件 create 只建新文件,read 可能短读。I/O 错误通过声明失败传播;流的终止错误最多产生一次,之后结束。TCP 支持同时读写,每个方向串行;部分写入后失败关闭连接,不重发整条消息。
|
||||
|
||||
HTTP 支持 HTTP/1、HTTP/2、TLS、连接池、流式响应、SSE 与 HTTP/1 WebSocket。普通应用使用 API 声明,底层服务和客户端放 Adapter。
|
||||
|
||||
- Limits 控制头、正文、期限和连接数;HTTP/2 另限制流数与接收窗口。
|
||||
- serve/send 缓冲完整正文;serveLive/open/openStream 按块处理。
|
||||
- 池和会话归创建它们的任务作用域,响应须消费或关闭后释放名额。
|
||||
- TLS 校验证书和名称,不自动降级;HTTP/2 使用 ALPN 或明文 prior knowledge。
|
||||
- HTTP 协议头和正文定界由引擎管理;应用不写 Content-Length/Transfer-Encoding/Upgrade。
|
||||
- 关闭 Listener 停止接收并排空;stop 取消服务并等待清理。
|
||||
- 不支持 CONNECT、非空 trailer、服务器推送、WebSocket-over-HTTP/2、WebSocket 子协议或压缩扩展。
|
||||
|
||||
## 检查、测试与构建
|
||||
|
||||
```sh
|
||||
dever new my_app
|
||||
dever fmt my_app --check
|
||||
dever check my_app
|
||||
dever test my_app --filter hello.greeting
|
||||
dever run my_app -- hello.greeting.greet '{"name":"Dever"}'
|
||||
dever build my_app --output my_app/program
|
||||
```
|
||||
|
||||
check 检查源码、配置、类型规则和生产入口可达性,不启动应用。重复实现和业务合理性仍需代码审查。
|
||||
|
||||
测试入口把文件主题转成 lowerCamelCase,例如 account_login.dever 对应 accountLogin() (),可调用本领域 App/Domain。外部 Port 使用用例独立 fake;Model 使用临时 SQLite。筛选不跳过源码检查,每例独立进程,默认 60 秒;任一失败返回非零。
|
||||
|
||||
`dever api <root> --output <新文件>` 导出接口快照。已有 dever.api 时,check/run/build 检查基线;变更需审查新快照,工具不自动覆盖。
|
||||
|
||||
构建输出须是新文件。生成的程序可脱离开发包运行,仍需配套配置和系统运行库。
|
||||
|
||||
安装环境与交叉编译见 [安装说明](build/README.md)和 [编译器开发](rust/README.md)。
|
||||
@@ -0,0 +1,57 @@
|
||||
# Dever
|
||||
|
||||
Dever 用于开发 HTTP 服务和命令行程序。源码使用 `.dever`,编译成原生二进制,运行时不需要 Dever 或 Rust 开发工具。
|
||||
|
||||
当前版本:`0.0.1`。
|
||||
|
||||
## 安装
|
||||
|
||||
```sh
|
||||
curl -fsSL https://git.dever.cc/dever/core/releases/download/v0.0.1/install | bash
|
||||
```
|
||||
|
||||
支持 Linux x86_64、glibc 2.41 及以上,已在 Debian 13 验证。安装器会后台准备编译环境;准备期间可以创建项目、编辑和检查代码,首次测试、运行或构建会等待准备完成。
|
||||
|
||||
CI 安装可加 `--wait`。离线包和 VS Code 插件见 [发布页](https://git.dever.cc/dever/core/releases/tag/v0.0.1),详细步骤见 [安装说明](build/README.md)。
|
||||
|
||||
## 开始使用
|
||||
|
||||
```sh
|
||||
dever new hello
|
||||
dever check hello
|
||||
dever test hello
|
||||
dever run hello -- hello.greeting.greet '{"name":"Dever"}'
|
||||
dever build hello --output ./hello-app
|
||||
./hello-app hello.greeting.greet '{"name":"World"}'
|
||||
```
|
||||
|
||||
`dever new` 生成源码、测试和说明。开发服务、数据库和后台任务可参考 [完整示例](examples/README.md)。
|
||||
|
||||
## 主要功能
|
||||
|
||||
- 静态类型、命名输出、函数分句和有序集合。
|
||||
- HTTP/1、HTTP/2、TLS、SSE、WebSocket 和文件上传。
|
||||
- Model、REST、SQLite/PostgreSQL、事务和租户数据库。
|
||||
- 认证、角色权限、持久化 Job 和并发任务。
|
||||
- Python、JavaScript、Go 外部组件。
|
||||
- VS Code 悬浮说明、源码跳转和自动调用图。
|
||||
|
||||
## 文档
|
||||
|
||||
| 内容 | 入口 |
|
||||
| --- | --- |
|
||||
| 语法与语言规则 | [语言规范](LANGUAGE.md) |
|
||||
| 项目结构与开发方法 | [开发指引](skills/references/development.md) |
|
||||
| 源码说明与调用流程 | [说明写法](DOCUMENTATION.md) |
|
||||
| 标准库 | [功能目录](lib/README.md) |
|
||||
| 示例项目 | [CMS](examples/README.md)、[入门示例](skills/references/examples.md) |
|
||||
| VS Code | [插件说明](editor/vscode/README.md) |
|
||||
| AI 开发 | [Dever Skill](skills/SKILL.md) |
|
||||
| 编译器开发 | [Rust 源码与测试](rust/README.md) |
|
||||
| 性能测试 | [测试方法](test/performance/README.md) |
|
||||
|
||||
## 发布环境
|
||||
|
||||
安装包附 SHA-256,当前未签名。ARM64 和 ESP32 尚未完成设备验证。
|
||||
|
||||
生产容器只需放入业务二进制、配置和所需资源,不需要开发包。首次构建会编译依赖,后续复用缓存。
|
||||
@@ -0,0 +1,64 @@
|
||||
# 安装 Dever
|
||||
|
||||
支持 Linux x86_64、glibc 2.41 及以上,已在 Debian 13 验证。应用开发只需安装 Dever,编译环境由安装包管理。
|
||||
|
||||
## 在线安装
|
||||
|
||||
```sh
|
||||
curl -fsSL https://git.dever.cc/dever/core/releases/download/v0.0.1/install | bash
|
||||
```
|
||||
|
||||
默认安装到 `~/.dever`。重新打开终端,或执行安装器打印的命令加载 PATH。
|
||||
|
||||
安装器先提供 Dever 命令,再后台下载编译环境。准备期间可使用 `new/fmt/check`;首次 `run/test/build` 会等待准备完成。下载中断后再次安装或构建即可继续。首次构建还会编译依赖,后续使用缓存。
|
||||
|
||||
CI 需要等待完整环境就绪:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://git.dever.cc/dever/core/releases/download/v0.0.1/install | bash -s -- --wait
|
||||
```
|
||||
|
||||
| 选项 | 用途 |
|
||||
| --- | --- |
|
||||
| `--prefix <目录>` | 指定安装目录 |
|
||||
| `--no-modify-path` | 不修改终端启动文件 |
|
||||
| `--wait` | 等待编译环境就绪 |
|
||||
| `--release-url <地址>` | 使用自托管发行资源 |
|
||||
|
||||
需要 Bash、支持并行下载的 curl、tar、xz、sha256sum、flock、getconf 和 nohup。`RUSTUP_DIST_SERVER` 可指定受信任的工具链镜像。
|
||||
|
||||
同版本安装会复用已有文件。验证重新打包的 `0.0.1` 时使用新的 `--prefix` 目录。安装不覆盖项目、系统编译器或用户 Cargo 配置。
|
||||
|
||||
## 离线安装
|
||||
|
||||
从 [发布页](https://git.dever.cc/dever/core/releases/tag/v0.0.1) 下载完整开发包及校验文件,在可写目录解压:
|
||||
|
||||
```sh
|
||||
sha256sum -c dever-0.0.1-linux-x86_64-development.tar.xz.sha256
|
||||
tar -xf dever-0.0.1-linux-x86_64-development.tar.xz
|
||||
export PATH="$PWD/dever/bin:$PATH"
|
||||
dever new hello
|
||||
dever test hello
|
||||
dever run hello -- hello.greeting.greet '{"name":"Dever"}'
|
||||
```
|
||||
|
||||
完整包包含编译工具和离线依赖,无需另外安装 Rust、Cargo 或 GCC。外部 Python/JavaScript/Go 组件仍需准备它们各自的运行资源。
|
||||
|
||||
## 缓存与排错
|
||||
|
||||
在线安装的共享工具在 `toolchains/`,依赖缓存在 `cache/cargo/`;离线包使用包内 `toolchain/`。项目编译缓存位于安装包的 `rust/target/`。
|
||||
|
||||
环境准备失败时,命令会给出 `prepare.log` 路径;构建错误给出 `build.log` 路径。失败返回非零状态,修复后重试原命令。
|
||||
|
||||
安装包附 SHA-256,当前未签名。包中保留第三方许可证和工具版本记录。
|
||||
|
||||
## 交付程序
|
||||
|
||||
```sh
|
||||
dever build hello --output ./hello-app
|
||||
./hello-app hello.greeting.greet '{"name":"Dever"}'
|
||||
```
|
||||
|
||||
部署只携带业务二进制、配置和必要资源;目标系统仍须具备匹配的系统运行库。安装包可以搬到新目录,移动后可能需要重新编译缓存。
|
||||
|
||||
编译器作者组装安装包的方法见 [源码开发](../rust/README.md)。
|
||||
@@ -0,0 +1,280 @@
|
||||
#!/usr/bin/env python3
|
||||
"""在 Debian 13 开发容器内组装 Linux x86_64 完整离线开发包。"""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tomllib
|
||||
|
||||
|
||||
SOURCE = Path(__file__).resolve().parents[1]
|
||||
TRIPLE = "x86_64-unknown-linux-gnu"
|
||||
PACKAGES = (
|
||||
"gcc-14-x86-64-linux-gnu", "cpp-14-x86-64-linux-gnu", "libgcc-14-dev",
|
||||
"libc6-dev", "linux-libc-dev", "binutils-x86-64-linux-gnu", "libc6", "libgcc-s1",
|
||||
)
|
||||
|
||||
|
||||
def native_input(path):
|
||||
"""只保留原生 C 构建所需工具;Rust LTO 使用包内 LLVM,不需要 GCC LTO。"""
|
||||
name = str(path)
|
||||
if name.startswith("/usr/include/"):
|
||||
return True
|
||||
if path.parent == Path("/usr/bin"):
|
||||
return path.name in {
|
||||
"x86_64-linux-gnu-gcc-14", "x86_64-linux-gnu-as",
|
||||
"x86_64-linux-gnu-ld", "x86_64-linux-gnu-ar", "x86_64-linux-gnu-ranlib",
|
||||
}
|
||||
if name.startswith("/usr/libexec/gcc/"):
|
||||
return path.name in {"cc1", "collect2", "liblto_plugin.so", "lto-wrapper"}
|
||||
if name.startswith("/usr/lib/gcc/"):
|
||||
return "/include/" in name or path.name in {
|
||||
"libgcc.a", "libgcc_eh.a", "libgcc_s.so", "crtbegin.o", "crtbeginS.o",
|
||||
"crtbeginT.o", "crtend.o", "crtendS.o",
|
||||
"liblto_plugin.so",
|
||||
}
|
||||
return path.parent in {Path("/usr/lib/x86_64-linux-gnu"), Path("/usr/lib64")}
|
||||
|
||||
|
||||
def output(*arguments, **options):
|
||||
return subprocess.check_output(arguments, text=True, **options).strip()
|
||||
|
||||
|
||||
def digest(path):
|
||||
with path.open("rb") as stream:
|
||||
return hashlib.file_digest(stream, "sha256").hexdigest()
|
||||
|
||||
|
||||
def copy(source, destination):
|
||||
"""只解引用作者明确选定的工具文件,不复制 Cargo home 或用户配置。"""
|
||||
if source.is_dir():
|
||||
shutil.copytree(source, destination, dirs_exist_ok=True)
|
||||
else:
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(source, destination)
|
||||
destination.chmod(0o755 if os.access(source, os.X_OK) else 0o644)
|
||||
|
||||
|
||||
def script(path, body):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text('#!/bin/sh\nset -eu\ntools=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)\n' + body)
|
||||
path.chmod(0o755)
|
||||
|
||||
|
||||
def prepare(compiler, work):
|
||||
"""两种交付方式共用现有本地打包入口,不另做编译器发行格式。"""
|
||||
work.mkdir()
|
||||
version = tomllib.loads((SOURCE / "rust/Cargo.toml").read_text())["workspace"]["package"]["version"]
|
||||
rust_version = tomllib.loads((SOURCE / "rust/rust-toolchain.toml").read_text())["toolchain"]["channel"]
|
||||
author = work / "author"
|
||||
for path in (
|
||||
"rust/Cargo.toml", "rust/Cargo.lock", "rust/rust-toolchain.toml", "rust/src",
|
||||
"lib", "skills", "LANGUAGE.md", "DOCUMENTATION.md",
|
||||
):
|
||||
copy(SOURCE / path, author / "source" / path)
|
||||
copy(compiler, author / "bin/dever")
|
||||
(author / "config").mkdir()
|
||||
(author / "config/setting.json").write_text(json.dumps({
|
||||
"format": "dever-local-bootstrap-input-v1", "version": version,
|
||||
"target": "linux-x86_64", "source": "source",
|
||||
"core": {"source": "bin/dever", "sha256": digest(compiler)},
|
||||
}))
|
||||
bundle = work / "dever"
|
||||
subprocess.run([str(compiler), "release", str(author), "--output", str(bundle)], check=True)
|
||||
return bundle, version, rust_version
|
||||
|
||||
|
||||
def record(bundle, paths):
|
||||
"""把交付附带文件纳入原有清单校验。"""
|
||||
manifest = json.loads((bundle / "bootstrap.json").read_text())
|
||||
for path in sorted(paths):
|
||||
if path.is_symlink():
|
||||
raise RuntimeError(f"staged artifact must be a regular file: {path}")
|
||||
if path.is_file():
|
||||
manifest["artifacts"].append({
|
||||
"path": path.relative_to(bundle).as_posix(), "size": path.stat().st_size,
|
||||
"sha256": digest(path), "executable": os.access(path, os.X_OK),
|
||||
})
|
||||
manifest["artifacts"].sort(key=lambda artifact: artifact["path"])
|
||||
(bundle / "bootstrap.json").write_text(json.dumps(manifest, indent=2) + "\n")
|
||||
return manifest
|
||||
|
||||
|
||||
def native_tools(tools):
|
||||
"""保留 Debian 工具的相对安装布局;glibc 只进链接 sysroot,不覆盖宿主加载器。"""
|
||||
base = {str(Path(path).resolve()) for path in output("dpkg-query", "-L", "libc6").splitlines()}
|
||||
packages = set(PACKAGES)
|
||||
executables = []
|
||||
for package in PACKAGES:
|
||||
for name in output("dpkg-query", "-L", package).splitlines():
|
||||
path = Path(name)
|
||||
if not path.is_file() or not native_input(path):
|
||||
continue
|
||||
copy(path, tools / "native" / path.relative_to("/"))
|
||||
# Debian 的链接脚本使用 /lib;发行包展开 usrmerge 别名,不改写官方库文件。
|
||||
if package == "libc6":
|
||||
copy(path, tools / "native" / path.relative_to("/usr"))
|
||||
if os.access(path, os.X_OK):
|
||||
with path.open("rb") as stream:
|
||||
if stream.read(4) == b"\x7fELF":
|
||||
executables.append(path)
|
||||
for name in ("as", "ld", "ar"):
|
||||
copy(Path(f"/usr/bin/x86_64-linux-gnu-{name}"), tools / "native/usr/bin" / name)
|
||||
libraries = set()
|
||||
for executable in executables:
|
||||
listing = subprocess.run(["ldd", str(executable)], text=True, capture_output=True)
|
||||
if listing.returncode and "not a dynamic executable" not in listing.stderr + listing.stdout:
|
||||
raise RuntimeError(f"ldd {executable}: {listing.stderr}{listing.stdout}")
|
||||
for line in listing.stdout.splitlines():
|
||||
if "=> not found" in line:
|
||||
raise RuntimeError(line)
|
||||
fields = line.split()
|
||||
if "=>" in fields and fields[2].startswith("/"):
|
||||
library = Path(fields[2])
|
||||
if str(library.resolve()) not in base:
|
||||
libraries.add(library)
|
||||
for library in sorted(libraries):
|
||||
copy(library, tools / "lib" / library.name)
|
||||
owner = output("dpkg-query", "-S", str(library.resolve())).splitlines()[0].split(": ", 1)[0]
|
||||
packages.add(owner)
|
||||
copy(Path("/usr/lib/x86_64-linux-gnu/libgcc_s.so.1"), tools / "lib/libgcc_s.so.1")
|
||||
packages.add("libgcc-s1")
|
||||
for package in sorted(packages):
|
||||
name = package.split(":", 1)[0]
|
||||
copy(Path("/usr/share/doc") / name / "copyright", tools / "licenses" / name / "copyright")
|
||||
copy(Path("/usr/share/common-licenses"), tools / "licenses/common")
|
||||
(tools / "native-packages.txt").write_text(output("dpkg-query", "-W", *sorted(packages)) + "\n")
|
||||
script(tools / "bin/cc", '''export LD_LIBRARY_PATH="$tools/lib"
|
||||
exec "$tools/native/usr/bin/x86_64-linux-gnu-gcc-14" \\
|
||||
--sysroot="$tools/native" \\
|
||||
-B"$tools/native/usr/lib/gcc/x86_64-linux-gnu/14/" \\
|
||||
-B"$tools/native/usr/libexec/gcc/x86_64-linux-gnu/14/" \\
|
||||
-B"$tools/native/usr/bin/" \\
|
||||
-B"$tools/native/usr/lib/x86_64-linux-gnu/" "$@"
|
||||
''')
|
||||
|
||||
|
||||
def trim_vendor(vendor, version):
|
||||
"""开发包编译业务程序;按 Cargo 完整依赖图保留运行库的全部特性和平台依赖。"""
|
||||
metadata = json.loads(output("cargo", f"+{version}", "metadata", "--locked", "--offline",
|
||||
"--all-features", "--format-version", "1", cwd=SOURCE / "rust"))
|
||||
dependencies = {node["id"]: node["dependencies"] for node in metadata["resolve"]["nodes"]}
|
||||
pending = [package["id"] for package in metadata["packages"] if package["name"] == "dever-runtime"]
|
||||
required = set()
|
||||
while pending:
|
||||
package = pending.pop()
|
||||
if package not in required:
|
||||
required.add(package)
|
||||
pending.extend(dependencies[package])
|
||||
for package in metadata["packages"]:
|
||||
if package["source"] and package["id"] not in required:
|
||||
directory = vendor / f'{package["name"]}-{package["version"]}'
|
||||
# 编译器二进制仍使用这些依赖;源码可移除,第三方许可必须随包保留。
|
||||
licenses = {path for path in directory.rglob("*") if path.is_file()
|
||||
and path.name.upper().startswith(("LICENSE", "COPYING", "COPYRIGHT", "NOTICE", "AUTHORS"))}
|
||||
if package["license_file"]:
|
||||
licenses.add(directory / package["license_file"])
|
||||
for path in licenses:
|
||||
copy(path, vendor.parent / "licenses/cargo" / path.relative_to(vendor))
|
||||
shutil.rmtree(directory)
|
||||
|
||||
|
||||
def rust_tools(tools, version, vendor):
|
||||
rust = Path(output("rustc", f"+{version}", "--print", "sysroot"))
|
||||
if f"host: {TRIPLE}" not in output("rustc", f"+{version}", "-vV"):
|
||||
raise RuntimeError(f"this package requires the official {TRIPLE} toolchain")
|
||||
for name in ("cargo", "rustc"):
|
||||
copy(rust / "bin" / name, tools / "rust/bin" / name)
|
||||
for library in (rust / "lib").glob("*.so*"):
|
||||
if not library.is_symlink():
|
||||
copy(library, tools / "rust/lib" / library.name)
|
||||
for name in ("lib", "bin"):
|
||||
copy(rust / "lib/rustlib" / TRIPLE / name, tools / "rust/lib/rustlib" / TRIPLE / name)
|
||||
# 只裁剪已链接的工具;标准库 rlib/rmeta 中的符号、类型元数据与位码仍用于编译。
|
||||
binaries = [*(tools / "rust/bin").iterdir(), *(tools / "rust/lib").glob("*.so*")]
|
||||
binaries.extend(path for path in (tools / "rust/lib/rustlib" / TRIPLE / "bin").rglob("*")
|
||||
if path.is_file())
|
||||
for binary in binaries:
|
||||
# 官方 .so 目录也含链接脚本,按原样保留,只处理 ELF 文件。
|
||||
with binary.open("rb") as stream:
|
||||
if stream.read(4) == b"\x7fELF":
|
||||
subprocess.run(["strip", "--strip-unneeded", str(binary)], check=True)
|
||||
copy(rust / "share/doc/rust", tools / "licenses/rust")
|
||||
if vendor:
|
||||
copy(vendor, tools / "vendor")
|
||||
else:
|
||||
subprocess.run([
|
||||
"cargo", f"+{version}", "vendor", "--locked", "--offline", "--versioned-dirs",
|
||||
str(tools / "vendor"),
|
||||
], cwd=SOURCE / "rust", check=True)
|
||||
trim_vendor(tools / "vendor", version)
|
||||
config = tools / ".cargo/config.toml"
|
||||
config.parent.mkdir()
|
||||
config.write_text('''[source.crates-io]
|
||||
replace-with = "vendored-sources"
|
||||
|
||||
[source.vendored-sources]
|
||||
directory = "vendor"
|
||||
''')
|
||||
script(tools / "bin/cargo", '''export CARGO_HOME="$tools/.cargo"
|
||||
export RUSTC="$tools/rust/bin/rustc"
|
||||
export CC="$tools/bin/cc"
|
||||
export AR="$tools/native/usr/bin/x86_64-linux-gnu-ar"
|
||||
export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER="$CC"
|
||||
export LD_LIBRARY_PATH="$tools/lib"
|
||||
export PATH="$tools/native/usr/bin:/usr/bin:/bin"
|
||||
exec "$tools/rust/bin/cargo" --config "$tools/.cargo/config.toml" "$@"
|
||||
''')
|
||||
script(tools / "bin/rustc", '''export LD_LIBRARY_PATH="$tools/lib"
|
||||
exec "$tools/rust/bin/rustc" "$@"
|
||||
''')
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--compiler", required=True, type=Path)
|
||||
parser.add_argument("--work", required=True, type=Path, help="new staging directory")
|
||||
parser.add_argument("--output", required=True, type=Path, help="new .tar.xz or .tar.gz archive")
|
||||
parser.add_argument("--vendor", type=Path, help="optional cargo vendor --locked --versioned-dirs result")
|
||||
args = parser.parse_args()
|
||||
system = Path("/etc/os-release").read_text()
|
||||
if 'ID=debian\n' not in system or 'VERSION_ID="13"' not in system:
|
||||
parser.error("prepare this package in the Debian 13 development container")
|
||||
args.compiler = args.compiler.resolve(strict=True)
|
||||
args.work = args.work.absolute()
|
||||
args.output = args.output.absolute()
|
||||
if not args.output.name.endswith((".tar.xz", ".tar.gz")):
|
||||
parser.error("output must end in .tar.xz or .tar.gz")
|
||||
if args.output.exists() or Path(str(args.output) + ".sha256").exists():
|
||||
parser.error("output archive and checksum must not exist")
|
||||
bundle, _, rust_version = prepare(args.compiler, args.work)
|
||||
author = args.work / "author"
|
||||
tools = bundle / "toolchain"
|
||||
tools.mkdir()
|
||||
native_tools(tools)
|
||||
rust_tools(tools, rust_version, args.vendor)
|
||||
# 先实际链接,避免把缺少 sysroot 文件的包花时间压缩后才发现。
|
||||
smoke = author / "link.rs"
|
||||
smoke.write_text('fn main() { println!("private Rust/C linker ready"); }\n')
|
||||
subprocess.run([str(tools / "bin/rustc"), str(smoke), "-C",
|
||||
"linker=" + str(tools / "bin/cc"), "-o", str(author / "link")], check=True)
|
||||
subprocess.run([str(author / "link")], check=True)
|
||||
copy(SOURCE / "build/README.md", bundle / "DEVELOPMENT.md")
|
||||
manifest = record(bundle, [bundle / "DEVELOPMENT.md", *tools.rglob("*")])
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
# 归档必须先于第一次构建,不能把 Cargo 编译缓存放进发行包。
|
||||
with args.output.open("xb") as stream:
|
||||
compression = "xz -T4 -6" if args.output.name.endswith(".xz") else "gzip -9"
|
||||
subprocess.run(["tar", "-C", str(args.work), "-I", compression, "-cf", "-", "dever"],
|
||||
stdout=stream, check=True)
|
||||
Path(str(args.output) + ".sha256").write_text(f"{digest(args.output)} {args.output.name}\n")
|
||||
print(json.dumps({"archive": str(args.output), "bytes": args.output.stat().st_size,
|
||||
"files": len(manifest["artifacts"]), "compiler_sha256": digest(args.compiler)}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
"""在开发容器组装在线安装资源;不下载工具链、不发布或激活版本。"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tomllib
|
||||
|
||||
from development import SOURCE, TRIPLE, copy, digest, native_tools, output, prepare, record, script
|
||||
|
||||
|
||||
def archive(directory, destination):
|
||||
with destination.open("xb") as stream:
|
||||
subprocess.run([
|
||||
"tar", "--sort=name", "--mtime=@0", "--owner=0", "--group=0", "--numeric-owner",
|
||||
"-C", str(directory.parent), "-I", "xz -T4 -6", "-cf", "-", directory.name,
|
||||
], stdout=stream, check=True)
|
||||
return digest(destination)
|
||||
|
||||
|
||||
def checksum(path):
|
||||
Path(str(path) + ".sha256").write_text(f"{digest(path)} {path.name}\n")
|
||||
|
||||
|
||||
def dependencies(bundle, rust_version):
|
||||
"""Cargo 自己裁剪锁文件;安装时只下载本机运行库依赖,不带作者编译依赖。"""
|
||||
project = bundle / "toolchain/project"
|
||||
(project / "src").mkdir(parents=True)
|
||||
(project / "src/main.rs").write_text("fn main() {}\n")
|
||||
features = sorted(tomllib.loads((bundle / "rust/src/runtime/Cargo.toml").read_text())["features"])
|
||||
(project / "Cargo.toml").write_text(
|
||||
'[package]\nname = "dever-development-dependencies"\nversion = "0.0.0"\nedition = "2024"\n'
|
||||
'[workspace]\n[dependencies]\n'
|
||||
f'dever-runtime = {{ path = "../../rust/src/runtime", features = {json.dumps(features)} }}\n'
|
||||
)
|
||||
copy(bundle / "rust/Cargo.lock", project / "Cargo.lock")
|
||||
subprocess.run(["cargo", f"+{rust_version}", "metadata", "--offline", "--format-version", "1",
|
||||
"--manifest-path", str(project / "Cargo.toml")], stdout=subprocess.DEVNULL, check=True)
|
||||
|
||||
|
||||
def compiler_licenses(tools, rust_version):
|
||||
metadata = json.loads(output("cargo", f"+{rust_version}", "metadata", "--locked", "--offline",
|
||||
"--all-features", "--format-version", "1", cwd=SOURCE / "rust"))
|
||||
for package in metadata["packages"]:
|
||||
if not package["source"]:
|
||||
continue
|
||||
source = Path(package["manifest_path"]).parent
|
||||
licenses = {path for path in source.rglob("*") if path.is_file()
|
||||
and path.name.upper().startswith(("LICENSE", "COPYING", "COPYRIGHT", "NOTICE", "AUTHORS"))}
|
||||
if package["license_file"]:
|
||||
licenses.add(source / package["license_file"])
|
||||
for path in licenses:
|
||||
copy(path, tools / "licenses/cargo" / source.name / path.relative_to(source))
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--compiler", required=True, type=Path)
|
||||
parser.add_argument("--work", required=True, type=Path)
|
||||
parser.add_argument("--output", required=True, type=Path, help="new release assets directory")
|
||||
args = parser.parse_args()
|
||||
system = Path("/etc/os-release").read_text()
|
||||
if 'ID=debian\n' not in system or 'VERSION_ID="13"' not in system:
|
||||
parser.error("prepare native tools in the Debian 13 development container")
|
||||
compiler = args.compiler.resolve(strict=True)
|
||||
work, destination = args.work.absolute(), args.output.absolute()
|
||||
if work.exists() or destination.exists():
|
||||
parser.error("work and output must be new directories")
|
||||
destination.mkdir(parents=True)
|
||||
bundle, version, rust_version = prepare(compiler, work)
|
||||
tools = bundle / "toolchain"
|
||||
tools.mkdir()
|
||||
native = work / "native-tools"
|
||||
native.mkdir()
|
||||
native_tools(native)
|
||||
native_archive = destination / "native.tar.xz"
|
||||
native_id = archive(native, native_archive)
|
||||
native_archive = native_archive.rename(destination / f"dever-native-linux-x86_64-{native_id}.tar.xz")
|
||||
checksum(native_archive)
|
||||
copy(native / "bin/cc", tools / "bin/cc")
|
||||
script(tools / "bin/cargo", '''export CARGO_HOME="$tools/cache"
|
||||
export RUSTC="$tools/rust/bin/rustc"
|
||||
export CC="$tools/bin/cc"
|
||||
export AR="$tools/native/usr/bin/x86_64-linux-gnu-ar"
|
||||
export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER="$CC"
|
||||
export LD_LIBRARY_PATH="$tools/lib"
|
||||
export PATH="$tools/native/usr/bin:/usr/bin:/bin"
|
||||
exec "$tools/rust/bin/cargo" "$@"
|
||||
''')
|
||||
script(tools / "bin/rustc", '''export LD_LIBRARY_PATH="$tools/lib"
|
||||
exec "$tools/rust/bin/rustc" "$@"
|
||||
''')
|
||||
(tools / "online").write_text(f"{rust_version} {native_id}\n")
|
||||
copy(SOURCE / "build/prepare.sh", tools / "prepare")
|
||||
dependencies(bundle, rust_version)
|
||||
compiler_licenses(tools, rust_version)
|
||||
copy(SOURCE / "build/README.md", bundle / "DEVELOPMENT.md")
|
||||
record(bundle, [bundle / "DEVELOPMENT.md", *tools.rglob("*")])
|
||||
core_archive = destination / f"dever-{version}-linux-x86_64.tar.xz"
|
||||
archive(bundle, core_archive)
|
||||
checksum(core_archive)
|
||||
installer = (SOURCE / "install").read_text().replace('@DEVER_VERSION@', version)
|
||||
(destination / "install").write_text(installer)
|
||||
(destination / "install").chmod(0o755)
|
||||
checksum(destination / "install")
|
||||
print(json.dumps({"version": version, "compiler_sha256": digest(compiler),
|
||||
"core_bytes": core_archive.stat().st_size,
|
||||
"native_bytes": native_archive.stat().st_size,
|
||||
"installer_bytes": (destination / "install").stat().st_size,
|
||||
"rust": rust_version, "target": TRIPLE}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env bash
|
||||
# 在线包的唯一环境准备入口;安装器后台启动,编译入口等待同一把锁。
|
||||
set -euo pipefail
|
||||
package=$(cd -- "$(dirname -- "$0")/.." && pwd -P)
|
||||
tools="$package/toolchain"
|
||||
root=$(cd "$package/../.." && pwd -P)
|
||||
[[ ! -f $tools/ready ]] || exit 0
|
||||
exec 9>"$root/.prepare.lock"
|
||||
previous=
|
||||
until flock -w 1 9; do
|
||||
status='Waiting for Dever environment preparation...'
|
||||
if [[ -f $tools/status ]]; then read -r status < "$tools/status" || true; fi
|
||||
if [[ $status != "$previous" ]]; then printf '%s\n' "$status" >&2; previous=$status; fi
|
||||
done
|
||||
[[ ! -f $tools/ready ]] || exit 0
|
||||
exec 3>&2
|
||||
exec >"$tools/prepare.log" 2>&1
|
||||
stage=
|
||||
rust_stage=
|
||||
phase='Preparing Dever environment'
|
||||
finish() {
|
||||
local code=$?
|
||||
if (( code )); then
|
||||
printf 'Failed: %s (exit %s). Details: %s\n' "$phase" "$code" "$tools/prepare.log" > "$tools/status"
|
||||
cat "$tools/status" >&3
|
||||
fi
|
||||
if [[ -n $stage ]]; then rm -rf -- "$stage"; fi
|
||||
if [[ -n $rust_stage ]]; then rm -rf -- "$rust_stage"; fi
|
||||
}
|
||||
trap finish EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
progress() {
|
||||
phase=$1
|
||||
printf '%s\n' "$phase" > "$tools/status"
|
||||
printf '%s\n' "$phase" >&3
|
||||
}
|
||||
read -r rust_version native_id < "$tools/online"
|
||||
mapfile -t sources < "$tools/sources"
|
||||
release_url=${sources[0]}
|
||||
rust_dist=${sources[1]}
|
||||
triple=x86_64-unknown-linux-gnu
|
||||
rust_root="$root/toolchains/rust-$rust_version-$triple"
|
||||
native_root="$root/toolchains/native-$native_id"
|
||||
downloads="$root/cache/downloads"
|
||||
mkdir -p "$downloads"
|
||||
stage=$(mktemp -d "$root/.prepare.XXXXXX")
|
||||
transfers=()
|
||||
archives=()
|
||||
queue() {
|
||||
local url=$1 name=$2
|
||||
archives+=("$name")
|
||||
if (( ${#transfers[@]} )); then transfers+=(--next); fi
|
||||
transfers+=(--fail --location --silent --show-error --retry 2 --connect-timeout 15
|
||||
--speed-limit 1024 --speed-time 60 --continue-at - --output "$downloads/$name.part" "$url")
|
||||
transfers+=(--next --fail --location --silent --show-error --retry 2 --connect-timeout 15
|
||||
--output "$downloads/$name.sha256" "$url.sha256")
|
||||
}
|
||||
native_archive="dever-native-linux-x86_64-$native_id.tar.xz"
|
||||
if [[ ! -d $native_root ]]; then queue "$release_url/$native_archive" "$native_archive"; fi
|
||||
if [[ ! -d $rust_root ]]; then
|
||||
for component in rustc rust-std cargo; do
|
||||
name="$component-$rust_version-$triple.tar.xz"
|
||||
queue "$rust_dist/dist/$name" "$name"
|
||||
done
|
||||
fi
|
||||
if (( ${#archives[@]} )); then
|
||||
progress 'Downloading Dever build environment...'
|
||||
curl --parallel --parallel-immediate --parallel-max 3 --fail-early "${transfers[@]}"
|
||||
progress 'Verifying Dever build environment...'
|
||||
for name in "${archives[@]}"; do
|
||||
read -r expected remainder < "$downloads/$name.sha256"
|
||||
if [[ $name == "$native_archive" && $expected != "$native_id" ]] ||
|
||||
! printf '%s %s\n' "$expected" "$downloads/$name.part" | sha256sum --check --status; then
|
||||
rm -f -- "$downloads/$name.part" "$downloads/$name.sha256"
|
||||
echo "Checksum mismatch: $name" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
fi
|
||||
progress 'Installing Dever build environment...'
|
||||
if [[ ! -d $rust_root ]]; then
|
||||
# 官方脚本不支持含空格的 prefix;先在独占临时目录安装,再原子发布。
|
||||
rust_stage=$(mktemp -d /tmp/dever-toolchain.XXXXXX)
|
||||
for component in rustc rust-std cargo; do
|
||||
name="$component-$rust_version-$triple"
|
||||
tar -xJf "$downloads/$name.tar.xz.part" -C "$rust_stage"
|
||||
bash "$rust_stage/$name/install.sh" --prefix="$rust_stage/toolchain" --disable-ldconfig
|
||||
rm -rf -- "$rust_stage/$name"
|
||||
done
|
||||
mv "$rust_stage/toolchain" "$stage/rust"
|
||||
mv "$stage/rust" "$rust_root"
|
||||
fi
|
||||
if [[ ! -d $native_root ]]; then
|
||||
tar -xJf "$downloads/$native_archive.part" -C "$stage"
|
||||
mv "$stage/native-tools" "$native_root"
|
||||
fi
|
||||
"$tools/bin/rustc" --version
|
||||
progress 'Preparing Dever project dependencies...'
|
||||
"$tools/bin/cargo" fetch --locked --target "$triple" --manifest-path "$tools/project/Cargo.toml"
|
||||
for name in "$native_archive" "rustc-$rust_version-$triple.tar.xz" \
|
||||
"rust-std-$rust_version-$triple.tar.xz" "cargo-$rust_version-$triple.tar.xz"; do
|
||||
rm -f -- "$downloads/$name.part" "$downloads/$name.sha256"
|
||||
done
|
||||
touch "$tools/ready"
|
||||
progress 'Dever build environment is ready.'
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"database": {
|
||||
"default": {
|
||||
"type": "postgres",
|
||||
"url": "postgresql://dever@127.0.0.1:5432/dever_default?sslmode=disable",
|
||||
"tls": "disabled",
|
||||
"min_connections": 0,
|
||||
"max_connections": 2,
|
||||
"max_page_size": 100,
|
||||
"wait_timeout_ms": 1000,
|
||||
"io_timeout_ms": 5000
|
||||
},
|
||||
"postgres_test": {
|
||||
"type": "postgres",
|
||||
"url": "postgresql://dever@127.0.0.1:5432/dever_{case}?sslmode=disable",
|
||||
"tls": "disabled",
|
||||
"min_connections": 0,
|
||||
"max_connections": 2,
|
||||
"max_page_size": 100,
|
||||
"wait_timeout_ms": 1000,
|
||||
"io_timeout_ms": 5000
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
*.vsix
|
||||
dist/
|
||||
/core.[0-9]*
|
||||
@@ -0,0 +1,12 @@
|
||||
**/*
|
||||
!package.json
|
||||
!README.md
|
||||
!language-configuration.json
|
||||
!syntaxes/**
|
||||
!extension.cjs
|
||||
!graph.cjs
|
||||
!inspection.cjs
|
||||
!navigation.cjs
|
||||
!reading.cjs
|
||||
!hover.cjs
|
||||
!dist/**
|
||||
@@ -0,0 +1,50 @@
|
||||
# Dever for VS Code
|
||||
|
||||
为 `.dever` 提供高亮、悬浮说明、源码跳转和调用流程。
|
||||
|
||||
## 安装
|
||||
|
||||
从 [发布页](https://git.dever.cc/dever/core/releases/tag/v0.0.1) 下载 `dever-language-0.0.1.vsix`,在 VS Code 执行 **Extensions: Install from VSIX**。
|
||||
|
||||
插件默认调用 PATH 中的 `dever`,需要带 `inspect` 的编译器。可用 `dever.command` 设置命令及固定参数,值为数组;远程执行时,编译器须能读取相同的源码路径。未信任的工作区不会执行编译器。
|
||||
|
||||
## 使用
|
||||
|
||||
| 操作 | 功能 |
|
||||
| --- | --- |
|
||||
| 悬浮函数 | 显示签名、主题说明和调用摘要 |
|
||||
| F12 | 跳转定义 |
|
||||
| Shift+F12 | 查看引用 |
|
||||
| 显示调用层次结构 | 查看调用方和被调用方 |
|
||||
| Dever: Preview Documentation | 打开主题说明 |
|
||||
| Dever: Show Calls | 查看每处调用及源码位置 |
|
||||
| Dever: Open Domain Links | 阅读领域调用图 |
|
||||
|
||||
源码与说明保存后刷新索引。未保存修改或源码检查错误会暂停相关导航并提示原因。查询不运行应用,也不调用 AI。
|
||||
|
||||
调用图内置 Mermaid,可离线阅读和点击节点跳转。修改代码后生成流程:
|
||||
|
||||
```sh
|
||||
dever inspect . --links
|
||||
dever inspect . --links --check
|
||||
```
|
||||
|
||||
图保留分句、参数求值顺序、短路、集合处理、任务、事务和 Port/Adapter 选择。它展示可能执行的路径;部署配置决定实际 Adapter。
|
||||
|
||||
当前支持函数、API/CMD、REST、认证、Job、回调、SQL 和 Port/Adapter 导航,尚不提供通用类型和字段引用导航。源码与说明的组织见 [说明写法](https://git.dever.cc/dever/core/src/branch/main/DOCUMENTATION.md)。
|
||||
|
||||
## 开发插件
|
||||
|
||||
在本目录执行:
|
||||
|
||||
```sh
|
||||
npm ci
|
||||
npm run build
|
||||
DEVER_TEST_BIN=/path/to/dever npm run test:navigation
|
||||
npm test -- --markdown-grammar /path/to/markdown.tmLanguage.json
|
||||
npm run package
|
||||
```
|
||||
|
||||
流程图浏览器测试使用 `npm run test:flows`,需设置 `DEVER_TEST_BIN` 和 `DEVER_BROWSER_TOOLS`,后者指向已有 playwright-core、Chromium 的 node_modules;系统库路径按测试环境设置。
|
||||
|
||||
构建会把 Markdown 解析器和 Mermaid 放入 VSIX,用户无需另装 npm 或渲染库。开发阶段版本统一为 `0.0.1`。
|
||||
@@ -0,0 +1,12 @@
|
||||
const esbuild = require('esbuild');
|
||||
const fs = require('node:fs/promises');
|
||||
|
||||
async function build() {
|
||||
await fs.mkdir('dist', { recursive: true });
|
||||
await Promise.all([
|
||||
fs.copyFile(require.resolve('@mermaid-js/tiny'), 'dist/mermaid.js'),
|
||||
fs.copyFile('flow-browser.js', 'dist/flow-browser.js'),
|
||||
esbuild.build({ entryPoints: ['flow-preview.cjs'], bundle: true, outfile: 'dist/flow-preview.cjs', minify: true, legalComments: 'eof', platform: 'node', external: ['vscode'] })
|
||||
]);
|
||||
}
|
||||
build().catch(error => { console.error(error); process.exitCode = 1; });
|
||||
@@ -0,0 +1,7 @@
|
||||
const { registerNavigation } = require('./navigation.cjs');
|
||||
|
||||
function activate(context) {
|
||||
registerNavigation(context);
|
||||
}
|
||||
|
||||
module.exports = { activate };
|
||||
@@ -0,0 +1,39 @@
|
||||
(() => {
|
||||
const mermaid = globalThis.mermaid;
|
||||
const vscode = acquireVsCodeApi();
|
||||
mermaid.initialize({ startOnLoad: false, securityLevel: 'loose', theme: document.body.classList.contains('vscode-dark') ? 'dark' : 'default', flowchart: { htmlLabels: false } });
|
||||
|
||||
// 渲染失败保留原始 Mermaid 并显示原因,不把空白图当成成功。
|
||||
async function render() {
|
||||
let sequence = 0;
|
||||
for (const code of document.querySelectorAll('pre > code.language-mermaid')) {
|
||||
try {
|
||||
const { svg } = await mermaid.render(`dever-flow-${sequence++}`, code.textContent);
|
||||
const container = document.createElement('div');
|
||||
container.className = 'dever-flow';
|
||||
container.innerHTML = svg;
|
||||
code.parentElement.replaceWith(container);
|
||||
} catch (error) {
|
||||
const message = document.createElement('p');
|
||||
message.className = 'dever-flow-error';
|
||||
message.textContent = `流程图渲染失败:${error.message}`;
|
||||
code.parentElement.before(message);
|
||||
}
|
||||
// Mermaid 的连续 Promise 会占满微任务队列;每张图后让页面处理加载、滚动和点击。
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
}
|
||||
document.getElementById(document.body.dataset.anchor)?.scrollIntoView();
|
||||
document.body.dataset.rendered = 'true';
|
||||
}
|
||||
|
||||
document.addEventListener('click', event => {
|
||||
const link = event.target.closest('a');
|
||||
if (!link) return;
|
||||
const href = link.getAttribute('href') || link.getAttribute('xlink:href');
|
||||
if (!href) return;
|
||||
event.preventDefault();
|
||||
if (href.startsWith('#')) document.getElementById(decodeURIComponent(href.slice(1)))?.scrollIntoView();
|
||||
else vscode.postMessage({ type: 'open', href });
|
||||
});
|
||||
render();
|
||||
})();
|
||||
@@ -0,0 +1,84 @@
|
||||
const vscode = require('vscode');
|
||||
const fs = require('node:fs/promises');
|
||||
const path = require('node:path');
|
||||
const { randomBytes } = require('node:crypto');
|
||||
const { pathToFileURL, fileURLToPath } = require('node:url');
|
||||
const markdown = require('markdown-it')({ html: true });
|
||||
const validateLink = markdown.validateLink;
|
||||
markdown.validateLink = href => href.startsWith('file:') || validateLink(href);
|
||||
|
||||
// 流程图与调用表共用阅读面板;链接打开真实源码或当前索引。
|
||||
async function openFlowPreview(context, target, root, calls) {
|
||||
const panel = vscode.window.createWebviewPanel('dever.links', 'Dever 调用流程', vscode.ViewColumn.Beside, {
|
||||
enableScripts: true,
|
||||
localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, 'dist')]
|
||||
});
|
||||
let current = target.fsPath;
|
||||
async function render(filename, anchor = '') {
|
||||
const text = calls ? await calls.read(filename) : await fs.readFile(filename, 'utf8');
|
||||
if (!calls && !text.startsWith('<!-- Generated by dever inspect --links. Do not edit. -->')) {
|
||||
throw new Error('此文件不是生成的领域索引,请运行 dever inspect . --links。');
|
||||
}
|
||||
current = filename;
|
||||
panel.title = calls ? `Dever · ${filename}` : `Dever · ${path.basename(path.dirname(filename))}`;
|
||||
const script = panel.webview.asWebviewUri(vscode.Uri.joinPath(context.extensionUri, 'dist', 'flow-browser.js'));
|
||||
const mermaid = panel.webview.asWebviewUri(vscode.Uri.joinPath(context.extensionUri, 'dist', 'mermaid.js'));
|
||||
const nonce = randomBytes(16).toString('hex');
|
||||
panel.webview.html = `<!DOCTYPE html><html><head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'nonce-${nonce}'; style-src ${panel.webview.cspSource} 'unsafe-inline'; img-src ${panel.webview.cspSource} data:; font-src ${panel.webview.cspSource} data:;">
|
||||
<style>body{max-width:1100px;margin:24px auto;padding:0 24px;font:var(--vscode-font-size) var(--vscode-font-family);color:var(--vscode-editor-foreground);background:var(--vscode-editor-background)}a{color:var(--vscode-textLink-foreground)}table{border-collapse:collapse}td,th{padding:6px 12px;border:1px solid var(--vscode-panel-border)}pre{overflow:auto}details{margin:16px 0}.dever-flow{overflow:auto;margin:20px 0}.dever-flow svg{max-width:none!important}.dever-flow-error{color:var(--vscode-errorForeground)}</style>
|
||||
</head><body data-anchor="${markdown.utils.escapeHtml(anchor)}">${markdown.render(text)}
|
||||
<script nonce="${nonce}" src="${mermaid}"></script><script nonce="${nonce}" src="${script}"></script></body></html>`;
|
||||
}
|
||||
const messages = panel.webview.onDidReceiveMessage(async message => {
|
||||
if (message.type !== 'open' || typeof message.href !== 'string') return;
|
||||
try {
|
||||
if (calls && message.href.startsWith('command:dever.showCalls?')) {
|
||||
const [requestedRoot, name] = JSON.parse(decodeURIComponent(message.href.split('?')[1]));
|
||||
if (requestedRoot !== root) throw new Error('链接超出当前项目。');
|
||||
await render(name);
|
||||
return;
|
||||
}
|
||||
if (calls && message.href.startsWith('dever-source:')) {
|
||||
const target = vscode.Uri.parse(message.href);
|
||||
if (JSON.parse(target.query).root !== root) throw new Error('链接超出当前项目。');
|
||||
const document = await vscode.workspace.openTextDocument(target);
|
||||
const location = /^#L(\d+)(?:,(\d+))?$/.exec(new URL(message.href).hash);
|
||||
const line = location ? Number(location[1]) - 1 : 0;
|
||||
const column = location?.[2] ? Number(location[2]) - 1 : 0;
|
||||
await vscode.window.showTextDocument(document, { viewColumn: vscode.ViewColumn.One, selection: new vscode.Range(line, column, line, column) });
|
||||
return;
|
||||
}
|
||||
const destination = new URL(message.href, pathToFileURL(calls ? root + path.sep : current));
|
||||
if (destination.protocol !== 'file:') throw new Error('流程阅读页只打开项目源码和领域索引。');
|
||||
const filename = fileURLToPath(destination);
|
||||
const relative = path.relative(root, filename);
|
||||
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
||||
throw new Error('链接超出当前项目。');
|
||||
}
|
||||
if (!calls && path.basename(filename) === 'link.md') {
|
||||
await render(filename, decodeURIComponent(destination.hash.slice(1)));
|
||||
} else {
|
||||
const location = /^#L(\d+)(?:,(\d+))?$/.exec(destination.hash);
|
||||
const line = location ? Number(location[1]) - 1 : 0;
|
||||
const column = location?.[2] ? Number(location[2]) - 1 : 0;
|
||||
const document = await vscode.workspace.openTextDocument(vscode.Uri.file(filename));
|
||||
await vscode.window.showTextDocument(document, { viewColumn: vscode.ViewColumn.One, selection: new vscode.Range(line, column, line, column) });
|
||||
}
|
||||
} catch (error) { vscode.window.showErrorMessage(error.message); }
|
||||
});
|
||||
panel.onDidDispose(() => messages.dispose());
|
||||
if (calls) {
|
||||
const changed = calls.changed.event(() => render(current).catch(error => {
|
||||
panel.dispose();
|
||||
vscode.window.showErrorMessage(error.message);
|
||||
}));
|
||||
panel.onDidDispose(() => changed.dispose());
|
||||
}
|
||||
context.subscriptions.push(panel);
|
||||
try { await render(calls ? calls.name : current); } catch (error) { panel.dispose(); throw error; }
|
||||
return panel;
|
||||
}
|
||||
|
||||
module.exports = { openFlowPreview };
|
||||
@@ -0,0 +1,28 @@
|
||||
// 所有关系来自编译器;这里只按源码位置选择符号和分组显示。
|
||||
function contains(range, position) {
|
||||
const compare = (left, right) => left.line - right.line || left.character - right.character;
|
||||
return compare(range.start, position) <= 0 && compare(position, range.end) < 0;
|
||||
}
|
||||
|
||||
// 自动分派用于调用层级;不能抢走显式绑定或 Port 声明的“转到定义”。
|
||||
const generated = new Set(['verify', 'rest-field', 'rest-model', 'adapter']);
|
||||
function at(index, file, position) {
|
||||
const calls = index.calls.filter(call => !generated.has(call.kind) && call.site.file === file && contains(call.site, position));
|
||||
const targets = new Set(calls.map(call => call.callee));
|
||||
if (targets.size) return index.symbols.filter(symbol => targets.has(symbol.id));
|
||||
return index.symbols.filter(symbol => symbol.declarations.some(location =>
|
||||
location.file === file && contains(location, position)));
|
||||
}
|
||||
|
||||
function relations(index, id, incoming) {
|
||||
const grouped = new Map();
|
||||
for (const call of index.calls) {
|
||||
if ((incoming ? call.callee : call.caller) !== id) continue;
|
||||
const target = incoming ? call.caller : call.callee;
|
||||
if (!grouped.has(target)) grouped.set(target, []);
|
||||
grouped.get(target).push(call);
|
||||
}
|
||||
return [...grouped].map(([id, calls]) => ({ symbol: index.symbols.find(symbol => symbol.id === id), calls }));
|
||||
}
|
||||
|
||||
module.exports = { at, relations, contains };
|
||||
@@ -0,0 +1,60 @@
|
||||
const vscode = require('vscode');
|
||||
const { relations } = require('./graph.cjs');
|
||||
const { uri } = require('./inspection.cjs');
|
||||
|
||||
function command(name, args) {
|
||||
return `command:${name}?${encodeURIComponent(JSON.stringify(args))}`;
|
||||
}
|
||||
|
||||
async function description(inspection, index, symbol, root) {
|
||||
const value = index.documentation.find(item => item.symbol === symbol.name);
|
||||
if (!value) return undefined;
|
||||
const documentIndex = { files: [index.documentation_files[value.file]] };
|
||||
await inspection.verify(documentIndex, 0, root);
|
||||
return { ...value, uri: uri(documentIndex, 0, root) };
|
||||
}
|
||||
|
||||
function registerHover(context, selector, inspection, lookup) {
|
||||
context.subscriptions.push(vscode.languages.registerHoverProvider(selector, {
|
||||
async provideHover(document, position) {
|
||||
const state = await lookup(document, position);
|
||||
if (!state?.symbols.length) return undefined;
|
||||
const contents = [];
|
||||
for (const symbol of state.symbols) {
|
||||
const page = new vscode.MarkdownString();
|
||||
page.isTrusted = { enabledCommands: ['dever.showCalls', 'dever.openDomainLinks', 'dever.previewDocumentation'] };
|
||||
page.appendMarkdown(`**${symbol.name}**\n\n`);
|
||||
if (state.index.documentation_errors.length) {
|
||||
page.appendMarkdown('**说明校验未通过。** 失效或重复的标题详见“Dever”输出;签名和调用关系仍来自源码。\n\n');
|
||||
}
|
||||
for (const signature of symbol.signatures) page.appendCodeblock(signature, 'dever');
|
||||
const detail = await description(inspection, state.index, symbol, state.root);
|
||||
if (detail) {
|
||||
page.baseUri = detail.uri;
|
||||
page.appendMarkdown(`${detail.markdown}\n\n`);
|
||||
} else {
|
||||
page.appendMarkdown('尚未提供领域说明;签名和关系来自已保存源码。\n\n');
|
||||
}
|
||||
for (const incoming of [true, false]) {
|
||||
const related = relations(state.index, symbol.id, incoming);
|
||||
const label = incoming ? '调用方' : '调用目标';
|
||||
const names = related.slice(0, 4).map(item => `\`${item.symbol.name}\``).join('、');
|
||||
page.appendMarkdown(`${label}:${names || '当前静态索引中无记录'}${related.length > 4 ? `,共 ${related.length} 个` : ''}。\n\n`);
|
||||
}
|
||||
const links = [];
|
||||
for (const site of symbol.declarations.slice(0, 1)) {
|
||||
const target = uri(state.index, site.file, state.root).with({ fragment: `L${site.start.line + 1},${site.start.character + 1}` });
|
||||
links.push(`[源码](${target.toString()})`);
|
||||
}
|
||||
if (detail) links.push(`[说明](${command('dever.previewDocumentation', [state.root, symbol.name])})`);
|
||||
links.push(`[调用详情](${command('dever.showCalls', [state.root, symbol.name])})`);
|
||||
links.push(`[领域流程](${command('dever.openDomainLinks', [state.root, symbol.name])})`);
|
||||
page.appendMarkdown(links.join(' · '));
|
||||
contents.push(page);
|
||||
}
|
||||
return new vscode.Hover(contents);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
module.exports = { registerHover, description, command };
|
||||
@@ -0,0 +1,132 @@
|
||||
const vscode = require('vscode');
|
||||
const path = require('node:path');
|
||||
const fs = require('node:fs/promises');
|
||||
const { createHash } = require('node:crypto');
|
||||
const { execFile } = require('node:child_process');
|
||||
const { promisify } = require('node:util');
|
||||
const execute = promisify(execFile);
|
||||
|
||||
function isSource(document) {
|
||||
return document.languageId === 'dever';
|
||||
}
|
||||
|
||||
async function projectRoot(document) {
|
||||
if (document.uri.scheme === 'dever-source') return JSON.parse(document.uri.query).root;
|
||||
const folder = vscode.workspace.getWorkspaceFolder(document.uri)?.uri.fsPath;
|
||||
for (let directory = path.dirname(document.uri.fsPath); ; directory = path.dirname(directory)) {
|
||||
try {
|
||||
await fs.stat(path.join(directory, 'config', 'setting.json'));
|
||||
return directory;
|
||||
} catch (error) {
|
||||
if (error.code !== 'ENOENT') throw error;
|
||||
}
|
||||
if (directory === folder || directory === path.dirname(directory)) break;
|
||||
}
|
||||
return folder || document.uri.fsPath;
|
||||
}
|
||||
|
||||
function uri(index, file, root) {
|
||||
const source = index.files[file];
|
||||
return source.text === undefined ? vscode.Uri.file(source.path) : vscode.Uri.from({
|
||||
scheme: 'dever-source', path: `/${source.path}`, query: JSON.stringify({ root, path: source.path })
|
||||
});
|
||||
}
|
||||
|
||||
function range(location) {
|
||||
return new vscode.Range(location.start.line, location.start.character, location.end.line, location.end.character);
|
||||
}
|
||||
|
||||
function createInspection(context) {
|
||||
const cache = new Map();
|
||||
const changed = new vscode.EventEmitter();
|
||||
const output = vscode.window.createOutputChannel('Dever');
|
||||
context.subscriptions.push(changed, output);
|
||||
const invalidate = () => { cache.clear(); changed.fire(); };
|
||||
const watcher = vscode.workspace.createFileSystemWatcher('**/{*.dever,README.md,doc/*.md,dever.lock,dever.api,setting.json}');
|
||||
context.subscriptions.push(watcher, watcher.onDidChange(invalidate), watcher.onDidCreate(invalidate), watcher.onDidDelete(invalidate));
|
||||
context.subscriptions.push(vscode.workspace.onDidSaveTextDocument(document => {
|
||||
if (isSource(document) || /[/\\]doc[/\\][^/\\]+\.md$/.test(document.uri.fsPath)) invalidate();
|
||||
}));
|
||||
context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(event => {
|
||||
if (event.affectsConfiguration('dever.command')) invalidate();
|
||||
}));
|
||||
|
||||
async function load(root) {
|
||||
if (!vscode.workspace.isTrusted) throw new Error('请信任工作区后再运行 Dever 源码检查。');
|
||||
if (!cache.has(root)) {
|
||||
const command = vscode.workspace.getConfiguration('dever', vscode.Uri.file(root)).get('command');
|
||||
const promise = execute(command[0], [...command.slice(1), 'inspect', root, '--json'], {
|
||||
maxBuffer: 32 * 1024 * 1024, windowsHide: true
|
||||
}).then(({ stdout }) => {
|
||||
const index = JSON.parse(stdout);
|
||||
for (const error of index.documentation_errors) output.appendLine(error);
|
||||
return index;
|
||||
}).catch(error => {
|
||||
if (cache.get(root) === promise) cache.delete(root);
|
||||
const message = error.stderr?.trim() || error.message;
|
||||
output.appendLine(message);
|
||||
throw new Error(message);
|
||||
});
|
||||
cache.set(root, promise);
|
||||
}
|
||||
const pending = cache.get(root);
|
||||
const index = await pending;
|
||||
if (cache.get(root) !== pending) throw new Error('检查期间源码已变化,请重新查询调用关系。');
|
||||
return index;
|
||||
}
|
||||
|
||||
async function verify(index, file, root) {
|
||||
const document = await vscode.workspace.openTextDocument(uri(index, file, root));
|
||||
if (document.isDirty) throw new Error('调用关系基于已保存源码,请先保存文件。');
|
||||
const digest = createHash('sha256').update(document.getText()).digest('hex');
|
||||
if (digest !== index.files[file].sha256) {
|
||||
invalidate();
|
||||
throw new Error('源码已变化,请刷新调用关系。');
|
||||
}
|
||||
return document;
|
||||
}
|
||||
|
||||
async function current(document) {
|
||||
if (document.isDirty) return undefined;
|
||||
const root = await projectRoot(document);
|
||||
if (vscode.workspace.textDocuments.some(value => value.isDirty && isSource(value)
|
||||
&& (value.uri.fsPath === root || value.uri.fsPath.startsWith(`${root}${path.sep}`)))) return undefined;
|
||||
const index = await load(root);
|
||||
const sourcePath = document.uri.scheme === 'dever-source' ? JSON.parse(document.uri.query).path : document.uri.fsPath;
|
||||
const file = index.files.findIndex(source => source.path === sourcePath);
|
||||
if (file < 0) return undefined;
|
||||
await verify(index, file, root);
|
||||
return { root, index, file };
|
||||
}
|
||||
|
||||
async function location(index, site, root) {
|
||||
await verify(index, site.file, root);
|
||||
return new vscode.Location(uri(index, site.file, root), range(site));
|
||||
}
|
||||
|
||||
async function checkLinks(root) {
|
||||
if (!vscode.workspace.isTrusted) throw new Error('请信任工作区后再检查领域流程。');
|
||||
if (vscode.workspace.textDocuments.some(document => document.isDirty && isSource(document)
|
||||
&& document.uri.fsPath.startsWith(`${root}${path.sep}`))) throw new Error('请先保存源码,再查看领域流程。');
|
||||
const command = vscode.workspace.getConfiguration('dever', vscode.Uri.file(root)).get('command');
|
||||
try {
|
||||
await execute(command[0], [...command.slice(1), 'inspect', root, '--links', '--check'], { windowsHide: true });
|
||||
} catch (error) {
|
||||
throw new Error(error.stderr?.trim() || error.stdout?.trim() || error.message);
|
||||
}
|
||||
}
|
||||
|
||||
context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider('dever-source', {
|
||||
async provideTextDocumentContent(uri) {
|
||||
const { root, path } = JSON.parse(uri.query);
|
||||
const index = await load(root);
|
||||
const source = index.files.find(source => source.path === path)
|
||||
|| index.documentation_files.find(description => description.path === path);
|
||||
if (source?.text === undefined) throw new Error(`未找到内置源码:${path}`);
|
||||
return source.text;
|
||||
}
|
||||
}));
|
||||
return { load, current, verify, location, checkLinks, invalidate, changed };
|
||||
}
|
||||
|
||||
module.exports = { createInspection, range, uri, isSource, projectRoot };
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"comments": {
|
||||
"lineComment": "#"
|
||||
},
|
||||
"brackets": [["{", "}"], ["[", "]"], ["(", ")"]],
|
||||
"autoClosingPairs": [
|
||||
{ "open": "{", "close": "}", "notIn": ["string", "comment"] },
|
||||
{ "open": "[", "close": "]", "notIn": ["string", "comment"] },
|
||||
{ "open": "(", "close": ")", "notIn": ["string", "comment"] },
|
||||
{ "open": "\"", "close": "\"", "notIn": ["string", "comment"] }
|
||||
],
|
||||
"surroundingPairs": [["{", "}"], ["[", "]"], ["(", ")"], ["\"", "\""]],
|
||||
"wordPattern": "[A-Za-z_][A-Za-z0-9_]*",
|
||||
"indentationRules": {
|
||||
"increaseIndentPattern": "^\\s*(?!#)(?:[^\"#]|\"(?:\\\\.|[^\"\\\\])*\")*[({\\[]\\s*(?:#.*)?$",
|
||||
"decreaseIndentPattern": "^\\s*[})\\]]"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
const vscode = require('vscode');
|
||||
const { createInspection, range, uri } = require('./inspection.cjs');
|
||||
const { at, relations } = require('./graph.cjs');
|
||||
const { registerReading } = require('./reading.cjs');
|
||||
const { registerHover } = require('./hover.cjs');
|
||||
|
||||
function registerNavigation(context) {
|
||||
const inspection = createInspection(context);
|
||||
const selector = [
|
||||
{ language: 'dever', scheme: 'file' },
|
||||
{ language: 'dever', scheme: 'dever-source' }
|
||||
];
|
||||
|
||||
async function item(index, symbol, root) {
|
||||
const first = symbol.declarations[0];
|
||||
if (!first) return undefined;
|
||||
await inspection.verify(index, first.file, root);
|
||||
const end = symbol.declarations.at(-1).end;
|
||||
const span = range({ start: first.start, end });
|
||||
const selected = new vscode.Range(span.start, span.start);
|
||||
const value = new vscode.CallHierarchyItem(vscode.SymbolKind.Function, symbol.name, symbol.kind,
|
||||
uri(index, first.file, root), span, selected);
|
||||
return value;
|
||||
}
|
||||
|
||||
async function lookup(document, position) {
|
||||
const state = await inspection.current(document);
|
||||
return state ? { ...state, symbols: at(state.index, state.file, position) } : undefined;
|
||||
}
|
||||
|
||||
context.subscriptions.push(vscode.languages.registerDefinitionProvider(selector, {
|
||||
async provideDefinition(document, position) {
|
||||
const state = await lookup(document, position);
|
||||
if (!state) return [];
|
||||
return Promise.all(state.symbols.flatMap(symbol => symbol.declarations)
|
||||
.map(site => inspection.location(state.index, site, state.root)));
|
||||
}
|
||||
}));
|
||||
context.subscriptions.push(vscode.languages.registerReferenceProvider(selector, {
|
||||
async provideReferences(document, position, options) {
|
||||
const state = await lookup(document, position);
|
||||
if (!state) return [];
|
||||
const ids = new Set(state.symbols.map(symbol => symbol.id));
|
||||
const sites = state.index.calls.filter(call => ids.has(call.callee)).map(call => call.site);
|
||||
if (options.includeDeclaration) sites.push(...state.symbols.flatMap(symbol => symbol.declarations));
|
||||
const unique = [...new Map(sites.map(site => [JSON.stringify(site), site])).values()];
|
||||
return Promise.all(unique.map(site => inspection.location(state.index, site, state.root)));
|
||||
}
|
||||
}));
|
||||
|
||||
async function hierarchy(value, incoming) {
|
||||
const document = await vscode.workspace.openTextDocument(value.uri);
|
||||
const state = await inspection.current(document);
|
||||
if (!state) throw new Error('请保存源码后重新打开调用层级。');
|
||||
const { root, index, file } = state;
|
||||
const start = value.selectionRange.start;
|
||||
const symbol = index.symbols.find(symbol => symbol.name === value.name && symbol.declarations.some(site =>
|
||||
site.file === file && site.start.line === start.line && site.start.character === start.character));
|
||||
if (!symbol) throw new Error('函数位置已变化,请重新打开调用层级。');
|
||||
await inspection.verify(index, symbol.declarations[0].file, root);
|
||||
const result = [];
|
||||
for (const relation of relations(index, symbol.id, incoming)) {
|
||||
const target = await item(index, relation.symbol, root);
|
||||
if (!target) continue;
|
||||
for (const call of relation.calls) await inspection.verify(index, call.site.file, root);
|
||||
const ranges = relation.calls.map(call => range(call.site));
|
||||
result.push(incoming ? new vscode.CallHierarchyIncomingCall(target, ranges)
|
||||
: new vscode.CallHierarchyOutgoingCall(target, ranges));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
context.subscriptions.push(vscode.languages.registerCallHierarchyProvider(selector, {
|
||||
async prepareCallHierarchy(document, position) {
|
||||
const state = await lookup(document, position);
|
||||
if (!state) return [];
|
||||
return (await Promise.all(state.symbols.map(symbol => item(state.index, symbol, state.root)))).filter(Boolean);
|
||||
},
|
||||
provideCallHierarchyIncomingCalls(value) { return hierarchy(value, true); },
|
||||
provideCallHierarchyOutgoingCalls(value) { return hierarchy(value, false); }
|
||||
}));
|
||||
context.subscriptions.push(vscode.languages.registerCodeLensProvider(selector, {
|
||||
onDidChangeCodeLenses: inspection.changed.event,
|
||||
async provideCodeLenses(document) {
|
||||
const state = await inspection.current(document);
|
||||
if (!state) return [];
|
||||
return state.index.symbols.filter(symbol => symbol.kind !== 'generated'
|
||||
&& symbol.declarations[0]?.file === state.file).map(symbol => {
|
||||
const incoming = relations(state.index, symbol.id, true);
|
||||
const count = incoming.reduce((total, relation) => total + relation.calls.length, 0);
|
||||
const start = range(symbol.declarations[0]).start;
|
||||
return new vscode.CodeLens(new vscode.Range(start, start), {
|
||||
title: `${incoming.length} 个调用方 · ${count} 处调用 · 查看调用关系`,
|
||||
command: 'dever.showCalls', arguments: [state.root, symbol.name]
|
||||
});
|
||||
});
|
||||
}
|
||||
}));
|
||||
registerReading(context, inspection, lookup);
|
||||
registerHover(context, selector, inspection, lookup);
|
||||
}
|
||||
|
||||
module.exports = { registerNavigation };
|
||||
Generated
+2458
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"name": "dever-language",
|
||||
"displayName": "Dever Language",
|
||||
"description": "Dever source navigation, domain documentation, hover and call flows.",
|
||||
"version": "0.0.1",
|
||||
"main": "./extension.cjs",
|
||||
"activationEvents": [
|
||||
"onLanguage:dever"
|
||||
],
|
||||
"publisher": "shemic",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"engines": {
|
||||
"vscode": "^1.85.0"
|
||||
},
|
||||
"categories": [
|
||||
"Programming Languages"
|
||||
],
|
||||
"keywords": [
|
||||
"dever",
|
||||
"markdown",
|
||||
"syntax highlighting"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "ssh://git@git.dever.cc:2222/dever/core.git",
|
||||
"directory": "editor/vscode"
|
||||
},
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "dever.previewDocumentation",
|
||||
"title": "Dever: Preview Documentation"
|
||||
},
|
||||
{
|
||||
"command": "dever.showCalls",
|
||||
"title": "Dever: Show Calls"
|
||||
},
|
||||
{
|
||||
"command": "dever.openDomainLinks",
|
||||
"title": "Dever: Open Domain Links"
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
"title": "Dever",
|
||||
"properties": {
|
||||
"dever.command": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"minItems": 1,
|
||||
"default": [
|
||||
"dever"
|
||||
],
|
||||
"description": "Compiler executable followed by fixed arguments; inspect is appended without a shell."
|
||||
}
|
||||
}
|
||||
},
|
||||
"languages": [
|
||||
{
|
||||
"id": "dever",
|
||||
"aliases": [
|
||||
"Dever",
|
||||
"dever"
|
||||
],
|
||||
"extensions": [
|
||||
".dever"
|
||||
],
|
||||
"configuration": "./language-configuration.json"
|
||||
}
|
||||
],
|
||||
"grammars": [
|
||||
{
|
||||
"language": "dever",
|
||||
"scopeName": "source.dever",
|
||||
"path": "./syntaxes/dever.tmLanguage.json"
|
||||
},
|
||||
{
|
||||
"scopeName": "markdown.dever.codeblock",
|
||||
"path": "./syntaxes/dever-markdown.tmLanguage.json",
|
||||
"injectTo": [
|
||||
"text.html.markdown"
|
||||
],
|
||||
"embeddedLanguages": {
|
||||
"meta.embedded.block.dever": "dever"
|
||||
},
|
||||
"tokenTypes": {
|
||||
"meta.embedded.block.dever": "other"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node build.cjs",
|
||||
"vscode:prepublish": "npm run build",
|
||||
"test": "node ../../test/editor-vscode/highlight.test.cjs",
|
||||
"test:navigation": "node --test ../../test/editor-vscode/navigation.test.cjs",
|
||||
"test:flows": "node --test ../../test/editor-vscode/flows.test.cjs",
|
||||
"package": "vsce package --no-dependencies --skip-license"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mermaid-js/tiny": "12.2.0",
|
||||
"@vscode/vsce": "4.0.0",
|
||||
"esbuild": "0.28.2",
|
||||
"markdown-it": "15.0.3",
|
||||
"vscode-oniguruma": "2.0.1",
|
||||
"vscode-textmate": "9.2.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
const vscode = require('vscode');
|
||||
const path = require('node:path');
|
||||
const { relations } = require('./graph.cjs');
|
||||
const { uri, projectRoot } = require('./inspection.cjs');
|
||||
const { openFlowPreview } = require('./dist/flow-preview.cjs');
|
||||
const { description, command } = require('./hover.cjs');
|
||||
|
||||
function registerReading(context, inspection, lookup) {
|
||||
const changed = new vscode.EventEmitter();
|
||||
const previews = new Map();
|
||||
context.subscriptions.push(changed, inspection.changed.event(() => {
|
||||
for (const uri of previews.values()) changed.fire(uri);
|
||||
}));
|
||||
context.subscriptions.push(vscode.workspace.onDidCloseTextDocument(document => {
|
||||
previews.delete(document.uri.toString());
|
||||
}));
|
||||
const provider = {
|
||||
onDidChange: changed.event,
|
||||
async provideTextDocumentContent(documentUri) {
|
||||
previews.set(documentUri.toString(), documentUri);
|
||||
const { root, name } = JSON.parse(documentUri.query);
|
||||
const index = await inspection.load(root);
|
||||
const symbols = index.symbols.filter(symbol => symbol.name === name);
|
||||
if (!symbols.length) throw new Error(`函数已不存在:${name}`);
|
||||
const lines = [`# ${name}`, '', '静态源码关系;分支可能只执行一部分,任务派发不表示同步执行。', '', index.coverage, ''];
|
||||
if (index.documentation_errors.length) lines.push('**说明校验未通过。** 详见“Dever”输出。', '');
|
||||
function link(site) {
|
||||
const file = index.files[site.file];
|
||||
const target = uri(index, site.file, root).with({ fragment: `L${site.start.line + 1},${site.start.character + 1}` });
|
||||
return `[${file.path}:${site.start.line + 1}](${target.toString()})`;
|
||||
}
|
||||
for (const symbol of symbols) {
|
||||
for (const signature of symbol.signatures) lines.push('```dever', signature, '```', '');
|
||||
const detail = await description(inspection, index, symbol, root);
|
||||
if (detail) lines.push(detail.markdown, '', `[主题说明](${detail.uri.toString()})`, '');
|
||||
for (const site of symbol.declarations) {
|
||||
await inspection.verify(index, site.file, root);
|
||||
lines.push('## 定义', '', link(site), '');
|
||||
}
|
||||
for (const incoming of [true, false]) {
|
||||
lines.push(incoming ? '## 谁调用它' : '## 它调用谁', '');
|
||||
const related = relations(index, symbol.id, incoming);
|
||||
if (!related.length) lines.push('当前已检查源码中没有此类关系。', '');
|
||||
else lines.push('| 函数 | 领域 | 关系 | 调用位置 | 定义 / 上下游 |', '| --- | --- | --- | --- | --- |');
|
||||
for (const relation of related) {
|
||||
for (const call of relation.calls) {
|
||||
await inspection.verify(index, call.site.file, root);
|
||||
const definitions = relation.symbol.declarations.map(link).join('、') || '内建操作';
|
||||
const chain = command('dever.showCalls', [root, relation.symbol.name]);
|
||||
lines.push(`| ${relation.symbol.name} | ${relation.symbol.name.split('.').slice(0, 2).join('.')} | ${call.kind} | ${link(call.site)} | ${definitions} · [上下游](${chain}) |`);
|
||||
}
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
};
|
||||
context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider('dever-calls', provider));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('dever.showCalls', async (root, name) => {
|
||||
try {
|
||||
if (!root || !name) {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) return;
|
||||
const state = await lookup(editor.document, editor.selection.active);
|
||||
if (!state) throw new Error('请先保存源码,再查看调用关系。');
|
||||
if (!state.symbols.length) throw new Error('请把光标放在函数或调用位置。');
|
||||
root = state.root;
|
||||
name = state.symbols[0].name;
|
||||
}
|
||||
const target = vscode.Uri.from({ scheme: 'dever-calls', path: '/calls.md', query: JSON.stringify({ root, name }) });
|
||||
await openFlowPreview(context, target, root, {
|
||||
name,
|
||||
changed: inspection.changed,
|
||||
read: name => provider.provideTextDocumentContent(target.with({ query: JSON.stringify({ root, name }) }))
|
||||
});
|
||||
} catch (error) { vscode.window.showErrorMessage(error.message); }
|
||||
}));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('dever.previewDocumentation', async (root, name) => {
|
||||
try {
|
||||
const document = vscode.window.activeTextEditor?.document;
|
||||
if (!root || !name) {
|
||||
if (!document) return;
|
||||
const state = await lookup(document, vscode.window.activeTextEditor.selection.active);
|
||||
root = state?.root || await projectRoot(document);
|
||||
name = state?.symbols[0]?.name;
|
||||
}
|
||||
const index = await inspection.load(root);
|
||||
const symbol = index.symbols.find(value => value.name === name);
|
||||
const detail = symbol && await description(inspection, index, symbol, root);
|
||||
if (!detail) throw new Error('该符号尚未提供 doc/ 说明。');
|
||||
await vscode.commands.executeCommand('markdown.showPreviewToSide', detail.uri.with({ fragment: `L${detail.line + 1}` }));
|
||||
} catch (error) { vscode.window.showErrorMessage(error.message); }
|
||||
}));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('dever.openDomainLinks', async (root, name) => {
|
||||
try {
|
||||
const document = vscode.window.activeTextEditor?.document;
|
||||
if (!root && !document) return;
|
||||
root ||= await projectRoot(document);
|
||||
const index = await inspection.load(root);
|
||||
const symbol = index.symbols.find(value => value.name === name);
|
||||
const source = symbol?.declarations[0];
|
||||
const filename = source ? index.files[source.file].path : document.uri.fsPath;
|
||||
const relative = path.relative(path.join(root, 'module'), filename).split(path.sep);
|
||||
if (relative.length < 3 || relative[0] === '..') {
|
||||
throw new Error('请在 module/<组件>/<领域>/ 内的源码中执行此命令。');
|
||||
}
|
||||
const target = vscode.Uri.file(path.join(root, 'module', ...relative.slice(0, 2), 'link.md'));
|
||||
await inspection.checkLinks(root);
|
||||
const panel = await openFlowPreview(context, target, root);
|
||||
const changed = inspection.changed.event(() => panel.dispose());
|
||||
panel.onDidDispose(() => changed.dispose());
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(`${error.message} 可在项目根运行 dever inspect . --links 生成索引。`);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
module.exports = { registerReading };
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "Dever Markdown blocks",
|
||||
"scopeName": "markdown.dever.codeblock",
|
||||
"injectionSelector": "L:text.html.markdown -markup.fenced_code.block -markup.raw.block -markup.quote -markup.list -meta.embedded.block -comment",
|
||||
"patterns": [
|
||||
{
|
||||
"name": "markup.fenced_code.block.markdown",
|
||||
"contentName": "meta.embedded.block.dever source.dever",
|
||||
"begin": "^ {0,3}(`{3,})[ \\t]*(dever|typescript dever)[ \\t]*$",
|
||||
"end": "^ {0,3}\\1`* *$",
|
||||
"beginCaptures": {
|
||||
"1": { "name": "punctuation.definition.markdown.dever" },
|
||||
"2": { "name": "fenced_code.block.language.markdown" }
|
||||
},
|
||||
"endCaptures": { "0": { "name": "punctuation.definition.markdown.dever" } },
|
||||
"patterns": [{ "include": "source.dever" }]
|
||||
},
|
||||
{
|
||||
"name": "markup.fenced_code.block.markdown",
|
||||
"contentName": "meta.embedded.block.dever source.dever",
|
||||
"begin": "^ {0,3}(~{3,})[ \\t]*(dever|typescript dever)[ \\t]*$",
|
||||
"end": "^ {0,3}\\1~* *$",
|
||||
"beginCaptures": {
|
||||
"1": { "name": "punctuation.definition.markdown.dever" },
|
||||
"2": { "name": "fenced_code.block.language.markdown" }
|
||||
},
|
||||
"endCaptures": { "0": { "name": "punctuation.definition.markdown.dever" } },
|
||||
"patterns": [{ "include": "source.dever" }]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"name": "Dever",
|
||||
"scopeName": "source.dever",
|
||||
"patterns": [
|
||||
{ "include": "#comments" },
|
||||
{ "include": "#strings" },
|
||||
{ "include": "#unsupported" },
|
||||
{ "include": "#declarations" },
|
||||
{ "include": "#constants" },
|
||||
{ "include": "#numbers" },
|
||||
{ "include": "#names" },
|
||||
{ "include": "#keywords" },
|
||||
{ "include": "#operators" }
|
||||
],
|
||||
"repository": {
|
||||
"comments": {
|
||||
"name": "comment.line.number-sign.dever",
|
||||
"begin": "#",
|
||||
"end": "$"
|
||||
},
|
||||
"strings": {
|
||||
"name": "string.quoted.double.dever",
|
||||
"begin": "\"",
|
||||
"end": "\"|$",
|
||||
"beginCaptures": { "0": { "name": "punctuation.definition.string.begin.dever" } },
|
||||
"endCaptures": { "0": { "name": "punctuation.definition.string.end.dever" } },
|
||||
"patterns": [
|
||||
{ "name": "constant.character.escape.dever", "match": "\\\\(?:[\"\\\\nrt]|u\\{[0-9A-Fa-f]{1,6}\\})" },
|
||||
{ "name": "invalid.illegal.escape.dever", "match": "\\\\." }
|
||||
]
|
||||
},
|
||||
"declarations": {
|
||||
"patterns": [
|
||||
{
|
||||
"match": "\\b(type)\\s+([A-Z][A-Za-z0-9_]*)",
|
||||
"captures": {
|
||||
"1": { "name": "storage.type.dever" },
|
||||
"2": { "name": "entity.name.type.dever" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": "^\\s*(?:(public)\\s+)?(get|post|put|delete|cmd|rest)\\b",
|
||||
"captures": {
|
||||
"1": { "name": "storage.modifier.dever" },
|
||||
"2": { "name": "keyword.control.api.dever" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": "^\\s*(external)\\s+(command|exec|pip|npm|go)\\b",
|
||||
"captures": {
|
||||
"1": { "name": "storage.modifier.external.dever" },
|
||||
"2": { "name": "keyword.other.external.dever" }
|
||||
}
|
||||
},
|
||||
{ "name": "keyword.other.declaration.dever", "match": "^\\s*(database|relation|seed|migrate|sql|index|unique|schedule)\\b" }
|
||||
]
|
||||
},
|
||||
"constants": {
|
||||
"name": "constant.language.dever",
|
||||
"match": "\\b(true|false|null|other)\\b"
|
||||
},
|
||||
"unsupported": {
|
||||
"name": "invalid.illegal.keyword.dever",
|
||||
"match": "\\b(package|exposes|if|else|switch|match|when|by|for|while|break|continue|iterate|recur|repeat|return|function|throw|catch|try|import|use|class|interface|unsafe|macro)\\b"
|
||||
},
|
||||
"numbers": {
|
||||
"name": "constant.numeric.dever",
|
||||
"match": "(?<![A-Za-z0-9_])[0-9]+(?:\\.[0-9]+)?(?:[eE][+-]?[0-9]+)?(?![A-Za-z0-9_])"
|
||||
},
|
||||
"names": {
|
||||
"patterns": [
|
||||
{ "name": "variable.other.member.dever", "match": "\\b[a-z_][A-Za-z0-9_]*(?=\\s*:)" },
|
||||
{ "name": "entity.name.function.dever", "match": "\\b[a-z_][A-Za-z0-9_]*(?=\\s*\\()" },
|
||||
{ "name": "support.type.dever", "match": "\\b(Int|Float|Decimal|Bool|Text|Bytes|Secret|List|Map|Stream|AsyncStream|Channel|Task|Group|Json|Date|Time|DateTime|Duration|Id|Uuid)\\b" },
|
||||
{ "name": "entity.name.type.dever", "match": "\\b[A-Z][A-Za-z0-9_]*\\b" },
|
||||
{ "name": "entity.name.namespace.dever", "match": "\\b(dever|app|domain|model|port|adapter)(?=\\.)" }
|
||||
]
|
||||
},
|
||||
"keywords": {
|
||||
"patterns": [
|
||||
{ "name": "keyword.operator.logical.dever", "match": "\\b(and|or|not)\\b" },
|
||||
{ "name": "storage.modifier.dever", "match": "\\b(public|global|pure|transaction|job|recover|fails|private|owner)\\b" },
|
||||
{ "name": "keyword.other.model.dever", "match": "\\b(default|unique|index|raw|lower|upper|before|after|drop)\\b" },
|
||||
{ "name": "keyword.other.adapter.dever", "match": "\\b(setting|lib|allow)\\b" }
|
||||
]
|
||||
},
|
||||
"operators": {
|
||||
"patterns": [
|
||||
{ "name": "keyword.operator.dever", "match": "//|==|!=|<=|>=|[=+*/%<>?-]" },
|
||||
{ "name": "punctuation.section.dever", "match": "[{}()\\[\\]]" },
|
||||
{ "name": "punctuation.separator.dever", "match": "[,:.]" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
# Dever 示例
|
||||
|
||||
入门项目见 [随包示例](../skills/references/examples.md)。CMS 演示多租户内容管理,包括登录、权限、文章修订、上传和定时发布。
|
||||
|
||||
CMS 只维护一份 [Dever 源码](cms/dever/module/news/article/api/admin/manage.dever),领域说明存放在 README 和 doc 中:
|
||||
|
||||
```text
|
||||
module/
|
||||
platform/tenant/ 平台租户
|
||||
user/account/ 凭据、成员、会话、bootstrap CMD
|
||||
user/authorization/ 核心角色授权的业务入口
|
||||
content/category/ 分类
|
||||
content/revision/ 文章编辑与修订快照
|
||||
media/asset/ 上传、Storage Port/Adapter
|
||||
publishing/schedule/ 定时发布与 Job
|
||||
audit/operation/ 操作审计
|
||||
system/health/ 健康 CMD
|
||||
news/article/
|
||||
app/editing.dever
|
||||
app/publication.dever
|
||||
api/admin/manage.dever
|
||||
api/front/browse.dever
|
||||
domain.dever
|
||||
model.dever
|
||||
model/publication.dever
|
||||
test/
|
||||
content/category/lifecycle.dever
|
||||
news/article/publish.dever
|
||||
user/account/bootstrap.dever
|
||||
user/account/validation.dever
|
||||
user/account/lifecycle.dever
|
||||
```
|
||||
|
||||
## 数据和权限
|
||||
|
||||
平台租户、账号、成员和会话使用 `global type`,保存在平台库。文章、分类、修订、媒体、排期和审计放在各租户自己的数据库。
|
||||
|
||||
账户、成员和会话同属账户领域,避免登录调用与 Model ID 引用形成循环依赖。租户文章的作者编号来自已验证身份,使用 Int 存储;物理外键只连接同一个数据库中的 Model,不能从租户库指向平台账户库。
|
||||
|
||||
verify 先核实身份数据,核心再按站点和角色检查接口权限,通过后才访问租户数据。
|
||||
|
||||
文章接口位于 `/news/article/admin/manage/`:create 创建,list/detail 查询,remove 删除草稿,publish 发布。
|
||||
|
||||
编辑使用 `/content/revision/admin/manage/create`,传入文章 ID、expected_version、标题正文和可空分类/媒体 ID。版本冲突返回 409,文章更新与修订快照一起提交。文章不用自动 REST 写接口,以免跳过这些规则;列表返回 `app.ArticleView`,不泄漏私有 Model。
|
||||
|
||||
定时发布使用 `/publishing/schedule/admin/manage/schedule`,传入文章 ID 和 publish_at。创建时检查作者和草稿状态;每个排期有独立任务,不会合并同时发布的文章。
|
||||
|
||||
执行任务前,核心再次核验身份和权限。发布、审计和排期状态一起提交或回滚,完成任务重复执行不重复写入。手动重复发布返回 409。
|
||||
|
||||
## 先检查,再运行
|
||||
|
||||
在仓库根目录执行:
|
||||
|
||||
```bash
|
||||
dever check examples/cms/dever
|
||||
dever test examples/cms/dever --filter news.article.publish
|
||||
dever fmt examples/cms/dever --check
|
||||
```
|
||||
|
||||
测试使用临时 SQLite,不连接部署数据库。去掉 `--filter` 可执行全部应用测试。
|
||||
|
||||
实际部署前先替换 JWT secret,并把两个站点的 origin 和 Host 改成真实 HTTPS 入口;origin 是用户访问的外部地址,不是内部监听地址。
|
||||
|
||||
用 bootstrap CMD 建立平台租户、Owner 成员和密码:
|
||||
|
||||
```sh
|
||||
dever run examples/cms/dever -- user.account.bootstrap '{"tenant_key":"tenant-one","tenant_name":"Tenant One","email":"owner@example.com","display_name":"Owner","password":"change-me-now"}'
|
||||
```
|
||||
|
||||
替换示例账号与密码,再用返回的真实 tenant_id 和 user_id 执行以下命令,不要猜测 ID 是 1:
|
||||
|
||||
```text
|
||||
dever tenant migrate examples/cms/dever <tenant_id>
|
||||
dever tenant owner examples/cms/dever <tenant_id> admin <user_id>
|
||||
dever tenant owner examples/cms/dever <tenant_id> front <user_id>
|
||||
```
|
||||
|
||||
这会迁移租户数据库,并建立后台、前台的核心 Owner。之后各站点通过 `user/authorization/<site>/manage` 管理普通角色。运行配置在项目的 `config/setting.json`,数据在 `data/`;`dever run examples/cms/dever` 不带 CMD 参数时启动 HTTP 服务。
|
||||
|
||||
应用测试覆盖分类、发布规则、账号和会话。真实登录、HTTP、租户隔离及发布流程使用 [CMS 验收入口](../test/performance/cms.py)。
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"runtime": {
|
||||
"mode": "all",
|
||||
"shutdown_ms": 30000
|
||||
},
|
||||
"job": {
|
||||
"workers": 1,
|
||||
"poll_ms": 1000,
|
||||
"lease_ms": 60000,
|
||||
"retry_base_ms": 1000,
|
||||
"retry_max_ms": 300000
|
||||
},
|
||||
"http": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 8080
|
||||
},
|
||||
"database": {
|
||||
"default": {
|
||||
"type": "sqlite",
|
||||
"path": "data/db/platform.db",
|
||||
"tenant_directory": "data/tenants",
|
||||
"max_connections": 4,
|
||||
"max_page_size": 100
|
||||
}
|
||||
},
|
||||
"tenant": {
|
||||
"database": "default",
|
||||
"max_pools": 32,
|
||||
"idle_timeout_ms": 300000
|
||||
},
|
||||
"auth": {
|
||||
"providers": {
|
||||
"session": {
|
||||
"verify": "user.account.verify",
|
||||
"jwtSecret": "replace-this-example-secret-before-deployment-2026",
|
||||
"ttlSeconds": 3600,
|
||||
"cookie": "cms_session"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sites": {
|
||||
"admin": {
|
||||
"path": "admin",
|
||||
"hosts": ["cms.example.com"],
|
||||
"auth": "session",
|
||||
"origin": "https://cms.example.com"
|
||||
},
|
||||
"front": {
|
||||
"path": "front",
|
||||
"hosts": ["cms.example.com"],
|
||||
"auth": "session",
|
||||
"origin": "https://cms.example.com"
|
||||
}
|
||||
},
|
||||
"log": {
|
||||
"level": "info"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# 操作审计
|
||||
|
||||
保存登录、退出、发布、上传和授权等操作记录。
|
||||
|
||||
- [操作审计应用](doc/app.md)
|
||||
- [操作审计模型](doc/model.md)
|
||||
- [调用流程](link.md)
|
||||
@@ -0,0 +1,10 @@
|
||||
record(kind: model.OperationKind, resource: Text, resource_id: Text) () {
|
||||
stored = model.create(
|
||||
{
|
||||
kind = kind
|
||||
resource = resource
|
||||
resource_id = resource_id
|
||||
}
|
||||
)
|
||||
discarded = stored
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# 操作审计应用
|
||||
|
||||
## audit.operation.record
|
||||
|
||||
保存一次操作审计,记录操作类型、资源名称和资源标识。
|
||||
@@ -0,0 +1,9 @@
|
||||
# 操作审计模型
|
||||
|
||||
## audit.operation.model.OperationKind
|
||||
|
||||
审计操作类型:登录、退出、发布、上传和授权。
|
||||
|
||||
## audit.operation.model.Operation
|
||||
|
||||
租户操作审计记录,包含操作者、操作类型和目标资源。
|
||||
@@ -0,0 +1,60 @@
|
||||
<!-- Generated by dever inspect --links. Do not edit. -->
|
||||
<!-- source-sha256: 5cc5a514d99bdf266632507f65376a89a8ba16e0a87baa6f0f2be4435fa35943 -->
|
||||
|
||||
# audit.operation
|
||||
|
||||
按求值顺序展示调用、分支和任务调度,包含 API 与自动 REST 入口。点击节点打开源码;双框为调用,虚线为调度或错误路径。外部库和驱动不展开。
|
||||
|
||||
图不是某次运行的日志,分句和 Adapter 按输入或配置选择。修改源码后运行 `dever inspect . --links`。
|
||||
|
||||
## 入口
|
||||
|
||||
没有已收录的入口。
|
||||
|
||||
## 外部调用方
|
||||
|
||||
| 能力 | 调用方 | 方式 | 调用位置 |
|
||||
| --- | --- | --- | --- |
|
||||
| [audit.operation.record](app.dever#L1) | [media.asset.record](../../media/asset/app.dever#L17) | 调用 | [../../media/asset/app.dever:26:3](../../media/asset/app.dever#L26,3) |
|
||||
| [audit.operation.record](app.dever#L1) | [news.article.publishDraft](../../news/article/app/publication.dever#L48) | 调用 | [../../news/article/app/publication.dever:65:3](../../news/article/app/publication.dever#L65,3) |
|
||||
|
||||
## 能力与内部调用
|
||||
|
||||
### audit.operation.model.Operation
|
||||
|
||||
定义:[model.dever:9:1](model.dever#L9,1)
|
||||
|
||||
没有已收录的下游调用。
|
||||
|
||||
### audit.operation.model.OperationKind
|
||||
|
||||
定义:[model.dever:1:1](model.dever#L1,1)
|
||||
|
||||
没有已收录的下游调用。
|
||||
|
||||
### audit.operation.record
|
||||
|
||||
定义:[app.dever:1:1](app.dever#L1,1)
|
||||
|
||||
<a id="flow-c1586d946bef8bfb-1"></a>
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
n0(["record"])
|
||||
click n0 href "app.dever#L1,1" "打开源码"
|
||||
n1(["返回"])
|
||||
n3["audit.operation.model.create"]
|
||||
click n3 href "app.dever#L2,12" "打开源码"
|
||||
n4["赋值 stored"]
|
||||
click n4 href "app.dever#L2,12" "打开源码"
|
||||
n5["赋值 discarded"]
|
||||
click n5 href "app.dever#L9,15" "打开源码"
|
||||
n0 --> n3
|
||||
n3 --> n4
|
||||
n4 --> n5
|
||||
n5 --> n1
|
||||
```
|
||||
|
||||
| 目标 | 归属 | 方式 | 调用位置 |
|
||||
| --- | --- | --- | --- |
|
||||
| [audit.operation.model.create](model.dever#L9) | 领域内 | 内建操作 | [app.dever:2:12](app.dever#L2,12) |
|
||||
@@ -0,0 +1,14 @@
|
||||
type OperationKind {
|
||||
Login = "登录"
|
||||
Logout = "登出"
|
||||
Publish = "发布"
|
||||
Upload = "上传"
|
||||
Access = "授权"
|
||||
}
|
||||
|
||||
type Operation {
|
||||
actor_id: Int?
|
||||
kind: OperationKind index
|
||||
resource: Text(1, 120)
|
||||
resource_id: Text(1, 120)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
# 内容分类
|
||||
|
||||
创建分类并管理启用状态,供文章校验所选分类。
|
||||
|
||||
- [分类接口](doc/api-admin-manage.md)
|
||||
- [内容分类应用](doc/app.md)
|
||||
- [内容分类模型](doc/model.md)
|
||||
- [调用流程](link.md)
|
||||
@@ -0,0 +1,3 @@
|
||||
post create = app.create
|
||||
|
||||
delete disable = app.disable
|
||||
@@ -0,0 +1,39 @@
|
||||
type CategoryView {
|
||||
id: model.id
|
||||
name: Text
|
||||
slug: Text
|
||||
status: Text
|
||||
}
|
||||
|
||||
create(name: Text) (category: CategoryView) {
|
||||
stored = model.create(
|
||||
{
|
||||
name = name
|
||||
slug = name
|
||||
}
|
||||
)
|
||||
category = CategoryView {
|
||||
id = stored.id
|
||||
name = stored.name
|
||||
slug = stored.slug
|
||||
status = "active"
|
||||
}
|
||||
}
|
||||
|
||||
disable(id: model.id) (ok: Bool) {
|
||||
changed = model.update(
|
||||
id,
|
||||
{
|
||||
status = model.CategoryStatus.Disabled
|
||||
}
|
||||
)
|
||||
discarded = changed
|
||||
ok = true
|
||||
}
|
||||
|
||||
requireActive(id: model.id) () {
|
||||
stored = model.get(id)
|
||||
dever.api.require(stored.status == model.CategoryStatus.Active, dever.api.Error.Conflict)
|
||||
}
|
||||
|
||||
requireActive(id: null) () {}
|
||||
@@ -0,0 +1,9 @@
|
||||
# 分类接口
|
||||
|
||||
## content.category.api.admin.manage.create
|
||||
|
||||
创建分类。
|
||||
|
||||
## content.category.api.admin.manage.disable
|
||||
|
||||
禁用分类。
|
||||
@@ -0,0 +1,17 @@
|
||||
# 内容分类应用
|
||||
|
||||
## content.category.CategoryView
|
||||
|
||||
分类接口返回的编号、名称、slug 和状态。
|
||||
|
||||
## content.category.create
|
||||
|
||||
创建分类。名称去除首尾空白,slug 同时转为小写并保持唯一。
|
||||
|
||||
## content.category.disable
|
||||
|
||||
把已有分类标为禁用,记录仍保留。
|
||||
|
||||
## content.category.requireActive
|
||||
|
||||
分类编号为空时跳过;否则要求分类已启用,禁用时返回冲突。
|
||||
@@ -0,0 +1,9 @@
|
||||
# 内容分类模型
|
||||
|
||||
## content.category.model.CategoryStatus
|
||||
|
||||
分类的启用或禁用状态。
|
||||
|
||||
## content.category.model.Category
|
||||
|
||||
分类名称、唯一 slug 和状态。
|
||||
@@ -0,0 +1,402 @@
|
||||
<!-- Generated by dever inspect --links. Do not edit. -->
|
||||
<!-- source-sha256: 22ff06f2e26c68d4bf521c52d2ecba992a5af0f615c4f380876f2fe34a8b4aa0 -->
|
||||
|
||||
# content.category
|
||||
|
||||
按求值顺序展示调用、分支和任务调度,包含 API 与自动 REST 入口。点击节点打开源码;双框为调用,虚线为调度或错误路径。外部库和驱动不展开。
|
||||
|
||||
图不是某次运行的日志,分句和 Adapter 按输入或配置选择。修改源码后运行 `dever inspect . --links`。
|
||||
|
||||
## 入口
|
||||
|
||||
- API [content.category.api.admin.manage.create](api/admin/manage.dever#L1) → [content.category.create](app.dever#L8)
|
||||
- API [content.category.api.admin.manage.disable](api/admin/manage.dever#L3) → [content.category.disable](app.dever#L23)
|
||||
|
||||
## 外部调用方
|
||||
|
||||
| 能力 | 调用方 | 方式 | 调用位置 |
|
||||
| --- | --- | --- | --- |
|
||||
| [content.category.requireActive](app.dever#L34) | [news.article.create](../../news/article/app/editing.dever#L16) | 调用 | [../../news/article/app/editing.dever:24:3](../../news/article/app/editing.dever#L24,3) |
|
||||
| [content.category.requireActive](app.dever#L34) | [news.article.edit](../../news/article/app/editing.dever#L77) | 调用 | [../../news/article/app/editing.dever:89:3](../../news/article/app/editing.dever#L89,3) |
|
||||
| [content.category.requireActive](app.dever#L34) | [news.article.publishDraft](../../news/article/app/publication.dever#L48) | 调用 | [../../news/article/app/publication.dever:51:3](../../news/article/app/publication.dever#L51,3) |
|
||||
|
||||
## 能力与内部调用
|
||||
|
||||
### content.category.CategoryView
|
||||
|
||||
定义:[app.dever:1:1](app.dever#L1,1)
|
||||
|
||||
没有已收录的下游调用。
|
||||
|
||||
### content.category.api.admin.manage.create
|
||||
|
||||
定义:[api/admin/manage.dever:1:1](api/admin/manage.dever#L1,1)
|
||||
|
||||
<a id="flow-d5f276e1d0182184-1"></a>
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
n0(["content.category.api.admin.manage.create"])
|
||||
click n0 href "api/admin/manage.dever#L1,15" "打开源码"
|
||||
n1{"路由 POST /content/category/admin/manage/create,topic #34;admin/manage#34; 接受请求?"}
|
||||
n2(["返回 HTTP 404"])
|
||||
n3["进入 API"]
|
||||
n4["取得认证时间"]
|
||||
n5(["返回 HTTP 500"])
|
||||
n6["按 topic #34;admin/manage#34; 选择站点与 Provider"]
|
||||
n7(["返回 HTTP 403"])
|
||||
n8{"读取 Bearer 或站点 Cookie;两者互斥"}
|
||||
n9(["返回 HTTP 401"])
|
||||
n10["认证上下文就绪"]
|
||||
n11(["返回 HTTP 401"])
|
||||
n12["校验 Token 的签名、有效期及站点"]
|
||||
n13{"Cookie 且非 GET / HEAD / OPTIONS?"}
|
||||
n14["检查站点 Origin"]
|
||||
n15(["返回 HTTP 403"])
|
||||
n16(["返回 HTTP 401"])
|
||||
n17{"按 Provider.verify 选择验证函数"}
|
||||
n18[["user.account.verify"]]
|
||||
click n18 href "api/admin/manage.dever#L1,15" "打开源码"
|
||||
n19(["返回 HTTP 业务错误映射状态;其他故障 500"])
|
||||
n20["用验证结果建立身份"]
|
||||
n21(["返回 HTTP 认证错误状态"])
|
||||
n22(["返回 HTTP 500"])
|
||||
n23["建立请求数据库上下文"]
|
||||
n24{"有已验证租户?"}
|
||||
n25["数据库上下文就绪"]
|
||||
n26{"部署启用了租户存储?"}
|
||||
n27(["返回 HTTP 403"])
|
||||
n28["按已验证租户选择数据库"]
|
||||
n29(["返回 HTTP 503"])
|
||||
n30{"检查租户组件启用状态:content"}
|
||||
n31(["返回 HTTP 403"])
|
||||
n32(["返回 HTTP 503"])
|
||||
n33["进入写入口事务"]
|
||||
n34["失败:保留 HTTP 错误状态"]
|
||||
n35["回滚入口拥有的事务"]
|
||||
n36(["返回 HTTP 500"])
|
||||
n37(["返回错误响应"])
|
||||
n38{"权限检查:content.category.{site}.create"}
|
||||
n39["权限通过"]
|
||||
n40["读取 API 输入"]
|
||||
n41["解码具体参数并检查字段合同"]
|
||||
n42[["content.category.create"]]
|
||||
click n42 href "api/admin/manage.dever#L1,15" "打开源码"
|
||||
n43["按输出类型检查可序列化值并编码响应"]
|
||||
n44["提交入口拥有的事务"]
|
||||
n45(["返回 HTTP 500"])
|
||||
n46["发布暂存的认证 Header / Cookie"]
|
||||
n47(["返回成功响应"])
|
||||
n0 --> n1
|
||||
n1 -->|"未匹配此入口:继续路由;均未匹配"| n2
|
||||
n1 --> n3
|
||||
n3 --> n4
|
||||
n4 -->|"失败"| n5
|
||||
n4 --> n6
|
||||
n6 -->|"无站点绑定"| n7
|
||||
n6 --> n8
|
||||
n8 -->|"凭据格式错误或冲突"| n9
|
||||
n8 -->|"无凭据"| n11
|
||||
n8 -->|"有凭据"| n13
|
||||
n13 -->|"否"| n12
|
||||
n13 -->|"是"| n14
|
||||
n14 -->|"不符合"| n15
|
||||
n14 -->|"通过"| n12
|
||||
n12 -->|"无 Provider 或 Token 无效"| n16
|
||||
n12 --> n17
|
||||
n17 -->|"user.account.verify"| n18
|
||||
n18 -->|"验证函数失败"| n19
|
||||
n18 --> n20
|
||||
n20 -->|"身份不合法"| n21
|
||||
n20 -->|"通过"| n10
|
||||
n17 -->|"无匹配验证函数"| n22
|
||||
n10 --> n23
|
||||
n23 --> n24
|
||||
n24 -->|"否"| n26
|
||||
n26 -->|"是:缺少租户身份"| n27
|
||||
n26 -->|"否"| n25
|
||||
n24 -->|"是"| n28
|
||||
n28 -->|"选择失败"| n29
|
||||
n28 --> n30
|
||||
n30 -->|"未启用"| n31
|
||||
n30 -->|"查询失败"| n32
|
||||
n30 -->|"允许"| n25
|
||||
n25 --> n33
|
||||
n34 --> n35
|
||||
n35 -->|"事务结束失败"| n36
|
||||
n35 --> n37
|
||||
n33 --> n38
|
||||
n38 -->|"拒绝:Access 状态;查询故障:500"| n34
|
||||
n38 -->|"允许"| n39
|
||||
n39 --> n40
|
||||
n40 -->|"失败:输入错误状态"| n34
|
||||
n40 --> n41
|
||||
n41 -->|"失败:400"| n34
|
||||
n41 --> n42
|
||||
n42 -->|"业务错误映射状态;其他故障:500"| n34
|
||||
n42 --> n43
|
||||
n43 -->|"编码失败:500"| n34
|
||||
n43 --> n44
|
||||
n44 -->|"提交失败"| n45
|
||||
n44 --> n46
|
||||
n46 --> n47
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>源码与子流程</summary>
|
||||
|
||||
| 节点 | 源码 | 子流程 |
|
||||
| --- | --- | --- |
|
||||
| user.account.verify | [api/admin/manage.dever:1:15](api/admin/manage.dever#L1,15) | [user.account.verify](../../user/account/link.md#flow-aaaacb87d61f819c-1) |
|
||||
| content.category.create | [api/admin/manage.dever:1:15](api/admin/manage.dever#L1,15) | [content.category.create](link.md#flow-dffb2069d02e287c-1) |
|
||||
|
||||
</details>
|
||||
|
||||
| 目标 | 归属 | 方式 | 调用位置 |
|
||||
| --- | --- | --- | --- |
|
||||
| [content.category.create](app.dever#L8) | 领域内 | API 绑定 | [api/admin/manage.dever:1:15](api/admin/manage.dever#L1,15) |
|
||||
| [user.account.verify](../../user/account/app/account.dever#L133) | 跨领域 [user.account](../../user/account/link.md) | 身份验证 | [api/admin/manage.dever:1:15](api/admin/manage.dever#L1,15) |
|
||||
|
||||
### content.category.api.admin.manage.disable
|
||||
|
||||
定义:[api/admin/manage.dever:3:1](api/admin/manage.dever#L3,1)
|
||||
|
||||
<a id="flow-5e559918fcf97f3a-1"></a>
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
n0(["content.category.api.admin.manage.disable"])
|
||||
click n0 href "api/admin/manage.dever#L3,18" "打开源码"
|
||||
n1{"路由 DELETE /content/category/admin/manage/disable,topic #34;admin/manage#34; 接受请求?"}
|
||||
n2(["返回 HTTP 404"])
|
||||
n3["进入 API"]
|
||||
n4["取得认证时间"]
|
||||
n5(["返回 HTTP 500"])
|
||||
n6["按 topic #34;admin/manage#34; 选择站点与 Provider"]
|
||||
n7(["返回 HTTP 403"])
|
||||
n8{"读取 Bearer 或站点 Cookie;两者互斥"}
|
||||
n9(["返回 HTTP 401"])
|
||||
n10["认证上下文就绪"]
|
||||
n11(["返回 HTTP 401"])
|
||||
n12["校验 Token 的签名、有效期及站点"]
|
||||
n13{"Cookie 且非 GET / HEAD / OPTIONS?"}
|
||||
n14["检查站点 Origin"]
|
||||
n15(["返回 HTTP 403"])
|
||||
n16(["返回 HTTP 401"])
|
||||
n17{"按 Provider.verify 选择验证函数"}
|
||||
n18[["user.account.verify"]]
|
||||
click n18 href "api/admin/manage.dever#L3,18" "打开源码"
|
||||
n19(["返回 HTTP 业务错误映射状态;其他故障 500"])
|
||||
n20["用验证结果建立身份"]
|
||||
n21(["返回 HTTP 认证错误状态"])
|
||||
n22(["返回 HTTP 500"])
|
||||
n23["建立请求数据库上下文"]
|
||||
n24{"有已验证租户?"}
|
||||
n25["数据库上下文就绪"]
|
||||
n26{"部署启用了租户存储?"}
|
||||
n27(["返回 HTTP 403"])
|
||||
n28["按已验证租户选择数据库"]
|
||||
n29(["返回 HTTP 503"])
|
||||
n30{"检查租户组件启用状态:content"}
|
||||
n31(["返回 HTTP 403"])
|
||||
n32(["返回 HTTP 503"])
|
||||
n33["进入写入口事务"]
|
||||
n34["失败:保留 HTTP 错误状态"]
|
||||
n35["回滚入口拥有的事务"]
|
||||
n36(["返回 HTTP 500"])
|
||||
n37(["返回错误响应"])
|
||||
n38{"权限检查:content.category.{site}.disable"}
|
||||
n39["权限通过"]
|
||||
n40["读取 API 输入"]
|
||||
n41["解码具体参数并检查字段合同"]
|
||||
n42[["content.category.disable"]]
|
||||
click n42 href "api/admin/manage.dever#L3,18" "打开源码"
|
||||
n43["按输出类型检查可序列化值并编码响应"]
|
||||
n44["提交入口拥有的事务"]
|
||||
n45(["返回 HTTP 500"])
|
||||
n46["发布暂存的认证 Header / Cookie"]
|
||||
n47(["返回成功响应"])
|
||||
n0 --> n1
|
||||
n1 -->|"未匹配此入口:继续路由;均未匹配"| n2
|
||||
n1 --> n3
|
||||
n3 --> n4
|
||||
n4 -->|"失败"| n5
|
||||
n4 --> n6
|
||||
n6 -->|"无站点绑定"| n7
|
||||
n6 --> n8
|
||||
n8 -->|"凭据格式错误或冲突"| n9
|
||||
n8 -->|"无凭据"| n11
|
||||
n8 -->|"有凭据"| n13
|
||||
n13 -->|"否"| n12
|
||||
n13 -->|"是"| n14
|
||||
n14 -->|"不符合"| n15
|
||||
n14 -->|"通过"| n12
|
||||
n12 -->|"无 Provider 或 Token 无效"| n16
|
||||
n12 --> n17
|
||||
n17 -->|"user.account.verify"| n18
|
||||
n18 -->|"验证函数失败"| n19
|
||||
n18 --> n20
|
||||
n20 -->|"身份不合法"| n21
|
||||
n20 -->|"通过"| n10
|
||||
n17 -->|"无匹配验证函数"| n22
|
||||
n10 --> n23
|
||||
n23 --> n24
|
||||
n24 -->|"否"| n26
|
||||
n26 -->|"是:缺少租户身份"| n27
|
||||
n26 -->|"否"| n25
|
||||
n24 -->|"是"| n28
|
||||
n28 -->|"选择失败"| n29
|
||||
n28 --> n30
|
||||
n30 -->|"未启用"| n31
|
||||
n30 -->|"查询失败"| n32
|
||||
n30 -->|"允许"| n25
|
||||
n25 --> n33
|
||||
n34 --> n35
|
||||
n35 -->|"事务结束失败"| n36
|
||||
n35 --> n37
|
||||
n33 --> n38
|
||||
n38 -->|"拒绝:Access 状态;查询故障:500"| n34
|
||||
n38 -->|"允许"| n39
|
||||
n39 --> n40
|
||||
n40 -->|"失败:输入错误状态"| n34
|
||||
n40 --> n41
|
||||
n41 -->|"失败:400"| n34
|
||||
n41 --> n42
|
||||
n42 -->|"业务错误映射状态;其他故障:500"| n34
|
||||
n42 --> n43
|
||||
n43 -->|"编码失败:500"| n34
|
||||
n43 --> n44
|
||||
n44 -->|"提交失败"| n45
|
||||
n44 --> n46
|
||||
n46 --> n47
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>源码与子流程</summary>
|
||||
|
||||
| 节点 | 源码 | 子流程 |
|
||||
| --- | --- | --- |
|
||||
| user.account.verify | [api/admin/manage.dever:3:18](api/admin/manage.dever#L3,18) | [user.account.verify](../../user/account/link.md#flow-aaaacb87d61f819c-1) |
|
||||
| content.category.disable | [api/admin/manage.dever:3:18](api/admin/manage.dever#L3,18) | [content.category.disable](link.md#flow-85101fdf961a71c3-1) |
|
||||
|
||||
</details>
|
||||
|
||||
| 目标 | 归属 | 方式 | 调用位置 |
|
||||
| --- | --- | --- | --- |
|
||||
| [content.category.disable](app.dever#L23) | 领域内 | API 绑定 | [api/admin/manage.dever:3:18](api/admin/manage.dever#L3,18) |
|
||||
| [user.account.verify](../../user/account/app/account.dever#L133) | 跨领域 [user.account](../../user/account/link.md) | 身份验证 | [api/admin/manage.dever:3:18](api/admin/manage.dever#L3,18) |
|
||||
|
||||
### content.category.create
|
||||
|
||||
定义:[app.dever:8:1](app.dever#L8,1)
|
||||
|
||||
<a id="flow-dffb2069d02e287c-1"></a>
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
n0(["create"])
|
||||
click n0 href "app.dever#L8,1" "打开源码"
|
||||
n1(["返回"])
|
||||
n3["content.category.model.create"]
|
||||
click n3 href "app.dever#L9,12" "打开源码"
|
||||
n4["赋值 stored"]
|
||||
click n4 href "app.dever#L9,12" "打开源码"
|
||||
n5["赋值 category"]
|
||||
click n5 href "app.dever#L15,14" "打开源码"
|
||||
n0 --> n3
|
||||
n3 --> n4
|
||||
n4 --> n5
|
||||
n5 --> n1
|
||||
```
|
||||
|
||||
| 目标 | 归属 | 方式 | 调用位置 |
|
||||
| --- | --- | --- | --- |
|
||||
| [content.category.model.create](model.dever#L6) | 领域内 | 内建操作 | [app.dever:9:12](app.dever#L9,12) |
|
||||
|
||||
### content.category.disable
|
||||
|
||||
定义:[app.dever:23:1](app.dever#L23,1)
|
||||
|
||||
<a id="flow-85101fdf961a71c3-1"></a>
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
n0(["disable"])
|
||||
click n0 href "app.dever#L23,1" "打开源码"
|
||||
n1(["返回"])
|
||||
n3["content.category.model.update"]
|
||||
click n3 href "app.dever#L24,13" "打开源码"
|
||||
n4["赋值 changed"]
|
||||
click n4 href "app.dever#L24,13" "打开源码"
|
||||
n5["赋值 discarded"]
|
||||
click n5 href "app.dever#L30,15" "打开源码"
|
||||
n6["赋值 ok"]
|
||||
click n6 href "app.dever#L31,8" "打开源码"
|
||||
n0 --> n3
|
||||
n3 --> n4
|
||||
n4 --> n5
|
||||
n5 --> n6
|
||||
n6 --> n1
|
||||
```
|
||||
|
||||
| 目标 | 归属 | 方式 | 调用位置 |
|
||||
| --- | --- | --- | --- |
|
||||
| [content.category.model.update](model.dever#L6) | 领域内 | 内建操作 | [app.dever:24:13](app.dever#L24,13) |
|
||||
|
||||
### content.category.model.Category
|
||||
|
||||
定义:[model.dever:6:1](model.dever#L6,1)
|
||||
|
||||
没有已收录的下游调用。
|
||||
|
||||
### content.category.model.CategoryStatus
|
||||
|
||||
定义:[model.dever:1:1](model.dever#L1,1)
|
||||
|
||||
没有已收录的下游调用。
|
||||
|
||||
### content.category.requireActive
|
||||
|
||||
定义:[app.dever:34:1](app.dever#L34,1)、[app.dever:39:1](app.dever#L39,1)
|
||||
|
||||
<a id="flow-eee0931d93e09e79-1"></a>
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
n0(["requireActive"])
|
||||
click n0 href "app.dever#L34,1" "打开源码"
|
||||
n1(["返回"])
|
||||
n3{"按实参匹配分句(互斥)"}
|
||||
n4["id: model.id"]
|
||||
click n4 href "app.dever#L34,1" "打开源码"
|
||||
n5["content.category.model.get"]
|
||||
click n5 href "app.dever#L35,12" "打开源码"
|
||||
n6["赋值 stored"]
|
||||
click n6 href "app.dever#L35,12" "打开源码"
|
||||
n7[["dever.api.require"]]
|
||||
click n7 href "app.dever#L36,3" "打开源码"
|
||||
n8["id: null"]
|
||||
click n8 href "app.dever#L39,1" "打开源码"
|
||||
n0 --> n3
|
||||
n3 -->|"id: model.id"| n4
|
||||
n4 --> n5
|
||||
n5 --> n6
|
||||
n6 --> n7
|
||||
n7 --> n1
|
||||
n3 -->|"id: null"| n8
|
||||
n8 --> n1
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>源码与子流程</summary>
|
||||
|
||||
| 节点 | 源码 | 子流程 |
|
||||
| --- | --- | --- |
|
||||
| dever.api.require | [app.dever:36:3](app.dever#L36,3) | 依赖或生成的分派(本页不展开) |
|
||||
|
||||
</details>
|
||||
|
||||
| 目标 | 归属 | 方式 | 调用位置 |
|
||||
| --- | --- | --- | --- |
|
||||
| [content.category.model.get](model.dever#L6) | 领域内 | 内建操作 | [app.dever:35:12](app.dever#L35,12) |
|
||||
| `dever.api.require` | 依赖 | 调用 | [app.dever:36:3](app.dever#L36,3) |
|
||||
@@ -0,0 +1,10 @@
|
||||
type CategoryStatus {
|
||||
Active = "启用"
|
||||
Disabled = "禁用"
|
||||
}
|
||||
|
||||
type Category {
|
||||
name: Text(1, 80)
|
||||
slug: Text(1, 100) lower unique
|
||||
status: CategoryStatus default CategoryStatus.Active index
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
# 内容修订
|
||||
|
||||
在编辑文章的同一事务中保存版本快照。
|
||||
|
||||
- [修订接口](doc/api-admin-manage.md)
|
||||
- [内容修订应用](doc/app.md)
|
||||
- [内容修订模型](doc/model.md)
|
||||
- [调用流程](link.md)
|
||||
@@ -0,0 +1 @@
|
||||
post create = app.create
|
||||
@@ -0,0 +1,45 @@
|
||||
type RevisionView {
|
||||
id: model.id
|
||||
article_id: news.article.model.id
|
||||
version: Int
|
||||
title: Text
|
||||
body: Text
|
||||
status: Text
|
||||
}
|
||||
|
||||
transaction create(
|
||||
selected_article_id: news.article.model.id,
|
||||
expected_version: Int,
|
||||
title: Text,
|
||||
body: Text,
|
||||
category_id: content.category.model.id?,
|
||||
asset_id: media.asset.model.id?
|
||||
) (revision: RevisionView) {
|
||||
edited = news.article.edit(
|
||||
selected_article_id,
|
||||
expected_version,
|
||||
title,
|
||||
body,
|
||||
category_id,
|
||||
asset_id
|
||||
)
|
||||
stored = model.create(
|
||||
{
|
||||
article_id = selected_article_id
|
||||
version = edited.version
|
||||
title = edited.title
|
||||
body = edited.body
|
||||
category_id = edited.category_id
|
||||
asset_id = edited.asset_id
|
||||
status = model.RevisionStatus.Draft
|
||||
}
|
||||
)
|
||||
revision = RevisionView {
|
||||
id = stored.id
|
||||
article_id = stored.article_id
|
||||
version = stored.version
|
||||
title = stored.title
|
||||
body = stored.body
|
||||
status = "draft"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# 修订接口
|
||||
|
||||
## content.revision.api.admin.manage.create
|
||||
|
||||
按版本编辑文章,并保存修订快照。
|
||||
@@ -0,0 +1,9 @@
|
||||
# 内容修订应用
|
||||
|
||||
## content.revision.RevisionView
|
||||
|
||||
编辑后返回的修订编号、文章编号、版本及内容。
|
||||
|
||||
## content.revision.create
|
||||
|
||||
按 expected_version 编辑文章并保存修订快照,两步在同一事务中完成。版本冲突、分类或媒体不可用时整次操作失败。
|
||||
@@ -0,0 +1,9 @@
|
||||
# 内容修订模型
|
||||
|
||||
## content.revision.model.RevisionStatus
|
||||
|
||||
修订的草稿或已发布状态。
|
||||
|
||||
## content.revision.model.Revision
|
||||
|
||||
文章某一版本的内容快照;同一文章和版本只能保存一条。
|
||||
@@ -0,0 +1,212 @@
|
||||
<!-- Generated by dever inspect --links. Do not edit. -->
|
||||
<!-- source-sha256: 3a206461ddeadfa4087f3a0db7ee697474811b4cc2f983c8d57592e0bf48eeed -->
|
||||
|
||||
# content.revision
|
||||
|
||||
按求值顺序展示调用、分支和任务调度,包含 API 与自动 REST 入口。点击节点打开源码;双框为调用,虚线为调度或错误路径。外部库和驱动不展开。
|
||||
|
||||
图不是某次运行的日志,分句和 Adapter 按输入或配置选择。修改源码后运行 `dever inspect . --links`。
|
||||
|
||||
## 入口
|
||||
|
||||
- API [content.revision.api.admin.manage.create](api/admin/manage.dever#L1) → [content.revision.create](app.dever#L10)
|
||||
|
||||
## 外部调用方
|
||||
|
||||
当前加载的源码中没有已收录的外部调用。
|
||||
|
||||
## 能力与内部调用
|
||||
|
||||
### content.revision.RevisionView
|
||||
|
||||
定义:[app.dever:1:1](app.dever#L1,1)
|
||||
|
||||
没有已收录的下游调用。
|
||||
|
||||
### content.revision.api.admin.manage.create
|
||||
|
||||
定义:[api/admin/manage.dever:1:1](api/admin/manage.dever#L1,1)
|
||||
|
||||
<a id="flow-61096d20b01e1b77-1"></a>
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
n0(["content.revision.api.admin.manage.create"])
|
||||
click n0 href "api/admin/manage.dever#L1,15" "打开源码"
|
||||
n1{"路由 POST /content/revision/admin/manage/create,topic #34;admin/manage#34; 接受请求?"}
|
||||
n2(["返回 HTTP 404"])
|
||||
n3["进入 API"]
|
||||
n4["取得认证时间"]
|
||||
n5(["返回 HTTP 500"])
|
||||
n6["按 topic #34;admin/manage#34; 选择站点与 Provider"]
|
||||
n7(["返回 HTTP 403"])
|
||||
n8{"读取 Bearer 或站点 Cookie;两者互斥"}
|
||||
n9(["返回 HTTP 401"])
|
||||
n10["认证上下文就绪"]
|
||||
n11(["返回 HTTP 401"])
|
||||
n12["校验 Token 的签名、有效期及站点"]
|
||||
n13{"Cookie 且非 GET / HEAD / OPTIONS?"}
|
||||
n14["检查站点 Origin"]
|
||||
n15(["返回 HTTP 403"])
|
||||
n16(["返回 HTTP 401"])
|
||||
n17{"按 Provider.verify 选择验证函数"}
|
||||
n18[["user.account.verify"]]
|
||||
click n18 href "api/admin/manage.dever#L1,15" "打开源码"
|
||||
n19(["返回 HTTP 业务错误映射状态;其他故障 500"])
|
||||
n20["用验证结果建立身份"]
|
||||
n21(["返回 HTTP 认证错误状态"])
|
||||
n22(["返回 HTTP 500"])
|
||||
n23["建立请求数据库上下文"]
|
||||
n24{"有已验证租户?"}
|
||||
n25["数据库上下文就绪"]
|
||||
n26{"部署启用了租户存储?"}
|
||||
n27(["返回 HTTP 403"])
|
||||
n28["按已验证租户选择数据库"]
|
||||
n29(["返回 HTTP 503"])
|
||||
n30{"检查租户组件启用状态:content, media, news"}
|
||||
n31(["返回 HTTP 403"])
|
||||
n32(["返回 HTTP 503"])
|
||||
n33["进入写入口事务"]
|
||||
n34["失败:保留 HTTP 错误状态"]
|
||||
n35["回滚入口拥有的事务"]
|
||||
n36(["返回 HTTP 500"])
|
||||
n37(["返回错误响应"])
|
||||
n38{"权限检查:content.revision.{site}.create"}
|
||||
n39["权限通过"]
|
||||
n40["读取 API 输入"]
|
||||
n41["解码具体参数并检查字段合同"]
|
||||
n42[["content.revision.create"]]
|
||||
click n42 href "api/admin/manage.dever#L1,15" "打开源码"
|
||||
n43["按输出类型检查可序列化值并编码响应"]
|
||||
n44["提交入口拥有的事务"]
|
||||
n45(["返回 HTTP 500"])
|
||||
n46["发布暂存的认证 Header / Cookie"]
|
||||
n47(["返回成功响应"])
|
||||
n0 --> n1
|
||||
n1 -->|"未匹配此入口:继续路由;均未匹配"| n2
|
||||
n1 --> n3
|
||||
n3 --> n4
|
||||
n4 -->|"失败"| n5
|
||||
n4 --> n6
|
||||
n6 -->|"无站点绑定"| n7
|
||||
n6 --> n8
|
||||
n8 -->|"凭据格式错误或冲突"| n9
|
||||
n8 -->|"无凭据"| n11
|
||||
n8 -->|"有凭据"| n13
|
||||
n13 -->|"否"| n12
|
||||
n13 -->|"是"| n14
|
||||
n14 -->|"不符合"| n15
|
||||
n14 -->|"通过"| n12
|
||||
n12 -->|"无 Provider 或 Token 无效"| n16
|
||||
n12 --> n17
|
||||
n17 -->|"user.account.verify"| n18
|
||||
n18 -->|"验证函数失败"| n19
|
||||
n18 --> n20
|
||||
n20 -->|"身份不合法"| n21
|
||||
n20 -->|"通过"| n10
|
||||
n17 -->|"无匹配验证函数"| n22
|
||||
n10 --> n23
|
||||
n23 --> n24
|
||||
n24 -->|"否"| n26
|
||||
n26 -->|"是:缺少租户身份"| n27
|
||||
n26 -->|"否"| n25
|
||||
n24 -->|"是"| n28
|
||||
n28 -->|"选择失败"| n29
|
||||
n28 --> n30
|
||||
n30 -->|"未启用"| n31
|
||||
n30 -->|"查询失败"| n32
|
||||
n30 -->|"允许"| n25
|
||||
n25 --> n33
|
||||
n34 --> n35
|
||||
n35 -->|"事务结束失败"| n36
|
||||
n35 --> n37
|
||||
n33 --> n38
|
||||
n38 -->|"拒绝:Access 状态;查询故障:500"| n34
|
||||
n38 -->|"允许"| n39
|
||||
n39 --> n40
|
||||
n40 -->|"失败:输入错误状态"| n34
|
||||
n40 --> n41
|
||||
n41 -->|"失败:400"| n34
|
||||
n41 --> n42
|
||||
n42 -->|"业务错误映射状态;其他故障:500"| n34
|
||||
n42 --> n43
|
||||
n43 -->|"编码失败:500"| n34
|
||||
n43 --> n44
|
||||
n44 -->|"提交失败"| n45
|
||||
n44 --> n46
|
||||
n46 --> n47
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>源码与子流程</summary>
|
||||
|
||||
| 节点 | 源码 | 子流程 |
|
||||
| --- | --- | --- |
|
||||
| user.account.verify | [api/admin/manage.dever:1:15](api/admin/manage.dever#L1,15) | [user.account.verify](../../user/account/link.md#flow-aaaacb87d61f819c-1) |
|
||||
| content.revision.create | [api/admin/manage.dever:1:15](api/admin/manage.dever#L1,15) | [content.revision.create](link.md#flow-8c2beaa3a2e4fcf0-1) |
|
||||
|
||||
</details>
|
||||
|
||||
| 目标 | 归属 | 方式 | 调用位置 |
|
||||
| --- | --- | --- | --- |
|
||||
| [content.revision.create](app.dever#L10) | 领域内 | API 绑定 | [api/admin/manage.dever:1:15](api/admin/manage.dever#L1,15) |
|
||||
| [user.account.verify](../../user/account/app/account.dever#L133) | 跨领域 [user.account](../../user/account/link.md) | 身份验证 | [api/admin/manage.dever:1:15](api/admin/manage.dever#L1,15) |
|
||||
|
||||
### content.revision.create
|
||||
|
||||
定义:[app.dever:10:1](app.dever#L10,1)
|
||||
|
||||
<a id="flow-8c2beaa3a2e4fcf0-1"></a>
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
n0(["create"])
|
||||
click n0 href "app.dever#L10,1" "打开源码"
|
||||
n1["开始事务"]
|
||||
n2(["提交事务,返回"])
|
||||
n3(["回滚事务,传播错误"])
|
||||
n4[["news.article.edit"]]
|
||||
click n4 href "app.dever#L18,12" "打开源码"
|
||||
n5["赋值 edited"]
|
||||
click n5 href "app.dever#L18,12" "打开源码"
|
||||
n6["content.revision.model.create"]
|
||||
click n6 href "app.dever#L26,12" "打开源码"
|
||||
n7["赋值 stored"]
|
||||
click n7 href "app.dever#L26,12" "打开源码"
|
||||
n8["赋值 revision"]
|
||||
click n8 href "app.dever#L37,14" "打开源码"
|
||||
n0 --> n1
|
||||
n1 -.->|"事务体任一未捕获错误"| n3
|
||||
n1 --> n4
|
||||
n4 --> n5
|
||||
n5 --> n6
|
||||
n6 --> n7
|
||||
n7 --> n8
|
||||
n8 --> n2
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>源码与子流程</summary>
|
||||
|
||||
| 节点 | 源码 | 子流程 |
|
||||
| --- | --- | --- |
|
||||
| news.article.edit | [app.dever:18:12](app.dever#L18,12) | [news.article.edit](../../news/article/link.md#flow-2c557350a87b85a1-1) |
|
||||
|
||||
</details>
|
||||
|
||||
| 目标 | 归属 | 方式 | 调用位置 |
|
||||
| --- | --- | --- | --- |
|
||||
| [news.article.edit](../../news/article/app/editing.dever#L77) | 跨领域 [news.article](../../news/article/link.md) | 调用 | [app.dever:18:12](app.dever#L18,12) |
|
||||
| [content.revision.model.create](model.dever#L6) | 领域内 | 内建操作 | [app.dever:26:12](app.dever#L26,12) |
|
||||
|
||||
### content.revision.model.Revision
|
||||
|
||||
定义:[model.dever:6:1](model.dever#L6,1)
|
||||
|
||||
没有已收录的下游调用。
|
||||
|
||||
### content.revision.model.RevisionStatus
|
||||
|
||||
定义:[model.dever:1:1](model.dever#L1,1)
|
||||
|
||||
没有已收录的下游调用。
|
||||
@@ -0,0 +1,16 @@
|
||||
type RevisionStatus {
|
||||
Draft = "草稿"
|
||||
Published = "已发布"
|
||||
}
|
||||
|
||||
type Revision {
|
||||
article_id: news.article.model.id index
|
||||
version: Int
|
||||
title: Text(1, 160)
|
||||
body: Text
|
||||
category_id: content.category.model.id?
|
||||
asset_id: media.asset.model.id?
|
||||
status: RevisionStatus default RevisionStatus.Draft index
|
||||
}
|
||||
|
||||
unique(article_id, version)
|
||||
@@ -0,0 +1,10 @@
|
||||
# 媒体资源
|
||||
|
||||
保存上传文件及元数据,供文章引用可用媒体。
|
||||
|
||||
- [媒体本地适配器](doc/adapter.md)
|
||||
- [上传接口](doc/api-admin-manage.md)
|
||||
- [媒体资源应用](doc/app.md)
|
||||
- [媒体资源模型](doc/model.md)
|
||||
- [媒体存储端口](doc/port.md)
|
||||
- [调用流程](link.md)
|
||||
@@ -0,0 +1,3 @@
|
||||
port.store(file: Upload) (key: Uuid) {
|
||||
key = dever.storage.put(file)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
long post upload = app.store
|
||||
@@ -0,0 +1,41 @@
|
||||
type StoreResult {
|
||||
id: model.id
|
||||
storage_key: Uuid
|
||||
filename: Text
|
||||
content_type: Text
|
||||
size: Int
|
||||
}
|
||||
|
||||
store(file: Upload) (result: StoreResult) {
|
||||
filename = dever.api.uploadFilename(file)
|
||||
content_type = dever.api.uploadContentType(file)
|
||||
size = dever.api.uploadSize(file)
|
||||
key = port.store(file)
|
||||
result = record(key, filename, content_type, size)
|
||||
}
|
||||
|
||||
transaction record(key: Uuid, filename: Text, content_type: Text, size: Int) (result: StoreResult) {
|
||||
asset = model.create(
|
||||
{
|
||||
storage_key = key
|
||||
filename = filename
|
||||
content_type = content_type
|
||||
size = size
|
||||
}
|
||||
)
|
||||
audit.operation.record(audit.operation.model.OperationKind.Upload, "asset", filename)
|
||||
result = StoreResult {
|
||||
id = asset.id
|
||||
storage_key = asset.storage_key
|
||||
filename = asset.filename
|
||||
content_type = asset.content_type
|
||||
size = asset.size
|
||||
}
|
||||
}
|
||||
|
||||
requireReady(id: model.id) () {
|
||||
stored = model.get(id)
|
||||
dever.api.require(stored.status == model.AssetStatus.Ready, dever.api.Error.Conflict)
|
||||
}
|
||||
|
||||
requireReady(id: null) () {}
|
||||
@@ -0,0 +1,5 @@
|
||||
# 媒体本地适配器
|
||||
|
||||
## media.asset.adapter.port.store
|
||||
|
||||
使用本地 storage 保存上传内容,返回文件 Uuid。
|
||||
@@ -0,0 +1,5 @@
|
||||
# 上传接口
|
||||
|
||||
## media.asset.api.admin.manage.upload
|
||||
|
||||
接收 multipart 文件上传,保存文件及资源元数据。
|
||||
@@ -0,0 +1,17 @@
|
||||
# 媒体资源应用
|
||||
|
||||
## media.asset.StoreResult
|
||||
|
||||
上传完成后返回资源编号、存储键、文件名、内容类型和字节数。
|
||||
|
||||
## media.asset.store
|
||||
|
||||
读取上传元数据并保存文件,再写入资源记录。文件存储不占用数据库事务。
|
||||
|
||||
## media.asset.record
|
||||
|
||||
在同一事务中写入资源元数据和上传审计。
|
||||
|
||||
## media.asset.requireReady
|
||||
|
||||
媒体编号为空时跳过;否则要求资源处于 Ready 状态,不可用时返回冲突。
|
||||
@@ -0,0 +1,9 @@
|
||||
# 媒体资源模型
|
||||
|
||||
## media.asset.model.AssetStatus
|
||||
|
||||
媒体资源的就绪或已删除状态。
|
||||
|
||||
## media.asset.model.Asset
|
||||
|
||||
已存文件的唯一存储键、元数据及状态。
|
||||
@@ -0,0 +1,5 @@
|
||||
# 媒体存储端口
|
||||
|
||||
## media.asset.port.store
|
||||
|
||||
保存一次性上传内容,返回存储键;失败使用 dever.storage.PutResult。
|
||||
@@ -0,0 +1,350 @@
|
||||
<!-- Generated by dever inspect --links. Do not edit. -->
|
||||
<!-- source-sha256: 11ad386ddcac8cef62a4a2d0330b0c2b4678196a227a7676c7e6a55aba63b5f1 -->
|
||||
|
||||
# media.asset
|
||||
|
||||
按求值顺序展示调用、分支和任务调度,包含 API 与自动 REST 入口。点击节点打开源码;双框为调用,虚线为调度或错误路径。外部库和驱动不展开。
|
||||
|
||||
图不是某次运行的日志,分句和 Adapter 按输入或配置选择。修改源码后运行 `dever inspect . --links`。
|
||||
|
||||
## 入口
|
||||
|
||||
- API [media.asset.api.admin.manage.upload](api/admin/manage.dever#L1) → [media.asset.store](app.dever#L9)
|
||||
|
||||
## 外部调用方
|
||||
|
||||
| 能力 | 调用方 | 方式 | 调用位置 |
|
||||
| --- | --- | --- | --- |
|
||||
| [media.asset.requireReady](app.dever#L36) | [news.article.create](../../news/article/app/editing.dever#L16) | 调用 | [../../news/article/app/editing.dever:25:3](../../news/article/app/editing.dever#L25,3) |
|
||||
| [media.asset.requireReady](app.dever#L36) | [news.article.edit](../../news/article/app/editing.dever#L77) | 调用 | [../../news/article/app/editing.dever:90:3](../../news/article/app/editing.dever#L90,3) |
|
||||
| [media.asset.requireReady](app.dever#L36) | [news.article.publishDraft](../../news/article/app/publication.dever#L48) | 调用 | [../../news/article/app/publication.dever:52:3](../../news/article/app/publication.dever#L52,3) |
|
||||
|
||||
## 能力与内部调用
|
||||
|
||||
### media.asset.StoreResult
|
||||
|
||||
定义:[app.dever:1:1](app.dever#L1,1)
|
||||
|
||||
没有已收录的下游调用。
|
||||
|
||||
### media.asset.adapter.port.store
|
||||
|
||||
定义:[adapter.dever:1:1](adapter.dever#L1,1)
|
||||
|
||||
<a id="flow-eb9faa6e4f022f76-1"></a>
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
n0(["port.store"])
|
||||
click n0 href "adapter.dever#L1,1" "打开源码"
|
||||
n1(["返回"])
|
||||
n3[["dever.storage.put"]]
|
||||
click n3 href "adapter.dever#L2,9" "打开源码"
|
||||
n4["赋值 key"]
|
||||
click n4 href "adapter.dever#L2,9" "打开源码"
|
||||
n0 --> n3
|
||||
n3 --> n4
|
||||
n4 --> n1
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>源码与子流程</summary>
|
||||
|
||||
| 节点 | 源码 | 子流程 |
|
||||
| --- | --- | --- |
|
||||
| dever.storage.put | [adapter.dever:2:9](adapter.dever#L2,9) | 依赖或生成的分派(本页不展开) |
|
||||
|
||||
</details>
|
||||
|
||||
| 目标 | 归属 | 方式 | 调用位置 |
|
||||
| --- | --- | --- | --- |
|
||||
| `dever.storage.put` | 依赖 | 调用 | [adapter.dever:2:9](adapter.dever#L2,9) |
|
||||
|
||||
### media.asset.api.admin.manage.upload
|
||||
|
||||
定义:[api/admin/manage.dever:1:1](api/admin/manage.dever#L1,1)
|
||||
|
||||
<a id="flow-7e3fa2eda06052de-1"></a>
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
n0(["media.asset.api.admin.manage.upload"])
|
||||
click n0 href "api/admin/manage.dever#L1,20" "打开源码"
|
||||
n1{"路由 POST /media/asset/admin/manage/upload,topic #34;admin/manage#34; 接受请求?"}
|
||||
n2(["返回 HTTP 404"])
|
||||
n3["进入 API"]
|
||||
n4["取得认证时间"]
|
||||
n5(["返回 HTTP 500"])
|
||||
n6["按 topic #34;admin/manage#34; 选择站点与 Provider"]
|
||||
n7(["返回 HTTP 403"])
|
||||
n8{"读取 Bearer 或站点 Cookie;两者互斥"}
|
||||
n9(["返回 HTTP 401"])
|
||||
n10["认证上下文就绪"]
|
||||
n11(["返回 HTTP 401"])
|
||||
n12["校验 Token 的签名、有效期及站点"]
|
||||
n13{"Cookie 且非 GET / HEAD / OPTIONS?"}
|
||||
n14["检查站点 Origin"]
|
||||
n15(["返回 HTTP 403"])
|
||||
n16(["返回 HTTP 401"])
|
||||
n17{"按 Provider.verify 选择验证函数"}
|
||||
n18[["user.account.verify"]]
|
||||
click n18 href "api/admin/manage.dever#L1,20" "打开源码"
|
||||
n19(["返回 HTTP 业务错误映射状态;其他故障 500"])
|
||||
n20["用验证结果建立身份"]
|
||||
n21(["返回 HTTP 认证错误状态"])
|
||||
n22(["返回 HTTP 500"])
|
||||
n23["建立请求数据库上下文"]
|
||||
n24{"有已验证租户?"}
|
||||
n25["数据库上下文就绪"]
|
||||
n26{"部署启用了租户存储?"}
|
||||
n27(["返回 HTTP 403"])
|
||||
n28["按已验证租户选择数据库"]
|
||||
n29(["返回 HTTP 503"])
|
||||
n30{"检查租户组件启用状态:audit, media"}
|
||||
n31(["返回 HTTP 403"])
|
||||
n32(["返回 HTTP 503"])
|
||||
n33["失败:保留 HTTP 错误状态"]
|
||||
n34(["返回错误响应"])
|
||||
n35{"权限检查:media.asset.{site}.upload"}
|
||||
n36["权限通过"]
|
||||
n37["读取 multipart、取得 Upload 和字段"]
|
||||
n38["解码具体参数并检查字段合同"]
|
||||
n39[["media.asset.store"]]
|
||||
click n39 href "api/admin/manage.dever#L1,20" "打开源码"
|
||||
n40["按输出类型检查可序列化值并编码响应"]
|
||||
n41["发布暂存的认证 Header / Cookie"]
|
||||
n42(["返回成功响应"])
|
||||
n0 --> n1
|
||||
n1 -->|"未匹配此入口:继续路由;均未匹配"| n2
|
||||
n1 --> n3
|
||||
n3 --> n4
|
||||
n4 -->|"失败"| n5
|
||||
n4 --> n6
|
||||
n6 -->|"无站点绑定"| n7
|
||||
n6 --> n8
|
||||
n8 -->|"凭据格式错误或冲突"| n9
|
||||
n8 -->|"无凭据"| n11
|
||||
n8 -->|"有凭据"| n13
|
||||
n13 -->|"否"| n12
|
||||
n13 -->|"是"| n14
|
||||
n14 -->|"不符合"| n15
|
||||
n14 -->|"通过"| n12
|
||||
n12 -->|"无 Provider 或 Token 无效"| n16
|
||||
n12 --> n17
|
||||
n17 -->|"user.account.verify"| n18
|
||||
n18 -->|"验证函数失败"| n19
|
||||
n18 --> n20
|
||||
n20 -->|"身份不合法"| n21
|
||||
n20 -->|"通过"| n10
|
||||
n17 -->|"无匹配验证函数"| n22
|
||||
n10 --> n23
|
||||
n23 --> n24
|
||||
n24 -->|"否"| n26
|
||||
n26 -->|"是:缺少租户身份"| n27
|
||||
n26 -->|"否"| n25
|
||||
n24 -->|"是"| n28
|
||||
n28 -->|"选择失败"| n29
|
||||
n28 --> n30
|
||||
n30 -->|"未启用"| n31
|
||||
n30 -->|"查询失败"| n32
|
||||
n30 -->|"允许"| n25
|
||||
n33 --> n34
|
||||
n25 --> n35
|
||||
n35 -->|"拒绝:Access 状态;查询故障:500"| n33
|
||||
n35 -->|"允许"| n36
|
||||
n36 --> n37
|
||||
n37 -->|"失败:上传错误状态"| n33
|
||||
n37 --> n38
|
||||
n38 -->|"失败:400"| n33
|
||||
n38 --> n39
|
||||
n39 -->|"业务错误映射状态;其他故障:500"| n33
|
||||
n39 --> n40
|
||||
n40 -->|"编码失败:500"| n33
|
||||
n40 --> n41
|
||||
n41 --> n42
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>源码与子流程</summary>
|
||||
|
||||
| 节点 | 源码 | 子流程 |
|
||||
| --- | --- | --- |
|
||||
| user.account.verify | [api/admin/manage.dever:1:20](api/admin/manage.dever#L1,20) | [user.account.verify](../../user/account/link.md#flow-aaaacb87d61f819c-1) |
|
||||
| media.asset.store | [api/admin/manage.dever:1:20](api/admin/manage.dever#L1,20) | [media.asset.store](link.md#flow-9ce0fcefee26c58b-1) |
|
||||
|
||||
</details>
|
||||
|
||||
| 目标 | 归属 | 方式 | 调用位置 |
|
||||
| --- | --- | --- | --- |
|
||||
| [media.asset.store](app.dever#L9) | 领域内 | API 绑定 | [api/admin/manage.dever:1:20](api/admin/manage.dever#L1,20) |
|
||||
| [user.account.verify](../../user/account/app/account.dever#L133) | 跨领域 [user.account](../../user/account/link.md) | 身份验证 | [api/admin/manage.dever:1:20](api/admin/manage.dever#L1,20) |
|
||||
|
||||
### media.asset.model.Asset
|
||||
|
||||
定义:[model.dever:6:1](model.dever#L6,1)
|
||||
|
||||
没有已收录的下游调用。
|
||||
|
||||
### media.asset.model.AssetStatus
|
||||
|
||||
定义:[model.dever:1:1](model.dever#L1,1)
|
||||
|
||||
没有已收录的下游调用。
|
||||
|
||||
### media.asset.port.store
|
||||
|
||||
定义:[port.dever:1:1](port.dever#L1,1)
|
||||
|
||||
没有已收录的下游调用。
|
||||
|
||||
### media.asset.record
|
||||
|
||||
定义:[app.dever:17:1](app.dever#L17,1)
|
||||
|
||||
<a id="flow-b00da9b3a217d427-1"></a>
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
n0(["record"])
|
||||
click n0 href "app.dever#L17,1" "打开源码"
|
||||
n1["开始事务"]
|
||||
n2(["提交事务,返回"])
|
||||
n3(["回滚事务,传播错误"])
|
||||
n4["media.asset.model.create"]
|
||||
click n4 href "app.dever#L18,11" "打开源码"
|
||||
n5["赋值 asset"]
|
||||
click n5 href "app.dever#L18,11" "打开源码"
|
||||
n6[["audit.operation.record"]]
|
||||
click n6 href "app.dever#L26,3" "打开源码"
|
||||
n7["赋值 result"]
|
||||
click n7 href "app.dever#L27,12" "打开源码"
|
||||
n0 --> n1
|
||||
n1 -.->|"事务体任一未捕获错误"| n3
|
||||
n1 --> n4
|
||||
n4 --> n5
|
||||
n5 --> n6
|
||||
n6 --> n7
|
||||
n7 --> n2
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>源码与子流程</summary>
|
||||
|
||||
| 节点 | 源码 | 子流程 |
|
||||
| --- | --- | --- |
|
||||
| audit.operation.record | [app.dever:26:3](app.dever#L26,3) | [audit.operation.record](../../audit/operation/link.md#flow-c1586d946bef8bfb-1) |
|
||||
|
||||
</details>
|
||||
|
||||
| 目标 | 归属 | 方式 | 调用位置 |
|
||||
| --- | --- | --- | --- |
|
||||
| [media.asset.model.create](model.dever#L6) | 领域内 | 内建操作 | [app.dever:18:11](app.dever#L18,11) |
|
||||
| [audit.operation.record](../../audit/operation/app.dever#L1) | 跨领域 [audit.operation](../../audit/operation/link.md) | 调用 | [app.dever:26:3](app.dever#L26,3) |
|
||||
|
||||
### media.asset.requireReady
|
||||
|
||||
定义:[app.dever:36:1](app.dever#L36,1)、[app.dever:41:1](app.dever#L41,1)
|
||||
|
||||
<a id="flow-2c00b9e4b7136711-1"></a>
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
n0(["requireReady"])
|
||||
click n0 href "app.dever#L36,1" "打开源码"
|
||||
n1(["返回"])
|
||||
n3{"按实参匹配分句(互斥)"}
|
||||
n4["id: model.id"]
|
||||
click n4 href "app.dever#L36,1" "打开源码"
|
||||
n5["media.asset.model.get"]
|
||||
click n5 href "app.dever#L37,12" "打开源码"
|
||||
n6["赋值 stored"]
|
||||
click n6 href "app.dever#L37,12" "打开源码"
|
||||
n7[["dever.api.require"]]
|
||||
click n7 href "app.dever#L38,3" "打开源码"
|
||||
n8["id: null"]
|
||||
click n8 href "app.dever#L41,1" "打开源码"
|
||||
n0 --> n3
|
||||
n3 -->|"id: model.id"| n4
|
||||
n4 --> n5
|
||||
n5 --> n6
|
||||
n6 --> n7
|
||||
n7 --> n1
|
||||
n3 -->|"id: null"| n8
|
||||
n8 --> n1
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>源码与子流程</summary>
|
||||
|
||||
| 节点 | 源码 | 子流程 |
|
||||
| --- | --- | --- |
|
||||
| dever.api.require | [app.dever:38:3](app.dever#L38,3) | 依赖或生成的分派(本页不展开) |
|
||||
|
||||
</details>
|
||||
|
||||
| 目标 | 归属 | 方式 | 调用位置 |
|
||||
| --- | --- | --- | --- |
|
||||
| [media.asset.model.get](model.dever#L6) | 领域内 | 内建操作 | [app.dever:37:12](app.dever#L37,12) |
|
||||
| `dever.api.require` | 依赖 | 调用 | [app.dever:38:3](app.dever#L38,3) |
|
||||
|
||||
### media.asset.store
|
||||
|
||||
定义:[app.dever:9:1](app.dever#L9,1)
|
||||
|
||||
<a id="flow-9ce0fcefee26c58b-1"></a>
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
n0(["store"])
|
||||
click n0 href "app.dever#L9,1" "打开源码"
|
||||
n1(["返回"])
|
||||
n3["dever.api.uploadFilename"]
|
||||
click n3 href "app.dever#L10,14" "打开源码"
|
||||
n4["赋值 filename"]
|
||||
click n4 href "app.dever#L10,14" "打开源码"
|
||||
n5["dever.api.uploadContentType"]
|
||||
click n5 href "app.dever#L11,18" "打开源码"
|
||||
n6["赋值 content_type"]
|
||||
click n6 href "app.dever#L11,18" "打开源码"
|
||||
n7["dever.api.uploadSize"]
|
||||
click n7 href "app.dever#L12,10" "打开源码"
|
||||
n8["赋值 size"]
|
||||
click n8 href "app.dever#L12,10" "打开源码"
|
||||
n9[["media.asset.adapter.port.store"]]
|
||||
click n9 href "app.dever#L13,9" "打开源码"
|
||||
n10["赋值 key"]
|
||||
click n10 href "app.dever#L13,9" "打开源码"
|
||||
n11[["media.asset.record"]]
|
||||
click n11 href "app.dever#L14,12" "打开源码"
|
||||
n12["赋值 result"]
|
||||
click n12 href "app.dever#L14,12" "打开源码"
|
||||
n0 --> n3
|
||||
n3 --> n4
|
||||
n4 --> n5
|
||||
n5 --> n6
|
||||
n6 --> n7
|
||||
n7 --> n8
|
||||
n8 --> n9
|
||||
n9 --> n10
|
||||
n10 --> n11
|
||||
n11 --> n12
|
||||
n12 --> n1
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>源码与子流程</summary>
|
||||
|
||||
| 节点 | 源码 | 子流程 |
|
||||
| --- | --- | --- |
|
||||
| media.asset.adapter.port.store | [app.dever:13:9](app.dever#L13,9) | [media.asset.adapter.port.store](link.md#flow-eb9faa6e4f022f76-1) |
|
||||
| media.asset.record | [app.dever:14:12](app.dever#L14,12) | [media.asset.record](link.md#flow-b00da9b3a217d427-1) |
|
||||
|
||||
</details>
|
||||
|
||||
| 目标 | 归属 | 方式 | 调用位置 |
|
||||
| --- | --- | --- | --- |
|
||||
| `dever.api.uploadFilename` | 内建操作 | 内建操作 | [app.dever:10:14](app.dever#L10,14) |
|
||||
| `dever.api.uploadContentType` | 内建操作 | 内建操作 | [app.dever:11:18](app.dever#L11,18) |
|
||||
| `dever.api.uploadSize` | 内建操作 | 内建操作 | [app.dever:12:10](app.dever#L12,10) |
|
||||
| [media.asset.adapter.port.store](adapter.dever#L1) | 领域内 | 调用 | [app.dever:13:9](app.dever#L13,9) |
|
||||
| [media.asset.record](app.dever#L17) | 领域内 | 调用 | [app.dever:14:12](app.dever#L14,12) |
|
||||
@@ -0,0 +1,12 @@
|
||||
type AssetStatus {
|
||||
Ready = "就绪"
|
||||
Removed = "已删除"
|
||||
}
|
||||
|
||||
type Asset {
|
||||
storage_key: Uuid unique
|
||||
filename: Text(1, 255)
|
||||
content_type: Text(1, 160)
|
||||
size: Int
|
||||
status: AssetStatus default AssetStatus.Ready index
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
store(file: Upload) (key: Uuid) fails dever.storage.PutResult
|
||||
@@ -0,0 +1,12 @@
|
||||
# 文章
|
||||
|
||||
管理作者草稿和版本,手动或定时发布为前台快照。
|
||||
|
||||
- [文章管理接口](doc/api-admin-manage.md)
|
||||
- [文章浏览接口](doc/api-front-browse.md)
|
||||
- [文章草稿](doc/app-editing.md)
|
||||
- [文章发布](doc/app-publication.md)
|
||||
- [文章规则](doc/domain.md)
|
||||
- [文章公开读模型](doc/model-publication.md)
|
||||
- [文章模型](doc/model.md)
|
||||
- [调用流程](link.md)
|
||||
@@ -0,0 +1,9 @@
|
||||
get list = app.list
|
||||
|
||||
get detail = app.detail
|
||||
|
||||
post create = app.create
|
||||
|
||||
delete remove = app.remove
|
||||
|
||||
post publish = app.publish
|
||||
@@ -0,0 +1,3 @@
|
||||
get list = app.published
|
||||
|
||||
get detail = app.publishedDetail
|
||||
@@ -0,0 +1,111 @@
|
||||
type ArticleView {
|
||||
id: model.id
|
||||
slug: Text
|
||||
title: Text
|
||||
body: Text
|
||||
version: Int
|
||||
category_id: content.category.model.id?
|
||||
asset_id: media.asset.model.id?
|
||||
}
|
||||
|
||||
type ArticlePage {
|
||||
rows: List<ArticleView>
|
||||
total: Int
|
||||
}
|
||||
|
||||
create(
|
||||
slug: Text,
|
||||
title: Text,
|
||||
body: Text,
|
||||
category_id: content.category.model.id?,
|
||||
asset_id: media.asset.model.id?
|
||||
) (article: ArticleView) {
|
||||
dever.api.require(text.trim(body) != "", dever.api.Error.Invalid)
|
||||
content.category.requireActive(category_id)
|
||||
media.asset.requireReady(asset_id)
|
||||
stored = model.create(
|
||||
{
|
||||
author_id = user.account.currentUser()
|
||||
slug = slug
|
||||
title = title
|
||||
body = body
|
||||
category_id = category_id
|
||||
asset_id = asset_id
|
||||
}
|
||||
)
|
||||
article = detail(stored.id)
|
||||
}
|
||||
|
||||
list(size: Int) (articles: ArticlePage) {
|
||||
author = user.account.currentUser()
|
||||
articles = ArticlePage {
|
||||
rows = model.ownedArticles(author, domain.pageSize(size))
|
||||
total = model.count(
|
||||
{
|
||||
where = author_id == author
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
detail(id: model.id) (article: ArticleView) {
|
||||
stored = model.get(id)
|
||||
dever.api.require(stored.author_id == user.account.currentUser(), dever.api.Error.Forbidden)
|
||||
article = ArticleView {
|
||||
id = stored.id
|
||||
slug = stored.slug
|
||||
title = stored.title
|
||||
body = stored.body
|
||||
version = stored.version
|
||||
category_id = stored.category_id
|
||||
asset_id = stored.asset_id
|
||||
}
|
||||
}
|
||||
|
||||
remove(id: model.id) (removed: Int) {
|
||||
stored = model.get(id)
|
||||
dever.api.require(stored.author_id == user.account.currentUser(), dever.api.Error.Forbidden)
|
||||
dever.api.require(stored.status == model.ArticleStatus.Draft, dever.api.Error.Conflict)
|
||||
removed = model.delete(
|
||||
{
|
||||
where = id == stored.id and version == stored.version
|
||||
}
|
||||
)
|
||||
domain.requireUpdated(removed)
|
||||
}
|
||||
|
||||
edit(
|
||||
id: model.id,
|
||||
expected_version: Int,
|
||||
title: Text,
|
||||
body: Text,
|
||||
category_id: content.category.model.id?,
|
||||
asset_id: media.asset.model.id?
|
||||
) (article: ArticleView) {
|
||||
stored = model.get(id)
|
||||
dever.api.require(stored.author_id == user.account.currentUser(), dever.api.Error.Forbidden)
|
||||
dever.api.require(stored.status == model.ArticleStatus.Draft, dever.api.Error.Conflict)
|
||||
dever.api.require(text.trim(body) != "", dever.api.Error.Invalid)
|
||||
content.category.requireActive(category_id)
|
||||
media.asset.requireReady(asset_id)
|
||||
changed = model.update(
|
||||
{
|
||||
where = id == stored.id and version == expected_version and status == model.ArticleStatus.Draft
|
||||
},
|
||||
{
|
||||
title = title
|
||||
body = body
|
||||
category_id = category_id
|
||||
asset_id = asset_id
|
||||
version = expected_version + 1
|
||||
}
|
||||
)
|
||||
domain.requireUpdated(changed)
|
||||
article = detail(stored.id)
|
||||
}
|
||||
|
||||
requireSchedulable(article_id: model.id) () {
|
||||
stored = model.get(article_id)
|
||||
dever.api.require(stored.author_id == user.account.currentUser(), dever.api.Error.Forbidden)
|
||||
dever.api.require(stored.status == model.ArticleStatus.Draft, dever.api.Error.Conflict)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
transaction publish(article_id: model.id) (publication: model.publication.Publication) {
|
||||
stored = model.get(article_id)
|
||||
dever.api.require(stored.author_id == user.account.currentUser(), dever.api.Error.Forbidden)
|
||||
dever.api.require(stored.status == model.ArticleStatus.Draft, dever.api.Error.Conflict)
|
||||
publication = publishDraft(article_id)
|
||||
}
|
||||
|
||||
published(size: Int) (articles: model.publication.Page) {
|
||||
articles = model.publication.list(
|
||||
{
|
||||
order = [published_at.desc, id.desc]
|
||||
size = domain.pageSize(size)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
publishedDetail(id: model.publication.id) (article: model.publication.Publication) {
|
||||
article = model.publication.get(id)
|
||||
dever.api.require(text.trim(article.body) != "", dever.api.Error.NotFound)
|
||||
}
|
||||
|
||||
transaction publishScheduled(article_id: model.id) (
|
||||
publication: model.publication.Publication
|
||||
) {
|
||||
stored = model.get(article_id)
|
||||
publication = scheduledPublication(stored.status, article_id)
|
||||
}
|
||||
|
||||
scheduledPublication(status: model.ArticleStatus.Published, article_id: model.id) (
|
||||
publication: model.publication.Publication
|
||||
) {
|
||||
stored = model.get(article_id)
|
||||
publication = existingPublication(
|
||||
model.publication.first(
|
||||
{
|
||||
where = slug == stored.slug
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
scheduledPublication(status: model.ArticleStatus.Draft, article_id: model.id) (
|
||||
publication: model.publication.Publication
|
||||
) {
|
||||
publication = publishDraft(article_id)
|
||||
}
|
||||
|
||||
publishDraft(article_id: model.id) (publication: model.publication.Publication) {
|
||||
stored = model.get(article_id)
|
||||
dever.api.require(stored.status == model.ArticleStatus.Draft, dever.api.Error.Conflict)
|
||||
content.category.requireActive(stored.category_id)
|
||||
media.asset.requireReady(stored.asset_id)
|
||||
published_at = dever.time.now()
|
||||
changed = model.update(
|
||||
{
|
||||
where = id == stored.id and version == stored.version and status == model.ArticleStatus.Draft
|
||||
},
|
||||
{
|
||||
status = model.ArticleStatus.Published
|
||||
published_at = published_at
|
||||
version = stored.version + 1
|
||||
}
|
||||
)
|
||||
domain.requireUpdated(changed)
|
||||
audit.operation.record(
|
||||
audit.operation.model.OperationKind.Publish,
|
||||
"article",
|
||||
stored.slug
|
||||
)
|
||||
publication = model.publication.upsert(
|
||||
{
|
||||
slug = stored.slug
|
||||
},
|
||||
{
|
||||
title = stored.title
|
||||
body = stored.body
|
||||
published_at = published_at
|
||||
},
|
||||
{
|
||||
title = stored.title
|
||||
body = stored.body
|
||||
published_at = published_at
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
existingPublication(stored: model.publication.Publication) (
|
||||
publication: model.publication.Publication
|
||||
) {
|
||||
publication = stored
|
||||
}
|
||||
|
||||
existingPublication(stored: null) (publication: model.publication.Publication) {
|
||||
fail(dever.api.Error.NotFound)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# 文章管理接口
|
||||
|
||||
## news.article.api.admin.manage.list
|
||||
|
||||
读取当前作者的文章列表与总数。
|
||||
|
||||
## news.article.api.admin.manage.detail
|
||||
|
||||
读取当前作者的文章详情。
|
||||
|
||||
## news.article.api.admin.manage.create
|
||||
|
||||
创建当前作者的草稿。
|
||||
|
||||
## news.article.api.admin.manage.remove
|
||||
|
||||
删除当前作者的草稿。
|
||||
|
||||
## news.article.api.admin.manage.publish
|
||||
|
||||
发布当前作者的草稿。
|
||||
@@ -0,0 +1,9 @@
|
||||
# 文章浏览接口
|
||||
|
||||
## news.article.api.front.browse.list
|
||||
|
||||
读取当前租户的已发布文章。
|
||||
|
||||
## news.article.api.front.browse.detail
|
||||
|
||||
读取已发布文章详情。
|
||||
@@ -0,0 +1,33 @@
|
||||
# 文章草稿
|
||||
|
||||
## news.article.ArticleView
|
||||
|
||||
编辑接口返回的文章内容、版本、分类和媒体编号。
|
||||
|
||||
## news.article.ArticlePage
|
||||
|
||||
当前作者的文章列表及总数。
|
||||
|
||||
## news.article.create
|
||||
|
||||
为当前作者创建草稿。正文不能为空白,所选分类和媒体须可用;正文保留原文。
|
||||
|
||||
## news.article.list
|
||||
|
||||
读取当前作者的最新文章和总数,size 必须为 1–100。
|
||||
|
||||
## news.article.detail
|
||||
|
||||
读取当前作者的一篇文章,访问其他作者的文章返回禁止访问。
|
||||
|
||||
## news.article.remove
|
||||
|
||||
删除当前作者的草稿,并检查版本未被并发修改。已发布文章不能删除。
|
||||
|
||||
## news.article.edit
|
||||
|
||||
按 expected_version 修改当前作者的草稿,成功后版本加一。正文为空白或分类、媒体不可用时失败。
|
||||
|
||||
## news.article.requireSchedulable
|
||||
|
||||
要求文章属于当前作者且仍为草稿,供创建发布计划使用。
|
||||
@@ -0,0 +1,29 @@
|
||||
# 文章发布
|
||||
|
||||
## news.article.publish
|
||||
|
||||
发布当前作者的草稿;状态更新、审计和公开快照在同一事务中完成。
|
||||
|
||||
## news.article.published
|
||||
|
||||
按发布时间和编号倒序读取发布快照,size 必须为 1–100。
|
||||
|
||||
## news.article.publishedDetail
|
||||
|
||||
读取一份发布快照;正文为空白时返回不存在。
|
||||
|
||||
## news.article.publishScheduled
|
||||
|
||||
执行定时发布。已发布文章复用现有快照,草稿进入发布流程。
|
||||
|
||||
## news.article.scheduledPublication
|
||||
|
||||
根据文章状态选择复用快照或发布草稿。
|
||||
|
||||
## news.article.publishDraft
|
||||
|
||||
检查草稿、分类和媒体,更新文章状态与版本,写入审计并保存发布快照;并发修改返回冲突。
|
||||
|
||||
## news.article.existingPublication
|
||||
|
||||
取出已有发布快照;找不到时返回不存在。
|
||||
@@ -0,0 +1,9 @@
|
||||
# 文章规则
|
||||
|
||||
## news.article.domain.pageSize
|
||||
|
||||
只接受 1–100 的列表大小,其他值返回请求无效。
|
||||
|
||||
## news.article.domain.requireUpdated
|
||||
|
||||
要求数据库恰好修改一条记录,否则返回冲突。
|
||||
@@ -0,0 +1,5 @@
|
||||
# 文章公开读模型
|
||||
|
||||
## news.article.model.publication.Publication
|
||||
|
||||
前台读取的发布快照,保存 slug、标题、正文和发布时间。
|
||||
@@ -0,0 +1,9 @@
|
||||
# 文章模型
|
||||
|
||||
## news.article.model.ArticleStatus
|
||||
|
||||
文章的草稿或已发布状态。
|
||||
|
||||
## news.article.model.Article
|
||||
|
||||
作者所属的文章草稿和发布状态。版本、作者与发布状态不直接作为公开字段返回。
|
||||
@@ -0,0 +1,13 @@
|
||||
pageSize(value: Int >= 1 and <= 100) (size: Int) pure {
|
||||
size = value
|
||||
}
|
||||
|
||||
pageSize(value: other) (size: Int) pure {
|
||||
fail(dever.api.Error.Invalid)
|
||||
}
|
||||
|
||||
requireUpdated(changed: 1) () pure {}
|
||||
|
||||
requireUpdated(changed: other) () pure {
|
||||
fail(dever.api.Error.Conflict)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,25 @@
|
||||
type ArticleStatus {
|
||||
Draft = "草稿"
|
||||
Published = "已发布"
|
||||
}
|
||||
|
||||
type Article {
|
||||
private owner author_id: Int = dever.auth.userId()
|
||||
slug: Text(1, 180) lower unique
|
||||
title: Text(1, 160)
|
||||
body: Text raw
|
||||
category_id: content.category.model.id?
|
||||
asset_id: media.asset.model.id?
|
||||
private version: Int default 1
|
||||
private status: ArticleStatus default ArticleStatus.Draft index
|
||||
private published_at: DateTime?
|
||||
}
|
||||
|
||||
index(author_id, created_at)
|
||||
|
||||
sql ownedArticles(author: Int, size: Int) (
|
||||
articles: List<app.ArticleView>
|
||||
) {
|
||||
sqlite = "SELECT id, slug, title, body, version, category_id, asset_id FROM news_article_model WHERE author_id = ?1 ORDER BY id DESC LIMIT ?2"
|
||||
postgres = "SELECT id, slug, title, body, version, category_id, asset_id FROM news_article_model WHERE author_id = $1 ORDER BY id DESC LIMIT $2"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
type Publication {
|
||||
slug: Text(1, 180) unique
|
||||
title: Text(1, 160)
|
||||
body: Text
|
||||
published_at: DateTime index
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
# 租户
|
||||
|
||||
管理平台租户记录和启用状态,数据库准备由部署命令完成。
|
||||
|
||||
- [租户应用](doc/app.md)
|
||||
- [租户规则](doc/domain.md)
|
||||
- [租户模型](doc/model.md)
|
||||
- [调用流程](link.md)
|
||||
@@ -0,0 +1,41 @@
|
||||
type TenantView {
|
||||
id: model.id
|
||||
key: Text
|
||||
name: Text
|
||||
}
|
||||
|
||||
provision(key: Text, name: Text) (tenant: TenantView) {
|
||||
stored = model.create(
|
||||
{
|
||||
key = key
|
||||
name = name
|
||||
}
|
||||
)
|
||||
tenant = TenantView {
|
||||
id = stored.id
|
||||
key = stored.key
|
||||
name = stored.name
|
||||
}
|
||||
}
|
||||
|
||||
active(tenant_key: Text) (tenant: TenantView) {
|
||||
stored = model.first(
|
||||
{
|
||||
where = key == domain.normalizeKey(tenant_key)
|
||||
}
|
||||
)
|
||||
tenant = activeView(stored)
|
||||
}
|
||||
|
||||
activeView(stored: model.Tenant) (tenant: TenantView) {
|
||||
dever.api.require(stored.status == model.TenantStatus.Active, dever.api.Error.Unauthorized)
|
||||
tenant = TenantView {
|
||||
id = stored.id
|
||||
key = stored.key
|
||||
name = stored.name
|
||||
}
|
||||
}
|
||||
|
||||
activeView(stored: null) (tenant: TenantView) {
|
||||
fail(dever.api.Error.Unauthorized)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
# 租户应用
|
||||
|
||||
## platform.tenant.TenantView
|
||||
|
||||
租户编号、唯一键和显示名称。
|
||||
|
||||
## platform.tenant.provision
|
||||
|
||||
创建租户记录,key 去除首尾空白后转为小写并保持唯一。
|
||||
|
||||
## platform.tenant.active
|
||||
|
||||
按规范化 key 查找启用租户,不存在或已禁用时返回未授权。
|
||||
|
||||
## platform.tenant.activeView
|
||||
|
||||
检查租户启用状态并转换为 TenantView,空记录返回未授权。
|
||||
@@ -0,0 +1,5 @@
|
||||
# 租户规则
|
||||
|
||||
## platform.tenant.domain.normalizeKey
|
||||
|
||||
去掉租户键两端空白并转为小写,用于与 Model 写入规则一致地查询。
|
||||
@@ -0,0 +1,9 @@
|
||||
# 租户模型
|
||||
|
||||
## platform.tenant.model.TenantStatus
|
||||
|
||||
租户的启用或禁用状态。
|
||||
|
||||
## platform.tenant.model.Tenant
|
||||
|
||||
平台库中的租户记录;key 唯一,UUID 自动生成。
|
||||
@@ -0,0 +1,3 @@
|
||||
normalizeKey(value: Text) (key: Text) pure {
|
||||
key = text.lower(text.trim(value))
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
<!-- Generated by dever inspect --links. Do not edit. -->
|
||||
<!-- source-sha256: 3d8386ca09494f4f62fa83613bf8379bbfa858632449d9d19fe86f3a86ad1d9c -->
|
||||
|
||||
# platform.tenant
|
||||
|
||||
按求值顺序展示调用、分支和任务调度,包含 API 与自动 REST 入口。点击节点打开源码;双框为调用,虚线为调度或错误路径。外部库和驱动不展开。
|
||||
|
||||
图不是某次运行的日志,分句和 Adapter 按输入或配置选择。修改源码后运行 `dever inspect . --links`。
|
||||
|
||||
## 入口
|
||||
|
||||
没有已收录的入口。
|
||||
|
||||
## 外部调用方
|
||||
|
||||
| 能力 | 调用方 | 方式 | 调用位置 |
|
||||
| --- | --- | --- | --- |
|
||||
| [platform.tenant.active](app.dever#L21) | [user.account.login](../../user/account/app/account.dever#L70) | 调用 | [../../user/account/app/account.dever:78:14](../../user/account/app/account.dever#L78,14) |
|
||||
| [platform.tenant.active](app.dever#L21) | [user.account.establishLogin](../../user/account/app/account.dever#L91) | 调用 | [../../user/account/app/account.dever:99:14](../../user/account/app/account.dever#L99,14) |
|
||||
| [platform.tenant.active](app.dever#L21) | [user.account.verify](../../user/account/app/account.dever#L133) | 调用 | [../../user/account/app/account.dever:137:14](../../user/account/app/account.dever#L137,14) |
|
||||
| [platform.tenant.provision](app.dever#L7) | [user.account.registerAccount](../../user/account/app/account.dever#L41) | 调用 | [../../user/account/app/account.dever:48:12](../../user/account/app/account.dever#L48,12) |
|
||||
|
||||
## 能力与内部调用
|
||||
|
||||
### platform.tenant.TenantView
|
||||
|
||||
定义:[app.dever:1:1](app.dever#L1,1)
|
||||
|
||||
没有已收录的下游调用。
|
||||
|
||||
### platform.tenant.active
|
||||
|
||||
定义:[app.dever:21:1](app.dever#L21,1)
|
||||
|
||||
<a id="flow-720cd3e6ee01541e-1"></a>
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
n0(["active"])
|
||||
click n0 href "app.dever#L21,1" "打开源码"
|
||||
n1(["返回"])
|
||||
n3[["platform.tenant.domain.normalizeKey"]]
|
||||
click n3 href "app.dever#L24,22" "打开源码"
|
||||
n4["platform.tenant.model.first"]
|
||||
click n4 href "app.dever#L22,12" "打开源码"
|
||||
n5["赋值 stored"]
|
||||
click n5 href "app.dever#L22,12" "打开源码"
|
||||
n6[["platform.tenant.activeView"]]
|
||||
click n6 href "app.dever#L27,12" "打开源码"
|
||||
n7["赋值 tenant"]
|
||||
click n7 href "app.dever#L27,12" "打开源码"
|
||||
n0 --> n3
|
||||
n3 --> n4
|
||||
n4 --> n5
|
||||
n5 --> n6
|
||||
n6 --> n7
|
||||
n7 --> n1
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>源码与子流程</summary>
|
||||
|
||||
| 节点 | 源码 | 子流程 |
|
||||
| --- | --- | --- |
|
||||
| platform.tenant.domain.normalizeKey | [app.dever:24:22](app.dever#L24,22) | [platform.tenant.domain.normalizeKey](link.md#flow-675cb56e9567397f-1) |
|
||||
| platform.tenant.activeView | [app.dever:27:12](app.dever#L27,12) | [platform.tenant.activeView](link.md#flow-b450477f16968f30-1) |
|
||||
|
||||
</details>
|
||||
|
||||
| 目标 | 归属 | 方式 | 调用位置 |
|
||||
| --- | --- | --- | --- |
|
||||
| [platform.tenant.model.first](model.dever#L6) | 领域内 | 内建操作 | [app.dever:22:12](app.dever#L22,12) |
|
||||
| [platform.tenant.domain.normalizeKey](domain.dever#L1) | 领域内 | 调用 | [app.dever:24:22](app.dever#L24,22) |
|
||||
| [platform.tenant.activeView](app.dever#L30) | 领域内 | 调用 | [app.dever:27:12](app.dever#L27,12) |
|
||||
|
||||
### platform.tenant.activeView
|
||||
|
||||
定义:[app.dever:30:1](app.dever#L30,1)、[app.dever:39:1](app.dever#L39,1)
|
||||
|
||||
<a id="flow-b450477f16968f30-1"></a>
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
n0(["activeView"])
|
||||
click n0 href "app.dever#L30,1" "打开源码"
|
||||
n1(["返回"])
|
||||
n2(["传播错误"])
|
||||
n3{"按实参匹配分句(互斥)"}
|
||||
n4["stored: model.Tenant"]
|
||||
click n4 href "app.dever#L30,1" "打开源码"
|
||||
n5[["dever.api.require"]]
|
||||
click n5 href "app.dever#L31,3" "打开源码"
|
||||
n6["赋值 tenant"]
|
||||
click n6 href "app.dever#L32,12" "打开源码"
|
||||
n7["stored: null"]
|
||||
click n7 href "app.dever#L39,1" "打开源码"
|
||||
n8["fail dever.api.Error.Unauthorized"]
|
||||
click n8 href "app.dever#L40,8" "打开源码"
|
||||
n0 --> n3
|
||||
n3 -->|"stored: model.Tenant"| n4
|
||||
n4 --> n5
|
||||
n5 --> n6
|
||||
n6 --> n1
|
||||
n3 -->|"stored: null"| n7
|
||||
n7 --> n8
|
||||
n8 --> n2
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>源码与子流程</summary>
|
||||
|
||||
| 节点 | 源码 | 子流程 |
|
||||
| --- | --- | --- |
|
||||
| dever.api.require | [app.dever:31:3](app.dever#L31,3) | 依赖或生成的分派(本页不展开) |
|
||||
|
||||
</details>
|
||||
|
||||
| 目标 | 归属 | 方式 | 调用位置 |
|
||||
| --- | --- | --- | --- |
|
||||
| `dever.api.require` | 依赖 | 调用 | [app.dever:31:3](app.dever#L31,3) |
|
||||
|
||||
### platform.tenant.domain.normalizeKey
|
||||
|
||||
定义:[domain.dever:1:1](domain.dever#L1,1)
|
||||
|
||||
<a id="flow-675cb56e9567397f-1"></a>
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
n0(["normalizeKey"])
|
||||
click n0 href "domain.dever#L1,1" "打开源码"
|
||||
n1(["返回"])
|
||||
n3[["text.trim"]]
|
||||
click n3 href "domain.dever#L2,20" "打开源码"
|
||||
n4[["text.lower"]]
|
||||
click n4 href "domain.dever#L2,9" "打开源码"
|
||||
n5["赋值 key"]
|
||||
click n5 href "domain.dever#L2,9" "打开源码"
|
||||
n0 --> n3
|
||||
n3 --> n4
|
||||
n4 --> n5
|
||||
n5 --> n1
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>源码与子流程</summary>
|
||||
|
||||
| 节点 | 源码 | 子流程 |
|
||||
| --- | --- | --- |
|
||||
| text.trim | [domain.dever:2:20](domain.dever#L2,20) | 依赖或生成的分派(本页不展开) |
|
||||
| text.lower | [domain.dever:2:9](domain.dever#L2,9) | 依赖或生成的分派(本页不展开) |
|
||||
|
||||
</details>
|
||||
|
||||
| 目标 | 归属 | 方式 | 调用位置 |
|
||||
| --- | --- | --- | --- |
|
||||
| `text.lower` | 依赖 | 调用 | [domain.dever:2:9](domain.dever#L2,9) |
|
||||
| `text.trim` | 依赖 | 调用 | [domain.dever:2:20](domain.dever#L2,20) |
|
||||
|
||||
### platform.tenant.model.Tenant
|
||||
|
||||
定义:[model.dever:6:1](model.dever#L6,1)
|
||||
|
||||
没有已收录的下游调用。
|
||||
|
||||
### platform.tenant.model.TenantStatus
|
||||
|
||||
定义:[model.dever:1:1](model.dever#L1,1)
|
||||
|
||||
没有已收录的下游调用。
|
||||
|
||||
### platform.tenant.provision
|
||||
|
||||
定义:[app.dever:7:1](app.dever#L7,1)
|
||||
|
||||
<a id="flow-cc4bbca9579ab41a-1"></a>
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
n0(["provision"])
|
||||
click n0 href "app.dever#L7,1" "打开源码"
|
||||
n1(["返回"])
|
||||
n3["platform.tenant.model.create"]
|
||||
click n3 href "app.dever#L8,12" "打开源码"
|
||||
n4["赋值 stored"]
|
||||
click n4 href "app.dever#L8,12" "打开源码"
|
||||
n5["赋值 tenant"]
|
||||
click n5 href "app.dever#L14,12" "打开源码"
|
||||
n0 --> n3
|
||||
n3 --> n4
|
||||
n4 --> n5
|
||||
n5 --> n1
|
||||
```
|
||||
|
||||
| 目标 | 归属 | 方式 | 调用位置 |
|
||||
| --- | --- | --- | --- |
|
||||
| [platform.tenant.model.create](model.dever#L6) | 领域内 | 内建操作 | [app.dever:8:12](app.dever#L8,12) |
|
||||
@@ -0,0 +1,11 @@
|
||||
type TenantStatus {
|
||||
Active = "启用"
|
||||
Disabled = "禁用"
|
||||
}
|
||||
|
||||
global type Tenant {
|
||||
uuid: Uuid generated unique
|
||||
key: Text(1, 64) lower unique
|
||||
name: Text(1, 100)
|
||||
status: TenantStatus default TenantStatus.Active index
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# 发布计划
|
||||
|
||||
为文章安排一次定时发布,重复任务不会重复发布。
|
||||
|
||||
- [发布计划接口](doc/api-admin-manage.md)
|
||||
- [发布计划应用](doc/app.md)
|
||||
- [发布计划任务](doc/job.md)
|
||||
- [发布计划模型](doc/model.md)
|
||||
- [调用流程](link.md)
|
||||
@@ -0,0 +1 @@
|
||||
post schedule = app.create
|
||||
@@ -0,0 +1,74 @@
|
||||
type PublishRequest {
|
||||
schedule_id: model.id
|
||||
}
|
||||
|
||||
type ScheduleView {
|
||||
id: model.id
|
||||
article_id: news.article.model.id
|
||||
publish_at: DateTime
|
||||
status: Text
|
||||
}
|
||||
|
||||
transaction create(article_id: news.article.model.id, publish_at: DateTime) (
|
||||
schedule: ScheduleView
|
||||
) {
|
||||
news.article.requireSchedulable(article_id)
|
||||
stored = model.create(
|
||||
{
|
||||
article_id = article_id
|
||||
publish_at = publish_at
|
||||
}
|
||||
)
|
||||
queued = dever.job.enqueueAt(
|
||||
job.publish,
|
||||
PublishRequest {
|
||||
schedule_id = stored.id
|
||||
},
|
||||
uuid.toText(stored.uuid),
|
||||
publish_at
|
||||
)
|
||||
discarded = queued
|
||||
schedule = ScheduleView {
|
||||
id = stored.id
|
||||
article_id = stored.article_id
|
||||
publish_at = stored.publish_at
|
||||
status = "pending"
|
||||
}
|
||||
}
|
||||
|
||||
transaction publish(input: PublishRequest) () {
|
||||
scheduled = model.get(input.schedule_id)
|
||||
complete(scheduled.status, scheduled.id)
|
||||
}
|
||||
|
||||
complete(status: model.ScheduleStatus.Completed, schedule_id: model.id) () {}
|
||||
|
||||
complete(status: model.ScheduleStatus.Cancelled, schedule_id: model.id) () {}
|
||||
|
||||
complete(status: model.ScheduleStatus.Pending, schedule_id: model.id) () {
|
||||
scheduled = model.get(schedule_id)
|
||||
dever.api.require(model.exists(
|
||||
{
|
||||
where = id == schedule_id and publish_at <= dever.time.now()
|
||||
}
|
||||
), dever.api.Error.Invalid)
|
||||
changed = model.update(
|
||||
{
|
||||
where = id == scheduled.id and status == model.ScheduleStatus.Pending
|
||||
},
|
||||
{
|
||||
status = model.ScheduleStatus.Completed
|
||||
}
|
||||
)
|
||||
publishClaimed(changed, scheduled.article_id)
|
||||
}
|
||||
|
||||
publishClaimed(changed: 0, article_id: news.article.model.id) () {}
|
||||
|
||||
publishClaimed(changed: 1, article_id: news.article.model.id) () {
|
||||
publication = news.article.publishScheduled(article_id)
|
||||
}
|
||||
|
||||
publishClaimed(changed: other, article_id: news.article.model.id) () {
|
||||
fail(dever.api.Error.Conflict)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# 发布计划接口
|
||||
|
||||
## publishing.schedule.api.admin.manage.schedule
|
||||
|
||||
为当前作者的草稿建立定时发布计划。
|
||||
@@ -0,0 +1,25 @@
|
||||
# 发布计划应用
|
||||
|
||||
## publishing.schedule.PublishRequest
|
||||
|
||||
发布任务的输入,只保存计划编号。
|
||||
|
||||
## publishing.schedule.ScheduleView
|
||||
|
||||
创建计划后返回文章编号、计划时间和状态。
|
||||
|
||||
## publishing.schedule.create
|
||||
|
||||
为当前作者的草稿建立发布计划并按时入队;计划记录和入队在同一事务内完成。每篇文章只能有一条计划。
|
||||
|
||||
## publishing.schedule.publish
|
||||
|
||||
加载发布计划并按其状态执行,供 Job 调用。
|
||||
|
||||
## publishing.schedule.complete
|
||||
|
||||
仅处理已经到时的 Pending 计划;已完成或取消的计划不再执行。状态更新与文章发布在同一事务中完成。
|
||||
|
||||
## publishing.schedule.publishClaimed
|
||||
|
||||
成功更新一条计划后发布文章;零条表示无需重复执行,其他数量返回冲突。
|
||||
@@ -0,0 +1,5 @@
|
||||
# 发布计划任务
|
||||
|
||||
## publishing.schedule.job.publish
|
||||
|
||||
执行发布计划,最多执行 3 次(含首次),单次期限 30 秒。
|
||||
@@ -0,0 +1,9 @@
|
||||
# 发布计划模型
|
||||
|
||||
## publishing.schedule.model.ScheduleStatus
|
||||
|
||||
计划的待发布、已完成或已取消状态。
|
||||
|
||||
## publishing.schedule.model.Schedule
|
||||
|
||||
每篇文章的唯一发布计划,保存执行时间和状态。
|
||||
Whitespace-only changes.
@@ -0,0 +1,5 @@
|
||||
database default
|
||||
|
||||
job publish(input: app.PublishRequest) () retry(3) timeout(30000) {
|
||||
app.publish(input)
|
||||
}
|
||||
@@ -0,0 +1,430 @@
|
||||
<!-- Generated by dever inspect --links. Do not edit. -->
|
||||
<!-- source-sha256: be17003a018467df95914a96c87fcb8890e4ade3118c3511d7e00fde98087ea7 -->
|
||||
|
||||
# publishing.schedule
|
||||
|
||||
按求值顺序展示调用、分支和任务调度,包含 API 与自动 REST 入口。点击节点打开源码;双框为调用,虚线为调度或错误路径。外部库和驱动不展开。
|
||||
|
||||
图不是某次运行的日志,分句和 Adapter 按输入或配置选择。修改源码后运行 `dever inspect . --links`。
|
||||
|
||||
## 入口
|
||||
|
||||
- API [publishing.schedule.api.admin.manage.schedule](api/admin/manage.dever#L1) → [publishing.schedule.create](app.dever#L12)
|
||||
- Job [publishing.schedule.job.publish](job.dever#L3)
|
||||
|
||||
## 外部调用方
|
||||
|
||||
当前加载的源码中没有已收录的外部调用。
|
||||
|
||||
## 能力与内部调用
|
||||
|
||||
### publishing.schedule.PublishRequest
|
||||
|
||||
定义:[app.dever:1:1](app.dever#L1,1)
|
||||
|
||||
没有已收录的下游调用。
|
||||
|
||||
### publishing.schedule.ScheduleView
|
||||
|
||||
定义:[app.dever:5:1](app.dever#L5,1)
|
||||
|
||||
没有已收录的下游调用。
|
||||
|
||||
### publishing.schedule.api.admin.manage.schedule
|
||||
|
||||
定义:[api/admin/manage.dever:1:1](api/admin/manage.dever#L1,1)
|
||||
|
||||
<a id="flow-4a14d9ba44a23d84-1"></a>
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
n0(["publishing.schedule.api.admin.manage.schedule"])
|
||||
click n0 href "api/admin/manage.dever#L1,17" "打开源码"
|
||||
n1{"路由 POST /publishing/schedule/admin/manage/schedule,topic #34;admin/manage#34; 接受请求?"}
|
||||
n2(["返回 HTTP 404"])
|
||||
n3["进入 API"]
|
||||
n4["取得认证时间"]
|
||||
n5(["返回 HTTP 500"])
|
||||
n6["按 topic #34;admin/manage#34; 选择站点与 Provider"]
|
||||
n7(["返回 HTTP 403"])
|
||||
n8{"读取 Bearer 或站点 Cookie;两者互斥"}
|
||||
n9(["返回 HTTP 401"])
|
||||
n10["认证上下文就绪"]
|
||||
n11(["返回 HTTP 401"])
|
||||
n12["校验 Token 的签名、有效期及站点"]
|
||||
n13{"Cookie 且非 GET / HEAD / OPTIONS?"}
|
||||
n14["检查站点 Origin"]
|
||||
n15(["返回 HTTP 403"])
|
||||
n16(["返回 HTTP 401"])
|
||||
n17{"按 Provider.verify 选择验证函数"}
|
||||
n18[["user.account.verify"]]
|
||||
click n18 href "api/admin/manage.dever#L1,17" "打开源码"
|
||||
n19(["返回 HTTP 业务错误映射状态;其他故障 500"])
|
||||
n20["用验证结果建立身份"]
|
||||
n21(["返回 HTTP 认证错误状态"])
|
||||
n22(["返回 HTTP 500"])
|
||||
n23["建立请求数据库上下文"]
|
||||
n24{"有已验证租户?"}
|
||||
n25["数据库上下文就绪"]
|
||||
n26{"部署启用了租户存储?"}
|
||||
n27(["返回 HTTP 403"])
|
||||
n28["按已验证租户选择数据库"]
|
||||
n29(["返回 HTTP 503"])
|
||||
n30{"检查租户组件启用状态:news, publishing"}
|
||||
n31(["返回 HTTP 403"])
|
||||
n32(["返回 HTTP 503"])
|
||||
n33["进入写入口事务"]
|
||||
n34["失败:保留 HTTP 错误状态"]
|
||||
n35["回滚入口拥有的事务"]
|
||||
n36(["返回 HTTP 500"])
|
||||
n37(["返回错误响应"])
|
||||
n38{"权限检查:publishing.schedule.{site}.schedule"}
|
||||
n39["权限通过"]
|
||||
n40["从可信身份建立 Job 执行上下文"]
|
||||
n41["读取 API 输入"]
|
||||
n42["解码具体参数并检查字段合同"]
|
||||
n43[["publishing.schedule.create"]]
|
||||
click n43 href "api/admin/manage.dever#L1,17" "打开源码"
|
||||
n44["按输出类型检查可序列化值并编码响应"]
|
||||
n45["提交入口拥有的事务"]
|
||||
n46(["返回 HTTP 500"])
|
||||
n47["发布暂存的认证 Header / Cookie"]
|
||||
n48(["返回成功响应"])
|
||||
n0 --> n1
|
||||
n1 -->|"未匹配此入口:继续路由;均未匹配"| n2
|
||||
n1 --> n3
|
||||
n3 --> n4
|
||||
n4 -->|"失败"| n5
|
||||
n4 --> n6
|
||||
n6 -->|"无站点绑定"| n7
|
||||
n6 --> n8
|
||||
n8 -->|"凭据格式错误或冲突"| n9
|
||||
n8 -->|"无凭据"| n11
|
||||
n8 -->|"有凭据"| n13
|
||||
n13 -->|"否"| n12
|
||||
n13 -->|"是"| n14
|
||||
n14 -->|"不符合"| n15
|
||||
n14 -->|"通过"| n12
|
||||
n12 -->|"无 Provider 或 Token 无效"| n16
|
||||
n12 --> n17
|
||||
n17 -->|"user.account.verify"| n18
|
||||
n18 -->|"验证函数失败"| n19
|
||||
n18 --> n20
|
||||
n20 -->|"身份不合法"| n21
|
||||
n20 -->|"通过"| n10
|
||||
n17 -->|"无匹配验证函数"| n22
|
||||
n10 --> n23
|
||||
n23 --> n24
|
||||
n24 -->|"否"| n26
|
||||
n26 -->|"是:缺少租户身份"| n27
|
||||
n26 -->|"否"| n25
|
||||
n24 -->|"是"| n28
|
||||
n28 -->|"选择失败"| n29
|
||||
n28 --> n30
|
||||
n30 -->|"未启用"| n31
|
||||
n30 -->|"查询失败"| n32
|
||||
n30 -->|"允许"| n25
|
||||
n25 --> n33
|
||||
n34 --> n35
|
||||
n35 -->|"事务结束失败"| n36
|
||||
n35 --> n37
|
||||
n33 --> n38
|
||||
n38 -->|"拒绝:Access 状态;查询故障:500"| n34
|
||||
n38 -->|"允许"| n39
|
||||
n39 --> n40
|
||||
n40 -->|"失败:认证错误状态"| n34
|
||||
n40 --> n41
|
||||
n41 -->|"失败:输入错误状态"| n34
|
||||
n41 --> n42
|
||||
n42 -->|"失败:400"| n34
|
||||
n42 --> n43
|
||||
n43 -->|"业务错误映射状态;其他故障:500"| n34
|
||||
n43 --> n44
|
||||
n44 -->|"编码失败:500"| n34
|
||||
n44 --> n45
|
||||
n45 -->|"提交失败"| n46
|
||||
n45 --> n47
|
||||
n47 --> n48
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>源码与子流程</summary>
|
||||
|
||||
| 节点 | 源码 | 子流程 |
|
||||
| --- | --- | --- |
|
||||
| user.account.verify | [api/admin/manage.dever:1:17](api/admin/manage.dever#L1,17) | [user.account.verify](../../user/account/link.md#flow-aaaacb87d61f819c-1) |
|
||||
| publishing.schedule.create | [api/admin/manage.dever:1:17](api/admin/manage.dever#L1,17) | [publishing.schedule.create](link.md#flow-a5c2828f7faa5041-1) |
|
||||
|
||||
</details>
|
||||
|
||||
| 目标 | 归属 | 方式 | 调用位置 |
|
||||
| --- | --- | --- | --- |
|
||||
| [publishing.schedule.create](app.dever#L12) | 领域内 | API 绑定 | [api/admin/manage.dever:1:17](api/admin/manage.dever#L1,17) |
|
||||
| [user.account.verify](../../user/account/app/account.dever#L133) | 跨领域 [user.account](../../user/account/link.md) | 身份验证 | [api/admin/manage.dever:1:17](api/admin/manage.dever#L1,17) |
|
||||
|
||||
### publishing.schedule.complete
|
||||
|
||||
定义:[app.dever:44:1](app.dever#L44,1)、[app.dever:46:1](app.dever#L46,1)、[app.dever:48:1](app.dever#L48,1)
|
||||
|
||||
<a id="flow-87bb9cb4f5f0c2f6-1"></a>
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
n0(["complete"])
|
||||
click n0 href "app.dever#L44,1" "打开源码"
|
||||
n1(["返回"])
|
||||
n3{"按实参匹配分句(互斥)"}
|
||||
n4["status: model.ScheduleStatus.Completed"]
|
||||
click n4 href "app.dever#L44,1" "打开源码"
|
||||
n5["status: model.ScheduleStatus.Cancelled"]
|
||||
click n5 href "app.dever#L46,1" "打开源码"
|
||||
n6["status: model.ScheduleStatus.Pending"]
|
||||
click n6 href "app.dever#L48,1" "打开源码"
|
||||
n7["publishing.schedule.model.get"]
|
||||
click n7 href "app.dever#L49,15" "打开源码"
|
||||
n8["赋值 scheduled"]
|
||||
click n8 href "app.dever#L49,15" "打开源码"
|
||||
n9[["dever.time.now"]]
|
||||
click n9 href "app.dever#L52,53" "打开源码"
|
||||
n10["publishing.schedule.model.exists"]
|
||||
click n10 href "app.dever#L50,21" "打开源码"
|
||||
n11[["dever.api.require"]]
|
||||
click n11 href "app.dever#L50,3" "打开源码"
|
||||
n12["publishing.schedule.model.update"]
|
||||
click n12 href "app.dever#L55,13" "打开源码"
|
||||
n13["赋值 changed"]
|
||||
click n13 href "app.dever#L55,13" "打开源码"
|
||||
n14[["publishing.schedule.publishClaimed"]]
|
||||
click n14 href "app.dever#L63,3" "打开源码"
|
||||
n0 --> n3
|
||||
n3 -->|"status: model.ScheduleStatus.Completed"| n4
|
||||
n4 --> n1
|
||||
n3 -->|"status: model.ScheduleStatus.Cancelled"| n5
|
||||
n5 --> n1
|
||||
n3 -->|"status: model.ScheduleStatus.Pending"| n6
|
||||
n6 --> n7
|
||||
n7 --> n8
|
||||
n8 --> n9
|
||||
n9 --> n10
|
||||
n10 --> n11
|
||||
n11 --> n12
|
||||
n12 --> n13
|
||||
n13 --> n14
|
||||
n14 --> n1
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>源码与子流程</summary>
|
||||
|
||||
| 节点 | 源码 | 子流程 |
|
||||
| --- | --- | --- |
|
||||
| dever.time.now | [app.dever:52:53](app.dever#L52,53) | 依赖或生成的分派(本页不展开) |
|
||||
| dever.api.require | [app.dever:50:3](app.dever#L50,3) | 依赖或生成的分派(本页不展开) |
|
||||
| publishing.schedule.publishClaimed | [app.dever:63:3](app.dever#L63,3) | [publishing.schedule.publishClaimed](link.md#flow-c4a292464addac3f-1) |
|
||||
|
||||
</details>
|
||||
|
||||
| 目标 | 归属 | 方式 | 调用位置 |
|
||||
| --- | --- | --- | --- |
|
||||
| [publishing.schedule.model.get](model.dever#L7) | 领域内 | 内建操作 | [app.dever:49:15](app.dever#L49,15) |
|
||||
| `dever.api.require` | 依赖 | 调用 | [app.dever:50:3](app.dever#L50,3) |
|
||||
| [publishing.schedule.model.exists](model.dever#L7) | 领域内 | 内建操作 | [app.dever:50:21](app.dever#L50,21) |
|
||||
| `dever.time.now` | 依赖 | 调用 | [app.dever:52:53](app.dever#L52,53) |
|
||||
| [publishing.schedule.model.update](model.dever#L7) | 领域内 | 内建操作 | [app.dever:55:13](app.dever#L55,13) |
|
||||
| [publishing.schedule.publishClaimed](app.dever#L66) | 领域内 | 调用 | [app.dever:63:3](app.dever#L63,3) |
|
||||
|
||||
### publishing.schedule.create
|
||||
|
||||
定义:[app.dever:12:1](app.dever#L12,1)
|
||||
|
||||
<a id="flow-a5c2828f7faa5041-1"></a>
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
n0(["create"])
|
||||
click n0 href "app.dever#L12,1" "打开源码"
|
||||
n1["开始事务"]
|
||||
n2(["提交事务,返回"])
|
||||
n3(["回滚事务,传播错误"])
|
||||
n4[["news.article.requireSchedulable"]]
|
||||
click n4 href "app.dever#L15,3" "打开源码"
|
||||
n5["publishing.schedule.model.create"]
|
||||
click n5 href "app.dever#L16,12" "打开源码"
|
||||
n6["赋值 stored"]
|
||||
click n6 href "app.dever#L16,12" "打开源码"
|
||||
n7[["uuid.toText"]]
|
||||
click n7 href "app.dever#L27,5" "打开源码"
|
||||
n8["持久化任务入队"]
|
||||
click n8 href "app.dever#L22,12" "打开源码"
|
||||
n9[["Job:publishing.schedule.job.publish"]]
|
||||
click n9 href "app.dever#L22,12" "打开源码"
|
||||
n10["赋值 queued"]
|
||||
click n10 href "app.dever#L22,12" "打开源码"
|
||||
n11["赋值 discarded"]
|
||||
click n11 href "app.dever#L30,15" "打开源码"
|
||||
n12["赋值 schedule"]
|
||||
click n12 href "app.dever#L31,14" "打开源码"
|
||||
n0 --> n1
|
||||
n1 -.->|"事务体任一未捕获错误"| n3
|
||||
n1 --> n4
|
||||
n4 --> n5
|
||||
n5 --> n6
|
||||
n6 --> n7
|
||||
n7 --> n8
|
||||
n8 -.->|"由 Worker 后续调度,不等待执行"| n9
|
||||
n8 --> n10
|
||||
n10 --> n11
|
||||
n11 --> n12
|
||||
n12 --> n2
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>源码与子流程</summary>
|
||||
|
||||
| 节点 | 源码 | 子流程 |
|
||||
| --- | --- | --- |
|
||||
| news.article.requireSchedulable | [app.dever:15:3](app.dever#L15,3) | [news.article.requireSchedulable](../../news/article/link.md#flow-dc698efe57d88983-1) |
|
||||
| uuid.toText | [app.dever:27:5](app.dever#L27,5) | 依赖或生成的分派(本页不展开) |
|
||||
| Job:publishing.schedule.job.publish | [app.dever:22:12](app.dever#L22,12) | [publishing.schedule.job.publish](link.md#flow-c9df8cbe8f8cd013-1) |
|
||||
|
||||
</details>
|
||||
|
||||
| 目标 | 归属 | 方式 | 调用位置 |
|
||||
| --- | --- | --- | --- |
|
||||
| [news.article.requireSchedulable](../../news/article/app/editing.dever#L107) | 跨领域 [news.article](../../news/article/link.md) | 调用 | [app.dever:15:3](app.dever#L15,3) |
|
||||
| [publishing.schedule.model.create](model.dever#L7) | 领域内 | 内建操作 | [app.dever:16:12](app.dever#L16,12) |
|
||||
| [publishing.schedule.job.publish](job.dever#L3) | 领域内 | 入队 | [app.dever:23:5](app.dever#L23,5) |
|
||||
| `uuid.toText` | 依赖 | 调用 | [app.dever:27:5](app.dever#L27,5) |
|
||||
|
||||
### publishing.schedule.job.publish
|
||||
|
||||
定义:[job.dever:3:1](job.dever#L3,1)
|
||||
|
||||
<a id="flow-c9df8cbe8f8cd013-1"></a>
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
n0(["publish"])
|
||||
click n0 href "job.dever#L3,1" "打开源码"
|
||||
n1(["返回"])
|
||||
n3[["publishing.schedule.publish"]]
|
||||
click n3 href "job.dever#L4,3" "打开源码"
|
||||
n0 --> n3
|
||||
n3 --> n1
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>源码与子流程</summary>
|
||||
|
||||
| 节点 | 源码 | 子流程 |
|
||||
| --- | --- | --- |
|
||||
| publishing.schedule.publish | [job.dever:4:3](job.dever#L4,3) | [publishing.schedule.publish](link.md#flow-1452f352cf5266e6-1) |
|
||||
|
||||
</details>
|
||||
|
||||
| 目标 | 归属 | 方式 | 调用位置 |
|
||||
| --- | --- | --- | --- |
|
||||
| [publishing.schedule.publish](app.dever#L39) | 领域内 | 调用 | [job.dever:4:3](job.dever#L4,3) |
|
||||
|
||||
### publishing.schedule.model.Schedule
|
||||
|
||||
定义:[model.dever:7:1](model.dever#L7,1)
|
||||
|
||||
没有已收录的下游调用。
|
||||
|
||||
### publishing.schedule.model.ScheduleStatus
|
||||
|
||||
定义:[model.dever:1:1](model.dever#L1,1)
|
||||
|
||||
没有已收录的下游调用。
|
||||
|
||||
### publishing.schedule.publish
|
||||
|
||||
定义:[app.dever:39:1](app.dever#L39,1)
|
||||
|
||||
<a id="flow-1452f352cf5266e6-1"></a>
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
n0(["publish"])
|
||||
click n0 href "app.dever#L39,1" "打开源码"
|
||||
n1["开始事务"]
|
||||
n2(["提交事务,返回"])
|
||||
n3(["回滚事务,传播错误"])
|
||||
n4["publishing.schedule.model.get"]
|
||||
click n4 href "app.dever#L40,15" "打开源码"
|
||||
n5["赋值 scheduled"]
|
||||
click n5 href "app.dever#L40,15" "打开源码"
|
||||
n6[["publishing.schedule.complete"]]
|
||||
click n6 href "app.dever#L41,3" "打开源码"
|
||||
n0 --> n1
|
||||
n1 -.->|"事务体任一未捕获错误"| n3
|
||||
n1 --> n4
|
||||
n4 --> n5
|
||||
n5 --> n6
|
||||
n6 --> n2
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>源码与子流程</summary>
|
||||
|
||||
| 节点 | 源码 | 子流程 |
|
||||
| --- | --- | --- |
|
||||
| publishing.schedule.complete | [app.dever:41:3](app.dever#L41,3) | [publishing.schedule.complete](link.md#flow-87bb9cb4f5f0c2f6-1) |
|
||||
|
||||
</details>
|
||||
|
||||
| 目标 | 归属 | 方式 | 调用位置 |
|
||||
| --- | --- | --- | --- |
|
||||
| [publishing.schedule.model.get](model.dever#L7) | 领域内 | 内建操作 | [app.dever:40:15](app.dever#L40,15) |
|
||||
| [publishing.schedule.complete](app.dever#L44) | 领域内 | 调用 | [app.dever:41:3](app.dever#L41,3) |
|
||||
|
||||
### publishing.schedule.publishClaimed
|
||||
|
||||
定义:[app.dever:66:1](app.dever#L66,1)、[app.dever:68:1](app.dever#L68,1)、[app.dever:72:1](app.dever#L72,1)
|
||||
|
||||
<a id="flow-c4a292464addac3f-1"></a>
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
n0(["publishClaimed"])
|
||||
click n0 href "app.dever#L66,1" "打开源码"
|
||||
n1(["返回"])
|
||||
n2(["传播错误"])
|
||||
n3{"按实参匹配分句(互斥)"}
|
||||
n4["changed: 0"]
|
||||
click n4 href "app.dever#L66,1" "打开源码"
|
||||
n5["changed: 1"]
|
||||
click n5 href "app.dever#L68,1" "打开源码"
|
||||
n6[["news.article.publishScheduled"]]
|
||||
click n6 href "app.dever#L69,17" "打开源码"
|
||||
n7["赋值 publication"]
|
||||
click n7 href "app.dever#L69,17" "打开源码"
|
||||
n8["changed: other"]
|
||||
click n8 href "app.dever#L72,1" "打开源码"
|
||||
n9["fail dever.api.Error.Conflict"]
|
||||
click n9 href "app.dever#L73,8" "打开源码"
|
||||
n0 --> n3
|
||||
n3 -->|"changed: 0"| n4
|
||||
n4 --> n1
|
||||
n3 -->|"changed: 1"| n5
|
||||
n5 --> n6
|
||||
n6 --> n7
|
||||
n7 --> n1
|
||||
n3 -->|"changed: other"| n8
|
||||
n8 --> n9
|
||||
n9 --> n2
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>源码与子流程</summary>
|
||||
|
||||
| 节点 | 源码 | 子流程 |
|
||||
| --- | --- | --- |
|
||||
| news.article.publishScheduled | [app.dever:69:17](app.dever#L69,17) | [news.article.publishScheduled](../../news/article/link.md#flow-5ba4105cff8222e5-1) |
|
||||
|
||||
</details>
|
||||
|
||||
| 目标 | 归属 | 方式 | 调用位置 |
|
||||
| --- | --- | --- | --- |
|
||||
| [news.article.publishScheduled](../../news/article/app/publication.dever#L22) | 跨领域 [news.article](../../news/article/link.md) | 调用 | [app.dever:69:17](app.dever#L69,17) |
|
||||
@@ -0,0 +1,12 @@
|
||||
type ScheduleStatus {
|
||||
Pending = "待发布"
|
||||
Completed = "已完成"
|
||||
Cancelled = "已取消"
|
||||
}
|
||||
|
||||
type Schedule {
|
||||
uuid: Uuid generated unique
|
||||
article_id: news.article.model.id unique
|
||||
publish_at: DateTime index
|
||||
status: ScheduleStatus default ScheduleStatus.Pending index
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# 健康检查
|
||||
|
||||
提供命令行健康检查。
|
||||
|
||||
- [健康检查命令](doc/api.md)
|
||||
- [系统健康](doc/app.md)
|
||||
- [调用流程](link.md)
|
||||
Loaded 100 of 1479 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user