@lightcone-ai/daemon 0.9.78 → 0.9.79

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.
@@ -34,19 +34,42 @@ const REQUIREMENTS = {
34
34
  };
35
35
 
36
36
  const CREATOR_HOME_URL = 'https://creator.douyin.com/creator-micro/home';
37
- const IMAGE_PUBLISH_URL = 'https://creator.douyin.com/creator-micro/content/upload-image-text';
38
- const VIDEO_PUBLISH_URL = 'https://creator.douyin.com/creator-micro/content/upload';
37
+ const UPLOAD_URL = 'https://creator.douyin.com/creator-micro/content/upload';
38
+ const IMAGE_PUBLISH_URL = `${UPLOAD_URL}?default-tab=3&enter_from=publish`;
39
+ const VIDEO_PUBLISH_URL = `${UPLOAD_URL}?default-tab=1&enter_from=publish`;
39
40
 
40
- const FILE_INPUT_SELECTOR = [
41
+ const IMAGE_FILE_INPUT_SELECTOR = [
41
42
  'input[type="file"][accept*="image"]',
42
43
  'input[type="file"][accept*=".jpg"]',
43
44
  'input[type="file"][accept*=".jpeg"]',
44
45
  'input[type="file"][accept*=".png"]',
45
46
  'input[type="file"][accept*=".webp"]',
47
+ 'input[type="file"][accept="image"]',
46
48
  'input[type="file"]',
47
49
  ].join(', ');
48
- const TITLE_SELECTOR = 'input[placeholder*="标题"], textarea[placeholder*="标题"]';
49
- const CONTENT_SELECTOR = '[contenteditable="true"], textarea[placeholder*="描述"], textarea[placeholder*="作品描述"], [placeholder*="添加作品描述"]';
50
+ const VIDEO_FILE_INPUT_SELECTOR = [
51
+ 'input[type="file"][accept*="video"]',
52
+ 'input[type="file"][accept*=".mp4"]',
53
+ 'input[type="file"][accept*=".mov"]',
54
+ 'input[type="file"][accept*=".webm"]',
55
+ 'input[type="file"]',
56
+ ].join(', ');
57
+ const FILE_INPUT_SELECTOR = `${IMAGE_FILE_INPUT_SELECTOR}, ${VIDEO_FILE_INPUT_SELECTOR}`;
58
+ const TITLE_SELECTOR = [
59
+ 'input[placeholder*="标题"]',
60
+ 'textarea[placeholder*="标题"]',
61
+ '[contenteditable="true"][data-placeholder*="标题"]',
62
+ '[contenteditable="true"][aria-label*="标题"]',
63
+ ].join(', ');
64
+ const CONTENT_SELECTOR = [
65
+ 'textarea[placeholder*="描述"]',
66
+ 'textarea[placeholder*="作品描述"]',
67
+ '[placeholder*="添加作品描述"]',
68
+ '[contenteditable="true"][data-placeholder*="描述"]',
69
+ '[contenteditable="true"][aria-label*="描述"]',
70
+ '.ProseMirror[contenteditable="true"]',
71
+ '[contenteditable="true"]',
72
+ ].join(', ');
50
73
 
51
74
  const ERROR_SELECTORS = [
52
75
  '[class*="error"]',
@@ -88,16 +111,16 @@ export class DouyinAdapter {
88
111
  if (images.length > 0) {
89
112
  await this._ensureUploaderReady('image');
90
113
  await humanPause(900, 2200, 'before-upload');
91
- await this._uploadFiles(images);
114
+ await this._uploadFiles(images, 'image');
92
115
  await this._waitForUploadSettled(images.length, 120_000);
93
116
  await humanPause(2500, 5500, 'after-upload');
94
117
  }
95
118
 
96
119
  const fullText = formatTextWithTags(text, tags);
97
- await this._fillField(CONTENT_SELECTOR, fullText);
120
+ await this._fillField(CONTENT_SELECTOR, fullText, 'content');
98
121
 
99
122
  if (title) {
100
- await this._fillField(TITLE_SELECTOR, title);
123
+ await this._fillField(TITLE_SELECTOR, title, 'title');
101
124
  }
102
125
 
103
126
  await humanPause(4500, 9000, 'before-publish-check');
@@ -123,7 +146,7 @@ export class DouyinAdapter {
123
146
 
124
147
  await this._ensureUploaderReady('video');
125
148
  await humanPause(900, 2200, 'before-upload');
126
- await this._uploadFiles([video]);
149
+ await this._uploadFiles([video], 'video');
127
150
  await this._waitForUploadSettled(1, 180_000);
128
151
  await humanPause(3000, 6500, 'after-video-upload');
129
152
 
@@ -132,10 +155,10 @@ export class DouyinAdapter {
132
155
  }
133
156
 
134
157
  const fullText = formatTextWithTags(text, tags);
135
- await this._fillField(CONTENT_SELECTOR, fullText);
158
+ await this._fillField(CONTENT_SELECTOR, fullText, 'content');
136
159
 
137
160
  if (title) {
138
- await this._fillField(TITLE_SELECTOR, title);
161
+ await this._fillField(TITLE_SELECTOR, title, 'title');
139
162
  }
140
163
 
141
164
  await humanPause(4500, 9000, 'before-publish-check');
@@ -216,9 +239,10 @@ export class DouyinAdapter {
216
239
  }
217
240
 
218
241
  async _ensureUploaderReady(kind) {
219
- if (await this._waitForSelectorQuiet(FILE_INPUT_SELECTOR, 8000)) return;
242
+ const selector = kind === 'video' ? VIDEO_FILE_INPUT_SELECTOR : IMAGE_FILE_INPUT_SELECTOR;
243
+ if (await this._waitForSelectorQuiet(selector, 8000)) return;
220
244
  await this._ensureComposer(kind);
221
- if (await this._waitForSelectorQuiet(FILE_INPUT_SELECTOR, 12_000)) return;
245
+ if (await this._waitForSelectorQuiet(selector, 12_000)) return;
222
246
 
223
247
  const labels = kind === 'video'
224
248
  ? ['上传视频', '点击上传', '选择视频', '上传']
@@ -226,7 +250,7 @@ export class DouyinAdapter {
226
250
  for (const label of labels) {
227
251
  const clicked = await this._clickByText(label);
228
252
  if (!clicked) continue;
229
- if (await this._waitForSelectorQuiet(FILE_INPUT_SELECTOR, 8000)) return;
253
+ if (await this._waitForSelectorQuiet(selector, 8000)) return;
230
254
  }
231
255
 
232
256
  const state = await this._inspectPage();
@@ -273,7 +297,7 @@ export class DouyinAdapter {
273
297
  .map(t => t.trim())
274
298
  .filter(Boolean)
275
299
  .slice(0, 5);
276
- const buttons = [...document.querySelectorAll('button, [role="button"]')].map(e => ({
300
+ const buttons = [...document.querySelectorAll('button, [role="button"], .semi-button, [class*="button"], [class*="Button"]')].map(e => ({
277
301
  text: (e.innerText || e.textContent || '').trim(),
278
302
  disabled: !!e.disabled || e.getAttribute('aria-disabled') === 'true' || e.className?.toString().includes('disabled'),
279
303
  })).filter(b => b.text).slice(0, 30);
@@ -388,12 +412,58 @@ export class DouyinAdapter {
388
412
  throw new Error(`PUBLISH_TIMEOUT: ${hint}`);
389
413
  }
390
414
 
391
- async _fillField(selector, value) {
415
+ async _fillField(selector, value, kind = 'content') {
392
416
  await humanPause(500, 1400, 'before-focus-field');
393
417
  const result = await this._cdp.send('Runtime.evaluate', {
394
418
  expression: `
395
419
  (function() {
396
- const el = document.querySelector(${JSON.stringify(selector)});
420
+ const selector = ${JSON.stringify(selector)};
421
+ const kind = ${JSON.stringify(kind)};
422
+ const visible = (el) => {
423
+ if (!el) return false;
424
+ const r = el.getBoundingClientRect();
425
+ const s = getComputedStyle(el);
426
+ return r.width > 0 && r.height > 0 && r.left > -1000 && s.display !== 'none' && s.visibility !== 'hidden';
427
+ };
428
+ const metaText = (el) => [
429
+ el.getAttribute('placeholder'),
430
+ el.getAttribute('data-placeholder'),
431
+ el.getAttribute('aria-label'),
432
+ el.getAttribute('title'),
433
+ el.className?.toString?.(),
434
+ el.closest('[class*="editor"], [class*="form"], [class*="field"], [class*="mention"], [class*="caption"]')?.innerText,
435
+ ].filter(Boolean).join(' ');
436
+ const candidates = [
437
+ ...document.querySelectorAll('input, textarea, [contenteditable="true"]')
438
+ ].filter(visible).filter(el => !el.disabled && el.getAttribute('aria-disabled') !== 'true');
439
+
440
+ function score(el) {
441
+ const text = metaText(el);
442
+ let s = 0;
443
+ if (kind === 'title') {
444
+ if (/标题|title|caption/i.test(text)) s += 120;
445
+ if (/添加作品标题|图文标题|作品标题/.test(text)) s += 80;
446
+ if (/描述|正文|description/i.test(text)) s -= 120;
447
+ if (el.tagName === 'INPUT') s += 20;
448
+ } else {
449
+ if (/描述|作品描述|添加作品描述|正文|description/i.test(text)) s += 120;
450
+ if (/标题|title|caption/i.test(text)) s -= 100;
451
+ if (el.tagName === 'TEXTAREA') s += 30;
452
+ if (el.getAttribute('contenteditable') === 'true') s += 15;
453
+ }
454
+ const r = el.getBoundingClientRect();
455
+ if (r.width < 40 || r.height < 12) s -= 40;
456
+ return s;
457
+ }
458
+
459
+ let el = null;
460
+ const direct = [...document.querySelectorAll(selector)].filter(visible);
461
+ if (direct.length) {
462
+ el = direct.sort((a, b) => score(b) - score(a))[0];
463
+ }
464
+ if (!el || score(el) <= 0) {
465
+ el = candidates.sort((a, b) => score(b) - score(a))[0];
466
+ }
397
467
  if (!el) return false;
398
468
  el.focus();
399
469
  if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
@@ -417,7 +487,7 @@ export class DouyinAdapter {
417
487
  `,
418
488
  returnByValue: true,
419
489
  });
420
- if (result.result?.value !== true) throw new Error(`PUBLISH_FAILED: 找不到输入框:${selector}`);
490
+ if (result.result?.value !== true) throw new Error(`PUBLISH_FAILED: 找不到${kind === 'title' ? '标题' : '描述'}输入框:${selector}`);
421
491
  await this._typeTextHumanLike(value);
422
492
  await this._cdp.send('Runtime.evaluate', {
423
493
  expression: `
@@ -446,10 +516,32 @@ export class DouyinAdapter {
446
516
  }
447
517
  }
448
518
 
449
- async _uploadFiles(filePaths) {
519
+ async _uploadFiles(filePaths, kind = 'image') {
450
520
  await humanPause(600, 1700, 'before-set-files');
451
521
  const result = await this._cdp.send('Runtime.evaluate', {
452
- expression: `document.querySelector(${JSON.stringify(FILE_INPUT_SELECTOR)})`,
522
+ expression: `
523
+ (function() {
524
+ const kind = ${JSON.stringify(kind)};
525
+ const inputs = [...document.querySelectorAll('input[type="file"]')];
526
+ if (!inputs.length) return null;
527
+ const score = (el) => {
528
+ const accept = (el.getAttribute('accept') || '').toLowerCase();
529
+ let s = 0;
530
+ if (kind === 'image') {
531
+ if (/image|jpg|jpeg|png|webp/.test(accept)) s += 100;
532
+ if (/video|mp4|mov|webm/.test(accept)) s -= 100;
533
+ } else {
534
+ if (/video|mp4|mov|webm/.test(accept)) s += 100;
535
+ if (/image|jpg|jpeg|png|webp/.test(accept)) s -= 100;
536
+ }
537
+ const containerText = (el.closest('div')?.innerText || '').slice(0, 200);
538
+ if (kind === 'image' && /图文|图片/.test(containerText)) s += 20;
539
+ if (kind === 'video' && /视频/.test(containerText)) s += 20;
540
+ return s;
541
+ };
542
+ return inputs.sort((a, b) => score(b) - score(a))[0] ?? null;
543
+ })()
544
+ `,
453
545
  returnByValue: false,
454
546
  });
455
547
  if (!result.result?.objectId) throw new Error('No file input found on page');
@@ -12,7 +12,7 @@
12
12
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
13
13
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
14
14
  import { z } from 'zod';
15
- import { existsSync, statSync, realpathSync } from 'fs';
15
+ import { existsSync, statSync, realpathSync, mkdirSync, writeFileSync } from 'fs';
16
16
  import path from 'path';
17
17
  import { getSession, closeSession } from './chrome-pool.js';
18
18
  import { XhsAdapter } from './adapters/xhs.js';
@@ -23,7 +23,9 @@ import { withProfileLock } from '../../src/profile-lock.js';
23
23
  const SERVER_URL = process.env.SERVER_URL ?? '';
24
24
  const MACHINE_API_KEY = process.env.MACHINE_API_KEY ?? '';
25
25
  const AGENT_ID = process.env.AGENT_ID ?? '';
26
+ const TEAM_ID = process.env.TEAM_ID ?? '';
26
27
  const WORKSPACE_DIR = process.env.WORKSPACE_DIR ?? process.cwd();
28
+ const TEAM_WORKSPACE_DIR = path.dirname(WORKSPACE_DIR);
27
29
 
28
30
  // ── Platform registry ──────────────────────────────────────────────────────────
29
31
 
@@ -115,6 +117,74 @@ const MEDIA_LIMITS = {
115
117
  kuaishou: { maxImages: 12, imageExts: ['.jpg', '.jpeg', '.png'], videoExts: ['.mp4', '.mov'] },
116
118
  };
117
119
 
120
+ function isInsideDir(filePath, dir) {
121
+ const rel = path.relative(dir, filePath);
122
+ return rel === '' || (!!rel && !rel.startsWith('..') && !path.isAbsolute(rel));
123
+ }
124
+
125
+ function decodeWorkspaceContent(content) {
126
+ if (typeof content !== 'string') throw new Error('workspace file content is not a string');
127
+ if (!content.startsWith('data:')) return Buffer.from(content, 'utf8');
128
+
129
+ const commaIdx = content.indexOf(',');
130
+ if (commaIdx === -1) throw new Error('invalid data URL in workspace file');
131
+ const header = content.slice(5, commaIdx);
132
+ const body = content.slice(commaIdx + 1);
133
+ if (header.split(';').includes('base64')) return Buffer.from(body, 'base64');
134
+ return Buffer.from(decodeURIComponent(body), 'utf8');
135
+ }
136
+
137
+ function workspacePathFromMediaPath(filePath, approvalData) {
138
+ if (!filePath) return null;
139
+
140
+ const normalized = filePath.replaceAll('\\', '/');
141
+ const virtualMatch = normalized.match(/^\/agent-workspace\/([^/]+)\/workspace\/(.+)$/);
142
+ if (virtualMatch) return { teamId: virtualMatch[1], relPath: virtualMatch[2] };
143
+
144
+ const workspaceSegmentMatch = normalized.match(/\/workspace\/((?:artifacts|notes|tmp)\/.+)$/);
145
+ if (workspaceSegmentMatch) {
146
+ return { teamId: approvalData?.teamId ?? TEAM_ID, relPath: workspaceSegmentMatch[1] };
147
+ }
148
+
149
+ if (!path.isAbsolute(filePath) && /^(artifacts|notes|tmp)\//.test(normalized)) {
150
+ return { teamId: approvalData?.teamId ?? TEAM_ID, relPath: normalized };
151
+ }
152
+
153
+ return null;
154
+ }
155
+
156
+ async function materializeWorkspaceMedia(filePath, approvalData) {
157
+ if (!filePath || existsSync(filePath)) return filePath;
158
+
159
+ const workspacePath = workspacePathFromMediaPath(filePath, approvalData);
160
+ if (!workspacePath?.teamId || !workspacePath.relPath) return filePath;
161
+
162
+ const localPath = path.resolve(TEAM_WORKSPACE_DIR, workspacePath.relPath);
163
+ const allowedRoots = ['artifacts', 'notes', 'tmp'].map(dir => path.join(TEAM_WORKSPACE_DIR, dir));
164
+ if (!allowedRoots.some(root => isInsideDir(localPath, root))) {
165
+ throw new Error(`workspace media path is outside allowed team workspace directories: ${filePath}`);
166
+ }
167
+
168
+ if (existsSync(localPath)) return localPath;
169
+
170
+ const data = await api(
171
+ 'GET',
172
+ `/team-memory?path=${encodeURIComponent(workspacePath.relPath)}&teamId=${encodeURIComponent(workspacePath.teamId)}`
173
+ );
174
+ mkdirSync(path.dirname(localPath), { recursive: true });
175
+ writeFileSync(localPath, decodeWorkspaceContent(data.content));
176
+ console.error(`[publisher] Materialized team workspace media ${workspacePath.relPath} -> ${localPath}`);
177
+ return localPath;
178
+ }
179
+
180
+ async function materializeMedia({ images = [], video, cover }, approvalData) {
181
+ return {
182
+ images: await Promise.all(images.map(filePath => materializeWorkspaceMedia(filePath, approvalData))),
183
+ video: video ? await materializeWorkspaceMedia(video, approvalData) : video,
184
+ cover: cover ? await materializeWorkspaceMedia(cover, approvalData) : cover,
185
+ };
186
+ }
187
+
118
188
  function validateLocalFile(filePath, { kind, required = false, allowedExts = [] }) {
119
189
  if (!filePath) {
120
190
  if (required) throw new Error(`${kind} file path is required`);
@@ -128,10 +198,15 @@ function validateLocalFile(filePath, { kind, required = false, allowedExts = []
128
198
  }
129
199
 
130
200
  const realFile = realpathSync(filePath);
131
- const realWorkspace = realpathSync(WORKSPACE_DIR);
132
- const rel = path.relative(realWorkspace, realFile);
133
- if (rel.startsWith('..') || path.isAbsolute(rel)) {
134
- throw new Error(`${kind} file must be inside the agent workspace: ${filePath}`);
201
+ const allowedRoots = [
202
+ realpathSync(WORKSPACE_DIR),
203
+ ...['artifacts', 'notes', 'tmp']
204
+ .map(dir => path.join(TEAM_WORKSPACE_DIR, dir))
205
+ .filter(existsSync)
206
+ .map(dir => realpathSync(dir)),
207
+ ];
208
+ if (!allowedRoots.some(root => isInsideDir(realFile, root))) {
209
+ throw new Error(`${kind} file must be inside the agent workspace or team shared artifacts/notes/tmp directory: ${filePath}`);
135
210
  }
136
211
 
137
212
  const stat = statSync(realFile);
@@ -191,8 +266,9 @@ images/video 字段填写本地绝对路径(在 agent workspace 目录下)
191
266
  async ({ platform, content_type, title, text, tags, images, video, cover, approval_action_id }) => {
192
267
  const label = PLATFORM_LABELS[platform] ?? platform;
193
268
  try {
194
- await validateApproval(approval_action_id, platform);
195
- const media = validateMedia({ platform, contentType: content_type, images: images ?? [], video, cover });
269
+ const approvalData = await validateApproval(approval_action_id, platform);
270
+ const localMedia = await materializeMedia({ images: images ?? [], video, cover }, approvalData);
271
+ const media = validateMedia({ platform, contentType: content_type, ...localMedia });
196
272
  const result = await withPublisherProfile(platform, async () => {
197
273
  const adapter = await getAdapter(platform);
198
274
  if (content_type === 'image_text') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightcone-ai/daemon",
3
- "version": "0.9.78",
3
+ "version": "0.9.79",
4
4
  "type": "module",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -238,6 +238,7 @@ export class AgentManager {
238
238
  credentialGrants,
239
239
  config,
240
240
  agentId,
241
+ teamId,
241
242
  workspaceDir,
242
243
  serverUrl: this.serverUrl,
243
244
  authToken: config.authToken,
@@ -97,6 +97,7 @@ export function buildCodexSpawn({
97
97
  credentialGrants,
98
98
  config,
99
99
  agentId,
100
+ teamId,
100
101
  workspaceDir,
101
102
  serverUrl,
102
103
  authToken: config.authToken || machineApiKey,
@@ -58,6 +58,7 @@ export function buildKimiSpawn({ config, agentId, teamId, workspaceDir, chatBrid
58
58
  credentialGrants,
59
59
  config,
60
60
  agentId,
61
+ teamId,
61
62
  workspaceDir,
62
63
  serverUrl,
63
64
  authToken: config.authToken || machineApiKey,
package/src/mcp-config.js CHANGED
@@ -18,7 +18,7 @@ function resolveSkillArg(arg, config) {
18
18
  return arg;
19
19
  }
20
20
 
21
- function baseEnvForServer(serverKey, { serverUrl, authToken, agentId, workspaceDir }) {
21
+ function baseEnvForServer(serverKey, { serverUrl, authToken, agentId, teamId, workspaceDir }) {
22
22
  if (serverKey === 'workspace-migrate') {
23
23
  return { SERVER_URL: serverUrl, MACHINE_API_KEY: authToken, AGENT_ID: agentId };
24
24
  }
@@ -27,6 +27,7 @@ function baseEnvForServer(serverKey, { serverUrl, authToken, agentId, workspaceD
27
27
  SERVER_URL: serverUrl,
28
28
  MACHINE_API_KEY: authToken,
29
29
  AGENT_ID: agentId,
30
+ TEAM_ID: teamId ?? '',
30
31
  WORKSPACE_DIR: workspaceDir,
31
32
  };
32
33
  }
@@ -38,6 +39,7 @@ export function buildSkillMcpServers({
38
39
  credentialGrants,
39
40
  config,
40
41
  agentId,
42
+ teamId,
41
43
  workspaceDir,
42
44
  serverUrl,
43
45
  authToken,
@@ -61,7 +63,7 @@ export function buildSkillMcpServers({
61
63
  args: resolvedArgs,
62
64
  env: {
63
65
  ...resolvedEnv,
64
- ...baseEnvForServer(mc.server, { serverUrl, authToken, agentId, workspaceDir }),
66
+ ...baseEnvForServer(mc.server, { serverUrl, authToken, agentId, teamId, workspaceDir }),
65
67
  },
66
68
  };
67
69
  }