From f408096611ea35ce088513e151510e854a2cb9ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A7=9A=E7=AB=A0=E6=B5=A9?= Date: Fri, 11 Sep 2026 10:17:03 +0800 Subject: [PATCH] docs: document repository structure and remove stale plan --- ...1656-dobotai-settings-and-admin-console.md | 1257 ----------------- README.md | 41 + 2 files changed, 41 insertions(+), 1257 deletions(-) delete mode 100644 .hermes/plans/2026-08-13_121656-dobotai-settings-and-admin-console.md diff --git a/.hermes/plans/2026-08-13_121656-dobotai-settings-and-admin-console.md b/.hermes/plans/2026-08-13_121656-dobotai-settings-and-admin-console.md deleted file mode 100644 index 01af1ff..0000000 --- a/.hermes/plans/2026-08-13_121656-dobotai-settings-and-admin-console.md +++ /dev/null @@ -1,1257 +0,0 @@ -# DobotAI 设置控制流与管理员配置中心实施计划 - -> **执行方式:** 由当前Hermes主代理严格串行执行;不使用子代理或任务委派;一次只实现、测试和提交一个特性。 - -**目标:** 为DobotAI增加安全的Web初始化后台、首位管理员绑定、独立`/setting`控制流、个人多语言、技术支持群/AI表格对话式绑定、配置总览与组件状态,并在最后支持钉钉Secret轮换及应用切换。 - -**架构:** 将“启动配置、业务配置、个人偏好、设置对话”从问答链路拆出。Client ID和版本化业务配置存PostgreSQL;Client Secret等长期Secret写入权限`0600`的独立文件且永不回显。钉钉消息在进入`qa.Service`前由设置路由拦截,因此设置内容不会创建WeKnora Session、问答Turn、知识缺失Case或AI表格Outbox。 - -**技术栈:** Go 1.25、`net/http`、PostgreSQL/pgx、标准库`html/template`与`embed`、钉钉Stream SDK、现有DingTalk/AI表格客户端。 - ---- - -## 1. 已确认的产品规则 - -1. 钉钉Client ID/AppKey和Client Secret/AppSecret必须先在Web后台配置并验证。 -2. 钉钉凭据未配置或验证失败时,只开放后台Setup;不启动Stream,不允许钉钉问答。 -3. 首位管理员由“后台一次性绑定码→本人私聊机器人→后台确认”产生。 -4. 管理员权限主身份为`tenant_key + staff_id`;`union_id`、姓名、部门仅用于关联和展示。 -5. `/setting`、`/settings`、`/设置`及设置状态中的后续消息属于控制流。 -6. 设置流量不进入`messages`、`turns`、`session_mappings`、WeKnora、覆盖判定、支持Case、问题AI表格或FAQ表格。 -7. 普通用户只能修改个人设置;管理员同时拥有个人设置和系统管理能力。 -8. 技术支持群通过目标群内`@机器人 /setting`绑定;不要求管理员寻找群ID。 -9. 问题表和FAQ表通过发送钉钉表格链接绑定;系统解析ID、验证权限和字段类型后才激活。 -10. 后台显示配置总览和组件状态;任何Secret永不回显。 -11. Secret轮换与更换应用是两个入口;新应用先作为候选验证,正式问答消费者不能双开。 -12. 本计划不执行部署、数据库迁移、Docker、Push或外部真实写入;这些在对应里程碑完成后单独请求确认。 - -## 2. 当前代码基线 - -计划编写时: - -```text -branch: main -HEAD: 2c0ed7c feat: expose runtime build version -main相对origin/main: 领先12个Commit -``` - -当前工作区仍有未提交功能: - -```text -PDF图片出站到Private OSS -钉钉立即ACK与MsgID去重 -WeKnora失效Session自动重建 -``` - -当前架构约束: - -- `cmd/dobotai/main.go`在进程启动时直接构造WeKnora、QA和DingTalk Stream。 -- 钉钉凭据来自环境变量;FAQ、问题表和支持群ID也来自环境变量。 -- `internal/httpapi/server.go`只有JSON API,没有HTML后台。 -- `internal/channels/dingtalk/handler.go`完成基础校验后直接构造`core.QARequest`并调用QA。 -- 数据库最新迁移为`000011`。 - -## 3. 范围分层 - -### 首个可用版本(里程碑M1–M3) - -- 固定当前未提交代码。 -- Setup模式和Web配置后台。 -- 钉钉凭据测试、保存、Stream启停状态。 -- 首位管理员绑定。 -- `/setting`独立路由和“零问答副作用”门禁。 -- 配置总览与组件状态。 - -### 第二批(M4–M6) - -- 个人多语言。 -- 技术支持群绑定。 -- 问题AI表格、FAQ表格链接绑定和字段映射。 - -### 最后实施(M7) - -- 同App ID轮换Secret。 -- 切换为新的钉钉应用。 -- 候选验证、维护窗口、回滚。 - -### 暂不实现 - -- 公网后台。 -- 钉钉OAuth网页登录。 -- 多租户自助开通。 -- 通过聊天设置任何Secret。 -- 自动迁移旧pending Case/Outbox到新目标。 -- 未经确认向真实群发送测试消息或向真实AI表格写测试记录。 - ---- - -# 里程碑M0:固定现有工作区基线 - -## Task 0.1:创建安全基线和忽略规则 - -**目标:** 在开发配置系统前固定当前工作区,避免历史功能与新功能混在同一Commit。 - -**文件:** - -- 修改:`.gitignore` -- 检查:`.deploy/` -- 检查:`backups/` -- 检查:`check-live-dobotai.py` - -**步骤:** - -1. 保存`git status`、`git diff`和未跟踪源码清单到`backups/`。 -2. 确认`.deploy/`、`backups/`、运行二进制和数据库Dump不应进入源码。 -3. 只把确定的部署/备份产物加入`.gitignore`;`check-live-dobotai.py`先分类后决定是否纳入工具目录。 -4. 运行: - -```bash -git diff --check -git status --short -``` - -5. Commit: - -```bash -git commit -m "chore: ignore local deployment artifacts" -``` - -## Task 0.2:提交PDF图片出站 - -**目标:** 单独固定Private OSS图片出站功能。 - -**主要文件:** - -- 创建:`cmd/dobotai/outbound_images.go` -- 创建:`internal/outboundimage/aliyun_oss.go` -- 创建:`internal/outboundimage/publisher.go` -- 创建:`internal/outboundimage/rewriter.go` -- 创建:`internal/outboundimage/*_test.go` -- 修改:`cmd/dobotai/main.go` -- 修改:`cmd/dobotai/main_test.go` -- 修改:`internal/channels/dingtalk/handler.go` -- 修改:`internal/channels/dingtalk/handler_test.go` -- 修改:`.env.example` -- 修改:`go.mod` -- 修改:`go.sum` - -**注意:** `main.go`、`handler.go`和测试文件含多项功能,必须使用`git add -p`逐块暂存,禁止整文件误收异步ACK或Session恢复。 - -**验证:** - -```bash -go test ./internal/outboundimage ./internal/channels/dingtalk ./cmd/dobotai -count=1 -go test -race ./internal/outboundimage ./internal/channels/dingtalk -count=1 -go vet ./internal/outboundimage ./internal/channels/dingtalk ./cmd/dobotai -``` - -**Commit:** - -```bash -git commit -m "feat: publish outbound answer images to private OSS" -``` - -## Task 0.3:提交钉钉立即ACK与MsgID去重 - -**主要文件:** - -- 修改:`internal/channels/dingtalk/stream.go` -- 修改:`internal/channels/dingtalk/stream_test.go` -- 必要时局部修改:`internal/channels/dingtalk/handler.go` -- 必要时局部修改:`internal/channels/dingtalk/handler_test.go` - -**验证:** - -```bash -go test ./internal/channels/dingtalk -count=1 -go test -race ./internal/channels/dingtalk -count=1 -``` - -必须验证: - -```text -Callback立即返回ACK -同MsgID五分钟内只处理一次 -不同MsgID不互相阻塞 -处理失败不会造成并发双答 -``` - -**Commit:** - -```bash -git commit -m "fix: acknowledge DingTalk callbacks before async processing" -``` - -## Task 0.4:提交WeKnora失效Session自动恢复 - -**主要文件:** - -- 修改:`internal/weknora/client.go` -- 修改:`internal/weknora/client_test.go` -- 修改:`internal/qa/service.go` -- 修改:`internal/qa/service_test.go` -- 修改:`internal/store/postgres/store.go` - -**验证:** - -```bash -go test ./internal/weknora ./internal/qa ./internal/store/postgres -count=1 -go test -race ./internal/qa -count=1 -``` - -必须验证只对明确的WeKnora `Session not found`执行一次失效、重建和重试;其他404/网络错误不重试。 - -**Commit:** - -```bash -git commit -m "fix: recreate stale WeKnora sessions once" -``` - -## M0退出条件 - -```text -工作区无未提交生产源码 -现有目标测试全部通过 -每项历史功能有独立Commit -未Push、未部署 -``` - ---- - -# 里程碑M1:配置域、Secret存储与Setup模式 - -## Task 1.1:新增配置域模型 - -**目标:** 定义状态、配置版本和组件状态,不先接UI。 - -**创建:** - -- `internal/settings/types.go` -- `internal/settings/service.go` -- `internal/settings/service_test.go` - -**核心类型:** - -```go -type SystemState string - -const ( - StateUninitialized SystemState = "uninitialized" - StateCredentialsSaved SystemState = "credentials_saved" - StateDingTalkVerified SystemState = "dingtalk_verified" - StateWaitingAdmin SystemState = "waiting_first_admin" - StateActive SystemState = "active" - StateCredentialError SystemState = "credential_error" -) - -type ComponentState string - -const ( - ComponentOK ComponentState = "ok" - ComponentWarning ComponentState = "warning" - ComponentError ComponentState = "error" - ComponentUnconfigured ComponentState = "unconfigured" - ComponentDisabled ComponentState = "disabled" -) -``` - -**TDD:** - -- 无凭据→`uninitialized`。 -- 完整但未验证→`credentials_saved`。 -- 凭据验证且管理员数为0→`waiting_first_admin`。 -- 凭据验证且存在有效管理员→`active`。 -- Secret和Client ID不完整→`credential_error`,不能误报Active。 - -**验证:** - -```bash -go test ./internal/settings -count=1 -``` - -**Commit:** `feat: add settings lifecycle domain` - -## Task 1.2:新增配置数据库迁移 - -**创建:** - -- `db/migrations/000012_system_settings.up.sql` -- `db/migrations/000012_system_settings.down.sql` -- `db/migrations/000012_system_settings_contract_test.go` - -**表:** - -```text -system_config_revisions -system_resource_bindings -administrators -config_audit_events -``` - -建议字段: - -```sql -system_config_revisions( - id TEXT PRIMARY KEY, - tenant_key TEXT NOT NULL, - kind TEXT NOT NULL, - revision INTEGER NOT NULL, - state TEXT NOT NULL, - public_config JSONB NOT NULL, - secret_ref TEXT NOT NULL DEFAULT '', - validation_status TEXT NOT NULL, - validation_code TEXT NOT NULL DEFAULT '', - validated_at TIMESTAMPTZ, - activated_at TIMESTAMPTZ, - created_by_staff_id TEXT NOT NULL DEFAULT '', - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - UNIQUE(tenant_key, kind, revision) -); -``` - -`public_config`只允许标识符、开关和非Secret选项。数据库约束/仓库测试必须拒绝把`client_secret`、`access_token`、`api_key`等键写入`public_config`。 - -管理员唯一约束: - -```sql -UNIQUE(tenant_key, staff_id) -``` - -**验证:** - -```bash -go test ./db/migrations -run 'Test.*012' -count=1 -``` - -**Commit:** `feat: add versioned system configuration schema` - -## Task 1.3:实现PostgreSQL配置仓库 - -**创建:** - -- `internal/store/postgres/settings.go` -- `internal/store/postgres/settings_test.go` - -**接口:** - -```go -type Repository interface { - CurrentConfig(context.Context, string, ConfigKind) (ConfigRevision, error) - CreateDraft(context.Context, ConfigRevision) error - MarkValidated(context.Context, string, ValidationResult) error - Activate(context.Context, string) error - CountActiveAdministrators(context.Context, string) (int, error) - AddAdministrator(context.Context, Administrator) error - RecordAudit(context.Context, AuditEvent) error -} -``` - -激活必须在事务中完成:同一`tenant_key + kind`最多一个active版本。 - -**验证:** - -```bash -go test ./internal/store/postgres -run 'Test.*Settings|Test.*Config' -count=1 -``` - -**Commit:** `feat: persist versioned system settings` - -## Task 1.4:实现权限0600的Secret文件存储 - -**创建:** - -- `internal/secretstore/file.go` -- `internal/secretstore/file_test.go` - -**默认目录:** - -```text -~/.config/dobotai/secrets/ -``` - -**规则:** - -- 目录`0700`,单个Secret文件`0600`。 -- 使用临时文件、`fsync`和原子`rename`。 -- API只允许`Put`、`Configured`、`Use`、`Delete`;不提供面向HTTP的明文读取返回。 -- 错误和日志不能包含Secret值。 -- `secret_ref`保存随机文件名/版本引用,不保存Secret。 -- 测试以临时目录运行,不读取真实Secret。 - -**验证:** - -```bash -go test ./internal/secretstore -count=1 -go test -race ./internal/secretstore -count=1 -``` - -**Commit:** `feat: add atomic file-backed secret storage` - -## Task 1.5:解耦进程启动与钉钉通道启动 - -**创建:** - -- `cmd/dobotai/runtime.go` -- `cmd/dobotai/runtime_test.go` - -**修改:** - -- `cmd/dobotai/main.go` -- `internal/httpapi/server.go` -- `internal/httpapi/server_test.go` - -**目标结构:** - -```text -进程启动 -→ PostgreSQL和配置服务 -→ HTTP后台始终启动 -→ 根据配置状态: - setup:不构造WeKnora/QA/Stream/Outbox - active:构造完整运行组件 -``` - -不能让`httpapi.NewWithOptions`在Setup模式强制要求QA服务。建议将管理后台与QA API拆成独立Handler并挂到同一Mux: - -```text -/internal/adminweb -/internal/httpapi -``` - -Setup状态下: - -```text -GET /healthz 200,标明setup -GET /admin/setup 200 -POST /v1/qa 503 channel_not_configured -DingTalk Stream 未创建 -问题表/FAQ表/支持群Poller 未启动 -``` - -**验证:** - -```bash -go test ./cmd/dobotai ./internal/httpapi -count=1 -go test -race ./cmd/dobotai -count=1 -``` - -**Commit:** `refactor: support setup-only runtime mode` - -## Task 1.6:实现钉钉凭据验证器 - -**创建:** - -- `internal/dingtalkconfig/verifier.go` -- `internal/dingtalkconfig/verifier_test.go` - -**行为:** - -1. 使用候选Client ID/Secret调用钉钉Token接口。 -2. 使用短超时,限制响应大小。 -3. 返回结构化结果:`oauth_ok`、稳定错误码、验证时间。 -4. 不返回、不记录Access Token。 -5. 第一步只验证OAuth;Stream连接验证在运行管理器中单独记录,避免Token成功被误报为全通道成功。 - -**测试:** 使用`httptest.Server`覆盖成功、错误凭据、限流、超时、超大响应和脱敏错误。 - -**Commit:** `feat: verify DingTalk application credentials` - ---- - -# 里程碑M2:Web Setup、首位管理员和配置总览 - -## Task 2.1:建立管理员Web安全边界 - -**创建:** - -- `internal/adminweb/server.go` -- `internal/adminweb/auth.go` -- `internal/adminweb/auth_test.go` - -**规则:** - -- 只允许HTTP监听地址为loopback;复用并泛化`httpapi.ValidateLoopbackListenAddress`。 -- Setup使用本机生成的Bootstrap Token;token文件`0600`。 -- 正常管理员后台首版复用`DOBOTAI_ADMIN_TOKEN`,使用HttpOnly、SameSite=Strict、短时会话Cookie;不把Token放URL。 -- 所有后台响应`Cache-Control: no-store`。 -- 所有修改请求要求CSRF Token。 -- 管理员建立后,Bootstrap权限立即失效。 - -**测试:** 非loopback拒绝、无Token拒绝、CSRF失败拒绝、Cookie安全属性、Bootstrap失效。 - -**Commit:** `feat: secure loopback admin web access` - -## Task 2.2:实现Setup页面 - -**创建:** - -- `internal/adminweb/templates/setup.html` -- `internal/adminweb/templates/layout.html` -- `internal/adminweb/assets/admin.css` -- `internal/adminweb/setup.go` -- `internal/adminweb/setup_test.go` - -**页面:** - -```text -步骤1:输入Client ID和Client Secret -步骤2:测试凭据 -步骤3:保存候选配置 -步骤4:建立Stream验证通道 -步骤5:绑定首位管理员 -``` - -**安全:** - -- HTML中绝不能出现已保存Secret。 -- 编辑页面只显示`Client Secret:已配置`。 -- 表单提交体不得进入请求日志。 -- 凭据验证失败时不激活。 - -**Commit:** `feat: add DingTalk setup wizard` - -## Task 2.3:实现首位管理员绑定码 - -**创建迁移:** - -- `db/migrations/000013_admin_bootstrap.up.sql` -- `db/migrations/000013_admin_bootstrap.down.sql` -- `db/migrations/000013_admin_bootstrap_contract_test.go` - -**创建代码:** - -- `internal/settings/bootstrap.go` -- `internal/settings/bootstrap_test.go` -- `internal/store/postgres/admin_bootstrap.go` - -**表/状态:** - -```text -admin_bootstrap_challenges -- id -- tenant_key -- code_hash -- expires_at -- attempts -- max_attempts -- candidate_staff_id -- candidate_union_id -- candidate_display_name -- candidate_department -- status: pending/candidate_detected/confirmed/expired/locked -``` - -**规则:** - -- 6位随机码,10分钟有效,只保存哈希。 -- 生成新码使旧码失效。 -- 最多5次失败。 -- 只接受私聊;群内绑定码拒绝。 -- 钉钉回调只登记候选人,不立即授权。 -- 后台确认后创建`super_admin`并让码永久失效。 - -**Commit:** `feat: add first administrator bootstrap challenge` - -## Task 2.4:实现配置总览和组件状态页 - -**创建:** - -- `internal/status/service.go` -- `internal/status/service_test.go` -- `internal/adminweb/status.go` -- `internal/adminweb/templates/status.html` -- `internal/adminweb/status_test.go` - -**第一版组件卡:** - -```text -运行版本 -PostgreSQL -钉钉OAuth -钉钉Stream -管理员 -WeKnora -问题表Outbox -FAQ Outbox -技术支持通知 -Private OSS图片 -``` - -**每个组件只返回:** - -```text -状态、稳定错误码、最后成功时间、最后检查时间、非敏感摘要 -``` - -禁止返回Secret、Token、完整签名URL和原始上游错误正文。 - -**状态来源:** - -- 静态配置:配置仓库。 -- Stream:运行管理器内存状态和最后事件。 -- Outbox:PostgreSQL聚合计数。 -- 外部组件:短超时健康探针;页面加载不应长时间阻塞,允许显示最近缓存结果。 - -**Commit:** `feat: add configuration and component status dashboard` - -## M2退出条件 - -```text -无钉钉凭据时后台可用且Stream未启动 -错误凭据不能进入Active -正确凭据可进入waiting_first_admin -首位管理员必须经钉钉本人+后台双确认 -后台不回显任何Secret -``` - ---- - -# 里程碑M3:独立/setting控制流 - -## Task 3.1:实现严格命令解析器 - -**创建:** - -- `internal/settingscommand/parser.go` -- `internal/settingscommand/parser_test.go` - -**接受:** - -```text -/setting -/settings -/设置 -/setting 123456 -``` - -**兼容:** `/settiing`只返回“是否要打开/setting”的提示,不直接执行敏感操作。 - -**拒绝误触发:** - -```text -如何设置机器人? -请回答/setting是什么意思 -abc /setting -``` - -**Commit:** `feat: parse explicit settings commands` - -## Task 3.2:在QA之前增加设置路由 - -**修改:** - -- `internal/channels/dingtalk/handler.go` -- `internal/channels/dingtalk/handler_test.go` - -**创建:** - -- `internal/settingscommand/router.go` -- `internal/settingscommand/router_test.go` - -**接口:** - -```go -type Router interface { - Match(context.Context, IncomingSettingMessage) (bool, error) - Handle(context.Context, IncomingSettingMessage) error -} -``` - -`HandlerOptions`增加`SettingsRouter`。处理顺序固定: - -```text -群聊未@过滤 -→ 基础回调身份/回复目标校验 -→ settings.Match - ├─ true:settings.Handle并return - └─ false:解析普通消息并调用QA -``` - -图片和富文本中的命令首版不触发设置,仅纯文本触发。 - -**核心门禁测试:** 设置命令命中后: - -```text -QAAsker调用次数=0 -ImageDownloader调用次数=0 -OutboundRewriter调用次数=0 -Card问答Sender调用次数=0 -``` - -**Commit:** `feat: route settings commands before QA` - -## Task 3.3:新增设置会话迁移与状态机 - -**创建:** - -- `db/migrations/000014_setting_sessions.up.sql` -- `db/migrations/000014_setting_sessions.down.sql` -- `db/migrations/000014_setting_sessions_contract_test.go` -- `internal/settingssession/service.go` -- `internal/settingssession/service_test.go` -- `internal/store/postgres/setting_sessions.go` - -**字段:** - -```text -tenant_key -staff_id -conversation_id -conversation_type -current_step -pending_action -draft_config JSONB -expires_at -updated_at -``` - -**规则:** - -- 10分钟过期。 -- 同一管理员同一时刻最多一个活动流程。 -- `取消`立即删除草稿;`上一步`退回已定义步骤。 -- 只保存结构化草稿,不保存完整消息、完整链接或Secret。 - -**Commit:** `feat: add expiring settings conversation state` - -## Task 3.4:证明设置流量零问答持久化 - -**创建:** - -- `internal/channels/dingtalk/settings_isolation_test.go` -- `internal/store/postgres/settings_isolation_contract_test.go` - -**测试矩阵:** - -```text -普通用户私聊/setting -管理员私聊/setting -群内管理员@机器人 /setting -/setting 绑定码 -设置状态中的“中文” -设置状态中的AI表格链接 -取消 -超时后的普通问题 -``` - -**每个设置消息断言:** - -```text -messages +0 -turns +0 -session_mappings +0 -coverage_decisions +0 -support_cases +0 -question_table_exports +0 -faq export状态无变化 -WeKnora CreateSession/AgentChat调用 +0 -``` - -超时后不符合显式命令的文本重新按普通问答处理,防止设置状态永久吞消息。 - -**Commit:** `test: enforce settings traffic isolation from QA` - ---- - -# 里程碑M4:个人多语言与系统默认语言 - -## Task 4.1:新增用户偏好数据模型 - -**创建:** - -- `db/migrations/000015_user_preferences.up.sql` -- `db/migrations/000015_user_preferences.down.sql` -- `db/migrations/000015_user_preferences_contract_test.go` -- `internal/preferences/service.go` -- `internal/preferences/service_test.go` -- `internal/store/postgres/preferences.go` - -**唯一键:** - -```sql -UNIQUE(tenant_key, channel, external_actor_id) -``` - -**首版语言:** - -```text -auto -zh-CN -en -ja -ko -es -``` - -管理员与普通用户使用同一偏好表;管理员角色不覆盖个人语言。 - -**Commit:** `feat: persist per-user language preferences` - -## Task 4.2:实现普通用户/管理员设置菜单 - -**修改:** - -- `internal/settingscommand/router.go` -- `internal/settingscommand/router_test.go` - -**行为:** - -- 普通用户:只显示个人语言。 -- 管理员:显示“我的设置”和“系统管理”。 -- 群内个人设置引导到私聊,避免群内刷卡。 -- 系统默认语言只有管理员可改。 - -**Commit:** `feat: add role-aware settings menus` - -## Task 4.3:把语言偏好传入WeKnora - -**修改:** - -- `internal/core/types.go` -- `internal/channels/dingtalk/handler.go` -- `internal/qa/service.go` -- 对应测试文件 - -**推荐实现:** 不污染原始审计问题文本。`turn.Question`继续保存用户原文;仅在发往WeKnora的`AgentChatRequest.Query`构造阶段附加受控语言指令。 - -**优先级:** - -```text -本次显式语言要求 -> 用户个人语言 -> 群默认语言 -> 系统默认语言 -> auto跟随问题语言 -``` - -“本次显式语言要求”首版只识别明确、有限模式,避免模型猜测覆盖用户偏好。 - -**验证:** 审计问题仍为原文,WeKnora请求包含语言约束,设置流量仍不进入QA。 - -**Commit:** `feat: apply user language preference to answers` - ---- - -# 里程碑M5:技术支持群对话式绑定 - -## Task 5.1:扩展资源绑定模型 - -**修改:** - -- `internal/settings/types.go` -- `internal/store/postgres/settings.go` -- 对应测试 - -**资源类型:** - -```text -support_group -question_table -faq_table -card_template -``` - -每个业务对象/Outbox必须能记录使用的配置版本,避免换目标后旧pending任务静默改道。 - -**默认策略:** - -```text -新Case使用新支持群 -旧Case继续使用创建时的绑定版本 -``` - -**Commit:** `feat: version external resource bindings` - -## Task 5.2:实现群内绑定流程 - -**修改:** - -- `internal/settingscommand/router.go` -- `internal/channels/dingtalk/handler.go` -- 对应测试 - -**流程:** - -```text -管理员在群内@机器人 /setting -→ 验证tenant_key + staff_id管理员权限 -→ 从回调读取ConversationID和ConversationTitle -→ 展示“设为技术支持群” -→ 二次确认 -→ 创建待验证绑定 -``` - -普通用户执行同样操作只能收到私聊个人设置提示。 - -**Commit:** `feat: bind support group from DingTalk conversation` - -## Task 5.3:实现显式测试发送与激活 - -**修改:** - -- `internal/channels/dingtalk/group_sender.go` -- `internal/status/service.go` -- 设置路由和测试 - -**规则:** - -- 只有管理员点击“发送测试消息”才有外部副作用。 -- 测试成功后才能激活。 -- 失败只记录稳定错误码,不保存Webhook/Token/原始上游响应。 - -**集成验收:** 需要主人明确允许向目标群发送一条标记为测试的消息。 - -**Commit:** `feat: verify and activate support group binding` - ---- - -# 里程碑M6:问题AI表格与FAQ表格链接绑定 - -## Task 6.1:实现安全链接解析器 - -**创建:** - -- `internal/notablebinding/link.go` -- `internal/notablebinding/link_test.go` - -**输出:** - -```text -base_id / dentry_id -sheet_id -view_id -可选record_id(只用于验证,不作为绑定目标) -``` - -**规则:** - -- 仅接受审核过的钉钉域名和HTTPS。 -- 限制链接长度。 -- 不跟随任意重定向。 -- 解析后立即丢弃完整原始链接。 -- 日志和审计只记录脱敏资源摘要。 - -**Commit:** `feat: parse DingTalk AI Table resource links` - -## Task 6.2:实现表结构读取和逻辑字段映射 - -**创建:** - -- `internal/notablebinding/schema.go` -- `internal/notablebinding/schema_test.go` - -**修改:** - -- `internal/questiontable/client.go` -- 对应客户端测试 - -**规则:** - -- 按“字段显示名+字段类型”匹配,不能只按名字。 -- 缺失、重复、类型错误都阻止激活。 -- 映射保存为配置版本中的逻辑字段→Provider字段ID。 -- 问题表9字段和FAQ表4字段分别校验。 - -**Commit:** `feat: validate and map AI Table schemas` - -## Task 6.3:实现问题表绑定流程 - -**修改:** - -- `internal/settingscommand/router.go` -- `cmd/dobotai/question_table_sync.go` -- `cmd/dobotai/main.go` -- 对应测试 - -**流程:** - -```text -管理员私聊/setting -→ 绑定问题AI表格 -→ 发送任意记录链接 -→ 解析ID并丢弃原链接 -→ 当前管理员作为候选Operator -→ 读权限和字段校验 -→ 展示映射 -→ 经确认执行测试写入/回读 -→ 激活配置版本 -``` - -真实写入测试必须由主人单独确认。 - -**Outbox策略:** 激活前不启动Dispatcher;切换后旧pending记录保留原配置版本,若旧代码尚无该字段则先迁移补充。 - -**Commit:** `feat: bind question AI Table through settings` - -## Task 6.4:实现FAQ表绑定流程 - -**修改:** - -- `internal/settingscommand/router.go` -- `cmd/dobotai/faq_workflow.go` -- `cmd/dobotai/main.go` -- 对应测试 - -**额外验证:** `RecordLinkBuilder`生成的“去回答”链接可打开正确表/视图,且不依赖测试行作为永久目标。 - -**Commit:** `feat: bind FAQ AI Table through settings` - -## Task 6.5:将环境变量配置迁移为数据库配置 - -**修改:** - -- `.env.example` -- `cmd/dobotai/main.go` -- `cmd/dobotai/faq_workflow.go` -- `cmd/dobotai/question_table_sync.go` -- `cmd/dobotai/support_notifications.go` -- 对应测试 - -**迁移策略:** - -1. 首次启动发现数据库无绑定、旧环境变量完整时,后台显示“检测到旧配置”。 -2. 不自动激活;管理员在后台执行导入和验证。 -3. 验证通过后生成数据库配置版本。 -4. 部署完成后再从生产`.env`删除旧标识配置。 -5. Secret仍从SecretStore读取,不进入数据库。 - -**Commit:** `refactor: load DingTalk resource bindings from database` - ---- - -# 里程碑M7:Secret轮换与钉钉应用切换 - -## Task 7.1:实现运行组件管理器 - -**创建:** - -- `internal/runtime/manager.go` -- `internal/runtime/manager_test.go` - -**职责:** - -```text -StartActiveConfig -EnterMaintenance -Drain当前请求(最长30秒) -StopStream -StartStream -Start/Stop问题表、FAQ表和支持通知Poller -ExposeComponentStatus -Rollback -``` - -所有状态转换串行化;同一时刻最多一个正式Stream消费者。 - -**Commit:** `refactor: manage runtime components through lifecycle manager` - -## Task 7.2:实现同Client ID的Secret轮换 - -**流程:** - -```text -输入新Secret -→ 使用候选Secret验证OAuth -→ 保存候选Secret文件 -→ 进入维护窗口 -→ 停旧Stream -→ 激活新Secret引用 -→ 启新Stream -→ 健康检查 -→ 成功后保留旧Secret 24小时回滚 -``` - -失败时自动恢复旧Secret和旧Stream。 - -**Commit:** `feat: rotate DingTalk client secret safely` - -## Task 7.3:实现候选应用验证模式 - -**规则:** - -- 新Client ID+Secret保存为candidate。 -- 候选Stream只能处理`/setting verify`和绑定验证,不能处理普通问答。 -- 校验OAuth、Stream、通讯录、卡片模板、支持群、问题表和FAQ表权限。 -- 有外部副作用的群消息/测试行必须显式确认。 - -**Commit:** `feat: validate candidate DingTalk application` - -## Task 7.4:实现应用切换和回滚 - -**流程:** - -```text -影响预览 -→ 管理员输入确认词 -→ 30秒维护窗口 -→ Drain旧请求 -→ 停旧正式Stream -→ 激活候选配置 -→ 启新正式Stream -→ 自动无副作用验收 -→ 成功/失败自动回滚 -``` - -**强制规则:** - -```text -旧正式Stream未停止前,新Stream不得处理普通问答 -新应用Stream/管理员识别/基础问答失败→整体回滚 -群/表格/卡片验证失败→默认禁止切换,不带病激活 -旧Secret 24小时后删除,只保留脱敏审计 -``` - -**Commit:** `feat: switch DingTalk applications with rollback` - ---- - -# 4. 全局测试与验收矩阵 - -## 每个Task后 - -只运行目标测试,不反复全仓读取或全量测试。 - -```bash -go test -count=1 -go test -race -count=1 -go vet -git diff --check -``` - -## 每个里程碑后 - -```bash -go test ./cmd/dobotai ./internal/httpapi ./internal/adminweb ./internal/settings ./internal/settingscommand ./internal/store/postgres ./db/migrations -count=1 -go build -o /tmp/dobotai-next ./cmd/dobotai -rm -f /tmp/dobotai-next -``` - -只有主人明确允许时才执行全仓测试。 - -## 关键验收 - -### Setup模式 - -```text -DingTalk凭据为空:服务启动,后台可访问,Stream=未启动 -部分凭据:后台显示错误,Stream=未启动 -错误凭据:不能激活 -正确凭据:OAuth通过,等待Stream/管理员绑定 -``` - -### Secret安全 - -```text -数据库无Secret明文 -HTML无Secret回显 -日志无Secret/Token -Secret目录0700、文件0600 -替换Secret不留下无限历史副本 -``` - -### 设置隔离 - -```text -/setting及后续设置消息: -messages/turns/session_mappings/coverage/support_cases/AI表格Outbox增量全部为0 -WeKnora调用次数为0 -``` - -### 权限 - -```text -普通用户不能改系统配置 -管理员拥有个人语言设置 -首位管理员不能由“先到先得”产生 -群绑定必须校验当前操作者是管理员 -``` - -### 应用切换 - -```text -任意时刻正式Stream消费者≤1 -切换失败恢复旧配置 -回滚后旧机器人继续正常问答 -无重复Turn和重复群通知 -``` - -# 5. 预计文件变化汇总 - -**新增包:** - -```text -internal/settings/ -internal/secretstore/ -internal/dingtalkconfig/ -internal/adminweb/ -internal/status/ -internal/settingscommand/ -internal/settingssession/ -internal/preferences/ -internal/notablebinding/ -internal/runtime/ -``` - -**新增迁移:** - -```text -000012_system_settings -000013_admin_bootstrap -000014_setting_sessions -000015_user_preferences -后续如需Outbox配置版本字段,再新增000016,禁止回改已发布迁移 -``` - -**重点修改:** - -```text -cmd/dobotai/main.go -cmd/dobotai/faq_workflow.go -cmd/dobotai/question_table_sync.go -internal/httpapi/server.go -internal/channels/dingtalk/handler.go -internal/core/types.go -internal/qa/service.go -internal/questiontable/client.go -internal/store/postgres/*.go -.env.example -``` - -# 6. 风险与控制 - -1. **当前工作区混杂三项未提交功能。** 先完成M0,使用`git add -p`拆分,禁止直接开始设置功能。 -2. **Setup模式与当前强制初始化冲突。** 先建立运行状态机,再做页面,避免HTML先行但进程仍无法无凭据启动。 -3. **动态切换涉及多个长期goroutine。** 到M7才引入统一运行管理器;首版不提前做热切换。 -4. **设置消息泄漏到问答链路。** M3以数据库增量和Mock调用次数双重合同测试锁死。 -5. **AI表格字段ID属于具体表。** 每次绑定都读取Schema并保存版本映射,禁止沿用旧字段ID。 -6. **切换资源后旧Outbox改道。** 每条Case/Outbox固定配置版本;不静默改发。 -7. **后台认证首版依赖本地Token。** 因后台只监听loopback且通过SSH隧道访问,首版可接受;公网或多管理员Web登录另立需求。 -8. **真实验证有外部副作用。** 发送群测试消息、AI表格写测试行、部署和迁移均需主人单独确认。 - -# 7. 开始执行时的第一批操作 - -只执行M0,不同时开始M1: - -```text -1. 重新核对当前diff归属 -2. 建立合并前备份 -3. 提交忽略规则 -4. 拆分并提交图片出站 -5. 拆分并提交异步ACK/去重 -6. 拆分并提交Session恢复 -7. 跑目标测试和构建 -8. 汇报Commit与工作区状态 -9. 等主人确认后进入M1 -``` - -每个阶段都遵循: - -```text -先写失败测试 -→ 确认失败原因正确 -→ 最小实现 -→ 目标测试通过 -→ 竞态/静态检查 -→ 独立Commit -→ 不Push、不部署 -``` diff --git a/README.md b/README.md index b3fd777..a3a7ae3 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,45 @@ DobotAI 是 WeKnora 的企业外部渠道集成层,负责钉钉协议、身份 - **DobotAI PostgreSQL**:只保存渠道与业务编排数据,不保存 WeKnora 知识正文。 - **Redis**:仅用于队列、重试、幂等租约和短期状态。 +## 目录结构 + +```text +. +├── cmd/ +│ ├── dobotai/ 主程序入口、运行时装配与后台 Worker +│ └── directory-import/ 钉钉通讯录导入命令 +├── internal/ +│ ├── channels/dingtalk/ 钉钉 Stream、卡片、媒体、反馈与群通知 +│ ├── qa/ 问答主流程、WeKnora 调用和检索信号记录 +│ ├── weknora/ SSE、工具结果和引用解析 +│ ├── coverage/ 覆盖判定、Support Case 和人工提醒 +│ ├── reasoningmode/ /think 推理模式 +│ ├── settings/ /setting 与首位管理员绑定 +│ ├── outboundimage/ resource:// 图片私有 OSS 外发 +│ ├── questiontable/ 问题表工作流 +│ ├── faqtable/ FAQ 表工作流 +│ ├── directory/ 钉钉通讯录查询与导入 +│ ├── audit/ 审计查询 +│ ├── httpapi/ /healthz、/version、/v1/qa 与审计 API +│ ├── core/ 核心领域类型 +│ ├── session/ 会话键和会话标题 +│ ├── agentbinding/ 渠道到 WeKnora Agent 的绑定 +│ ├── feedback/ 点赞/点踩领域逻辑 +│ ├── secretstore/ 本地密钥引用存储 +│ ├── dingtalkconfig/ 钉钉凭据校验 +│ ├── runtime/ 运行时管理 +│ └── store/postgres/ PostgreSQL 持久化实现 +├── db/migrations/ SQL 迁移及其契约测试 +├── deploy/dobotai-dingtalk/ 容器 Compose、安装/配置/启动/绑定脚本和部署手册 +├── docs/ 架构说明与 API 边界文档 +├── scripts/ 本地部署与部署资产契约测试 +├── Dockerfile DobotAI 容器镜像构建 +├── .env.example 无真实凭据的配置示例 +└── README.md 项目功能、边界和开发说明 +``` + +目录职责规则:`cmd/`只装配进程,`internal/`承载业务模块,`internal/store/postgres/`只实现数据库持久化,`db/migrations/`只放Schema变更,`deploy/`只放容器运行资产。历史计划、备份、镜像归档和真实环境文件均不进入Git。 + ## 钉钉 Stream 使用官方 `dingtalk-stream-sdk-go`,不暴露公网回调入口。 @@ -87,6 +126,7 @@ dobotai bootstrap-admin - 有结构化检索引用的回答标记为已覆盖; - 没有引用但已执行检索的回答标记为未覆盖; +- 没有引用且未执行检索的文本标记为 `covered_non_knowledge_response`,不创建 Support Case; - 纯图片视觉识别单独记录为 `covered_visual_recognition`,不计入“未覆盖”; - 无权威知识时,产品统一回复:`当前暂无已发布的权威知识,暂不建议操作。`; - 人工通知只带当前问题和同一用户此前最多5个问题,不包含回答、引用或置信度; @@ -128,6 +168,7 @@ DobotAI 使用独立 PostgreSQL,禁止指向 WeKnora 数据库。生产部署 000001_core ... 000018_coverage_visual_recognition_reason 000019_reasoning_mode_state 000020_feedback_support_trigger +000021_coverage_non_knowledge_response_reason ``` 迁移前应执行逻辑备份并验证恢复;迁移、二进制部署和钉钉渠道切换是独立步骤,不能把健康检查代替真实渠道验收。 -- 2.52.0