Skip to content

Commit 2bccc98

Browse files
feat: 资产自由召唤协议升级 — 稳定@token/真子图/HITL隔离/proposal落盘/白名单 (v0.1.15)
按 PROJECT_REVIEW 落地顺序:芯片序列化为 @[kind:id];混合组队嵌入能力真实子图; HITL 按 runId 清理避免跨通道误伤;propose 草稿落盘可水合;Agent/Capability allowedToolNames。 Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 7912011 commit 2bccc98

18 files changed

Lines changed: 778 additions & 271 deletions

docs/PROJECT_REVIEW.md

Lines changed: 196 additions & 163 deletions
Large diffs are not rendered by default.

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "one",
3-
"version": "0.1.13",
3+
"version": "0.1.15",
44
"description": "One desktop app built with Electron, React, and TypeScript.",
55
"main": "./out/main/index.cjs",
66
"author": "shijianzhong",

src/main/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ export const AgentSchema = z.object({
5858
description: z.string().optional(),
5959
instructions: z.string(),
6060
skillIds: z.array(z.string()).optional(),
61+
/** 资产级工具白名单;缺省/空 = 不限制 */
62+
allowedToolNames: z.array(z.string()).optional(),
6163
modelId: z.string().optional(),
6264
temperature: z.number().min(0).max(2).optional(),
6365
maxTokens: z.number().int().positive().optional(),
@@ -90,6 +92,8 @@ export const CapabilitySchema = z.object({
9092
const g = val as { nodes?: unknown; edges?: unknown }
9193
return Array.isArray(g.nodes) && Array.isArray(g.edges)
9294
}),
95+
/** 能力级工具白名单(作用于该能力图内节点);缺省/空 = 不限制 */
96+
allowedToolNames: z.array(z.string()).optional(),
9397
registry: RegistryProvenanceSchema.optional(),
9498
createdAt: ts,
9599
updatedAt: ts,

src/main/ipc/home.ts

Lines changed: 95 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,16 @@ import { buildL2Injection, refineL2 } from '../storage/memory/l2'
5454
import { getClient } from '../llm/retry'
5555
import { resolveThinkingConfig } from '../llm/thinking'
5656
import { listToolsForAgents } from '../tools/mcp'
57+
import { filterToolsByAllowlist } from '../tools/allowlist'
5758
import { listMemoryKeysForPrompt } from '../tools/builtin/memory'
5859
import { resolveApprovalDecision } from '../tools/sessionApprovals'
5960
import {
6061
newRequestId,
61-
rejectAllUserInputs,
62+
newRunId,
63+
rejectUserInputsForRun,
6264
waitForUserInput,
6365
} from '../orchestrator/userInput'
66+
import { listDrafts, removeDraft, writeDraft } from '../crash-recovery'
6467
import type { BuildDeps } from '../orchestrator/builder'
6568
import type { AgentExecutorOptions } from '../orchestrator/patterns/agent'
6669
import { logger } from '../logger'
@@ -76,13 +79,49 @@ const DEFAULT_USER_ID = 'local'
7679

7780
/** 当前聊天的 AbortController(home:cancel 用;组队运行内 ask_user 挂起也受它取消) */
7881
let currentAbortController: AbortController | null = null
82+
/** 当前首页 HITL run 作用域(与 orchestrate 通道隔离) */
83+
let currentHitlRunId: string | null = null
7984

80-
/** 创建提案草稿暂存(draftId → {draft, ts});确认/取消删除,超时(30min)惰性清理。 */
85+
/** 创建提案草稿暂存(draftId → {draft, ts});确认/取消删除,超时(30min)惰性清理;同步落盘防崩溃丢失。 */
8186
const DRAFT_TTL_MS = 30 * 60 * 1000
8287
/** 草稿驻留硬上限:正常流程单 figure 确认即删到不了上限;防异常 propose 风暴撑内存 */
8388
const MAX_PENDING_DRAFTS = 100
89+
const CREATE_DRAFT_PREFIX = 'create-'
8490
const pendingDrafts = new Map<string, { draft: CreateDraft; ts: number }>()
8591

92+
function createDraftFileName(draftId: string): string {
93+
// draftId 已是 uuid 形态;剥路径字符防穿越
94+
const safe = draftId.replace(/[/\\]/g, '')
95+
return `${CREATE_DRAFT_PREFIX}${safe}.json`
96+
}
97+
98+
function persistCreateDraft(draftId: string, entry: { draft: CreateDraft; ts: number }): void {
99+
writeDraft(createDraftFileName(draftId), JSON.stringify(entry))
100+
}
101+
102+
function forgetCreateDraft(draftId: string): void {
103+
pendingDrafts.delete(draftId)
104+
removeDraft(createDraftFileName(draftId))
105+
}
106+
107+
/** 启动时从 drafts/ 水合未确认创建卡(崩溃恢复后 listPendingDrafts 可重挂) */
108+
function hydrateCreateDraftsFromDisk(): void {
109+
for (const f of listDrafts()) {
110+
if (!f.name.startsWith(CREATE_DRAFT_PREFIX) || !f.name.endsWith('.json')) continue
111+
try {
112+
const parsed = JSON.parse(f.content) as { draft?: CreateDraft; ts?: number }
113+
if (!parsed?.draft?.draftId || typeof parsed.ts !== 'number') continue
114+
if (Date.now() - parsed.ts > DRAFT_TTL_MS) {
115+
removeDraft(f.name)
116+
continue
117+
}
118+
pendingDrafts.set(parsed.draft.draftId, { draft: parsed.draft, ts: parsed.ts })
119+
} catch {
120+
removeDraft(f.name)
121+
}
122+
}
123+
}
124+
86125
/** propose_* 工具结果是否表示失败(Zod invalid_args / empty_payload 等) */
87126
function parseProposeFailure(content: string): {
88127
error: string
@@ -117,7 +156,7 @@ function parseProposeFailure(content: string): {
117156
function pruneDrafts(): void {
118157
const now = Date.now()
119158
for (const [id, entry] of pendingDrafts) {
120-
if (now - entry.ts > DRAFT_TTL_MS) pendingDrafts.delete(id)
159+
if (now - entry.ts > DRAFT_TTL_MS) forgetCreateDraft(id)
121160
}
122161
}
123162

@@ -152,6 +191,7 @@ function makeCompressFn(
152191
}
153192

154193
export function registerHomeHandlers(): void {
194+
hydrateCreateDraftsFromDisk()
155195
withHandler<{ runId: string }>('home:chat', async (_e, input) => {
156196
const { message, sessionId } = input as { message: string; sessionId?: string }
157197

@@ -263,9 +303,13 @@ export function registerHomeHandlers(): void {
263303
// 虽 JS 闭包延迟取值不会报错,但先创建可避免 TDZ 风险 + 代码意图更清晰
264304
if (currentAbortController) {
265305
logger.warn('[home] 已有运行中的聊天,自动取消旧运行')
306+
const prevRun = currentHitlRunId
266307
currentAbortController.abort()
308+
if (prevRun) rejectUserInputsForRun(prevRun, 'aborted')
267309
}
268310
currentAbortController = new AbortController()
311+
const hitlRunId = newRunId('home')
312+
currentHitlRunId = hitlRunId
269313
const { signal } = currentAbortController
270314

271315
// R1/R2:builtin + 显式 exposeToAgents 且已连接的 MCP 工具(同一快照供主 Agent / 组队节点共用)
@@ -323,9 +367,11 @@ export function registerHomeHandlers(): void {
323367
// 超上限挤掉最旧草稿(Map 迭代序即插入序)
324368
if (pendingDrafts.size >= MAX_PENDING_DRAFTS && !pendingDrafts.has(stamped.draftId)) {
325369
const oldest = pendingDrafts.keys().next().value
326-
if (oldest) pendingDrafts.delete(oldest)
370+
if (oldest) forgetCreateDraft(oldest)
327371
}
328-
pendingDrafts.set(stamped.draftId, { draft: stamped, ts: Date.now() })
372+
const entry = { draft: stamped, ts: Date.now() }
373+
pendingDrafts.set(stamped.draftId, entry)
374+
persistCreateDraft(stamped.draftId, entry)
329375
emitStream({ type: 'proposal', draft: stamped })
330376
logger.info(`[home:create] propose invoked: kind=${stamped.kind} draftId=${stamped.draftId}`)
331377
},
@@ -337,7 +383,7 @@ export function registerHomeHandlers(): void {
337383
emitStream({ type: 'orch_event', event })
338384
emit({ type: 'request_info', request_id: requestId, node_id: 'home', question, context })
339385
try {
340-
const answer = await waitForUserInput(requestId, { nodeId: 'home', question }, signal)
386+
const answer = await waitForUserInput(requestId, { nodeId: 'home', question }, signal, hitlRunId)
341387
emit({ type: 'request_resolved', request_id: requestId, node_id: 'home', response: answer })
342388
return answer
343389
} catch (e) {
@@ -353,7 +399,12 @@ export function registerHomeHandlers(): void {
353399
emitStream({ type: 'orch_event', event })
354400
emit({ type: 'approval_request', request_id: requestId, node_id: 'home', tool_name: toolName, args })
355401
try {
356-
const response = await waitForUserInput(requestId, { nodeId: 'home', question: `approve ${toolName}` }, signal)
402+
const response = await waitForUserInput(
403+
requestId,
404+
{ nodeId: 'home', question: `approve ${toolName}` },
405+
signal,
406+
hitlRunId,
407+
)
357408
emit({ type: 'approval_resolved', request_id: requestId, node_id: 'home', response })
358409
return resolveApprovalDecision(response, sid, toolName)
359410
} catch (e) {
@@ -372,10 +423,13 @@ export function registerHomeHandlers(): void {
372423
instructions?: string
373424
description?: string
374425
skillIds?: string[]
426+
allowedToolNames?: string[]
375427
modelId?: string
376428
temperature?: number
377429
maxTokens?: number
378430
outputConstraints?: string
431+
sourceAgentId?: string
432+
sourceCapabilityId?: string
379433
}
380434
const nodeThinking = resolveThinkingConfig(
381435
d.modelId ?? modelId,
@@ -395,21 +449,30 @@ export function registerHomeHandlers(): void {
395449
const finalNodeInstructions = d.outputConstraints
396450
? `${nodeInstructions}\n\n【输出约束】\n${d.outputConstraints}`
397451
: nodeInstructions
452+
// 资产级工具白名单:节点快照 → 源角色 → 源能力
453+
let allow = d.allowedToolNames
454+
if (!allow?.length && d.sourceAgentId) {
455+
allow = getAgent(d.sourceAgentId)?.allowedToolNames
456+
}
457+
if (!allow?.length && d.sourceCapabilityId) {
458+
allow = getCapability(d.sourceCapabilityId)?.allowedToolNames
459+
}
460+
const nodeTools = filterToolsByAllowlist(agentTools, allow)
398461
const cfg: AgentConfig = {
399462
// 铁律20:executor_id == 节点 id(runner 按节点 id 路由/查找),
400463
// 不能用 d.label(角色显示名)——否则 executors.get(node.id) 找不到 → 空白气泡
401464
name: node.id,
402465
description: d.description,
403466
instructions: finalNodeInstructions,
404467
modelId: d.modelId ?? modelId,
405-
tools: agentTools,
468+
tools: nodeTools,
406469
defaultOptions: { maxTokens: d.maxTokens ?? 16384, temperature: d.temperature },
407470
outputConstraints: d.outputConstraints,
408471
thinking: nodeThinking,
409472
}
410473
const opts: AgentExecutorOptions = {
411474
config: cfg,
412-
llmOpts: { apiKey, baseURL, authHeader },
475+
llmOpts: { apiKey, baseURL, authHeader, apiFormat },
413476
toolCtx: {
414477
sessionId: sid,
415478
signal,
@@ -421,7 +484,12 @@ export function registerHomeHandlers(): void {
421484
emitStream({ type: 'orch_event', event })
422485
emit({ type: 'request_info', request_id: requestId, node_id: node.id, question, context })
423486
try {
424-
const answer = await waitForUserInput(requestId, { nodeId: node.id, question }, signal)
487+
const answer = await waitForUserInput(
488+
requestId,
489+
{ nodeId: node.id, question },
490+
signal,
491+
hitlRunId,
492+
)
425493
emit({ type: 'request_resolved', request_id: requestId, node_id: node.id, response: answer })
426494
return answer
427495
} catch (e) {
@@ -436,7 +504,12 @@ export function registerHomeHandlers(): void {
436504
emitStream({ type: 'orch_event', event })
437505
emit({ type: 'approval_request', request_id: requestId, node_id: node.id, tool_name: toolName, args })
438506
try {
439-
const response = await waitForUserInput(requestId, { nodeId: node.id, question: `approve ${toolName}` }, signal)
507+
const response = await waitForUserInput(
508+
requestId,
509+
{ nodeId: node.id, question: `approve ${toolName}` },
510+
signal,
511+
hitlRunId,
512+
)
440513
emit({ type: 'approval_resolved', request_id: requestId, node_id: node.id, response })
441514
return resolveApprovalDecision(response, sid, toolName)
442515
} catch (e) {
@@ -695,22 +768,26 @@ export function registerHomeHandlers(): void {
695768
emitStream({ type: 'message_stop', stop_reason: 'error' })
696769
throw e
697770
} finally {
698-
// 聊天结束(含异常/取消):驳回残留挂起提问 + 清控制器,防泄漏到下一场。
699-
// 只清自己的控制器:若期间新运行已接管(入口自动取消旧运行),不动新句柄
700-
rejectAllUserInputs('run_finished')
771+
// 仅驳回本 run 的挂起提问,避免误伤编辑器 orchestrate 通道
772+
rejectUserInputsForRun(hitlRunId, 'run_finished')
701773
// SkillContextProvider.afterRun(铁律22):运行结束审计
702774
for (const p of skillProviders) p.afterRun()
703-
if (currentAbortController?.signal === signal) currentAbortController = null
775+
if (currentAbortController?.signal === signal) {
776+
currentAbortController = null
777+
if (currentHitlRunId === hitlRunId) currentHitlRunId = null
778+
}
704779
}
705780

706781
return { runId: sid }
707782
})
708783

709784
withHandler<void>('home:cancel', () => {
710-
rejectAllUserInputs('aborted') // 先驳回挂起提问,让工具侧收尾
785+
const runId = currentHitlRunId
786+
if (runId) rejectUserInputsForRun(runId, 'aborted')
711787
if (currentAbortController) {
712788
currentAbortController.abort()
713789
currentAbortController = null
790+
currentHitlRunId = null
714791
logger.info('[home:cancel] 已取消当前聊天/组队运行')
715792
}
716793
})
@@ -778,7 +855,7 @@ export function registerHomeHandlers(): void {
778855
throw new Error(`未知创建类型:${String(kind)}`)
779856
}
780857

781-
pendingDrafts.delete(draftId)
858+
forgetCreateDraft(draftId)
782859
// R3:confirm 成功后写 meta.create.status=confirmed(供 B 期降级/事实源)
783860
const sessionId = cached?.sessionId
784861
if (sessionId) {
@@ -801,7 +878,7 @@ export function registerHomeHandlers(): void {
801878
// 前端确认卡点「取消」:丢弃草稿,不入库。
802879
withHandler<void>('home:cancelCreate', (_e, input) => {
803880
const { draftId } = input as { draftId: string }
804-
pendingDrafts.delete(draftId)
881+
forgetCreateDraft(draftId)
805882
logger.info('[home:create] 已取消草稿:', draftId)
806883
})
807884

0 commit comments

Comments
 (0)