personal-bot 0.2.4 → 0.2.6
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.
- package/CONTEXT.md +9 -7
- package/README.md +15 -8
- package/README.zh-CN.md +15 -8
- package/docs/README.md +3 -1
- package/docs/agent-collaboration.md +27 -13
- package/docs/architecture-guidelines.md +2 -2
- package/docs/capability-design.md +6 -4
- package/docs/dsh-implementation-plan.md +158 -0
- package/docs/dsh-support-proposal.md +314 -0
- package/docs/dsh.md +103 -0
- package/docs/file-storage.md +14 -9
- package/docs/history.md +29 -18
- package/docs/installation-design.md +3 -1
- package/docs/interaction-design.md +13 -0
- package/docs/release.md +3 -3
- package/docs/subagent-upgrade-proposal.md +3 -3
- package/docs/technical-design.md +29 -15
- package/docs/validation.md +49 -3
- package/lib/client.js +1071 -0
- package/lib/common/files.d.ts +2 -0
- package/lib/common/files.js +44 -0
- package/lib/common/json.d.ts +6 -0
- package/lib/common/json.js +16 -0
- package/lib/common/markdown.d.ts +5 -0
- package/lib/common/markdown.js +26 -0
- package/lib/common/paths.d.ts +1 -0
- package/lib/common/paths.js +2 -0
- package/lib/common/validation.d.ts +3 -0
- package/lib/common/validation.js +14 -0
- package/lib/common/version.d.ts +3 -0
- package/lib/common/version.js +13 -0
- package/lib/dsh.d.ts +4 -0
- package/lib/dsh.js +22 -0
- package/lib/extension/dsh/agent-events.d.ts +24 -0
- package/lib/extension/dsh/agent-events.js +242 -0
- package/lib/extension/dsh/agent-runtime.d.ts +49 -0
- package/lib/extension/dsh/agent-runtime.js +348 -0
- package/lib/extension/dsh/command.d.ts +4 -0
- package/lib/extension/dsh/command.js +83 -0
- package/lib/extension/dsh/files.d.ts +5 -0
- package/lib/extension/dsh/files.js +67 -0
- package/lib/extension/dsh/history.d.ts +15 -0
- package/lib/extension/dsh/history.js +88 -0
- package/lib/extension/dsh/management.d.ts +21 -0
- package/lib/extension/dsh/management.js +217 -0
- package/lib/extension/dsh/workspace.d.ts +48 -0
- package/lib/extension/dsh/workspace.js +290 -0
- package/lib/integrations/agent-resources.d.ts +1 -0
- package/lib/integrations/agent-resources.js +18 -0
- package/lib/integrations/dsh/dsh-agents.d.ts +30 -0
- package/lib/integrations/dsh/dsh-agents.js +104 -0
- package/lib/integrations/dsh/history-summary.d.ts +11 -0
- package/lib/integrations/dsh/history-summary.js +40 -0
- package/lib/integrations/dsh/history.d.ts +16 -0
- package/lib/integrations/dsh/history.js +156 -0
- package/lib/integrations/dsh/input.d.ts +21 -0
- package/lib/integrations/dsh/input.js +62 -0
- package/lib/integrations/dsh/messages.d.ts +36 -0
- package/lib/integrations/dsh/messages.js +11 -0
- package/lib/integrations/dsh/notifications.d.ts +8 -0
- package/lib/integrations/dsh/notifications.js +55 -0
- package/lib/integrations/dsh/resources.d.ts +10 -0
- package/lib/integrations/dsh/resources.js +41 -0
- package/lib/integrations/dsh/schema.d.ts +7 -0
- package/lib/integrations/dsh/schema.js +28 -0
- package/lib/integrations/dsh/session.d.ts +14 -0
- package/lib/integrations/dsh/session.js +59 -0
- package/lib/integrations/history-summary.d.ts +2 -0
- package/lib/integrations/history-summary.js +21 -0
- package/lib/integrations/process.d.ts +14 -0
- package/lib/integrations/process.js +48 -0
- package/lib/integrations/python-activation.d.ts +2 -0
- package/lib/integrations/python-activation.js +29 -0
- package/lib/integrations/team-context.d.ts +18 -0
- package/lib/integrations/team-context.js +57 -0
- package/lib/model/agent.d.ts +92 -0
- package/lib/model/agent.js +1 -0
- package/lib/model/history.d.ts +43 -0
- package/lib/model/history.js +1 -0
- package/lib/model/team.d.ts +85 -0
- package/lib/model/team.js +1 -0
- package/lib/services/agents/agent-notices.d.ts +13 -0
- package/lib/services/agents/agent-notices.js +25 -0
- package/lib/services/agents/agent-run.d.ts +39 -0
- package/lib/services/agents/agent-run.js +138 -0
- package/lib/services/agents/agent-service.d.ts +166 -0
- package/lib/services/agents/agent-service.js +325 -0
- package/lib/services/agents/team-service.d.ts +168 -0
- package/lib/services/agents/team-service.js +343 -0
- package/lib/services/history/history-service.d.ts +103 -0
- package/lib/services/history/history-service.js +269 -0
- package/lib/services/workspace/python-environment-service.d.ts +15 -0
- package/lib/services/workspace/python-environment-service.js +50 -0
- package/lib/services/workspace/workspace-files.d.ts +2 -0
- package/lib/services/workspace/workspace-files.js +17 -0
- package/lib/services/workspace/workspace-preparation.d.ts +2 -0
- package/lib/services/workspace/workspace-preparation.js +18 -0
- package/lib/storage/agent-files.d.ts +30 -0
- package/lib/storage/agent-files.js +166 -0
- package/lib/storage/atomic-json.d.ts +11 -0
- package/lib/storage/atomic-json.js +101 -0
- package/lib/storage/file-access.d.ts +2 -0
- package/lib/storage/file-access.js +26 -0
- package/lib/storage/history-files.d.ts +10 -0
- package/lib/storage/history-files.js +63 -0
- package/lib/storage/team-files.d.ts +16 -0
- package/lib/storage/team-files.js +111 -0
- package/lib/tools/agents/actions.d.ts +52 -0
- package/lib/tools/agents/actions.js +49 -0
- package/lib/tools/agents/contract.d.ts +163 -0
- package/lib/tools/agents/contract.js +56 -0
- package/lib/tools/agents/dsh-contract.d.ts +52 -0
- package/lib/tools/agents/dsh-contract.js +15 -0
- package/lib/tools/agents/dsh-tool.d.ts +5 -0
- package/lib/tools/agents/dsh-tool.js +38 -0
- package/lib/tools/history/actions.d.ts +70 -0
- package/lib/tools/history/actions.js +7 -0
- package/lib/tools/history/contract.d.ts +81 -0
- package/lib/tools/history/contract.js +32 -0
- package/lib/tools/history/dsh-tool.d.ts +4 -0
- package/lib/tools/history/dsh-tool.js +22 -0
- package/lib/tools/result.d.ts +18 -0
- package/lib/tools/result.js +10 -0
- package/lib/tools/teams/actions.d.ts +24 -0
- package/lib/tools/teams/actions.js +38 -0
- package/lib/tools/teams/contract.d.ts +232 -0
- package/lib/tools/teams/contract.js +67 -0
- package/lib/tools/teams/dsh-tool.d.ts +8 -0
- package/lib/tools/teams/dsh-tool.js +49 -0
- package/lib/ui/dsh/attachments.d.ts +5 -0
- package/lib/ui/dsh/attachments.js +39 -0
- package/lib/ui/dsh/elements.d.ts +17 -0
- package/lib/ui/dsh/elements.js +79 -0
- package/lib/ui/dsh/index.d.ts +4 -0
- package/lib/ui/dsh/index.js +13 -0
- package/lib/ui/dsh/management.d.ts +4 -0
- package/lib/ui/dsh/management.js +711 -0
- package/lib/ui/dsh/remote.d.ts +15 -0
- package/lib/ui/dsh/remote.js +11 -0
- package/lib/ui/dsh/styles.d.ts +1 -0
- package/lib/ui/dsh/styles.js +132 -0
- package/lib/ui/presentation.d.ts +3 -0
- package/lib/ui/presentation.js +12 -0
- package/npm-shrinkwrap.json +11123 -793
- package/package.json +94 -11
- package/python/README.md +6 -6
- package/resources/agents/coordinator.md +1 -1
- package/resources/dsh/cordis.patch.yml +3 -0
- package/resources/prompts/workspace.md +1 -1
- package/resources/references/compute.md +1 -1
- package/resources/references/task-conductor.md +6 -4
- package/resources/skills/provider-connections/SKILL.md +10 -0
- package/scripts/README.md +9 -1
- package/src/dsh.ts +22 -0
- package/src/extension/agent-events.ts +51 -46
- package/src/extension/agents-command.ts +48 -34
- package/src/extension/command.ts +42 -23
- package/src/extension/dsh/agent-events.ts +148 -0
- package/src/extension/dsh/agent-runtime.ts +251 -0
- package/src/extension/dsh/command.ts +78 -0
- package/src/extension/dsh/files.ts +61 -0
- package/src/extension/dsh/history.ts +69 -0
- package/src/extension/dsh/management.ts +108 -0
- package/src/extension/dsh/workspace.ts +206 -0
- package/src/extension/events.ts +6 -4
- package/src/extension/history.ts +15 -12
- package/src/extension/subagent-entry.ts +34 -63
- package/src/extension/teams-command.ts +53 -46
- package/src/integrations/{pi/agent-resources.ts → agent-resources.ts} +2 -2
- package/src/integrations/dsh/dsh-agents.ts +97 -0
- package/src/integrations/dsh/history-summary.ts +43 -0
- package/src/integrations/dsh/history.ts +76 -0
- package/src/integrations/dsh/input.ts +66 -0
- package/src/integrations/dsh/messages.ts +22 -0
- package/src/integrations/dsh/notifications.ts +46 -0
- package/src/integrations/dsh/resources.ts +44 -0
- package/src/integrations/dsh/schema.ts +26 -0
- package/src/integrations/dsh/session.ts +67 -0
- package/src/integrations/history-summary.ts +20 -0
- package/src/integrations/pi/history.ts +4 -14
- package/src/integrations/pi/pi-agents.ts +17 -6
- package/src/integrations/pi/team-context.ts +6 -58
- package/src/integrations/team-context.ts +60 -0
- package/src/model/agent.ts +2 -0
- package/src/model/history.ts +3 -1
- package/src/pbot.ts +1 -1
- package/src/services/agents/agent-notices.ts +28 -0
- package/src/services/agents/agent-run.ts +114 -0
- package/src/services/agents/agent-service.ts +127 -64
- package/src/services/history/history-service.ts +120 -59
- package/src/storage/agent-files.ts +17 -4
- package/src/storage/history-files.ts +30 -5
- package/src/tools/agents/actions.ts +47 -0
- package/src/tools/agents/contract.ts +8 -5
- package/src/tools/agents/dsh-contract.ts +16 -0
- package/src/tools/agents/dsh-tool.ts +41 -0
- package/src/tools/agents/tool.ts +7 -43
- package/src/tools/history/actions.ts +9 -0
- package/src/tools/history/contract.ts +34 -0
- package/src/tools/history/dsh-tool.ts +22 -0
- package/src/tools/history/tool.ts +7 -30
- package/src/tools/teams/actions.ts +38 -0
- package/src/tools/teams/contract.ts +7 -3
- package/src/tools/teams/dsh-tool.ts +55 -0
- package/src/tools/teams/tool.ts +6 -31
- package/src/ui/agent-display.ts +7 -4
- package/src/ui/dsh/attachments.ts +42 -0
- package/src/ui/dsh/elements.ts +50 -0
- package/src/ui/dsh/index.ts +18 -0
- package/src/ui/dsh/management.ts +412 -0
- package/src/ui/dsh/remote.ts +20 -0
- package/src/ui/dsh/styles.ts +132 -0
- package/src/ui/pi/menu.ts +37 -0
- package/src/ui/pi/reader.ts +46 -0
- package/src/ui/presentation.ts +12 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
3
|
+
import type { Agent } from "@deepseek-ai/dsh-agent";
|
|
4
|
+
import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
|
|
5
|
+
import { AgentFiles } from "../../storage/agent-files.js";
|
|
6
|
+
import { HistoryFiles } from "../../storage/history-files.js";
|
|
7
|
+
import { HistoryService } from "../../services/history/history-service.js";
|
|
8
|
+
import { DshHistory } from "../../integrations/dsh/history.js";
|
|
9
|
+
import { dshHistorySummarizer } from "../../integrations/dsh/history-summary.js";
|
|
10
|
+
import { finalDshAnswer } from "../../integrations/dsh/messages.js";
|
|
11
|
+
|
|
12
|
+
/** One server-owned organizer per workspace, shared by its parent Sessions. */
|
|
13
|
+
export class DshHistoryWork {
|
|
14
|
+
readonly service: HistoryService;
|
|
15
|
+
readonly #parents = new Set<Agent>();
|
|
16
|
+
#controller: AbortController | undefined;
|
|
17
|
+
#work: Promise<void> | undefined;
|
|
18
|
+
#closing: Promise<void> | undefined;
|
|
19
|
+
constructor(private readonly ctx: Context, root: string) {
|
|
20
|
+
const agents = new AgentFiles(dshHomePath("pbot", "subagents"), "dsh");
|
|
21
|
+
this.service = new HistoryService(root, new HistoryFiles(join(root, ".dsh/pbot/history"), "dsh"),
|
|
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")));
|
|
24
|
+
}
|
|
25
|
+
attach(agent: Agent) { this.#parents.add(agent); return () => { this.#parents.delete(agent); }; }
|
|
26
|
+
cancel() { this.#controller?.abort(); }
|
|
27
|
+
schedule(agent: Agent) {
|
|
28
|
+
if (this.#closing || [...this.#parents].some(parent => parent.status !== "idle" || parent.inbox.nextTurn.length || parent.inbox.nextStep.length)) return;
|
|
29
|
+
this.cancel();
|
|
30
|
+
const controller = new AbortController(), selection = agent.model.current(), sessionId = agent.id;
|
|
31
|
+
this.#controller = controller;
|
|
32
|
+
const signal = controller.signal;
|
|
33
|
+
const work = (this.#work ?? Promise.resolve()).catch(() => {}).then(async () => {
|
|
34
|
+
signal.throwIfAborted();
|
|
35
|
+
await this.service.refresh({ host: "dsh", sessionId }, signal);
|
|
36
|
+
signal.throwIfAborted();
|
|
37
|
+
await this.service.organize(dshHistorySummarizer(this.ctx, selection, sessionId), signal);
|
|
38
|
+
}).catch(error => { if (!signal.aborted) this.ctx.logger.warn("pbot 历史整理未完成:" + String(error)); });
|
|
39
|
+
this.#work = work;
|
|
40
|
+
void work.finally(() => { if (this.#work === work) { this.#work = undefined; this.#controller = undefined; } });
|
|
41
|
+
}
|
|
42
|
+
close() {
|
|
43
|
+
if (this.#closing) return this.#closing;
|
|
44
|
+
this.cancel();
|
|
45
|
+
this.#closing = (async () => { await this.#work; await this.service.close(); this.#parents.clear(); })();
|
|
46
|
+
return this.#closing;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function registerDshHistoryEvents(ctx: Context, agent: Agent, work: DshHistoryWork, check: () => void) {
|
|
51
|
+
let normal = false, final = false;
|
|
52
|
+
ctx.effect(() => work.attach(agent));
|
|
53
|
+
ctx.on("session/event", (session, event) => {
|
|
54
|
+
if (session !== agent.session) return;
|
|
55
|
+
if (event.type === "turn/start") { normal = false; final = false; }
|
|
56
|
+
if (event.type === "assistant/message") final = Boolean(finalDshAnswer(event).trim());
|
|
57
|
+
if (event.type === "agent/inbox/spliced" && event.data.inserted.some(message => message.source.kind === "user")) { normal = false; work.cancel(); }
|
|
58
|
+
if (event.type === "turn/end") normal = event.data.reason.kind === "completed" && final;
|
|
59
|
+
});
|
|
60
|
+
ctx.on("agent/pre-step", async (_payload, next) => { work.cancel(); check(); return next(); });
|
|
61
|
+
ctx.on("agent/status", ({ agent: changed, status }) => {
|
|
62
|
+
if (changed !== agent || status !== "idle" || !normal) return;
|
|
63
|
+
normal = false;
|
|
64
|
+
if (!agent.inbox.nextTurn.length && !agent.inbox.nextStep.length) {
|
|
65
|
+
try { check(); } catch (error) { if ((error as Error).name === "AbortError") return; throw error; }
|
|
66
|
+
work.schedule(agent);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
2
|
+
import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
3
|
+
import type {} from "@deepseek-ai/dsh-typert-registry";
|
|
4
|
+
import { SessionId } from "@deepseek-ai/dsh-session";
|
|
5
|
+
import { extractSessionEventText } from "@deepseek-ai/dsh-session-query";
|
|
6
|
+
import { Type, type Static } from "typebox";
|
|
7
|
+
import { json, type Json } from "../../common/json.js";
|
|
8
|
+
import { requireThat, schema } from "../../common/validation.js";
|
|
9
|
+
import { packageVersion } from "../../common/version.js";
|
|
10
|
+
import { historyInput } from "../../tools/history/contract.js";
|
|
11
|
+
import { executeHistory } from "../../tools/history/actions.js";
|
|
12
|
+
import { pbotReadRemote } from "../../ui/dsh/remote.js";
|
|
13
|
+
import type { DshWorkspaces, DshWorkspace } from "./workspace.js";
|
|
14
|
+
|
|
15
|
+
const requestSchema = Type.Object({ parentSessionId: Type.String({ minLength: 1 }), input: Type.Optional(Type.Unknown()) }, { additionalProperties: false });
|
|
16
|
+
const expertSchema = Type.Object({ agentId: Type.String({ minLength: 1 }), offset: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
17
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 100 })), seq: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
18
|
+
textOffset: Type.Optional(Type.Integer({ minimum: 0 })), textLimit: Type.Optional(Type.Integer({ minimum: 2, maximum: 24000 })),
|
|
19
|
+
format: Type.Optional(Type.Union([Type.Literal("text"), Type.Literal("json")])) }, { additionalProperties: false });
|
|
20
|
+
const teamSchema = Type.Object({ teamId: Type.String({ minLength: 1 }), afterSeq: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
21
|
+
seq: Type.Optional(Type.Integer({ minimum: 1 })), offset: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
22
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 16000 })) }, { additionalProperties: false });
|
|
23
|
+
const validRequest = schema(requestSchema), validExpert = schema(expertSchema), validTeam = schema(teamSchema);
|
|
24
|
+
|
|
25
|
+
export function dshStatus(workspaces: DshWorkspaces, workspace: DshWorkspace): Json {
|
|
26
|
+
workspace.check();
|
|
27
|
+
const agents = workspace.agents.list(workspace.owner);
|
|
28
|
+
return json({ host: "dsh", release: packageVersion, workspaceRoot: workspace.root, sessionId: workspace.agent.id,
|
|
29
|
+
model: workspace.agent.model.current() ?? null, python: workspaces.python.status,
|
|
30
|
+
controlError: workspace.controlError ?? null, history: workspace.history.status,
|
|
31
|
+
agents, teams: workspace.teams.list(workspace.owner, agents) });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Native Remote owns transport and access checks. These workspace reads do not admit or cancel execution. */
|
|
35
|
+
export class PbotManagement extends TypertRemoteService {
|
|
36
|
+
constructor(ctx: Context, private readonly workspaces: DshWorkspaces) { super(ctx, "pbotManagement", { namespace: "pbot" }); }
|
|
37
|
+
|
|
38
|
+
private async workspace(request: Json, signal: AbortSignal) {
|
|
39
|
+
signal.throwIfAborted(); requireThat(validRequest(request), "管理查询参数无效。");
|
|
40
|
+
const args = request as Static<typeof requestSchema>;
|
|
41
|
+
const agent = this.ctx.agents.get(SessionId(args.parentSessionId));
|
|
42
|
+
requireThat(agent, "父会话未加载或已关闭,请在原生会话页面重新打开。");
|
|
43
|
+
const workspace = await this.workspaces.get(agent);
|
|
44
|
+
signal.throwIfAborted(); workspace.check();
|
|
45
|
+
return { workspace, input: args.input, signal: AbortSignal.any([signal, workspace.lifetime.signal]) };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Remote
|
|
49
|
+
async snapshot(request: Json, signal: AbortSignal): Promise<Json> {
|
|
50
|
+
const { workspace, input } = await this.workspace(request, signal);
|
|
51
|
+
requireThat(input === undefined, "工作区状态查询不接受额外参数。");
|
|
52
|
+
return dshStatus(this.workspaces, workspace);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@Remote
|
|
56
|
+
async expert(request: Json, signal: AbortSignal): Promise<Json> {
|
|
57
|
+
const current = await this.workspace(request, signal), { workspace, input } = current;
|
|
58
|
+
requireThat(validExpert(input), "专家记录查询参数无效。");
|
|
59
|
+
const args = input as Static<typeof expertSchema>;
|
|
60
|
+
const view = workspace.agents.status(args.agentId, workspace.owner);
|
|
61
|
+
using observation = await this.ctx.sessionQuery.observeSession(SessionId(view.agentId), { projectionMode: "none", signal: current.signal });
|
|
62
|
+
workspace.check(); current.signal.throwIfAborted();
|
|
63
|
+
const header = observation.header;
|
|
64
|
+
requireThat(header.id === view.agentId && header.cwd === view.cwd && header.parentSession === workspace.agent.id &&
|
|
65
|
+
header.origin === "subagent" && !header.isSeeded, "原生专家记录的身份或归属不匹配。");
|
|
66
|
+
const events = observation.events;
|
|
67
|
+
if (args.seq !== undefined) {
|
|
68
|
+
const event = events.find(value => value.seq === args.seq);
|
|
69
|
+
requireThat(event, "原生记录位置不存在。");
|
|
70
|
+
const text = (args.format === "text" ? extractSessionEventText(event) : "") || JSON.stringify(event, null, 2), offset = args.textOffset ?? 0;
|
|
71
|
+
requireThat(offset <= text.length, "原生记录分页位置超出范围。");
|
|
72
|
+
let end = Math.min(text.length, offset + (args.textLimit ?? 12000));
|
|
73
|
+
if (end < text.length && /[\uD800-\uDBFF]/.test(text[end - 1]!)) end--;
|
|
74
|
+
return json({ agent: view, cursor: observation.cursor, seq: event.seq, offset, text: text.slice(offset, end),
|
|
75
|
+
totalLength: text.length, nextOffset: end < text.length ? end : null });
|
|
76
|
+
}
|
|
77
|
+
const limit = args.limit ?? 30, offset = args.offset ?? Math.max(0, events.length - limit);
|
|
78
|
+
requireThat(offset <= events.length, "原生记录分页位置超出范围。");
|
|
79
|
+
return json({ agent: view, header, cursor: observation.cursor, total: events.length, offset,
|
|
80
|
+
nextOffset: offset + limit < events.length ? offset + limit : null,
|
|
81
|
+
events: events.slice(offset, offset + limit).map(event => ({ seq: event.seq, type: event.type, time: event.time,
|
|
82
|
+
...(event.type === "user/message" ? { source: event.data.source.kind } : {}), preview: extractSessionEventText(event).slice(0, 800) })) });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@Remote
|
|
86
|
+
async team(request: Json, signal: AbortSignal): Promise<Json> {
|
|
87
|
+
const { workspace, input } = await this.workspace(request, signal);
|
|
88
|
+
requireThat(validTeam(input), "团队记录查询参数无效。");
|
|
89
|
+
const args = input as Static<typeof teamSchema>;
|
|
90
|
+
return json({ team: workspace.teams.status(args.teamId, workspace.owner, workspace.agents.list(workspace.owner)),
|
|
91
|
+
page: workspace.teams.read(args.teamId, { kind: "user", owner: workspace.owner }, args) });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@Remote
|
|
95
|
+
async history(request: Json, signal: AbortSignal): Promise<Json> {
|
|
96
|
+
const current = await this.workspace(request, signal), { workspace, input } = current;
|
|
97
|
+
const result = await executeHistory(workspace.history, historyInput(input), { host: "dsh", sessionId: workspace.agent.id }, current.signal);
|
|
98
|
+
workspace.check(); current.signal.throwIfAborted(); return json(result);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
declare module "@deepseek-ai/cordis" { interface Context { pbotManagement: PbotManagement } }
|
|
103
|
+
|
|
104
|
+
export function registerDshManagement(ctx: Context, workspaces: DshWorkspaces) {
|
|
105
|
+
ctx.typert.register({ package: pbotReadRemote.package, face: "host", schemas: [],
|
|
106
|
+
model: { services: [], events: [], objects: [] }, invocations: pbotReadRemote.descriptors });
|
|
107
|
+
return new PbotManagement(ctx, workspaces);
|
|
108
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
2
|
+
import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
|
|
3
|
+
import type { Agent } from "@deepseek-ai/dsh-agent";
|
|
4
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
5
|
+
import type { AgentOwner } from "../../model/agent.js";
|
|
6
|
+
import { DshAgents } from "../../integrations/dsh/dsh-agents.js";
|
|
7
|
+
import { dshResources, dshExpertTools } from "../../integrations/dsh/resources.js";
|
|
8
|
+
import { AgentService } from "../../services/agents/agent-service.js";
|
|
9
|
+
import { TeamService } from "../../services/agents/team-service.js";
|
|
10
|
+
import { registerDshAgentTool } from "../../tools/agents/dsh-tool.js";
|
|
11
|
+
import { registerDshTeamTool, registerDshTeamMessageTool } from "../../tools/teams/dsh-tool.js";
|
|
12
|
+
import { DshAgentRuntime } from "./agent-runtime.js";
|
|
13
|
+
import { DshAgentEvents } from "./agent-events.js";
|
|
14
|
+
import { registerDshFileEvents } from "./files.js";
|
|
15
|
+
import { DshHistoryWork, registerDshHistoryEvents } from "./history.js";
|
|
16
|
+
import type { HistoryService } from "../../services/history/history-service.js";
|
|
17
|
+
import { registerDshHistoryTool } from "../../tools/history/dsh-tool.js";
|
|
18
|
+
import { activatePython } from "../../integrations/python-activation.js";
|
|
19
|
+
import { PythonEnvironmentService } from "../../services/workspace/python-environment-service.js";
|
|
20
|
+
import { requireThat } from "../../common/validation.js";
|
|
21
|
+
|
|
22
|
+
export const dshDependencies = ["agents", "sessions", "sessionPersistence", "sessionQuery", "llm", "tools", "systemPrompt", "skills", "fs", "attachments", "workingDirectory", "approval"];
|
|
23
|
+
|
|
24
|
+
export class DshWorkspace {
|
|
25
|
+
readonly lifetime = new AbortController();
|
|
26
|
+
readonly host: DshAgents;
|
|
27
|
+
readonly agents: AgentService;
|
|
28
|
+
readonly teams: TeamService;
|
|
29
|
+
readonly history: HistoryService;
|
|
30
|
+
readonly owner: AgentOwner;
|
|
31
|
+
readonly #experts = new Map<string, DshAgentRuntime>();
|
|
32
|
+
#scope: { dispose(): Promise<void> } | undefined;
|
|
33
|
+
#events: DshAgentEvents | undefined;
|
|
34
|
+
#closing: Promise<void> | undefined;
|
|
35
|
+
constructor(readonly ctx: Context, readonly root: string, readonly agent: Agent,
|
|
36
|
+
readonly python: PythonEnvironmentService, readonly component: AbortSignal,
|
|
37
|
+
private readonly historyWork: DshHistoryWork, private readonly releaseHistory: () => Promise<void>) {
|
|
38
|
+
this.history = historyWork.service;
|
|
39
|
+
this.owner = { sessionId: agent.id, leaf: null, branch: [] };
|
|
40
|
+
this.host = new DshAgents(ctx, agent, {
|
|
41
|
+
setup: (d, _directory, generation) => async (agentCtx, child) => {
|
|
42
|
+
for (const key of ["noExtensions", "noPromptTemplates", "noContextFiles"] as const) requireThat(d.options[key] === undefined, "dsh 不支持宿主选项:" + key);
|
|
43
|
+
await agentCtx.plugin({ name: "pbot-expert", inject: dshDependencies, apply: async scope => {
|
|
44
|
+
const runtime = new DshAgentRuntime(scope, child, d, generation, this.agents.files, this.teams,
|
|
45
|
+
{ check: this.check, release: () => this.host.release(d.agentId, generation) });
|
|
46
|
+
this.#experts.set(d.agentId, runtime);
|
|
47
|
+
scope.effect(() => () => { if (this.#experts.get(d.agentId) === runtime) this.#experts.delete(d.agentId); });
|
|
48
|
+
await dshResources(scope, child, root, "general-purpose", () => python.status, runtime.check);
|
|
49
|
+
const teamTool = dshExpertTools(scope, child, d.options);
|
|
50
|
+
if (teamTool) registerDshTeamMessageTool(scope, child, this.teams, runtime.actor, runtime.check);
|
|
51
|
+
registerDshFileEvents(scope, child, root, true, runtime.check);
|
|
52
|
+
runtime.initialize(teamTool);
|
|
53
|
+
} });
|
|
54
|
+
return { commit: this.check };
|
|
55
|
+
},
|
|
56
|
+
ready: (child, generation) => {
|
|
57
|
+
const runtime = this.#experts.get(child.id);
|
|
58
|
+
requireThat(runtime?.generation === generation, "专家执行组件未就绪。"); runtime.start();
|
|
59
|
+
},
|
|
60
|
+
stopping: async (child, generation) => { const runtime = this.#experts.get(child.id); if (runtime?.generation === generation) await runtime.stop(); },
|
|
61
|
+
}, this.lifetime.signal);
|
|
62
|
+
this.agents = new AgentService(root, dshHomePath("pbot", "subagents"), this.host);
|
|
63
|
+
this.teams = new TeamService(root, this.agents.files, this.agents.teamFiles);
|
|
64
|
+
}
|
|
65
|
+
check = () => { this.component.throwIfAborted(); this.lifetime.signal.throwIfAborted(); };
|
|
66
|
+
get controlError() { return this.#events?.error; }
|
|
67
|
+
async open() {
|
|
68
|
+
this.check();
|
|
69
|
+
this.#scope = await this.agent.ctx.plugin({ name: "pbot-coordinator", inject: dshDependencies, apply: async ctx => {
|
|
70
|
+
await dshResources(ctx, this.agent, this.root, "coordinator", () => this.python.status, this.check);
|
|
71
|
+
registerDshFileEvents(ctx, this.agent, this.root, false, this.check);
|
|
72
|
+
registerDshAgentTool(ctx, this.agent, this.agents, this.owner, this.check);
|
|
73
|
+
registerDshTeamTool(ctx, this.agent, this.teams, this.agents, this.owner, this.check);
|
|
74
|
+
registerDshTeamMessageTool(ctx, this.agent, this.teams, () => ({ kind: "coordinator", owner: this.owner }), this.check);
|
|
75
|
+
registerDshHistoryTool(ctx, this.agent, this.history, this.lifetime.signal, this.check);
|
|
76
|
+
registerDshHistoryEvents(ctx, this.agent, this.historyWork, this.check);
|
|
77
|
+
this.#events = new DshAgentEvents(ctx, this.agent, this.agents, this.teams, this.owner, this.check);
|
|
78
|
+
this.#events.start();
|
|
79
|
+
} });
|
|
80
|
+
this.check();
|
|
81
|
+
}
|
|
82
|
+
expert(id: string) {
|
|
83
|
+
this.check(); const view = this.agents.status(id, this.owner), runtime = this.#experts.get(id);
|
|
84
|
+
requireThat(view.owned, "专家不属于当前父会话。");
|
|
85
|
+
requireThat(view.alive, "专家已停止;需要明确恢复后才能对话。");
|
|
86
|
+
requireThat(view.state !== "starting", "专家尚未就绪,请等待启动或恢复完成。");
|
|
87
|
+
requireThat(runtime?.generation === view.generation, "原生专家由其他执行实例持有;当前组件不能接管对话。");
|
|
88
|
+
return runtime;
|
|
89
|
+
}
|
|
90
|
+
close() {
|
|
91
|
+
if (this.#closing) return this.#closing;
|
|
92
|
+
this.lifetime.abort();
|
|
93
|
+
this.#closing = (async () => {
|
|
94
|
+
const errors: unknown[] = [];
|
|
95
|
+
try { await this.#events?.close(); } catch (error) { errors.push(error); }
|
|
96
|
+
try { this.teams.pauseAll(this.owner, "父会话或执行组件关闭"); } catch (error) { errors.push(error); }
|
|
97
|
+
try { await this.agents.close(this.owner); } catch (error) { errors.push(error); }
|
|
98
|
+
try { await this.host.close(); } catch (error) { errors.push(error); }
|
|
99
|
+
this.teams.dispose();
|
|
100
|
+
try { await this.#scope?.dispose(); } catch (error) { errors.push(error); }
|
|
101
|
+
try { await this.releaseHistory(); } catch (error) { errors.push(error); }
|
|
102
|
+
if (errors.length) throw new AggregateError(errors, "dsh 工作区关闭失败;请核对执行资源和控制记录。");
|
|
103
|
+
})();
|
|
104
|
+
return this.#closing;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The execution component owns Python; closing one Session must not undo its activation. */
|
|
109
|
+
export class DshWorkspaces {
|
|
110
|
+
readonly python = new PythonEnvironmentService(process.cwd());
|
|
111
|
+
readonly #parents = new Map<Agent, Promise<DshWorkspace>>();
|
|
112
|
+
readonly #parentEffects = new Map<Agent, () => Promise<void>>();
|
|
113
|
+
readonly #histories = new Map<string, { work: DshHistoryWork; users: number }>();
|
|
114
|
+
readonly #lifetime = new AbortController();
|
|
115
|
+
#restorePython: (() => void) | undefined;
|
|
116
|
+
#preparing: Promise<{ directory: string; python: string }> | undefined;
|
|
117
|
+
#closing: Promise<void> | undefined;
|
|
118
|
+
constructor(readonly ctx: Context) {}
|
|
119
|
+
|
|
120
|
+
private async history(root: string): Promise<{ work: DshHistoryWork; release(): Promise<void> }> {
|
|
121
|
+
this.#lifetime.signal.throwIfAborted();
|
|
122
|
+
let entry = this.#histories.get(root);
|
|
123
|
+
if (entry && entry.users === 0) {
|
|
124
|
+
await entry.work.close(); this.#lifetime.signal.throwIfAborted();
|
|
125
|
+
if (this.#histories.get(root) === entry) this.#histories.delete(root);
|
|
126
|
+
return this.history(root);
|
|
127
|
+
}
|
|
128
|
+
if (!entry) { entry = { work: new DshHistoryWork(this.ctx, root), users: 0 }; this.#histories.set(root, entry); }
|
|
129
|
+
const owned = entry; owned.users++;
|
|
130
|
+
let released = false;
|
|
131
|
+
return { work: owned.work, release: async () => {
|
|
132
|
+
if (released) return; released = true;
|
|
133
|
+
if (--owned.users === 0) {
|
|
134
|
+
try { await owned.work.close(); } finally { if (this.#histories.get(root) === owned) this.#histories.delete(root); }
|
|
135
|
+
}
|
|
136
|
+
} };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async open(agent: Agent) {
|
|
140
|
+
this.#lifetime.signal.throwIfAborted();
|
|
141
|
+
requireThat(this.ctx.agents.roots().includes(agent) && agent.session.header.origin !== "subagent", "专家不拥有协调者管理入口。");
|
|
142
|
+
const current = this.#parents.get(agent);
|
|
143
|
+
if (current) return current;
|
|
144
|
+
const opening = (async () => {
|
|
145
|
+
const root = await realpath(agent.session.header.cwd ?? process.cwd());
|
|
146
|
+
this.#lifetime.signal.throwIfAborted();
|
|
147
|
+
const history = await this.history(root);
|
|
148
|
+
let workspace: DshWorkspace | undefined;
|
|
149
|
+
try {
|
|
150
|
+
workspace = new DshWorkspace(this.ctx, root, agent, this.python, this.#lifetime.signal, history.work, history.release);
|
|
151
|
+
const owned = workspace;
|
|
152
|
+
const dispose = agent.ctx.effect(() => async () => {
|
|
153
|
+
this.#parents.delete(agent); this.#parentEffects.delete(agent); await owned.close();
|
|
154
|
+
});
|
|
155
|
+
this.#parentEffects.set(agent, dispose);
|
|
156
|
+
await workspace.open(); this.#lifetime.signal.throwIfAborted(); return workspace;
|
|
157
|
+
} catch (error) {
|
|
158
|
+
try {
|
|
159
|
+
const dispose = this.#parentEffects.get(agent);
|
|
160
|
+
if (dispose) await dispose(); else if (workspace) await workspace.close(); else await history.release();
|
|
161
|
+
} catch (cleanup) { throw new AggregateError([error, cleanup], "dsh 工作区装配与清理均未完成。"); }
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
})();
|
|
165
|
+
this.#parents.set(agent, opening);
|
|
166
|
+
try { return await opening; }
|
|
167
|
+
catch (error) { this.#parents.delete(agent); throw error; }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async get(agent: Agent) {
|
|
171
|
+
this.#lifetime.signal.throwIfAborted();
|
|
172
|
+
requireThat(this.ctx.agents.get(agent.id) === agent, "父会话已关闭。");
|
|
173
|
+
const workspace = await this.open(agent);
|
|
174
|
+
workspace.check();
|
|
175
|
+
return workspace;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
preparePython() {
|
|
179
|
+
this.#lifetime.signal.throwIfAborted();
|
|
180
|
+
if (this.#preparing) return this.#preparing;
|
|
181
|
+
this.#preparing = (async () => {
|
|
182
|
+
const result = await this.python.setup(this.#lifetime.signal);
|
|
183
|
+
this.#lifetime.signal.throwIfAborted();
|
|
184
|
+
if (!this.#restorePython) this.#restorePython = activatePython(result.directory);
|
|
185
|
+
return result;
|
|
186
|
+
})().finally(() => { this.#preparing = undefined; });
|
|
187
|
+
return this.#preparing;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
close() {
|
|
191
|
+
if (this.#closing) return this.#closing;
|
|
192
|
+
this.#lifetime.abort();
|
|
193
|
+
this.#closing = (async () => {
|
|
194
|
+
try {
|
|
195
|
+
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()));
|
|
197
|
+
const effects = await Promise.allSettled([...this.#parentEffects.values()].map(dispose => dispose()));
|
|
198
|
+
const histories = await Promise.allSettled([...this.#histories.values()].map(value => value.work.close()));
|
|
199
|
+
await this.python.close();
|
|
200
|
+
const errors = [...new Set([...closed, ...effects, ...histories].flatMap(value => value.status === "rejected" ? [value.reason] : []))];
|
|
201
|
+
if (errors.length) throw new AggregateError(errors, "部分 dsh 工作区未能完成关闭。");
|
|
202
|
+
} finally { this.#parents.clear(); this.#parentEffects.clear(); this.#histories.clear(); this.#restorePython?.(); this.#restorePython = undefined; }
|
|
203
|
+
})();
|
|
204
|
+
return this.#closing;
|
|
205
|
+
}
|
|
206
|
+
}
|
package/src/extension/events.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { resourceRoot } from "../common/paths.js";
|
|
7
|
-
import { agentText } from "../integrations/
|
|
7
|
+
import { agentText } from "../integrations/agent-resources.js";
|
|
8
8
|
import type { Confirmations } from "../ui/confirmation.js";
|
|
9
9
|
import type { Lifecycle } from "./lifecycle.js";
|
|
10
10
|
|
|
@@ -12,12 +12,14 @@ export function registerEvents(pi: ExtensionAPI, lifecycle: Lifecycle, confirmat
|
|
|
12
12
|
pi.on("session_start", async (_event, ctx) => {
|
|
13
13
|
try {
|
|
14
14
|
await lifecycle.open(ctx.cwd);
|
|
15
|
-
ctx.ui.
|
|
15
|
+
if (ctx.mode === "tui") ctx.ui.setStatus("pbot-python", "Python 准备中");
|
|
16
|
+
else ctx.ui.notify("正在准备共享 Python 环境;首次安装可能需要数分钟。", "info");
|
|
16
17
|
try { await lifecycle.get().preparePython(); }
|
|
17
18
|
catch (error) {
|
|
18
19
|
const message = error instanceof Error ? error.message : String(error);
|
|
19
|
-
if (ctx.
|
|
20
|
-
|
|
20
|
+
if (ctx.mode === "tui") ctx.ui.notify("Python 未就绪;/pbot status 查看详情。", "warning");
|
|
21
|
+
else if (ctx.hasUI) ctx.ui.notify(message, "warning"); else console.error(message);
|
|
22
|
+
} finally { if (ctx.mode === "tui") ctx.ui.setStatus("pbot-python", undefined); }
|
|
21
23
|
} catch (error) {
|
|
22
24
|
if (ctx.hasUI) ctx.ui.notify(error instanceof Error ? error.message : "pbot 启动失败。", "error");
|
|
23
25
|
else throw error;
|
package/src/extension/history.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { historyLocation, historySummarizer } from "../integrations/pi/history.js";
|
|
3
3
|
import type { Lifecycle, WorkspaceInstance } from "./lifecycle.js";
|
|
4
|
+
import { readPage } from "../ui/pi/reader.js";
|
|
5
|
+
import { choose } from "../ui/pi/menu.js";
|
|
4
6
|
|
|
5
7
|
export function registerHistoryEvents(pi: ExtensionAPI, lifecycle: Lifecycle) {
|
|
6
8
|
pi.on("session_start", async (_event, ctx) => {
|
|
@@ -25,8 +27,8 @@ export function registerHistoryEvents(pi: ExtensionAPI, lifecycle: Lifecycle) {
|
|
|
25
27
|
});
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
const label = (node: { kind: string; from: string; ready: boolean; preview: string }) =>
|
|
29
|
-
`${node.from.slice(0,
|
|
30
|
+
const label = (node: { host: string; kind: string; from: string; ready: boolean; preview: string }) =>
|
|
31
|
+
`${node.host} · ${node.from.slice(0, 10)} · ${node.kind === "session" ? "会话" : node.kind === "summary" ? "摘要" : "问答"}${node.ready ? "" : "(待整理)"} · ${node.preview.replace(/\s+/g, " ").slice(0, 48)}`;
|
|
30
32
|
|
|
31
33
|
export async function showHistory(instance: WorkspaceInstance, ctx: ExtensionCommandContext, query: string) {
|
|
32
34
|
await instance.history.refresh(historyLocation(ctx), instance.signal(ctx.signal));
|
|
@@ -37,35 +39,36 @@ export async function showHistory(instance: WorkspaceInstance, ctx: ExtensionCom
|
|
|
37
39
|
for (;;) {
|
|
38
40
|
instance.check();
|
|
39
41
|
const result = service.search({ query, offset });
|
|
40
|
-
const choices = result.matches.map((node, index) => `${index + 1}. ${label(node)}
|
|
41
|
-
const more = "下一页", search = "
|
|
42
|
-
const selected = await ctx
|
|
42
|
+
const choices = result.matches.map((node, index) => `${index + 1}. ${label(node)}`);
|
|
43
|
+
const more = "下一页", previous = "上一页", search = "搜索", back = "返回";
|
|
44
|
+
const selected = await choose(ctx, `历史 · ${query || "最近会话"} · ${result.total} 项`, [...choices, ...(offset ? [previous] : []), ...(result.nextOffset === null ? [] : [more]), search, back]);
|
|
43
45
|
if (!selected || selected === back) return;
|
|
44
46
|
if (selected === more) { offset = result.nextOffset!; continue; }
|
|
47
|
+
if (selected === previous) { offset = Math.max(0, offset - 20); continue; }
|
|
45
48
|
if (selected === search) {
|
|
46
49
|
const value = await ctx.ui.input("全文搜索(留空列出会话)", query);
|
|
47
50
|
if (value !== undefined) { query = value; offset = 0; }
|
|
48
51
|
continue;
|
|
49
52
|
}
|
|
50
|
-
let
|
|
53
|
+
let current = result.matches[choices.indexOf(selected)]!, id = current.id, source = current.host, textOffset = 0, childrenOffset = 0;
|
|
51
54
|
for (;;) {
|
|
52
55
|
instance.check();
|
|
53
|
-
const node = service.read(id, textOffset, 12000, childrenOffset);
|
|
54
|
-
const body = "
|
|
56
|
+
const node = service.read(id, textOffset, 12000, childrenOffset, source);
|
|
57
|
+
const body = "正文", nextText = "后续正文", nextChildren = "更多下级", first = "回到开头";
|
|
55
58
|
const links = [...node.parents.map(parent => ({ ...parent, direction: "↑" })), ...node.children.map(child => ({ ...child, direction: "↓" }))];
|
|
56
59
|
const names = links.map((value, index) => `${index + 1}. ${value.direction} ${label(value)}`);
|
|
57
|
-
const action = await ctx
|
|
60
|
+
const action = await choose(ctx, label(node), [body, ...names, ...(node.nextOffset === null ? [] : [nextText]),
|
|
58
61
|
...(node.nextChildrenOffset === null ? [] : [nextChildren]), ...(textOffset || childrenOffset ? [first] : []), back]);
|
|
59
62
|
if (!action || action === back) break;
|
|
60
63
|
if (action === body || action === nextText) {
|
|
61
64
|
if (action === nextText) textOffset = node.nextOffset!;
|
|
62
|
-
const page = service.read(id, textOffset, 12000, childrenOffset);
|
|
63
|
-
await ctx
|
|
65
|
+
const page = service.read(id, textOffset, 12000, childrenOffset, source);
|
|
66
|
+
await readPage(ctx, `历史正文 · ${textOffset + 1}–${textOffset + page.text.length}/${page.totalLength}`, page.text || "摘要待整理,请展开下级记录。");
|
|
64
67
|
continue;
|
|
65
68
|
}
|
|
66
69
|
if (action === nextChildren) { childrenOffset = node.nextChildrenOffset!; continue; }
|
|
67
70
|
if (action === first) { textOffset = 0; childrenOffset = 0; continue; }
|
|
68
|
-
|
|
71
|
+
const link = links[names.indexOf(action)]!; id = link.id; source = link.host; textOffset = 0; childrenOffset = 0;
|
|
69
72
|
}
|
|
70
73
|
}
|
|
71
74
|
}
|