阶段 6 注:当时的 intake 路径
ima-skills-1.1.7 (1)已按原始 bytes 归档为third_party/ima-skills/1.1.7/original;下文旧路径只表示历史基线。当前唯一规范契约为docs/IMA_OPENAPI_CONTRACT_1_1_7.md。
编制日期:2026-07-12
对应总计划:OPTIMIZATION_PLAN.md 的阶段 3 中用户 URL 检测与下载再上传、阶段 4、阶段 5
实施基线:批次 A/B 已实施,当前uv run python -m unittest discover -s tests -v为 103/103 通过
计划状态:已实施(2026-07-12)
生产依赖策略:默认不新增生产依赖,继续使用 Python 标准库;若压缩音频时长检测确需第三方库,必须另行确认
批次 C 解决三组已经互相耦合的问题:
- 用户传给
ima kb add-url的地址目前只能做静态后缀判断,不能可靠地区分页、微信公众号文章和可下载文件。 - 当前上传流程会一次性把文件读入内存,缺少文件状态复核、凭证时效、COS key 规范化、重名改名和阶段性失败结果。
- CLI 注册、校验、业务编排、人类输出和 JSON 序列化混在大型模块中,阻碍 URL 与批量上传流程安全落地。
完成后,以下工作流应成为同一套可测试服务:
用户 URL
-> 安全探测
-> 网页 / 微信文章 ----------------> import_urls
-> 文件型 URL -> 有界临时下载 -----> UploadService
本地文件 --------------------------------> UploadService
UploadService
-> 文件预检
-> 批量重名检查
-> 冲突策略
-> create_media
-> 流式 COS PUT
-> 文件状态复核
-> add_knowledge
CLI 则只负责:
- 注册命令和参数;
- 将参数转换为类型明确的请求;
- 调用服务;
- 把结构化结果交给统一输出层。
本批次不会通过重试、静默替换或猜测类型来掩盖不确定性。每个已经发生远程副作用的阶段都必须在结果中可辨识,同时不得泄露 URL 查询参数、IMA 凭证、COS 临时凭证、签名或本地临时路径。
- 对用户输入 URL 执行语法、协议、目标主机和重定向安全校验;
- 支持 HEAD 探测,并在服务端不支持或信息不足时使用无正文消费的受限 GET 探测;
- 结合最终 URL、Content-Type、Content-Disposition 和已知 URL 模式判定网页或文件;
- 明确识别微信公众号文章;
- 明确拒绝 file URL、Bilibili 视频、YouTube 视频和不支持的内容类型;
- 为文件型 URL 推断并清洗安全文件名;
- 将文件型 URL 流式下载到随机临时目录;
- 在响应头和实际读取两个层面限制下载大小;
- 无论成功、失败或中断都清理临时目录;
- 把下载后的文件交给与本地文件完全相同的上传服务;
- 抽取独立 UploadService;
- 将 COS PUT 改为固定 Content-Length 的低内存流式传输;
- 上传前、打开文件后、上传完成后复核文件身份、大小和修改状态;
- 校验 COS 凭证字段、开始时间、过期时间、官方域名和对象 key;
- 将上传超时默认值调整为 300 秒并允许命令行配置;
- 实现
--on-conflict error|rename,默认 error; - 对 rename 结果再次执行服务端重名检查;
- 保证 add_knowledge.title、file_info.file_name 和 create_media.file_name 使用同一个最终文件名;
- COS 失败或文件发生变化后绝不调用 add_knowledge;
- add_knowledge 失败时返回可诊断但不含密钥的孤立 media 状态;
- 让 add-url 的网页导入结果、文件上传结果和本地分类失败统一为输入顺序稳定的逐项结果;
- 对部分成功返回稳定 JSON 和专用非零退出码;
- 允许 add-file 重复传入
--file,并在真正上传前一次性完成批量重名检查; - 将命令处理器改为返回 CommandResult,不再捕获 stdout 后反向解析 JSON;
- 分离命令注册、业务服务、共享校验、分页收集和输出;
- 让所有游标或 offset 分页命令支持统一的
--all与最大页数保护; - 保留现有命令名称、单页默认行为、JSON 顶层业务字段和 ima-note 兼容入口;
- 用明确类型替换已知的 object、getattr 和无约束 dict 编排;
- 更新 README、活动 CLI skill、契约文档和离线测试。
- 删除、合并或重新发布现有 skills 目录;
- 将上游 ima-skills-1.1.7 (1) 目录改造成正式包内容;
- CI、lint、类型检查、coverage、wheel 安装和发布流水线;
- 新增任意 IMA Base URL、代理 API 或非官方 IMA 服务端;
- 把 IMA 长期凭证、COS 临时凭证或用户 Cookie 转发到用户 URL;
- 自动登录、处理需要浏览器会话的下载页或绕过反爬机制;
- 支持 FTP、SFTP、data URL、file URL 或自定义协议;
- 支持 Bilibili、YouTube、本地 HTML 和视频上传;
- 静默替换知识库中已有同名文件;
- 删除已经创建但未成功注册的远端 media;1.1.7 没有提供对应删除接口;
- 断点续传、多分片 COS 上传或并行上传;
- 对 COS PUT 或 add_knowledge 自动重试;
- 自动猜测未知二进制文件的真实格式;
- 为 MP3、M4A、AAC 引入第三方音频解析依赖;
- 通用的知识库名、文件夹名和笔记标题自动解析;
- 真实账户写入 smoke test作为自动验收的一部分;
- 批次 D 的 canonical skill、许可证、版本、打包和发布工作。
批次 B 已完成:
- get_media_info;
- 笔记型媒体跨模块读取;
- IMA 返回的受控媒体 URL 读取和导出;
- 媒体 URL allowlist 与临时 header 的重定向保护;
- 统一错误、退出码、JSON envelope、配置和严格协议解析。
批次 C 新增的是“用户主动提供的任意公网 HTTP(S) URL”。它与批次 B 的媒体 URL 客户端不能共用信任模型:
- 批次 B 的 SourceHttpClient 只访问 ima.qq.com 或 myqcloud.com,并可能携带 IMA 返回的临时 headers;
- 批次 C 的 RemoteHttpClient 访问经过公网地址校验的用户 URL,永远不携带 IMA 或 COS 凭证;
- 两者可以共享纯函数级的 header、MIME 和大小工具,但不能共享 opener、redirect handler 或默认 headers。
当前 knowledge_cli.validate_urls 会检查 URL path 的扩展名,并提示用户自行下载后使用 add-file。
问题:
- 无法识别没有扩展名但 Content-Type 为 application/pdf 的下载;
- 无法识别 Content-Disposition 给出的真实文件名;
- 无法处理 arxiv、GitHub raw 和重定向后的文件地址;
- 会把带 .pdf 路径但实际返回 HTML 的地址误判;
- 与 1.1.7 明确要求的自动分流不一致;
- URL 解析和业务路由仍在 CLI 模块中。
直接对任意用户 URL 发起请求会引入 SSRF 和凭证泄露风险:
- localhost、回环、链路本地、私网和保留地址可能被访问;
- 域名可以先解析到公网、连接时再次解析到内网;
- 重定向可能从公网跳到内网;
- URL 可以包含 userinfo、控制字符或非默认端口;
- 系统代理可能改变实际连接目标;
- 异常消息可能回显带 token 的完整查询字符串。
批次 B 的媒体 allowlist 不能直接用于这里,因为用户 URL 本来就不局限于 IMA/COS 域名。
需要明确:
- HEAD 失败时何时回退 GET;
- Content-Type、Content-Disposition、最终 URL path 冲突时谁优先;
- application/octet-stream 如何处理;
- 微信、Bilibili 和 YouTube 在重定向前后如何识别;
- 未知 MIME 是拒绝、网页导入还是文件下载;
- 服务端缺少 Content-Type 时能否仅凭扩展名继续。
若没有固定决策表,同一个 URL 会在不同代码路径得到不同处理。
项目当前没有用户 URL 下载服务,因此也没有:
- Content-Length 预检;
- 读取过程中的硬上限;
- Content-Length 一致性校验;
- 随机临时目录;
- 安全文件名清洗;
- 中断和异常清理;
- 下载后的类型二次预检;
- 不泄露临时路径的错误模型。
ImportUrlResult.ret_code 已被解析,但 handle_add_url 无论结果如何都返回 0。
这会导致:
- 自动化无法判断是否所有 URL 都成功;
- 人类输出和 JSON 对同一结果缺少统一状态;
- 混合网页与文件型 URL 后无法表达逐项阶段;
- 服务端部分失败、下载失败和上传失败无法放在同一结果模型中。
knowledge_upload.upload_to_cos 当前调用 Path.read_bytes。
后果:
- 200 MiB 文件会额外占用约 200 MiB 内存;
- 文件越大,内存峰值和复制成本越高;
- 无法在传输期间检测短读或文件变化;
- 与总计划“200 MB 文件不一次性载入内存”的验收标准冲突。
当前 inspect_upload_file 已检查扩展名、MIME 和大小,但仍未完整覆盖:
- 空文件;
- 文件名最长 1024 字符;
- 控制字符、路径分隔符和危险文件名;
- stat 与真正打开文件之间发生替换;
- 上传过程中大小或 mtime 改变;
- WAV 音频超过 2 小时;
- 多文件时的输入内重名。
当前已有:
- bucket_name 与 region 的 DNS label 校验;
- 固定 myqcloud.com HTTPS origin;
- secret 字段 repr 隐藏。
仍缺少:
- start_time 与 expired_time 的相对关系;
- 凭证是否尚未生效或已经过期;
- 上传前剩余有效期是否过短;
- cos_key 为空、绝对 URL、反斜杠、控制字符、查询串或路径穿越;
- 用于签名的 pathname 与实际请求 path 是否完全一致;
- custom_domain 明确忽略的回归测试。
当前流程在 is_repeated=true 时立即抛 InputError。
缺少:
- 用户显式选择保留两者的命令参数;
- 时间戳文件名生成;
- 多个同名输入的稳定后缀;
- 改名后再次询问服务端;
- 最终文件名贯穿 create_media、COS、file_info 和 title 的不变量;
- 批量上传在任何 create_media 前完成一次重名检查。
可能发生:
- create_media 成功、COS 失败;
- COS 成功、文件被修改,因此不能 add_knowledge;
- COS 成功、add_knowledge 失败,留下孤立 media;
- 多项任务中前几项成功,后续项失败;
- URL 下载成功,但本地预检失败。
当前异常只描述最终错误,无法稳定表达 stage、media_id、是否可能存在孤立对象和未执行项。
cli._run_handler 会:
- 捕获 handler 的 stdout/stderr;
- 要求 handler 在 JSON 模式打印内部 JSON;
- 再 json.loads;
- 最后包一层公共 envelope。
问题:
- 业务处理器必须知道 as_json;
- 人类输出和机器输出重复实现;
- 任意调试打印都会变成 internal_error;
- handler 的返回值只剩 int,无法表达 partial;
- 测试不得不围绕 stdout,而不是业务结果;
- 服务层很难独立复用。
当前:
- knowledge_cli 约 500 行;
- notes_cli 约 400 行;
- cli 同时处理 parser、凭证、依赖构造、输出捕获和 auth 展示;
- add_kb_subcommands 与 add_note_subcommands 使用 argparse 私有类型注解;
- path_node_to_dict、import_url_result_to_dict 使用 object/getattr;
- 多个 API 方法返回 dict[str, Any],服务编排依赖字符串键。
limit、URL、文件、ID 和 cursor 校验分别出现在:
- CLI handler;
- API client;
- knowledge_upload;
- security。
CLI 需要尽早给出友好错误,API client 又必须保护公共 Python 调用;但两层目前没有共享纯函数,容易出现上限或错误文案漂移。
当前分页命令只取一页,缺少:
- 统一 --all;
- 最大页数;
- cursor 循环检测;
- is_end=false 但 next_cursor 为空的协议保护;
- offset 搜索无进展保护;
- 多页结果去重和稳定顺序;
- 达到保护上限时的 partial 结果。
批次 B 已固定公共字段,但没有:
- status;
- partial 的结果与 error 共存规则;
- 批量计数;
- 单项 stage;
- 分页元数据;
- schema 的兼容承诺文档。
必须保持:
- ima、ima-note 两个入口;
- 现有命令和参数名称;
- add-file 单个 --file 的调用方式;
- 默认只取一页;
- JSON schema_version=1;
- 已存在的顶层业务字段;
- --doc-id 的弃用兼容;
- api.py 中现有公共导出。
允许的兼容扩展:
- --file 可以重复;
- add-url 和 add-file 新增 --on-conflict;
- 新增 --download-timeout、--upload-timeout;
- 分页命令新增 --all、--max-pages;
- JSON 新增 status、summary、pagination 和逐项 stage;
- 查询字符串敏感 URL 的展示值改为安全形式。
禁止为了目录美观一次性移动所有实现。旧模块在过渡期保留薄兼容 façade,等调用点和测试全部迁移后再决定是否删除。
计划采用以下依赖方向:
flowchart LR
CLI["cli.py:入口与异常边界"] --> CMD["commands/*:参数注册与展示"]
CMD --> RESULT["command_result.py:结构化命令结果"]
CMD --> PAGE["pagination.py:有界分页"]
CMD --> URL["url_ingest.py:URL 分流服务"]
CMD --> UPLOAD["upload_service.py:上传状态机"]
URL --> REMOTE["remote_http.py:无凭证公网 HTTP"]
URL --> UPLOAD
UPLOAD --> COS["cos_http.py:COS 流式 PUT"]
UPLOAD --> KBAPI["knowledge_api.py:IMA API"]
CMD --> NOTEAPI["notes_api.py:IMA API"]
RESULT --> OUTPUT["output.py:唯一渲染出口"]
依赖规则:
- API client 不创建 service;
- service 不打印、不读取 argparse.Namespace;
- command 不直接构造 COS 请求;
- output 不调用 API;
- remote_http 不导入 Credentials、ImaHttpClient 或 CosCredential;
- cos_http 只接受已经验证的上传目标和只读文件流;
- security 与 validation 提供无副作用纯函数;
- 测试可为 clock、DNS resolver、connection factory、temp directory 和 API client 注入替身。
保留并扩展:
ima kb add-url
--kb-id KB_ID
--url URL [--url URL ...]
[--folder-id FOLDER_ID]
[--on-conflict error|rename]
[--download-timeout 300]
[--upload-timeout 300]
[--json]
ima kb add-file
--kb-id KB_ID
--file PATH [--file PATH ...]
[--folder-id FOLDER_ID]
[--content-type MIME]
[--on-conflict error|rename]
[--upload-timeout 300]
[--json]
约束:
- add-url 仍限制 1 至 10 个 URL;
- add-file 限制 1 至 2000 个文件,与 check_repeated_names 上限一致;
- 多文件 add-file 不允许使用单个全局 --content-type;每个文件必须可由扩展名识别;
- 单文件时继续支持 --content-type;
- timeout 必须为 1 至 3600 秒的整数;
- --on-conflict 默认 error;
- rename 只适用于文件分支,网页 URL 不执行重名检查;
- 不提供 overwrite 或 replace。
分页命令扩展:
ima note search ... [--all] [--max-pages 100]
ima note folders ... [--all] [--max-pages 100]
ima note list ... [--all] [--max-pages 100]
ima kb search-base ... [--all] [--max-pages 100]
ima kb browse ... [--all] [--max-pages 100]
ima kb search ... [--all] [--max-pages 100]
ima kb addable ... [--all] [--max-pages 100]
新增 CommandResult,至少包含:
- payload:JSON 业务字段;
- human_lines:人类模式 stdout 行;
- warnings:人类模式 stderr、JSON warnings;
- status:success、empty、partial、failed;
- exit_code;
- error:仅 partial/failed 可选的安全聚合错误。
状态规则:
| status | ok | 退出码 | 含义 |
|---|---|---|---|
| success | true | 0 | 请求完全完成且至少一个结果成功 |
| empty | true | 0 | 请求成功但结果为空 |
| partial | false | 9 | 已产生可用结果,但未完成全部请求 |
| failed | false | 9 | 批量逐项结果已产生,但没有任何项成功 |
| error | false | 2 至 8、70、130 | 在形成可用逐项结果前发生分类异常 |
新增 ExitCode.PARTIAL = 9。
退出码 9 只用于“结果本身可用,但整体目标没有完全实现”。配置、协议、IMA envelope、单项命令和无法形成逐项结果的错误继续使用批次 B 的退出码。
JSON 模式规则:
- stdout 只有一个 JSON 文档;
- stderr 为空;
- partial/failed 同时包含 results 和 error;
- status 为新增的稳定字段;
- 既有业务字段保持顶层位置;
- warnings 始终是字符串数组;
- error 继续使用批次 B 的安全结构。
partial 示例:
{
"schema_version": 1,
"ok": false,
"status": "partial",
"command": "kb.add-url",
"warnings": [],
"knowledge_base_id": "kb_xxx",
"summary": {
"total": 2,
"succeeded": 1,
"failed": 1,
"not_attempted": 0
},
"results": [
{
"input_index": 0,
"route": "web",
"url": "https://example.com/article",
"status": "success",
"stage": "complete",
"ret_code": 0,
"media_id": "media_xxx"
},
{
"input_index": 1,
"route": "unsupported",
"url": "https://www.youtube.com/watch",
"status": "failed",
"stage": "probe",
"ret_code": null,
"media_id": "",
"error": {
"code": "unsupported_url_type",
"retryable": false
}
}
],
"error": {
"code": "partial_failure",
"message": "1 of 2 items failed.",
"exit_code": 9,
"retryable": false
}
}示例中的 URL 不包含 query、fragment 或 userinfo。
探测和下载前必须满足:
- scheme 只能是 http 或 https;
- host 必须存在且是合法 DNS 名称;
- 禁止 userinfo;
- 禁止控制字符;
- http 只允许默认端口 80,https 只允许默认端口 443;
- 禁止 localhost 和以 .localhost 结尾的名称;
- 禁止 IP literal;
- DNS 解析出的每一个 IPv4/IPv6 地址都必须是全局可路由地址;
- 任一地址为 private、loopback、link-local、multicast、reserved、unspecified 时整次目标校验失败;
- 每一个重定向目标都重新执行完整校验;
- 最多跟随 5 次重定向;
- 禁止从 HTTP(S) 跳到其他协议;
- 禁用环境代理,避免校验目标与实际连接目标分离;
- 连接使用已校验并固定的 IP,HTTP Host 与 HTTPS SNI 仍使用原始域名;
- 不发送 Cookie、Authorization、IMA header、COS header 或用户自定义 header;
- 请求头只允许固定 User-Agent、Accept、Accept-Encoding、Range 和必要的 Host;
- 错误和结果中的 URL 只显示 scheme、host、port 和 path,剥离 query 与 fragment。
DNS resolver 和连接工厂必须可注入,以便测试无需真实 DNS 或网络。
每个 URL 的探测顺序:
- 语法和明显不支持模式预检;
- 对规范化 URL 发起 HEAD;
- 跟随并校验重定向;
- 若 HEAD 返回 405、501,或缺少足够的 Content-Type/Content-Disposition,则发起 GET;
- GET 使用 Accept-Encoding: identity;如服务器支持,附带 Range: bytes=0-0;
- GET 只读取响应头,不为探测保存正文;
- 记录最终安全 URL、Content-Type、Content-Length、Content-Disposition;
- 使用固定决策表分类。
分类优先级:
- file URL、Bilibili 视频和 YouTube 视频立即 unsupported;
- 明确认识的文件 MIME 进入 file;
- text/html 或 application/xhtml+xml 根据 host/path 区分 wechat、web、unsupported video;
- application/octet-stream 仅在 Content-Disposition 或最终 path 给出受支持扩展名时进入 file;
- 缺失 MIME 时,仅允许已知文件 URL 模式或受支持扩展名进入 file;
- 其他 MIME 进入 unsupported,不猜成网页。
已知文件 URL 模式至少覆盖:
- arxiv.org/pdf/;
- 受支持文件扩展名;
- GitHub raw 内容域名和 raw 路径;
- 重定向后最终路径的受支持扩展名。
冲突裁决:
- 明确文件 MIME 优先于路径扩展名;
- 明确 HTML MIME 优先于伪装成文件的 path;
- application/octet-stream 需要文件名或 path 佐证;
- Content-Disposition 只用于文件名和 octet-stream 佐证,不覆盖明确 HTML;
- MIME 与扩展名不一致时按 MIME 选择 media_type,并产生安全 warning;
- 不读取正文做 magic sniffing。
推断优先级与 1.1.7 一致:
- Content-Disposition 的 filename*;
- Content-Disposition 的 filename;
- 最终 URL path 的最后一段;
- download 加上 Content-Type 对应扩展名。
清洗规则:
- 只取 basename,不接受目录;
- 将正斜杠、反斜杠和控制字符替换为下划线;
- 去除 NUL、前后空白、Windows 尾随点和空格;
- 拒绝空名称、单点和双点;
- 规避 CON、PRN、AUX、NUL、COM1 至 COM9、LPT1 至 LPT9;
- 扩展名由已判定 media_type 校验,不允许任意脚本扩展名;
- 操作系统临时文件名限制在 240 个 UTF-8 字节内,保留扩展名;
- API 最终文件名限制在 1024 个 Unicode 字符内;
- 清洗导致名称变化时记录 warning;
- 不在错误中显示原始危险 filename。
RemoteHttpClient.download_to 的不变量:
- 使用随机 TemporaryDirectory;
- 临时目录权限在平台允许时限制为当前用户;
- 先写 .part;
- 以 64 KiB 块读取;
- Content-Length 超过对应媒体类型上限时在写文件前拒绝;
- Content-Length 缺失时仍按实际累计字节执行硬上限;
- 总绝对上限为 200 MiB;
- 完成后校验实际大小与合法 Content-Length 一致;
- 关闭并 flush 文件后再改名为安全目标名;
- 返回 DownloadedFile 的 path 字段 repr=False;
- 返回 bytes_count、sha256、content_type、safe_name 和 final_host;
- 网络中断不保留半文件;
- service 退出 TemporaryDirectory 上下文前必须完成上传;
- 任何日志、JSON 和异常都不暴露临时绝对路径。
下载是安全读取,可在完全重建临时文件的前提下进行最多 2 次有限重试;不做断点续传。类型错误、大小错误、4xx、重定向安全错误不重试。
UploadService 的每个候选项遵循:
stateDiagram-v2
[*] --> Preflight
Preflight --> ConflictCheck
ConflictCheck --> Renamed: repeated and policy=rename
ConflictCheck --> Failed: repeated and policy=error
Renamed --> ConflictRecheck
ConflictRecheck --> Ready
ConflictCheck --> Ready: not repeated
Ready --> SnapshotCheck
SnapshotCheck --> CreateMedia
CreateMedia --> CredentialCheck
CredentialCheck --> CosUpload
CosUpload --> PostUploadCheck
CosUpload --> Failed: COS failure
PostUploadCheck --> Register
PostUploadCheck --> Failed: file changed
Register --> Complete
Register --> Orphaned: add_knowledge failure
Complete --> [*]
Failed --> [*]
Orphaned --> [*]
严格顺序:
- 所有候选项本地预检;
- 所有候选项一次性 check_repeated_names;
- 处理冲突并对 rename 名称再次检查;
- 每项 create_media;
- 校验该项 COS 凭证;
- 流式 PUT;
- 复核文件;
- add_knowledge。
任何前置批量检查失败时,不得调用 create_media。
UploadFileInfo 扩展为不可变快照,至少记录:
- resolved path,repr=False;
- st_dev;
- st_ino;
- st_size;
- st_mtime_ns;
- media_type;
- content_type;
- source_name;
- final_name;
- file_ext。
执行规则:
- 预检 stat 一次;
- create_media 前重新 stat;
- 打开文件后 fstat,并与快照比较;
- COS 发送使用同一个已打开文件描述符;
- 发送完成后再次 fstat;
- add_knowledge 前再次 stat path,确认路径仍指向相同文件;
- 任一 identity、size、mtime_ns 变化均抛出 file_changed_during_upload;
- 发生变化时,即使 COS 已成功也不得 add_knowledge;
- 结果标记 stage=post_upload_check 和 orphaned_media=true;
- 不尝试删除或覆盖远端对象。
error 策略:
- 默认;
- 发现任一服务端重名或批次内最终名称重复时,在任何 create_media 前终止该批次;
- 单文件命令返回 InputError;
- 多项命令形成逐项结果时,冲突项为 failed,未发生副作用的其他项为 not_attempted,整体退出 9。
rename 策略:
- 格式为 stem_YYYYMMDDHHmmss.ext;
- clock 通过依赖注入固定;
- 保留原扩展名;
- 同一秒内出现多个相同名称时追加 _2、_3;
- 名称长度超限时先截短 stem,再追加后缀;
- 生成后检查批次内唯一性;
- 对所有改名候选再次调用 check_repeated_names;
- 若仍重复,增加序号并最多重试 100 个候选名;
- 超出上限安全失败,不回退覆盖;
- 最终名称同时用于 create_media.file_name、file_info.file_name、add_knowledge.title 和结果输出;
- 本地文件本身不重命名。
cos_http.py 使用标准库 http.client 和默认 TLS 上下文。
不变量:
- 只连接 build_and_validate_cos_origin 返回的官方 HTTPS host;
- 忽略 custom_domain;
- 使用固定 Content-Length;
- 每次最多从文件流读取 64 KiB;
- 禁止 read() 无 size;
- 不把文件复制为 bytes;
- 签名 header 中 host、content-length 与实际请求完全一致;
- 签名 pathname 与发送 pathname 完全一致;
- 不自动跟随 3xx;
- 不自动重试;
- 2xx 才算成功;
- 错误正文最多读取 16 KiB,且不回显;
- timeout 默认 300 秒;
- 连接、发送和读取错误统一为 KnowledgeUploadError;
- Authorization、security token、secret ID、secret key 永不进入 repr、异常、JSON 或日志。
大文件验收不依赖真实 COS:
- 测试替身拒绝无界 read;
- 记录最大单次 read 不超过 64 KiB;
- 断言 Path.read_bytes 未被调用;
- 可选本地压力测试使用稀疏 200 MiB 文件,峰值额外内存应明显低于文件大小。
凭证检查:
- token、secret_id、secret_key、bucket_name、region、cos_key 非空;
- start_time、expired_time 是非 bool 整数;
- start_time < expired_time;
- 允许最多 300 秒本地时钟偏差;
- 凭证不得已经过期;
- 开始上传时至少剩余 30 秒有效期;
- 凭证剩余时间不足时在网络前失败;
- 任何错误不包含凭证字段值。
cos_key 规则:
- 作为相对对象 key 处理;
- 不得是 URL;
- 不得含 scheme、authority、query 或 fragment;
- 不得以斜杠或反斜杠开头;
- 不得含反斜杠、控制字符、空 segment、单点或双点 segment;
- 逐 segment 执行一次 URL 编码;
- slash 作为层级分隔符保留;
- 原始 key、规范 key、签名 path 和请求 path 的关系有固定测试向量;
- file_info.cos_key 使用验证后的规范 key。
输入顺序是结果顺序。
add-url:
- 先探测全部 URL;
- web/wechat 项合并为一次 import_urls 调用;
- file 项下载后形成上传候选;
- unsupported 项保留逐项失败;
- 服务端 results map 按请求 URL 回填,不能依赖 JSON map 顺序;
- 缺少请求项或出现未知结果项视为 ApiProtocolError;
- 不把网页 URL 混入文件重名检查。
add-file:
- 先预检全部本地文件;
- 再一次性重名检查;
- 单个 --file 保持既有顶层结果字段;
- 多个 --file 增加 results 和 summary。
继续策略:
- 单项输入、文件类型或冲突错误不会阻止其他已经通过预检的 add-url 项;
- IMA 配置、认证、顶层协议错误会停止剩余项;
- COS 单项失败后不调用该项 add_knowledge,但可继续下一个已准备项;
- KeyboardInterrupt 立即停止,已完成项不回滚,退出 130;
- 未开始项标记 not_attempted;
- 任何 partial/failed 结果不得被包装成 exit 0。
每个结果项至少包含:
- input_index;
- source_kind:local_file、web_url、file_url;
- 安全 source 名称;
- route;
- status;
- stage;
- final_name;
- renamed;
- media_id;
- ret_code;
- orphaned_media;
- 安全 error。
新增通用 PageRequest 和 PageCollection。
默认行为:
- 未传 --all 时只取一页;
- 现有 --cursor、--start 和 --limit 语义不变;
- limit 是每页大小,不改成总数上限。
--all 行为:
- 从用户给定 cursor/start 开始;
- max-pages 默认 100,允许 1 至 1000;
- is_end=true 时停止;
- cursor 重复时抛 ApiProtocolError;
- is_end=false 且 next_cursor 为空时抛 ApiProtocolError;
- offset 页返回空列表但 is_end=false 时抛 ApiProtocolError;
- offset 通过实际返回数量推进,且必须严格增加;
- 结果保持服务端页序和页内顺序;
- 达到 max-pages 但尚未结束时返回 status=partial、exit 9、truncated=true;
- JSON 保留原 next_cursor/is_end,并增加 pagination 对象;
- pagination 包含 all_requested、pages_fetched、max_pages、truncated、start 和 next。
不在本批次对业务条目按 ID 自动去重,因为服务端顺序和重复本身可能有语义;只检测游标无进展。
迁移后:
- command handler 不接收 as_json;
- command handler 不调用 print;
- service 不知道 stdout/stderr;
- output.py 是唯一 JSON dump 出口;
- 人类 warning 只写 stderr;
- 人类结果只写 stdout;
- JSON warning 合并到 warnings;
- parser 错误仍能根据 argv 中的 --json 进入统一 JSON 错误;
- help 仍为 argparse 人类文本;
- partial 人类模式先输出逐项结果,再在 stderr 输出安全汇总,返回 9。
人类展示:
- 已有名称时名称优先;
- ID 放在次级行,不在标题中堆叠;
- URL 不显示 query/fragment;
- 文件路径只显示 basename;
- 不打印内部 create_media、COS 签名等步骤;
- 失败时显示项、阶段和可操作建议。
采用:
- dataclass:服务领域结果、上传快照、URL 探测结果、CommandResult;
- Enum:route、stage、status、conflict policy;
- TypedDict:为保持运行时 dict 兼容的 API client 返回值;
- Protocol:API client、resolver、connection factory、clock 等可注入依赖;
- Mapping:只用于通用输出边界。
不再使用:
- 已知模型上的 object/getattr;
- service 中的 dict[str, Any] 字符串键编排;
- 把异常状态编码为空字符串;
- bool(value) 解析协议布尔值。
knowledge_api 和 notes_api 原有 dataclass 的导入路径继续有效。新服务类型从 api.py 导出,但私密的 DownloadedFile path 和凭证类型不做可序列化导出。
阶段 5 要求“评估按名称解析知识库、文件夹和笔记”。
本批次裁决为:
- 不增加隐式模糊匹配;
- 不为格式化额外发起隐藏 API 请求;
- 不在多个同名结果中自动选择;
- 人类输出仅在现有响应已经包含名称时优先显示名称;
- JSON 保留完整 ID;
- 若后续实现名称选择,应使用显式参数、精确匹配和歧义错误,并单独设计契约。
理由:
- 文件夹名称在不同层级可重复;
- 笔记标题不唯一;
- 自动搜索会改变写操作调用数量和失败面;
- 1.1.7 明确要求目标不确定时不要猜测。
不新增生产依赖的前提下:
- WAV 使用标准库 wave 读取帧数和采样率,超过 2 小时在 create_media 前拒绝;
- 非法 WAV 作为输入错误处理;
- MP3、M4A、AAC 继续执行类型和 200 MiB 大小检查,不猜测时长;
- README 明确压缩音频时长仍由服务端最终校验;
- 若产品要求客户端完整覆盖所有音频格式,先提交依赖评估,再征得确认。
新增 docs/URL_INGEST_CONTRACT_1_1_7.md。
内容:
- 来源优先级;
- URL 安全边界;
- 重定向上限;
- HEAD/GET 策略;
- MIME 决策表;
- 文件名推断;
- 大小限制;
- 安全展示;
- 逐项状态;
- 1.1.7 歧义和本项目裁决。
验收:
- 每个分类分支有输入条件、route 和失败码;
- 文档不含真实 URL token、Cookie 或用户数据;
- 代码常量能逐项对应文档。
新增 docs/UPLOAD_CONTRACT_1_1_7.md。
内容:
- Gate 顺序;
- 快照字段;
- 重名策略;
- rename 算法;
- COS target/key;
- 凭证时效;
- timeout;
- 阶段性失败;
- 孤立 media;
- 音频时长边界。
验收:
- 明确无 replace;
- 明确不重试 COS PUT/add_knowledge;
- 明确无删除 API 时只报告孤立状态。
新增 docs/CLI_JSON_SCHEMA_V1.md。
内容:
- 公共成功、空、partial、failed、error 示例;
- status 与 ok 的关系;
- 退出码;
- warnings;
- pagination;
- summary;
- 逐项结果;
- URL/路径清洗;
- 兼容字段承诺。
验收:
- 现有批次 B JSON fixture 仍合法;
- partial 有结果也有 error;
- stderr 规则明确。
新增 tests/fixtures/url_ingest/README.md 和脱敏 fixture:
- html_page.json;
- wechat_page.json;
- pdf_by_mime.json;
- pdf_by_disposition.json;
- octet_stream_by_filename.json;
- redirect_to_file.json;
- unsupported_video.json;
- private_redirect.json;
- missing_content_type.json;
- import_urls_mixed_result.json。
fixture 只描述请求方法、状态、headers 和安全 URL,不保存真实正文或凭证。
在实施重构前增加测试:
- 所有命令 help 中现有参数仍存在;
- 单页 JSON 关键字段;
- add-file 单文件参数;
- add-url 网页 URL API payload;
- ima-note 入口改写;
- parser JSON 错误;
- auth 缺凭证语义。
这些测试是 CLI 拆分的安全网,不能在同一个提交中先删后补。
修改 errors.py:
- 增加 ExitCode.PARTIAL=9;
- 增加 RemoteFetchError,退出码 7;
- 为安全 details 增加 stage、item_index、completed、failed、not_attempted;
- details 仍采用白名单,不允许 url、path、headers、credential;
- 保留所有批次 B 类和退出码。
新增 command_result.py:
- CommandStatus;
- CommandResult;
- ItemStatus;
- BatchSummary;
- 安全聚合函数。
测试:
- 状态与退出码组合不合法时构造失败;
- partial 必须带非零退出码;
- success/empty 必须为 0;
- summary 计数一致;
- error 字段不接受敏感 details。
修改 security.py 或新增 validation.py 中专用函数:
- parse_public_http_url;
- safe_display_url;
- validate_public_addresses;
- validate_redirect_target;
- sanitize_remote_filename。
测试覆盖:
- http/https;
- userinfo;
- 非默认端口;
- IP literal;
- localhost;
- IPv4/IPv6 私网;
- 混合公私 DNS 结果;
- 控制字符;
- query 清洗;
- Windows 保留名;
- path traversal filename。
新增 remote_http.py。
职责:
- DNS 解析与固定连接;
- HEAD;
- 受限 GET 探测;
- 手动重定向;
- header 规范化;
- 下载到文件流;
- 长度和 chunk 上限;
- 安全错误转换。
接口建议:
probe(url) -> RemoteProbe
download_to(probe, destination, max_bytes) -> RemoteDownload
RemoteProbe 只保存原始 URL repr=False,公开 safe_url、final_host、content_type、content_length、disposition 和 redirect_count。
测试完全使用 resolver/connection fake,不访问互联网。
新增 url_ingest.py 中 UrlClassifier。
输入:
- 安全语法结果;
- RemoteProbe;
- 支持的 MIME/扩展名表。
输出:
- UrlRoute.WEB;
- UrlRoute.WECHAT;
- UrlRoute.FILE;
- UrlRoute.UNSUPPORTED;
- media_type;
- content_type;
- 推断文件名;
- warnings。
分类器必须是纯函数,fixture 表驱动测试不创建临时文件或网络。
在 url_ingest.py 中实现 RemoteDownloadSession:
- 管理 TemporaryDirectory 生命周期;
- 生成安全文件名;
- 写 .part;
- 调用 RemoteHttpClient.download_to;
- fsync/flush 后改名;
- 生成 UploadCandidate;
- 离开上下文自动清理。
测试:
- 成功时文件存在于上下文内;
- 离开上下文后目录不存在;
- 大小超限、短读、网络中断、写失败均无残留;
- 结果 repr 不含 path;
- sha256 正确;
- Content-Length 不可信时仍有硬上限。
修改 knowledge_api.py:
- 为 import_urls 返回值增加 TypedDict;
- ImportUrlResult.ret_code 保持严格 int;
- 按请求 URL 验证 results;
- 缺失请求项、未知额外项、错误 inner url 均为 ApiProtocolError;
- 返回结果顺序与输入顺序一致;
- 不在协议错误中回显 URL。
若真实 1.1.7 fixture 证明服务端允许省略 inner url,以契约文档裁决为准,不以空字符串猜测。
新增 url_ingest.py 中 UrlIngestService。
依赖:
- KnowledgeBaseApiClient Protocol;
- RemoteHttpClient;
- UploadService;
- temp factory;
- clock。
职责:
- 最多 10 项;
- 探测和分类;
- 聚合 web/wechat;
- 下载 file;
- 调用 UploadService;
- 保持输入顺序;
- 合并服务端 ret_code 与本地阶段;
- 输出 UrlIngestResult;
- 计算 CommandStatus 和退出码。
服务不打印、不生成 argparse 错误文本、不序列化完整 URL。
新增:
- tests/test_remote_http.py;
- tests/test_url_ingest.py;
- tests/test_url_security.py,或扩展 test_security.py。
关键断言:
- 从未构造 IMA/COS header;
- 每跳重新校验;
- DNS 固定;
- 私网重定向在第二次请求前失败;
- 最多 5 跳;
- HEAD 回退;
- HTML、微信、文件、unsupported 分流;
- 文件 URL 进入 UploadService;
- 网页 URL 只进入 import_urls;
- 混合结果顺序稳定;
- URL query token 不出现在 stdout、stderr、repr 和异常。
修改 knowledge_upload.py,将它收敛为:
- 兼容导出;
- 文件类型表;
- inspect_upload_file;
- COS 签名纯函数。
上传状态机移到 upload_service.py,网络 PUT 移到 cos_http.py。
UploadFileInfo 增加快照字段和 final_name,path repr=False。
新增预检:
- 空文件;
- 名称长度;
- 安全 basename;
- WAV 时长;
- stat 错误分类;
- 多文件 content-type 规则。
修改 security.py,并在 cos_http.py 增加 CosUploadTarget:
- validate_cos_credential;
- normalize_cos_key;
- build target;
- 统一 encoded pathname;
- clock skew;
- 最小剩余有效期。
测试使用固定 clock 和固定签名向量。
新增 cos_http.py。
接口建议:
upload(file_handle, snapshot, credential, timeout) -> CosUploadResult
CosUploadResult 仅包含:
- bytes_sent;
- status_code;
- 可选安全 ETag;
- elapsed_ms。
不得包含 Authorization、token、secret、完整 URL 或文件 path。
连接层通过 Protocol 注入,以验证:
- 固定 Content-Length;
- 64 KiB;
- 无 read_bytes;
- 3xx/4xx/5xx;
- 短读;
- timeout;
- response body 中断;
- connection close。
新增 upload_service.py。
主要方法:
prepare_local_files(...)
upload_candidates(...)
upload_one(...)
服务返回 UploadBatchResult 和 UploadItemResult,不返回裸 dict。
每个外部调用前后写入内存中的 stage;异常转换为逐项安全错误。stage 不写磁盘日志。
UploadService:
- 一次发送全部 params;
- 验证返回名称集合与请求一致;
- 检测输入内重名;
- error 策略无副作用停止;
- rename 使用注入 clock;
- 重新 check;
- 最多 100 次候选;
- 最终名称全链路一致。
修改 knowledge_api.check_repeated_names:
- 返回顺序不可信,service 按 name 关联;
- 缺失、重复或未知 name 为 ApiProtocolError;
- 不通过 zip 静默配对。
UploadService:
- create_media 前 verify_snapshot;
- 打开后 verify_fstat;
- 上传后 verify_fstat;
- 注册前 verify_path_identity;
- 变更后标记 orphaned_media;
- 禁止 add_knowledge。
测试在 fake uploader 回调中修改、截断、替换文件,分别验证每个分支。
结果规则:
- create_media 失败:media_id 为空、orphaned=false;
- COS 失败:media_id 可返回、orphaned=true;
- post-check 失败:orphaned=true;
- add_knowledge 失败:orphaned=true、stage=register;
- complete:orphaned=false;
- 不输出 cos_key 和凭证;
- 人类建议说明可重试整个命令可能创建新 media,不承诺自动清理。
新增:
- tests/test_cos_http.py;
- tests/test_upload_service.py。
扩展:
- tests/test_knowledge_upload.py;
- tests/test_knowledge_api.py;
- tests/test_security.py。
必须覆盖每个 Gate 的成功和失败,以及调用顺序断言。
修改 output.py:
- render_command_result;
- render_json_result;
- render_human_result;
- partial JSON;
- warning channel;
- schema reserved fields;
- status;
- summary;
- error。
保留 emit_json_success、emit_json_error、emit_human_error 作为兼容函数,内部委托新实现。
修改 cli.py:
- 删除 _run_handler 的 stdout/stderr 捕获和 json.loads;
- handler 直接返回 CommandResult;
- 顶层只执行一次 render;
- ImaCliError 仍由统一异常边界处理;
- KeyboardInterrupt 仍为 130;
- 未分类异常继续清洗为 70;
- JSON 模式在解析前从 argv 识别。
验收:
- command 内部不再接收 as_json;
- JSON 输出不依赖 handler 自己 print;
- 测试可直接断言 CommandResult。
新增 validation.py:
- require_non_empty_id;
- validate_limit;
- validate_max_pages;
- validate_timeout;
- validate_cursor;
- validate_url_count;
- validate_file_count;
- parse_conflict_policy。
API client 继续调用同一纯函数或保留等价保护,不能只依赖 CLI。
新增 pagination.py:
- CursorPage;
- OffsetPage;
- PageCollection;
- collect_cursor_pages;
- collect_offset_pages;
- loop/no-progress 检测;
- max-pages partial。
新增 tests/test_pagination.py,使用纯 fake fetcher。
新增:
- src/ima_note_cli/commands/init.py;
- commands/auth.py;
- commands/notes.py;
- commands/knowledge.py。
每个模块分为:
- register(parent_parser);
- execute(args, context);
- payload builder;
- human formatter。
注册函数接收公开的 ArgumentParser,不把 argparse._SubParsersAction 暴露为核心接口。
cli.py 或 commands/init.py 定义 AppContext:
- credentials;
- notes client factory;
- knowledge client factory;
- source client factory;
- remote client factory;
- upload service factory;
- clock/temp factory。
只为当前命令创建所需依赖:
- auth 不创建 API client;
- note 不创建 Knowledge client; -普通 kb 查询不创建 RemoteHttpClient/CosUploader;
- kb add-url 才创建 URL 与上传服务;
- kb add-file 才创建上传服务;
- kb read/export 继续创建批次 B 的 MediaContentService。
将 notes_cli.py 的行为迁移到 commands/notes.py:
- search/get/folders/list/create/append;
- 内容准备 warning;
- JSON payload;
- 人类格式;
- 分页 --all。
notes_cli.py 暂时保留兼容 re-export 或薄 façade。
迁移不得改变:
- note_id/doc_id 字段;
- Markdown 图片过滤;
- UTF-8;
- 搜索参数;
- ima-note 行为。
将 knowledge_cli.py 的查询、add-note、media-info、read、export 迁移到 commands/knowledge.py。
接入 pagination,并保留批次 B 的媒体安全边界。
path_node_to_dict 和 import_url_result_to_dict 改为明确类型参数,不再使用 getattr。
commands/knowledge.py:
- add-url 调用 UrlIngestService;
- add-file 调用 UploadService;
- 生成兼容顶层字段;
- 多项结果和 partial;
- timeout 与 conflict 参数;
- warning;
- 安全人类输出。
CLI 不再直接执行 check_repeated_names、create_media、upload_to_cos 或 add_file。
knowledge_cli.py 与 notes_cli.py:
- 保留历史导入函数需要的最小 wrapper;
- validate_urls 转发到新 validation/classifier 的纯语法预检;
- 不保留两份业务实现;
- 标注内部弃用但不在本批次删除。
若仓库内外没有公共承诺,也至少保留到批次 D 后再评估。
修改 api.py:
- 导出 UploadService、UrlIngestService;
- 导出安全的结果类型;
- 导出 ConflictPolicy;
- 保留现有导出;
- 不导出内部连接类、临时路径或凭证序列化辅助。
修改 knowledge_api.py 的返回类型注解,保持运行时 dict 兼容的位置使用 TypedDict。
新增 tests/test_cli_batch_c.py。
至少覆盖:
- add-url 网页成功;
- add-url 文件成功;
- 混合 partial 退出 9;
- 全部逐项失败;
- JSON stderr 为空;
- 人类 warning 在 stderr;
- URL token 不泄露;
- add-file 单文件兼容字段;
- add-file 多文件 summary;
- conflict error/rename;
- timeout 参数;
- --all;
- max-pages partial;
- parser 错误;
- ima-note;
- 批次 B media 命令回归。
修改 README.md:
- add-url 自动分流;
- add-file 多文件;
- conflict;
- timeout;
- partial 退出码 9;
- --all;
- JSON status;
- URL 与临时文件安全;
- 流式上传;
- 音频限制;
- 孤立 media 提示。
修改 skills/ima-note-cli/SKILL.md:
- 只同步当前 CLI 的真实命令和安全行为;
- 不在本批次解决多个 skill 版本的 canonical 问题;
- 不复制上游 CJS 命令作为 Python CLI 的运行依赖。
修改:
- docs/KNOWLEDGE_MEDIA_CONTRACT_1_1_7.md,补充与用户 URL 客户端的边界;
- docs/API_CONTRACT_1_1_7.md,补充 import_urls 结果关联和上传裁决引用;
- OPTIMIZATION_PLAN.md,链接本计划并在实施完成后勾选对应项。
不修改 ima-skills-1.1.7 (1) 原始资料。
最后执行:
- 全量单测;
- compileall;
- 文档链接和敏感词搜索;
- 所有网络入口盘点;
- read_bytes 和无界 read 搜索;
- 所有 print/json.dumps 入口盘点;
- git diff --check;
- 工作区状态确认。
| 文件 | 用途 |
|---|---|
| BATCH_C_IMPLEMENTATION_PLAN.md | 本计划 |
| docs/URL_INGEST_CONTRACT_1_1_7.md | 用户 URL 分流与安全裁决 |
| docs/UPLOAD_CONTRACT_1_1_7.md | 上传 Gate、冲突、COS 与失败裁决 |
| docs/CLI_JSON_SCHEMA_V1.md | JSON/状态/退出码契约 |
| src/ima_note_cli/command_result.py | 命令和批量结果模型 |
| src/ima_note_cli/validation.py | 共享输入校验 |
| src/ima_note_cli/pagination.py | 有界分页收集 |
| src/ima_note_cli/remote_http.py | 无凭证公网 URL 探测与下载 |
| src/ima_note_cli/url_ingest.py | URL 分类、下载会话和分流服务 |
| src/ima_note_cli/cos_http.py | COS target、签名请求和流式 PUT |
| src/ima_note_cli/upload_service.py | 上传状态机与批量编排 |
| src/ima_note_cli/commands/init.py | 命令包公共接口 |
| src/ima_note_cli/commands/auth.py | auth 注册和展示 |
| src/ima_note_cli/commands/notes.py | Notes 命令适配 |
| src/ima_note_cli/commands/knowledge.py | Knowledge 命令适配 |
| tests/test_command_result.py | 状态、汇总和退出码 |
| tests/test_pagination.py | cursor/offset/max-pages |
| tests/test_remote_http.py | 公网 HTTP 与重定向安全 |
| tests/test_url_ingest.py | URL 分类和混合流程 |
| tests/test_cos_http.py | 流式 COS PUT |
| tests/test_upload_service.py | 上传 Gate 和阶段失败 |
| tests/test_cli_batch_c.py | CLI/JSON/partial 回归 |
| tests/fixtures/url_ingest/README.md | fixture 来源和脱敏说明 |
| tests/fixtures/url_ingest/*.json | URL 探测与导入样例 |
最终实施时可以把紧密耦合的小模型放入对应 service 模块,避免为每个 dataclass 建文件;但不能重新把网络、状态机和 CLI 合回一个大型模块。
| 文件 | 修改 |
|---|---|
| OPTIMIZATION_PLAN.md | 链接本计划,完成后更新阶段状态 |
| README.md | URL、上传、分页、JSON 和退出码 |
| docs/API_CONTRACT_1_1_7.md | import_urls、上传和 partial 裁决 |
| docs/KNOWLEDGE_MEDIA_CONTRACT_1_1_7.md | 区分媒体 URL 与用户 URL |
| src/ima_note_cli/errors.py | partial 和 RemoteFetchError |
| src/ima_note_cli/output.py | 直接渲染 CommandResult |
| src/ima_note_cli/security.py | 公网 URL、COS key 和凭证时效 |
| src/ima_note_cli/knowledge_api.py | 严格结果关联和 TypedDict |
| src/ima_note_cli/knowledge_upload.py | 预检/签名兼容 façade |
| src/ima_note_cli/knowledge_cli.py | 迁移后薄兼容层 |
| src/ima_note_cli/notes_cli.py | 迁移后薄兼容层 |
| src/ima_note_cli/cli.py | AppContext、直接结果和命令注册 |
| src/ima_note_cli/api.py | 新服务和安全结果导出 |
| tests/test_cli.py | 既有命令兼容 |
| tests/test_cli_batch_b.py | 批次 B 输出回归 |
| tests/test_output.py | status/partial |
| tests/test_errors.py | 退出码 9 与安全 details |
| tests/test_security.py | 公网 URL、COS key、凭证时间 |
| tests/test_knowledge_api.py | URL map 和重名 map 关联 |
| tests/test_knowledge_upload.py | 新预检与兼容函数 |
| skills/ima-note-cli/SKILL.md | 活动 CLI 行为最小同步 |
- pyproject.toml;
- uv.lock;
- src/ima_note_cli/http.py;
- src/ima_note_cli/config.py;
- src/ima_note_cli/protocol.py;
- src/ima_note_cli/source_http.py;
- src/ima_note_cli/media_service.py;
- src/ima_note_cli/notes_content.py;
- ima-skills-1.1.7 (1) 原始目录;
- skills/ima-skills-1.1.2;
- skills/ima-note;
- CI 和发布元数据。
若实施中需要修改 source_http.py,只允许抽取无状态 MIME/长度辅助,不能放宽其 IMA/COS allowlist 或临时 header 重定向规则。
- 只接受 http/https;
- file URL 在网络前拒绝;
- userinfo 在网络前拒绝;
- 非默认端口在网络前拒绝;
- IP literal、localhost、私网、回环、链路本地和保留地址在网络前拒绝;
- DNS 混合返回中只要有一个非公网地址即拒绝;
- 每个 redirect 重新校验;
- 连接固定到已经校验的 IP,Host/SNI 保留域名;
- 环境代理不改变目标;
- 最多 5 次 redirect;
- URL query/fragment 不出现在错误、repr、JSON 和人类输出;
- 请求不包含 IMA/COS 凭证和 Cookie。
- HTML 普通页面进入 import_urls;
- 微信页面进入 import_urls;
- Bilibili/YouTube 为 unsupported;
- 明确文件 MIME 进入文件流程;
- Content-Disposition filename 和 filename* 可推断文件名;
- octet-stream 只有在受支持文件名/path 佐证时进入文件流程;
- HTML 可覆盖伪 .pdf path;
- arxiv 与 GitHub raw 有 fixture;
- 未知 MIME 安全拒绝;
- MIME/扩展名冲突产生 warning;
- 不读取正文做类型猜测。
- 64 KiB 分块;
- Content-Length 预检;
- 实际读取硬上限;
- 绝对上限 200 MiB;
- 类型特定上限沿用上传表;
- 长度不一致失败;
- sha256 正确;
- .part 不残留;
- TemporaryDirectory 在所有分支清理;
- temp path 不序列化;
- 安全读取重试最多 2 次且每次重建文件;
- 4xx、类型、大小和安全错误不重试。
- 空文件拒绝;
- 不支持扩展名拒绝;
- 视频拒绝;
- 类型大小上限正确;
- 文件名最长 1024 字符;
- 危险 basename 拒绝或清洗;
- 单文件 content-type 兼容;
- 多文件带全局 content-type 在网络前拒绝;
- WAV 超过 2 小时拒绝;
- 非 WAV 压缩音频不伪造时长结果。
- check_repeated_names 始终早于 create_media;
- 批量只做一次初始检查;
- error 为默认;
- error 不覆盖、不改名;
- rename 格式正确;
- 同秒同名有稳定序号;
- rename 后再次服务端检查;
- 最多 100 次候选保护;
- 最终名称在 create_media、file_info、title 中完全一致;
- 本地文件不被重命名;
- 返回 map 缺失/额外项为协议错误。
- secret repr 隐藏;
- 过期、未生效、顺序错误和剩余时间不足在网络前失败;
- bucket/region 注入在网络前失败;
- custom_domain 被忽略;
- cos_key URL、穿越、反斜杠、控制字符和空 segment 拒绝;
- 签名 path 与请求 path 同一值;
- 只访问官方 myqcloud.com HTTPS host;
- 凭证值不进入错误和 JSON。
- 不调用 Path.read_bytes;
- 不执行无界 read;
- 最大 read 为 64 KiB;
- Content-Length 等于快照大小;
- 默认 timeout 300 秒;
- 允许 1 至 3600 秒配置;
- 3xx 不跟随;
- 非 2xx 失败;
- 错误正文读取有 16 KiB 上限;
- COS PUT 不自动重试;
- add_knowledge 不自动重试;
- 200 MiB 压力验证不会额外占用同等内存。
- create_media 前变化会停止且无远端副作用;
- 打开后 identity 不同会停止;
- 上传中截断/增长会失败;
- COS 失败后不调用 add_knowledge;
- 上传后 mtime/size/identity 变化后不调用 add_knowledge;
- add_knowledge 仅在所有 Gate 通过后调用;
- 每个 Gate 有调用顺序测试。
- create_media 失败不标记孤立;
- COS 后失败标记 orphaned_media;
- add_knowledge 失败保留安全 media_id;
- 不承诺不存在服务端临时对象;
- 不尝试不存在的删除 API;
- 结果含 stage;
- 结果不含 credential、cos_key、完整 URL 和 temp path。
- add-url 1 至 10;
- add-file 1 至 2000;
- 结果保持输入顺序;
- all success 为 success/0;
- 空查询为 empty/0;
- 混合结果为 partial/9;
- 全部逐项失败为 failed/9;
- 无逐项结果的输入错误仍为 2;
- JSON partial stdout 单文档、stderr 空;
- 人类 partial 有逐项结果和 stderr 汇总;
- KeyboardInterrupt 保留已完成项语义但退出 130。
- handler 不接收 as_json;
- handler 不 print;
- service 不依赖 argparse;
- cli 不捕获 stdout 再 json.loads;
- output.py 是唯一 JSON dump 出口;
- argparse 私有类型不作为核心公共接口;
- knowledge_cli/notes_cli 只保留兼容 façade;
- 已知模型不再使用 object/getattr;
- api.py 旧导出保持;
- ima-note 兼容入口保持。
- 默认仍为单页;
- --all 覆盖所有支持命令;
- --max-pages 默认 100;
- 范围 1 至 1000;
- cursor 循环失败;
- 空 next_cursor 且未结束失败;
- offset 无进展失败;
- 达到 max-pages 为 partial/9;
- pagination JSON 字段一致;
- 现有顶层 next_cursor/is_end 保留。
- 当前 103 个基线测试继续通过;
- 新增测试全部离线;
- 无真实凭证和用户数据;
- README、CLI help、活动 skill 和契约一致;
- 不修改上游 ima-skills-1.1.7 (1);
- 不新增生产依赖;
- pyproject.toml 和 uv.lock 无无关变化;
- 不提前实施批次 D;
- git diff --check 通过;
- 用户原有工作区改动未被覆盖。
实施过程中按聚焦顺序执行:
uv run python -m unittest tests.test_command_result tests.test_output tests.test_errors -v
uv run python -m unittest tests.test_remote_http tests.test_url_ingest tests.test_security -v
uv run python -m unittest tests.test_cos_http tests.test_upload_service tests.test_knowledge_upload -v
uv run python -m unittest tests.test_pagination tests.test_knowledge_api tests.test_notes_api -v
uv run python -m unittest tests.test_cli tests.test_cli_batch_b tests.test_cli_batch_c -v
uv run python -m unittest discover -s tests -v
uv run python -m compileall -q src tests命令面 smoke:
uv run ima --help
uv run ima note list --help
uv run ima kb add-url --help
uv run ima kb add-file --help
uv run ima-note --help静态检查:
rtk rg -n "read_bytes|read\(\)" src/ima_note_cli
rtk rg -n "urlopen|build_opener|HTTPConnection|HTTPSConnection|Request\(" src/ima_note_cli
rtk rg -n "Authorization|security-token|secret_id|secret_key|api_key|Cookie" src tests
rtk rg -n "print\(|json\.dumps|redirect_stdout|redirect_stderr|json\.loads" src/ima_note_cli
rtk rg -n "object|getattr\(" src/ima_note_cli/commands src/ima_note_cli/*service.py
rtk rg -n "file://|localhost|is_private|is_loopback|is_link_local" src tests
rtk rg -n "on-conflict|upload-timeout|download-timeout|--all|max-pages" src tests README.md skills/ima-note-cli
rtk git diff --check
rtk git status --short解释:
- read_bytes 在媒体导出测试或无关安全代码中出现时需人工判断;上传生产路径不得出现;
- 凭证词汇允许出现在字段定义、header 构造、redaction 和虚构测试中,真实值不得出现;
- json.loads 可用于解析服务端响应或测试,不得继续用于反向解析 handler stdout;
- 每个网络入口必须能指向目标校验、大小上限、超时、重定向和重试策略。
自动验收不访问互联网、不使用真实账户、不写入真实知识库。
若需要人工 smoke,分两级:
在明确允许联网后,使用无敏感参数的公开测试 URL 验证:
- HTML;
- 302 到 HTML;
- PDF;
- Content-Disposition;
- 超限 Content-Length。
只运行 probe/classify,不调用 import_urls、create_media 或 COS。
只有在用户明确授权、使用专用知识库并接受产生孤立 media 的可能性后执行:
1. 上传小型 TXT。
2. 再次上传,验证默认 conflict error。
3. 使用 rename,验证最终名称。
4. 添加一个网页 URL。
5. 添加一个小型文件 URL。
6. 使用一个成功和一个服务端拒绝 URL 验证退出码 9。
smoke 输出必须先检查:
- 无凭证;
- 无完整签名 URL;
- 无本地绝对临时路径;
- 无 Authorization/Cookie。
包含:
- 三份契约文档;
- fixture;
- CommandResult;
- ExitCode.PARTIAL;
- 现有 CLI 快照测试。
回滚边界:不改变生产工作流。
包含:
- security;
- remote_http;
- URL classifier;
- filename;
- 纯离线测试。
回滚边界:尚未接入 add-url。
包含:
- cos_http;
- 上传快照;
- 凭证/key;
- conflict;
- 状态机;
- Gate 测试。
回滚边界:旧 add-file 暂未切换。
包含:
- 临时下载;
- UrlIngestService;
- import_urls 关联;
- 文件 URL 接 UploadService;
- partial。
回滚边界:服务可独立测试,CLI 暂未切换。
包含:
- output;
- cli;
- commands;
- validation;
- AppContext;
- 移除 stdout 反解析。
回滚边界:所有旧命令兼容测试必须先绿。
包含:
- pagination;
- --all;
- add-url;
- add-file;
- timeout/conflict;
- CLI batch C 测试。
回滚边界:可以独立回退新增参数和 service 接线,不回退批次 B。
包含:
- README;
- 活动 skill;
- 契约引用;
- 全量测试;
- compileall;
- 对抗性安全检查。
每个切片必须保持全量测试通过;不得把 URL 网络层、COS 重写和 CLI 拆分压成一个不可审阅提交。
应对:
- HEAD 信息不足才 GET;
- 决策依赖最终 GET headers;
- 不下载探测正文;
- 用 fixture 覆盖 405、501、缺 MIME 和重定向。
应对:
- 全地址校验;
- 每跳校验;
- DNS 固定连接;
- 禁用代理;
- 默认端口;
- 无凭证;
- 私网对抗测试。
应对:
- 校验后连接固定 IP;
- HTTPS 使用原域名 SNI 和证书验证;
- 不让连接层再次按域名解析;
- resolver/connection 行为有单元测试。
应对:
- 明确 CLI 只自动下载公网 URL;
- 内网文件要求用户先安全下载再 add-file;
- 不提供跳过安全检查开关;
- 文档给出可操作替代方案。
应对:
- 固定优先级;
- HTML 不因 .pdf path 下载;
- octet-stream 要求文件名佐证;
- 不做 magic sniffing;
- 冲突产生 warning。
应对:
- basename;
- 控制字符和分隔符清洗;
- 保留名保护;
- 长度限制;
- 随机临时目录;
- 原始危险名称不回显。
应对:
- CosUploadTarget 单一对象生成 origin、host、pathname;
- 同一 headers map 用于签名和请求;
- 固定向量;
- 编码一次;
- 禁止 redirect。
应对:
- stat/fstat;
- 同一文件描述符;
- 上传后复核;
- 不调用 add_knowledge;
- 报告孤立状态。
应对:
- rename 后重新 API 检查;
- 服务端最终仍可能竞态,add_knowledge 错误按阶段报告;
- 不自动覆盖;
- clock 和序号确定性测试。
应对:
- 明确结果;
- 保留 media_id;
- 不泄露凭证;
- 不虚构删除能力;
- 文档说明重试可能产生新 media。
应对:
- 重构前冻结快照;
- schema v1 只做加法;
- 单文件顶层字段保持;
- 兼容 façade;
- 旧测试与新测试并行。
应对:
- 只有此前被错误视为成功的部分失败改为 9;
- 全成功仍为 0;
- README 和 schema 文档明确;
- JSON 仍保留逐项 ret_code/media_id;
- 提供 fixture 测试。
应对:
- 所有本地预检和重名检查先完成;
- 配置/协议类错误停止剩余项;
- 逐项阶段和 not_attempted 明确;
- 不并行上传;
- KeyboardInterrupt 立即停止。
应对:
- cursor 集合;
- offset 进度;
- 默认 100 页;
- 最大 1000;
- 达到上限 partial/9;
- 默认仍为一页。
应对:
- 本批次只用 wave 覆盖 WAV;
- 压缩格式不猜测;
- 需要依赖时先评估并确认;
- 不偷偷修改 pyproject/uv.lock。
应对:
- 只最小同步活动 CLI skill;
- 不整理多个 skill 目录;
- 不引入 CI/lint/release;
- 不改上游资料;
- 文件清单与验收中单独检查。
批次 C 只有在以下条件同时满足时才算完成:
- 用户 URL 有独立、无凭证、逐跳校验的公网 HTTP 客户端。
- 私网、localhost、IP literal、userinfo、非默认端口和危险重定向在网络前或下一跳前被拒绝。
- HTML、微信、文件、Bilibili、YouTube 和 unknown 分类有固定契约与测试。
- 文件名按 Content-Disposition、URL path、MIME 顺序推断并安全清洗。
- 文件型 URL 使用有界临时下载,所有失败路径无残留。
- 下载和上传均不一次性加载大文件。
- UploadService 独立于 CLI,并实现完整 Gate 顺序。
- 空文件、大小、文件名和 WAV 时长在 create_media 前检查。
- check_repeated_names 始终早于 create_media,批量初始检查一次完成。
- 默认 conflict error,不支持 replace。
- rename 可重复验证,最终名称贯穿 create_media、file_info 和 title。
- COS 凭证、官方 host、有效期和 key 在网络前严格校验。
- COS PUT 为固定长度 64 KiB 流式发送,默认 timeout 300 秒,不自动重试或跟随重定向。
- 上传前后文件身份、大小和修改时间得到复核。
- COS 或复核失败后绝不调用 add_knowledge。
- add_knowledge 失败明确报告孤立 media,且不泄露任何凭证。
- add-url 网页和文件分支可以混合执行,结果保持输入顺序。
- import_urls ret_code 非零会导致 partial/failed 非零退出。
- JSON partial 是单个安全文档,stderr 为空,退出码为 9。
- Command handler 返回 CommandResult,不再通过 stdout 反向解析 JSON。
- 命令注册、服务、校验、分页和输出边界清晰,service 可脱离终端测试。
- 所有可分页命令支持有上限的 --all,默认单页行为不变。
- 现有命令、JSON 业务字段、api.py 导出和 ima-note 入口保持兼容。
- 当前 103 个批次 B 基线测试继续通过,新测试全部通过。
- 所有测试离线,无真实凭证、用户数据或真实知识库写入。
- README、CLI help、活动 CLI skill 和三份新契约与实现一致。
- 未新增生产依赖;pyproject.toml 和 uv.lock 无无关变化。
- 未修改上游 ima-skills-1.1.7 (1) 原始资料。
- 未提前实施批次 D 的 skill 归一、CI 和发布工作。
- compileall、静态安全检查、git diff --check 和工作区检查通过。
完成批次 C 后,项目的核心 API、原文、URL 导入、文件上传和 CLI 结构优化即具备闭环;随后进入批次 D,处理 canonical skill、工程化门禁、打包和发布。