@bamdra/bamdra-user-bind 0.1.1 → 0.1.2
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 +24 -1
- package/README.zh-CN.md +24 -1
- package/dist/index.js +448 -44
- package/openclaw.plugin.json +1 -0
- package/package.json +7 -5
- package/skills/bamdra-user-bind-admin/SKILL.md +43 -0
- package/skills/bamdra-user-bind-profile/SKILL.md +54 -0
package/README.md
CHANGED
|
@@ -30,10 +30,24 @@ The file count is intentionally small because this first public version is shipp
|
|
|
30
30
|
|
|
31
31
|
- runtime primary store:
|
|
32
32
|
`~/.openclaw/data/bamdra-user-bind/profiles.sqlite`
|
|
33
|
+
- editable per-user Markdown mirror:
|
|
34
|
+
`~/.openclaw/data/bamdra-user-bind/profiles/private/{userId}.md`
|
|
33
35
|
- export directory:
|
|
34
36
|
`~/.openclaw/data/bamdra-user-bind/exports/`
|
|
35
37
|
|
|
36
|
-
The runtime
|
|
38
|
+
The runtime queries the SQLite store as the controlled source of truth. The Markdown mirror exists so humans can edit a per-user profile the way they would edit a `USER.md`-style file, without turning the whole directory into an unrestricted agent-readable knowledge base.
|
|
39
|
+
|
|
40
|
+
The Markdown mirror root is configurable through `profileMarkdownRoot`.
|
|
41
|
+
|
|
42
|
+
## Default Profile Starter
|
|
43
|
+
|
|
44
|
+
New profile mirrors start with a practical template, including example defaults such as:
|
|
45
|
+
|
|
46
|
+
- preferred address: `老板`
|
|
47
|
+
- timezone: `Asia/Shanghai`
|
|
48
|
+
- preference: `幽默诙谐的对话风格,但是不过分`
|
|
49
|
+
|
|
50
|
+
Users can edit that Markdown directly and the plugin will sync the changes back into the controlled store.
|
|
37
51
|
|
|
38
52
|
## Security Boundary
|
|
39
53
|
|
|
@@ -51,6 +65,15 @@ The runtime only queries the SQLite store. Export files exist for backup and man
|
|
|
51
65
|
- optional companion:
|
|
52
66
|
`bamdra-memory-vector`
|
|
53
67
|
|
|
68
|
+
## Bundled Skills
|
|
69
|
+
|
|
70
|
+
This package now ships standalone skills under `skills/`:
|
|
71
|
+
|
|
72
|
+
- `bamdra-user-bind-profile`
|
|
73
|
+
- `bamdra-user-bind-admin`
|
|
74
|
+
|
|
75
|
+
When installed into OpenClaw, bootstrap can materialize these into `~/.openclaw/skills/` and attach them automatically.
|
|
76
|
+
|
|
54
77
|
## Build
|
|
55
78
|
|
|
56
79
|
```bash
|
package/README.zh-CN.md
CHANGED
|
@@ -28,10 +28,24 @@
|
|
|
28
28
|
|
|
29
29
|
- 运行时主存储:
|
|
30
30
|
`~/.openclaw/data/bamdra-user-bind/profiles.sqlite`
|
|
31
|
+
- 可编辑的用户画像 Markdown 镜像:
|
|
32
|
+
`~/.openclaw/data/bamdra-user-bind/profiles/private/{userId}.md`
|
|
31
33
|
- 导出目录:
|
|
32
34
|
`~/.openclaw/data/bamdra-user-bind/exports/`
|
|
33
35
|
|
|
34
|
-
运行时只查询 SQLite
|
|
36
|
+
运行时只查询 SQLite 主库,Markdown 镜像是给人维护的用户画像层,导出文件则用于备份和人工查看。
|
|
37
|
+
|
|
38
|
+
`profileMarkdownRoot` 可以改成你自己的目录,例如 Obsidian 仓库中的私有画像目录。
|
|
39
|
+
|
|
40
|
+
## 默认画像模板
|
|
41
|
+
|
|
42
|
+
新画像文件会带一个起步模板,默认示例包括:
|
|
43
|
+
|
|
44
|
+
- 建议称呼:`老板`
|
|
45
|
+
- 时区:`Asia/Shanghai`
|
|
46
|
+
- 偏好:`幽默诙谐的对话风格,但是不过分`
|
|
47
|
+
|
|
48
|
+
用户可以直接编辑这份 Markdown,插件会把改动同步回受控存储。
|
|
35
49
|
|
|
36
50
|
## 安全边界
|
|
37
51
|
|
|
@@ -49,6 +63,15 @@
|
|
|
49
63
|
- 可选配套:
|
|
50
64
|
`bamdra-memory-vector`
|
|
51
65
|
|
|
66
|
+
## 随包 Skill
|
|
67
|
+
|
|
68
|
+
这个包会附带独立 skill:
|
|
69
|
+
|
|
70
|
+
- `bamdra-user-bind-profile`
|
|
71
|
+
- `bamdra-user-bind-admin`
|
|
72
|
+
|
|
73
|
+
安装到 OpenClaw 后,bootstrap 可以把它们复制到 `~/.openclaw/skills/` 并自动挂到合适的 agent。
|
|
74
|
+
|
|
52
75
|
## 构建
|
|
53
76
|
|
|
54
77
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -25,12 +25,34 @@ __export(index_exports, {
|
|
|
25
25
|
register: () => register
|
|
26
26
|
});
|
|
27
27
|
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
|
|
29
|
+
// ../node_modules/.pnpm/tsup@8.5.1_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js
|
|
30
|
+
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
31
|
+
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
32
|
+
|
|
33
|
+
// src/index.ts
|
|
28
34
|
var import_node_crypto = require("crypto");
|
|
29
35
|
var import_node_fs = require("fs");
|
|
30
36
|
var import_node_os = require("os");
|
|
31
37
|
var import_node_path = require("path");
|
|
32
38
|
var import_node_sqlite = require("node:sqlite");
|
|
39
|
+
var import_node_url = require("url");
|
|
40
|
+
var PLUGIN_ID = "bamdra-user-bind";
|
|
33
41
|
var GLOBAL_API_KEY = "__OPENCLAW_BAMDRA_USER_BIND__";
|
|
42
|
+
var PROFILE_SKILL_ID = "bamdra-user-bind-profile";
|
|
43
|
+
var ADMIN_SKILL_ID = "bamdra-user-bind-admin";
|
|
44
|
+
var SELF_TOOL_NAMES = [
|
|
45
|
+
"user_bind_get_my_profile",
|
|
46
|
+
"user_bind_update_my_profile",
|
|
47
|
+
"user_bind_refresh_my_binding"
|
|
48
|
+
];
|
|
49
|
+
var ADMIN_TOOL_NAMES = [
|
|
50
|
+
"user_bind_admin_query",
|
|
51
|
+
"user_bind_admin_edit",
|
|
52
|
+
"user_bind_admin_merge",
|
|
53
|
+
"user_bind_admin_list_issues",
|
|
54
|
+
"user_bind_admin_sync"
|
|
55
|
+
];
|
|
34
56
|
var TABLES = {
|
|
35
57
|
profiles: "bamdra_user_bind_profiles",
|
|
36
58
|
bindings: "bamdra_user_bind_bindings",
|
|
@@ -38,11 +60,13 @@ var TABLES = {
|
|
|
38
60
|
audits: "bamdra_user_bind_audits"
|
|
39
61
|
};
|
|
40
62
|
var UserBindStore = class {
|
|
41
|
-
constructor(dbPath, exportPath) {
|
|
63
|
+
constructor(dbPath, exportPath, profileMarkdownRoot) {
|
|
42
64
|
this.dbPath = dbPath;
|
|
43
65
|
this.exportPath = exportPath;
|
|
66
|
+
this.profileMarkdownRoot = profileMarkdownRoot;
|
|
44
67
|
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(dbPath), { recursive: true });
|
|
45
68
|
(0, import_node_fs.mkdirSync)(exportPath, { recursive: true });
|
|
69
|
+
(0, import_node_fs.mkdirSync)(profileMarkdownRoot, { recursive: true });
|
|
46
70
|
this.db = new import_node_sqlite.DatabaseSync(dbPath);
|
|
47
71
|
this.db.exec(`
|
|
48
72
|
CREATE TABLE IF NOT EXISTS ${TABLES.profiles} (
|
|
@@ -55,6 +79,8 @@ var UserBindStore = class {
|
|
|
55
79
|
preferences TEXT,
|
|
56
80
|
personality TEXT,
|
|
57
81
|
role TEXT,
|
|
82
|
+
timezone TEXT,
|
|
83
|
+
notes TEXT,
|
|
58
84
|
visibility TEXT NOT NULL DEFAULT 'private',
|
|
59
85
|
source TEXT NOT NULL,
|
|
60
86
|
updated_at TEXT NOT NULL
|
|
@@ -91,17 +117,17 @@ var UserBindStore = class {
|
|
|
91
117
|
changed_fields TEXT NOT NULL
|
|
92
118
|
);
|
|
93
119
|
`);
|
|
120
|
+
this.ensureColumn(TABLES.profiles, "timezone", "TEXT");
|
|
121
|
+
this.ensureColumn(TABLES.profiles, "notes", "TEXT");
|
|
94
122
|
}
|
|
95
123
|
db;
|
|
124
|
+
markdownSyncing = /* @__PURE__ */ new Set();
|
|
96
125
|
close() {
|
|
97
126
|
this.db.close();
|
|
98
127
|
}
|
|
99
128
|
getProfile(userId) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
FROM ${TABLES.profiles} WHERE user_id = ?
|
|
103
|
-
`).get(userId);
|
|
104
|
-
return row ? mapProfileRow(row) : null;
|
|
129
|
+
this.syncMarkdownToStore(userId);
|
|
130
|
+
return this.getProfileFromDatabase(userId);
|
|
105
131
|
}
|
|
106
132
|
findBinding(channelType, openId) {
|
|
107
133
|
if (!openId) {
|
|
@@ -153,25 +179,30 @@ var UserBindStore = class {
|
|
|
153
179
|
);
|
|
154
180
|
}
|
|
155
181
|
upsertIdentity(args) {
|
|
182
|
+
if (!this.markdownSyncing.has(args.userId)) {
|
|
183
|
+
this.syncMarkdownToStore(args.userId);
|
|
184
|
+
}
|
|
156
185
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
157
|
-
const current = this.
|
|
186
|
+
const current = this.getProfileFromDatabase(args.userId);
|
|
158
187
|
const next = {
|
|
159
188
|
userId: args.userId,
|
|
160
189
|
name: args.profilePatch.name ?? current?.name ?? null,
|
|
161
190
|
gender: args.profilePatch.gender ?? current?.gender ?? null,
|
|
162
191
|
email: args.profilePatch.email ?? current?.email ?? null,
|
|
163
192
|
avatar: args.profilePatch.avatar ?? current?.avatar ?? null,
|
|
164
|
-
nickname: args.profilePatch.nickname ?? current?.nickname ??
|
|
165
|
-
preferences: args.profilePatch.preferences ?? current?.preferences ??
|
|
193
|
+
nickname: args.profilePatch.nickname ?? current?.nickname ?? "\u8001\u677F",
|
|
194
|
+
preferences: args.profilePatch.preferences ?? current?.preferences ?? "\u5E7D\u9ED8\u8BD9\u8C10\u7684\u5BF9\u8BDD\u98CE\u683C\uFF0C\u4F46\u662F\u4E0D\u8FC7\u5206",
|
|
166
195
|
personality: args.profilePatch.personality ?? current?.personality ?? null,
|
|
167
196
|
role: args.profilePatch.role ?? current?.role ?? null,
|
|
197
|
+
timezone: args.profilePatch.timezone ?? current?.timezone ?? "Asia/Shanghai",
|
|
198
|
+
notes: args.profilePatch.notes ?? current?.notes ?? defaultProfileNotes(),
|
|
168
199
|
visibility: args.profilePatch.visibility ?? current?.visibility ?? "private",
|
|
169
200
|
source: args.source,
|
|
170
201
|
updatedAt: now
|
|
171
202
|
};
|
|
172
203
|
this.db.prepare(`
|
|
173
|
-
INSERT INTO ${TABLES.profiles} (user_id, name, gender, email, avatar, nickname, preferences, personality, role, visibility, source, updated_at)
|
|
174
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
204
|
+
INSERT INTO ${TABLES.profiles} (user_id, name, gender, email, avatar, nickname, preferences, personality, role, timezone, notes, visibility, source, updated_at)
|
|
205
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
175
206
|
ON CONFLICT(user_id) DO UPDATE SET
|
|
176
207
|
name = excluded.name,
|
|
177
208
|
gender = excluded.gender,
|
|
@@ -181,6 +212,8 @@ var UserBindStore = class {
|
|
|
181
212
|
preferences = excluded.preferences,
|
|
182
213
|
personality = excluded.personality,
|
|
183
214
|
role = excluded.role,
|
|
215
|
+
timezone = excluded.timezone,
|
|
216
|
+
notes = excluded.notes,
|
|
184
217
|
visibility = excluded.visibility,
|
|
185
218
|
source = excluded.source,
|
|
186
219
|
updated_at = excluded.updated_at
|
|
@@ -194,28 +227,35 @@ var UserBindStore = class {
|
|
|
194
227
|
next.preferences,
|
|
195
228
|
next.personality,
|
|
196
229
|
next.role,
|
|
230
|
+
next.timezone,
|
|
231
|
+
next.notes,
|
|
197
232
|
next.visibility,
|
|
198
233
|
next.source,
|
|
199
234
|
next.updatedAt
|
|
200
235
|
);
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
236
|
+
if (args.channelType !== "manual" || args.openId) {
|
|
237
|
+
const bindingId = hashId(`${args.channelType}:${args.openId ?? args.userId}`);
|
|
238
|
+
this.db.prepare(`
|
|
239
|
+
INSERT INTO ${TABLES.bindings} (binding_id, user_id, channel_type, open_id, external_user_id, union_id, source, updated_at)
|
|
240
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
241
|
+
ON CONFLICT(binding_id) DO UPDATE SET
|
|
242
|
+
user_id = excluded.user_id,
|
|
243
|
+
channel_type = excluded.channel_type,
|
|
244
|
+
open_id = excluded.open_id,
|
|
245
|
+
source = excluded.source,
|
|
246
|
+
updated_at = excluded.updated_at
|
|
247
|
+
`).run(
|
|
248
|
+
bindingId,
|
|
249
|
+
args.userId,
|
|
250
|
+
args.channelType,
|
|
251
|
+
args.openId,
|
|
252
|
+
args.userId,
|
|
253
|
+
null,
|
|
254
|
+
args.source,
|
|
255
|
+
now
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
this.writeProfileMarkdown(next);
|
|
219
259
|
this.writeExports();
|
|
220
260
|
return next;
|
|
221
261
|
}
|
|
@@ -255,6 +295,8 @@ var UserBindStore = class {
|
|
|
255
295
|
preferences: into.preferences ?? from.preferences,
|
|
256
296
|
personality: into.personality ?? from.personality,
|
|
257
297
|
role: into.role ?? from.role,
|
|
298
|
+
timezone: into.timezone ?? from.timezone,
|
|
299
|
+
notes: joinNotes(into.notes, from.notes),
|
|
258
300
|
visibility: into.visibility
|
|
259
301
|
}
|
|
260
302
|
});
|
|
@@ -267,9 +309,76 @@ var UserBindStore = class {
|
|
|
267
309
|
this.writeExports();
|
|
268
310
|
return merged;
|
|
269
311
|
}
|
|
312
|
+
getProfileFromDatabase(userId) {
|
|
313
|
+
const row = this.db.prepare(`
|
|
314
|
+
SELECT user_id, name, gender, email, avatar, nickname, preferences, personality, role, timezone, notes, visibility, source, updated_at
|
|
315
|
+
FROM ${TABLES.profiles} WHERE user_id = ?
|
|
316
|
+
`).get(userId);
|
|
317
|
+
return row ? mapProfileRow(row) : null;
|
|
318
|
+
}
|
|
319
|
+
profileMarkdownPath(userId) {
|
|
320
|
+
return (0, import_node_path.join)(this.profileMarkdownRoot, `${sanitizeFilename(userId)}.md`);
|
|
321
|
+
}
|
|
322
|
+
syncMarkdownToStore(userId) {
|
|
323
|
+
if (this.markdownSyncing.has(userId)) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
this.markdownSyncing.add(userId);
|
|
327
|
+
try {
|
|
328
|
+
const markdownPath = this.profileMarkdownPath(userId);
|
|
329
|
+
if (!(0, import_node_fs.existsSync)(markdownPath)) {
|
|
330
|
+
const current2 = this.getProfileFromDatabase(userId);
|
|
331
|
+
if (current2) {
|
|
332
|
+
this.writeProfileMarkdown(current2);
|
|
333
|
+
}
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const current = this.getProfileFromDatabase(userId);
|
|
337
|
+
const parsed = parseProfileMarkdown((0, import_node_fs.readFileSync)(markdownPath, "utf8"));
|
|
338
|
+
const markdownMtime = (0, import_node_fs.statSync)(markdownPath).mtime.toISOString();
|
|
339
|
+
const dbTime = current?.updatedAt ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
340
|
+
const patch = {
|
|
341
|
+
...parsed.profilePatch,
|
|
342
|
+
notes: parsed.notes
|
|
343
|
+
};
|
|
344
|
+
if (!current) {
|
|
345
|
+
this.upsertIdentity({
|
|
346
|
+
userId,
|
|
347
|
+
channelType: "manual",
|
|
348
|
+
openId: null,
|
|
349
|
+
source: "markdown-profile",
|
|
350
|
+
profilePatch: {
|
|
351
|
+
...patch,
|
|
352
|
+
visibility: patch.visibility ?? "private"
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (markdownMtime <= dbTime && !hasProfileDifference(current, patch)) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
this.upsertIdentity({
|
|
361
|
+
userId,
|
|
362
|
+
channelType: "manual",
|
|
363
|
+
openId: null,
|
|
364
|
+
source: "markdown-profile",
|
|
365
|
+
profilePatch: {
|
|
366
|
+
...current,
|
|
367
|
+
...patch
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
} finally {
|
|
371
|
+
this.markdownSyncing.delete(userId);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
writeProfileMarkdown(profile) {
|
|
375
|
+
const markdownPath = this.profileMarkdownPath(profile.userId);
|
|
376
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(markdownPath), { recursive: true });
|
|
377
|
+
(0, import_node_fs.writeFileSync)(markdownPath, renderProfileMarkdown(profile), "utf8");
|
|
378
|
+
}
|
|
270
379
|
writeExports() {
|
|
271
380
|
const profiles = this.db.prepare(`
|
|
272
|
-
SELECT user_id, name, gender, email, avatar, nickname, preferences, personality, role, visibility, source, updated_at
|
|
381
|
+
SELECT user_id, name, gender, email, avatar, nickname, preferences, personality, role, timezone, notes, visibility, source, updated_at
|
|
273
382
|
FROM ${TABLES.profiles}
|
|
274
383
|
ORDER BY updated_at DESC
|
|
275
384
|
`).all();
|
|
@@ -281,6 +390,13 @@ var UserBindStore = class {
|
|
|
281
390
|
(0, import_node_fs.writeFileSync)((0, import_node_path.join)(this.exportPath, "users.yaml"), renderYamlList(profiles), "utf8");
|
|
282
391
|
(0, import_node_fs.writeFileSync)((0, import_node_path.join)(this.exportPath, "bindings.yaml"), renderYamlList(bindings), "utf8");
|
|
283
392
|
}
|
|
393
|
+
ensureColumn(tableName, columnName, definition) {
|
|
394
|
+
const rows = this.db.prepare(`PRAGMA table_info(${tableName})`).all();
|
|
395
|
+
if (rows.some((row) => String(row.name) === columnName)) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
this.db.exec(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${definition}`);
|
|
399
|
+
}
|
|
284
400
|
};
|
|
285
401
|
var UserBindRuntime = class {
|
|
286
402
|
constructor(host, inputConfig) {
|
|
@@ -288,7 +404,8 @@ var UserBindRuntime = class {
|
|
|
288
404
|
this.config = normalizeConfig(inputConfig);
|
|
289
405
|
this.store = new UserBindStore(
|
|
290
406
|
(0, import_node_path.join)(this.config.localStorePath, "profiles.sqlite"),
|
|
291
|
-
this.config.exportPath
|
|
407
|
+
this.config.exportPath,
|
|
408
|
+
this.config.profileMarkdownRoot
|
|
292
409
|
);
|
|
293
410
|
}
|
|
294
411
|
store;
|
|
@@ -299,6 +416,12 @@ var UserBindRuntime = class {
|
|
|
299
416
|
this.store.close();
|
|
300
417
|
}
|
|
301
418
|
register() {
|
|
419
|
+
queueMicrotask(() => {
|
|
420
|
+
try {
|
|
421
|
+
bootstrapOpenClawHost(this.config);
|
|
422
|
+
} catch {
|
|
423
|
+
}
|
|
424
|
+
});
|
|
302
425
|
this.registerHooks();
|
|
303
426
|
this.registerTools();
|
|
304
427
|
exposeGlobalApi(this);
|
|
@@ -509,7 +632,9 @@ var UserBindRuntime = class {
|
|
|
509
632
|
nickname: { type: "string" },
|
|
510
633
|
preferences: { type: "string" },
|
|
511
634
|
personality: { type: "string" },
|
|
512
|
-
role: { type: "string" }
|
|
635
|
+
role: { type: "string" },
|
|
636
|
+
timezone: { type: "string" },
|
|
637
|
+
notes: { type: "string" }
|
|
513
638
|
}
|
|
514
639
|
},
|
|
515
640
|
execute: async (_id, params) => asTextResult(await this.updateMyProfile(params, sanitizeProfilePatch(params)))
|
|
@@ -526,13 +651,7 @@ var UserBindRuntime = class {
|
|
|
526
651
|
},
|
|
527
652
|
execute: async (_id, params) => asTextResult(await this.refreshMyBinding(params))
|
|
528
653
|
});
|
|
529
|
-
for (const toolName of
|
|
530
|
-
"user_bind_admin_query",
|
|
531
|
-
"user_bind_admin_edit",
|
|
532
|
-
"user_bind_admin_merge",
|
|
533
|
-
"user_bind_admin_list_issues",
|
|
534
|
-
"user_bind_admin_sync"
|
|
535
|
-
]) {
|
|
654
|
+
for (const toolName of ADMIN_TOOL_NAMES) {
|
|
536
655
|
registerTool({
|
|
537
656
|
name: toolName,
|
|
538
657
|
description: `Administrative natural-language tool for ${toolName.replace("user_bind_admin_", "")}`,
|
|
@@ -590,12 +709,14 @@ async function activate(api) {
|
|
|
590
709
|
}
|
|
591
710
|
function normalizeConfig(input) {
|
|
592
711
|
const root = (0, import_node_path.join)((0, import_node_os.homedir)(), ".openclaw", "data", "bamdra-user-bind");
|
|
712
|
+
const storeRoot = input?.localStorePath ?? root;
|
|
593
713
|
return {
|
|
594
714
|
enabled: input?.enabled ?? true,
|
|
595
|
-
localStorePath:
|
|
596
|
-
exportPath: input?.exportPath ?? (0, import_node_path.join)(
|
|
715
|
+
localStorePath: storeRoot,
|
|
716
|
+
exportPath: input?.exportPath ?? (0, import_node_path.join)(storeRoot, "exports"),
|
|
717
|
+
profileMarkdownRoot: input?.profileMarkdownRoot ?? (0, import_node_path.join)(storeRoot, "profiles", "private"),
|
|
597
718
|
cacheTtlMs: input?.cacheTtlMs ?? 30 * 60 * 1e3,
|
|
598
|
-
adminAgents: input?.adminAgents
|
|
719
|
+
adminAgents: input?.adminAgents?.length ? input.adminAgents : ["main"]
|
|
599
720
|
};
|
|
600
721
|
}
|
|
601
722
|
function exposeGlobalApi(runtime) {
|
|
@@ -608,6 +729,142 @@ function exposeGlobalApi(runtime) {
|
|
|
608
729
|
}
|
|
609
730
|
};
|
|
610
731
|
}
|
|
732
|
+
function bootstrapOpenClawHost(config) {
|
|
733
|
+
const currentFile = (0, import_node_url.fileURLToPath)(importMetaUrl);
|
|
734
|
+
const runtimeDir = (0, import_node_path.dirname)(currentFile);
|
|
735
|
+
const packageRoot = (0, import_node_path.resolve)(runtimeDir, "..");
|
|
736
|
+
const openclawHome = (0, import_node_path.resolve)((0, import_node_os.homedir)(), ".openclaw");
|
|
737
|
+
const configPath = (0, import_node_path.join)(openclawHome, "openclaw.json");
|
|
738
|
+
const extensionRoot = (0, import_node_path.join)(openclawHome, "extensions");
|
|
739
|
+
const globalSkillsDir = (0, import_node_path.join)(openclawHome, "skills");
|
|
740
|
+
const profileSkillSource = (0, import_node_path.join)(packageRoot, "skills", PROFILE_SKILL_ID);
|
|
741
|
+
const adminSkillSource = (0, import_node_path.join)(packageRoot, "skills", ADMIN_SKILL_ID);
|
|
742
|
+
const profileSkillTarget = (0, import_node_path.join)(globalSkillsDir, PROFILE_SKILL_ID);
|
|
743
|
+
const adminSkillTarget = (0, import_node_path.join)(globalSkillsDir, ADMIN_SKILL_ID);
|
|
744
|
+
if (!runtimeDir.startsWith(extensionRoot) || !(0, import_node_fs.existsSync)(configPath)) {
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
(0, import_node_fs.mkdirSync)(globalSkillsDir, { recursive: true });
|
|
748
|
+
materializeBundledSkill(profileSkillSource, profileSkillTarget);
|
|
749
|
+
materializeBundledSkill(adminSkillSource, adminSkillTarget);
|
|
750
|
+
const original = (0, import_node_fs.readFileSync)(configPath, "utf8");
|
|
751
|
+
const parsed = JSON.parse(original);
|
|
752
|
+
const changed = ensureHostConfig(parsed, config, profileSkillTarget, adminSkillTarget);
|
|
753
|
+
if (!changed) {
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
(0, import_node_fs.writeFileSync)(configPath, `${JSON.stringify(parsed, null, 2)}
|
|
757
|
+
`, "utf8");
|
|
758
|
+
}
|
|
759
|
+
function ensureHostConfig(config, pluginConfig, profileSkillTarget, adminSkillTarget) {
|
|
760
|
+
let changed = false;
|
|
761
|
+
const plugins = ensureObject(config, "plugins");
|
|
762
|
+
const entries = ensureObject(plugins, "entries");
|
|
763
|
+
const load = ensureObject(plugins, "load");
|
|
764
|
+
const tools = ensureObject(config, "tools");
|
|
765
|
+
const skills = ensureObject(config, "skills");
|
|
766
|
+
const skillsLoad = ensureObject(skills, "load");
|
|
767
|
+
const agents = ensureObject(config, "agents");
|
|
768
|
+
const entry = ensureObject(entries, PLUGIN_ID);
|
|
769
|
+
const entryConfig = ensureObject(entry, "config");
|
|
770
|
+
changed = ensureArrayIncludes(plugins, "allow", PLUGIN_ID) || changed;
|
|
771
|
+
changed = ensureArrayIncludes(load, "paths", (0, import_node_path.join)((0, import_node_os.homedir)(), ".openclaw", "extensions")) || changed;
|
|
772
|
+
changed = ensureArrayIncludes(skillsLoad, "extraDirs", (0, import_node_path.join)((0, import_node_os.homedir)(), ".openclaw", "skills")) || changed;
|
|
773
|
+
if (entry.enabled !== true) {
|
|
774
|
+
entry.enabled = true;
|
|
775
|
+
changed = true;
|
|
776
|
+
}
|
|
777
|
+
changed = ensureToolNames(tools, [...SELF_TOOL_NAMES, ...ADMIN_TOOL_NAMES]) || changed;
|
|
778
|
+
if (entryConfig.enabled !== true) {
|
|
779
|
+
entryConfig.enabled = true;
|
|
780
|
+
changed = true;
|
|
781
|
+
}
|
|
782
|
+
if (typeof entryConfig.localStorePath !== "string" || entryConfig.localStorePath.length === 0) {
|
|
783
|
+
entryConfig.localStorePath = pluginConfig.localStorePath;
|
|
784
|
+
changed = true;
|
|
785
|
+
}
|
|
786
|
+
if (typeof entryConfig.exportPath !== "string" || entryConfig.exportPath.length === 0) {
|
|
787
|
+
entryConfig.exportPath = pluginConfig.exportPath;
|
|
788
|
+
changed = true;
|
|
789
|
+
}
|
|
790
|
+
if (typeof entryConfig.profileMarkdownRoot !== "string" || entryConfig.profileMarkdownRoot.length === 0) {
|
|
791
|
+
entryConfig.profileMarkdownRoot = pluginConfig.profileMarkdownRoot;
|
|
792
|
+
changed = true;
|
|
793
|
+
}
|
|
794
|
+
if (!Array.isArray(entryConfig.adminAgents) || entryConfig.adminAgents.length === 0) {
|
|
795
|
+
entryConfig.adminAgents = [...pluginConfig.adminAgents];
|
|
796
|
+
changed = true;
|
|
797
|
+
}
|
|
798
|
+
changed = ensureAgentSkills(agents, PROFILE_SKILL_ID) || changed;
|
|
799
|
+
changed = ensureAdminSkill(agents, ADMIN_SKILL_ID, pluginConfig.adminAgents) || changed;
|
|
800
|
+
if (!(0, import_node_fs.existsSync)(profileSkillTarget) || !(0, import_node_fs.existsSync)(adminSkillTarget)) {
|
|
801
|
+
changed = true;
|
|
802
|
+
}
|
|
803
|
+
return changed;
|
|
804
|
+
}
|
|
805
|
+
function materializeBundledSkill(sourceDir, targetDir) {
|
|
806
|
+
if (!(0, import_node_fs.existsSync)(sourceDir) || (0, import_node_fs.existsSync)(targetDir)) {
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(targetDir), { recursive: true });
|
|
810
|
+
(0, import_node_fs.cpSync)(sourceDir, targetDir, { recursive: true });
|
|
811
|
+
}
|
|
812
|
+
function ensureToolNames(tools, values) {
|
|
813
|
+
let changed = false;
|
|
814
|
+
for (const value of values) {
|
|
815
|
+
changed = ensureArrayIncludes(tools, "allow", value) || changed;
|
|
816
|
+
}
|
|
817
|
+
return changed;
|
|
818
|
+
}
|
|
819
|
+
function ensureAgentSkills(agents, skillId) {
|
|
820
|
+
const list = Array.isArray(agents.list) ? agents.list : [];
|
|
821
|
+
let changed = false;
|
|
822
|
+
for (const item of list) {
|
|
823
|
+
if (!item || typeof item !== "object") {
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
const agent = item;
|
|
827
|
+
const current = Array.isArray(agent.skills) ? [...agent.skills] : [];
|
|
828
|
+
if (!current.includes(skillId)) {
|
|
829
|
+
current.push(skillId);
|
|
830
|
+
agent.skills = current;
|
|
831
|
+
changed = true;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return changed;
|
|
835
|
+
}
|
|
836
|
+
function ensureAdminSkill(agents, skillId, adminAgents) {
|
|
837
|
+
const list = Array.isArray(agents.list) ? agents.list : [];
|
|
838
|
+
let changed = false;
|
|
839
|
+
let attached = false;
|
|
840
|
+
for (const item of list) {
|
|
841
|
+
if (!item || typeof item !== "object") {
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
const agent = item;
|
|
845
|
+
const agentId = getConfiguredAgentId(agent);
|
|
846
|
+
if (!agentId || !adminAgents.includes(agentId)) {
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
const current = Array.isArray(agent.skills) ? [...agent.skills] : [];
|
|
850
|
+
if (!current.includes(skillId)) {
|
|
851
|
+
current.push(skillId);
|
|
852
|
+
agent.skills = current;
|
|
853
|
+
changed = true;
|
|
854
|
+
}
|
|
855
|
+
attached = true;
|
|
856
|
+
}
|
|
857
|
+
if (!attached && list.length > 0 && list[0] && typeof list[0] === "object") {
|
|
858
|
+
const agent = list[0];
|
|
859
|
+
const current = Array.isArray(agent.skills) ? [...agent.skills] : [];
|
|
860
|
+
if (!current.includes(skillId)) {
|
|
861
|
+
current.push(skillId);
|
|
862
|
+
agent.skills = current;
|
|
863
|
+
changed = true;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
return changed;
|
|
867
|
+
}
|
|
611
868
|
function mapProfileRow(row) {
|
|
612
869
|
return {
|
|
613
870
|
userId: String(row.user_id),
|
|
@@ -619,6 +876,8 @@ function mapProfileRow(row) {
|
|
|
619
876
|
preferences: asNullableString(row.preferences),
|
|
620
877
|
personality: asNullableString(row.personality),
|
|
621
878
|
role: asNullableString(row.role),
|
|
879
|
+
timezone: asNullableString(row.timezone),
|
|
880
|
+
notes: asNullableString(row.notes),
|
|
622
881
|
visibility: row.visibility === "shared" ? "shared" : "private",
|
|
623
882
|
source: String(row.source ?? "local"),
|
|
624
883
|
updatedAt: String(row.updated_at ?? (/* @__PURE__ */ new Date(0)).toISOString())
|
|
@@ -641,14 +900,16 @@ function parseIdentityContext(context) {
|
|
|
641
900
|
}
|
|
642
901
|
function getAgentIdFromContext(context) {
|
|
643
902
|
const record = context && typeof context === "object" ? context : {};
|
|
644
|
-
return asNullableString(record.agentId) ?? asNullableString(record.agent?.id);
|
|
903
|
+
return asNullableString(record.agentId) ?? asNullableString(record.agent?.id) ?? asNullableString(record.agent?.name);
|
|
645
904
|
}
|
|
646
905
|
function sanitizeProfilePatch(params) {
|
|
647
906
|
return {
|
|
648
907
|
nickname: asNullableString(params.nickname),
|
|
649
908
|
preferences: asNullableString(params.preferences),
|
|
650
909
|
personality: asNullableString(params.personality),
|
|
651
|
-
role: asNullableString(params.role)
|
|
910
|
+
role: asNullableString(params.role),
|
|
911
|
+
timezone: asNullableString(params.timezone),
|
|
912
|
+
notes: asNullableString(params.notes)
|
|
652
913
|
};
|
|
653
914
|
}
|
|
654
915
|
function parseAdminInstruction(instruction) {
|
|
@@ -685,6 +946,7 @@ function extractProfilePatch(input) {
|
|
|
685
946
|
const role = input.match(/(?:role|职责|角色)[=:: ]([^,,]+)/i);
|
|
686
947
|
const preferences = input.match(/(?:preferences|偏好)[=:: ]([^,,]+)/i);
|
|
687
948
|
const personality = input.match(/(?:personality|性格)[=:: ]([^,,]+)/i);
|
|
949
|
+
const timezone = input.match(/(?:timezone|时区)[=:: ]([^,,]+)/i);
|
|
688
950
|
if (nickname) {
|
|
689
951
|
patch.nickname = nickname[1].trim();
|
|
690
952
|
}
|
|
@@ -697,6 +959,9 @@ function extractProfilePatch(input) {
|
|
|
697
959
|
if (personality) {
|
|
698
960
|
patch.personality = personality[1].trim();
|
|
699
961
|
}
|
|
962
|
+
if (timezone) {
|
|
963
|
+
patch.timezone = timezone[1].trim();
|
|
964
|
+
}
|
|
700
965
|
return patch;
|
|
701
966
|
}
|
|
702
967
|
function renderIdentityContext(identity) {
|
|
@@ -710,6 +975,9 @@ function renderIdentityContext(identity) {
|
|
|
710
975
|
if (identity.profile.nickname) {
|
|
711
976
|
lines.push(`Preferred address: ${identity.profile.nickname}`);
|
|
712
977
|
}
|
|
978
|
+
if (identity.profile.timezone) {
|
|
979
|
+
lines.push(`Timezone: ${identity.profile.timezone}`);
|
|
980
|
+
}
|
|
713
981
|
if (identity.profile.preferences) {
|
|
714
982
|
lines.push(`Preferences: ${identity.profile.preferences}`);
|
|
715
983
|
}
|
|
@@ -719,8 +987,95 @@ function renderIdentityContext(identity) {
|
|
|
719
987
|
if (identity.profile.role) {
|
|
720
988
|
lines.push(`Role: ${identity.profile.role}`);
|
|
721
989
|
}
|
|
990
|
+
if (identity.profile.notes) {
|
|
991
|
+
lines.push(`Profile notes: ${identity.profile.notes}`);
|
|
992
|
+
}
|
|
722
993
|
return lines.join("\n");
|
|
723
994
|
}
|
|
995
|
+
function renderProfileMarkdown(profile) {
|
|
996
|
+
const frontmatter = [
|
|
997
|
+
"---",
|
|
998
|
+
`userId: ${escapeFrontmatter(profile.userId)}`,
|
|
999
|
+
`name: ${escapeFrontmatter(profile.name)}`,
|
|
1000
|
+
`nickname: ${escapeFrontmatter(profile.nickname)}`,
|
|
1001
|
+
`timezone: ${escapeFrontmatter(profile.timezone ?? "Asia/Shanghai")}`,
|
|
1002
|
+
`preferences: ${escapeFrontmatter(profile.preferences ?? "\u5E7D\u9ED8\u8BD9\u8C10\u7684\u5BF9\u8BDD\u98CE\u683C\uFF0C\u4F46\u662F\u4E0D\u8FC7\u5206")}`,
|
|
1003
|
+
`personality: ${escapeFrontmatter(profile.personality)}`,
|
|
1004
|
+
`role: ${escapeFrontmatter(profile.role)}`,
|
|
1005
|
+
`visibility: ${escapeFrontmatter(profile.visibility)}`,
|
|
1006
|
+
`source: ${escapeFrontmatter(profile.source)}`,
|
|
1007
|
+
`updatedAt: ${escapeFrontmatter(profile.updatedAt)}`,
|
|
1008
|
+
"---"
|
|
1009
|
+
].join("\n");
|
|
1010
|
+
const notes = profile.notes ?? defaultProfileNotes();
|
|
1011
|
+
return `${frontmatter}
|
|
1012
|
+
|
|
1013
|
+
# \u7528\u6237\u753B\u50CF
|
|
1014
|
+
|
|
1015
|
+
\u8FD9\u4E2A\u6587\u4EF6\u662F\u5F53\u524D\u7528\u6237\u7684\u53EF\u7F16\u8F91\u753B\u50CF\u955C\u50CF\u3002\u4F60\u53EF\u4EE5\u76F4\u63A5\u4FEE\u6539\u4E0A\u9762\u7684\u5B57\u6BB5\u548C\u4E0B\u9762\u7684\u8BF4\u660E\u5185\u5BB9\u3002
|
|
1016
|
+
|
|
1017
|
+
## \u4F7F\u7528\u5EFA\u8BAE
|
|
1018
|
+
|
|
1019
|
+
- \u5E38\u7528\u79F0\u547C\uFF1A\u4F8B\u5982\u201C\u8001\u677F\u201D
|
|
1020
|
+
- \u65F6\u533A\uFF1A\u4F8B\u5982 Asia/Shanghai
|
|
1021
|
+
- \u98CE\u683C\u504F\u597D\uFF1A\u4F8B\u5982\u201C\u5E7D\u9ED8\u8BD9\u8C10\u7684\u5BF9\u8BDD\u98CE\u683C\uFF0C\u4F46\u662F\u4E0D\u8FC7\u5206\u201D
|
|
1022
|
+
- \u89D2\u8272\u4FE1\u606F\uFF1A\u4F8B\u5982\u5DE5\u4F5C\u804C\u8D23\u3001\u534F\u4F5C\u8FB9\u754C\u3001\u5E38\u89C1\u4EFB\u52A1
|
|
1023
|
+
- \u5176\u4ED6\u5907\u6CE8\uFF1A\u4F8B\u5982\u7981\u5FCC\u3001\u4E60\u60EF\u3001\u8F93\u51FA\u504F\u597D
|
|
1024
|
+
|
|
1025
|
+
## \u5907\u6CE8
|
|
1026
|
+
|
|
1027
|
+
${notes}
|
|
1028
|
+
`;
|
|
1029
|
+
}
|
|
1030
|
+
function parseProfileMarkdown(markdown) {
|
|
1031
|
+
const lines = markdown.split(/\r?\n/);
|
|
1032
|
+
const patch = {};
|
|
1033
|
+
let notes = "";
|
|
1034
|
+
let index = 0;
|
|
1035
|
+
if (lines[index] === "---") {
|
|
1036
|
+
index += 1;
|
|
1037
|
+
while (index < lines.length && lines[index] !== "---") {
|
|
1038
|
+
const line = lines[index];
|
|
1039
|
+
const separatorIndex = line.indexOf(":");
|
|
1040
|
+
if (separatorIndex > 0) {
|
|
1041
|
+
const key = line.slice(0, separatorIndex).trim();
|
|
1042
|
+
const value = line.slice(separatorIndex + 1).trim();
|
|
1043
|
+
applyFrontmatterField(patch, key, value);
|
|
1044
|
+
}
|
|
1045
|
+
index += 1;
|
|
1046
|
+
}
|
|
1047
|
+
if (lines[index] === "---") {
|
|
1048
|
+
index += 1;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
notes = lines.slice(index).join("\n").trim() || null;
|
|
1052
|
+
return {
|
|
1053
|
+
profilePatch: patch,
|
|
1054
|
+
notes
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
function applyFrontmatterField(patch, key, value) {
|
|
1058
|
+
const normalized = value === "null" ? null : value;
|
|
1059
|
+
if (key === "name") {
|
|
1060
|
+
patch.name = normalized;
|
|
1061
|
+
} else if (key === "nickname") {
|
|
1062
|
+
patch.nickname = normalized;
|
|
1063
|
+
} else if (key === "timezone") {
|
|
1064
|
+
patch.timezone = normalized;
|
|
1065
|
+
} else if (key === "preferences") {
|
|
1066
|
+
patch.preferences = normalized;
|
|
1067
|
+
} else if (key === "personality") {
|
|
1068
|
+
patch.personality = normalized;
|
|
1069
|
+
} else if (key === "role") {
|
|
1070
|
+
patch.role = normalized;
|
|
1071
|
+
} else if (key === "visibility") {
|
|
1072
|
+
patch.visibility = normalized === "shared" ? "shared" : "private";
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
function hasProfileDifference(current, patch) {
|
|
1076
|
+
const entries = Object.entries(patch);
|
|
1077
|
+
return entries.some(([key, value]) => value != null && current[key] !== value);
|
|
1078
|
+
}
|
|
724
1079
|
function renderYamlList(rows) {
|
|
725
1080
|
if (rows.length === 0) {
|
|
726
1081
|
return "[]\n";
|
|
@@ -765,6 +1120,55 @@ function asTextResult(value) {
|
|
|
765
1120
|
function asNullableString(value) {
|
|
766
1121
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
767
1122
|
}
|
|
1123
|
+
function ensureObject(parent, key) {
|
|
1124
|
+
const current = parent[key];
|
|
1125
|
+
if (current && typeof current === "object" && !Array.isArray(current)) {
|
|
1126
|
+
return current;
|
|
1127
|
+
}
|
|
1128
|
+
const next = {};
|
|
1129
|
+
parent[key] = next;
|
|
1130
|
+
return next;
|
|
1131
|
+
}
|
|
1132
|
+
function ensureArrayIncludes(parent, key, value) {
|
|
1133
|
+
const current = Array.isArray(parent[key]) ? [...parent[key]] : [];
|
|
1134
|
+
if (current.includes(value)) {
|
|
1135
|
+
if (!Array.isArray(parent[key])) {
|
|
1136
|
+
parent[key] = current;
|
|
1137
|
+
}
|
|
1138
|
+
return false;
|
|
1139
|
+
}
|
|
1140
|
+
current.push(value);
|
|
1141
|
+
parent[key] = current;
|
|
1142
|
+
return true;
|
|
1143
|
+
}
|
|
1144
|
+
function getConfiguredAgentId(agent) {
|
|
1145
|
+
return asNullableString(agent.id) ?? asNullableString(agent.name) ?? asNullableString(agent.agentId);
|
|
1146
|
+
}
|
|
1147
|
+
function defaultProfileNotes() {
|
|
1148
|
+
return [
|
|
1149
|
+
"- \u5EFA\u8BAE\u79F0\u547C\uFF1A\u8001\u677F",
|
|
1150
|
+
"- \u65F6\u533A\uFF1AAsia/Shanghai",
|
|
1151
|
+
"- \u504F\u597D\uFF1A\u5E7D\u9ED8\u8BD9\u8C10\u7684\u5BF9\u8BDD\u98CE\u683C\uFF0C\u4F46\u662F\u4E0D\u8FC7\u5206",
|
|
1152
|
+
"- \u4F60\u53EF\u4EE5\u5728\u8FD9\u91CC\u7EE7\u7EED\u8865\u5145\u5DE5\u4F5C\u80CC\u666F\u3001\u8868\u8FBE\u4E60\u60EF\u3001\u7981\u5FCC\u548C\u957F\u671F\u504F\u597D\u3002"
|
|
1153
|
+
].join("\n");
|
|
1154
|
+
}
|
|
1155
|
+
function escapeFrontmatter(value) {
|
|
1156
|
+
if (!value) {
|
|
1157
|
+
return "null";
|
|
1158
|
+
}
|
|
1159
|
+
return value.replace(/\n/g, "\\n");
|
|
1160
|
+
}
|
|
1161
|
+
function joinNotes(primary, secondary) {
|
|
1162
|
+
if (primary && secondary && primary !== secondary) {
|
|
1163
|
+
return `${primary}
|
|
1164
|
+
|
|
1165
|
+
${secondary}`;
|
|
1166
|
+
}
|
|
1167
|
+
return primary ?? secondary ?? null;
|
|
1168
|
+
}
|
|
1169
|
+
function sanitizeFilename(value) {
|
|
1170
|
+
return value.replace(/[^A-Za-z0-9._-]+/g, "_");
|
|
1171
|
+
}
|
|
768
1172
|
function hashId(value) {
|
|
769
1173
|
return (0, import_node_crypto.createHash)("sha1").update(value).digest("hex").slice(0, 24);
|
|
770
1174
|
}
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bamdra/bamdra-user-bind",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Identity resolution, user profile binding, and admin-safe profile tools for OpenClaw channels.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://www.bamdra.com",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"types": "./dist/index.d.ts",
|
|
17
17
|
"files": [
|
|
18
18
|
"dist",
|
|
19
|
+
"skills",
|
|
19
20
|
"openclaw.plugin.json",
|
|
20
21
|
"README.md",
|
|
21
22
|
"LICENSE"
|
|
@@ -31,6 +32,10 @@
|
|
|
31
32
|
"engines": {
|
|
32
33
|
"node": ">=22"
|
|
33
34
|
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"bundle": "node ../bamdra-openclaw-memory/scripts/run-local-bin.mjs tsup && node -e \"const fs=require('node:fs');const path='./dist/index.js';const text=fs.readFileSync(path,'utf8').replace(/require\\\\(\\\"sqlite\\\"\\\\)/g,'require(\\\"node:sqlite\\\")');fs.writeFileSync(path,text);\"",
|
|
37
|
+
"prepublishOnly": "pnpm run bundle"
|
|
38
|
+
},
|
|
34
39
|
"openclaw": {
|
|
35
40
|
"id": "bamdra-user-bind",
|
|
36
41
|
"type": "tool",
|
|
@@ -51,8 +56,5 @@
|
|
|
51
56
|
"dependencies": {},
|
|
52
57
|
"devDependencies": {
|
|
53
58
|
"@types/node": "^24.5.2"
|
|
54
|
-
},
|
|
55
|
-
"scripts": {
|
|
56
|
-
"bundle": "node ../bamdra-openclaw-memory/scripts/run-local-bin.mjs tsup && node -e \"const fs=require('node:fs');const path='./dist/index.js';const text=fs.readFileSync(path,'utf8').replace(/require\\\\(\\\"sqlite\\\"\\\\)/g,'require(\\\"node:sqlite\\\")');fs.writeFileSync(path,text);\""
|
|
57
59
|
}
|
|
58
|
-
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: bamdra-user-bind-admin
|
|
3
|
+
description: Use the admin-safe natural-language tools to inspect, fix, merge, and sync user bindings and profiles.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Bamdra User Bind Admin
|
|
7
|
+
|
|
8
|
+
Use this skill only on an authorized admin agent.
|
|
9
|
+
|
|
10
|
+
Its purpose is operational: inspect user bindings, repair incorrect profile fields, merge duplicate users, and check sync issues without exposing unrestricted bulk access.
|
|
11
|
+
|
|
12
|
+
## Allowed Jobs
|
|
13
|
+
|
|
14
|
+
- query a specific user profile or binding by `userId`
|
|
15
|
+
- correct nickname, role, timezone, preferences, or personality fields
|
|
16
|
+
- merge duplicate user records
|
|
17
|
+
- inspect sync failures and identity resolution issues
|
|
18
|
+
- request a resync for a known user
|
|
19
|
+
|
|
20
|
+
## Tooling
|
|
21
|
+
|
|
22
|
+
Use the admin tools in natural language:
|
|
23
|
+
|
|
24
|
+
- `user_bind_admin_query`
|
|
25
|
+
- `user_bind_admin_edit`
|
|
26
|
+
- `user_bind_admin_merge`
|
|
27
|
+
- `user_bind_admin_list_issues`
|
|
28
|
+
- `user_bind_admin_sync`
|
|
29
|
+
|
|
30
|
+
## Good Requests
|
|
31
|
+
|
|
32
|
+
- “查询 user:u_123 的画像和绑定关系”
|
|
33
|
+
- “把 user:u_123 的称呼改成老板,时区改成 Asia/Shanghai”
|
|
34
|
+
- “合并 user:u_old 到 user:u_new”
|
|
35
|
+
- “列出最近的绑定失败问题”
|
|
36
|
+
|
|
37
|
+
## Safety Rules
|
|
38
|
+
|
|
39
|
+
- do not perform blind bulk edits
|
|
40
|
+
- prefer specific target users over fuzzy descriptions
|
|
41
|
+
- if a request is ambiguous, narrow it before making changes
|
|
42
|
+
- remember that every admin action is auditable
|
|
43
|
+
- do not expose unrelated users when answering a narrow admin query
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: bamdra-user-bind-profile
|
|
3
|
+
description: Use the current bound user profile as the primary personalization layer for tone, address, timezone, and stable preferences.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Bamdra User Bind Profile
|
|
7
|
+
|
|
8
|
+
Treat `bamdra-user-bind` as the current user's editable personalization layer.
|
|
9
|
+
|
|
10
|
+
This skill is about the current bound user only. It should cover most of what a per-user `USER.md` would normally do, without leaking across users.
|
|
11
|
+
|
|
12
|
+
## What To Use It For
|
|
13
|
+
|
|
14
|
+
- preferred form of address
|
|
15
|
+
- timezone-aware responses
|
|
16
|
+
- stable tone and formatting preferences
|
|
17
|
+
- role-aware defaults
|
|
18
|
+
- long-lived collaboration preferences
|
|
19
|
+
- private notes the user intentionally keeps in their profile
|
|
20
|
+
|
|
21
|
+
## Profile Source Of Truth
|
|
22
|
+
|
|
23
|
+
The runtime profile comes from `bamdra-user-bind`.
|
|
24
|
+
|
|
25
|
+
Humans can edit the Markdown mirror for the current user, and the plugin will sync that into the controlled store. Treat the bound profile as more authoritative than guesswork.
|
|
26
|
+
|
|
27
|
+
## Behavior Rules
|
|
28
|
+
|
|
29
|
+
- personalize naturally when the stored profile clearly helps
|
|
30
|
+
- use the stored nickname if the user has not asked for a different form of address in the current turn
|
|
31
|
+
- respect the stored timezone for scheduling, reminders, dates, and time-sensitive explanations
|
|
32
|
+
- prefer the stored tone/style preferences when shaping responses
|
|
33
|
+
- if the current turn conflicts with the stored profile, follow the current turn
|
|
34
|
+
- do not invent profile traits that are not present
|
|
35
|
+
|
|
36
|
+
## Privacy Rules
|
|
37
|
+
|
|
38
|
+
- do not try to infer or mention other users' profiles
|
|
39
|
+
- do not describe the private profile store unless the user asks
|
|
40
|
+
- do not treat profile storage as a global contact directory
|
|
41
|
+
- do not ask for profile data that already exists in the bound profile unless it is stale or clearly insufficient
|
|
42
|
+
|
|
43
|
+
## Updating Profile Information
|
|
44
|
+
|
|
45
|
+
When the user clearly provides a stable preference or asks to remember how to work with them, it is appropriate to update their own profile.
|
|
46
|
+
|
|
47
|
+
Good examples:
|
|
48
|
+
|
|
49
|
+
- “以后叫我老板”
|
|
50
|
+
- “我在 Asia/Shanghai 时区”
|
|
51
|
+
- “我偏好幽默一点,但别太浮夸”
|
|
52
|
+
- “我更喜欢先给结论,再展开”
|
|
53
|
+
|
|
54
|
+
Do not update the profile for transient moods, one-off formatting requests, or unstable short-term details.
|