@lightcone-ai/daemon 0.15.51 → 0.15.52

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.
@@ -14,6 +14,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
14
14
  import { z } from 'zod';
15
15
  import { existsSync, statSync, realpathSync, mkdirSync, writeFileSync } from 'fs';
16
16
  import path from 'path';
17
+ import { homedir } from 'os';
17
18
  import { getSession, closeSession } from './chrome-pool.js';
18
19
  import { XhsAdapter } from './adapters/xhs.js';
19
20
  import { DouyinAdapter } from './adapters/douyin.js';
@@ -53,7 +54,21 @@ const ADAPTER_REGISTRY = Object.freeze({
53
54
  bilibili: BilibiliAdapter,
54
55
  });
55
56
 
56
- function getProfileDir(platform) {
57
+ // D11/D14: profile dir routing.
58
+ // Priority order:
59
+ // 1. credential_id is given AND ~/.lightcone/chrome-profiles/cred-{id} exists → use it (multi-account safe)
60
+ // 2. credential_id is given AND that path doesn't exist → return canonical cred-path anyway
61
+ // (caller will see missing-profile error explicitly; legacy fallback would silently mix accounts)
62
+ // 3. credential_id is missing (legacy single-account flow) → fall back to env var
63
+ function profileDirByCredentialId(credentialId) {
64
+ if (!credentialId) return null;
65
+ return path.join(homedir(), '.lightcone', 'chrome-profiles', `cred-${credentialId}`);
66
+ }
67
+
68
+ function getProfileDir(platform, credentialId = null) {
69
+ if (credentialId) {
70
+ return profileDirByCredentialId(credentialId);
71
+ }
57
72
  const key = PLATFORM_ENV_KEYS[platform];
58
73
  if (!key) return null;
59
74
  return process.env[key] ?? null;
@@ -74,22 +89,28 @@ function createStaticAdapter(platform) {
74
89
  return createAdapter(platform, null);
75
90
  }
76
91
 
77
- async function getAdapter(platform) {
78
- const profileDir = getProfileDir(platform);
92
+ async function getAdapter(platform, credentialId = null) {
93
+ const profileDir = getProfileDir(platform, credentialId);
79
94
  if (!profileDir) {
80
- throw new Error(`No profile dir for platform="${platform}". Has the user logged in and authorized this agent?`);
95
+ throw new Error(`No profile dir for platform="${platform}" credential_id="${credentialId ?? 'none'}". Has the user logged in and authorized this agent?`);
81
96
  }
82
- const cdp = await getSession(platform, profileDir);
97
+ // Pool session by (platform, credentialId) when credential is specified — different
98
+ // credentials on the same platform must NOT share a CDP session (D17 §6.6 isolation).
99
+ const sessionKey = credentialId ? `${platform}:cred-${credentialId}` : platform;
100
+ const cdp = await getSession(sessionKey, profileDir);
83
101
  return createAdapter(platform, cdp);
84
102
  }
85
103
 
86
- async function withPublisherProfile(platform, fn) {
87
- const profileDir = getProfileDir(platform);
104
+ async function withPublisherProfile(platform, fn, credentialId = null) {
105
+ const profileDir = getProfileDir(platform, credentialId);
88
106
  if (!profileDir) {
89
- throw new Error(`No profile dir for platform="${platform}". Has the user logged in and authorized this agent?`);
107
+ throw new Error(`No profile dir for platform="${platform}" credential_id="${credentialId ?? 'none'}". Has the user logged in and authorized this agent?`);
90
108
  }
91
- return withProfileLock(platform, profileDir, {
92
- owner: `publisher:${platform}`,
109
+ // Lock key: cred-{id} when credential-routed (D17 §6.6: same-platform multi-credential
110
+ // must not block each other). Falls back to platform name for legacy single-credential flow.
111
+ const lockKey = credentialId ? `cred-${credentialId}` : platform;
112
+ return withProfileLock(lockKey, profileDir, {
113
+ owner: `publisher:${lockKey}`,
93
114
  timeoutMs: 30_000,
94
115
  staleMs: 20 * 60 * 1000,
95
116
  }, fn);
@@ -343,8 +364,11 @@ images/video 字段填写本地绝对路径(在 agent workspace 目录下)
343
364
  const req = staticAdapter.getRequirements(content_type);
344
365
  if (!req) throw new Error(`INPUT_UNSUPPORTED_CONTENT_TYPE: ${platform} does not support ${content_type}`);
345
366
  const media = validateMedia({ platform, contentType: content_type, ...localMedia });
367
+ // D11/D14: route by credential_id when approval action carries one (multi-account safe);
368
+ // fall back to legacy env var path when missing (single-account back-compat).
369
+ const credentialIdForPublish = approvalData?.credentialId ?? approvalData?.credential_id ?? null;
346
370
  const { publishResult, healthCheck } = await withPublisherProfile(platform, async () => {
347
- const adapter = await getAdapter(platform);
371
+ const adapter = await getAdapter(platform, credentialIdForPublish);
348
372
  let publishResult = null;
349
373
  if (content_type === 'image_text') {
350
374
  publishResult = await adapter.publishImageText({ title, text, tags: tags ?? [], images: media.images });
@@ -355,8 +379,7 @@ images/video 字段填写本地绝对路径(在 agent workspace 目录下)
355
379
  }
356
380
 
357
381
  let healthCheck = null;
358
- try {
359
- healthCheck = await adapter.checkLoginStatus();
382
+ try { healthCheck = await adapter.checkLoginStatus();
360
383
  } catch (error) {
361
384
  healthCheck = {
362
385
  loggedIn: null,
@@ -111,10 +111,25 @@ function collectPayloadFlags(payload = {}) {
111
111
  ? payload.publish_history
112
112
  : (Array.isArray(payload?.publishHistory) ? payload.publishHistory : []);
113
113
 
114
+ // D11/D14: credential identity + status must reach precheck so we can fail-fast
115
+ // when the bound credential is revoked/expired (publisher_checklist requirement).
116
+ const credentialId = pickField(payload, [
117
+ 'credential_id',
118
+ 'credentialId',
119
+ 'target_credential_id',
120
+ 'targetCredentialId',
121
+ ]);
122
+ const credentialStatus = String(pickField(payload, [
123
+ 'credential_status',
124
+ 'credentialStatus',
125
+ ]) ?? '').trim().toLowerCase();
126
+
114
127
  return {
115
128
  accountId: accountId ? String(accountId).trim() : null,
116
129
  accountRole: normalizeRole(accountRole),
117
130
  autoPublishViaDelegation,
131
+ credentialId: credentialId ? String(credentialId).trim() : null,
132
+ credentialStatus: credentialStatus || null,
118
133
  contentIndicatesCollaboration,
119
134
  hasAdDisclosure,
120
135
  contentContainsAiGenerated,
@@ -151,6 +166,24 @@ function evaluatePrimaryAccountProtection(flags) {
151
166
  };
152
167
  }
153
168
 
169
+ // M4.3: publisher_checklist 凭据状态检查项 (frag.publisher.platform_checklist).
170
+ // When the action carries an explicit credential_status that is not 'active',
171
+ // fail-fast as a blocker so we don't burn a publish attempt on a revoked
172
+ // or expired credential. Active / unknown / missing status pass through —
173
+ // the actual chrome-profile validity is checked downstream when opening
174
+ // the browser session.
175
+ const ACTIVE_CREDENTIAL_STATUSES = new Set(['', 'active', 'unknown']);
176
+
177
+ function evaluateCredentialStatus(flags) {
178
+ const status = flags.credentialStatus;
179
+ if (!status) return null;
180
+ if (ACTIVE_CREDENTIAL_STATUSES.has(status)) return null;
181
+ return {
182
+ code: 'credential_invalid',
183
+ message: `credential status is "${status}" — re-authorize before publishing (D11/D14)`,
184
+ };
185
+ }
186
+
154
187
  function evaluateLabelBlockers(flags, policyScan = {}) {
155
188
  const blockers = [];
156
189
  if (policyScan.ai_label_required && flags.contentContainsAiGenerated && !flags.hasAiLabel) {
@@ -202,6 +235,9 @@ export async function runPublishPrecheck({
202
235
  const primaryProtection = evaluatePrimaryAccountProtection(flags);
203
236
  if (primaryProtection) blockers.push(primaryProtection);
204
237
 
238
+ const credentialIssue = evaluateCredentialStatus(flags);
239
+ if (credentialIssue) blockers.push(credentialIssue);
240
+
205
241
  blockers.push(...evaluateLabelBlockers(flags, policyScan));
206
242
 
207
243
  let advisory = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightcone-ai/daemon",
3
- "version": "0.15.51",
3
+ "version": "0.15.52",
4
4
  "type": "module",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -685,6 +685,12 @@ export class AgentManager {
685
685
  '__MACHINE_API_KEY__': authToken,
686
686
  '__AGENT_ID__': agentId,
687
687
  '__WORKSPACE_ID__': workspaceId ?? '',
688
+ // D11/D14: legacy per-platform PROFILE_DIR placeholders. Resolved to
689
+ // `{platform}-{userId}` for backward compatibility with single-account
690
+ // workspaces. Multi-account flows route by credential_id at the
691
+ // publisher MCP / publish-job-runner layer (see profileDirByCredentialId);
692
+ // these placeholders are kept so existing single-credential configs keep
693
+ // working without manual migration.
688
694
  '${XHS_PROFILE_DIR}': path.join(profileRoot, `xhs-${userId}`),
689
695
  '${DOUYIN_PROFILE_DIR}': path.join(profileRoot, `douyin-${userId}`),
690
696
  '${KUAISHOU_PROFILE_DIR}': path.join(profileRoot, `kuaishou-${userId}`),
@@ -1,5 +1,6 @@
1
1
  import { existsSync, statSync, realpathSync, mkdirSync, writeFileSync, renameSync, rmSync, readdirSync } from 'fs';
2
2
  import path from 'path';
3
+ import { homedir } from 'os';
3
4
  import { getSession, closeSession } from '../mcp-servers/publisher/chrome-pool.js';
4
5
  import { XhsAdapter } from '../mcp-servers/publisher/adapters/xhs.js';
5
6
  import { DouyinAdapter } from '../mcp-servers/publisher/adapters/douyin.js';
@@ -10,6 +11,12 @@ import { runPublishPrecheck } from '../mcp-servers/publisher/precheck.js';
10
11
  import { withProfileLock } from './profile-lock.js';
11
12
  import { profileDir as getBrowserProfileDir } from './browser-login.js';
12
13
 
14
+ // D11/D14: per-credential profile dir, multi-account safe.
15
+ function profileDirByCredentialId(credentialId) {
16
+ if (!credentialId) return null;
17
+ return path.join(homedir(), '.lightcone', 'chrome-profiles', `cred-${credentialId}`);
18
+ }
19
+
13
20
  const PLATFORM_ENV_KEYS = {
14
21
  xhs: 'XHS_PROFILE_DIR',
15
22
  douyin: 'DOUYIN_PROFILE_DIR',
@@ -42,7 +49,18 @@ function asObject(value) {
42
49
  return {};
43
50
  }
44
51
 
45
- export function resolveProfileDir(platform, { ownerId } = {}) {
52
+ export function resolveProfileDir(platform, { ownerId, credentialId } = {}) {
53
+ const normalizedCredentialId = normalizeText(credentialId);
54
+ if (normalizedCredentialId) {
55
+ return {
56
+ profileDir: profileDirByCredentialId(normalizedCredentialId),
57
+ source: 'credential',
58
+ envKey: null,
59
+ ownerId: normalizeText(ownerId),
60
+ credentialId: normalizedCredentialId,
61
+ };
62
+ }
63
+
46
64
  const envKey = PLATFORM_ENV_KEYS[platform];
47
65
  const envValue = normalizeText(envKey ? process.env[envKey] : null);
48
66
  if (envValue) {
@@ -51,6 +69,7 @@ export function resolveProfileDir(platform, { ownerId } = {}) {
51
69
  source: 'env',
52
70
  envKey,
53
71
  ownerId: normalizeText(ownerId),
72
+ credentialId: null,
54
73
  };
55
74
  }
56
75
 
@@ -61,6 +80,7 @@ export function resolveProfileDir(platform, { ownerId } = {}) {
61
80
  source: 'owner',
62
81
  envKey,
63
82
  ownerId: normalizedOwnerId,
83
+ credentialId: null,
64
84
  };
65
85
  }
66
86
 
@@ -69,16 +89,23 @@ export function resolveProfileDir(platform, { ownerId } = {}) {
69
89
  source: 'missing_owner',
70
90
  envKey,
71
91
  ownerId: null,
92
+ credentialId: null,
72
93
  };
73
94
  }
74
95
 
75
- export function resolveExistingProfileDir(platform, { ownerId } = {}) {
76
- const resolved = resolveProfileDir(platform, { ownerId });
96
+ export function resolveExistingProfileDir(platform, { ownerId, credentialId } = {}) {
97
+ const resolved = resolveProfileDir(platform, { ownerId, credentialId });
77
98
  if (!resolved.profileDir) {
78
- throw new Error('publish:job missing owner_id; server must include it for cross-machine profile resolution');
99
+ throw new Error('publish:job missing owner_id and credential_id; server must include at least one for profile resolution');
79
100
  }
80
101
 
81
102
  if (!existsSync(resolved.profileDir)) {
103
+ if (resolved.source === 'credential') {
104
+ throw new Error(
105
+ `Credential profile dir not found at ${resolved.profileDir}; ` +
106
+ `the credential ${resolved.credentialId} has not completed browser-login on this machine yet (D17 §6.6 implementation hard prerequisite).`
107
+ );
108
+ }
82
109
  if (resolved.source === 'env') {
83
110
  throw new Error(`Profile dir from ${resolved.envKey} not found: ${resolved.profileDir}`);
84
111
  }
@@ -102,17 +129,21 @@ function createStaticAdapter(platform) {
102
129
  return new AdapterClass(null);
103
130
  }
104
131
 
105
- async function getAdapter(platform, { ownerId } = {}) {
106
- const resolvedProfileDir = resolveExistingProfileDir(platform, { ownerId });
107
- const cdp = await getSession(platform, resolvedProfileDir);
132
+ async function getAdapter(platform, { ownerId, credentialId } = {}) {
133
+ const resolvedProfileDir = resolveExistingProfileDir(platform, { ownerId, credentialId });
134
+ // Pool by credentialId when available — different credentials must NOT share a CDP session.
135
+ const sessionKey = credentialId ? `${platform}:cred-${credentialId}` : platform;
136
+ const cdp = await getSession(sessionKey, resolvedProfileDir);
108
137
  const AdapterClass = getAdapterClass(platform);
109
138
  return new AdapterClass(cdp);
110
139
  }
111
140
 
112
- async function withPublisherProfile(platform, { ownerId } = {}, fn) {
113
- const resolvedProfileDir = resolveExistingProfileDir(platform, { ownerId });
114
- return withProfileLock(platform, resolvedProfileDir, {
115
- owner: `publisher:${platform}`,
141
+ async function withPublisherProfile(platform, { ownerId, credentialId } = {}, fn) {
142
+ const resolvedProfileDir = resolveExistingProfileDir(platform, { ownerId, credentialId });
143
+ // Lock key: cred-{id} when credential-routed (D17 §6.6); fallback to platform for legacy single-credential.
144
+ const lockKey = credentialId ? `cred-${credentialId}` : platform;
145
+ return withProfileLock(lockKey, resolvedProfileDir, {
146
+ owner: `publisher:${lockKey}`,
116
147
  timeoutMs: 30_000,
117
148
  staleMs: 20 * 60 * 1000,
118
149
  }, fn);
@@ -467,6 +498,8 @@ function buildJobInput(job = {}) {
467
498
  payload.content_type ?? payload.contentType ?? job.content_type ?? job.contentType
468
499
  );
469
500
  const workspaceId = normalizeText(job.workspace_id ?? job.workspaceId ?? payload.workspace_id ?? payload.workspaceId);
501
+ const credentialId = normalizeText(job.credential_id ?? job.credentialId ?? payload.credential_id ?? payload.credentialId);
502
+ const accountId = normalizeText(job.account_id ?? job.accountId ?? payload.account_id ?? payload.accountId);
470
503
  const title = payload.title;
471
504
  const text = payload.text;
472
505
  const tags = ensureStringArray(payload.tags);
@@ -479,6 +512,8 @@ function buildJobInput(job = {}) {
479
512
  platform,
480
513
  contentType,
481
514
  workspaceId,
515
+ credentialId,
516
+ accountId,
482
517
  title,
483
518
  text,
484
519
  tags,
@@ -537,6 +572,8 @@ export async function runPublishJob({ serverUrl, machineApiKey, agentId, workspa
537
572
  images,
538
573
  video,
539
574
  cover,
575
+ credentialId,
576
+ accountId,
540
577
  } = buildJobInput(job);
541
578
  const ownerId = normalizeText(job?.owner_id ?? job?.ownerId);
542
579
  const jobId = normalizeText(job?.id);
@@ -545,6 +582,8 @@ export async function runPublishJob({ serverUrl, machineApiKey, agentId, workspa
545
582
  contentType: contentType || null,
546
583
  jobId: jobId ?? null,
547
584
  ownerId: ownerId ?? null,
585
+ credentialId: credentialId ?? null,
586
+ accountId: accountId ?? null,
548
587
  };
549
588
 
550
589
  if (!platform) throw attachPublishStage(new Error('publish job missing platform'), 'input_validation', stageContext);
@@ -618,9 +657,9 @@ export async function runPublishJob({ serverUrl, machineApiKey, agentId, workspa
618
657
  });
619
658
 
620
659
  currentStage = 'publish_profile';
621
- const { publishResult, healthCheck } = await withPublisherProfile(platform, { ownerId }, async () => {
660
+ const { publishResult, healthCheck } = await withPublisherProfile(platform, { ownerId, credentialId }, async () => {
622
661
  currentStage = 'adapter_session';
623
- const adapter = await getAdapter(platform, { ownerId });
662
+ const adapter = await getAdapter(platform, { ownerId, credentialId });
624
663
 
625
664
  currentStage = 'pre_publish_login';
626
665
  const prePublishLogin = await adapter.checkLoginStatus();