Compare commits

..

3 Commits

6 changed files with 161 additions and 2 deletions

11
CHANGELOG.md Normal file
View File

@@ -0,0 +1,11 @@
# Changelog
## 0.1.0-mvp
- Added no-reply API service (`/v1/chat/completions`, `/v1/responses`, `/v1/models`)
- Added optional bearer auth (`AUTH_TOKEN`)
- Added WhisperGate plugin with deterministic rule gate
- Added discord-specific 🔚 prompt injection for bypass/end-symbol paths
- Added containerization (`Dockerfile`, `docker-compose.yml`)
- Added helper scripts for smoke/dev lifecycle and rule validation
- Added no-touch config rendering and integration docs

View File

@@ -1,4 +1,4 @@
.PHONY: check check-rules up down smoke
.PHONY: check check-rules test-api up down smoke render-config
check:
cd plugin && npm run check
@@ -6,6 +6,9 @@ check:
check-rules:
node scripts/validate-rules.mjs
test-api:
node scripts/test-no-reply-api.mjs
up:
./scripts/dev-up.sh
@@ -14,3 +17,6 @@ down:
smoke:
./scripts/smoke-no-reply-api.sh
render-config:
node scripts/render-openclaw-config.mjs

View File

@@ -21,7 +21,8 @@ The no-reply provider returns `NO_REPLY` for any input.
- `no-reply-api/` — OpenAI-compatible minimal API that always returns `NO_REPLY`
- `docs/` — rollout and configuration notes
- `scripts/` — smoke/dev/helper checks
- `Makefile` — common dev commands (`make check`, `make check-rules`, `make up`)
- `Makefile` — common dev commands (`make check`, `make check-rules`, `make test-api`, `make up`)
- `CHANGELOG.md` — milestone summary
---

34
docs/INTEGRATION.md Normal file
View File

@@ -0,0 +1,34 @@
# WhisperGate Integration (No-touch Template)
This guide **does not** change your current OpenClaw config automatically.
It only generates a JSON snippet you can review.
## Generate config snippet
```bash
node scripts/render-openclaw-config.mjs \
/absolute/path/to/WhisperGate/plugin \
openai \
whispergate-no-reply-v1 \
561921120408698910
```
Arguments:
1. plugin path
2. provider alias
3. model name
4. bypass user ids (comma-separated, optional)
## Output
The script prints JSON for:
- `plugins.load.paths`
- `plugins.entries.whispergate.config`
You can merge this snippet manually into your `openclaw.json`.
## Notes
- This repo does not run config mutation commands.
- Keep no-reply API bound to loopback/private network.
- If you use API auth, set `AUTH_TOKEN` and align provider apiKey usage.

View File

@@ -0,0 +1,25 @@
const pluginPath = process.argv[2] || "/opt/WhisperGate/plugin";
const provider = process.argv[3] || "openai";
const model = process.argv[4] || "whispergate-no-reply-v1";
const bypass = (process.argv[5] || "").split(",").filter(Boolean);
const payload = {
plugins: {
load: { paths: [pluginPath] },
entries: {
whispergate: {
enabled: true,
config: {
enabled: true,
discordOnly: true,
bypassUserIds: bypass,
endSymbols: ["。", "", "", ".", "!", "?"],
noReplyProvider: provider,
noReplyModel: model,
},
},
},
},
};
console.log(JSON.stringify(payload, null, 2));

View File

@@ -0,0 +1,82 @@
import { spawn } from "node:child_process";
const BASE = "http://127.0.0.1:18787";
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}
async function waitForHealth(retries = 30) {
for (let i = 0; i < retries; i++) {
try {
const r = await fetch(`${BASE}/health`);
if (r.ok) return true;
} catch {}
await sleep(200);
}
return false;
}
function assert(cond, msg) {
if (!cond) throw new Error(msg);
}
async function run() {
const token = "test-token";
const child = spawn("node", ["no-reply-api/server.mjs"], {
cwd: process.cwd(),
env: { ...process.env, PORT: "18787", AUTH_TOKEN: token, NO_REPLY_MODEL: "wg-test-model" },
stdio: ["ignore", "pipe", "pipe"],
});
child.stdout.on("data", () => {});
child.stderr.on("data", () => {});
try {
const ok = await waitForHealth();
assert(ok, "health check failed");
const unauth = await fetch(`${BASE}/v1/models`);
assert(unauth.status === 401, `expected 401, got ${unauth.status}`);
const models = await fetch(`${BASE}/v1/models`, {
headers: { Authorization: `Bearer ${token}` },
});
assert(models.ok, "authorized /v1/models failed");
const modelsJson = await models.json();
assert(modelsJson?.data?.[0]?.id === "wg-test-model", "model id mismatch");
const cc = await fetch(`${BASE}/v1/chat/completions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ model: "wg-test-model", messages: [{ role: "user", content: "hi" }] }),
});
assert(cc.ok, "chat completions failed");
const ccJson = await cc.json();
assert(ccJson?.choices?.[0]?.message?.content === "NO_REPLY", "chat completion not NO_REPLY");
const rsp = await fetch(`${BASE}/v1/responses`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ model: "wg-test-model", input: "hi" }),
});
assert(rsp.ok, "responses failed");
const rspJson = await rsp.json();
assert(rspJson?.output?.[0]?.content?.[0]?.text === "NO_REPLY", "responses not NO_REPLY");
console.log("test-no-reply-api: ok");
} finally {
child.kill("SIGTERM");
}
}
run().catch((err) => {
console.error(`test-no-reply-api: fail: ${err.message}`);
process.exit(1);
});