feat: complete Dirigent rename + all TASKLIST items
- Task 1: Identity prompt now includes Discord userId
- Task 2: Added configurable schedulingIdentifier (default: ➡️)
- Task 3: Moderator handoff uses <@userId>+identifier instead of semantic messages
- Task 4: All prompts/comments/help text converted to English
- Task 5: Full project rename WhisperGate → Dirigent across all files
Breaking: config key changed from plugins.entries.whispergate to plugins.entries.dirigent
Breaking: channel policies file renamed to dirigent-channel-policies.json
Breaking: tool name changed from whispergate_tools to dirigent_tools
This commit is contained in:
@@ -4,15 +4,15 @@
|
|||||||
|
|
||||||
- Added no-reply API service (`/v1/chat/completions`, `/v1/responses`, `/v1/models`)
|
- Added no-reply API service (`/v1/chat/completions`, `/v1/responses`, `/v1/models`)
|
||||||
- Added optional bearer auth (`AUTH_TOKEN`)
|
- Added optional bearer auth (`AUTH_TOKEN`)
|
||||||
- Added WhisperGate plugin with deterministic rule gate
|
- Added Dirigent plugin with deterministic rule gate
|
||||||
- Added discord-specific 🔚 prompt injection for bypass/end-symbol paths
|
- Added discord-specific 🔚 prompt injection for bypass/end-symbol paths
|
||||||
- Added containerization (`Dockerfile`, `docker-compose.yml`)
|
- Added containerization (`Dockerfile`, `docker-compose.yml`)
|
||||||
- Added helper scripts for smoke/dev lifecycle and rule validation
|
- Added helper scripts for smoke/dev lifecycle and rule validation
|
||||||
- Added no-touch config rendering and integration docs
|
- Added no-touch config rendering and integration docs
|
||||||
- Added installer script with rollback (`scripts/install-whispergate-openclaw.sh`)
|
- Added installer script with rollback (`scripts/install-dirigent-openclaw.sh`)
|
||||||
- supports `--install` / `--uninstall`
|
- supports `--install` / `--uninstall`
|
||||||
- uninstall restores all recorded changes
|
- uninstall restores all recorded changes
|
||||||
- writes install/uninstall records under `~/.openclaw/whispergate-install-records/`
|
- writes install/uninstall records under `~/.openclaw/dirigent-install-records/`
|
||||||
- Added discord-control-api with:
|
- Added discord-control-api with:
|
||||||
- `channel-private-create` (create private channel for allowlist)
|
- `channel-private-create` (create private channel for allowlist)
|
||||||
- `channel-private-update` (update allowlist/overwrites for existing channel)
|
- `channel-private-update` (update allowlist/overwrites for existing channel)
|
||||||
|
|||||||
18
README.md
18
README.md
@@ -1,10 +1,10 @@
|
|||||||
# WhisperGate
|
# Dirigent
|
||||||
|
|
||||||
Rule-based no-reply gate + turn manager for OpenClaw (Discord).
|
Rule-based no-reply gate + turn manager for OpenClaw (Discord).
|
||||||
|
|
||||||
## What it does
|
## What it does
|
||||||
|
|
||||||
WhisperGate adds deterministic logic **before model selection** and **turn-based speaking** for multi-agent Discord channels:
|
Dirigent adds deterministic logic **before model selection** and **turn-based speaking** for multi-agent Discord channels:
|
||||||
|
|
||||||
- **Rule gate (before_model_resolve)**
|
- **Rule gate (before_model_resolve)**
|
||||||
1. Non-Discord → skip
|
1. Non-Discord → skip
|
||||||
@@ -27,11 +27,11 @@ WhisperGate adds deterministic logic **before model selection** and **turn-based
|
|||||||
|
|
||||||
- **Per-channel policy runtime**
|
- **Per-channel policy runtime**
|
||||||
- Policies stored in a standalone JSON file
|
- Policies stored in a standalone JSON file
|
||||||
- Update at runtime via `whispergate_tools` (memory first → persist to file)
|
- Update at runtime via `dirigent_tools` (memory first → persist to file)
|
||||||
|
|
||||||
- **Discord control actions (optional)**
|
- **Discord control actions (optional)**
|
||||||
- Private channel create/update + member list
|
- Private channel create/update + member list
|
||||||
- Unified via `whispergate_tools`
|
- Unified via `dirigent_tools`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ Discord 扩展能力见:`docs/DISCORD_CONTROL.md`。
|
|||||||
|
|
||||||
## Runtime tools & commands
|
## Runtime tools & commands
|
||||||
|
|
||||||
### Tool: `whispergate_tools`
|
### Tool: `dirigent_tools`
|
||||||
|
|
||||||
Actions:
|
Actions:
|
||||||
- `policy-get`, `policy-set-channel`, `policy-delete-channel`
|
- `policy-get`, `policy-set-channel`, `policy-delete-channel`
|
||||||
@@ -77,10 +77,10 @@ Actions:
|
|||||||
### Slash command (Discord)
|
### Slash command (Discord)
|
||||||
|
|
||||||
```
|
```
|
||||||
/whispergate status
|
/dirigent status
|
||||||
/whispergate turn-status
|
/dirigent turn-status
|
||||||
/whispergate turn-advance
|
/dirigent turn-advance
|
||||||
/whispergate turn-reset
|
/dirigent turn-reset
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
47
TASKLIST.md
47
TASKLIST.md
@@ -3,32 +3,47 @@
|
|||||||
> Note: Project rename from WhisperGate → Dirigent implies updating all code/docs references (plugin/tool names, strings, files, configs).
|
> Note: Project rename from WhisperGate → Dirigent implies updating all code/docs references (plugin/tool names, strings, files, configs).
|
||||||
|
|
||||||
## 1) Identity Prompt Enhancements
|
## 1) Identity Prompt Enhancements
|
||||||
- Current prompt only includes agent-id + discord name.
|
- ✅ Added Discord userId to identity injection via `resolveDiscordUserId()`.
|
||||||
- **Add Discord userId** to identity injection.
|
- Identity format now: `You are <name> (Discord account: <accountId>, Discord userId: <userId>).`
|
||||||
|
|
||||||
## 2) Scheduling Identifier (Default: ➡️)
|
## 2) Scheduling Identifier (Default: ➡️)
|
||||||
- Add a **configurable scheduling identifier** (default: `➡️`).
|
- ✅ Added `schedulingIdentifier` config field (default: `➡️`) to `DirigentConfig` and `openclaw.plugin.json`.
|
||||||
- Update agent prompt to explain:
|
- ✅ Updated `buildEndMarkerInstruction()` to explain scheduling identifier semantics to agents:
|
||||||
- The scheduling identifier itself is meaningless.
|
- The identifier itself is meaningless.
|
||||||
- When receiving `<@USER_ID>` + scheduling identifier, the agent should check chat history and decide whether to reply.
|
- When receiving `<@USER_ID>` + identifier, check chat history and decide whether to reply.
|
||||||
- If no reply needed, return `NO_REPLY`.
|
- If nothing to say, reply `NO_REPLY`.
|
||||||
|
|
||||||
## 3) Moderator Handoff Message Format
|
## 3) Moderator Handoff Message Format
|
||||||
- Moderator should **no longer send semantic messages** to activate agents.
|
- ✅ Moderator no longer sends semantic messages.
|
||||||
- Replace with: `<@TARGET_USER_ID>` + scheduling identifier (e.g., `<@123>➡️`).
|
- Handoff format is now: `<@TARGET_USER_ID>` + scheduling identifier (e.g., `<@123>➡️`).
|
||||||
|
|
||||||
## 4) Prompt Language
|
## 4) Prompt Language
|
||||||
- **All prompts must be in English** (including end-marker instructions and group-chat rules).
|
- ✅ All prompts converted to English:
|
||||||
|
- `buildEndMarkerInstruction()` — English with scheduling identifier explanation
|
||||||
|
- `buildAgentIdentity()` — English format
|
||||||
|
- Slash command help text — English
|
||||||
|
- Error messages — English
|
||||||
|
- Code comments — English
|
||||||
|
|
||||||
## 5) Full Project Rename
|
## 5) Full Project Rename
|
||||||
- Project name changed to **Dirigent**.
|
- ✅ Plugin id: `whispergate` → `dirigent`
|
||||||
- Update **all strings** across repo:
|
- ✅ Plugin name: `WhisperGate` → `Dirigent`
|
||||||
- plugin name/id
|
- ✅ Tool name: `whispergate_tools` → `dirigent_tools`
|
||||||
- tool name(s)
|
- ✅ Config type: `WhisperGateConfig` → `DirigentConfig`
|
||||||
- docs, config, scripts, examples
|
- ✅ Config lookup key: `entries.whispergate` → `entries.dirigent`
|
||||||
- any text mentions
|
- ✅ Channel policies file: `whispergate-channel-policies.json` → `dirigent-channel-policies.json`
|
||||||
|
- ✅ Log prefixes: `whispergate:` → `dirigent:`
|
||||||
|
- ✅ Slash command: `/whispergate` → `/dirigent`
|
||||||
|
- ✅ Gateway browser/device identifier: `whispergate` → `dirigent`
|
||||||
|
- ✅ Scripts renamed: `install-whispergate-*` → `install-dirigent-*`
|
||||||
|
- ✅ All docs, configs, examples updated
|
||||||
|
- ✅ dist/ folder: `dist/whispergate/` → `dist/dirigent/`
|
||||||
|
- ✅ package.json names updated
|
||||||
|
- ✅ README.md, CHANGELOG.md updated
|
||||||
|
- ✅ Version bumped to 0.2.0
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Open Items / Notes
|
## Open Items / Notes
|
||||||
- User requested the previous README commit should have been pushed to `main` directly (was pushed to a branch). Address separately if needed.
|
- User requested the previous README commit should have been pushed to `main` directly (was pushed to a branch). Address separately if needed.
|
||||||
|
- **Migration note**: Existing deployments need to update their `openclaw.json` config from `plugins.entries.whispergate` → `plugins.entries.dirigent` and rename the channel policies file.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "whispergate-discord-control-api",
|
"name": "dirigent-discord-control-api",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
1150
dist/whispergate/index.ts
vendored
1150
dist/whispergate/index.ts
vendored
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,11 @@
|
|||||||
services:
|
services:
|
||||||
whispergate-no-reply-api:
|
dirigent-no-reply-api:
|
||||||
build:
|
build:
|
||||||
context: ./no-reply-api
|
context: ./no-reply-api
|
||||||
container_name: whispergate-no-reply-api
|
container_name: dirigent-no-reply-api
|
||||||
ports:
|
ports:
|
||||||
- "8787:8787"
|
- "8787:8787"
|
||||||
environment:
|
environment:
|
||||||
- PORT=8787
|
- PORT=8787
|
||||||
- NO_REPLY_MODEL=whispergate-no-reply-v1
|
- NO_REPLY_MODEL=dirigent-no-reply-v1
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"load": {
|
"load": {
|
||||||
"paths": ["/path/to/WhisperGate/dist/whispergate"]
|
"paths": ["/path/to/Dirigent/dist/dirigent"]
|
||||||
},
|
},
|
||||||
"entries": {
|
"entries": {
|
||||||
"whispergate": {
|
"dirigent": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"config": {
|
"config": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@@ -13,8 +13,8 @@
|
|||||||
"humanList": ["561921120408698910"],
|
"humanList": ["561921120408698910"],
|
||||||
"agentList": [],
|
"agentList": [],
|
||||||
"endSymbols": ["🔚"],
|
"endSymbols": ["🔚"],
|
||||||
"channelPoliciesFile": "~/.openclaw/whispergate-channel-policies.json",
|
"channelPoliciesFile": "~/.openclaw/dirigent-channel-policies.json",
|
||||||
"noReplyProvider": "whisper-gateway",
|
"noReplyProvider": "dirigentway",
|
||||||
"noReplyModel": "no-reply",
|
"noReplyModel": "no-reply",
|
||||||
"enableDiscordControlTool": true,
|
"enableDiscordControlTool": true,
|
||||||
"enableWhispergatePolicyTool": true,
|
"enableWhispergatePolicyTool": true,
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
},
|
},
|
||||||
"models": {
|
"models": {
|
||||||
"providers": {
|
"providers": {
|
||||||
"whisper-gateway": {
|
"dirigentway": {
|
||||||
"apiKey": "<NO_REPLY_API_TOKEN_OR_PLACEHOLDER>",
|
"apiKey": "<NO_REPLY_API_TOKEN_OR_PLACEHOLDER>",
|
||||||
"baseUrl": "http://127.0.0.1:8787/v1",
|
"baseUrl": "http://127.0.0.1:8787/v1",
|
||||||
"api": "openai-completions",
|
"api": "openai-completions",
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
{
|
{
|
||||||
"id": "main",
|
"id": "main",
|
||||||
"tools": {
|
"tools": {
|
||||||
"allow": ["whispergate"]
|
"allow": ["dirigent"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
目标:补齐 OpenClaw 内置 message 工具当前未覆盖的两个能力:
|
目标:补齐 OpenClaw 内置 message 工具当前未覆盖的两个能力:
|
||||||
|
|
||||||
> 现在可以通过 WhisperGate 插件内置的可选工具 `discord_control` 直接调用(无需手写 curl)。
|
> 现在可以通过 Dirigent 插件内置的可选工具 `discord_control` 直接调用(无需手写 curl)。
|
||||||
> 注意:该工具是 optional,需要在 agent tools allowlist 中显式允许(例如允许 `whispergate` 或 `discord_control`)。
|
> 注意:该工具是 optional,需要在 agent tools allowlist 中显式允许(例如允许 `dirigent` 或 `discord_control`)。
|
||||||
|
|
||||||
1. 创建指定名单可见的私人频道
|
1. 创建指定名单可见的私人频道
|
||||||
2. 查看 server 成员列表(分页)
|
2. 查看 server 成员列表(分页)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# WhisperGate Implementation Notes
|
# Dirigent Implementation Notes
|
||||||
|
|
||||||
## Decision path
|
## Decision path
|
||||||
|
|
||||||
WhisperGate evaluates in strict order:
|
Dirigent evaluates in strict order:
|
||||||
|
|
||||||
1. channel check (discord-only)
|
1. channel check (discord-only)
|
||||||
2. bypass sender check
|
2. bypass sender check
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# WhisperGate Integration (No-touch Template)
|
# Dirigent Integration (No-touch Template)
|
||||||
|
|
||||||
This guide **does not** change your current OpenClaw config automatically.
|
This guide **does not** change your current OpenClaw config automatically.
|
||||||
It only generates a JSON snippet you can review.
|
It only generates a JSON snippet you can review.
|
||||||
@@ -7,9 +7,9 @@ It only generates a JSON snippet you can review.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
node scripts/render-openclaw-config.mjs \
|
node scripts/render-openclaw-config.mjs \
|
||||||
/absolute/path/to/WhisperGate/plugin \
|
/absolute/path/to/Dirigent/plugin \
|
||||||
openai \
|
openai \
|
||||||
whispergate-no-reply-v1 \
|
dirigent-no-reply-v1 \
|
||||||
561921120408698910
|
561921120408698910
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ Arguments:
|
|||||||
|
|
||||||
The script prints JSON for:
|
The script prints JSON for:
|
||||||
- `plugins.load.paths`
|
- `plugins.load.paths`
|
||||||
- `plugins.entries.whispergate.config`
|
- `plugins.entries.dirigent.config`
|
||||||
|
|
||||||
You can merge this snippet manually into your `openclaw.json`.
|
You can merge this snippet manually into your `openclaw.json`.
|
||||||
|
|
||||||
@@ -32,20 +32,20 @@ You can merge this snippet manually into your `openclaw.json`.
|
|||||||
For production-like install with automatic rollback on error (Node-only installer):
|
For production-like install with automatic rollback on error (Node-only installer):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node ./scripts/install-whispergate-openclaw.mjs --install
|
node ./scripts/install-dirigent-openclaw.mjs --install
|
||||||
# or wrapper
|
# or wrapper
|
||||||
./scripts/install-whispergate-openclaw.sh --install
|
./scripts/install-dirigent-openclaw.sh --install
|
||||||
```
|
```
|
||||||
|
|
||||||
Uninstall (revert all recorded config changes):
|
Uninstall (revert all recorded config changes):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node ./scripts/install-whispergate-openclaw.mjs --uninstall
|
node ./scripts/install-dirigent-openclaw.mjs --uninstall
|
||||||
# or wrapper
|
# or wrapper
|
||||||
./scripts/install-whispergate-openclaw.sh --uninstall
|
./scripts/install-dirigent-openclaw.sh --uninstall
|
||||||
# or specify a record explicitly
|
# or specify a record explicitly
|
||||||
# RECORD_FILE=~/.openclaw/whispergate-install-records/whispergate-YYYYmmddHHMMSS.json \
|
# RECORD_FILE=~/.openclaw/dirigent-install-records/dirigent-YYYYmmddHHMMSS.json \
|
||||||
# node ./scripts/install-whispergate-openclaw.mjs --uninstall
|
# node ./scripts/install-dirigent-openclaw.mjs --uninstall
|
||||||
```
|
```
|
||||||
|
|
||||||
Environment overrides:
|
Environment overrides:
|
||||||
@@ -66,15 +66,15 @@ The script:
|
|||||||
- writes via `openclaw config set ... --json`
|
- writes via `openclaw config set ... --json`
|
||||||
- creates config backup first
|
- creates config backup first
|
||||||
- restores backup automatically if any install step fails
|
- restores backup automatically if any install step fails
|
||||||
- restarts gateway during install, then validates `whisper-gateway/no-reply` is visible via `openclaw models list/status`
|
- restarts gateway during install, then validates `dirigentway/no-reply` is visible via `openclaw models list/status`
|
||||||
- writes a change record for every install/uninstall:
|
- writes a change record for every install/uninstall:
|
||||||
- directory: `~/.openclaw/whispergate-install-records/`
|
- directory: `~/.openclaw/dirigent-install-records/`
|
||||||
- latest pointer: `~/.openclaw/whispergate-install-record-latest.json`
|
- latest pointer: `~/.openclaw/dirigent-install-record-latest.json`
|
||||||
|
|
||||||
Policy state semantics:
|
Policy state semantics:
|
||||||
- channel policy file is loaded once into memory on startup
|
- channel policy file is loaded once into memory on startup
|
||||||
- runtime decisions use in-memory state
|
- runtime decisions use in-memory state
|
||||||
- use `whispergate_policy` tool to update state (memory first, then file persist)
|
- use `dirigent_policy` tool to update state (memory first, then file persist)
|
||||||
- manual file edits do not auto-apply until next restart
|
- manual file edits do not auto-apply until next restart
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
# PR Summary (WhisperGate + Discord Control)
|
# PR Summary (Dirigent + Discord Control)
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
This PR delivers two tracks:
|
This PR delivers two tracks:
|
||||||
|
|
||||||
1. WhisperGate deterministic no-reply gate for Discord sessions
|
1. Dirigent deterministic no-reply gate for Discord sessions
|
||||||
2. Discord control extension API for private-channel/member-list gaps
|
2. Discord control extension API for private-channel/member-list gaps
|
||||||
|
|
||||||
## Delivered Features
|
## Delivered Features
|
||||||
|
|
||||||
### WhisperGate
|
### Dirigent
|
||||||
|
|
||||||
- Deterministic rule chain:
|
- Deterministic rule chain:
|
||||||
1) non-discord => skip
|
1) non-discord => skip
|
||||||
|
|||||||
@@ -8,17 +8,17 @@ node scripts/package-plugin.mjs
|
|||||||
|
|
||||||
Output:
|
Output:
|
||||||
|
|
||||||
- `dist/whispergate/index.ts`
|
- `dist/dirigent/index.ts`
|
||||||
- `dist/whispergate/rules.ts`
|
- `dist/dirigent/rules.ts`
|
||||||
- `dist/whispergate/openclaw.plugin.json`
|
- `dist/dirigent/openclaw.plugin.json`
|
||||||
- `dist/whispergate/README.md`
|
- `dist/dirigent/README.md`
|
||||||
- `dist/whispergate/package.json`
|
- `dist/dirigent/package.json`
|
||||||
|
|
||||||
## Use packaged plugin path
|
## Use packaged plugin path
|
||||||
|
|
||||||
Point OpenClaw `plugins.load.paths` to:
|
Point OpenClaw `plugins.load.paths` to:
|
||||||
|
|
||||||
`/absolute/path/to/WhisperGate/dist/whispergate`
|
`/absolute/path/to/Dirigent/dist/dirigent`
|
||||||
|
|
||||||
## Verify package completeness
|
## Verify package completeness
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# WhisperGate Rollout Checklist
|
# Dirigent Rollout Checklist
|
||||||
|
|
||||||
## Stage 0: Local sanity
|
## Stage 0: Local sanity
|
||||||
|
|
||||||
@@ -29,5 +29,5 @@
|
|||||||
|
|
||||||
## Rollback
|
## Rollback
|
||||||
|
|
||||||
- Disable plugin entry `whispergate.enabled=false` OR remove plugin path
|
- Disable plugin entry `dirigent.enabled=false` OR remove plugin path
|
||||||
- Keep API service running; it is inert when plugin disabled
|
- Keep API service running; it is inert when plugin disabled
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Run Modes
|
# Run Modes
|
||||||
|
|
||||||
WhisperGate has two runtime components:
|
Dirigent has two runtime components:
|
||||||
|
|
||||||
1. `plugin/` (OpenClaw plugin)
|
1. `plugin/` (OpenClaw plugin)
|
||||||
2. `no-reply-api/` (deterministic NO_REPLY service)
|
2. `no-reply-api/` (deterministic NO_REPLY service)
|
||||||
@@ -20,7 +20,7 @@ Then configure OpenClaw provider `baseURL` to `http://127.0.0.1:8787/v1`.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/dev-up.sh
|
./scripts/dev-up.sh
|
||||||
# or: docker compose up -d --build whispergate-no-reply-api
|
# or: docker compose up -d --build dirigent-no-reply-api
|
||||||
```
|
```
|
||||||
|
|
||||||
Stop:
|
Stop:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# WhisperGate 测试记录报告(阶段性)
|
# Dirigent 测试记录报告(阶段性)
|
||||||
|
|
||||||
日期:2026-02-25
|
日期:2026-02-25
|
||||||
|
|
||||||
@@ -6,19 +6,19 @@
|
|||||||
|
|
||||||
本轮覆盖:
|
本轮覆盖:
|
||||||
|
|
||||||
1. WhisperGate 基础静态与脚本测试
|
1. Dirigent 基础静态与脚本测试
|
||||||
2. no-reply-api 隔离集成测试
|
2. no-reply-api 隔离集成测试
|
||||||
3. discord-control-api 功能测试(dryRun + 实操)
|
3. discord-control-api 功能测试(dryRun + 实操)
|
||||||
|
|
||||||
未覆盖:
|
未覆盖:
|
||||||
|
|
||||||
- WhisperGate 插件真实挂载 OpenClaw 后的端到端(E2E)
|
- Dirigent 插件真实挂载 OpenClaw 后的端到端(E2E)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 二、测试环境
|
## 二、测试环境
|
||||||
|
|
||||||
- 代码仓库:`/root/.openclaw/workspace-operator/WhisperGate`
|
- 代码仓库:`/root/.openclaw/workspace-operator/Dirigent`
|
||||||
- OpenClaw 配置来源:本机已有配置(读取 Discord token)
|
- OpenClaw 配置来源:本机已有配置(读取 Discord token)
|
||||||
- Discord guild(server)ID:`1368531017534537779`
|
- Discord guild(server)ID:`1368531017534537779`
|
||||||
- allowlist user IDs:
|
- allowlist user IDs:
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
|
|
||||||
## 三、已执行测试与结果
|
## 三、已执行测试与结果
|
||||||
|
|
||||||
### A. WhisperGate 基础测试
|
### A. Dirigent 基础测试
|
||||||
|
|
||||||
命令:
|
命令:
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ make check check-rules test-api
|
|||||||
|
|
||||||
## 五、待测项(下一阶段)
|
## 五、待测项(下一阶段)
|
||||||
|
|
||||||
### 1) WhisperGate 插件 E2E(需临时接入 OpenClaw 配置)
|
### 1) Dirigent 插件 E2E(需临时接入 OpenClaw 配置)
|
||||||
|
|
||||||
目标:验证插件真实挂载后的完整链路。
|
目标:验证插件真实挂载后的完整链路。
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ make check check-rules test-api
|
|||||||
|
|
||||||
### 2) 回归测试
|
### 2) 回归测试
|
||||||
|
|
||||||
- discord-control-api 引入后,不影响 WhisperGate 原有流程
|
- discord-control-api 引入后,不影响 Dirigent 原有流程
|
||||||
- 规则校验脚本在最新代码继续稳定通过
|
- 规则校验脚本在最新代码继续稳定通过
|
||||||
|
|
||||||
### 3) 运行与安全校验
|
### 3) 运行与安全校验
|
||||||
@@ -127,4 +127,4 @@ make check check-rules test-api
|
|||||||
## 六、当前结论
|
## 六、当前结论
|
||||||
|
|
||||||
- 新增 Discord 管控能力(私密频道 + 成员列表)已完成核心测试,可用。
|
- 新增 Discord 管控能力(私密频道 + 成员列表)已完成核心测试,可用。
|
||||||
- 项目剩余主要测试工作集中在 WhisperGate 插件与 OpenClaw 的真实 E2E 联调。
|
- 项目剩余主要测试工作集中在 Dirigent 插件与 OpenClaw 的真实 E2E 联调。
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
WhisperGate implements turn-based speaking for Discord group channels where multiple AI agents coexist. Only one agent (the "current speaker") is allowed to respond at a time. Others are silenced via a no-reply model override.
|
Dirigent implements turn-based speaking for Discord group channels where multiple AI agents coexist. Only one agent (the "current speaker") is allowed to respond at a time. Others are silenced via a no-reply model override.
|
||||||
|
|
||||||
## The Problem
|
## The Problem
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ When the current speaker responds with **NO_REPLY** (decides the message is not
|
|||||||
|
|
||||||
1. A message arrives in the Discord channel
|
1. A message arrives in the Discord channel
|
||||||
2. OpenClaw routes it to **all** agent sessions in that channel simultaneously
|
2. OpenClaw routes it to **all** agent sessions in that channel simultaneously
|
||||||
3. The WhisperGate plugin intercepts at `before_model_resolve`:
|
3. The Dirigent plugin intercepts at `before_model_resolve`:
|
||||||
- Current speaker → allowed to process
|
- Current speaker → allowed to process
|
||||||
- Everyone else → forced to no-reply model (message is "consumed" silently)
|
- Everyone else → forced to no-reply model (message is "consumed" silently)
|
||||||
4. Current speaker processes the message and returns NO_REPLY
|
4. Current speaker processes the message and returns NO_REPLY
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# WhisperGate Quick Verification
|
# Dirigent Quick Verification
|
||||||
|
|
||||||
## 1) Start no-reply API
|
## 1) Start no-reply API
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ npm start
|
|||||||
curl -sS http://127.0.0.1:8787/health
|
curl -sS http://127.0.0.1:8787/health
|
||||||
curl -sS -X POST http://127.0.0.1:8787/v1/chat/completions \
|
curl -sS -X POST http://127.0.0.1:8787/v1/chat/completions \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
-d '{"model":"whispergate-no-reply-v1","messages":[{"role":"user","content":"hi"}]}'
|
-d '{"model":"dirigent-no-reply-v1","messages":[{"role":"user","content":"hi"}]}'
|
||||||
```
|
```
|
||||||
|
|
||||||
Or run bundled smoke check:
|
Or run bundled smoke check:
|
||||||
|
|||||||
4
no-reply-api/package-lock.json
generated
4
no-reply-api/package-lock.json
generated
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "whispergate-no-reply-api",
|
"name": "dirigent-no-reply-api",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "whispergate-no-reply-api",
|
"name": "dirigent-no-reply-api",
|
||||||
"version": "0.1.0"
|
"version": "0.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "whispergate-no-reply-api",
|
"name": "dirigent-no-reply-api",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import http from "node:http";
|
import http from "node:http";
|
||||||
|
|
||||||
const port = Number(process.env.PORT || 8787);
|
const port = Number(process.env.PORT || 8787);
|
||||||
const modelName = process.env.NO_REPLY_MODEL || "whispergate-no-reply-v1";
|
const modelName = process.env.NO_REPLY_MODEL || "dirigent-no-reply-v1";
|
||||||
const authToken = process.env.AUTH_TOKEN || "";
|
const authToken = process.env.AUTH_TOKEN || "";
|
||||||
|
|
||||||
function sendJson(res, status, payload) {
|
function sendJson(res, status, payload) {
|
||||||
@@ -17,7 +17,7 @@ function isAuthorized(req) {
|
|||||||
|
|
||||||
function noReplyChatCompletion(reqBody) {
|
function noReplyChatCompletion(reqBody) {
|
||||||
return {
|
return {
|
||||||
id: `chatcmpl_whispergate_${Date.now()}`,
|
id: `chatcmpl_dirigent_${Date.now()}`,
|
||||||
object: "chat.completion",
|
object: "chat.completion",
|
||||||
created: Math.floor(Date.now() / 1000),
|
created: Math.floor(Date.now() / 1000),
|
||||||
model: reqBody?.model || modelName,
|
model: reqBody?.model || modelName,
|
||||||
@@ -34,7 +34,7 @@ function noReplyChatCompletion(reqBody) {
|
|||||||
|
|
||||||
function noReplyResponses(reqBody) {
|
function noReplyResponses(reqBody) {
|
||||||
return {
|
return {
|
||||||
id: `resp_whispergate_${Date.now()}`,
|
id: `resp_dirigent_${Date.now()}`,
|
||||||
object: "response",
|
object: "response",
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
model: reqBody?.model || modelName,
|
model: reqBody?.model || modelName,
|
||||||
@@ -57,7 +57,7 @@ function listModels() {
|
|||||||
id: modelName,
|
id: modelName,
|
||||||
object: "model",
|
object: "model",
|
||||||
created: Math.floor(Date.now() / 1000),
|
created: Math.floor(Date.now() / 1000),
|
||||||
owned_by: "whispergate"
|
owned_by: "dirigent"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
@@ -65,7 +65,7 @@ function listModels() {
|
|||||||
|
|
||||||
const server = http.createServer((req, res) => {
|
const server = http.createServer((req, res) => {
|
||||||
if (req.method === "GET" && req.url === "/health") {
|
if (req.method === "GET" && req.url === "/health") {
|
||||||
return sendJson(res, 200, { ok: true, service: "whispergate-no-reply-api", model: modelName });
|
return sendJson(res, 200, { ok: true, service: "dirigent-no-reply-api", model: modelName });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.method === "GET" && req.url === "/v1/models") {
|
if (req.method === "GET" && req.url === "/v1/models") {
|
||||||
@@ -108,5 +108,5 @@ const server = http.createServer((req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
server.listen(port, () => {
|
server.listen(port, () => {
|
||||||
console.log(`[whispergate-no-reply-api] listening on :${port}`);
|
console.log(`[dirigent-no-reply-api] listening on :${port}`);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# WhisperGate Plugin
|
# Dirigent Plugin
|
||||||
|
|
||||||
## Hook strategy
|
## Hook strategy
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ Optional:
|
|||||||
- `enableWhispergatePolicyTool` (default true)
|
- `enableWhispergatePolicyTool` (default true)
|
||||||
|
|
||||||
Unified optional tool:
|
Unified optional tool:
|
||||||
- `whispergateway_tools`
|
- `dirigent_tools`
|
||||||
- Discord actions: `channel-private-create`, `channel-private-update`, `member-list`
|
- Discord actions: `channel-private-create`, `channel-private-update`, `member-list`
|
||||||
- Policy actions: `policy-get`, `policy-set-channel`, `policy-delete-channel`
|
- Policy actions: `policy-get`, `policy-set-channel`, `policy-delete-channel`
|
||||||
- `bypassUserIds` (deprecated alias of `humanList`)
|
- `bypassUserIds` (deprecated alias of `humanList`)
|
||||||
@@ -51,14 +51,14 @@ Policy file behavior:
|
|||||||
- loaded once on startup into memory
|
- loaded once on startup into memory
|
||||||
- runtime decisions read memory state only
|
- runtime decisions read memory state only
|
||||||
- direct file edits do NOT affect memory state
|
- direct file edits do NOT affect memory state
|
||||||
- `whispergateway_tools` policy actions update memory first, then persist to file (atomic write)
|
- `dirigent_tools` policy actions update memory first, then persist to file (atomic write)
|
||||||
|
|
||||||
## Optional tool: `whispergateway_tools`
|
## Optional tool: `dirigent_tools`
|
||||||
|
|
||||||
This plugin registers one unified optional tool: `whispergateway_tools`.
|
This plugin registers one unified optional tool: `dirigent_tools`.
|
||||||
To use it, add tool allowlist entry for either:
|
To use it, add tool allowlist entry for either:
|
||||||
- tool name: `whispergateway_tools`
|
- tool name: `dirigent_tools`
|
||||||
- plugin id: `whispergate`
|
- plugin id: `dirigent`
|
||||||
|
|
||||||
Supported actions:
|
Supported actions:
|
||||||
- Discord: `channel-private-create`, `channel-private-update`, `member-list`
|
- Discord: `channel-private-create`, `channel-private-update`, `member-list`
|
||||||
|
|||||||
247
plugin/index.ts
247
plugin/index.ts
@@ -1,7 +1,7 @@
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||||
import { evaluateDecision, resolvePolicy, type ChannelPolicy, type Decision, type WhisperGateConfig } from "./rules.js";
|
import { evaluateDecision, resolvePolicy, type ChannelPolicy, type Decision, type DirigentConfig } from "./rules.js";
|
||||||
import { checkTurn, advanceTurn, resetTurn, onNewMessage, onSpeakerDone, initTurnOrder, getTurnDebugInfo } from "./turn-manager.js";
|
import { checkTurn, advanceTurn, resetTurn, onNewMessage, onSpeakerDone, initTurnOrder, getTurnDebugInfo } from "./turn-manager.js";
|
||||||
import { startModeratorPresence, stopModeratorPresence } from "./moderator-presence.js";
|
import { startModeratorPresence, stopModeratorPresence } from "./moderator-presence.js";
|
||||||
|
|
||||||
@@ -31,15 +31,20 @@ const sessionAccountId = new Map<string, string>(); // Track sessionKey -> accou
|
|||||||
const sessionTurnHandled = new Set<string>(); // Track sessions where turn was already advanced in before_message_write
|
const sessionTurnHandled = new Set<string>(); // Track sessions where turn was already advanced in before_message_write
|
||||||
const MAX_SESSION_DECISIONS = 2000;
|
const MAX_SESSION_DECISIONS = 2000;
|
||||||
const DECISION_TTL_MS = 5 * 60 * 1000;
|
const DECISION_TTL_MS = 5 * 60 * 1000;
|
||||||
function buildEndMarkerInstruction(endSymbols: string[], isGroupChat: boolean): string {
|
|
||||||
|
function buildEndMarkerInstruction(endSymbols: string[], isGroupChat: boolean, schedulingIdentifier: string): string {
|
||||||
const symbols = endSymbols.length > 0 ? endSymbols.join("") : "🔚";
|
const symbols = endSymbols.length > 0 ? endSymbols.join("") : "🔚";
|
||||||
let instruction = `你的这次发言必须以${symbols}作为结尾。除非你的回复是 gateway 关键词(如 NO_REPLY、HEARTBEAT_OK),这些关键词不要加${symbols}。`;
|
let instruction = `Your response MUST end with ${symbols}. Exception: gateway keywords (e.g. NO_REPLY, HEARTBEAT_OK) must NOT include ${symbols}.`;
|
||||||
if (isGroupChat) {
|
if (isGroupChat) {
|
||||||
instruction += `\n\n群聊发言规则:如果这条消息与你无关、不需要你回应、或你没有有价值的补充,请主动回复 NO_REPLY。不要为了说话而说话。`;
|
instruction += `\n\nGroup chat rules: If this message is not relevant to you, does not need your response, or you have nothing valuable to add, reply with NO_REPLY. Do not speak just for the sake of speaking.`;
|
||||||
}
|
}
|
||||||
return instruction;
|
return instruction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildSchedulingIdentifierInstruction(schedulingIdentifier: string): string {
|
||||||
|
return `\n\nScheduling identifier: "${schedulingIdentifier}". This identifier itself is meaningless — it carries no semantic content. When you receive a message containing <@YOUR_USER_ID> followed by the scheduling identifier, check recent chat history and decide whether you have something to say. If not, reply NO_REPLY.`;
|
||||||
|
}
|
||||||
|
|
||||||
const policyState: PolicyState = {
|
const policyState: PolicyState = {
|
||||||
filePath: "",
|
filePath: "",
|
||||||
channelPolicies: {},
|
channelPolicies: {},
|
||||||
@@ -170,31 +175,33 @@ function pruneDecisionMap(now = Date.now()) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function getLivePluginConfig(api: OpenClawPluginApi, fallback: WhisperGateConfig): WhisperGateConfig {
|
function getLivePluginConfig(api: OpenClawPluginApi, fallback: DirigentConfig): DirigentConfig {
|
||||||
const root = (api.config as Record<string, unknown>) || {};
|
const root = (api.config as Record<string, unknown>) || {};
|
||||||
const plugins = (root.plugins as Record<string, unknown>) || {};
|
const plugins = (root.plugins as Record<string, unknown>) || {};
|
||||||
const entries = (plugins.entries as Record<string, unknown>) || {};
|
const entries = (plugins.entries as Record<string, unknown>) || {};
|
||||||
const entry = (entries.whispergate as Record<string, unknown>) || {};
|
// Support both "dirigent" and legacy "whispergate" config keys
|
||||||
|
const entry = (entries.dirigent as Record<string, unknown>) || (entries.whispergate as Record<string, unknown>) || {};
|
||||||
const cfg = (entry.config as Record<string, unknown>) || {};
|
const cfg = (entry.config as Record<string, unknown>) || {};
|
||||||
if (Object.keys(cfg).length > 0) {
|
if (Object.keys(cfg).length > 0) {
|
||||||
// Merge with defaults to ensure optional fields have values
|
// Merge with defaults to ensure optional fields have values
|
||||||
return {
|
return {
|
||||||
enableDiscordControlTool: true,
|
enableDiscordControlTool: true,
|
||||||
enableWhispergatePolicyTool: true,
|
enableDirigentPolicyTool: true,
|
||||||
discordControlApiBaseUrl: "http://127.0.0.1:8790",
|
discordControlApiBaseUrl: "http://127.0.0.1:8790",
|
||||||
enableDebugLogs: false,
|
enableDebugLogs: false,
|
||||||
debugLogChannelIds: [],
|
debugLogChannelIds: [],
|
||||||
|
schedulingIdentifier: "➡️",
|
||||||
...cfg,
|
...cfg,
|
||||||
} as WhisperGateConfig;
|
} as DirigentConfig;
|
||||||
}
|
}
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolvePoliciesPath(api: OpenClawPluginApi, config: WhisperGateConfig): string {
|
function resolvePoliciesPath(api: OpenClawPluginApi, config: DirigentConfig): string {
|
||||||
return api.resolvePath(config.channelPoliciesFile || "~/.openclaw/whispergate-channel-policies.json");
|
return api.resolvePath(config.channelPoliciesFile || "~/.openclaw/dirigent-channel-policies.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensurePolicyStateLoaded(api: OpenClawPluginApi, config: WhisperGateConfig) {
|
function ensurePolicyStateLoaded(api: OpenClawPluginApi, config: DirigentConfig) {
|
||||||
if (policyState.filePath) return;
|
if (policyState.filePath) return;
|
||||||
const filePath = resolvePoliciesPath(api, config);
|
const filePath = resolvePoliciesPath(api, config);
|
||||||
policyState.filePath = filePath;
|
policyState.filePath = filePath;
|
||||||
@@ -211,7 +218,7 @@ function ensurePolicyStateLoaded(api: OpenClawPluginApi, config: WhisperGateConf
|
|||||||
const parsed = JSON.parse(raw) as Record<string, ChannelPolicy>;
|
const parsed = JSON.parse(raw) as Record<string, ChannelPolicy>;
|
||||||
policyState.channelPolicies = parsed && typeof parsed === "object" ? parsed : {};
|
policyState.channelPolicies = parsed && typeof parsed === "object" ? parsed : {};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
api.logger.warn(`whispergate: failed init policy file ${filePath}: ${String(err)}`);
|
api.logger.warn(`dirigent: failed init policy file ${filePath}: ${String(err)}`);
|
||||||
policyState.channelPolicies = {};
|
policyState.channelPolicies = {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -294,6 +301,7 @@ function ensureTurnOrder(api: OpenClawPluginApi, channelId: string): void {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Build agent identity string for injection into group chat prompts.
|
* Build agent identity string for injection into group chat prompts.
|
||||||
|
* Includes agent name, Discord accountId, and Discord userId.
|
||||||
*/
|
*/
|
||||||
function buildAgentIdentity(api: OpenClawPluginApi, agentId: string): string | undefined {
|
function buildAgentIdentity(api: OpenClawPluginApi, agentId: string): string | undefined {
|
||||||
const root = (api.config as Record<string, unknown>) || {};
|
const root = (api.config as Record<string, unknown>) || {};
|
||||||
@@ -318,9 +326,16 @@ function buildAgentIdentity(api: OpenClawPluginApi, agentId: string): string | u
|
|||||||
const agent = agents.find((a: Record<string, unknown>) => a.id === agentId);
|
const agent = agents.find((a: Record<string, unknown>) => a.id === agentId);
|
||||||
const name = (agent?.name as string) || agentId;
|
const name = (agent?.name as string) || agentId;
|
||||||
|
|
||||||
// Find Discord bot user ID from account token (not available directly)
|
// Resolve Discord userId from bot token
|
||||||
// We'll use accountId as the identifier
|
const discordUserId = resolveDiscordUserId(api, accountId);
|
||||||
return `你是 ${name}(Discord 账号: ${accountId})。`;
|
|
||||||
|
let identity = `You are ${name} (Discord account: ${accountId}`;
|
||||||
|
if (discordUserId) {
|
||||||
|
identity += `, Discord userId: ${discordUserId}`;
|
||||||
|
}
|
||||||
|
identity += `).`;
|
||||||
|
|
||||||
|
return identity;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Moderator bot helpers ---
|
// --- Moderator bot helpers ---
|
||||||
@@ -349,7 +364,7 @@ function resolveDiscordUserId(api: OpenClawPluginApi, accountId: string): string
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Get the moderator bot's Discord user ID from its token */
|
/** Get the moderator bot's Discord user ID from its token */
|
||||||
function getModeratorUserId(config: WhisperGateConfig): string | undefined {
|
function getModeratorUserId(config: DirigentConfig): string | undefined {
|
||||||
if (!config.moderatorBotToken) return undefined;
|
if (!config.moderatorBotToken) return undefined;
|
||||||
return userIdFromToken(config.moderatorBotToken);
|
return userIdFromToken(config.moderatorBotToken);
|
||||||
}
|
}
|
||||||
@@ -367,13 +382,13 @@ async function sendModeratorMessage(token: string, channelId: string, content: s
|
|||||||
});
|
});
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
const text = await r.text();
|
const text = await r.text();
|
||||||
logger.warn(`whispergate: moderator send failed (${r.status}): ${text}`);
|
logger.warn(`dirigent: moderator send failed (${r.status}): ${text}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
logger.info(`whispergate: moderator message sent to channel=${channelId}`);
|
logger.info(`dirigent: moderator message sent to channel=${channelId}`);
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn(`whispergate: moderator send error: ${String(err)}`);
|
logger.warn(`dirigent: moderator send error: ${String(err)}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -386,7 +401,7 @@ function persistPolicies(api: OpenClawPluginApi): void {
|
|||||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||||
fs.writeFileSync(tmp, before, "utf8");
|
fs.writeFileSync(tmp, before, "utf8");
|
||||||
fs.renameSync(tmp, filePath);
|
fs.renameSync(tmp, filePath);
|
||||||
api.logger.info(`whispergate: policy file persisted: ${filePath}`);
|
api.logger.info(`dirigent: policy file persisted: ${filePath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function pickDefined(input: Record<string, unknown>) {
|
function pickDefined(input: Record<string, unknown>) {
|
||||||
@@ -401,7 +416,7 @@ function shouldDebugLog(cfg: DebugConfig, channelId?: string): boolean {
|
|||||||
if (!cfg.enableDebugLogs) return false;
|
if (!cfg.enableDebugLogs) return false;
|
||||||
const allow = Array.isArray(cfg.debugLogChannelIds) ? cfg.debugLogChannelIds : [];
|
const allow = Array.isArray(cfg.debugLogChannelIds) ? cfg.debugLogChannelIds : [];
|
||||||
if (allow.length === 0) return true;
|
if (allow.length === 0) return true;
|
||||||
if (!channelId) return true; // 允许打印,方便排查 channelId 为空的场景
|
if (!channelId) return true;
|
||||||
return allow.includes(channelId);
|
return allow.includes(channelId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,36 +446,37 @@ function debugCtxSummary(ctx: Record<string, unknown>, event: Record<string, unk
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
id: "whispergate",
|
id: "dirigent",
|
||||||
name: "WhisperGate",
|
name: "Dirigent",
|
||||||
register(api: OpenClawPluginApi) {
|
register(api: OpenClawPluginApi) {
|
||||||
// Merge pluginConfig with defaults (in case config is missing from openclaw.json)
|
// Merge pluginConfig with defaults (in case config is missing from openclaw.json)
|
||||||
const baseConfig = {
|
const baseConfig = {
|
||||||
enableDiscordControlTool: true,
|
enableDiscordControlTool: true,
|
||||||
enableWhispergatePolicyTool: true,
|
enableDirigentPolicyTool: true,
|
||||||
discordControlApiBaseUrl: "http://127.0.0.1:8790",
|
discordControlApiBaseUrl: "http://127.0.0.1:8790",
|
||||||
|
schedulingIdentifier: "➡️",
|
||||||
...(api.pluginConfig || {}),
|
...(api.pluginConfig || {}),
|
||||||
} as WhisperGateConfig & {
|
} as DirigentConfig & {
|
||||||
enableDiscordControlTool: boolean;
|
enableDiscordControlTool: boolean;
|
||||||
discordControlApiBaseUrl: string;
|
discordControlApiBaseUrl: string;
|
||||||
discordControlApiToken?: string;
|
discordControlApiToken?: string;
|
||||||
discordControlCallerId?: string;
|
discordControlCallerId?: string;
|
||||||
enableWhispergatePolicyTool: boolean;
|
enableDirigentPolicyTool: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const liveAtRegister = getLivePluginConfig(api, baseConfig as WhisperGateConfig);
|
const liveAtRegister = getLivePluginConfig(api, baseConfig as DirigentConfig);
|
||||||
ensurePolicyStateLoaded(api, liveAtRegister);
|
ensurePolicyStateLoaded(api, liveAtRegister);
|
||||||
|
|
||||||
// Start moderator bot presence (keep it "online" on Discord)
|
// Start moderator bot presence (keep it "online" on Discord)
|
||||||
if (liveAtRegister.moderatorBotToken) {
|
if (liveAtRegister.moderatorBotToken) {
|
||||||
startModeratorPresence(liveAtRegister.moderatorBotToken, api.logger);
|
startModeratorPresence(liveAtRegister.moderatorBotToken, api.logger);
|
||||||
api.logger.info("whispergate: moderator bot presence starting");
|
api.logger.info("dirigent: moderator bot presence starting");
|
||||||
}
|
}
|
||||||
|
|
||||||
api.registerTool(
|
api.registerTool(
|
||||||
{
|
{
|
||||||
name: "whispergate_tools",
|
name: "dirigent_tools",
|
||||||
description: "WhisperGate unified tool: Discord admin actions + in-memory policy management.",
|
description: "Dirigent unified tool: Discord admin actions + in-memory policy management.",
|
||||||
parameters: {
|
parameters: {
|
||||||
type: "object",
|
type: "object",
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
@@ -498,12 +514,12 @@ export default {
|
|||||||
required: ["action"],
|
required: ["action"],
|
||||||
},
|
},
|
||||||
async execute(_id: string, params: Record<string, unknown>) {
|
async execute(_id: string, params: Record<string, unknown>) {
|
||||||
const live = getLivePluginConfig(api, baseConfig as WhisperGateConfig) as WhisperGateConfig & {
|
const live = getLivePluginConfig(api, baseConfig as DirigentConfig) as DirigentConfig & {
|
||||||
discordControlApiBaseUrl?: string;
|
discordControlApiBaseUrl?: string;
|
||||||
discordControlApiToken?: string;
|
discordControlApiToken?: string;
|
||||||
discordControlCallerId?: string;
|
discordControlCallerId?: string;
|
||||||
enableDiscordControlTool?: boolean;
|
enableDiscordControlTool?: boolean;
|
||||||
enableWhispergatePolicyTool?: boolean;
|
enableDirigentPolicyTool?: boolean;
|
||||||
};
|
};
|
||||||
ensurePolicyStateLoaded(api, live);
|
ensurePolicyStateLoaded(api, live);
|
||||||
|
|
||||||
@@ -528,14 +544,14 @@ export default {
|
|||||||
const text = await r.text();
|
const text = await r.text();
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
return {
|
return {
|
||||||
content: [{ type: "text", text: `whispergate_tools discord failed (${r.status}): ${text}` }],
|
content: [{ type: "text", text: `dirigent_tools discord failed (${r.status}): ${text}` }],
|
||||||
isError: true,
|
isError: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return { content: [{ type: "text", text }] };
|
return { content: [{ type: "text", text }] };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (live.enableWhispergatePolicyTool === false) {
|
if (live.enableDirigentPolicyTool === false) {
|
||||||
return { content: [{ type: "text", text: "policy actions disabled by config" }], isError: true };
|
return { content: [{ type: "text", text: "policy actions disabled by config" }], isError: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -613,9 +629,9 @@ export default {
|
|||||||
// ctx.channelId is the platform name (e.g. "discord"), NOT the Discord channel snowflake.
|
// ctx.channelId is the platform name (e.g. "discord"), NOT the Discord channel snowflake.
|
||||||
// Extract the real Discord channel ID from conversationId or event.to.
|
// Extract the real Discord channel ID from conversationId or event.to.
|
||||||
const preChannelId = extractDiscordChannelId(c, e);
|
const preChannelId = extractDiscordChannelId(c, e);
|
||||||
const livePre = getLivePluginConfig(api, baseConfig as WhisperGateConfig) as WhisperGateConfig & DebugConfig;
|
const livePre = getLivePluginConfig(api, baseConfig as DirigentConfig) as DirigentConfig & DebugConfig;
|
||||||
if (shouldDebugLog(livePre, preChannelId)) {
|
if (shouldDebugLog(livePre, preChannelId)) {
|
||||||
api.logger.info(`whispergate: debug message_received preflight ctx=${JSON.stringify(debugCtxSummary(c, e))}`);
|
api.logger.info(`dirigent: debug message_received preflight ctx=${JSON.stringify(debugCtxSummary(c, e))}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Turn management on message received
|
// Turn management on message received
|
||||||
@@ -631,7 +647,7 @@ export default {
|
|||||||
const moderatorUserId = getModeratorUserId(livePre);
|
const moderatorUserId = getModeratorUserId(livePre);
|
||||||
if (moderatorUserId && from === moderatorUserId) {
|
if (moderatorUserId && from === moderatorUserId) {
|
||||||
if (shouldDebugLog(livePre, preChannelId)) {
|
if (shouldDebugLog(livePre, preChannelId)) {
|
||||||
api.logger.info(`whispergate: ignoring moderator message in channel=${preChannelId}`);
|
api.logger.info(`dirigent: ignoring moderator message in channel=${preChannelId}`);
|
||||||
}
|
}
|
||||||
// Don't call onNewMessage — moderator messages are transparent to turn logic
|
// Don't call onNewMessage — moderator messages are transparent to turn logic
|
||||||
} else {
|
} else {
|
||||||
@@ -645,18 +661,18 @@ export default {
|
|||||||
if (isNew) {
|
if (isNew) {
|
||||||
// Re-initialize turn order with updated channel membership
|
// Re-initialize turn order with updated channel membership
|
||||||
ensureTurnOrder(api, preChannelId);
|
ensureTurnOrder(api, preChannelId);
|
||||||
api.logger.info(`whispergate: new account ${senderAccountId} seen in channel=${preChannelId}, turn order updated`);
|
api.logger.info(`dirigent: new account ${senderAccountId} seen in channel=${preChannelId}, turn order updated`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onNewMessage(preChannelId, senderAccountId, isHuman);
|
onNewMessage(preChannelId, senderAccountId, isHuman);
|
||||||
if (shouldDebugLog(livePre, preChannelId)) {
|
if (shouldDebugLog(livePre, preChannelId)) {
|
||||||
api.logger.info(`whispergate: turn onNewMessage channel=${preChannelId} from=${from} isHuman=${isHuman} accountId=${senderAccountId ?? "unknown"}`);
|
api.logger.info(`dirigent: turn onNewMessage channel=${preChannelId} from=${from} isHuman=${isHuman} accountId=${senderAccountId ?? "unknown"}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
api.logger.warn(`whispergate: message hook failed: ${String(err)}`);
|
api.logger.warn(`dirigent: message hook failed: ${String(err)}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -664,14 +680,14 @@ export default {
|
|||||||
const key = ctx.sessionKey;
|
const key = ctx.sessionKey;
|
||||||
if (!key) return;
|
if (!key) return;
|
||||||
|
|
||||||
const live = getLivePluginConfig(api, baseConfig as WhisperGateConfig) as WhisperGateConfig & DebugConfig;
|
const live = getLivePluginConfig(api, baseConfig as DirigentConfig) as DirigentConfig & DebugConfig;
|
||||||
ensurePolicyStateLoaded(api, live);
|
ensurePolicyStateLoaded(api, live);
|
||||||
|
|
||||||
const prompt = ((event as Record<string, unknown>).prompt as string) || "";
|
const prompt = ((event as Record<string, unknown>).prompt as string) || "";
|
||||||
|
|
||||||
if (live.enableDebugLogs) {
|
if (live.enableDebugLogs) {
|
||||||
api.logger.info(
|
api.logger.info(
|
||||||
`whispergate: DEBUG_BEFORE_MODEL_RESOLVE ctx=${JSON.stringify({ sessionKey: ctx.sessionKey, messageProvider: ctx.messageProvider, agentId: ctx.agentId })} ` +
|
`dirigent: DEBUG_BEFORE_MODEL_RESOLVE ctx=${JSON.stringify({ sessionKey: ctx.sessionKey, messageProvider: ctx.messageProvider, agentId: ctx.agentId })} ` +
|
||||||
`promptPreview=${prompt.slice(0, 300)}`,
|
`promptPreview=${prompt.slice(0, 300)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -711,7 +727,7 @@ export default {
|
|||||||
pruneDecisionMap();
|
pruneDecisionMap();
|
||||||
if (shouldDebugLog(live, derived.channelId)) {
|
if (shouldDebugLog(live, derived.channelId)) {
|
||||||
api.logger.info(
|
api.logger.info(
|
||||||
`whispergate: debug before_model_resolve recompute session=${key} ` +
|
`dirigent: debug before_model_resolve recompute session=${key} ` +
|
||||||
`channel=${derived.channel} channelId=${derived.channelId ?? ""} senderId=${derived.senderId ?? ""} ` +
|
`channel=${derived.channel} channelId=${derived.channelId ?? ""} senderId=${derived.senderId ?? ""} ` +
|
||||||
`convSenderId=${String((derived.conv as Record<string, unknown>).sender_id ?? "")} ` +
|
`convSenderId=${String((derived.conv as Record<string, unknown>).sender_id ?? "")} ` +
|
||||||
`convSender=${String((derived.conv as Record<string, unknown>).sender ?? "")} ` +
|
`convSender=${String((derived.conv as Record<string, unknown>).sender ?? "")} ` +
|
||||||
@@ -732,7 +748,7 @@ export default {
|
|||||||
// Forced no-reply - record this session as not allowed to speak
|
// Forced no-reply - record this session as not allowed to speak
|
||||||
sessionAllowed.set(key, false);
|
sessionAllowed.set(key, false);
|
||||||
api.logger.info(
|
api.logger.info(
|
||||||
`whispergate: turn gate blocked session=${key} accountId=${accountId} currentSpeaker=${turnCheck.currentSpeaker} reason=${turnCheck.reason}`,
|
`dirigent: turn gate blocked session=${key} accountId=${accountId} currentSpeaker=${turnCheck.currentSpeaker} reason=${turnCheck.reason}`,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
providerOverride: live.noReplyProvider,
|
providerOverride: live.noReplyProvider,
|
||||||
@@ -745,7 +761,6 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!rec.decision.shouldUseNoReply) {
|
if (!rec.decision.shouldUseNoReply) {
|
||||||
// 如果之前有 no-reply 执行过,现在不需要了,清除 override 恢复原模型
|
|
||||||
if (rec.needsRestore) {
|
if (rec.needsRestore) {
|
||||||
sessionDecision.delete(key);
|
sessionDecision.delete(key);
|
||||||
return {
|
return {
|
||||||
@@ -756,16 +771,14 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 标记这次执行了 no-reply,下次需要恢复模型
|
|
||||||
rec.needsRestore = true;
|
rec.needsRestore = true;
|
||||||
sessionDecision.set(key, rec);
|
sessionDecision.set(key, rec);
|
||||||
|
|
||||||
// 无论是否有缓存,只要 debug flag 开启就打印决策详情
|
|
||||||
if (live.enableDebugLogs) {
|
if (live.enableDebugLogs) {
|
||||||
const prompt = ((event as Record<string, unknown>).prompt as string) || "";
|
const prompt = ((event as Record<string, unknown>).prompt as string) || "";
|
||||||
const hasConvMarker = prompt.includes("Conversation info (untrusted metadata):");
|
const hasConvMarker = prompt.includes("Conversation info (untrusted metadata):");
|
||||||
api.logger.info(
|
api.logger.info(
|
||||||
`whispergate: DEBUG_NO_REPLY_TRIGGER session=${key} ` +
|
`dirigent: DEBUG_NO_REPLY_TRIGGER session=${key} ` +
|
||||||
`channel=${derived.channel} channelId=${derived.channelId ?? ""} senderId=${derived.senderId ?? ""} ` +
|
`channel=${derived.channel} channelId=${derived.channelId ?? ""} senderId=${derived.senderId ?? ""} ` +
|
||||||
`convSenderId=${String((derived.conv as Record<string, unknown>).sender_id ?? "")} ` +
|
`convSenderId=${String((derived.conv as Record<string, unknown>).sender_id ?? "")} ` +
|
||||||
`convSender=${String((derived.conv as Record<string, unknown>).sender ?? "")} ` +
|
`convSender=${String((derived.conv as Record<string, unknown>).sender ?? "")} ` +
|
||||||
@@ -776,7 +789,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
api.logger.info(
|
api.logger.info(
|
||||||
`whispergate: override model for session=${key}, provider=${live.noReplyProvider}, model=${live.noReplyModel}, reason=${rec.decision.reason}`,
|
`dirigent: override model for session=${key}, provider=${live.noReplyProvider}, model=${live.noReplyModel}, reason=${rec.decision.reason}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -789,7 +802,7 @@ export default {
|
|||||||
const key = ctx.sessionKey;
|
const key = ctx.sessionKey;
|
||||||
if (!key) return;
|
if (!key) return;
|
||||||
|
|
||||||
const live = getLivePluginConfig(api, baseConfig as WhisperGateConfig) as WhisperGateConfig & DebugConfig;
|
const live = getLivePluginConfig(api, baseConfig as DirigentConfig) as DirigentConfig & DebugConfig;
|
||||||
ensurePolicyStateLoaded(api, live);
|
ensurePolicyStateLoaded(api, live);
|
||||||
|
|
||||||
let rec = sessionDecision.get(key);
|
let rec = sessionDecision.get(key);
|
||||||
@@ -810,7 +823,7 @@ export default {
|
|||||||
rec = { decision, createdAt: Date.now() };
|
rec = { decision, createdAt: Date.now() };
|
||||||
if (shouldDebugLog(live, derived.channelId)) {
|
if (shouldDebugLog(live, derived.channelId)) {
|
||||||
api.logger.info(
|
api.logger.info(
|
||||||
`whispergate: debug before_prompt_build recompute session=${key} ` +
|
`dirigent: debug before_prompt_build recompute session=${key} ` +
|
||||||
`channel=${derived.channel} channelId=${derived.channelId ?? ""} senderId=${derived.senderId ?? ""} ` +
|
`channel=${derived.channel} channelId=${derived.channelId ?? ""} senderId=${derived.senderId ?? ""} ` +
|
||||||
`convSenderId=${String((derived.conv as Record<string, unknown>).sender_id ?? "")} ` +
|
`convSenderId=${String((derived.conv as Record<string, unknown>).sender_id ?? "")} ` +
|
||||||
`convSender=${String((derived.conv as Record<string, unknown>).sender ?? "")} ` +
|
`convSender=${String((derived.conv as Record<string, unknown>).sender ?? "")} ` +
|
||||||
@@ -826,7 +839,7 @@ export default {
|
|||||||
if (sessionInjected.has(key)) {
|
if (sessionInjected.has(key)) {
|
||||||
if (shouldDebugLog(live, undefined)) {
|
if (shouldDebugLog(live, undefined)) {
|
||||||
api.logger.info(
|
api.logger.info(
|
||||||
`whispergate: debug before_prompt_build session=${key} inject skipped (already injected)`,
|
`dirigent: debug before_prompt_build session=${key} inject skipped (already injected)`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -835,7 +848,7 @@ export default {
|
|||||||
if (!rec.decision.shouldInjectEndMarkerPrompt) {
|
if (!rec.decision.shouldInjectEndMarkerPrompt) {
|
||||||
if (shouldDebugLog(live, undefined)) {
|
if (shouldDebugLog(live, undefined)) {
|
||||||
api.logger.info(
|
api.logger.info(
|
||||||
`whispergate: debug before_prompt_build session=${key} inject=false reason=${rec.decision.reason}`,
|
`dirigent: debug before_prompt_build session=${key} inject=false reason=${rec.decision.reason}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -846,26 +859,33 @@ export default {
|
|||||||
const derived = deriveDecisionInputFromPrompt(prompt, ctx.messageProvider, ctx.channelId);
|
const derived = deriveDecisionInputFromPrompt(prompt, ctx.messageProvider, ctx.channelId);
|
||||||
const policy = resolvePolicy(live, derived.channelId, policyState.channelPolicies);
|
const policy = resolvePolicy(live, derived.channelId, policyState.channelPolicies);
|
||||||
const isGroupChat = derived.conv.is_group_chat === true || derived.conv.is_group_chat === "true";
|
const isGroupChat = derived.conv.is_group_chat === true || derived.conv.is_group_chat === "true";
|
||||||
const instruction = buildEndMarkerInstruction(policy.endSymbols, isGroupChat);
|
const schedulingId = live.schedulingIdentifier || "➡️";
|
||||||
|
const instruction = buildEndMarkerInstruction(policy.endSymbols, isGroupChat, schedulingId);
|
||||||
|
|
||||||
// Inject agent identity for group chats
|
// Inject agent identity for group chats (includes userId now)
|
||||||
let identity = "";
|
let identity = "";
|
||||||
if (isGroupChat && ctx.agentId) {
|
if (isGroupChat && ctx.agentId) {
|
||||||
const idStr = buildAgentIdentity(api, ctx.agentId);
|
const idStr = buildAgentIdentity(api, ctx.agentId);
|
||||||
if (idStr) identity = idStr + "\n\n";
|
if (idStr) identity = idStr + "\n\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add scheduling identifier instruction for group chats
|
||||||
|
let schedulingInstruction = "";
|
||||||
|
if (isGroupChat) {
|
||||||
|
schedulingInstruction = buildSchedulingIdentifierInstruction(schedulingId);
|
||||||
|
}
|
||||||
|
|
||||||
// Mark session as injected (one-time injection)
|
// Mark session as injected (one-time injection)
|
||||||
sessionInjected.add(key);
|
sessionInjected.add(key);
|
||||||
|
|
||||||
api.logger.info(`whispergate: prepend end marker instruction for session=${key}, reason=${rec.decision.reason} isGroupChat=${isGroupChat}`);
|
api.logger.info(`dirigent: prepend end marker instruction for session=${key}, reason=${rec.decision.reason} isGroupChat=${isGroupChat}`);
|
||||||
return { prependContext: identity + instruction };
|
return { prependContext: identity + instruction + schedulingInstruction };
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register slash commands for Discord
|
// Register slash commands for Discord
|
||||||
api.registerCommand({
|
api.registerCommand({
|
||||||
name: "whispergate",
|
name: "dirigent",
|
||||||
description: "WhisperGate 频道策略管理",
|
description: "Dirigent channel policy management",
|
||||||
acceptsArgs: true,
|
acceptsArgs: true,
|
||||||
handler: async (cmdCtx) => {
|
handler: async (cmdCtx) => {
|
||||||
const args = cmdCtx.args || "";
|
const args = cmdCtx.args || "";
|
||||||
@@ -873,11 +893,11 @@ export default {
|
|||||||
const subCmd = parts[0] || "help";
|
const subCmd = parts[0] || "help";
|
||||||
|
|
||||||
if (subCmd === "help") {
|
if (subCmd === "help") {
|
||||||
return { text: `WhisperGate 命令:\n` +
|
return { text: `Dirigent commands:\n` +
|
||||||
`/whispergate status - 显示当前频道状态\n` +
|
`/dirigent status - Show current channel status\n` +
|
||||||
`/whispergate turn-status - 显示轮流发言状态\n` +
|
`/dirigent turn-status - Show turn-based speaking status\n` +
|
||||||
`/whispergate turn-advance - 手动推进轮流\n` +
|
`/dirigent turn-advance - Manually advance turn\n` +
|
||||||
`/whispergate turn-reset - 重置轮流顺序` };
|
`/dirigent turn-reset - Reset turn order` };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subCmd === "status") {
|
if (subCmd === "status") {
|
||||||
@@ -886,65 +906,52 @@ export default {
|
|||||||
|
|
||||||
if (subCmd === "turn-status") {
|
if (subCmd === "turn-status") {
|
||||||
const channelId = cmdCtx.channelId;
|
const channelId = cmdCtx.channelId;
|
||||||
if (!channelId) return { text: "无法获取频道ID", isError: true };
|
if (!channelId) return { text: "Cannot get channel ID", isError: true };
|
||||||
return { text: JSON.stringify(getTurnDebugInfo(channelId), null, 2) };
|
return { text: JSON.stringify(getTurnDebugInfo(channelId), null, 2) };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subCmd === "turn-advance") {
|
if (subCmd === "turn-advance") {
|
||||||
const channelId = cmdCtx.channelId;
|
const channelId = cmdCtx.channelId;
|
||||||
if (!channelId) return { text: "无法获取频道ID", isError: true };
|
if (!channelId) return { text: "Cannot get channel ID", isError: true };
|
||||||
const next = advanceTurn(channelId);
|
const next = advanceTurn(channelId);
|
||||||
return { text: JSON.stringify({ ok: true, nextSpeaker: next }) };
|
return { text: JSON.stringify({ ok: true, nextSpeaker: next }) };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subCmd === "turn-reset") {
|
if (subCmd === "turn-reset") {
|
||||||
const channelId = cmdCtx.channelId;
|
const channelId = cmdCtx.channelId;
|
||||||
if (!channelId) return { text: "无法获取频道ID", isError: true };
|
if (!channelId) return { text: "Cannot get channel ID", isError: true };
|
||||||
resetTurn(channelId);
|
resetTurn(channelId);
|
||||||
return { text: JSON.stringify({ ok: true }) };
|
return { text: JSON.stringify({ ok: true }) };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { text: `未知子命令: ${subCmd}`, isError: true };
|
return { text: `Unknown subcommand: ${subCmd}`, isError: true };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle NO_REPLY detection before message write
|
// Handle NO_REPLY detection before message write
|
||||||
// This is where we detect if agent output is NO_REPLY and handle turn advancement
|
|
||||||
// NOTE: This hook is synchronous, do not use async/await
|
|
||||||
api.on("before_message_write", (event, ctx) => {
|
api.on("before_message_write", (event, ctx) => {
|
||||||
try {
|
try {
|
||||||
// Debug: print all available keys in event and ctx
|
|
||||||
api.logger.info(
|
api.logger.info(
|
||||||
`whispergate: DEBUG before_message_write eventKeys=${JSON.stringify(Object.keys(event ?? {}))} ctxKeys=${JSON.stringify(Object.keys(ctx ?? {}))}`,
|
`dirigent: DEBUG before_message_write eventKeys=${JSON.stringify(Object.keys(event ?? {}))} ctxKeys=${JSON.stringify(Object.keys(ctx ?? {}))}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// before_message_write ctx only has { agentId, sessionKey }.
|
|
||||||
// Use session mappings populated during before_model_resolve for channelId/accountId.
|
|
||||||
// Content comes from event.message (AgentMessage).
|
|
||||||
let key = ctx.sessionKey;
|
let key = ctx.sessionKey;
|
||||||
let channelId: string | undefined;
|
let channelId: string | undefined;
|
||||||
let accountId: string | undefined;
|
let accountId: string | undefined;
|
||||||
|
|
||||||
// Get from session mapping (set in before_model_resolve)
|
|
||||||
if (key) {
|
if (key) {
|
||||||
channelId = sessionChannelId.get(key);
|
channelId = sessionChannelId.get(key);
|
||||||
accountId = sessionAccountId.get(key);
|
accountId = sessionAccountId.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract content from event.message (AgentMessage)
|
|
||||||
// Only process assistant messages — before_message_write fires for both
|
|
||||||
// user (incoming) and assistant (outgoing) messages. Incoming messages may
|
|
||||||
// contain end symbols from OTHER agents, which would incorrectly advance the turn.
|
|
||||||
let content = "";
|
let content = "";
|
||||||
const msg = (event as Record<string, unknown>).message as Record<string, unknown> | undefined;
|
const msg = (event as Record<string, unknown>).message as Record<string, unknown> | undefined;
|
||||||
if (msg) {
|
if (msg) {
|
||||||
const role = msg.role as string | undefined;
|
const role = msg.role as string | undefined;
|
||||||
if (role && role !== "assistant") return;
|
if (role && role !== "assistant") return;
|
||||||
// AgentMessage may have content as string or nested
|
|
||||||
if (typeof msg.content === "string") {
|
if (typeof msg.content === "string") {
|
||||||
content = msg.content;
|
content = msg.content;
|
||||||
} else if (Array.isArray(msg.content)) {
|
} else if (Array.isArray(msg.content)) {
|
||||||
// content might be an array of parts (Anthropic format)
|
|
||||||
for (const part of msg.content) {
|
for (const part of msg.content) {
|
||||||
if (typeof part === "string") content += part;
|
if (typeof part === "string") content += part;
|
||||||
else if (part && typeof part === "object" && typeof (part as Record<string, unknown>).text === "string") {
|
else if (part && typeof part === "object" && typeof (part as Record<string, unknown>).text === "string") {
|
||||||
@@ -953,30 +960,25 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Fallback to event.content
|
|
||||||
if (!content) {
|
if (!content) {
|
||||||
content = ((event as Record<string, unknown>).content as string) || "";
|
content = ((event as Record<string, unknown>).content as string) || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always log for debugging - show all available info
|
|
||||||
api.logger.info(
|
api.logger.info(
|
||||||
`whispergate: DEBUG before_message_write session=${key ?? "undefined"} channel=${channelId ?? "undefined"} accountId=${accountId ?? "undefined"} contentType=${typeof content} content=${String(content).slice(0, 200)}`,
|
`dirigent: DEBUG before_message_write session=${key ?? "undefined"} channel=${channelId ?? "undefined"} accountId=${accountId ?? "undefined"} contentType=${typeof content} content=${String(content).slice(0, 200)}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!key || !channelId || !accountId) return;
|
if (!key || !channelId || !accountId) return;
|
||||||
|
|
||||||
// Only the current speaker should advance the turn.
|
|
||||||
// Other agents also trigger before_message_write (for incoming messages or forced no-reply),
|
|
||||||
// but they must not affect turn state.
|
|
||||||
const currentTurn = getTurnDebugInfo(channelId);
|
const currentTurn = getTurnDebugInfo(channelId);
|
||||||
if (currentTurn.currentSpeaker !== accountId) {
|
if (currentTurn.currentSpeaker !== accountId) {
|
||||||
api.logger.info(
|
api.logger.info(
|
||||||
`whispergate: before_message_write skipping non-current-speaker session=${key} accountId=${accountId} currentSpeaker=${currentTurn.currentSpeaker}`,
|
`dirigent: before_message_write skipping non-current-speaker session=${key} accountId=${accountId} currentSpeaker=${currentTurn.currentSpeaker}`,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const live = getLivePluginConfig(api, baseConfig as WhisperGateConfig) as WhisperGateConfig & DebugConfig;
|
const live = getLivePluginConfig(api, baseConfig as DirigentConfig) as DirigentConfig & DebugConfig;
|
||||||
ensurePolicyStateLoaded(api, live);
|
ensurePolicyStateLoaded(api, live);
|
||||||
const policy = resolvePolicy(live, channelId, policyState.channelPolicies);
|
const policy = resolvePolicy(live, channelId, policyState.channelPolicies);
|
||||||
|
|
||||||
@@ -987,83 +989,76 @@ export default {
|
|||||||
const hasEndSymbol = !!lastChar && policy.endSymbols.includes(lastChar);
|
const hasEndSymbol = !!lastChar && policy.endSymbols.includes(lastChar);
|
||||||
const wasNoReply = isEmpty || isNoReply;
|
const wasNoReply = isEmpty || isNoReply;
|
||||||
|
|
||||||
// Log turn state for debugging
|
|
||||||
const turnDebug = getTurnDebugInfo(channelId);
|
const turnDebug = getTurnDebugInfo(channelId);
|
||||||
api.logger.info(
|
api.logger.info(
|
||||||
`whispergate: DEBUG turn state channel=${channelId} turnOrder=${JSON.stringify(turnDebug.turnOrder)} currentSpeaker=${turnDebug.currentSpeaker} noRepliedThisCycle=${JSON.stringify([...turnDebug.noRepliedThisCycle])}`,
|
`dirigent: DEBUG turn state channel=${channelId} turnOrder=${JSON.stringify(turnDebug.turnOrder)} currentSpeaker=${turnDebug.currentSpeaker} noRepliedThisCycle=${JSON.stringify([...turnDebug.noRepliedThisCycle])}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if this session was forced no-reply or allowed to speak
|
|
||||||
const wasAllowed = sessionAllowed.get(key);
|
const wasAllowed = sessionAllowed.get(key);
|
||||||
|
|
||||||
if (wasNoReply) {
|
if (wasNoReply) {
|
||||||
api.logger.info(
|
api.logger.info(
|
||||||
`whispergate: DEBUG NO_REPLY detected session=${key} wasAllowed=${wasAllowed}`,
|
`dirigent: DEBUG NO_REPLY detected session=${key} wasAllowed=${wasAllowed}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (wasAllowed === undefined) return; // No record, skip
|
if (wasAllowed === undefined) return;
|
||||||
|
|
||||||
if (wasAllowed === false) {
|
if (wasAllowed === false) {
|
||||||
// Forced no-reply - do not advance turn
|
|
||||||
sessionAllowed.delete(key);
|
sessionAllowed.delete(key);
|
||||||
api.logger.info(
|
api.logger.info(
|
||||||
`whispergate: before_message_write forced no-reply session=${key} channel=${channelId} - not advancing turn`,
|
`dirigent: before_message_write forced no-reply session=${key} channel=${channelId} - not advancing turn`,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allowed to speak (current speaker) but chose NO_REPLY - advance turn
|
ensureTurnOrder(api, channelId);
|
||||||
ensureTurnOrder(api, channelId, live);
|
|
||||||
const nextSpeaker = onSpeakerDone(channelId, accountId, true);
|
const nextSpeaker = onSpeakerDone(channelId, accountId, true);
|
||||||
sessionAllowed.delete(key);
|
sessionAllowed.delete(key);
|
||||||
sessionTurnHandled.add(key);
|
sessionTurnHandled.add(key);
|
||||||
|
|
||||||
api.logger.info(
|
api.logger.info(
|
||||||
`whispergate: before_message_write real no-reply session=${key} channel=${channelId} nextSpeaker=${nextSpeaker ?? "dormant"}`,
|
`dirigent: before_message_write real no-reply session=${key} channel=${channelId} nextSpeaker=${nextSpeaker ?? "dormant"}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// If all agents NO_REPLY'd (dormant), don't trigger handoff
|
|
||||||
if (!nextSpeaker) {
|
if (!nextSpeaker) {
|
||||||
if (shouldDebugLog(live, channelId)) {
|
if (shouldDebugLog(live, channelId)) {
|
||||||
api.logger.info(
|
api.logger.info(
|
||||||
`whispergate: before_message_write all agents no-reply, going dormant - no handoff`,
|
`dirigent: before_message_write all agents no-reply, going dormant - no handoff`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger moderator handoff message (fire-and-forget, don't await)
|
// Trigger moderator handoff message using scheduling identifier format
|
||||||
if (live.moderatorBotToken) {
|
if (live.moderatorBotToken) {
|
||||||
const nextUserId = resolveDiscordUserId(api, nextSpeaker);
|
const nextUserId = resolveDiscordUserId(api, nextSpeaker);
|
||||||
if (nextUserId) {
|
if (nextUserId) {
|
||||||
const handoffMsg = `轮到(<@${nextUserId}>)了,如果没有想说的请直接回复NO_REPLY`;
|
const schedulingId = live.schedulingIdentifier || "➡️";
|
||||||
|
const handoffMsg = `<@${nextUserId}>${schedulingId}`;
|
||||||
void sendModeratorMessage(live.moderatorBotToken, channelId, handoffMsg, api.logger).catch((err) => {
|
void sendModeratorMessage(live.moderatorBotToken, channelId, handoffMsg, api.logger).catch((err) => {
|
||||||
api.logger.warn(`whispergate: before_message_write handoff failed: ${String(err)}`);
|
api.logger.warn(`dirigent: before_message_write handoff failed: ${String(err)}`);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
api.logger.warn(`whispergate: cannot resolve Discord userId for next speaker accountId=${nextSpeaker}`);
|
api.logger.warn(`dirigent: cannot resolve Discord userId for next speaker accountId=${nextSpeaker}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (hasEndSymbol) {
|
} else if (hasEndSymbol) {
|
||||||
// End symbol detected — advance turn NOW (before message is broadcast to other agents)
|
ensureTurnOrder(api, channelId);
|
||||||
// This prevents the race condition where other agents receive the message
|
|
||||||
// before message_sent fires and advances the turn.
|
|
||||||
ensureTurnOrder(api, channelId, live);
|
|
||||||
const nextSpeaker = onSpeakerDone(channelId, accountId, false);
|
const nextSpeaker = onSpeakerDone(channelId, accountId, false);
|
||||||
sessionAllowed.delete(key);
|
sessionAllowed.delete(key);
|
||||||
sessionTurnHandled.add(key);
|
sessionTurnHandled.add(key);
|
||||||
|
|
||||||
api.logger.info(
|
api.logger.info(
|
||||||
`whispergate: before_message_write end-symbol turn advance session=${key} channel=${channelId} nextSpeaker=${nextSpeaker ?? "dormant"}`,
|
`dirigent: before_message_write end-symbol turn advance session=${key} channel=${channelId} nextSpeaker=${nextSpeaker ?? "dormant"}`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
api.logger.info(
|
api.logger.info(
|
||||||
`whispergate: before_message_write no turn action needed session=${key} channel=${channelId}`,
|
`dirigent: before_message_write no turn action needed session=${key} channel=${channelId}`,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
api.logger.warn(`whispergate: before_message_write hook failed: ${String(err)}`);
|
api.logger.warn(`dirigent: before_message_write hook failed: ${String(err)}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1074,22 +1069,17 @@ export default {
|
|||||||
const c = (ctx || {}) as Record<string, unknown>;
|
const c = (ctx || {}) as Record<string, unknown>;
|
||||||
const e = (event || {}) as Record<string, unknown>;
|
const e = (event || {}) as Record<string, unknown>;
|
||||||
|
|
||||||
// Always log raw context first for debugging
|
|
||||||
api.logger.info(
|
api.logger.info(
|
||||||
`whispergate: DEBUG message_sent RAW ctxKeys=${JSON.stringify(Object.keys(c))} eventKeys=${JSON.stringify(Object.keys(e))} ` +
|
`dirigent: DEBUG message_sent RAW ctxKeys=${JSON.stringify(Object.keys(c))} eventKeys=${JSON.stringify(Object.keys(e))} ` +
|
||||||
`ctx.channelId=${String(c.channelId ?? "undefined")} ctx.conversationId=${String(c.conversationId ?? "undefined")} ` +
|
`ctx.channelId=${String(c.channelId ?? "undefined")} ctx.conversationId=${String(c.conversationId ?? "undefined")} ` +
|
||||||
`ctx.accountId=${String(c.accountId ?? "undefined")} event.to=${String(e.to ?? "undefined")} ` +
|
`ctx.accountId=${String(c.accountId ?? "undefined")} event.to=${String(e.to ?? "undefined")} ` +
|
||||||
`session=${key ?? "undefined"}`,
|
`session=${key ?? "undefined"}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// ctx.channelId is the platform name (e.g. "discord"), NOT the Discord channel snowflake.
|
|
||||||
// Extract real Discord channel ID from conversationId or event.to.
|
|
||||||
let channelId = extractDiscordChannelId(c, e);
|
let channelId = extractDiscordChannelId(c, e);
|
||||||
// Fallback: sessionKey mapping
|
|
||||||
if (!channelId && key) {
|
if (!channelId && key) {
|
||||||
channelId = sessionChannelId.get(key);
|
channelId = sessionChannelId.get(key);
|
||||||
}
|
}
|
||||||
// Fallback: parse from sessionKey
|
|
||||||
if (!channelId && key) {
|
if (!channelId && key) {
|
||||||
const skMatch = key.match(/:channel:(\d+)$/);
|
const skMatch = key.match(/:channel:(\d+)$/);
|
||||||
if (skMatch) channelId = skMatch[1];
|
if (skMatch) channelId = skMatch[1];
|
||||||
@@ -1097,14 +1087,13 @@ export default {
|
|||||||
const accountId = (ctx.accountId as string | undefined) || (key ? sessionAccountId.get(key) : undefined);
|
const accountId = (ctx.accountId as string | undefined) || (key ? sessionAccountId.get(key) : undefined);
|
||||||
const content = (event.content as string) || "";
|
const content = (event.content as string) || "";
|
||||||
|
|
||||||
// Debug log
|
|
||||||
api.logger.info(
|
api.logger.info(
|
||||||
`whispergate: DEBUG message_sent RESOLVED session=${key ?? "undefined"} channelId=${channelId ?? "undefined"} accountId=${accountId ?? "undefined"} content=${content.slice(0, 100)}`,
|
`dirigent: DEBUG message_sent RESOLVED session=${key ?? "undefined"} channelId=${channelId ?? "undefined"} accountId=${accountId ?? "undefined"} content=${content.slice(0, 100)}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!channelId || !accountId) return;
|
if (!channelId || !accountId) return;
|
||||||
|
|
||||||
const live = getLivePluginConfig(api, baseConfig as WhisperGateConfig) as WhisperGateConfig & DebugConfig;
|
const live = getLivePluginConfig(api, baseConfig as DirigentConfig) as DirigentConfig & DebugConfig;
|
||||||
ensurePolicyStateLoaded(api, live);
|
ensurePolicyStateLoaded(api, live);
|
||||||
const policy = resolvePolicy(live, channelId, policyState.channelPolicies);
|
const policy = resolvePolicy(live, channelId, policyState.channelPolicies);
|
||||||
|
|
||||||
@@ -1119,7 +1108,7 @@ export default {
|
|||||||
if (key && sessionTurnHandled.has(key)) {
|
if (key && sessionTurnHandled.has(key)) {
|
||||||
sessionTurnHandled.delete(key);
|
sessionTurnHandled.delete(key);
|
||||||
api.logger.info(
|
api.logger.info(
|
||||||
`whispergate: message_sent skipping turn advance (already handled in before_message_write) session=${key} channel=${channelId}`,
|
`dirigent: message_sent skipping turn advance (already handled in before_message_write) session=${key} channel=${channelId}`,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1128,22 +1117,22 @@ export default {
|
|||||||
const nextSpeaker = onSpeakerDone(channelId, accountId, wasNoReply);
|
const nextSpeaker = onSpeakerDone(channelId, accountId, wasNoReply);
|
||||||
const trigger = wasNoReply ? (isEmpty ? "empty" : "no_reply_keyword") : "end_symbol";
|
const trigger = wasNoReply ? (isEmpty ? "empty" : "no_reply_keyword") : "end_symbol";
|
||||||
api.logger.info(
|
api.logger.info(
|
||||||
`whispergate: turn onSpeakerDone channel=${channelId} from=${accountId} next=${nextSpeaker ?? "dormant"} trigger=${trigger}`,
|
`dirigent: turn onSpeakerDone channel=${channelId} from=${accountId} next=${nextSpeaker ?? "dormant"} trigger=${trigger}`,
|
||||||
);
|
);
|
||||||
// Moderator handoff: when current speaker NO_REPLY'd and there's a next speaker,
|
// Moderator handoff using scheduling identifier format
|
||||||
// send a handoff message via the moderator bot to trigger the next agent
|
|
||||||
if (wasNoReply && nextSpeaker && live.moderatorBotToken) {
|
if (wasNoReply && nextSpeaker && live.moderatorBotToken) {
|
||||||
const nextUserId = resolveDiscordUserId(api, nextSpeaker);
|
const nextUserId = resolveDiscordUserId(api, nextSpeaker);
|
||||||
if (nextUserId) {
|
if (nextUserId) {
|
||||||
const handoffMsg = `轮到(<@${nextUserId}>)了,如果没有想说的请直接回复NO_REPLY`;
|
const schedulingId = live.schedulingIdentifier || "➡️";
|
||||||
|
const handoffMsg = `<@${nextUserId}>${schedulingId}`;
|
||||||
sendModeratorMessage(live.moderatorBotToken, channelId, handoffMsg, api.logger);
|
sendModeratorMessage(live.moderatorBotToken, channelId, handoffMsg, api.logger);
|
||||||
} else {
|
} else {
|
||||||
api.logger.warn(`whispergate: cannot resolve Discord userId for next speaker accountId=${nextSpeaker}`);
|
api.logger.warn(`dirigent: cannot resolve Discord userId for next speaker accountId=${nextSpeaker}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
api.logger.warn(`whispergate: message_sent hook failed: ${String(err)}`);
|
api.logger.warn(`dirigent: message_sent hook failed: ${String(err)}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ function connect(token: string, logger: Logger, isResume = false) {
|
|||||||
try {
|
try {
|
||||||
ws = new WebSocket(url);
|
ws = new WebSocket(url);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn(`whispergate: moderator ws constructor failed: ${String(err)}`);
|
logger.warn(`dirigent: moderator ws constructor failed: ${String(err)}`);
|
||||||
scheduleReconnect(token, logger, false);
|
scheduleReconnect(token, logger, false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -119,8 +119,8 @@ function connect(token: string, logger: Logger, isResume = false) {
|
|||||||
intents: 0,
|
intents: 0,
|
||||||
properties: {
|
properties: {
|
||||||
os: "linux",
|
os: "linux",
|
||||||
browser: "whispergate",
|
browser: "dirigent",
|
||||||
device: "whispergate",
|
device: "dirigent",
|
||||||
},
|
},
|
||||||
presence: {
|
presence: {
|
||||||
status: "online",
|
status: "online",
|
||||||
@@ -154,19 +154,19 @@ function connect(token: string, logger: Logger, isResume = false) {
|
|||||||
if (t === "READY") {
|
if (t === "READY") {
|
||||||
sessionId = d.session_id;
|
sessionId = d.session_id;
|
||||||
resumeUrl = d.resume_gateway_url;
|
resumeUrl = d.resume_gateway_url;
|
||||||
logger.info("whispergate: moderator bot connected and online");
|
logger.info("dirigent: moderator bot connected and online");
|
||||||
}
|
}
|
||||||
if (t === "RESUMED") {
|
if (t === "RESUMED") {
|
||||||
logger.info("whispergate: moderator bot resumed");
|
logger.info("dirigent: moderator bot resumed");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 7: // Reconnect request
|
case 7: // Reconnect request
|
||||||
logger.info("whispergate: moderator bot reconnect requested by Discord");
|
logger.info("dirigent: moderator bot reconnect requested by Discord");
|
||||||
cleanup();
|
cleanup();
|
||||||
scheduleReconnect(token, logger, true);
|
scheduleReconnect(token, logger, true);
|
||||||
break;
|
break;
|
||||||
case 9: // Invalid Session
|
case 9: // Invalid Session
|
||||||
logger.warn(`whispergate: moderator bot invalid session, resumable=${d}`);
|
logger.warn(`dirigent: moderator bot invalid session, resumable=${d}`);
|
||||||
cleanup();
|
cleanup();
|
||||||
sessionId = d ? sessionId : null;
|
sessionId = d ? sessionId : null;
|
||||||
// Wait longer before re-identifying
|
// Wait longer before re-identifying
|
||||||
@@ -189,18 +189,18 @@ function connect(token: string, logger: Logger, isResume = false) {
|
|||||||
|
|
||||||
// Non-recoverable codes — stop reconnecting
|
// Non-recoverable codes — stop reconnecting
|
||||||
if (code === 4004) {
|
if (code === 4004) {
|
||||||
logger.warn("whispergate: moderator bot token invalid (4004), stopping");
|
logger.warn("dirigent: moderator bot token invalid (4004), stopping");
|
||||||
started = false;
|
started = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (code === 4010 || code === 4011 || code === 4013 || code === 4014) {
|
if (code === 4010 || code === 4011 || code === 4013 || code === 4014) {
|
||||||
logger.warn(`whispergate: moderator bot fatal close (${code}), re-identifying`);
|
logger.warn(`dirigent: moderator bot fatal close (${code}), re-identifying`);
|
||||||
sessionId = null;
|
sessionId = null;
|
||||||
scheduleReconnect(token, logger, false);
|
scheduleReconnect(token, logger, false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`whispergate: moderator bot disconnected (code=${code}), will reconnect`);
|
logger.info(`dirigent: moderator bot disconnected (code=${code}), will reconnect`);
|
||||||
const canResume = !!sessionId && code !== 4012;
|
const canResume = !!sessionId && code !== 4012;
|
||||||
scheduleReconnect(token, logger, canResume);
|
scheduleReconnect(token, logger, canResume);
|
||||||
};
|
};
|
||||||
@@ -220,7 +220,7 @@ function scheduleReconnect(token: string, logger: Logger, resume: boolean) {
|
|||||||
const jitter = Math.random() * 1000;
|
const jitter = Math.random() * 1000;
|
||||||
const delay = baseDelay + jitter;
|
const delay = baseDelay + jitter;
|
||||||
|
|
||||||
logger.info(`whispergate: moderator reconnect in ${Math.round(delay)}ms (attempt ${reconnectAttempts})`);
|
logger.info(`dirigent: moderator reconnect in ${Math.round(delay)}ms (attempt ${reconnectAttempts})`);
|
||||||
|
|
||||||
reconnectTimer = setTimeout(() => {
|
reconnectTimer = setTimeout(() => {
|
||||||
reconnectTimer = null;
|
reconnectTimer = null;
|
||||||
@@ -234,7 +234,7 @@ function scheduleReconnect(token: string, logger: Logger, resume: boolean) {
|
|||||||
*/
|
*/
|
||||||
export function startModeratorPresence(token: string, logger: Logger): void {
|
export function startModeratorPresence(token: string, logger: Logger): void {
|
||||||
if (started) {
|
if (started) {
|
||||||
logger.info("whispergate: moderator presence already started, skipping");
|
logger.info("dirigent: moderator presence already started, skipping");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
started = true;
|
started = true;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"id": "whispergate",
|
"id": "dirigent",
|
||||||
"name": "WhisperGate",
|
"name": "Dirigent",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"description": "Rule-based no-reply gate with provider/model override",
|
"description": "Rule-based no-reply gate with provider/model override and turn management",
|
||||||
"entry": "./index.ts",
|
"entry": "./index.ts",
|
||||||
"configSchema": {
|
"configSchema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -13,9 +13,10 @@
|
|||||||
"listMode": { "type": "string", "enum": ["human-list", "agent-list"], "default": "human-list" },
|
"listMode": { "type": "string", "enum": ["human-list", "agent-list"], "default": "human-list" },
|
||||||
"humanList": { "type": "array", "items": { "type": "string" }, "default": [] },
|
"humanList": { "type": "array", "items": { "type": "string" }, "default": [] },
|
||||||
"agentList": { "type": "array", "items": { "type": "string" }, "default": [] },
|
"agentList": { "type": "array", "items": { "type": "string" }, "default": [] },
|
||||||
"channelPoliciesFile": { "type": "string", "default": "~/.openclaw/whispergate-channel-policies.json" },
|
"channelPoliciesFile": { "type": "string", "default": "~/.openclaw/dirigent-channel-policies.json" },
|
||||||
"bypassUserIds": { "type": "array", "items": { "type": "string" }, "default": [] },
|
"bypassUserIds": { "type": "array", "items": { "type": "string" }, "default": [] },
|
||||||
"endSymbols": { "type": "array", "items": { "type": "string" }, "default": ["🔚"] },
|
"endSymbols": { "type": "array", "items": { "type": "string" }, "default": ["🔚"] },
|
||||||
|
"schedulingIdentifier": { "type": "string", "default": "➡️" },
|
||||||
"noReplyProvider": { "type": "string" },
|
"noReplyProvider": { "type": "string" },
|
||||||
"noReplyModel": { "type": "string" },
|
"noReplyModel": { "type": "string" },
|
||||||
"enableDiscordControlTool": { "type": "boolean", "default": true },
|
"enableDiscordControlTool": { "type": "boolean", "default": true },
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "whispergate-plugin",
|
"name": "dirigent-plugin",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "WhisperGate OpenClaw plugin",
|
"description": "Dirigent OpenClaw plugin",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"check": "node ../scripts/check-plugin-files.mjs",
|
"check": "node ../scripts/check-plugin-files.mjs",
|
||||||
"check:rules": "node ../scripts/validate-rules.mjs"
|
"check:rules": "node ../scripts/validate-rules.mjs"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export type WhisperGateConfig = {
|
export type DirigentConfig = {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
discordOnly?: boolean;
|
discordOnly?: boolean;
|
||||||
listMode?: "human-list" | "agent-list";
|
listMode?: "human-list" | "agent-list";
|
||||||
@@ -8,6 +8,8 @@ export type WhisperGateConfig = {
|
|||||||
// backward compatibility
|
// backward compatibility
|
||||||
bypassUserIds?: string[];
|
bypassUserIds?: string[];
|
||||||
endSymbols?: string[];
|
endSymbols?: string[];
|
||||||
|
/** Scheduling identifier sent by moderator to activate agents (default: ➡️) */
|
||||||
|
schedulingIdentifier?: string;
|
||||||
noReplyProvider: string;
|
noReplyProvider: string;
|
||||||
noReplyModel: string;
|
noReplyModel: string;
|
||||||
/** Discord bot token for the moderator bot (used for turn handoff messages) */
|
/** Discord bot token for the moderator bot (used for turn handoff messages) */
|
||||||
@@ -51,7 +53,7 @@ function getLastChar(input: string): string {
|
|||||||
return chars[chars.length - 1] || "";
|
return chars[chars.length - 1] || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolvePolicy(config: WhisperGateConfig, channelId?: string, channelPolicies?: Record<string, ChannelPolicy>) {
|
export function resolvePolicy(config: DirigentConfig, channelId?: string, channelPolicies?: Record<string, ChannelPolicy>) {
|
||||||
const globalMode = config.listMode || "human-list";
|
const globalMode = config.listMode || "human-list";
|
||||||
const globalHuman = config.humanList || config.bypassUserIds || [];
|
const globalHuman = config.humanList || config.bypassUserIds || [];
|
||||||
const globalAgent = config.agentList || [];
|
const globalAgent = config.agentList || [];
|
||||||
@@ -76,7 +78,7 @@ export function resolvePolicy(config: WhisperGateConfig, channelId?: string, cha
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function evaluateDecision(params: {
|
export function evaluateDecision(params: {
|
||||||
config: WhisperGateConfig;
|
config: DirigentConfig;
|
||||||
channel?: string;
|
channel?: string;
|
||||||
channelId?: string;
|
channelId?: string;
|
||||||
channelPolicies?: Record<string, ChannelPolicy>;
|
channelPolicies?: Record<string, ChannelPolicy>;
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ set -euo pipefail
|
|||||||
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
cd "$ROOT_DIR"
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
echo "[whispergate] building/starting no-reply API container"
|
echo "[dirigent] building/starting no-reply API container"
|
||||||
docker compose up -d --build whispergate-no-reply-api
|
docker compose up -d --build dirigent-no-reply-api
|
||||||
|
|
||||||
echo "[whispergate] health check"
|
echo "[dirigent] health check"
|
||||||
curl -sS http://127.0.0.1:8787/health
|
curl -sS http://127.0.0.1:8787/health
|
||||||
|
|
||||||
echo "[whispergate] done"
|
echo "[dirigent] done"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { execFileSync } from "node:child_process";
|
|||||||
|
|
||||||
const modeArg = process.argv[2];
|
const modeArg = process.argv[2];
|
||||||
if (modeArg !== "--install" && modeArg !== "--uninstall") {
|
if (modeArg !== "--install" && modeArg !== "--uninstall") {
|
||||||
console.error("Usage: install-whispergate-openclaw.mjs --install | --uninstall");
|
console.error("Usage: install-dirigent-openclaw.mjs --install | --uninstall");
|
||||||
process.exit(2);
|
process.exit(2);
|
||||||
}
|
}
|
||||||
const mode = modeArg === "--install" ? "install" : "uninstall";
|
const mode = modeArg === "--install" ? "install" : "uninstall";
|
||||||
@@ -14,7 +14,7 @@ const mode = modeArg === "--install" ? "install" : "uninstall";
|
|||||||
const env = process.env;
|
const env = process.env;
|
||||||
const OPENCLAW_CONFIG_PATH = env.OPENCLAW_CONFIG_PATH || path.join(os.homedir(), ".openclaw", "openclaw.json");
|
const OPENCLAW_CONFIG_PATH = env.OPENCLAW_CONFIG_PATH || path.join(os.homedir(), ".openclaw", "openclaw.json");
|
||||||
const __dirname = path.dirname(new URL(import.meta.url).pathname);
|
const __dirname = path.dirname(new URL(import.meta.url).pathname);
|
||||||
const PLUGIN_PATH = env.PLUGIN_PATH || path.resolve(__dirname, "..", "dist", "whispergate");
|
const PLUGIN_PATH = env.PLUGIN_PATH || path.resolve(__dirname, "..", "dist", "dirigent");
|
||||||
const NO_REPLY_PROVIDER_ID = env.NO_REPLY_PROVIDER_ID || "whisper-gateway";
|
const NO_REPLY_PROVIDER_ID = env.NO_REPLY_PROVIDER_ID || "whisper-gateway";
|
||||||
const NO_REPLY_MODEL_ID = env.NO_REPLY_MODEL_ID || "no-reply";
|
const NO_REPLY_MODEL_ID = env.NO_REPLY_MODEL_ID || "no-reply";
|
||||||
const NO_REPLY_BASE_URL = env.NO_REPLY_BASE_URL || "http://127.0.0.1:8787/v1";
|
const NO_REPLY_BASE_URL = env.NO_REPLY_BASE_URL || "http://127.0.0.1:8787/v1";
|
||||||
@@ -22,19 +22,19 @@ const NO_REPLY_API_KEY = env.NO_REPLY_API_KEY || "wg-local-test-token";
|
|||||||
const LIST_MODE = env.LIST_MODE || "human-list";
|
const LIST_MODE = env.LIST_MODE || "human-list";
|
||||||
const HUMAN_LIST_JSON = env.HUMAN_LIST_JSON || '["561921120408698910","1474088632750047324"]';
|
const HUMAN_LIST_JSON = env.HUMAN_LIST_JSON || '["561921120408698910","1474088632750047324"]';
|
||||||
const AGENT_LIST_JSON = env.AGENT_LIST_JSON || "[]";
|
const AGENT_LIST_JSON = env.AGENT_LIST_JSON || "[]";
|
||||||
const CHANNEL_POLICIES_FILE = (env.CHANNEL_POLICIES_FILE || "~/.openclaw/whispergate-channel-policies.json").replace(/^~(?=$|\/)/, os.homedir());
|
const CHANNEL_POLICIES_FILE = (env.CHANNEL_POLICIES_FILE || "~/.openclaw/dirigent-channel-policies.json").replace(/^~(?=$|\/)/, os.homedir());
|
||||||
const CHANNEL_POLICIES_JSON = env.CHANNEL_POLICIES_JSON || "{}";
|
const CHANNEL_POLICIES_JSON = env.CHANNEL_POLICIES_JSON || "{}";
|
||||||
const END_SYMBOLS_JSON = env.END_SYMBOLS_JSON || '["🔚"]';
|
const END_SYMBOLS_JSON = env.END_SYMBOLS_JSON || '["🔚"]';
|
||||||
|
|
||||||
const STATE_DIR = (env.STATE_DIR || "~/.openclaw/whispergate-install-records").replace(/^~(?=$|\/)/, os.homedir());
|
const STATE_DIR = (env.STATE_DIR || "~/.openclaw/dirigent-install-records").replace(/^~(?=$|\/)/, os.homedir());
|
||||||
const LATEST_RECORD_LINK = (env.LATEST_RECORD_LINK || "~/.openclaw/whispergate-install-record-latest.json").replace(/^~(?=$|\/)/, os.homedir());
|
const LATEST_RECORD_LINK = (env.LATEST_RECORD_LINK || "~/.openclaw/dirigent-install-record-latest.json").replace(/^~(?=$|\/)/, os.homedir());
|
||||||
|
|
||||||
const ts = new Date().toISOString().replace(/[-:TZ.]/g, "").slice(0, 14);
|
const ts = new Date().toISOString().replace(/[-:TZ.]/g, "").slice(0, 14);
|
||||||
const BACKUP_PATH = `${OPENCLAW_CONFIG_PATH}.bak-whispergate-${mode}-${ts}`;
|
const BACKUP_PATH = `${OPENCLAW_CONFIG_PATH}.bak-dirigent-${mode}-${ts}`;
|
||||||
const RECORD_PATH = path.join(STATE_DIR, `whispergate-${ts}.json`);
|
const RECORD_PATH = path.join(STATE_DIR, `dirigent-${ts}.json`);
|
||||||
|
|
||||||
const PATH_PLUGINS_LOAD = "plugins.load.paths";
|
const PATH_PLUGINS_LOAD = "plugins.load.paths";
|
||||||
const PATH_PLUGIN_ENTRY = "plugins.entries.whispergate";
|
const PATH_PLUGIN_ENTRY = "plugins.entries.dirigent";
|
||||||
const PATH_PROVIDERS = "models.providers";
|
const PATH_PROVIDERS = "models.providers";
|
||||||
|
|
||||||
function runOpenclaw(args, { allowFail = false } = {}) {
|
function runOpenclaw(args, { allowFail = false } = {}) {
|
||||||
@@ -83,7 +83,7 @@ function findLatestInstallRecord() {
|
|||||||
if (!fs.existsSync(STATE_DIR)) return "";
|
if (!fs.existsSync(STATE_DIR)) return "";
|
||||||
const files = fs
|
const files = fs
|
||||||
.readdirSync(STATE_DIR)
|
.readdirSync(STATE_DIR)
|
||||||
.filter((f) => /^whispergate-\d+\.json$/.test(f))
|
.filter((f) => /^dirigent-\d+\.json$/.test(f))
|
||||||
.sort()
|
.sort()
|
||||||
.reverse();
|
.reverse();
|
||||||
for (const f of files) {
|
for (const f of files) {
|
||||||
@@ -99,18 +99,18 @@ function findLatestInstallRecord() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!fs.existsSync(OPENCLAW_CONFIG_PATH)) {
|
if (!fs.existsSync(OPENCLAW_CONFIG_PATH)) {
|
||||||
console.error(`[whispergate] config not found: ${OPENCLAW_CONFIG_PATH}`);
|
console.error(`[dirigent] config not found: ${OPENCLAW_CONFIG_PATH}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode === "install") {
|
if (mode === "install") {
|
||||||
fs.copyFileSync(OPENCLAW_CONFIG_PATH, BACKUP_PATH);
|
fs.copyFileSync(OPENCLAW_CONFIG_PATH, BACKUP_PATH);
|
||||||
console.log(`[whispergate] backup: ${BACKUP_PATH}`);
|
console.log(`[dirigent] backup: ${BACKUP_PATH}`);
|
||||||
|
|
||||||
if (!fs.existsSync(CHANNEL_POLICIES_FILE)) {
|
if (!fs.existsSync(CHANNEL_POLICIES_FILE)) {
|
||||||
fs.mkdirSync(path.dirname(CHANNEL_POLICIES_FILE), { recursive: true });
|
fs.mkdirSync(path.dirname(CHANNEL_POLICIES_FILE), { recursive: true });
|
||||||
fs.writeFileSync(CHANNEL_POLICIES_FILE, `${CHANNEL_POLICIES_JSON}\n`);
|
fs.writeFileSync(CHANNEL_POLICIES_FILE, `${CHANNEL_POLICIES_JSON}\n`);
|
||||||
console.log(`[whispergate] initialized channel policies file: ${CHANNEL_POLICIES_FILE}`);
|
console.log(`[dirigent] initialized channel policies file: ${CHANNEL_POLICIES_FILE}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const before = {
|
const before = {
|
||||||
@@ -127,7 +127,7 @@ if (mode === "install") {
|
|||||||
if (!paths.includes(PLUGIN_PATH)) paths.push(PLUGIN_PATH);
|
if (!paths.includes(PLUGIN_PATH)) paths.push(PLUGIN_PATH);
|
||||||
plugins.load.paths = paths;
|
plugins.load.paths = paths;
|
||||||
plugins.entries = plugins.entries && typeof plugins.entries === "object" ? plugins.entries : {};
|
plugins.entries = plugins.entries && typeof plugins.entries === "object" ? plugins.entries : {};
|
||||||
plugins.entries.whispergate = {
|
plugins.entries.dirigent = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
config: {
|
config: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -169,23 +169,23 @@ if (mode === "install") {
|
|||||||
[PATH_PROVIDERS]: getJson(PATH_PROVIDERS),
|
[PATH_PROVIDERS]: getJson(PATH_PROVIDERS),
|
||||||
};
|
};
|
||||||
writeRecord("install", before, after);
|
writeRecord("install", before, after);
|
||||||
console.log("[whispergate] install ok (config written)");
|
console.log("[dirigent] install ok (config written)");
|
||||||
console.log(`[whispergate] record: ${RECORD_PATH}`);
|
console.log(`[dirigent] record: ${RECORD_PATH}`);
|
||||||
console.log("[whispergate] >>> restart gateway to apply: openclaw gateway restart");
|
console.log("[dirigent] >>> restart gateway to apply: openclaw gateway restart");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
fs.copyFileSync(BACKUP_PATH, OPENCLAW_CONFIG_PATH);
|
fs.copyFileSync(BACKUP_PATH, OPENCLAW_CONFIG_PATH);
|
||||||
console.error(`[whispergate] install failed; rollback complete: ${String(e)}`);
|
console.error(`[dirigent] install failed; rollback complete: ${String(e)}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const recFile = env.RECORD_FILE || findLatestInstallRecord();
|
const recFile = env.RECORD_FILE || findLatestInstallRecord();
|
||||||
if (!recFile || !fs.existsSync(recFile)) {
|
if (!recFile || !fs.existsSync(recFile)) {
|
||||||
console.error("[whispergate] no install record found. set RECORD_FILE=<path> to an install record.");
|
console.error("[dirigent] no install record found. set RECORD_FILE=<path> to an install record.");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.copyFileSync(OPENCLAW_CONFIG_PATH, BACKUP_PATH);
|
fs.copyFileSync(OPENCLAW_CONFIG_PATH, BACKUP_PATH);
|
||||||
console.log(`[whispergate] backup before uninstall: ${BACKUP_PATH}`);
|
console.log(`[dirigent] backup before uninstall: ${BACKUP_PATH}`);
|
||||||
|
|
||||||
const rec = readRecord(recFile);
|
const rec = readRecord(recFile);
|
||||||
const before = rec.applied || {};
|
const before = rec.applied || {};
|
||||||
@@ -200,8 +200,8 @@ if (mode === "install") {
|
|||||||
if (target[PATH_PLUGINS_LOAD]?.exists) plugins.load.paths = target[PATH_PLUGINS_LOAD].value;
|
if (target[PATH_PLUGINS_LOAD]?.exists) plugins.load.paths = target[PATH_PLUGINS_LOAD].value;
|
||||||
else delete plugins.load.paths;
|
else delete plugins.load.paths;
|
||||||
|
|
||||||
if (target[PATH_PLUGIN_ENTRY]?.exists) plugins.entries.whispergate = target[PATH_PLUGIN_ENTRY].value;
|
if (target[PATH_PLUGIN_ENTRY]?.exists) plugins.entries.dirigent = target[PATH_PLUGIN_ENTRY].value;
|
||||||
else delete plugins.entries.whispergate;
|
else delete plugins.entries.dirigent;
|
||||||
|
|
||||||
setJson("plugins", plugins);
|
setJson("plugins", plugins);
|
||||||
|
|
||||||
@@ -214,11 +214,11 @@ if (mode === "install") {
|
|||||||
[PATH_PROVIDERS]: getJson(PATH_PROVIDERS),
|
[PATH_PROVIDERS]: getJson(PATH_PROVIDERS),
|
||||||
};
|
};
|
||||||
writeRecord("uninstall", before, after);
|
writeRecord("uninstall", before, after);
|
||||||
console.log("[whispergate] uninstall ok");
|
console.log("[dirigent] uninstall ok");
|
||||||
console.log(`[whispergate] record: ${RECORD_PATH}`);
|
console.log(`[dirigent] record: ${RECORD_PATH}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
fs.copyFileSync(BACKUP_PATH, OPENCLAW_CONFIG_PATH);
|
fs.copyFileSync(BACKUP_PATH, OPENCLAW_CONFIG_PATH);
|
||||||
console.error(`[whispergate] uninstall failed; rollback complete: ${String(e)}`);
|
console.error(`[dirigent] uninstall failed; rollback complete: ${String(e)}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
exec node "$SCRIPT_DIR/install-whispergate-openclaw.mjs" "$@"
|
exec node "$SCRIPT_DIR/install-dirigent-openclaw.mjs" "$@"
|
||||||
@@ -3,7 +3,7 @@ import path from "node:path";
|
|||||||
|
|
||||||
const root = process.cwd();
|
const root = process.cwd();
|
||||||
const pluginDir = path.join(root, "plugin");
|
const pluginDir = path.join(root, "plugin");
|
||||||
const outDir = path.join(root, "dist", "whispergate");
|
const outDir = path.join(root, "dist", "dirigent");
|
||||||
|
|
||||||
fs.rmSync(outDir, { recursive: true, force: true });
|
fs.rmSync(outDir, { recursive: true, force: true });
|
||||||
fs.mkdirSync(outDir, { recursive: true });
|
fs.mkdirSync(outDir, { recursive: true });
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
const pluginPath = process.argv[2] || "/opt/WhisperGate/plugin";
|
const pluginPath = process.argv[2] || "/opt/Dirigent/plugin";
|
||||||
const provider = process.argv[3] || "openai";
|
const provider = process.argv[3] || "openai";
|
||||||
const model = process.argv[4] || "whispergate-no-reply-v1";
|
const model = process.argv[4] || "dirigent-no-reply-v1";
|
||||||
const bypass = (process.argv[5] || "").split(",").filter(Boolean);
|
const bypass = (process.argv[5] || "").split(",").filter(Boolean);
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
plugins: {
|
plugins: {
|
||||||
load: { paths: [pluginPath] },
|
load: { paths: [pluginPath] },
|
||||||
entries: {
|
entries: {
|
||||||
whispergate: {
|
dirigent: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
config: {
|
config: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|||||||
@@ -19,14 +19,14 @@ echo "[3] chat/completions"
|
|||||||
curl -sS -X POST "${BASE_URL}/v1/chat/completions" \
|
curl -sS -X POST "${BASE_URL}/v1/chat/completions" \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
"${AUTH_HEADER[@]}" \
|
"${AUTH_HEADER[@]}" \
|
||||||
-d '{"model":"whispergate-no-reply-v1","messages":[{"role":"user","content":"hello"}]}' \
|
-d '{"model":"dirigent-no-reply-v1","messages":[{"role":"user","content":"hello"}]}' \
|
||||||
| sed -n '1,20p'
|
| sed -n '1,20p'
|
||||||
|
|
||||||
echo "[4] responses"
|
echo "[4] responses"
|
||||||
curl -sS -X POST "${BASE_URL}/v1/responses" \
|
curl -sS -X POST "${BASE_URL}/v1/responses" \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
"${AUTH_HEADER[@]}" \
|
"${AUTH_HEADER[@]}" \
|
||||||
-d '{"model":"whispergate-no-reply-v1","input":"hello"}' \
|
-d '{"model":"dirigent-no-reply-v1","input":"hello"}' \
|
||||||
| sed -n '1,20p'
|
| sed -n '1,20p'
|
||||||
|
|
||||||
echo "smoke ok"
|
echo "smoke ok"
|
||||||
|
|||||||
Reference in New Issue
Block a user