Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions api/webprojects/container/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,7 @@
function renderReanchor() {
const old = details.querySelector('[data-reanchor]')
if (old) old.remove()
if (!activeThread || !selectedAnchor || !canManage(activeThread)) return
if (!activeThread || activeThread.status === 'deleted' || !selectedAnchor || !canManage(activeThread)) return
const thread = activeThread, anchor = {...selectedAnchor}
const control = button('用所选位置重新关联', () => mutate('/comment-threads/' + encodeURIComponent(thread.id) + '/reanchor',
{anchor, release_id: settings.releaseId, page_key: page.key, page_path: page.path}, thread.revision, control, openThread), {'data-reanchor': ''})
Expand All @@ -519,18 +519,22 @@
if (mode !== 'comment' || sequence !== detailSequence) return
activeThread = current
details.replaceChildren(element('h3', {}, '讨论 ' + current.id + ' · 事件记录'))
const kinds = {comment: '评论', reply: '回复', resolve: '已解决', reopen: '已重开', reanchor: '重新关联'}
const kinds = {comment: '评论', reply: '回复', resolve: '已解决', delete: '已删除', reopen: '已重开', reanchor: '重新关联'}
for (const event of events) {
const item = element('div', {class: 'event'})
item.append(element('small', {}, (kinds[event.kind] || event.kind) + ' · ' + (event.created_at || '')), renderAuthor(event.actor_user_id, event.actor_application_id, event.actor_name_snapshot, event.actor_user),
element('p', {class: 'body'}, event.body || ''))
details.append(item)
}
const input = element('textarea', {'aria-label': '回复内容', maxlength: '4000'})
const reply = button('提交回复', () => mutate('/comment-threads/' + encodeURIComponent(current.id) + '/replies', {body: input.value.trim()}, null, reply, openThread))
details.append(input, reply)
if (current.status !== 'deleted') {
const input = element('textarea', {'aria-label': '回复内容', maxlength: '4000'})
const reply = button('提交回复', () => mutate('/comment-threads/' + encodeURIComponent(current.id) + '/replies', {body: input.value.trim()}, null, reply, openThread))
details.append(input, reply)
} else {
details.append(element('p', {class: 'notice'}, '讨论已删除;保留事件记录,重新打开后可继续回复。'))
}
if (canManage(current)) {
const action = current.status === 'resolved' ? 'reopen' : 'resolve'
const action = current.status === 'open' ? 'resolve' : 'reopen'
const change = button(action === 'resolve' ? '标记已解决' : '重新打开', () => mutate('/comment-threads/' + encodeURIComponent(current.id) + '/' + action, {}, current.revision, change, openThread))
details.append(change)
}
Expand Down
2 changes: 1 addition & 1 deletion api/webprojects/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func InitRouter() error {
for _, suffix := range []string{"", "/:thread_id", "/:thread_id/events"} {
data.AddRoute(http.MethodGet, "/api/web-share/:id/comment-threads"+suffix, requireModule, comments)
}
for _, suffix := range []string{"", "/:thread_id/replies", "/:thread_id/resolve", "/:thread_id/reopen", "/:thread_id/reanchor"} {
for _, suffix := range []string{"", "/:thread_id/replies", "/:thread_id/resolve", "/:thread_id/delete", "/:thread_id/reopen", "/:thread_id/reanchor"} {
data.AddRoute(http.MethodPost, "/api/web-share/:id/comment-threads"+suffix, requireModule, comments)
}
read := middleware.RequireIdentity("web-projects:read", false)
Expand Down
36 changes: 27 additions & 9 deletions docs/specs/web-container-comments-requirements.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 网页托管增强容器与评论能力需求

状态:已实现,已通过 Pi 隔离环境验收,待合并
状态:基础容器已部署;Skill 上传检查与评论删除已实现,验证范围为静态检查和构建,未执行测试套件

更新时间:2026-09-16

Expand All @@ -14,7 +14,7 @@

1. 访问者能确认当前登录态,并返回系统主页或网页托管页。
2. 已登录且有页面访问权的用户能对整页、文字、图片和组件发起讨论。
3. 评论形成可追溯消息流,支持回复、解决、重开和重新关联。
3. 评论形成可追溯消息流,支持回复、解决、删除、重开和重新关联。
4. 页面重新发布后,仍存在的内容继续显示评论;无法定位的未解决评论集中在页尾提示。

## 2. 核心判断
Expand All @@ -38,7 +38,7 @@

## 3. 用户与权限

| 角色 | 页面可见条件 | 查看评论 | 创建/回复 | 解决、重开、重新关联 |
| 角色 | 页面可见条件 | 查看评论 | 创建/回复 | 解决、删除、重开、重新关联 |
| --- | --- | --- | --- | --- |
| 未登录用户 | 仅公开项目 | 不允许,且不显示入口 | 不允许 | 不允许 |
| 已登录普通用户 | 满足项目 `private/members/authenticated/public` 规则 | 允许 | 允许 | 仅自己创建的主题 |
Expand Down Expand Up @@ -96,6 +96,12 @@
- 交互模块不覆盖透明遮罩,评论从侧栏发起;拖拽、缩放、Marker、信息窗和原按钮继续由页面自身处理。
- 托管页面作为 iframe 子模块加载时不重复注入菜单。

### 5.3 删除与恢复

删除针对整个讨论及回复,状态变为 `deleted` 并追加不可变 `delete` 事件,保留正文用于审计。作者或项目所有者在有项目访问权限时可执行删除、查询已删除历史和重新打开;其他用户对已删除主题详情及事件得到 404。所有操作继续校验用户有效状态、应用 scope、当前项目 ACL 与 revision。

普通列表、标记和页尾不显示已删除讨论;已删除主题不能回复、解决或重新关联。作者或所有者用重开操作恢复为 `open` 后可继续讨论。单条回复独立删除不在当前命令范围内。

## 6. 页面更新与定位

页面应遵循 [HTML 评论结构标准](../../skills/home-server-web-share/references/html-comment-contract.md)。核心标记为:
Expand All @@ -119,13 +125,17 @@

## 7. 应用与 Skill

`home-server-web-share` skill 提供:
`home-server-web-share` Skill 提供以下能力:

- 按状态或 `request_id` 查询评论。
- 查看主题及完整事件分页。
- 创建整页、文字、图片或模块评论。
- 回复、解决、重开和重新关联。
- 生成/修改 HTML 时遵循稳定锚点和交互保护标准。
| 能力 | 行为 |
| --- | --- |
| 上传前检查 | `check-html` 离线检查 HTML/ZIP;`upload` 自动检查发送内容,并读取目标项目实际容器模式决定阻断项 |
| 评论查询 | 按状态或 request_id 查询主题,自动读取完整事件分页;默认 all 排除 deleted,显式 deleted 查询受作者/所有者限制 |
| 创建与回复 | 整页、文字、图片和模块评论,支持用户及 AK/SK 应用 |
| 评论处理 | 解决、删除、重新打开和重新关联;删除保留审计,重开可恢复已删除讨论 |
| HTML 生成约定 | 使用稳定锚点、语义边界和地图等原交互保护,不重写已有业务模块 |

上传检查应报告文件、行号、问题类型、原因和建议,区分托管格式阻断项、增强模式阻断项与待核对警告;有适用阻断项时不发送上传。缺少标记的旧页面只提示保守定位,不自动重写。静态检查不执行 JavaScript,不证明动态 SPA、地图手势或布局兼容已经通过;详细范围见 [上传检查规则](../../skills/home-server-web-share/references/html-upload-check.md)。

每个写操作必须带调用方生成的 `request_id`。同一身份、项目和 `request_id` 的相同请求返回原结果;参数不同返回冲突。连接中断后先按 `request_id` 查询,不自动重试未知结果。

Expand All @@ -143,6 +153,7 @@
- 用户和 AK/SK 应用的回复都显示身份快照;应用代写同时保留用户 ID 与应用 ID。
- 评论、回复、解决、重开和重新关联事件记录调用方实际查看的 release;历史版本必须属于当前项目。
- 解决后可回复、可重新打开;状态操作使用 revision 防止覆盖并发更新。
- 删除后普通列表、标记和页尾隐藏整个讨论;只有有项目访问权的作者/所有者可查询审计并重开恢复,其他用户不能读回正文或回复。
- 同一 `request_id` 的并发重复请求只产生一个事件/主题。

### 8.3 更新与兼容
Expand All @@ -159,6 +170,13 @@
- 后端迁移、构建、API 生命周期、前端测试和真实 Chromium 测试全部通过。
- Pi 使用专用数据库、存储、配置、HTTPS 入口和回环端口完成真实服务验收:私有 404、匿名隐藏、默认浏览、原交互、文字/模块评论、应用回复、解决/重开、跨版本定位、失锚页尾与 Skill 命令链均通过。

### 8.5 Skill 检查与删除验收标准

- 离线检查不读取配置/凭证、不联网、不执行脚本,并输出适用模式、摘要、问题和修正建议。
- 上传检查与实际请求使用相同内容,托管阻断项适用于所有模式,增强阻断项依据目标项目实际模式判断。
- 删除、审计查询和恢复遵循作者/所有者、scope、revision、request_id 规则;追加一个对应事件,不物理删除已有消息。
- 基础容器的历史 Pi 验收结论不代表新增检查与删除生命周期已完成自动化验收;本扩展未运行测试套件。

## 9. 当前边界

- 首版不支持地图内部 POI、经纬度或屏幕坐标评论;评论绑定地图模块整体。
Expand Down
30 changes: 23 additions & 7 deletions docs/specs/web-container-comments-technical-design.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 网页托管增强容器与评论能力技术方案

状态:实现版,已通过 Pi 隔离环境验收
状态:实现版;基础容器已部署,新增 Skill 检查与删除仅完成静态检查和构建,未执行测试套件

更新时间:2026-09-16

Expand Down Expand Up @@ -45,7 +45,7 @@ flowchart LR
| 浏览器运行时 | `api/webprojects/container/runtime.js` | 菜单、模式生命周期、DOM 定位、侧栏和页尾降级 |
| 项目编辑页 | `front_vue/src/views/WebProjectEditor.vue` | 选择增强/原始容器 |
| 应用权限 | `domain/service/applications`、`front_vue/src/views/Applications.vue` | `web-comments:read/write` scope |
| Agent 接口 | `skills/home-server-web-share` | 查询和处理评论、HTML 结构标准 |
| Agent 接口 | `skills/home-server-web-share` | 上传前兼容性检查、查询和处理评论、HTML 结构标准 |

## 3. 请求链路

Expand Down Expand Up @@ -146,12 +146,13 @@ stateDiagram-v2
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| GET | `/api/web-share/{project_id}/view-context` | 页面身份、容器和评论能力;公开页允许匿名 |
| GET | `/api/web-share/{project_id}/comment-threads` | 主题列表;支持 `status=all/open/resolved`、`cursor`、`limit`、`request_id` |
| GET | `/api/web-share/{project_id}/comment-threads` | 主题列表;支持 `status=all/open/resolved/deleted`、`cursor`、`limit`、`request_id`;默认 all 不含 deleted |
| POST | `/api/web-share/{project_id}/comment-threads` | 创建主题 |
| GET | `/api/web-share/{project_id}/comment-threads/{thread_id}` | 主题当前状态 |
| GET | `/api/web-share/{project_id}/comment-threads/{thread_id}/events` | 事件流;支持 `cursor`/`after_seq`、`limit`、`request_id` |
| POST | `/api/web-share/{project_id}/comment-threads/{thread_id}/replies` | 回复,允许已解决主题 |
| POST | `/api/web-share/{project_id}/comment-threads/{thread_id}/resolve` | 解决;要求 `If-Match` |
| POST | `/api/web-share/{project_id}/comment-threads/{thread_id}/delete` | 软删除整个讨论与回复、追加审计;要求 `If-Match` |
| POST | `/api/web-share/{project_id}/comment-threads/{thread_id}/reopen` | 重开;要求 `If-Match` |
| POST | `/api/web-share/{project_id}/comment-threads/{thread_id}/reanchor` | 重新关联;要求 `If-Match` 和当前 release |

Expand Down Expand Up @@ -183,7 +184,7 @@ stateDiagram-v2
- `exact/prefix/suffix/label` 分别最多 4096/128/128/256 个 Unicode 字符。
- 页面路径必须是清理后的项目相对路径,不能包含反斜杠、NUL、查询或片段。
- 创建与重新关联使用的 `release_id` 必须等于当前发布版本;版本变化返回冲突。
- 回复、解决和重开可携带调用方实际查看的 `release_id`,该版本必须属于当前项目;版本文件已淘汰时可由本项目持久化的主题或事件证明归属,旧客户端省略时兼容记录当前版本。
- 回复、解决、删除和重开可携带调用方实际查看的 `release_id`,该版本必须属于当前项目;版本文件已淘汰时可由本项目持久化的主题或事件证明归属,旧客户端省略时兼容记录当前版本。

## 6. 数据模型

Expand Down Expand Up @@ -222,11 +223,11 @@ erDiagram

### 6.1 主题表

`web_comment_thread` 保存列表所需当前状态:当前 release/page/anchor、状态、revision 和作者快照;同时保留 `original_*` 字段用于审计原始位置。状态只有 `open/resolved`。
`web_comment_thread` 保存列表所需当前状态:当前 release/page/anchor、状态、revision 和作者快照;同时保留 `original_*` 字段用于审计原始位置。状态为 `open/resolved/deleted`。`deleted` 隐藏整个讨论和回复,不物理清除数据;现有 VARCHAR 字段可容纳,无需增加 DDL。

### 6.2 事件表

`web_comment_event` 保存 `comment/reply/resolve/reopen/reanchor`。事件不可更新;`sequence` 与写入后的主题 revision 相同,唯一键 `(thread_id, sequence)` 保证顺序。`source_release_id` 记录执行动作时调用方实际查看的页面版本,而不是事务执行时的最新版本。
`web_comment_event` 保存 `comment/reply/resolve/delete/reopen/reanchor`。事件不可更新;`sequence` 与写入后的主题 revision 相同,唯一键 `(thread_id, sequence)` 保证顺序。`source_release_id` 记录执行动作时调用方实际查看的页面版本,而不是事务执行时的最新版本。

幂等唯一键是 `(project_id, actor_user_id, actor_application_id, request_id)`。服务端保存标准输入的 SHA-256:

Expand All @@ -243,16 +244,31 @@ stateDiagram-v2
open --> resolved: resolve + If-Match
resolved --> resolved: reply,revision+1
resolved --> open: reopen + If-Match
open --> deleted: delete + If-Match
resolved --> deleted: delete + If-Match
deleted --> open: reopen + If-Match
open --> open: reanchor + If-Match
resolved --> resolved: reanchor + If-Match
```

回复也递增 revision,防止项目所有者在未看到新回复时用旧 revision 解决主题。状态变更和重新关联要求 `If-Match`;不一致返回冲突。

删除、已删除历史查询和恢复限于作者或项目所有者,仍须通过项目 ACL、有效身份与应用 scope。其他读者对 deleted 主题及事件得到 404。deleted 不接受回复、解决和重新关联;相同 request_id 与 payload 的删除重试仍返回幂等结果。按调用方 request_id 精确查询时可返回其有权读取的已删除主题,避免误判未知写入尚未发生。

## 7. HTML 定位协议

完整规范见 [HTML 评论结构标准](../../skills/home-server-web-share/references/html-comment-contract.md)。当前 schema 版本为 `1`。

### 7.1 上传前静态检查

`skills/home-server-web-share/scripts/html_check.py` 使用标准库 HTMLParser 与 ZIP reader 进行有界离线检查,不提取归档、不执行脚本。`manage.py check-html` 可无配置调用;`upload` 使用 `build_multipart(..., include_content=True)` 返回的实际内容检查,避免检查与发送之间重新读取文件。

检查分为 `hosting` 与 `enhanced` 两类,覆盖 ZIP 入口/路径/白名单/CRC/容量、静态标记版本/唯一性/正文根/类型/保护区、meta CSP、资源地址与交互模块提示。上传在认证前阻断托管错误,认证后 GET 项目实际 `container_mode`,再决定增强错误是否阻断;通过后才发上传。dry-run 的 mode 为 unknown,不查询服务端。

报告含 SHA-256、文件、行号、错误/警告计数与建议;最多输出 200 条明细,计数仍覆盖全部问题。CSP 检查只针对静态 meta,动态脚本、响应头策略、外部 CSS/JS 和 HTML5 DOM 修复仍需运行环境核对。[检查规则](../../skills/home-server-web-share/references/html-upload-check.md)

### 7.2 结构与定位

```html
<html data-hs-comment-schema="1" data-hs-page-id="trip-guide">
<main data-hs-comment-root>
Expand Down Expand Up @@ -318,7 +334,7 @@ stateDiagram-v2
| 运行时单元测试 | page identity、重复原文消歧、跨页面隔离、双身份显示 |
| Chromium 测试 | 默认浏览、匿名隐藏、文字/图片/模块、地图式交互、页尾失锚、窄屏、iframe、生命周期清理 |
| 前端测试与构建 | 容器模式表单、scope 选择、既有页面回归、生产构建 |
| Skill 测试 | 参数边界、自动分页、正文文件、request_id 读回、revision 和 dry-run 脱敏 |
| Skill 验证 | 上传前静态检查、实际内容摘要、参数边界、自动分页、正文文件、request_id 读回、删除/恢复、revision 和 dry-run 脱敏 |
| Pi 隔离验收 | 专用数据库、存储、配置和端口;真实服务验证访问矩阵、发布更新、失锚与原交互 |

完成标准以 [需求文档第 8 节](web-container-comments-requirements.md#8-验收标准) 为准。
Loading
Loading