clawgram 2.21.1 → 2.23.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/README.md +34 -8
- package/dist/actions.js +124 -0
- package/dist/attachments.js +142 -0
- package/dist/channel.js +426 -720
- package/dist/chat-info.js +28 -22
- package/dist/cli-core.js +109 -20
- package/dist/expiring-map.js +96 -0
- package/dist/gramjs-client.js +104 -29
- package/dist/group-reply-address.js +6 -16
- package/dist/group-visible-reply-guard.js +12 -32
- package/dist/helpers.js +71 -5
- package/dist/history.js +54 -22
- package/dist/html-render.js +44 -1
- package/dist/joins.js +8 -6
- package/dist/manage.js +9 -15
- package/dist/media.js +15 -16
- package/dist/normalize.js +6 -1
- package/dist/outbound.js +260 -0
- package/dist/proxy-config.js +2 -4
- package/dist/reactions.js +3 -12
- package/dist/secret-refs.js +1 -0
- package/dist/send-scope.js +97 -0
- package/dist/silent-reaction.js +9 -5
- package/dist/state-dir.js +24 -0
- package/dist/system-notice.js +49 -4
- package/dist/update-config.js +139 -33
- package/dist/util.js +36 -0
- package/npm-shrinkwrap.json +4531 -0
- package/openclaw.plugin.json +39 -5
- package/package.json +12 -4
package/dist/update-config.js
CHANGED
|
@@ -3,12 +3,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.keepSecretRefs = keepSecretRefs;
|
|
6
7
|
exports.createConfigBackup = createConfigBackup;
|
|
8
|
+
exports.secretRefFieldsFor = secretRefFieldsFor;
|
|
7
9
|
exports.updateConfigFileDirectly = updateConfigFileDirectly;
|
|
8
10
|
const node_fs_1 = require("node:fs");
|
|
9
11
|
const node_path_1 = __importDefault(require("node:path"));
|
|
10
12
|
const json5_1 = __importDefault(require("json5"));
|
|
11
13
|
const constants_1 = require("./constants");
|
|
14
|
+
const secret_refs_1 = require("./secret-refs");
|
|
15
|
+
const util_1 = require("./util");
|
|
12
16
|
function formatBackupTimestamp(date) {
|
|
13
17
|
const year = String(date.getUTCFullYear());
|
|
14
18
|
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
@@ -24,9 +28,6 @@ function buildConfigBackupPath(configPath) {
|
|
|
24
28
|
const suffix = `${formatBackupTimestamp(new Date())}-clawgram-auth`;
|
|
25
29
|
return node_path_1.default.join(dir, `${fileName}.bak-${suffix}`);
|
|
26
30
|
}
|
|
27
|
-
function isPlainObject(value) {
|
|
28
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
29
|
-
}
|
|
30
31
|
function buildAccountPayload(auth) {
|
|
31
32
|
return {
|
|
32
33
|
enabled: true,
|
|
@@ -35,19 +36,39 @@ function buildAccountPayload(auth) {
|
|
|
35
36
|
sessionString: auth.sessionString,
|
|
36
37
|
};
|
|
37
38
|
}
|
|
39
|
+
/** Первый конфиг закрыт: см. одноимённую функцию в cli-core.ts. */
|
|
38
40
|
function buildAccountConfigFragment(auth) {
|
|
39
41
|
return {
|
|
40
42
|
...buildAccountPayload(auth),
|
|
41
|
-
allowFrom: [
|
|
42
|
-
|
|
43
|
-
"*": {
|
|
44
|
-
enabled: true,
|
|
45
|
-
groupPolicy: "mention",
|
|
46
|
-
allowFrom: ["*"],
|
|
47
|
-
},
|
|
48
|
-
},
|
|
43
|
+
allowFrom: auth.selfId ? [auth.selfId] : [],
|
|
44
|
+
readChats: [],
|
|
49
45
|
};
|
|
50
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Credentials already moved into the secret store must survive re-auth.
|
|
49
|
+
*
|
|
50
|
+
* An account whose apiHash/sessionString were migrated to `{source, provider,
|
|
51
|
+
* id}` had them overwritten with the literal strings typed during the
|
|
52
|
+
* interactive flow: the spread put the fresh payload on top of the reference.
|
|
53
|
+
* So an operator re-authorising after a session expiry silently undid the
|
|
54
|
+
* migration and left plaintext credentials in a file that gets backed up and
|
|
55
|
+
* synced — the exact thing secret-refs.ts was written to prevent.
|
|
56
|
+
*
|
|
57
|
+
* The reference wins. The new value is handed back so the caller can tell the
|
|
58
|
+
* operator to store it where the reference points; it is never written to the
|
|
59
|
+
* config, and never printed here.
|
|
60
|
+
*/
|
|
61
|
+
function keepSecretRefs(existingAccount, payload) {
|
|
62
|
+
const next = { ...payload };
|
|
63
|
+
const kept = [];
|
|
64
|
+
for (const field of ["apiHash", "sessionString"]) {
|
|
65
|
+
if ((0, secret_refs_1.asSecretRef)(existingAccount?.[field])) {
|
|
66
|
+
next[field] = existingAccount[field];
|
|
67
|
+
kept.push(field);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { payload: next, kept };
|
|
71
|
+
}
|
|
51
72
|
function applyAuthToConfig(config, accountId, auth) {
|
|
52
73
|
const channels = config.channels && typeof config.channels === "object" ? config.channels : {};
|
|
53
74
|
const channelConfig = channels[constants_1.CHANNEL_ID] && typeof channels[constants_1.CHANNEL_ID] === "object" ? channels[constants_1.CHANNEL_ID] : {};
|
|
@@ -63,16 +84,12 @@ function applyAuthToConfig(config, accountId, auth) {
|
|
|
63
84
|
...accounts,
|
|
64
85
|
[accountId]: {
|
|
65
86
|
...existingAccount,
|
|
66
|
-
...buildAccountPayload(auth),
|
|
87
|
+
...keepSecretRefs(existingAccount, buildAccountPayload(auth)).payload,
|
|
67
88
|
enabled: existingAccount.enabled ?? true,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
groupPolicy: "mention",
|
|
73
|
-
allowFrom: ["*"],
|
|
74
|
-
},
|
|
75
|
-
},
|
|
89
|
+
// Существующие настройки не трогаем — повторная авторизация не
|
|
90
|
+
// повод переписать чужие решения. Отсутствующие садятся закрытыми.
|
|
91
|
+
allowFrom: existingAccount.allowFrom ?? (auth.selfId ? [auth.selfId] : []),
|
|
92
|
+
readChats: existingAccount.readChats ?? [],
|
|
76
93
|
},
|
|
77
94
|
},
|
|
78
95
|
},
|
|
@@ -244,12 +261,27 @@ function scanValue(raw, start) {
|
|
|
244
261
|
kind: "scalar",
|
|
245
262
|
};
|
|
246
263
|
}
|
|
247
|
-
|
|
264
|
+
/**
|
|
265
|
+
* Позиция СРАЗУ ЗА закрывающей скобкой объекта — как `end` у среза.
|
|
266
|
+
*
|
|
267
|
+
* Имя обманывало: `scanEnclosedValue` возвращает индекс после `}`, а вставка
|
|
268
|
+
* свойства обращалась с этим числом как с позицией самой скобки и клала
|
|
269
|
+
* свойство ЗА объектом. Итог — испорченный конфиг: аккаунт, добавленный к
|
|
270
|
+
* существующему `accounts`, оказывался соседом `accounts` внутри `clawgram`,
|
|
271
|
+
* а при отсутствующем `channels` вставка уезжала за корневую `}` и файл
|
|
272
|
+
* переставал быть JSON вовсе. Ловилось только на путях вставки, а обычная
|
|
273
|
+
* переавторизация идёт путём замены — поэтому и жило (находка A6-17).
|
|
274
|
+
*/
|
|
275
|
+
function findObjectEndExclusive(raw, objectStart) {
|
|
248
276
|
return scanEnclosedValue(raw, objectStart, "{", "}");
|
|
249
277
|
}
|
|
278
|
+
/** Позиция самой закрывающей скобки — точка, ПЕРЕД которой вставляют. */
|
|
279
|
+
function findObjectCloseBrace(raw, objectStart) {
|
|
280
|
+
return findObjectEndExclusive(raw, objectStart) - 1;
|
|
281
|
+
}
|
|
250
282
|
function listObjectProperties(raw, objectStart) {
|
|
251
283
|
const properties = [];
|
|
252
|
-
const objectEnd =
|
|
284
|
+
const objectEnd = findObjectEndExclusive(raw, objectStart);
|
|
253
285
|
let cursor = skipTrivia(raw, objectStart + 1);
|
|
254
286
|
while (cursor < objectEnd) {
|
|
255
287
|
if (raw[cursor] === "}") {
|
|
@@ -311,16 +343,16 @@ function replaceRange(raw, start, end, value) {
|
|
|
311
343
|
return `${raw.slice(0, start)}${value}${raw.slice(end)}`;
|
|
312
344
|
}
|
|
313
345
|
function insertObjectProperty(raw, objectStart, key, value, format) {
|
|
314
|
-
const
|
|
346
|
+
const closeBrace = findObjectCloseBrace(raw, objectStart);
|
|
315
347
|
const parentIndent = getLineIndent(raw, objectStart);
|
|
316
348
|
const propertyIndent = `${parentIndent}${format.indentUnit}`;
|
|
317
349
|
const propertyText = `${JSON.stringify(key)}: ${formatConfigValue(value, propertyIndent, format)}`;
|
|
318
350
|
const properties = listObjectProperties(raw, objectStart);
|
|
319
351
|
if (properties.length === 0) {
|
|
320
352
|
const insertion = `${format.eol}${propertyIndent}${propertyText}${format.eol}${parentIndent}`;
|
|
321
|
-
return replaceRange(raw,
|
|
353
|
+
return replaceRange(raw, closeBrace, closeBrace, insertion);
|
|
322
354
|
}
|
|
323
|
-
let insertAt =
|
|
355
|
+
let insertAt = closeBrace;
|
|
324
356
|
while (insertAt > objectStart + 1 && /[ \t\r\n]/.test(raw[insertAt - 1])) {
|
|
325
357
|
insertAt -= 1;
|
|
326
358
|
}
|
|
@@ -333,23 +365,74 @@ function replaceObjectPropertyValue(raw, property, value, format) {
|
|
|
333
365
|
const formattedValue = formatConfigValue(value, propertyIndent, format);
|
|
334
366
|
return replaceRange(raw, property.valueStart, property.valueEnd, formattedValue);
|
|
335
367
|
}
|
|
368
|
+
/**
|
|
369
|
+
* Спуск по пути объектов. Недостающие звенья создаются пустыми объектами.
|
|
370
|
+
*
|
|
371
|
+
* Возвращает и текст, и позицию начала найденного объекта: после вставки
|
|
372
|
+
* прежние смещения указывают не туда.
|
|
373
|
+
*/
|
|
374
|
+
function descend(raw, objectStart, path) {
|
|
375
|
+
let start = objectStart;
|
|
376
|
+
for (let i = 0; i < path.length; i += 1) {
|
|
377
|
+
const property = findObjectProperty(raw, start, path[i]);
|
|
378
|
+
// Недостающее звено не достраивается по одному: вставить пустой объект и
|
|
379
|
+
// тут же найти его снова — значит положиться на разбор свойств сразу
|
|
380
|
+
// после правки текста, а это самое хрупкое место здесь. Вместо этого
|
|
381
|
+
// вызывающий вставляет весь остаток пути одним литералом.
|
|
382
|
+
if (!property)
|
|
383
|
+
return { objectStart: start, missing: path.slice(i) };
|
|
384
|
+
const valueStart = skipTrivia(raw, property.valueStart);
|
|
385
|
+
if (raw[valueStart] !== "{") {
|
|
386
|
+
throw new Error(`clawgram: ${path[i]} в конфиге не объект — правка вручную безопаснее`);
|
|
387
|
+
}
|
|
388
|
+
start = valueStart;
|
|
389
|
+
}
|
|
390
|
+
return { objectStart: start, missing: [] };
|
|
391
|
+
}
|
|
392
|
+
/** Вложенный литерал `{a: {b: {c: value}}}` для недостающего остатка пути. */
|
|
393
|
+
function nest(path, value) {
|
|
394
|
+
return path.reduceRight((inner, key) => ({ [key]: inner }), value);
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Правится один аккаунт, а не весь блок `channels`.
|
|
398
|
+
*
|
|
399
|
+
* Раньше блок находился хирургически, а его значение целиком пересобиралось
|
|
400
|
+
* через `JSON.stringify`: пропадали все комментарии JSON5, висячие запятые и
|
|
401
|
+
* ручное форматирование — включая блоки ДРУГИХ каналов, к авторизации
|
|
402
|
+
* отношения не имеющих. Файл потому и JSON5, что в нём пишут пояснения; после
|
|
403
|
+
* каждой переавторизации они исчезали, а diff тонул в переформатировании,
|
|
404
|
+
* пряча ровно то изменение, ради которого всё делалось (находка A6-06).
|
|
405
|
+
*
|
|
406
|
+
* Чего это НЕ спасает: комментарии внутри самого правимого аккаунта. Его
|
|
407
|
+
* значение пересобирается — там меняются учётные данные, и разбирать его
|
|
408
|
+
* посвойственно значит полагаться на разбор комментариев в позициях, который
|
|
409
|
+
* здесь и так самое хрупкое место. Всё за пределами этого аккаунта остаётся
|
|
410
|
+
* как было.
|
|
411
|
+
*/
|
|
336
412
|
function buildUpdatedConfigText(raw, accountId, auth) {
|
|
337
413
|
const parsed = json5_1.default.parse(raw);
|
|
338
|
-
if (!isPlainObject(parsed)) {
|
|
414
|
+
if (!(0, util_1.isPlainObject)(parsed)) {
|
|
339
415
|
throw new Error("OpenClaw config root must be an object.");
|
|
340
416
|
}
|
|
341
|
-
const updatedConfig = applyAuthToConfig(parsed, accountId, auth);
|
|
342
|
-
const updatedChannels = isPlainObject(updatedConfig.channels) ? updatedConfig.channels : {};
|
|
343
417
|
const format = detectTextFormat(raw);
|
|
344
418
|
const rootStart = skipTrivia(raw, 0);
|
|
345
419
|
if (raw[rootStart] !== "{") {
|
|
346
420
|
throw new Error("OpenClaw config file is not a JSON object.");
|
|
347
421
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
422
|
+
// Что должно оказаться в аккаунте — считает та же функция, что и раньше:
|
|
423
|
+
// правила про SecretRef и закрытый посев живут в одном месте.
|
|
424
|
+
const updatedConfig = applyAuthToConfig(parsed, accountId, auth);
|
|
425
|
+
const account = updatedConfig?.channels?.[constants_1.CHANNEL_ID]?.accounts?.[accountId] ?? {};
|
|
426
|
+
const path = ["channels", constants_1.CHANNEL_ID, "accounts"];
|
|
427
|
+
const { objectStart, missing } = descend(raw, rootStart, path);
|
|
428
|
+
if (missing.length) {
|
|
429
|
+
// Раздела нет — вставляем недостающий остаток вместе с аккаунтом.
|
|
430
|
+
return insertObjectProperty(raw, objectStart, missing[0], nest(missing.slice(1), { [accountId]: account }), format);
|
|
351
431
|
}
|
|
352
|
-
|
|
432
|
+
const existing = findObjectProperty(raw, objectStart, accountId);
|
|
433
|
+
return existing
|
|
434
|
+
? replaceObjectPropertyValue(raw, existing, account, format)
|
|
435
|
+
: insertObjectProperty(raw, objectStart, accountId, account, format);
|
|
353
436
|
}
|
|
354
437
|
/**
|
|
355
438
|
* The config this writes holds `apiHash` and `sessionString` in plaintext, so
|
|
@@ -391,11 +474,34 @@ async function createConfigBackup(configPath) {
|
|
|
391
474
|
await node_fs_1.promises.chmod(backupPath, mode);
|
|
392
475
|
return backupPath;
|
|
393
476
|
}
|
|
477
|
+
/**
|
|
478
|
+
* Which credentials stayed as secret-store references, so the caller can say
|
|
479
|
+
* so. Silence here would be the worst outcome: the operator would believe the
|
|
480
|
+
* freshly issued credential is in the config and find out at the next start.
|
|
481
|
+
*/
|
|
482
|
+
function secretRefFieldsFor(raw, accountId) {
|
|
483
|
+
let parsed;
|
|
484
|
+
try {
|
|
485
|
+
parsed = json5_1.default.parse(raw);
|
|
486
|
+
}
|
|
487
|
+
catch {
|
|
488
|
+
return [];
|
|
489
|
+
}
|
|
490
|
+
if (!(0, util_1.isPlainObject)(parsed))
|
|
491
|
+
return [];
|
|
492
|
+
const channels = (0, util_1.isPlainObject)(parsed.channels) ? parsed.channels : {};
|
|
493
|
+
const channel = (0, util_1.isPlainObject)(channels[constants_1.CHANNEL_ID]) ? channels[constants_1.CHANNEL_ID] : {};
|
|
494
|
+
const accounts = (0, util_1.isPlainObject)(channel.accounts) ? channel.accounts : {};
|
|
495
|
+
const account = (0, util_1.isPlainObject)(accounts[accountId]) ? accounts[accountId] : {};
|
|
496
|
+
return ["apiHash", "sessionString"].filter((field) => (0, secret_refs_1.asSecretRef)(account[field]));
|
|
497
|
+
}
|
|
394
498
|
async function updateConfigFileDirectly(configPath, accountId, auth) {
|
|
395
499
|
const raw = await node_fs_1.promises.readFile(configPath, "utf8");
|
|
500
|
+
const keptRefs = secretRefFieldsFor(raw, accountId);
|
|
396
501
|
const nextRaw = buildUpdatedConfigText(raw, accountId, auth);
|
|
397
502
|
if (nextRaw === raw) {
|
|
398
|
-
return;
|
|
503
|
+
return keptRefs;
|
|
399
504
|
}
|
|
400
505
|
await writeConfigAtomically(configPath, nextRaw);
|
|
506
|
+
return keptRefs;
|
|
401
507
|
}
|
package/dist/util.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Small readers reimplemented once per module.
|
|
4
|
+
*
|
|
5
|
+
* `readString`, `readNumber` and `isPlainObject` each existed in two or three
|
|
6
|
+
* files with identical bodies. Identical is the good case: `toStringId` had
|
|
7
|
+
* drifted, and one path accepted an id the other rejected (finding A12-05).
|
|
8
|
+
* Nothing here is clever; the point is that there is one of each.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.readString = readString;
|
|
12
|
+
exports.readNumber = readNumber;
|
|
13
|
+
exports.isPlainObject = isPlainObject;
|
|
14
|
+
/** A non-empty trimmed string, or nothing. */
|
|
15
|
+
function readString(value) {
|
|
16
|
+
if (typeof value !== "string") {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
const trimmed = value.trim();
|
|
20
|
+
return trimmed === "" ? undefined : trimmed;
|
|
21
|
+
}
|
|
22
|
+
/** A finite number, from a number or from its decimal spelling. */
|
|
23
|
+
function readNumber(value) {
|
|
24
|
+
if (typeof value === "number") {
|
|
25
|
+
return Number.isFinite(value) ? value : undefined;
|
|
26
|
+
}
|
|
27
|
+
if (value === undefined || value === null) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
const parsed = Number(String(value));
|
|
31
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
32
|
+
}
|
|
33
|
+
/** An object that is not an array and not null — a config or params bag. */
|
|
34
|
+
function isPlainObject(value) {
|
|
35
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
36
|
+
}
|