botschat 0.1.10 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -15
- package/migrations/0012_push_tokens.sql +11 -0
- package/package.json +20 -1
- package/packages/api/src/do/connection-do.ts +142 -24
- package/packages/api/src/env.ts +6 -0
- package/packages/api/src/index.ts +7 -0
- package/packages/api/src/routes/auth.ts +85 -9
- package/packages/api/src/routes/channels.ts +3 -2
- package/packages/api/src/routes/dev-auth.ts +45 -0
- package/packages/api/src/routes/push.ts +52 -0
- package/packages/api/src/routes/upload.ts +73 -38
- package/packages/api/src/utils/fcm.ts +167 -0
- package/packages/api/src/utils/firebase.ts +218 -0
- package/packages/plugin/dist/src/channel.d.ts +6 -0
- package/packages/plugin/dist/src/channel.d.ts.map +1 -1
- package/packages/plugin/dist/src/channel.js +71 -15
- package/packages/plugin/dist/src/channel.js.map +1 -1
- package/packages/plugin/package.json +1 -1
- package/packages/web/dist/assets/index-B9qN5gs6.js +1 -0
- package/packages/web/dist/assets/index-BQNMGVyU.js +2 -0
- package/packages/web/dist/assets/{index-Ev5M8VmV.css → index-Bd_RDcgO.css} +1 -1
- package/packages/web/dist/assets/index-Civeg2lm.js +1 -0
- package/packages/web/dist/assets/index-Dk33VSnY.js +2 -0
- package/packages/web/dist/assets/index-Kr85Nj_-.js +1516 -0
- package/packages/web/dist/assets/index-lVB82JKU.js +1 -0
- package/packages/web/dist/assets/index.esm-CtMkqqqb.js +599 -0
- package/packages/web/dist/assets/web-CUXjh_UA.js +1 -0
- package/packages/web/dist/assets/web-vKLTVUul.js +1 -0
- package/packages/web/dist/index.html +6 -4
- package/packages/web/dist/sw.js +158 -1
- package/packages/web/index.html +4 -2
- package/packages/web/package.json +4 -1
- package/packages/web/src/App.tsx +117 -1
- package/packages/web/src/api.ts +21 -1
- package/packages/web/src/components/AccountSettings.tsx +131 -0
- package/packages/web/src/components/ChatWindow.tsx +302 -70
- package/packages/web/src/components/CronSidebar.tsx +89 -24
- package/packages/web/src/components/DataConsentModal.tsx +249 -0
- package/packages/web/src/components/LoginPage.tsx +55 -7
- package/packages/web/src/components/MessageContent.tsx +71 -9
- package/packages/web/src/components/MobileLayout.tsx +28 -118
- package/packages/web/src/components/SessionTabs.tsx +41 -2
- package/packages/web/src/components/Sidebar.tsx +88 -66
- package/packages/web/src/e2e.ts +26 -5
- package/packages/web/src/firebase.ts +215 -3
- package/packages/web/src/foreground.ts +51 -0
- package/packages/web/src/index.css +10 -2
- package/packages/web/src/main.tsx +24 -2
- package/packages/web/src/push.ts +205 -0
- package/packages/web/src/ws.ts +20 -8
- package/scripts/dev.sh +158 -26
- package/scripts/mock-openclaw.mjs +382 -0
- package/scripts/test-e2e-chat.ts +2 -2
- package/scripts/test-e2e-live.ts +1 -1
- package/wrangler.toml +3 -0
- package/packages/web/dist/assets/index-DpW6VzZK.js +0 -1497
package/scripts/dev.sh
CHANGED
|
@@ -1,24 +1,44 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# BotsChat local dev startup script
|
|
3
3
|
# Usage:
|
|
4
|
-
# ./scripts/dev.sh — build
|
|
5
|
-
# ./scripts/dev.sh reset — nuke local DB, re-migrate, then start
|
|
4
|
+
# ./scripts/dev.sh — full dev env: build + migrate + server + mock AI + open browser
|
|
5
|
+
# ./scripts/dev.sh reset — nuke local DB, re-migrate, then start full dev env
|
|
6
|
+
# ./scripts/dev.sh server — only start wrangler dev server (no mock, no browser)
|
|
6
7
|
# ./scripts/dev.sh migrate — only run D1 migrations (no server)
|
|
7
8
|
# ./scripts/dev.sh build — only build web frontend (no server)
|
|
8
9
|
# ./scripts/dev.sh sync — sync plugin to mini.local + rebuild + restart gateway
|
|
9
10
|
# ./scripts/dev.sh logs — tail gateway logs on mini.local
|
|
11
|
+
# ./scripts/dev.sh mock — start mock OpenClaw standalone (foreground)
|
|
10
12
|
set -euo pipefail
|
|
11
13
|
|
|
12
14
|
cd "$(dirname "$0")/.."
|
|
13
15
|
ROOT="$(pwd)"
|
|
14
16
|
|
|
17
|
+
# ── Auto-set DEV_AUTH_SECRET ──────────────────────────────────────────
|
|
18
|
+
# For local dev, any string works. Use a fixed default so new developers
|
|
19
|
+
# can run `./scripts/dev.sh` without setting env vars first.
|
|
20
|
+
if [[ -z "${DEV_AUTH_SECRET:-}" ]]; then
|
|
21
|
+
DEV_AUTH_SECRET="botschat-local-dev-secret"
|
|
22
|
+
export DEV_AUTH_SECRET
|
|
23
|
+
fi
|
|
24
|
+
|
|
15
25
|
# ── Colours ──────────────────────────────────────────────────────────
|
|
16
|
-
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; CYAN='\033[0;36m'; NC='\033[0m'
|
|
26
|
+
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; CYAN='\033[0;36m'; DIM='\033[2m'; NC='\033[0m'
|
|
17
27
|
info() { echo -e "${CYAN}▸${NC} $*"; }
|
|
18
28
|
ok() { echo -e "${GREEN}✔${NC} $*"; }
|
|
19
29
|
warn() { echo -e "${YELLOW}▲${NC} $*"; }
|
|
20
30
|
fail() { echo -e "${RED}✖${NC} $*"; exit 1; }
|
|
21
31
|
|
|
32
|
+
# ── Process tracking ─────────────────────────────────────────────────
|
|
33
|
+
WRANGLER_PID=""
|
|
34
|
+
MOCK_PID=""
|
|
35
|
+
|
|
36
|
+
cleanup() {
|
|
37
|
+
[[ -n "$MOCK_PID" ]] && kill "$MOCK_PID" 2>/dev/null || true
|
|
38
|
+
[[ -n "$WRANGLER_PID" ]] && kill "$WRANGLER_PID" 2>/dev/null || true
|
|
39
|
+
wait 2>/dev/null || true
|
|
40
|
+
}
|
|
41
|
+
|
|
22
42
|
# ── Helpers ──────────────────────────────────────────────────────────
|
|
23
43
|
|
|
24
44
|
kill_port() {
|
|
@@ -32,6 +52,55 @@ kill_port() {
|
|
|
32
52
|
fi
|
|
33
53
|
}
|
|
34
54
|
|
|
55
|
+
wait_for_server() {
|
|
56
|
+
info "Waiting for server…"
|
|
57
|
+
local max=60 i=0
|
|
58
|
+
while ! curl -sf --max-time 1 -o /dev/null http://localhost:8787/ 2>/dev/null; do
|
|
59
|
+
sleep 1
|
|
60
|
+
i=$((i + 1))
|
|
61
|
+
if [[ $i -ge $max ]]; then
|
|
62
|
+
fail "Server didn't start within ${max}s"
|
|
63
|
+
fi
|
|
64
|
+
done
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
get_mock_token() {
|
|
68
|
+
local BASE_URL="http://localhost:8787"
|
|
69
|
+
local TOKEN_JSON AUTH_TOKEN PAT_JSON PAT
|
|
70
|
+
|
|
71
|
+
TOKEN_JSON=$(curl -sf -X POST "$BASE_URL/api/dev-auth/login" \
|
|
72
|
+
-H 'Content-Type: application/json' \
|
|
73
|
+
-d "{\"secret\":\"$DEV_AUTH_SECRET\",\"userId\":\"dev-test-user\"}" 2>&1) || {
|
|
74
|
+
fail "Cannot reach $BASE_URL — is the server running?"
|
|
75
|
+
}
|
|
76
|
+
AUTH_TOKEN=$(echo "$TOKEN_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])" 2>/dev/null) || {
|
|
77
|
+
fail "Failed to parse auth token: $TOKEN_JSON"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
PAT_JSON=$(curl -sf -X POST "$BASE_URL/api/pairing-tokens" \
|
|
81
|
+
-H "Authorization: Bearer $AUTH_TOKEN" \
|
|
82
|
+
-H 'Content-Type: application/json' \
|
|
83
|
+
-d '{"label":"mock-openclaw"}' 2>&1) || {
|
|
84
|
+
fail "Failed to create pairing token"
|
|
85
|
+
}
|
|
86
|
+
PAT=$(echo "$PAT_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])" 2>/dev/null) || {
|
|
87
|
+
fail "Failed to parse pairing token: $PAT_JSON"
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
echo "$PAT"
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
open_browser() {
|
|
94
|
+
local url="http://localhost:8787/?dev_token=${DEV_AUTH_SECRET}"
|
|
95
|
+
if command -v open &>/dev/null; then
|
|
96
|
+
open "$url"
|
|
97
|
+
elif command -v xdg-open &>/dev/null; then
|
|
98
|
+
xdg-open "$url"
|
|
99
|
+
else
|
|
100
|
+
warn "Open in browser: $url"
|
|
101
|
+
fi
|
|
102
|
+
}
|
|
103
|
+
|
|
35
104
|
do_migrate() {
|
|
36
105
|
info "Applying D1 migrations (local)…"
|
|
37
106
|
npx wrangler d1 migrations apply botschat-db --local
|
|
@@ -51,44 +120,98 @@ do_build_web() {
|
|
|
51
120
|
ok "Web build complete (packages/web/dist)"
|
|
52
121
|
}
|
|
53
122
|
|
|
123
|
+
# ── Server-only start (foreground, no mock/browser) ──────────────────
|
|
124
|
+
|
|
54
125
|
do_start() {
|
|
55
126
|
kill_port 8787
|
|
56
127
|
info "Starting wrangler dev on 0.0.0.0:8787…"
|
|
57
|
-
exec npx wrangler dev --config wrangler.toml --ip 0.0.0.0 --var ENVIRONMENT:development
|
|
128
|
+
exec npx wrangler dev --config wrangler.toml --ip 0.0.0.0 --var ENVIRONMENT:development --var DEV_AUTH_SECRET:"$DEV_AUTH_SECRET"
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# ── Full dev environment (server + mock + browser) ───────────────────
|
|
132
|
+
|
|
133
|
+
do_start_full() {
|
|
134
|
+
kill_port 8787
|
|
135
|
+
trap cleanup EXIT INT TERM
|
|
136
|
+
|
|
137
|
+
info "Starting wrangler dev on 0.0.0.0:8787…"
|
|
138
|
+
npx wrangler dev --config wrangler.toml --ip 0.0.0.0 \
|
|
139
|
+
--var ENVIRONMENT:development \
|
|
140
|
+
--var DEV_AUTH_SECRET:"$DEV_AUTH_SECRET" &
|
|
141
|
+
WRANGLER_PID=$!
|
|
142
|
+
|
|
143
|
+
wait_for_server
|
|
144
|
+
ok "Server ready on http://localhost:8787"
|
|
145
|
+
|
|
146
|
+
info "Starting Mock OpenClaw…"
|
|
147
|
+
local PAT
|
|
148
|
+
PAT=$(get_mock_token)
|
|
149
|
+
mkdir -p "$ROOT/.wrangler"
|
|
150
|
+
node "$ROOT/scripts/mock-openclaw.mjs" --token "$PAT" > "$ROOT/.wrangler/mock-openclaw.log" 2>&1 &
|
|
151
|
+
MOCK_PID=$!
|
|
152
|
+
ok "Mock OpenClaw connected (pid=$MOCK_PID)"
|
|
153
|
+
|
|
154
|
+
open_browser
|
|
155
|
+
|
|
156
|
+
echo ""
|
|
157
|
+
echo -e "${CYAN}╭──────────────────────────────────────────────────╮${NC}"
|
|
158
|
+
echo -e "${CYAN}│${NC} ${GREEN}BotsChat Dev Environment Ready${NC} ${CYAN}│${NC}"
|
|
159
|
+
echo -e "${CYAN}│${NC} ${CYAN}│${NC}"
|
|
160
|
+
echo -e "${CYAN}│${NC} Server: http://localhost:8787 ${CYAN}│${NC}"
|
|
161
|
+
echo -e "${CYAN}│${NC} Mock AI: running ${DIM}(log: .wrangler/mock-openclaw.log)${NC}"
|
|
162
|
+
echo -e "${CYAN}│${NC} Auth: auto-login enabled ${CYAN}│${NC}"
|
|
163
|
+
echo -e "${CYAN}│${NC} ${CYAN}│${NC}"
|
|
164
|
+
echo -e "${CYAN}│${NC} Press ${YELLOW}Ctrl+C${NC} to stop all services ${CYAN}│${NC}"
|
|
165
|
+
echo -e "${CYAN}╰──────────────────────────────────────────────────╯${NC}"
|
|
166
|
+
echo ""
|
|
167
|
+
|
|
168
|
+
wait $WRANGLER_PID 2>/dev/null || true
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# ── Standalone mock (foreground) ─────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
do_mock() {
|
|
174
|
+
info "Getting auth token via dev-auth…"
|
|
175
|
+
local PAT
|
|
176
|
+
PAT=$(get_mock_token)
|
|
177
|
+
ok "Pairing token: ${PAT:0:16}***"
|
|
178
|
+
info "Starting Mock OpenClaw…"
|
|
179
|
+
exec node "$ROOT/scripts/mock-openclaw.mjs" --token "$PAT" "$@"
|
|
58
180
|
}
|
|
59
181
|
|
|
182
|
+
# ── Sync plugin to mini.local ────────────────────────────────────────
|
|
183
|
+
|
|
60
184
|
do_sync_plugin() {
|
|
61
|
-
local
|
|
62
|
-
local
|
|
185
|
+
local REMOTE="mini.local"
|
|
186
|
+
local DEV_DIR="~/Projects/botschat-app/botsChat/packages/plugin"
|
|
187
|
+
local EXT_DIR="~/.openclaw/extensions/botschat"
|
|
63
188
|
|
|
64
|
-
info "Syncing plugin to mini.local…"
|
|
189
|
+
info "Syncing plugin source to mini.local dev repo…"
|
|
65
190
|
rsync -avz --exclude node_modules --exclude .git --exclude dist --exclude .wrangler \
|
|
66
|
-
packages/plugin/ "$
|
|
67
|
-
ok "Plugin
|
|
191
|
+
packages/plugin/ "$REMOTE:$DEV_DIR/"
|
|
192
|
+
ok "Plugin source synced → $DEV_DIR"
|
|
68
193
|
|
|
69
|
-
info "Building plugin, deploying to extensions, restarting gateway
|
|
70
|
-
ssh "$
|
|
71
|
-
cd
|
|
194
|
+
info "Building plugin in dev repo, deploying to extensions, restarting gateway…"
|
|
195
|
+
ssh "$REMOTE" "export PATH=\"/opt/homebrew/bin:\$PATH\"
|
|
196
|
+
cd $DEV_DIR
|
|
72
197
|
npm run build
|
|
73
|
-
EXT_DIR
|
|
74
|
-
rsync -av --delete dist/
|
|
75
|
-
rsync -av bin/
|
|
76
|
-
cp -f package.json openclaw.plugin.json
|
|
77
|
-
echo
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
echo "Gateway restarted (PID=$!)"'
|
|
82
|
-
ok "Plugin synced, deployed to extensions, gateway restarted"
|
|
198
|
+
echo '--- Deploying built artifacts to $EXT_DIR ---'
|
|
199
|
+
rsync -av --delete dist/ $EXT_DIR/dist/
|
|
200
|
+
rsync -av bin/ $EXT_DIR/bin/ 2>/dev/null || true
|
|
201
|
+
cp -f package.json openclaw.plugin.json $EXT_DIR/ 2>/dev/null || true
|
|
202
|
+
echo '--- Restarting gateway via launchctl ---'
|
|
203
|
+
launchctl kickstart -k gui/\$(id -u)/ai.openclaw.gateway
|
|
204
|
+
echo 'Gateway restarted'"
|
|
205
|
+
ok "Plugin deployed to extensions, gateway restarted"
|
|
83
206
|
|
|
84
207
|
sleep 4
|
|
85
208
|
info "Checking connection…"
|
|
86
|
-
ssh "$
|
|
209
|
+
ssh "$REMOTE" 'tail -10 ~/.openclaw/logs/gateway.log | grep -i "authenticated\|error\|Task scan\|botschat"'
|
|
87
210
|
}
|
|
88
211
|
|
|
89
212
|
do_logs() {
|
|
90
213
|
info "Tailing gateway logs on mini.local…"
|
|
91
|
-
ssh mini.local 'tail -f /
|
|
214
|
+
ssh mini.local 'tail -f ~/.openclaw/logs/gateway.log'
|
|
92
215
|
}
|
|
93
216
|
|
|
94
217
|
# ── Main ─────────────────────────────────────────────────────────────
|
|
@@ -99,6 +222,11 @@ case "$cmd" in
|
|
|
99
222
|
reset)
|
|
100
223
|
do_reset
|
|
101
224
|
do_build_web
|
|
225
|
+
do_start_full
|
|
226
|
+
;;
|
|
227
|
+
server)
|
|
228
|
+
do_build_web
|
|
229
|
+
do_migrate
|
|
102
230
|
do_start
|
|
103
231
|
;;
|
|
104
232
|
migrate)
|
|
@@ -113,10 +241,14 @@ case "$cmd" in
|
|
|
113
241
|
logs)
|
|
114
242
|
do_logs
|
|
115
243
|
;;
|
|
244
|
+
mock)
|
|
245
|
+
shift
|
|
246
|
+
do_mock "$@"
|
|
247
|
+
;;
|
|
116
248
|
*)
|
|
117
|
-
# Default: build + migrate +
|
|
249
|
+
# Default: full dev experience — build + migrate + server + mock + browser
|
|
118
250
|
do_build_web
|
|
119
251
|
do_migrate
|
|
120
|
-
|
|
252
|
+
do_start_full
|
|
121
253
|
;;
|
|
122
254
|
esac
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Mock OpenClaw — a lightweight WebSocket client that simulates an OpenClaw
|
|
4
|
+
* plugin for local BotsChat development. No OpenClaw dependency required.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* node scripts/mock-openclaw.mjs --token bc_pat_xxx
|
|
8
|
+
* node scripts/mock-openclaw.mjs --token bc_pat_xxx --delay 500 --stream
|
|
9
|
+
*
|
|
10
|
+
* Options:
|
|
11
|
+
* --token <pat> Pairing token (required)
|
|
12
|
+
* --url <url> Server URL (default: http://localhost:8787)
|
|
13
|
+
* --agents <list> Comma-separated agent IDs (default: main)
|
|
14
|
+
* --delay <ms> Reply delay in ms (default: 300)
|
|
15
|
+
* --stream Enable streaming replies (chunk by chunk)
|
|
16
|
+
* --model <name> Default model name (default: mock/echo-1.0)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { randomUUID } from "node:crypto";
|
|
20
|
+
import { parseArgs } from "node:util";
|
|
21
|
+
|
|
22
|
+
// ── CLI args ─────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
const { values: args } = parseArgs({
|
|
25
|
+
options: {
|
|
26
|
+
token: { type: "string" },
|
|
27
|
+
url: { type: "string", default: "http://localhost:8787" },
|
|
28
|
+
agents: { type: "string", default: "main" },
|
|
29
|
+
delay: { type: "string", default: "300" },
|
|
30
|
+
stream: { type: "boolean", default: false },
|
|
31
|
+
model: { type: "string", default: "mock/echo-1.0" },
|
|
32
|
+
help: { type: "boolean", short: "h", default: false },
|
|
33
|
+
},
|
|
34
|
+
strict: true,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (args.help || !args.token) {
|
|
38
|
+
console.log(`Mock OpenClaw — simulate an OpenClaw plugin for local testing
|
|
39
|
+
|
|
40
|
+
Usage:
|
|
41
|
+
node scripts/mock-openclaw.mjs --token <pairing-token> [options]
|
|
42
|
+
|
|
43
|
+
Options:
|
|
44
|
+
--token <pat> Pairing token (required)
|
|
45
|
+
--url <url> Server URL (default: http://localhost:8787)
|
|
46
|
+
--agents <list> Comma-separated agent IDs (default: main)
|
|
47
|
+
--delay <ms> Reply delay in ms (default: 300)
|
|
48
|
+
--stream Enable streaming replies
|
|
49
|
+
--model <name> Default model name (default: mock/echo-1.0)
|
|
50
|
+
-h, --help Show this help`);
|
|
51
|
+
process.exit(args.help ? 0 : 1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const TOKEN = args.token;
|
|
55
|
+
const SERVER_URL = args.url;
|
|
56
|
+
const AGENTS = args.agents.split(",").map((s) => s.trim());
|
|
57
|
+
const DELAY_MS = parseInt(args.delay, 10);
|
|
58
|
+
const STREAMING = args.stream;
|
|
59
|
+
const MODEL = args.model;
|
|
60
|
+
|
|
61
|
+
// ── Colours ──────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
const c = {
|
|
64
|
+
reset: "\x1b[0m",
|
|
65
|
+
dim: "\x1b[2m",
|
|
66
|
+
cyan: "\x1b[36m",
|
|
67
|
+
green: "\x1b[32m",
|
|
68
|
+
yellow:"\x1b[33m",
|
|
69
|
+
red: "\x1b[31m",
|
|
70
|
+
magenta:"\x1b[35m",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function log(icon, msg) {
|
|
74
|
+
const ts = new Date().toISOString().slice(11, 23);
|
|
75
|
+
console.log(`${c.dim}${ts}${c.reset} ${icon} ${msg}`);
|
|
76
|
+
}
|
|
77
|
+
const logInfo = (msg) => log(`${c.cyan}▸${c.reset}`, msg);
|
|
78
|
+
const logOk = (msg) => log(`${c.green}✔${c.reset}`, msg);
|
|
79
|
+
const logWarn = (msg) => log(`${c.yellow}▲${c.reset}`, msg);
|
|
80
|
+
const logErr = (msg) => log(`${c.red}✖${c.reset}`, msg);
|
|
81
|
+
const logRecv = (msg) => log(`${c.magenta}◂${c.reset}`, msg);
|
|
82
|
+
const logSend = (msg) => log(`${c.cyan}▸${c.reset}`, msg);
|
|
83
|
+
|
|
84
|
+
// ── Mock models ──────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
const MOCK_MODELS = [
|
|
87
|
+
{ id: "mock/echo-1.0", name: "Echo 1.0", provider: "mock" },
|
|
88
|
+
{ id: "mock/echo-streaming", name: "Echo Streaming", provider: "mock" },
|
|
89
|
+
{ id: "anthropic/claude-sonnet-4-20250514", name: "Claude Sonnet 4", provider: "anthropic" },
|
|
90
|
+
{ id: "openai/gpt-4o", name: "GPT-4o", provider: "openai" },
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
// ── WebSocket connection ─────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
const MIN_BACKOFF = 1_000;
|
|
96
|
+
const MAX_BACKOFF = 30_000;
|
|
97
|
+
let backoff = MIN_BACKOFF;
|
|
98
|
+
let ws = null;
|
|
99
|
+
let pingTimer = null;
|
|
100
|
+
let intentionalClose = false;
|
|
101
|
+
let userId = null;
|
|
102
|
+
|
|
103
|
+
function buildWsUrl() {
|
|
104
|
+
let host = SERVER_URL.replace(/^https?:\/\//, "");
|
|
105
|
+
const isPlainHttp = SERVER_URL.startsWith("http://");
|
|
106
|
+
const scheme = isPlainHttp ? "ws" : "wss";
|
|
107
|
+
return `${scheme}://${host}/api/gateway/mock?token=${encodeURIComponent(TOKEN)}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function connect() {
|
|
111
|
+
const url = buildWsUrl();
|
|
112
|
+
logInfo(`Connecting to ${url.replace(/token=.*/, "token=***")}`);
|
|
113
|
+
|
|
114
|
+
ws = new WebSocket(url);
|
|
115
|
+
|
|
116
|
+
ws.addEventListener("open", () => {
|
|
117
|
+
logInfo("Connected, sending auth…");
|
|
118
|
+
send({ type: "auth", token: TOKEN, agents: AGENTS, model: MODEL });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
ws.addEventListener("message", (event) => {
|
|
122
|
+
const data = typeof event.data === "string" ? event.data : event.data.toString();
|
|
123
|
+
let msg;
|
|
124
|
+
try {
|
|
125
|
+
msg = JSON.parse(data);
|
|
126
|
+
} catch {
|
|
127
|
+
logErr(`Bad JSON: ${data.slice(0, 100)}`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
handleMessage(msg);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
ws.addEventListener("close", (event) => {
|
|
134
|
+
logWarn(`Disconnected: code=${event.code} reason=${event.reason || "?"}`);
|
|
135
|
+
stopPing();
|
|
136
|
+
if (!intentionalClose) scheduleReconnect();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
ws.addEventListener("error", (event) => {
|
|
140
|
+
logErr(`WebSocket error: ${event.message || "unknown"}`);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function send(msg) {
|
|
145
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
146
|
+
ws.send(JSON.stringify(msg));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function scheduleReconnect() {
|
|
150
|
+
logInfo(`Reconnecting in ${backoff}ms…`);
|
|
151
|
+
setTimeout(() => {
|
|
152
|
+
backoff = Math.min(backoff * 2, MAX_BACKOFF);
|
|
153
|
+
connect();
|
|
154
|
+
}, backoff);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function startPing() {
|
|
158
|
+
stopPing();
|
|
159
|
+
pingTimer = setInterval(() => {
|
|
160
|
+
send({ type: "status", connected: true, agents: AGENTS, model: MODEL });
|
|
161
|
+
}, 25_000);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function stopPing() {
|
|
165
|
+
if (pingTimer) { clearInterval(pingTimer); pingTimer = null; }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ── Message handlers ─────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
function handleMessage(msg) {
|
|
171
|
+
switch (msg.type) {
|
|
172
|
+
case "auth.ok":
|
|
173
|
+
userId = msg.userId;
|
|
174
|
+
backoff = MIN_BACKOFF;
|
|
175
|
+
logOk(`Authenticated (userId=${userId})`);
|
|
176
|
+
startPing();
|
|
177
|
+
break;
|
|
178
|
+
|
|
179
|
+
case "auth.fail":
|
|
180
|
+
logErr(`Auth failed: ${msg.reason}`);
|
|
181
|
+
intentionalClose = true;
|
|
182
|
+
ws?.close(4001, "auth failed");
|
|
183
|
+
break;
|
|
184
|
+
|
|
185
|
+
case "ping":
|
|
186
|
+
send({ type: "pong" });
|
|
187
|
+
break;
|
|
188
|
+
|
|
189
|
+
case "user.message":
|
|
190
|
+
logRecv(`[user.message] sessionKey=${msg.sessionKey} text="${truncate(msg.text, 80)}"`);
|
|
191
|
+
handleUserMessage(msg);
|
|
192
|
+
break;
|
|
193
|
+
|
|
194
|
+
case "user.media":
|
|
195
|
+
logRecv(`[user.media] sessionKey=${msg.sessionKey} url=${msg.mediaUrl}`);
|
|
196
|
+
setTimeout(() => {
|
|
197
|
+
send({
|
|
198
|
+
type: "agent.text",
|
|
199
|
+
sessionKey: msg.sessionKey,
|
|
200
|
+
text: `📎 Received media: ${msg.mediaUrl}`,
|
|
201
|
+
messageId: randomUUID(),
|
|
202
|
+
});
|
|
203
|
+
logSend("[agent.text] media acknowledgement");
|
|
204
|
+
}, DELAY_MS);
|
|
205
|
+
break;
|
|
206
|
+
|
|
207
|
+
case "user.command":
|
|
208
|
+
logRecv(`[user.command] command=${msg.command} args=${msg.args || ""}`);
|
|
209
|
+
setTimeout(() => {
|
|
210
|
+
send({
|
|
211
|
+
type: "agent.text",
|
|
212
|
+
sessionKey: msg.sessionKey,
|
|
213
|
+
text: `Command received: /${msg.command} ${msg.args || ""}`.trim(),
|
|
214
|
+
messageId: randomUUID(),
|
|
215
|
+
});
|
|
216
|
+
}, DELAY_MS);
|
|
217
|
+
break;
|
|
218
|
+
|
|
219
|
+
case "user.action":
|
|
220
|
+
logRecv(`[user.action] action=${msg.action} params=${JSON.stringify(msg.params)}`);
|
|
221
|
+
setTimeout(() => {
|
|
222
|
+
send({
|
|
223
|
+
type: "agent.text",
|
|
224
|
+
sessionKey: msg.sessionKey,
|
|
225
|
+
text: `Action received: ${msg.action}`,
|
|
226
|
+
messageId: randomUUID(),
|
|
227
|
+
});
|
|
228
|
+
}, DELAY_MS);
|
|
229
|
+
break;
|
|
230
|
+
|
|
231
|
+
case "task.scan.request":
|
|
232
|
+
logRecv("[task.scan.request]");
|
|
233
|
+
send({ type: "task.scan.result", tasks: [] });
|
|
234
|
+
logSend("[task.scan.result] empty tasks");
|
|
235
|
+
break;
|
|
236
|
+
|
|
237
|
+
case "models.request":
|
|
238
|
+
logRecv("[models.request]");
|
|
239
|
+
send({ type: "models.list", models: MOCK_MODELS });
|
|
240
|
+
logSend(`[models.list] ${MOCK_MODELS.length} models`);
|
|
241
|
+
break;
|
|
242
|
+
|
|
243
|
+
case "task.schedule":
|
|
244
|
+
logRecv(`[task.schedule] cronJobId=${msg.cronJobId} schedule=${msg.schedule}`);
|
|
245
|
+
send({
|
|
246
|
+
type: "task.schedule.ack",
|
|
247
|
+
cronJobId: msg.cronJobId || `mock_cron_${Date.now()}`,
|
|
248
|
+
taskId: msg.taskId,
|
|
249
|
+
ok: true,
|
|
250
|
+
});
|
|
251
|
+
logSend("[task.schedule.ack] ok");
|
|
252
|
+
break;
|
|
253
|
+
|
|
254
|
+
case "task.delete":
|
|
255
|
+
logRecv(`[task.delete] cronJobId=${msg.cronJobId}`);
|
|
256
|
+
break;
|
|
257
|
+
|
|
258
|
+
case "task.run":
|
|
259
|
+
logRecv(`[task.run] cronJobId=${msg.cronJobId}`);
|
|
260
|
+
handleTaskRun(msg);
|
|
261
|
+
break;
|
|
262
|
+
|
|
263
|
+
case "settings.defaultModel":
|
|
264
|
+
logRecv(`[settings.defaultModel] model=${msg.defaultModel}`);
|
|
265
|
+
send({ type: "defaultModel.updated", model: msg.defaultModel });
|
|
266
|
+
logSend(`[defaultModel.updated] ${msg.defaultModel}`);
|
|
267
|
+
break;
|
|
268
|
+
|
|
269
|
+
default:
|
|
270
|
+
logWarn(`Unhandled message type: ${msg.type}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ── User message reply ───────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
async function handleUserMessage(msg) {
|
|
277
|
+
const replyText = `Mock reply: ${msg.text}`;
|
|
278
|
+
|
|
279
|
+
await sleep(DELAY_MS);
|
|
280
|
+
|
|
281
|
+
if (STREAMING) {
|
|
282
|
+
const runId = randomUUID();
|
|
283
|
+
send({ type: "agent.stream.start", sessionKey: msg.sessionKey, runId });
|
|
284
|
+
|
|
285
|
+
const words = replyText.split(" ");
|
|
286
|
+
for (let i = 0; i < words.length; i++) {
|
|
287
|
+
await sleep(50);
|
|
288
|
+
const chunk = (i === 0 ? "" : " ") + words[i];
|
|
289
|
+
send({ type: "agent.stream.chunk", sessionKey: msg.sessionKey, runId, text: chunk });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
send({ type: "agent.stream.end", sessionKey: msg.sessionKey, runId });
|
|
293
|
+
logSend(`[agent.stream] ${words.length} chunks`);
|
|
294
|
+
|
|
295
|
+
send({
|
|
296
|
+
type: "agent.text",
|
|
297
|
+
sessionKey: msg.sessionKey,
|
|
298
|
+
text: replyText,
|
|
299
|
+
messageId: randomUUID(),
|
|
300
|
+
});
|
|
301
|
+
} else {
|
|
302
|
+
send({
|
|
303
|
+
type: "agent.text",
|
|
304
|
+
sessionKey: msg.sessionKey,
|
|
305
|
+
text: replyText,
|
|
306
|
+
messageId: randomUUID(),
|
|
307
|
+
});
|
|
308
|
+
logSend(`[agent.text] "${truncate(replyText, 60)}"`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ── Task run simulation ──────────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
async function handleTaskRun(msg) {
|
|
315
|
+
const jobId = `mock_job_${Date.now()}`;
|
|
316
|
+
const sessionKey = `agent:${msg.agentId || "main"}:botschat:${userId}:task:mock`;
|
|
317
|
+
const startedAt = Math.floor(Date.now() / 1000);
|
|
318
|
+
|
|
319
|
+
send({
|
|
320
|
+
type: "job.update",
|
|
321
|
+
cronJobId: msg.cronJobId,
|
|
322
|
+
jobId,
|
|
323
|
+
sessionKey,
|
|
324
|
+
status: "running",
|
|
325
|
+
startedAt,
|
|
326
|
+
});
|
|
327
|
+
logSend(`[job.update] running jobId=${jobId}`);
|
|
328
|
+
|
|
329
|
+
send({ type: "job.output", cronJobId: msg.cronJobId, jobId, text: "Mock job started…\n" });
|
|
330
|
+
await sleep(1000);
|
|
331
|
+
send({ type: "job.output", cronJobId: msg.cronJobId, jobId, text: "Mock job processing…\n" });
|
|
332
|
+
await sleep(1000);
|
|
333
|
+
send({ type: "job.output", cronJobId: msg.cronJobId, jobId, text: "Mock job complete.\n" });
|
|
334
|
+
|
|
335
|
+
const finishedAt = Math.floor(Date.now() / 1000);
|
|
336
|
+
send({
|
|
337
|
+
type: "job.update",
|
|
338
|
+
cronJobId: msg.cronJobId,
|
|
339
|
+
jobId,
|
|
340
|
+
sessionKey,
|
|
341
|
+
status: "ok",
|
|
342
|
+
summary: "Mock task executed successfully",
|
|
343
|
+
startedAt,
|
|
344
|
+
finishedAt,
|
|
345
|
+
durationMs: (finishedAt - startedAt) * 1000,
|
|
346
|
+
});
|
|
347
|
+
logSend(`[job.update] ok (${finishedAt - startedAt}s)`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ── Utilities ────────────────────────────────────────────────────────
|
|
351
|
+
|
|
352
|
+
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
|
|
353
|
+
function truncate(s, n) { return s && s.length > n ? s.slice(0, n) + "…" : s; }
|
|
354
|
+
|
|
355
|
+
// ── Graceful shutdown ────────────────────────────────────────────────
|
|
356
|
+
|
|
357
|
+
function shutdown() {
|
|
358
|
+
logInfo("Shutting down…");
|
|
359
|
+
intentionalClose = true;
|
|
360
|
+
stopPing();
|
|
361
|
+
ws?.close(1000, "shutdown");
|
|
362
|
+
process.exit(0);
|
|
363
|
+
}
|
|
364
|
+
process.on("SIGINT", shutdown);
|
|
365
|
+
process.on("SIGTERM", shutdown);
|
|
366
|
+
|
|
367
|
+
// ── Start ────────────────────────────────────────────────────────────
|
|
368
|
+
|
|
369
|
+
console.log(`
|
|
370
|
+
${c.cyan}╭──────────────────────────────────────╮
|
|
371
|
+
│ Mock OpenClaw v1.0 │
|
|
372
|
+
│ Local testing without deployment │
|
|
373
|
+
╰──────────────────────────────────────╯${c.reset}
|
|
374
|
+
Server: ${SERVER_URL}
|
|
375
|
+
Token: ${TOKEN.slice(0, 12)}***
|
|
376
|
+
Agents: ${AGENTS.join(", ")}
|
|
377
|
+
Model: ${MODEL}
|
|
378
|
+
Delay: ${DELAY_MS}ms
|
|
379
|
+
Streaming: ${STREAMING}
|
|
380
|
+
`);
|
|
381
|
+
|
|
382
|
+
connect();
|
package/scripts/test-e2e-chat.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import WebSocket from "ws";
|
|
2
2
|
import { deriveKey, encryptText, decryptText, toBase64, fromBase64 } from "../packages/e2e-crypto/e2e-crypto.js";
|
|
3
3
|
|
|
4
|
-
const E2E_PWD = "
|
|
4
|
+
const E2E_PWD = process.env.TEST_E2E_PASSWORD || "";
|
|
5
5
|
|
|
6
6
|
async function main() {
|
|
7
7
|
// Login
|
|
8
8
|
const res = await fetch("http://localhost:8787/api/auth/login", {
|
|
9
9
|
method: "POST",
|
|
10
10
|
headers: { "Content-Type": "application/json" },
|
|
11
|
-
body: JSON.stringify({ email: "tong@mini.local", password: "
|
|
11
|
+
body: JSON.stringify({ email: "tong@mini.local", password: process.env.TEST_PASSWORD || "" }),
|
|
12
12
|
});
|
|
13
13
|
if (!res.ok) { console.log("Login failed:", res.status); process.exit(1); }
|
|
14
14
|
const login = await res.json() as { id: string; token: string };
|
package/scripts/test-e2e-live.ts
CHANGED
|
@@ -21,7 +21,7 @@ import assert from "node:assert";
|
|
|
21
21
|
const API_BASE = "http://localhost:8787";
|
|
22
22
|
const E2E_PASSWORD = "e2e-test-2026";
|
|
23
23
|
const TEST_EMAIL = "tong@mini.local";
|
|
24
|
-
const TEST_PASS = "
|
|
24
|
+
const TEST_PASS = process.env.TEST_E2E_PASSWORD || "";
|
|
25
25
|
const SECRET_TEXT = `E2E_TEST_SECRET_${Date.now()}`;
|
|
26
26
|
|
|
27
27
|
async function login(): Promise<{ token: string; userId: string }> {
|
package/wrangler.toml
CHANGED
|
@@ -36,6 +36,9 @@ new_sqlite_classes = ["ConnectionDO"]
|
|
|
36
36
|
ENVIRONMENT = "production"
|
|
37
37
|
# Firebase project ID for Google/GitHub Sign-In token verification.
|
|
38
38
|
FIREBASE_PROJECT_ID = "botschat-130ff"
|
|
39
|
+
# Google OAuth Client IDs — used to verify native iOS/Android Google Sign-In tokens.
|
|
40
|
+
GOOGLE_WEB_CLIENT_ID = "670756745871-1anrnjukhhu0a550j86blm771a2k3jou.apps.googleusercontent.com"
|
|
41
|
+
GOOGLE_IOS_CLIENT_ID = "670756745871-ct1k370fk3h5oj3f3gdabqv1nq2b7r54.apps.googleusercontent.com"
|
|
39
42
|
# IMPORTANT: For production, set JWT_SECRET via `wrangler secret put JWT_SECRET`.
|
|
40
43
|
# The app will refuse to start in non-development mode without it.
|
|
41
44
|
|