@lightcone-ai/daemon 0.9.77 → 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,12 +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';
39
-
40
- const FILE_INPUT_SELECTOR = 'input[type="file"]';
41
- const TITLE_SELECTOR = 'input[placeholder*="标题"], textarea[placeholder*="标题"]';
42
- const CONTENT_SELECTOR = '[contenteditable="true"], textarea[placeholder*="描述"], textarea[placeholder*="作品描述"], [placeholder*="添加作品描述"]';
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`;
40
+
41
+ const IMAGE_FILE_INPUT_SELECTOR = [
42
+ 'input[type="file"][accept*="image"]',
43
+ 'input[type="file"][accept*=".jpg"]',
44
+ 'input[type="file"][accept*=".jpeg"]',
45
+ 'input[type="file"][accept*=".png"]',
46
+ 'input[type="file"][accept*=".webp"]',
47
+ 'input[type="file"][accept="image"]',
48
+ 'input[type="file"]',
49
+ ].join(', ');
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(', ');
43
73
 
44
74
  const ERROR_SELECTORS = [
45
75
  '[class*="error"]',
@@ -79,18 +109,18 @@ export class DouyinAdapter {
79
109
  await humanPause(2500, 5500, 'composer-ready');
80
110
 
81
111
  if (images.length > 0) {
82
- await this._waitForSelector(FILE_INPUT_SELECTOR, 15000);
112
+ await this._ensureUploaderReady('image');
83
113
  await humanPause(900, 2200, 'before-upload');
84
- await this._uploadFiles(images);
114
+ await this._uploadFiles(images, 'image');
85
115
  await this._waitForUploadSettled(images.length, 120_000);
86
116
  await humanPause(2500, 5500, 'after-upload');
87
117
  }
88
118
 
89
119
  const fullText = formatTextWithTags(text, tags);
90
- await this._fillField(CONTENT_SELECTOR, fullText);
120
+ await this._fillField(CONTENT_SELECTOR, fullText, 'content');
91
121
 
92
122
  if (title) {
93
- await this._fillField(TITLE_SELECTOR, title);
123
+ await this._fillField(TITLE_SELECTOR, title, 'title');
94
124
  }
95
125
 
96
126
  await humanPause(4500, 9000, 'before-publish-check');
@@ -114,9 +144,9 @@ export class DouyinAdapter {
114
144
  await this._assertReadyForPublish();
115
145
  await humanPause(2500, 5500, 'composer-ready');
116
146
 
117
- await this._waitForSelector(FILE_INPUT_SELECTOR, 15000);
147
+ await this._ensureUploaderReady('video');
118
148
  await humanPause(900, 2200, 'before-upload');
119
- await this._uploadFiles([video]);
149
+ await this._uploadFiles([video], 'video');
120
150
  await this._waitForUploadSettled(1, 180_000);
121
151
  await humanPause(3000, 6500, 'after-video-upload');
122
152
 
@@ -125,10 +155,10 @@ export class DouyinAdapter {
125
155
  }
126
156
 
127
157
  const fullText = formatTextWithTags(text, tags);
128
- await this._fillField(CONTENT_SELECTOR, fullText);
158
+ await this._fillField(CONTENT_SELECTOR, fullText, 'content');
129
159
 
130
160
  if (title) {
131
- await this._fillField(TITLE_SELECTOR, title);
161
+ await this._fillField(TITLE_SELECTOR, title, 'title');
132
162
  }
133
163
 
134
164
  await humanPause(4500, 9000, 'before-publish-check');
@@ -152,6 +182,7 @@ export class DouyinAdapter {
152
182
  await this._cdp.send('Page.navigate', { url });
153
183
  await this._waitForCreatorShell(20_000);
154
184
  await humanPause(1800, 4200, 'after-navigation');
185
+ await this._ensureComposer(kind);
155
186
  }
156
187
 
157
188
  async _waitForCreatorShell(timeoutMs = 20_000) {
@@ -187,6 +218,67 @@ export class DouyinAdapter {
187
218
  throw new Error(`Timeout waiting for selector: ${selector}`);
188
219
  }
189
220
 
221
+ async _ensureComposer(kind) {
222
+ if (await this._waitForUploaderOrEditor(8000)) return;
223
+
224
+ const labels = kind === 'video'
225
+ ? ['发布视频', '上传视频', '视频', '发布作品', '上传']
226
+ : ['发布图文', '上传图文', '图文', '发布图片', '上传图片', '图片', '发布作品', '上传'];
227
+
228
+ for (const label of labels) {
229
+ const clicked = await this._clickByText(label);
230
+ if (!clicked) continue;
231
+ if (await this._waitForUploaderOrEditor(12_000)) return;
232
+ }
233
+
234
+ await this._cdp.send('Page.navigate', { url: kind === 'video' ? VIDEO_PUBLISH_URL : IMAGE_PUBLISH_URL });
235
+ if (await this._waitForUploaderOrEditor(15_000)) return;
236
+
237
+ const state = await this._inspectPage();
238
+ throw new Error(`PUBLISH_FAILED: 找不到抖音${kind === 'image' ? '图文' : '视频'}上传入口。${this._formatPageHint(state)}`);
239
+ }
240
+
241
+ async _ensureUploaderReady(kind) {
242
+ const selector = kind === 'video' ? VIDEO_FILE_INPUT_SELECTOR : IMAGE_FILE_INPUT_SELECTOR;
243
+ if (await this._waitForSelectorQuiet(selector, 8000)) return;
244
+ await this._ensureComposer(kind);
245
+ if (await this._waitForSelectorQuiet(selector, 12_000)) return;
246
+
247
+ const labels = kind === 'video'
248
+ ? ['上传视频', '点击上传', '选择视频', '上传']
249
+ : ['上传图片', '添加图片', '点击上传', '选择图片', '上传'];
250
+ for (const label of labels) {
251
+ const clicked = await this._clickByText(label);
252
+ if (!clicked) continue;
253
+ if (await this._waitForSelectorQuiet(selector, 8000)) return;
254
+ }
255
+
256
+ const state = await this._inspectPage();
257
+ throw new Error(`PUBLISH_FAILED: 找不到抖音文件上传输入框。${this._formatPageHint(state)}`);
258
+ }
259
+
260
+ async _waitForUploaderOrEditor(timeoutMs) {
261
+ const deadline = Date.now() + timeoutMs;
262
+ while (Date.now() < deadline) {
263
+ const state = await this._inspectPage();
264
+ if (state.hasLoginHint) {
265
+ throw new Error(`LOGIN_EXPIRED: 当前页面 ${state.url},请重新扫码连接抖音`);
266
+ }
267
+ if (state.hasUploader || state.hasPublishEditor) return true;
268
+ await sleep(500);
269
+ }
270
+ return false;
271
+ }
272
+
273
+ async _waitForSelectorQuiet(selector, timeoutMs = 8000) {
274
+ try {
275
+ await this._waitForSelector(selector, timeoutMs);
276
+ return true;
277
+ } catch {
278
+ return false;
279
+ }
280
+ }
281
+
190
282
  async _inspectPage() {
191
283
  const result = await this._cdp.send('Runtime.evaluate', {
192
284
  expression: `
