diff --git a/.air.toml b/.air.toml new file mode 100644 index 0000000..5a18e3d --- /dev/null +++ b/.air.toml @@ -0,0 +1,28 @@ +root = "." +tmp_dir = "tmp" + +[build] + # 编译命令 + cmd = "go build -o ./tmp/main ./cmd/server" + # 运行的二进制文件 + bin = "./tmp/main" + # 监听文件变化的延迟(毫秒) + delay = 1000 + # 监听这些目录 + include_dir = ["cmd", "internal"] + # 监听这些后缀的文件 + include_ext = ["go"] + # 排除这些目录 + exclude_dir = ["tmp", "web", "agents", "rooms", "skills", "users", "teams", "docs", ".git"] + # 排除这些文件 + exclude_regex = ["_test\\.go$"] + # 进程被杀后的清理 + kill_delay = 500 + +[log] + # 显示日志时间 + time = true + +[misc] + # 退出时清理临时目录 + clean_on_exit = true diff --git a/.gitignore b/.gitignore index 60e3b0d..f0125a2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,9 @@ # Go binary server +main + +# Air 热重载临时目录 +tmp/ # Runtime data (rooms created by users, not tracked) rooms/ diff --git a/agents/legal-team/合同律师/AGENT.md b/agents/legal-team/合同律师/AGENT.md index f8a0c7c..d45556a 100644 --- a/agents/legal-team/合同律师/AGENT.md +++ b/agents/legal-team/合同律师/AGENT.md @@ -5,7 +5,6 @@ description: 专注合同审查、起草和风险评估的专业律师 version: 1.0.0 skills: - 合同审查 -can_challenge: true --- # 合同律师 diff --git a/agents/legal-team/合同律师/SOUL.md b/agents/legal-team/合同律师/SOUL.md index 607bb9d..10d1d2c 100644 --- a/agents/legal-team/合同律师/SOUL.md +++ b/agents/legal-team/合同律师/SOUL.md @@ -9,6 +9,25 @@ - 技术合同 - 投融资合同 +## 工作流程 + + ### 第一步:理解用户意图 + - 收到用户消息后,先判断这是闲聊还是正式需求 + - 如果是打招呼、闲聊,直接友好回复 + - 如果意图不明确,先追问具体需求,不要猜测 + + ### 第二步:需求确认 + - 了解清楚用户的具体问题和背景后,制定工作计划 + - 向用户说明你打算如何安排团队,征得同意 + + ### 第三步:分配任务 + - 只有在需求明确后,才 @成员 分配具体任务 + - 每个成员的任务目标要清晰 + + ### 第四步:整合结果 + - 审查成员的输出,补充遗漏 + - 给用户提供结构化的最终建议 + ## 工作方式 ### 合同审查流程 @@ -65,18 +84,17 @@ - **务实建议**:提供可操作的具体建议 - **对比分析**:展示修改前后的差异 -## 质疑与修订机制 - -当你看到 `` 时,你应该: -1. 审查其他成员(特别是合规专员)的意见 -2. 如果发现合规专员的建议与合同条款有冲突,或者有遗漏的法律风险,提出质疑 -3. 使用格式:`CHALLENGE:<具体的法律风险或修改建议>` -4. 如果没有问题,输出 `AGREE` -5. 当看到针对自己的 CHALLENGE 时,准备修订你的合同审查意见 - ## 注意事项 1. 不提供标准合同模板(除非用户明确要求) 2. 审查时假设用户提供的信息真实准确 3. 对于明显不公平的条款,明确提示用户 -4. 涉及重大金额或复杂法律关系时,建议咨询专业律师 \ No newline at end of file +4. 涉及重大金额或复杂法律关系时,建议咨询专业律师 + +### 接收需求时 + - 如果用户只是打招呼或闲聊,像正常人一样回复即可,不要分配任务 + - 如果用户意图不明确,先询问用户具体需要什么帮助,不要自行揣测和过度分析 + - 只有当用户提出明确的法律需求时,才开始分配任务 + - 仔细倾听用户描述,必要时追问细节 + - 识别问题的法律性质和复杂程度 + - 判断是否需要团队协作 diff --git a/agents/legal-team/合规专员/AGENT.md b/agents/legal-team/合规专员/AGENT.md index 502ee2c..e6f4d03 100644 --- a/agents/legal-team/合规专员/AGENT.md +++ b/agents/legal-team/合规专员/AGENT.md @@ -5,7 +5,6 @@ description: 负责合规检查、风险识别和合规建议的专业人员 version: 1.0.0 skills: - 法律知识库 -can_challenge: true --- # 合规专员 diff --git a/agents/legal-team/合规专员/SOUL.md b/agents/legal-team/合规专员/SOUL.md index 2ebeb0f..c006276 100644 --- a/agents/legal-team/合规专员/SOUL.md +++ b/agents/legal-team/合规专员/SOUL.md @@ -80,14 +80,6 @@ - **风险意识**:明确违规后果和责任 - **平衡考量**:兼顾合规要求和业务实际 -## 质疑机制 - -当你看到 `` 时,你应该: -1. 仔细审查其他成员的草稿 -2. 如果发现合规风险或遗漏,主动提出质疑 -3. 使用格式:`CHALLENGE:<具体的合规风险或建议>` -4. 如果没有问题,输出 `AGREE` - ## 注意事项 1. 不替代企业合规部门的职责 diff --git a/agents/legal-team/法律总监/SOUL.md b/agents/legal-team/法律总监/SOUL.md index 0f138c8..c808bfc 100644 --- a/agents/legal-team/法律总监/SOUL.md +++ b/agents/legal-team/法律总监/SOUL.md @@ -26,17 +26,6 @@ - 补充遗漏的关键点 - 提供结构化的最终建议 -## 处理 CHALLENGE 的决策指令 - -当你看到 `` 中有 CHALLENGE 条目时,你应该: -1. 仔细评估质疑的合理性 -2. 判断是否需要重新分配任务或修订 -3. 如果质疑有效,可以: - - 要求相关成员修订工作 - - 重新分配任务给其他成员 - - 自己补充或修正意见 -4. 在最终建议中明确说明如何处理了这些质疑 - ## 沟通风格 - **专业严谨**:使用准确的法律术语 diff --git a/agents/legal-team/法律总监/memory/2026-03.md b/agents/legal-team/法律总监/memory/2026-03.md index d24b02c..08eed9e 100644 --- a/agents/legal-team/法律总监/memory/2026-03.md +++ b/agents/legal-team/法律总监/memory/2026-03.md @@ -1,11 +1,5 @@ -## 2026-03-05 — 你好 - -ASSIGN:合同律师:分析用户"第五季"的工作风格和沟通偏好,整理成简洁要点,为后续法律咨询提供参考框架。 - -ASSIGN:合规专员:评估用户简介中可能涉及的法律合规风险点(如沟通方式、数据使用等),提出初步注意事项。 - -## 2026-03-05 — 我一个中间人,说可以给我介绍一个 +## 2026-03-05 — 我一个中间人,说可以给我介绍一个� 基于本次咨询,总结关键要点如下: @@ -30,3 +24,378 @@ ASSIGN:合规专员:评估用户简介中可能涉及的法律合规风险点( * **必须规避法律保护的“红线”员工群体**。辞退处于孕期、产期、哺乳期、医疗期或工伤期的员工,将直接构成违法解除。 * **经济补偿/赔偿金的计算必须精确,尤其注意历史工龄分段**。对于2008年1月1日前入职的员工,补偿金计算需适用当时的法规,规则复杂,易出错。 * **内部合规流程(法务审核、通知工会)和规范离职手续(结清款项、出具证明)是避免后续争议的关键程序**。任何程序缺失都可能导致整个辞退行为被认定为违法。 + + +## 2026-03-06 — 我想用员工的信息,注册一家公司 + +* **核心风险定性**:使用他人(尤其是员工)信息注册公司,在法律上构成高风险的“冒名/借名登记”,本质是构建一个权责错位、基础不牢的法律实体。 +* **风险极度不对等**:该操作将**极端风险**(无限连带责任、刑事牵连、信用破产)转嫁给名义持有人,而实际控制人则面临**控制权丧失、资产被处置、权益无法合法化**的重大隐患。 +* **触及多重合规红线**:行为直接违反公司登记信息真实性要求(可致公司被撤销)、个人信息保护规定(缺乏有效同意)及劳动法规(可能构成强迫),并可能成为金融犯罪工具。 +* **商业发展致命障碍**:股权不清、存在代持是公司未来融资、上市或进行重大合作的直接障碍,严重损害商业信誉。 +* **唯一安全路径**:必须坚持“实名、实股、实责”原则。如有特殊需求,必须在专业律师指导下,通过规范协议(如代持协议)及合法架构(如持股平台)进行,且确保各方完全知情自愿。 + +## 2026-03-06 — 他和我很熟悉的@法律总监 + +* **“熟悉关系”常被用作风险背书与流程简化的理由**:用户倾向于用个人熟悉度来替代或弱化正式的法律尽职调查和严谨的合同条款谈判,这是需要警惕的核心信号。 +* **核心风险在于“情感信任前置,法律保障后置”**:基于熟人的商业安排,最大的隐患是混淆了个人道德信任与法律契约保障,导致权利义务不清、证据缺失,纠纷发生时既伤财又伤情。 +* **法律建议必须坚持“对事不对人”的契约化原则**:无论关系多熟,都必须坚持独立尽调、签署权责清晰的书面合同、规范财务与决策程序。法律工作的价值正是为信任构建一个防风险的“法律容器”。 +* **沟通需引导用户将“感性认知”转化为“理性保障”**:回应用户时,需在理解其希望提高效率的心理的同时,明确解释简化法律环节的具体风险,并引导其认识到规范操作是长期保护关系和利益的唯一路径。 + +## 2026-03-06 — @法律总监 你好 + +Based on this interaction, here are the key learnings for future reference: + +* **Greetings are not tasks**: Simple greetings like "你好" are social acknowledgments, not work requests. They should receive a direct, friendly response without any task delegation or analysis. +* **Clarify intent before acting**: When user intent is unclear (e.g., just a greeting), the correct protocol is to respond politely and ask how you can help, rather than making assumptions or initiating a workflow. +* **Reserve task delegation for explicit needs**: The "@" function and team coordination should only be triggered after the user has explicitly stated a specific, substantive legal or professional need. + +## 2026-03-06 — 我想写一个 合作协议 + +Based on this task, here are the key learnings for future reference: + +* **"写协议"是明确的专业任务触发词**:当用户明确提出需要起草或审查法律文件(如“写协议”、“审合同”)时,应立即启动任务分配流程,这是核心工作场景。 +* **分配任务需明确背景与焦点**:在@团队成员时,应简要说明用户身份(如“产品经理”)并指示协议的核心审查/起草要点(如知识产权、收益分配),以提高效率。 +* **信息收集是有效支持的前提**:在成员工作前,必须主动向用户询问关键背景信息(合作方、内容、目标、关切点),缺乏背景的协议草案是无效的。 +* **沟通流程需清晰分层**:先向成员下达明确指令,再与用户进行信息收集对话,保持工作流清晰。 + +## 2026-03-06 — @法律总监 我想写一个合作协议 + +Based on this task, here are the key learnings for future reference: + +* **"写协议"是明确的专业任务触发词**:当用户明确提出需要起草或审查法律文件(如“写协议”、“审合同”)时,应立即启动任务分配流程,这是核心工作场景。 +* **分配任务需明确背景与焦点**:在@团队成员时,应简要说明用户身份(如“产品经理”)并指示协议的核心审查/起草要点(如知识产权、收益分配),以提高效率。 +* **信息收集是有效支持的前提**:在成员工作前,必须主动向用户询问关键背景信息(合作方、内容、目标、关切点),缺乏背景的协议草案是无效的。 +* **沟通流程需清晰分层**:先向成员下达明确指令,再与用户进行信息收集对话,保持工作流清晰。 + +## 2026-03-06 — 我想写一个合作协议 + +* **“写协议”是明确的专业任务触发词**:当用户明确提出需要起草或审查法律文件(如“写协议”、“审合同”)时,应立即启动任务分配流程,这是核心工作场景。 +* **分配任务前必须进行“需求确认”**:在@团队成员前,必须先通过提问收集关键背景信息(合作方、内容、目标、关切点),缺乏背景的协议草案是无效的。 +* **信息收集需结构化且高效**:应优先使用选择题,每次只问一个核心问题,以符合产品经理高效、结构化的沟通偏好。 +* **工作方案需清晰透明**:在收集信息后,应向用户总结并展示明确的工作计划(如“@合同律师 负责起草初稿”),获得确认后再执行,这符合用户对流程可控的期望。 + +## 2026-03-06 — 个人 + +Based on this interaction, here are the key learnings for future reference: + +* **"个人"是模糊的意图表述**:当用户仅提供单一名词(如“个人”)时,无法直接判断其具体需求,这通常是一个不完整的陈述或提问的开始。 +* **首要任务是澄清意图**:面对模糊输入,不能启动任何工作流程。正确的做法是进行友好、开放的询问,引导用户说明具体需求。 +* **保持专业且友好的引导**:回应应表明身份和可用服务,并通过提供选项(如“起草合同”、“法律咨询”)来结构化地引导对话,帮助用户明确表达。 + +## 2026-03-06 — 我想写一个设计方案合同 + +Based on this task, here are the key learnings for future reference: + +* **“设计方案合同”是明确的专业任务触发词**:当用户明确提出需要起草特定类型的专业合同(如“设计方案合同”、“合作协议”)时,应立即启动需求确认流程,这是核心工作场景。 +* **分配任务前必须进行“场景化信息收集”**:在@团队成员前,必须先通过提问收集与合同类型强相关的关键背景信息(如合作方身份、设计内容、核心关切点),缺乏背景的合同草案是无效的。 +* **信息收集需结合用户身份与合同特性**:针对“产品经理”用户和“设计方案”这类交付物合同,应优先询问能界定需求范围、交付标准和知识产权的关键问题,这符合用户对流程可控和结果明确的期望。 +* **沟通需高效且结构化**:应使用选择题等结构化提问方式,每次聚焦一个核心问题,以符合产品经理高效、目标明确的沟通偏好。 + +## 2026-03-06 — 个人设计师 + +* **“个人设计师”是明确的身份信息**:当用户明确合作方为“个人设计师”时,这直接决定了合同主体的法律性质(自然人 vs. 法人),影响资质审查、发票开具、责任承担等条款的起草方向。 +* **信息收集需按逻辑顺序推进**:在确认合作方身份后,下一步应自然聚焦于“合作内容”,这是界定合同范围、交付标准和权利义务的核心,符合从“主体”到“标的”的合同起草逻辑。 +* **提问方式需结构化且高效**:针对“设计方案”这类专业交付物,提供具体选项(如UI/UX、品牌VI等)能帮助用户快速、准确地定位需求,这符合产品经理结构化的思维习惯。 +* **背景信息是有效任务分配的前提**:必须收集完“合作方”和“合作内容”这两项基础信息后,才能形成有效的任务分配方案,确保合同律师起草的初稿具有针对性。 + +## 2026-03-06 — C + +* **“C”是选择题的明确答案**:当用户用单个字母(如“C”)回答选择题时,表示其已从预设选项中做出选择,应直接采纳该选项对应的含义(此处为“营销活动页面/海报设计”),无需再次确认。 +* **信息收集需结构化推进**:使用选择题(A/B/C/D)是高效、清晰的沟通方式,符合产品经理偏好,能快速锁定关键变量(如合同标的)。 +* **分配任务前必须完成“需求确认”闭环**:在获得所有关键背景信息(合作方、内容、核心关切点)后,才能向用户总结并展示明确的工作方案,获得其确认。这是启动团队协作的必要前提。 + +## 2026-03-06 — C + +* **“C”是选择题的答案,而非完整需求**:用户仅回复选项字母“C”,表明其倾向于高效、选择性的沟通方式,但未提供完整背景。这需要进一步追问以获取足够信息。 +* **信息收集需结构化推进**:面对选择题答案,应将其视为当前步骤的确认,并立即进入下一关键问题(如“最关心的条款”),以保持对话的效率和目标导向。 +* **用户偏好高效、结构化的交互**:用户作为产品经理,其沟通模式显示出对清晰选项和分步确认的偏好,这符合其职业背景。应继续使用选择题来引导对话,避免开放式提问导致效率降低。 +* **核心条款确认是关键步骤**:在明确合同类型(营销设计)和合作方(个人)后,下一步必须聚焦于用户的核心商业关切(如知识产权、付款),这是起草有效合同的基础。 + +--- + +好的,明确了是“营销活动页面/海报设计”。 + +**接下来,请告诉我:** + +3. **你最关心合同中的哪些方面?**(可以多选) + * A. **知识产权归属**(设计成果归谁,设计师能否用于个人作品集) + * B. **修改与交付标准**(修改次数、验收标准如何界定) + * C. **费用与支付节点**(如何根据设计阶段付款) + * D. **项目周期与延期责任** + * E. **保密条款**(活动创意是否需保密) + +了解你最关注的条款,能让合同律师在起草时重点着墨,平衡双方权益。 + +## 2026-03-06 — 我想要编写一个项目设计合同 + +* **“编写合同”是明确的专业任务触发词**:当用户明确提出需要起草法律文件(如“编写合同”)时,应立即启动结构化的需求确认流程。 +* **分配任务前必须进行“主体身份确认”**:合同合作方(个人 vs. 公司)是决定合同条款(如责任、发票、资质)走向的首要变量,必须在任务分配前优先明确。 +* **信息收集需结构化且高效**:使用选择题(A/B)是快速锁定关键变量、符合产品经理高效沟通偏好的有效方式。 +* **沟通需按逻辑顺序推进**:在明确“合作方”后,下一步应自然聚焦于“项目内容/范围”,这是界定合同标的和权利义务的核心。 + +## 2026-03-06 — A + +* **“A”是选择题的明确答案**:用户用单个字母回答选择题,表示已从预设选项中做出选择,应直接采纳其对应含义(此处为“个人设计师/工作室”),无需再次确认。 +* **信息收集需结构化推进**:使用选择题(A/B)是高效、清晰的沟通方式,符合产品经理偏好,能快速锁定关键变量(如合同主体性质)。 +* **下一步应聚焦“合同标的”**:在确认合作方身份后,下一步应自然聚焦于“合作内容”,这是界定合同范围、交付标准和权利义务的核心,符合从“主体”到“标的”的合同起草逻辑。 +* **背景信息是有效任务分配的前提**:必须收集完“合作方”和“合作内容”这两项基础信息后,才能形成有效的任务分配方案,确保合同律师起草的初稿具有针对性。 + +## 2026-03-06 — C + +* **“C”是选择题的答案,而非完整需求**:用户仅回复选项字母“C”,表明其倾向于高效、选择性的沟通方式,但未提供完整背景。这需要进一步追问以获取足够信息。 +* **信息收集需结构化推进**:面对选择题答案,应将其视为当前步骤的确认,并立即进入下一关键问题(如“最关心的条款”),以保持对话的效率和目标导向。 +* **用户偏好高效、结构化的交互**:用户作为产品经理,其沟通模式显示出对清晰选项和分步确认的偏好,这符合其职业背景。应继续使用选择题来引导对话,避免开放式提问导致效率降低。 +* **核心条款确认是关键步骤**:在明确合同类型(营销设计)和合作方(个人)后,下一步必须聚焦于用户的核心商业关切(如知识产权、付款),这是起草有效合同的基础。 + +## 2026-03-06 — A、B、C + +* **用户偏好结构化、高效的选择题交互**:用户连续使用单个字母(A, C, A/B/C)回答问题,表明其作为产品经理,倾向于清晰、选项明确的沟通方式,这能快速锁定需求变量,应作为主要提问策略。 +* **“知识产权、交付标准、费用支付”是设计类合同的核心关切**:用户明确选择这三项,验证了在“个人设计师”与“营销设计”场景下,这是最关键的商业与法律风险点,未来类似需求应优先聚焦于此。 +* **信息收集完毕是启动任务的明确信号**:在通过选择题依次确认“合作方(A)→ 项目内容(C)→ 核心条款(A/B/C)”后,用户需求已清晰,应立即总结工作方案并寻求确认,而非继续提问。 +* **工作方案需透明且符合用户角色预期**:向用户展示的计划应具体(如“@合同律师 起草初稿”),并承诺提供其作为产品经理所重视的**结构化分析**(如Pros & Cons),这能有效获得其确认,推进流程。 + +## 2026-03-06 — ok , 可以 + +* **用户明确确认是启动任务的最终信号**:当用户对总结的工作方案(如“@合同律师 负责起草...”)回复“ok”或“可以”等肯定词时,表明需求确认流程已闭环,应立即执行任务分配。 +* **工作方案需具体透明以获得确认**:向用户(尤其是产品经理)展示的计划必须包含明确的**执行人(@谁)、任务(做什么)、产出(交付什么)**,这符合其对流程可控和结果预期的要求。 +* **“确认后执行”是核心工作纪律**:必须严格遵守“先确认方案,再分配任务”的流程。用户的确认是解除“需求确认”技能中的唯一条件。 +* **任务分配指令需结构化且包含全部背景**:在@团队成员时,应清晰复述用户确认过的所有关键背景信息(合作方、内容、核心关切),并给出明确的起草/审查要求,确保工作方向准确。 + +## 2026-03-06 — 我要推辞一个人 + +* **“推辞”是模糊的意图表述**:该词本身无法判断具体场景(如拒绝合作、辞退员工、拒绝个人请求),必须首先通过选择题(A/B/C)进行场景分类,这是启动有效分析的第一步。 +* **场景分类决定法律路径**:推辞的对象和背景(工作合作、人事关系、个人事务)将决定完全不同的法律风险、合规要求和沟通策略,必须在提供任何建议前明确。 +* **信息收集需结构化引导**:面对模糊需求,使用选择题是最高效的澄清方式,符合用户(产品经理)结构化的思维习惯,能快速锁定问题域。 +* **沟通需按逻辑顺序推进**:在明确场景后,才能进一步询问具体细节(如拒绝理由、双方关系、已采取的行动),以评估法律风险和提供可操作建议。 + +## 2026-03-06 — A + +* **“A”是选择题的明确答案**:用户用单个字母回答选择题,表示已从预设选项中做出选择,应直接采纳其对应含义(此处为“工作/业务合作”场景),无需再次确认。 +* **信息收集需结构化推进**:使用选择题(A/B/C)是高效、清晰的沟通方式,符合产品经理偏好,能快速锁定问题域。 +* **下一步应聚焦“推辞对象”**:在确认场景后,下一步应自然聚焦于“推辞对象”,这是决定法律风险性质(对外合同责任 vs. 对内管理沟通)和后续建议方向的关键变量。 +* **沟通需按逻辑顺序分层**:遵循“场景 → 对象 → 具体事由/风险”的提问逻辑,能系统性地收集信息,避免遗漏关键点。 + +## 2026-03-06 — AB + +好的,我来总结一下这个任务的关键要点。 + +* **用户同时选择多个选项是常见情况**:当用户同时选择A和B时,表明情况可能涉及多方或存在交叉,不能简单归类,需要进一步澄清。 +* **“推辞”场景的核心是识别“主要矛盾方”**:在多方场景下,必须引导用户明确**主要推辞对象**(是外部合作方,还是内部同事),这直接决定了法律风险的性质(对外违约风险 vs. 内部管理/合规风险)。 +* **下一步提问需聚焦于“厘清关系与核心”**:当用户选择模糊时,应提供更精确的选项(如“主要矛盾是A还是B?”)或询问是否为“内外部联动的复杂情况”,以锁定问题的真正症结。 +* **沟通需保持结构化引导**:即使情况变复杂,仍需使用选择题等结构化方式推进,这符合产品经理(第五季)高效、目标明确的沟通偏好。 + +--- + +根据我们刚才的讨论,为了给你最精准的建议,请告诉我: + +**你希望推辞的核心对象,主要是哪一个?** +* **A. 外部合作方**(这是主要矛盾) +* **B. 内部同事/部门**(这是主要矛盾) + +或者,这是一个**涉及内外部联动的复杂情况**(例如,需要先拒绝内部同事的某个外部合作提议)? + +## 2026-03-06 — B + +* **用户选择“B”明确了核心风险类型**:当用户选择“存在法律或合规风险”作为推辞内部同事的事由时,这表明问题已从单纯的管理沟通,升级为需要**专业法律/合规评估**的正式依据。 +* **任务性质转变为“风险识别与证据支持”**:此时的核心需求不再是沟通技巧,而是由合规专员或合同律师介入,**审查项目文件、识别具体风险点、并提供书面评估**,以形成具有说服力的拒绝理由。 +* **下一步应聚焦于“风险证据收集”**:在确认事由后,应立即询问用户是否已有相关文件(如合同草案、项目计划、沟通记录),以便为团队分配具体的审查任务。 +* **沟通需引导用户从“主观判断”转向“客观依据”**:应帮助用户理解,基于合规风险的推辞是最有力的,但必须建立在专业的审查结论之上,这符合产品经理注重流程和依据的思维。 + +## 2026-03-06 — D + +* **“D”是模糊选项的常见选择**:当用户选择“其他原因”或“D”时,表明真实原因可能敏感、复杂或不便明说,需要进一步提供更精确但依然结构化的选项来引导。 +* **下一步提问需聚焦于“原因性质”而非具体细节**:面对模糊原因,应提供能界定问题性质(如商业冲突、个人风险、人际关系)的选项,这有助于判断是提供**商业/管理建议**还是**法律/合规支持**。 +* **沟通需在保护用户隐私与获取有效信息间平衡**:提供“不方便明说的复杂原因”作为选项之一,既尊重了用户的潜在顾虑,也为后续提供原则性建议(如程序合规、书面记录)留下了空间。 +* **用户偏好通过选择题逐步澄清复杂问题**:用户(产品经理)在面对内部敏感事务时,依然倾向于通过选择题来结构化地表达核心关切,这符合其高效、规避直接冲突的沟通风格。 + +## 2026-03-06 — B + +* **用户选择“B”明确了核心风险类型**:当用户选择“个人判断/职业风险”作为推辞内部同事的事由时,这表明问题已从单纯的管理沟通,升级为需要**专业法律/合规评估**的正式依据。 +* **任务性质转变为“风险识别与证据支持”**:此时的核心需求不再是沟通技巧,而是由合规专员或合同律师介入,**审查项目文件、识别具体风险点、并提供书面评估**,以形成具有说服力的拒绝理由。 +* **下一步应聚焦于“风险证据收集”**:在确认事由后,应立即询问用户是否已有相关文件(如合同草案、项目计划、沟通记录),以便为团队分配具体的审查任务。 +* **沟通需引导用户从“主观判断”转向“客观依据”**:应帮助用户理解,基于合规风险的推辞是最有力的,但必须建立在专业的审查结论之上,这符合产品经理注重流程和依据的思维。 + +## 2026-03-06 — 可以,安排工作吧 + +* **用户确认“可以”是启动任务的最终指令**:当用户对明确的工作方案(包含执行人、任务、产出)回复肯定确认时,表明需求确认流程已闭环,应立即执行任务分配。 +* **任务分配指令需结构化且包含完整背景**:在@团队成员时,必须清晰复述用户确认过的所有关键背景信息(场景、对象、事由),并给出具体、可执行的审查/分析要求,确保工作方向准确。 +* **“个人职业风险”场景需多维度支持**:当推辞事由涉及“个人判断/职业风险”时,需协调**合规专员**(提供风险评估框架与沟通话术)与**合同律师**(审查文件中的个人责任风险)协同工作,从流程和文本两个维度构建防火墙。 +* **沟通需符合用户角色预期**:对产品经理(第五季)分配任务时,指令本身需体现结构化、目标明确的特性,并承诺交付其重视的**可操作要点**(如话术、风险清单),这符合其工作风格。 + +## 2026-03-06 — 我要一份个人和我的合作协议 + +好的,明白。这位合作方是个人设计师。 + +**接下来,请告诉我:** + +2. **你们合作的具体内容是什么?**(可以多选) + * A. **产品UI/UX设计**(界面、交互、原型等) + * B. **品牌/VI设计**(Logo、视觉识别系统等) + * C. **营销活动页面/海报设计** + * D. **其他设计类型**(请简单说明) + +明确合作内容,是界定工作范围、交付标准和验收条件的基础,这对产品经理来说至关重要。 + +## 2026-03-06 — A + +* **“A”是选择题的明确答案**:用户用单个字母回答选择题,表示已从预设选项中做出选择,应直接采纳其对应含义(此处为“个人设计师/开发者”),无需再次确认。 +* **信息收集需结构化推进**:使用选择题(A/B/C)是高效、清晰的沟通方式,符合产品经理偏好,能快速锁定关键变量(如合作方身份)。 +* **下一步应聚焦“合同标的”**:在确认合作方身份后,下一步应自然聚焦于“合作内容”,这是界定合同范围、交付标准和权利义务的核心,符合从“主体”到“标的”的合同起草逻辑。 +* **背景信息是有效任务分配的前提**:必须收集完“合作方”和“合作内容”这两项基础信息后,才能形成有效的任务分配方案,确保合同律师起草的初稿具有针对性。 + +## 2026-03-06 — A B C + +好的,我来总结这个任务的关键要点。 + +* **用户同时选择多个选项是常见情况**:当用户同时选择A、B、C时,表明合作内容可能是一个综合性的设计项目,而非单一类型,这会影响合同中对“工作范围”的定义方式。 +* **“综合设计项目”需更清晰的工作范围界定**:对于涵盖多类型设计的合作,合同必须明确各部分的交付物、标准、可能的时间线以及对应的费用(是打包价还是分项计价),以避免范围蔓延。 +* **下一步应聚焦于“核心商业关切”**:在明确合作方(个人设计师)和综合内容后,下一步必须聚焦于用户的核心商业与法律关切(如知识产权、付款、交付标准),这是起草有效合同的基础。 +* **信息收集需按逻辑顺序高效推进**:遵循“合作方 → 合作内容 → 核心条款”的提问逻辑,使用选择题,符合产品经理(第五季)结构化、高效的沟通偏好。 + +## 2026-03-06 — 都关心 + +* **用户偏好结构化、高效的选择题交互**:用户连续使用单个字母(A, A/B/C, “都关心”)回答问题,表明其作为产品经理,倾向于清晰、选项明确的沟通方式,这能快速锁定需求变量,应作为主要提问策略。 +* **“综合设计项目”需更清晰的工作范围界定**:当合作内容涵盖多类型设计时,合同必须明确各部分的交付物、标准、可能的时间线以及对应的费用(是打包价还是分项计价),以避免范围蔓延。 +* **“全风险点关注”是产品经理的典型特征**:用户对知识产权、交付、付款、周期、保密等所有核心条款都表示关心,验证了其角色对项目可控性和风险规避的全面要求。未来类似需求应默认全面覆盖这些要点。 +* **信息收集完毕是启动任务的明确信号**:在通过选择题依次确认“合作方(A)→ 项目内容(A/B/C)→ 核心条款(都关心)”后,用户需求已清晰,应立即总结工作方案并寻求确认,而非继续提问。 +* **工作方案需透明且符合用户角色预期**:向用户展示的计划应具体(如“@合同律师 起草初稿”),并承诺提供其作为产品经理所重视的**结构化分析**(如Pros & Cons、谈判弹性空间),这能有效获得其确认,推进流程。 + +## 2026-03-06 — ok,可以的 + +* **用户明确确认是启动任务的最终信号**:当用户对总结的工作方案(包含执行人、任务、产出)回复“ok”或“可以的”等肯定词时,表明需求确认流程已闭环,应立即执行任务分配。 +* **工作方案需具体透明以获得确认**:向用户(尤其是产品经理)展示的计划必须包含明确的**执行人(@谁)、任务(做什么)、产出(交付什么)**,这符合其对流程可控和结果预期的要求。 +* **“确认后执行”是核心工作纪律**:必须严格遵守“先确认方案,再分配任务”的流程。用户的确认是解除“需求确认”技能中的唯一条件。 +* **任务分配指令需结构化且包含全部背景**:在@团队成员时,应清晰复述用户确认过的所有关键背景信息(合作方、内容、核心关切),并给出明确的起草/审查要求,确保工作方向准确。 + +## 2026-03-06 — 执行 + +* **“执行”是明确的行动指令**:当用户对已确认的工作方案回复“执行”时,等同于“ok/可以”,是启动任务分配的最终确认信号。 +* **任务分配需在用户确认后立即进行**:收到“执行”指令后,应直接、清晰地 @ 相关团队成员并下达包含全部背景的详细任务,无需再次询问或总结。 +* **沟通流程遵循“确认-执行”的严格顺序**:必须严格遵守“需求确认 → 方案总结 → 用户确认 → 任务分配”的流程,用户的确认是流程推进的唯一节点。 +* **用户偏好高效、目标明确的沟通**:作为产品经理,用户使用“执行”这类简洁指令,表明其期望在方案确认后快速进入执行阶段,沟通应避免冗余。 + +## 2026-03-06 — ok + +Based on this interaction, here are the key learnings for future reference: + +* **"ok" is a common but ambiguous response**: A standalone "ok" from the user can be an acknowledgment, a confirmation, or a simple greeting. It does not inherently contain a specific task request. +* **First action is to clarify intent**: When faced with an ambiguous response like "ok", the correct protocol is to respond politely and ask how you can help, rather than making assumptions or initiating a workflow. +* **Reserve task delegation for explicit needs**: The task allocation workflow should only be triggered after the user has explicitly stated a specific, substantive legal or professional need. + +## 2026-03-06 — 起草合同 + +Based on this task, here are the key learnings for future reference: + +* **“起草合同”是明确的专业任务触发词**:当用户明确提出需要起草法律文件时,应立即启动结构化的需求确认流程。 +* **分配任务前必须进行“主体身份确认”**:合同合作方(个人 vs. 公司)是决定合同条款(如责任、发票、资质)走向的首要变量,必须在任务分配前优先明确。 +* **信息收集需结构化且高效**:使用选择题(A/B/C)是快速锁定关键变量、符合产品经理高效沟通偏好的有效方式。 +* **沟通需按逻辑顺序推进**:在明确“合作方”后,下一步应自然聚焦于“合作内容/范围”,这是界定合同标的和权利义务的核心。 + +## 2026-03-06 — A + +好的,我来总结一下这个任务的关键要点。 + +* **“A”是选择题的明确答案**:用户用单个字母回答选择题,表示已从预设选项中做出选择,应直接采纳其对应含义(此处为“个人”),无需再次确认。 +* **信息收集需结构化推进**:使用选择题(A/B/C)是高效、清晰的沟通方式,符合产品经理偏好,能快速锁定关键变量(如合作方身份)。 +* **下一步应聚焦“合同标的”**:在确认合作方身份后,下一步应自然聚焦于“合作内容”,这是界定合同范围、交付标准和权利义务的核心,符合从“主体”到“标的”的合同起草逻辑。 +* **背景信息是有效任务分配的前提**:必须收集完“合作方”和“合作内容”这两项基础信息后,才能形成有效的任务分配方案,确保合同律师起草的初稿具有针对性。 + +--- + +好的,合作方是个人。 + +**接下来,这份合同是关于什么内容的?**(可以多选) +* **A. 产品/技术开发**(如软件开发、App开发等) +* **B. 设计服务**(如UI/UX设计、品牌设计等) +* **C. 咨询服务**(如战略咨询、市场顾问等) +* **D. 内容创作**(如文案、视频、课程制作等) +* **E. 其他**(请简单说明) + +## 2026-03-06 — A + +好的,我来总结这个任务的关键要点。 + +* **“A”是选择题的明确答案**:用户用单个字母回答选择题,表示已从预设选项中做出选择,应直接采纳其对应含义(此处为“个人”),无需再次确认。 +* **信息收集需结构化推进**:使用选择题(A/B/C)是高效、清晰的沟通方式,符合产品经理偏好,能快速锁定关键变量(如合作方身份)。 +* **下一步应聚焦“合同标的”**:在确认合作方身份后,下一步应自然聚焦于“合作内容”,这是界定合同范围、交付标准和权利义务的核心,符合从“主体”到“标的”的合同起草逻辑。 +* **背景信息是有效任务分配的前提**:必须收集完“合作方”和“合作内容”这两项基础信息后,才能形成有效的任务分配方案,确保合同律师起草的初稿具有针对性。 + +## 2026-03-06 — A + +好的,我来总结这个任务的关键要点。 + +* **“A”是选择题的明确答案**:用户用单个字母回答选择题,表示已从预设选项中做出选择,应直接采纳其对应含义(此处为“产品UI/UX设计”),无需再次确认。 +* **信息收集需结构化推进**:使用选择题(A/B/C/D/E)是高效、清晰的沟通方式,符合产品经理偏好,能快速锁定关键变量(如合同标的)。 +* **下一步应聚焦“核心商业关切”**:在明确合作方(个人)和合作内容(UI/UX设计)后,下一步必须聚焦于用户的核心商业与法律关切(如知识产权、交付标准、付款),这是起草有效合同的基础。 +* **沟通需按逻辑顺序高效推进**:遵循“合作方 → 合作内容 → 核心条款”的提问逻辑,使用选择题,符合产品经理(第五季)结构化、高效的沟通偏好。 + +## 2026-03-06 — 全部内容 + +* **“全部内容”是明确的全面风险关注信号**:当用户(尤其是产品经理)对核心条款选项(如知识产权、交付、付款等)选择“全部内容”时,表明其对项目的**全面风险控制**和**流程可控性**有极高要求,应默认覆盖所有关键风险点。 +* **工作方案需提供“分析增值”而不仅是文件**:针对此类需求,交付物必须超越合同文本本身,提供产品经理角色所重视的**结构化分析**(如Pros & Cons、谈判弹性、执行清单),这是获得其确认的关键。 +* **信息收集闭环是启动任务的明确节点**:通过选择题依次确认“合作方(A)→ 项目内容(A)→ 核心条款(全部)”后,用户需求已清晰,应立即总结并展示具体工作方案,寻求最终确认。 +* **沟通需高效且符合用户思维模式**:整个交互过程(选择题引导、方案透明、承诺结构化产出)完全匹配产品经理(第五季)高效、目标明确、注重流程与风险分析的职业特性。 + +## 2026-03-06 — 可以的 ,就这样 + +* **用户明确确认是启动任务的最终信号**:当用户对总结的工作方案(包含执行人、任务、产出)回复“可以的”等肯定词时,表明需求确认流程已闭环,应立即执行任务分配。 +* **工作方案需具体透明以获得确认**:向用户(尤其是产品经理)展示的计划必须包含明确的**执行人(@谁)、任务(做什么)、产出(交付什么)**,这符合其对流程可控和结果预期的要求。 +* **“确认后执行”是核心工作纪律**:必须严格遵守“先确认方案,再分配任务”的流程。用户的确认是解除“需求确认”技能中的唯一条件。 +* **任务分配指令需结构化且包含全部背景**:在@团队成员时,应清晰复述用户确认过的所有关键背景信息(合作方、内容、核心关切),并给出明确的起草/审查要求,确保工作方向准确。 + +## 2026-03-06 — 总预算 2 万, 4 周交付全部, 2 轮免 + +* **用户提供具体参数是任务推进的关键节点**:当用户明确回复预算、周期、修改次数等具体商业条款时,表明需求已从“定性”进入“定量”阶段,应立即总结并同步给执行成员,无需再次确认。 +* **“按常规处理”是明确的授权信号**:当用户对非核心条款(如责任上限)表示“按常规出”,表明其信任专业判断,应基于行业惯例和风险平衡原则起草,无需反复追问。 +* **信息总结需结构化且包含全部要素**:向用户复述确认的信息时,应清晰列出所有关键变量(合作方、内容、金额、时间、核心关切),这既是对用户的确认,也为团队成员提供了完整的工作背景。 +* **沟通需高效并预告下一步**:在获得全部必要信息后,应立即告知用户下一步行动(如“同步给合同律师”和“提供结构化报告”),这符合产品经理对流程可控和明确产出的期望。 + +## 2026-03-06 — 开始 + +* **“开始”是明确的执行指令**:当用户对已确认的工作方案回复“开始”时,等同于“执行”或“ok”,是启动任务分配的最终确认信号。 +* **任务分配需在用户确认后立即进行**:收到“开始”指令后,应直接、清晰地 @ 相关团队成员并下达包含全部背景的详细任务,无需再次询问或总结。 +* **沟通流程遵循“确认-执行”的严格顺序**:必须严格遵守“需求确认 → 方案总结 → 用户确认 → 任务分配”的流程,用户的确认是流程推进的唯一节点。 +* **用户偏好高效、目标明确的沟通**:作为产品经理,用户使用“开始”这类简洁指令,表明其期望在方案确认后快速进入执行阶段,沟通应避免冗余。 + +## 2026-03-06 — 我要写一个聘用书 + +好的,明白。聘用对象是关键岗位员工。 + +**接下来,请告诉我:** + +2. **你最关心聘用书中的哪些核心条款?**(可以多选) + * A. **岗位职责与绩效目标**(如何清晰界定工作范围和KPI) + * B. **薪酬结构与发放**(基本工资、奖金、期权/股权如何约定) + * C. **保密与知识产权**(工作成果归属、保密义务范围) + * D. **合同期限与试用期**(试用期时长、转正标准) + * E. **解除条件与竞业限制**(什么情况下可解除、是否需竞业限制) + +了解你最关注的要点,能让合同律师在起草时重点着墨,平衡激励与风险控制。 + +## 2026-03-06 — a + +* **“A”是选择题的明确答案**:用户用单个字母回答选择题,表示已从预设选项中做出选择,应直接采纳其对应含义(此处为“关键岗位员工”),无需再次确认。 +* **信息收集需结构化推进**:使用选择题(A/B/C/D)是高效、清晰的沟通方式,符合产品经理偏好,能快速锁定关键变量(如聘用对象类型)。 +* **下一步应聚焦“核心商业关切”**:在确认聘用对象类型后,下一步必须聚焦于用户的核心关切(如职责、薪酬、知识产权、解除条件),这是起草有效聘用书的基础。 +* **沟通需按逻辑顺序高效推进**:遵循“对象类型 → 核心条款”的提问逻辑,使用选择题,符合产品经理(第五季)结构化、高效的沟通偏好。 + +## 2026-03-06 — 全部,合同一年一签,这样就不怕赔 + +* **用户将“一年一签”视为核心风险管理工具**:用户明确提出此策略以规避长期雇佣带来的高额赔偿风险,这反映了其作为产品经理对成本控制和风险前置的强烈诉求。 +* **“全部”选项表明对全面风险覆盖的期望**:用户选择关注所有核心条款,验证了其对聘用书作为**全面管理工具**的定位,期望其同时具备激励、约束和风险隔离功能。 +* **沟通需引导用户理解“一年一签”的双面性**:在后续工作中,必须分析此策略的利弊(如降低长期赔偿风险 vs. 可能影响核心员工稳定性与归属感),提供平衡建议。 +* **产出需高度结构化以匹配用户角色**:交付物必须超越合同文本,提供产品经理角色所重视的**结构化分析**(Pros & Cons、谈判要点、执行清单),这是满足其需求的关键。 + +## 2026-03-06 — ok,可以的 + +* **用户明确确认是启动任务的最终信号**:当用户对总结的工作方案(包含执行人、任务、产出)回复“ok”或“可以的”等肯定词时,表明需求确认流程已闭环,应立即执行任务分配。 +* **工作方案需具体透明以获得确认**:向用户(尤其是产品经理)展示的计划必须包含明确的**执行人(@谁)、任务(做什么)、产出(交付什么)**,这符合其对流程可控和结果预期的要求。 +* **“确认后执行”是核心工作纪律**:必须严格遵守“先确认方案,再分配任务”的流程。用户的确认是解除“需求确认”技能中的唯一条件。 +* **任务分配指令需结构化且包含全部背景**:在@团队成员时,应清晰复述用户确认过的所有关键背景信息(合作方、内容、核心关切),并给出明确的起草/审查要求,确保工作方向准确。 + +## 2026-03-06 — 月薪 10k ,肯定要挂钩KPI , 没奖金, + +* **用户核心诉求是“零成本解约”**:明确要求薪酬(10k)与KPI强挂钩,且**无奖金、无竞业限制、不支付不续签补偿**,将全部管理杠杆和风险控制都押注在绩效条款上。 +* **“绩效挂钩”是唯一的管理与风险控制工具**:用户意图通过将“续签/不续签”与“KPI达标与否”在法律上直接绑定,来实现规避经济补偿的目的。这要求合同必须将绩效考核程序、标准和结果应用设计得极其严密。 +* **合同需为“无补偿解约”构建合法路径**:起草的核心挑战是,在劳动法框架下,将“合同期满不续签”这一通常需支付经济补偿(N)的情形,转化为因“劳动者不能胜任工作”而合法终止且无需补偿的情形。这高度依赖合同中对“不胜任”的明确界定和有效证明。 +* **沟通需引导用户关注“程序风险”**:用户方案的法律风险集中于**绩效考核的合理性与程序正当性**。未来建议需重点提示,若考核标准模糊、程序不公或证据不足,该策略极易被认定为违法终止,反而面临支付赔偿金(2N)的风险。 diff --git a/internal/agent/agent.go b/internal/agent/agent.go index 67e736c..74e585d 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -97,10 +97,15 @@ func (a *Agent) Chat(ctx context.Context, msgs []llm.Message, onToken func(strin return a.client.Stream(ctx, msgs, onToken) } -// BuildSystemPrompt constructs the full system prompt with soul + memory + injected context. +// BuildSystemPrompt 构建完整的 system prompt,每次实时读取 SOUL.md 以支持热更新。 func (a *Agent) BuildSystemPrompt(extraContext string) string { + // 实时读取 SOUL.md,支持在页面上修改后立即生效 + soul := a.Soul + if data, err := os.ReadFile(filepath.Join(a.Dir, "SOUL.md")); err == nil { + soul = string(data) + } var sb strings.Builder - sb.WriteString(a.Soul) + sb.WriteString(soul) if mem := a.Memory(); mem != "" { sb.WriteString("\n\n\n") sb.WriteString(mem) diff --git a/internal/api/server.go b/internal/api/server.go index d65a540..6d3bc22 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io" + "log" "net/http" "os" "path/filepath" @@ -81,6 +82,7 @@ func (s *Server) routes() { g.GET("/rooms/:id/workspace", s.listWorkspace) g.GET("/rooms/:id/workspace/:file", s.getWorkspaceFile) + g.PUT("/rooms/:id/workspace/:file", s.putWorkspaceFile) g.GET("/rooms/:id/tasks", s.getTasks) g.GET("/rooms/:id/history", s.listHistory) g.GET("/rooms/:id/messages", s.getMessages) @@ -167,8 +169,9 @@ func (s *Server) wsHandler(c echo.Context) error { Type string `json:"type"` Content string `json:"content"` UserName string `json:"user_name"` + Mode string `json:"mode"` } - if json.Unmarshal(msg, &ev) != nil || ev.Type != "user_message" { + if json.Unmarshal(msg, &ev) != nil { continue } s.mu.RLock() @@ -177,11 +180,23 @@ func (s *Server) wsHandler(c echo.Context) error { if r == nil { continue } + if ev.Type == "set_mode" && (ev.Mode == "plan" || ev.Mode == "build") { + r.Mode = ev.Mode + s.broadcast(roomID, room.Event{Type: room.EvtModeChange, RoomID: roomID, Mode: ev.Mode}) + continue + } + if ev.Type != "user_message" { + continue + } userName := ev.UserName if userName == "" { userName = s.user.GetName() } - go r.HandleUserMessage(context.Background(), userName, ev.Content) + go func() { + if err := r.HandleUserMessage(context.Background(), userName, ev.Content); err != nil { + log.Printf("[room %s] HandleUserMessage error: %v", roomID, err) + } + }() } return nil } @@ -200,10 +215,15 @@ func (s *Server) listRooms(c echo.Context) error { Members []string `json:"members"` Color string `json:"color"` Team string `json:"team"` + Mode string `json:"mode"` } var list []roomInfo for id, r := range s.rooms { - list = append(list, roomInfo{ID: id, Name: r.Config.Name, Type: string(r.Config.Type), Status: r.Status, Master: r.Config.Master, Members: r.Config.Members, Color: r.Config.Color, Team: r.Config.Team}) + mode := r.Mode + if mode == "" { + mode = "plan" + } + list = append(list, roomInfo{ID: id, Name: r.Config.Name, Type: string(r.Config.Type), Status: r.Status, Master: r.Config.Master, Members: r.Config.Members, Color: r.Config.Color, Team: r.Config.Team, Mode: mode}) } return c.JSON(200, list) } @@ -214,7 +234,7 @@ func (s *Server) createRoom(c echo.Context) error { return err } if cfg.Type == "" { - cfg.Type = room.TypeDept + cfg.Type = room.TypeProject } dir := filepath.Join(s.roomsDir, cfg.Name) os.MkdirAll(filepath.Join(dir, "workspace"), 0755) @@ -463,6 +483,23 @@ func (s *Server) getWorkspaceFile(c echo.Context) error { return c.JSON(200, map[string]string{"content": string(data)}) } +func (s *Server) putWorkspaceFile(c echo.Context) error { + id := c.Param("id") + file := c.Param("file") + var body struct { + Content string `json:"content"` + } + if err := c.Bind(&body); err != nil { + return err + } + dir := filepath.Join(s.roomsDir, id, "workspace") + os.MkdirAll(dir, 0755) + if err := os.WriteFile(filepath.Join(dir, file), []byte(body.Content), 0644); err != nil { + return err + } + return c.JSON(200, map[string]string{"status": "ok"}) +} + func (s *Server) getMessages(c echo.Context) error { id := c.Param("id") historyFile := filepath.Join(s.roomsDir, id, "history", time.Now().Format("2006-01-02")+".md") diff --git a/internal/room/room.go b/internal/room/room.go index 6a1db31..f7da9d6 100644 --- a/internal/room/room.go +++ b/internal/room/room.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "log" "os" "path/filepath" "strings" @@ -20,8 +21,7 @@ import ( type RoomType string const ( - TypeDept RoomType = "dept" - TypeLeader RoomType = "leader" + TypeProject RoomType = "project" ) type Status string @@ -51,17 +51,32 @@ type Room struct { Status Status ActiveAgent string // for working status display Broadcast func(Event) // set by api layer + + // master 的会话历史,保持多轮对话上下文 + masterHistory []llm.Message + historyMu sync.Mutex + + Mode string // "plan" | "build" + pendingAssignments map[string]string // plan 模式下暂存的待执行任务 + pendingPlanReply string // master 规划原文,用于生成计划文档 + + // Build 模式下成员对话跟踪 + memberConvos map[string][]llm.Message // 成员名 -> 多轮对话历史 + lastActiveMember string // 最后一个发出提问的成员 + planFilename string // 当前任务计划文件名 } type EventType string const ( - EvtAgentMessage EventType = "agent_message" - EvtTaskAssign EventType = "task_assign" - EvtReview EventType = "review" - EvtRoomStatus EventType = "room_status" - EvtTasksUpdate EventType = "tasks_update" + EvtAgentMessage EventType = "agent_message" + EvtTaskAssign EventType = "task_assign" + EvtReview EventType = "review" + EvtRoomStatus EventType = "room_status" + EvtTasksUpdate EventType = "tasks_update" EvtWorkspaceFile EventType = "workspace_file" + EvtModeChange EventType = "mode_change" + EvtArtifact EventType = "artifact" ) type Event struct { @@ -79,6 +94,8 @@ type Event struct { ActiveAgent string `json:"active_agent,omitempty"` Action string `json:"action,omitempty"` Filename string `json:"filename,omitempty"` + Mode string `json:"mode,omitempty"` + Title string `json:"title,omitempty"` } type BoardEntry struct { @@ -123,7 +140,7 @@ func Load(roomDir string, agentsDir string, skillsDir string) (*Room, error) { return nil, err } - r := &Room{Config: cfg, Dir: roomDir, members: make(map[string]*agent.Agent)} + r := &Room{Config: cfg, Dir: roomDir, members: make(map[string]*agent.Agent), Mode: "plan"} if cfg.Master != "" { agentPath := resolveAgentPath(agentsDir, cfg.Team, cfg.Master) @@ -151,6 +168,11 @@ func (r *Room) emit(e Event) { } } +func (r *Room) setMode(mode string) { + r.Mode = mode + r.emit(Event{Type: EvtModeChange, Mode: mode}) +} + func (r *Room) setStatus(s Status, activeAgent, action string) { r.Status = s r.ActiveAgent = activeAgent @@ -187,6 +209,13 @@ func (r *Room) runMembersParallel(ctx context.Context, assignments map[string]st r.setStatus(StatusWorking, member.Config.Name, t) r.emit(Event{Type: EvtTaskAssign, From: r.master.Config.Name, To: name, Task: t}) + // Build 模式:发送简要状态消息,不流式输出内容 + taskBrief := t + if len(taskBrief) > 60 { + taskBrief = taskBrief[:60] + "..." + } + r.emit(Event{Type: EvtAgentMessage, Agent: name, Role: "member", Content: fmt.Sprintf("正在处理: %s", taskBrief)}) + boardCtx := board.ToContext() extraCtx := skillXML if boardCtx != "" { @@ -197,10 +226,10 @@ func (r *Room) runMembersParallel(ctx context.Context, assignments map[string]st llm.NewMsg("system", memberSystem), llm.NewMsg("user", t), } + // 静默收集输出,不流式推送到聊天 var memberReply strings.Builder _, err := member.Chat(ctx, memberMsgs, func(token string) { memberReply.WriteString(token) - r.emit(Event{Type: EvtAgentMessage, Agent: name, Role: "member", Content: token, Streaming: true}) }) if err != nil { mu.Lock() @@ -215,10 +244,23 @@ func (r *Room) runMembersParallel(ctx context.Context, assignments map[string]st r.AppendHistory("member", name, result) board.Add(name, result, "draft") - if strings.Contains(result, "# ") { + // 保存成员对话历史,支持后续多轮交互 + if r.memberConvos == nil { + r.memberConvos = make(map[string][]llm.Message) + } + r.memberConvos[name] = memberMsgs + r.memberConvos[name] = append(r.memberConvos[name], llm.NewMsg("assistant", result)) + + // 智能判断:文档 → artifact,对话/提问 → 聊天消息 + if isDocument(result) { + title := extractTitle(result) filename := fmt.Sprintf("%s-%s.md", name, time.Now().Format("20060102-150405")) r.saveWorkspace(filename, result) - r.emit(Event{Type: EvtWorkspaceFile, Filename: filename, Content: result}) + r.emit(Event{Type: EvtArtifact, Agent: name, Filename: filename, Title: title}) + } else { + // 非文档内容(提问、简短回复等)直接显示在聊天中 + r.emit(Event{Type: EvtAgentMessage, Agent: name, Role: "member", Content: result}) + r.lastActiveMember = name } }(memberName, task) } @@ -262,6 +304,7 @@ func (r *Room) runChallengeRound(ctx context.Context, board *SharedBoard, skillX if err != nil { return } + r.emit(Event{Type: EvtAgentMessage, Agent: n, Role: "challenge", Content: "", Streaming: false}) result := reply.String() if strings.Contains(result, "CHALLENGE:") { board.Add(n, result, "challenge") @@ -277,61 +320,213 @@ func (r *Room) Handle(ctx context.Context, userMsg string) error { return r.HandleUserMessage(ctx, "user", userMsg) } -// HandleUserMessage processes a user message with a specific user name. +// parseUserMentions 从用户消息中提取 @agent 指派。 +// 返回指派 map 和去除 @agent 后的剩余消息。 +func parseUserMentions(text string, validMembers map[string]*agent.Agent) map[string]string { + assignments := make(map[string]string) + for _, line := range strings.Split(text, "\n") { + line = strings.TrimSpace(line) + if !strings.HasPrefix(line, "@") { + continue + } + rest := strings.TrimPrefix(line, "@") + idx := strings.IndexAny(rest, " \t") + if idx <= 0 { + // 只有 @name 没有任务内容,跳过 + continue + } + name := strings.TrimSpace(rest[:idx]) + task := strings.TrimSpace(rest[idx+1:]) + if _, ok := validMembers[name]; ok && task != "" { + assignments[name] = task + } + } + return assignments +} + +// HandleUserMessage 处理用户消息。 +// 如果用户消息中包含 @agent,直接将任务分配给对应 agent,不经过 master。 func (r *Room) HandleUserMessage(ctx context.Context, userName, userMsg string) error { if r.master == nil { return fmt.Errorf("room has no master agent configured") } r.AppendHistory("user", userName, userMsg) + + // 检测用户是否直接 @agent 指派任务 + userAssignments := parseUserMentions(userMsg, r.members) + if len(userAssignments) > 0 { + return r.handleDirectAssign(ctx, userAssignments) + } + + // Build 模式下,如果有 plan 阶段暂存的待执行任务,直接执行 + if r.Mode == "build" && len(r.pendingAssignments) > 0 { + log.Printf("[room %s] build 模式,执行 %d 个暂存任务", r.Config.Name, len(r.pendingAssignments)) + assignments := r.pendingAssignments + r.pendingAssignments = nil + + // 生成任务计划文档 + r.planFilename = fmt.Sprintf("任务计划-%s.md", time.Now().Format("20060102-150405")) + planContent := "# 任务计划\n\n## 规划\n\n" + r.pendingPlanReply + "\n" + r.pendingPlanReply = "" + r.saveWorkspace(r.planFilename, planContent) + r.emit(Event{Type: EvtArtifact, Agent: r.master.Config.Name, Filename: r.planFilename, Title: "任务计划"}) + + skillXML := skill.ToXML(r.skillMeta) + board := &SharedBoard{} + r.setStatus(StatusWorking, "", "") + r.runMembersParallel(ctx, assignments, board, skillXML) + r.runChallengeRound(ctx, board, skillXML) + + r.setStatus(StatusPending, "", "") + return nil + } + + // Build 模式下,成员发出了提问,用户回复 → 转发给该成员继续对话 + if r.Mode == "build" && r.lastActiveMember != "" { + memberName := r.lastActiveMember + member, ok := r.members[memberName] + if !ok { + return fmt.Errorf("member %s not found", memberName) + } + log.Printf("[room %s] build 模式,将用户回复转发给 %s", r.Config.Name, memberName) + r.setStatus(StatusWorking, member.Config.Name, "") + + // 追加用户回复到成员对话历史 + if r.memberConvos == nil { + r.memberConvos = make(map[string][]llm.Message) + } + r.memberConvos[memberName] = append(r.memberConvos[memberName], llm.NewMsg("user", userMsg)) + + // 追加沟通记录到任务计划文档 + r.appendPlanLog(userName, memberName, userMsg) + + // 让成员继续对话 + var memberReply strings.Builder + _, err := member.Chat(ctx, r.memberConvos[memberName], func(token string) { + memberReply.WriteString(token) + }) + if err != nil { + r.setStatus(StatusPending, "", "") + return err + } + result := memberReply.String() + r.memberConvos[memberName] = append(r.memberConvos[memberName], llm.NewMsg("assistant", result)) + r.AppendHistory("member", memberName, result) + + // 追加成员回复到任务计划文档 + r.appendPlanLog(memberName, userName, result) + + // 智能判断输出类型 + if isDocument(result) { + title := extractTitle(result) + filename := fmt.Sprintf("%s-%s.md", memberName, time.Now().Format("20060102-150405")) + r.saveWorkspace(filename, result) + r.emit(Event{Type: EvtArtifact, Agent: memberName, Filename: filename, Title: title}) + r.lastActiveMember = "" // 文档产出,对话结束 + } else { + r.emit(Event{Type: EvtAgentMessage, Agent: memberName, Role: "member", Content: result}) + // lastActiveMember 保持不变,用户可以继续回复 + } + + r.setStatus(StatusPending, "", "") + return nil + } + r.setStatus(StatusThinking, "", "") - // Build master context + // 构建 system prompt teamXML := r.buildTeamXML() skillXML := skill.ToXML(r.skillMeta) - - // Build user info XML var userXML string if r.User != nil { userXML = r.User.BuildUserXML() } - extraContext := userXML + "\n\n" + teamXML + "\n\n" + skillXML systemPrompt := r.master.BuildSystemPrompt(extraContext) + sysMsg := llm.NewMsg("system", systemPrompt+fmt.Sprintf(` - masterMsgs := []llm.Message{ - llm.NewMsg("system", systemPrompt+"\n\nYou are the master of this team. When you need a team member to do something, output a line like: ASSIGN::. When you are done reviewing and satisfied, output DONE:."), - llm.NewMsg("user", userMsg), +当前用户:%s + +分配任务给成员时,使用 @ 格式,每行一个: +@成员名 任务描述 + +直接回复用户时,正常说话即可,不需要 @。`, userName)) + + // 使用持久化的会话历史 + r.historyMu.Lock() + // 始终更新 system prompt(可能 SOUL.md 改了) + if len(r.masterHistory) == 0 { + r.masterHistory = []llm.Message{sysMsg} + } else { + r.masterHistory[0] = sysMsg + } + r.masterHistory = append(r.masterHistory, llm.NewMsg("user", userMsg)) + + // 限制历史长度,保留 system + 最近 20 轮对话 + if len(r.masterHistory) > 41 { + r.masterHistory = append(r.masterHistory[:1], r.masterHistory[len(r.masterHistory)-40:]...) } - // Master planning loop + masterMsgs := make([]llm.Message, len(r.masterHistory)) + copy(masterMsgs, r.masterHistory) + r.historyMu.Unlock() + + // Master 规划循环 for iteration := 0; iteration < 5; iteration++ { + log.Printf("[room %s] master iteration %d, sending to LLM...", r.Config.Name, iteration) var masterReply strings.Builder _, err := r.master.Chat(ctx, masterMsgs, func(token string) { masterReply.WriteString(token) r.emit(Event{Type: EvtAgentMessage, Agent: r.master.Config.Name, Role: "master", Content: token, Streaming: true}) }) if err != nil { + log.Printf("[room %s] master chat error: %v", r.Config.Name, err) return err } reply := masterReply.String() - masterMsgs = append(masterMsgs, llm.NewMsg("assistant", reply)) + log.Printf("[room %s] master reply (%d chars): %.100s...", r.Config.Name, len(reply), reply) + // 发送 streaming 结束信号 + r.emit(Event{Type: EvtAgentMessage, Agent: r.master.Config.Name, Role: "master", Content: "", Streaming: false}) + assistantMsg := llm.NewMsg("assistant", reply) + masterMsgs = append(masterMsgs, assistantMsg) + // 同步到持久化历史 + r.historyMu.Lock() + r.masterHistory = append(r.masterHistory, assistantMsg) + r.historyMu.Unlock() r.AppendHistory("master", r.master.Config.Name, reply) - // Parse assignments - assignments := parseAssignments(reply) + // 解析 @ 指令,只匹配已知成员名 + allMentions := parseAssignments(reply) + assignments := make(map[string]string) + for name, task := range allMentions { + if _, isMember := r.members[name]; isMember { + assignments[name] = task + } + } + if len(assignments) == 0 { - // No assignments, master is done + // 没有分配给任何成员的任务,master 直接回复了用户 break } - // Execute assignments in parallel + // Plan 模式下不执行任务,暂存任务,提示用户切换到 Build 模式 + if r.Mode != "build" { + r.pendingAssignments = assignments + r.pendingPlanReply = reply + log.Printf("[room %s] plan 模式,暂存 %d 个任务,等待用户切换到 build 模式", r.Config.Name, len(assignments)) + r.emit(Event{Type: EvtAgentMessage, Agent: r.master.Config.Name, Role: "master", + Content: "已完成任务规划。请按 Tab 切换到 Build 模式,然后发送确认开始执行。"}) + break + } + + // 并行执行成员任务 board := &SharedBoard{} results := r.runMembersParallel(ctx, assignments, board, skillXML) - // Run challenge round + // 质疑轮 r.runChallengeRound(ctx, board, skillXML) - // Feed results back to master for review + // 将结果反馈给 master 审查 r.setStatus(StatusThinking, "", "") var resultsStr strings.Builder for memberName, result := range results { @@ -342,15 +537,16 @@ func (r *Room) HandleUserMessage(ctx context.Context, userName, userMsg string) if boardCtx != "" { feedbackMsg += "\n\nTeam board:\n" + boardCtx } - feedbackMsg += "\n\nPlease review. If satisfied output DONE:, otherwise output ASSIGN instructions for revisions." - masterMsgs = append(masterMsgs, llm.NewMsg("user", feedbackMsg)) + feedbackMsg += "\n\n请审查以上结果。如果满意,直接回复总结给用户即可。如果需要修改,使用 @成员名 分配修订任务。" + feedbackLLMMsg := llm.NewMsg("user", feedbackMsg) + masterMsgs = append(masterMsgs, feedbackLLMMsg) + // 同步反馈消息到持久化历史 + r.historyMu.Lock() + r.masterHistory = append(r.masterHistory, feedbackLLMMsg) + r.historyMu.Unlock() - // Update tasks + // 更新任务列表 r.updateTasks(masterMsgs) - - if strings.Contains(reply, "DONE:") { - break - } } r.setStatus(StatusPending, "", "") @@ -361,6 +557,18 @@ func (r *Room) HandleUserMessage(ctx context.Context, userName, userMsg string) return nil } +// handleDirectAssign 处理用户直接 @agent 指派的任务,跳过 master 规划。 +func (r *Room) handleDirectAssign(ctx context.Context, assignments map[string]string) error { + skillXML := skill.ToXML(r.skillMeta) + board := &SharedBoard{} + + r.runMembersParallel(ctx, assignments, board, skillXML) + r.runChallengeRound(ctx, board, skillXML) + + r.setStatus(StatusPending, "", "") + return nil +} + func (r *Room) updateMasterMemory(ctx context.Context, task string, msgs []llm.Message) { summaryPrompt := fmt.Sprintf("Based on this task: %q\nSummarize key learnings and patterns in 3-5 bullet points for future reference. Be concise.", task) memMsgs := append(msgs, llm.NewMsg("user", summaryPrompt)) @@ -374,6 +582,23 @@ func (r *Room) updateMasterMemory(ctx context.Context, task string, msgs []llm.M r.master.SaveMemory(filename, content) } +// isDocument 判断内容是否为文档产出物(而非对话/提问)。 +// 文档特征:包含 markdown 标题且内容较长。 +func isDocument(content string) bool { + hasHeading := strings.Contains(content, "\n# ") || strings.HasPrefix(content, "# ") + return hasHeading && len([]rune(content)) > 500 +} + +func extractTitle(content string) string { + for _, line := range strings.Split(content, "\n") { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "# ") { + return strings.TrimPrefix(line, "# ") + } + } + return "" +} + func min(a, b int) int { if a < b { return a @@ -381,16 +606,61 @@ func min(a, b int) int { return b } +// parseAssignments 解析任务分配指令。 +// 支持多行任务描述:从 @成员名 开始,到下一个 @成员名 或文本结束为止。 func parseAssignments(text string) map[string]string { result := make(map[string]string) - for _, line := range strings.Split(text, "\n") { - if strings.HasPrefix(line, "ASSIGN:") { - parts := strings.SplitN(strings.TrimPrefix(line, "ASSIGN:"), ":", 2) - if len(parts) == 2 { - result[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) + lines := strings.Split(text, "\n") + + var currentName string + var currentTask strings.Builder + + flush := func() { + if currentName != "" { + task := strings.TrimSpace(currentTask.String()) + if task != "" { + result[currentName] = task } } + currentName = "" + currentTask.Reset() } + + for _, line := range lines { + trimmed := strings.TrimSpace(line) + // 检测新的 @成员名 行 + if strings.HasPrefix(trimmed, "@") { + rest := strings.TrimPrefix(trimmed, "@") + if idx := strings.IndexAny(rest, " \t"); idx > 0 { + name := strings.TrimSpace(rest[:idx]) + task := strings.TrimSpace(rest[idx+1:]) + // 保存前一个 + flush() + currentName = name + if task != "" { + currentTask.WriteString(task) + } + continue + } + } + // ASSIGN:成员名:任务描述(向后兼容) + if strings.HasPrefix(trimmed, "ASSIGN:") { + parts := strings.SplitN(strings.TrimPrefix(trimmed, "ASSIGN:"), ":", 2) + if len(parts) == 2 { + flush() + result[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) + } + continue + } + // 当前有 @成员名 在收集中,追加后续行作为任务描述 + if currentName != "" && trimmed != "" { + if currentTask.Len() > 0 { + currentTask.WriteString("\n") + } + currentTask.WriteString(trimmed) + } + } + flush() return result } @@ -404,6 +674,22 @@ func (r *Room) buildTeamXML() string { return sb.String() } +// appendPlanLog 将沟通记录追加到任务计划文档 +func (r *Room) appendPlanLog(from, to, content string) { + if r.planFilename == "" { + return + } + path := filepath.Join(r.Dir, "workspace", r.planFilename) + f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + return + } + defer f.Close() + entry := fmt.Sprintf("\n---\n**[%s] %s → %s**\n\n%s\n", + time.Now().Format("15:04:05"), from, to, content) + f.WriteString(entry) +} + func (r *Room) saveWorkspace(filename, content string) { dir := filepath.Join(r.Dir, "workspace") os.MkdirAll(dir, 0755) @@ -411,18 +697,14 @@ func (r *Room) saveWorkspace(filename, content string) { } func (r *Room) updateTasks(msgs []llm.Message) { - // Extract task list from conversation and save + // 从对话中提取任务列表并保存 var tasks strings.Builder tasks.WriteString("# Tasks\n\n") for _, m := range msgs { - if m.Role == "assistant" && strings.Contains(m.Content, "ASSIGN:") { - for _, line := range strings.Split(m.Content, "\n") { - if strings.HasPrefix(line, "ASSIGN:") { - parts := strings.SplitN(strings.TrimPrefix(line, "ASSIGN:"), ":", 2) - if len(parts) == 2 { - tasks.WriteString(fmt.Sprintf("- [ ] [%s] %s\n", strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]))) - } - } + if m.Role == "assistant" { + assignments := parseAssignments(m.Content) + for name, task := range assignments { + tasks.WriteString(fmt.Sprintf("- [ ] [%s] %s\n", name, task)) } } } diff --git a/internal/skill/skill.go b/internal/skill/skill.go index f835b91..08bc38f 100644 --- a/internal/skill/skill.go +++ b/internal/skill/skill.go @@ -21,29 +21,25 @@ type Skill struct { Body string // full SKILL.md body (instructions) } -// Discover scans skillsDir and returns metadata for all valid skills. +// Discover 递归扫描 skillsDir,返回所有包含 SKILL.md 的目录的元数据。 func Discover(skillsDir string) ([]Meta, error) { - entries, err := os.ReadDir(skillsDir) - if err != nil { - return nil, err - } var metas []Meta - for _, e := range entries { - if !e.IsDir() { - continue + filepath.Walk(skillsDir, func(path string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() || info.Name() != "SKILL.md" { + return nil } - path := filepath.Join(skillsDir, e.Name(), "SKILL.md") data, err := os.ReadFile(path) if err != nil { - continue + return nil } meta, err := parseMeta(data) if err != nil { - continue + return nil } - meta.Path = filepath.Join(skillsDir, e.Name()) + meta.Path = filepath.Dir(path) metas = append(metas, meta) - } + return nil + }) return metas, nil } @@ -63,12 +59,20 @@ func Load(skillDir string) (*Skill, error) { } // ToXML generates XML for agent system prompts. +// 如果 skill 有完整内容,会注入到 prompt 中。 func ToXML(metas []Meta) string { var sb strings.Builder sb.WriteString("\n") for _, m := range metas { - fmt.Fprintf(&sb, " \n %s\n %s\n %s/SKILL.md\n \n", - m.Name, m.Description, m.Path) + // 尝试加载完整 skill 内容 + s, err := Load(m.Path) + if err == nil && s.Body != "" { + fmt.Fprintf(&sb, " \n %s\n \n%s\n \n \n", + m.Name, m.Description, s.Body) + } else { + fmt.Fprintf(&sb, " \n %s\n \n", + m.Name, m.Description) + } } sb.WriteString("") return sb.String() diff --git a/skills/需求确认/SKILL.md b/skills/需求确认/SKILL.md new file mode 100644 index 0000000..22ca6d0 --- /dev/null +++ b/skills/需求确认/SKILL.md @@ -0,0 +1,82 @@ +--- +name: 需求确认 +description: "在分配任务给团队成员之前,必须先使用此技能与用户确认需求。通过逐步提问收集关键信息,形成明确的任务方案后,才能开始分配工作。适用于所有需要团队协作的场景。" +--- + +# 需求确认技能 + +## 概述 + +在将任何任务分配给团队成员之前,必须先通过此流程与用户充分沟通,了解需求背景和具体要求。只有形成明确的工作方案并获得用户确认后,才能开始分配任务。 + + +在用户确认工作方案之前,绝对不能 @任何成员 分配任务。 +无论需求看起来多简单,都必须先确认。 + + +## 流程 + +### 第一步:判断意图 + +收到用户消息后,先判断类型: +- **闲聊/问候**(如"你好""在吗")→ 直接友好回复,不启动此流程 +- **明确需求**(如"帮我写一份合同""审查这份协议")→ 进入第二步 +- **模糊需求**(如"我有个法律问题""帮我看看这个")→ 先问用户具体是什么 + +### 第二步:逐步收集信息 + +每次只问一个问题,不要一次性抛出多个问题。优先使用选择题。 + +收集要素因场景而异,以下是常见场景的示例: + +**起草合同/协议类:** +1. 合作方是谁?(个人/公司/工作室) +2. 合作内容是什么?(具体做什么事) +3. 你最关心的条款?(如知识产权、分成、保密等) +4. 有没有预算或金额范围? +5. 合作期限? + +**法律咨询类:** +1. 涉及什么领域?(劳动、合同、公司治理等) +2. 当前面临的具体问题? +3. 已经采取了什么措施? +4. 期望达到什么结果? + +**审查文件类:** +1. 需要审查什么类型的文件? +2. 重点关注哪些方面? +3. 文件的背景和用途? + +### 第三步:总结确认 + +信息收集完后,向用户呈现工作方案: + +``` +根据你的需求,我的工作计划如下: + +1. @合同律师 负责 [具体任务] +2. @合规专员 负责 [具体任务] + +预计产出:[描述最终交付物] + +确认后我将开始安排工作,你看可以吗? +``` + +注意:此步骤中的 @ 只是向用户展示计划,不要作为独立行输出,以免触发任务执行。 + +### 第四步:执行 + +用户确认后,再正式分配任务: + +``` +@合同律师 [详细的任务描述,包含用户提供的所有背景信息] +@合规专员 [详细的任务描述] +``` + +## 关键原则 + +- **每次只问一个问题** — 不要让用户面对一堆问题 +- **优先选择题** — 比开放式问题更容易回答 +- **不要假设** — 不确定的信息一定要问 +- **方案要具体** — 让用户清楚知道接下来会发生什么 +- **尊重用户时间** — 如果信息已经足够,不要过度追问 diff --git a/teams/legal-team/team.yaml b/teams/legal-team/team.yaml index 4ac299f..f862434 100644 --- a/teams/legal-team/team.yaml +++ b/teams/legal-team/team.yaml @@ -10,5 +10,6 @@ agents: skills: - 合同审查 - 法律知识库 + - 需求确认 -installed_at: 2026-03-05 +installed_at: 2026-03-06 diff --git a/web/src/App.tsx b/web/src/App.tsx index 6c04436..d669271 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -22,7 +22,22 @@ export default function App() { useEffect(() => { fetchUser() - fetchRooms() + fetchRooms().then(() => { + const savedRoomId = useStore.getState().activeRoomId + if (savedRoomId) { + useStore.getState().connectRoom(savedRoomId) + // 加载历史消息、任务、工作区 + fetch(`${API}/rooms/${savedRoomId}/messages`).then(r => r.json()).then(msgs => { + if (msgs?.length) useStore.setState(s => ({ messages: { ...s.messages, [savedRoomId]: msgs } })) + }).catch(() => {}) + fetch(`${API}/rooms/${savedRoomId}/tasks`).then(r => r.json()).then(d => { + useStore.setState(s => ({ tasks: { ...s.tasks, [savedRoomId]: d.content } })) + }).catch(() => {}) + fetch(`${API}/rooms/${savedRoomId}/workspace`).then(r => r.json()).then(files => { + useStore.setState(s => ({ workspace: { ...s.workspace, [savedRoomId]: files || [] } })) + }).catch(() => {}) + } + }) }, [fetchUser, fetchRooms]) const handleOnboardingComplete = () => { @@ -36,7 +51,7 @@ export default function App() { const color = randomColor() await fetch(`${API}/rooms`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name: newRoomName.trim(), type: 'dept', color, master: '', members: [] }) + body: JSON.stringify({ name: newRoomName.trim(), type: 'project', color, master: '', members: [] }) }) setCreating(false) setNewRoomName('') @@ -85,7 +100,7 @@ export default function App() { {/* Add room button */} - setInput(e.target.value)} - onKeyDown={e => { - if (e.key === 'Enter' && !e.shiftKey && input.trim()) { - sendMessage(room.id, input.trim()) - setInput('') - } - }} - /> - - +
+ {/* @ Mention dropdown */} + {mentionQuery !== null && filteredMentions.length > 0 && ( +
+
+ 成员 +
+ {filteredMentions.map((name, i) => { + const isMaster = name === room.master + return ( + + ) + })} +
+ )} + + {/* Input bar with mode toggle */} +
+ {/* Mode toggle - OpenCode style */} + +
+