personal-bot 0.2.9 → 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 (58) hide show
  1. package/README.md +22 -11
  2. package/README.zh-CN.md +20 -9
  3. package/docs/README.md +3 -2
  4. package/docs/agent-collaboration.md +1 -1
  5. package/docs/dsh.md +4 -6
  6. package/docs/file-storage.md +7 -4
  7. package/docs/history.md +2 -9
  8. package/docs/installation-design.md +4 -1
  9. package/docs/model-context.md +4 -4
  10. package/docs/technical-design.md +11 -9
  11. package/docs/workspace-configuration.md +55 -0
  12. package/lib/client.js +28 -3
  13. package/lib/dsh.js +0 -5
  14. package/lib/extension/dsh/command.js +14 -10
  15. package/lib/extension/dsh/history.js +1 -1
  16. package/lib/extension/dsh/management.d.ts +4 -0
  17. package/lib/extension/dsh/management.js +20 -5
  18. package/lib/extension/dsh/workspace.d.ts +7 -1
  19. package/lib/extension/dsh/workspace.js +68 -1
  20. package/lib/extension/setup-cli.js +1 -1
  21. package/lib/integrations/dsh/skills.d.ts +4 -0
  22. package/lib/integrations/dsh/skills.js +27 -0
  23. package/lib/model/history.d.ts +1 -0
  24. package/lib/model/workspace-config.d.ts +9 -0
  25. package/lib/model/workspace-config.js +1 -0
  26. package/lib/services/history/history-service.d.ts +1 -2
  27. package/lib/services/history/history-service.js +27 -40
  28. package/lib/services/workspace/workspace-config.d.ts +7 -0
  29. package/lib/services/workspace/workspace-config.js +50 -0
  30. package/lib/services/workspace/workspace-preparation.d.ts +1 -0
  31. package/lib/services/workspace/workspace-preparation.js +8 -0
  32. package/lib/storage/history-files.d.ts +5 -1
  33. package/lib/storage/history-files.js +68 -15
  34. package/lib/ui/dsh/management.js +27 -3
  35. package/npm-shrinkwrap.json +7 -3
  36. package/package.json +7 -4
  37. package/python/README.md +2 -2
  38. package/src/dsh.ts +0 -5
  39. package/src/extension/command.ts +25 -8
  40. package/src/extension/dsh/command.ts +13 -8
  41. package/src/extension/dsh/history.ts +2 -2
  42. package/src/extension/dsh/management.ts +21 -5
  43. package/src/extension/dsh/workspace.ts +62 -5
  44. package/src/extension/events.ts +18 -1
  45. package/src/extension/lifecycle.ts +9 -2
  46. package/src/extension/setup-cli.ts +1 -1
  47. package/src/extension/subagent-entry.ts +4 -1
  48. package/src/integrations/dsh/skills.ts +30 -0
  49. package/src/integrations/pi/skills.ts +52 -0
  50. package/src/model/history.ts +1 -0
  51. package/src/model/workspace-config.ts +10 -0
  52. package/src/pbot.ts +2 -3
  53. package/src/services/history/history-service.ts +15 -25
  54. package/src/services/workspace/workspace-config.ts +49 -0
  55. package/src/services/workspace/workspace-preparation.ts +8 -0
  56. package/src/storage/history-files.ts +53 -15
  57. package/src/ui/dsh/management.ts +20 -3
  58. package/docs/dsh.en.md +0 -339
