personal-bot 0.2.4 → 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 (214) hide show
  1. package/CONTEXT.md +9 -7
  2. package/README.md +13 -6
  3. package/README.zh-CN.md +13 -6
  4. package/docs/README.md +3 -1
  5. package/docs/agent-collaboration.md +26 -12
  6. package/docs/architecture-guidelines.md +2 -2
  7. package/docs/capability-design.md +6 -4
  8. package/docs/dsh-implementation-plan.md +158 -0
  9. package/docs/dsh-support-proposal.md +314 -0
  10. package/docs/dsh.md +103 -0
  11. package/docs/file-storage.md +14 -9
  12. package/docs/history.md +29 -18
  13. package/docs/installation-design.md +3 -1
  14. package/docs/interaction-design.md +13 -0
  15. package/docs/release.md +3 -3
  16. package/docs/technical-design.md +29 -15
  17. package/docs/validation.md +38 -2
  18. package/lib/client.js +1071 -0
  19. package/lib/common/files.d.ts +2 -0
  20. package/lib/common/files.js +44 -0
  21. package/lib/common/json.d.ts +6 -0
  22. package/lib/common/json.js +16 -0
  23. package/lib/common/markdown.d.ts +5 -0
  24. package/lib/common/markdown.js +26 -0
  25. package/lib/common/paths.d.ts +1 -0
  26. package/lib/common/paths.js +2 -0
  27. package/lib/common/validation.d.ts +3 -0
  28. package/lib/common/validation.js +14 -0
  29. package/lib/common/version.d.ts +3 -0
  30. package/lib/common/version.js +13 -0
  31. package/lib/dsh.d.ts +4 -0
  32. package/lib/dsh.js +22 -0
  33. package/lib/extension/dsh/agent-events.d.ts +24 -0
  34. package/lib/extension/dsh/agent-events.js +242 -0
  35. package/lib/extension/dsh/agent-runtime.d.ts +49 -0
  36. package/lib/extension/dsh/agent-runtime.js +348 -0
  37. package/lib/extension/dsh/command.d.ts +4 -0
  38. package/lib/extension/dsh/command.js +83 -0
  39. package/lib/extension/dsh/files.d.ts +5 -0
  40. package/lib/extension/dsh/files.js +67 -0
  41. package/lib/extension/dsh/history.d.ts +15 -0
  42. package/lib/extension/dsh/history.js +88 -0
  43. package/lib/extension/dsh/management.d.ts +21 -0
  44. package/lib/extension/dsh/management.js +217 -0
  45. package/lib/extension/dsh/workspace.d.ts +48 -0
  46. package/lib/extension/dsh/workspace.js +290 -0
  47. package/lib/integrations/agent-resources.d.ts +1 -0
  48. package/lib/integrations/agent-resources.js +18 -0
  49. package/lib/integrations/dsh/dsh-agents.d.ts +30 -0
  50. package/lib/integrations/dsh/dsh-agents.js +104 -0
  51. package/lib/integrations/dsh/history-summary.d.ts +11 -0
  52. package/lib/integrations/dsh/history-summary.js +40 -0
  53. package/lib/integrations/dsh/history.d.ts +16 -0
  54. package/lib/integrations/dsh/history.js +156 -0
  55. package/lib/integrations/dsh/input.d.ts +21 -0
  56. package/lib/integrations/dsh/input.js +62 -0
  57. package/lib/integrations/dsh/messages.d.ts +36 -0
  58. package/lib/integrations/dsh/messages.js +11 -0
  59. package/lib/integrations/dsh/notifications.d.ts +8 -0
  60. package/lib/integrations/dsh/notifications.js +55 -0
  61. package/lib/integrations/dsh/resources.d.ts +10 -0
  62. package/lib/integrations/dsh/resources.js +41 -0
  63. package/lib/integrations/dsh/schema.d.ts +7 -0
  64. package/lib/integrations/dsh/schema.js +28 -0
  65. package/lib/integrations/dsh/session.d.ts +14 -0
  66. package/lib/integrations/dsh/session.js +59 -0
  67. package/lib/integrations/history-summary.d.ts +2 -0
  68. package/lib/integrations/history-summary.js +21 -0
  69. package/lib/integrations/process.d.ts +14 -0
  70. package/lib/integrations/process.js +48 -0
  71. package/lib/integrations/python-activation.d.ts +2 -0
  72. package/lib/integrations/python-activation.js +29 -0
  73. package/lib/integrations/team-context.d.ts +18 -0
  74. package/lib/integrations/team-context.js +57 -0
  75. package/lib/model/agent.d.ts +92 -0
  76. package/lib/model/agent.js +1 -0
  77. package/lib/model/history.d.ts +43 -0
  78. package/lib/model/history.js +1 -0
  79. package/lib/model/team.d.ts +85 -0
  80. package/lib/model/team.js +1 -0
  81. package/lib/services/agents/agent-notices.d.ts +13 -0
  82. package/lib/services/agents/agent-notices.js +25 -0
  83. package/lib/services/agents/agent-run.d.ts +39 -0
  84. package/lib/services/agents/agent-run.js +138 -0
  85. package/lib/services/agents/agent-service.d.ts +166 -0
  86. package/lib/services/agents/agent-service.js +325 -0
  87. package/lib/services/agents/team-service.d.ts +168 -0
  88. package/lib/services/agents/team-service.js +343 -0
  89. package/lib/services/history/history-service.d.ts +103 -0
  90. package/lib/services/history/history-service.js +269 -0
  91. package/lib/services/workspace/python-environment-service.d.ts +15 -0
  92. package/lib/services/workspace/python-environment-service.js +50 -0
  93. package/lib/services/workspace/workspace-files.d.ts +2 -0
  94. package/lib/services/workspace/workspace-files.js +17 -0
  95. package/lib/services/workspace/workspace-preparation.d.ts +2 -0
  96. package/lib/services/workspace/workspace-preparation.js +18 -0
  97. package/lib/storage/agent-files.d.ts +30 -0
  98. package/lib/storage/agent-files.js +166 -0
  99. package/lib/storage/atomic-json.d.ts +11 -0
  100. package/lib/storage/atomic-json.js +101 -0
  101. package/lib/storage/file-access.d.ts +2 -0
  102. package/lib/storage/file-access.js +26 -0
  103. package/lib/storage/history-files.d.ts +10 -0
  104. package/lib/storage/history-files.js +63 -0
  105. package/lib/storage/team-files.d.ts +16 -0
  106. package/lib/storage/team-files.js +111 -0
  107. package/lib/tools/agents/actions.d.ts +52 -0
  108. package/lib/tools/agents/actions.js +49 -0
  109. package/lib/tools/agents/contract.d.ts +163 -0
  110. package/lib/tools/agents/contract.js +56 -0
  111. package/lib/tools/agents/dsh-contract.d.ts +52 -0
  112. package/lib/tools/agents/dsh-contract.js +15 -0
  113. package/lib/tools/agents/dsh-tool.d.ts +5 -0
  114. package/lib/tools/agents/dsh-tool.js +38 -0
  115. package/lib/tools/history/actions.d.ts +70 -0
  116. package/lib/tools/history/actions.js +7 -0
  117. package/lib/tools/history/contract.d.ts +81 -0
  118. package/lib/tools/history/contract.js +32 -0
  119. package/lib/tools/history/dsh-tool.d.ts +4 -0
  120. package/lib/tools/history/dsh-tool.js +22 -0
  121. package/lib/tools/result.d.ts +18 -0
  122. package/lib/tools/result.js +10 -0
  123. package/lib/tools/teams/actions.d.ts +24 -0
  124. package/lib/tools/teams/actions.js +38 -0
  125. package/lib/tools/teams/contract.d.ts +232 -0
  126. package/lib/tools/teams/contract.js +67 -0
  127. package/lib/tools/teams/dsh-tool.d.ts +8 -0
  128. package/lib/tools/teams/dsh-tool.js +49 -0
  129. package/lib/ui/dsh/attachments.d.ts +5 -0
  130. package/lib/ui/dsh/attachments.js +39 -0
  131. package/lib/ui/dsh/elements.d.ts +17 -0
  132. package/lib/ui/dsh/elements.js +79 -0
  133. package/lib/ui/dsh/index.d.ts +4 -0
  134. package/lib/ui/dsh/index.js +13 -0
  135. package/lib/ui/dsh/management.d.ts +4 -0
  136. package/lib/ui/dsh/management.js +711 -0
  137. package/lib/ui/dsh/remote.d.ts +15 -0
  138. package/lib/ui/dsh/remote.js +11 -0
  139. package/lib/ui/dsh/styles.d.ts +1 -0
  140. package/lib/ui/dsh/styles.js +132 -0
  141. package/lib/ui/presentation.d.ts +3 -0
  142. package/lib/ui/presentation.js +12 -0
  143. package/npm-shrinkwrap.json +11140 -830
  144. package/package.json +94 -12
  145. package/python/README.md +6 -6
  146. package/resources/agents/coordinator.md +1 -1
  147. package/resources/dsh/cordis.patch.yml +3 -0
  148. package/resources/prompts/workspace.md +1 -1
  149. package/resources/references/compute.md +1 -1
  150. package/resources/references/task-conductor.md +6 -4
  151. package/resources/skills/provider-connections/SKILL.md +10 -0
  152. package/scripts/README.md +9 -1
  153. package/src/dsh.ts +22 -0
  154. package/src/extension/agent-events.ts +51 -46
  155. package/src/extension/agents-command.ts +48 -34
  156. package/src/extension/command.ts +42 -23
  157. package/src/extension/dsh/agent-events.ts +148 -0
  158. package/src/extension/dsh/agent-runtime.ts +251 -0
  159. package/src/extension/dsh/command.ts +78 -0
  160. package/src/extension/dsh/files.ts +61 -0
  161. package/src/extension/dsh/history.ts +69 -0
  162. package/src/extension/dsh/management.ts +108 -0
  163. package/src/extension/dsh/workspace.ts +206 -0
  164. package/src/extension/events.ts +6 -4
  165. package/src/extension/history.ts +15 -12
  166. package/src/extension/subagent-entry.ts +34 -63
  167. package/src/extension/teams-command.ts +53 -46
  168. package/src/integrations/{pi/agent-resources.ts → agent-resources.ts} +2 -2
  169. package/src/integrations/dsh/dsh-agents.ts +97 -0
  170. package/src/integrations/dsh/history-summary.ts +43 -0
  171. package/src/integrations/dsh/history.ts +76 -0
  172. package/src/integrations/dsh/input.ts +66 -0
  173. package/src/integrations/dsh/messages.ts +22 -0
  174. package/src/integrations/dsh/notifications.ts +46 -0
  175. package/src/integrations/dsh/resources.ts +44 -0
  176. package/src/integrations/dsh/schema.ts +26 -0
  177. package/src/integrations/dsh/session.ts +67 -0
  178. package/src/integrations/history-summary.ts +20 -0
  179. package/src/integrations/pi/history.ts +4 -14
  180. package/src/integrations/pi/pi-agents.ts +17 -6
  181. package/src/integrations/pi/team-context.ts +6 -58
  182. package/src/integrations/team-context.ts +60 -0
  183. package/src/model/agent.ts +2 -0
  184. package/src/model/history.ts +3 -1
  185. package/src/pbot.ts +1 -1
  186. package/src/services/agents/agent-notices.ts +28 -0
  187. package/src/services/agents/agent-run.ts +114 -0
  188. package/src/services/agents/agent-service.ts +127 -64
  189. package/src/services/history/history-service.ts +120 -59
  190. package/src/storage/agent-files.ts +17 -4
  191. package/src/storage/history-files.ts +30 -5
  192. package/src/tools/agents/actions.ts +47 -0
  193. package/src/tools/agents/contract.ts +8 -5
  194. package/src/tools/agents/dsh-contract.ts +16 -0
  195. package/src/tools/agents/dsh-tool.ts +41 -0
  196. package/src/tools/agents/tool.ts +7 -43
  197. package/src/tools/history/actions.ts +9 -0
  198. package/src/tools/history/contract.ts +34 -0
  199. package/src/tools/history/dsh-tool.ts +22 -0
  200. package/src/tools/history/tool.ts +7 -30
  201. package/src/tools/teams/actions.ts +38 -0
  202. package/src/tools/teams/contract.ts +7 -3
  203. package/src/tools/teams/dsh-tool.ts +55 -0
  204. package/src/tools/teams/tool.ts +6 -31
  205. package/src/ui/agent-display.ts +7 -4
  206. package/src/ui/dsh/attachments.ts +42 -0
  207. package/src/ui/dsh/elements.ts +50 -0
  208. package/src/ui/dsh/index.ts +18 -0
  209. package/src/ui/dsh/management.ts +412 -0
  210. package/src/ui/dsh/remote.ts +20 -0
  211. package/src/ui/dsh/styles.ts +132 -0
  212. package/src/ui/pi/menu.ts +37 -0
  213. package/src/ui/pi/reader.ts +46 -0
  214. package/src/ui/presentation.ts +12 -0
