@lightcone-ai/daemon 0.9.76 → 0.9.78

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.
@@ -37,7 +37,14 @@ const CREATOR_HOME_URL = 'https://creator.douyin.com/creator-micro/home';
37
37
  const IMAGE_PUBLISH_URL = 'https://creator.douyin.com/creator-micro/content/upload-image-text';
38
38
  const VIDEO_PUBLISH_URL = 'https://creator.douyin.com/creator-micro/content/upload';
39
39
 
40
- const FILE_INPUT_SELECTOR = 'input[type="file"]';
40
+ const FILE_INPUT_SELECTOR = [
41
+ 'input[type="file"][accept*="image"]',
42
+ 'input[type="file"][accept*=".jpg"]',
43
+ 'input[type="file"][accept*=".jpeg"]',
44
+ 'input[type="file"][accept*=".png"]',
45
+ 'input[type="file"][accept*=".webp"]',
46
+ 'input[type="file"]',
47
+ ].join(', ');
41
48
  const TITLE_SELECTOR = 'input[placeholder*="标题"], textarea[placeholder*="标题"]';
42
49
  const CONTENT_SELECTOR = '[contenteditable="true"], textarea[placeholder*="描述"], textarea[placeholder*="作品描述"], [placeholder*="添加作品描述"]';
43
50
 