@@ -205,15 +297,31 @@ export class DouyinAdapter {
205
297
  .map(t => t.trim())
206
298
  .filter(Boolean)
207
299
  .slice(0, 5);
208
- 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 => ({
209
301
  text: (e.innerText || e.textContent || '').trim(),
210
302
  disabled: !!e.disabled || e.getAttribute('aria-disabled') === 'true' || e.className?.toString().includes('disabled'),
211
- }));
303
+ })).filter(b => b.text).slice(0, 30);
304
+ const links = [...document.querySelectorAll('a')].map(e => ({
305
+ text: (e.innerText || e.textContent || '').trim(),
306
+ href: e.href || '',
307
+ })).filter(a => a.text).slice(0, 30);
308
+ const uploadish = [...document.querySelectorAll('input, button, [role="button"], a, div, span')]
309
+ .map(e => ({
310
+ tag: e.tagName,
311
+ text: (e.innerText || e.textContent || e.getAttribute('aria-label') || e.getAttribute('title') || '').trim(),
312
+ type: e.getAttribute('type') || '',
313
+ accept: e.getAttribute('accept') || '',
314
+ cls: e.className?.toString?.() || '',
315
+ }))
316
+ .filter(e => /上传|图文|图片|视频|作品|file|upload/i.test([e.text, e.type, e.accept, e.cls].join(' ')))
317
+ .slice(0, 40);
212
318
  return {
213
319
  url,
214
320
  text: text.slice(0, 5000),
215
321
  errors,
216
322
  buttons,
323
+ links,
324
+ uploadish,
217
325
  hasLoginHint: /登录|扫码|验证码|手机号/.test(text) && !/发布|作品管理|创作服务平台/.test(text),
218
326
  isCreatorLoggedIn: /创作服务平台|创作者中心|发布作品|作品管理|内容管理/.test(text),
219
327
  hasUploader: !!document.querySelector(${JSON.stringify(FILE_INPUT_SELECTOR)}),
@@ -224,7 +332,19 @@ export class DouyinAdapter {
224
332
  `,
225
333
  returnByValue: true,
226
334
  });
227
- return result.result?.value ?? { url: await this._getUrl(), text: '', errors: [], buttons: [] };
335
+ return result.result?.value ?? { url: await this._getUrl(), text: '', errors: [], buttons: [], links: [], uploadish: [] };
336
+ }
337
+
338
+ _formatPageHint(state) {
339
+ const buttons = (state.buttons ?? []).map(b => b.text).filter(Boolean).slice(0, 12).join(' / ');
340
+ const links = (state.links ?? []).map(a => a.text).filter(Boolean).slice(0, 12).join(' / ');
341
+ const uploadish = (state.uploadish ?? [])
342
+ .map(e => [e.tag, e.text, e.type, e.accept].filter(Boolean).join(':'))
343
+ .filter(Boolean)
344
+ .slice(0, 12)
345
+ .join(' / ');
346
+ const text = (state.text ?? '').replace(/\s+/g, ' ').slice(0, 240);
347
+ return `当前 url=${state.url}; buttons=${buttons || 'none'}; links=${links || 'none'}; uploadCandidates=${uploadish || 'none'}; text=${text || 'empty'}`;
228
348
  }
229
349
 
230
350
  async _assertReadyForPublish() {
@@ -292,12 +412,58 @@ export class DouyinAdapter {
292
412
  throw new Error(`PUBLISH_TIMEOUT: ${hint}`);
293
413
  }
294
414
 
295
- async _fillField(selector, value) {
415
+ async _fillField(selector, value, kind = 'content') {
296
416
  await humanPause(500, 1400, 'before-focus-field');
297
417
  const result = await this._cdp.send('Runtime.evaluate', {
298
418
  expression: `
299
419
  (function() {
300
- 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
+ }
301
467
  if (!el) return false;
302
468
  el.focus();
303
469
  if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
@@ -321,7 +487,7 @@ export class DouyinAdapter {
321
487
  `,
322
488
  returnByValue: true,
323
489
  });
324
- 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}`);
325
491
  await this._typeTextHumanLike(value);
326
492
  await this._cdp.send('Runtime.evaluate', {
327
493
  expression: `
@@ -350,10 +516,32 @@ export class DouyinAdapter {
350
516
  }
351
517
  }
352
518
 
353
- async _uploadFiles(filePaths) {
519
+ async _uploadFiles(filePaths, kind = 'image') {
354
520
  await humanPause(600, 1700, 'before-set-files');
355
521
  const result = await this._cdp.send('Runtime.evaluate', {
356
- 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
+ `,
357
545
  returnByValue: false,
358
546
  });
359
547
  if (!result.result?.objectId) throw new Error('No file input found on page');
@@ -368,8 +556,23 @@ export class DouyinAdapter {
368
556
  const result = await this._cdp.send('Runtime.evaluate', {
369
557
  expression: `
370
558
  (function() {
371
- const els = [...document.querySelectorAll('button, [role="button"]')];
372
- const el = els.find(e => e.innerText?.trim() === ${JSON.stringify(text)} || e.textContent?.trim() === ${JSON.stringify(text)} || e.innerText?.includes(${JSON.stringify(text)}));
559
+ const visible = (el) => {
560
+ const r = el.getBoundingClientRect();
561
+ const s = getComputedStyle(el);
562
+ return r.width > 0 && r.height > 0 && r.left > -1000 && s.display !== 'none' && s.visibility !== 'hidden';
563
+ };
564
+ const els = [...document.querySelectorAll('button, [role="button"], a, div, span')]
565
+ .filter(visible)
566
+ .filter(e => {
567
+ const txt = (e.innerText || e.textContent || e.getAttribute('aria-label') || e.getAttribute('title') || '').trim();
568
+ if (!txt) return false;
569
+ return txt === ${JSON.stringify(text)} || txt.includes(${JSON.stringify(text)});
570
+ });
571
+ const el = els.sort((a, b) => {
572
+ const ar = a.getBoundingClientRect();
573
+ const br = b.getBoundingClientRect();
574
+ return (ar.width * ar.height) - (br.width * br.height);
575
+ })[0];
373
576
  if (!el) return null;
374
577
  const r = el.getBoundingClientRect();
375
578
  return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
@@ -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.77",
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
  }