@web-auto/webauto 0.1.18 → 0.1.19
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 +122 -53
- package/apps/desktop-console/dist/main/index.mjs +227 -12
- package/apps/desktop-console/dist/renderer/index.js +237 -8
- package/apps/desktop-console/entry/ui-cli.mjs +282 -16
- package/apps/desktop-console/entry/ui-console.mjs +46 -15
- package/apps/webauto/entry/account.mjs +126 -27
- package/apps/webauto/entry/lib/account-detect.mjs +399 -9
- package/apps/webauto/entry/lib/account-store.mjs +201 -109
- package/apps/webauto/entry/lib/iflow-reply.mjs +194 -0
- package/apps/webauto/entry/lib/profile-policy.mjs +48 -0
- package/apps/webauto/entry/lib/profilepool.mjs +12 -0
- package/apps/webauto/entry/lib/schedule-store.mjs +29 -2
- package/apps/webauto/entry/lib/session-init.mjs +227 -0
- package/apps/webauto/entry/lib/upgrade-check.mjs +269 -0
- package/apps/webauto/entry/lib/xhs-unified-blocks.mjs +160 -0
- package/apps/webauto/entry/lib/xhs-unified-output-blocks.mjs +83 -0
- package/apps/webauto/entry/lib/xhs-unified-plan-blocks.mjs +55 -0
- package/apps/webauto/entry/lib/xhs-unified-profile-blocks.mjs +542 -0
- package/apps/webauto/entry/lib/xhs-unified-runtime-blocks.mjs +436 -0
- package/apps/webauto/entry/profilepool.mjs +56 -9
- package/apps/webauto/entry/smart-reply-cli.mjs +267 -0
- package/apps/webauto/entry/weibo-unified.mjs +84 -11
- package/apps/webauto/entry/xhs-orchestrate.mjs +43 -1
- package/apps/webauto/entry/xhs-unified.mjs +92 -997
- package/bin/webauto.mjs +22 -4
- package/dist/modules/camo-backend/src/index.js +33 -0
- package/dist/modules/camo-backend/src/internal/BrowserSession.js +232 -49
- package/dist/modules/camo-backend/src/internal/engine-manager.js +14 -13
- package/dist/modules/camo-backend/src/internal/ws-server.js +16 -19
- package/dist/modules/camo-runtime/src/utils/browser-service.mjs +38 -6
- package/dist/modules/workflow/blocks/EnsureSession.js +0 -8
- package/dist/modules/workflow/blocks/WeiboCollectFromLinksBlock.js +78 -6
- package/dist/modules/workflow/blocks/WeiboCollectSearchLinksBlock.js +266 -192
- package/dist/modules/workflow/definitions/weibo-search-workflow-v1.js +2 -0
- package/dist/modules/workflow/src/runner.js +2 -0
- package/dist/modules/xiaohongshu/app/src/blocks/ReplyInteractBlock.js +150 -37
- package/dist/modules/xiaohongshu/app/src/blocks/SmartReplyBlock.js +491 -0
- package/modules/camo-backend/src/index.ts +31 -0
- package/modules/camo-backend/src/internal/BrowserSession.ts +224 -53
- package/modules/camo-backend/src/internal/engine-manager.ts +14 -15
- package/modules/camo-backend/src/internal/ws-server.ts +17 -17
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/common.mjs +12 -2
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/persistence.mjs +57 -0
- package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +2475 -243
- package/modules/camo-runtime/src/autoscript/runtime.mjs +35 -30
- package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +80 -443
- package/modules/camo-runtime/src/container/runtime-core/checkpoint.mjs +39 -6
- package/modules/camo-runtime/src/container/runtime-core/operations/index.mjs +206 -39
- package/modules/camo-runtime/src/container/runtime-core/operations/tab-pool.mjs +0 -79
- package/modules/camo-runtime/src/container/runtime-core/operations/viewport.mjs +46 -0
- package/modules/camo-runtime/src/utils/browser-service.mjs +41 -6
- package/modules/camo-runtime/src/utils/js-policy.mjs +28 -0
- package/modules/workflow/blocks/EnsureSession.ts +0 -4
- package/modules/workflow/blocks/WeiboCollectFromLinksBlock.ts +81 -6
- package/modules/workflow/blocks/WeiboCollectSearchLinksBlock.ts +316 -0
- package/modules/workflow/definitions/weibo-search-workflow-v1.ts +2 -0
- package/modules/workflow/src/runner.ts +2 -0
- package/modules/xiaohongshu/app/src/blocks/ReplyInteractBlock.ts +198 -53
- package/modules/xiaohongshu/app/src/blocks/SmartReplyBlock.ts +706 -0
- package/package.json +2 -2
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/comments.mjs +0 -498
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/detail.mjs +0 -181
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +0 -691
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +0 -388
- package/modules/camo-runtime/src/container/runtime-core/operations/selector-scripts.mjs +0 -135
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
listProfiles,
|
|
5
5
|
resolveFingerprintsRoot,
|
|
6
|
-
resolveNextProfileId,
|
|
7
6
|
resolveProfilesRoot,
|
|
8
7
|
resolveWebautoRoot,
|
|
9
8
|
} from './profilepool.mjs';
|
|
@@ -16,6 +15,8 @@ const STATUS_ACTIVE = 'active';
|
|
|
16
15
|
const STATUS_VALID = 'valid';
|
|
17
16
|
const STATUS_INVALID = 'invalid';
|
|
18
17
|
const STATUS_PENDING = 'pending';
|
|
18
|
+
export const REQUIRED_ACCOUNT_PLATFORMS = Object.freeze(['xiaohongshu', 'weibo']);
|
|
19
|
+
const MIN_REQUIRED_ACCOUNT_BINDINGS = 1;
|
|
19
20
|
|
|
20
21
|
function nowIso() {
|
|
21
22
|
return new Date().toISOString();
|
|
@@ -143,8 +144,16 @@ function removeAccountDirById(id) {
|
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
function hasPersistentAccountId(record) {
|
|
146
|
-
|
|
147
|
-
|
|
147
|
+
return Boolean(normalizeText(record?.accountId));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function assertExistingProfile(profileId) {
|
|
151
|
+
const id = ensureSafeName(profileId, 'profileId');
|
|
152
|
+
const { profiles } = listProfiles();
|
|
153
|
+
if (!profiles.includes(id)) {
|
|
154
|
+
throw new Error(`profile not found: ${id}. create/login account profile first`);
|
|
155
|
+
}
|
|
156
|
+
return id;
|
|
148
157
|
}
|
|
149
158
|
|
|
150
159
|
function isWithinDir(rootDir, targetPath) {
|
|
@@ -267,16 +276,57 @@ function ensureAliasUnique(accounts, alias, exceptId = '') {
|
|
|
267
276
|
}
|
|
268
277
|
}
|
|
269
278
|
|
|
270
|
-
function
|
|
271
|
-
const
|
|
272
|
-
if (!
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if (
|
|
278
|
-
if (
|
|
279
|
-
|
|
279
|
+
function filterByPlatform(records, platform = null) {
|
|
280
|
+
const wanted = normalizeText(platform) ? normalizePlatform(platform) : null;
|
|
281
|
+
if (!wanted) return records;
|
|
282
|
+
return records.filter((item) => normalizePlatform(item?.platform) === wanted);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function resolveSingleOrThrow(records, field, key, platform = null) {
|
|
286
|
+
if (!Array.isArray(records) || records.length === 0) return null;
|
|
287
|
+
if (records.length === 1) return records[0];
|
|
288
|
+
const platformText = normalizeText(platform) ? ` under platform=${normalizePlatform(platform)}` : '';
|
|
289
|
+
const detail = records.map((item) => `${String(item?.id || '?')}(${normalizePlatform(item?.platform)})`).join(', ');
|
|
290
|
+
throw new Error(`${field} is not unique${platformText}: ${key} -> [${detail}]`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function resolveAccountOrThrow(index, key, options = {}) {
|
|
294
|
+
const lookup = String(key || '').trim();
|
|
295
|
+
if (!lookup) throw new Error('account id, alias, profileId, or accountId is required');
|
|
296
|
+
const platform = normalizeText(options?.platform) ? normalizePlatform(options.platform) : null;
|
|
297
|
+
const rows = Array.isArray(index?.accounts) ? index.accounts : [];
|
|
298
|
+
|
|
299
|
+
const byId = filterByPlatform(rows.filter((item) => String(item?.id || '').trim() === lookup), platform);
|
|
300
|
+
const idMatch = resolveSingleOrThrow(byId, 'account id', lookup, platform);
|
|
301
|
+
if (idMatch) return idMatch;
|
|
302
|
+
|
|
303
|
+
const aliasTarget = lookup.toLowerCase();
|
|
304
|
+
const byAlias = filterByPlatform(
|
|
305
|
+
rows.filter((item) => String(item?.alias || '').trim().toLowerCase() === aliasTarget),
|
|
306
|
+
platform,
|
|
307
|
+
);
|
|
308
|
+
const aliasMatch = resolveSingleOrThrow(byAlias, 'alias', lookup, platform);
|
|
309
|
+
if (aliasMatch) return aliasMatch;
|
|
310
|
+
|
|
311
|
+
const byProfile = filterByPlatform(
|
|
312
|
+
rows.filter((item) => String(item?.profileId || '').trim() === lookup),
|
|
313
|
+
platform,
|
|
314
|
+
);
|
|
315
|
+
const profileMatch = resolveSingleOrThrow(byProfile, 'profileId', lookup, platform);
|
|
316
|
+
if (profileMatch) return profileMatch;
|
|
317
|
+
|
|
318
|
+
const accountIdTarget = normalizeText(lookup);
|
|
319
|
+
if (accountIdTarget) {
|
|
320
|
+
const byAccountId = filterByPlatform(
|
|
321
|
+
rows.filter((item) => normalizeText(item?.accountId) === accountIdTarget),
|
|
322
|
+
platform,
|
|
323
|
+
);
|
|
324
|
+
const accountIdMatch = resolveSingleOrThrow(byAccountId, 'accountId', lookup, platform);
|
|
325
|
+
if (accountIdMatch) return accountIdMatch;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const platformHint = platform ? ` under platform=${platform}` : '';
|
|
329
|
+
throw new Error(`account not found${platformHint}: ${lookup}`);
|
|
280
330
|
}
|
|
281
331
|
|
|
282
332
|
function resolveBindingKey(profileId, platform) {
|
|
@@ -352,6 +402,28 @@ function buildProfileAccountView(profileId, record = null) {
|
|
|
352
402
|
};
|
|
353
403
|
}
|
|
354
404
|
|
|
405
|
+
function resolveProfileCoverage(rows) {
|
|
406
|
+
const byProfile = new Map();
|
|
407
|
+
for (const row of rows) {
|
|
408
|
+
const profileId = normalizeText(row?.profileId);
|
|
409
|
+
if (!profileId) continue;
|
|
410
|
+
if (!byProfile.has(profileId)) {
|
|
411
|
+
byProfile.set(profileId, {
|
|
412
|
+
profileId,
|
|
413
|
+
platforms: new Set(),
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
if (row?.valid === true && normalizeText(row?.accountId)) {
|
|
417
|
+
byProfile.get(profileId).platforms.add(normalizePlatform(row.platform));
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return byProfile;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function isProfileFullyBound(platforms) {
|
|
424
|
+
return (platforms instanceof Set ? platforms.size : 0) >= MIN_REQUIRED_ACCOUNT_BINDINGS;
|
|
425
|
+
}
|
|
426
|
+
|
|
355
427
|
export function listAccountProfiles(options = {}) {
|
|
356
428
|
const index = loadIndex();
|
|
357
429
|
const platformFilter = normalizeText(options?.platform) ? normalizePlatform(options.platform) : null;
|
|
@@ -392,8 +464,6 @@ export function listAccountProfiles(options = {}) {
|
|
|
392
464
|
return normalizePlatform(a?.platform).localeCompare(normalizePlatform(b?.platform));
|
|
393
465
|
})
|
|
394
466
|
.map((record) => buildProfileAccountView(record?.profileId, record));
|
|
395
|
-
const validProfilesSet = new Set();
|
|
396
|
-
const invalidProfilesSet = new Set();
|
|
397
467
|
const validProfilesByPlatform = {};
|
|
398
468
|
const invalidProfilesByPlatform = {};
|
|
399
469
|
for (const row of rows) {
|
|
@@ -401,33 +471,52 @@ export function listAccountProfiles(options = {}) {
|
|
|
401
471
|
if (!validProfilesByPlatform[platform]) validProfilesByPlatform[platform] = [];
|
|
402
472
|
if (!invalidProfilesByPlatform[platform]) invalidProfilesByPlatform[platform] = [];
|
|
403
473
|
if (row.valid) {
|
|
404
|
-
validProfilesSet.add(row.profileId);
|
|
405
474
|
if (!validProfilesByPlatform[platform].includes(row.profileId)) {
|
|
406
475
|
validProfilesByPlatform[platform].push(row.profileId);
|
|
407
476
|
}
|
|
408
477
|
} else {
|
|
409
|
-
invalidProfilesSet.add(row.profileId);
|
|
410
478
|
if (!invalidProfilesByPlatform[platform].includes(row.profileId)) {
|
|
411
479
|
invalidProfilesByPlatform[platform].push(row.profileId);
|
|
412
480
|
}
|
|
413
481
|
}
|
|
414
482
|
}
|
|
415
|
-
const
|
|
416
|
-
const
|
|
483
|
+
const coverage = resolveProfileCoverage(rows);
|
|
484
|
+
const savedProfiles = [];
|
|
485
|
+
const unsavedProfiles = [];
|
|
486
|
+
for (const [profileId, entry] of coverage.entries()) {
|
|
487
|
+
if (isProfileFullyBound(entry.platforms)) savedProfiles.push(profileId);
|
|
488
|
+
else unsavedProfiles.push(profileId);
|
|
489
|
+
}
|
|
490
|
+
savedProfiles.sort((a, b) => a.localeCompare(b));
|
|
491
|
+
unsavedProfiles.sort((a, b) => a.localeCompare(b));
|
|
492
|
+
const validProfiles = savedProfiles;
|
|
493
|
+
const invalidProfiles = unsavedProfiles;
|
|
417
494
|
return {
|
|
418
495
|
root: resolveAccountsRoot(),
|
|
419
496
|
count: rows.length,
|
|
420
497
|
profiles: rows,
|
|
421
498
|
validProfiles,
|
|
422
499
|
invalidProfiles,
|
|
500
|
+
savedProfiles,
|
|
501
|
+
unsavedProfiles,
|
|
423
502
|
validProfilesByPlatform,
|
|
424
503
|
invalidProfilesByPlatform,
|
|
425
504
|
};
|
|
426
505
|
}
|
|
427
506
|
|
|
428
|
-
export function
|
|
507
|
+
export function listSavedProfiles() {
|
|
508
|
+
return listAccountProfiles().savedProfiles || [];
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export function isProfileSaved(profileId) {
|
|
512
|
+
const id = normalizeText(profileId);
|
|
513
|
+
if (!id) return false;
|
|
514
|
+
return listSavedProfiles().includes(id);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export function getAccount(idOrAlias, options = {}) {
|
|
429
518
|
const index = loadIndex();
|
|
430
|
-
return resolveAccountOrThrow(index, idOrAlias);
|
|
519
|
+
return resolveAccountOrThrow(index, idOrAlias, options);
|
|
431
520
|
}
|
|
432
521
|
|
|
433
522
|
export async function addAccount(input = {}) {
|
|
@@ -435,21 +524,12 @@ export async function addAccount(input = {}) {
|
|
|
435
524
|
const platform = normalizePlatform(input.platform);
|
|
436
525
|
const hasCustomId = Boolean(normalizeText(input.id));
|
|
437
526
|
const explicitProfileId = normalizeText(input.profileId);
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
let profileId = explicitProfileId;
|
|
441
|
-
|
|
442
|
-
if (!hasCustomId && !explicitProfileId) {
|
|
443
|
-
// Default path: account/profile share the same minimal available slot.
|
|
444
|
-
seq = resolveNextAutoSeq(index, platform, null);
|
|
445
|
-
profileId = `${autoPrefix}-${seq}`;
|
|
446
|
-
} else {
|
|
447
|
-
profileId = explicitProfileId || resolveNextProfileId(autoPrefix);
|
|
448
|
-
const profileSeq = resolveProfileSeq(profileId, platform);
|
|
449
|
-
seq = resolveNextAutoSeq(index, platform, hasCustomId ? null : profileSeq);
|
|
527
|
+
if (!explicitProfileId) {
|
|
528
|
+
throw new Error('profileId is required; automatic profile creation is disabled');
|
|
450
529
|
}
|
|
451
|
-
|
|
452
|
-
|
|
530
|
+
const profileId = assertExistingProfile(explicitProfileId);
|
|
531
|
+
const profileSeq = resolveProfileSeq(profileId, platform);
|
|
532
|
+
const seq = resolveNextAutoSeq(index, platform, hasCustomId ? null : profileSeq);
|
|
453
533
|
|
|
454
534
|
const id = ensureSafeName(
|
|
455
535
|
hasCustomId ? normalizeId(input.id, platform, seq) : buildAutoAccountId(platform, seq),
|
|
@@ -560,9 +640,8 @@ export async function updateAccount(idOrAlias, patch = {}) {
|
|
|
560
640
|
next.reason = normalizeText(patch.reason);
|
|
561
641
|
}
|
|
562
642
|
if (Object.prototype.hasOwnProperty.call(patch, 'profileId')) {
|
|
563
|
-
const profileId =
|
|
643
|
+
const profileId = assertExistingProfile(normalizeText(patch.profileId));
|
|
564
644
|
if (profileId !== next.profileId) {
|
|
565
|
-
await ensureProfile(profileId);
|
|
566
645
|
next.profileId = profileId;
|
|
567
646
|
if (!Object.prototype.hasOwnProperty.call(patch, 'fingerprintId')) {
|
|
568
647
|
next.fingerprintId = profileId;
|
|
@@ -623,6 +702,7 @@ export async function updateAccount(idOrAlias, patch = {}) {
|
|
|
623
702
|
|
|
624
703
|
export function upsertProfileAccountState(input = {}) {
|
|
625
704
|
const profileId = ensureSafeName(normalizeText(input.profileId), 'profileId');
|
|
705
|
+
assertExistingProfile(profileId);
|
|
626
706
|
const platform = normalizePlatform(input.platform);
|
|
627
707
|
const accountId = normalizeText(input.accountId || input.platformAccountId || null);
|
|
628
708
|
const alias = normalizeAlias(input.alias);
|
|
@@ -639,76 +719,26 @@ export function upsertProfileAccountState(input = {}) {
|
|
|
639
719
|
const purgeIds = new Set();
|
|
640
720
|
|
|
641
721
|
if (!accountId) {
|
|
642
|
-
if (
|
|
643
|
-
const
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
saveIndex(index);
|
|
659
|
-
persistAccountMeta(next);
|
|
660
|
-
return buildProfileAccountView(profileId, next);
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
if (pendingMode) {
|
|
664
|
-
const profileSeq = resolveProfileSeq(profileId, platform);
|
|
665
|
-
const seq = resolveNextAutoSeq(index, platform, profileSeq);
|
|
666
|
-
const id = ensureSafeName(buildAutoAccountId(platform, seq), 'id');
|
|
667
|
-
const createdAt = nowIso();
|
|
668
|
-
const record = {
|
|
669
|
-
id,
|
|
670
|
-
seq,
|
|
671
|
-
platform,
|
|
672
|
-
status: STATUS_PENDING,
|
|
673
|
-
valid: false,
|
|
674
|
-
reason: reason || 'waiting_login',
|
|
675
|
-
accountId: null,
|
|
676
|
-
name: `${platform}-${profileId}`,
|
|
677
|
-
alias: alias || null,
|
|
678
|
-
username: null,
|
|
679
|
-
profileId,
|
|
680
|
-
fingerprintId: profileId,
|
|
681
|
-
createdAt,
|
|
682
|
-
updatedAt: createdAt,
|
|
683
|
-
detectedAt,
|
|
684
|
-
aliasSource: alias ? 'auto' : null,
|
|
685
|
-
};
|
|
686
|
-
index.accounts.push(record);
|
|
687
|
-
index.nextSeq = Math.max(Number(index.nextSeq) || 1, seq + 1);
|
|
688
|
-
saveIndex(index);
|
|
689
|
-
persistAccountMeta(record);
|
|
690
|
-
return buildProfileAccountView(profileId, record);
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
const staleIds = index.accounts
|
|
694
|
-
.filter((item) => (
|
|
695
|
-
String(item?.profileId || '').trim() === profileId
|
|
696
|
-
&& normalizePlatform(item?.platform) === platform
|
|
697
|
-
&& !hasPersistentAccountId(item)
|
|
698
|
-
))
|
|
699
|
-
.map((item) => String(item?.id || '').trim())
|
|
700
|
-
.filter(Boolean);
|
|
701
|
-
if (staleIds.length > 0) {
|
|
702
|
-
index.accounts = index.accounts.filter((item) => {
|
|
703
|
-
const id = String(item?.id || '').trim();
|
|
704
|
-
return !staleIds.includes(id);
|
|
705
|
-
});
|
|
706
|
-
saveIndex(index);
|
|
707
|
-
for (const id of staleIds) deleteAccountMeta(id);
|
|
722
|
+
if (!pendingMode) {
|
|
723
|
+
const staleIds = index.accounts
|
|
724
|
+
.filter((item) => (
|
|
725
|
+
String(item?.profileId || '').trim() === profileId
|
|
726
|
+
&& normalizePlatform(item?.platform) === platform
|
|
727
|
+
))
|
|
728
|
+
.map((item) => String(item?.id || '').trim())
|
|
729
|
+
.filter(Boolean);
|
|
730
|
+
if (staleIds.length > 0) {
|
|
731
|
+
index.accounts = index.accounts.filter((item) => {
|
|
732
|
+
const id = String(item?.id || '').trim();
|
|
733
|
+
return !staleIds.includes(id);
|
|
734
|
+
});
|
|
735
|
+
saveIndex(index);
|
|
736
|
+
for (const id of staleIds) deleteAccountMeta(id);
|
|
737
|
+
}
|
|
708
738
|
}
|
|
709
|
-
|
|
710
739
|
return buildProfileAccountView(profileId, {
|
|
711
740
|
profileId,
|
|
741
|
+
platform,
|
|
712
742
|
accountId: null,
|
|
713
743
|
alias: null,
|
|
714
744
|
status,
|
|
@@ -808,27 +838,89 @@ export function upsertProfileAccountState(input = {}) {
|
|
|
808
838
|
return buildProfileAccountView(profileId, next);
|
|
809
839
|
}
|
|
810
840
|
|
|
811
|
-
export function markProfileInvalid(profileId, reason = 'login_guard') {
|
|
841
|
+
export function markProfileInvalid(profileId, reason = 'login_guard', platform = DEFAULT_PLATFORM) {
|
|
812
842
|
const id = ensureSafeName(normalizeText(profileId), 'profileId');
|
|
813
843
|
return upsertProfileAccountState({
|
|
814
844
|
profileId: id,
|
|
815
|
-
platform:
|
|
845
|
+
platform: normalizePlatform(platform),
|
|
816
846
|
accountId: null,
|
|
817
847
|
reason,
|
|
818
848
|
});
|
|
819
849
|
}
|
|
820
850
|
|
|
821
|
-
export function markProfilePending(profileId, reason = 'waiting_login') {
|
|
851
|
+
export function markProfilePending(profileId, reason = 'waiting_login', platform = DEFAULT_PLATFORM) {
|
|
822
852
|
const id = ensureSafeName(normalizeText(profileId), 'profileId');
|
|
823
853
|
return upsertProfileAccountState({
|
|
824
854
|
profileId: id,
|
|
825
|
-
platform:
|
|
855
|
+
platform: normalizePlatform(platform),
|
|
826
856
|
accountId: null,
|
|
827
857
|
status: STATUS_PENDING,
|
|
828
858
|
reason,
|
|
829
859
|
});
|
|
830
860
|
}
|
|
831
861
|
|
|
862
|
+
export function cleanupIncompleteProfiles(options = {}) {
|
|
863
|
+
const deleteProfileDirs = options?.deleteProfileDirs !== false;
|
|
864
|
+
const index = loadIndex();
|
|
865
|
+
const byProfile = new Map();
|
|
866
|
+
for (const row of index.accounts) {
|
|
867
|
+
const profileId = normalizeText(row?.profileId);
|
|
868
|
+
if (!profileId) continue;
|
|
869
|
+
if (!byProfile.has(profileId)) {
|
|
870
|
+
byProfile.set(profileId, {
|
|
871
|
+
profileId,
|
|
872
|
+
accountRecordIds: [],
|
|
873
|
+
platforms: new Set(),
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
const entry = byProfile.get(profileId);
|
|
877
|
+
if (row?.id) entry.accountRecordIds.push(row.id);
|
|
878
|
+
if (normalizeText(row?.accountId) && normalizeStatus(row?.status) === STATUS_VALID) {
|
|
879
|
+
entry.platforms.add(normalizePlatform(row?.platform));
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const removedProfiles = [];
|
|
884
|
+
const removedRecordIds = [];
|
|
885
|
+
const purgeRecordIds = new Set();
|
|
886
|
+
const profileIds = new Set([...byProfile.keys(), ...listProfiles().profiles]);
|
|
887
|
+
for (const profileId of profileIds) {
|
|
888
|
+
const entry = byProfile.get(profileId) || {
|
|
889
|
+
profileId,
|
|
890
|
+
accountRecordIds: [],
|
|
891
|
+
platforms: new Set(),
|
|
892
|
+
};
|
|
893
|
+
if (isProfileFullyBound(entry.platforms)) continue;
|
|
894
|
+
removedProfiles.push(entry.profileId);
|
|
895
|
+
for (const id of entry.accountRecordIds) purgeRecordIds.add(id);
|
|
896
|
+
}
|
|
897
|
+
if (purgeRecordIds.size > 0) {
|
|
898
|
+
index.accounts = index.accounts.filter((row) => {
|
|
899
|
+
const id = String(row?.id || '').trim();
|
|
900
|
+
if (!id || !purgeRecordIds.has(id)) return true;
|
|
901
|
+
removedRecordIds.push(id);
|
|
902
|
+
return false;
|
|
903
|
+
});
|
|
904
|
+
saveIndex(index);
|
|
905
|
+
for (const id of removedRecordIds) {
|
|
906
|
+
deleteAccountMeta(id);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
if (deleteProfileDirs) {
|
|
911
|
+
const profilesRoot = resolveProfilesRoot();
|
|
912
|
+
for (const profileId of removedProfiles) {
|
|
913
|
+
const profilePath = path.join(profilesRoot, profileId);
|
|
914
|
+
if (!isWithinDir(profilesRoot, profilePath)) continue;
|
|
915
|
+
fs.rmSync(profilePath, { recursive: true, force: true });
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
return {
|
|
919
|
+
removedProfiles,
|
|
920
|
+
removedRecords: removedRecordIds,
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
|
|
832
924
|
export function removeAccount(idOrAlias, options = {}) {
|
|
833
925
|
const index = loadIndex();
|
|
834
926
|
const account = resolveAccountOrThrow(index, idOrAlias);
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* iflow-reply.mjs
|
|
3
|
+
*
|
|
4
|
+
* 使用 iflow -p 生成智能回复
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawn } from 'child_process';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} SmartReplyOptions
|
|
11
|
+
* @property {string} noteContent - 帖子正文
|
|
12
|
+
* @property {string} commentText - 命中的评论全文
|
|
13
|
+
* @property {string} replyIntent - 回复的中心意思(用户提供)
|
|
14
|
+
* @property {string} [style] - 回复风格
|
|
15
|
+
* @property {number} [maxLength] - 最大字数
|
|
16
|
+
* @property {string} [model] - 指定模型
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {Object} SmartReplyResult
|
|
21
|
+
* @property {boolean} ok
|
|
22
|
+
* @property {string} [reply]
|
|
23
|
+
* @property {string} [error]
|
|
24
|
+
* @property {number} [executionTimeMs]
|
|
25
|
+
* @property {{input: number, output: number, total: number}} [tokenUsage]
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build prompt for iflow
|
|
30
|
+
* @param {SmartReplyOptions} opts
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
function buildPrompt(opts) {
|
|
34
|
+
const { noteContent, commentText, replyIntent, style, maxLength } = opts;
|
|
35
|
+
const styleDesc = style || '友好、自然、口语化';
|
|
36
|
+
const maxLen = maxLength || 100;
|
|
37
|
+
|
|
38
|
+
return `你是一个小红书评论回复助手。请根据以下信息生成一条回复。
|
|
39
|
+
|
|
40
|
+
## 帖子正文
|
|
41
|
+
${noteContent}
|
|
42
|
+
|
|
43
|
+
## 命中的评论
|
|
44
|
+
${commentText}
|
|
45
|
+
|
|
46
|
+
## 回复要求
|
|
47
|
+
- 回复的中心意思:${replyIntent}
|
|
48
|
+
- 回复风格:${styleDesc}
|
|
49
|
+
- 字数限制:${maxLen}字以内
|
|
50
|
+
- 不要使用表情符号开头
|
|
51
|
+
- 不要过于正式,保持自然对话感
|
|
52
|
+
- 可以适当使用 1-2 个表情符号
|
|
53
|
+
|
|
54
|
+
请直接输出回复内容,不要有任何解释或说明。`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Generate smart reply using iflow -p
|
|
59
|
+
* @param {SmartReplyOptions} opts
|
|
60
|
+
* @returns {Promise<SmartReplyResult>}
|
|
61
|
+
*/
|
|
62
|
+
export async function generateSmartReply(opts) {
|
|
63
|
+
const prompt = buildPrompt(opts);
|
|
64
|
+
const startTime = Date.now();
|
|
65
|
+
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
const args = ['-p', prompt];
|
|
68
|
+
if (opts.model) {
|
|
69
|
+
args.unshift('-m', opts.model);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const child = spawn('iflow', args, {
|
|
73
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
let stdout = '';
|
|
77
|
+
let stderr = '';
|
|
78
|
+
|
|
79
|
+
child.stdout.on('data', (chunk) => {
|
|
80
|
+
stdout += String(chunk);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
child.stderr.on('data', (chunk) => {
|
|
84
|
+
stderr += String(chunk);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const timeout = setTimeout(() => {
|
|
88
|
+
try {
|
|
89
|
+
child.kill('SIGTERM');
|
|
90
|
+
} catch {}
|
|
91
|
+
resolve({
|
|
92
|
+
ok: false,
|
|
93
|
+
error: 'timeout (30s)',
|
|
94
|
+
executionTimeMs: Date.now() - startTime,
|
|
95
|
+
});
|
|
96
|
+
}, 30000);
|
|
97
|
+
|
|
98
|
+
child.on('error', (err) => {
|
|
99
|
+
clearTimeout(timeout);
|
|
100
|
+
resolve({
|
|
101
|
+
ok: false,
|
|
102
|
+
error: err.message,
|
|
103
|
+
executionTimeMs: Date.now() - startTime,
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
child.on('close', (code) => {
|
|
108
|
+
clearTimeout(timeout);
|
|
109
|
+
const executionTimeMs = Date.now() - startTime;
|
|
110
|
+
|
|
111
|
+
if (code !== 0) {
|
|
112
|
+
resolve({
|
|
113
|
+
ok: false,
|
|
114
|
+
error: stderr || `exit code ${code}`,
|
|
115
|
+
executionTimeMs,
|
|
116
|
+
});
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const lines = stdout.trim().split('\n');
|
|
121
|
+
let replyText = '';
|
|
122
|
+
let tokenUsage = { input: 0, output: 0, total: 0 };
|
|
123
|
+
|
|
124
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
125
|
+
const line = lines[i].trim();
|
|
126
|
+
if (line.startsWith('{') && line.endsWith('}')) {
|
|
127
|
+
try {
|
|
128
|
+
const info = JSON.parse(line);
|
|
129
|
+
if (info.tokenUsage) {
|
|
130
|
+
tokenUsage = info.tokenUsage;
|
|
131
|
+
replyText = lines.slice(0, i).join('\n').trim();
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
} catch {}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!replyText) {
|
|
139
|
+
replyText = stdout.trim();
|
|
140
|
+
const execInfoMatch = replyText.match(/<Execution Info>[\s\S]*$/);
|
|
141
|
+
if (execInfoMatch) {
|
|
142
|
+
replyText = replyText.slice(0, execInfoMatch.index).trim();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
replyText = replyText
|
|
147
|
+
.replace(/^["']|["']$/g, '')
|
|
148
|
+
.replace(/\n+/g, ' ')
|
|
149
|
+
.trim();
|
|
150
|
+
|
|
151
|
+
if (!replyText) {
|
|
152
|
+
resolve({
|
|
153
|
+
ok: false,
|
|
154
|
+
error: 'empty reply from iflow',
|
|
155
|
+
executionTimeMs,
|
|
156
|
+
});
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
resolve({
|
|
161
|
+
ok: true,
|
|
162
|
+
reply: replyText,
|
|
163
|
+
executionTimeMs,
|
|
164
|
+
tokenUsage,
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Batch generate replies
|
|
172
|
+
* @param {Array<{noteContent: string, commentText: string, replyIntent: string}>} items
|
|
173
|
+
* @param {{style?: string, maxLength?: number, model?: string, concurrency?: number}} [opts]
|
|
174
|
+
* @returns {Promise<Array<SmartReplyResult & {index: number}>>}
|
|
175
|
+
*/
|
|
176
|
+
export async function generateBatchReplies(items, opts = {}) {
|
|
177
|
+
const concurrency = opts.concurrency || 3;
|
|
178
|
+
const results = [];
|
|
179
|
+
|
|
180
|
+
for (let i = 0; i < items.length; i += concurrency) {
|
|
181
|
+
const batch = items.slice(i, i + concurrency);
|
|
182
|
+
const batchResults = await Promise.all(
|
|
183
|
+
batch.map((item, batchIndex) =>
|
|
184
|
+
generateSmartReply({ ...item, ...opts }).then((r) => ({
|
|
185
|
+
...r,
|
|
186
|
+
index: i + batchIndex,
|
|
187
|
+
})),
|
|
188
|
+
),
|
|
189
|
+
);
|
|
190
|
+
results.push(...batchResults);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return results;
|
|
194
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { listProfiles } from './profilepool.mjs';
|
|
2
|
+
import { isProfileSaved } from './account-store.mjs';
|
|
3
|
+
|
|
4
|
+
const TEMP_PROFILE_PATTERNS = [
|
|
5
|
+
/^test(?:$|[-_])/i,
|
|
6
|
+
/^tmp(?:$|[-_])/i,
|
|
7
|
+
/^temp(?:$|[-_])/i,
|
|
8
|
+
/^profile-\d+$/i,
|
|
9
|
+
/^debug(?:$|[-_])/i,
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
function normalizeProfileId(input) {
|
|
13
|
+
return String(input || '').trim();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isTemporaryProfileId(profileId) {
|
|
17
|
+
const id = normalizeProfileId(profileId);
|
|
18
|
+
if (!id) return false;
|
|
19
|
+
return TEMP_PROFILE_PATTERNS.some((pattern) => pattern.test(id));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function assertProfileUsable(profileId) {
|
|
23
|
+
const id = normalizeProfileId(profileId);
|
|
24
|
+
if (!id) {
|
|
25
|
+
throw new Error('profileId is required');
|
|
26
|
+
}
|
|
27
|
+
if (isTemporaryProfileId(id)) {
|
|
28
|
+
throw new Error(`forbidden temporary profileId: ${id}`);
|
|
29
|
+
}
|
|
30
|
+
const profiles = listProfiles().profiles || [];
|
|
31
|
+
if (!profiles.includes(id)) {
|
|
32
|
+
throw new Error(`profile not found: ${id}. create/login account profile first`);
|
|
33
|
+
}
|
|
34
|
+
if (!isProfileSaved(id)) {
|
|
35
|
+
throw new Error(`profile not saved: ${id}. require at least one valid social account binding`);
|
|
36
|
+
}
|
|
37
|
+
return id;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function assertProfilesUsable(profileIds) {
|
|
41
|
+
const list = Array.from(new Set((Array.isArray(profileIds) ? profileIds : [])
|
|
42
|
+
.map((item) => normalizeProfileId(item))
|
|
43
|
+
.filter(Boolean)));
|
|
44
|
+
if (list.length === 0) {
|
|
45
|
+
throw new Error('missing --profile/--profiles/--profilepool');
|
|
46
|
+
}
|
|
47
|
+
return list.map((profileId) => assertProfileUsable(profileId));
|
|
48
|
+
}
|
|
@@ -114,6 +114,18 @@ export async function ensureProfile(profileId) {
|
|
|
114
114
|
return { root, profileDir, profileId: id };
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
export function assertProfileExists(profileId) {
|
|
118
|
+
const id = String(profileId || '').trim();
|
|
119
|
+
if (!id) throw new Error('profileId is required');
|
|
120
|
+
if (id.includes('/') || id.includes('\\')) throw new Error('invalid profileId');
|
|
121
|
+
const root = resolveProfilesRoot();
|
|
122
|
+
const profileDir = path.join(root, id);
|
|
123
|
+
if (!fs.existsSync(profileDir) || !fs.statSync(profileDir).isDirectory()) {
|
|
124
|
+
throw new Error(`profile not found: ${id}. create/login account profile first`);
|
|
125
|
+
}
|
|
126
|
+
return { root, profileDir, profileId: id };
|
|
127
|
+
}
|
|
128
|
+
|
|
117
129
|
async function ensureFingerprint(profileId) {
|
|
118
130
|
try {
|
|
119
131
|
const modulePath = path.resolve(process.cwd(), 'dist', 'modules', 'camo-backend', 'src', 'internal', 'fingerprint.js');
|