personal-bot 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/CONTEXT.md +13 -7
  2. package/README.md +46 -9
  3. package/README.zh-CN.md +46 -9
  4. package/bin/pbot.mjs +3 -0
  5. package/docs/README.md +4 -1
  6. package/docs/agent-collaboration.md +26 -12
  7. package/docs/architecture-guidelines.md +2 -2
  8. package/docs/capability-design.md +6 -4
  9. package/docs/dsh-implementation-plan.md +158 -0
  10. package/docs/dsh-support-proposal.md +314 -0
  11. package/docs/dsh.md +103 -0
  12. package/docs/file-storage.md +14 -9
  13. package/docs/history.md +29 -18
  14. package/docs/installation-design.md +226 -0
  15. package/docs/interaction-design.md +13 -0
  16. package/docs/release.md +5 -5
  17. package/docs/technical-design.md +29 -15
  18. package/docs/validation.md +53 -3
  19. package/lib/client.js +1071 -0
  20. package/lib/common/files.d.ts +2 -0
  21. package/lib/common/files.js +44 -0
  22. package/lib/common/json.d.ts +6 -0
  23. package/lib/common/json.js +16 -0
  24. package/lib/common/markdown.d.ts +5 -0
  25. package/lib/common/markdown.js +26 -0
  26. package/lib/common/package-source.js +16 -0
  27. package/lib/common/paths.d.ts +1 -0
  28. package/lib/common/paths.js +2 -0
  29. package/lib/common/validation.d.ts +3 -0
  30. package/lib/common/validation.js +14 -0
  31. package/lib/common/version.d.ts +3 -0
  32. package/lib/common/version.js +13 -0
  33. package/lib/dsh.d.ts +4 -0
  34. package/lib/dsh.js +22 -0
  35. package/lib/extension/dsh/agent-events.d.ts +24 -0
  36. package/lib/extension/dsh/agent-events.js +242 -0
  37. package/lib/extension/dsh/agent-runtime.d.ts +49 -0
  38. package/lib/extension/dsh/agent-runtime.js +348 -0
  39. package/lib/extension/dsh/command.d.ts +4 -0
  40. package/lib/extension/dsh/command.js +83 -0
  41. package/lib/extension/dsh/files.d.ts +5 -0
  42. package/lib/extension/dsh/files.js +67 -0
  43. package/lib/extension/dsh/history.d.ts +15 -0
  44. package/lib/extension/dsh/history.js +88 -0
  45. package/lib/extension/dsh/management.d.ts +21 -0
  46. package/lib/extension/dsh/management.js +217 -0
  47. package/lib/extension/dsh/workspace.d.ts +48 -0
  48. package/lib/extension/dsh/workspace.js +290 -0
  49. package/lib/extension/setup-cli.js +136 -0
  50. package/lib/extension/setup-guard.js +11 -0
  51. package/lib/integrations/agent-resources.d.ts +1 -0
  52. package/lib/integrations/agent-resources.js +18 -0
  53. package/lib/integrations/dsh/dsh-agents.d.ts +30 -0
  54. package/lib/integrations/dsh/dsh-agents.js +104 -0
  55. package/lib/integrations/dsh/history-summary.d.ts +11 -0
  56. package/lib/integrations/dsh/history-summary.js +40 -0
  57. package/lib/integrations/dsh/history.d.ts +16 -0
  58. package/lib/integrations/dsh/history.js +156 -0
  59. package/lib/integrations/dsh/input.d.ts +21 -0
  60. package/lib/integrations/dsh/input.js +62 -0
  61. package/lib/integrations/dsh/messages.d.ts +36 -0
  62. package/lib/integrations/dsh/messages.js +11 -0
  63. package/lib/integrations/dsh/notifications.d.ts +8 -0
  64. package/lib/integrations/dsh/notifications.js +55 -0
  65. package/lib/integrations/dsh/resources.d.ts +10 -0
  66. package/lib/integrations/dsh/resources.js +41 -0
  67. package/lib/integrations/dsh/schema.d.ts +7 -0
  68. package/lib/integrations/dsh/schema.js +28 -0
  69. package/lib/integrations/dsh/session.d.ts +14 -0
  70. package/lib/integrations/dsh/session.js +59 -0
  71. package/lib/integrations/history-summary.d.ts +2 -0
  72. package/lib/integrations/history-summary.js +21 -0
  73. package/lib/integrations/installation/pi-bridge.js +131 -0
  74. package/lib/integrations/installation/pi-setup.js +193 -0
  75. package/lib/integrations/installation/processes.js +155 -0
  76. package/lib/integrations/installation/workspace.js +21 -0
  77. package/lib/integrations/process.d.ts +14 -0
  78. package/lib/integrations/process.js +48 -0
  79. package/lib/integrations/python-activation.d.ts +2 -0
  80. package/lib/integrations/python-activation.js +29 -0
  81. package/lib/integrations/team-context.d.ts +18 -0
  82. package/lib/integrations/team-context.js +57 -0
  83. package/lib/model/agent.d.ts +92 -0
  84. package/lib/model/agent.js +1 -0
  85. package/lib/model/history.d.ts +43 -0
  86. package/lib/model/history.js +1 -0
  87. package/lib/model/team.d.ts +85 -0
  88. package/lib/model/team.js +1 -0
  89. package/lib/services/agents/agent-notices.d.ts +13 -0
  90. package/lib/services/agents/agent-notices.js +25 -0
  91. package/lib/services/agents/agent-run.d.ts +39 -0
  92. package/lib/services/agents/agent-run.js +138 -0
  93. package/lib/services/agents/agent-service.d.ts +166 -0
  94. package/lib/services/agents/agent-service.js +325 -0
  95. package/lib/services/agents/team-service.d.ts +168 -0
  96. package/lib/services/agents/team-service.js +343 -0
  97. package/lib/services/history/history-service.d.ts +103 -0
  98. package/lib/services/history/history-service.js +269 -0
  99. package/lib/services/installation/setup-service.js +121 -0
  100. package/lib/services/workspace/python-environment-service.d.ts +15 -0
  101. package/lib/services/workspace/python-environment-service.js +50 -0
  102. package/lib/services/workspace/workspace-files.d.ts +2 -0
  103. package/lib/services/workspace/workspace-files.js +17 -0
  104. package/lib/services/workspace/workspace-preparation.d.ts +2 -0
  105. package/lib/services/workspace/workspace-preparation.js +18 -0
  106. package/lib/storage/agent-files.d.ts +30 -0
  107. package/lib/storage/agent-files.js +166 -0
  108. package/lib/storage/atomic-json.d.ts +11 -0
  109. package/lib/storage/atomic-json.js +101 -0
  110. package/lib/storage/file-access.d.ts +2 -0
  111. package/lib/storage/file-access.js +26 -0
  112. package/lib/storage/history-files.d.ts +10 -0
  113. package/lib/storage/history-files.js +63 -0
  114. package/lib/storage/team-files.d.ts +16 -0
  115. package/lib/storage/team-files.js +111 -0
  116. package/lib/tools/agents/actions.d.ts +52 -0
  117. package/lib/tools/agents/actions.js +49 -0
  118. package/lib/tools/agents/contract.d.ts +163 -0
  119. package/lib/tools/agents/contract.js +56 -0
  120. package/lib/tools/agents/dsh-contract.d.ts +52 -0
  121. package/lib/tools/agents/dsh-contract.js +15 -0
  122. package/lib/tools/agents/dsh-tool.d.ts +5 -0
  123. package/lib/tools/agents/dsh-tool.js +38 -0
  124. package/lib/tools/history/actions.d.ts +70 -0
  125. package/lib/tools/history/actions.js +7 -0
  126. package/lib/tools/history/contract.d.ts +81 -0
  127. package/lib/tools/history/contract.js +32 -0
  128. package/lib/tools/history/dsh-tool.d.ts +4 -0
  129. package/lib/tools/history/dsh-tool.js +22 -0
  130. package/lib/tools/result.d.ts +18 -0
  131. package/lib/tools/result.js +10 -0
  132. package/lib/tools/teams/actions.d.ts +24 -0
  133. package/lib/tools/teams/actions.js +38 -0
  134. package/lib/tools/teams/contract.d.ts +232 -0
  135. package/lib/tools/teams/contract.js +67 -0
  136. package/lib/tools/teams/dsh-tool.d.ts +8 -0
  137. package/lib/tools/teams/dsh-tool.js +49 -0
  138. package/lib/ui/dsh/attachments.d.ts +5 -0
  139. package/lib/ui/dsh/attachments.js +39 -0
  140. package/lib/ui/dsh/elements.d.ts +17 -0
  141. package/lib/ui/dsh/elements.js +79 -0
  142. package/lib/ui/dsh/index.d.ts +4 -0
  143. package/lib/ui/dsh/index.js +13 -0
  144. package/lib/ui/dsh/management.d.ts +4 -0
  145. package/lib/ui/dsh/management.js +711 -0
  146. package/lib/ui/dsh/remote.d.ts +15 -0
  147. package/lib/ui/dsh/remote.js +11 -0
  148. package/lib/ui/dsh/styles.d.ts +1 -0
  149. package/lib/ui/dsh/styles.js +132 -0
  150. package/lib/ui/presentation.d.ts +3 -0
  151. package/lib/ui/presentation.js +12 -0
  152. package/lib/ui/setup.js +52 -0
  153. package/npm-shrinkwrap.json +11118 -783
  154. package/package.json +113 -9
  155. package/python/README.md +6 -6
  156. package/resources/agents/coordinator.md +1 -1
  157. package/resources/dsh/cordis.patch.yml +3 -0
  158. package/resources/installer/plugins.json +16 -0
  159. package/resources/prompts/workspace.md +1 -1
  160. package/resources/references/compute.md +1 -1
  161. package/resources/references/task-conductor.md +6 -4
  162. package/resources/skills/provider-connections/SKILL.md +10 -0
  163. package/scripts/README.md +13 -1
  164. package/src/common/package-source.ts +16 -0
  165. package/src/dsh.ts +22 -0
  166. package/src/extension/agent-events.ts +51 -46
  167. package/src/extension/agents-command.ts +48 -34
  168. package/src/extension/command.ts +42 -23
  169. package/src/extension/dsh/agent-events.ts +148 -0
  170. package/src/extension/dsh/agent-runtime.ts +251 -0
  171. package/src/extension/dsh/command.ts +78 -0
  172. package/src/extension/dsh/files.ts +61 -0
  173. package/src/extension/dsh/history.ts +69 -0
  174. package/src/extension/dsh/management.ts +108 -0
  175. package/src/extension/dsh/workspace.ts +206 -0
  176. package/src/extension/events.ts +6 -4
  177. package/src/extension/history.ts +15 -12
  178. package/src/extension/setup-cli.ts +109 -0
  179. package/src/extension/setup-guard.ts +12 -0
  180. package/src/extension/subagent-entry.ts +34 -63
  181. package/src/extension/teams-command.ts +53 -46
  182. package/src/integrations/{pi/agent-resources.ts → agent-resources.ts} +2 -2
  183. package/src/integrations/dsh/dsh-agents.ts +97 -0
  184. package/src/integrations/dsh/history-summary.ts +43 -0
  185. package/src/integrations/dsh/history.ts +76 -0
  186. package/src/integrations/dsh/input.ts +66 -0
  187. package/src/integrations/dsh/messages.ts +22 -0
  188. package/src/integrations/dsh/notifications.ts +46 -0
  189. package/src/integrations/dsh/resources.ts +44 -0
  190. package/src/integrations/dsh/schema.ts +26 -0
  191. package/src/integrations/dsh/session.ts +67 -0
  192. package/src/integrations/history-summary.ts +20 -0
  193. package/src/integrations/installation/pi-bridge.ts +107 -0
  194. package/src/integrations/installation/pi-setup.ts +162 -0
  195. package/src/integrations/installation/processes.ts +124 -0
  196. package/src/integrations/installation/workspace.ts +18 -0
  197. package/src/integrations/pi/history.ts +4 -14
  198. package/src/integrations/pi/pi-agents.ts +17 -6
  199. package/src/integrations/pi/team-context.ts +6 -58
  200. package/src/integrations/team-context.ts +60 -0
  201. package/src/model/agent.ts +2 -0
  202. package/src/model/history.ts +3 -1
  203. package/src/pbot.ts +1 -1
  204. package/src/services/agents/agent-notices.ts +28 -0
  205. package/src/services/agents/agent-run.ts +114 -0
  206. package/src/services/agents/agent-service.ts +127 -64
  207. package/src/services/history/history-service.ts +120 -59
  208. package/src/services/installation/setup-service.ts +145 -0
  209. package/src/storage/agent-files.ts +17 -4
  210. package/src/storage/history-files.ts +30 -5
  211. package/src/tools/agents/actions.ts +47 -0
  212. package/src/tools/agents/contract.ts +8 -5
  213. package/src/tools/agents/dsh-contract.ts +16 -0
  214. package/src/tools/agents/dsh-tool.ts +41 -0
  215. package/src/tools/agents/tool.ts +7 -43
  216. package/src/tools/history/actions.ts +9 -0
  217. package/src/tools/history/contract.ts +34 -0
  218. package/src/tools/history/dsh-tool.ts +22 -0
  219. package/src/tools/history/tool.ts +7 -30
  220. package/src/tools/teams/actions.ts +38 -0
  221. package/src/tools/teams/contract.ts +7 -3
  222. package/src/tools/teams/dsh-tool.ts +55 -0
  223. package/src/tools/teams/tool.ts +6 -31
  224. package/src/ui/agent-display.ts +7 -4
  225. package/src/ui/dsh/attachments.ts +42 -0
  226. package/src/ui/dsh/elements.ts +50 -0
  227. package/src/ui/dsh/index.ts +18 -0
  228. package/src/ui/dsh/management.ts +412 -0
  229. package/src/ui/dsh/remote.ts +20 -0
  230. package/src/ui/dsh/styles.ts +132 -0
  231. package/src/ui/pi/menu.ts +37 -0
  232. package/src/ui/pi/reader.ts +46 -0
  233. package/src/ui/presentation.ts +12 -0
  234. package/src/ui/setup.ts +40 -0