@@ -1,12 +1,13 @@
1
- import { existsSync, mkdirSync, readdirSync, rmdirSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { object } from "../common/json.js";
1
+ import { existsSync, mkdirSync, readdirSync, rmdirSync, unlinkSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { canonical, object } from "../common/json.js";
4
4
  import { requireThat } from "../common/validation.js";
5
- import { readJsonFile, writeJsonFile } from "./atomic-json.js";
5
+ import { internalPath, readJsonFile, syncDirectory, writeJsonFile } from "./atomic-json.js";
6
6
  import { fileId } from "./file-access.js";
7
7
  const strings = (value) => Array.isArray(value) && value.every(item => typeof item === "string");
8
8
  function record(value) {
9
- requireThat(object(value) && typeof value.id === "string" && typeof value.sessionId === "string" &&
9
+ requireThat(object(value) && (value.host === "pi" || value.host === "dsh") &&
10
+ typeof value.id === "string" && typeof value.sessionId === "string" &&
10
11
  typeof value.name === "string" && Array.isArray(value.nodes), "历史索引无效。");
11
12
  for (const node of value.nodes)
12
13
  requireThat(object(node) && typeof node.id === "string" &&
@@ -21,25 +22,77 @@ function record(value) {
21
22
  export class HistoryFiles {
22
23
  directory;
23
24
  host;
25
+ #warnings = [];
24
26
  constructor(directory, host = "pi") {
25
27
  this.directory = directory;
26
28
  this.host = host;
27
29
  }
28
- list(workspace) {
30
+ get warnings() { return this.#warnings; }
31
+ list() {
32
+ this.#warnings = [];
29
33
  if (!existsSync(this.directory))
30
34
  return [];
31
- return readdirSync(this.directory).filter(name => name.endsWith(".json"))
32
- .map(name => readJsonFile(this.directory, name, value => {
33
- const result = record(value);
34
- requireThat(this.host !== "dsh" || result.id.startsWith("dsh-"), "dsh 历史根 ID 缺少来源前缀。");
35
- return result;
36
- }))
37
- .filter(value => workspace === undefined || ("workspace" in value && value.workspace === workspace));
35
+ const records = [];
36
+ for (const name of readdirSync(this.directory).filter(name => name.endsWith(".json"))) {
37
+ try {
38
+ const value = readJsonFile(this.directory, name, record);
39
+ requireThat(name === fileId(value.id) + ".json", "历史文件名与根 ID 不匹配。");
40
+ if (value.host === this.host)
41
+ records.push(value);
42
+ }
43
+ catch (error) {
44
+ this.#warnings.push(join(this.directory, name) + ": " + String(error));
45
+ }
46
+ }
47
+ return records;
48
+ }
49
+ /** Publish each legacy archive with its directory's host before removing the old copy. */
50
+ migrate(root) {
51
+ const directory = join(root, "." + this.host, "pbot/history");
52
+ if (!existsSync(directory))
53
+ return;
54
+ this.control(() => {
55
+ if (!existsSync(directory))
56
+ return;
57
+ const legacyLock = join(directory, ".write");
58
+ mkdirSync(legacyLock, { mode: 0o700 });
59
+ try {
60
+ for (const name of readdirSync(directory).filter(name => name.endsWith(".json"))) {
61
+ const source = internalPath(directory, name), target = internalPath(this.directory, name);
62
+ requireThat(source !== target, "新旧历史目录指向同一位置。");
63
+ const value = readJsonFile(directory, name, value => {
64
+ requireThat(object(value) && (value.host === undefined || value.host === this.host), "旧历史归档与来源目录不匹配。");
65
+ return record({ ...value, host: this.host });
66
+ });
67
+ requireThat(name === fileId(value.id) + ".json", "历史文件名与根 ID 不匹配。");
68
+ if (!writeJsonFile(this.directory, name, value, record, true)) {
69
+ requireThat(canonical(readJsonFile(this.directory, name, record)) === canonical(value), "历史归档迁移冲突,请核对新旧文件:" + name);
70
+ }
71
+ syncDirectory(this.directory);
72
+ unlinkSync(source);
73
+ syncDirectory(directory);
74
+ }
75
+ }
76
+ finally {
77
+ rmdirSync(legacyLock);
78
+ }
79
+ rmdirSync(directory);
80
+ const parent = dirname(directory);
81
+ syncDirectory(parent);
82
+ try {
83
+ rmdirSync(parent);
84
+ syncDirectory(dirname(parent));
85
+ }
86
+ catch (error) {
87
+ if (!["ENOTEMPTY", "EEXIST"].includes(error.code ?? ""))
88
+ throw error;
89
+ }
90
+ });
38
91
  }
39
92
  /** The callback is synchronous: no SDK reads or model work can hold this source's write lock. */
40
93
  control(operation) {
41
94
  mkdirSync(this.directory, { recursive: true, mode: 0o700 });
42
- const lock = join(this.directory, ".write");
95
+ const lock = join(this.directory, ".write-" + this.host);
43
96
  try {
44
97
  mkdirSync(lock, { mode: 0o700 });
45
98
  }
@@ -56,7 +109,7 @@ export class HistoryFiles {
56
109
  }
57
110
  }
58
111
  save(value) {
59
- requireThat(this.host !== "dsh" || value.id.startsWith("dsh-"), "dsh 历史根 ID 缺少来源前缀。");
112
+ requireThat(value.host === this.host, "历史归档与来源不匹配。");
60
113
  mkdirSync(this.directory, { recursive: true, mode: 0o700 });
61
114
  writeJsonFile(this.directory, fileId(value.id) + ".json", value, record);
62
115
  }
@@ -78,6 +78,8 @@ export function showManagement(ctx, parentSessionId) {
78
78
  }
79
79
  function form(parent) { const node = element("form"); parent.append(node); return node; }
80
80
  function activate(id) {
81
+ if (snapshot?.enabled === false && id !== "status")
82
+ return;
81
83
  tab = id;
82
84
  for (const [key, section] of sections)
83
85
  section.hidden = key !== id;
@@ -157,7 +159,7 @@ export function showManagement(ctx, parentSessionId) {
157
159
  counts.classList.add("pbot-counts");
158
160
  const materials = group(status, "pbot-setting");
159
161
  materials.append(element("h3", "资料目录"));
160
- button(materials, "准备目录", async () => { await command("init", undefined, "资料目录已准备。"); });
162
+ const initializeButton = button(materials, "准备目录", async () => { await command("init", undefined, "工作区已初始化并启用。"); });
161
163
  const pythonCard = group(status, "pbot-setting"), pythonInfo = group(pythonCard);
162
164
  const pythonButton = button(pythonCard, "准备 Python", async () => { await command("python setup", undefined, "Python 准备完成。"); });
163
165
  const diagnostic = details(status, "环境详情"), statusBody = block("");
@@ -625,13 +627,35 @@ export function showManagement(ctx, parentSessionId) {
625
627
  return;
626
628
  location.textContent = text(snapshot.workspaceRoot).split(/[\\/]/).filter(Boolean).at(-1) ?? "";
627
629
  location.title = text(snapshot.workspaceRoot);
630
+ if (snapshot.enabled === false) {
631
+ const configuration = record(snapshot.configuration);
632
+ counts.textContent = configuration.error ? "Personal Bot 配置错误" : "Personal Bot 未启用";
633
+ workspaceInfo.replaceChildren(facts([["状态", configuration.error ? "配置错误" : "未启用"], ["工作区", snapshot.workspaceRoot], ["配置文件", configuration.path]]), note(configuration.error ? "修复配置文件后执行 /pbot init。" : "执行 /pbot init,或点击下方按钮初始化并启用。"));
634
+ pythonInfo.replaceChildren(note("启用工作区后自动准备共享 Python 环境。"));
635
+ materials.hidden = false;
636
+ initializeButton.textContent = "初始化并启用";
637
+ enabled(initializeButton, !configuration.error);
638
+ for (const [id, control] of tabs)
639
+ enabled(control, id === "status");
640
+ enabled(pythonButton, false);
641
+ activate("status");
642
+ statusBody.textContent = JSON.stringify(snapshot, null, 2);
643
+ if (configuration.error)
644
+ inform(text(configuration.error), "error");
645
+ return;
646
+ }
647
+ for (const control of tabs.values())
648
+ enabled(control, true);
649
+ materials.hidden = false;
650
+ initializeButton.textContent = "准备目录";
651
+ enabled(initializeButton, true);
628
652
  const agents = rows(snapshot.agents), teams = rows(snapshot.teams), python = record(snapshot.python), history = record(snapshot.history);
629
653
  tabCounts.get("experts").textContent = text(agents.length);
630
654
  tabCounts.get("teams").textContent = text(teams.length);
631
655
  const active = agents.filter(agent => ["starting", "busy", "stopping"].includes(text(agent.state))).length;
632
- counts.textContent = `专家 ${agents.length} · 执行中 ${active} · 团队 ${teams.length}`;
656
+ counts.textContent = `Personal Bot 已启动 · 专家 ${agents.length} · 执行中 ${active} · 团队 ${teams.length}`;
633
657
  const model = object(snapshot.model) ? snapshot.model : {};
634
- workspaceInfo.replaceChildren(facts([["工作区", snapshot.workspaceRoot], ["模型", model.model || "尚未选择"], ["Provider", model.provider || "—"],
658
+ workspaceInfo.replaceChildren(facts([["状态", "已启动"], ["工作区", snapshot.workspaceRoot], ["配置文件", record(snapshot.configuration).path], ["模型", model.model || "尚未选择"], ["Provider", model.provider || "—"],
635
659
  ["历史", history.organizing ? "整理中" : history.pending === null ? "查询时更新索引" : `${history.pending} 项待整理`]]));
636
660
  pythonInfo.replaceChildren(element("h3", "Python"), badge(python.state));
637
661
  if (python.error)
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "personal-bot",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "personal-bot",
9
- "version": "0.2.9",
9
+ "version": "0.2.10",
10
10
  "dependencies": {
11
11
  "ajv": "8.17.1",
12
12
  "ajv-formats": "3.0.1",
@@ -29,6 +29,7 @@
29
29
  "@deepseek-ai/dsh-fs-patch": "0.2.2-rc.1",
30
30
  "@deepseek-ai/dsh-home-paths": "0.2.2-rc.1",
31
31
  "@deepseek-ai/dsh-llm": "0.2.2-rc.1",
32
+ "@deepseek-ai/dsh-scope": "0.2.2-rc.1",
32
33
  "@deepseek-ai/dsh-session": "0.2.2-rc.1",
33
34
  "@deepseek-ai/dsh-session-persistence": "0.2.2-rc.1",
34
35
  "@deepseek-ai/dsh-session-query": "0.2.2-rc.1",
@@ -68,6 +69,7 @@
68
69
  "@deepseek-ai/dsh-fs-patch": "*",
69
70
  "@deepseek-ai/dsh-home-paths": "*",
70
71
  "@deepseek-ai/dsh-llm": "*",
72
+ "@deepseek-ai/dsh-scope": "*",
71
73
  "@deepseek-ai/dsh-session": "*",
72
74
  "@deepseek-ai/dsh-session-persistence": "*",
73
75
  "@deepseek-ai/dsh-session-query": "*",
@@ -122,6 +124,9 @@
122
124
  "@deepseek-ai/dsh-llm": {
123
125
  "optional": true
124
126
  },
127
+ "@deepseek-ai/dsh-scope": {
128
+ "optional": true
129
+ },
125
130
  "@deepseek-ai/dsh-session": {
126
131
  "optional": true
127
132
  },
@@ -4687,7 +4692,6 @@
4687
4692
  "integrity": "sha512-FOJbXYvrskIcrb/4eVMMQfp0kaReTSBtIry2KsGNURb0OJfelqeSKaB1MsQQq/sOxbt3nPJIN9HY00SwmrTk3Q==",
4688
4693
  "dev": true,
4689
4694
  "license": "MIT",
4690
- "peer": true,
4691
4695
  "peerDependencies": {
4692
4696
  "@deepseek-ai/cordis": "~4.1.0"
4693
4697
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "personal-bot",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/realqiyan/personal-bot.git"
@@ -22,9 +22,7 @@
22
22
  "extensions": [
23
23
  "./src/pbot.ts"
24
24
  ],
25
- "skills": [
26
- "./resources/skills"
27
- ],
25
+ "skills": [],
28
26
  "prompts": [],
29
27
  "themes": []
30
28
  },
@@ -101,6 +99,7 @@
101
99
  "@deepseek-ai/dsh-tools": "*",
102
100
  "@deepseek-ai/dsh-home-paths": "*",
103
101
  "@deepseek-ai/dsh-system-prompt": "*",
102
+ "@deepseek-ai/dsh-scope": "*",
104
103
  "@deepseek-ai/dsh-skill": "*",
105
104
  "@deepseek-ai/dsh-skill-filesystem": "*",
106
105
  "@deepseek-ai/dsh-working-directory": "*",
@@ -159,6 +158,9 @@
159
158
  "@deepseek-ai/dsh-system-prompt": {
160
159
  "optional": true
161
160
  },
161
+ "@deepseek-ai/dsh-scope": {
162
+ "optional": true
163
+ },
162
164
  "@deepseek-ai/dsh-skill": {
163
165
  "optional": true
164
166
  },
@@ -223,6 +225,7 @@
223
225
  "@deepseek-ai/dsh-tools": "0.2.2-rc.1",
224
226
  "@deepseek-ai/dsh-home-paths": "0.2.2-rc.1",
225
227
  "@deepseek-ai/dsh-system-prompt": "0.2.2-rc.1",
228
+ "@deepseek-ai/dsh-scope": "0.2.2-rc.1",
226
229
  "@deepseek-ai/dsh-skill": "0.2.2-rc.1",
227
230
  "@deepseek-ai/dsh-skill-filesystem": "0.2.2-rc.1",
228
231
  "@deepseek-ai/dsh-working-directory": "0.2.2-rc.1",
package/python/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## 自动准备与激活
6
6
 
7
- Pi 加载 Personal Bot、原生 `/reload`,或 dsh 加载 pbot Host 组件时自动执行 setup,无需另行确认或指定路径。同一系统用户的两种宿主共用该环境:
7
+ 工作区的 `.pbot/config.json` 设置 `enabled: true` 后,Pi 加载或原生 `/reload` 时自动执行 setup;dsh 在首个已启用工作区加载时准备环境。无需另行确认或指定 Python 路径。同一系统用户的两种宿主共用该环境:
8
8
 
9
9
  1. 共享 venv 不存在时创建,不使用系统 site-packages。
10
10
  2. 按 `requirements.lock` 补齐或对齐依赖,允许升级或降级;已匹配时不重新安装。
@@ -18,7 +18,7 @@ Pi 加载 Personal Bot、原生 `/reload`,或 dsh 加载 pbot Host 组件时
18
18
 
19
19
  首次准备从当前 PATH 选择 python3,无该命令时使用 python;需要 Python 3.11+ 和 venv/ensurepip。系统 Python 只用于创建独立 venv,不在系统环境中安装依赖。现有环境不自动替换基础解释器,也不删除额外安装的包。
20
20
 
21
- 首次安装可能需要数分钟和网络。环境准备不依赖 `/pbot init` 或工作区资料;不需要 Personal Bot 时通过所属宿主的原生配置取消加载。`/pbot status` 显示环境状态。所属激活生命周期关闭时恢复本扩展修改的进程环境变量;dsh 按整个 pbot 组件共享激活,不随某个浏览器页面或单个工作区关闭而恢复,不覆盖期间其他代码对变量的新修改。外部终端和 shell 配置不变。
21
+ 首次安装可能需要数分钟和网络。`/pbot init` 创建共用启用配置并启动环境准备;手工设置 enabled 为 true 后重载也会准备环境。不需要 Personal Bot 的工作区可保留配置缺失或设置 enabled 为 false,再重载或重启宿主;也可通过宿主原生配置取消加载整个插件。`/pbot status` 显示环境状态。所属激活生命周期关闭时恢复本扩展修改的进程环境变量;dsh 按整个 pbot 组件共享激活,不随某个浏览器页面或单个工作区关闭而恢复,不覆盖期间其他代码对变量的新修改。外部终端和 shell 配置不变。
22
22
 
23
23
  ## 失败与手工重试
24
24
 
package/src/dsh.ts CHANGED
@@ -1,7 +1,4 @@
1
- import { join } from "node:path";
2
1
  import type { Context } from "@deepseek-ai/cordis";
3
- import * as SkillFilesystem from "@deepseek-ai/dsh-skill-filesystem";
4
- import { resourceRoot } from "./common/paths.js";
5
2
  import { registerDshCommand } from "./extension/dsh/command.js";
6
3
  import { registerDshManagement } from "./extension/dsh/management.js";
7
4
  import { DshWorkspaces, dshDependencies } from "./extension/dsh/workspace.js";
@@ -13,7 +10,6 @@ export const inject = ["commands", "typert", ...dshDependencies];
13
10
  export async function apply(ctx: Context) {
14
11
  const [major, minor] = process.versions.node.split(".").map(Number);
15
12
  requireThat(major! > 24 || (major === 24 && minor! >= 9), "dsh 接入需要 Node.js 24.9.0 或更新版本。");
16
- await ctx.plugin(SkillFilesystem, { providerName: "pbot", includeDefaultRoots: false, bundledSkillDir: join(resourceRoot, "skills") });
17
13
  const workspaces = new DshWorkspaces(ctx);
18
14
  ctx.effect(() => () => workspaces.close());
19
15
  const management = registerDshManagement(ctx, workspaces);
@@ -22,5 +18,4 @@ export async function apply(ctx: Context) {
22
18
  if (ctx.agents.roots().includes(agent) && agent.session.header.origin !== "subagent") await workspaces.open(agent);
23
19
  });
24
20
  for (const agent of ctx.agents.roots()) if (agent.session.header.origin !== "subagent") await workspaces.open(agent);
25
- void workspaces.preparePython().catch(error => ctx.logger.warn(String(error)));
26
21
  }
@@ -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
  }