dsh-agent-message 1.5.0 → 1.6.0
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/README.en.md +28 -16
- package/README.md +28 -16
- package/lib/client.js +100 -231
- package/lib/index.js +106 -15
- package/package.json +18 -10
package/README.en.md
CHANGED
|
@@ -12,8 +12,8 @@ English | [中文](./README.md)
|
|
|
12
12
|
|
|
13
13
|
In DeepSeek Harness, a single process hosts multiple Agent sessions at once. This plugin equips each session with three tools so they can "message" each other:
|
|
14
14
|
|
|
15
|
-
- Before sending, first **list every sendable session** (
|
|
16
|
-
- Once found, **deliver the message to the target session** — ordinary messages always enter a new independent turn; if the target is offline (not loaded since the last process restart), the plugin resumes it through Harness's public API, delivers the message, and
|
|
15
|
+
- Before sending, first **list every sendable independent session** (non-archived, excluding actual subagents, including offline sessions that have not been reopened), and find the target by its title;
|
|
16
|
+
- Once found, **deliver the message to the target session** — ordinary messages always enter a new independent turn; if the target is offline (not loaded since the last process restart), the plugin resumes it through Harness's public API, delivers the message, and keeps the handle loaded for later communication until plugin teardown;
|
|
17
17
|
- When needed, **query the delivery status of a message on demand** (queued / claimed / discarded / unknown), with the target runtime status reported separately for supervision scenarios.
|
|
18
18
|
|
|
19
19
|
Typical scenarios: an orchestrator Agent dispatching work to a developer Agent, two Agents collaborating in a relay, a main session sending instructions to a test session, or a supervisor Agent watching over several workers.
|
|
@@ -25,15 +25,16 @@ Typical scenarios: an orchestrator Agent dispatching work to a developer Agent,
|
|
|
25
25
|
| `list_peer_agents` | List all **sendable independent sessions**: non-archived, excluding actual subagents while retaining ordinary forks; returns id, title, working directory, and runtime status |
|
|
26
26
|
| `send_agent_message` | Send a message to a session id; `followup` creates an independent turn by default and offline targets are resumed automatically; explicit modes are `followup`, `inject`, and `steer` |
|
|
27
27
|
| `check_delivery` | Query receipts on demand (pending/claimed/discarded/unknown); a successful send first returns accepted, while pre-admission failure is a tool error; explicit message ids remain queryable after restart |
|
|
28
|
-
|
|
|
29
|
-
| Visible Agent message card | Relay keeps its true plugin provenance while the Client presents it as a left-aligned Agent message card; `From Session · <name>:` opens the sender by click or keyboard |
|
|
28
|
+
| Native Harness `@` session reference | Reuse Harness's structured Session Reference directly. The current Agent receives the stable Session ID plus a bounded, read-only snapshot marked as untrusted. Referencing alone does not send a message or wake the target |
|
|
29
|
+
| Visible Agent message card | Relay keeps its true plugin provenance while the Client presents it as a left-aligned Agent message card; `From Session · <name>:` opens the sender by click or keyboard, with labels following the Harness language setting |
|
|
30
30
|
| Copy session id | A "Copy ID" button is added to the session header for one-click copying of the current session id |
|
|
31
|
+
| Delivery card settings | In Settings → Plugin configuration, toggle delivery mode and target runtime details; `messageId` is always shown |
|
|
31
32
|
|
|
32
33
|
### Sender navigation example
|
|
33
34
|
|
|
34
35
|

|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
Current relay messages are displayed as visible Agent message cards; clicking the header opens the sender session. The persisted source remains plugin `relay` provenance rather than impersonating human input. The full session id remains in typed source metadata and in a Host-generated model-visible protocol header, so the receiving Agent never has to guess the sender.
|
|
37
38
|
|
|
38
39
|
### Delivery modes (the `mode` parameter of `send_agent_message`)
|
|
39
40
|
|
|
@@ -55,7 +56,7 @@ dsh plugin --profile web add dsh-agent-message
|
|
|
55
56
|
|
|
56
57
|
It self-registers on install; no extra configuration is needed.
|
|
57
58
|
|
|
58
|
-
Compatibility: Node.js 24 and DeepSeek Harness `>=0.1.
|
|
59
|
+
Compatibility: Node.js 24 and DeepSeek Harness `>=0.1.1-rc.2 <0.2.0`; currently verified with Node.js `24.x` and Harness `0.1.1-rc.2`.
|
|
59
60
|
|
|
60
61
|
### Option 2: Install from GitHub
|
|
61
62
|
|
|
@@ -77,9 +78,9 @@ The plugin ships a `cordis.patch.yml` (pointed to by `dsh.bundle.patch` in `pack
|
|
|
77
78
|
|
|
78
79
|
## Usage
|
|
79
80
|
|
|
80
|
-
1. Type `@`
|
|
81
|
-
2.
|
|
82
|
-
3. For an explicit forwarding request, A only delivers and reports the
|
|
81
|
+
1. Type `@` in session A's composer and choose the target session from Harness's native candidate menu;
|
|
82
|
+
2. Harness gives A a bounded, read-only, untrusted snapshot of that Session, but does not message or wake B. A calls `send_agent_message` only when the current request or an orchestration responsibility already granted by the user explicitly requires cross-session communication. For example, `@B tell it to stop after opening the draft PR` sends, while `@B analyze its latest conversation result` only uses the referenced snapshot;
|
|
83
|
+
3. For an explicit forwarding request, A only delivers and reports whether the message was accepted or failed. It must not execute the forwarded task itself or ask B for an extra acknowledgement. B sends a message to `senderSessionId` only when the body explicitly asks it to return business content;
|
|
83
84
|
4. You can still ask the Agent to call `list_peer_agents` and send directly with a full session id;
|
|
84
85
|
5. Session B receives a native `UserMessage` with a typed relay source plus a minimal Host-generated source header on the first body line, so B does not have to guess the sender. The Client presents it as a visible Agent message card whose header opens the sender session;
|
|
85
86
|
6. (Supervision) Say "check the status of my messages to `<session id>`" — it calls `check_delivery`.
|
|
@@ -99,13 +100,15 @@ Delivery paths of `send_agent_message`:
|
|
|
99
100
|
|
|
100
101
|
Session enumeration, batched titles, and offline log reads use Harness's `sessionQuery.listSessions()`, `readTitleSnapshots()`, and `readSession()` respectively. `SessionId` is the only address; `parentSession` records fork lineage only, and only `origin: subagent` identifies an actual subagent. The plugin does not scan `sessionPersistence` directly to rebuild a parallel session directory.
|
|
101
102
|
|
|
102
|
-
After `send_agent_message` submits the native message to the target Inbox, it immediately returns `accepted` with the native `messageId
|
|
103
|
+
After `send_agent_message` submits the native message to the target Inbox, it immediately returns `accepted` with the native `messageId`; both the terse tool result and the full tool card show a selectable `messageId` for copying, while the complete result also stays in tool presentation metadata. `check_delivery` then derives `pending` (still queued), `claimed` (claimed by a turn), `discarded` (cancelled), or `unknown` from Inbox events on demand. `claimed` is transport evidence only: it does not prove that the message was read, answered, or completed. Pre-admission failure remains a Harness tool error and writes nothing to the target Inbox. Runtime state is reported separately as `targetRuntimeStatus`, so unrelated Agent activity never changes the message state. A known `messageId` remains queryable from the target Inbox log after a process restart.
|
|
103
104
|
|
|
104
105
|
Every cross-session message is created by Harness `createUserMessage()`, and `UserMessage.id` is its only message identity. Its source always uses `kind: dsh-agent-message` and `form: relay`, plus the protocol version, sender/target Session ids, and display title. Because current Harness model requests do not expand custom source fields, the Host also writes a minimal `<dsh-agent-message>` header containing only `senderSessionId` on the first body line. The typed source is the durable/UI truth; the header is only the model-visible projection needed for reply addressing. The plugin registers no global system prompt; send admission lives only in the `send_agent_message` tool contract. The Client only projects relay as a visible Agent message card and never rewrites an Agent message as human `user` provenance.
|
|
105
106
|
|
|
106
|
-
|
|
107
|
+
Relay only means “a message addressed by another session”; it neither requires nor forbids a reply. When the body explicitly requests business content in return, the receiving Agent may send a message to `senderSessionId` with the same tool. Otherwise it must not send a transport acknowledgement or a bare “received.” The plugin does not automatically correlate requests and replies or forward ordinary Agent answers.
|
|
107
108
|
|
|
108
|
-
|
|
109
|
+
Composer-side `@` is provided entirely by Harness's native `ui-reference` / `session-reference` services. Harness retains the stable Session ID in the structured reference and owns bounded snapshot loading and injection. The plugin no longer registers an `@` source, parses titles for addressing, or maintains a second session-reference UI.
|
|
110
|
+
|
|
111
|
+
See [`docs/architecture-v2.md`](./docs/architecture-v2.md) for the current architecture contract.
|
|
109
112
|
|
|
110
113
|
## Directory structure
|
|
111
114
|
|
|
@@ -113,21 +116,30 @@ The composer-side `@` session locator reuses Harness's native `inputTriggers` co
|
|
|
113
116
|
dsh-agent-message/
|
|
114
117
|
├── lib/
|
|
115
118
|
│ ├── index.js # host half: list_peer_agents / send_agent_message / check_delivery
|
|
116
|
-
│ └── client.js # client half:
|
|
119
|
+
│ └── client.js # client half: relay display, sender navigation, settings, and copy-session-id button
|
|
117
120
|
├── cordis.patch.yml # self-registration patch (pointed to by dsh.bundle.patch)
|
|
118
121
|
├── package.json # DSH plugin manifest (dsh.bundle / dsh.client / dshx.contributes)
|
|
119
|
-
├── docs/ #
|
|
122
|
+
├── docs/ # active/proposed architecture and README example screenshot
|
|
123
|
+
├── scripts/web-smoke.mjs # real Web Profile startup and configuration smoke test
|
|
120
124
|
├── README.md # Chinese documentation
|
|
121
125
|
└── README.en.md # English documentation
|
|
122
126
|
```
|
|
123
127
|
|
|
124
|
-
##
|
|
128
|
+
## Development validation
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
pnpm test
|
|
132
|
+
pnpm run test:web-smoke
|
|
133
|
+
pnpm peers check
|
|
134
|
+
```
|
|
125
135
|
|
|
126
|
-
-
|
|
136
|
+
`test:web-smoke` requires a local `dsh` installation and a configured Web Profile. It checks startup, plugin composition, and HTTP reachability only; see the [architecture validation record](./docs/architecture-v2.md#14-验证记录) for real interaction coverage.
|
|
127
137
|
|
|
128
138
|
## Limitations
|
|
129
139
|
|
|
130
140
|
- The target session must be **non-archived** and present in local persistence; archived sessions are always rejected.
|
|
141
|
+
- These tools are only for communication between independent sessions; actual subagents are neither listed as targets nor allowed to call them.
|
|
142
|
+
- One Session pair, regardless of direction, may receive at most 10 deliveries in a rolling 60-second window. The 11th is rejected before the target Inbox is changed. This window belongs to the current Harness process and resets on restart.
|
|
131
143
|
- Automatically resuming an offline session uses the **default model** (it does not inherit a model manually selected earlier in that session); if resume fails, the message is not written to the target Inbox.
|
|
132
144
|
- Bulk receipt queries without `messageId` rely on in-memory bookkeeping and cover only the most recent 1000 sends in the current process (FIFO eviction). After a restart, a known `messageId` remains queryable, but the volatile `sentAt` and `mode` fields are no longer returned.
|
|
133
145
|
- Cross-process / cross-machine communication is out of scope.
|
package/README.md
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
|
|
13
13
|
在 DeepSeek Harness 里,一个进程会同时挂着多个 Agent 会话。本插件给每个会话装上三个工具,让它们能互相"发消息":
|
|
14
14
|
|
|
15
|
-
-
|
|
16
|
-
- 找到后,**把消息投递到目标会话**——普通消息统一进入独立的新 turn;目标离线(进程重启后还没打开)时,插件通过 Harness
|
|
15
|
+
- 发消息前,先**列出所有可发送的独立会话**(未归档、排除真实子代理,含离线未打开的),按标题找到目标;
|
|
16
|
+
- 找到后,**把消息投递到目标会话**——普通消息统一进入独立的新 turn;目标离线(进程重启后还没打开)时,插件通过 Harness 公开接口恢复会话、投递,并保持加载供后续通信,插件卸载时再释放 handle;
|
|
17
17
|
- 需要时,可以**按需查询**某条消息的送达状态(排队中/已认领/被丢弃/未知),并单独查看目标是否正在运行,供监督场景使用。
|
|
18
18
|
|
|
19
19
|
典型场景:编排者 Agent 给开发 Agent 派活、两个 Agent 协作接力、主会话给测试会话发指令、监督者 Agent 盯梢多个 worker。
|
|
@@ -25,15 +25,16 @@
|
|
|
25
25
|
| `list_peer_agents` | 列出所有**可发送**的独立会话:未归档、排除真实子代理;普通 fork 保留。返回 id、标题、工作目录和运行状态 |
|
|
26
26
|
| `send_agent_message` | 给指定会话 ID 发消息;默认使用 `followup` 创建独立的新 turn,离线时自动恢复后投递;显式支持 `followup`、`inject`、`steer` |
|
|
27
27
|
| `check_delivery` | 按需查询消息回执(pending/claimed/discarded/unknown);发送成功时先返回 accepted,接纳前失败由工具错误表示;指定消息 ID 时支持重启后恢复查询 |
|
|
28
|
-
| `@`
|
|
29
|
-
| 可见的 Agent 消息卡片 | relay 仍保留真实的插件来源,但 Client 将它显示为左侧 Agent 消息卡片;`From Session · <名称>:`
|
|
28
|
+
| Harness 原生 `@` 会话引用 | 直接复用 Harness 的结构化 Session Reference;选择后当前 Agent 获得稳定 Session ID 和一份有界、只读、标记为不可信的会话快照。引用本身不会发送消息或唤醒目标 |
|
|
29
|
+
| 可见的 Agent 消息卡片 | relay 仍保留真实的插件来源,但 Client 将它显示为左侧 Agent 消息卡片;`From Session · <名称>:` 可点击或通过键盘打开发送方会话,文案跟随 Harness 语言设置 |
|
|
30
30
|
| 复制会话 ID | 会话头部新增「复制ID」按钮,一键复制当前会话 ID |
|
|
31
|
+
| 投递卡片设置 | 在设置 → 插件配置中,可切换是否显示投递模式和目标运行状态;`messageId` 始终显示 |
|
|
31
32
|
|
|
32
33
|
### 发送方导航示例
|
|
33
34
|
|
|
34
35
|

|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
当前 relay 消息显示为可见的 Agent 消息卡片;点击消息头即可跳转到发送方会话。持久化来源仍是插件 `relay`,不会伪装成人类输入。完整会话 ID 同时保留在 typed source 和 Host 生成的模型可见协议头中,避免接收 Agent 猜测发送方。
|
|
37
38
|
|
|
38
39
|
### 投递模式(`send_agent_message` 的 `mode` 参数)
|
|
39
40
|
|
|
@@ -56,7 +57,7 @@ dsh plugin --profile web add dsh-agent-message
|
|
|
56
57
|
|
|
57
58
|
装完即自动注册,无需任何额外配置。
|
|
58
59
|
|
|
59
|
-
兼容范围:Node.js 24、DeepSeek Harness `>=0.1.
|
|
60
|
+
兼容范围:Node.js 24、DeepSeek Harness `>=0.1.1-rc.2 <0.2.0`;当前验证版本为 Node.js `24.x`、Harness `0.1.1-rc.2`。
|
|
60
61
|
|
|
61
62
|
### 方式二:从 GitHub 安装
|
|
62
63
|
|
|
@@ -78,9 +79,9 @@ Agent 会用 bash 执行这条命令,装完自动挂载、所有会话立即
|
|
|
78
79
|
|
|
79
80
|
## 使用
|
|
80
81
|
|
|
81
|
-
1. 在会话 A
|
|
82
|
-
2.
|
|
83
|
-
3. 显式要求转告时,A
|
|
82
|
+
1. 在会话 A 的输入框中键入 `@`,从 Harness 原生候选菜单中选择目标会话;
|
|
83
|
+
2. Harness 会把该 Session 的有界、只读、不可信快照提供给 A,但不会向 B 发消息或唤醒 B。只有当前请求或用户已授予的编排职责明确要求跨会话传递信息时,A 才调用 `send_agent_message`。例如 `@B 告诉他最后提交 PR draft 就停止` 会发送;`@B 帮我分析他最新的对话结果` 只使用引用快照;
|
|
84
|
+
3. 显式要求转告时,A 只负责投递并报告“已接受”或失败,不代为执行被转发的任务,也不要求 B 额外回复“收到”;如果正文明确要求 B 把业务内容返回 A,B 才向 `senderSessionId` 发送消息;
|
|
84
85
|
4. 也可以让 Agent 调 `list_peer_agents`,再用完整会话 ID 直接发送;
|
|
85
86
|
5. 会话 B 收到的是带 typed relay source 的原生 `UserMessage`;正文首行还有 Host 生成的最小来源协议,B 不需要猜测发送方;Client 将其显示为可见 Agent 消息卡片,并可从消息头打开发送方会话;
|
|
86
87
|
6. (监督场景)说「查一下我发给 `<会话ID>` 的消息状态」——它会调 `check_delivery`。
|
|
@@ -100,13 +101,15 @@ Agent 会用 bash 执行这条命令,装完自动挂载、所有会话立即
|
|
|
100
101
|
|
|
101
102
|
会话枚举、批量标题和离线日志读取分别使用 Harness 的 `sessionQuery.listSessions()`、`readTitleSnapshots()` 与 `readSession()`。`SessionId` 是唯一地址;`parentSession` 只记录分叉血缘,只有 `origin: subagent` 才会被识别为真实子代理。插件不直接扫描 `sessionPersistence` 重建另一份会话目录。
|
|
102
103
|
|
|
103
|
-
`send_agent_message` 成功把原生消息提交给目标 Inbox 后立即返回 `accepted` 和该消息的原生 `messageId
|
|
104
|
+
`send_agent_message` 成功把原生消息提交给目标 Inbox 后立即返回 `accepted` 和该消息的原生 `messageId`;精简工具结果和完整工具卡片都会显示可选择复制的 `messageId`,完整结果同时保留在工具呈现元数据中。`check_delivery` 根据 Inbox 事件按需返回 `pending`(仍在排队)、`claimed`(已被某轮认领)、`discarded`(被取消)或 `unknown`。`claimed` 只是传输证据,不表示已读、回复或任务完成。接纳前失败由 Harness 工具错误表示,不写入目标 Inbox。目标是否正在运行通过独立的 `targetRuntimeStatus` 返回,不把 Agent 的整体运行状态误当成某条消息正在处理。指定 `messageId` 时可从目标现有 Inbox 日志恢复状态,因此进程重启后仍可查询。
|
|
104
105
|
|
|
105
106
|
所有跨会话消息都由 Harness `createUserMessage()` 创建,`UserMessage.id` 是唯一消息身份。`source.kind` 固定为 `dsh-agent-message`,`form` 固定为 `relay`,并携带协议版本、发送/目标 Session 和显示标题。由于当前 Harness 不会把自定义 source 字段展开给模型,Host 还会在正文首行写入只含 `senderSessionId` 的最小 `<dsh-agent-message>` 协议头;source 是持久化/UI 真相,协议头只是回复寻址所需的模型可见投影。插件不注册全局系统提示词,发送准入只存在于 `send_agent_message` 的工具合同中。Client 只把 relay 投影为可见的 Agent 消息卡片,不会反向把 Agent 消息伪装成人类 `user` 来源。
|
|
106
107
|
|
|
107
|
-
|
|
108
|
+
relay 只表达“另一会话发来的消息”,本身不等于必须回复或禁止回复。正文明确要求返回业务内容时,接收 Agent 可用同一工具向 `senderSessionId` 发送消息;没有明确要求时不回传 transport ack 或单纯的“收到”。插件不自动关联请求与回复,也不自动转发 Agent 的普通回答。
|
|
108
109
|
|
|
109
|
-
输入框的 `@`
|
|
110
|
+
输入框的 `@` 完全由 Harness 原生 `ui-reference` / `session-reference` 提供,结构化引用保留稳定 Session ID,并由 Harness 负责读取和注入有界快照。插件不再注册自己的 `@` source、不解析标题寻址,也不维护第二套会话引用 UI。
|
|
111
|
+
|
|
112
|
+
完整的现役架构合同见 [`docs/architecture-v2.md`](./docs/architecture-v2.md)。
|
|
110
113
|
|
|
111
114
|
## 目录结构
|
|
112
115
|
|
|
@@ -114,21 +117,30 @@ Agent 会用 bash 执行这条命令,装完自动挂载、所有会话立即
|
|
|
114
117
|
dsh-agent-message/
|
|
115
118
|
├── lib/
|
|
116
119
|
│ ├── index.js # host 半区:list_peer_agents / send_agent_message / check_delivery
|
|
117
|
-
│ └── client.js # client
|
|
120
|
+
│ └── client.js # client 半区:relay 显示、发送方导航、设置与复制会话ID按钮
|
|
118
121
|
├── cordis.patch.yml # 自注册补丁(dsh.bundle.patch 指向它)
|
|
119
122
|
├── package.json # DSH 插件清单(dsh.bundle / dsh.client / dshx.contributes)
|
|
120
|
-
├── docs/ #
|
|
123
|
+
├── docs/ # 现役/候选架构与 README 示例截图
|
|
124
|
+
├── scripts/web-smoke.mjs # 真实 Web Profile 启动与配置冒烟
|
|
121
125
|
├── README.md # 中文文档
|
|
122
126
|
└── README.en.md # English documentation
|
|
123
127
|
```
|
|
124
128
|
|
|
125
|
-
##
|
|
129
|
+
## 开发验证
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
pnpm test
|
|
133
|
+
pnpm run test:web-smoke
|
|
134
|
+
pnpm peers check
|
|
135
|
+
```
|
|
126
136
|
|
|
127
|
-
-
|
|
137
|
+
`test:web-smoke` 需要本机已安装 `dsh` 并配置 Web Profile,它只验证启动、插件组合和 HTTP 可达性;真实交互验收记录见 [`docs/architecture-v2.md`](./docs/architecture-v2.md#14-验证记录)。
|
|
128
138
|
|
|
129
139
|
## 限制
|
|
130
140
|
|
|
131
141
|
- 目标会话必须**未归档**且存在于本机持久化里;归档会话一律拒绝发送。
|
|
142
|
+
- 工具只用于独立 Session 之间通信;真实子代理既不会出现在目标列表中,也不能作为调用方使用这些工具。
|
|
143
|
+
- 同一对 Session(不分发送方向)在滚动 60 秒内最多投递 10 条消息;第 11 条会在写入目标 Inbox 前被拒绝。该窗口只属于当前 Harness 进程,重启后清空。
|
|
132
144
|
- 自动恢复离线会话时会使用**默认模型**(不继承它上次手动切换的模型选择);恢复失败时消息不会被写入目标 Inbox。
|
|
133
145
|
- 不指定 `messageId` 的批量回执依赖内存记账,只覆盖本进程最近 1000 条发送记录(FIFO 淘汰);进程重启后仍可凭已知 `messageId` 查询,但不再返回易失的 `sentAt` 和 `mode`。
|
|
134
146
|
- 跨进程/跨机器通信不在本插件范围内。
|
package/lib/client.js
CHANGED
|
@@ -5,152 +5,106 @@ window.__ModuleLoader__.load({
|
|
|
5
5
|
var exports = module.exports;
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
7
|
const React = require("react");
|
|
8
|
-
const { createRoot } = require("react-dom/client");
|
|
9
|
-
const { IconQueueOutline14, StateDot } = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
10
8
|
|
|
11
9
|
const name = "dsh-agent-message-client";
|
|
12
|
-
const inject = ["slots", "timer", "sessions", "
|
|
10
|
+
const inject = ["slots", "timer", "sessions", "settingsScope", "locale"];
|
|
13
11
|
|
|
14
12
|
function apply(ctx) {
|
|
15
|
-
const senderSelector =
|
|
16
|
-
const referenceSource = "agent-message-session";
|
|
17
|
-
const candidateRows = new WeakMap();
|
|
18
|
-
const mountedRoots = new Map();
|
|
13
|
+
const senderSelector = "[data-context-relay-sender]";
|
|
19
14
|
const sessionLinks = new Set();
|
|
20
|
-
const
|
|
15
|
+
const subscribeLocale = function (listener) { return ctx.locale.subscribe(listener); };
|
|
16
|
+
const getLocaleSnapshot = function () { return ctx.locale.getSnapshot(); };
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
const language = String(document.documentElement.lang || navigator.language || "zh").toLowerCase();
|
|
24
|
-
return language.startsWith("zh") ? zh : en;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function sessionRows(includeArchived) {
|
|
28
|
-
const snapshot = ctx.sessions.list.getSnapshot();
|
|
29
|
-
const archived = new Set(workspaces?.list.getSnapshot().archivedSessionIds || []);
|
|
30
|
-
return snapshot.ids
|
|
31
|
-
.map(function (id) { return snapshot.byId[id]; })
|
|
32
|
-
.filter(function (row) {
|
|
33
|
-
return row && !row.blank && row.origin !== "subagent"
|
|
34
|
-
&& (includeArchived || !archived.has(row.id));
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function sessionRow(id) {
|
|
39
|
-
return ctx.sessions.list.getSnapshot().byId[id];
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function sessionTitle(row) {
|
|
43
|
-
return String(row?.displayTitle || row?.title || row?.id || "");
|
|
44
|
-
}
|
|
18
|
+
const AGENT_MESSAGE_SETTINGS_NAMESPACE = "agent-message";
|
|
45
19
|
|
|
46
|
-
function
|
|
47
|
-
|
|
48
|
-
return chars.length > 40 ? chars.slice(0, 39).join("") + "…" : chars.join("");
|
|
20
|
+
function useLocale() {
|
|
21
|
+
React.useSyncExternalStore(subscribeLocale, getLocaleSnapshot);
|
|
49
22
|
}
|
|
50
23
|
|
|
51
|
-
function
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
24
|
+
function AgentMessageSettingsCard(props) {
|
|
25
|
+
useLocale();
|
|
26
|
+
const snapshot = props.useAgentMessageSettings(function (value) { return value; });
|
|
27
|
+
const current = snapshot.value?.showTechnicalDetails !== false;
|
|
28
|
+
const [draft, setDraft] = React.useState(current);
|
|
29
|
+
const [saving, setSaving] = React.useState(false);
|
|
30
|
+
const [error, setError] = React.useState(false);
|
|
31
|
+
React.useEffect(function () { setDraft(current); }, [snapshot.revision, current]);
|
|
32
|
+
const dirty = draft !== current;
|
|
33
|
+
const writable = snapshot.writable && snapshot.status === "ready";
|
|
34
|
+
function save() {
|
|
35
|
+
if (!dirty || saving || !writable) return;
|
|
36
|
+
setSaving(true);
|
|
37
|
+
setError(false);
|
|
38
|
+
props.setShowTechnicalDetails(draft)
|
|
39
|
+
.catch(function () { setError(true); })
|
|
40
|
+
.finally(function () { setSaving(false); });
|
|
41
|
+
}
|
|
42
|
+
function reset() {
|
|
43
|
+
if (!writable) return;
|
|
44
|
+
setDraft(snapshot.base?.showTechnicalDetails !== false);
|
|
56
45
|
}
|
|
57
|
-
|
|
46
|
+
return React.createElement("section", { style: { padding: "12px 0", maxWidth: "760px" } },
|
|
47
|
+
React.createElement("h3", { style: { margin: "0 0 4px" } }, uiText("跨会话消息", "Agent messages")),
|
|
48
|
+
React.createElement("p", { style: { color: "var(--dsw-alias-label-tertiary)", margin: "0 0 12px", fontSize: "13px" } }, uiText("控制投递卡片是否显示传输细节。messageId 始终保留。", "Control transport details in delivery cards. The messageId is always kept.")),
|
|
49
|
+
React.createElement("label", { style: { display: "flex", gap: "8px", alignItems: "center" } },
|
|
50
|
+
React.createElement("input", { type: "checkbox", checked: draft, disabled: !writable, onChange: function (event) { setDraft(event.target.checked); } }),
|
|
51
|
+
uiText("显示投递模式和目标运行状态", "Show delivery mode and target runtime status")),
|
|
52
|
+
error ? React.createElement("p", { role: "alert", style: { color: "var(--dsw-alias-state-error)", margin: "10px 0 0", fontSize: "13px" } }, uiText("保存失败,设置可能已被其他页面修改。", "Save failed; the setting may have changed elsewhere.")) : null,
|
|
53
|
+
React.createElement("div", { style: { display: "flex", gap: "8px", marginTop: "14px" } },
|
|
54
|
+
React.createElement("button", { type: "button", disabled: !dirty || saving || !writable, onClick: reset }, uiText("恢复默认", "Reset")),
|
|
55
|
+
React.createElement("button", { type: "button", disabled: !dirty || saving || !writable, onClick: save }, saving ? uiText("保存中…", "Saving…") : uiText("保存", "Save")))
|
|
56
|
+
);
|
|
58
57
|
}
|
|
59
58
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
59
|
+
if (ctx.settingsScope !== undefined && ctx.slots !== undefined) {
|
|
60
|
+
const scope = ctx.settingsScope.bind({ namespace: AGENT_MESSAGE_SETTINGS_NAMESPACE });
|
|
61
|
+
const agentMessageSettingsStore = {
|
|
62
|
+
getSnapshot: function () { return scope.getSnapshot(); },
|
|
63
|
+
subscribe: function (listener) { return scope.subscribe(listener); },
|
|
64
|
+
};
|
|
65
|
+
ctx.slots.inject("settings.plugin.item", function () {
|
|
66
|
+
return ctx.slots.register({
|
|
67
|
+
name: "settings.plugin.item",
|
|
68
|
+
key: AGENT_MESSAGE_SETTINGS_NAMESPACE,
|
|
69
|
+
inject: function () {
|
|
70
|
+
return {
|
|
71
|
+
hooks: { agentMessageSettings: agentMessageSettingsStore },
|
|
72
|
+
setShowTechnicalDetails: function (value) { return scope.set("showTechnicalDetails", value); },
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
}, AgentMessageSettingsCard);
|
|
69
76
|
});
|
|
70
77
|
}
|
|
71
78
|
|
|
72
|
-
function
|
|
73
|
-
return
|
|
74
|
-
? React.createElement(StateDot, { state: "done", size: 10, className: "agent-msg-status-dot" })
|
|
75
|
-
: React.createElement("span", { className: "agent-msg-status-dot agent-msg-status-dot-idle", "aria-hidden": "true" });
|
|
79
|
+
function uiText(zh, en) {
|
|
80
|
+
return getLocaleSnapshot().active === "zh" ? zh : en;
|
|
76
81
|
}
|
|
77
82
|
|
|
78
|
-
function
|
|
79
|
-
return
|
|
80
|
-
React.createElement(IconQueueOutline14, { size: 14, className: "agent-msg-session-icon" }),
|
|
81
|
-
React.createElement("span", { className: "agent-msg-reference-title" }, "@" + sessionTitle(props.row)));
|
|
83
|
+
function sessionRow(id) {
|
|
84
|
+
return ctx.sessions.list.getSnapshot().byId[id];
|
|
82
85
|
}
|
|
83
86
|
|
|
84
|
-
function
|
|
85
|
-
return
|
|
86
|
-
React.createElement(IconQueueOutline14, { size: 14, className: "agent-msg-session-icon" }),
|
|
87
|
-
React.createElement("span", null, "From Session · " + props.title + ":"));
|
|
87
|
+
function sessionTitle(row) {
|
|
88
|
+
return String(row?.displayTitle || row?.title || row?.id || "");
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
function
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
element.appendChild(host);
|
|
97
|
-
}
|
|
98
|
-
renderInto(host, React.createElement(SessionReference, { row: row }));
|
|
91
|
+
function cleanupRoots(node) {
|
|
92
|
+
if (!(node instanceof Element)) return;
|
|
93
|
+
const links = node.matches(".agent-msg-session-link")
|
|
94
|
+
? [node].concat(Array.from(node.querySelectorAll(".agent-msg-session-link")))
|
|
95
|
+
: Array.from(node.querySelectorAll(".agent-msg-session-link"));
|
|
96
|
+
links.forEach(function (element) { sessionLinks.delete(element); });
|
|
99
97
|
}
|
|
100
98
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
return sessionRows(false)
|
|
111
|
-
.filter(function (row) {
|
|
112
|
-
return String(row.id) !== String(session.sessionId)
|
|
113
|
-
&& (query === "" || sessionTitle(row).toLowerCase().includes(query));
|
|
114
|
-
})
|
|
115
|
-
.sort(function (a, b) {
|
|
116
|
-
if (a.running !== b.running) return a.running ? -1 : 1;
|
|
117
|
-
return sessionTitle(a).localeCompare(sessionTitle(b));
|
|
118
|
-
})
|
|
119
|
-
.map(function (row) {
|
|
120
|
-
const candidate = {
|
|
121
|
-
name: sessionTitle(row),
|
|
122
|
-
description: row.running ? uiText("运行中", "Running") : uiText("空闲", "Idle"),
|
|
123
|
-
icon: "",
|
|
124
|
-
};
|
|
125
|
-
candidateRows.set(candidate, row);
|
|
126
|
-
return candidate;
|
|
127
|
-
});
|
|
128
|
-
},
|
|
129
|
-
onPick({ candidate, session }) {
|
|
130
|
-
const row = candidateRows.get(candidate);
|
|
131
|
-
if (!row) return undefined;
|
|
132
|
-
const label = "@" + compactSessionTitle(row);
|
|
133
|
-
return {
|
|
134
|
-
claim: {
|
|
135
|
-
token: label + " ",
|
|
136
|
-
hint: uiText("输入要处理的内容", "Describe what to do"),
|
|
137
|
-
async submit(args) {
|
|
138
|
-
const content = String(args || "").trim();
|
|
139
|
-
if (content === "") return { kind: "error", text: uiText("请输入要处理的内容", "Describe what to do") };
|
|
140
|
-
const binding = ctx.sessions.binding(session.sessionId);
|
|
141
|
-
if (!binding) return { kind: "error", text: uiText("当前会话不可用", "Current session is unavailable") };
|
|
142
|
-
const result = await binding.session.prompt([{
|
|
143
|
-
type: "text",
|
|
144
|
-
text: "@" + String(row.id) + " " + content,
|
|
145
|
-
}], "queue");
|
|
146
|
-
return result.ok
|
|
147
|
-
? { kind: "success" }
|
|
148
|
-
: { kind: "error", text: result.error.message };
|
|
149
|
-
},
|
|
150
|
-
},
|
|
151
|
-
};
|
|
152
|
-
},
|
|
153
|
-
}), "dsh-agent-message: @ session source");
|
|
99
|
+
function restoreSender(element) {
|
|
100
|
+
if (element.dataset.agentMsgOriginalText !== undefined) {
|
|
101
|
+
element.textContent = element.dataset.agentMsgOriginalText;
|
|
102
|
+
}
|
|
103
|
+
delete element.dataset.agentMsgOriginalText;
|
|
104
|
+
delete element.dataset.agentMsgSessionId;
|
|
105
|
+
delete element.dataset.agentMsgSenderTitle;
|
|
106
|
+
element.classList.remove("agent-msg-session-link", "agent-msg-sender-link");
|
|
107
|
+
["role", "tabindex", "title", "aria-label"].forEach(function (name) { element.removeAttribute(name); });
|
|
154
108
|
}
|
|
155
109
|
|
|
156
110
|
function senderLink(target) {
|
|
@@ -164,25 +118,6 @@ window.__ModuleLoader__.load({
|
|
|
164
118
|
return match ? { element: element, sessionId: match[0] } : null;
|
|
165
119
|
}
|
|
166
120
|
|
|
167
|
-
function titleFrom(text) {
|
|
168
|
-
const line = String(text || "").split("\n", 1)[0].trim();
|
|
169
|
-
const current = line.match(/^From (?:Session|Agent)(?: ·)? (.+?):(?: @session-[\w-]+)?$/);
|
|
170
|
-
const previous = line.match(/^来自 Agent · (.+)$/);
|
|
171
|
-
const legacy = line.match(/^来自 Agent「(.+)」\s*[·::]?$/);
|
|
172
|
-
return (current || previous || legacy)?.[1]?.trim() || "";
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function senderTitle(element) {
|
|
176
|
-
if (element.dataset.agentMsgSenderTitle) return element.dataset.agentMsgSenderTitle;
|
|
177
|
-
const label = element.previousElementSibling;
|
|
178
|
-
const title = titleFrom(label?.textContent);
|
|
179
|
-
if (title && label) {
|
|
180
|
-
label.textContent = "";
|
|
181
|
-
label.classList.add("agent-msg-sender-prefix");
|
|
182
|
-
}
|
|
183
|
-
return title;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
121
|
function prepareSenderLinks(root) {
|
|
187
122
|
if (!(root instanceof Element)) return;
|
|
188
123
|
const elements = root.matches(senderSelector)
|
|
@@ -191,19 +126,19 @@ window.__ModuleLoader__.load({
|
|
|
191
126
|
elements.forEach(function (element) {
|
|
192
127
|
const link = senderLink(element);
|
|
193
128
|
if (!link) return;
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
129
|
+
const title = sessionTitle(sessionRow(link.sessionId)) || "@" + link.sessionId;
|
|
130
|
+
if (element.dataset.agentMsgOriginalText === undefined) {
|
|
131
|
+
element.dataset.agentMsgOriginalText = element.textContent || "";
|
|
132
|
+
}
|
|
197
133
|
element.dataset.agentMsgSessionId = link.sessionId;
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
else element.textContent = "From Session · " + title + ":";
|
|
134
|
+
element.dataset.agentMsgSenderTitle = title;
|
|
135
|
+
element.textContent = uiText("来自会话 · ", "From Session · ") + title + ":";
|
|
201
136
|
element.classList.add("agent-msg-session-link", "agent-msg-sender-link");
|
|
202
137
|
sessionLinks.add(element);
|
|
203
138
|
element.setAttribute("role", "link");
|
|
204
139
|
element.setAttribute("tabindex", "0");
|
|
205
|
-
element.setAttribute("title", "打开发送方会话");
|
|
206
|
-
element.setAttribute("aria-label", "打开发送方会话:" + (title || link.sessionId));
|
|
140
|
+
element.setAttribute("title", uiText("打开发送方会话", "Open sender session"));
|
|
141
|
+
element.setAttribute("aria-label", uiText("打开发送方会话:", "Open sender session: ") + (title || link.sessionId));
|
|
207
142
|
});
|
|
208
143
|
}
|
|
209
144
|
|
|
@@ -234,74 +169,18 @@ window.__ModuleLoader__.load({
|
|
|
234
169
|
});
|
|
235
170
|
}
|
|
236
171
|
|
|
237
|
-
function prepareSessionReferences(root) {
|
|
238
|
-
if (!(root instanceof Element)) return;
|
|
239
|
-
const elements = root.matches('[data-ref-chip="subagent"]')
|
|
240
|
-
? [root].concat(Array.from(root.querySelectorAll('[data-ref-chip="subagent"]')))
|
|
241
|
-
: Array.from(root.querySelectorAll('[data-ref-chip="subagent"]'));
|
|
242
|
-
elements.forEach(function (element) {
|
|
243
|
-
if (element.classList.contains("agent-msg-sender-link")) return;
|
|
244
|
-
const id = element.dataset.agentMsgSessionId
|
|
245
|
-
|| String(element.textContent || "").trim().match(/^@(session-[\w-]+)$/)?.[1];
|
|
246
|
-
const row = id ? sessionRow(id) : undefined;
|
|
247
|
-
if (!id || !row) return;
|
|
248
|
-
element.dataset.agentMsgSessionId = id;
|
|
249
|
-
element.classList.add("agent-msg-session-link", "agent-msg-reference-link");
|
|
250
|
-
sessionLinks.add(element);
|
|
251
|
-
element.setAttribute("role", "link");
|
|
252
|
-
element.setAttribute("tabindex", "0");
|
|
253
|
-
element.setAttribute("title", uiText("打开引用会话", "Open referenced session"));
|
|
254
|
-
element.setAttribute("aria-label", uiText("打开引用会话:", "Open referenced session: ") + sessionTitle(row));
|
|
255
|
-
enhanceReferenceHost(element, row);
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function prepareSessionMenu(root) {
|
|
260
|
-
if (!(root instanceof Element)) return;
|
|
261
|
-
const menu = root.matches('[role="listbox"]') ? root : root.closest('[role="listbox"]') || root.querySelector('[role="listbox"]');
|
|
262
|
-
if (!menu) return;
|
|
263
|
-
const group = menu.querySelector('[data-source="' + referenceSource + '"]');
|
|
264
|
-
if (!group) return;
|
|
265
|
-
group.textContent = uiText("会话", "Sessions");
|
|
266
|
-
let element = group.nextElementSibling;
|
|
267
|
-
while (element && !element.hasAttribute("data-source")) {
|
|
268
|
-
if (element.matches('button[role="option"]')) {
|
|
269
|
-
element.classList.add("agent-msg-session-candidate");
|
|
270
|
-
const spans = element.querySelectorAll(":scope > span");
|
|
271
|
-
const icon = spans[0];
|
|
272
|
-
const description = spans[2];
|
|
273
|
-
if (icon) {
|
|
274
|
-
icon.classList.add("agent-msg-candidate-icon");
|
|
275
|
-
renderInto(icon, React.createElement(IconQueueOutline14, { size: 14 }));
|
|
276
|
-
}
|
|
277
|
-
if (description) {
|
|
278
|
-
const running = /^(运行中|Running)$/.test(String(description.textContent || "").trim())
|
|
279
|
-
|| element.dataset.agentMsgRunning === "true";
|
|
280
|
-
element.dataset.agentMsgRunning = String(running);
|
|
281
|
-
renderInto(description, React.createElement("span", { className: "agent-msg-status-label" },
|
|
282
|
-
React.createElement(SessionActivity, { running: running }),
|
|
283
|
-
React.createElement("span", null, running ? uiText("运行中", "Running") : uiText("空闲", "Idle"))));
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
element = element.nextElementSibling;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
172
|
function prepare(root) {
|
|
291
173
|
prepareRelayCards(root);
|
|
292
174
|
prepareSenderLinks(root);
|
|
293
|
-
prepareSessionReferences(root);
|
|
294
|
-
prepareSessionMenu(root);
|
|
295
175
|
}
|
|
296
176
|
|
|
297
|
-
function
|
|
177
|
+
function refreshSenderLinks() {
|
|
298
178
|
sessionLinks.forEach(function (element) {
|
|
299
179
|
if (!element.isConnected) {
|
|
300
180
|
sessionLinks.delete(element);
|
|
301
181
|
return;
|
|
302
182
|
}
|
|
303
|
-
|
|
304
|
-
else prepareSessionReferences(element);
|
|
183
|
+
prepareSenderLinks(element);
|
|
305
184
|
});
|
|
306
185
|
}
|
|
307
186
|
|
|
@@ -351,6 +230,7 @@ window.__ModuleLoader__.load({
|
|
|
351
230
|
}
|
|
352
231
|
|
|
353
232
|
function CopyButton(props) {
|
|
233
|
+
useLocale();
|
|
354
234
|
const [copyState, setCopyState] = React.useState("idle");
|
|
355
235
|
async function onClick() {
|
|
356
236
|
const text = String(props.sessionId || "");
|
|
@@ -363,39 +243,29 @@ window.__ModuleLoader__.load({
|
|
|
363
243
|
type: "button",
|
|
364
244
|
onClick: onClick,
|
|
365
245
|
onMouseLeave: onMouseLeave,
|
|
366
|
-
title: "复制会话 ID",
|
|
367
|
-
"aria-label": "复制会话 ID",
|
|
246
|
+
title: uiText("复制会话 ID", "Copy session ID"),
|
|
247
|
+
"aria-label": uiText("复制会话 ID", "Copy session ID"),
|
|
368
248
|
className: "agent-msg-copy-id"
|
|
369
|
-
}, copyState === "copied"
|
|
249
|
+
}, copyState === "copied"
|
|
250
|
+
? uiText("已复制", "Copied")
|
|
251
|
+
: copyState === "failed"
|
|
252
|
+
? uiText("复制失败", "Copy failed")
|
|
253
|
+
: uiText("复制ID", "Copy ID"));
|
|
370
254
|
}
|
|
371
255
|
|
|
372
256
|
ctx.slots.inject("conversation.session.header.actions", () => ctx.slots.register(
|
|
373
|
-
{ name: "conversation.session.header.actions", id: "copy-session-id", order: 30, label: "复制会话ID" },
|
|
257
|
+
{ name: "conversation.session.header.actions", id: "copy-session-id", order: 30, label: uiText("复制会话ID", "Copy session ID") },
|
|
374
258
|
(props) => React.createElement(CopyButton, { sessionId: props.sessionId })
|
|
375
259
|
));
|
|
376
260
|
|
|
377
261
|
const css =
|
|
378
262
|
".agent-msg-copy-id { cursor: pointer; font-size: 12px; line-height: 1; padding: 5px 10px; border: 1px solid rgba(127,127,127,.35); border-radius: 6px; background: transparent; color: inherit; opacity: .85; } " +
|
|
379
263
|
".agent-msg-copy-id:hover { opacity: 1; border-color: rgba(127,127,127,.7); } " +
|
|
380
|
-
".agent-msg-sender-prefix { display: none !important; } " +
|
|
381
264
|
".agent-msg-session-link { cursor: pointer; text-decoration: none; } " +
|
|
382
|
-
".agent-msg-sender-link[data-ref-chip=\"subagent\"] { display: block; width: fit-content; padding: 0; background: transparent; color: inherit; font-size: 13px; font-weight: 600; line-height: 1.5; } " +
|
|
383
|
-
".agent-msg-reference-link { display: inline-flex !important; align-items: center; color: var(--dsw-alias-state-business-primary) !important; } " +
|
|
384
|
-
".agent-msg-reference-host, .agent-msg-reference-content { display: inline-flex; min-width: 0; align-items: center; } " +
|
|
385
|
-
".agent-msg-reference-content { gap: 5px; max-width: 100%; } " +
|
|
386
|
-
".agent-msg-reference-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } " +
|
|
387
|
-
".agent-msg-session-icon, .agent-msg-candidate-icon { display: inline-flex; flex: none; color: var(--dsw-alias-state-business-primary); } " +
|
|
388
|
-
".agent-msg-relay-sender-content { display: inline-flex; align-items: center; gap: 5px; } " +
|
|
389
265
|
"[data-agent-msg-relay-card] [data-disclosure-row] { display: none !important; } " +
|
|
390
266
|
"[data-agent-msg-relay-card] [data-context-form=\"relay\"] { width: fit-content; max-width: min(78%, 760px); max-height: none; margin: 0; padding: 14px 16px; overflow: visible; border: 1px solid rgba(127,127,127,.24); border-radius: 16px 16px 16px 4px; background: var(--dsw-alias-markdown-code-block); color: var(--dsw-alias-label-primary); font: inherit; } " +
|
|
391
267
|
"[data-agent-msg-relay-card] [data-context-relay-sender] { display: block; width: fit-content; margin: 0 0 8px; color: var(--dsw-alias-state-business-primary); font-size: 13px; font-weight: 600; line-height: 1.5; } " +
|
|
392
268
|
"[data-agent-msg-relay-card] [data-context-text] { margin: 0; overflow: visible; white-space: pre-wrap; overflow-wrap: anywhere; color: inherit; font: inherit; } " +
|
|
393
|
-
".agent-msg-status-dot { flex: none; } " +
|
|
394
|
-
".agent-msg-status-dot-idle { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--dsw-alias-label-tertiary); opacity: .7; } " +
|
|
395
|
-
".agent-msg-status-label { display: inline-flex; align-items: center; gap: 6px; } " +
|
|
396
|
-
".agent-msg-session-candidate { border-radius: 0 !important; } " +
|
|
397
|
-
".agent-msg-session-candidate > span:nth-child(2) { flex: 1; min-width: 0; max-width: none; } " +
|
|
398
|
-
".agent-msg-session-candidate > span:nth-child(3) { display: flex; flex: none; justify-content: flex-end; margin-left: auto; } " +
|
|
399
269
|
".agent-msg-session-link:hover { text-decoration: underline; text-underline-offset: 3px; } " +
|
|
400
270
|
".agent-msg-session-link:focus-visible { outline: 2px solid currentColor; outline-offset: 3px; border-radius: 2px; }";
|
|
401
271
|
const tag = document.createElement("style");
|
|
@@ -410,18 +280,17 @@ window.__ModuleLoader__.load({
|
|
|
410
280
|
});
|
|
411
281
|
});
|
|
412
282
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
413
|
-
const unsubscribeSessions = ctx.sessions.list.subscribe(
|
|
414
|
-
const
|
|
283
|
+
const unsubscribeSessions = ctx.sessions.list.subscribe(refreshSenderLinks);
|
|
284
|
+
const unsubscribeLocale = ctx.locale.subscribe(refreshSenderLinks);
|
|
415
285
|
document.addEventListener("click", openSender);
|
|
416
286
|
document.addEventListener("keydown", onKeyDown);
|
|
417
287
|
ctx.effect(() => () => {
|
|
418
288
|
observer.disconnect();
|
|
419
289
|
unsubscribeSessions();
|
|
420
|
-
|
|
290
|
+
unsubscribeLocale();
|
|
421
291
|
document.removeEventListener("click", openSender);
|
|
422
292
|
document.removeEventListener("keydown", onKeyDown);
|
|
423
|
-
|
|
424
|
-
mountedRoots.clear();
|
|
293
|
+
sessionLinks.forEach(restoreSender);
|
|
425
294
|
sessionLinks.clear();
|
|
426
295
|
tag.remove();
|
|
427
296
|
});
|
package/lib/index.js
CHANGED
|
@@ -1,18 +1,39 @@
|
|
|
1
1
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
2
2
|
import { createUserMessage, freezeMessage } from '@deepseek-ai/dsh-llm/message'
|
|
3
|
+
import z from '@deepseek-ai/schemastery'
|
|
4
|
+
import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings'
|
|
3
5
|
|
|
4
6
|
export const name = 'dsh-agent-message'
|
|
5
7
|
export const inject = ['agents', 'tools', 'sessionQuery']
|
|
6
8
|
|
|
9
|
+
const AGENT_MESSAGE_SETTINGS_NAMESPACE = settingsNamespace('agent-message')
|
|
10
|
+
const AGENT_MESSAGE_SETTINGS_SCHEMA = z.object({ showTechnicalDetails: z.boolean().default(true) })
|
|
11
|
+
|
|
7
12
|
export function apply(ctx) {
|
|
8
13
|
const agents = ctx.agents
|
|
14
|
+
let settingsSource = () => ({ showTechnicalDetails: true })
|
|
9
15
|
/** messageId -> { to, at, mode };发送成功即记账,供批量查询及补充本进程发送信息。 */
|
|
10
16
|
const sent = new Map()
|
|
11
17
|
/** 记账表 FIFO 上限:超过则淘汰最老记录,内存恒定。 */
|
|
12
18
|
const SENT_MAX = 1000
|
|
19
|
+
const PAIR_MESSAGE_LIMIT = 10
|
|
20
|
+
const PAIR_WINDOW_MS = 60_000
|
|
13
21
|
const receiptMeaning = 'claimed 仅表示目标 turn 已从 Inbox 认领消息;传输回执不表示对方已读、回复或完成。'
|
|
14
22
|
/** 插件恢复的 Session handle;保留到插件卸载,避免 idle dispose 移除 Harness store 投影。 */
|
|
15
23
|
const resumedHandles = new Map()
|
|
24
|
+
/** 无向 Session 对 -> 最近成功或正在进行的投递预留;只保护当前 Harness 进程。 */
|
|
25
|
+
const pairSends = new Map()
|
|
26
|
+
let lastPairPruneAt = 0
|
|
27
|
+
|
|
28
|
+
// 设置由 Cordis 注入;旧版测试宿主没有 inject 时继续使用默认行为。
|
|
29
|
+
if (typeof ctx.inject === 'function') {
|
|
30
|
+
installSettingsSection(ctx, AGENT_MESSAGE_SETTINGS_NAMESPACE, AGENT_MESSAGE_SETTINGS_SCHEMA, { showTechnicalDetails: true }, {
|
|
31
|
+
setSource(source) {
|
|
32
|
+
settingsSource = source
|
|
33
|
+
},
|
|
34
|
+
onChange() {},
|
|
35
|
+
})
|
|
36
|
+
}
|
|
16
37
|
|
|
17
38
|
ctx.effect(() => async () => {
|
|
18
39
|
const handles = [...resumedHandles.values()]
|
|
@@ -37,6 +58,35 @@ export function apply(ctx) {
|
|
|
37
58
|
if (sent.size > SENT_MAX) sent.delete(sent.keys().next().value)
|
|
38
59
|
}
|
|
39
60
|
|
|
61
|
+
function reservePairSend(from, to) {
|
|
62
|
+
const now = Date.now()
|
|
63
|
+
if (now < lastPairPruneAt || now - lastPairPruneAt >= PAIR_WINDOW_MS) {
|
|
64
|
+
for (const [key, entries] of pairSends) {
|
|
65
|
+
const active = entries.filter((entry) => now - entry.at < PAIR_WINDOW_MS)
|
|
66
|
+
if (active.length === 0) pairSends.delete(key)
|
|
67
|
+
else pairSends.set(key, active)
|
|
68
|
+
}
|
|
69
|
+
lastPairPruneAt = now
|
|
70
|
+
}
|
|
71
|
+
const key = JSON.stringify([String(from), String(to)].sort())
|
|
72
|
+
const recent = (pairSends.get(key) ?? []).filter((entry) => now - entry.at < PAIR_WINDOW_MS)
|
|
73
|
+
if (recent.length >= PAIR_MESSAGE_LIMIT) {
|
|
74
|
+
pairSends.set(key, recent)
|
|
75
|
+
const retryAfterSeconds = Math.max(1, Math.ceil((PAIR_WINDOW_MS - (now - recent[0].at)) / 1000))
|
|
76
|
+
throw new Error('同一对会话 60 秒内最多投递 10 条消息;请勿立即重试,约 ' + retryAfterSeconds + ' 秒后窗口重置')
|
|
77
|
+
}
|
|
78
|
+
const reservation = { at: now }
|
|
79
|
+
recent.push(reservation)
|
|
80
|
+
pairSends.set(key, recent)
|
|
81
|
+
return () => {
|
|
82
|
+
const current = pairSends.get(key)
|
|
83
|
+
if (current === undefined) return
|
|
84
|
+
const index = current.indexOf(reservation)
|
|
85
|
+
if (index !== -1) current.splice(index, 1)
|
|
86
|
+
if (current.length === 0) pairSends.delete(key)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
40
90
|
function titleOf(agent) {
|
|
41
91
|
const service = ctx.get('sessionTitle')
|
|
42
92
|
if (service !== undefined) {
|
|
@@ -71,6 +121,12 @@ export function apply(ctx) {
|
|
|
71
121
|
return header?.origin === 'subagent'
|
|
72
122
|
}
|
|
73
123
|
|
|
124
|
+
function assertPeerCaller(agent) {
|
|
125
|
+
if (agent !== undefined && isSubagentSession(agent.session.header)) {
|
|
126
|
+
throw new Error('子代理不能使用独立会话通信工具')
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
74
130
|
function sessionQuery() {
|
|
75
131
|
const query = ctx.get('sessionQuery')
|
|
76
132
|
if (query === undefined) throw new Error('本部署缺少 sessionQuery,无法查询逻辑会话')
|
|
@@ -180,6 +236,7 @@ export function apply(ctx) {
|
|
|
180
236
|
},
|
|
181
237
|
async execute(_args, exec) {
|
|
182
238
|
const me = exec.agent
|
|
239
|
+
assertPeerCaller(me)
|
|
183
240
|
const archived = archivedIds()
|
|
184
241
|
const query = sessionQuery()
|
|
185
242
|
const live = new Map()
|
|
@@ -230,6 +287,7 @@ export function apply(ctx) {
|
|
|
230
287
|
'默认使用 followup 创建独立的新 turn;' +
|
|
231
288
|
'目标离线(进程里未加载)时自动恢复该会话后投递。' +
|
|
232
289
|
'用户无需说出模式名:目标为 running 且整句明确要求立即介入时用 steer,明确要求不打断当前任务、只补充上下文时用 inject;不确定时使用 followup。' +
|
|
290
|
+
'同一对 Session 双向合计 60 秒内最多投递 10 条消息,超过时拒绝本次投递。' +
|
|
233
291
|
'归档会话和子代理一律拒绝。' +
|
|
234
292
|
'注意:它不同于 send_message(后者是给你的后台子代理续聊)。',
|
|
235
293
|
parameters: {
|
|
@@ -240,14 +298,36 @@ export function apply(ctx) {
|
|
|
240
298
|
output: {
|
|
241
299
|
schema: { type: 'json' },
|
|
242
300
|
render(_args, value) {
|
|
243
|
-
return [{ type: 'text', text:
|
|
301
|
+
return [{ type: 'text', text: '已投递。\nmessageId: ' + value.messageId }]
|
|
302
|
+
},
|
|
303
|
+
presentationMeta(_args, value) {
|
|
304
|
+
return value
|
|
244
305
|
},
|
|
245
306
|
},
|
|
307
|
+
presentResult(_args, result) {
|
|
308
|
+
const meta = result.meta
|
|
309
|
+
if (result.isError || meta === null || typeof meta !== 'object' || Array.isArray(meta)) return
|
|
310
|
+
const messageId = typeof meta.messageId === 'string' ? meta.messageId : ''
|
|
311
|
+
const text = (typeof meta.text === 'string' ? meta.text : '已投递。')
|
|
312
|
+
+ (messageId ? '\nmessageId: ' + messageId : '')
|
|
313
|
+
const details = settingsSource().showTechnicalDetails
|
|
314
|
+
? '\n' + JSON.stringify({ to: meta.to, mode: meta.mode, state: meta.state, targetRuntimeStatus: meta.targetRuntimeStatus })
|
|
315
|
+
: ''
|
|
316
|
+
return {
|
|
317
|
+
card: 'generic',
|
|
318
|
+
title: '消息已投递',
|
|
319
|
+
content: [{ type: 'text', text: text + details }],
|
|
320
|
+
}
|
|
321
|
+
},
|
|
246
322
|
async execute(args, exec) {
|
|
247
323
|
const me = exec.agent
|
|
248
324
|
if (me === undefined) throw new Error('no calling agent')
|
|
325
|
+
assertPeerCaller(me)
|
|
249
326
|
const to = args.to
|
|
250
327
|
if (to === '' || String(to) === String(me.id)) throw new Error('不能给自己发消息')
|
|
328
|
+
if (args.content.includes('<dsh-agent-message>') || args.content.includes('</dsh-agent-message>')) {
|
|
329
|
+
throw new Error('消息正文不能包含保留协议标签 dsh-agent-message')
|
|
330
|
+
}
|
|
251
331
|
|
|
252
332
|
const archived = archivedIds()
|
|
253
333
|
if (archived.has(String(to))) throw new Error('对方会话已归档,无法发送(请先取消归档)')
|
|
@@ -260,6 +340,14 @@ export function apply(ctx) {
|
|
|
260
340
|
throw new Error(mode + ' 仅用于 running 会话;目标当前状态:' + target.status)
|
|
261
341
|
}
|
|
262
342
|
|
|
343
|
+
let inspected
|
|
344
|
+
if (target === undefined) {
|
|
345
|
+
inspected = await readLogicalSession(to)
|
|
346
|
+
if (inspected === undefined) throw new Error('会话不存在:' + to)
|
|
347
|
+
if (isSubagentSession(inspected.session)) throw new Error('目标是子代理,不能通过会话通信插件直接发送')
|
|
348
|
+
if (mode !== 'followup') throw new Error('目标离线(进程里未加载):' + mode + ' 仅用于 running 会话')
|
|
349
|
+
}
|
|
350
|
+
|
|
263
351
|
const myTitle = titleOf(me) || String(me.id)
|
|
264
352
|
const source = {
|
|
265
353
|
kind: name,
|
|
@@ -279,19 +367,21 @@ export function apply(ctx) {
|
|
|
279
367
|
let usedMode = ''
|
|
280
368
|
let targetRuntimeStatus = 'offline'
|
|
281
369
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
370
|
+
const rollbackPairSend = reservePairSend(me.id, to)
|
|
371
|
+
try {
|
|
372
|
+
if (target !== undefined) {
|
|
373
|
+
if (mode === 'followup') { target.followup(message); usedMode = mode }
|
|
374
|
+
else if (mode === 'inject') { target.inject(message); usedMode = 'inject' }
|
|
375
|
+
else { target.steer(message); usedMode = 'steer' }
|
|
376
|
+
targetRuntimeStatus = target.status
|
|
377
|
+
} else {
|
|
378
|
+
const resumed = await resumeAndFollowup(to, message, inspected)
|
|
379
|
+
usedMode = 'followup'
|
|
380
|
+
targetRuntimeStatus = resumed.status
|
|
381
|
+
}
|
|
382
|
+
} catch (error) {
|
|
383
|
+
rollbackPairSend()
|
|
384
|
+
throw error
|
|
295
385
|
}
|
|
296
386
|
|
|
297
387
|
rememberSent(message.id, String(to), usedMode)
|
|
@@ -324,7 +414,8 @@ export function apply(ctx) {
|
|
|
324
414
|
return [{ type: 'text', text: JSON.stringify(value, null, 2) }]
|
|
325
415
|
},
|
|
326
416
|
},
|
|
327
|
-
async execute(args) {
|
|
417
|
+
async execute(args, exec) {
|
|
418
|
+
assertPeerCaller(exec?.agent)
|
|
328
419
|
const target = agents.get(args.to)
|
|
329
420
|
const targetRuntimeStatus = target !== undefined ? target.status : 'offline'
|
|
330
421
|
if (args.messageId !== undefined) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-agent-message",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"packageManager": "pnpm@11.1.1",
|
|
5
5
|
"description": "跨会话 Agent 通信:让 DeepSeek Harness 里不同的 Agent 会话互相收发消息。",
|
|
6
6
|
"keywords": ["deepseek", "deepseek-harness", "agent", "multi-agent", "messaging"],
|
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
},
|
|
13
13
|
"type": "module",
|
|
14
14
|
"main": "./lib/index.js",
|
|
15
|
-
"scripts": {
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node --test",
|
|
17
|
+
"test:web-smoke": "node scripts/web-smoke.mjs"
|
|
18
|
+
},
|
|
16
19
|
"exports": {
|
|
17
20
|
".": { "default": "./lib/index.js" },
|
|
18
21
|
"./client": { "default": "./lib/client.js" },
|
|
@@ -26,10 +29,11 @@
|
|
|
26
29
|
"platform": "web",
|
|
27
30
|
"inject": [
|
|
28
31
|
"@deepseek-ai/dsh-client-runtime",
|
|
32
|
+
"@deepseek-ai/dsh-client-locale",
|
|
29
33
|
"@deepseek-ai/dsh-client-ui-slots",
|
|
30
|
-
"@deepseek-ai/dsh-client-ui-
|
|
31
|
-
"@deepseek-ai/dsh-client-ui-
|
|
32
|
-
"@deepseek-ai/dsh-client-ui-
|
|
34
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
35
|
+
"@deepseek-ai/dsh-client-ui-settings-plugins",
|
|
36
|
+
"@deepseek-ai/dsh-client-ui-conversation"
|
|
33
37
|
]
|
|
34
38
|
}
|
|
35
39
|
},
|
|
@@ -41,16 +45,20 @@
|
|
|
41
45
|
},
|
|
42
46
|
"peerDependencies": {
|
|
43
47
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
44
|
-
"@deepseek-ai/dsh-
|
|
45
|
-
"@deepseek-ai/dsh-
|
|
48
|
+
"@deepseek-ai/dsh-settings": ">=0.1.1-rc.2 <0.2.0",
|
|
49
|
+
"@deepseek-ai/dsh-llm": ">=0.1.1-rc.2 <0.2.0",
|
|
50
|
+
"@deepseek-ai/dsh-tools": ">=0.1.1-rc.2 <0.2.0",
|
|
51
|
+
"@deepseek-ai/schemastery": ">=3.18.1 <4.0.0"
|
|
46
52
|
},
|
|
47
53
|
"devDependencies": {
|
|
48
|
-
"@deepseek-ai/dsh-llm": "0.1.
|
|
49
|
-
"@deepseek-ai/dsh-
|
|
54
|
+
"@deepseek-ai/dsh-llm": "0.1.1-rc.2",
|
|
55
|
+
"@deepseek-ai/dsh-settings": "0.1.1-rc.2",
|
|
56
|
+
"@deepseek-ai/dsh-tools": "0.1.1-rc.2",
|
|
57
|
+
"@deepseek-ai/schemastery": "3.18.1"
|
|
50
58
|
},
|
|
51
59
|
"engines": {
|
|
52
60
|
"node": ">=24 <25",
|
|
53
|
-
"dsh": ">=0.1.
|
|
61
|
+
"dsh": ">=0.1.1-rc.2 <0.2.0"
|
|
54
62
|
},
|
|
55
63
|
"license": "MIT"
|
|
56
64
|
}
|