@@ -0,0 +1,711 @@
1
+ import { commandAttachments } from "./attachments.js";
2
+ import { object, json } from "../../common/json.js";
3
+ import { stateLabel } from "../presentation.js";
4
+ import { badge, block, details, element, empty, facts, field, group, lines, note, objectInput, pageTitle, record, rows, select, text, time } from "./elements.js";
5
+ import { managementStyles } from "./styles.js";
6
+ /** The view owns drafts, uploads and queries. Accepted commands belong to the server workspace. */
7
+ export function showManagement(ctx, parentSessionId) {
8
+ const opener = document.activeElement, dialog = element("dialog"), style = element("style");
9
+ dialog.className = "pbot-management";
10
+ dialog.setAttribute("aria-label", "Personal Bot 工作台");
11
+ style.textContent = managementStyles;
12
+ const shell = group(dialog, "pbot-shell"), header = element("header", "", "pbot-header");
13
+ shell.append(header);
14
+ const heading = group(header, "pbot-header-copy");
15
+ heading.append(element("div", "Personal Bot", "pbot-brand"));
16
+ const location = element("p", "正在读取工作区…", "pbot-location");
17
+ heading.append(location);
18
+ const nav = element("nav", "", "pbot-nav"), main = element("main", "", "pbot-main"), footer = element("footer", "", "pbot-footer");
19
+ nav.setAttribute("role", "tablist");
20
+ nav.setAttribute("aria-label", "工作台导航");
21
+ const notice = element("div", "", "pbot-notice"), connection = element("span", "正在连接…");
22
+ notice.setAttribute("role", "status");
23
+ main.append(notice);
24
+ footer.append(connection);
25
+ shell.append(nav, main, footer);
26
+ dialog.prepend(style);
27
+ const lifetime = new AbortController();
28
+ let closed = false, timer, tab = "status";
29
+ let snapshot, expertId = "", teamId = "", expertOffset;
30
+ let snapshotRevision = 0, expertRevision = 0, teamRevision = 0, historyRevision = 0, recordRevision = 0;
31
+ let expertPageKey = "", teamPageKey = "", pollError = false;
32
+ const sections = new Map(), tabs = new Map(), tabCounts = new Map();
33
+ function inform(message, tone = "info") {
34
+ if (closed)
35
+ return;
36
+ notice.textContent = message;
37
+ notice.dataset.tone = tone;
38
+ notice.setAttribute("role", tone === "error" ? "alert" : "status");
39
+ }
40
+ function enabled(button, value) {
41
+ button.dataset.unavailable = String(!value);
42
+ button.disabled = !value || button.dataset.busy === "true";
43
+ }
44
+ async function attempt(button, action) {
45
+ if (button.dataset.busy === "true")
46
+ return;
47
+ button.dataset.busy = "true";
48
+ button.disabled = true;
49
+ button.setAttribute("aria-busy", "true");
50
+ try {
51
+ await action();
52
+ }
53
+ catch (error) {
54
+ if (!lifetime.signal.aborted)
55
+ inform(error instanceof Error ? error.message : String(error), "error");
56
+ }
57
+ finally {
58
+ if (!closed) {
59
+ delete button.dataset.busy;
60
+ button.removeAttribute("aria-busy");
61
+ button.disabled = button.dataset.unavailable === "true";
62
+ }
63
+ }
64
+ }
65
+ function button(parent, title, action, className = "") {
66
+ const node = element("button", title, className);
67
+ node.type = "button";
68
+ node.addEventListener("click", () => { void attempt(node, action); });
69
+ parent.append(node);
70
+ return node;
71
+ }
72
+ function submit(form, title, action) {
73
+ const node = element("button", title, "pbot-primary");
74
+ node.type = "submit";
75
+ form.append(node);
76
+ form.addEventListener("submit", event => { event.preventDefault(); void attempt(node, action); });
77
+ return node;
78
+ }
79
+ function form(parent) { const node = element("form"); parent.append(node); return node; }
80
+ function activate(id) {
81
+ tab = id;
82
+ for (const [key, section] of sections)
83
+ section.hidden = key !== id;
84
+ for (const [key, item] of tabs) {
85
+ item.setAttribute("aria-selected", String(key === id));
86
+ item.tabIndex = key === id ? 0 : -1;
87
+ }
88
+ main.scrollTop = 0;
89
+ }
90
+ function section(id, title) {
91
+ const node = element("section");
92
+ node.hidden = id !== tab;
93
+ node.id = `pbot-panel-${id}`;
94
+ node.setAttribute("role", "tabpanel");
95
+ node.setAttribute("aria-labelledby", `pbot-tab-${id}`);
96
+ main.append(node);
97
+ sections.set(id, node);
98
+ const control = button(nav, title, () => activate(id));
99
+ control.id = `pbot-tab-${id}`;
100
+ control.setAttribute("role", "tab");
101
+ control.setAttribute("aria-controls", node.id);
102
+ control.setAttribute("aria-selected", String(id === tab));
103
+ control.tabIndex = id === tab ? 0 : -1;
104
+ const count = element("small");
105
+ count.setAttribute("aria-hidden", "true");
106
+ control.append(count);
107
+ tabCounts.set(id, count);
108
+ tabs.set(id, control);
109
+ return node;
110
+ }
111
+ nav.addEventListener("keydown", event => {
112
+ const controls = [...tabs.values()], current = controls.indexOf(document.activeElement);
113
+ if (current < 0)
114
+ return;
115
+ const index = event.key === "Home" ? 0 : event.key === "End" ? controls.length - 1
116
+ : ["ArrowRight", "ArrowDown"].includes(event.key) ? (current + 1) % controls.length
117
+ : ["ArrowLeft", "ArrowUp"].includes(event.key) ? (current + controls.length - 1) % controls.length : -1;
118
+ if (index < 0)
119
+ return;
120
+ event.preventDefault();
121
+ const id = [...tabs.keys()][index];
122
+ activate(id);
123
+ controls[index].focus();
124
+ });
125
+ async function query(method, input) {
126
+ lifetime.signal.throwIfAborted();
127
+ const result = await ctx.remote.pbot[method](json({ parentSessionId, ...(input === undefined ? {} : { input }) }), lifetime.signal);
128
+ if (!result.ok)
129
+ throw new Error(result.error.message);
130
+ lifetime.signal.throwIfAborted();
131
+ return record(result.value);
132
+ }
133
+ async function command(verb, input, message = "操作已接纳。", attachments = []) {
134
+ // Closing the view must not cancel an admitted command.
135
+ const result = await ctx.remote.commands.execute(parentSessionId, "/pbot " + verb + (input === undefined ? "" : " " + JSON.stringify(input)), attachments);
136
+ if (!result.ok)
137
+ throw new Error(result.error.message + ";如连接中断,请先刷新记录确认操作是否已接纳。");
138
+ if (!result.value)
139
+ throw new Error("管理命令未被接纳。");
140
+ if (result.value.result.kind === "error")
141
+ throw new Error(result.value.result.text);
142
+ inform(message);
143
+ if (!closed) {
144
+ try {
145
+ await refresh();
146
+ }
147
+ catch (error) {
148
+ inform("操作已接纳;刷新状态未成功:" + String(error), "error");
149
+ }
150
+ }
151
+ return result.value.result.text ?? "";
152
+ }
153
+ const status = section("status", "工作区");
154
+ pageTitle(status, "工作区", "");
155
+ const counts = note(""), workspaceInfo = group(status);
156
+ status.querySelector("header").after(counts);
157
+ counts.classList.add("pbot-counts");
158
+ const materials = group(status, "pbot-setting");
159
+ materials.append(element("h3", "资料目录"));
160
+ button(materials, "准备目录", async () => { await command("init", undefined, "资料目录已准备。"); });
161
+ const pythonCard = group(status, "pbot-setting"), pythonInfo = group(pythonCard);
162
+ const pythonButton = button(pythonCard, "准备 Python", async () => { await command("python setup", undefined, "Python 准备完成。"); });
163
+ const diagnostic = details(status, "环境详情"), statusBody = block("");
164
+ statusBody.classList.add("pbot-raw");
165
+ diagnostic.append(statusBody);
166
+ const experts = section("experts", "专家");
167
+ pageTitle(experts, "专家", "");
168
+ const create = details(experts, "创建专家"), createForm = form(create), newName = field(createForm, "名称"), initial = field(createForm, "任务(可选)", true);
169
+ newName.placeholder = "例如:研究员";
170
+ initial.placeholder = "任务目标与交付要求";
171
+ const options = details(createForm, "模型与选项"), modelFields = group(options, "pbot-grid");
172
+ options.querySelector("summary").after(note("留空沿用当前会话。"));
173
+ const provider = field(modelFields, "Provider"), model = field(modelFields, "模型");
174
+ const effort = field(options, "Reasoning effort"), cwd = field(options, "工作目录");
175
+ const files = field(options, "文件路径(每行一个)", true);
176
+ const advanced = field(details(options, "其他选项"), "JSON", true, "{}");
177
+ submit(createForm, "创建", async () => {
178
+ const args = { ...objectInput(advanced.value), action: "start", ...(newName.value ? { name: newName.value } : {}),
179
+ ...(initial.value ? { message: initial.value } : {}), ...(provider.value ? { provider: provider.value } : {}),
180
+ ...(model.value ? { model: model.value } : {}), ...(effort.value ? { thinking: effort.value } : {}),
181
+ ...(cwd.value ? { cwd: cwd.value } : {}), ...(lines(files.value).length ? { files: lines(files.value) } : {}) };
182
+ const output = objectInput(await command("agent", args, "专家已创建。"));
183
+ if (initial.value === (args.message ?? ""))
184
+ initial.value = "";
185
+ create.open = false;
186
+ await chooseExpert(text(output.agentId));
187
+ });
188
+ const expertSplit = group(experts, "pbot-split"), expertAside = group(expertSplit), expertFilter = field(expertAside, "筛选专家");
189
+ expertFilter.placeholder = "名称或状态";
190
+ const expertList = group(expertAside, "pbot-list");
191
+ expertList.setAttribute("aria-label", "专家列表");
192
+ const expertDetail = group(expertSplit, "pbot-detail"), expertEmpty = empty("选择专家", "查看记录或发送消息。");
193
+ expertDetail.append(expertEmpty);
194
+ const expertPanel = group(expertDetail);
195
+ expertPanel.hidden = true;
196
+ const expertHeading = group(expertPanel, "pbot-detail-title"), expertTitle = element("h3"), expertBadge = element("span");
197
+ expertHeading.append(expertTitle, expertBadge);
198
+ const expertInfo = group(expertPanel), expertError = element("p", "", "pbot-warning");
199
+ expertPanel.append(expertError);
200
+ expertError.hidden = true;
201
+ const expertControls = group(expertPanel, "pbot-row"), requireExpert = () => { if (!expertId)
202
+ throw new Error("请先选择专家。"); return expertId; };
203
+ const stopButton = button(expertControls, "停止专家", async () => { await command("agent", { action: "stop", agentId: requireExpert() }, "已请求停止专家。"); await loadExpert(); }, "pbot-danger");
204
+ const editExpert = details(expertPanel, "名称与会话详情"), renameForm = form(editExpert), rename = field(renameForm, "新名称");
205
+ rename.required = true;
206
+ submit(renameForm, "重命名", async () => { await command("agent", { action: "rename", agentId: requireExpert(), name: rename.value }, "专家名称已更新。"); await loadExpert(); });
207
+ const expertState = block("");
208
+ expertState.classList.add("pbot-raw");
209
+ editExpert.append(expertState);
210
+ const resume = details(expertPanel, "恢复专家"), resumeForm = form(resume), resumeText = field(resumeForm, "新任务", true);
211
+ resumeText.required = true;
212
+ const resumeButton = submit(resumeForm, "恢复并发送新任务", async () => {
213
+ const message = resumeText.value;
214
+ await command("agent", { action: "resume", agentId: requireExpert(), message }, "专家已恢复,新任务已排队。");
215
+ if (resumeText.value === message)
216
+ resumeText.value = "";
217
+ await loadExpert();
218
+ });
219
+ const chatCard = group(expertPanel, "pbot-card");
220
+ chatCard.append(element("h3", "人工对话"), note("发送前暂停团队新安排。"));
221
+ const chatForm = form(chatCard), chat = field(chatForm, "消息", true);
222
+ chat.maxLength = 24000;
223
+ chat.placeholder = "补充资料、调整方向,或提出新问题…";
224
+ const delivery = select(chatForm, "投递时机", [["steer", "下一个安全步骤"], ["followUp", "下一轮"]]);
225
+ const localFiles = field(chatForm, "附件");
226
+ localFiles.type = "file";
227
+ localFiles.multiple = true;
228
+ const chatFiles = field(details(chatForm, "服务器文件"), "路径(每行一个)", true);
229
+ const expertDrafts = new Map();
230
+ function clearSentDraft(id, input, paths, chosen) {
231
+ const draft = expertDrafts.get(id);
232
+ if (draft) {
233
+ if (draft.text === input.text)
234
+ draft.text = "";
235
+ if (draft.paths === paths)
236
+ draft.paths = "";
237
+ if (draft.files.length === chosen.length && draft.files.every((file, index) => file === chosen[index]))
238
+ draft.files = [];
239
+ }
240
+ }
241
+ const sendButton = submit(chatForm, "发送人工消息", async () => {
242
+ const id = requireExpert(), fileText = chatFiles.value, files = lines(fileText), chosen = Array.from(localFiles.files ?? []);
243
+ if (files.length && chosen.length)
244
+ throw new Error("一次对话请选择服务器文件或本机附件,不能同时使用。");
245
+ if (!chat.value.trim() && !files.length && !chosen.length) {
246
+ chat.focus();
247
+ throw new Error("请输入消息或添加附件。");
248
+ }
249
+ const input = { agentId: id, text: chat.value, delivery: delivery.value, files };
250
+ const attachments = await commandAttachments(ctx, parentSessionId, chosen, lifetime.signal, inform);
251
+ lifetime.signal.throwIfAborted();
252
+ await command("chat", input, "消息已排队,专家会在所选时机接收。", attachments);
253
+ clearSentDraft(id, input, fileText, chosen);
254
+ if (expertId === id) {
255
+ if (chat.value === input.text)
256
+ chat.value = "";
257
+ if (chatFiles.value === fileText)
258
+ chatFiles.value = "";
259
+ const current = Array.from(localFiles.files ?? []);
260
+ if (current.length === chosen.length && current.every((file, index) => file === chosen[index]))
261
+ localFiles.value = "";
262
+ await loadExpert();
263
+ }
264
+ });
265
+ const transcript = group(expertPanel, "pbot-transcript");
266
+ transcript.append(element("h3", "对话与活动"));
267
+ const recordControls = group(transcript, "pbot-pagination"), eventList = group(transcript), rawEvents = details(transcript, "全部原生事件"), rawEventList = group(rawEvents);
268
+ const eventDetail = details(transcript, "完整记录"), eventBody = block(""), eventPaging = element("div", "", "pbot-pagination");
269
+ eventDetail.append(eventBody, eventPaging);
270
+ eventDetail.hidden = true;
271
+ async function chooseExpert(id) {
272
+ if (id !== expertId) {
273
+ if (expertId)
274
+ expertDrafts.set(expertId, { text: chat.value, delivery: delivery.value, paths: chatFiles.value, files: Array.from(localFiles.files ?? []) });
275
+ const draft = expertDrafts.get(id);
276
+ chat.value = draft?.text ?? "";
277
+ delivery.value = draft?.delivery ?? "steer";
278
+ chatFiles.value = draft?.paths ?? "";
279
+ const transfer = new DataTransfer();
280
+ for (const file of draft?.files ?? [])
281
+ transfer.items.add(file);
282
+ localFiles.files = transfer.files;
283
+ rename.value = "";
284
+ resumeText.value = "";
285
+ }
286
+ expertId = id;
287
+ expertOffset = undefined;
288
+ expertPageKey = "";
289
+ ++recordRevision;
290
+ ++expertRevision;
291
+ eventBody.textContent = "";
292
+ eventDetail.hidden = true;
293
+ eventPaging.replaceChildren();
294
+ expertPanel.hidden = true;
295
+ expertEmpty.hidden = false;
296
+ expertEmpty.replaceChildren(note("正在读取专家记录…"));
297
+ renderLists();
298
+ await loadExpert();
299
+ }
300
+ function showExpert(agent) {
301
+ expertTitle.textContent = text(agent.name);
302
+ expertBadge.replaceChildren(badge(agent.state));
303
+ expertPanel.hidden = false;
304
+ expertEmpty.hidden = true;
305
+ const config = object(agent.options) ? agent.options : {};
306
+ expertInfo.replaceChildren(note(text(agent.activity) || "等待新的任务。"), facts([["模型", config.model || "继承父会话"], ["工作目录", agent.cwd]]));
307
+ expertState.textContent = JSON.stringify(agent, null, 2);
308
+ expertError.textContent = text(agent.error);
309
+ expertError.hidden = !agent.error;
310
+ const alive = agent.alive === true, owned = agent.owned === true;
311
+ enabled(stopButton, owned && alive && agent.state !== "stopping");
312
+ enabled(sendButton, owned && alive && !["starting", "stopping"].includes(text(agent.state)));
313
+ enabled(resumeButton, owned && !alive);
314
+ resume.hidden = alive;
315
+ }
316
+ async function loadRecord(seq, offset = 0, format = "text") {
317
+ const revision = ++recordRevision, id = requireExpert(), data = await query("expert", { agentId: id, seq, textOffset: offset, format });
318
+ if (id !== expertId || revision !== recordRevision)
319
+ return;
320
+ eventDetail.hidden = false;
321
+ eventDetail.open = true;
322
+ eventBody.textContent = text(data.text);
323
+ eventBody.classList.toggle("pbot-raw", format === "json");
324
+ eventPaging.replaceChildren(element("span", `记录 ${seq} · ${offset + 1}–${offset + text(data.text).length} / ${data.totalLength}`));
325
+ button(eventPaging, format === "text" ? "查看原始事件" : "查看正文", () => loadRecord(seq, 0, format === "text" ? "json" : "text"));
326
+ if (offset)
327
+ button(eventPaging, "从头读取", () => loadRecord(seq, 0, format));
328
+ if (typeof data.nextOffset === "number")
329
+ button(eventPaging, "继续读取", () => loadRecord(seq, data.nextOffset, format));
330
+ }
331
+ async function loadExpert() {
332
+ if (!expertId)
333
+ return;
334
+ const revision = ++expertRevision, id = expertId, data = await query("expert", { agentId: id, ...(expertOffset === undefined ? {} : { offset: expertOffset }) });
335
+ if (id !== expertId || revision !== expertRevision)
336
+ return;
337
+ showExpert(record(data.agent));
338
+ const actualOffset = Number(data.offset), key = `${id}:${data.cursor}:${actualOffset}`;
339
+ if (key === expertPageKey)
340
+ return;
341
+ expertPageKey = key;
342
+ recordControls.replaceChildren();
343
+ button(recordControls, "最新记录", async () => { expertOffset = undefined; await loadExpert(); });
344
+ if (actualOffset > 0)
345
+ button(recordControls, "上一页", async () => { expertOffset = Math.max(0, actualOffset - 30); await loadExpert(); });
346
+ if (typeof data.nextOffset === "number")
347
+ button(recordControls, "下一页", async () => { expertOffset = data.nextOffset; await loadExpert(); });
348
+ recordControls.append(element("span", `${data.total} 条记录 · 从 ${actualOffset + 1} 开始`));
349
+ const expanded = new Set(Array.from(rawEventList.querySelectorAll("details[open]")).map(item => item.dataset.seq));
350
+ eventList.replaceChildren();
351
+ rawEventList.replaceChildren();
352
+ const labels = { "user/message": "输入", "assistant/message": "专家回复", "tool/result": "工具结果", "turn/end": "本轮结束" };
353
+ for (const event of rows(data.events)) {
354
+ const seq = Number(event.seq), label = event.type === "user/message"
355
+ ? event.source === "user" ? "输入" : event.source === "pbot-input" ? "任务" : undefined : labels[text(event.type)];
356
+ if (label && text(event.preview)) {
357
+ const item = element("article", "", "pbot-event"), meta = group(item, "pbot-row");
358
+ meta.append(element("h4", label), note(time(event.time)));
359
+ item.append(block(event.preview));
360
+ button(item, "阅读全文", () => loadRecord(seq), "pbot-quiet");
361
+ eventList.append(item);
362
+ }
363
+ const item = details(rawEventList, `${seq} · ${event.type} · ${time(event.time)}`);
364
+ item.dataset.seq = text(seq);
365
+ item.open = expanded.has(text(seq));
366
+ item.append(block(event.preview));
367
+ button(item, "读取事件", () => loadRecord(seq, 0, "json"));
368
+ }
369
+ if (!eventList.childElementCount)
370
+ eventList.append(empty("还没有对话内容", "发送任务后,可在这里查看专家的回复与工具结果。"));
371
+ }
372
+ const teams = section("teams", "团队");
373
+ pageTitle(teams, "团队", "");
374
+ const createTeam = details(teams, "创建团队"), teamForm = form(createTeam), teamName = field(teamForm, "团队名称"), brief = field(teamForm, "共同目标", true);
375
+ teamName.required = true;
376
+ brief.required = true;
377
+ teamName.placeholder = "例如:投资研究小组";
378
+ teamForm.append(note("为成员填写职责,留空则不加入。"));
379
+ const memberFields = group(teamForm);
380
+ submit(teamForm, "创建团队", async () => {
381
+ const members = Array.from(memberFields.querySelectorAll("input[data-agent-id]")).filter(input => input.value.trim())
382
+ .map(input => ({ agentId: input.dataset.agentId, role: input.value }));
383
+ const value = objectInput(await command("team", { action: "create", name: teamName.value, brief: brief.value, members }, "团队已创建,可以安排首个批次。"));
384
+ createTeam.open = false;
385
+ await chooseTeam(text(value.teamId));
386
+ });
387
+ const teamSplit = group(teams, "pbot-split"), teamList = group(teamSplit, "pbot-list"), teamDetail = group(teamSplit, "pbot-detail");
388
+ teamList.setAttribute("aria-label", "团队列表");
389
+ const teamEmpty = empty("选择团队", "查看进展与公共消息。"), teamPanel = group(teamDetail);
390
+ teamDetail.prepend(teamEmpty);
391
+ teamPanel.hidden = true;
392
+ const teamHeading = group(teamPanel, "pbot-detail-title"), teamTitle = element("h3"), teamBadge = element("span");
393
+ teamHeading.append(teamTitle, teamBadge);
394
+ const teamInfo = group(teamPanel), teamState = block("");
395
+ teamState.classList.add("pbot-raw");
396
+ const teamControls = group(teamPanel, "pbot-row"), requireTeam = () => { if (!teamId)
397
+ throw new Error("请先选择团队。"); return teamId; };
398
+ const pauseButton = button(teamControls, "暂停新安排", async () => { await command("team", { action: "pause", teamId: requireTeam() }, "已暂停新安排;在途任务仍可能完成。"); await loadTeam(); });
399
+ const teamConfig = details(teamPanel, "成员与团队详情");
400
+ teamConfig.append(teamState);
401
+ const closeTeamButton = button(teamConfig, "关闭团队", async () => { await command("team", { action: "close", teamId: requireTeam() }, "团队已关闭,成员专家仍可独立使用。"); await loadTeam(); }, "pbot-danger");
402
+ const batch = details(teamPanel, "新批次"), batchForm = form(batch), instruction = field(batchForm, "任务与交付要求", true);
403
+ instruction.required = true;
404
+ const batchOptions = group(batchForm, "pbot-grid"), mode = select(batchOptions, "讨论模式", [["roundRobin", "轮流讨论"], ["parallel", "并行轮次"]]);
405
+ const rounds = field(batchOptions, "每位成员轮数", false, "2"), concurrency = field(batchOptions, "并发上限", false, "1"), timeout = field(batchOptions, "整批期限(秒)", false, "900");
406
+ for (const [input, max] of [[rounds, 10], [concurrency, 50], [timeout, 86400]]) {
407
+ input.setAttribute("type", "number");
408
+ input.setAttribute("min", "1");
409
+ input.setAttribute("max", text(max));
410
+ input.required = true;
411
+ }
412
+ const runButton = submit(batchForm, "开始新批次", async () => {
413
+ await command("team", { action: "run", teamId: requireTeam(), instruction: instruction.value,
414
+ mode: mode.value, rounds: Number(rounds.value), maxConcurrent: Number(concurrency.value), timeoutSeconds: Number(timeout.value) }, "新批次已开始。");
415
+ await loadTeam();
416
+ });
417
+ const publicCard = group(teamPanel, "pbot-card");
418
+ const postForm = form(publicCard), post = field(postForm, "公共消息", true);
419
+ post.required = true;
420
+ const replyTo = field(details(postForm, "回复消息"), "回复序号");
421
+ replyTo.setAttribute("type", "number");
422
+ replyTo.setAttribute("min", "1");
423
+ const postButton = submit(postForm, "发布公共消息", async () => {
424
+ const id = requireTeam(), message = post.value;
425
+ await command("post", { teamId: id, text: message, ...(replyTo.value ? { replyTo: Number(replyTo.value) } : {}) }, "公共消息已发布。");
426
+ if (teamId === id && post.value === message)
427
+ post.value = "";
428
+ const draft = teamDrafts.get(id);
429
+ if (draft?.text === message)
430
+ draft.text = "";
431
+ await loadTeam();
432
+ });
433
+ const teamMessages = group(teamPanel), teamPaging = group(teamPanel, "pbot-pagination");
434
+ let teamAfter = 0, teamPage = { afterSeq: 0 };
435
+ const teamDrafts = new Map();
436
+ async function chooseTeam(id) {
437
+ if (id !== teamId) {
438
+ if (teamId)
439
+ teamDrafts.set(teamId, { text: post.value, reply: replyTo.value, instruction: instruction.value, mode: mode.value, rounds: rounds.value, concurrency: concurrency.value, timeout: timeout.value });
440
+ const draft = teamDrafts.get(id);
441
+ post.value = draft?.text ?? "";
442
+ replyTo.value = draft?.reply ?? "";
443
+ instruction.value = draft?.instruction ?? "";
444
+ mode.value = draft?.mode ?? "roundRobin";
445
+ rounds.value = draft?.rounds ?? "2";
446
+ concurrency.value = draft?.concurrency ?? "1";
447
+ timeout.value = draft?.timeout ?? "900";
448
+ }
449
+ teamId = id;
450
+ teamAfter = 0;
451
+ teamPageKey = "";
452
+ ++teamRevision;
453
+ teamPanel.hidden = true;
454
+ teamEmpty.hidden = false;
455
+ teamEmpty.replaceChildren(note("正在读取团队记录…"));
456
+ renderLists();
457
+ await loadTeam({ afterSeq: 0 });
458
+ }
459
+ async function loadTeam(nextPage) {
460
+ if (!teamId)
461
+ return;
462
+ if (nextPage)
463
+ teamPage = nextPage;
464
+ const page = teamPage, id = teamId;
465
+ const revision = ++teamRevision, data = await query("team", { teamId: id, ...page });
466
+ if (id !== teamId || revision !== teamRevision)
467
+ return;
468
+ const team = record(data.team), messages = record(data.page), run = object(team.run) ? team.run : undefined;
469
+ teamPanel.hidden = false;
470
+ teamEmpty.hidden = true;
471
+ teamTitle.textContent = text(team.name);
472
+ teamBadge.replaceChildren(badge(team.closed ? "closed" : run?.state ?? "open"));
473
+ teamInfo.replaceChildren(block(team.brief), facts([["成员", rows(team.members).map(member => `${member.name} · ${member.role}`).join("\n")],
474
+ ["批次进展", run ? `已安排 ${run.scheduled} · 已交付 ${run.published} · 在途 ${team.activeCount}` : "尚未开始"],
475
+ ...(run?.reason ? [["暂停原因", run.reason]] : [])]));
476
+ teamState.textContent = JSON.stringify(team, null, 2);
477
+ enabled(pauseButton, !team.closed && run?.state === "running");
478
+ enabled(closeTeamButton, !team.closed);
479
+ enabled(runButton, !team.closed && run?.state !== "running" && Number(team.activeCount) === 0);
480
+ enabled(postButton, !team.closed);
481
+ const key = JSON.stringify([id, page, messages]);
482
+ if (key === teamPageKey)
483
+ return;
484
+ teamPageKey = key;
485
+ teamMessages.replaceChildren();
486
+ teamPaging.replaceChildren();
487
+ for (const message of rows(messages.messages)) {
488
+ const author = record(message.author), item = element("article", "", "pbot-message"), meta = group(item, "pbot-row");
489
+ meta.append(element("h4", text(author.name)), note(`#${message.seq}${message.replyTo ? ` · 回复 #${message.replyTo}` : ""}`));
490
+ item.append(block(message.text));
491
+ if (typeof message.nextOffset === "number")
492
+ button(item, "继续读取这条消息", () => loadTeam({ seq: message.seq, offset: message.nextOffset }), "pbot-quiet");
493
+ teamMessages.append(item);
494
+ }
495
+ if (!teamMessages.childElementCount)
496
+ teamMessages.append(empty("还没有公共消息", "发布资料或开启批次后,成员会在这里共享结果。"));
497
+ button(teamPaging, "从第一条读取", async () => { teamAfter = 0; await loadTeam({ afterSeq: 0 }); });
498
+ if (typeof messages.nextAfterSeq === "number")
499
+ button(teamPaging, "后续消息", async () => { teamAfter = messages.nextAfterSeq; await loadTeam({ afterSeq: teamAfter }); });
500
+ if (page.seq !== undefined)
501
+ button(teamPaging, "返回消息列表", () => loadTeam({ afterSeq: teamAfter }));
502
+ }
503
+ const history = section("history", "历史");
504
+ pageTitle(history, "历史", "");
505
+ const searchForm = form(history);
506
+ searchForm.className = "pbot-search";
507
+ const historyQuery = field(searchForm, "关键词"), host = select(searchForm, "来源", [["", "全部"], ["pi", "Pi"], ["dsh", "dsh"]]);
508
+ historyQuery.placeholder = "留空查看最近会话";
509
+ submit(searchForm, "查询", () => searchHistory());
510
+ const dateFilters = details(history, "时间范围"), dateGrid = group(dateFilters, "pbot-grid");
511
+ const historyFrom = field(dateGrid, "开始时间"), historyTo = field(dateGrid, "结束时间");
512
+ for (const input of [historyFrom, historyTo])
513
+ input.type = "datetime-local";
514
+ const historyBody = group(history, "pbot-history-results"), historyPaging = group(history, "pbot-pagination"), historyNode = group(history, "pbot-card");
515
+ historyNode.hidden = true;
516
+ historyBody.append(empty("查询历史", "输入关键词,或留空查看最近会话。"));
517
+ let historySearch = {};
518
+ function warnings(parent, values) {
519
+ if (Array.isArray(values))
520
+ for (const warning of values)
521
+ if (warning)
522
+ parent.append(element("p", text(warning), "pbot-warning"));
523
+ }
524
+ async function searchHistory(offset = 0, keepFilters = false) {
525
+ if (!keepFilters)
526
+ historySearch = { query: historyQuery.value, ...(host.value ? { host: host.value } : {}),
527
+ ...(historyFrom.value ? { from: new Date(historyFrom.value).toISOString() } : {}), ...(historyTo.value ? { to: new Date(historyTo.value).toISOString() } : {}) };
528
+ const revision = ++historyRevision, data = await query("history", { action: "search", ...historySearch, offset });
529
+ if (revision !== historyRevision)
530
+ return;
531
+ historyNode.hidden = true;
532
+ historyBody.replaceChildren(note(`共 ${data.total} 项`));
533
+ warnings(historyBody, data.warnings);
534
+ historyPaging.replaceChildren();
535
+ for (const match of rows(data.matches)) {
536
+ const item = element("article"), meta = group(item, "pbot-between");
537
+ item.dataset.id = text(match.id);
538
+ item.dataset.host = text(match.host);
539
+ meta.append(element("h4", text(match.name)), element("span", text(match.host), "pbot-badge"));
540
+ item.append(note(`${time(match.from)} · ${match.kind === "session" ? "会话" : match.kind === "summary" ? "摘要" : "问答"}`), block(match.snippet || match.preview));
541
+ button(item, "读取", () => readHistory(text(match.id), text(match.host)), "pbot-quiet");
542
+ historyBody.append(item);
543
+ }
544
+ if (!rows(data.matches).length)
545
+ historyBody.append(empty("没有找到匹配内容", "试试更短的关键词,或调整来源和时间范围。"));
546
+ if (offset)
547
+ button(historyPaging, "上一页", () => searchHistory(Math.max(0, offset - 20), true));
548
+ if (typeof data.nextOffset === "number")
549
+ button(historyPaging, "下一页", () => searchHistory(data.nextOffset, true));
550
+ }
551
+ async function readHistory(id, source, offset = 0, childrenOffset = 0) {
552
+ const revision = ++historyRevision, data = await query("history", { action: "read", id, host: source, offset, childrenOffset });
553
+ if (revision !== historyRevision)
554
+ return;
555
+ const node = record(data.node);
556
+ historyNode.hidden = false;
557
+ historyNode.replaceChildren(element("h3", text(node.name)), note(`${text(node.host)} · ${time(node.from)} · ${node.completed ? "原交互已完成" : "原交互未确认完成"}`), block(node.text || "摘要尚未生成,可展开下级记录阅读原文。"));
558
+ warnings(historyNode, data.warnings);
559
+ for (const item of historyBody.querySelectorAll("article"))
560
+ item.setAttribute("aria-current", String(item.dataset.id === id && item.dataset.host === source));
561
+ const paging = group(historyNode, "pbot-pagination");
562
+ if (offset)
563
+ button(paging, "正文开头", () => readHistory(id, source, 0, childrenOffset));
564
+ if (typeof node.nextOffset === "number")
565
+ button(paging, "继续正文", () => readHistory(id, source, node.nextOffset, childrenOffset));
566
+ for (const [label, values] of [["上级摘要", node.parents], ["下级记录", node.children]]) {
567
+ if (!rows(values).length)
568
+ continue;
569
+ const related = group(historyNode, "pbot-stack");
570
+ related.append(element("h4", label));
571
+ for (const item of rows(values))
572
+ button(related, text(item.preview || item.name), () => readHistory(text(item.id), text(item.host)), "pbot-quiet");
573
+ }
574
+ if (childrenOffset)
575
+ button(paging, "首批下级", () => readHistory(id, source, offset));
576
+ if (typeof node.nextChildrenOffset === "number")
577
+ button(paging, "更多下级", () => readHistory(id, source, offset, node.nextChildrenOffset));
578
+ const provenance = block(node.source);
579
+ provenance.classList.add("pbot-raw");
580
+ details(historyNode, "原始来源位置").append(provenance);
581
+ historyNode.scrollIntoView({ block: "start" });
582
+ }
583
+ const expertButtons = new Map(), teamButtons = new Map();
584
+ function syncList(container, buttons, values, kind) {
585
+ const ids = new Set(values.map(value => text(value[kind + "Id"])));
586
+ for (const [id, node] of buttons)
587
+ if (!ids.has(id)) {
588
+ node.remove();
589
+ buttons.delete(id);
590
+ }
591
+ container.querySelector(":scope > .pbot-empty")?.remove();
592
+ for (const value of values) {
593
+ const id = text(value[kind + "Id"]), state = kind === "agent" ? value.state : value.closed ? "closed" : object(value.run) ? value.run.state : "open";
594
+ let node = buttons.get(id);
595
+ if (!node) {
596
+ node = button(container, "", () => kind === "agent" ? chooseExpert(id) : chooseTeam(id), "pbot-list-item");
597
+ buttons.set(id, node);
598
+ }
599
+ node.setAttribute("aria-label", text(value.name));
600
+ node.setAttribute("aria-pressed", String(id === (kind === "agent" ? expertId : teamId)));
601
+ const signature = JSON.stringify([value.name, state, value.activity, value.brief]);
602
+ if (node.dataset.content !== signature) {
603
+ node.dataset.content = signature;
604
+ node.replaceChildren();
605
+ const row = group(node, "pbot-between");
606
+ row.append(element("strong", text(value.name)), badge(state));
607
+ if (kind === "team" || !["idle", "stopped"].includes(text(state)))
608
+ node.append(note(text(kind === "agent" ? value.activity : value.brief)));
609
+ }
610
+ node.hidden = kind === "agent" && !`${value.name} ${stateLabel(state)}`.toLowerCase().includes(expertFilter.value.trim().toLowerCase());
611
+ }
612
+ if (!values.length || [...buttons.values()].every(node => node.hidden)) {
613
+ container.append(empty(values.length ? "没有匹配的专家" : kind === "agent" ? "还没有专家" : "还没有团队", values.length ? "调整名称或状态关键词。" : kind === "agent" ? "展开上方“创建专家”开始。" : "先创建专家,再为成员分配职责。"));
614
+ }
615
+ }
616
+ function renderLists() {
617
+ if (!snapshot)
618
+ return;
619
+ syncList(expertList, expertButtons, rows(snapshot.agents), "agent");
620
+ syncList(teamList, teamButtons, rows(snapshot.teams), "team");
621
+ }
622
+ expertFilter.addEventListener("input", renderLists);
623
+ function renderSnapshot() {
624
+ if (!snapshot)
625
+ return;
626
+ location.textContent = text(snapshot.workspaceRoot).split(/[\\/]/).filter(Boolean).at(-1) ?? "";
627
+ location.title = text(snapshot.workspaceRoot);
628
+ const agents = rows(snapshot.agents), teams = rows(snapshot.teams), python = record(snapshot.python), history = record(snapshot.history);
629
+ tabCounts.get("experts").textContent = text(agents.length);
630
+ tabCounts.get("teams").textContent = text(teams.length);
631
+ const active = agents.filter(agent => ["starting", "busy", "stopping"].includes(text(agent.state))).length;
632
+ counts.textContent = `专家 ${agents.length} · 执行中 ${active} · 团队 ${teams.length}`;
633
+ const model = object(snapshot.model) ? snapshot.model : {};
634
+ workspaceInfo.replaceChildren(facts([["工作区", snapshot.workspaceRoot], ["模型", model.model || "尚未选择"], ["Provider", model.provider || "—"],
635
+ ["历史", history.organizing ? "整理中" : history.pending === null ? "查询时更新索引" : `${history.pending} 项待整理`]]));
636
+ pythonInfo.replaceChildren(element("h3", "Python"), badge(python.state));
637
+ if (python.error)
638
+ pythonInfo.append(element("p", text(python.error), "pbot-warning"));
639
+ enabled(pythonButton, python.state !== "preparing");
640
+ const { agents: _agents, teams: _teams, ...summary } = snapshot;
641
+ statusBody.textContent = JSON.stringify(summary, null, 2);
642
+ if (snapshot.controlError)
643
+ inform(text(snapshot.controlError), "error");
644
+ renderLists();
645
+ const owned = agents.filter(value => value.owned), ids = new Set(owned.map(agent => text(agent.agentId)));
646
+ for (const input of memberFields.querySelectorAll("input[data-agent-id]"))
647
+ if (!ids.has(input.dataset.agentId))
648
+ input.parentElement?.remove();
649
+ memberFields.querySelector(":scope > p")?.remove();
650
+ for (const agent of owned) {
651
+ const id = text(agent.agentId);
652
+ let input = Array.from(memberFields.querySelectorAll("input[data-agent-id]")).find(input => input.dataset.agentId === id);
653
+ if (!input) {
654
+ input = field(memberFields, `${agent.name} · 职责`);
655
+ input.dataset.agentId = id;
656
+ }
657
+ const label = `${agent.name} · 职责`;
658
+ input.setAttribute("aria-label", label);
659
+ input.parentElement.querySelector("span").textContent = label;
660
+ }
661
+ if (!owned.length)
662
+ memberFields.append(note("还没有可加入的专家,请先在专家页创建。"));
663
+ }
664
+ async function refresh() {
665
+ const revision = ++snapshotRevision, value = await query("snapshot");
666
+ if (revision !== snapshotRevision)
667
+ return;
668
+ snapshot = value;
669
+ renderSnapshot();
670
+ connection.textContent = "已连接 · 状态自动更新";
671
+ }
672
+ button(footer, "刷新状态", refresh, "pbot-quiet");
673
+ const close = button(header, "×", () => dispose(), "pbot-close pbot-quiet");
674
+ close.setAttribute("aria-label", "关闭工作台");
675
+ close.title = "关闭 · Esc";
676
+ const dispose = ctx.effect(() => {
677
+ document.body.append(dialog);
678
+ dialog.showModal();
679
+ tabs.get("status").focus();
680
+ return () => { closed = true; lifetime.abort(); if (timer !== undefined)
681
+ window.clearTimeout(timer); dialog.close(); dialog.remove(); if (opener instanceof HTMLElement && opener.isConnected)
682
+ opener.focus(); };
683
+ });
684
+ dialog.addEventListener("cancel", event => { event.preventDefault(); void dispose(); });
685
+ async function poll() {
686
+ try {
687
+ await refresh();
688
+ if (tab === "experts" && expertId)
689
+ await loadExpert();
690
+ if (tab === "teams" && teamId)
691
+ await loadTeam();
692
+ if (pollError) {
693
+ inform("连接已恢复,状态已更新。");
694
+ pollError = false;
695
+ }
696
+ }
697
+ catch (error) {
698
+ if (!closed) {
699
+ connection.textContent = "连接中断 · 正在重试";
700
+ if (!pollError)
701
+ inform("读取状态失败:" + String(error), "error");
702
+ pollError = true;
703
+ }
704
+ }
705
+ finally {
706
+ if (!closed)
707
+ timer = window.setTimeout(() => { void poll(); }, 2000);
708
+ }
709
+ }
710
+ void poll();
711
+ }