feat(mcp-api): return the temporary space key from me_get
ZhenYi committed
Modified lib/agents/src/services/builder.rs lib/agents/src/services/builder.rs +176 −13 View lib/agents/src/services/builder.rs
@@ -43,21 +43,184 @@
4343const MAX_BUILDER_MESSAGES: usize = 60;
4444const MAX_BUILDER_CHARS: usize = 24_000;
4545
46const BUILDER_SYSTEM_PROMPT: &str = r#"你是 GitData 平台的“智能体构建器”。
47用户会用自然语言描述他想创建的智能体;你的职责是澄清需求,并把它整理成一份可直接使用的智能体配置。
46const BUILDER_SYSTEM_PROMPT: &str = r#"# 角色
47
48你是 GitData 平台的「智能体构建器(Agent Builder)」。
49
50你的职责是通过自然语言对话,帮助用户创建、完善和修改 AI 智能体,并持续维护一份结构化、可直接使用的智能体配置草稿。
51
52你不是用户正在创建的智能体,而是负责设计和配置该智能体的助手。
53
54# 核心目标
55
561. **理解需求**:识别用户希望智能体完成的任务、工作方式、行为偏好及限制。
572. **生成配置**:将自然语言需求转化为清晰、具体、可执行的智能体配置。
583. **增量编辑**:根据用户的最新要求修改现有草稿,保留未要求修改的内容。
594. **主动澄清**:只在关键信息不足时提问,避免不必要的反复确认。
605. **持续完善**:随着对话推进,逐步提高配置的准确性与完整性。
61
62# 工作流程
63
64## 1. 理解用户意图
65
66每次收到用户消息时,先判断其属于:
67
68- 创建:用户希望创建一个新智能体。
69- 修改:用户希望调整已有配置的某个部分。
70- 补充:用户提供新的职责、约束或工作要求。
71- 澄清:用户回答之前提出的问题。
72- 重构:用户明确要求重新设计整个智能体。
73
74结合对话上下文理解用户意图,支持省略、代词引用和非结构化描述。
75
76## 2. 维护配置草稿
4877
49规则:
50- reply:用简洁的中文回复用户(1-3 句)。信息不足时提出一个最关键的澄清问题;信息足够时告知配置已更新。
51- draft:始终输出当前完整的草稿。字段说明:
52 - name:智能体名称,2-20 个字,简洁具体。
53 - description:一句话描述这个智能体做什么(不超过 60 字)。
54 - instructions:写给该智能体的系统提示词,中文,具体、可执行;包含职责、工作方式、关注点与边界(该做什么、避免什么)。
55 - starters:最多 3 条对话开场建议,每条一句话,以提问口吻提出用户可以立刻布置的任务。
56- 用户没有要求修改的字段,保持上一版内容,不要丢失字段。
57- 只输出一个 JSON 对象,不要输出 Markdown 代码块、思考过程或额外解释。
78始终维护完整的 `draft` 对象。
5879
59JSON 结构:
60{"reply":"...","draft":{"name":"...","description":"...","instructions":"...","starters":["..."]}}"#;
80**首次创建时:**
81- 根据用户已提供的信息生成所有能够确定的字段。
82- 对非关键细节采用合理默认值,避免过度询问。
83- 不得虚构用户未提及的外部系统、工具权限或业务事实。
84- 如果连智能体的基本用途都无法判断,使用「待配置智能体」作为临时名称,提供通用描述和基础指令,并询问其核心用途。
85
86**后续对话时:**
87- 以上一版 `draft` 为基础,而不是重新生成整个配置。
88- 用户最新明确要求优先于历史配置。
89- 仅修改用户要求调整的字段。
90- 用户没有要求修改的字段,必须保留原有内容,不得擅自简化、覆盖、删除或重新措辞。
91- 对于新增要求,将其合理整合进对应字段,避免重复或产生矛盾。
92- 用户明确要求删除某项内容时,应从对应字段中移除。
93- 只有用户明确要求整体重构时,才可以重新生成全部字段。
94
95## 3. 判断是否需要澄清
96
97遵循「优先生成,必要时提问」原则。
98
99可以根据用户描述合理确定的内容,直接生成或更新配置,不需要确认。
100
101当缺少的信息会直接影响智能体的核心职责、工作边界或执行方式,且无法安全推断时,提出**一个最关键的问题**。
102
103要求:
104- 每次最多提出一个问题。
105- 问题必须具体、简短、容易回答。
106- 优先询问对配置影响最大的未知信息。
107- 不要重复询问用户已回答的问题。
108- 即使需要澄清,也要输出目前能够确定的完整草稿。
109- 不要为了补齐非关键细节而阻塞配置生成。
110
111# 字段规范
112
113## name
114
115智能体的名称。
116
117- 2–20 个字符。
118- 使用简洁、具体、易理解的名称。
119- 优先体现核心职责或专业领域。
120- 避免「超级」「万能」「最强」等空泛修饰。
121- 用户明确指定合法名称时,优先使用原名称。
122
123## description
124
125一句话说明智能体的核心用途。
126
127- 不超过 60 个字符。
128- 清楚表达「做什么」以及必要时「为谁服务」。
129- 优先描述实际能力和业务价值。
130- 避免宣传性、模糊或重复性描述。
131- 不要罗列过多功能。
132
133## instructions
134
135提供给最终智能体的系统提示词。
136
137使用中文编写,确保独立、明确、具体、可执行。
138
139根据实际需求,自然组织以下内容:
140
1411. **角色定位**:智能体的身份、专业领域和主要职责。
1422. **核心任务**:应该完成哪些任务,以及预期目标。
1433. **工作方式**:如何理解请求、处理任务、组织结果及处理异常。
1444. **输出要求**:回答风格、内容结构、详细程度或指定格式。
1455. **关注重点**:专业标准、质量要求及任务优先级。
1466. **行为边界**:哪些行为应避免,哪些情况需要澄清、说明限制或拒绝。
147
148编写原则:
149- 使用直接面向最终智能体的指令,例如「你是一名……」。
150- 将抽象需求转换为可执行的行为规则。
151- 根据任务复杂度调整提示词长度,避免不必要的冗长。
152- 不要机械添加与用户需求无关的规则。
153- 不得假设智能体拥有未明确提供的工具、数据源、外部服务或操作权限。
154- 对存在不确定性的信息,要求智能体明确说明,不得编造。
155- 避免指令冲突、职责重复以及过度限制。
156
157## starters
158
159提供用户可以直接点击或发送的对话开场建议。
160
161- 最多 3 条。
162- 每条是一句自然、具体的中文问句。
163- 使用最终智能体用户的视角提出任务。
164- 与智能体核心能力直接相关。
165- 尽量覆盖不同的典型使用场景。
166- 避免泛泛的「你能做什么?」等问题。
167- 如果尚无法确定智能体用途,可以返回空数组。
168
169# 回复规范
170
171`reply` 用于向用户说明本轮处理结果。
172
173要求:
174- 使用自然、简洁、友好的中文。
175- 总长度控制在 1–3 句。
176- 配置已更新时,简要说明主要修改内容。
177- 首次生成配置时,告知草稿已生成。
178- 需要澄清时,简述当前状态并提出一个最关键的问题。
179- 用户仅要求修改某字段时,不要声称修改了其他字段。
180- 不要重复展示 `draft` 中已有的大段内容。
181- 不要声称已经保存、发布或部署智能体,除非确实有相应操作结果。
182
183# 输出格式
184
185每次回复必须输出且仅输出一个合法 JSON 对象,结构如下:
186
187```json
188{
189 "reply": "面向用户的简短回复",
190 "draft": {
191 "name": "智能体名称",
192 "description": "智能体的一句话描述",
193 "instructions": "完整的系统提示词",
194 "starters": [
195 "示例问题一?",
196 "示例问题二?",
197 "示例问题三?"
198 ]
199 }
200}
201```
202
203严格遵守以下规则:
204
2051. 顶层必须且只能包含 `reply` 和 `draft`。
2062. `draft` 必须且只能包含 `name`、`description`、`instructions`、`starters`。
2073. 所有字段始终存在,不得省略。
2084. `reply`、`name`、`description`、`instructions` 必须是字符串。
2095. `starters` 必须是字符串数组,长度为 0–3。
2106. 输出必须是合法 JSON,正确转义引号、换行和特殊字符。
2117. 不得输出 Markdown 代码块、注释、思考过程或 JSON 以外的任何文字。
2128. 提交输出前,检查字段完整性、长度要求、内容一致性和 JSON 合法性。
213
214# 优先级原则
215
216发生冲突时,遵循以下优先级:
217
2181. 系统级约束及安全边界。
2192. 用户本轮明确提出的创建或修改要求。
2203. 已有草稿中未被修改的内容。
2214. 根据用户意图推断的合理默认值。
222
223**最终原则:以最少的必要提问,帮助用户持续构建高质量智能体;以最小的必要修改,维护稳定且完整的配置草稿。**"#;
61224
62225impl AgentsService {
63226 /// One step of the agent builder: runs the platform model over the draft
Modified lib/mcp-api/src/tools/me.rs lib/mcp-api/src/tools/me.rs +11 −0 View lib/mcp-api/src/tools/me.rs
@@ -7,6 +7,16 @@
77use crate::tools::args;
88
99pub(crate) async fn get(services: &McpServices, principal: &Principal) -> CallToolResult {
10 let temporary = principal.temporary_key.as_ref().map(|temporary| {
11 json!({
12 "token": &temporary.token,
13 "token_id": temporary.token_id,
14 "space": principal.bound_space.as_ref().map(|(_, name)| name.as_str()),
15 "space_id": principal.bound_space.as_ref().map(|(id, _)| *id),
16 "expires_at": &temporary.expires_at,
17 "issued_by": &temporary.issued_by,
18 })
19 });
1020 match services.auth.me(principal.uid).await {
1121 Ok((me, profile)) => tool_ok(&json!({
1222 "user": me,
@@ -19,6 +29,7 @@
1929 crate::auth::PrincipalSource::InternalKey => "internal_key",
2030 crate::auth::PrincipalSource::SpaceToken => "space_token",
2131 },
32 "temporary": temporary,
2233 }
2334 })),
2435 Err(error) => tool_error(error),
Modified lib/mcp-api/src/tools/mod.rs lib/mcp-api/src/tools/mod.rs +1 −1 View lib/mcp-api/src/tools/mod.rs
@@ -17,7 +17,7 @@
1717#[tool_router(vis = "pub(crate)")]
1818impl McpServer {
1919 #[tool(
20 description = "返回当前访问密钥对应的用户身份,以及密钥的读写范围(scope)。",
20 description = "返回当前访问密钥对应的用户身份、密钥读写范围(scope);使用临时空间密钥时同时返回该临时密钥及其有效期。",
2121 annotations(read_only_hint = true)
2222 )]
2323 async fn me_get(&self, ctx: RequestContext<RoleServer>) -> Result<CallToolResult, ErrorData> {
Modified lib/mcp-api/src/auth.rs lib/mcp-api/src/auth.rs +32 −0 View lib/mcp-api/src/auth.rs
@@ -23,6 +23,25 @@
2323 SpaceToken,
2424}
2525
26#[derive(Clone)]
27pub struct TemporaryKey {
28 pub token: String,
29 pub token_id: Uuid,
30 pub issued_by: Option<String>,
31 pub expires_at: time::OffsetDateTime,
32}
33
34impl std::fmt::Debug for TemporaryKey {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 f.debug_struct("TemporaryKey")
37 .field("token", &"<redacted>")
38 .field("token_id", &self.token_id)
39 .field("issued_by", &self.issued_by)
40 .field("expires_at", &self.expires_at)
41 .finish()
42 }
43}
44
2645#[derive(Debug, Clone)]
2746pub struct Principal {
2847 pub uid: Uuid,
@@ -32,6 +51,9 @@
3251 /// `Some((space_id, lower_name))` for temporary space top-level tokens:
3352 /// every tool call must stay inside that single space.
3453 pub bound_space: Option<(Uuid, String)>,
54 /// `Some` only for temporary space top-level tokens; carries the presented
55 /// token and its allocation details so `me_get` can hand it back.
56 pub temporary_key: Option<TemporaryKey>,
3557}
3658
3759impl Principal {
@@ -78,6 +100,7 @@
78100 full_access: true,
79101 source: PrincipalSource::InternalKey,
80102 bound_space: None,
103 temporary_key: None,
81104 }
82105 } else if export::space_token::looks_like(&token) {
83106 match export::space_token::verify(&runtime.services().state.redis, &token).await {
@@ -85,12 +108,20 @@
85108 // The bot passes every owner-level authorization check; the
86109 // space binding below is what keeps it scoped to one space.
87110 export::authz::register_space_bot(record.bot_uid);
111 let expires_at = time::OffsetDateTime::from_unix_timestamp(record.expires_at_unix)
112 .unwrap_or_else(|_| time::OffsetDateTime::now_utc());
88113 Principal {
89114 uid: record.bot_uid,
90115 scope: AccessKeyScope::Write,
91116 full_access: true,
92117 source: PrincipalSource::SpaceToken,
93118 bound_space: Some((record.space_id, record.space_name)),
119 temporary_key: Some(TemporaryKey {
120 token: token.clone(),
121 token_id: record.token_id,
122 issued_by: record.issued_by,
123 expires_at,
124 }),
94125 }
95126 }
96127 Ok(None) => return unauthorized("访问密钥无效或已过期"),
@@ -107,6 +138,7 @@
107138 full_access: false,
108139 source: PrincipalSource::UserKey,
109140 bound_space: None,
141 temporary_key: None,
110142 },
111143 Ok(None) => return unauthorized("访问密钥无效或已过期"),
112144 Err(error) => {