@@ -79,7 +86,7 @@ export class DouyinAdapter {
79
86
  await humanPause(2500, 5500, 'composer-ready');
80
87
 
81
88
  if (images.length > 0) {
82
- await this._waitForSelector(FILE_INPUT_SELECTOR, 15000);
89
+ await this._ensureUploaderReady('image');
83
90
  await humanPause(900, 2200, 'before-upload');
84
91
  await this._uploadFiles(images);
85
92
  await this._waitForUploadSettled(images.length, 120_000);
@@ -114,7 +121,7 @@ export class DouyinAdapter {
114
121
  await this._assertReadyForPublish();
115
122
  await humanPause(2500, 5500, 'composer-ready');
116
123
 
117
- await this._waitForSelector(FILE_INPUT_SELECTOR, 15000);
124
+ await this._ensureUploaderReady('video');
118
125
  await humanPause(900, 2200, 'before-upload');
119
126
  await this._uploadFiles([video]);
120
127
  await this._waitForUploadSettled(1, 180_000);
@@ -152,6 +159,7 @@ export class DouyinAdapter {
152
159
  await this._cdp.send('Page.navigate', { url });
153
160
  await this._waitForCreatorShell(20_000);
154
161
  await humanPause(1800, 4200, 'after-navigation');
162
+ await this._ensureComposer(kind);
155
163
  }
156
164
 
157
165
  async _waitForCreatorShell(timeoutMs = 20_000) {
@@ -187,6 +195,66 @@ export class DouyinAdapter {
187
195
  throw new Error(`Timeout waiting for selector: ${selector}`);
188
196
  }
189
197
 
198
+ async _ensureComposer(kind) {
199
+ if (await this._waitForUploaderOrEditor(8000)) return;
200
+
201
+ const labels = kind === 'video'
202
+ ? ['发布视频', '上传视频', '视频', '发布作品', '上传']
203
+ : ['发布图文', '上传图文', '图文', '发布图片', '上传图片', '图片', '发布作品', '上传'];
204
+
205
+ for (const label of labels) {
206
+ const clicked = await this._clickByText(label);
207
+ if (!clicked) continue;
208
+ if (await this._waitForUploaderOrEditor(12_000)) return;
209
+ }
210
+
211
+ await this._cdp.send('Page.navigate', { url: kind === 'video' ? VIDEO_PUBLISH_URL : IMAGE_PUBLISH_URL });
212
+ if (await this._waitForUploaderOrEditor(15_000)) return;
213
+
214
+ const state = await this._inspectPage();
215
+ throw new Error(`PUBLISH_FAILED: 找不到抖音${kind === 'image' ? '图文' : '视频'}上传入口。${this._formatPageHint(state)}`);
216
+ }
217
+
218
+ async _ensureUploaderReady(kind) {
219
+ if (await this._waitForSelectorQuiet(FILE_INPUT_SELECTOR, 8000)) return;
220
+ await this._ensureComposer(kind);
221
+ if (await this._waitForSelectorQuiet(FILE_INPUT_SELECTOR, 12_000)) return;
222
+
223
+ const labels = kind === 'video'
224
+ ? ['上传视频', '点击上传', '选择视频', '上传']
225
+ : ['上传图片', '添加图片', '点击上传', '选择图片', '上传'];
226
+ for (const label of labels) {
227
+ const clicked = await this._clickByText(label);
228
+ if (!clicked) continue;
229
+ if (await this._waitForSelectorQuiet(FILE_INPUT_SELECTOR, 8000)) return;
230
+ }
231
+
232
+ const state = await this._inspectPage();
233
+ throw new Error(`PUBLISH_FAILED: 找不到抖音文件上传输入框。${this._formatPageHint(state)}`);
234
+ }
235
+
236
+ async _waitForUploaderOrEditor(timeoutMs) {
237
+ const deadline = Date.now() + timeoutMs;
238
+ while (Date.now() < deadline) {
239
+ const state = await this._inspectPage();
240
+ if (state.hasLoginHint) {
241
+ throw new Error(`LOGIN_EXPIRED: 当前页面 ${state.url},请重新扫码连接抖音`);
242
+ }
243
+ if (state.hasUploader || state.hasPublishEditor) return true;
244
+ await sleep(500);
245
+ }
246
+ return false;
247
+ }
248
+
249
+ async _waitForSelectorQuiet(selector, timeoutMs = 8000) {
250
+ try {
251
+ await this._waitForSelector(selector, timeoutMs);
252
+ return true;
253
+ } catch {
254
+ return false;
255
+ }
256
+ }
257
+
190
258
  async _inspectPage() {
191
259
  const result = await this._cdp.send('Runtime.evaluate', {
192
260
  expression: `
@@ -208,12 +276,28 @@ export class DouyinAdapter {
208
276
  const buttons = [...document.querySelectorAll('button, [role="button"]')].map(e => ({
209
277
  text: (e.innerText || e.textContent || '').trim(),
210
278
  disabled: !!e.disabled || e.getAttribute('aria-disabled') === 'true' || e.className?.toString().includes('disabled'),
211
- }));
279
+ })).filter(b => b.text).slice(0, 30);
280
+ const links = [...document.querySelectorAll('a')].map(e => ({
281
+ text: (e.innerText || e.textContent || '').trim(),
282
+ href: e.href || '',
283
+ })).filter(a => a.text).slice(0, 30);
284
+ const uploadish = [...document.querySelectorAll('input, button, [role="button"], a, div, span')]
285
+ .map(e => ({
286
+ tag: e.tagName,
287
+ text: (e.innerText || e.textContent || e.getAttribute('aria-label') || e.getAttribute('title') || '').trim(),
288
+ type: e.getAttribute('type') || '',
289
+ accept: e.getAttribute('accept') || '',
290
+ cls: e.className?.toString?.() || '',
291
+ }))
292
+ .filter(e => /上传|图文|图片|视频|作品|file|upload/i.test([e.text, e.type, e.accept, e.cls].join(' ')))
293
+ .slice(0, 40);
212
294
  return {
213
295
  url,
214
296
  text: text.slice(0, 5000),
215
297
  errors,
216
298
  buttons,
299
+ links,
300
+ uploadish,
217
301
  hasLoginHint: /登录|扫码|验证码|手机号/.test(text) && !/发布|作品管理|创作服务平台/.test(text),
218
302
  isCreatorLoggedIn: /创作服务平台|创作者中心|发布作品|作品管理|内容管理/.test(text),
219
303
  hasUploader: !!document.querySelector(${JSON.stringify(FILE_INPUT_SELECTOR)}),
@@ -224,7 +308,19 @@ export class DouyinAdapter {
224
308
  `,
225
309
  returnByValue: true,
226
310
  });
227
- return result.result?.value ?? { url: await this._getUrl(), text: '', errors: [], buttons: [] };
311
+ return result.result?.value ?? { url: await this._getUrl(), text: '', errors: [], buttons: [], links: [], uploadish: [] };
312
+ }
313
+
314
+ _formatPageHint(state) {
315
+ const buttons = (state.buttons ?? []).map(b => b.text).filter(Boolean).slice(0, 12).join(' / ');
316
+ const links = (state.links ?? []).map(a => a.text).filter(Boolean).slice(0, 12).join(' / ');
317
+ const uploadish = (state.uploadish ?? [])
318
+ .map(e => [e.tag, e.text, e.type, e.accept].filter(Boolean).join(':'))
319
+ .filter(Boolean)
320
+ .slice(0, 12)
321
+ .join(' / ');
322
+ const text = (state.text ?? '').replace(/\s+/g, ' ').slice(0, 240);
323
+ return `当前 url=${state.url}; buttons=${buttons || 'none'}; links=${links || 'none'}; uploadCandidates=${uploadish || 'none'}; text=${text || 'empty'}`;
228
324
  }
229
325
 
230
326
  async _assertReadyForPublish() {
@@ -368,8 +464,23 @@ export class DouyinAdapter {
368
464
  const result = await this._cdp.send('Runtime.evaluate', {
369
465
  expression: `
370
466
  (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)}));
467
+ const visible = (el) => {
468
+ const r = el.getBoundingClientRect();
469
+ const s = getComputedStyle(el);
470
+ return r.width > 0 && r.height > 0 && r.left > -1000 && s.display !== 'none' && s.visibility !== 'hidden';
471
+ };
472
+ const els = [...document.querySelectorAll('button, [role="button"], a, div, span')]
473
+ .filter(visible)
474
+ .filter(e => {
475
+ const txt = (e.innerText || e.textContent || e.getAttribute('aria-label') || e.getAttribute('title') || '').trim();
476
+ if (!txt) return false;
477
+ return txt === ${JSON.stringify(text)} || txt.includes(${JSON.stringify(text)});
478
+ });
479
+ const el = els.sort((a, b) => {
480
+ const ar = a.getBoundingClientRect();
481
+ const br = b.getBoundingClientRect();
482
+ return (ar.width * ar.height) - (br.width * br.height);
483
+ })[0];
373
484
  if (!el) return null;
374
485
  const r = el.getBoundingClientRect();
375
486
  return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightcone-ai/daemon",
3
- "version": "0.9.76",
3
+ "version": "0.9.78",
4
4
  "type": "module",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -113,46 +113,8 @@ server.tool('send_message', 'Send a message to a team, DM, or thread', {
113
113
  return { content: [{ type: 'text', text: `Sent. messageId=${data.messageId} threadTarget=${data.threadTarget}` }] };
114
114
  });
115
115
 
116
- // ── read_history ──────────────────────────────────────────────────────────────
117
- server.tool('read_history', 'Read message history from a team, DM, or thread. Supports pagination via before/after and context jumps via around.', {
118
- team: z.string().describe('Target: #team-name | dm:@agentName | #team-name:shortMsgId'),
119
- limit: z.number().optional().describe('Max messages to return (default 50, max 100)'),
120
- around: z.union([z.string(), z.number()]).optional().describe('Center the result window around a messageId or seq number'),
121
- before: z.number().optional().describe('Return messages before this seq'),
122
- after: z.number().optional().describe('Return messages after this seq'),
123
- }, async ({ team, limit, around, before, after }) => {
124
- const params = new URLSearchParams({ team, limit: String(Math.min(limit ?? 50, 100)) });
125
- if (around != null) params.set('around', String(around));
126
- if (before != null) params.set('before', String(before));
127
- if (after != null) params.set('after', String(after));
128
- const data = await api('GET', `/history?${params}`);
129
- const msgs = data.messages ?? [];
130
- if (msgs.length === 0) return { content: [{ type: 'text', text: 'No messages found.' }] };
131
-
132
- const text = msgs.map(m => {
133
- const senderType = m.senderType === 'agent' ? ' type=agent' : '';
134
- const taskSuffix = m.taskStatus ? ` [task #${m.taskNumber} status=${m.taskStatus}${m.taskAssigneeId ? ` assignee=${m.taskAssigneeType}:${m.taskAssigneeId}` : ''}]` : '';
135
- return `[seq=${m.seq} msg=${m.id} time=${m.createdAt}${senderType}] @${m.senderName}: ${m.content}${taskSuffix}`;
136
- }).join('\n');
137
-
138
- let footer = '';
139
- if (data.has_more && msgs.length > 0) {
140
- const minSeq = msgs[0].seq;
141
- const maxSeq = msgs[msgs.length - 1].seq;
142
- if (around) {
143
- footer = `\n\n--- Use before=${minSeq} to load older or after=${maxSeq} to load newer. ---`;
144
- } else if (after) {
145
- footer = `\n\n--- ${msgs.length} messages shown. Use after=${maxSeq} for more. ---`;
146
- } else {
147
- footer = `\n\n--- ${msgs.length} messages shown. Use before=${minSeq} for older. ---`;
148
- }
149
- }
150
-
151
- return { content: [{ type: 'text', text: `## History for ${team} (${msgs.length} messages)\n\n${text}${footer}` }] };
152
- });
153
-
154
116
  // ── search_messages ──────────────────────────────────────────────────────────
155
- server.tool('search_messages', 'Search messages within a specific team. You must specify the team. Use this to find relevant conversations, then inspect a hit with read_history(team=..., around=messageId).', {
117
+ server.tool('search_messages', 'Search messages within a specific team. You must specify the team. Use this to find relevant conversations by keyword.', {
156
118
  query: z.string().describe('Search query'),
157
119
  team: z.string().describe('Target team to search within, e.g. "#general", "dm:@richard". Required — you may only search teams you are a member of.'),
158
120
  limit: z.number().optional().describe('Max results (default 10, max 20)'),
@@ -171,7 +133,6 @@ server.tool('search_messages', 'Search messages within a specific team. You must
171
133
  `team: #${r.teamName}`,
172
134
  `sender: @${r.senderName}${r.senderType === 'agent' ? ' (agent)' : ''}`,
173
135
  `content: ${r.snippet}`,
174
- `next: read_history(team="#${r.teamName}", around="${r.id}", limit=20)`,
175
136
  ].join('\n')).join('\n\n');
176
137
  return { content: [{ type: 'text', text: `## Search Results for "${trimmed}" (${data.results.length} results)\n\n${formatted}` }] };
177
138
  } catch (err) {
@@ -15,15 +15,14 @@ You have MCP tools from the "chat" server. Use ONLY these for communication:
15
15
  1. **${t("check_messages")}** — Non-blocking check for new messages. Use freely during work — at natural breakpoints or after notifications.
16
16
  2. **${t("send_message")}** — Send a message to a team or DM.
17
17
  3. **${t("list_server")}** — List all teams in this server, which ones you have joined, plus all agents and humans.
18
- 4. **${t("read_history")}** — Read past messages from a team, DM, or thread. Supports \`before\` / \`after\` pagination and \`around\` for centered context.
19
- 5. **${t("search_messages")}** — Search messages visible to you, then inspect a hit with \`${t("read_history")}\`.
20
- 6. **${t("list_tasks")}** — View a team's task board.
21
- 7. **${t("create_tasks")}** — Create new task-messages in a team (supports batch; equivalent to sending a new message and publishing it as a task-message, not claiming it for yourself).
22
- 8. **${t("claim_tasks")}** — Claim tasks by number (supports batch, handles conflicts).
23
- 9. **${t("unclaim_task")}** — Release your claim on a task.
24
- 10. **${t("update_task_status")}** — Change a task's status (e.g. to in_review or done).
25
- 11. **${t("upload_image")}** — Upload an image file for a temporary public preview URL in a chat message. This is not durable artifact storage.
26
- 12. **${t("view_file")}** — Download an attached image by its attachment ID so you can view it. Use when messages contain image attachments.
18
+ 4. **${t("search_messages")}** — Search messages visible to you by keyword.
19
+ 5. **${t("list_tasks")}** — View a team's task board.
20
+ 6. **${t("create_tasks")}** — Create new task-messages in a team (supports batch; equivalent to sending a new message and publishing it as a task-message, not claiming it for yourself).
21
+ 7. **${t("claim_tasks")}** — Claim tasks by number (supports batch, handles conflicts).
22
+ 8. **${t("unclaim_task")}** — Release your claim on a task.
23
+ 9. **${t("update_task_status")}** — Change a task's status (e.g. to in_review or done).
24
+ 10. **${t("upload_image")}** — Upload an image file for a temporary public preview URL in a chat message. This is not durable artifact storage.
25
+ 11. **${t("view_file")}** — Download an attached image by its attachment ID so you can view it. Use when messages contain image attachments.
27
26
 
28
27
  CRITICAL RULES:
29
28
  - Always communicate through ${t("send_message")}. This is your only output method.
@@ -73,7 +72,6 @@ Threads are sub-conversations attached to a specific message. They let you discu
73
72
  - When you receive a message from a thread (the target has a \`:shortid\` suffix), **always reply using that same target** to keep the conversation in the thread.
74
73
  - **Start a new thread**: Use the \`msg=\` field from the header as the thread suffix. For example, if you see \`[target=#general msg=a1b2c3d4 ...]\`, reply with \`send_message(target="#general:a1b2c3d4", content="...")\`. The thread will be auto-created if it doesn't exist yet.
75
74
  - When you send a message, the response includes the message ID. You can use it to start a thread on your own message.
76
- - You can read thread history: \`read_history(team="#general:a1b2c3d4")\`
77
75
  - Threads cannot be nested — you cannot start a thread inside a thread.
78
76
 
79
77
  ### Discovering people and teams
@@ -87,12 +85,6 @@ Each team has a **name** and optionally a **description** that define its purpos
87
85
  - **Stay on topic** — when proactively sharing results or updates, post in the team most relevant to the work. Don't scatter messages across unrelated teams.
88
86
  - If unsure where something belongs, call \`list_server\` to review team descriptions.
89
87
 
90
- ### Reading history
91
-
92
- \`read_history(team="#team-name")\` or \`read_history(team="dm:@peer-name")\` or \`read_history(team="#team:shortid")\`
93
-
94
- To jump directly to a specific hit with nearby context, use \`read_history(team="...", around="messageId")\` or \`read_history(team="...", around=12345)\`.
95
-
96
88
  ### Tasks
97
89
 
98
90
  When someone sends a message that asks you to do something — fix a bug, write code, review a PR, deploy, investigate an issue — that is work. Claim it before you start.
@@ -106,8 +98,6 @@ When someone sends a message that asks you to do something — fix a bug, write
106
98
 
107
99
  Only top-level team / DM messages can become tasks. Messages inside threads are discussion context — reply there, but keep claims and conversions to top-level messages.
108
100
 
109
- \`read_history\` shows messages in their current state. If a message was later converted to a task, it will show the \`[task #N ...]\` suffix.
110
-
111
101
  **Status flow:** \`todo\` → \`in_progress\` → \`in_review\` → \`done\`
112
102
 
113
103
  **Assignee** is independent from status — a task can be claimed or unclaimed at any status except \`done\`.