@@ -0,0 +1,269 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { requireThat } from "../../common/validation.js";
3
+ /** Shared prefixes reuse nodes; only consecutive exchanges on one native branch can merge. */
4
+ export function indexHistory(session, previous, host = "pi") {
5
+ const id = previous?.id ?? (host === "dsh" ? "dsh-" : "") + randomUUID(), nodes = new Map();
6
+ const old = new Map(previous?.nodes.map(node => [node.id, node])), changed = new Set();
7
+ const add = (value, level, children = []) => {
8
+ const key = `${id}:${level}:${value.entryIds.at(-1)}`, found = nodes.get(key);
9
+ if (found)
10
+ return found;
11
+ const before = old.get(key), same = before && JSON.stringify(before.children) === JSON.stringify(children) &&
12
+ (level ? children.every(child => !changed.has(child)) : before.text === value.text && before.completed === value.completed);
13
+ const node = { ...value, id: key, kind: level ? "summary" : "exchange", level, children, parents: [],
14
+ text: level ? same ? before.text : "" : value.text, ready: level ? Boolean(same && before.ready) : true };
15
+ if (!same)
16
+ changed.add(key);
17
+ nodes.set(key, node);
18
+ return node;
19
+ };
20
+ const roots = new Set();
21
+ for (const branch of session.branches) {
22
+ let layer = branch.map(exchange => add(exchange, 0)), level = 1;
23
+ while (layer.length >= 4) {
24
+ const count = Math.floor(layer.length / 4) * 4, next = [];
25
+ layer.slice(count).forEach(node => roots.add(node.id));
26
+ for (let i = 0; i < count; i += 4) {
27
+ const children = layer.slice(i, i + 4), first = children[0], last = children.at(-1);
28
+ next.push(add({ entryIds: [first.entryIds[0], last.entryIds.at(-1)], text: "", from: first.from, to: last.to,
29
+ completed: children.every(node => node.completed) }, level, children.map(node => node.id)));
30
+ }
31
+ layer = next;
32
+ level++;
33
+ }
34
+ layer.forEach(node => roots.add(node.id));
35
+ }
36
+ const dates = [...nodes.values()].flatMap(node => [node.from, node.to]).sort();
37
+ nodes.set(id, { id, kind: "session", level: -1, entryIds: [], text: session.name, ready: true, completed: [...nodes.values()].every(node => node.completed),
38
+ from: dates[0] ?? "", to: dates.at(-1) ?? "", parents: [],
39
+ children: [...roots].sort((a, b) => nodes.get(a).from.localeCompare(nodes.get(b).from)) });
40
+ for (const node of nodes.values())
41
+ for (const child of node.children)
42
+ nodes.get(child).parents.push(node.id);
43
+ return { id, sessionId: session.sessionId, name: session.name, nodes: [...nodes.values()] };
44
+ }
45
+ const page = (values, offset, limit) => ({ values: values.slice(offset, offset + limit),
46
+ nextOffset: offset + limit < values.length ? offset + limit : null });
47
+ const brief = ({ host, record }, node) => ({ host, id: node.id, kind: node.kind, sessionId: record.sessionId,
48
+ name: record.name, preview: node.text.replace(/\s+/g, " ").slice(0, 100), from: node.from, to: node.to, ready: node.ready, completed: node.completed });
49
+ const rawSource = (record) => record && JSON.stringify({ name: record.name,
50
+ exchanges: record.nodes.filter(node => node.kind === "exchange").map(node => [node.id, node.entryIds, node.text, node.from, node.to, node.completed]) });
51
+ const summarySource = (node) => [node.id, node.entryIds, node.children, node.text, node.from, node.to, node.completed, node.ready];
52
+ /** Validate the exact inputs again under the source lock; another writer's ready summary wins. */
53
+ export function completeHistorySummary(record, id, children, text) {
54
+ const node = record.nodes.find(value => value.id === id);
55
+ if (!node || node.kind !== "summary" || node.ready || JSON.stringify(node.children) !== JSON.stringify(children.map(child => child.id)))
56
+ return false;
57
+ const current = node.children.map(id => record.nodes.find(value => value.id === id));
58
+ if (current.some(value => !value) || JSON.stringify(current.map(value => summarySource(value))) !== JSON.stringify(children.map(summarySource)))
59
+ return false;
60
+ requireThat(text.trim(), "历史摘要为空。");
61
+ node.text = text.trim();
62
+ node.ready = true;
63
+ return true;
64
+ }
65
+ export class HistoryService {
66
+ root;
67
+ files;
68
+ source;
69
+ previousFiles;
70
+ otherFiles;
71
+ #records = [];
72
+ #closed = false;
73
+ #loaded = false;
74
+ #refresh;
75
+ #work;
76
+ #sourceWarnings = [];
77
+ #summaryError;
78
+ constructor(root, files, source, previousFiles, otherFiles) {
79
+ this.root = root;
80
+ this.files = files;
81
+ this.source = source;
82
+ this.previousFiles = previousFiles;
83
+ this.otherFiles = otherFiles;
84
+ requireThat(source.host === files.host && (!otherFiles || otherFiles.host !== files.host), "历史来源与归档目录不匹配。");
85
+ requireThat(files.host === "pi" || !previousFiles, "dsh 不导入 Pi 旧归档。");
86
+ }
87
+ check() { requireThat(!this.#closed, "历史服务已关闭。"); }
88
+ get warnings() { return [...this.#sourceWarnings, ...(this.#summaryError ? [this.files.host + " 摘要:" + this.#summaryError] : [])]; }
89
+ get status() {
90
+ return { loaded: this.#loaded, organizing: Boolean(this.#work), pending: this.#loaded ? this.#records.filter(value => value.host === this.files.host)
91
+ .reduce((total, value) => total + value.record.nodes.filter(node => node.kind === "summary" && !node.ready).length, 0) : null, warnings: this.warnings };
92
+ }
93
+ refresh(location, signal) {
94
+ this.check();
95
+ // Each caller keeps its own cancellation. A closed page cannot cancel a server-owned refresh.
96
+ const work = (this.#refresh ?? Promise.resolve()).catch(() => { }).then(() => this.load(location, signal));
97
+ this.#refresh = work;
98
+ return work.finally(() => { if (this.#refresh === work)
99
+ this.#refresh = undefined; });
100
+ }
101
+ async load(location, signal) {
102
+ this.check();
103
+ signal?.throwIfAborted();
104
+ const warnings = [], records = [];
105
+ const warning = (host, error) => { this.check(); signal?.throwIfAborted(); warnings.push(host + ": " + String(error)); };
106
+ let known = [];
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
+ }
119
+ }
120
+ let sessions = [];
121
+ try {
122
+ const scanned = await this.source.scan(location, signal);
123
+ sessions = scanned.sessions;
124
+ warnings.push(...scanned.warnings.map(value => this.files.host + ": " + value));
125
+ }
126
+ catch (error) {
127
+ warning(this.files.host, error);
128
+ }
129
+ this.check();
130
+ signal?.throwIfAborted();
131
+ if (sessions.length || imports.length || (this.files.host === "pi" && before.some(value => "file" in value))) {
132
+ known = this.files.control(() => {
133
+ this.check();
134
+ 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
+ }
151
+ for (const session of sessions) {
152
+ const previous = current.get(session.sessionId), captured = before.find(record => record.sessionId === session.sessionId);
153
+ // A concurrent source update must not be overwritten by this older scan. Summary-only updates remain reusable.
154
+ if (captured && rawSource(captured) !== rawSource(previous))
155
+ continue;
156
+ if (!captured && previous && !imported.has(session.sessionId))
157
+ continue;
158
+ const value = indexHistory(session, previous, this.files.host);
159
+ if (JSON.stringify(previous) !== JSON.stringify(value))
160
+ this.files.save(value);
161
+ current.set(session.sessionId, value);
162
+ }
163
+ return [...current.values()];
164
+ });
165
+ }
166
+ if (importedPrevious)
167
+ this.previousFiles = undefined;
168
+ }
169
+ catch (error) {
170
+ warning(this.files.host, error);
171
+ }
172
+ records.push(...known.map(record => ({ host: this.files.host, record })));
173
+ if (this.otherFiles) {
174
+ try {
175
+ records.push(...this.otherFiles.list().map(record => ({ host: this.otherFiles.host, record })));
176
+ }
177
+ catch (error) {
178
+ warning(this.otherFiles.host, error);
179
+ }
180
+ }
181
+ this.check();
182
+ signal?.throwIfAborted();
183
+ this.#records = records;
184
+ this.#sourceWarnings = warnings;
185
+ this.#loaded = true;
186
+ }
187
+ search({ host, query = "", sessionId, from, to, offset = 0, limit = 20 } = {}) {
188
+ this.check();
189
+ const needle = query.trim().toLowerCase(), after = from === undefined ? -Infinity : Date.parse(from), before = to === undefined ? Infinity : Date.parse(to);
190
+ requireThat(!Number.isNaN(after) && !Number.isNaN(before) && after <= before, "时间范围无效,请使用 ISO 日期或时间。");
191
+ const matches = this.#records.filter(value => (!host || value.host === host) && (!sessionId || value.record.sessionId === sessionId)).flatMap(value => value.record.nodes
192
+ .filter(node => (needle ? node.kind !== "session" && node.ready && node.text.toLowerCase().includes(needle) : node.kind === "session") &&
193
+ Date.parse(node.to) >= after && Date.parse(node.from) <= before)
194
+ .map(node => {
195
+ const start = Math.max(0, node.text.toLowerCase().indexOf(needle) - 80);
196
+ return { ...brief(value, node), parents: node.parents, snippet: node.text.slice(start, start + 400) };
197
+ })).sort((a, b) => b.to.localeCompare(a.to) || a.id.localeCompare(b.id));
198
+ const result = page(matches, offset, limit);
199
+ return { matches: result.values, total: matches.length, nextOffset: result.nextOffset, warnings: this.warnings };
200
+ }
201
+ find(id, host) {
202
+ const values = this.#records.filter(value => (!host || value.host === host) && value.record.nodes.some(node => node.id === id));
203
+ requireThat(values.length, "历史节点不存在,请重新搜索。");
204
+ requireThat(values.length === 1, "历史节点来源不明确,请指定 host。");
205
+ const value = values[0], node = value.record.nodes.find(node => node.id === id);
206
+ return { value, node };
207
+ }
208
+ read(id, offset = 0, limit = 12000, childrenOffset = 0, host) {
209
+ this.check();
210
+ const { value, node } = this.find(id, host), lookup = new Map(value.record.nodes.map(node => [node.id, node]));
211
+ requireThat(offset <= node.text.length, "读取位置超出正文。");
212
+ if (offset && /[\uDC00-\uDFFF]/.test(node.text[offset] ?? ""))
213
+ offset--;
214
+ let end = Math.min(node.text.length, offset + limit);
215
+ if (end < node.text.length && /[\uD800-\uDBFF]/.test(node.text[end - 1] ?? ""))
216
+ end += end - offset === 1 ? 1 : -1;
217
+ const children = page(node.children, childrenOffset, 20);
218
+ return { ...brief(value, node), text: node.text.slice(offset, end), totalLength: node.text.length,
219
+ nextOffset: end < node.text.length ? end : null,
220
+ parents: node.parents.map(id => brief(value, lookup.get(id))), children: children.values.map(id => brief(value, lookup.get(id))),
221
+ nextChildrenOffset: children.nextOffset, source: { host: value.host, sessionId: value.record.sessionId, entryIds: node.entryIds }, warnings: this.warnings };
222
+ }
223
+ /** Only this source is organized. Model calls run outside the write lock. */
224
+ organize(summarize, signal) {
225
+ this.check();
226
+ const run = async () => {
227
+ for (;;) {
228
+ this.check();
229
+ signal.throwIfAborted();
230
+ let pending;
231
+ for (const record of this.files.list()) {
232
+ const lookup = new Map(record.nodes.map(node => [node.id, node]));
233
+ const node = [...record.nodes].sort((a, b) => a.level - b.level).find(node => node.kind === "summary" && !node.ready && node.children.every(id => lookup.get(id).ready));
234
+ if (node) {
235
+ pending = { record, node, children: node.children.map(id => lookup.get(id)) };
236
+ break;
237
+ }
238
+ }
239
+ if (!pending)
240
+ return;
241
+ const { record, node, children } = pending;
242
+ const text = await summarize(children.map(child => `[${child.from} — ${child.to}${child.completed ? "" : ";未完成"}]\n${child.text}`).join("\n\n"), signal);
243
+ this.check();
244
+ signal.throwIfAborted();
245
+ const committed = this.files.control(() => {
246
+ this.check();
247
+ signal.throwIfAborted();
248
+ const current = this.files.list().find(value => value.id === record.id);
249
+ if (!current || !completeHistorySummary(current, node.id, children, text))
250
+ return;
251
+ this.files.save(current);
252
+ return current;
253
+ });
254
+ if (!committed)
255
+ return;
256
+ this.#records = this.#records.map(value => value.host === this.files.host && value.record.id === committed.id ? { host: value.host, record: committed } : value);
257
+ }
258
+ };
259
+ const work = (this.#work ?? Promise.resolve()).catch(() => { }).then(run).then(() => { this.#summaryError = undefined; }).catch(error => {
260
+ if (!signal.aborted && !this.#closed)
261
+ this.#summaryError = String(error);
262
+ throw error;
263
+ });
264
+ this.#work = work;
265
+ return work.finally(() => { if (this.#work === work)
266
+ this.#work = undefined; });
267
+ }
268
+ async close() { this.#closed = true; await Promise.allSettled([this.#work, this.#refresh]); this.#records = []; this.#loaded = false; }
269
+ }
@@ -0,0 +1,121 @@
1
+ import { npmName, ordinaryNpm } from "../../common/package-source.js";
2
+ export const pbotPackage = {
3
+ id: "pbot", label: "Personal Bot", description: "工作区个人助手", source: "npm:personal-bot", defaultSelected: true,
4
+ };
5
+ export function parseCatalog(value) {
6
+ if (!Array.isArray(value))
7
+ throw new Error("插件清单必须是数组。");
8
+ const ids = new Set(["pbot"]), names = new Set(["personal-bot"]);
9
+ return value.map((item) => {
10
+ if (!item || typeof item !== "object")
11
+ throw new Error("无效插件清单项。");
12
+ const entry = item;
13
+ for (const field of ["id", "label", "description", "source"]) {
14
+ if (typeof entry[field] !== "string" || !entry[field].trim() || /[\x00-\x1f\x7f]/.test(entry[field])) {
15
+ throw new Error(`无效插件字段:${field}`);
16
+ }
17
+ }
18
+ const plugin = entry;
19
+ const name = npmName(plugin.source);
20
+ if (!/^[a-z0-9-]+$/.test(plugin.id) || !name || !ordinaryNpm(plugin.source) ||
21
+ typeof plugin.defaultSelected !== "boolean" || ids.has(plugin.id) || names.has(name)) {
22
+ throw new Error(`无效或重复的插件:${plugin.id}`);
23
+ }
24
+ ids.add(plugin.id);
25
+ names.add(name);
26
+ return { id: plugin.id, label: plugin.label, description: plugin.description,
27
+ source: `npm:${name}`, defaultSelected: plugin.defaultSelected };
28
+ });
29
+ }
30
+ export function packageChoices(catalog, installed) {
31
+ const first = !installed.some(pkg => pkg.name === "personal-bot");
32
+ return [pbotPackage, ...catalog].map(plugin => {
33
+ const matches = installed.filter(pkg => pkg.name === npmName(plugin.source));
34
+ // A project npm entry overrides the same npm identity in user settings. Different
35
+ // git/local identities still need an explicit decision before changing sources.
36
+ const localNpm = matches.some(pkg => pkg.scope === "project" && npmName(pkg.source));
37
+ const existing = matches.filter(pkg => !(localNpm && pkg.scope === "user" && npmName(pkg.source)));
38
+ const custom = existing.some(pkg => !ordinaryNpm(pkg.source));
39
+ return { plugin, required: plugin.id === "pbot", existing,
40
+ selected: plugin.id === "pbot" || (first ? plugin.defaultSelected : existing.length > 0), keep: custom };
41
+ });
42
+ }
43
+ export function parseSetupArgs(args) {
44
+ if (!args.length || args.length === 1 && ["--help", "-h"].includes(args[0]))
45
+ return { action: "help" };
46
+ if (args.length === 1 && ["--version", "-v"].includes(args[0]))
47
+ return { action: "version" };
48
+ if (args[0] !== "setup")
49
+ throw new Error("用法:pbot setup [--workspace <目录>]");
50
+ let workspace;
51
+ for (let index = 1; index < args.length; index++) {
52
+ const arg = args[index];
53
+ if ((arg === "--help" || arg === "-h") && args.length === 2)
54
+ return { action: "help" };
55
+ if (arg !== "--workspace" || workspace !== undefined)
56
+ throw new Error(`不支持的参数:${arg}`);
57
+ const value = args[++index];
58
+ if (!value || value.startsWith("--") || /[\x00-\x1f\x7f]/.test(value))
59
+ throw new Error("--workspace 需要目录。");
60
+ workspace = value;
61
+ }
62
+ return workspace === undefined ? { action: "setup" } : { action: "setup", workspace };
63
+ }
64
+ export async function executeSetup(plan, operations, report, signal) {
65
+ const results = [];
66
+ const add = (result) => { results.push(result); report(result); };
67
+ let pi;
68
+ try {
69
+ signal.throwIfAborted();
70
+ if (plan.tmux) {
71
+ await operations.installTmux(plan.tmux);
72
+ add({ name: "tmux", state: "done", detail: "已安装" });
73
+ }
74
+ signal.throwIfAborted();
75
+ pi = await operations.ensurePi(plan.pi, plan.piVersion);
76
+ add({ name: "Pi", state: "done", detail: pi.version });
77
+ }
78
+ catch (error) {
79
+ signal.throwIfAborted();
80
+ add({ name: "运行环境", state: "failed", detail: String(error) });
81
+ return { results };
82
+ }
83
+ for (const choice of plan.packages) {
84
+ signal.throwIfAborted();
85
+ const name = choice.plugin.label;
86
+ if (!choice.selected) {
87
+ add({ name, state: "skipped", detail: "本次不处理" });
88
+ continue;
89
+ }
90
+ if (choice.error) {
91
+ add({ name, state: "failed", detail: choice.error });
92
+ if (choice.required)
93
+ return { results };
94
+ continue;
95
+ }
96
+ if (choice.keep) {
97
+ try {
98
+ const source = await operations.verifyKeptPackage(pi, plan.workspace, choice);
99
+ add({ name, state: "kept", detail: source });
100
+ }
101
+ catch (error) {
102
+ signal.throwIfAborted();
103
+ add({ name, state: "failed", detail: String(error) });
104
+ if (choice.required)
105
+ return { results };
106
+ }
107
+ continue;
108
+ }
109
+ try {
110
+ const version = await operations.installPackage(pi, plan.workspace, choice);
111
+ add({ name, state: "done", detail: version });
112
+ }
113
+ catch (error) {
114
+ signal.throwIfAborted();
115
+ add({ name, state: "failed", detail: String(error) });
116
+ if (choice.required)
117
+ return { results };
118
+ }
119
+ }
120
+ return { pi, results };
121
+ }
@@ -0,0 +1,15 @@
1
+ export declare class PythonEnvironmentService {
2
+ #private;
3
+ private readonly root;
4
+ constructor(root: string);
5
+ get status(): {
6
+ state: "unprepared" | "preparing" | "ready" | "error";
7
+ directory?: string;
8
+ error?: string;
9
+ };
10
+ setup(signal?: AbortSignal): Promise<{
11
+ directory: string;
12
+ python: string;
13
+ }>;
14
+ close(): Promise<void>;
15
+ }
@@ -0,0 +1,50 @@
1
+ import { join } from "node:path";
2
+ import { object } from "../../common/json.js";
3
+ import { runCommand } from "../../integrations/process.js";
4
+ import { requireThat } from "../../common/validation.js";
5
+ import { resourceRoot } from "../../common/paths.js";
6
+ export class PythonEnvironmentService {
7
+ root;
8
+ #shutdown = new AbortController();
9
+ #pending;
10
+ #status = { state: "unprepared" };
11
+ constructor(root) {
12
+ this.root = root;
13
+ }
14
+ get status() { return { ...this.#status }; }
15
+ setup(signal) {
16
+ const cancel = signal ? AbortSignal.any([this.#shutdown.signal, signal]) : this.#shutdown.signal;
17
+ if (cancel.aborted)
18
+ return Promise.reject(cancel.reason);
19
+ if (this.#pending)
20
+ return this.#pending;
21
+ this.#status = { state: "preparing" };
22
+ this.#pending = (async () => {
23
+ try {
24
+ const environment = {};
25
+ for (const [key, value] of Object.entries(process.env))
26
+ if (value !== undefined)
27
+ environment[key] = value;
28
+ const result = await runCommand("/bin/sh", ["-c", 'exec "$(command -v python3 || command -v python)" "$@"',
29
+ "pbot-python", "-I", "-B", join(resourceRoot, "../scripts/setup-python.py")], undefined, this.root, environment, cancel, { timeoutMs: 15 * 60_000 });
30
+ cancel.throwIfAborted();
31
+ requireThat(result.success && object(result.data) && typeof result.data.directory === "string" && typeof result.data.python === "string", "共享 Python 环境未就绪;修复后执行 /pbot python setup。\n" + JSON.stringify(result.data).slice(-6000));
32
+ const ready = { directory: result.data.directory, python: result.data.python };
33
+ this.#status = { state: "ready", directory: ready.directory };
34
+ return ready;
35
+ }
36
+ catch (error) {
37
+ this.#status = { state: "error", error: error instanceof Error ? error.message : String(error) };
38
+ throw error;
39
+ }
40
+ finally {
41
+ this.#pending = undefined;
42
+ }
43
+ })();
44
+ return this.#pending;
45
+ }
46
+ async close() {
47
+ this.#shutdown.abort();
48
+ await this.#pending?.catch(() => undefined);
49
+ }
50
+ }
@@ -0,0 +1,2 @@
1
+ export declare function mutationScope(root: string, target: string): "outside" | "inside";
2
+ export declare function authorizeMutation(root: string, path: string, change: unknown, confirm: (value: unknown) => Promise<boolean>, signal: AbortSignal): Promise<void>;
@@ -0,0 +1,17 @@
1
+ import { relative, isAbsolute } from "node:path";
2
+ import { mutationTarget } from "../../common/files.js";
3
+ import { requireThat } from "../../common/validation.js";
4
+ export function mutationScope(root, target) {
5
+ const rel = relative(root, target);
6
+ if (rel === ".." || rel.startsWith("../") || isAbsolute(rel))
7
+ return "outside";
8
+ return "inside";
9
+ }
10
+ export async function authorizeMutation(root, path, change, confirm, signal) {
11
+ signal.throwIfAborted();
12
+ const target = await mutationTarget(path), scope = mutationScope(root, target);
13
+ if (scope === "outside")
14
+ requireThat(await confirm({ path: target, change }), "用户未确认文件修改。");
15
+ signal.throwIfAborted();
16
+ requireThat(await mutationTarget(path) === target, "确认期间文件路径已变化。");
17
+ }
@@ -0,0 +1,2 @@
1
+ export declare const directories: string[];
2
+ export declare function prepareWorkspace(root: string): Promise<void>;
@@ -0,0 +1,18 @@
1
+ import { lstat, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { requireThat } from "../../common/validation.js";
4
+ import { resourceRoot } from "../../common/paths.js";
5
+ export const directories = ["areas", "projects", "knowledge", "providers", "scratch", "agents"];
6
+ export async function prepareWorkspace(root) {
7
+ for (const part of directories) {
8
+ const path = join(root, part);
9
+ await mkdir(path, { mode: 0o700 }).catch(error => { if (error.code !== "EEXIST")
10
+ throw error; });
11
+ const entry = await lstat(path);
12
+ requireThat(entry.isDirectory() && !entry.isSymbolicLink(), "工作区目录不能使用符号链接:" + path);
13
+ }
14
+ const agents = await readFile(join(resourceRoot, "templates/workspace-agents.md"), "utf8");
15
+ await writeFile(join(root, "AGENTS.md"), agents, { flag: "wx" })
16
+ .catch(error => { if (error.code !== "EEXIST")
17
+ throw error; });
18
+ }
@@ -0,0 +1,30 @@
1
+ import type { AgentDescriptor, AgentLaunch, AgentMessage, AgentStatus, AgentCycle, AgentSessionRef, AgentSessionReference } from "../model/agent.js";
2
+ export declare function descriptor(v: unknown): AgentDescriptor;
3
+ export declare function sessionRef(v: unknown): AgentSessionRef;
4
+ export declare function launch(v: unknown): AgentLaunch;
5
+ export declare function message(v: unknown): AgentMessage;
6
+ export declare function cycle(v: unknown): AgentCycle;
7
+ export declare function status(v: unknown): AgentStatus;
8
+ export declare class AgentFiles {
9
+ #private;
10
+ readonly root: string;
11
+ readonly host: AgentSessionReference["host"];
12
+ constructor(root: string, host?: AgentSessionReference["host"]);
13
+ check(): void;
14
+ fail(error: unknown): void;
15
+ directory(id: string): string;
16
+ read<T>(id: string, path: string, parse: (v: unknown) => T): T;
17
+ optional<T>(id: string, path: string, parse: (v: unknown) => T): T | undefined;
18
+ write<T>(id: string, path: string, value: T, parse: (v: unknown) => T, create?: boolean): void;
19
+ create(value: AgentDescriptor, run: AgentLaunch): void;
20
+ list(): AgentDescriptor[];
21
+ messages(id: string): AgentMessage[];
22
+ completions(id: string): AgentCycle[];
23
+ reference(id: string): AgentSessionReference;
24
+ session(id: string): string | null;
25
+ /** The child fixes the native path before accepting input; never reset it to null. */
26
+ bindSession(id: string, file: string): void;
27
+ /** Short controller edits are serialized; an abandoned lock fails closed. */
28
+ control<T>(work: () => T): T;
29
+ enqueue(id: string, text: string, delivery: AgentMessage["delivery"], files?: string[], requestId?: string): AgentMessage;
30
+ }