create-dwy 0.26.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/package.json +1 -1
- package/src/cli-help.js +11 -13
- package/src/sync-all.js +86 -11
- package/src/sync-plan.js +9 -1
- package/templates/ai-tools/CHANGELOG.md +13 -0
- package/templates/ai-tools/VERSION +1 -1
- package/templates/ai-tools/skills/Android/dwy-android-architecture/SKILL.md +10 -0
- package/templates/ai-tools/skills/Android/dwy-android-development/SKILL.md +16 -0
- package/templates/ai-tools/skills/Apple/dwy-apple-appkit/SKILL.md +17 -0
- package/templates/ai-tools/skills/Apple/dwy-apple-architecture/SKILL.md +12 -0
- package/templates/ai-tools/skills/Apple/dwy-apple-architecture/references/extraction.md +9 -0
- package/templates/ai-tools/{rules/iOS/dwy-apple-layering.md → skills/Apple/dwy-apple-architecture/references/layering.md} +2 -10
- package/templates/ai-tools/skills/Apple/dwy-apple-architecture/references/pitfalls.md +3 -0
- package/templates/ai-tools/skills/Apple/dwy-ios-development/SKILL.md +14 -0
- package/templates/ai-tools/skills/Apple/dwy-ios-development/references/appearance.md +6 -0
- package/templates/ai-tools/{rules/iOS/dwy-ios-layout-adaptation.md → skills/Apple/dwy-ios-development/references/layout.md} +0 -6
- package/templates/ai-tools/{rules/iOS/dwy-ios-pitfalls.md → skills/Apple/dwy-ios-development/references/pitfalls.md} +1 -9
- package/templates/ai-tools/skills/Apple/dwy-ios-development/references/presentations.md +11 -0
- package/templates/ai-tools/skills/Apple/dwy-macos-development/SKILL.md +14 -0
- package/templates/ai-tools/skills/Apple/dwy-macos-development/references/pitfalls.md +9 -0
- package/templates/ai-tools/skills/Apple/dwy-macos-development/references/presentations.md +5 -0
- package/templates/ai-tools/skills/Apple/dwy-macos-development/references/ui-adaptation.md +6 -0
- package/templates/ai-tools/skills/Apple/dwy-swift-development/SKILL.md +16 -0
- package/templates/ai-tools/{rules/iOS/dwy-swift-core.md → skills/Apple/dwy-swift-development/references/conventions.md} +2 -8
- package/templates/ai-tools/skills/Apple/dwy-swift-development/references/pitfalls.md +5 -0
- package/templates/ai-tools/skills/Apple/dwy-swift-development/references/presentations.md +8 -0
- package/templates/ai-tools/skills/Docker/dwy-docker/SKILL.md +2 -0
- package/templates/ai-tools/skills/Flutter/dwy-flutter-development/SKILL.md +15 -0
- package/templates/ai-tools/skills/HarmonyOS/dwy-harmony-development/SKILL.md +15 -0
- package/templates/ai-tools/skills/Python/dwy-apple-notify/SKILL.md +24 -0
- package/templates/ai-tools/skills/Python/dwy-apple-notify/references/v0/0.1.6.md +81 -0
- package/templates/ai-tools/skills/Python/dwy-auth/SKILL.md +24 -0
- package/templates/ai-tools/skills/Python/dwy-auth/references/v0/0.2.0.md +163 -0
- package/templates/ai-tools/skills/Python/dwy-device-fingerprint/SKILL.md +19 -0
- package/templates/ai-tools/skills/Python/dwy-device-fingerprint/references/v0/0.1.1.md +90 -0
- package/templates/ai-tools/skills/Python/dwy-eapi/SKILL.md +1 -1
- package/templates/ai-tools/skills/Python/dwy-eapi/versions.yml +3 -3
- package/templates/ai-tools/skills/Python/dwy-email-notify/SKILL.md +19 -0
- package/templates/ai-tools/skills/Python/dwy-email-notify/references/v0/0.1.0.md +31 -0
- package/templates/ai-tools/skills/Python/dwy-notify/SKILL.md +24 -0
- package/templates/ai-tools/skills/Python/dwy-notify/references/v0/0.3.2.md +128 -0
- package/templates/ai-tools/skills/Python/dwy-payment/SKILL.md +19 -0
- package/templates/ai-tools/skills/Python/dwy-payment/references/v0/0.3.0.md +90 -0
- package/templates/ai-tools/skills/Python/dwy-python-api/SKILL.md +8 -0
- package/templates/ai-tools/skills/Python/dwy-python-architecture/SKILL.md +8 -0
- package/templates/ai-tools/skills/Python/dwy-python-development/SKILL.md +17 -0
- package/templates/ai-tools/{rules/Python → skills/Python/dwy-python-development/references}/dwy-python-base-lib.md +3 -3
- package/templates/ai-tools/skills/Python/dwy-webhooks/SKILL.md +24 -0
- package/templates/ai-tools/skills/Python/dwy-webhooks/references/v0/0.6.2.md +238 -0
- package/templates/ai-tools/skills/Python/dwy-wechatpay-notify/SKILL.md +19 -0
- package/templates/ai-tools/skills/Python/dwy-wechatpay-notify/references/v0/0.1.1.md +101 -0
- package/templates/ai-tools/skills/Vue/dwy-admin-kit/SKILL.md +12 -0
- package/templates/ai-tools/skills/Vue/dwy-admin-kit/references/v0/0.3.0.md +176 -0
- package/templates/ai-tools/skills/Vue/dwy-auth-web/SKILL.md +10 -0
- package/templates/ai-tools/skills/Vue/dwy-ekit/SKILL.md +2 -3
- package/templates/ai-tools/skills/Vue/dwy-eui/SKILL.md +1 -1
- package/templates/ai-tools/skills/Vue/dwy-eui/versions.yml +2 -2
- package/templates/ai-tools/skills/Vue/dwy-vue-architecture/SKILL.md +8 -0
- package/templates/ai-tools/skills/Vue/dwy-vue-development/SKILL.md +16 -0
- package/templates/ai-tools/{rules/Vue → skills/Vue/dwy-vue-development/references}/dwy-vue-base-lib.md +3 -3
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-architecture/SKILL.md +7 -7
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/SKILL.md +2 -0
- package/templates/ai-tools/skills//346/225/260/346/215/256/345/272/223/dwy-alembic/SKILL.md +8 -0
- package/templates/ai-tools/skills//346/225/260/346/215/256/345/272/223/dwy-postgres/SKILL.md +10 -0
- package/templates/ai-tools/skills//346/225/260/346/215/256/345/272/223/dwy-redis/SKILL.md +8 -0
- package/templates/ai-tools/skills//351/200/232/347/224/250/dwy-dependency-choice/SKILL.md +1 -1
- package/templates/ai-tools/skills//351/200/232/347/224/250/dwy-development-review/SKILL.md +2 -0
- package/templates/ai-tools/skills//351/200/232/347/224/250/dwy-git-commit/SKILL.md +1 -1
- /package/templates/ai-tools/{rules/Android → skills/Android/dwy-android-architecture/references}/dwy-android-layering.md +0 -0
- /package/templates/ai-tools/{rules/Android → skills/Android/dwy-android-architecture/references}/dwy-android-mvp.md +0 -0
- /package/templates/ai-tools/{rules/Android → skills/Android/dwy-android-development/references}/dwy-android-core.md +0 -0
- /package/templates/ai-tools/{rules/Android → skills/Android/dwy-android-development/references}/dwy-android-pitfalls.md +0 -0
- /package/templates/ai-tools/{rules/Android → skills/Android/dwy-android-development/references}/dwy-android-support-only.md +0 -0
- /package/templates/ai-tools/{rules/Docker → skills/Docker/dwy-docker/references}/dwy-docker.md +0 -0
- /package/templates/ai-tools/{rules/Flutter → skills/Flutter/dwy-flutter-development/references}/dwy-flutter-core.md +0 -0
- /package/templates/ai-tools/{rules/Flutter → skills/Flutter/dwy-flutter-development/references}/dwy-flutter-network.md +0 -0
- /package/templates/ai-tools/{rules/Flutter → skills/Flutter/dwy-flutter-development/references}/dwy-flutter-router.md +0 -0
- /package/templates/ai-tools/{rules/Flutter → skills/Flutter/dwy-flutter-development/references}/dwy-flutter-testing.md +0 -0
- /package/templates/ai-tools/{rules/HarmonyOS → skills/HarmonyOS/dwy-harmony-development/references}/dwy-arkts-strict.md +0 -0
- /package/templates/ai-tools/{rules/HarmonyOS → skills/HarmonyOS/dwy-harmony-development/references}/dwy-arkts-ui-build.md +0 -0
- /package/templates/ai-tools/{rules/HarmonyOS → skills/HarmonyOS/dwy-harmony-development/references}/dwy-arkts-ui-design.md +0 -0
- /package/templates/ai-tools/{rules/HarmonyOS → skills/HarmonyOS/dwy-harmony-development/references}/dwy-harmony-pitfalls.md +0 -0
- /package/templates/ai-tools/{rules/Python → skills/Python/dwy-python-api/references}/dwy-python-backend.md +0 -0
- /package/templates/ai-tools/{rules/Python → skills/Python/dwy-python-architecture/references}/dwy-python-layering.md +0 -0
- /package/templates/ai-tools/{rules/Python → skills/Python/dwy-python-development/references}/dwy-python-core.md +0 -0
- /package/templates/ai-tools/{rules/Python → skills/Python/dwy-python-development/references}/dwy-python-pitfalls.md +0 -0
- /package/templates/ai-tools/{rules/Python → skills/Python/dwy-python-development/references}/dwy-python-testing.md +0 -0
- /package/templates/ai-tools/{rules/Vue → skills/Vue/dwy-vue-architecture/references}/dwy-vue-layering.md +0 -0
- /package/templates/ai-tools/{rules/Vue → skills/Vue/dwy-vue-development/references}/dwy-vue-core.md +0 -0
- /package/templates/ai-tools/{rules/Vue → skills/Vue/dwy-vue-development/references}/dwy-vue-pinia.md +0 -0
- /package/templates/ai-tools/{rules/Vue → skills/Vue/dwy-vue-development/references}/dwy-vue-pitfalls.md +0 -0
- /package/templates/ai-tools/{rules/Vue → skills/Vue/dwy-vue-development/references}/dwy-vue-testing.md +0 -0
- /package/templates/ai-tools/{rules//345/274/200/345/217/221/346/265/201/347/250/213 → skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references}/dwy-prod-branch.md" +0 -0
- /package/templates/ai-tools/{rules//346/225/260/346/215/256/345/272/223 → skills//346/225/260/346/215/256/345/272/223/dwy-alembic/references}/dwy-db-migration.md" +0 -0
- /package/templates/ai-tools/{rules//346/225/260/346/215/256/345/272/223 → skills//346/225/260/346/215/256/345/272/223/dwy-postgres/references}/dwy-postgres.md" +0 -0
- /package/templates/ai-tools/{rules//346/225/260/346/215/256/345/272/223 → skills//346/225/260/346/215/256/345/272/223/dwy-redis/references}/dwy-redis.md" +0 -0
- /package/templates/ai-tools/{rules//345/274/200/345/217/221/346/265/201/347/250/213 → skills//351/200/232/347/224/250/dwy-dependency-choice/references}/dwy-dependency-freshness.md" +0 -0
- /package/templates/ai-tools/{rules//345/274/200/345/217/221/346/265/201/347/250/213 → skills//351/200/232/347/224/250/dwy-dependency-choice/references}/dwy-oss-commercial-license.md" +0 -0
- /package/templates/ai-tools/{rules//345/274/200/345/217/221/346/265/201/347/250/213 → skills//351/200/232/347/224/250/dwy-development-review/references}/dwy-code-craft.md" +0 -0
- /package/templates/ai-tools/{rules//345/274/200/345/217/221/346/265/201/347/250/213 → skills//351/200/232/347/224/250/dwy-git-commit/references}/dwy-git-commit.md" +0 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<!-- 来源:dwy-notify@0.3.2 的 backend/packages/notify/README.md;文中相对链接相对原仓库路径。 -->
|
|
2
|
+
|
|
3
|
+
# dwy-notify
|
|
4
|
+
|
|
5
|
+
独立多目标异步通知包,基于 `apprise==2.1.0`,公共配置和结果类型由本包拥有。依赖 `dwyeapi==0.12.1` 基础规范,不要求运行 Webhook 服务。
|
|
6
|
+
|
|
7
|
+
来源无关的通知集成入口:
|
|
8
|
+
|
|
9
|
+
- `dwy_notify.events.NotificationEvent`、`EventSink`:已验证事件的接收合同,不包含原始签名或邮件正文。
|
|
10
|
+
- `dwy_notify.relay.NotificationRelay`:单次转发和有界成功去重;调用方通过 `appearance` 注入模块名称与可选头像,不按来源猜测业务预设。
|
|
11
|
+
- `dwy_notify.delivery`:`DeliveryPolicy`、`NotificationDeliveryError`、`deliver_notifications`、`delivery_failure`,复用逐目标检查点和安全退避;不创建数据库。
|
|
12
|
+
- `dwy_notify.sqlalchemy`:通过 `dwy-notify[sqlalchemy]` 安装的 `build_delivery_table`、`SQLAlchemyDeliveryQueue`;会话、表、生命周期、迁移均归宿主,队列显式接收 `appearance`。
|
|
13
|
+
- `dwy_notify.integration`:`NotificationSettings`、`configured_notifier` 等目标选择与历史 Discord 配置适配,保留旧 URL 指纹;新集成优先使用 `NotificationConfig`。
|
|
14
|
+
|
|
15
|
+
基础发送器不依赖 Apple、邮件或 Webhook 业务包。新模块使用自己的展示预设;历史兼容字段的旧名称仅为保证已有队列目标指纹稳定,不作为新业务的默认名称。
|
|
16
|
+
|
|
17
|
+
实际 wheel 的发布验收在仓库外执行 `tests/delivery_wheel_smoke.py EXPECTED_VERSION`;需安装 `sqlalchemy` extra 与测试 SQLite 驱动。它核对安装版本、基础导入不加载 ORM/FastAPI/旧业务包,以及队列持久化显式预设和重启去重;不向外部服务发消息。
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install dwy-notify==0.3.0
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
import asyncio
|
|
25
|
+
import os
|
|
26
|
+
|
|
27
|
+
from dwy_notify import NotificationAppearance, NotificationConfig, Notifier
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def main() -> None:
|
|
31
|
+
notifier = Notifier(NotificationConfig(
|
|
32
|
+
targets={
|
|
33
|
+
"discord": os.environ["DISCORD_NOTIFY_URL"],
|
|
34
|
+
"wecom": os.environ["WECOM_NOTIFY_URL"],
|
|
35
|
+
},
|
|
36
|
+
appearance=NotificationAppearance(username="业务通知"),
|
|
37
|
+
))
|
|
38
|
+
result = await notifier.send("任务完成", title="每日汇总")
|
|
39
|
+
for delivery in result.deliveries:
|
|
40
|
+
if not delivery.success:
|
|
41
|
+
print(delivery.target, delivery.error_code, delivery.retry_after, delivery.permanent)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
asyncio.run(main())
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`DISCORD_NOTIFY_URL` 来自宿主环境,可用 `discord://WEBHOOK_ID/WEBHOOK_TOKEN` 或 `https://discord.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN`。`WECOM_NOTIFY_URL` 使用 `wecombot://WEBHOOK_KEY`。凭据应来自环境变量或秘密存储,不写入版本库。配置使用 `SecretStr` 隐藏 URL;调用方不要解密后记录。
|
|
48
|
+
|
|
49
|
+
`NotificationConfig(targets=..., timeout_seconds=10)` 为 frozen Pydantic 模型,目标映射非空,超时必须为有限正数且不超过 30 秒,不接受布尔值。`Notifier` 在构造时固定目标快照,不读取环境变量。`targets` 返回目标名元组,`target_fingerprint(name)` 返回 URL 的 SHA-256 摘要,供调用方检测配置变更;摘要不包含明文 URL。
|
|
50
|
+
|
|
51
|
+
`await notifier.send(body, title="", targets=None)` 默认发送全部目标,返回 `NotificationResult`。`success` 仅在至少一个目标且全部成功时为真,`succeeded` / `failed` 是目标名元组,详情位于 `deliveries`。每条 `NotificationDelivery` 包含 `target`、`success`、`message_id`、`error_code`、`retry_after`、`permanent`。正文必须为非空白字符串,标题必须为字符串,显式 targets 必须非空且不重复;无效输入或未知目标在发请求前抛出固定 `ValueError`。调用方应遵守 `retry_after` 并排除永久失败,再用失败目标列表重试。
|
|
52
|
+
|
|
53
|
+
安全边界:
|
|
54
|
+
|
|
55
|
+
- HTTP 渠道强制 TLS 校验、禁重定向、禁 SDK retry 和 optional 成功;覆盖 URL 中相反选项。普通渠道沿用 Apprise 的 HTTPS 协议和安全参数,不使用全局 monkeypatch。
|
|
56
|
+
- Discord 复用上游解析、消息构造和分段,仅适配单次 HTTP 发送:`allowed_mentions={"parse": []}`、始终设置 flags 的 `4` 位,并用 `wait=true` 确认真实消息 ID;响应 ID 必须为 1 至 128 位 ASCII 数字。需要分段时使用 Apprise 的 `?overflow=split`,分段遇失败停止后续请求。企业微信仅在 JSON `errcode` 为整数 `0` 时成功。
|
|
57
|
+
- Discord / 企业微信返回 HTTP 状态对应的安全错误码,429 的有效秒数放入 `retry_after`,408 与 5xx 可重试。普通渠道以 SDK 返回值判断成功,失败统一 `send_failed`,不暴露原始响应或异常。插件日志通过本包子类隔离;urllib3 原始 HTTP 日志仅在通知工作线程的发送区间被过滤,宿主其它线程、普通 HTTP 调用与应用日志保留。
|
|
58
|
+
- 支持 Apprise 基于 `requests` 的 HTTPS 渠道,例如 Discord、企业微信、钉钉、飞书和 `jsons` 自定义 HTTPS;不支持 SMTP、本地命令等非 HTTP 协议或明文协议,也拒绝固定版本中自带内部重试的 Fluxer、Revolt、Matrix、SparkPost、SendPulse,构造时返回 `unsupported_transport`。可选渠道的额外依赖仍需由宿主安装。
|
|
59
|
+
- 同一目标串行,不同目标可并发。取消等待会传播 `CancelledError`;已开始的同步请求在线程中继续,由连接/读取超时结束,同目标锁直到请求结束才释放。超时是 socket 超时,不是整条通知的硬截止时间;上游分段、多接收人或节流可能增加总耗时。
|
|
60
|
+
- `Notifier` 不提供持久化、调度或自动重试。分段通知中已经成功的部分可能在调用方整体重试时重复;HTTP 接收后丢失响应也无法保证恰好一次。
|
|
61
|
+
|
|
62
|
+
仅 Discord、企业微信、钉钉和飞书通过模拟 HTTP 验证;未进行真实渠道投递验收,也未逐一验收全部 Apprise 插件。发布必须通过 GitHub Actions OIDC,不使用长期发布 token。
|
|
63
|
+
|
|
64
|
+
## 发送者名称与可选头像
|
|
65
|
+
|
|
66
|
+
`NotificationAppearance(username=..., avatar_url=...)` 只描述平台基础展示,不包含购买、审核等业务预设。每个目标最终必须具有发送者名称;所有配置层都未提供名称时,`send` 在任何目标发出网络请求前抛出 `ValueError("missing_username")`。正文 `title` 不能代替发送者名称。头像可省略,沿用平台默认头像,不注入 Apprise Logo。
|
|
67
|
+
|
|
68
|
+
应用在 `NotificationConfig.appearance` 设置默认展示;单次 `send(..., appearance=...)` 可覆盖;具体业务库通过 `send(..., default_appearance=...)` 提供低优先级预设。按字段选择:单次设置 > 应用配置 > 目标 URL 的显式名称/头像 > 业务预设。未设置字段继承;名称不能显式设为 `None` 或空白,头像可通过 `avatar_url=None` 清除预设、恢复平台默认。仅覆盖头像时,名称继续从应用或具体模块继承。
|
|
69
|
+
|
|
70
|
+
名称限制为 1–80 字符且不包含控制字符;图片 URL 限长 2048,要求 HTTPS 且不携带账号密码。图片须由平台匿名访问,不接受本地路径,不在应用发送进程中下载图片。当前 Discord 适配使用这些字段,其它渠道保留自身展示行为。展示不参与目标 URL 指纹,不改变接收方去重或发送重试。
|
|
71
|
+
|
|
72
|
+
兼容层若把隐式默认名称编码进旧 URL,构造 `Notifier(..., url_appearance_is_default=True)` 可让这些展示字段退到业务预设之后,同时保留原目标 URL 与指纹;真实用户显式设置的 URL 保持默认优先级。
|
|
73
|
+
|
|
74
|
+
## 追加公开展示字段
|
|
75
|
+
|
|
76
|
+
`NotificationField` 与 `render_notification_fields` 从 `dwy_notify` 根导出,用于给正文追加少量业务标签。宿主先从用户、订单等对象中筛选公开字符串,再构造字段;不要传完整对象、凭据或个人敏感信息。它们不读取数据库或网络,不修改官方回调数据,也不改变 `send` 和队列接口。
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from dwy_notify import NotificationField, render_notification_fields
|
|
80
|
+
|
|
81
|
+
body = "支付成功"
|
|
82
|
+
fields = (
|
|
83
|
+
NotificationField(label="来源", value="网站"),
|
|
84
|
+
NotificationField(label="套餐", value="专业版"),
|
|
85
|
+
)
|
|
86
|
+
# 2048 是宿主选定的正文总预算,额外预留一个拼接换行的 UTF-8 字节。
|
|
87
|
+
extra = render_notification_fields(fields, max_bytes=2048 - len(body.encode("utf-8")) - 1)
|
|
88
|
+
content = f"{body}\n{extra}" if extra else body
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
模型冻结、拒绝额外属性,`label` / `value` 必须为严格字符串,分别为 1–32 / 1–256 个字符,不能全为空白,也不能包含控制字符、格式控制字符(含双向文本控制和零宽连接符)、孤立代理项或 Unicode 行分隔符。校验错误的文本不回显输入;调用方记录 Pydantic 结构化错误时仍须移除 `input`。
|
|
92
|
+
|
|
93
|
+
渲染接收最多八个 `NotificationField` 的元组,保留顺序,每行采用「标签:值」。`max_bytes` 必须是整数(不接受布尔),表示本段剩余 UTF-8 字节预算;正文、标题、分隔符由宿主另外预留。值截短后加 `…`,其余字段放不下时加 `[已省略]`;标签不会截短,提示也放不下时返回空串,零或负预算同样返回空串。字段数量、类型或预算类型无效时抛出固定原因码 `ValueError`,不会回显内容。
|
|
94
|
+
|
|
95
|
+
输出沿用当前传输:企业微信发送 `text.content`,不添加 `mentioned_list` / `mentioned_mobile_list`,Markdown 和 `@all` 为普通文本;Discord 的 `allowed_mentions={"parse": []}` 继续禁用提醒。渲染器不添加 Markdown 转义,不提供跨所有 Apprise 渠道的富文本安全保证。
|
|
96
|
+
|
|
97
|
+
## 与业务共用事务及失败重放
|
|
98
|
+
|
|
99
|
+
`SQLAlchemyDeliveryQueue` 通过 `dwy-notify[sqlalchemy]` 使用 PostgreSQL 或 SQLite。`enqueue` 继续创建并提交独立事务;需要与支付、订单等业务状态同时提交时,调用 `enqueue_in_session(db, app_id, event_id, event_type, content)`。`db` 是宿主的 `AsyncSession`,队列不会 begin、commit、rollback 或 close 它。返回 `True` 表示新入队,`False` 表示同一 `(app_id, event_id)` 已存在;冲突忽略不会使外层事务失效。
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from dwy_notify.sqlalchemy import SQLAlchemyDeliveryQueue
|
|
103
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
async def enqueue_payment_notice(
|
|
107
|
+
db: AsyncSession, queue: SQLAlchemyDeliveryQueue, order_id: str, content: str,
|
|
108
|
+
) -> bool:
|
|
109
|
+
"""由宿主把业务更新和本次入队一同提交,异常时一同回滚。"""
|
|
110
|
+
return await queue.enqueue_in_session(db, "payments", order_id, "payment.paid", content)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
async def replay_failed_notice(
|
|
114
|
+
db: AsyncSession, queue: SQLAlchemyDeliveryQueue, order_id: str,
|
|
115
|
+
) -> bool:
|
|
116
|
+
"""宿主完成操作者鉴权与审计后,仅重放账本中失败的指定目标。"""
|
|
117
|
+
return await queue.requeue_failed_in_session(db, "payments", order_id, ("wecom",))
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
scope 非空白且不超过注入表 `app_id` 的长度(默认 128);`event_id`、`event_type` 非空白且不超过 128 字符;正文非空白且最多 65536 字符。内容完整保留,超限拒绝,不截断。`ValueError` 仅包含固定原因码,不回显标识、正文或目标 URL。
|
|
121
|
+
|
|
122
|
+
`allowed_scopes` 限制消费者领取范围:`None` 领取全部 scope,空集合不领取;显式集合只处理其中的 scope。同表其它 scope 的积压正文、状态和租约保持原样,不会因当前消费者的配置被清除。
|
|
123
|
+
|
|
124
|
+
`requeue_failed_in_session` 要求队列注入 `notifier`,`targets` 为非空、不重复的目标名元组。只把指定的 `dead` / `retry` 目标重置为 `pending`、尝试次数 0、当前时刻可发;父记录重新聚合调度与失败状态。正文、展示快照、原目标指纹及其它目标的状态保持不变;当前配置新增的目标不会收到历史消息。无记录、尚未初始化目标快照或选中目标没有失败状态时返回 `False`。
|
|
125
|
+
|
|
126
|
+
成功目标抛出 `notification_target_sent`;快照中不存在的目标抛出 `notification_target_not_in_snapshot`;当前已移除或指纹变化的目标分别抛出 `notification_target_missing` / `notification_target_changed`;活跃父租约抛出 `notification_active_lease`。先验证全部选择再重置,混合选择不会部分生效。重放自身不发送消息,由既有消费者在事务提交后处理;外层回滚会撤销重放。该接口持有数据库写锁直到宿主提交或回滚,宿主应及时结束事务;SQLite 需要支持 `RETURNING` 的 3.35 及以上版本。
|
|
127
|
+
|
|
128
|
+
过期租约的 token 会在重放时清除,旧消费者无法覆盖新状态。网络已接收但响应丢失的失败记录仍可能在重放后产生重复消息,沿用原投递语义。
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dwy-payment
|
|
3
|
+
description: 使用或排查 dwy-payment 服务端支付能力时使用;先核实项目安装版本及公开 API。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# dwy-payment
|
|
7
|
+
|
|
8
|
+
支付网关、订单和回调;HTTP、认证与权益由宿主处理,核实回调验签、幂等和持久化边界。
|
|
9
|
+
|
|
10
|
+
## 查找与使用
|
|
11
|
+
|
|
12
|
+
项目使用 0.3.0 时可阅读 [0.3.0 接入和示例](references/v0/0.3.0.md);其他版本先查该版本的资料和代码,不把此快照当作其他版本的合同。
|
|
13
|
+
|
|
14
|
+
1. 检查项目锁文件和当前 Python 环境;用 `importlib.metadata.version("dwy-payment")` 查询实际发行版本,依赖范围不等于实际安装版本。未安装时不得假设可用或自动安装。
|
|
15
|
+
2. 在实际环境中检查 `dwy_payment` 的公开接口、类型及项目已有调用;editable 安装以实际源码为准。说明、实现或安装版本不一致时以运行代码为准。
|
|
16
|
+
3. 本索引基于仓库 0.3.0,能力与签名可能随小版本变化。精确版本源码位于仓库 `backend/packages/payment`,文档为该目录的 `README.md`;只在确认其版本与消费项目一致时使用。缺少安装源码时查对应 `dwy-payment@版本` 标签,不能用默认分支冒充。
|
|
17
|
+
4. 在已有调用和该版本文档中核对配置、必需依赖、输入校验、失败处理及生命周期后接入。没有足够证据时说明缺口,不猜 API。
|
|
18
|
+
|
|
19
|
+
不要因为发现此库就改动项目依赖;新增依赖遵循项目选型规则。关联 FastAPI 或其他自有包时同时参考可用的对应 Skill。
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<!-- 来源:dwy-payment@0.3.0 的 backend/packages/payment/README.md;文中相对链接相对原仓库路径。 -->
|
|
2
|
+
|
|
3
|
+
# dwy-payment
|
|
4
|
+
|
|
5
|
+
业务无关的服务端支付与退款能力。Python 3.11/3.12,微信普通商户APIv3 Native支付、回调核验、支付和退款资金记录;支付宝订单码作为可选extra保留。审核、管理员密码、业务订单、流量和权益由宿主实现。
|
|
6
|
+
|
|
7
|
+
## 安装与接入
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
python -m pip install 'dwy-payment==0.3.0'
|
|
11
|
+
# 已有支付宝业务可按需安装,不是微信接入的必需依赖。
|
|
12
|
+
python -m pip install 'dwy-payment[alipay]==0.3.0'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
安装不初始化数据库、不挂载HTTP、不读取环境变量、不连接微信。商户凭据由宿主安全读取并构造 `WechatPayConfig`;不要在前端或客户端存放私钥。回调地址必须指向宿主提供的公网HTTPS接口,不是企业微信机器人地址。
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from dwy_payment import PaymentConfig, WechatPayConfig, create_payment
|
|
19
|
+
from dwy_payment.gateways.wechat_native import WechatNativeGateway
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def checkout(session_factory, *, merchant: WechatPayConfig, business_key: str):
|
|
23
|
+
"""宿主先提交自己的业务订单;business_key用于跨重试关联,不依赖请求会话。"""
|
|
24
|
+
config = PaymentConfig(wechat=merchant)
|
|
25
|
+
result = await create_payment(
|
|
26
|
+
session_factory=session_factory, gateway=WechatNativeGateway(merchant), config=config,
|
|
27
|
+
user_id="customer_1", amount_fen=1000, description="商品订单",
|
|
28
|
+
idempotency_key=business_key,
|
|
29
|
+
)
|
|
30
|
+
return result
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`user_id`只是宿主关联标识,不外键到用户表。`create_payment`先独立提交稳定意图再触网,同键同参数复用原单,同键不同参数拒绝;未知下单按原号和原截止重取二维码,不另建资金记录。微信官方说明支持[同订单号与原参数重新获取二维码](https://pay.wechatpay.cn/doc/v3/merchant/4054257695)。跨项目首期采用独立数据库或schema,业务订单号、支付单号、退款单号须分别保存映射,幂等键可在结果中帮助业务恢复关联。
|
|
34
|
+
|
|
35
|
+
金额使用整数分,只支持已实现的CNY范围。`PaymentService.create`保留旧同事务桥接接口,成功时不提交,要求宿主完成关联提交;它不提供独立指令的崩溃持久保证。没有幂等键时每次创建新支付;新接入使用上面的独立命令与稳定键。
|
|
36
|
+
|
|
37
|
+
宿主负责HTTP读取上限、路由与鉴权,使用 `PaymentService.handle_notify(headers, body)` 处理支付回调,使用渠道 `parse_refund_notify` 和 `RefundResultService.apply` 处理退款回调。退款结果更新与同事务钩子由宿主统一commit后再返回渠道成功应答。微信SDK验签、商户/应用与金额核对不能绕过;不自行构造已验证对象当作渠道证据。
|
|
38
|
+
|
|
39
|
+
## 退款与资金安全
|
|
40
|
+
|
|
41
|
+
`dwy_payment.refunds.commands.request_refund` 接受已持久支付记录、固定退款号和整数金额,显式提供独立会话工厂与微信网关。调用前业务方完成审核、密码确认、权益预留并提交;不要持有父业务锁跨独立资金命令。命令不接收管理员密码,也不判断退款政策。
|
|
42
|
+
|
|
43
|
+
金额占用包含已成功、处理中和待核对退款,仅明确关闭后释放;同一支付的本库退款意图串行核算,不能并发超退。网络超时保留同号查询,只有渠道明确不存在才同号同参数恢复发送,不能以新退款号重试。资金占用不能阻止商户后台在本库之外发起退款;外部退款观察后保守占用并查证,最终以渠道事实为准。
|
|
44
|
+
|
|
45
|
+
历史已退金额缺少可证明来源的退款明细时,新退款保持阻挡并要求人工核对;不凭金额相等推断两笔退款是同一笔。已存在同号意图仍允许查询恢复。
|
|
46
|
+
|
|
47
|
+
`RefundRecord`及其已核验状态是逐笔资金事实。`PaymentOrder.status/refunded_fen/refund_no`保留既有宿主业务投影兼容语义,不能将这些字段当作累计渠道到账证明;纯资金接入读取退款明细或资金事件,不调用 `mark_refunded` 推断到账。
|
|
48
|
+
|
|
49
|
+
`RefundResultService` 只登记核验状态,不自行提交;`reconcile_refunds` 由宿主每分钟调度,并显式提供会话工厂和网关。宿主负责任务生命周期与异常监控。退款“已受理”不代表成功,`ABNORMAL`保留占用并继续核对,不能据此再次付款或解除业务冻结。
|
|
50
|
+
|
|
51
|
+
## 可靠结果交付
|
|
52
|
+
|
|
53
|
+
付款成功及已核验退款结果与 `PaymentEvent` 同事务登记。`pending_payment_events(db, limit=20)` 可重放未确认事件;业务方幂等消费后,调用 `acknowledge_payment_event(db, event_id)`,与本地业务状态一起提交。消费失败不确认,下次再处理。该队列是单宿主消费组;并发读取可能重复,跨数据库消费需要本地去重,不能承诺恰好一次。
|
|
54
|
+
|
|
55
|
+
事件只包含资金字段和支付/退款标识,不包含商品或用户资料;业务方通过自己的映射找到订单。事件的版本用于处理乱序结果,不能让旧异常状态覆盖新成功。事件存储不设置自动删除,保留策略由宿主审计要求决定。
|
|
56
|
+
|
|
57
|
+
既有 `PaymentPaidHook` / `RefundResultHook` 是提交前同事务扩展点,可在宿主内维护同库业务或登记自己的持久任务;不得自行commit或执行外部网络副作用。兼容钩子失败会回滚本地资金投影和事件,等待渠道重试或查单恢复,不代表真实收款失败。需要独立提交资金事实的新接入使用空钩子并在提交后消费事件,无需实现统一商品处理器。
|
|
58
|
+
|
|
59
|
+
## 存储与迁移
|
|
60
|
+
|
|
61
|
+
使用SQLAlchemy异步会话,资金模型复用 `dwyeapi.database.Base`,无需 `dwy-shared`、`dwy-auth`、`app` 或 `quant_billing`。宿主通过Alembic显式安装/升级资金表,包内迁移入口与现有数据库升级说明随版本交付;不在导入或启动时自动改表。数据库连接取宿主配置,文档及日志不得写入凭据。
|
|
62
|
+
|
|
63
|
+
宿主在自己的Alembic revision中调用版本固定的辅助;Alembic只需安装在迁移环境,不是支付运行时强依赖:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from alembic import op
|
|
67
|
+
from dwy_payment.migrations import downgrade_payment_tables, upgrade_payment_tables
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def upgrade():
|
|
71
|
+
# 新库创建三张资金表;已有完整0.2表新增幂等键、固定付款截止时间和事件表。
|
|
72
|
+
upgrade_payment_tables(op)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def downgrade():
|
|
76
|
+
# 受控退回0.2,保留原支付/退款记录,但会删除事件、幂等键和截止时间证据。
|
|
77
|
+
downgrade_payment_tables(op)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
首次创建也可显式调用 `create_payment_tables(op)`;`validate_payment_tables(connection)`只检查,不修改数据库。三张表是 `payment_orders`、`payment_refunds`、`payment_events`;辅助不会对共享Base执行 `create_all`,也不会创建用户、商品、审核或权益表。已有表缺列、类型/空值不符、缺少关键主键、唯一性或索引时直接失败,不会通过 `checkfirst` 假装迁移成功。唯一约束的自动命名和默认表达式的等价数据库表示不作脆弱比较。
|
|
81
|
+
|
|
82
|
+
现有宿主应在自己的最新revision之后新增升级revision,保留历史revision及原表数据,不重建或清空资金表,也不回填旧付款事件。0.3.0迁移快照随版本固定,后续结构变化应增加新的迁移步骤。辅助需要真实连接进行schema核对,仅支持SQLite/PostgreSQL在线迁移,显式拒绝离线 `--sql`;数据库驱动由宿主选择并安装。
|
|
83
|
+
|
|
84
|
+
旧记录的 `idempotency_key` 与 `expires_at` 均保持NULL,不猜测原商户付款截止时间。新支付意图必须固定保存截止时间,重试沿用原值;不能根据当前 `timeout_seconds` 配置重新计算旧请求的渠道 `time_expire`。
|
|
85
|
+
|
|
86
|
+
服务构造函数支持显式配置与钩子;`runtime.configure`、`configure_refunds`只是单应用进程的便利装配,默认不读取宿主数据库。退款独立会话必须显式注入。同进程多应用应使用显式服务依赖,不重复覆盖全局装配;本版不提供完整多租户资金服务。
|
|
87
|
+
|
|
88
|
+
## 验收边界
|
|
89
|
+
|
|
90
|
+
包测试和独立wheel验证使用模拟渠道及隔离数据库,不执行真实收款或退款。Native验收不代表JSAPI、App、小程序等产品已实现。发布由 `.github/workflows/publish-payment.yml` 使用GitHub Actions OIDC完成,包标签为 `dwy-payment@0.3.0`。
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dwy-python-api
|
|
3
|
+
description: 开发、修改、测试或排查 Python FastAPI 接口与服务时使用。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Python API 开发
|
|
7
|
+
|
|
8
|
+
确认项目实际 FastAPI、Pydantic、数据库与依赖版本,复用已有服务和认证实现。按任务阅读 [后端接口与数据访问](references/dwy-python-backend.md);旧项目遵循其实际技术栈,不擅自更换 ORM 或依赖。涉及 dwyeapi 或其他自有包时使用对应 Skill,并核实项目实际版本。
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dwy-python-development
|
|
3
|
+
description: 开发、修改或排查 Python 应用、脚本或基础库时使用;覆盖团队写法、测试、异步问题和自有基础库发现。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Python 开发
|
|
7
|
+
|
|
8
|
+
先检查项目现有版本、依赖和架构,优先复用已安装库。项目未使用自有库时,仅提出可用能力,不自动添加依赖。涉及 FastAPI 使用可用的 dwy-python-api,涉及分层使用 dwy-python-architecture。
|
|
9
|
+
|
|
10
|
+
- [Python 规范](references/dwy-python-core.md):语言、依赖与配置。
|
|
11
|
+
- [测试](references/dwy-python-testing.md):编写和运行测试。
|
|
12
|
+
- [踩坑](references/dwy-python-pitfalls.md):排查环境、异步与运行时问题。
|
|
13
|
+
- [基础库](references/dwy-python-base-lib.md):定位 dwyeapi 既有能力;以项目实际依赖与版本为准。
|
|
14
|
+
|
|
15
|
+
自有库导航:基础设施查 `dwy-eapi`;认证查 `dwy-auth`;支付查 `dwy-payment`;通用通知查 `dwy-notify`;Webhook 查 `dwy-webhooks`;Apple、邮件和微信支付来源分别查 `dwy-apple-notify`、`dwy-email-notify`、`dwy-wechatpay-notify`;设备信息查 `dwy-device-fingerprint`。对应 Skill 未安装时直接核实项目实际依赖和源码,不自动安装。
|
|
16
|
+
|
|
17
|
+
参考资料中的强制选型只适用于已经采用该技术栈或用户明确选择的新项目;现有项目以其依赖、版本和用户要求为准。
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: 已使用 dwyeapi 的项目如何查找和复用基础能力
|
|
3
3
|
paths:
|
|
4
4
|
- "**/*.py"
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
# Python 后端 dwyeapi
|
|
7
|
+
# Python 后端 dwyeapi 基础能力
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
本资料适用于已采用 dwyeapi 的项目;版本及公开接口以实际安装包和 dwy-eapi Skill 为准。项目未采用时此处仅用于了解能力,不要求引入或替换现有实现。
|
|
10
10
|
|
|
11
11
|
`dwyeapi` 是团队内部 PyPI 基础设施包,已封装后端项目所需的通用能力。
|
|
12
12
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dwy-webhooks
|
|
3
|
+
description: 使用或排查 dwy-webhooks Webhook 集成能力时使用;先核实项目安装版本及公开 API。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# dwy-webhooks
|
|
7
|
+
|
|
8
|
+
Webhook 事件、接收安装和发送能力;按版本检查可选 extra、签名校验和宿主处理边界。
|
|
9
|
+
|
|
10
|
+
## 查找与使用
|
|
11
|
+
|
|
12
|
+
项目使用 0.6.2 时可阅读 [0.6.2 接入和示例](references/v0/0.6.2.md);其他版本先查该版本的资料和代码,不把此快照当作其他版本的合同。
|
|
13
|
+
|
|
14
|
+
1. 检查项目锁文件和当前 Python 环境;用 `importlib.metadata.version("dwy-webhooks")` 查询实际发行版本,依赖范围不等于实际安装版本。未安装时不得假设可用或自动安装。
|
|
15
|
+
2. 在实际环境中检查 `dwy_webhooks` 的公开接口、类型及项目已有调用;editable 安装以实际源码为准。说明、实现或安装版本不一致时以运行代码为准。
|
|
16
|
+
3. 本索引基于仓库 0.6.2,能力与签名可能随小版本变化。精确版本源码位于仓库 `backend/packages/webhooks`,文档为该目录的 `README.md`;只在确认其版本与消费项目一致时使用。缺少安装源码时查对应 `dwy-webhooks@版本` 标签,不能用默认分支冒充。
|
|
17
|
+
4. 在已有调用和该版本文档中核对配置、必需依赖、输入校验、失败处理及生命周期后接入。没有足够证据时说明缺口,不猜 API。
|
|
18
|
+
|
|
19
|
+
## 0.x 已核实差异
|
|
20
|
+
|
|
21
|
+
- 0.6.1 公开无数据库依赖的逐目标投递与退避函数;0.6.2 要求发送者名称由配置、URL 或模块预设提供。旧版本不可假定具备相同合同。
|
|
22
|
+
- Apple 来源能力从本包拆分到 dwy-apple-notify,具体边界按消费版本的代码和迁移说明核实。
|
|
23
|
+
|
|
24
|
+
不要因为发现此库就改动项目依赖;新增依赖遵循项目选型规则。关联 FastAPI 或其他自有包时同时参考可用的对应 Skill。
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
<!-- 来源:dwy-webhooks@0.6.2 的 backend/packages/webhooks/README.md;文中相对链接相对原仓库路径。 -->
|
|
2
|
+
|
|
3
|
+
# dwy-webhooks
|
|
4
|
+
|
|
5
|
+
## 模块展示预设
|
|
6
|
+
|
|
7
|
+
业务展示由各模块拥有:Apple 购买为「购买通知」、ASC 为「应用审核通知」、经营日报/周报为「经营报表」、Resend 为「邮件通知」,其它来源共用「系统通知」。模块预设不下沉到 dwy-notify。独立宿主队列可以通过 `dwy_webhooks.appearance.notification_appearance(source)` 获取同一预设,并传给 `deliver_notifications(..., appearance=...)`。
|
|
8
|
+
|
|
9
|
+
五张 PNG、SVG 源图和 Lucide 许可维护在 `backend/packages/webhooks/assets/`;默认公网地址固定到资产提交 `091ad4fea18ed3840fec83dc833f18b759256ca7`,例如 `https://raw.githubusercontent.com/danweiyuancircle/dwy-ai-experience/091ad4fea18ed3840fec83dc833f18b759256ca7/backend/packages/webhooks/assets/purchase.png`。Discord 自行读取公开图片,各应用不需要部署图片服务。
|
|
10
|
+
|
|
11
|
+
应用可在各来源的 `notifications.appearance` 设置 `username` 和 `avatar_url`,覆盖单个字段或整套预设;`avatar_url: null` 使用平台默认头像。现有目标 URL 中显式配置的展示也优先于模块默认值。直接转发和持久队列共用预设;首次发送前把预设写入现有 JSON outbox,失败重试保留原预设,无需数据库迁移。
|
|
12
|
+
|
|
13
|
+
## Apple 经营日报与周报
|
|
14
|
+
|
|
15
|
+
`dwy-webhooks[analytics]==0.6.2` 提供公开的 `dwy_webhooks.analytics`。默认北京时间 `Asia/Shanghai` 每天 08:30,固定统计发送日期前两天的数据(如 30 日取 28 日、31 日取 29 日),为 Apple UTC 报表预留生成时间;周一附加截至前两天的连续七天周报。未发布或不可用的指标按状态展示,不等待或自动补发旧日报。
|
|
16
|
+
|
|
17
|
+
调用项目注入 `AppleAnalyticsConfig`、会话及 `build_analytics_tables(metadata)` 的表定义,再使用 `install_analytics_monitor(app, config, sessions, tables)` 安装。数据库迁移和 engine 生命周期仍由项目拥有;示例见 `backend/packages/webhooks/examples/new_project/analytics.py`。
|
|
18
|
+
|
|
19
|
+
指标包含官方可取得的首次/重新下载、曝光与页面访问事件次数、USD 估算销售/收益/退款、订阅状态/新增/续费/流失、地区与来源。缺报及未取得的维度不会补零。跨行 Unique Counts 不能用来拼接官方下载转化率;MRR、留存、去重付费用户及付费 cohort 未经验证不计算。
|
|
20
|
+
|
|
21
|
+
`AppleAnalyticsClient.initialize(app_id)` 显式申请 ONGOING 报表,不在常驻采集时自动升级权限。初次生成需要等待;报告原始统计日独立于通知发送时区。API Bearer 不传给报表下载地址,网络读取与 gzip/TSV 解析均有预算。
|
|
22
|
+
|
|
23
|
+
运行时使用账号/App 隔离的持久租约,日期快照和 outbox 同事务提交,复用统一通知发送与逐目标重试。近七天修正替换旧快照;重启不补发所有历史日报;网络确认丢失仍存在重复送达窗口。可选 `feedback(app_id, date)` 回调提供文字评论计数,不依赖评论模块或 App Cloud。
|
|
24
|
+
|
|
25
|
+
`ReviewStore.summary(app_id, day)` 返回已采集完整 UTC 创建日的 `review_count`、`low_star_reviews`;无完整基线、未来或已清理日期返回空映射。该摘要不是商店全部星级,不包含正文或昵称。
|
|
26
|
+
|
|
27
|
+
基于 EAPI 的通用 Webhook 模块。接收 Resend、App Store Connect、Apple Server Notifications V2,并按项目配置通过 `dwy-notify` 转发到配置的通知目标或外部注入的业务接收函数;也支持主动采集 App Store 文字评论及星级。
|
|
28
|
+
|
|
29
|
+
## 安装与依赖
|
|
30
|
+
|
|
31
|
+
核心依赖固定兼容版本 `dwyeapi==0.12.1` 与 `dwy-notify==0.2.0`;平台 SDK 按需选择 `dwy-webhooks[resend,apple]`,评论与经营采集分别选择 `reviews`、`analytics`。旧 `discord` extra 保留为空,不安装 `discord-webhook` SDK。ASC Webhook 使用标准库 HMAC;只有 Apple Server Notifications 需要 Apple SDK。源码由 dwy-shared 维护,项目安装独立 wheel,不引用共享仓库的绝对源码路径。发布配置不表示已经发布。
|
|
32
|
+
|
|
33
|
+
## 统一通知配置(0.6.0)
|
|
34
|
+
|
|
35
|
+
`NotificationConfig(targets={目标名: SecretStr或字符串}, timeout_seconds=10)` 使用显式传入的 Apprise URL。Discord 形如 `discord://Mail@WEBHOOK_ID/WEBHOOK_TOKEN`,企业微信机器人形如 `wecombot://BOT_KEY`;其它已支持的 Apprise HTTPS 渠道尽量只改配置。URL 由宿主密钥配置注入,日志和队列不保存明文 URL。直接通知也能在普通 Python 程序使用:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from dwy_notify import NotificationConfig, Notifier
|
|
39
|
+
|
|
40
|
+
async def notify(targets: dict[str, str]) -> bool:
|
|
41
|
+
"""targets 由宿主配置注入;结果按目标汇总,失败不会被当成成功。"""
|
|
42
|
+
result = await Notifier(NotificationConfig(targets=targets)).send("任务已完成", title="任务通知")
|
|
43
|
+
return result.success
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`result.deliveries` 保留每个目标的结果、消息 ID、安全错误码和重试信息;`succeeded`、`failed` 是目标名元组。`send(..., targets=("mail",))` 可显式选择配置内目标。SDK 类型不出现在公开 API。
|
|
47
|
+
|
|
48
|
+
当前支持基于 `requests` 的 HTTPS 渠道;SMTP、本地命令、明文协议及带不可禁用内部重试的插件会在构造时拒绝,具体边界见 `backend/packages/notify/README.md`。普通渠道以 SDK 返回值判断成功,失败统一为安全的 `send_failed`;可能需要宿主补装渠道可选依赖。取消等待会传播,但已开始的同步请求仍在线程中由 socket 超时结束;分段或多接收人通知没有整条硬截止,整体重试可能重复已成功片段。
|
|
49
|
+
|
|
50
|
+
Webhook 全局 `notifications` 仅作默认值,Resend、ASC、Apple 来源各自的 `notifications` 可覆盖。评论的 `AppleReviewsConfig.notifications` 和日报的 `AppleAnalyticsConfig.notifications` 独立配置;例如评论只发 Discord,日报同时发 Discord 和企业微信。各安装器只读取自己的配置,不要求安装另一个功能或额外注入 Notifier;自定义 sink / sender 仍有最高优先级。完整装配示例见 `backend/packages/webhooks/examples/new_project/notifications.py`。
|
|
51
|
+
|
|
52
|
+
旧 `discord_webhook_url` / `discord_username` 和 `DiscordWebhookSender` 保留兼容,内部同样转调 `dwy-notify`,没有第二套发送 SDK。新配置用 URL 的用户名设置渠道名称;同一层不能同时配置新旧目标,模型会拒绝。来源配置可以覆盖全局默认。
|
|
53
|
+
|
|
54
|
+
持久队列将逐目标状态、次数、下次重试时间和目标指纹写进既有 payload JSON,无需 schema 迁移;已成功目标不会随失败目标重发。旧待发载荷首次处理时绑定当时目标,之后目标删除或 URL 改变不会把旧消息静默投给新接收方。部分永久失败仍保留其它目标的成功状态;网络成功但确认丢失仍可能重复送达。
|
|
55
|
+
|
|
56
|
+
## Apple 内购详情与通知
|
|
57
|
+
|
|
58
|
+
安装 `dwy-webhooks[apple]==0.6.1` 即可在其他 FastAPI 项目使用,不需要 App Cloud、ADBox、账户或商品权益表:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from fastapi import FastAPI
|
|
62
|
+
from dwy_notify import NotificationConfig
|
|
63
|
+
from dwy_webhooks import WebhookConfig, install_webhooks
|
|
64
|
+
from dwy_webhooks.config import AppleNotificationApp, AppleNotificationsConfig
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def create_app(apple_id: int, certificates: tuple[bytes, ...], discord_url: str) -> FastAPI:
|
|
68
|
+
"""discord_url 是宿主提供的 Apprise URL;证书传字节。"""
|
|
69
|
+
app = FastAPI()
|
|
70
|
+
install_webhooks(app, WebhookConfig(
|
|
71
|
+
notifications=NotificationConfig(targets={"purchases": discord_url}),
|
|
72
|
+
apple_notifications=AppleNotificationsConfig(apps={
|
|
73
|
+
"my-app": AppleNotificationApp(
|
|
74
|
+
name="My App", bundle_id="com.example.myapp", environment="Production",
|
|
75
|
+
app_apple_id=apple_id, root_certificates=certificates,
|
|
76
|
+
),
|
|
77
|
+
}),
|
|
78
|
+
))
|
|
79
|
+
return app
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Apple V2 回调路径为 `/webhooks/apple/my-app`。旧 Discord 配置默认发送者为「Apple 应用通知」,`AppleNotificationsConfig.discord_username` 可覆写;新通知 URL 用用户名配置名称。应用名称由 `AppleNotificationApp.name` 注入。默认注册器的去重及可注入持久 sink 语义沿用原接口。
|
|
83
|
+
|
|
84
|
+
通知展示交易金额/币种、购买店面、时间、本人购买/家庭共享、退款申请的用户选择分类、退款结果的 Apple 分类及适用续订设置。金额按整数毫单位精确换算,退款仅展示原交易价格;缺失显示未提供,未知分类保留代码,不推断用户自由文本、开发者收入或购买设备平台。
|
|
85
|
+
|
|
86
|
+
已有业务队列可使用公开 `dwy_webhooks.apple`:`AppleNotificationVerifier`、`VerifiedAppleNotification`、`AppleTransactionDetails`、`AppleRenewalDetails` 和 `apple_notification_content(notice, app_name="My App")`;格式化后的字符串直接交既有 `DiscordWebhookSender`,其 `username` 可覆写。`apple_transaction_details`、`apple_renewal_details` 和 `apple_refund_request_reason` 只消费已经完成官方验签的 SDK 对象,本身不代替验签。
|
|
87
|
+
|
|
88
|
+
`VerifiedAppleNotification` 保留原八字段构造及 `public_data()` 白名单,新增可选 `transaction_details`、`renewal_details`、`consumption_request_reason`。详情模型不包含账户 token、原始 JWS、签名摘要或权益策略。商品白名单、账户绑定、持久化及申请退款不提前撤销权益等业务规则仍由宿主管理。
|
|
89
|
+
|
|
90
|
+
PyPI 从此版本起只上传 `py3-none-any.whl`;通用 wheel 含可读 Python 模块,不提供源码隐藏。源码包只参与内部归档检查,不上传 PyPI。
|
|
91
|
+
|
|
92
|
+
## 配置与接入
|
|
93
|
+
|
|
94
|
+
### ASC 审核通知的平台与版本
|
|
95
|
+
|
|
96
|
+
审核事件本身只包含状态变化和 `appStoreVersions` 资源 ID;载荷里的 `version: 1` 不是应用版本号。安装 `dwy-webhooks[asc]` 并显式配置 `AppStoreConnectWebhookConfig.api` 后,适配器会按事件中的具体版本 ID 查询 `platform` 与 `versionString`,校验所属 App,再将平台、版本号补入通知。查询结果的最新审核状态不会覆盖事件原始状态。
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from pydantic import SecretStr
|
|
100
|
+
|
|
101
|
+
from dwy_webhooks.config import (
|
|
102
|
+
AppStoreConnectAPIConfig,
|
|
103
|
+
AppStoreConnectApp,
|
|
104
|
+
AppStoreConnectWebhookConfig,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def asc_config(
|
|
109
|
+
*, apps: dict[str, AppStoreConnectApp], issuer_id: str, key_id: str, private_key: SecretStr,
|
|
110
|
+
) -> AppStoreConnectWebhookConfig:
|
|
111
|
+
"""参数由宿主 Settings 提供;API 账号凭据与各 App 的 HMAC 密钥分别注入。"""
|
|
112
|
+
return AppStoreConnectWebhookConfig(
|
|
113
|
+
apps=apps,
|
|
114
|
+
api=AppStoreConnectAPIConfig(issuer_id=issuer_id, key_id=key_id, private_key=private_key),
|
|
115
|
+
)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
宿主可以显式复用已有评论或经营报表的 ASC 账号凭据;无需启用这些功能,也不由库读取其他模块或环境。所用密钥需要有权读取该 App 的版本信息,Webhook 签名密钥不能代替 ASC API 账号凭据。
|
|
119
|
+
|
|
120
|
+
查询使用固定地址 `https://api.appstoreconnect.apple.com/v1/appStoreVersions/{id}`,不跟随重定向,限制超时和响应体积。查询失败返回 502,留给 Apple 重投,不确认或发送缺少详情的完整事件;未配置 API 的旧接入仍接收通知,但平台、版本明确标记“未查询(未配置 ASC API)”。构建上传事件和官方 ping 不触发版本查询。无需数据库迁移。
|
|
121
|
+
|
|
122
|
+
库不读取环境变量、`.env`、项目设置或证书文件。`WebhookConfig` 是纯 Pydantic 模型;项目可组合进自己的 EAPI Settings。密钥由项目取得后传入,Apple 根证书传入字节。
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from dwy_notify import NotificationConfig
|
|
126
|
+
from dwy_webhooks import WebhookConfig, install_webhooks
|
|
127
|
+
from dwy_webhooks.config import AppleNotificationsConfig, ResendWebhookConfig
|
|
128
|
+
|
|
129
|
+
# project_settings 由宿主取得密钥;URL 为 Apprise 格式,不写进源码。
|
|
130
|
+
config = WebhookConfig(
|
|
131
|
+
notifications=NotificationConfig(targets=project_settings.default_notification_targets),
|
|
132
|
+
resend=ResendWebhookConfig(
|
|
133
|
+
secret=project_settings.resend_webhook_secret,
|
|
134
|
+
name="My Project", events=frozenset({"email.received", "email.failed"}),
|
|
135
|
+
notifications=NotificationConfig(targets={
|
|
136
|
+
"mail": project_settings.discord_notification_url,
|
|
137
|
+
}),
|
|
138
|
+
),
|
|
139
|
+
apple_notifications=AppleNotificationsConfig(
|
|
140
|
+
apps=project_settings.apple_notification_apps,
|
|
141
|
+
notifications=NotificationConfig(targets={
|
|
142
|
+
"purchases": project_settings.wecom_notification_url,
|
|
143
|
+
}),
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
runtime = install_webhooks(app, config)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Resend 使用 Webhook 签名密钥,不使用发送邮件的 API Key。未配置来源不注册路由;各来源可独立覆盖通知目标、事件集合和接收路径。
|
|
150
|
+
|
|
151
|
+
- `AppStoreConnectWebhookConfig(apps={app_id: AppStoreConnectApp(name=..., secret=...)})` 配置审核、构建及 ping 通知。
|
|
152
|
+
- `AppleNotificationsConfig(apps={key: AppleNotificationApp(name=..., bundle_id=..., environment="Production", app_apple_id=..., root_certificates=(certificate_bytes, ...))})` 配置 Server Notifications V2。
|
|
153
|
+
- 将前两者放入 `WebhookConfig.app_store_connect`、`.apple_notifications` 后,一次注册即可接入。
|
|
154
|
+
- 默认路径:`/webhooks/resend`、`/webhooks/app-store-connect/{app_id}`、`/webhooks/apple/{app_id}`;完整公开 URL 由项目域名及反向代理决定。
|
|
155
|
+
|
|
156
|
+
## 资源与接收语义
|
|
157
|
+
|
|
158
|
+
- 默认 relay 在全部目标发送成功后确认,实例内保留有界去重记录,部分失败仅重试未成功目标。失败返回非 2xx 供提供方重试;重启可能再次发送,不承诺持久或恰好一次投递。
|
|
159
|
+
- 项目可注入 `ReceiptStore`,或通过 `WebhookSinks` 按来源注入接收函数。函数接收已验证的 `WebhookEvent`,返回 True 表示接受、False 表示重复;异常不能被确认成功。
|
|
160
|
+
- `SQLAlchemyDeliveryQueue` 提供持久队列,sessions、table、sender、source 和可选策略由外部注入。使用 EAPI 创建的会话,不创建、迁移或关闭项目数据库。一个表固定一个来源,防止跨来源编号碰撞。
|
|
161
|
+
- 已有其它队列表结构可直接使用 `dwy_webhooks.notification_delivery.deliver_notifications(notifier, payload, *, policy, clock, checkpoint)`,复用同一逐目标算法。`payload` 是当前记录独立解码的可变字典,含 `content` 和可选的 `notifications` 状态;其它宿主字段保留。`checkpoint(**values)` 必须按当前租约原子持久化 `payload` 或续租 `lease_until`,成功返回 True;False 会停止并返回 `{"status": "processing"}`,调用方不得把它标记成功或清除新租约。正常返回的聚合状态仍须由调用方按租约提交;取消及检查点异常直接传播。`delivery_failure(attempts, failure, *, policy, clock)` 复用相同的有界退避与安全错误码,不执行持久化。
|
|
162
|
+
- `build_delivery_table` 注册到项目 metadata,由项目自己的 Alembic 管理 schema。库不在注册路由时自动建表。
|
|
163
|
+
- `install_webhooks` 保留宿主生命周期,只关闭自己拥有的验证器;`create_app(config)` 可建立独立 ASGI 应用,配置仍然必填。普通错误复用 EAPI,第三方成功 ACK 保持提供方协议。
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
from dwy_webhooks import WebhookSinks, install_webhooks
|
|
167
|
+
|
|
168
|
+
# persist_verified_event 是项目自己的持久入口,或已注入资源的队列实例。
|
|
169
|
+
install_webhooks(app, config, sinks=WebhookSinks(app_store_connect=persist_verified_event))
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Apple 商品目录、账户绑定和权益发放保留在业务项目。通用模块只验证通知并提供安全事件元数据,不把测试通知或续订设置变更报告为收入。
|
|
173
|
+
|
|
174
|
+
## 安全和扩展
|
|
175
|
+
|
|
176
|
+
来源分别验签,并在读取原始字节时限制体积。Apple 外层、交易及续订 JWS 分别走官方 SDK,生产强制 App Apple ID。Discord 固定官方 HTTPS 地址,拒绝重定向和 mentions,等待有效消息编号;错误不记录 token 或原始响应正文。
|
|
177
|
+
|
|
178
|
+
新增来源增加 provider;通知渠道通过 `dwy-notify` 的 Apprise URL 选择,SDK 异常与安全修正集中在通知包。注册入口不覆盖项目已有的全局异常或缓存配置。
|
|
179
|
+
|
|
180
|
+
`examples/new_project/app.py` 是不依赖 App Cloud 的接入示例。隔离 wheel 验收要求 `app_cloud`、`apple_iap`、`cloud_kernel`、旧 `cloud_webhooks` 和产品包缺席,公共配置、注册和三类来源仍能通过测试。
|
|
181
|
+
|
|
182
|
+
## App Store 评论定时通知
|
|
183
|
+
|
|
184
|
+
`dwy_webhooks.reviews` 提供主动采集能力,独立于审核/构建 Webhook 和内购通知。评论通知安装 `dwy-webhooks[reviews]`:`reviews` 直接声明 HTTPX、PyJWT 和 cryptography,使用账号 API 密钥签发 ES256 JWT 并访问 ASC REST API;不需要 Apple Server SDK 或 Resend SDK,也不依赖其它包偶然传入 PyJWT。无需 Redis 服务、ARQ Worker 或新增调度框架。
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
uv add 'dwy-webhooks[reviews]==0.6.1'
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
from sqlalchemy import MetaData
|
|
192
|
+
from dwy_notify import NotificationConfig
|
|
193
|
+
from dwy_webhooks.reviews import AppleReviewsConfig, build_review_tables, install_review_monitor
|
|
194
|
+
|
|
195
|
+
# project_settings、EAPI 创建的 sessions 及 app 都由消费项目提供。
|
|
196
|
+
config = AppleReviewsConfig(
|
|
197
|
+
apps=project_settings.review_apps,
|
|
198
|
+
issuer_id=project_settings.apple_issuer_id,
|
|
199
|
+
key_id=project_settings.apple_key_id,
|
|
200
|
+
private_key=project_settings.apple_private_key,
|
|
201
|
+
notifications=NotificationConfig(targets=project_settings.review_notification_targets),
|
|
202
|
+
poll_interval_seconds=3600,
|
|
203
|
+
retention_days=30,
|
|
204
|
+
)
|
|
205
|
+
metadata = MetaData()
|
|
206
|
+
tables = build_review_tables(metadata)
|
|
207
|
+
# 先通过消费项目的 Alembic 创建上述三张表,再启动任务;库不自动建表。
|
|
208
|
+
runtime = install_review_monitor(app, config, sessions, tables)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
- 默认每小时采集一次,可配置 `poll_interval_seconds`(60–604800 秒);FastAPI lifespan 管理 asyncio 后台任务。每分钟只检查数据库计划和执行清理,不代表每分钟请求 Apple。
|
|
212
|
+
- 每个应用持久化下次执行时间和租约;多消费者不会同时提交同一轮。重启保留计划,失败一分钟后尝试恢复;完整分页失败不会推进基线。
|
|
213
|
+
- 每轮完整读取全部评论历史,最多 100 页、每页 200 条,每页响应不超过 2 MiB;整轮同步最长 120 秒。超过这些上限会失败并保留原有基线,不能把它视为只拉取最近 N 天;`retention_days` 控制数据库保留范围。
|
|
214
|
+
- 首次只建立保留期内历史基线并发送一条摘要;随后新评论或改文/改星产生通知。通知截断到 Discord 限长,完整原文存于保留期内的数据库记录。
|
|
215
|
+
- `retention_days` 默认 30,允许 1–3650(包括 60/90)。按 Apple 评论创建时间保留最近 N 天;评论原文、指纹及该评论的投递载荷都会清理。基线摘要按生成时间清理,最小的应用调度/基线状态长期保留,其中没有评论正文或昵称。
|
|
216
|
+
- 扩大保留期时静默补入较老历史,避免当作新评论刷屏;既有保留窗口内的新评论、修改仍通知。已超出窗口的旧评论修改不追踪。
|
|
217
|
+
- 清理独立于 Apple 拉取成功与否;投递前也检查过期状态。持久队列提供有界重试,不承诺网络超时场景下恰好一次送达。
|
|
218
|
+
- 表、会话、时钟、HTTP 客户端和 sender 可显式注入;库不读取 `.env`、打开项目数据库或自动执行迁移。关闭任务后才允许宿主释放数据库。
|
|
219
|
+
- 本功能读取文字评论及其附带星级,不提供纯星级逐条事件、总评分统计或 Discord 内回复。
|
|
220
|
+
|
|
221
|
+
本轮清单版本为 `0.6.1`,不代表已发布到公开 PyPI。消费项目应在发布完成后固定版本并验收实际安装产物。
|
|
222
|
+
|
|
223
|
+
## Discord 发送者名称
|
|
224
|
+
|
|
225
|
+
旧配置的发送者默认使用「Apple 应用通知」,通过 Discord 原生 `username` 字段逐条指定,不修改平台侧 Webhook 配置。调用项目可按自己的场景覆写:
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
sender = DiscordWebhookSender(webhook_url, username="团队评分通知")
|
|
229
|
+
# 评分安装器自动使用此名称;传入自定义 sender 时由该 sender 决定名称。
|
|
230
|
+
config = AppleReviewsConfig(
|
|
231
|
+
apps=apps, issuer_id=issuer_id, key_id=key_id, private_key=private_key,
|
|
232
|
+
discord_webhook_url=webhook_url, discord_username="团队评分通知",
|
|
233
|
+
)
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
`WebhookConfig.discord_username` 设置通用来源的名称;`ResendWebhookConfig`、`AppStoreConnectWebhookConfig` 和 `AppleNotificationsConfig` 的同名字段可逐来源覆写,省略时继承全局值。邮件等非 Apple 场景应显式设置合适名称。名称必须为 1–80 字符,不能全为空白或包含控制字符。
|
|
237
|
+
|
|
238
|
+
评分正文以加粗标题区分监控启用、新增和更新,独立展示应用名称与五颗可视星级。调用项目继续管理凭据、数据库连接和 Alembic 迁移,复用时无需复制评分源码。
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dwy-wechatpay-notify
|
|
3
|
+
description: 使用或排查 dwy-wechatpay-notify 微信支付通知能力时使用;先核实项目安装版本及公开 API。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# dwy-wechatpay-notify
|
|
7
|
+
|
|
8
|
+
微信支付与退款通知验证;可选 FastAPI 路由需要显式 verifier 和持久 sink。
|
|
9
|
+
|
|
10
|
+
## 查找与使用
|
|
11
|
+
|
|
12
|
+
项目使用 0.1.1 时可阅读 [0.1.1 接入和示例](references/v0/0.1.1.md);其他版本先查该版本的资料和代码,不把此快照当作其他版本的合同。
|
|
13
|
+
|
|
14
|
+
1. 检查项目锁文件和当前 Python 环境;用 `importlib.metadata.version("dwy-wechatpay-notify")` 查询实际发行版本,依赖范围不等于实际安装版本。未安装时不得假设可用或自动安装。
|
|
15
|
+
2. 在实际环境中检查 `dwy_wechatpay_notify` 的公开接口、类型及项目已有调用;editable 安装以实际源码为准。说明、实现或安装版本不一致时以运行代码为准。
|
|
16
|
+
3. 本索引基于仓库 0.1.1,能力与签名可能随小版本变化。精确版本源码位于仓库 `backend/packages/wechatpay-notify`,文档为该目录的 `README.md`;只在确认其版本与消费项目一致时使用。缺少安装源码时查对应 `dwy-wechatpay-notify@版本` 标签,不能用默认分支冒充。
|
|
17
|
+
4. 在已有调用和该版本文档中核对配置、必需依赖、输入校验、失败处理及生命周期后接入。没有足够证据时说明缺口,不猜 API。
|
|
18
|
+
|
|
19
|
+
不要因为发现此库就改动项目依赖;新增依赖遵循项目选型规则。关联 FastAPI 或其他自有包时同时参考可用的对应 Skill。
|