@lightcone-ai/daemon 0.15.50 → 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
package/src/agent-manager.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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);
|
|
@@ -229,37 +260,6 @@ export async function fetchPublishJobWorkspaceFile({ serverUrl, machineApiKey, j
|
|
|
229
260
|
return res.json();
|
|
230
261
|
}
|
|
231
262
|
|
|
232
|
-
export async function streamPublishJobVideo({
|
|
233
|
-
serverUrl,
|
|
234
|
-
machineApiKey,
|
|
235
|
-
jobId,
|
|
236
|
-
videoId,
|
|
237
|
-
localPath,
|
|
238
|
-
writeBinaryFile = writeFileAtomically,
|
|
239
|
-
}) {
|
|
240
|
-
const normalizedJobId = String(jobId ?? '').trim();
|
|
241
|
-
if (!normalizedJobId) {
|
|
242
|
-
throw new Error('publish job id is required to fetch video files');
|
|
243
|
-
}
|
|
244
|
-
const normalizedVideoId = String(videoId ?? '').trim();
|
|
245
|
-
if (!normalizedVideoId) {
|
|
246
|
-
throw new Error('video id is required to fetch video files');
|
|
247
|
-
}
|
|
248
|
-
const url = `${String(serverUrl).replace(/\/$/, '')}/internal/agent/publish-jobs/${encodeURIComponent(normalizedJobId)}/video-files/${encodeURIComponent(normalizedVideoId)}`;
|
|
249
|
-
const res = await fetch(url, {
|
|
250
|
-
headers: {
|
|
251
|
-
Authorization: `Bearer ${machineApiKey}`,
|
|
252
|
-
},
|
|
253
|
-
});
|
|
254
|
-
if (!res.ok) {
|
|
255
|
-
const text = await res.text().catch(() => '');
|
|
256
|
-
throw new Error(`publish-jobs video-files GET failed (${res.status}): ${text}`);
|
|
257
|
-
}
|
|
258
|
-
const data = Buffer.from(await res.arrayBuffer());
|
|
259
|
-
writeBinaryFile(localPath, data);
|
|
260
|
-
return localPath;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
263
|
export function workspacePathFromMediaPath(filePath, workspaceId) {
|
|
264
264
|
if (!filePath) return null;
|
|
265
265
|
|
|
@@ -498,6 +498,8 @@ function buildJobInput(job = {}) {
|
|
|
498
498
|
payload.content_type ?? payload.contentType ?? job.content_type ?? job.contentType
|
|
499
499
|
);
|
|
500
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);
|
|
501
503
|
const title = payload.title;
|
|
502
504
|
const text = payload.text;
|
|
503
505
|
const tags = ensureStringArray(payload.tags);
|
|
@@ -510,6 +512,8 @@ function buildJobInput(job = {}) {
|
|
|
510
512
|
platform,
|
|
511
513
|
contentType,
|
|
512
514
|
workspaceId,
|
|
515
|
+
credentialId,
|
|
516
|
+
accountId,
|
|
513
517
|
title,
|
|
514
518
|
text,
|
|
515
519
|
tags,
|
|
@@ -568,6 +572,8 @@ export async function runPublishJob({ serverUrl, machineApiKey, agentId, workspa
|
|
|
568
572
|
images,
|
|
569
573
|
video,
|
|
570
574
|
cover,
|
|
575
|
+
credentialId,
|
|
576
|
+
accountId,
|
|
571
577
|
} = buildJobInput(job);
|
|
572
578
|
const ownerId = normalizeText(job?.owner_id ?? job?.ownerId);
|
|
573
579
|
const jobId = normalizeText(job?.id);
|
|
@@ -576,6 +582,8 @@ export async function runPublishJob({ serverUrl, machineApiKey, agentId, workspa
|
|
|
576
582
|
contentType: contentType || null,
|
|
577
583
|
jobId: jobId ?? null,
|
|
578
584
|
ownerId: ownerId ?? null,
|
|
585
|
+
credentialId: credentialId ?? null,
|
|
586
|
+
accountId: accountId ?? null,
|
|
579
587
|
};
|
|
580
588
|
|
|
581
589
|
if (!platform) throw attachPublishStage(new Error('publish job missing platform'), 'input_validation', stageContext);
|
|
@@ -649,9 +657,9 @@ export async function runPublishJob({ serverUrl, machineApiKey, agentId, workspa
|
|
|
649
657
|
});
|
|
650
658
|
|
|
651
659
|
currentStage = 'publish_profile';
|
|
652
|
-
const { publishResult, healthCheck } = await withPublisherProfile(platform, { ownerId }, async () => {
|
|
660
|
+
const { publishResult, healthCheck } = await withPublisherProfile(platform, { ownerId, credentialId }, async () => {
|
|
653
661
|
currentStage = 'adapter_session';
|
|
654
|
-
const adapter = await getAdapter(platform, { ownerId });
|
|
662
|
+
const adapter = await getAdapter(platform, { ownerId, credentialId });
|
|
655
663
|
|
|
656
664
|
currentStage = 'pre_publish_login';
|
|
657
665
|
const prePublishLogin = await adapter.checkLoginStatus();
|