@web-auto/webauto 0.1.17 → 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.
Files changed (65) hide show
  1. package/README.md +122 -53
  2. package/apps/desktop-console/dist/main/index.mjs +229 -14
  3. package/apps/desktop-console/dist/renderer/index.js +237 -8
  4. package/apps/desktop-console/entry/ui-cli.mjs +290 -21
  5. package/apps/desktop-console/entry/ui-console.mjs +46 -15
  6. package/apps/webauto/entry/account.mjs +126 -27
  7. package/apps/webauto/entry/lib/account-detect.mjs +399 -9
  8. package/apps/webauto/entry/lib/account-store.mjs +201 -109
  9. package/apps/webauto/entry/lib/iflow-reply.mjs +194 -0
  10. package/apps/webauto/entry/lib/profile-policy.mjs +48 -0
  11. package/apps/webauto/entry/lib/profilepool.mjs +12 -0
  12. package/apps/webauto/entry/lib/schedule-store.mjs +29 -2
  13. package/apps/webauto/entry/lib/session-init.mjs +227 -0
  14. package/apps/webauto/entry/lib/upgrade-check.mjs +269 -0
  15. package/apps/webauto/entry/lib/xhs-unified-blocks.mjs +160 -0
  16. package/apps/webauto/entry/lib/xhs-unified-output-blocks.mjs +83 -0
  17. package/apps/webauto/entry/lib/xhs-unified-plan-blocks.mjs +55 -0
  18. package/apps/webauto/entry/lib/xhs-unified-profile-blocks.mjs +542 -0
  19. package/apps/webauto/entry/lib/xhs-unified-runtime-blocks.mjs +436 -0
  20. package/apps/webauto/entry/profilepool.mjs +56 -9
  21. package/apps/webauto/entry/smart-reply-cli.mjs +267 -0
  22. package/apps/webauto/entry/weibo-unified.mjs +84 -11
  23. package/apps/webauto/entry/xhs-orchestrate.mjs +43 -1
  24. package/apps/webauto/entry/xhs-unified.mjs +92 -997
  25. package/bin/webauto.mjs +22 -4
  26. package/dist/modules/camo-backend/src/index.js +33 -0
  27. package/dist/modules/camo-backend/src/internal/BrowserSession.js +232 -49
  28. package/dist/modules/camo-backend/src/internal/engine-manager.js +14 -13
  29. package/dist/modules/camo-backend/src/internal/ws-server.js +16 -19
  30. package/dist/modules/camo-runtime/src/utils/browser-service.mjs +38 -6
  31. package/dist/modules/workflow/blocks/EnsureSession.js +0 -8
  32. package/dist/modules/workflow/blocks/WeiboCollectFromLinksBlock.js +78 -6
  33. package/dist/modules/workflow/blocks/WeiboCollectSearchLinksBlock.js +266 -192
  34. package/dist/modules/workflow/definitions/weibo-search-workflow-v1.js +2 -0
  35. package/dist/modules/workflow/src/runner.js +2 -0
  36. package/dist/modules/xiaohongshu/app/src/blocks/ReplyInteractBlock.js +150 -37
  37. package/dist/modules/xiaohongshu/app/src/blocks/SmartReplyBlock.js +491 -0
  38. package/modules/camo-backend/src/index.ts +31 -0
  39. package/modules/camo-backend/src/internal/BrowserSession.ts +224 -53
  40. package/modules/camo-backend/src/internal/engine-manager.ts +14 -15
  41. package/modules/camo-backend/src/internal/ws-server.ts +17 -17
  42. package/modules/camo-runtime/src/autoscript/action-providers/xhs/common.mjs +12 -2
  43. package/modules/camo-runtime/src/autoscript/action-providers/xhs/persistence.mjs +57 -0
  44. package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +2475 -243
  45. package/modules/camo-runtime/src/autoscript/runtime.mjs +35 -30
  46. package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +80 -443
  47. package/modules/camo-runtime/src/container/runtime-core/checkpoint.mjs +39 -6
  48. package/modules/camo-runtime/src/container/runtime-core/operations/index.mjs +206 -39
  49. package/modules/camo-runtime/src/container/runtime-core/operations/tab-pool.mjs +0 -79
  50. package/modules/camo-runtime/src/container/runtime-core/operations/viewport.mjs +46 -0
  51. package/modules/camo-runtime/src/utils/browser-service.mjs +41 -6
  52. package/modules/camo-runtime/src/utils/js-policy.mjs +28 -0
  53. package/modules/workflow/blocks/EnsureSession.ts +0 -4
  54. package/modules/workflow/blocks/WeiboCollectFromLinksBlock.ts +81 -6
  55. package/modules/workflow/blocks/WeiboCollectSearchLinksBlock.ts +316 -0
  56. package/modules/workflow/definitions/weibo-search-workflow-v1.ts +2 -0
  57. package/modules/workflow/src/runner.ts +2 -0
  58. package/modules/xiaohongshu/app/src/blocks/ReplyInteractBlock.ts +198 -53
  59. package/modules/xiaohongshu/app/src/blocks/SmartReplyBlock.ts +706 -0
  60. package/package.json +2 -2
  61. package/modules/camo-runtime/src/autoscript/action-providers/xhs/comments.mjs +0 -498
  62. package/modules/camo-runtime/src/autoscript/action-providers/xhs/detail.mjs +0 -181
  63. package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +0 -691
  64. package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +0 -388
  65. 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
- ensureProfile,
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
- if (normalizeText(record?.accountId)) return true;
147
- return normalizeStatus(record?.status) === STATUS_PENDING;
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 resolveAccountOrThrow(index, key) {
271
- const idOrAlias = String(key || '').trim();
272
- if (!idOrAlias) throw new Error('account id or alias is required');
273
- const byId = index.accounts.find((item) => item?.id === idOrAlias);
274
- if (byId) return byId;
275
- const target = idOrAlias.toLowerCase();
276
- const byAlias = index.accounts.filter((item) => String(item?.alias || '').trim().toLowerCase() === target);
277
- if (byAlias.length === 1) return byAlias[0];
278
- if (byAlias.length > 1) throw new Error(`alias is not unique: ${idOrAlias}`);
279
- throw new Error(`account not found: ${idOrAlias}`);
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 validProfiles = Array.from(validProfilesSet);
416
- const invalidProfiles = Array.from(invalidProfilesSet).filter((profileId) => !validProfilesSet.has(profileId));
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 getAccount(idOrAlias) {
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
- const autoPrefix = resolveProfilePrefix(platform);
439
- let seq = null;
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
- await ensureProfile(profileId);
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 = ensureSafeName(normalizeText(patch.profileId), '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 (target && hasPersistentAccountId(target)) {
643
- const nextStatus = pendingMode ? STATUS_PENDING : STATUS_INVALID;
644
- const next = {
645
- ...target,
646
- platform,
647
- profileId,
648
- fingerprintId: profileId,
649
- status: nextStatus,
650
- valid: false,
651
- reason: reason || (nextStatus === STATUS_PENDING ? 'waiting_login' : 'invalid'),
652
- detectedAt,
653
- updatedAt: nowIso(),
654
- };
655
- const rowIndex = index.accounts.findIndex((item) => item?.id === target.id);
656
- if (rowIndex < 0) throw new Error(`account not found: ${target.id}`);
657
- index.accounts[rowIndex] = next;
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: DEFAULT_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: DEFAULT_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');