Compare commits
3 Commits
16fdb6600a
...
0a224983fd
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a224983fd | |||
| 1acaebf73f | |||
| b99ec7c281 |
11
CHANGELOG.md
Normal file
11
CHANGELOG.md
Normal 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
|
||||||
8
Makefile
8
Makefile
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: check check-rules up down smoke
|
.PHONY: check check-rules test-api up down smoke render-config
|
||||||
|
|
||||||
check:
|
check:
|
||||||
cd plugin && npm run check
|
cd plugin && npm run check
|
||||||
@@ -6,6 +6,9 @@ check:
|
|||||||
check-rules:
|
check-rules:
|
||||||
node scripts/validate-rules.mjs
|
node scripts/validate-rules.mjs
|
||||||
|
|
||||||
|
test-api:
|
||||||
|
node scripts/test-no-reply-api.mjs
|
||||||
|
|
||||||
up:
|
up:
|
||||||
./scripts/dev-up.sh
|
./scripts/dev-up.sh
|
||||||
|
|
||||||
@@ -14,3 +17,6 @@ down:
|
|||||||
|
|
||||||
smoke:
|
smoke:
|
||||||
./scripts/smoke-no-reply-api.sh
|
./scripts/smoke-no-reply-api.sh
|
||||||
|
|
||||||
|
render-config:
|
||||||
|
node scripts/render-openclaw-config.mjs
|
||||||
|
|||||||
@@ -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`
|
- `no-reply-api/` — OpenAI-compatible minimal API that always returns `NO_REPLY`
|
||||||
- `docs/` — rollout and configuration notes
|
- `docs/` — rollout and configuration notes
|
||||||
- `scripts/` — smoke/dev/helper checks
|
- `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
34
docs/INTEGRATION.md
Normal 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.
|
||||||
25
scripts/render-openclaw-config.mjs
Normal file
25
scripts/render-openclaw-config.mjs
Normal 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));
|
||||||
82
scripts/test-no-reply-api.mjs
Normal file
82
scripts/test-no-reply-api.mjs
Normal 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);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user