# blog — 泛微E10工作日报

`blog` 模块把 E10「工作日报」的查询与写入链路收敛成一组业务 operation，供人和 Agent 统一调用。源资料为生产环境已验证的 `weaver-e10-blog` Agent Skill（v1.3.0，含接口 reference 与实测脚本）。CLI 命令后缀 `blog`，Skill 后缀 `blog`，两者同名；源资料指纹见 `skills/weaver-e10-blog/references/source-manifest.json`。

## 模块来源

- `SKILL.md` 与 `references/blog-*.md`：群体日报查询、单人日报查询、HRM 姓名解析、评论查询、默认共享、导出、发布、编辑、评论、点赞、提醒的接口出入参与实测结论。
- `references/blog-disabled-capabilities.md`：按 userId 列表批量查个人、unRead、EB 日报应用接口族、删除日报等未暴露能力的判定依据。
- `scripts/*.mjs`（query-day / query-weaver-e10-blog / publish-blog / edit-blog / comment-blog / like-blog / remind-blog / export-blog）：参数形态与调用顺序样例。

## CLI 结构

```text
weaver-work-cli blog --help
weaver-work-cli blog schema
weaver-work-cli --json blog run <operation> --input-json '<json>'
weaver-work-cli --json blog run <operation> --input <path>
```

便捷子命令（均为薄包装，最终调用同一 operation）：

- `blog query` → `blog.query`
- `blog query-user` → `blog.query_user`
- `blog resolve-users` → `blog.resolve_users`
- `blog comments` → `blog.comments`
- `blog default-permission` → `blog.default_permission`
- `blog export` → `blog.export`
- `blog publish|edit|comment|like|remind prepare|apply` → 对应写入链

源码结构：

```text
src/shortcuts/blog/
  index.ts            Commander 命令注册、schema/run、便捷子命令、JSON 输入解析
  manifest.ts         operation 合约清单、输入示例、未暴露能力与理由
  operations.ts       run 入口：选择 handler、校验输入对象与禁用字段
  errors.ts           结构化错误（validation/authentication/network/api/confirmation/partial）
  host.ts             E10 请求适配 + 业务码位置漂移归一化 + 导出流校验
  continuation.ts     prepare/apply 写入链的确认凭据签发与校验
  operations/
    registry.ts       manifest 与 handler 一一对应校验
    types.ts          operation 类型、统一 JSON Schema 片段
    shared.ts         输入校验、日期/时区、指纹、HRM 人员解析、导出落点
    query.ts          blog.query
    query-user.ts     blog.query_user、blog.resolve_users
    comments.ts       blog.comments
    export.ts         blog.export
    publish.ts        blog.default_permission、发布 prepare/apply
    edit.ts           编辑 prepare/apply
    comment.ts        评论 prepare/apply
    like.ts           点赞/取消点赞 prepare/apply
    remind.ts         提醒 prepare/apply
```

## operation 表

| operation | 风险 | 说明 |
| --- | --- | --- |
| `blog.query` | read | 群体日报查询（`user_type`：directSubordinate / indirectSubordinate / allSubordinate / myFollow），客户端按日期窗口裁剪；未提交按人天列在 `not_submitted`，整窗未交见 `not_submitted_people` |
| `blog.query_user` | read | 单人日报查询（detailsByDate 逐日扫描，区间上限 62 天；按人查询的唯一可靠路径；不传 `user_id`/`name` 时查当前登录人）；每天带 `submit_status`，并汇总 正常/补交/未提交 天数 |
| `blog.resolve_users` | read | HRM 姓名精确解析为 userId（取 `id` 字段，不是 `userid`） |
| `blog.comments` | read | 查某条日报评论列表（commentPage） |
| `blog.default_permission` | read | 查某日期默认共享对象，供发布时回填 `permissionList` |
| `blog.export` | local-file-write | 导出日报 xlsx（拒绝覆盖已存在文件） |
| `blog.publish.prepare` / `.apply` | read-before-write / high-risk-write | 发布日报；prepare 查当日重复 + 回填默认共享；apply 写前再查重 + 写后回查 |
| `blog.edit.prepare` / `.apply` | read-before-write / high-risk-write | 编辑日报；prepare 取原内容做差异；apply 指纹校验 + 回查内容一致性 |
| `blog.comment.prepare` / `.apply` | read-before-write / high-risk-write | 评论日报；目标不存在即报错；apply 从评论列表回查该评论 |
| `blog.like.prepare` / `.apply` | read-before-write / high-risk-write | 点赞 / 取消点赞；无只读回查接口，以业务码为准 |
| `blog.remind.prepare` / `.apply` | read-before-write / high-risk-write | 提醒提交；默认文案「请填写 YYYY年 M月 D日 的日报！」；逐人发送，部分失败返回 `PARTIAL` |

