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,11 @@
|
|
|
1
|
+
import { lastAssistantStreamChunk } from "@deepseek-ai/dsh-llm";
|
|
2
|
+
export function finalDshAnswer(event) {
|
|
3
|
+
const { message, interrupted, stream } = event.data;
|
|
4
|
+
if (interrupted || message.content.some(block => block.type === "tool-call") || lastAssistantStreamChunk(stream, "finish")?.reason.kind !== "stop")
|
|
5
|
+
return "";
|
|
6
|
+
return message.content.filter(block => block.type === "text").map(block => block.text).join("\n");
|
|
7
|
+
}
|
|
8
|
+
export function requestSource(message, generation) {
|
|
9
|
+
const source = message.source;
|
|
10
|
+
return source.kind === "pbot-input" && source.generation === generation ? source : undefined;
|
|
11
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SessionEvent } from "@deepseek-ai/dsh-session";
|
|
2
|
+
import type { UserMessage } from "@deepseek-ai/dsh-llm";
|
|
3
|
+
import "./messages.js";
|
|
4
|
+
/** Only committed native context or successful model tool results claim notices.
|
|
5
|
+
* PTC inner reads require their enclosing transport to have committed success. */
|
|
6
|
+
export declare function deliveredDshNotices(events: readonly SessionEvent[]): Set<string>;
|
|
7
|
+
/** Pending inbox entries prevent duplicate submission, but are not delivery receipts. */
|
|
8
|
+
export declare function pendingDshNotices(messages: readonly UserMessage[]): string[];
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { object } from "../../common/json.js";
|
|
2
|
+
import "./messages.js";
|
|
3
|
+
function keys(value, field) {
|
|
4
|
+
if (!object(value))
|
|
5
|
+
return [];
|
|
6
|
+
const items = value[field];
|
|
7
|
+
return Array.isArray(items) && items.every(item => typeof item === "string") ? items : [];
|
|
8
|
+
}
|
|
9
|
+
/** Only committed native context or successful model tool results claim notices.
|
|
10
|
+
* PTC inner reads require their enclosing transport to have committed success. */
|
|
11
|
+
export function deliveredDshNotices(events) {
|
|
12
|
+
const delivered = new Set();
|
|
13
|
+
const reads = new Set(), nested = new Map();
|
|
14
|
+
for (const event of events) {
|
|
15
|
+
if (event.type === "turn/start" || event.type === "step/start" || event.type === "turn/end") {
|
|
16
|
+
reads.clear();
|
|
17
|
+
nested.clear();
|
|
18
|
+
}
|
|
19
|
+
if (event.type === "user/message" && event.data.source.kind === "pbot-results") {
|
|
20
|
+
for (const key of keys(event.data.source, "keys"))
|
|
21
|
+
delivered.add(key);
|
|
22
|
+
}
|
|
23
|
+
if (event.type === "tool/call" && event.data.name === "pbot_agent") {
|
|
24
|
+
try {
|
|
25
|
+
const args = JSON.parse(event.data.arguments);
|
|
26
|
+
if (object(args) && args.action === "read")
|
|
27
|
+
reads.add(event.data.callId);
|
|
28
|
+
}
|
|
29
|
+
catch { /* An invalid call cannot claim a report. */ }
|
|
30
|
+
}
|
|
31
|
+
if (event.type === "tool/ptc-dispatch" && event.data.name === "pbot_agent" && !event.data.isError &&
|
|
32
|
+
object(event.data.arguments) && event.data.arguments.action === "read") {
|
|
33
|
+
const values = nested.get(event.data.rootCallId) ?? [];
|
|
34
|
+
values.push(...keys(event.data.meta, "pbotAgentReadKeys"));
|
|
35
|
+
nested.set(event.data.rootCallId, values);
|
|
36
|
+
}
|
|
37
|
+
if (event.type === "tool/result") {
|
|
38
|
+
const message = event.data.message;
|
|
39
|
+
if (!message.isError) {
|
|
40
|
+
if (reads.has(message.toolCallId))
|
|
41
|
+
for (const key of keys(event.data.meta, "pbotAgentReadKeys"))
|
|
42
|
+
delivered.add(key);
|
|
43
|
+
for (const key of nested.get(message.toolCallId) ?? [])
|
|
44
|
+
delivered.add(key);
|
|
45
|
+
}
|
|
46
|
+
reads.delete(message.toolCallId);
|
|
47
|
+
nested.delete(message.toolCallId);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return delivered;
|
|
51
|
+
}
|
|
52
|
+
/** Pending inbox entries prevent duplicate submission, but are not delivery receipts. */
|
|
53
|
+
export function pendingDshNotices(messages) {
|
|
54
|
+
return messages.flatMap(message => message.source.kind === "pbot-results" ? keys(message.source, "keys") : []);
|
|
55
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
2
|
+
import type { Agent } from "@deepseek-ai/dsh-agent";
|
|
3
|
+
import type { AgentOptions } from "../../model/agent.js";
|
|
4
|
+
export declare function dshResources(ctx: Context, agent: Agent, root: string, role: "coordinator" | "general-purpose", python: () => {
|
|
5
|
+
state: string;
|
|
6
|
+
directory?: string;
|
|
7
|
+
error?: string;
|
|
8
|
+
}, check: () => void): Promise<void>;
|
|
9
|
+
/** Map the expert's native loadout; this is not isolation from Bash, CLI, or MCP. */
|
|
10
|
+
export declare function dshExpertTools(ctx: Context, agent: Agent, options: AgentOptions): boolean;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { FileSystemSkillProvider } from "@deepseek-ai/dsh-skill-filesystem";
|
|
4
|
+
import { agentText } from "../agent-resources.js";
|
|
5
|
+
import { resourceRoot } from "../../common/paths.js";
|
|
6
|
+
import { requireThat } from "../../common/validation.js";
|
|
7
|
+
export async function dshResources(ctx, agent, root, role, python, check) {
|
|
8
|
+
let text = await agentText(role, root);
|
|
9
|
+
const workspace = await readFile(join(resourceRoot, "prompts/workspace.md"), "utf8");
|
|
10
|
+
check();
|
|
11
|
+
ctx.systemPrompt.section({ name: "pbot:role", order: 1, text: () => text, interpolate: false });
|
|
12
|
+
ctx.systemPrompt.section({ name: "pbot:workspace", order: 2, text: workspace, interpolate: false });
|
|
13
|
+
ctx.systemPrompt.context({ name: "pbot:environment", order: 300, required: true, interpolate: false,
|
|
14
|
+
text: () => JSON.stringify({ host: "dsh", workspaceRoot: root, cwd: ctx.workingDirectory.get(agent.session), resourceRoot, python: python() }) });
|
|
15
|
+
ctx.skills.registerProvider(control => new FileSystemSkillProvider(ctx, control, {
|
|
16
|
+
providerName: "pbot", includeDefaultRoots: false, customSkillDirs: [join(root, ".agents/skills")],
|
|
17
|
+
bundledSkillDir: join(resourceRoot, "skills"), watch: false,
|
|
18
|
+
}));
|
|
19
|
+
ctx.on("agent/pre-step", async (_payload, next) => {
|
|
20
|
+
check();
|
|
21
|
+
text = await agentText(role, root);
|
|
22
|
+
check();
|
|
23
|
+
return next();
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/** Map the expert's native loadout; this is not isolation from Bash, CLI, or MCP. */
|
|
27
|
+
export function dshExpertTools(ctx, agent, options) {
|
|
28
|
+
const names = new Set(ctx.tools.definitions(agent).map(tool => tool.name));
|
|
29
|
+
const forbidden = new Set(["pbot_agent", "pbot_team", "pbot_history", "subagent", "list_subagent_models", "list_agents", "send_message", "interrupt_agent"]);
|
|
30
|
+
if (options.noSkills)
|
|
31
|
+
forbidden.add("skill");
|
|
32
|
+
if (options.tools) {
|
|
33
|
+
for (const name of options.tools)
|
|
34
|
+
requireThat((names.has(name) || name === "pbot_team_message") && !forbidden.has(name), "专家工具不可用:" + name);
|
|
35
|
+
ctx.tools.restrict({ allow: options.tools.filter(name => name !== "pbot_team_message") });
|
|
36
|
+
}
|
|
37
|
+
const deny = [...forbidden].filter(name => names.has(name));
|
|
38
|
+
if (deny.length)
|
|
39
|
+
ctx.tools.restrict({ deny });
|
|
40
|
+
return !options.tools || options.tools.includes("pbot_team_message");
|
|
41
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { TSchema } from "typebox";
|
|
2
|
+
import type { JsonSchemaNode } from "@deepseek-ai/dsh-tools";
|
|
3
|
+
/** Remove TypeBox's non-JSON metadata before passing a schema to the native host. */
|
|
4
|
+
export declare function dshParameters(value: TSchema): Record<string, unknown>;
|
|
5
|
+
/** Collaboration result unions are disjoint. rc.1 enforces structural oneOf schemas;
|
|
6
|
+
* the producing tool also validates the complete contract, including these bounds. */
|
|
7
|
+
export declare function dshOutputSchema(value: TSchema): JsonSchemaNode;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { requireThat } from "../../common/validation.js";
|
|
2
|
+
/** Remove TypeBox's non-JSON metadata before passing a schema to the native host. */
|
|
3
|
+
export function dshParameters(value) {
|
|
4
|
+
return JSON.parse(JSON.stringify(value));
|
|
5
|
+
}
|
|
6
|
+
/** Collaboration result unions are disjoint. rc.1 enforces structural oneOf schemas;
|
|
7
|
+
* the producing tool also validates the complete contract, including these bounds. */
|
|
8
|
+
export function dshOutputSchema(value) {
|
|
9
|
+
const bounds = new Set(["minLength", "maxLength", "pattern", "minimum", "maximum", "minItems", "maxItems"]);
|
|
10
|
+
const supported = new Set(["type", "required", "additionalProperties", "enum", "const", "description", "title", "default", "examples"]);
|
|
11
|
+
function convert(node) {
|
|
12
|
+
const result = {};
|
|
13
|
+
for (const [key, item] of Object.entries(node)) {
|
|
14
|
+
if (key === "anyOf")
|
|
15
|
+
result.oneOf = item.map(convert);
|
|
16
|
+
else if (key === "properties")
|
|
17
|
+
result.properties = Object.fromEntries(Object.entries(item).map(([name, child]) => [name, convert(child)]));
|
|
18
|
+
else if (key === "items")
|
|
19
|
+
result.items = convert(item);
|
|
20
|
+
else if (!bounds.has(key)) {
|
|
21
|
+
requireThat(supported.has(key), "dsh 输出 schema 不支持字段:" + key);
|
|
22
|
+
result[key] = item;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
return convert(dshParameters(value));
|
|
28
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
2
|
+
import type { Agent, AgentHandle, AgentSetup } from "@deepseek-ai/dsh-agent";
|
|
3
|
+
import type { SessionHeader } from "@deepseek-ai/dsh-session";
|
|
4
|
+
import type { AgentDescriptor } from "../../model/agent.js";
|
|
5
|
+
/** Read access must neither claim a writer nor activate an Agent. */
|
|
6
|
+
export declare function readDshAgentSession(ctx: Context, d: AgentDescriptor): Promise<{
|
|
7
|
+
header: SessionHeader;
|
|
8
|
+
events: readonly import("@deepseek-ai/dsh-session").SessionEvent[];
|
|
9
|
+
}>;
|
|
10
|
+
/** An empty Agent is not recoverable until this native barrier has materialized it. */
|
|
11
|
+
export declare function flushDshAgent(agent: Agent): Promise<void>;
|
|
12
|
+
export declare function openDshAgent(ctx: Context, d: AgentDescriptor, parent: Agent, resume: boolean, setup: AgentSetup, signal: AbortSignal): Promise<AgentHandle>;
|
|
13
|
+
/** A report refers to one durable turn boundary, never the latest answer. */
|
|
14
|
+
export declare function readDshReport(ctx: Context, d: AgentDescriptor, reference: string): Promise<string>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { SessionId } from "@deepseek-ai/dsh-session";
|
|
2
|
+
import { ReasoningEffortId } from "@deepseek-ai/dsh-llm";
|
|
3
|
+
import { requireThat } from "../../common/validation.js";
|
|
4
|
+
function validateHeader(d, header) {
|
|
5
|
+
requireThat(header.id === d.agentId && header.cwd === d.cwd && header.parentSession === d.parentSessionId &&
|
|
6
|
+
header.origin === "subagent" && !header.isSeeded, "dsh 专家会话身份、父会话或工作目录不匹配。");
|
|
7
|
+
}
|
|
8
|
+
/** Read access must neither claim a writer nor activate an Agent. */
|
|
9
|
+
export async function readDshAgentSession(ctx, d) {
|
|
10
|
+
const reader = await ctx.sessionPersistence.open(SessionId(d.agentId), "read");
|
|
11
|
+
try {
|
|
12
|
+
validateHeader(d, reader.header);
|
|
13
|
+
return { header: reader.header, events: (await reader.read()).events };
|
|
14
|
+
}
|
|
15
|
+
finally {
|
|
16
|
+
await reader.close();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** An empty Agent is not recoverable until this native barrier has materialized it. */
|
|
20
|
+
export async function flushDshAgent(agent) {
|
|
21
|
+
requireThat(await agent.ctx.sessions.flush(agent.session), "dsh 会话没有持久化屏障。");
|
|
22
|
+
}
|
|
23
|
+
export async function openDshAgent(ctx, d, parent, resume, setup, signal) {
|
|
24
|
+
signal.throwIfAborted();
|
|
25
|
+
requireThat(parent.id === d.parentSessionId && ctx.agents.get(parent.id) === parent, "dsh 父会话已失效。");
|
|
26
|
+
if (resume)
|
|
27
|
+
await readDshAgentSession(ctx, d);
|
|
28
|
+
await flushDshAgent(parent);
|
|
29
|
+
signal.throwIfAborted();
|
|
30
|
+
const compose = async (agentCtx, agent) => {
|
|
31
|
+
validateHeader(d, agent.session.header);
|
|
32
|
+
if (resume)
|
|
33
|
+
agent.inbox.clear();
|
|
34
|
+
const commit = await setup(agentCtx, agent);
|
|
35
|
+
await flushDshAgent(agent);
|
|
36
|
+
return commit;
|
|
37
|
+
};
|
|
38
|
+
const agentOptions = { ...(d.options.provider ? { provider: d.options.provider } : {}),
|
|
39
|
+
...(d.options.model ? { model: d.options.model } : {}),
|
|
40
|
+
...(d.options.thinking ? { reasoningEffort: ReasoningEffortId(d.options.thinking) } : {}) };
|
|
41
|
+
return resume
|
|
42
|
+
? ctx.agents.resume({ resumeSessionId: SessionId(d.agentId), parentAgent: parent, agentOptions, setup: compose, signal })
|
|
43
|
+
: ctx.agents.create({ sessionId: SessionId(d.agentId), parentAgent: parent,
|
|
44
|
+
meta: { cwd: d.cwd, parentSession: parent.id, origin: "subagent", delegationDepth: 1 },
|
|
45
|
+
agentOptions, setup: compose, signal });
|
|
46
|
+
}
|
|
47
|
+
/** A report refers to one durable turn boundary, never the latest answer. */
|
|
48
|
+
export async function readDshReport(ctx, d, reference) {
|
|
49
|
+
const match = /^dsh:(0|[1-9]\d*)$/.exec(reference);
|
|
50
|
+
requireThat(match, "dsh 报告位置无效。");
|
|
51
|
+
const end = Number(match[1]);
|
|
52
|
+
requireThat(Number.isSafeInteger(end), "dsh 报告位置无效。");
|
|
53
|
+
const { events } = await readDshAgentSession(ctx, d);
|
|
54
|
+
const boundary = events[end];
|
|
55
|
+
requireThat(boundary?.type === "turn/end", "dsh 报告结束位置不存在。");
|
|
56
|
+
const last = events.slice(0, end).findLast(event => event.type === "assistant/message" && event.data.turn === boundary.data.turn);
|
|
57
|
+
return last?.type === "assistant/message"
|
|
58
|
+
? last.data.message.content.filter(block => block.type === "text").map(block => block.text).join("\n") : "";
|
|
59
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { requireThat } from "../common/validation.js";
|
|
2
|
+
export function reduceHistorySummary(budget, ask) {
|
|
3
|
+
return async (text, signal) => {
|
|
4
|
+
while (text.length > budget) {
|
|
5
|
+
const parts = [];
|
|
6
|
+
for (let offset = 0; offset < text.length;) {
|
|
7
|
+
signal.throwIfAborted();
|
|
8
|
+
let end = Math.min(text.length, offset + budget);
|
|
9
|
+
if (end < text.length && /[\uD800-\uDBFF]/.test(text[end - 1]))
|
|
10
|
+
end--;
|
|
11
|
+
parts.push(await ask(text.slice(offset, end), signal));
|
|
12
|
+
offset = end;
|
|
13
|
+
}
|
|
14
|
+
const smaller = parts.join("\n\n");
|
|
15
|
+
requireThat(smaller.length < text.length, "历史摘要未缩短,稍后重试。");
|
|
16
|
+
text = smaller;
|
|
17
|
+
}
|
|
18
|
+
signal.throwIfAborted();
|
|
19
|
+
return ask(text, signal);
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
+
import { npmName } from "../../common/package-source.js";
|
|
6
|
+
async function sdkAt(root) {
|
|
7
|
+
const manifest = JSON.parse(await readFile(join(root, "package.json"), "utf8"));
|
|
8
|
+
if (manifest.name !== "@earendil-works/pi-coding-agent")
|
|
9
|
+
throw new Error("不是支持的 Pi 安装。");
|
|
10
|
+
return import(pathToFileURL(join(root, "dist/index.js")).href);
|
|
11
|
+
}
|
|
12
|
+
const sourceOf = (entry) => {
|
|
13
|
+
if (typeof entry === "string")
|
|
14
|
+
return entry;
|
|
15
|
+
if (entry && typeof entry === "object" && "source" in entry && typeof entry.source === "string")
|
|
16
|
+
return entry.source;
|
|
17
|
+
throw new Error("无效的 Pi packages 声明。");
|
|
18
|
+
};
|
|
19
|
+
export async function inspectPackages(root, workspace) {
|
|
20
|
+
const sdk = await sdkAt(root), agentDir = sdk.getAgentDir();
|
|
21
|
+
// Inspect project declarations as data only. This does not load extensions, execute
|
|
22
|
+
// project npmCommand, grant project trust, or persist migrated settings.
|
|
23
|
+
const settings = sdk.SettingsManager.fromStorage({ withLock(scope, read) {
|
|
24
|
+
const file = scope === "global" ? join(agentDir, "settings.json") : join(workspace, sdk.CONFIG_DIR_NAME, "settings.json");
|
|
25
|
+
let content;
|
|
26
|
+
try {
|
|
27
|
+
content = readFileSync(file, "utf8");
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (error.code !== "ENOENT")
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
if (content !== undefined) {
|
|
34
|
+
const value = JSON.parse(content);
|
|
35
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
36
|
+
throw new Error(`无效的 Pi 配置:${file}`);
|
|
37
|
+
const packages = value.packages;
|
|
38
|
+
if (packages !== undefined) {
|
|
39
|
+
if (!Array.isArray(packages))
|
|
40
|
+
throw new Error(`packages 必须是数组:${file}`);
|
|
41
|
+
packages.forEach(sourceOf);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (read(content) !== undefined)
|
|
45
|
+
throw new Error("安装检查不得修改 Pi 配置。");
|
|
46
|
+
} }, { projectTrusted: true });
|
|
47
|
+
const errors = settings.drainErrors();
|
|
48
|
+
if (errors.length)
|
|
49
|
+
throw new Error(errors.map(item => `${item.scope}: ${item.error.message}`).join("\n"));
|
|
50
|
+
const manager = new sdk.DefaultPackageManager({ cwd: workspace, agentDir, settingsManager: settings });
|
|
51
|
+
const result = [];
|
|
52
|
+
for (const pkg of manager.listConfiguredPackages()) {
|
|
53
|
+
const entries = (pkg.scope === "project" ? settings.getProjectSettings() : settings.getGlobalSettings()).packages ?? [];
|
|
54
|
+
const entry = entries.find(item => sourceOf(item) === pkg.source);
|
|
55
|
+
let name = npmName(pkg.source), version;
|
|
56
|
+
if (pkg.installedPath) {
|
|
57
|
+
let file = pkg.installedPath;
|
|
58
|
+
if ((await stat(file)).isDirectory())
|
|
59
|
+
file = join(file, "package.json");
|
|
60
|
+
else
|
|
61
|
+
file = ""; // Single-file extensions have no npm identity.
|
|
62
|
+
if (file) {
|
|
63
|
+
try {
|
|
64
|
+
const manifest = JSON.parse(await readFile(file, "utf8"));
|
|
65
|
+
if (name && manifest.name !== name)
|
|
66
|
+
throw new Error(`安装包身份不匹配:${pkg.source}`);
|
|
67
|
+
name = typeof manifest.name === "string" ? manifest.name : name;
|
|
68
|
+
version = typeof manifest.version === "string" ? manifest.version : undefined;
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (error.code !== "ENOENT")
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
result.push({ source: pkg.source, scope: pkg.scope, filtered: pkg.filtered, entry,
|
|
77
|
+
...(pkg.installedPath ? { installedPath: pkg.installedPath } : {}), ...(name ? { name } : {}), ...(version ? { version } : {}) });
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
export async function replaceProjectSource(root, workspace, old, target) {
|
|
82
|
+
const fromProject = old.scope === "project" && !npmName(old.source);
|
|
83
|
+
const fromUser = old.scope === "user" && !!npmName(old.source);
|
|
84
|
+
if (!fromProject && !fromUser)
|
|
85
|
+
throw new Error("此操作只替换工作区自定义来源,或继承全局 npm 资源过滤。");
|
|
86
|
+
const sdk = await sdkAt(root);
|
|
87
|
+
const settings = sdk.SettingsManager.create(workspace, sdk.getAgentDir(), { projectTrusted: true });
|
|
88
|
+
const errors = settings.drainErrors();
|
|
89
|
+
if (errors.length)
|
|
90
|
+
throw new Error(errors.map(item => item.error.message).join("\n"));
|
|
91
|
+
const packages = settings.getProjectSettings().packages ?? [];
|
|
92
|
+
const originals = fromProject ? packages : settings.getGlobalSettings().packages ?? [];
|
|
93
|
+
const previous = originals.find(entry => sourceOf(entry) === old.source);
|
|
94
|
+
if (previous === undefined || JSON.stringify(previous) !== JSON.stringify(old.entry))
|
|
95
|
+
throw new Error("插件配置已发生变化,请重新运行 setup。");
|
|
96
|
+
if (!packages.some(entry => sourceOf(entry) === target))
|
|
97
|
+
throw new Error("目标包尚未安装,保留原声明。");
|
|
98
|
+
const manager = new sdk.DefaultPackageManager({ cwd: workspace, agentDir: sdk.getAgentDir(), settingsManager: settings });
|
|
99
|
+
const installed = manager.getInstalledPath(target, "project");
|
|
100
|
+
if (!installed || !npmName(target))
|
|
101
|
+
throw new Error("目标 npm 包尚未安装,保留原声明。");
|
|
102
|
+
const manifest = JSON.parse(await readFile(join(installed, "package.json"), "utf8"));
|
|
103
|
+
if (manifest.name !== npmName(target))
|
|
104
|
+
throw new Error("目标安装包身份不符,保留原声明。");
|
|
105
|
+
// Change only declarations; do not call remove(), which deletes managed git caches.
|
|
106
|
+
const replacement = typeof previous === "string" ? target : { ...previous, source: target };
|
|
107
|
+
const next = fromProject
|
|
108
|
+
? packages.filter(entry => sourceOf(entry) !== target).map(entry => sourceOf(entry) === old.source ? replacement : entry)
|
|
109
|
+
: packages.map(entry => sourceOf(entry) === target ? replacement : entry);
|
|
110
|
+
settings.setProjectPackages(next);
|
|
111
|
+
await settings.flush();
|
|
112
|
+
const failures = settings.drainErrors();
|
|
113
|
+
if (failures.length)
|
|
114
|
+
throw new Error(failures.map(item => item.error.message).join("\n"));
|
|
115
|
+
}
|
|
116
|
+
async function main() {
|
|
117
|
+
const [action, root, workspace, input] = process.argv.slice(2);
|
|
118
|
+
if (!root || !workspace)
|
|
119
|
+
throw new Error("缺少 Pi 路径或工作区。");
|
|
120
|
+
if (action === "inspect")
|
|
121
|
+
console.log(JSON.stringify(await inspectPackages(root, workspace)));
|
|
122
|
+
else if (action === "replace" && input) {
|
|
123
|
+
const { old, target } = JSON.parse(input);
|
|
124
|
+
await replaceProjectSource(root, workspace, old, target);
|
|
125
|
+
}
|
|
126
|
+
else
|
|
127
|
+
throw new Error("无效的安装检查操作。");
|
|
128
|
+
}
|
|
129
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
130
|
+
await main().catch(error => { console.error(error instanceof Error ? error.message : String(error)); process.exitCode = 1; });
|
|
131
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { access, readFile, realpath } from "node:fs/promises";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { checkStableVersion, npmName } from "../../common/package-source.js";
|
|
7
|
+
import { findExecutable, npmGlobalBinary, SetupProcesses } from "./processes.js";
|
|
8
|
+
export const piPackageName = "@earendil-works/pi-coding-agent";
|
|
9
|
+
export class PiBootstrapRequired extends Error {
|
|
10
|
+
}
|
|
11
|
+
const bridge = fileURLToPath(new URL("./pi-bridge.js", import.meta.url));
|
|
12
|
+
const guard = fileURLToPath(new URL("../../extension/setup-guard.js", import.meta.url));
|
|
13
|
+
async function packageRoot(command, version, env) {
|
|
14
|
+
const resolved = await realpath(command);
|
|
15
|
+
let directory = dirname(resolved);
|
|
16
|
+
while (true) {
|
|
17
|
+
try {
|
|
18
|
+
const pkg = JSON.parse(await readFile(join(directory, "package.json"), "utf8"));
|
|
19
|
+
if (pkg.name === piPackageName && pkg.version === version)
|
|
20
|
+
return directory;
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error.code !== "ENOENT")
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
const parent = dirname(directory);
|
|
27
|
+
if (parent === directory)
|
|
28
|
+
break;
|
|
29
|
+
directory = parent;
|
|
30
|
+
}
|
|
31
|
+
const roots = [join(dirname(dirname(resolved)), "install"), env.PI_MANAGED_INSTALL_ROOT].filter((root) => !!root);
|
|
32
|
+
for (const root of roots) {
|
|
33
|
+
try {
|
|
34
|
+
// The native managed launcher resides beside install/. Only accept a matching
|
|
35
|
+
// launcher, not an unrelated PI_MANAGED_INSTALL_ROOT inherited from another Pi.
|
|
36
|
+
const launcher = await realpath(join(dirname(root), "bin/pi"));
|
|
37
|
+
if (launcher !== resolved)
|
|
38
|
+
continue;
|
|
39
|
+
const current = (await readFile(join(root, "current-version"), "utf8")).trim();
|
|
40
|
+
if (current !== version || !/^\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(current))
|
|
41
|
+
continue;
|
|
42
|
+
const candidate = join(root, "releases", current, "node_modules", piPackageName);
|
|
43
|
+
const pkg = JSON.parse(await readFile(join(candidate, "package.json"), "utf8"));
|
|
44
|
+
if (pkg.name === piPackageName && pkg.version === version)
|
|
45
|
+
return candidate;
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (error.code !== "ENOENT")
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
throw new Error(`无法定位 ${command} 对应的 Pi 包。请先按此 Pi 的原生安装方式修复,不会覆盖该命令。`);
|
|
53
|
+
}
|
|
54
|
+
export class PiSetup {
|
|
55
|
+
processes;
|
|
56
|
+
npm;
|
|
57
|
+
cwd;
|
|
58
|
+
constructor(processes, npm, cwd) {
|
|
59
|
+
this.processes = processes;
|
|
60
|
+
this.npm = npm;
|
|
61
|
+
this.cwd = cwd;
|
|
62
|
+
}
|
|
63
|
+
async inspectPi(command) {
|
|
64
|
+
const executable = command ?? await findExecutable("pi", this.processes.env);
|
|
65
|
+
if (!executable)
|
|
66
|
+
return undefined;
|
|
67
|
+
const version = await this.processes.run(executable, ["--version"], this.cwd);
|
|
68
|
+
if (!/^\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(version))
|
|
69
|
+
throw new Error(`PATH 中的 ${executable} 不是可识别的 Pi。`);
|
|
70
|
+
return { command: executable, version, root: await packageRoot(executable, version, this.processes.env) };
|
|
71
|
+
}
|
|
72
|
+
async latest(name) {
|
|
73
|
+
const text = await this.processes.run(this.npm, ["view", `${name}@latest`, "version", "engines", "--json",
|
|
74
|
+
"--fetch-retries=0", "--fetch-timeout=15000"], this.cwd);
|
|
75
|
+
const value = JSON.parse(text);
|
|
76
|
+
if (typeof value.version !== "string")
|
|
77
|
+
throw new Error(`无法取得 ${name} 的最新版本。`);
|
|
78
|
+
return checkStableVersion(value.version, value.engines?.node);
|
|
79
|
+
}
|
|
80
|
+
async inspectPackages(pi, workspace) {
|
|
81
|
+
if (!pi) {
|
|
82
|
+
// A fresh machine can be planned without loading a Pi SDK. Existing declarations
|
|
83
|
+
// require Pi's own resolver; don't guess git cache paths or package identities.
|
|
84
|
+
const agentDir = this.processes.env.PI_CODING_AGENT_DIR ?? join(this.processes.env.HOME ?? homedir(), ".pi/agent");
|
|
85
|
+
for (const file of [join(agentDir, "settings.json"), join(workspace, ".pi/settings.json")]) {
|
|
86
|
+
try {
|
|
87
|
+
const settings = JSON.parse(await readFile(file, "utf8"));
|
|
88
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings))
|
|
89
|
+
throw new Error(`无效的 Pi 配置:${file}`);
|
|
90
|
+
const packages = settings.packages;
|
|
91
|
+
if (packages !== undefined && !Array.isArray(packages))
|
|
92
|
+
throw new Error(`packages 必须是数组:${file}`);
|
|
93
|
+
if (Array.isArray(packages) && packages.length > 0) {
|
|
94
|
+
throw new PiBootstrapRequired(`已有插件配置但未找到 Pi:${file}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (error.code !== "ENOENT")
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
const data = await this.processes.run(process.execPath, [bridge, "inspect", pi.root, workspace], this.cwd);
|
|
105
|
+
return JSON.parse(data);
|
|
106
|
+
}
|
|
107
|
+
async installTmux(command) {
|
|
108
|
+
await this.processes.run(command.command, command.args, this.cwd, { interactive: true });
|
|
109
|
+
const tmux = await findExecutable("tmux", this.processes.env);
|
|
110
|
+
if (!tmux)
|
|
111
|
+
throw new Error("tmux 安装后仍不在 PATH 中,请修复 PATH 后重试。");
|
|
112
|
+
await this.processes.run(tmux, ["-V"], this.cwd);
|
|
113
|
+
}
|
|
114
|
+
async ensurePi(existing, version) {
|
|
115
|
+
if (existing) {
|
|
116
|
+
if (existing.version !== version)
|
|
117
|
+
await this.processes.run(existing.command, ["update"], this.cwd, { interactive: true });
|
|
118
|
+
const installed = await this.inspectPi(existing.command);
|
|
119
|
+
if (!installed || installed.version !== version)
|
|
120
|
+
throw new Error(`Pi 实际版本 ${installed?.version ?? "未知"} 与目标 ${version} 不同。请用原安装方式更新后重试。`);
|
|
121
|
+
return installed;
|
|
122
|
+
}
|
|
123
|
+
const prefix = await this.processes.run(this.npm, ["prefix", "--global"], this.cwd);
|
|
124
|
+
if (!prefix || !prefix.startsWith("/"))
|
|
125
|
+
throw new Error("无法确定 npm 全局安装目录。");
|
|
126
|
+
try {
|
|
127
|
+
await access(prefix, constants.W_OK);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
throw new Error(`npm 全局目录不可写:${prefix}。请先配置当前用户的 npm prefix 或 Node.js 版本管理器。`);
|
|
131
|
+
}
|
|
132
|
+
await this.processes.run(this.npm, ["install", "--global", "--ignore-scripts", "--engine-strict",
|
|
133
|
+
`${piPackageName}@${version}`], this.cwd, { interactive: true });
|
|
134
|
+
const installed = await this.inspectPi(npmGlobalBinary(prefix));
|
|
135
|
+
if (!installed || installed.version !== version)
|
|
136
|
+
throw new Error("Pi 安装版本验证失败。");
|
|
137
|
+
const onPath = await findExecutable("pi", this.processes.env);
|
|
138
|
+
if (!onPath || await realpath(onPath) !== await realpath(installed.command)) {
|
|
139
|
+
throw new Error(`Pi 已安装到 ${installed.command},请把 ${join(prefix, "bin")} 加入 PATH 后重新运行 setup。`);
|
|
140
|
+
}
|
|
141
|
+
return installed;
|
|
142
|
+
}
|
|
143
|
+
async verifyKeptPackage(pi, workspace, choice) {
|
|
144
|
+
const old = choice.existing[0];
|
|
145
|
+
if (choice.existing.length !== 1 || !old)
|
|
146
|
+
throw new Error("保留来源不可用或存在多份来源,请先通过 pi config 处理。");
|
|
147
|
+
const current = (await this.inspectPackages(pi, workspace)).find(pkg => pkg.source === old.source && pkg.scope === old.scope);
|
|
148
|
+
if (!current?.installedPath || current.name !== npmName(choice.plugin.source) || JSON.stringify(current.entry) !== JSON.stringify(old.entry)) {
|
|
149
|
+
throw new Error("保留来源已变化或不可读取,请重新运行 setup。");
|
|
150
|
+
}
|
|
151
|
+
return current.source;
|
|
152
|
+
}
|
|
153
|
+
async installPackage(pi, workspace, choice) {
|
|
154
|
+
const name = npmName(choice.plugin.source);
|
|
155
|
+
if (!name || !choice.target)
|
|
156
|
+
throw new Error("缺少目标包或目标版本。");
|
|
157
|
+
// Re-read just before mutation so a stale plan cannot replace a changed source.
|
|
158
|
+
const current = (await this.inspectPackages(pi, workspace)).filter(pkg => pkg.name === name);
|
|
159
|
+
for (const old of choice.existing) {
|
|
160
|
+
const actual = current.find(pkg => pkg.scope === old.scope && pkg.source === old.source);
|
|
161
|
+
if (!actual || JSON.stringify(actual.entry) !== JSON.stringify(old.entry))
|
|
162
|
+
throw new Error(`${name} 配置已改变,请重新运行 setup。`);
|
|
163
|
+
}
|
|
164
|
+
if (current.some(pkg => !choice.existing.some(old => old.scope === pkg.scope && old.source === pkg.source) &&
|
|
165
|
+
!(pkg.scope === "user" && npmName(pkg.source) && choice.existing.some(old => old.scope === "project" && npmName(old.source))))) {
|
|
166
|
+
throw new Error(`${name} 出现新的来源,请重新运行 setup。`);
|
|
167
|
+
}
|
|
168
|
+
const otherSources = choice.existing.filter(pkg => !npmName(pkg.source));
|
|
169
|
+
if (otherSources.length > 1 || otherSources.some(pkg => pkg.scope === "user")) {
|
|
170
|
+
throw new Error("存在不同身份的全局来源或多份自定义来源。请先用 pi config 调整加载范围后重试。");
|
|
171
|
+
}
|
|
172
|
+
const globals = choice.existing.filter(pkg => pkg.scope === "user" && npmName(pkg.source));
|
|
173
|
+
if (globals.length > 1 && globals.some(pkg => pkg.filtered))
|
|
174
|
+
throw new Error("存在多份全局 npm 资源过滤,请先用 pi config 调整后重试。");
|
|
175
|
+
const template = otherSources[0] ?? globals.find(pkg => pkg.filtered);
|
|
176
|
+
const target = `npm:${name}@latest`;
|
|
177
|
+
await this.processes.run(pi.command, ["install", "--local", target], workspace, { interactive: true });
|
|
178
|
+
const records = await this.inspectPackages(pi, workspace);
|
|
179
|
+
const installed = records.find(pkg => pkg.scope === "project" && pkg.source === target);
|
|
180
|
+
if (!installed?.installedPath || installed.version !== choice.target) {
|
|
181
|
+
throw new Error(`${name} 实际版本 ${installed?.version ?? "未知"} 与目标 ${choice.target} 不同,请检查 registry 后重试。`);
|
|
182
|
+
}
|
|
183
|
+
if (template) {
|
|
184
|
+
await this.processes.run(process.execPath, [bridge, "replace", pi.root, workspace,
|
|
185
|
+
JSON.stringify({ old: template, target })], this.cwd);
|
|
186
|
+
}
|
|
187
|
+
return installed.version;
|
|
188
|
+
}
|
|
189
|
+
async start(pi, workspace) {
|
|
190
|
+
await access(guard, constants.R_OK);
|
|
191
|
+
await this.processes.run(pi.command, ["--extension", guard, "/pbot init"], resolve(workspace), { interactive: true, timeoutMs: 0 });
|
|
192
|
+
}
|
|
193
|
+
}
|