personal-bot 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTEXT.md +13 -7
- package/README.md +46 -9
- package/README.zh-CN.md +46 -9
- package/bin/pbot.mjs +3 -0
- package/docs/README.md +4 -1
- package/docs/agent-collaboration.md +26 -12
- package/docs/architecture-guidelines.md +2 -2
- package/docs/capability-design.md +6 -4
- package/docs/dsh-implementation-plan.md +158 -0
- package/docs/dsh-support-proposal.md +314 -0
- package/docs/dsh.md +103 -0
- package/docs/file-storage.md +14 -9
- package/docs/history.md +29 -18
- package/docs/installation-design.md +226 -0
- package/docs/interaction-design.md +13 -0
- package/docs/release.md +5 -5
- package/docs/technical-design.md +29 -15
- package/docs/validation.md +53 -3
- package/lib/client.js +1071 -0
- package/lib/common/files.d.ts +2 -0
- package/lib/common/files.js +44 -0
- package/lib/common/json.d.ts +6 -0
- package/lib/common/json.js +16 -0
- package/lib/common/markdown.d.ts +5 -0
- package/lib/common/markdown.js +26 -0
- package/lib/common/package-source.js +16 -0
- package/lib/common/paths.d.ts +1 -0
- package/lib/common/paths.js +2 -0
- package/lib/common/validation.d.ts +3 -0
- package/lib/common/validation.js +14 -0
- package/lib/common/version.d.ts +3 -0
- package/lib/common/version.js +13 -0
- package/lib/dsh.d.ts +4 -0
- package/lib/dsh.js +22 -0
- package/lib/extension/dsh/agent-events.d.ts +24 -0
- package/lib/extension/dsh/agent-events.js +242 -0
- package/lib/extension/dsh/agent-runtime.d.ts +49 -0
- package/lib/extension/dsh/agent-runtime.js +348 -0
- package/lib/extension/dsh/command.d.ts +4 -0
- package/lib/extension/dsh/command.js +83 -0
- package/lib/extension/dsh/files.d.ts +5 -0
- package/lib/extension/dsh/files.js +67 -0
- package/lib/extension/dsh/history.d.ts +15 -0
- package/lib/extension/dsh/history.js +88 -0
- package/lib/extension/dsh/management.d.ts +21 -0
- package/lib/extension/dsh/management.js +217 -0
- package/lib/extension/dsh/workspace.d.ts +48 -0
- package/lib/extension/dsh/workspace.js +290 -0
- package/lib/extension/setup-cli.js +136 -0
- package/lib/extension/setup-guard.js +11 -0
- package/lib/integrations/agent-resources.d.ts +1 -0
- package/lib/integrations/agent-resources.js +18 -0
- package/lib/integrations/dsh/dsh-agents.d.ts +30 -0
- package/lib/integrations/dsh/dsh-agents.js +104 -0
- package/lib/integrations/dsh/history-summary.d.ts +11 -0
- package/lib/integrations/dsh/history-summary.js +40 -0
- package/lib/integrations/dsh/history.d.ts +16 -0
- package/lib/integrations/dsh/history.js +156 -0
- package/lib/integrations/dsh/input.d.ts +21 -0
- package/lib/integrations/dsh/input.js +62 -0
- package/lib/integrations/dsh/messages.d.ts +36 -0
- package/lib/integrations/dsh/messages.js +11 -0
- package/lib/integrations/dsh/notifications.d.ts +8 -0
- package/lib/integrations/dsh/notifications.js +55 -0
- package/lib/integrations/dsh/resources.d.ts +10 -0
- package/lib/integrations/dsh/resources.js +41 -0
- package/lib/integrations/dsh/schema.d.ts +7 -0
- package/lib/integrations/dsh/schema.js +28 -0
- package/lib/integrations/dsh/session.d.ts +14 -0
- package/lib/integrations/dsh/session.js +59 -0
- package/lib/integrations/history-summary.d.ts +2 -0
- package/lib/integrations/history-summary.js +21 -0
- package/lib/integrations/installation/pi-bridge.js +131 -0
- package/lib/integrations/installation/pi-setup.js +193 -0
- package/lib/integrations/installation/processes.js +155 -0
- package/lib/integrations/installation/workspace.js +21 -0
- package/lib/integrations/process.d.ts +14 -0
- package/lib/integrations/process.js +48 -0
- package/lib/integrations/python-activation.d.ts +2 -0
- package/lib/integrations/python-activation.js +29 -0
- package/lib/integrations/team-context.d.ts +18 -0
- package/lib/integrations/team-context.js +57 -0
- package/lib/model/agent.d.ts +92 -0
- package/lib/model/agent.js +1 -0
- package/lib/model/history.d.ts +43 -0
- package/lib/model/history.js +1 -0
- package/lib/model/team.d.ts +85 -0
- package/lib/model/team.js +1 -0
- package/lib/services/agents/agent-notices.d.ts +13 -0
- package/lib/services/agents/agent-notices.js +25 -0
- package/lib/services/agents/agent-run.d.ts +39 -0
- package/lib/services/agents/agent-run.js +138 -0
- package/lib/services/agents/agent-service.d.ts +166 -0
- package/lib/services/agents/agent-service.js +325 -0
- package/lib/services/agents/team-service.d.ts +168 -0
- package/lib/services/agents/team-service.js +343 -0
- package/lib/services/history/history-service.d.ts +103 -0
- package/lib/services/history/history-service.js +269 -0
- package/lib/services/installation/setup-service.js +121 -0
- package/lib/services/workspace/python-environment-service.d.ts +15 -0
- package/lib/services/workspace/python-environment-service.js +50 -0
- package/lib/services/workspace/workspace-files.d.ts +2 -0
- package/lib/services/workspace/workspace-files.js +17 -0
- package/lib/services/workspace/workspace-preparation.d.ts +2 -0
- package/lib/services/workspace/workspace-preparation.js +18 -0
- package/lib/storage/agent-files.d.ts +30 -0
- package/lib/storage/agent-files.js +166 -0
- package/lib/storage/atomic-json.d.ts +11 -0
- package/lib/storage/atomic-json.js +101 -0
- package/lib/storage/file-access.d.ts +2 -0
- package/lib/storage/file-access.js +26 -0
- package/lib/storage/history-files.d.ts +10 -0
- package/lib/storage/history-files.js +63 -0
- package/lib/storage/team-files.d.ts +16 -0
- package/lib/storage/team-files.js +111 -0
- package/lib/tools/agents/actions.d.ts +52 -0
- package/lib/tools/agents/actions.js +49 -0
- package/lib/tools/agents/contract.d.ts +163 -0
- package/lib/tools/agents/contract.js +56 -0
- package/lib/tools/agents/dsh-contract.d.ts +52 -0
- package/lib/tools/agents/dsh-contract.js +15 -0
- package/lib/tools/agents/dsh-tool.d.ts +5 -0
- package/lib/tools/agents/dsh-tool.js +38 -0
- package/lib/tools/history/actions.d.ts +70 -0
- package/lib/tools/history/actions.js +7 -0
- package/lib/tools/history/contract.d.ts +81 -0
- package/lib/tools/history/contract.js +32 -0
- package/lib/tools/history/dsh-tool.d.ts +4 -0
- package/lib/tools/history/dsh-tool.js +22 -0
- package/lib/tools/result.d.ts +18 -0
- package/lib/tools/result.js +10 -0
- package/lib/tools/teams/actions.d.ts +24 -0
- package/lib/tools/teams/actions.js +38 -0
- package/lib/tools/teams/contract.d.ts +232 -0
- package/lib/tools/teams/contract.js +67 -0
- package/lib/tools/teams/dsh-tool.d.ts +8 -0
- package/lib/tools/teams/dsh-tool.js +49 -0
- package/lib/ui/dsh/attachments.d.ts +5 -0
- package/lib/ui/dsh/attachments.js +39 -0
- package/lib/ui/dsh/elements.d.ts +17 -0
- package/lib/ui/dsh/elements.js +79 -0
- package/lib/ui/dsh/index.d.ts +4 -0
- package/lib/ui/dsh/index.js +13 -0
- package/lib/ui/dsh/management.d.ts +4 -0
- package/lib/ui/dsh/management.js +711 -0
- package/lib/ui/dsh/remote.d.ts +15 -0
- package/lib/ui/dsh/remote.js +11 -0
- package/lib/ui/dsh/styles.d.ts +1 -0
- package/lib/ui/dsh/styles.js +132 -0
- package/lib/ui/presentation.d.ts +3 -0
- package/lib/ui/presentation.js +12 -0
- package/lib/ui/setup.js +52 -0
- package/npm-shrinkwrap.json +11118 -783
- package/package.json +113 -9
- package/python/README.md +6 -6
- package/resources/agents/coordinator.md +1 -1
- package/resources/dsh/cordis.patch.yml +3 -0
- package/resources/installer/plugins.json +16 -0
- package/resources/prompts/workspace.md +1 -1
- package/resources/references/compute.md +1 -1
- package/resources/references/task-conductor.md +6 -4
- package/resources/skills/provider-connections/SKILL.md +10 -0
- package/scripts/README.md +13 -1
- package/src/common/package-source.ts +16 -0
- package/src/dsh.ts +22 -0
- package/src/extension/agent-events.ts +51 -46
- package/src/extension/agents-command.ts +48 -34
- package/src/extension/command.ts +42 -23
- package/src/extension/dsh/agent-events.ts +148 -0
- package/src/extension/dsh/agent-runtime.ts +251 -0
- package/src/extension/dsh/command.ts +78 -0
- package/src/extension/dsh/files.ts +61 -0
- package/src/extension/dsh/history.ts +69 -0
- package/src/extension/dsh/management.ts +108 -0
- package/src/extension/dsh/workspace.ts +206 -0
- package/src/extension/events.ts +6 -4
- package/src/extension/history.ts +15 -12
- package/src/extension/setup-cli.ts +109 -0
- package/src/extension/setup-guard.ts +12 -0
- package/src/extension/subagent-entry.ts +34 -63
- package/src/extension/teams-command.ts +53 -46
- package/src/integrations/{pi/agent-resources.ts → agent-resources.ts} +2 -2
- package/src/integrations/dsh/dsh-agents.ts +97 -0
- package/src/integrations/dsh/history-summary.ts +43 -0
- package/src/integrations/dsh/history.ts +76 -0
- package/src/integrations/dsh/input.ts +66 -0
- package/src/integrations/dsh/messages.ts +22 -0
- package/src/integrations/dsh/notifications.ts +46 -0
- package/src/integrations/dsh/resources.ts +44 -0
- package/src/integrations/dsh/schema.ts +26 -0
- package/src/integrations/dsh/session.ts +67 -0
- package/src/integrations/history-summary.ts +20 -0
- package/src/integrations/installation/pi-bridge.ts +107 -0
- package/src/integrations/installation/pi-setup.ts +162 -0
- package/src/integrations/installation/processes.ts +124 -0
- package/src/integrations/installation/workspace.ts +18 -0
- package/src/integrations/pi/history.ts +4 -14
- package/src/integrations/pi/pi-agents.ts +17 -6
- package/src/integrations/pi/team-context.ts +6 -58
- package/src/integrations/team-context.ts +60 -0
- package/src/model/agent.ts +2 -0
- package/src/model/history.ts +3 -1
- package/src/pbot.ts +1 -1
- package/src/services/agents/agent-notices.ts +28 -0
- package/src/services/agents/agent-run.ts +114 -0
- package/src/services/agents/agent-service.ts +127 -64
- package/src/services/history/history-service.ts +120 -59
- package/src/services/installation/setup-service.ts +145 -0
- package/src/storage/agent-files.ts +17 -4
- package/src/storage/history-files.ts +30 -5
- package/src/tools/agents/actions.ts +47 -0
- package/src/tools/agents/contract.ts +8 -5
- package/src/tools/agents/dsh-contract.ts +16 -0
- package/src/tools/agents/dsh-tool.ts +41 -0
- package/src/tools/agents/tool.ts +7 -43
- package/src/tools/history/actions.ts +9 -0
- package/src/tools/history/contract.ts +34 -0
- package/src/tools/history/dsh-tool.ts +22 -0
- package/src/tools/history/tool.ts +7 -30
- package/src/tools/teams/actions.ts +38 -0
- package/src/tools/teams/contract.ts +7 -3
- package/src/tools/teams/dsh-tool.ts +55 -0
- package/src/tools/teams/tool.ts +6 -31
- package/src/ui/agent-display.ts +7 -4
- package/src/ui/dsh/attachments.ts +42 -0
- package/src/ui/dsh/elements.ts +50 -0
- package/src/ui/dsh/index.ts +18 -0
- package/src/ui/dsh/management.ts +412 -0
- package/src/ui/dsh/remote.ts +20 -0
- package/src/ui/dsh/styles.ts +132 -0
- package/src/ui/pi/menu.ts +37 -0
- package/src/ui/pi/reader.ts +46 -0
- package/src/ui/presentation.ts +12 -0
- package/src/ui/setup.ts +40 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
2
|
+
import type {} from "@deepseek-ai/dsh-commands";
|
|
3
|
+
import { Type, type Static } from "typebox";
|
|
4
|
+
import { object, json } from "../../common/json.js";
|
|
5
|
+
import { requireThat, schema } from "../../common/validation.js";
|
|
6
|
+
import { prepareWorkspace } from "../../services/workspace/workspace-preparation.js";
|
|
7
|
+
import { executeAgent, type AgentInput } from "../../tools/agents/actions.js";
|
|
8
|
+
import { dshAgentActionSchema } from "../../tools/agents/dsh-contract.js";
|
|
9
|
+
import { executeTeam, executeTeamMessage } from "../../tools/teams/actions.js";
|
|
10
|
+
import { teamActionSchema, teamParametersSchema, teamMessageActionSchema, teamMessageParametersSchema } from "../../tools/teams/contract.js";
|
|
11
|
+
import { dshStatus, type PbotManagement } from "./management.js";
|
|
12
|
+
import type { DshWorkspaces } from "./workspace.js";
|
|
13
|
+
|
|
14
|
+
const validAgent = schema(dshAgentActionSchema), validTeam = schema(teamActionSchema), validMessage = schema(teamMessageActionSchema);
|
|
15
|
+
const chatSchema = Type.Object({ agentId: Type.String({ minLength: 1 }), text: Type.Optional(Type.String({ maxLength: 24000 })),
|
|
16
|
+
delivery: Type.Optional(Type.Union([Type.Literal("steer"), Type.Literal("followUp")])),
|
|
17
|
+
files: Type.Optional(Type.Array(Type.String({ minLength: 1 }), { maxItems: 32 })) }, { additionalProperties: false });
|
|
18
|
+
const validChat = schema(chatSchema);
|
|
19
|
+
const usage = '用法:/pbot status | init | python setup | agent {"action":…} | chat {"agentId":…,"text":…} | team {"action":…} | post {"teamId":…,"text":…} | history {"action":…} | records {"agentId":…}';
|
|
20
|
+
|
|
21
|
+
export function registerDshCommand(ctx: Context, workspaces: DshWorkspaces, management: PbotManagement) {
|
|
22
|
+
ctx.commands.register({ name: "pbot", description: "Personal Bot 工作区与协作管理", recordInput: false,
|
|
23
|
+
input: { hint: "status | init | python setup | agent/chat/team/post/history/records JSON", attachments: true },
|
|
24
|
+
async handler({ agent, rawInput, signal, attachments, commandId }) {
|
|
25
|
+
signal.throwIfAborted();
|
|
26
|
+
const workspace = await workspaces.get(agent);
|
|
27
|
+
signal.throwIfAborted();
|
|
28
|
+
const command = rawInput.trim(), split = command.search(/\s/);
|
|
29
|
+
const verb = split < 0 ? command : command.slice(0, split);
|
|
30
|
+
requireThat(verb === "chat" || !attachments.length, "输入框附件仅用于 chat 人工对话。");
|
|
31
|
+
if (command === "init") {
|
|
32
|
+
await prepareWorkspace(workspace.root); workspace.check();
|
|
33
|
+
return { kind: "success", text: "资料目录已准备;已有文件保留。" };
|
|
34
|
+
}
|
|
35
|
+
if (command === "python setup") {
|
|
36
|
+
const result = await workspaces.preparePython(); workspace.check();
|
|
37
|
+
return { kind: "success", text: JSON.stringify(result, null, 2) };
|
|
38
|
+
}
|
|
39
|
+
if (!command || command === "status") return { kind: "success", text: JSON.stringify(dshStatus(workspaces, workspace), null, 2) };
|
|
40
|
+
requireThat(split >= 0, usage);
|
|
41
|
+
const input: unknown = JSON.parse(command.slice(split));
|
|
42
|
+
requireThat(object(input), "命令参数必须是 JSON 对象。");
|
|
43
|
+
let result: unknown;
|
|
44
|
+
switch (verb) {
|
|
45
|
+
case "agent": {
|
|
46
|
+
requireThat(validAgent(input), "dsh 专家参数无效或包含不支持的宿主选项。");
|
|
47
|
+
const selection = agent.model.current();
|
|
48
|
+
const output = await executeAgent(workspace.agents, input as AgentInput, workspace.owner, selection ? {
|
|
49
|
+
provider: selection.provider, model: selection.model,
|
|
50
|
+
...(selection.reasoningEffort && (!input.provider || input.provider === selection.provider) && (!input.model || input.model === selection.model)
|
|
51
|
+
? { thinking: selection.reasoningEffort } : {}),
|
|
52
|
+
} : {});
|
|
53
|
+
result = output.structuredContent; break;
|
|
54
|
+
}
|
|
55
|
+
case "chat": {
|
|
56
|
+
requireThat(validChat(input), "人工对话参数无效。");
|
|
57
|
+
const args = input as Static<typeof chatSchema>;
|
|
58
|
+
result = await workspace.expert(args.agentId).interact(args.text ?? "", args.delivery, args.files, attachments, signal); break;
|
|
59
|
+
}
|
|
60
|
+
case "team": {
|
|
61
|
+
requireThat(validTeam(input), "团队参数无效。");
|
|
62
|
+
result = executeTeam(workspace.teams, workspace.agents, input as Static<typeof teamParametersSchema>, workspace.owner).structuredContent; break;
|
|
63
|
+
}
|
|
64
|
+
case "post": {
|
|
65
|
+
const args = { ...input, action: "post" as const };
|
|
66
|
+
requireThat(input.action === undefined && validMessage(args), "公共消息参数无效。");
|
|
67
|
+
result = executeTeamMessage(workspace.teams, args as Static<typeof teamMessageParametersSchema>,
|
|
68
|
+
{ kind: "user", owner: workspace.owner }, commandId).structuredContent; break;
|
|
69
|
+
}
|
|
70
|
+
case "history": result = await management.history({ parentSessionId: agent.id, input: json(input) }, signal); break;
|
|
71
|
+
case "records": result = await management.expert({ parentSessionId: agent.id, input: json(input) }, signal); break;
|
|
72
|
+
default: throw new Error(usage);
|
|
73
|
+
}
|
|
74
|
+
workspace.check();
|
|
75
|
+
return { kind: "success", text: JSON.stringify(result, null, 2) };
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
2
|
+
import type { Agent } from "@deepseek-ai/dsh-agent";
|
|
3
|
+
import type { ToolExecution, ToolExecutionToken } from "@deepseek-ai/dsh-tools";
|
|
4
|
+
import { parsePatch } from "@deepseek-ai/dsh-fs-patch";
|
|
5
|
+
import type {} from "@deepseek-ai/dsh-user-approval";
|
|
6
|
+
import { object } from "../../common/json.js";
|
|
7
|
+
import { requireThat } from "../../common/validation.js";
|
|
8
|
+
import { mutationScope } from "../../services/workspace/workspace-files.js";
|
|
9
|
+
|
|
10
|
+
/** Native filesystem mutations share the original workspace's confirmation rule.
|
|
11
|
+
* This does not confine Bash, Python, MCP, or other extensions. */
|
|
12
|
+
export function registerDshFileEvents(ctx: Context, agent: Agent, root: string, expert: boolean, check: () => void) {
|
|
13
|
+
const lifetime = new AbortController();
|
|
14
|
+
const mutations = new Set(["write", "edit", "apply_patch"]);
|
|
15
|
+
const captured = new Map<ToolExecutionToken, { cwd: string; keys: string[]; approved: boolean }>();
|
|
16
|
+
ctx.effect(() => () => { lifetime.abort(); captured.clear(); });
|
|
17
|
+
async function targets(exec: Readonly<ToolExecution>) {
|
|
18
|
+
check(); const args = exec.arguments;
|
|
19
|
+
requireThat(exec.agent === agent && object(args), "文件操作会话或参数无效。");
|
|
20
|
+
const signal = AbortSignal.any([lifetime.signal, exec.signal]); signal.throwIfAborted();
|
|
21
|
+
let paths: string[];
|
|
22
|
+
if (exec.name === "apply_patch") {
|
|
23
|
+
requireThat(typeof args.input === "string", "补丁参数无效。");
|
|
24
|
+
paths = parsePatch(args.input).flatMap(operation => [operation.path, ...(operation.kind === "update" && operation.moveTo ? [operation.moveTo] : [])]);
|
|
25
|
+
} else {
|
|
26
|
+
requireThat(typeof args.file_path === "string" && args.file_path.trim().length > 0, "文件路径无效。");
|
|
27
|
+
paths = [args.file_path];
|
|
28
|
+
}
|
|
29
|
+
const anchor = ctx.fs.processPathFromHostPath(root);
|
|
30
|
+
requireThat(anchor !== undefined, "原生文件系统未映射当前工作区。");
|
|
31
|
+
const cwd = ctx.workingDirectory.get(agent.session);
|
|
32
|
+
const resolved = await Promise.all(paths.map(path => ctx.fs.resolve(path, { cwd, signal })));
|
|
33
|
+
signal.throwIfAborted(); check();
|
|
34
|
+
const outside = resolved.map(target => ctx.fs.processPath(target)).filter(path => mutationScope(anchor, path) === "outside");
|
|
35
|
+
return { cwd, keys: resolved.map(target => target.targetKey), outside, signal };
|
|
36
|
+
}
|
|
37
|
+
ctx.on("tools/pre-execute", async (exec, next) => {
|
|
38
|
+
if (!mutations.has(exec.name)) return next();
|
|
39
|
+
const value = await targets(exec), record = { cwd: value.cwd, keys: value.keys, approved: !value.outside.length };
|
|
40
|
+
captured.set(exec.token, record);
|
|
41
|
+
if (value.outside.length && expert) return { kind: "deny", reason: "专家不能修改工作区外文件。" };
|
|
42
|
+
const policy = await next(); value.signal.throwIfAborted(); check();
|
|
43
|
+
if (policy.kind === "deny" || policy.kind === "cancel" || !value.outside.length) return policy;
|
|
44
|
+
const scopeReason = "Personal Bot: confirm changes outside the original workspace: " + JSON.stringify(value.outside);
|
|
45
|
+
const reason = [policy.kind === "ask" ? policy.reason : undefined, scopeReason].filter(Boolean).join("\n");
|
|
46
|
+
const outcome = await ctx.approval.request({ agent, toolName: exec.name, callId: exec.callId, signal: value.signal, reason,
|
|
47
|
+
displayReason: { en: reason, "zh-CN": "允许本次修改原工作区之外的文件:" + value.outside.join("、") + (policy.kind === "ask" && policy.reason ? "\n" + policy.reason : "") } });
|
|
48
|
+
value.signal.throwIfAborted(); check();
|
|
49
|
+
record.approved = outcome === "allowed-once";
|
|
50
|
+
return record.approved ? { kind: "allow" } : { kind: "deny", reason: "用户未确认文件修改。" };
|
|
51
|
+
});
|
|
52
|
+
ctx.on("tools/execute", async (exec, next) => {
|
|
53
|
+
if (!mutations.has(exec.name)) return next();
|
|
54
|
+
const record = captured.get(exec.token);
|
|
55
|
+
requireThat(record?.approved, "文件操作没有有效的范围检查或确认。");
|
|
56
|
+
const value = await targets(exec);
|
|
57
|
+
requireThat(record.cwd === value.cwd && record.keys.length === value.keys.length && record.keys.every((key, index) => key === value.keys[index]), "确认期间文件路径或工作目录已变化。");
|
|
58
|
+
return next();
|
|
59
|
+
});
|
|
60
|
+
ctx.on("tools/result", exec => { captured.delete(exec.token); });
|
|
61
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
3
|
+
import type { Agent } from "@deepseek-ai/dsh-agent";
|
|
4
|
+
import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
|
|
5
|
+
import { AgentFiles } from "../../storage/agent-files.js";
|
|
6
|
+
import { HistoryFiles } from "../../storage/history-files.js";
|
|
7
|
+
import { HistoryService } from "../../services/history/history-service.js";
|
|
8
|
+
import { DshHistory } from "../../integrations/dsh/history.js";
|
|
9
|
+
import { dshHistorySummarizer } from "../../integrations/dsh/history-summary.js";
|
|
10
|
+
import { finalDshAnswer } from "../../integrations/dsh/messages.js";
|
|
11
|
+
|
|
12
|
+
/** One server-owned organizer per workspace, shared by its parent Sessions. */
|
|
13
|
+
export class DshHistoryWork {
|
|
14
|
+
readonly service: HistoryService;
|
|
15
|
+
readonly #parents = new Set<Agent>();
|
|
16
|
+
#controller: AbortController | undefined;
|
|
17
|
+
#work: Promise<void> | undefined;
|
|
18
|
+
#closing: Promise<void> | undefined;
|
|
19
|
+
constructor(private readonly ctx: Context, root: string) {
|
|
20
|
+
const agents = new AgentFiles(dshHomePath("pbot", "subagents"), "dsh");
|
|
21
|
+
this.service = new HistoryService(root, new HistoryFiles(join(root, ".dsh/pbot/history"), "dsh"),
|
|
22
|
+
new DshHistory(ctx, root, () => agents.list().filter(value => value.workspace === root).map(value => value.agentId)),
|
|
23
|
+
undefined, new HistoryFiles(join(root, ".pi/pbot/history")));
|
|
24
|
+
}
|
|
25
|
+
attach(agent: Agent) { this.#parents.add(agent); return () => { this.#parents.delete(agent); }; }
|
|
26
|
+
cancel() { this.#controller?.abort(); }
|
|
27
|
+
schedule(agent: Agent) {
|
|
28
|
+
if (this.#closing || [...this.#parents].some(parent => parent.status !== "idle" || parent.inbox.nextTurn.length || parent.inbox.nextStep.length)) return;
|
|
29
|
+
this.cancel();
|
|
30
|
+
const controller = new AbortController(), selection = agent.model.current(), sessionId = agent.id;
|
|
31
|
+
this.#controller = controller;
|
|
32
|
+
const signal = controller.signal;
|
|
33
|
+
const work = (this.#work ?? Promise.resolve()).catch(() => {}).then(async () => {
|
|
34
|
+
signal.throwIfAborted();
|
|
35
|
+
await this.service.refresh({ host: "dsh", sessionId }, signal);
|
|
36
|
+
signal.throwIfAborted();
|
|
37
|
+
await this.service.organize(dshHistorySummarizer(this.ctx, selection, sessionId), signal);
|
|
38
|
+
}).catch(error => { if (!signal.aborted) this.ctx.logger.warn("pbot 历史整理未完成:" + String(error)); });
|
|
39
|
+
this.#work = work;
|
|
40
|
+
void work.finally(() => { if (this.#work === work) { this.#work = undefined; this.#controller = undefined; } });
|
|
41
|
+
}
|
|
42
|
+
close() {
|
|
43
|
+
if (this.#closing) return this.#closing;
|
|
44
|
+
this.cancel();
|
|
45
|
+
this.#closing = (async () => { await this.#work; await this.service.close(); this.#parents.clear(); })();
|
|
46
|
+
return this.#closing;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function registerDshHistoryEvents(ctx: Context, agent: Agent, work: DshHistoryWork, check: () => void) {
|
|
51
|
+
let normal = false, final = false;
|
|
52
|
+
ctx.effect(() => work.attach(agent));
|
|
53
|
+
ctx.on("session/event", (session, event) => {
|
|
54
|
+
if (session !== agent.session) return;
|
|
55
|
+
if (event.type === "turn/start") { normal = false; final = false; }
|
|
56
|
+
if (event.type === "assistant/message") final = Boolean(finalDshAnswer(event).trim());
|
|
57
|
+
if (event.type === "agent/inbox/spliced" && event.data.inserted.some(message => message.source.kind === "user")) { normal = false; work.cancel(); }
|
|
58
|
+
if (event.type === "turn/end") normal = event.data.reason.kind === "completed" && final;
|
|
59
|
+
});
|
|
60
|
+
ctx.on("agent/pre-step", async (_payload, next) => { work.cancel(); check(); return next(); });
|
|
61
|
+
ctx.on("agent/status", ({ agent: changed, status }) => {
|
|
62
|
+
if (changed !== agent || status !== "idle" || !normal) return;
|
|
63
|
+
normal = false;
|
|
64
|
+
if (!agent.inbox.nextTurn.length && !agent.inbox.nextStep.length) {
|
|
65
|
+
try { check(); } catch (error) { if ((error as Error).name === "AbortError") return; throw error; }
|
|
66
|
+
work.schedule(agent);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
2
|
+
import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
3
|
+
import type {} from "@deepseek-ai/dsh-typert-registry";
|
|
4
|
+
import { SessionId } from "@deepseek-ai/dsh-session";
|
|
5
|
+
import { extractSessionEventText } from "@deepseek-ai/dsh-session-query";
|
|
6
|
+
import { Type, type Static } from "typebox";
|
|
7
|
+
import { json, type Json } from "../../common/json.js";
|
|
8
|
+
import { requireThat, schema } from "../../common/validation.js";
|
|
9
|
+
import { packageVersion } from "../../common/version.js";
|
|
10
|
+
import { historyInput } from "../../tools/history/contract.js";
|
|
11
|
+
import { executeHistory } from "../../tools/history/actions.js";
|
|
12
|
+
import { pbotReadRemote } from "../../ui/dsh/remote.js";
|
|
13
|
+
import type { DshWorkspaces, DshWorkspace } from "./workspace.js";
|
|
14
|
+
|
|
15
|
+
const requestSchema = Type.Object({ parentSessionId: Type.String({ minLength: 1 }), input: Type.Optional(Type.Unknown()) }, { additionalProperties: false });
|
|
16
|
+
const expertSchema = Type.Object({ agentId: Type.String({ minLength: 1 }), offset: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
17
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 100 })), seq: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
18
|
+
textOffset: Type.Optional(Type.Integer({ minimum: 0 })), textLimit: Type.Optional(Type.Integer({ minimum: 2, maximum: 24000 })),
|
|
19
|
+
format: Type.Optional(Type.Union([Type.Literal("text"), Type.Literal("json")])) }, { additionalProperties: false });
|
|
20
|
+
const teamSchema = Type.Object({ teamId: Type.String({ minLength: 1 }), afterSeq: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
21
|
+
seq: Type.Optional(Type.Integer({ minimum: 1 })), offset: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
22
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 16000 })) }, { additionalProperties: false });
|
|
23
|
+
const validRequest = schema(requestSchema), validExpert = schema(expertSchema), validTeam = schema(teamSchema);
|
|
24
|
+
|
|
25
|
+
export function dshStatus(workspaces: DshWorkspaces, workspace: DshWorkspace): Json {
|
|
26
|
+
workspace.check();
|
|
27
|
+
const agents = workspace.agents.list(workspace.owner);
|
|
28
|
+
return json({ host: "dsh", release: packageVersion, workspaceRoot: workspace.root, sessionId: workspace.agent.id,
|
|
29
|
+
model: workspace.agent.model.current() ?? null, python: workspaces.python.status,
|
|
30
|
+
controlError: workspace.controlError ?? null, history: workspace.history.status,
|
|
31
|
+
agents, teams: workspace.teams.list(workspace.owner, agents) });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Native Remote owns transport and access checks. These workspace reads do not admit or cancel execution. */
|
|
35
|
+
export class PbotManagement extends TypertRemoteService {
|
|
36
|
+
constructor(ctx: Context, private readonly workspaces: DshWorkspaces) { super(ctx, "pbotManagement", { namespace: "pbot" }); }
|
|
37
|
+
|
|
38
|
+
private async workspace(request: Json, signal: AbortSignal) {
|
|
39
|
+
signal.throwIfAborted(); requireThat(validRequest(request), "管理查询参数无效。");
|
|
40
|
+
const args = request as Static<typeof requestSchema>;
|
|
41
|
+
const agent = this.ctx.agents.get(SessionId(args.parentSessionId));
|
|
42
|
+
requireThat(agent, "父会话未加载或已关闭,请在原生会话页面重新打开。");
|
|
43
|
+
const workspace = await this.workspaces.get(agent);
|
|
44
|
+
signal.throwIfAborted(); workspace.check();
|
|
45
|
+
return { workspace, input: args.input, signal: AbortSignal.any([signal, workspace.lifetime.signal]) };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Remote
|
|
49
|
+
async snapshot(request: Json, signal: AbortSignal): Promise<Json> {
|
|
50
|
+
const { workspace, input } = await this.workspace(request, signal);
|
|
51
|
+
requireThat(input === undefined, "工作区状态查询不接受额外参数。");
|
|
52
|
+
return dshStatus(this.workspaces, workspace);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@Remote
|
|
56
|
+
async expert(request: Json, signal: AbortSignal): Promise<Json> {
|
|
57
|
+
const current = await this.workspace(request, signal), { workspace, input } = current;
|
|
58
|
+
requireThat(validExpert(input), "专家记录查询参数无效。");
|
|
59
|
+
const args = input as Static<typeof expertSchema>;
|
|
60
|
+
const view = workspace.agents.status(args.agentId, workspace.owner);
|
|
61
|
+
using observation = await this.ctx.sessionQuery.observeSession(SessionId(view.agentId), { projectionMode: "none", signal: current.signal });
|
|
62
|
+
workspace.check(); current.signal.throwIfAborted();
|
|
63
|
+
const header = observation.header;
|
|
64
|
+
requireThat(header.id === view.agentId && header.cwd === view.cwd && header.parentSession === workspace.agent.id &&
|
|
65
|
+
header.origin === "subagent" && !header.isSeeded, "原生专家记录的身份或归属不匹配。");
|
|
66
|
+
const events = observation.events;
|
|
67
|
+
if (args.seq !== undefined) {
|
|
68
|
+
const event = events.find(value => value.seq === args.seq);
|
|
69
|
+
requireThat(event, "原生记录位置不存在。");
|
|
70
|
+
const text = (args.format === "text" ? extractSessionEventText(event) : "") || JSON.stringify(event, null, 2), offset = args.textOffset ?? 0;
|
|
71
|
+
requireThat(offset <= text.length, "原生记录分页位置超出范围。");
|
|
72
|
+
let end = Math.min(text.length, offset + (args.textLimit ?? 12000));
|
|
73
|
+
if (end < text.length && /[\uD800-\uDBFF]/.test(text[end - 1]!)) end--;
|
|
74
|
+
return json({ agent: view, cursor: observation.cursor, seq: event.seq, offset, text: text.slice(offset, end),
|
|
75
|
+
totalLength: text.length, nextOffset: end < text.length ? end : null });
|
|
76
|
+
}
|
|
77
|
+
const limit = args.limit ?? 30, offset = args.offset ?? Math.max(0, events.length - limit);
|
|
78
|
+
requireThat(offset <= events.length, "原生记录分页位置超出范围。");
|
|
79
|
+
return json({ agent: view, header, cursor: observation.cursor, total: events.length, offset,
|
|
80
|
+
nextOffset: offset + limit < events.length ? offset + limit : null,
|
|
81
|
+
events: events.slice(offset, offset + limit).map(event => ({ seq: event.seq, type: event.type, time: event.time,
|
|
82
|
+
...(event.type === "user/message" ? { source: event.data.source.kind } : {}), preview: extractSessionEventText(event).slice(0, 800) })) });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@Remote
|
|
86
|
+
async team(request: Json, signal: AbortSignal): Promise<Json> {
|
|
87
|
+
const { workspace, input } = await this.workspace(request, signal);
|
|
88
|
+
requireThat(validTeam(input), "团队记录查询参数无效。");
|
|
89
|
+
const args = input as Static<typeof teamSchema>;
|
|
90
|
+
return json({ team: workspace.teams.status(args.teamId, workspace.owner, workspace.agents.list(workspace.owner)),
|
|
91
|
+
page: workspace.teams.read(args.teamId, { kind: "user", owner: workspace.owner }, args) });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@Remote
|
|
95
|
+
async history(request: Json, signal: AbortSignal): Promise<Json> {
|
|
96
|
+
const current = await this.workspace(request, signal), { workspace, input } = current;
|
|
97
|
+
const result = await executeHistory(workspace.history, historyInput(input), { host: "dsh", sessionId: workspace.agent.id }, current.signal);
|
|
98
|
+
workspace.check(); current.signal.throwIfAborted(); return json(result);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
declare module "@deepseek-ai/cordis" { interface Context { pbotManagement: PbotManagement } }
|
|
103
|
+
|
|
104
|
+
export function registerDshManagement(ctx: Context, workspaces: DshWorkspaces) {
|
|
105
|
+
ctx.typert.register({ package: pbotReadRemote.package, face: "host", schemas: [],
|
|
106
|
+
model: { services: [], events: [], objects: [] }, invocations: pbotReadRemote.descriptors });
|
|
107
|
+
return new PbotManagement(ctx, workspaces);
|
|
108
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
2
|
+
import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
|
|
3
|
+
import type { Agent } from "@deepseek-ai/dsh-agent";
|
|
4
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
5
|
+
import type { AgentOwner } from "../../model/agent.js";
|
|
6
|
+
import { DshAgents } from "../../integrations/dsh/dsh-agents.js";
|
|
7
|
+
import { dshResources, dshExpertTools } from "../../integrations/dsh/resources.js";
|
|
8
|
+
import { AgentService } from "../../services/agents/agent-service.js";
|
|
9
|
+
import { TeamService } from "../../services/agents/team-service.js";
|
|
10
|
+
import { registerDshAgentTool } from "../../tools/agents/dsh-tool.js";
|
|
11
|
+
import { registerDshTeamTool, registerDshTeamMessageTool } from "../../tools/teams/dsh-tool.js";
|
|
12
|
+
import { DshAgentRuntime } from "./agent-runtime.js";
|
|
13
|
+
import { DshAgentEvents } from "./agent-events.js";
|
|
14
|
+
import { registerDshFileEvents } from "./files.js";
|
|
15
|
+
import { DshHistoryWork, registerDshHistoryEvents } from "./history.js";
|
|
16
|
+
import type { HistoryService } from "../../services/history/history-service.js";
|
|
17
|
+
import { registerDshHistoryTool } from "../../tools/history/dsh-tool.js";
|
|
18
|
+
import { activatePython } from "../../integrations/python-activation.js";
|
|
19
|
+
import { PythonEnvironmentService } from "../../services/workspace/python-environment-service.js";
|
|
20
|
+
import { requireThat } from "../../common/validation.js";
|
|
21
|
+
|
|
22
|
+
export const dshDependencies = ["agents", "sessions", "sessionPersistence", "sessionQuery", "llm", "tools", "systemPrompt", "skills", "fs", "attachments", "workingDirectory", "approval"];
|
|
23
|
+
|
|
24
|
+
export class DshWorkspace {
|
|
25
|
+
readonly lifetime = new AbortController();
|
|
26
|
+
readonly host: DshAgents;
|
|
27
|
+
readonly agents: AgentService;
|
|
28
|
+
readonly teams: TeamService;
|
|
29
|
+
readonly history: HistoryService;
|
|
30
|
+
readonly owner: AgentOwner;
|
|
31
|
+
readonly #experts = new Map<string, DshAgentRuntime>();
|
|
32
|
+
#scope: { dispose(): Promise<void> } | undefined;
|
|
33
|
+
#events: DshAgentEvents | undefined;
|
|
34
|
+
#closing: Promise<void> | undefined;
|
|
35
|
+
constructor(readonly ctx: Context, readonly root: string, readonly agent: Agent,
|
|
36
|
+
readonly python: PythonEnvironmentService, readonly component: AbortSignal,
|
|
37
|
+
private readonly historyWork: DshHistoryWork, private readonly releaseHistory: () => Promise<void>) {
|
|
38
|
+
this.history = historyWork.service;
|
|
39
|
+
this.owner = { sessionId: agent.id, leaf: null, branch: [] };
|
|
40
|
+
this.host = new DshAgents(ctx, agent, {
|
|
41
|
+
setup: (d, _directory, generation) => async (agentCtx, child) => {
|
|
42
|
+
for (const key of ["noExtensions", "noPromptTemplates", "noContextFiles"] as const) requireThat(d.options[key] === undefined, "dsh 不支持宿主选项:" + key);
|
|
43
|
+
await agentCtx.plugin({ name: "pbot-expert", inject: dshDependencies, apply: async scope => {
|
|
44
|
+
const runtime = new DshAgentRuntime(scope, child, d, generation, this.agents.files, this.teams,
|
|
45
|
+
{ check: this.check, release: () => this.host.release(d.agentId, generation) });
|
|
46
|
+
this.#experts.set(d.agentId, runtime);
|
|
47
|
+
scope.effect(() => () => { if (this.#experts.get(d.agentId) === runtime) this.#experts.delete(d.agentId); });
|
|
48
|
+
await dshResources(scope, child, root, "general-purpose", () => python.status, runtime.check);
|
|
49
|
+
const teamTool = dshExpertTools(scope, child, d.options);
|
|
50
|
+
if (teamTool) registerDshTeamMessageTool(scope, child, this.teams, runtime.actor, runtime.check);
|
|
51
|
+
registerDshFileEvents(scope, child, root, true, runtime.check);
|
|
52
|
+
runtime.initialize(teamTool);
|
|
53
|
+
} });
|
|
54
|
+
return { commit: this.check };
|
|
55
|
+
},
|
|
56
|
+
ready: (child, generation) => {
|
|
57
|
+
const runtime = this.#experts.get(child.id);
|
|
58
|
+
requireThat(runtime?.generation === generation, "专家执行组件未就绪。"); runtime.start();
|
|
59
|
+
},
|
|
60
|
+
stopping: async (child, generation) => { const runtime = this.#experts.get(child.id); if (runtime?.generation === generation) await runtime.stop(); },
|
|
61
|
+
}, this.lifetime.signal);
|
|
62
|
+
this.agents = new AgentService(root, dshHomePath("pbot", "subagents"), this.host);
|
|
63
|
+
this.teams = new TeamService(root, this.agents.files, this.agents.teamFiles);
|
|
64
|
+
}
|
|
65
|
+
check = () => { this.component.throwIfAborted(); this.lifetime.signal.throwIfAborted(); };
|
|
66
|
+
get controlError() { return this.#events?.error; }
|
|
67
|
+
async open() {
|
|
68
|
+
this.check();
|
|
69
|
+
this.#scope = await this.agent.ctx.plugin({ name: "pbot-coordinator", inject: dshDependencies, apply: async ctx => {
|
|
70
|
+
await dshResources(ctx, this.agent, this.root, "coordinator", () => this.python.status, this.check);
|
|
71
|
+
registerDshFileEvents(ctx, this.agent, this.root, false, this.check);
|
|
72
|
+
registerDshAgentTool(ctx, this.agent, this.agents, this.owner, this.check);
|
|
73
|
+
registerDshTeamTool(ctx, this.agent, this.teams, this.agents, this.owner, this.check);
|
|
74
|
+
registerDshTeamMessageTool(ctx, this.agent, this.teams, () => ({ kind: "coordinator", owner: this.owner }), this.check);
|
|
75
|
+
registerDshHistoryTool(ctx, this.agent, this.history, this.lifetime.signal, this.check);
|
|
76
|
+
registerDshHistoryEvents(ctx, this.agent, this.historyWork, this.check);
|
|
77
|
+
this.#events = new DshAgentEvents(ctx, this.agent, this.agents, this.teams, this.owner, this.check);
|
|
78
|
+
this.#events.start();
|
|
79
|
+
} });
|
|
80
|
+
this.check();
|
|
81
|
+
}
|
|
82
|
+
expert(id: string) {
|
|
83
|
+
this.check(); const view = this.agents.status(id, this.owner), runtime = this.#experts.get(id);
|
|
84
|
+
requireThat(view.owned, "专家不属于当前父会话。");
|
|
85
|
+
requireThat(view.alive, "专家已停止;需要明确恢复后才能对话。");
|
|
86
|
+
requireThat(view.state !== "starting", "专家尚未就绪,请等待启动或恢复完成。");
|
|
87
|
+
requireThat(runtime?.generation === view.generation, "原生专家由其他执行实例持有;当前组件不能接管对话。");
|
|
88
|
+
return runtime;
|
|
89
|
+
}
|
|
90
|
+
close() {
|
|
91
|
+
if (this.#closing) return this.#closing;
|
|
92
|
+
this.lifetime.abort();
|
|
93
|
+
this.#closing = (async () => {
|
|
94
|
+
const errors: unknown[] = [];
|
|
95
|
+
try { await this.#events?.close(); } catch (error) { errors.push(error); }
|
|
96
|
+
try { this.teams.pauseAll(this.owner, "父会话或执行组件关闭"); } catch (error) { errors.push(error); }
|
|
97
|
+
try { await this.agents.close(this.owner); } catch (error) { errors.push(error); }
|
|
98
|
+
try { await this.host.close(); } catch (error) { errors.push(error); }
|
|
99
|
+
this.teams.dispose();
|
|
100
|
+
try { await this.#scope?.dispose(); } catch (error) { errors.push(error); }
|
|
101
|
+
try { await this.releaseHistory(); } catch (error) { errors.push(error); }
|
|
102
|
+
if (errors.length) throw new AggregateError(errors, "dsh 工作区关闭失败;请核对执行资源和控制记录。");
|
|
103
|
+
})();
|
|
104
|
+
return this.#closing;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The execution component owns Python; closing one Session must not undo its activation. */
|
|
109
|
+
export class DshWorkspaces {
|
|
110
|
+
readonly python = new PythonEnvironmentService(process.cwd());
|
|
111
|
+
readonly #parents = new Map<Agent, Promise<DshWorkspace>>();
|
|
112
|
+
readonly #parentEffects = new Map<Agent, () => Promise<void>>();
|
|
113
|
+
readonly #histories = new Map<string, { work: DshHistoryWork; users: number }>();
|
|
114
|
+
readonly #lifetime = new AbortController();
|
|
115
|
+
#restorePython: (() => void) | undefined;
|
|
116
|
+
#preparing: Promise<{ directory: string; python: string }> | undefined;
|
|
117
|
+
#closing: Promise<void> | undefined;
|
|
118
|
+
constructor(readonly ctx: Context) {}
|
|
119
|
+
|
|
120
|
+
private async history(root: string): Promise<{ work: DshHistoryWork; release(): Promise<void> }> {
|
|
121
|
+
this.#lifetime.signal.throwIfAborted();
|
|
122
|
+
let entry = this.#histories.get(root);
|
|
123
|
+
if (entry && entry.users === 0) {
|
|
124
|
+
await entry.work.close(); this.#lifetime.signal.throwIfAborted();
|
|
125
|
+
if (this.#histories.get(root) === entry) this.#histories.delete(root);
|
|
126
|
+
return this.history(root);
|
|
127
|
+
}
|
|
128
|
+
if (!entry) { entry = { work: new DshHistoryWork(this.ctx, root), users: 0 }; this.#histories.set(root, entry); }
|
|
129
|
+
const owned = entry; owned.users++;
|
|
130
|
+
let released = false;
|
|
131
|
+
return { work: owned.work, release: async () => {
|
|
132
|
+
if (released) return; released = true;
|
|
133
|
+
if (--owned.users === 0) {
|
|
134
|
+
try { await owned.work.close(); } finally { if (this.#histories.get(root) === owned) this.#histories.delete(root); }
|
|
135
|
+
}
|
|
136
|
+
} };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async open(agent: Agent) {
|
|
140
|
+
this.#lifetime.signal.throwIfAborted();
|
|
141
|
+
requireThat(this.ctx.agents.roots().includes(agent) && agent.session.header.origin !== "subagent", "专家不拥有协调者管理入口。");
|
|
142
|
+
const current = this.#parents.get(agent);
|
|
143
|
+
if (current) return current;
|
|
144
|
+
const opening = (async () => {
|
|
145
|
+
const root = await realpath(agent.session.header.cwd ?? process.cwd());
|
|
146
|
+
this.#lifetime.signal.throwIfAborted();
|
|
147
|
+
const history = await this.history(root);
|
|
148
|
+
let workspace: DshWorkspace | undefined;
|
|
149
|
+
try {
|
|
150
|
+
workspace = new DshWorkspace(this.ctx, root, agent, this.python, this.#lifetime.signal, history.work, history.release);
|
|
151
|
+
const owned = workspace;
|
|
152
|
+
const dispose = agent.ctx.effect(() => async () => {
|
|
153
|
+
this.#parents.delete(agent); this.#parentEffects.delete(agent); await owned.close();
|
|
154
|
+
});
|
|
155
|
+
this.#parentEffects.set(agent, dispose);
|
|
156
|
+
await workspace.open(); this.#lifetime.signal.throwIfAborted(); return workspace;
|
|
157
|
+
} catch (error) {
|
|
158
|
+
try {
|
|
159
|
+
const dispose = this.#parentEffects.get(agent);
|
|
160
|
+
if (dispose) await dispose(); else if (workspace) await workspace.close(); else await history.release();
|
|
161
|
+
} catch (cleanup) { throw new AggregateError([error, cleanup], "dsh 工作区装配与清理均未完成。"); }
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
})();
|
|
165
|
+
this.#parents.set(agent, opening);
|
|
166
|
+
try { return await opening; }
|
|
167
|
+
catch (error) { this.#parents.delete(agent); throw error; }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async get(agent: Agent) {
|
|
171
|
+
this.#lifetime.signal.throwIfAborted();
|
|
172
|
+
requireThat(this.ctx.agents.get(agent.id) === agent, "父会话已关闭。");
|
|
173
|
+
const workspace = await this.open(agent);
|
|
174
|
+
workspace.check();
|
|
175
|
+
return workspace;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
preparePython() {
|
|
179
|
+
this.#lifetime.signal.throwIfAborted();
|
|
180
|
+
if (this.#preparing) return this.#preparing;
|
|
181
|
+
this.#preparing = (async () => {
|
|
182
|
+
const result = await this.python.setup(this.#lifetime.signal);
|
|
183
|
+
this.#lifetime.signal.throwIfAborted();
|
|
184
|
+
if (!this.#restorePython) this.#restorePython = activatePython(result.directory);
|
|
185
|
+
return result;
|
|
186
|
+
})().finally(() => { this.#preparing = undefined; });
|
|
187
|
+
return this.#preparing;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
close() {
|
|
191
|
+
if (this.#closing) return this.#closing;
|
|
192
|
+
this.#lifetime.abort();
|
|
193
|
+
this.#closing = (async () => {
|
|
194
|
+
try {
|
|
195
|
+
const opened = await Promise.allSettled(this.#parents.values());
|
|
196
|
+
const closed = await Promise.allSettled(opened.filter(v => v.status === "fulfilled").map(v => v.value.close()));
|
|
197
|
+
const effects = await Promise.allSettled([...this.#parentEffects.values()].map(dispose => dispose()));
|
|
198
|
+
const histories = await Promise.allSettled([...this.#histories.values()].map(value => value.work.close()));
|
|
199
|
+
await this.python.close();
|
|
200
|
+
const errors = [...new Set([...closed, ...effects, ...histories].flatMap(value => value.status === "rejected" ? [value.reason] : []))];
|
|
201
|
+
if (errors.length) throw new AggregateError(errors, "部分 dsh 工作区未能完成关闭。");
|
|
202
|
+
} finally { this.#parents.clear(); this.#parentEffects.clear(); this.#histories.clear(); this.#restorePython?.(); this.#restorePython = undefined; }
|
|
203
|
+
})();
|
|
204
|
+
return this.#closing;
|
|
205
|
+
}
|
|
206
|
+
}
|
package/src/extension/events.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { resourceRoot } from "../common/paths.js";
|
|
7
|
-
import { agentText } from "../integrations/
|
|
7
|
+
import { agentText } from "../integrations/agent-resources.js";
|
|
8
8
|
import type { Confirmations } from "../ui/confirmation.js";
|
|
9
9
|
import type { Lifecycle } from "./lifecycle.js";
|
|
10
10
|
|
|
@@ -12,12 +12,14 @@ export function registerEvents(pi: ExtensionAPI, lifecycle: Lifecycle, confirmat
|
|
|
12
12
|
pi.on("session_start", async (_event, ctx) => {
|
|
13
13
|
try {
|
|
14
14
|
await lifecycle.open(ctx.cwd);
|
|
15
|
-
ctx.ui.
|
|
15
|
+
if (ctx.mode === "tui") ctx.ui.setStatus("pbot-python", "Python 准备中");
|
|
16
|
+
else ctx.ui.notify("正在准备共享 Python 环境;首次安装可能需要数分钟。", "info");
|
|
16
17
|
try { await lifecycle.get().preparePython(); }
|
|
17
18
|
catch (error) {
|
|
18
19
|
const message = error instanceof Error ? error.message : String(error);
|
|
19
|
-
if (ctx.
|
|
20
|
-
|
|
20
|
+
if (ctx.mode === "tui") ctx.ui.notify("Python 未就绪;/pbot status 查看详情。", "warning");
|
|
21
|
+
else if (ctx.hasUI) ctx.ui.notify(message, "warning"); else console.error(message);
|
|
22
|
+
} finally { if (ctx.mode === "tui") ctx.ui.setStatus("pbot-python", undefined); }
|
|
21
23
|
} catch (error) {
|
|
22
24
|
if (ctx.hasUI) ctx.ui.notify(error instanceof Error ? error.message : "pbot 启动失败。", "error");
|
|
23
25
|
else throw error;
|
package/src/extension/history.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { historyLocation, historySummarizer } from "../integrations/pi/history.js";
|
|
3
3
|
import type { Lifecycle, WorkspaceInstance } from "./lifecycle.js";
|
|
4
|
+
import { readPage } from "../ui/pi/reader.js";
|
|
5
|
+
import { choose } from "../ui/pi/menu.js";
|
|
4
6
|
|
|
5
7
|
export function registerHistoryEvents(pi: ExtensionAPI, lifecycle: Lifecycle) {
|
|
6
8
|
pi.on("session_start", async (_event, ctx) => {
|
|
@@ -25,8 +27,8 @@ export function registerHistoryEvents(pi: ExtensionAPI, lifecycle: Lifecycle) {
|
|
|
25
27
|
});
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
const label = (node: { kind: string; from: string; ready: boolean; preview: string }) =>
|
|
29
|
-
`${node.from.slice(0,
|
|
30
|
+
const label = (node: { host: string; kind: string; from: string; ready: boolean; preview: string }) =>
|
|
31
|
+
`${node.host} · ${node.from.slice(0, 10)} · ${node.kind === "session" ? "会话" : node.kind === "summary" ? "摘要" : "问答"}${node.ready ? "" : "(待整理)"} · ${node.preview.replace(/\s+/g, " ").slice(0, 48)}`;
|
|
30
32
|
|
|
31
33
|
export async function showHistory(instance: WorkspaceInstance, ctx: ExtensionCommandContext, query: string) {
|
|
32
34
|
await instance.history.refresh(historyLocation(ctx), instance.signal(ctx.signal));
|
|
@@ -37,35 +39,36 @@ export async function showHistory(instance: WorkspaceInstance, ctx: ExtensionCom
|
|
|
37
39
|
for (;;) {
|
|
38
40
|
instance.check();
|
|
39
41
|
const result = service.search({ query, offset });
|
|
40
|
-
const choices = result.matches.map((node, index) => `${index + 1}. ${label(node)}
|
|
41
|
-
const more = "下一页", search = "
|
|
42
|
-
const selected = await ctx
|
|
42
|
+
const choices = result.matches.map((node, index) => `${index + 1}. ${label(node)}`);
|
|
43
|
+
const more = "下一页", previous = "上一页", search = "搜索", back = "返回";
|
|
44
|
+
const selected = await choose(ctx, `历史 · ${query || "最近会话"} · ${result.total} 项`, [...choices, ...(offset ? [previous] : []), ...(result.nextOffset === null ? [] : [more]), search, back]);
|
|
43
45
|
if (!selected || selected === back) return;
|
|
44
46
|
if (selected === more) { offset = result.nextOffset!; continue; }
|
|
47
|
+
if (selected === previous) { offset = Math.max(0, offset - 20); continue; }
|
|
45
48
|
if (selected === search) {
|
|
46
49
|
const value = await ctx.ui.input("全文搜索(留空列出会话)", query);
|
|
47
50
|
if (value !== undefined) { query = value; offset = 0; }
|
|
48
51
|
continue;
|
|
49
52
|
}
|
|
50
|
-
let
|
|
53
|
+
let current = result.matches[choices.indexOf(selected)]!, id = current.id, source = current.host, textOffset = 0, childrenOffset = 0;
|
|
51
54
|
for (;;) {
|
|
52
55
|
instance.check();
|
|
53
|
-
const node = service.read(id, textOffset, 12000, childrenOffset);
|
|
54
|
-
const body = "
|
|
56
|
+
const node = service.read(id, textOffset, 12000, childrenOffset, source);
|
|
57
|
+
const body = "正文", nextText = "后续正文", nextChildren = "更多下级", first = "回到开头";
|
|
55
58
|
const links = [...node.parents.map(parent => ({ ...parent, direction: "↑" })), ...node.children.map(child => ({ ...child, direction: "↓" }))];
|
|
56
59
|
const names = links.map((value, index) => `${index + 1}. ${value.direction} ${label(value)}`);
|
|
57
|
-
const action = await ctx
|
|
60
|
+
const action = await choose(ctx, label(node), [body, ...names, ...(node.nextOffset === null ? [] : [nextText]),
|
|
58
61
|
...(node.nextChildrenOffset === null ? [] : [nextChildren]), ...(textOffset || childrenOffset ? [first] : []), back]);
|
|
59
62
|
if (!action || action === back) break;
|
|
60
63
|
if (action === body || action === nextText) {
|
|
61
64
|
if (action === nextText) textOffset = node.nextOffset!;
|
|
62
|
-
const page = service.read(id, textOffset, 12000, childrenOffset);
|
|
63
|
-
await ctx
|
|
65
|
+
const page = service.read(id, textOffset, 12000, childrenOffset, source);
|
|
66
|
+
await readPage(ctx, `历史正文 · ${textOffset + 1}–${textOffset + page.text.length}/${page.totalLength}`, page.text || "摘要待整理,请展开下级记录。");
|
|
64
67
|
continue;
|
|
65
68
|
}
|
|
66
69
|
if (action === nextChildren) { childrenOffset = node.nextChildrenOffset!; continue; }
|
|
67
70
|
if (action === first) { textOffset = 0; childrenOffset = 0; continue; }
|
|
68
|
-
|
|
71
|
+
const link = links[names.indexOf(action)]!; id = link.id; source = link.host; textOffset = 0; childrenOffset = 0;
|
|
69
72
|
}
|
|
70
73
|
}
|
|
71
74
|
}
|