withheld（未暴露）：按 userIds 批量查个人（服务端忽略该过滤）、unRead（恒返回空）、EB 日报应用接口族（租户未启用）、删除日报、撤回已发送提醒。理由记录在 `weaver-work-cli blog schema` 的 `withheldOperations`。

## 关键设计

- **补交口径统一**：`createTime`（提交时间）与 `blogTime`（日报日期）同日为 `正常`，跨日为 `补交`，无日报为 `未提交`；不使用 blogs 接口的 `later` 字段（实测不可靠）。
- **窗口裁剪在客户端**：服务端对 blogs 接口不按 `filter.startDate/endDate` 硬过滤，返回全量倒序；模块按窗口过滤后才统计。
- **未提交按人天**：`not_submitted[]` 为「人 × 日」粒度（同一人部分日期未交也能暴露），`not_submitted_people[]` 为整窗完全没交的人（用于催交）。
- **写入链**：`prepare` 只读回查 + 预览 + 签发 continuation；`apply` 强制 `confirm=true` 与有效 continuation，并校验登录上下文（baseUrl/profile/userId）与目标指纹。写请求发出后网络中断返回 `partial/write_uncertain`，禁止自动重试，只能只读回查。
- **发布防重**：`prepare` 与 `apply` 都会查当日是否已有日报，已存在分别返回 `publish_already_exists` 与 `target_changed`，杜绝重复提交。
- **导出落盘**：先校验父目录存在与目标文件不存在，再以 `wx` 独占写入，避免 check-then-write 竞态覆盖用户文件；响应缺少 `PK` 魔数即判定非 xlsx。

## 已知限制

- 服务端**忽略 `pageNo`**（实测第 1/2/3 页返回同一批数据），`pageSize` 生效且上限 300；返回行数 ≈ 人数 × 天数，因此默认 `page_size=300` 最大化覆盖。若 `_note` 出现，说明单页已顶满、窗口更早部分未覆盖，需缩小日期范围或人员范围（不要靠调大 `page_no`）。
- 按 `userId` 列表批量查个人不可用：`filter.userIds/empIds/userId/creator` 被服务端静默忽略（恒定返回当前登录人且 HTTP 200），查个人统一走 `blog.query_user`。
- 日期区间按天计数、上限 62 天（`blog.query` 与 `blog.query_user` 口径一致），超限返回 `range_too_large`。
- 点赞无只读回查接口，`verified` 恒为 `false`，生效以业务码为准。
- EB 日报应用接口族（ebuilder / secondev）未实现：依赖租户是否启用 EB 日报应用，当前租户未启用。

## 安全边界

- 只调用 `/api/` 前缀路径，拒绝跨域、路径逃逸与协议外地址。
- 认证、Cookie、ETEAMSID、User-Agent 由 `src/internal/e10` 统一托管；业务输入出现 `cookie`/`eteamsid`/`token`/`authorization` 等字段直接报 `field_forbidden`。
- 不读取、不打印认证目录与认证文件。
- 导出为本地文件写入，目标已存在时拒绝覆盖（`output_exists`）。

## 测试

单元测试（manifest 合约、各 operation、host 错误映射）：

Windows PowerShell：

```powershell
npm test -- "test/business/blog-*.test.ts"
```

macOS/Linux（bash/zsh）：

```bash
npm test -- "test/business/blog-*.test.ts"
```

全量回归（含 Skill 布局校验）：

```bash
npm test
```

Skill 质检（安装前必须通过）：

```bash
weaver-work-cli skills validate skills/weaver-e10-blog --json --no-html
```
