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
package/lib/dsh.js CHANGED
@@ -1,6 +1,3 @@
1
- import { join } from "node:path";
2
- import * as SkillFilesystem from "@deepseek-ai/dsh-skill-filesystem";
3
- import { resourceRoot } from "./common/paths.js";
4
1
  import { registerDshCommand } from "./extension/dsh/command.js";
5
2
  import { registerDshManagement } from "./extension/dsh/management.js";
6
3
  import { DshWorkspaces, dshDependencies } from "./extension/dsh/workspace.js";
@@ -10,7 +7,6 @@ export const inject = ["commands", "typert", ...dshDependencies];
10
7
  export async function apply(ctx) {
11
8
  const [major, minor] = process.versions.node.split(".").map(Number);
12
9
  requireThat(major > 24 || (major === 24 && minor >= 9), "dsh 接入需要 Node.js 24.9.0 或更新版本。");
13
- await ctx.plugin(SkillFilesystem, { providerName: "pbot", includeDefaultRoots: false, bundledSkillDir: join(resourceRoot, "skills") });
14
10
  const workspaces = new DshWorkspaces(ctx);
15
11
  ctx.effect(() => () => workspaces.close());
16
12
  const management = registerDshManagement(ctx, workspaces);
@@ -22,5 +18,4 @@ export async function apply(ctx) {
22
18
  for (const agent of ctx.agents.roots())
23
19
  if (agent.session.header.origin !== "subagent")
24
20
  await workspaces.open(agent);
25
- void workspaces.preparePython().catch(error => ctx.logger.warn(String(error)));
26
21
  }
@@ -1,12 +1,11 @@
1
- import { Type } from "typebox";
1
+ import { Type } from "../../common/typebox.js";
2
2
  import { object, json } from "../../common/json.js";
3
3
  import { requireThat, schema } from "../../common/validation.js";
4
- import { prepareWorkspace } from "../../services/workspace/workspace-preparation.js";
5
4
  import { executeAgent } from "../../tools/agents/actions.js";
6
5
  import { dshAgentActionSchema } from "../../tools/agents/dsh-contract.js";
7
6
  import { executeTeam, executeTeamMessage } from "../../tools/teams/actions.js";
8
7
  import { teamActionSchema, teamParametersSchema, teamMessageActionSchema, teamMessageParametersSchema } from "../../tools/teams/contract.js";
9
- import { dshStatus } from "./management.js";
8
+ import { dshWorkspaceStatus } from "./management.js";
10
9
  const validAgent = schema(dshAgentActionSchema), validTeam = schema(teamActionSchema), validMessage = schema(teamMessageActionSchema);
11
10
  const chatSchema = Type.Object({ agentId: Type.String({ minLength: 1 }), text: Type.Optional(Type.String({ maxLength: 24000 })),
12
11
  delivery: Type.Optional(Type.Union([Type.Literal("steer"), Type.Literal("followUp")])),
@@ -18,23 +17,28 @@ export function registerDshCommand(ctx, workspaces, management) {
18
17
  input: { hint: "status | init | python setup | agent/chat/team/post/history/records JSON", attachments: true },
19
18
  async handler({ agent, rawInput, signal, attachments, commandId }) {
20
19
  signal.throwIfAborted();
20
+ const command = rawInput.trim(), split = command.search(/\s/);
21
+ if (!command || command === "status") {
22
+ requireThat(!attachments.length, "状态查询不接受附件。");
23
+ const status = await dshWorkspaceStatus(workspaces, agent);
24
+ signal.throwIfAborted();
25
+ return { kind: "success", text: JSON.stringify(status, null, 2) };
26
+ }
27
+ if (command === "init") {
28
+ requireThat(!attachments.length, "初始化不接受附件。");
29
+ await workspaces.initialize(agent);
30
+ signal.throwIfAborted();
31
+ return { kind: "success", text: JSON.stringify(await dshWorkspaceStatus(workspaces, agent), null, 2) };
32
+ }
21
33
  const workspace = await workspaces.get(agent);
22
34
  signal.throwIfAborted();
23
- const command = rawInput.trim(), split = command.search(/\s/);
24
35
  const verb = split < 0 ? command : command.slice(0, split);
25
36
  requireThat(verb === "chat" || !attachments.length, "输入框附件仅用于 chat 人工对话。");
26
- if (command === "init") {
27
- await prepareWorkspace(workspace.root);
28
- workspace.check();
29
- return { kind: "success", text: "资料目录已准备;已有文件保留。" };
30
- }
31
37
  if (command === "python setup") {
32
38
  const result = await workspaces.preparePython();
33
39
  workspace.check();
34
40
  return { kind: "success", text: JSON.stringify(result, null, 2) };
35
41
  }
36
- if (!command || command === "status")
37
- return { kind: "success", text: JSON.stringify(dshStatus(workspaces, workspace), null, 2) };
38
42
  requireThat(split >= 0, usage);
39
43
  const input = JSON.parse(command.slice(split));
40
44
  requireThat(object(input), "命令参数必须是 JSON 对象。");
@@ -17,7 +17,7 @@ export class DshHistoryWork {
17
17
  constructor(ctx, root) {
18
18
  this.ctx = ctx;
19
19
  const agents = new AgentFiles(dshHomePath("pbot", "subagents"), "dsh");
20
- this.service = new HistoryService(root, new HistoryFiles(join(root, ".dsh/pbot/history"), "dsh"), new DshHistory(ctx, root, () => agents.list().filter(value => value.workspace === root).map(value => value.agentId)), undefined, new HistoryFiles(join(root, ".pi/pbot/history")));
20
+ this.service = new HistoryService(root, new HistoryFiles(join(root, ".pbot/history"), "dsh"), new DshHistory(ctx, root, () => agents.list().filter(value => value.workspace === root).map(value => value.agentId)), new HistoryFiles(join(root, ".pbot/history")));
21
21
  }
22
22
  attach(agent) { this.#parents.add(agent); return () => { this.#parents.delete(agent); }; }
23
23
  cancel() { this.#controller?.abort(); }
@@ -1,12 +1,16 @@
1
+ import type { Agent } from "@deepseek-ai/dsh-agent";
1
2
  import type { Context } from "@deepseek-ai/cordis";
2
3
  import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
3
4
  import { type Json } from "../../common/json.js";
4
5
  import type { DshWorkspaces, DshWorkspace } from "./workspace.js";
5
6
  export declare function dshStatus(workspaces: DshWorkspaces, workspace: DshWorkspace): Json;
7
+ /** Status remains available when this workspace has not opted in. */
8
+ export declare function dshWorkspaceStatus(workspaces: DshWorkspaces, agent: Agent): Promise<Json>;
6
9
  /** Native Remote owns transport and access checks. These workspace reads do not admit or cancel execution. */
7
10
  export declare class PbotManagement extends TypertRemoteService {
8
11
  private readonly workspaces;
9
12
  constructor(ctx: Context, workspaces: DshWorkspaces);
13
+ private parent;
10
14
  private workspace;
11
15
  snapshot(request: Json, signal: AbortSignal): Promise<Json>;
12
16
  expert(request: Json, signal: AbortSignal): Promise<Json>;
@@ -87,7 +87,7 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
87
87
  import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
88
88
  import { SessionId } from "@deepseek-ai/dsh-session";
89
89
  import { extractSessionEventText } from "@deepseek-ai/dsh-session-query";
90
- import { Type } from "typebox";
90
+ import { Type } from "../../common/typebox.js";
91
91
  import { json } from "../../common/json.js";
92
92
  import { requireThat, schema } from "../../common/validation.js";
93
93
  import { packageVersion } from "../../common/version.js";
@@ -106,11 +106,20 @@ const validRequest = schema(requestSchema), validExpert = schema(expertSchema),
106
106
  export function dshStatus(workspaces, workspace) {
107
107
  workspace.check();
108
108
  const agents = workspace.agents.list(workspace.owner);
109
- return json({ host: "dsh", release: packageVersion, workspaceRoot: workspace.root, sessionId: workspace.agent.id,
109
+ return json({ host: "dsh", release: packageVersion, enabled: true, state: "running", workspaceRoot: workspace.root, sessionId: workspace.agent.id,
110
110
  model: workspace.agent.model.current() ?? null, python: workspaces.python.status,
111
111
  controlError: workspace.controlError ?? null, history: workspace.history.status,
112
112
  agents, teams: workspace.teams.list(workspace.owner, agents) });
113
113
  }
114
+ /** Status remains available when this workspace has not opted in. */
115
+ export async function dshWorkspaceStatus(workspaces, agent) {
116
+ const { root, configuration } = await workspaces.configuration(agent);
117
+ if (!configuration.enabled)
118
+ return json({ host: "dsh", release: packageVersion, enabled: false,
119
+ state: configuration.error ? "error" : "disabled", nextStep: configuration.error ? configuration.error : "/pbot init",
120
+ workspaceRoot: root, sessionId: agent.id, configuration, model: agent.model.current() ?? null, agents: [], teams: [] });
121
+ return json({ ...dshStatus(workspaces, await workspaces.get(agent)), configuration });
122
+ }
114
123
  /** Native Remote owns transport and access checks. These workspace reads do not admit or cancel execution. */
115
124
  let PbotManagement = (() => {
116
125
  let _classSuper = TypertRemoteService;
@@ -137,21 +146,27 @@ let PbotManagement = (() => {
137
146
  super(ctx, "pbotManagement", { namespace: "pbot" });
138
147
  this.workspaces = workspaces;
139
148
  }
140
- async workspace(request, signal) {
149
+ parent(request, signal) {
141
150
  signal.throwIfAborted();
142
151
  requireThat(validRequest(request), "管理查询参数无效。");
143
152
  const args = request;
144
153
  const agent = this.ctx.agents.get(SessionId(args.parentSessionId));
145
154
  requireThat(agent, "父会话未加载或已关闭,请在原生会话页面重新打开。");
155
+ return { agent, input: args.input };
156
+ }
157
+ async workspace(request, signal) {
158
+ const { agent, input } = this.parent(request, signal);
146
159
  const workspace = await this.workspaces.get(agent);
147
160
  signal.throwIfAborted();
148
161
  workspace.check();
149
- return { workspace, input: args.input, signal: AbortSignal.any([signal, workspace.lifetime.signal]) };
162
+ return { workspace, input, signal: AbortSignal.any([signal, workspace.lifetime.signal]) };
150
163
  }
151
164
  async snapshot(request, signal) {
152
- const { workspace, input } = await this.workspace(request, signal);
165
+ const { agent, input } = this.parent(request, signal);
153
166
  requireThat(input === undefined, "工作区状态查询不接受额外参数。");
154
- return dshStatus(this.workspaces, workspace);
167
+ const status = await dshWorkspaceStatus(this.workspaces, agent);
168
+ signal.throwIfAborted();
169
+ return status;
155
170
  }
156
171
  async expert(request, signal) {
157
172
  const env_1 = { stack: [], error: void 0, hasError: false };
@@ -8,6 +8,7 @@ import { DshAgentRuntime } from "./agent-runtime.js";
8
8
  import { DshHistoryWork } from "./history.js";
9
9
  import type { HistoryService } from "../../services/history/history-service.js";
10
10
  import { PythonEnvironmentService } from "../../services/workspace/python-environment-service.js";
11
+ import type { WorkspaceConfiguration } from "../../model/workspace-config.js";
11
12
  export declare const dshDependencies: string[];
12
13
  export declare class DshWorkspace {
13
14
  #private;
@@ -38,8 +39,13 @@ export declare class DshWorkspaces {
38
39
  readonly python: PythonEnvironmentService;
39
40
  constructor(ctx: Context);
40
41
  private history;
41
- open(agent: Agent): Promise<DshWorkspace>;
42
+ open(agent: Agent): Promise<DshWorkspace | undefined>;
42
43
  get(agent: Agent): Promise<DshWorkspace>;
44
+ configuration(agent: Agent): Promise<{
45
+ root: string;
46
+ configuration: WorkspaceConfiguration;
47
+ }>;
48
+ initialize(agent: Agent): Promise<DshWorkspace>;
43
49
  preparePython(): Promise<{
44
50
  directory: string;
45
51
  python: string;
@@ -14,6 +14,9 @@ import { registerDshHistoryTool } from "../../tools/history/dsh-tool.js";
14
14
  import { activatePython } from "../../integrations/python-activation.js";
15
15
  import { PythonEnvironmentService } from "../../services/workspace/python-environment-service.js";
16
16
  import { requireThat } from "../../common/validation.js";
17
+ import { registerDshSkills } from "../../integrations/dsh/skills.js";
18
+ import { readWorkspaceConfig, workspaceDisabledMessage } from "../../services/workspace/workspace-config.js";
19
+ import { initializeWorkspace } from "../../services/workspace/workspace-preparation.js";
17
20
  export const dshDependencies = ["agents", "sessions", "sessionPersistence", "sessionQuery", "llm", "tools", "systemPrompt", "skills", "fs", "attachments", "workingDirectory", "approval"];
18
21
  export class DshWorkspace {
19
22
  ctx;
@@ -52,6 +55,7 @@ export class DshWorkspace {
52
55
  this.#experts.set(d.agentId, runtime);
53
56
  scope.effect(() => () => { if (this.#experts.get(d.agentId) === runtime)
54
57
  this.#experts.delete(d.agentId); });
58
+ registerDshSkills(scope, child);
55
59
  await dshResources(scope, child, root, "general-purpose", () => python.status, runtime.check);
56
60
  const teamTool = dshExpertTools(scope, child, d.options);
57
61
  if (teamTool)
@@ -77,6 +81,7 @@ export class DshWorkspace {
77
81
  async open() {
78
82
  this.check();
79
83
  this.#scope = await this.agent.ctx.plugin({ name: "pbot-coordinator", inject: dshDependencies, apply: async (ctx) => {
84
+ registerDshSkills(ctx, this.agent);
80
85
  await dshResources(ctx, this.agent, this.root, "coordinator", () => this.python.status, this.check);
81
86
  registerDshFileEvents(ctx, this.agent, this.root, false, this.check);
82
87
  registerDshAgentTool(ctx, this.agent, this.agents, this.owner, this.check);
@@ -152,6 +157,8 @@ export class DshWorkspaces {
152
157
  ctx;
153
158
  python = new PythonEnvironmentService(process.cwd());
154
159
  #parents = new Map();
160
+ #initializing = new Map();
161
+ #configurations = new Map();
155
162
  #parentEffects = new Map();
156
163
  #histories = new Map();
157
164
  #lifetime = new AbortController();
@@ -202,6 +209,20 @@ export class DshWorkspaces {
202
209
  const opening = (async () => {
203
210
  const root = await realpath(agent.session.header.cwd ?? process.cwd());
204
211
  this.#lifetime.signal.throwIfAborted();
212
+ const configuration = await readWorkspaceConfig(root);
213
+ this.#lifetime.signal.throwIfAborted();
214
+ this.#configurations.set(agent, { root, configuration });
215
+ if (!configuration.enabled) {
216
+ if (configuration.error)
217
+ this.ctx.logger.warn(configuration.error);
218
+ const dispose = agent.ctx.effect(() => () => {
219
+ this.#parents.delete(agent);
220
+ this.#configurations.delete(agent);
221
+ this.#parentEffects.delete(agent);
222
+ });
223
+ this.#parentEffects.set(agent, dispose);
224
+ return undefined;
225
+ }
205
226
  const history = await this.history(root);
206
227
  let workspace;
207
228
  try {
@@ -209,12 +230,14 @@ export class DshWorkspaces {
209
230
  const owned = workspace;
210
231
  const dispose = agent.ctx.effect(() => async () => {
211
232
  this.#parents.delete(agent);
233
+ this.#configurations.delete(agent);
212
234
  this.#parentEffects.delete(agent);
213
235
  await owned.close();
214
236
  });
215
237
  this.#parentEffects.set(agent, dispose);
216
238
  await workspace.open();
217
239
  this.#lifetime.signal.throwIfAborted();
240
+ void this.preparePython().catch(error => this.ctx.logger.warn(String(error)));
218
241
  return workspace;
219
242
  }
220
243
  catch (error) {
@@ -246,9 +269,51 @@ export class DshWorkspaces {
246
269
  this.#lifetime.signal.throwIfAborted();
247
270
  requireThat(this.ctx.agents.get(agent.id) === agent, "父会话已关闭。");
248
271
  const workspace = await this.open(agent);
272
+ const configuration = this.#configurations.get(agent)?.configuration;
273
+ requireThat(workspace, configuration ? workspaceDisabledMessage(configuration) : "pbot 工作区实例不可用。");
249
274
  workspace.check();
250
275
  return workspace;
251
276
  }
277
+ async configuration(agent) {
278
+ this.#lifetime.signal.throwIfAborted();
279
+ requireThat(this.ctx.agents.get(agent.id) === agent, "父会话已关闭。");
280
+ await this.open(agent);
281
+ this.#lifetime.signal.throwIfAborted();
282
+ const current = this.#configurations.get(agent);
283
+ requireThat(current && this.ctx.agents.get(agent.id) === agent, "父会话已关闭。");
284
+ return current;
285
+ }
286
+ async initialize(agent) {
287
+ const { root } = await this.configuration(agent);
288
+ let pending = this.#initializing.get(root);
289
+ if (!pending) {
290
+ pending = (async () => {
291
+ await initializeWorkspace(root);
292
+ this.#lifetime.signal.throwIfAborted();
293
+ const configuration = await readWorkspaceConfig(root);
294
+ for (const [parent, current] of [...this.#configurations]) {
295
+ if (current.root !== root || this.ctx.agents.get(parent.id) !== parent)
296
+ continue;
297
+ const workspace = await this.#parents.get(parent);
298
+ if (workspace) {
299
+ this.#configurations.set(parent, { root, configuration });
300
+ continue;
301
+ }
302
+ await this.#parentEffects.get(parent)?.();
303
+ await this.open(parent);
304
+ }
305
+ })();
306
+ this.#initializing.set(root, pending);
307
+ }
308
+ try {
309
+ await pending;
310
+ }
311
+ finally {
312
+ if (this.#initializing.get(root) === pending)
313
+ this.#initializing.delete(root);
314
+ }
315
+ return this.get(agent);
316
+ }
252
317
  preparePython() {
253
318
  this.#lifetime.signal.throwIfAborted();
254
319
  if (this.#preparing)
@@ -268,8 +333,9 @@ export class DshWorkspaces {
268
333
  this.#lifetime.abort();
269
334
  this.#closing = (async () => {
270
335
  try {
336
+ await Promise.allSettled(this.#initializing.values());
271
337
  const opened = await Promise.allSettled(this.#parents.values());
272
- const closed = await Promise.allSettled(opened.filter(v => v.status === "fulfilled").map(v => v.value.close()));
338
+ const closed = await Promise.allSettled(opened.flatMap(value => value.status === "fulfilled" && value.value ? [value.value.close()] : []));
273
339
  const effects = await Promise.allSettled([...this.#parentEffects.values()].map(dispose => dispose()));
274
340
  const histories = await Promise.allSettled([...this.#histories.values()].map(value => value.work.close()));
275
341
  await this.python.close();
@@ -279,6 +345,7 @@ export class DshWorkspaces {
279
345
  }
280
346
  finally {
281
347
  this.#parents.clear();
348
+ this.#configurations.clear();
282
349
  this.#parentEffects.clear();
283
350
  this.#histories.clear();
284
351
  this.#restorePython?.();
@@ -115,7 +115,7 @@ export async function main(args) {
115
115
  if (!result.pi)
116
116
  return 1;
117
117
  ui.text("\n软件安装阶段结束。初始化、Python 环境和外部服务的状态由 Pi/pbot/插件分别报告。");
118
- if (await ui.confirm(failed ? "部分插件失败,是否仍先启动 Pi" : "立即启动 Pi", !failed)) {
118
+ if (await ui.confirm(failed ? "部分插件失败,是否仍启动 Pi 并初始化启用工作区" : "启动 Pi 并初始化启用工作区", !failed)) {
119
119
  process.stdin.pause();
120
120
  await host.start(result.pi, workspace);
121
121
  }
@@ -0,0 +1,4 @@
1
+ import type { Context } from "@deepseek-ai/cordis";
2
+ import type { Agent } from "@deepseek-ai/dsh-agent";
3
+ /** Keep bundled methods local to this Agent without overriding its native project/user methods. */
4
+ export declare function registerDshSkills(ctx: Context, agent: Agent): void;
@@ -0,0 +1,27 @@
1
+ import { join } from "node:path";
2
+ import { scopeParentOf } from "@deepseek-ai/dsh-scope";
3
+ import { FileSystemSkillProvider } from "@deepseek-ai/dsh-skill-filesystem";
4
+ import { resourceRoot } from "../../common/paths.js";
5
+ /** Keep bundled methods local to this Agent without overriding its native project/user methods. */
6
+ export function registerDshSkills(ctx, agent) {
7
+ let filesystem;
8
+ ctx.skills.registerProvider(control => {
9
+ filesystem = new FileSystemSkillProvider(ctx, control, {
10
+ providerName: "pbot", includeDefaultRoots: false, bundledSkillDir: join(resourceRoot, "skills"),
11
+ });
12
+ return {
13
+ name: "pbot",
14
+ async list(options) {
15
+ const inherited = await ctx.skills.snapshot({ ...options, scope: scopeParentOf(agent) });
16
+ const preferred = new Set(inherited.skills.filter(skill => skill.provider !== "pbot").map(skill => skill.name));
17
+ const listed = await filesystem.list(options);
18
+ const candidates = Array.isArray(listed) ? listed : listed.candidates;
19
+ return { candidates: candidates.filter(skill => !preferred.has(skill.name)),
20
+ complete: inherited.complete && (Array.isArray(listed) || listed.complete) };
21
+ },
22
+ get: (candidate, options) => filesystem.get(candidate, options),
23
+ };
24
+ });
25
+ ctx.effect(() => () => filesystem.dispose());
26
+ ctx.on("fs/observed", target => { filesystem.observeHostMutation(target.displayPath); });
27
+ }
@@ -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,9 @@
1
+ export interface WorkspaceConfig {
2
+ enabled?: boolean;
3
+ }
4
+ export interface WorkspaceConfiguration {
5
+ path: string;
6
+ present: boolean;
7
+ enabled: boolean;
8
+ error?: string;
9
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -18,9 +18,8 @@ export declare class HistoryService {
18
18
  readonly root: string;
19
19
  private readonly files;
20
20
  private readonly source;
21
- private previousFiles?;
22
21
  private readonly otherFiles?;
23
- constructor(root: string, files: HistoryFiles, source: HistorySource, previousFiles?: HistoryFiles | undefined, otherFiles?: HistoryFiles | undefined);
22
+ constructor(root: string, files: HistoryFiles, source: HistorySource, otherFiles?: HistoryFiles | undefined);
24
23
  check(): void;
25
24
  get warnings(): string[];
26
25
  get status(): {
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { requireThat } from "../../common/validation.js";
3
3
  /** Shared prefixes reuse nodes; only consecutive exchanges on one native branch can merge. */
4
- export function indexHistory(session, previous, host = "pi") {
4
+ export function indexHistory(session, previous, host = previous?.host ?? "pi") {
5
5
  const id = previous?.id ?? (host === "dsh" ? "dsh-" : "") + randomUUID(), nodes = new Map();
6
6
  const old = new Map(previous?.nodes.map(node => [node.id, node])), changed = new Set();
7
7
  const add = (value, level, children = []) => {
@@ -40,7 +40,7 @@ export function indexHistory(session, previous, host = "pi") {
40
40
  for (const node of nodes.values())
41
41
  for (const child of node.children)
42
42
  nodes.get(child).parents.push(node.id);
43
- return { id, sessionId: session.sessionId, name: session.name, nodes: [...nodes.values()] };
43
+ return { host, id, sessionId: session.sessionId, name: session.name, nodes: [...nodes.values()] };
44
44
  }
45
45
  const page = (values, offset, limit) => ({ values: values.slice(offset, offset + limit),
46
46
  nextOffset: offset + limit < values.length ? offset + limit : null });
@@ -66,7 +66,6 @@ export class HistoryService {
66
66
  root;
67
67
  files;
68
68
  source;
69
- previousFiles;
70
69
  otherFiles;
71
70
  #records = [];
72
71
  #closed = false;
@@ -75,17 +74,18 @@ export class HistoryService {
75
74
  #work;
76
75
  #sourceWarnings = [];
77
76
  #summaryError;
78
- constructor(root, files, source, previousFiles, otherFiles) {
77
+ constructor(root, files, source, otherFiles) {
79
78
  this.root = root;
80
79
  this.files = files;
81
80
  this.source = source;
82
- this.previousFiles = previousFiles;
83
81
  this.otherFiles = otherFiles;
84
82
  requireThat(source.host === files.host && (!otherFiles || otherFiles.host !== files.host), "历史来源与归档目录不匹配。");
85
- requireThat(files.host === "pi" || !previousFiles, "dsh 不导入 Pi 旧归档。");
86
83
  }
87
84
  check() { requireThat(!this.#closed, "历史服务已关闭。"); }
88
- get warnings() { return [...this.#sourceWarnings, ...(this.#summaryError ? [this.files.host + " 摘要:" + this.#summaryError] : [])]; }
85
+ get warnings() {
86
+ return [...new Set([...this.#sourceWarnings, ...this.files.warnings, ...(this.otherFiles?.warnings ?? []),
87
+ ...(this.#summaryError ? [this.files.host + " 摘要:" + this.#summaryError] : [])])];
88
+ }
89
89
  get status() {
90
90
  return { loaded: this.#loaded, organizing: Boolean(this.#work), pending: this.#loaded ? this.#records.filter(value => value.host === this.files.host)
91
91
  .reduce((total, value) => total + value.record.nodes.filter(node => node.kind === "summary" && !node.ready).length, 0) : null, warnings: this.warnings };
@@ -105,18 +105,13 @@ export class HistoryService {
105
105
  const warning = (host, error) => { this.check(); signal?.throwIfAborted(); warnings.push(host + ": " + String(error)); };
106
106
  let known = [];
107
107
  try {
108
- const before = this.files.list();
109
- known = before;
110
- let imports = [], importedPrevious = !this.previousFiles;
111
- if (this.previousFiles) {
112
- try {
113
- imports = this.previousFiles.list(this.root);
114
- importedPrevious = true;
115
- }
116
- catch (error) {
117
- warning("pi", error);
118
- }
108
+ try {
109
+ this.files.migrate(this.root);
110
+ }
111
+ finally {
112
+ known = this.files.list();
119
113
  }
114
+ const before = known;
120
115
  let sessions = [];
121
116
  try {
122
117
  const scanned = await this.source.scan(location, signal);
@@ -128,32 +123,20 @@ export class HistoryService {
128
123
  }
129
124
  this.check();
130
125
  signal?.throwIfAborted();
131
- if (sessions.length || imports.length || (this.files.host === "pi" && before.some(value => "file" in value))) {
126
+ if (sessions.length) {
132
127
  known = this.files.control(() => {
133
128
  this.check();
134
129
  signal?.throwIfAborted();
135
- const latest = this.files.list(), current = new Map(latest.map(record => [record.sessionId, record])), imported = new Set();
136
- if (this.files.host === "pi") {
137
- for (const value of latest)
138
- if ("file" in value) {
139
- const { id, sessionId, name, nodes } = value, clean = { id, sessionId, name, nodes };
140
- this.files.save(clean);
141
- current.set(sessionId, clean);
142
- }
143
- for (const { id, sessionId, name, nodes } of imports)
144
- if (!current.has(sessionId)) {
145
- const value = { id, sessionId, name, nodes };
146
- this.files.save(value);
147
- current.set(sessionId, value);
148
- imported.add(sessionId);
149
- }
150
- }
130
+ const latest = this.files.list(), current = new Map(latest.map(record => [record.sessionId, record]));
151
131
  for (const session of sessions) {
152
132
  const previous = current.get(session.sessionId), captured = before.find(record => record.sessionId === session.sessionId);
133
+ // An unreadable archive may own this session; avoid assigning it a second root ID.
134
+ if (!previous && this.files.warnings.length)
135
+ continue;
153
136
  // A concurrent source update must not be overwritten by this older scan. Summary-only updates remain reusable.
154
137
  if (captured && rawSource(captured) !== rawSource(previous))
155
138
  continue;
156
- if (!captured && previous && !imported.has(session.sessionId))
139
+ if (!captured && previous)
157
140
  continue;
158
141
  const value = indexHistory(session, previous, this.files.host);
159
142
  if (JSON.stringify(previous) !== JSON.stringify(value))
@@ -163,16 +146,20 @@ export class HistoryService {
163
146
  return [...current.values()];
164
147
  });
165
148
  }
166
- if (importedPrevious)
167
- this.previousFiles = undefined;
168
149
  }
169
150
  catch (error) {
170
151
  warning(this.files.host, error);
171
152
  }
172
- records.push(...known.map(record => ({ host: this.files.host, record })));
153
+ records.push(...known.map(record => ({ host: record.host, record })));
173
154
  if (this.otherFiles) {
174
155
  try {
175
- records.push(...this.otherFiles.list().map(record => ({ host: this.otherFiles.host, record })));
156
+ this.otherFiles.migrate(this.root);
157
+ }
158
+ catch (error) {
159
+ warning(this.otherFiles.host, error);
160
+ }
161
+ try {
162
+ records.push(...this.otherFiles.list().map(record => ({ host: record.host, record })));
176
163
  }
177
164
  catch (error) {
178
165
  warning(this.otherFiles.host, error);
@@ -0,0 +1,7 @@
1
+ import type { WorkspaceConfiguration } from "../../model/workspace-config.js";
2
+ export declare function workspaceConfigPath(root: string): string;
3
+ /** Read only this workspace's configuration; parent directories never grant activation. */
4
+ export declare function readWorkspaceConfig(root: string): Promise<WorkspaceConfiguration>;
5
+ /** Explicit init opts in this workspace while preserving its other configuration fields. */
6
+ export declare function initializeWorkspaceConfig(root: string): Promise<void>;
7
+ export declare function workspaceDisabledMessage(configuration: WorkspaceConfiguration): string;
@@ -0,0 +1,50 @@
1
+ import { mkdir, readFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { object } from "../../common/json.js";
4
+ import { readJsonFile, writeJsonFile } from "../../storage/atomic-json.js";
5
+ function config(value) {
6
+ if (!object(value) || value.enabled !== undefined && typeof value.enabled !== "boolean") {
7
+ throw new Error("配置必须是 JSON 对象,enabled 必须是布尔值。");
8
+ }
9
+ return value;
10
+ }
11
+ export function workspaceConfigPath(root) {
12
+ return join(root, ".pbot", "config.json");
13
+ }
14
+ /** Read only this workspace's configuration; parent directories never grant activation. */
15
+ export async function readWorkspaceConfig(root) {
16
+ const path = workspaceConfigPath(root);
17
+ let content;
18
+ try {
19
+ content = await readFile(path, "utf8");
20
+ }
21
+ catch (error) {
22
+ if (error.code === "ENOENT")
23
+ return { path, present: false, enabled: false };
24
+ return { path, present: true, enabled: false, error: `无法读取工作区配置 ${path}: ${String(error)}` };
25
+ }
26
+ try {
27
+ const value = config(JSON.parse(content));
28
+ return { path, present: true, enabled: value.enabled === true };
29
+ }
30
+ catch (error) {
31
+ return { path, present: true, enabled: false, error: `工作区配置无效 ${path}: ${String(error)}` };
32
+ }
33
+ }
34
+ /** Explicit init opts in this workspace while preserving its other configuration fields. */
35
+ export async function initializeWorkspaceConfig(root) {
36
+ const current = await readWorkspaceConfig(root);
37
+ if (current.error)
38
+ throw new Error(current.error);
39
+ if (current.enabled)
40
+ return;
41
+ const directory = dirname(current.path);
42
+ await mkdir(directory, { recursive: true });
43
+ const value = current.present ? readJsonFile(directory, "config.json", config) : {};
44
+ const written = writeJsonFile(directory, "config.json", { ...value, enabled: true }, config, !current.present);
45
+ if (!written)
46
+ await initializeWorkspaceConfig(root);
47
+ }
48
+ export function workspaceDisabledMessage(configuration) {
49
+ return configuration.error ?? `Personal Bot 未在此工作区启用。请执行 /pbot init 初始化并启用;配置文件:${configuration.path}`;
50
+ }
@@ -1,2 +1,3 @@
1
+ export declare function initializeWorkspace(root: string): Promise<void>;
1
2
  export declare const directories: string[];
2
3
  export declare function prepareWorkspace(root: string): Promise<void>;
@@ -2,6 +2,14 @@ import { lstat, mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { requireThat } from "../../common/validation.js";
4
4
  import { resourceRoot } from "../../common/paths.js";
5
+ import { initializeWorkspaceConfig, readWorkspaceConfig } from "./workspace-config.js";
6
+ export async function initializeWorkspace(root) {
7
+ const configuration = await readWorkspaceConfig(root);
8
+ if (configuration.error)
9
+ throw new Error(configuration.error);
10
+ await prepareWorkspace(root);
11
+ await initializeWorkspaceConfig(root);
12
+ }
5
13
  export const directories = ["areas", "projects", "knowledge", "providers", "scratch", "agents"];
6
14
  export async function prepareWorkspace(root) {
7
15
  for (const part of directories) {
@@ -1,9 +1,13 @@
1
1
  import type { HistoryHost, HistoryRecord } from "../model/history.js";
2
2
  export declare class HistoryFiles {
3
+ #private;
3
4
  readonly directory: string;
4
5
  readonly host: HistoryHost;
5
6
  constructor(directory: string, host?: HistoryHost);
6
- list(workspace?: string): HistoryRecord[];
7
+ get warnings(): readonly string[];
8
+ list(): HistoryRecord[];
9
+ /** Publish each legacy archive with its directory's host before removing the old copy. */
10
+ migrate(root: string): void;
7
11
  /** The callback is synchronous: no SDK reads or model work can hold this source's write lock. */
8
12
  control<T>(operation: () => T): T;
9
13
  save(value: HistoryRecord): void;