personal-bot 0.2.8 → 0.2.10

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.
Files changed (75) hide show
  1. package/CONTEXT.md +40 -59
  2. package/README.md +70 -119
  3. package/README.zh-CN.md +73 -116
  4. package/docs/README.md +22 -19
  5. package/docs/agent-collaboration.md +10 -12
  6. package/docs/architecture-guidelines.md +2 -2
  7. package/docs/capability-design.md +4 -12
  8. package/docs/dsh.md +32 -96
  9. package/docs/file-storage.md +8 -5
  10. package/docs/history.md +5 -12
  11. package/docs/installation-design.md +56 -198
  12. package/docs/model-context.md +23 -37
  13. package/docs/model-instruction-guidelines.md +2 -2
  14. package/docs/release.md +5 -5
  15. package/docs/technical-design.md +16 -12
  16. package/docs/workspace-configuration.md +55 -0
  17. package/lib/client.js +28 -3
  18. package/lib/common/typebox.LICENSE +23 -0
  19. package/lib/common/typebox.js +4740 -0
  20. package/lib/dsh.js +0 -5
  21. package/lib/extension/dsh/command.js +15 -11
  22. package/lib/extension/dsh/history.js +1 -1
  23. package/lib/extension/dsh/management.d.ts +4 -0
  24. package/lib/extension/dsh/management.js +21 -6
  25. package/lib/extension/dsh/workspace.d.ts +7 -1
  26. package/lib/extension/dsh/workspace.js +68 -1
  27. package/lib/extension/setup-cli.js +1 -1
  28. package/lib/integrations/dsh/skills.d.ts +4 -0
  29. package/lib/integrations/dsh/skills.js +27 -0
  30. package/lib/model/history.d.ts +1 -0
  31. package/lib/model/workspace-config.d.ts +9 -0
  32. package/lib/model/workspace-config.js +1 -0
  33. package/lib/services/history/history-service.d.ts +1 -2
  34. package/lib/services/history/history-service.js +27 -40
  35. package/lib/services/workspace/workspace-config.d.ts +7 -0
  36. package/lib/services/workspace/workspace-config.js +50 -0
  37. package/lib/services/workspace/workspace-preparation.d.ts +1 -0
  38. package/lib/services/workspace/workspace-preparation.js +8 -0
  39. package/lib/storage/history-files.d.ts +5 -1
  40. package/lib/storage/history-files.js +68 -15
  41. package/lib/tools/agents/contract.js +1 -1
  42. package/lib/tools/agents/dsh-contract.js +1 -1
  43. package/lib/tools/history/contract.js +1 -1
  44. package/lib/tools/teams/contract.js +1 -1
  45. package/lib/ui/dsh/management.js +27 -3
  46. package/npm-shrinkwrap.json +10 -3
  47. package/package.json +102 -31
  48. package/python/README.md +6 -6
  49. package/resources/prompts/workspace.md +3 -3
  50. package/scripts/README.md +16 -15
  51. package/src/dsh.ts +0 -5
  52. package/src/extension/command.ts +25 -8
  53. package/src/extension/dsh/command.ts +13 -8
  54. package/src/extension/dsh/history.ts +2 -2
  55. package/src/extension/dsh/management.ts +21 -5
  56. package/src/extension/dsh/workspace.ts +62 -5
  57. package/src/extension/events.ts +18 -1
  58. package/src/extension/lifecycle.ts +9 -2
  59. package/src/extension/setup-cli.ts +1 -1
  60. package/src/extension/subagent-entry.ts +4 -1
  61. package/src/integrations/dsh/skills.ts +30 -0
  62. package/src/integrations/pi/skills.ts +52 -0
  63. package/src/model/history.ts +1 -0
  64. package/src/model/workspace-config.ts +10 -0
  65. package/src/pbot.ts +2 -3
  66. package/src/services/history/history-service.ts +15 -25
  67. package/src/services/workspace/workspace-config.ts +49 -0
  68. package/src/services/workspace/workspace-preparation.ts +8 -0
  69. package/src/storage/history-files.ts +53 -15
  70. package/src/ui/dsh/management.ts +20 -3
  71. package/docs/dsh-implementation-plan.md +0 -160
  72. package/docs/dsh-support-proposal.md +0 -298
  73. package/docs/dsh.en.md +0 -333
  74. package/docs/subagent-upgrade-proposal.md +0 -357
  75. package/docs/validation.md +0 -189
@@ -1,6 +1,6 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { packageVersion } from "../common/version.js";
3
- import { prepareWorkspace } from "../services/workspace/workspace-preparation.js";
3
+ import { initializeWorkspace } from "../services/workspace/workspace-preparation.js";
4
4
  import type { Confirmations } from "../ui/confirmation.js";
5
5
  import { readPage } from "../ui/pi/reader.js";
6
6
  import { choose } from "../ui/pi/menu.js";
@@ -10,6 +10,7 @@ import type { Lifecycle } from "./lifecycle.js";
10
10
  import { showAgents } from "./agents-command.js";
11
11
  import { showTeams } from "./teams-command.js";
12
12
  import { showHistory } from "./history.js";
13
+ import { workspaceDisabledMessage } from "../services/workspace/workspace-config.js";
13
14
 
