ohmypetbook 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +71 -0
- package/daemon.js +402 -0
- package/lib/auth.js +137 -0
- package/lib/config.js +35 -0
- package/lib/gateway.js +39 -0
- package/lib/log.js +19 -0
- package/lib/login-server.js +86 -0
- package/lib/service.js +83 -0
- package/lib/sync.js +352 -0
- package/package.json +35 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 bdhwan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# ๐พ OhMyPetBook
|
|
2
|
+
|
|
3
|
+
OpenClaw ๋๋ฐ์ด์ค ๋๊ธฐํ ๋ฐ๋ชฌ. ๊ฐ ๋๋ฐ์ด์ค๋ฅผ ํ๋์ "pet"์ผ๋ก ๋ฑ๋กํ๊ณ , Firestore๋ฅผ ํตํด ์ค์ /ํ๊ฒฝ๋ณ์/์ํฌ์คํ์ด์ค๋ฅผ ์ค์๊ฐ ๋๊ธฐํํฉ๋๋ค.
|
|
4
|
+
|
|
5
|
+
## ์ค์น
|
|
6
|
+
|
|
7
|
+
### npm (๊ถ์ฅ)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g ohmypetbook
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### ์๋ผ์ธ ์ค์น
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# ์ค์น + ๋ก๊ทธ์ธ
|
|
17
|
+
curl -fsSL https://ohmypetbook.com/install.sh | bash -s -- --login
|
|
18
|
+
|
|
19
|
+
# ์ค์น๋ง
|
|
20
|
+
curl -fsSL https://ohmypetbook.com/install.sh | bash
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## ์ฌ์ฉ๋ฒ
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# ๋ก๊ทธ์ธ (๋ธ๋ผ์ฐ์ ์ธ์ฆ)
|
|
27
|
+
ohmypetbook login
|
|
28
|
+
|
|
29
|
+
# ์๋น์ค ๋ฑ๋ก (์๋ ์์)
|
|
30
|
+
ohmypetbook install
|
|
31
|
+
|
|
32
|
+
# ํฌ๊ทธ๋ผ์ด๋ ์คํ
|
|
33
|
+
ohmypetbook run
|
|
34
|
+
|
|
35
|
+
# ์ํ ํ์ธ
|
|
36
|
+
ohmypetbook status
|
|
37
|
+
|
|
38
|
+
# ์ค์ ํ์ธ/๋ณ๊ฒฝ
|
|
39
|
+
ohmypetbook config
|
|
40
|
+
ohmypetbook config set openclawPath /path/to/.openclaw
|
|
41
|
+
|
|
42
|
+
# ์๋น์ค ์ ๊ฑฐ
|
|
43
|
+
ohmypetbook uninstall
|
|
44
|
+
|
|
45
|
+
# ๋ก๊ทธ์์ (์ธ์ฆ + ์๋น์ค ์ ๊ฑฐ)
|
|
46
|
+
ohmypetbook logout
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## ๋์ ๋ฐฉ์
|
|
50
|
+
|
|
51
|
+
1. `ohmypetbook login` โ ๋ธ๋ผ์ฐ์ ๊ฐ ์ด๋ฆฌ๊ณ ๋ก๊ทธ์ธ/์น์ธ
|
|
52
|
+
2. ๋๋ฐ์ด์ค๊ฐ `users/{uid}/pets/{petId}`์ ๋ฑ๋ก๋จ
|
|
53
|
+
3. `~/.openclaw/openclaw.json`, `~/.openclaw/workspace/` ํ์ผ์ Firestore์ ์ค์๊ฐ ๋๊ธฐํ
|
|
54
|
+
4. ๋ธ๋ผ์ฐ์ ([ohmypetbook.com](https://ohmypetbook.com))์์ ์ค์ ํธ์ง ๊ฐ๋ฅ
|
|
55
|
+
5. ํ๊ฒฝ๋ณ์/์ํฌ๋ฆฟ ๋ณ๊ฒฝ ์ `~/.openclaw/.env`์ ๋ฐ์ ํ ๊ฒ์ดํธ์จ์ด ์๋ ์ฌ์์
|
|
56
|
+
|
|
57
|
+
## ๋ณด์
|
|
58
|
+
|
|
59
|
+
- `openclaw.json`์ **์ํธํ**๋์ด Firestore์ ์ ์ฅ (AES-256-GCM)
|
|
60
|
+
- ์ํฌ๋ฆฟ์ ์๋ฒ์ฌ์ด๋ ์ํธํ, `.env`์๋ง ํ๋ฌธ ์ ์ฅ (chmod 600)
|
|
61
|
+
- ์ํธํ ํค๋ 90์ผ๋ง๋ค ์๋ ๋กํ
์ด์
|
|
62
|
+
- Firebase Auth๋ก ๋ณธ์ธ ์ธ์ฆ, Firestore Rules๋ก ์ ๊ทผ ์ ์ด
|
|
63
|
+
|
|
64
|
+
## ์๊ตฌ์ฌํญ
|
|
65
|
+
|
|
66
|
+
- Node.js 18+
|
|
67
|
+
- [OpenClaw](https://openclaw.ai) ์ค์น๋จ
|
|
68
|
+
|
|
69
|
+
## ๋ผ์ด์ ์ค
|
|
70
|
+
|
|
71
|
+
MIT
|
package/daemon.js
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { initializeApp } from "firebase/app";
|
|
4
|
+
import { getAuth, signInWithCustomToken, signInAnonymously } from "firebase/auth";
|
|
5
|
+
import { getFirestore, doc, setDoc, getDoc, onSnapshot } from "firebase/firestore";
|
|
6
|
+
import { execSync } from "child_process";
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import os from "os";
|
|
10
|
+
import crypto from "crypto";
|
|
11
|
+
import readline from "readline";
|
|
12
|
+
|
|
13
|
+
import { firebaseConfig, PETBOOK_CONFIG, CONFIG_FILE, LOG_FILE, OPENCLAW_HOME, CLIENT_URL, CLAIM_DEVICE_URL, REFRESH_SESSION_URL, DECRYPT_SECRETS_URL } from "./lib/config.js";
|
|
14
|
+
import { log } from "./lib/log.js";
|
|
15
|
+
import {
|
|
16
|
+
loadAuth, saveAuth, savePetbookConfig, loadPetbookConfig,
|
|
17
|
+
generatePetId, deviceInfo, validatePet, setPetOffline
|
|
18
|
+
} from "./lib/auth.js";
|
|
19
|
+
import { pushToFirestore, listenFirestore, watchLocal, initRemoteHash, setLoadEnvSecretsCallback, setPushRef, setGetIdTokenCallback } from "./lib/sync.js";
|
|
20
|
+
import { installService, uninstallService } from "./lib/service.js";
|
|
21
|
+
|
|
22
|
+
const app = initializeApp(firebaseConfig);
|
|
23
|
+
const auth = getAuth(app);
|
|
24
|
+
const db = getFirestore(app);
|
|
25
|
+
|
|
26
|
+
// โโ Helpers โโ
|
|
27
|
+
|
|
28
|
+
function prompt(question) {
|
|
29
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
30
|
+
return new Promise(resolve => rl.question(question, ans => { rl.close(); resolve(ans.trim()); }));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function generateRequestId() {
|
|
34
|
+
return crypto.randomBytes(16).toString("base64url");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function restoreSession(refreshToken) {
|
|
38
|
+
const tokenResp = await fetch(
|
|
39
|
+
`https://securetoken.googleapis.com/v1/token?key=${firebaseConfig.apiKey}`,
|
|
40
|
+
{
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
43
|
+
body: `grant_type=refresh_token&refresh_token=${encodeURIComponent(refreshToken)}`,
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
if (!tokenResp.ok) throw new Error("refresh token ๋ง๋ฃ");
|
|
47
|
+
const tokenData = await tokenResp.json();
|
|
48
|
+
|
|
49
|
+
const resp = await fetch(REFRESH_SESSION_URL, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: { "Content-Type": "application/json" },
|
|
52
|
+
body: JSON.stringify({ idToken: tokenData.id_token }),
|
|
53
|
+
});
|
|
54
|
+
if (!resp.ok) throw new Error("์ธ์
๋ณต์ ์คํจ");
|
|
55
|
+
const { customToken } = await resp.json();
|
|
56
|
+
|
|
57
|
+
const cred = await signInWithCustomToken(auth, customToken);
|
|
58
|
+
const saved = loadAuth();
|
|
59
|
+
if (cred.user.refreshToken !== saved.refreshToken) {
|
|
60
|
+
saveAuth({ ...saved, refreshToken: cred.user.refreshToken });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// โโ ํ๊ฒฝ๋ณ์ & ์ํฌ๋ฆฟ ๋ก๋ฉ โโ
|
|
65
|
+
|
|
66
|
+
async function loadEnvAndSecrets(uid, petId) {
|
|
67
|
+
try {
|
|
68
|
+
// 1. ๊ณ์ ๋ ๋ฒจ
|
|
69
|
+
const userSnap = await getDoc(doc(db, "users", uid));
|
|
70
|
+
const userData = userSnap.exists() ? userSnap.data() : {};
|
|
71
|
+
|
|
72
|
+
// 2. ๋๋ฐ์ด์ค/์ํฌ์คํ์ด์ค ๋ ๋ฒจ (pet doc์ ์ด๋ฏธ ์์)
|
|
73
|
+
const petSnap = await getDoc(doc(db, "users", uid, "pets", petId));
|
|
74
|
+
const petData = petSnap.exists() ? petSnap.data() : {};
|
|
75
|
+
|
|
76
|
+
// ํ๊ฒฝ๋ณ์ ๋จธ์ง (๊ณ์ < ๋๋ฐ์ด์ค)
|
|
77
|
+
const envVars = {
|
|
78
|
+
...(userData.envVars || {}),
|
|
79
|
+
...(petData.deviceEnvVars || {}),
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// ์ํฌ๋ฆฟ ๋จธ์ง (๊ณ์ < ๋๋ฐ์ด์ค)
|
|
83
|
+
const allSecrets = {
|
|
84
|
+
...(userData.secrets || {}),
|
|
85
|
+
...(petData.deviceSecrets || {}),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// ์ํฌ๋ฆฟ ๋ณตํธํ
|
|
89
|
+
if (Object.keys(allSecrets).length > 0) {
|
|
90
|
+
try {
|
|
91
|
+
const idToken = await auth.currentUser.getIdToken();
|
|
92
|
+
const resp = await fetch(DECRYPT_SECRETS_URL, {
|
|
93
|
+
method: "POST",
|
|
94
|
+
headers: { "Content-Type": "application/json" },
|
|
95
|
+
body: JSON.stringify({ idToken, secrets: allSecrets }),
|
|
96
|
+
});
|
|
97
|
+
if (resp.ok) {
|
|
98
|
+
const { values } = await resp.json();
|
|
99
|
+
for (const [key, value] of Object.entries(values)) {
|
|
100
|
+
if (value !== null) envVars[key] = String(value);
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
log("โ ๏ธ ์ํฌ๋ฆฟ ๋ณตํธํ ์คํจ");
|
|
104
|
+
}
|
|
105
|
+
} catch (e) {
|
|
106
|
+
log(`โ ๏ธ ์ํฌ๋ฆฟ ๋ณตํธํ ์๋ฌ: ${e.message}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// .env ํ์ผ์ ์ฐ๊ธฐ โ openclaw gateway๊ฐ ์ฝ์
|
|
111
|
+
if (Object.keys(envVars).length > 0) {
|
|
112
|
+
const envPath = path.join(OPENCLAW_HOME, ".env");
|
|
113
|
+
const lines = Object.entries(envVars)
|
|
114
|
+
.filter(([k, v]) => k && v !== undefined)
|
|
115
|
+
.map(([k, v]) => `${k}=${v}`);
|
|
116
|
+
fs.writeFileSync(envPath, lines.join("\n") + "\n", "utf-8");
|
|
117
|
+
fs.chmodSync(envPath, 0o600);
|
|
118
|
+
log(`๐ .env ์
๋ฐ์ดํธ (${lines.length}๊ฐ ๋ณ์)`);
|
|
119
|
+
}
|
|
120
|
+
} catch (e) {
|
|
121
|
+
log(`โ ๏ธ ํ๊ฒฝ๋ณ์ ๋ก๋ ์คํจ: ${e.message}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// โโ Commands โโ
|
|
126
|
+
|
|
127
|
+
async function cmdLogin() {
|
|
128
|
+
const codeArg = process.argv[3];
|
|
129
|
+
const petId = generatePetId();
|
|
130
|
+
const info = deviceInfo();
|
|
131
|
+
|
|
132
|
+
// ์ฝ๋๊ฐ ์ง์ ์ฃผ์ด์ง ๊ฒฝ์ฐ โ ๋ฐ๋ก ๋ฑ๋ก
|
|
133
|
+
if (codeArg) {
|
|
134
|
+
return doClaimDevice(codeArg, petId, info);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 1. ์ต๋ช
๋ก๊ทธ์ธ
|
|
138
|
+
await signInAnonymously(auth);
|
|
139
|
+
const requestId = generateRequestId();
|
|
140
|
+
|
|
141
|
+
// 2. loginRequests ๋ฌธ์ ์์ฑ
|
|
142
|
+
await setDoc(doc(db, "loginRequests", requestId), {
|
|
143
|
+
petId,
|
|
144
|
+
...info,
|
|
145
|
+
status: "pending",
|
|
146
|
+
createdAt: new Date().toISOString(),
|
|
147
|
+
expiresAt: new Date(Date.now() + 10 * 60 * 1000).toISOString(),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// 3. URL ํ์
|
|
151
|
+
const url = `${CLIENT_URL}/auth/device?requestId=${requestId}`;
|
|
152
|
+
|
|
153
|
+
console.log("");
|
|
154
|
+
console.log(" \x1b[1m๐พ OhMyPetBook ๊ธฐ๊ธฐ ๋ฑ๋ก\x1b[0m");
|
|
155
|
+
console.log("");
|
|
156
|
+
console.log(" ์๋ URL์ ๋ธ๋ผ์ฐ์ ์์ ์ด์ด์ฃผ์ธ์:");
|
|
157
|
+
console.log(` \x1b[4m\x1b[36m${url}\x1b[0m`);
|
|
158
|
+
console.log("");
|
|
159
|
+
|
|
160
|
+
// 4. Firestore ๊ตฌ๋
+ ์๋ ์
๋ ฅ ๋์ ๋๊ธฐ
|
|
161
|
+
const result = await new Promise((resolve, reject) => {
|
|
162
|
+
let settled = false;
|
|
163
|
+
|
|
164
|
+
// Firestore ์ค์๊ฐ ๊ตฌ๋
|
|
165
|
+
const unsub = onSnapshot(doc(db, "loginRequests", requestId), (snap) => {
|
|
166
|
+
const data = snap.data();
|
|
167
|
+
if (data?.status === "approved" && data?.customToken && !settled) {
|
|
168
|
+
settled = true;
|
|
169
|
+
unsub();
|
|
170
|
+
resolve({ type: "auto", customToken: data.customToken, uid: data.uid, email: data.email });
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// ์๋ ์ฝ๋ ์
๋ ฅ (ํด๋ฐฑ)
|
|
175
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
176
|
+
console.log(" ๋ธ๋ผ์ฐ์ ์์ ์น์ธํ๋ฉด ์๋์ผ๋ก ์งํ๋ฉ๋๋ค.");
|
|
177
|
+
console.log(" ๋๋ ๋ฑ๋ก ์ฝ๋๋ฅผ ์ง์ ์
๋ ฅํ์ธ์:");
|
|
178
|
+
console.log("");
|
|
179
|
+
rl.question(" ๋ฑ๋ก ์ฝ๋ (์๋ ๋๊ธฐ ์ค...): ", (code) => {
|
|
180
|
+
rl.close();
|
|
181
|
+
if (!settled && code.trim()) {
|
|
182
|
+
settled = true;
|
|
183
|
+
unsub();
|
|
184
|
+
resolve({ type: "manual", code: code.trim() });
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// 10๋ถ ํ์์์
|
|
189
|
+
setTimeout(() => {
|
|
190
|
+
if (!settled) {
|
|
191
|
+
settled = true;
|
|
192
|
+
unsub();
|
|
193
|
+
reject(new Error("๋ฑ๋ก ์๊ฐ ์ด๊ณผ (10๋ถ)"));
|
|
194
|
+
}
|
|
195
|
+
}, 10 * 60 * 1000);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
if (result.type === "auto") {
|
|
199
|
+
// ์๋ ์น์ธ โ customToken์ผ๋ก ๋ฐ๋ก ๋ก๊ทธ์ธ
|
|
200
|
+
console.log("\n โ
๋ธ๋ผ์ฐ์ ์์ ์น์ธ๋จ!");
|
|
201
|
+
const cred = await signInWithCustomToken(auth, result.customToken);
|
|
202
|
+
const petName = info.hostname || os.hostname();
|
|
203
|
+
|
|
204
|
+
saveAuth({
|
|
205
|
+
uid: result.uid,
|
|
206
|
+
email: result.email,
|
|
207
|
+
petId,
|
|
208
|
+
petName,
|
|
209
|
+
refreshToken: cred.user.refreshToken,
|
|
210
|
+
savedAt: new Date().toISOString()
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
printSuccess(result.email, petName, petId);
|
|
214
|
+
} else {
|
|
215
|
+
// ์๋ ์ฝ๋ ์
๋ ฅ
|
|
216
|
+
await doClaimDevice(result.code, petId, info);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function doClaimDevice(code, petId, info) {
|
|
221
|
+
console.log(" ๐ ๋ฑ๋ก ์ค...");
|
|
222
|
+
|
|
223
|
+
const resp = await fetch(CLAIM_DEVICE_URL, {
|
|
224
|
+
method: "POST",
|
|
225
|
+
headers: { "Content-Type": "application/json" },
|
|
226
|
+
body: JSON.stringify({ code, petId, deviceInfo: info }),
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
if (!resp.ok) {
|
|
230
|
+
const err = await resp.json().catch(() => ({}));
|
|
231
|
+
console.error(` โ ${err.error || `๋ฑ๋ก ์คํจ (${resp.status})`}`);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const { customToken, uid, email } = await resp.json();
|
|
236
|
+
const cred = await signInWithCustomToken(auth, customToken);
|
|
237
|
+
const petName = info.hostname || os.hostname();
|
|
238
|
+
|
|
239
|
+
saveAuth({
|
|
240
|
+
uid,
|
|
241
|
+
email,
|
|
242
|
+
petId,
|
|
243
|
+
petName,
|
|
244
|
+
refreshToken: cred.user.refreshToken,
|
|
245
|
+
savedAt: new Date().toISOString()
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
printSuccess(email, petName, petId);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function printSuccess(email, petName, petId) {
|
|
252
|
+
console.log(` โ ๊ณ์ : \x1b[1m${email}\x1b[0m`);
|
|
253
|
+
console.log(` โ Pet: \x1b[1m${petName}\x1b[0m (\x1b[2m${petId.slice(0, 16)}...\x1b[0m)`);
|
|
254
|
+
console.log("");
|
|
255
|
+
console.log(" ๋ค์ ๋จ๊ณ:");
|
|
256
|
+
console.log(" \x1b[1mohmypetbook install\x1b[0m โ ์๋น์ค ๋ฑ๋ก (์๋ ์์)");
|
|
257
|
+
console.log(" \x1b[1mohmypetbook run\x1b[0m โ ํฌ๊ทธ๋ผ์ด๋ ์คํ");
|
|
258
|
+
console.log("");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function cmdRun() {
|
|
262
|
+
const saved = loadAuth();
|
|
263
|
+
if (!saved?.refreshToken) {
|
|
264
|
+
log("โ ์ธ์ฆ ์ ๋ณด ์์. `ohmypetbook login` ๋จผ์ ์คํํ์ธ์.");
|
|
265
|
+
process.exit(1);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
log(`๐พ ${saved.petName || "pet"} ์์ (${saved.email})`);
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
await restoreSession(saved.refreshToken);
|
|
272
|
+
} catch (e) {
|
|
273
|
+
log(`โ ์ธ์ฆ ์คํจ: ${e.message}. ์ฌ๋ฑ๋ก ํ์: ohmypetbook login`);
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const uid = auth.currentUser.uid;
|
|
278
|
+
const petId = saved.petId;
|
|
279
|
+
log(`โ ์ธ์ฆ ์๋ฃ: ${auth.currentUser.email}`);
|
|
280
|
+
|
|
281
|
+
if (!(await validatePet(db, uid, petId))) {
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
await loadEnvAndSecrets(uid, petId);
|
|
286
|
+
setLoadEnvSecretsCallback(() => loadEnvAndSecrets(uid, petId));
|
|
287
|
+
setPushRef(() => pushToFirestore(db, uid, petId));
|
|
288
|
+
setGetIdTokenCallback(() => auth.currentUser.getIdToken());
|
|
289
|
+
|
|
290
|
+
initRemoteHash();
|
|
291
|
+
await pushToFirestore(db, uid, petId);
|
|
292
|
+
listenFirestore(db, uid, petId);
|
|
293
|
+
log("๐ Firestore ์ค์๊ฐ ๋ฆฌ์ค๋ ์์");
|
|
294
|
+
|
|
295
|
+
watchLocal(db, uid, petId);
|
|
296
|
+
log("๐ ๋ก์ปฌ ํ์ผ ๊ฐ์ ์์");
|
|
297
|
+
log("๐ ๋ฐ๋ชฌ ์คํ ์ค...");
|
|
298
|
+
|
|
299
|
+
const shutdown = async () => {
|
|
300
|
+
log("์ข
๋ฃ ์ค...");
|
|
301
|
+
await setPetOffline(db, uid, petId);
|
|
302
|
+
process.exit(0);
|
|
303
|
+
};
|
|
304
|
+
process.on("SIGTERM", shutdown);
|
|
305
|
+
process.on("SIGINT", shutdown);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function cmdStatus() {
|
|
309
|
+
const saved = loadAuth();
|
|
310
|
+
console.log("\n \x1b[1m๐พ OhMyPetBook Status\x1b[0m\n");
|
|
311
|
+
if (saved) {
|
|
312
|
+
const expired = saved.expiresAt && new Date(saved.expiresAt) < new Date();
|
|
313
|
+
console.log(` Pet: ${saved.petName || "N/A"} (${saved.petId?.slice(0, 16) || "N/A"}...)`);
|
|
314
|
+
console.log(` ๊ณ์ : ${saved.email}`);
|
|
315
|
+
console.log(` ๋ง๋ฃ: ${saved.expiresAt ? new Date(saved.expiresAt).toLocaleDateString("ko-KR") : "N/A"} ${expired ? "\x1b[31m(๋ง๋ฃ๋จ)\x1b[0m" : "\x1b[32m(์ ํจ)\x1b[0m"}`);
|
|
316
|
+
console.log(` ๊ฒฝ๋ก: ${OPENCLAW_HOME}`);
|
|
317
|
+
console.log(` ์ค์ : ${CONFIG_FILE}`);
|
|
318
|
+
} else {
|
|
319
|
+
console.log(" \x1b[33m๋ฑ๋ก๋ pet ์์. ohmypetbook login ์คํํ์ธ์.\x1b[0m");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const platform = os.platform();
|
|
323
|
+
if (platform === "darwin") {
|
|
324
|
+
try {
|
|
325
|
+
execSync("launchctl list | grep ohmypetbook", { stdio: "pipe" });
|
|
326
|
+
console.log(" ์๋น์ค: \x1b[32m์คํ ์ค\x1b[0m");
|
|
327
|
+
} catch { console.log(" ์๋น์ค: \x1b[33m๋ฏธ๋ฑ๋ก\x1b[0m"); }
|
|
328
|
+
} else if (platform === "linux") {
|
|
329
|
+
try {
|
|
330
|
+
const s = execSync("systemctl --user is-active petbook-daemon", { encoding: "utf-8" }).trim();
|
|
331
|
+
console.log(` ์๋น์ค: \x1b[32m${s}\x1b[0m`);
|
|
332
|
+
} catch { console.log(" ์๋น์ค: \x1b[33m๋ฏธ๋ฑ๋ก\x1b[0m"); }
|
|
333
|
+
}
|
|
334
|
+
console.log(` ๋ก๊ทธ: ${LOG_FILE}\n`);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// โโ Main โโ
|
|
338
|
+
|
|
339
|
+
const [,, command] = process.argv;
|
|
340
|
+
|
|
341
|
+
switch (command) {
|
|
342
|
+
case "login": await cmdLogin(); break;
|
|
343
|
+
case "run": await cmdRun(); break;
|
|
344
|
+
case "install": {
|
|
345
|
+
if (!loadAuth()?.refreshToken) { console.error("โ ๋จผ์ ๋ฑ๋ก: ohmypetbook login"); process.exit(1); }
|
|
346
|
+
installService();
|
|
347
|
+
console.log("\n โ ์๋น์ค ๋ฑ๋ก ์๋ฃ. ๋ฐ๋ชฌ์ด ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์คํ๋ฉ๋๋ค.\n");
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
case "uninstall":
|
|
351
|
+
uninstallService();
|
|
352
|
+
console.log(" โ ์๋น์ค ์ ๊ฑฐ ์๋ฃ\n");
|
|
353
|
+
break;
|
|
354
|
+
case "status":
|
|
355
|
+
cmdStatus();
|
|
356
|
+
break;
|
|
357
|
+
case "config": {
|
|
358
|
+
const [,,, key, ...rest] = process.argv;
|
|
359
|
+
if (key === "openclawPath" && rest.length) {
|
|
360
|
+
savePetbookConfig({ openclawPath: rest.join(" ") });
|
|
361
|
+
console.log(` โ openclawPath = ${rest.join(" ")}`);
|
|
362
|
+
} else {
|
|
363
|
+
const cfg = loadPetbookConfig();
|
|
364
|
+
console.log("\n \x1b[1m~/.ohmypetbook/ohmypetbook.json\x1b[0m\n");
|
|
365
|
+
console.log(` openclawPath: ${cfg.openclawPath || "~/.openclaw (๊ธฐ๋ณธ๊ฐ)"}`);
|
|
366
|
+
console.log(` pet: ${cfg.auth?.petName || "์์"} (${cfg.auth?.petId?.slice(0, 16) || "N/A"})`);
|
|
367
|
+
console.log(` ๊ณ์ : ${cfg.auth?.email || "์์"}`);
|
|
368
|
+
console.log("");
|
|
369
|
+
console.log(" ๋ณ๊ฒฝ: ohmypetbook config openclawPath /path/to/.openclaw");
|
|
370
|
+
console.log("");
|
|
371
|
+
}
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
case "logout": {
|
|
375
|
+
const saved = loadAuth();
|
|
376
|
+
if (saved?.petId && saved?.uid && saved?.refreshToken) {
|
|
377
|
+
try {
|
|
378
|
+
await restoreSession(saved.refreshToken);
|
|
379
|
+
await setPetOffline(db, saved.uid, saved.petId);
|
|
380
|
+
} catch {}
|
|
381
|
+
}
|
|
382
|
+
uninstallService();
|
|
383
|
+
if (fs.existsSync(PETBOOK_CONFIG)) fs.unlinkSync(PETBOOK_CONFIG);
|
|
384
|
+
console.log("\n โ ๋ก๊ทธ์์ + ์๋น์ค ์ ๊ฑฐ ์๋ฃ\n");
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
default:
|
|
388
|
+
console.log(`
|
|
389
|
+
\x1b[1m๐พ ohmypetbook\x1b[0m โ OpenClaw ๋๋ฐ์ด์ค ๋๊ธฐํ ๋ฐ๋ชฌ
|
|
390
|
+
|
|
391
|
+
๊ฐ ๋๋ฐ์ด์ค = 1 pet. Firestore์ ์ค์๊ฐ ๋๊ธฐํ.
|
|
392
|
+
|
|
393
|
+
\x1b[1mCommands:\x1b[0m
|
|
394
|
+
ohmypetbook login [์ฝ๋] ๊ธฐ๊ธฐ ๋ฑ๋ก (URL + ์๋์น์ธ / ์ฝ๋ ์๋์
๋ ฅ)
|
|
395
|
+
ohmypetbook install ์๋น์ค ๋ฑ๋ก (๋ถํ
์ ์๋ ์์)
|
|
396
|
+
ohmypetbook uninstall ์๋น์ค ์ ๊ฑฐ
|
|
397
|
+
ohmypetbook run ํฌ๊ทธ๋ผ์ด๋ ์คํ
|
|
398
|
+
ohmypetbook status ์ํ ํ์ธ
|
|
399
|
+
ohmypetbook config ์ค์ ํ์ธ/๋ณ๊ฒฝ (openclawPath ๋ฑ)
|
|
400
|
+
ohmypetbook logout pet ํด์ + ์๋น์ค ์ ๊ฑฐ
|
|
401
|
+
`);
|
|
402
|
+
}
|
package/lib/auth.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import {
|
|
2
|
+
signInWithEmailAndPassword,
|
|
3
|
+
signInWithCredential, GoogleAuthProvider
|
|
4
|
+
} from "firebase/auth";
|
|
5
|
+
import { doc, getDoc, setDoc, updateDoc } from "firebase/firestore";
|
|
6
|
+
import crypto from "crypto";
|
|
7
|
+
import { execSync } from "child_process";
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import os from "os";
|
|
10
|
+
import { PETBOOK_HOME, PETBOOK_CONFIG, OPENCLAW_HOME, TOKEN_EXPIRY_DAYS } from "./config.js";
|
|
11
|
+
import { ensureDir, log } from "./log.js";
|
|
12
|
+
|
|
13
|
+
// โโ ohmypetbook.json ๊ด๋ฆฌ โโ
|
|
14
|
+
|
|
15
|
+
export function savePetbookConfig(data) {
|
|
16
|
+
ensureDir(PETBOOK_HOME);
|
|
17
|
+
const existing = loadPetbookConfig();
|
|
18
|
+
const merged = { ...existing, ...data };
|
|
19
|
+
fs.writeFileSync(PETBOOK_CONFIG, JSON.stringify(merged, null, 2), "utf-8");
|
|
20
|
+
fs.chmodSync(PETBOOK_CONFIG, 0o600);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function loadPetbookConfig() {
|
|
24
|
+
try { return JSON.parse(fs.readFileSync(PETBOOK_CONFIG, "utf-8")); } catch { return {}; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function saveAuth(data) {
|
|
28
|
+
savePetbookConfig({ auth: data, openclawPath: OPENCLAW_HOME });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function loadAuth() {
|
|
32
|
+
return loadPetbookConfig().auth || null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// โโ Token / Verification โโ
|
|
36
|
+
|
|
37
|
+
export function generateVerificationCode() {
|
|
38
|
+
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
39
|
+
let code = "";
|
|
40
|
+
for (let i = 0; i < 4; i++) code += chars[crypto.randomInt(chars.length)];
|
|
41
|
+
return code;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function getDeviceId() {
|
|
45
|
+
try {
|
|
46
|
+
const platform = os.platform();
|
|
47
|
+
let raw;
|
|
48
|
+
if (platform === "darwin") {
|
|
49
|
+
raw = execSync("/usr/sbin/ioreg -rd1 -c IOPlatformExpertDevice", { encoding: "utf-8" });
|
|
50
|
+
const match = raw.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
|
|
51
|
+
if (match) return match[1];
|
|
52
|
+
} else if (platform === "linux") {
|
|
53
|
+
raw = fs.readFileSync("/etc/machine-id", "utf-8").trim();
|
|
54
|
+
if (raw) return raw;
|
|
55
|
+
}
|
|
56
|
+
} catch {}
|
|
57
|
+
// fallback: hostname + arch
|
|
58
|
+
return `${os.hostname()}-${os.arch()}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function generatePetId() {
|
|
62
|
+
const deviceId = getDeviceId();
|
|
63
|
+
const hash = crypto.createHash("sha256").update(deviceId).digest("hex").slice(0, 16);
|
|
64
|
+
return `pet_${hash}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// โโ Firebase Auth โโ
|
|
68
|
+
|
|
69
|
+
export async function signInFromCredential(auth, credData) {
|
|
70
|
+
if (credData.type === "email") {
|
|
71
|
+
return signInWithEmailAndPassword(auth, credData.email, credData.password);
|
|
72
|
+
} else if (credData.type === "google") {
|
|
73
|
+
const credential = GoogleAuthProvider.credential(credData.googleIdToken);
|
|
74
|
+
return signInWithCredential(auth, credential);
|
|
75
|
+
}
|
|
76
|
+
throw new Error(`์ง์ํ์ง ์๋ ์ธ์ฆ ํ์
: ${credData.type}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// โโ Pet ๋ฑ๋ก/๊ฒ์ฆ (Firestore) โโ
|
|
80
|
+
|
|
81
|
+
export function deviceInfo() {
|
|
82
|
+
return {
|
|
83
|
+
hostname: os.hostname(),
|
|
84
|
+
platform: os.platform(),
|
|
85
|
+
arch: os.arch(),
|
|
86
|
+
nodeVersion: process.version
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function registerPet(db, uid, petName) {
|
|
91
|
+
const petId = generatePetId();
|
|
92
|
+
const expiresAt = new Date(Date.now() + TOKEN_EXPIRY_DAYS * 24 * 60 * 60 * 1000).toISOString();
|
|
93
|
+
const now = new Date().toISOString();
|
|
94
|
+
|
|
95
|
+
await setDoc(doc(db, "users", uid, "pets", petId), {
|
|
96
|
+
name: petName || os.hostname(),
|
|
97
|
+
...deviceInfo(),
|
|
98
|
+
openclawPath: OPENCLAW_HOME,
|
|
99
|
+
createdAt: now,
|
|
100
|
+
expiresAt,
|
|
101
|
+
lastSeen: now,
|
|
102
|
+
status: "online",
|
|
103
|
+
revoked: false
|
|
104
|
+
}, { merge: true });
|
|
105
|
+
|
|
106
|
+
return { petId, expiresAt };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function validatePet(db, uid, petId) {
|
|
110
|
+
if (!petId) return true;
|
|
111
|
+
const snap = await getDoc(doc(db, "users", uid, "pets", petId));
|
|
112
|
+
if (!snap.exists()) return true;
|
|
113
|
+
|
|
114
|
+
const data = snap.data();
|
|
115
|
+
if (data.revoked) {
|
|
116
|
+
log("๐ซ ์ด pet์ด ํ๊ธฐ๋์์ต๋๋ค. ์ฌ๋ฑ๋ก ํ์.");
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// lastSeen + status ์
๋ฐ์ดํธ
|
|
121
|
+
await updateDoc(doc(db, "users", uid, "pets", petId), {
|
|
122
|
+
lastSeen: new Date().toISOString(),
|
|
123
|
+
status: "online",
|
|
124
|
+
...deviceInfo()
|
|
125
|
+
});
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function setPetOffline(db, uid, petId) {
|
|
130
|
+
if (!petId) return;
|
|
131
|
+
try {
|
|
132
|
+
await updateDoc(doc(db, "users", uid, "pets", petId), {
|
|
133
|
+
status: "offline",
|
|
134
|
+
lastSeen: new Date().toISOString()
|
|
135
|
+
});
|
|
136
|
+
} catch {}
|
|
137
|
+
}
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
|
|
5
|
+
export const HOME = os.homedir();
|
|
6
|
+
export const PETBOOK_HOME = path.join(HOME, ".ohmypetbook");
|
|
7
|
+
export const PETBOOK_CONFIG = path.join(PETBOOK_HOME, "ohmypetbook.json");
|
|
8
|
+
export const LOG_FILE = path.join(PETBOOK_HOME, "ohmypetbook.log");
|
|
9
|
+
|
|
10
|
+
// ohmypetbook.json์์ openclaw ๊ฒฝ๋ก ์ฝ๊ธฐ (๊ธฐ๋ณธ: ~/.openclaw)
|
|
11
|
+
function loadPetbookConfig() {
|
|
12
|
+
try { return JSON.parse(fs.readFileSync(PETBOOK_CONFIG, "utf-8")); } catch { return {}; }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const petbookConfig = loadPetbookConfig();
|
|
16
|
+
export const OPENCLAW_HOME = petbookConfig.openclawPath || process.env.OPENCLAW_HOME || path.join(HOME, ".openclaw");
|
|
17
|
+
export const CONFIG_FILE = path.join(OPENCLAW_HOME, "openclaw.json");
|
|
18
|
+
export const CONFIG_DIR = path.join(OPENCLAW_HOME, "openclaw");
|
|
19
|
+
export const WORKSPACE_DIR = path.join(OPENCLAW_HOME, "workspace");
|
|
20
|
+
export const CLIENT_URL = "https://ohmypetbook.com";
|
|
21
|
+
export const TOKEN_EXPIRY_DAYS = 365;
|
|
22
|
+
export const CLAIM_DEVICE_URL = "https://claimdevice-gkspcxvo6q-du.a.run.app";
|
|
23
|
+
export const REFRESH_SESSION_URL = "https://refreshsession-gkspcxvo6q-du.a.run.app";
|
|
24
|
+
export const ENCRYPT_SECRET_URL = "https://asia-northeast3-openclaw-petbook.cloudfunctions.net/encryptSecret";
|
|
25
|
+
export const DECRYPT_SECRETS_URL = "https://asia-northeast3-openclaw-petbook.cloudfunctions.net/decryptSecrets";
|
|
26
|
+
|
|
27
|
+
export const firebaseConfig = {
|
|
28
|
+
apiKey: "AIzaSyDaOWYC3U3nMNVoO1hSwE4IndGavOdpr9o",
|
|
29
|
+
authDomain: "openclaw-petbook.firebaseapp.com",
|
|
30
|
+
projectId: "openclaw-petbook",
|
|
31
|
+
storageBucket: "openclaw-petbook.firebasestorage.app",
|
|
32
|
+
messagingSenderId: "724877033848",
|
|
33
|
+
appId: "1:724877033848:web:e364e397bb8a71cb00088d",
|
|
34
|
+
measurementId: "G-W0MB718DVQ"
|
|
35
|
+
};
|
package/lib/gateway.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { HOME } from "./config.js";
|
|
5
|
+
import { log } from "./log.js";
|
|
6
|
+
|
|
7
|
+
function findBin(name) {
|
|
8
|
+
try {
|
|
9
|
+
return execSync(`which ${name}`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
10
|
+
} catch {
|
|
11
|
+
try {
|
|
12
|
+
const nvmDir = path.join(HOME, ".nvm/versions/node");
|
|
13
|
+
const versions = fs.readdirSync(nvmDir);
|
|
14
|
+
if (versions.length) {
|
|
15
|
+
const bin = path.join(nvmDir, versions[versions.length - 1], "bin", name);
|
|
16
|
+
if (fs.existsSync(bin)) return bin;
|
|
17
|
+
}
|
|
18
|
+
} catch {}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function restartGateway() {
|
|
24
|
+
log("๐ ๊ฒ์ดํธ์จ์ด ์ฌ์์ ์ค...");
|
|
25
|
+
try {
|
|
26
|
+
const bin = findBin("openclaw");
|
|
27
|
+
if (bin) {
|
|
28
|
+
// openclaw ๋ฐ์ด๋๋ฆฌ์ ๋๋ ํ ๋ฆฌ๋ฅผ PATH ์์ ์ถ๊ฐ (launchd ํ๊ฒฝ์์ ์ฌ๋ฐ๋ฅธ node ์ฌ์ฉ)
|
|
29
|
+
const binDir = path.dirname(bin);
|
|
30
|
+
const env = { ...process.env, PATH: `${binDir}:${process.env.PATH || ""}` };
|
|
31
|
+
execSync(`${bin} gateway restart`, { timeout: 30000, stdio: "pipe", env });
|
|
32
|
+
log("โ ๊ฒ์ดํธ์จ์ด ์ฌ์์ ์๋ฃ");
|
|
33
|
+
} else {
|
|
34
|
+
log("โ openclaw ๋ช
๋ น์ด๋ฅผ ์ฐพ์ ์ ์์");
|
|
35
|
+
}
|
|
36
|
+
} catch (e) {
|
|
37
|
+
log(`โ ๊ฒ์ดํธ์จ์ด ์ฌ์์ ์คํจ: ${e.message}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
package/lib/log.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import { LOG_FILE, PETBOOK_HOME } from "./config.js";
|
|
3
|
+
|
|
4
|
+
export function ensureDir(dir) {
|
|
5
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function ts() {
|
|
9
|
+
return new Date().toLocaleTimeString("ko-KR", { hour12: false });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function log(msg) {
|
|
13
|
+
const line = `[${ts()}] ${msg}`;
|
|
14
|
+
console.log(line);
|
|
15
|
+
try {
|
|
16
|
+
ensureDir(PETBOOK_HOME);
|
|
17
|
+
fs.appendFileSync(LOG_FILE, line + "\n");
|
|
18
|
+
} catch {}
|
|
19
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createServer } from "http";
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import crypto from "crypto";
|
|
4
|
+
import os from "os";
|
|
5
|
+
import { CLIENT_URL } from "./config.js";
|
|
6
|
+
import { generateVerificationCode } from "./auth.js";
|
|
7
|
+
|
|
8
|
+
const SPINNER = ["โ ", "โ ", "โ น", "โ ธ", "โ ผ", "โ ด", "โ ฆ", "โ ง", "โ ", "โ "];
|
|
9
|
+
|
|
10
|
+
export function waitForBrowserAuth() {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const verifyCode = generateVerificationCode();
|
|
13
|
+
const port = 19876 + crypto.randomInt(1000);
|
|
14
|
+
|
|
15
|
+
const server = createServer((req, res) => {
|
|
16
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
17
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
18
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
19
|
+
|
|
20
|
+
if (req.method === "OPTIONS") { res.writeHead(200); res.end(); return; }
|
|
21
|
+
|
|
22
|
+
if (req.method === "GET" && req.url === "/info") {
|
|
23
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
24
|
+
res.end(JSON.stringify({
|
|
25
|
+
verifyCode,
|
|
26
|
+
hostname: os.hostname(),
|
|
27
|
+
platform: os.platform(),
|
|
28
|
+
arch: os.arch()
|
|
29
|
+
}));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (req.method === "POST" && req.url === "/callback") {
|
|
34
|
+
let body = "";
|
|
35
|
+
req.on("data", (chunk) => body += chunk);
|
|
36
|
+
req.on("end", () => {
|
|
37
|
+
try {
|
|
38
|
+
const data = JSON.parse(body);
|
|
39
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
40
|
+
res.end(JSON.stringify({ ok: true }));
|
|
41
|
+
server.close();
|
|
42
|
+
clearInterval(spinnerInterval);
|
|
43
|
+
process.stdout.write("\r\x1b[K");
|
|
44
|
+
resolve(data);
|
|
45
|
+
} catch {
|
|
46
|
+
res.writeHead(400);
|
|
47
|
+
res.end("Invalid payload");
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
res.writeHead(404);
|
|
53
|
+
res.end();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
server.listen(port, () => {
|
|
57
|
+
const authUrl = `${CLIENT_URL}/auth/device?port=${port}`;
|
|
58
|
+
console.log("");
|
|
59
|
+
console.log(" \x1b[1m๐ OhMyPetBook ๋ก๊ทธ์ธ\x1b[0m");
|
|
60
|
+
console.log("");
|
|
61
|
+
console.log(" ๋ธ๋ผ์ฐ์ ์์ ๋ก๊ทธ์ธํ์ธ์:");
|
|
62
|
+
console.log(` \x1b[4m\x1b[36m${authUrl}\x1b[0m`);
|
|
63
|
+
console.log("");
|
|
64
|
+
console.log(` ํ์ธ ์ฝ๋: \x1b[1m\x1b[33m${verifyCode}\x1b[0m`);
|
|
65
|
+
console.log(" (๋ธ๋ผ์ฐ์ ์ ํ์๋๋ ์ฝ๋์ ์ผ์นํ๋์ง ํ์ธํ์ธ์)");
|
|
66
|
+
console.log("");
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const cmd = os.platform() === "darwin" ? "open" : "xdg-open";
|
|
70
|
+
spawn(cmd, [authUrl], { detached: true, stdio: "ignore" }).unref();
|
|
71
|
+
} catch {}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
let si = 0;
|
|
75
|
+
const spinnerInterval = setInterval(() => {
|
|
76
|
+
process.stdout.write(`\r ${SPINNER[si++ % SPINNER.length]} ๋๊ธฐ ์ค...`);
|
|
77
|
+
}, 100);
|
|
78
|
+
|
|
79
|
+
server.on("error", (e) => { clearInterval(spinnerInterval); reject(e); });
|
|
80
|
+
setTimeout(() => {
|
|
81
|
+
clearInterval(spinnerInterval);
|
|
82
|
+
server.close();
|
|
83
|
+
reject(new Error("๋ก๊ทธ์ธ ํ์์์ (5๋ถ)"));
|
|
84
|
+
}, 5 * 60 * 1000);
|
|
85
|
+
});
|
|
86
|
+
}
|
package/lib/service.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import os from "os";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
import { HOME, LOG_FILE } from "./config.js";
|
|
7
|
+
import { ensureDir, log } from "./log.js";
|
|
8
|
+
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
|
|
11
|
+
export function installService() {
|
|
12
|
+
const platform = os.platform();
|
|
13
|
+
const nodeBin = process.execPath;
|
|
14
|
+
const daemonPath = path.resolve(__dirname, "..", "daemon.js");
|
|
15
|
+
|
|
16
|
+
if (platform === "darwin") {
|
|
17
|
+
const label = "com.ohmypetbook.daemon";
|
|
18
|
+
const plistPath = path.join(HOME, "Library/LaunchAgents", `${label}.plist`);
|
|
19
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
20
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
21
|
+
<plist version="1.0">
|
|
22
|
+
<dict>
|
|
23
|
+
<key>Label</key><string>${label}</string>
|
|
24
|
+
<key>ProgramArguments</key>
|
|
25
|
+
<array>
|
|
26
|
+
<string>${nodeBin}</string>
|
|
27
|
+
<string>${daemonPath}</string>
|
|
28
|
+
<string>run</string>
|
|
29
|
+
</array>
|
|
30
|
+
<key>RunAtLoad</key><true/>
|
|
31
|
+
<key>KeepAlive</key><true/>
|
|
32
|
+
<key>StandardOutPath</key><string>${LOG_FILE}</string>
|
|
33
|
+
<key>StandardErrorPath</key><string>${LOG_FILE}</string>
|
|
34
|
+
<key>EnvironmentVariables</key>
|
|
35
|
+
<dict>
|
|
36
|
+
<key>PATH</key><string>/usr/local/bin:/usr/bin:/bin:${path.dirname(nodeBin)}</string>
|
|
37
|
+
<key>HOME</key><string>${HOME}</string>
|
|
38
|
+
</dict>
|
|
39
|
+
</dict>
|
|
40
|
+
</plist>`;
|
|
41
|
+
ensureDir(path.dirname(plistPath));
|
|
42
|
+
fs.writeFileSync(plistPath, plist);
|
|
43
|
+
try { execSync(`launchctl unload ${plistPath} 2>/dev/null`); } catch {}
|
|
44
|
+
execSync(`launchctl load ${plistPath}`);
|
|
45
|
+
log(`โ macOS ์๋น์ค ๋ฑ๋ก: ${label}`);
|
|
46
|
+
|
|
47
|
+
} else if (platform === "linux") {
|
|
48
|
+
const serviceDir = path.join(HOME, ".config/systemd/user");
|
|
49
|
+
const servicePath = path.join(serviceDir, "ohmyohmypetbook-daemon.service");
|
|
50
|
+
const unit = `[Unit]
|
|
51
|
+
Description=PetBook Daemon
|
|
52
|
+
After=network-online.target
|
|
53
|
+
Wants=network-online.target
|
|
54
|
+
[Service]
|
|
55
|
+
Type=simple
|
|
56
|
+
ExecStart=${nodeBin} ${daemonPath} run
|
|
57
|
+
Restart=always
|
|
58
|
+
RestartSec=10
|
|
59
|
+
Environment=HOME=${HOME}
|
|
60
|
+
Environment=PATH=/usr/local/bin:/usr/bin:/bin:${path.dirname(nodeBin)}
|
|
61
|
+
[Install]
|
|
62
|
+
WantedBy=default.target`;
|
|
63
|
+
ensureDir(serviceDir);
|
|
64
|
+
fs.writeFileSync(servicePath, unit);
|
|
65
|
+
execSync("systemctl --user daemon-reload");
|
|
66
|
+
execSync("systemctl --user enable ohmypetbook-daemon");
|
|
67
|
+
execSync("systemctl --user restart ohmypetbook-daemon");
|
|
68
|
+
log("โ Linux ์๋น์ค ๋ฑ๋ก: ohmypetbook-daemon");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function uninstallService() {
|
|
73
|
+
if (os.platform() === "darwin") {
|
|
74
|
+
const p = path.join(HOME, "Library/LaunchAgents/com.ohmypetbook.daemon.plist");
|
|
75
|
+
try { execSync(`launchctl unload ${p}`); } catch {}
|
|
76
|
+
try { fs.unlinkSync(p); } catch {}
|
|
77
|
+
} else if (os.platform() === "linux") {
|
|
78
|
+
try { execSync("systemctl --user stop ohmypetbook-daemon"); } catch {}
|
|
79
|
+
try { execSync("systemctl --user disable ohmypetbook-daemon"); } catch {}
|
|
80
|
+
try { fs.unlinkSync(path.join(HOME, ".config/systemd/user/ohmyohmypetbook-daemon.service")); } catch {}
|
|
81
|
+
try { execSync("systemctl --user daemon-reload"); } catch {}
|
|
82
|
+
}
|
|
83
|
+
}
|
package/lib/sync.js
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { doc, setDoc, updateDoc, onSnapshot, collection, query, where, onSnapshot as onSnapshotCol } from "firebase/firestore";
|
|
2
|
+
import { watch } from "chokidar";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import os from "os";
|
|
6
|
+
import { CONFIG_FILE, CONFIG_DIR, OPENCLAW_HOME, WORKSPACE_DIR, ENCRYPT_SECRET_URL, DECRYPT_SECRETS_URL } from "./config.js";
|
|
7
|
+
import { deviceInfo } from "./auth.js";
|
|
8
|
+
import { ensureDir, log } from "./log.js";
|
|
9
|
+
import { restartGateway } from "./gateway.js";
|
|
10
|
+
|
|
11
|
+
// โโ .env ํ์ผ ๊ด๋ฆฌ โโ
|
|
12
|
+
|
|
13
|
+
const ENV_FILE = path.join(OPENCLAW_HOME, ".env");
|
|
14
|
+
|
|
15
|
+
// ํ๊ฒฝ๋ณ์+์ํฌ๋ฆฟ ๋ก๋ ์ฝ๋ฐฑ (daemon.js์์ ์ค์ , ์ํฌ๋ฆฟ ๋ณตํธํ ํฌํจ)
|
|
16
|
+
let loadEnvSecretsCallback = null;
|
|
17
|
+
export function setLoadEnvSecretsCallback(fn) { loadEnvSecretsCallback = fn; }
|
|
18
|
+
|
|
19
|
+
// idToken ์ฝ๋ฐฑ (daemon.js์์ ์ค์ )
|
|
20
|
+
let getIdTokenCallback = null;
|
|
21
|
+
export function setGetIdTokenCallback(fn) { getIdTokenCallback = fn; }
|
|
22
|
+
|
|
23
|
+
// โโ config ์ํธํ/๋ณตํธํ โโ
|
|
24
|
+
|
|
25
|
+
async function encryptConfig(configObj) {
|
|
26
|
+
if (!getIdTokenCallback) return null;
|
|
27
|
+
try {
|
|
28
|
+
const idToken = await getIdTokenCallback();
|
|
29
|
+
const configStr = JSON.stringify(configObj);
|
|
30
|
+
const resp = await fetch(ENCRYPT_SECRET_URL, {
|
|
31
|
+
method: "POST",
|
|
32
|
+
headers: { "Content-Type": "application/json" },
|
|
33
|
+
body: JSON.stringify({ idToken, value: configStr }),
|
|
34
|
+
});
|
|
35
|
+
if (!resp.ok) throw new Error(`encrypt failed: ${resp.status}`);
|
|
36
|
+
const { encData } = await resp.json();
|
|
37
|
+
return encData;
|
|
38
|
+
} catch (e) {
|
|
39
|
+
log(`โ ๏ธ config ์ํธํ ์คํจ: ${e.message}`);
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function decryptConfig(encData) {
|
|
45
|
+
if (!getIdTokenCallback || !encData) return null;
|
|
46
|
+
try {
|
|
47
|
+
const idToken = await getIdTokenCallback();
|
|
48
|
+
const resp = await fetch(DECRYPT_SECRETS_URL, {
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers: { "Content-Type": "application/json" },
|
|
51
|
+
body: JSON.stringify({ idToken, secrets: { _config: encData } }),
|
|
52
|
+
});
|
|
53
|
+
if (!resp.ok) throw new Error(`decrypt failed: ${resp.status}`);
|
|
54
|
+
const { values } = await resp.json();
|
|
55
|
+
return values._config ? JSON.parse(values._config) : null;
|
|
56
|
+
} catch (e) {
|
|
57
|
+
log(`โ ๏ธ config ๋ณตํธํ ์คํจ: ${e.message}`);
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
let skipLocalWatch = false;
|
|
65
|
+
let skipRemoteWatch = false;
|
|
66
|
+
let lastRemoteHash = "";
|
|
67
|
+
|
|
68
|
+
export function initRemoteHash() {
|
|
69
|
+
lastRemoteHash = JSON.stringify(readConfig());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function readConfig() {
|
|
73
|
+
try { return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8")); } catch { return {}; }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function writeConfig(data) {
|
|
77
|
+
skipLocalWatch = true;
|
|
78
|
+
ensureDir(OPENCLAW_HOME);
|
|
79
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2), "utf-8");
|
|
80
|
+
log("โ openclaw.json ์
๋ฐ์ดํธ");
|
|
81
|
+
setTimeout(() => { skipLocalWatch = false; }, 500);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function readOpenclawDir() {
|
|
85
|
+
ensureDir(CONFIG_DIR);
|
|
86
|
+
const result = {};
|
|
87
|
+
for (const f of fs.readdirSync(CONFIG_DIR)) {
|
|
88
|
+
const fp = path.join(CONFIG_DIR, f);
|
|
89
|
+
if (fs.statSync(fp).isFile()) result[f] = fs.readFileSync(fp, "utf-8");
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function writeOpenclawDir(files) {
|
|
95
|
+
ensureDir(CONFIG_DIR);
|
|
96
|
+
skipLocalWatch = true;
|
|
97
|
+
for (const [name, content] of Object.entries(files)) {
|
|
98
|
+
fs.writeFileSync(path.join(CONFIG_DIR, name), content, "utf-8");
|
|
99
|
+
}
|
|
100
|
+
log(`โ openclaw/ ์
๋ฐ์ดํธ (${Object.keys(files).length} files)`);
|
|
101
|
+
setTimeout(() => { skipLocalWatch = false; }, 500);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// โโ ์ํฌ์คํ์ด์ค ํ์ผ ์ฝ๊ธฐ/์ฐ๊ธฐ โโ
|
|
105
|
+
|
|
106
|
+
const WORKSPACE_FILES = ["AGENTS.md", "SOUL.md", "USER.md", "TOOLS.md", "IDENTITY.md", "HEARTBEAT.md"];
|
|
107
|
+
|
|
108
|
+
function readWorkspace() {
|
|
109
|
+
const result = {};
|
|
110
|
+
for (const name of WORKSPACE_FILES) {
|
|
111
|
+
const fp = path.join(WORKSPACE_DIR, name);
|
|
112
|
+
try { result[name] = fs.readFileSync(fp, "utf-8"); } catch {}
|
|
113
|
+
}
|
|
114
|
+
// memory/ ์ ์ธ (์ฉ๋ ์ด์)
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function writeWorkspace(files) {
|
|
119
|
+
if (!files) return;
|
|
120
|
+
skipLocalWatch = true;
|
|
121
|
+
ensureDir(WORKSPACE_DIR);
|
|
122
|
+
for (const [name, content] of Object.entries(files)) {
|
|
123
|
+
if (WORKSPACE_FILES.includes(name) && typeof content === "string") {
|
|
124
|
+
fs.writeFileSync(path.join(WORKSPACE_DIR, name), content, "utf-8");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
log(`โ workspace ์
๋ฐ์ดํธ`);
|
|
128
|
+
setTimeout(() => { skipLocalWatch = false; }, 500);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// โโ skills ์ ๋ณด ์ฝ๊ธฐ โโ
|
|
132
|
+
|
|
133
|
+
function readSkillsInfo() {
|
|
134
|
+
const config = readConfig();
|
|
135
|
+
const entries = config?.skills?.entries || {};
|
|
136
|
+
const skills = {};
|
|
137
|
+
for (const [name, data] of Object.entries(entries)) {
|
|
138
|
+
skills[name] = typeof data === "object" ? { ...data } : { enabled: true };
|
|
139
|
+
// apiKey ๋ฑ ๋ฏผ๊ฐ์ ๋ณด ๋ง์คํน
|
|
140
|
+
for (const key of Object.keys(skills[name])) {
|
|
141
|
+
if (/key|token|secret|password/i.test(key)) {
|
|
142
|
+
skills[name][key] = "***";
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return skills;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// โโ pet ๋จ์๋ก Firestore ๋๊ธฐํ โโ
|
|
150
|
+
|
|
151
|
+
export async function pushToFirestore(db, uid, petId) {
|
|
152
|
+
if (skipRemoteWatch) return;
|
|
153
|
+
skipRemoteWatch = true;
|
|
154
|
+
try {
|
|
155
|
+
// config ์ํธํ
|
|
156
|
+
const rawConfig = readConfig();
|
|
157
|
+
const encryptedConfig = await encryptConfig(rawConfig);
|
|
158
|
+
|
|
159
|
+
const data = {
|
|
160
|
+
openclawFiles: readOpenclawDir(),
|
|
161
|
+
workspace: readWorkspace(),
|
|
162
|
+
skills: readSkillsInfo(),
|
|
163
|
+
openclawPath: OPENCLAW_HOME,
|
|
164
|
+
updatedAt: new Date().toISOString(),
|
|
165
|
+
lastSeen: new Date().toISOString(),
|
|
166
|
+
status: "online"
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
if (encryptedConfig) {
|
|
170
|
+
data.encryptedConfig = encryptedConfig;
|
|
171
|
+
data.config = null; // ํ๋ฌธ ์ ๊ฑฐ
|
|
172
|
+
} else {
|
|
173
|
+
// ์ํธํ ์คํจ ์ ํด๋ฐฑ (์ต์ด ๋ก๊ทธ์ธ ๋ฑ idToken ์์ ๋)
|
|
174
|
+
data.config = rawConfig;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
await setDoc(doc(db, "users", uid, "pets", petId), data, { merge: true });
|
|
178
|
+
log("โฌ ๋ก์ปฌ โ Firestore ๋๊ธฐํ" + (encryptedConfig ? " (config ์ํธํ)" : ""));
|
|
179
|
+
} catch (e) {
|
|
180
|
+
log(`โ Push ์คํจ: ${e.message}`);
|
|
181
|
+
}
|
|
182
|
+
setTimeout(() => { skipRemoteWatch = false; }, 1000);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// โโ ์ปค๋งจ๋ ๋ฆฌ์ค๋ (์น โ ๋ฐ๋ชฌ) โโ
|
|
186
|
+
|
|
187
|
+
// pushToFirestore ์ฐธ์กฐ (refresh_info์์ ์ฌ์ฉ)
|
|
188
|
+
let _pushRef = null;
|
|
189
|
+
export function setPushRef(fn) { _pushRef = fn; }
|
|
190
|
+
|
|
191
|
+
const COMMAND_HANDLERS = {
|
|
192
|
+
restart_gateway: async () => {
|
|
193
|
+
restartGateway();
|
|
194
|
+
return { message: "๊ฒ์ดํธ์จ์ด ์ฌ์์ ์๋ฃ" };
|
|
195
|
+
},
|
|
196
|
+
refresh_info: async (params, { db, uid, petId }) => {
|
|
197
|
+
const info = deviceInfo();
|
|
198
|
+
const uptime = Math.floor(process.uptime());
|
|
199
|
+
const memTotal = Math.round(os.totalmem() / 1024 / 1024);
|
|
200
|
+
const memFree = Math.round(os.freemem() / 1024 / 1024);
|
|
201
|
+
|
|
202
|
+
// openclaw ๋ฒ์ ์ ๋ณด
|
|
203
|
+
let openclawVersion = '';
|
|
204
|
+
try {
|
|
205
|
+
const { execSync } = await import('child_process');
|
|
206
|
+
const nvmDir = path.join(os.homedir(), '.nvm/versions/node');
|
|
207
|
+
const versions = fs.readdirSync(nvmDir);
|
|
208
|
+
if (versions.length) {
|
|
209
|
+
const bin = path.join(nvmDir, versions[versions.length - 1], 'bin', 'openclaw');
|
|
210
|
+
if (fs.existsSync(bin)) {
|
|
211
|
+
openclawVersion = execSync(`${bin} --version`, { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
} catch {}
|
|
215
|
+
|
|
216
|
+
const data = {
|
|
217
|
+
...info,
|
|
218
|
+
uptime,
|
|
219
|
+
memTotal,
|
|
220
|
+
memFree,
|
|
221
|
+
cpus: os.cpus().length,
|
|
222
|
+
cpuModel: os.cpus()[0]?.model || '',
|
|
223
|
+
osRelease: os.release(),
|
|
224
|
+
openclawVersion,
|
|
225
|
+
lastSeen: new Date().toISOString(),
|
|
226
|
+
status: "online",
|
|
227
|
+
};
|
|
228
|
+
await setDoc(doc(db, "users", uid, "pets", petId), data, { merge: true });
|
|
229
|
+
if (_pushRef) await _pushRef();
|
|
230
|
+
return { message: "๋๋ฐ์ด์ค ์ ๋ณด ์
๋ฐ์ดํธ ์๋ฃ", ...data };
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
function listenCommands(db, uid, petId) {
|
|
235
|
+
const commandsCol = collection(db, "users", uid, "pets", petId, "commands");
|
|
236
|
+
const q = query(commandsCol, where("status", "==", "pending"));
|
|
237
|
+
|
|
238
|
+
return onSnapshot(q, async (snapshot) => {
|
|
239
|
+
for (const change of snapshot.docChanges()) {
|
|
240
|
+
if (change.type !== "added") continue;
|
|
241
|
+
const cmdDoc = change.doc;
|
|
242
|
+
const cmd = cmdDoc.data();
|
|
243
|
+
|
|
244
|
+
log(`๐จ ์ปค๋งจ๋ ์์ : ${cmd.action}`);
|
|
245
|
+
|
|
246
|
+
const handler = COMMAND_HANDLERS[cmd.action];
|
|
247
|
+
if (!handler) {
|
|
248
|
+
await updateDoc(cmdDoc.ref, {
|
|
249
|
+
status: "error",
|
|
250
|
+
error: `์ ์ ์๋ ์ปค๋งจ๋: ${cmd.action}`,
|
|
251
|
+
completedAt: new Date().toISOString()
|
|
252
|
+
});
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
await updateDoc(cmdDoc.ref, { status: "running" });
|
|
258
|
+
const result = await handler(cmd.params || {}, { db, uid, petId });
|
|
259
|
+
await updateDoc(cmdDoc.ref, {
|
|
260
|
+
status: "done",
|
|
261
|
+
result: result || {},
|
|
262
|
+
completedAt: new Date().toISOString()
|
|
263
|
+
});
|
|
264
|
+
log(`โ ์ปค๋งจ๋ ์๋ฃ: ${cmd.action}`);
|
|
265
|
+
} catch (e) {
|
|
266
|
+
await updateDoc(cmdDoc.ref, {
|
|
267
|
+
status: "error",
|
|
268
|
+
error: e.message,
|
|
269
|
+
completedAt: new Date().toISOString()
|
|
270
|
+
});
|
|
271
|
+
log(`โ ์ปค๋งจ๋ ์คํจ: ${cmd.action} โ ${e.message}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function listenFirestore(db, uid, petId) {
|
|
278
|
+
// pet config ๋ณ๊ฒฝ ๋ฆฌ์ค๋
|
|
279
|
+
const unsubPet = onSnapshot(doc(db, "users", uid, "pets", petId), async (snap) => {
|
|
280
|
+
if (!snap.exists() || skipRemoteWatch) return;
|
|
281
|
+
const data = snap.data();
|
|
282
|
+
|
|
283
|
+
// ํ๊ธฐ ๊ฐ์
|
|
284
|
+
if (data.revoked) {
|
|
285
|
+
log("๐ซ ์ด pet์ด ํ๊ธฐ๋์์ต๋๋ค. ๋ฐ๋ชฌ ์ข
๋ฃ.");
|
|
286
|
+
process.exit(0);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// config ๋ณตํธํ (์ํธํ๋ ๊ฒฝ์ฐ)
|
|
290
|
+
let config = data.config;
|
|
291
|
+
if (data.encryptedConfig) {
|
|
292
|
+
const decrypted = await decryptConfig(data.encryptedConfig);
|
|
293
|
+
if (decrypted) {
|
|
294
|
+
config = decrypted;
|
|
295
|
+
log("๐ config ๋ณตํธํ ์๋ฃ");
|
|
296
|
+
} else {
|
|
297
|
+
log("โ ๏ธ config ๋ณตํธํ ์คํจ, ๋ก์ปฌ ๋ณ๊ฒฝ ๊ฑด๋๋");
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// config ๋ณ๊ฒฝ ๊ฐ์ง
|
|
302
|
+
const hash = JSON.stringify(config || {});
|
|
303
|
+
const configChanged = hash !== lastRemoteHash && lastRemoteHash !== "";
|
|
304
|
+
lastRemoteHash = hash;
|
|
305
|
+
|
|
306
|
+
if (config) writeConfig(config);
|
|
307
|
+
if (data.openclawFiles) writeOpenclawDir(data.openclawFiles);
|
|
308
|
+
if (data.workspace) writeWorkspace(data.workspace);
|
|
309
|
+
log("โฌ Firestore โ ๋ก์ปฌ ๋๊ธฐํ");
|
|
310
|
+
|
|
311
|
+
// ํ๊ฒฝ๋ณ์/์ํฌ๋ฆฟ ๋ณ๊ฒฝ ์ โ ๋ณตํธํ ํฌํจ .env ์ฐ๊ธฐ + ๊ฒ์ดํธ์จ์ด ์ฌ์์
|
|
312
|
+
const hasEnvChange = data.deviceEnvVars || data.deviceSecrets;
|
|
313
|
+
if (hasEnvChange && loadEnvSecretsCallback) {
|
|
314
|
+
loadEnvSecretsCallback().then(() => restartGateway()).catch((e) => {
|
|
315
|
+
log(`โ ๏ธ ํ๊ฒฝ๋ณ์ ๋ก๋ ์คํจ: ${e.message}`);
|
|
316
|
+
restartGateway();
|
|
317
|
+
});
|
|
318
|
+
} else if (configChanged) {
|
|
319
|
+
restartGateway();
|
|
320
|
+
}
|
|
321
|
+
}, (error) => {
|
|
322
|
+
log(`โ Firestore ๋ฆฌ์ค๋ ์๋ฌ: ${error.message}`);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// ์ปค๋งจ๋ ๋ฆฌ์ค๋
|
|
326
|
+
const unsubCmd = listenCommands(db, uid, petId);
|
|
327
|
+
|
|
328
|
+
return () => { unsubPet(); unsubCmd(); };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function watchLocal(db, uid, petId) {
|
|
332
|
+
const targets = [CONFIG_FILE];
|
|
333
|
+
if (fs.existsSync(CONFIG_DIR)) targets.push(CONFIG_DIR);
|
|
334
|
+
if (fs.existsSync(WORKSPACE_DIR)) {
|
|
335
|
+
// ์ํฌ์คํ์ด์ค .md ํ์ผ๋ค๋ง ๊ฐ์
|
|
336
|
+
for (const f of WORKSPACE_FILES) {
|
|
337
|
+
const fp = path.join(WORKSPACE_DIR, f);
|
|
338
|
+
if (fs.existsSync(fp)) targets.push(fp);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const watcher = watch(targets, {
|
|
342
|
+
ignoreInitial: true,
|
|
343
|
+
awaitWriteFinish: { stabilityThreshold: 300 }
|
|
344
|
+
});
|
|
345
|
+
const handler = () => {
|
|
346
|
+
if (skipLocalWatch) return;
|
|
347
|
+
log("๐ ๋ก์ปฌ ๋ณ๊ฒฝ ๊ฐ์ง");
|
|
348
|
+
pushToFirestore(db, uid, petId);
|
|
349
|
+
};
|
|
350
|
+
watcher.on("change", handler).on("add", handler);
|
|
351
|
+
return watcher;
|
|
352
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ohmypetbook",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "OhMyPetBook daemon โ OpenClaw device sync agent",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "daemon.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"ohmypetbook": "./daemon.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node daemon.js run",
|
|
12
|
+
"login": "node daemon.js login"
|
|
13
|
+
},
|
|
14
|
+
"keywords": ["openclaw", "ohmypetbook", "daemon", "iot", "device-sync"],
|
|
15
|
+
"author": "bdhwan",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/bdhwan/ohmypetbook-daemon.git"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://ohmypetbook.com",
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"daemon.js",
|
|
27
|
+
"lib/",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"chokidar": "^4.0.3",
|
|
33
|
+
"firebase": "^11.4.0"
|
|
34
|
+
}
|
|
35
|
+
}
|