# Project Room(项目群聊) 一个项目 = 一个持续存在的群聊。用户、主代理、前端、后端、测试、运维在同一条群消息时间线上沟通,同时各自拥有**真实独立**的 Harness 会话、自己的工作上下文和真实的执行能力。 这不是套壳聊天界面:群里的每条投递都落到真实会话,任务状态只由真实执行与证据驱动,模型按成员独立路由。 - English: [`README.md`](./README.md) --- ## 1. 安装与启用 插件是**零运行时依赖**的纯 ESM 包:宿主半边只 import Node 内置模块,浏览器半边是一段自注册的 classic script,因此不需要构建工具链,也不需要在 profile 里额外安装任何包。 先克隆本仓库(`git clone https://github.com/duxingxiake3/dsh-project-room.git`),再在 Harness 的插件管理里安装克隆出的目录 ``;之后 profile 会得到一条 `link:` 依赖和一条 loader 行: ```yaml - insert: - id: project-room name: 'dsh-project-room' ``` > 注意写法:**`- insert:` 才会创建 loader 行**(行 id 与包名是两件事)。写成 `- id: project-room` 不会加载任何东西——这一点是在隔离 profile 里实测出来的。 启用成功后会写出两份可复核的记录: | 文件 | 内容 | |---|---| | `/storages/project-room/boot-report.json` | 解析到的核心版本、**解析来源文件路径**(`coreResolvedFrom`)、已注册工具名、当前上下文可用的工具名、能力标志、每次激活的历史 | | `/storages/project-room/boot-trace.jsonl` | 逐阶段的激活轨迹(一行一个 JSON)。进程若在启动中死掉,最后一行就是它走到哪一步 | `boot-report.json` 里的版本取自**运行进程自己的解析结果**,不是读某个路径猜的;同时安装多套运行时,也能看出实际执行的是哪一套。 ### 停用与卸载 - **停用**:插件管理里关闭 `project-room`。 - **卸载**:插件管理里移除 `dsh-project-room`。**删除插件不删除项目历史**——群、消息、任务、决定、成果都留在该目录的 `rooms.sqlite` 里,重装即可继续。 - 彻底清理是单独操作:关掉插件后删除上述目录。 - 插件**不会销毁借用的用户会话**(见第 6 节)。 --- ## 2. 建群 左侧栏「项目群」图标 → **新建项目群**。 | 字段 | 说明 | |---|---| | 群名称 | 任意,例如「验证码登录功能」 | | 工作区目录 | 成员真实执行任务的工作目录(成员会话的 `cwd`)。可留空后补 | | 整体目标 | 交给主代理的目标。提交后主代理会拆分工作、安排成员并推进 | | 初始成员 | 勾选主代理 / 前端 / 后端 / 测试 / 运维 / 设计 / 文档;也可只建主代理,之后再按需补齐 | | 每位成员的模型 | 每位成员单独选模型,或留空走角色/项目默认。选项来自宿主真实已接入的提供方与模型 ID | 建群在**一个事务**里写完群、成员与 `room.created` 事件,然后才创建成员会话。中断不会留下半个团队;成员身份先落盘,恢复时是核对而不是重建一个人。 --- ## 3. 每位成员独立选模型 四层配置,优先级从低到高: | 层级 | 位置 | 说明 | |---|---|---| | 项目默认 | 群设置 → 模型 | 没有单独配置的成员使用 | | 角色默认 | 群设置 → 模型 → 角色默认 | 按 frontend/backend/tester/devops 预设 | | 成员专属 | 成员详情 → 模型 | 指定到具体 memberId,优先级最高 | | 允许池自动选型 | 群设置 → 选型模式 | 用户启用后,主代理在**允许池内**选型并公开理由;不覆盖被锁定的成员 | 默认 `fixed`:由用户固定每位成员,不会偷偷换成别的模型。 **切换语义(安全边界)**:在成员详情选好模型点「应用模型」后,当前正在进行的请求沿用旧路由,**下一次请求**使用新模型;成员身份、头像、昵称、`@` 名称与任务归属全部不变,也不重建会话、不重复工具副作用。每次切换记录配置版本与原因,旧结果保留旧来源。 「实际路由」一行显示的是**真实请求时观测到的**提供方/模型,而不是配置里写了什么——只有真正跑过一次才会出现。 模型标识必须写成宿主已接入的 `提供方/模型ID`。口头品牌名(「用 GLM 吧」)不会被当作模型 ID;目录读取失败或凭据失效的提供方显示为不可用并给出原因,而不是从列表里消失。 --- ## 4. 三种 @,以及怎么把整体任务交出去 ### 4.1 三种合法输入方式 | 方式 | 操作 | 行为 | |---|---|---| | 头像右键「@ 他」 | 右键(触屏长按 / 键盘 Enter)任意头像 | 把该成员插入输入框,**不自动发送**,原草稿保留。对自己显示「@ 自己」,只作留注记,不触发模型 | | 输入 `@` 选人 | 输入框输入 `@` 或全角 `@` | 弹出本群成员列表;按显示名/handle/角色/别名筛选;↑↓ 移动、Enter/Tab 选人、Esc 关闭。第一项是「所有人」,并显示会触达几位 AI 成员 | | 直接输入 `@名字` | 例如 `@后端,接口字段怎么变了?` | 服务器用确定性规则解析成与芯片相同的 memberId 列表,不要求重新打开选人框 | 解析规则(`shared/mention-resolver.js`,前后端是**同一份代码**,由 `scripts/build-client.mjs` 内联进浏览器半边): 1. 先屏蔽正文结构——代码块、行内代码、`>` 引用行、链接目标、邮箱地址、URL——这些位置里的 `@` 不唤醒任何人。 2. `@` 前必须是文本开始、空白或允许的标点边界;`dev@example.com` 与 `@scope/pkg` 不触发。 3. 先按唯一 handle 精确匹配(ASCII 大小写不敏感),再按唯一别名、唯一显示名(NFC 规范化)精确匹配。**不做 LLM 猜人**。 4. 同一成员被不同形式点名只生成一份投递;芯片 + 文本重复点名也只投一次。 5. 名称含空格时用 `@「前端 小林」`;`@前端 小林` 只按「前端」这个完整别名解析。 6. `@前端你先看看` 这种没有名字边界的写法**不猜测拆分**,界面提示「请在名字后加空格或标点(例如 @前端)」。 7. 重名、无法识别、成员已退出(旧别名 tombstone)都在发送前提示,可以选人或按普通文本发送——**不会静默改投主代理**。 8. `@所有人` / `@all` 展开为**发送时**的活跃成员快照;`@用户` 只通知人类成员,不产生模型调用。 中文输入法处于 composition 阶段时 Enter 不会被抢走;`Enter` 发送、`Shift+Enter` 换行。 ### 4.2 提交整体任务 建群时填「整体目标」,或直接在群里对主代理说一句整体目标(未点名时默认由主代理接收)。主代理随后: 1. 检查现状与约束; 2. 通过群工具落盘结构化计划(工作项、依赖、验收条件、假设); 3. 校验提交(依赖无环、每项有验收条件、负责人存在); 4. 依赖满足的任务才进入 `ready` 并派发,未满足的保持可见的等待状态; 5. 持续协调、组织验收。 「先规划」模式下计划只落盘、不派发,等用户启动。 ### 4.3 用户直接改需求 `@前端 把这个入口改成手机验证码。` 这类明确指令走完整链路:保存原话 → 绑定目标任务 → 写成新的需求决定 revision(人类要求直接采纳)→ 标出确定性影响的任务并置 `needsSync` → 通知相关成员 → 要求每人回报同步状态。**旧版本的通过结果不能直接沿用**,必须按新版本重验。 `@前端 为什么这样设计?` 是提问,不会自动变成改代码的任务。 --- ## 5. 群里能看到什么 - **面板**:顶部是项目标题、状态、项目切换以及任务/成员入口。宽屏显示「项目群 / 时间线 / 任务与模型」三栏;空间不足时自动收起左右栏,入口变成抽屉,不会把聊天挤成一条缝。右上角的 `Aa` 提供**标准 / 大 / 特大**三档字号,选择保存在本机。 - **时间线**:按天分隔;自己的消息在右侧气泡里,成员消息在左侧。代码围栏显示为代码块,行内代码、加粗与已投递的点名都有明确样式;悬停消息可以回复,或在输入框里 `@` 对方。 - **输入区**:毛玻璃输入卡片;`@` 弹层支持键盘选择,引用条、接收人芯片与「将触达几位 AI 成员」的提示都在发送前可见。 - **成员栏**:头像菜单提供 `@ 他`、查看成员、查看/切换模型、查看当前任务、暂停与退出;状态点与模型来源(成员固定/角色默认/项目默认/允许池)在行内可见。 - **任务与模型**:同一栏分「任务 / 模型」两个标签。任务页有验收进度、任务卡(状态、版本、负责人、验收条件、依赖等待、产物核验状态)、项目决定与同步回执、投递与恢复;模型页是项目默认、角色默认、选型模式与允许池。 - **项目生命周期**:左栏分「进行中 / 已归档 / 回收站」;右上角 `…` 提供暂停/恢复、归档、移入回收站、清理预览与永久删除。暂停、归档、回收站都**先阻断新执行再请求停止**,并重新读取真实成员状态;未确认停止时显示“停止未确认”,可重试核对或取消请求。恢复只会回到暂停态。移入回收站默认提示保留 30 天(`trashRetentionDays`),到期不会自动清除。永久删除需输入完整项目名并勾选确认,逐项释放插件独占资源,全部成功后只清理插件私有数据;源代码、借用会话、worktree 与宿主日志默认保留,并留下最小审计墓碑与删除审计;部分释放失败会停在“清理受阻”并可重试。 - **深度研究**:主代理或用户可提出一次带范围、成员与预算的研究提议;群时间线上出现需要真实用户点击的审批卡。批准只覆盖当前 `proposalRevision + scopeDigest`,不会自动开始;可以拒绝、取消或调整范围/预算(调整会生成新版本并让旧批准失效),批准过期后不能启动。点击“开始研究”后经 ResearchGate 再核对项目版本、批准与预算,批准后为参与成员建立**专用研究 Session**(带 `agent/pre-step` 门禁、每研究运行锁、普通群工具禁用、写入路径真实解析门禁和 session 级用量归属;受监督原型暂不允许研究 Session 执行 shell,实验仍在受监督模式下由成员执行)。运行中可暂停/停止;归档/回收站会停止研究、撤销批准并释放专用会话,恢复后不能复用旧批准。重启后运行中的研究进入 `PAUSED_RECONCILE`,必须由人核对后才能恢复;多人类项目因无法验证具体审批人而 fail-closed。研究工具为 `room_research_propose / get / start / hypothesis / experiment / finding / report / flowback`,没有代理可调用的批准或删除入口。 - **投递状态是诚实的**:`已送达,尚未确认取用` 与 `对方本轮已取用` 是两个不同状态,后者只在宿主真实报告取用后才出现。无法确认时显示 `无法确认是否送达,正在核对`,并停止自动重发。 - **技术细节**不塞进群聊:工具执行与自然发言分开呈现,群消息只显示有意义的信息。 成员的正常回答会自动回到群里(每个群驱动轮次的最终回答),因此不会出现「代理明明回答了,群里却看不到」。工作中需要提前说话时用 `room_send`。 --- ## 6. 权限与边界 - 作者身份由服务端根据真实调用者绑定;正文里写「我是老板」不会获得任何权限。 - 群成员只能读写自己加入的群;工具不接受可伪造的 senderId。 - 独立根会话显式应用项目的工作区与工具配置;设计/文档这类只做细节的成员会被**真正移除**写文件与 shell 工具(不是靠提示词请求它别改)。 - 宿主原生权限提示仍归宿主管理;群里的聊天不会被当成原生批准。 - **借用的用户会话**:插件不会销毁它,也不会把它与项目无关的轮次发到群里。 - **生命周期操作只接受真实人类成员**:代理不能调用暂停、归档、回收站或永久删除,也不能用聊天内容伪造确认;永久删除还需要完整项目名与明确勾选。 - 写入工作:git 仓库里每个写入尝试使用独立 worktree;非 git 或 worktree 创建失败时**明确降级为串行写入并说明原因**,不会声称已隔离。成员退出或失败后保留未合并变更,不自动删除工作目录。 --- ## 7. 暂停、恢复、重启 | 操作 | 行为 | |---|---| | 暂停成员 | 停止该成员新的自动执行;消息与任务保留,新 `@` 显示为待处理 | | 恢复成员 | 先同步版本与未处理消息,再继续适用的任务 | | 停止当前工作 | 请求取消当前轮次,保留待处理队列(不承诺已执行的动作被回滚) | | 成员退出本群 | 终止新分配,保留历史作者身份;旧别名变 tombstone 不再误投新人;未完成任务转为待交接 | | 暂停项目 | 先阻断新任务准入与成员唤醒,再请求停止运行中的成员;只有重新读取到成员已停止才进入“已暂停”,否则停在“停止未确认”,可重试核对或取消请求 | | 恢复项目 | 只把已暂停项目恢复为 `active`,并重新校验/创建成员会话;失败会逐人报告 | | 归档项目 | 先阻断执行并请求停止;确认停止后归档,保留消息、任务、决定、成果与模型绑定历史;恢复后回到“已暂停”,不会自动启动 | | 移入回收站 | 立即进入停止阻断态并请求停止;确认停止后才写入可恢复快照并进入回收站;未确认时不会宣称“已删除”;源代码、借用会话、worktree 默认不删;默认提示保留 30 天 | | 从回收站恢复 | 先校验移入时记录的结构快照与资源摘要,再恢复为“已暂停”;不重放旧副作用、不恢复旧审批 | | 停止核对 / 取消请求 | 停止结果未知时项目显示“停止未确认”,可重试核对真实成员状态;尚未造成不可逆删除的请求可以取消并回到原状态 | | 永久删除 | 只允许在回收站中执行;要求完整项目名与明确勾选,拒绝仍有成员运行的项目;逐项释放插件独占资源,全部成功后删除插件私有数据并保留最小审计墓碑;部分释放失败会停在“清理受阻”,保留数据并可重试 | | 关闭群界面 | 只关闭视图,已启动任务继续 | | 重启 Harness | 按群数据库恢复:群、成员、消息、任务、决定与投递账本;**只恢复 active 项目**的 owned 成员会话(暂停/归档/回收站的不恢复),不确定的投递在核对后才重试,并给出简短恢复报告 | 数据库使用 schemaVersion 与迁移前备份;单写者锁保证不会有第二个进程同时改同一个项目——第二个实例会被明确拒绝,而不是并发破坏数据。 --- ## 8. 已知限制 这里只列最需要先知道的几条: - **成员会话的文件与命令工具来自 `memberAgentPreset`**:不配置时成员只有群协作类工具。预设是**两段契约**—— `meta.agentPreset` 只写会话头,真正的工具树由 `agentPresets.mount()` 挂到成员上下文上;插件两件都做 (create 与 resume 都挂),**两条路径都已在真实宿主验证**:成员真跑 `node --version`、真写文件并通过验收。 - **生命周期操作不会假装成员已停止**:归档/回收站会先阻断新执行,再请求停止,并**重新读取真实 agent 状态**;只有确认停止才进入目标状态,否则停在“停止未确认”(`BLOCKED`)。真实桌面端已实测:停止请求已发出时 `confirmed=false`,随后核对成功才归档。 - **项目生命周期是软删除闭环,不是“删库”**:移入回收站只改变项目状态并保留全部插件数据;永久删除只清理该项目的插件私有记录、资源归属账本与插件拥有的会话,**绝不默认删除源代码、用户工作区、借用会话、worktree 或宿主日志**。插件无法证明清理的外部副本会如实保留并报告。回收站到期只做日期提示,**不会自动清除**。部分资源释放失败会进入“清理受阻”(`PURGE_BLOCKED`),保留数据并允许幂等重试。 - **可选源码 / 临时文件 / worktree 清理未实现**:规范里的高级清理需要独立危险操作、逐路径授权、归属核验和宿主删除回执;本版不提供,清理预览会明确列出被保留的源码、worktree 与宿主日志。任何“清理源码”的操作都不应被当作已支持。 - **删除与研究审批都要求可验证的人类身份**:项目里有多个活跃人类成员时,生命周期操作与研究审批会 fail-closed,而不是猜一个审批人。 - **深度研究是受监督原型,不是完整自主研究**:已实现审批卡、scopeDigest/revision、取消/过期、预算预留、专用研究 Session(`agent/pre-step` 门禁、运行锁、按 researchId 精确归属用量、写入路径真实解析与 shell 禁用)、暂停/停止、归档/回收站撤销授权,以及可自动化的权限与恢复测试。**尚未实现**:共享成员会话的调用前拦截、自动研究编排/阶段循环、经验提示的产品层治理,以及完整真实研究演示。 - **插件必须被选为 bundle 才会生效**。`installed: true` + `enabled: false` 时,插件的 `cordis.patch.yml` 整个不生效、loader 树里没有它的行,**而且没有任何报错**。必须把包名加进 profile `package.json` 的 `dsh.profile.bundles`。判定看三处:`list_bundles.enabled`、loader 条目、 `boot-trace.jsonl` 里是否有本进程的 `activation.ready`。 - **跨提供方**验收需要宿主真实接入第二个提供方;只有一条路由时,跨提供方兼容性标为未实测 (不用 Mock 替代)。 - **价格与费用**只记真实用量,**不显示金额**:当前环境没有可靠价目来源,插件按"可靠价格缺失时显示未知"处理, 所以留空而不编造。 - `steer`(运行中于步骤边界插入)与 `queue`(排到下一轮)的实际差异需要一次长工具调用来观察; 界面会显示这一次**实际**用的是哪种。 - 群里的「活人感」是行为目标,不是提示词存在即成立:验收看的是真实依据、后续行动与产物, 不按消息条数或情绪词评分。 - 非 git 项目没有文件系统级隔离,串行写入只是协作机制,不能阻止所有 shell 写入。 - 多窗口/多进程同时管理同一项目:单写者锁会拒绝第二个写入者。 - **源码热重载需要配置监听目录**:插件若通过 junction 链接到 profile 之外,HMR 默认看不到 `src/`;把仓库目录加入 profile 的 `hmr.root` 即可就地重载,否则改完 `src/` 需要重载宿主。 --- ## 9. 开发与验证 ```powershell # 生成浏览器半边(从 shared/ 单一来源内联,禁止手改生成区) node scripts/build-client.mjs # 语法闸门(含「禁止 import @deepseek-ai 包」检查)+ 生成物是否最新 npm run check # 单元 + 集成测试 npm run test ``` ### 客户端半边必须声明 `inject` 浏览器半边的 `exports.inject` **不能为空**。客户端加载器会给每个条目一个 Cordis fiber,而**未声明的服务不是"取不到",是读取就抛错**,fiber 直接进入 `failed`;宿主随后的启动审计只报一句没有原因的: ``` web boot: 1 entry did not activate dsh-project-room: failed ``` 这就是桌面端启动不起来的直接原因。当前声明为 `exports.inject = ['slots']`(`apply` 需要槽位注册表)。凡是 `apply()` 中通过 `ctx.xxx` 直接读取的服务,都必须写进这个数组。 验证方式:`npm run harness` 会在真实 Chromium 里用一个**只提供已声明服务**的代理去执行 `apply`,一旦读了未声明的服务立刻报错——这条规则正是为了让"只报 failed、不给原因"的情况不再发生。 两个测试文件专门锁死曾经真实发生过的加载失败: - `tests/unit/activation-contract.test.js` —— `inject` 声明必须覆盖 `apply()` 读到的每个服务(否则 Cordis 直接抛错,插件永远不激活);浏览器半边不得引用任何 chunk。 - `tests/unit/client-materialize.test.js` —— 按真实加载器契约在 Node 里物化浏览器半边,断言它只注册 `sidebar.panellist:project-room` 与 `main:project-room`,且只依赖 `react` / `react/jsx-runtime`。