14
15
  export function registerCommand(pi: ExtensionAPI, lifecycle: Lifecycle, confirmations: Confirmations) {
15
16
  pi.registerCommand("pbot", {
@@ -22,17 +23,30 @@ export function registerCommand(pi: ExtensionAPI, lifecycle: Lifecycle, confirma
22
23
  handler: async (args, ctx) => {
23
24
  const [, command, argument = ""] = args.trim().match(/^(\S+)(?:\s+([\s\S]*))?$/) ?? [];
24
25
  confirmations.resume();
26
+ if (command === "init" && !argument) {
27
+ await ctx.waitForIdle();
28
+ const start = !lifecycle.active;
29
+ await initializeWorkspace(lifecycle.root ?? ctx.cwd);
30
+ ctx.ui.notify(start ? "工作区已初始化并启用,正在加载 Personal Bot。" : "资料目录已准备;新建的 AGENTS.md 经 /reload 加载。", "info");
31
+ if (start) await ctx.reload();
32
+ return;
33
+ }
34
+ if (!lifecycle.active) {
35
+ const configuration = lifecycle.configuration;
36
+ const status = { workspaceRoot: lifecycle.root ?? ctx.cwd, release: packageVersion, enabled: false,
37
+ state: configuration?.error || configuration?.enabled ? "error" : "disabled",
38
+ nextStep: configuration?.error ?? "/pbot init", configuration };
39
+ ctx.ui.notify(command === "status" ? JSON.stringify(status, null, 2)
40
+ : configuration ? workspaceDisabledMessage(configuration) : "pbot 工作区实例不可用。", configuration?.error ? "error" : "info");
41
+ return;
42
+ }
25
43
  async function execute(command: string | undefined, argument: string) {
26
44
  const instance = lifecycle.get();
27
- if (command === "init" && !argument) {
28
- await ctx.waitForIdle(); instance.check(); await prepareWorkspace(instance.root);
29
- ctx.ui.notify("资料目录已准备。新建的 AGENTS.md 在 /reload 或下次启动后由 Pi 加载。", "info"); return;
30
- }
31
45
  if (command === "status" && !argument) {
32
46
  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; }
47
+ if (ctx.mode !== "tui") { ctx.ui.notify(JSON.stringify({ workspaceRoot: instance.root, release: packageVersion, enabled: true, state: "running", configuration: lifecycle.configuration, python }, null, 2), "info"); return; }
34
48
  const agents = instance.agents.list(agentOwner(ctx)), history = instance.history.status;
35
- await readPage(ctx, "工作区", [instance.root, "", `版本 ${packageVersion}`,
49
+ await readPage(ctx, "工作区", [instance.root, "", "状态 已启动", `配置 ${lifecycle.configuration?.path ?? ""}`, `版本 ${packageVersion}`,
36
50
  `模型 ${ctx.model && !(ctx.model.provider === "unknown" && ctx.model.id === "unknown") ? ctx.model.provider + "/" + ctx.model.id : "未选择"}`,
37
51
  `专家 ${agents.length} 位 · ${agents.filter(value => value.alive).length} 位存活`,
38
52
  `Python ${stateLabel(python.state)}`, ...(python.directory ? [python.directory] : []), ...(python.error ? ["", python.error] : []),
@@ -58,7 +72,10 @@ export function registerCommand(pi: ExtensionAPI, lifecycle: Lifecycle, confirma
58
72
  const choice = await choose(ctx, `Personal Bot · Python ${stateLabel(instance.pythonEnvironment.status.state)}`, choices);
59
73
  if (!choice) return;
60
74
  const command = ["agents", "teams", "history", "status", "init", "python"][choices.indexOf(choice)]!;
61
- try { await execute(command, command === "python" ? "setup" : ""); }
75
+ try {
76
+ if (command === "init") { await ctx.waitForIdle(); instance.check(); await initializeWorkspace(instance.root); ctx.ui.notify("资料目录已准备。", "info"); }
77
+ else await execute(command, command === "python" ? "setup" : "");
78
+ }
62
79
  catch (error) { ctx.ui.notify(error instanceof Error ? error.message : String(error), "error"); }
63
80
  }
64
81
  },
@@ -3,12 +3,11 @@ import type {} from "@deepseek-ai/dsh-commands";
3
3
  import { Type, type Static } from "typebox";
4
4
  import { object, json } from "../../common/json.js";
5
5
  import { requireThat, schema } from "../../common/validation.js";
6
- import { prepareWorkspace } from "../../services/workspace/workspace-preparation.js";
7
6
  import { executeAgent, type AgentInput } from "../../tools/agents/actions.js";
8
7
  import { dshAgentActionSchema } from "../../tools/agents/dsh-contract.js";
9
8
  import { executeTeam, executeTeamMessage } from "../../tools/teams/actions.js";
10
9
  import { teamActionSchema, teamParametersSchema, teamMessageActionSchema, teamMessageParametersSchema } from "../../tools/teams/contract.js";
11
- import { dshStatus, type PbotManagement } from "./management.js";
10
+ import { dshWorkspaceStatus, type PbotManagement } from "./management.js";
12
11
  import type { DshWorkspaces } from "./workspace.js";
13
12
 
14
13
  const validAgent = schema(dshAgentActionSchema), validTeam = schema(teamActionSchema), validMessage = schema(teamMessageActionSchema);
@@ -23,20 +22,26 @@ export function registerDshCommand(ctx: Context, workspaces: DshWorkspaces, mana
23
22
  input: { hint: "status | init | python setup | agent/chat/team/post/history/records JSON", attachments: true },
24
23
  async handler({ agent, rawInput, signal, attachments, commandId }) {
25
24
  signal.throwIfAborted();
25
+ const command = rawInput.trim(), split = command.search(/\s/);
26
+ if (!command || command === "status") {
27
+ requireThat(!attachments.length, "状态查询不接受附件。");
28
+ const status = await dshWorkspaceStatus(workspaces, agent);
29
+ signal.throwIfAborted(); return { kind: "success", text: JSON.stringify(status, null, 2) };
30
+ }
31
+ if (command === "init") {
32
+ requireThat(!attachments.length, "初始化不接受附件。");
33
+ await workspaces.initialize(agent);
34
+ signal.throwIfAborted();
35
+ return { kind: "success", text: JSON.stringify(await dshWorkspaceStatus(workspaces, agent), null, 2) };
36
+ }
26
37
  const workspace = await workspaces.get(agent);
27
38
  signal.throwIfAborted();
28
- const command = rawInput.trim(), split = command.search(/\s/);
29
39
  const verb = split < 0 ? command : command.slice(0, split);
30
40
  requireThat(verb === "chat" || !attachments.length, "输入框附件仅用于 chat 人工对话。");
31
- if (command === "init") {
32
- await prepareWorkspace(workspace.root); workspace.check();
33
- return { kind: "success", text: "资料目录已准备;已有文件保留。" };
34
- }
35
41
  if (command === "python setup") {
36
42
  const result = await workspaces.preparePython(); workspace.check();
37
43
  return { kind: "success", text: JSON.stringify(result, null, 2) };
38
44
  }
39
- if (!command || command === "status") return { kind: "success", text: JSON.stringify(dshStatus(workspaces, workspace), null, 2) };
40
45
  requireThat(split >= 0, usage);
41
46
  const input: unknown = JSON.parse(command.slice(split));
42
47
  requireThat(object(input), "命令参数必须是 JSON 对象。");
@@ -18,9 +18,9 @@ export class DshHistoryWork {
18
18
  #closing: Promise<void> | undefined;
19
19
  constructor(private readonly ctx: Context, root: string) {
20
20
  const agents = new AgentFiles(dshHomePath("pbot", "subagents"), "dsh");
21
- this.service = new HistoryService(root, new HistoryFiles(join(root, ".dsh/pbot/history"), "dsh"),
21
+ this.service = new HistoryService(root, new HistoryFiles(join(root, ".pbot/history"), "dsh"),
22
22
  new DshHistory(ctx, root, () => agents.list().filter(value => value.workspace === root).map(value => value.agentId)),
23
- undefined, new HistoryFiles(join(root, ".pi/pbot/history")));
23
+ new HistoryFiles(join(root, ".pbot/history")));
24
24
  }
25
25
  attach(agent: Agent) { this.#parents.add(agent); return () => { this.#parents.delete(agent); }; }
26
26
  cancel() { this.#controller?.abort(); }
@@ -1,3 +1,4 @@
1
+ import type { Agent } from "@deepseek-ai/dsh-agent";
1
2
  import type { Context } from "@deepseek-ai/cordis";
2
3
  import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
3
4
  import type {} from "@deepseek-ai/dsh-typert-registry";
@@ -25,31 +26,46 @@ const validRequest = schema(requestSchema), validExpert = schema(expertSchema),
25
26
  export function dshStatus(workspaces: DshWorkspaces, workspace: DshWorkspace): Json {
26
27
  workspace.check();
27
28
  const agents = workspace.agents.list(workspace.owner);
28
- return json({ host: "dsh", release: packageVersion, workspaceRoot: workspace.root, sessionId: workspace.agent.id,
29
+ return json({ host: "dsh", release: packageVersion, enabled: true, state: "running", workspaceRoot: workspace.root, sessionId: workspace.agent.id,
29
30
  model: workspace.agent.model.current() ?? null, python: workspaces.python.status,
30
31
  controlError: workspace.controlError ?? null, history: workspace.history.status,
31
32
  agents, teams: workspace.teams.list(workspace.owner, agents) });
32
33
  }
33
34
 
35
+ /** Status remains available when this workspace has not opted in. */
36
+ export async function dshWorkspaceStatus(workspaces: DshWorkspaces, agent: Agent): Promise<Json> {
37
+ const { root, configuration } = await workspaces.configuration(agent);
38
+ if (!configuration.enabled) return json({ host: "dsh", release: packageVersion, enabled: false,
39
+ state: configuration.error ? "error" : "disabled", nextStep: configuration.error ? configuration.error : "/pbot init",
40
+ workspaceRoot: root, sessionId: agent.id, configuration, model: agent.model.current() ?? null, agents: [], teams: [] });
41
+ return json({ ...dshStatus(workspaces, await workspaces.get(agent)) as Record<string, Json>, configuration });
42
+ }
43
+
34
44
  /** Native Remote owns transport and access checks. These workspace reads do not admit or cancel execution. */
35
45
  export class PbotManagement extends TypertRemoteService {
36
46
  constructor(ctx: Context, private readonly workspaces: DshWorkspaces) { super(ctx, "pbotManagement", { namespace: "pbot" }); }
37
47
 
38
- private async workspace(request: Json, signal: AbortSignal) {
48
+ private parent(request: Json, signal: AbortSignal) {
39
49
  signal.throwIfAborted(); requireThat(validRequest(request), "管理查询参数无效。");
40
50
  const args = request as Static<typeof requestSchema>;
41
51
  const agent = this.ctx.agents.get(SessionId(args.parentSessionId));
42
52
  requireThat(agent, "父会话未加载或已关闭,请在原生会话页面重新打开。");
53
+ return { agent, input: args.input };
54
+ }
55
+
56
+ private async workspace(request: Json, signal: AbortSignal) {
57
+ const { agent, input } = this.parent(request, signal);
43
58
  const workspace = await this.workspaces.get(agent);
44
59
  signal.throwIfAborted(); workspace.check();
45
- return { workspace, input: args.input, signal: AbortSignal.any([signal, workspace.lifetime.signal]) };
60
+ return { workspace, input, signal: AbortSignal.any([signal, workspace.lifetime.signal]) };
46
61
  }
47
62
 
48
63
  @Remote
49
64
  async snapshot(request: Json, signal: AbortSignal): Promise<Json> {
50
- const { workspace, input } = await this.workspace(request, signal);
65
+ const { agent, input } = this.parent(request, signal);
51
66
  requireThat(input === undefined, "工作区状态查询不接受额外参数。");
52
- return dshStatus(this.workspaces, workspace);
67
+ const status = await dshWorkspaceStatus(this.workspaces, agent);
68
+ signal.throwIfAborted(); return status;
53
69
  }
54
70
 
55
71
  @Remote
@@ -18,6 +18,10 @@ import { registerDshHistoryTool } from "../../tools/history/dsh-tool.js";
18
18
  import { activatePython } from "../../integrations/python-activation.js";
19
19
  import { PythonEnvironmentService } from "../../services/workspace/python-environment-service.js";
20
20
  import { requireThat } from "../../common/validation.js";
21
+ import { registerDshSkills } from "../../integrations/dsh/skills.js";
22
+ import { readWorkspaceConfig, workspaceDisabledMessage } from "../../services/workspace/workspace-config.js";
23
+ import type { WorkspaceConfiguration } from "../../model/workspace-config.js";
24
+ import { initializeWorkspace } from "../../services/workspace/workspace-preparation.js";
21
25
 
22
26
  export const dshDependencies = ["agents", "sessions", "sessionPersistence", "sessionQuery", "llm", "tools", "systemPrompt", "skills", "fs", "attachments", "workingDirectory", "approval"];
23
27
 
@@ -45,6 +49,7 @@ export class DshWorkspace {
45
49
  { check: this.check, release: () => this.host.release(d.agentId, generation) });
46
50
  this.#experts.set(d.agentId, runtime);
47
51
  scope.effect(() => () => { if (this.#experts.get(d.agentId) === runtime) this.#experts.delete(d.agentId); });
52
+ registerDshSkills(scope, child);
48
53
  await dshResources(scope, child, root, "general-purpose", () => python.status, runtime.check);
49
54
  const teamTool = dshExpertTools(scope, child, d.options);
50
55
  if (teamTool) registerDshTeamMessageTool(scope, child, this.teams, runtime.actor, runtime.check);
@@ -67,6 +72,7 @@ export class DshWorkspace {
67
72
  async open() {
68
73
  this.check();
69
74
  this.#scope = await this.agent.ctx.plugin({ name: "pbot-coordinator", inject: dshDependencies, apply: async ctx => {
75
+ registerDshSkills(ctx, this.agent);
70
76
  await dshResources(ctx, this.agent, this.root, "coordinator", () => this.python.status, this.check);
71
77
  registerDshFileEvents(ctx, this.agent, this.root, false, this.check);
72
78
  registerDshAgentTool(ctx, this.agent, this.agents, this.owner, this.check);
@@ -108,7 +114,9 @@ export class DshWorkspace {
108
114
  /** The execution component owns Python; closing one Session must not undo its activation. */
109
115
  export class DshWorkspaces {
110
116
  readonly python = new PythonEnvironmentService(process.cwd());
111
- readonly #parents = new Map<Agent, Promise<DshWorkspace>>();
117
+ readonly #parents = new Map<Agent, Promise<DshWorkspace | undefined>>();
118
+ readonly #initializing = new Map<string, Promise<void>>();
119
+ readonly #configurations = new Map<Agent, { root: string; configuration: WorkspaceConfiguration }>();
112
120
  readonly #parentEffects = new Map<Agent, () => Promise<void>>();
113
121
  readonly #histories = new Map<string, { work: DshHistoryWork; users: number }>();
114
122
  readonly #lifetime = new AbortController();
@@ -144,16 +152,29 @@ export class DshWorkspaces {
144
152
  const opening = (async () => {
145
153
  const root = await realpath(agent.session.header.cwd ?? process.cwd());
146
154
  this.#lifetime.signal.throwIfAborted();
155
+ const configuration = await readWorkspaceConfig(root);
156
+ this.#lifetime.signal.throwIfAborted();
157
+ this.#configurations.set(agent, { root, configuration });
158
+ if (!configuration.enabled) {
159
+ if (configuration.error) this.ctx.logger.warn(configuration.error);
160
+ const dispose = agent.ctx.effect(() => () => {
161
+ this.#parents.delete(agent); this.#configurations.delete(agent); this.#parentEffects.delete(agent);
162
+ });
163
+ this.#parentEffects.set(agent, dispose);
164
+ return undefined;
165
+ }
147
166
  const history = await this.history(root);
148
167
  let workspace: DshWorkspace | undefined;
149
168
  try {
150
169
  workspace = new DshWorkspace(this.ctx, root, agent, this.python, this.#lifetime.signal, history.work, history.release);
151
170
  const owned = workspace;
152
171
  const dispose = agent.ctx.effect(() => async () => {
153
- this.#parents.delete(agent); this.#parentEffects.delete(agent); await owned.close();
172
+ this.#parents.delete(agent); this.#configurations.delete(agent); this.#parentEffects.delete(agent); await owned.close();
154
173
  });
155
174
  this.#parentEffects.set(agent, dispose);
156
- await workspace.open(); this.#lifetime.signal.throwIfAborted(); return workspace;
175
+ await workspace.open(); this.#lifetime.signal.throwIfAborted();
176
+ void this.preparePython().catch(error => this.ctx.logger.warn(String(error)));
177
+ return workspace;
157
178
  } catch (error) {
158
179
  try {
159
180
  const dispose = this.#parentEffects.get(agent);
@@ -171,10 +192,45 @@ export class DshWorkspaces {
171
192
  this.#lifetime.signal.throwIfAborted();
172
193
  requireThat(this.ctx.agents.get(agent.id) === agent, "父会话已关闭。");
173
194
  const workspace = await this.open(agent);
195
+ const configuration = this.#configurations.get(agent)?.configuration;
196
+ requireThat(workspace, configuration ? workspaceDisabledMessage(configuration) : "pbot 工作区实例不可用。");
174
197
  workspace.check();
175
198
  return workspace;
176
199
  }
177
200
 
201
+ async configuration(agent: Agent) {
202
+ this.#lifetime.signal.throwIfAborted();
203
+ requireThat(this.ctx.agents.get(agent.id) === agent, "父会话已关闭。");
204
+ await this.open(agent);
205
+ this.#lifetime.signal.throwIfAborted();
206
+ const current = this.#configurations.get(agent);
207
+ requireThat(current && this.ctx.agents.get(agent.id) === agent, "父会话已关闭。");
208
+ return current;
209
+ }
210
+
211
+ async initialize(agent: Agent): Promise<DshWorkspace> {
212
+ const { root } = await this.configuration(agent);
213
+ let pending = this.#initializing.get(root);
214
+ if (!pending) {
215
+ pending = (async () => {
216
+ await initializeWorkspace(root);
217
+ this.#lifetime.signal.throwIfAborted();
218
+ const configuration = await readWorkspaceConfig(root);
219
+ for (const [parent, current] of [...this.#configurations]) {
220
+ if (current.root !== root || this.ctx.agents.get(parent.id) !== parent) continue;
221
+ const workspace = await this.#parents.get(parent);
222
+ if (workspace) { this.#configurations.set(parent, { root, configuration }); continue; }
223
+ await this.#parentEffects.get(parent)?.();
224
+ await this.open(parent);
225
+ }
226
+ })();
227
+ this.#initializing.set(root, pending);
228
+ }
229
+ try { await pending; }
230
+ finally { if (this.#initializing.get(root) === pending) this.#initializing.delete(root); }
231
+ return this.get(agent);
232
+ }
233
+
178
234
  preparePython() {
179
235
  this.#lifetime.signal.throwIfAborted();
180
236
  if (this.#preparing) return this.#preparing;
@@ -192,14 +248,15 @@ export class DshWorkspaces {
192
248
  this.#lifetime.abort();
193
249
  this.#closing = (async () => {
194
250
  try {
251
+ await Promise.allSettled(this.#initializing.values());
195
252
  const opened = await Promise.allSettled(this.#parents.values());
196
- const closed = await Promise.allSettled(opened.filter(v => v.status === "fulfilled").map(v => v.value.close()));
253
+ const closed = await Promise.allSettled(opened.flatMap(value => value.status === "fulfilled" && value.value ? [value.value.close()] : []));
197
254
  const effects = await Promise.allSettled([...this.#parentEffects.values()].map(dispose => dispose()));
198
255
  const histories = await Promise.allSettled([...this.#histories.values()].map(value => value.work.close()));
199
256
  await this.python.close();
200
257
  const errors = [...new Set([...closed, ...effects, ...histories].flatMap(value => value.status === "rejected" ? [value.reason] : []))];
201
258
  if (errors.length) throw new AggregateError(errors, "部分 dsh 工作区未能完成关闭。");
202
- } finally { this.#parents.clear(); this.#parentEffects.clear(); this.#histories.clear(); this.#restorePython?.(); this.#restorePython = undefined; }
259
+ } finally { this.#parents.clear(); this.#configurations.clear(); this.#parentEffects.clear(); this.#histories.clear(); this.#restorePython?.(); this.#restorePython = undefined; }
203
260
  })();
204
261
  return this.#closing;
205
262
  }
@@ -7,11 +7,19 @@ import { resourceRoot } from "../common/paths.js";
7
7
  import { agentText } from "../integrations/agent-resources.js";
8
8
  import type { Confirmations } from "../ui/confirmation.js";
9
9
  import type { Lifecycle } from "./lifecycle.js";
10
+ import { readWorkspaceConfig, workspaceDisabledMessage } from "../services/workspace/workspace-config.js";
11
+ import { workspaceSkillPaths } from "../integrations/pi/skills.js";
10
12
 
11
13
  export function registerEvents(pi: ExtensionAPI, lifecycle: Lifecycle, confirmations: Confirmations) {
12
14
  pi.on("session_start", async (_event, ctx) => {
13
15
  try {
14
16
  await lifecycle.open(ctx.cwd);
17
+ if (!lifecycle.active) {
18
+ ctx.ui.setStatus("pbot", lifecycle.configuration?.error ? "PBot 配置错误" : "PBot 未启用 · /pbot init");
19
+ if (ctx.hasUI && lifecycle.configuration) ctx.ui.notify(workspaceDisabledMessage(lifecycle.configuration), lifecycle.configuration.error ? "error" : "info");
20
+ return;
21
+ }
22
+ ctx.ui.setStatus("pbot", "PBot 已启动");
15
23
  if (ctx.mode === "tui") ctx.ui.setStatus("pbot-python", "Python 准备中");
16
24
  else ctx.ui.notify("正在准备共享 Python 环境;首次安装可能需要数分钟。", "info");
17
25
  try { await lifecycle.get().preparePython(); }
@@ -21,10 +29,15 @@ export function registerEvents(pi: ExtensionAPI, lifecycle: Lifecycle, confirmat
21
29
  else if (ctx.hasUI) ctx.ui.notify(message, "warning"); else console.error(message);
22
30
  } finally { if (ctx.mode === "tui") ctx.ui.setStatus("pbot-python", undefined); }
23
31
  } catch (error) {
32
+ ctx.ui.setStatus("pbot", "PBot 启动失败");
24
33
  if (ctx.hasUI) ctx.ui.notify(error instanceof Error ? error.message : "pbot 启动失败。", "error");
25
34
  else throw error;
26
35
  }
27
36
  });
37
+ pi.on("resources_discover", (_event, ctx) => {
38
+ if (!lifecycle.active) return;
39
+ return { skillPaths: workspaceSkillPaths(pi, ctx.cwd) };
40
+ });
28
41
  pi.on("before_agent_start", async (event, ctx) => {
29
42
  const instance = lifecycle.active;
30
43
  if (!instance) return;
@@ -49,7 +62,11 @@ export function registerEvents(pi: ExtensionAPI, lifecycle: Lifecycle, confirmat
49
62
  pi.on("session_before_switch", () => { lifecycle.active?.cancelWork(); });
50
63
  pi.on("session_before_fork", () => { lifecycle.active?.cancelWork(); });
51
64
  pi.on("session_before_tree", () => { lifecycle.active?.cancelWork(); });
52
- pi.on("session_shutdown", event => lifecycle.close(event.reason === "reload"));
65
+ pi.on("session_shutdown", async (event, ctx) => {
66
+ const preserve = event.reason === "reload" && lifecycle.root && (await readWorkspaceConfig(lifecycle.root)).enabled;
67
+ try { await lifecycle.close(Boolean(preserve)); }
68
+ finally { ctx.ui.setStatus("pbot", undefined); ctx.ui.setStatus("pbot-python", undefined); }
69
+ });
53
70
  pi.on("tool_call", async (event, ctx) => {
54
71
  const instance = lifecycle.active;
55
72
  if (["write", "edit"].includes(event.toolName) && "path" in event.input && typeof event.input.path === "string") {
@@ -8,6 +8,8 @@ import type { AgentService } from "../services/agents/agent-service.js";
8
8
  import type { TeamService } from "../services/agents/team-service.js";
9
9
  import type { CallAccess } from "../tools/access.js";
10
10
  import type { HistoryService } from "../services/history/history-service.js";
11
+ import { readWorkspaceConfig, workspaceDisabledMessage } from "../services/workspace/workspace-config.js";
12
+ import type { WorkspaceConfiguration } from "../model/workspace-config.js";
11
13
 
12
14
  export class WorkspaceInstance implements CallAccess {
13
15
  readonly #controller = new AbortController();
@@ -62,16 +64,21 @@ export class WorkspaceInstance implements CallAccess {
62
64
  }
63
65
  export class Lifecycle {
64
66
  #active: WorkspaceInstance | undefined;
67
+ configuration: WorkspaceConfiguration | undefined;
68
+ root: string | undefined;
65
69
  constructor(private readonly create: (root: string) => WorkspaceInstance,
66
70
  private readonly register: (instance: WorkspaceInstance) => void, private readonly hide: () => void) {}
67
71
  get active() { return this.#active; }
68
72
  get() {
69
- requireThat(this.#active, "pbot 工作区实例不可用。");
73
+ requireThat(this.#active, this.configuration ? workspaceDisabledMessage(this.configuration) : "pbot 工作区实例不可用。");
70
74
  this.#active.check(); return this.#active;
71
75
  }
72
76
  async open(root: string) {
73
77
  await this.close();
74
- const instance = this.create(await realpath(root));
78
+ this.root = await realpath(root);
79
+ this.configuration = await readWorkspaceConfig(this.root);
80
+ if (!this.configuration.enabled) return;
81
+ const instance = this.create(this.root);
75
82
  this.#active = instance;
76
83
  try { this.register(instance); }
77
84
  catch (error) { await this.close(); throw error; }
@@ -92,7 +92,7 @@ export async function main(args: string[]): Promise<number> {
92
92
  const failed = result.results.some(item => item.state === "failed");
93
93
  if (!result.pi) return 1;
94
94
  ui.text("\n软件安装阶段结束。初始化、Python 环境和外部服务的状态由 Pi/pbot/插件分别报告。");
95
- if (await ui.confirm(failed ? "部分插件失败,是否仍先启动 Pi" : "立即启动 Pi", !failed)) {
95
+ if (await ui.confirm(failed ? "部分插件失败,是否仍启动 Pi 并初始化启用工作区" : "启动 Pi 并初始化启用工作区", !failed)) {
96
96
  process.stdin.pause();
97
97
  await host.start(result.pi, workspace);
98
98
  } else {
@@ -20,9 +20,10 @@ import { agentFileTools } from "../integrations/pi/agent-tools.js";
20
20
  import { resourceRoot } from "../common/paths.js";
21
21
  import { requireThat } from "../common/validation.js";
22
22
  import { packageVersion, releaseLine } from "../common/version.js";
23
+ import { readWorkspaceConfig, workspaceDisabledMessage } from "../services/workspace/workspace-config.js";
23
24
 
24
25
  /** Explicit child composition; never installs main commands or approval capabilities. */
25
- export default function subagent(pi: ExtensionAPI) {
26
+ export default async function subagent(pi: ExtensionAPI) {
26
27
  const id = process.env.PBOT_SUBAGENT_RUN_ID, directory = process.env.PBOT_SUBAGENT_DIR, generation = process.env.PBOT_SUBAGENT_GENERATION;
27
28
  requireThat(id && directory && generation, "缺少子会话启动身份。");
28
29
  const files = new AgentFiles(dirname(realpathSync(directory)));
@@ -30,6 +31,8 @@ export default function subagent(pi: ExtensionAPI) {
30
31
  const d = files.read(id, "descriptor.json", descriptor);
31
32
  requireThat(d.agentId === id && releaseLine(d.release) === releaseLine(packageVersion), "子会话身份或版本无效。");
32
33
  requireThat(files.read(id, "launch.json", launch).generation === generation, "子会话启动代次已失效。");
34
+ const configuration = await readWorkspaceConfig(d.workspace);
35
+ requireThat(configuration.enabled, workspaceDisabledMessage(configuration));
33
36
  const teams = new TeamService(d.workspace, files, new TeamFiles(files));
34
37
  const lifetime = new AbortController();
35
38
  let activeTeam: TeamLink | undefined, pendingPause: string | undefined;
@@ -0,0 +1,30 @@
1
+ import { join } from "node:path";
2
+ import type { Context } from "@deepseek-ai/cordis";
3
+ import type { Agent } from "@deepseek-ai/dsh-agent";
4
+ import { scopeParentOf } from "@deepseek-ai/dsh-scope";
5
+ import { FileSystemSkillProvider } from "@deepseek-ai/dsh-skill-filesystem";
6
+ import { resourceRoot } from "../../common/paths.js";
7
+
8
+ /** Keep bundled methods local to this Agent without overriding its native project/user methods. */
9
+ export function registerDshSkills(ctx: Context, agent: Agent): void {
10
+ let filesystem: FileSystemSkillProvider;
11
+ ctx.skills.registerProvider(control => {
12
+ filesystem = new FileSystemSkillProvider(ctx, control, {
13
+ providerName: "pbot", includeDefaultRoots: false, bundledSkillDir: join(resourceRoot, "skills"),
14
+ });
15
+ return {
16
+ name: "pbot",
17
+ async list(options) {
18
+ const inherited = await ctx.skills.snapshot({ ...options, scope: scopeParentOf(agent) });
19
+ const preferred = new Set(inherited.skills.filter(skill => skill.provider !== "pbot").map(skill => skill.name));
20
+ const listed = await filesystem.list(options);
21
+ const candidates = Array.isArray(listed) ? listed : listed.candidates;
22
+ return { candidates: candidates.filter(skill => !preferred.has(skill.name)),
23
+ complete: inherited.complete && (Array.isArray(listed) || listed.complete) };
24
+ },
25
+ get: (candidate, options) => filesystem.get(candidate, options),
26
+ };
27
+ });
28
+ ctx.effect(() => () => filesystem.dispose());
29
+ ctx.on("fs/observed", target => { filesystem.observeHostMutation(target.displayPath); });
30
+ }
@@ -0,0 +1,52 @@
1
+ import { basename, dirname, join, matchesGlob, normalize, relative, resolve, sep } from "node:path";
2
+ import { getAgentDir, loadSkillsFromDir, parseArgs, SettingsManager } from "@earendil-works/pi-coding-agent";
3
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
+ import { resourceRoot } from "../../common/paths.js";
5
+
6
+ /** Dynamic resources follow the owning package's native include/exclude settings. */
7
+ export function workspaceSkillPaths(pi: ExtensionAPI, cwd: string): string[] {
8
+ if (parseArgs(process.argv.slice(2)).noSkills) return [];
9
+ const root = resolve(resourceRoot, ".."), origin = pi.getCommands().find(command => command.name === "pbot")?.sourceInfo;
10
+ const files = loadSkillsFromDir({ dir: join(resourceRoot, "skills"), source: "pbot" }).skills.map(skill => skill.filePath);
11
+ const posix = (path: string) => normalize(path).split(sep).join("/");
12
+ const matches = (file: string, pattern: string, exact = false) => {
13
+ const target = posix(pattern), parent = dirname(file);
14
+ const paths = [relative(root, file), file, relative(root, parent), parent];
15
+ if (!exact) paths.push(basename(file), basename(parent));
16
+ return paths.some(path => exact ? posix(path) === target : matchesGlob(posix(path), target));
17
+ };
18
+ const own = (entry: string | { source: string }) => {
19
+ const source = typeof entry === "string" ? entry : entry.source;
20
+ return source === origin?.source || [join(cwd, ".pi"), getAgentDir(), cwd].some(base => resolve(base, source) === root);
21
+ };
22
+ let entries = (pi.getSettings().packages ?? []).filter(own);
23
+ if (origin?.origin === "package" && (!entries.length || entries.every(entry => typeof entry !== "string" && entry.autoload === false))) {
24
+ const global = SettingsManager.create(cwd, getAgentDir(), { projectTrusted: false }).getGlobalSettings();
25
+ entries = [...(global.packages ?? []).filter(own), ...entries];
26
+ }
27
+ let selected = new Set(entries.length && entries.every(entry => typeof entry !== "string" && entry.autoload === false) ? [] : files);
28
+ for (const entry of entries) {
29
+ if (typeof entry === "string" || entry.skills === undefined) continue;
30
+ const patterns = entry.skills;
31
+ if (entry.autoload === false) {
32
+ for (const pattern of patterns) {
33
+ const exact = /^[+-]/.test(pattern), target = pattern.replace(/^[!+-]/, "");
34
+ for (const file of files.filter(file => matches(file, target, exact))) {
35
+ if (/^[!-]/.test(pattern)) selected.delete(file); else selected.add(file);
36
+ }
37
+ }
38
+ continue;
39
+ }
40
+ if (!patterns.length) { selected.clear(); continue; }
41
+ const includes = patterns.filter(pattern => !/^[!+-]/.test(pattern));
42
+ selected = new Set(files.filter(file => !includes.length || includes.some(pattern => matches(file, pattern))));
43
+ for (const prefix of ["!", "+", "-"]) {
44
+ for (const pattern of patterns.filter(pattern => pattern.startsWith(prefix))) {
45
+ for (const file of files.filter(file => matches(file, pattern.slice(1), prefix !== "!"))) {
46
+ if (prefix === "+") selected.add(file); else selected.delete(file);
47
+ }
48
+ }
49
+ }
50
+ }
51
+ return [...selected];
52
+ }
@@ -19,6 +19,7 @@ export interface HistoryNode extends HistoryExchange {
19
19
  ready: boolean;
20
20
  }
21
21
  export interface HistoryRecord {
22
+ host: HistoryHost;
22
23
  id: string;
23
24
  sessionId: string;
24
25
  name: string;
@@ -0,0 +1,10 @@
1
+ export interface WorkspaceConfig {
2
+ enabled?: boolean;
3
+ }
4
+
5
+ export interface WorkspaceConfiguration {
6
+ path: string;
7
+ present: boolean;
8
+ enabled: boolean;
9
+ error?: string;
10
+ }
package/src/pbot.ts CHANGED
@@ -13,7 +13,6 @@ import { HistoryService } from "./services/history/history-service.js";
13
13
  import { HistoryFiles } from "./storage/history-files.js";
14
14
  import { PiHistory } from "./integrations/pi/history.js";
15
15
  import { sessionRef } from "./storage/agent-files.js";
16
- import { getAgentDir } from "@earendil-works/pi-coding-agent";
17
16
  import { join } from "node:path";
18
17
  import { registerHistoryEvents } from "./extension/history.js";
19
18
 
@@ -23,11 +22,11 @@ export default function pbot(pi: ExtensionAPI) {
23
22
  const confirmations = new Confirmations(), registry = new ToolRegistry(pi);
24
23
  const lifecycle = new Lifecycle(root => {
25
24
  const agents = new AgentService(root, agentDirectory(), new PiAgents());
26
- const history = new HistoryService(root, new HistoryFiles(join(root, ".pi/pbot/history")), new PiHistory(root, () =>
25
+ const history = new HistoryService(root, new HistoryFiles(join(root, ".pbot/history")), new PiHistory(root, () =>
27
26
  agents.files.list().filter(agent => agent.workspace === root).flatMap(agent => {
28
27
  const file = agents.files.optional(agent.agentId, "session-ref.json", sessionRef)?.file;
29
28
  return file ? [file] : [];
30
- })), new HistoryFiles(join(getAgentDir(), "pbot/history")), new HistoryFiles(join(root, ".dsh/pbot/history"), "dsh"));
29
+ })), new HistoryFiles(join(root, ".pbot/history"), "dsh"));
31
30
  return new WorkspaceInstance(root, new PythonEnvironmentService(root), agents, new TeamService(root, agents.files, agents.teamFiles), history);
32
31
  }, instance => registry.register(mainTools(instance.agents, instance.teams, instance.history, instance)), () => registry.hide());
33
32
  registerCommand(pi, lifecycle, confirmations);