personal-bot 0.2.4 → 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 +9 -7
- package/README.md +13 -6
- package/README.zh-CN.md +13 -6
- package/docs/README.md +3 -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 +3 -1
- package/docs/interaction-design.md +13 -0
- package/docs/release.md +3 -3
- package/docs/technical-design.md +29 -15
- package/docs/validation.md +38 -2
- 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/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/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/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/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/npm-shrinkwrap.json +11140 -830
- package/package.json +94 -12
- package/python/README.md +6 -6
- package/resources/agents/coordinator.md +1 -1
- package/resources/dsh/cordis.patch.yml +3 -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 +9 -1
- 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/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/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/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/extension/command.ts
CHANGED
|
@@ -2,6 +2,10 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { packageVersion } from "../common/version.js";
|
|
3
3
|
import { prepareWorkspace } from "../services/workspace/workspace-preparation.js";
|
|
4
4
|
import type { Confirmations } from "../ui/confirmation.js";
|
|
5
|
+
import { readPage } from "../ui/pi/reader.js";
|
|
6
|
+
import { choose } from "../ui/pi/menu.js";
|
|
7
|
+
import { stateLabel } from "../ui/presentation.js";
|
|
8
|
+
import { agentOwner } from "../integrations/pi/agent-context.js";
|
|
5
9
|
import type { Lifecycle } from "./lifecycle.js";
|
|
6
10
|
import { showAgents } from "./agents-command.js";
|
|
7
11
|
import { showTeams } from "./teams-command.js";
|
|
@@ -9,39 +13,54 @@ import { showHistory } from "./history.js";
|
|
|
9
13
|
|
|
10
14
|
export function registerCommand(pi: ExtensionAPI, lifecycle: Lifecycle, confirmations: Confirmations) {
|
|
11
15
|
pi.registerCommand("pbot", {
|
|
12
|
-
description: "
|
|
16
|
+
description: "工作区、专家、团队与历史",
|
|
13
17
|
getArgumentCompletions: prefix => {
|
|
14
18
|
if (/\s/.test(prefix)) return null;
|
|
15
19
|
return ["init", "status", "history", "agents", "teams", "python"]
|
|
16
|
-
.filter(command => command.startsWith(prefix))
|
|
17
|
-
.map(command => ({ value: command, label: command }));
|
|
20
|
+
.filter(command => command.startsWith(prefix)).map(command => ({ value: command, label: command }));
|
|
18
21
|
},
|
|
19
22
|
handler: async (args, ctx) => {
|
|
20
23
|
const [, command, argument = ""] = args.trim().match(/^(\S+)(?:\s+([\s\S]*))?$/) ?? [];
|
|
21
24
|
confirmations.resume();
|
|
22
|
-
|
|
23
|
-
await ctx.waitForIdle();
|
|
25
|
+
async function execute(command: string | undefined, argument: string) {
|
|
24
26
|
const instance = lifecycle.get();
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
if (command === "init" && !argument) {
|
|
28
|
+
await ctx.waitForIdle(); instance.check(); await prepareWorkspace(instance.root);
|
|
29
|
+
ctx.ui.notify("资料目录已准备。", "info"); return;
|
|
30
|
+
}
|
|
31
|
+
if (command === "status" && !argument) {
|
|
32
|
+
const python = instance.pythonEnvironment.status;
|
|
33
|
+
if (ctx.mode !== "tui") { ctx.ui.notify(JSON.stringify({ workspaceRoot: instance.root, release: packageVersion, python }, null, 2), "info"); return; }
|
|
34
|
+
const agents = instance.agents.list(agentOwner(ctx)), history = instance.history.status;
|
|
35
|
+
await readPage(ctx, "工作区", [instance.root, "", `版本 ${packageVersion}`,
|
|
36
|
+
`模型 ${ctx.model && !(ctx.model.provider === "unknown" && ctx.model.id === "unknown") ? ctx.model.provider + "/" + ctx.model.id : "未选择"}`,
|
|
37
|
+
`专家 ${agents.length} 位 · ${agents.filter(value => value.alive).length} 位存活`,
|
|
38
|
+
`Python ${stateLabel(python.state)}`, ...(python.directory ? [python.directory] : []), ...(python.error ? ["", python.error] : []),
|
|
39
|
+
"", `历史 ${history.organizing ? "整理中" : history.pending === null ? "查询时更新索引" : history.pending + " 项待整理"}`,
|
|
40
|
+
...history.warnings].join("\n")); return;
|
|
41
|
+
}
|
|
42
|
+
if (command === "history") { await showHistory(instance, ctx, argument); return; }
|
|
43
|
+
if (command === "agents") { await showAgents(pi, instance, ctx, confirmations); return; }
|
|
44
|
+
if (command === "teams") { await showTeams(pi, instance, ctx, confirmations); return; }
|
|
45
|
+
if (command === "python" && argument === "setup") {
|
|
46
|
+
const cancel = instance.signal(ctx.signal); await ctx.waitForIdle();
|
|
47
|
+
ctx.ui.notify("正在准备 Python…", "info");
|
|
48
|
+
const result = await instance.preparePython(cancel);
|
|
49
|
+
ctx.ui.notify(ctx.mode === "tui" ? "Python 已就绪。" : JSON.stringify(result, null, 2), "info"); return;
|
|
50
|
+
}
|
|
51
|
+
ctx.ui.notify("/pbot init | status | history [关键词] | agents | teams | python setup", "info");
|
|
28
52
|
}
|
|
29
|
-
if (command
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
ctx.ui.notify("正在准备共享 Python 环境;安装可能需要数分钟。", "info");
|
|
40
|
-
const result = await instance.preparePython(cancel);
|
|
41
|
-
ctx.ui.notify(JSON.stringify(result, null, 2), "info");
|
|
42
|
-
return;
|
|
53
|
+
if (command || ctx.mode !== "tui") { await execute(command, argument); return; }
|
|
54
|
+
if (confirmations.presenting) { ctx.ui.notify("请先处理待确认操作。", "info"); return; }
|
|
55
|
+
for (;;) {
|
|
56
|
+
const instance = lifecycle.get(), agents = instance.agents.list(agentOwner(ctx));
|
|
57
|
+
const choices = ["专家 · " + agents.length, "团队", "历史", "工作区状态", "准备资料目录", "准备 Python"];
|
|
58
|
+
const choice = await choose(ctx, `Personal Bot · Python ${stateLabel(instance.pythonEnvironment.status.state)}`, choices);
|
|
59
|
+
if (!choice) return;
|
|
60
|
+
const command = ["agents", "teams", "history", "status", "init", "python"][choices.indexOf(choice)]!;
|
|
61
|
+
try { await execute(command, command === "python" ? "setup" : ""); }
|
|
62
|
+
catch (error) { ctx.ui.notify(error instanceof Error ? error.message : String(error), "error"); }
|
|
43
63
|
}
|
|
44
|
-
ctx.ui.notify("用法:/pbot init | status | history [关键词] | agents | teams | python setup", "info");
|
|
45
64
|
},
|
|
46
65
|
});
|
|
47
66
|
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
3
|
+
import { createUserMessage, type UserMessage } from "@deepseek-ai/dsh-llm";
|
|
4
|
+
import { agentNotice, teamNotice } from "../../services/agents/agent-notices.js";
|
|
5
|
+
import { deliveredDshNotices, pendingDshNotices } from "../../integrations/dsh/notifications.js";
|
|
6
|
+
import { flushDshAgent } from "../../integrations/dsh/session.js";
|
|
7
|
+
import type { Agent } from "@deepseek-ai/dsh-agent";
|
|
8
|
+
import type { AgentService } from "../../services/agents/agent-service.js";
|
|
9
|
+
import type { TeamService } from "../../services/agents/team-service.js";
|
|
10
|
+
import type { AgentOwner, AgentCompletion } from "../../model/agent.js";
|
|
11
|
+
import type { TeamInput } from "../../model/team.js";
|
|
12
|
+
|
|
13
|
+
/** Server-owned scheduling survives browser disconnects; native cancellation pauses dispatch. */
|
|
14
|
+
export class DshAgentEvents {
|
|
15
|
+
readonly #dispatching = new Map<string, TeamInput>();
|
|
16
|
+
readonly #signals = new Map<AbortSignal, () => void>();
|
|
17
|
+
#timer: ReturnType<typeof setInterval> | undefined;
|
|
18
|
+
#scan: Promise<void> | undefined;
|
|
19
|
+
#closed = false;
|
|
20
|
+
#pausePending = false;
|
|
21
|
+
#paused = false;
|
|
22
|
+
#error: string | undefined;
|
|
23
|
+
#errorKey: string | undefined;
|
|
24
|
+
#busySince: number | undefined;
|
|
25
|
+
#recovery: "retry" | "repair" | "reload" | undefined;
|
|
26
|
+
#completed: AgentCompletion[] = [];
|
|
27
|
+
#autoWake = false;
|
|
28
|
+
constructor(readonly ctx: Context, readonly agent: Agent, readonly agents: AgentService,
|
|
29
|
+
readonly teams: TeamService, readonly owner: AgentOwner, readonly check: () => void) {}
|
|
30
|
+
get error() { return this.#error; }
|
|
31
|
+
start() {
|
|
32
|
+
this.teams.recover(this.owner);
|
|
33
|
+
this.ctx.on("agent/pre-step", async ({ signal }, next) => {
|
|
34
|
+
if (!this.#signals.has(signal)) {
|
|
35
|
+
const cancel = () => { if (!this.#closed) {
|
|
36
|
+
this.#paused = true; this.#autoWake = false; this.#pausePending = true;
|
|
37
|
+
try { this.pauseTeams(); } catch (error) { this.recordError(error); }
|
|
38
|
+
void this.refresh();
|
|
39
|
+
} };
|
|
40
|
+
this.#signals.set(signal, cancel); signal.addEventListener("abort", cancel, { once: true });
|
|
41
|
+
if (signal.aborted) cancel();
|
|
42
|
+
}
|
|
43
|
+
await this.refresh(); this.check();
|
|
44
|
+
const decision = await next(); this.check();
|
|
45
|
+
if (decision.kind !== "enter" || this.#paused) return decision;
|
|
46
|
+
const note = this.notice(decision.messages);
|
|
47
|
+
return note ? { ...decision, messages: [...decision.messages, note] } : decision;
|
|
48
|
+
});
|
|
49
|
+
this.ctx.on("session/event", (session, event) => {
|
|
50
|
+
if (session !== this.agent.session || this.#closed) return;
|
|
51
|
+
if (event.type === "agent/inbox/spliced" && event.data.inserted.some(message => message.source.kind === "user")) { this.#paused = false; this.#autoWake = true; }
|
|
52
|
+
if (event.type === "user/message" && event.data.source.kind === "pbot-results") this.#autoWake = true;
|
|
53
|
+
if (event.type === "turn/end") {
|
|
54
|
+
for (const [signal, cancel] of this.#signals) signal.removeEventListener("abort", cancel); this.#signals.clear();
|
|
55
|
+
if (event.data.reason.kind === "aborted") {
|
|
56
|
+
this.#paused = true; this.#autoWake = false; this.#pausePending = true;
|
|
57
|
+
try { this.pauseTeams(); } catch (error) { this.recordError(error); }
|
|
58
|
+
}
|
|
59
|
+
void this.refresh();
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
this.#timer = setInterval(() => { void this.refresh(); }, 1000); this.#timer.unref();
|
|
63
|
+
this.ctx.effect(() => () => this.close());
|
|
64
|
+
}
|
|
65
|
+
private pauseTeams() {
|
|
66
|
+
if (!this.#pausePending) return;
|
|
67
|
+
try { this.teams.pauseAll(this.owner, "主会话中断"); this.#pausePending = false; this.#dispatching.clear(); }
|
|
68
|
+
catch (error) {
|
|
69
|
+
if ((error as NodeJS.ErrnoException).code !== "PBOT_CONTROL_BUSY") { this.teams.fail(error); this.#dispatching.clear(); }
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
private recordError(error: unknown) {
|
|
74
|
+
if (this.#closed || (error as Error).name === "AbortError") return;
|
|
75
|
+
if ((error as NodeJS.ErrnoException).code === "PBOT_CONTROL_BUSY") {
|
|
76
|
+
this.#busySince ??= Date.now(); if (Date.now() - this.#busySince < 5000) return;
|
|
77
|
+
} else this.#busySince = undefined;
|
|
78
|
+
const message = String(error);
|
|
79
|
+
let recovery: "retry" | "repair" | "reload" = (error as NodeJS.ErrnoException).code === "PBOT_CONTROL_BUSY" ? "retry" : "repair";
|
|
80
|
+
try { this.teams.check(); } catch { recovery = "reload"; }
|
|
81
|
+
if (message !== this.#error || recovery !== this.#recovery) {
|
|
82
|
+
this.ctx.logger.warn("pbot 协作扫描失败:" + message);
|
|
83
|
+
this.#errorKey = "agent-error:" + randomUUID();
|
|
84
|
+
}
|
|
85
|
+
this.#error = message; this.#recovery = recovery;
|
|
86
|
+
}
|
|
87
|
+
private notice(proposed: readonly UserMessage[] = []) {
|
|
88
|
+
const known = new Set([...deliveredDshNotices(this.agent.session.snapshotEvents()),
|
|
89
|
+
...pendingDshNotices([...this.agent.inbox.nextTurn, ...this.agent.inbox.nextStep, ...proposed])]);
|
|
90
|
+
let value: { keys: string[]; content: string } | undefined;
|
|
91
|
+
if (this.#error && this.#errorKey) {
|
|
92
|
+
if (!known.has(this.#errorKey)) value = { keys: [this.#errorKey], content: "# 协作扫描故障\n\n" + this.#error +
|
|
93
|
+
"\n\n先核对控制记录和原生报告;结果未知不是重跑许可。" +
|
|
94
|
+
(this.#recovery === "reload" ? "团队服务已停用;修复文件后还需重载服务端 personal-bot 插件。" :
|
|
95
|
+
this.#recovery === "retry" ? "控制锁持续占用,后台会继续重试;不会自动删除锁或重新提交任务。" : "报告或控制记录读取失败,修复后后台会重试扫描。") +
|
|
96
|
+
(this.#pausePending ? "\n\n团队暂停尚未确认,不能据此开始成员私人任务。" : "") };
|
|
97
|
+
} else {
|
|
98
|
+
value = teamNotice(this.teams.notifications(this.owner, this.agents.list(this.owner)), known) ??
|
|
99
|
+
agentNotice(this.#completed, this.teams.links(), known);
|
|
100
|
+
}
|
|
101
|
+
return value ? createUserMessage({ content: [{ type: "text", text: value.content }],
|
|
102
|
+
source: { kind: "pbot-results", form: "notice", summary: "Personal Bot 协作更新", keys: value.keys } }) : undefined;
|
|
103
|
+
}
|
|
104
|
+
refresh() {
|
|
105
|
+
if (this.#closed || this.#scan) return this.#scan ?? Promise.resolve();
|
|
106
|
+
this.#scan = (async () => {
|
|
107
|
+
try { await this.scan(); } catch (error) { this.recordError(error); }
|
|
108
|
+
if (this.#closed || this.#paused || !this.#autoWake || this.agent.status !== "idle") return;
|
|
109
|
+
try {
|
|
110
|
+
this.check(); const note = this.notice();
|
|
111
|
+
if (note) {
|
|
112
|
+
// A failed admission cannot start an automatic retry loop. Actual receipt or new human input re-arms wakeup.
|
|
113
|
+
this.#autoWake = false; this.agent.followup(note); await flushDshAgent(this.agent);
|
|
114
|
+
}
|
|
115
|
+
} catch (error) { this.recordError(error); }
|
|
116
|
+
})().finally(() => { this.#scan = undefined; });
|
|
117
|
+
return this.#scan;
|
|
118
|
+
}
|
|
119
|
+
private async scan() {
|
|
120
|
+
this.check(); this.pauseTeams(); await this.agents.refresh(this.owner);
|
|
121
|
+
if (this.#closed) return; this.check();
|
|
122
|
+
const completed = await this.agents.completions(this.owner);
|
|
123
|
+
if (this.#closed) return; this.check(); this.#completed = completed;
|
|
124
|
+
try {
|
|
125
|
+
this.pauseTeams();
|
|
126
|
+
for (const input of this.teams.advance(this.owner, this.agents.list(this.owner), completed)) this.#dispatching.set(input.requestId, input);
|
|
127
|
+
for (const [id, input] of this.#dispatching) {
|
|
128
|
+
try { this.agents.enqueueTeam(input, this.owner); this.#dispatching.delete(id); }
|
|
129
|
+
catch (error) {
|
|
130
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
131
|
+
if (code === "PBOT_CONTROL_BUSY") throw error;
|
|
132
|
+
if (code !== "TEAM_INPUT_STALE") throw error;
|
|
133
|
+
this.teams.pauseMember(input.agentId, "安排失效;未自动重发"); this.#dispatching.delete(id);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if ((error as NodeJS.ErrnoException).code !== "PBOT_CONTROL_BUSY") { this.teams.fail(error); this.#dispatching.clear(); }
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
this.#error = undefined; this.#errorKey = undefined; this.#busySince = undefined; this.#recovery = undefined;
|
|
141
|
+
}
|
|
142
|
+
async close() {
|
|
143
|
+
this.#closed = true;
|
|
144
|
+
if (this.#timer) clearInterval(this.#timer); this.#timer = undefined;
|
|
145
|
+
for (const [signal, cancel] of this.#signals) signal.removeEventListener("abort", cancel); this.#signals.clear();
|
|
146
|
+
await this.#scan; this.#dispatching.clear();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
2
|
+
import type { Agent, InboxTarget } from "@deepseek-ai/dsh-agent";
|
|
3
|
+
import { foldConsumedWork } from "@deepseek-ai/dsh-agent";
|
|
4
|
+
import { createUserMessage, type UserMessage, type FileBlock, type ImageBlock } from "@deepseek-ai/dsh-llm";
|
|
5
|
+
import type { SessionEvent } from "@deepseek-ai/dsh-session";
|
|
6
|
+
import { AgentRun } from "../../services/agents/agent-run.js";
|
|
7
|
+
import type { TeamService } from "../../services/agents/team-service.js";
|
|
8
|
+
import { AgentFiles, launch } from "../../storage/agent-files.js";
|
|
9
|
+
import type { AgentDescriptor } from "../../model/agent.js";
|
|
10
|
+
import type { TeamLink } from "../../model/team.js";
|
|
11
|
+
import { requireThat } from "../../common/validation.js";
|
|
12
|
+
import { dshInput, dshHumanInput } from "../../integrations/dsh/input.js";
|
|
13
|
+
import { requestSource, finalDshAnswer } from "../../integrations/dsh/messages.js";
|
|
14
|
+
import { flushDshAgent } from "../../integrations/dsh/session.js";
|
|
15
|
+
import { createTeamContext, teamBrief } from "../../integrations/team-context.js";
|
|
16
|
+
|
|
17
|
+
/** One expert's native event adapter. AgentRun and TeamService own durable business state. */
|
|
18
|
+
export class DshAgentRuntime {
|
|
19
|
+
readonly execution: AgentRun;
|
|
20
|
+
readonly #lifetime = new AbortController();
|
|
21
|
+
readonly #inbox: Record<InboxTarget, UserMessage[]> = { "next-turn": [], "next-step": [] };
|
|
22
|
+
readonly #seen = new Map<string, Set<number>>();
|
|
23
|
+
#events = Promise.resolve();
|
|
24
|
+
#turnEvents: SessionEvent[] = [];
|
|
25
|
+
#answer = "";
|
|
26
|
+
#hasAnswer = false;
|
|
27
|
+
#activeTeam: TeamLink | undefined;
|
|
28
|
+
#pendingPause: string | undefined;
|
|
29
|
+
#pump: Promise<void> | undefined;
|
|
30
|
+
#timer: ReturnType<typeof setInterval> | undefined;
|
|
31
|
+
#stopping: Promise<void> | undefined;
|
|
32
|
+
#closing = false;
|
|
33
|
+
#failure: unknown;
|
|
34
|
+
#ready = false;
|
|
35
|
+
#teamTool = false;
|
|
36
|
+
constructor(readonly ctx: Context, readonly agent: Agent, readonly d: AgentDescriptor, readonly generation: string,
|
|
37
|
+
readonly files: AgentFiles, readonly teams: TeamService, readonly owner: { check(): void; release(): Promise<void> }) {
|
|
38
|
+
this.execution = new AgentRun(files, d.agentId, generation);
|
|
39
|
+
}
|
|
40
|
+
check = () => {
|
|
41
|
+
this.#lifetime.signal.throwIfAborted(); this.owner.check(); this.files.check();
|
|
42
|
+
const run = this.files.read(this.d.agentId, "launch.json", launch);
|
|
43
|
+
requireThat(run.generation === this.generation && !run.stop, "专家已停止或启动代次变化。");
|
|
44
|
+
};
|
|
45
|
+
actor = () => ({ kind: "member" as const, agentId: this.d.agentId, generation: this.generation,
|
|
46
|
+
...(this.#activeTeam && this.execution.current?.source === "parent" ? { requestId: this.#activeTeam.request.requestId } : {}) });
|
|
47
|
+
private current() { return this.files.read(this.d.agentId, "launch.json", launch).generation === this.generation; }
|
|
48
|
+
private pause(reason: string) {
|
|
49
|
+
this.#pendingPause = reason;
|
|
50
|
+
try { this.teams.pauseMember(this.d.agentId, reason); this.#pendingPause = undefined; }
|
|
51
|
+
catch (error) { if ((error as NodeJS.ErrnoException).code !== "PBOT_CONTROL_BUSY") throw error; }
|
|
52
|
+
}
|
|
53
|
+
private enqueue(work: () => void | Promise<void>) {
|
|
54
|
+
this.#events = this.#events.then(work).catch(error => this.fail(error));
|
|
55
|
+
}
|
|
56
|
+
private fail(error: unknown) {
|
|
57
|
+
if (this.#failure) return;
|
|
58
|
+
this.#failure = error; this.#lifetime.abort();
|
|
59
|
+
try {
|
|
60
|
+
if (this.current()) {
|
|
61
|
+
this.teams.files.setReady(this.d.agentId, this.generation, false);
|
|
62
|
+
this.execution.save({ state: "error", activity: "控制失败", error: String(error) });
|
|
63
|
+
this.pause("专家控制失败");
|
|
64
|
+
}
|
|
65
|
+
} catch (failure) { this.ctx.logger.error("pbot 专家状态保存失败", failure); }
|
|
66
|
+
void this.owner.release().catch(failure => this.ctx.logger.error("pbot 专家释放失败", failure));
|
|
67
|
+
}
|
|
68
|
+
initialize(teamTool: boolean) {
|
|
69
|
+
this.check(); this.#teamTool = teamTool;
|
|
70
|
+
this.teams.files.setReady(this.d.agentId, this.generation, false);
|
|
71
|
+
this.pause("专家载入:旧批次不自动恢复");
|
|
72
|
+
this.execution.recover();
|
|
73
|
+
for (const event of this.agent.session.snapshotEvents()) if (event.type === "user/message" && event.data.source.kind === "pbot-team-context") {
|
|
74
|
+
const { teamId, seqs } = event.data.source, seen = this.#seen.get(teamId) ?? new Set<number>();
|
|
75
|
+
for (const seq of seqs) seen.add(seq); this.#seen.set(teamId, seen);
|
|
76
|
+
}
|
|
77
|
+
this.ctx.on("session/event", (session, event) => {
|
|
78
|
+
if (session === this.agent.session) this.enqueue(() => this.event(event));
|
|
79
|
+
});
|
|
80
|
+
this.ctx.on("agent/status", ({ agent, status }) => {
|
|
81
|
+
if (agent !== this.agent) return;
|
|
82
|
+
this.enqueue(() => {
|
|
83
|
+
if (!this.current() || this.#closing || this.#failure) return;
|
|
84
|
+
this.execution.save({ state: status === "idle" ? "idle" : "busy", activity: status === "idle" ? "空闲" : "执行" });
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
this.ctx.on("agent/pre-step", async (_payload, next) => {
|
|
88
|
+
await this.#events; this.check();
|
|
89
|
+
if (this.#activeTeam && !this.humanPending() && this.execution.current?.source === "parent" && Date.now() >= this.#activeTeam.run.deadlineAt) {
|
|
90
|
+
this.pause("已到期限"); this.agent.cancel({ kind: "hook", reason: "团队请求已到期限" });
|
|
91
|
+
return { kind: "reject" };
|
|
92
|
+
}
|
|
93
|
+
const decision = await next();
|
|
94
|
+
this.check();
|
|
95
|
+
if (decision.kind !== "enter" || !this.#activeTeam || this.execution.current?.source !== "parent" || this.humanPending() ||
|
|
96
|
+
decision.messages.some(message => message.source.kind === "user")) return decision;
|
|
97
|
+
const link = this.teams.links().get(this.#activeTeam.request.requestId);
|
|
98
|
+
requireThat(link, "团队关联不可读取。");
|
|
99
|
+
const seen = this.#seen.get(link.team.teamId) ?? new Set<number>();
|
|
100
|
+
const context = createTeamContext(link, seen);
|
|
101
|
+
if (!context) return decision;
|
|
102
|
+
return { ...decision, messages: [...decision.messages, createUserMessage({ content: [{ type: "text", text: context.content }],
|
|
103
|
+
source: { kind: "pbot-team-context", form: "relay", teamId: link.team.teamId, seqs: context.seqs } })] };
|
|
104
|
+
});
|
|
105
|
+
this.ctx.systemPrompt.section({ name: "pbot:team", order: 600, interpolate: false,
|
|
106
|
+
text: () => this.#activeTeam && !this.humanPending() && this.execution.current?.source === "parent" ? teamBrief(this.#activeTeam) : "" });
|
|
107
|
+
this.execution.save({ state: "idle", activity: "空闲" });
|
|
108
|
+
this.ctx.effect(() => () => this.stop());
|
|
109
|
+
}
|
|
110
|
+
/** Called only after native Agent creation has resolved, never from setup. */
|
|
111
|
+
start() {
|
|
112
|
+
this.check();
|
|
113
|
+
this.#timer = setInterval(() => { void this.pump(); }, 250); this.#timer.unref();
|
|
114
|
+
void this.pump();
|
|
115
|
+
}
|
|
116
|
+
async interact(text: string, delivery: "steer" | "followUp" = "steer", files: readonly string[] = [],
|
|
117
|
+
attachments: readonly (ImageBlock | FileBlock)[] = [], caller?: AbortSignal) {
|
|
118
|
+
this.check(); requireThat((text.trim().length > 0 || files.length || attachments.length) && text.length <= 24000, "请输入消息或附加文件;消息最多 24000 字符。");
|
|
119
|
+
const signal = caller ? AbortSignal.any([caller, this.#lifetime.signal]) : this.#lifetime.signal;
|
|
120
|
+
const message = await dshHumanInput(this.ctx, this.d.cwd, text, files, attachments, signal);
|
|
121
|
+
this.check(); signal.throwIfAborted();
|
|
122
|
+
this.teams.pauseMember(this.d.agentId, "成员由人工接管");
|
|
123
|
+
if (delivery === "followUp") this.agent.followup(message); else this.agent.steer(message);
|
|
124
|
+
// Native inbox admission transfers ownership to the expert, independently of the UI request.
|
|
125
|
+
await flushDshAgent(this.agent);
|
|
126
|
+
return { status: "queued" as const, messageId: message.id };
|
|
127
|
+
}
|
|
128
|
+
private async event(event: SessionEvent) {
|
|
129
|
+
if (!this.current()) return;
|
|
130
|
+
if (event.type === "turn/start") {
|
|
131
|
+
this.#turnEvents = []; this.#answer = ""; this.#hasAnswer = false;
|
|
132
|
+
this.execution.begin();
|
|
133
|
+
}
|
|
134
|
+
if (this.execution.current) this.#turnEvents.push(event);
|
|
135
|
+
if (event.type === "agent/inbox/spliced") {
|
|
136
|
+
const change = event.data;
|
|
137
|
+
const removed = this.#inbox[change.target].splice(change.start, change.removedCount ?? 0, ...change.inserted);
|
|
138
|
+
if (change.inserted.some(message => message.source.kind === "user")) this.pause("成员由人工接管");
|
|
139
|
+
for (const message of removed) {
|
|
140
|
+
if (message.source.kind === "user" && change.outcome !== "canceled") this.execution.enterHuman();
|
|
141
|
+
const source = requestSource(message, this.generation);
|
|
142
|
+
if (!source) continue;
|
|
143
|
+
if (change.outcome === "canceled") {
|
|
144
|
+
this.execution.cancelQueued(source.requestId);
|
|
145
|
+
if (this.#activeTeam?.request.requestId === source.requestId && !this.execution.current?.requestIds.includes(source.requestId)) this.#activeTeam = undefined;
|
|
146
|
+
} else this.execution.enter(source.requestId);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (event.type === "user/message") {
|
|
150
|
+
const source = requestSource(event.data, this.generation);
|
|
151
|
+
if (source) this.execution.consume(source.requestId);
|
|
152
|
+
if (event.data.source.kind === "user") this.execution.humanInput();
|
|
153
|
+
if (event.data.source.kind === "pbot-team-context") {
|
|
154
|
+
const { teamId, seqs } = event.data.source, seen = this.#seen.get(teamId) ?? new Set<number>();
|
|
155
|
+
for (const seq of seqs) seen.add(seq); this.#seen.set(teamId, seen);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (event.type === "assistant/message") {
|
|
159
|
+
this.#answer = finalDshAnswer(event); this.#hasAnswer = true;
|
|
160
|
+
}
|
|
161
|
+
if (event.type === "turn/end") {
|
|
162
|
+
const accounted = foldConsumedWork(this.#turnEvents).end?.seq === event.seq;
|
|
163
|
+
await flushDshAgent(this.agent);
|
|
164
|
+
const reason = event.data.reason;
|
|
165
|
+
this.execution.finish({
|
|
166
|
+
state: reason.kind === "aborted" ? "cancelled" : reason.kind === "interrupted" || reason.kind === "forked" ? "interrupted"
|
|
167
|
+
: reason.kind === "completed" && accounted ? "completed" : "failed",
|
|
168
|
+
position: this.#hasAnswer ? "dsh:" + event.seq : null,
|
|
169
|
+
answer: this.#answer,
|
|
170
|
+
...(reason.kind === "error" ? { error: reason.error.message } : !["completed", "aborted"].includes(reason.kind) ? { error: reason.kind } : {}),
|
|
171
|
+
});
|
|
172
|
+
this.#activeTeam = undefined; this.#turnEvents = [];
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
private humanPending() { return [...this.agent.inbox.nextTurn, ...this.agent.inbox.nextStep].some(message => message.source.kind === "user"); }
|
|
176
|
+
private pump() {
|
|
177
|
+
if (this.#pump || this.#closing) return this.#pump ?? Promise.resolve();
|
|
178
|
+
this.#pump = this.deliver().catch(error => {
|
|
179
|
+
if ((error as NodeJS.ErrnoException).code === "PBOT_CONTROL_BUSY" || (this.#closing && this.#lifetime.signal.aborted)) return;
|
|
180
|
+
this.fail(error);
|
|
181
|
+
}).finally(() => { this.#pump = undefined; });
|
|
182
|
+
return this.#pump;
|
|
183
|
+
}
|
|
184
|
+
private async deliver() {
|
|
185
|
+
const run = this.files.read(this.d.agentId, "launch.json", launch);
|
|
186
|
+
if (run.generation !== this.generation || run.stop) { void this.owner.release().catch(error => this.fail(error)); return; }
|
|
187
|
+
await this.#events; this.check();
|
|
188
|
+
if (this.#pendingPause) { this.pause(this.#pendingPause); if (this.#pendingPause) return; }
|
|
189
|
+
const capable = this.#teamTool && !!this.ctx.tools.get("pbot_team_message", this.agent);
|
|
190
|
+
if (capable !== this.#ready) {
|
|
191
|
+
this.teams.files.setReady(this.d.agentId, this.generation, capable); this.#ready = capable;
|
|
192
|
+
if (!capable) this.pause("成员团队工具已停用");
|
|
193
|
+
}
|
|
194
|
+
if (this.#activeTeam) {
|
|
195
|
+
if (Date.now() >= this.#activeTeam.run.deadlineAt) {
|
|
196
|
+
this.pause("已到期限");
|
|
197
|
+
if (!this.humanPending() && !this.execution.interactive && this.execution.current?.source !== "mixed" && this.execution.current?.source !== "interactive") this.agent.cancel({ kind: "hook", reason: "团队请求已到期限" });
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const links = this.teams.links();
|
|
202
|
+
const idle = () => this.agent.status === "idle" && !this.agent.inbox.nextTurn.length && !this.agent.inbox.nextStep.length && !this.execution.current && !this.execution.pending && !this.execution.interactive;
|
|
203
|
+
for (const request of this.files.messages(this.d.agentId)) {
|
|
204
|
+
if (request.generation !== this.generation || this.execution.accepted(request.requestId)) continue;
|
|
205
|
+
const link = links.get(request.requestId);
|
|
206
|
+
if (link && (link.team.closed || link.run.state !== "running" || link.run.deadlineAt <= Date.now())) continue;
|
|
207
|
+
if (link && !idle()) break;
|
|
208
|
+
const message = await dshInput(this.ctx, this.d, request, this.#lifetime.signal);
|
|
209
|
+
this.check();
|
|
210
|
+
const submit = async () => {
|
|
211
|
+
this.check();
|
|
212
|
+
const accept = () => this.execution.claim(request.requestId);
|
|
213
|
+
if (link) {
|
|
214
|
+
if (!idle()) return;
|
|
215
|
+
this.#activeTeam = this.teams.claim(this.d.agentId, this.generation, request.requestId, accept);
|
|
216
|
+
if (!this.#activeTeam) return;
|
|
217
|
+
} else accept();
|
|
218
|
+
if (request.delivery === "followUp") this.agent.followup(message); else this.agent.steer(message);
|
|
219
|
+
await flushDshAgent(this.agent);
|
|
220
|
+
};
|
|
221
|
+
if (link) {
|
|
222
|
+
// Public status stays idle during native maintenance. Its idle admission is authoritative.
|
|
223
|
+
let entered = false;
|
|
224
|
+
try { await this.agent.runMaintenance(async signal => { entered = true; signal.throwIfAborted(); await submit(); }); }
|
|
225
|
+
catch (error) { if (entered) throw error; this.check(); }
|
|
226
|
+
} else await submit();
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
stop() {
|
|
231
|
+
if (this.#stopping) return this.#stopping;
|
|
232
|
+
this.#closing = true; this.#lifetime.abort();
|
|
233
|
+
if (this.#timer) clearInterval(this.#timer); this.#timer = undefined;
|
|
234
|
+
this.#stopping = (async () => {
|
|
235
|
+
try {
|
|
236
|
+
if (this.current()) {
|
|
237
|
+
this.files.control(() => {
|
|
238
|
+
const run = this.files.read(this.d.agentId, "launch.json", launch);
|
|
239
|
+
if (run.generation === this.generation && !run.stop) { run.stop = true; this.files.write(this.d.agentId, "launch.json", run, launch); }
|
|
240
|
+
});
|
|
241
|
+
this.teams.files.setReady(this.d.agentId, this.generation, false); this.#ready = false;
|
|
242
|
+
this.pause("专家停止");
|
|
243
|
+
if (!this.#failure) this.execution.save({ state: "stopping", activity: "停止" });
|
|
244
|
+
}
|
|
245
|
+
} finally { this.agent.cancel({ kind: "disposed" }); }
|
|
246
|
+
await this.agent.whenIdle(); await this.#pump; await this.#events;
|
|
247
|
+
if (this.current() && !this.#failure) this.execution.save({ state: "stopped", activity: "已停止" });
|
|
248
|
+
})();
|
|
249
|
+
return this.#stopping;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -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
|
+
}
|