@lightcone-ai/daemon 0.9.70 → 0.9.72

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.
@@ -2,6 +2,7 @@
2
2
  * Douyin (抖音) publisher adapter.
3
3
  * Uses 抖音创作服务平台: https://creator.douyin.com
4
4
  */
5
+ import { formatTextWithTags } from '../text.js';
5
6
 
6
7
  function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
7
8
 
@@ -51,7 +52,7 @@ export class DouyinAdapter {
51
52
  await sleep(3000);
52
53
  }
53
54
 
54
- const fullText = `${text}${tags.length > 0 ? '\n' + tags.map(t => `#${t}`).join(' ') : ''}`;
55
+ const fullText = formatTextWithTags(text, tags);
55
56
  await this._fillField('[class*="content"] [contenteditable], [placeholder*="添加作品描述"]', fullText);
56
57
 
57
58
  if (title) {
@@ -78,7 +79,7 @@ export class DouyinAdapter {
78
79
  await this._uploadFiles([video], 'video');
79
80
  await this._waitForText('上传成功', 120000);
80
81
 
81
- const fullText = `${text}${tags.length > 0 ? '\n' + tags.map(t => `#${t}`).join(' ') : ''}`;
82
+ const fullText = formatTextWithTags(text, tags);
82
83
  await this._fillField('[placeholder*="添加作品描述"], [class*="content"] [contenteditable]', fullText);
83
84
 
84
85
  if (title) {
@@ -2,6 +2,7 @@
2
2
  * Kuaishou (快手) publisher adapter.
3
3
  * Uses 快手创作者平台: https://cp.kuaishou.com
4
4
  */
5
+ import { formatTextWithTags } from '../text.js';
5
6
 
6
7
  function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
7
8
 
@@ -55,7 +56,7 @@ export class KuaishouAdapter {
55
56
  await this._fillField('[placeholder*="标题"]', title);
56
57
  }
57
58
 
58
- const fullText = `${text}${tags.length > 0 ? '\n' + tags.map(t => `#${t}`).join(' ') : ''}`;
59
+ const fullText = formatTextWithTags(text, tags);
59
60
  await this._fillField('[placeholder*="描述"], [contenteditable]', fullText);
60
61
 
61
62
  await sleep(1000);
@@ -82,7 +83,7 @@ export class KuaishouAdapter {
82
83
  await this._fillField('[placeholder*="标题"]', title);
83
84
  }
84
85
 
85
- const fullText = `${text}${tags.length > 0 ? '\n' + tags.map(t => `#${t}`).join(' ') : ''}`;
86
+ const fullText = formatTextWithTags(text, tags);
86
87
  await this._fillField('[placeholder*="描述"], [contenteditable]', fullText);
87
88
 
88
89
  await sleep(1000);
@@ -2,11 +2,13 @@
2
2
  * XHS (小红书) publisher adapter.
3
3
  * Uses deterministic CDP operations — no AI vision required.
4
4
  */
5
+ import { formatTextWithTags } from '../text.js';
5
6
 
6
7
  function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
7
8
  function randomInt(min, max) { return Math.floor(min + Math.random() * (max - min + 1)); }
8
9
 
9
10
  const PACE_SCALE = Math.max(0.2, Number(process.env.PUBLISHER_PACE_SCALE ?? '1') || 1);
11
+ const DRY_RUN = process.env.XHS_PUBLISH_DRY_RUN !== '0';
10
12
  async function humanPause(minMs, maxMs, label = '') {
11
13
  const ms = Math.round(randomInt(minMs, maxMs) * PACE_SCALE);
12
14
  if (label) console.error(`[XhsAdapter] pause ${label}: ${ms}ms`);
@@ -96,15 +98,19 @@ export class XhsAdapter {
96
98
  await humanPause(1200, 2800, 'after-title');
97
99
  }
98
100
 
99
- // Fill content text + tags
100
- const fullText = tags.length > 0 ? `${text}\n${tags.map(t => `#${t}`).join(' ')}` : text;
101
+ // Fill content text and only append tags that are not already present.
102
+ const fullText = formatTextWithTags(text, tags);
101
103
  await this._fillField(CONTENT_SELECTOR, fullText);
102
104
 
103
105
  await humanPause(4500, 9000, 'before-publish-check');
104
106
  await this._assertNoBlockingErrors();
105
107
  await this._assertPublishButtonReady();
106
108
 
107
- // Click publish button
109
+ if (DRY_RUN) {
110
+ console.error('[XhsAdapter] XHS_PUBLISH_DRY_RUN enabled; skipping final publish click.');
111
+ return { success: true, dry_run: true, post_url: null };
112
+ }
113
+
108
114
  await humanPause(1200, 3000, 'before-publish-click');
109
115
  const clicked = await this._clickByText('发布');
110
116
  if (!clicked) throw new Error('PUBLISH_FAILED: 找不到发布按钮,页面结构可能已变化');
@@ -137,13 +143,18 @@ export class XhsAdapter {
137
143
  await humanPause(1200, 2800, 'after-title');
138
144
  }
139
145
 
140
- const fullText = tags.length > 0 ? `${text}\n${tags.map(t => `#${t}`).join(' ')}` : text;
146
+ const fullText = formatTextWithTags(text, tags);
141
147
  await this._fillField(CONTENT_SELECTOR, fullText);
142
148
 
143
149
  await humanPause(4500, 9000, 'before-publish-check');
144
150
  await this._assertNoBlockingErrors();
145
151
  await this._assertPublishButtonReady();
146
152
 
153
+ if (DRY_RUN) {
154
+ console.error('[XhsAdapter] XHS_PUBLISH_DRY_RUN enabled; skipping final publish click.');
155
+ return { success: true, dry_run: true, post_url: null };
156
+ }
157
+
147
158
  await humanPause(1200, 3000, 'before-publish-click');
148
159
  const clicked = await this._clickByText('发布');
149
160
  if (!clicked) throw new Error('PUBLISH_FAILED: 找不到发布按钮,页面结构可能已变化');
@@ -205,6 +205,11 @@ images/video 字段填写本地绝对路径(在 agent workspace 目录下)
205
205
  });
206
206
 
207
207
  await completeApproval(approval_action_id, true, result, null);
208
+ if (result?.dry_run) {
209
+ return {
210
+ content: [{ type: 'text', text: `✓ ${label}发布流程已完成到发布前一步,已跳过最终“发布”点击。` }],
211
+ };
212
+ }
208
213
  const postUrl = result.post_url ? `\n发布链接: ${result.post_url}` : '';
209
214
  return {
210
215
  content: [{ type: 'text', text: `✓ 已成功发布到${label}。${postUrl}` }],
@@ -0,0 +1,27 @@
1
+ function escapeRegExp(value) {
2
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3
+ }
4
+
5
+ export function normalizeTags(tags = []) {
6
+ const seen = new Set();
7
+ const normalized = [];
8
+ for (const raw of tags) {
9
+ const tag = String(raw ?? '').trim().replace(/^[##]+/, '').trim();
10
+ if (!tag || seen.has(tag)) continue;
11
+ seen.add(tag);
12
+ normalized.push(tag);
13
+ }
14
+ return normalized;
15
+ }
16
+
17
+ export function formatTextWithTags(text, tags = []) {
18
+ const base = String(text ?? '').trimEnd();
19
+ const missingTags = normalizeTags(tags).filter(tag => {
20
+ const pattern = new RegExp(`(^|[\\s,,。!?!?::;;、])(?:#|#)${escapeRegExp(tag)}(?=$|[\\s,,。!?!?::;;、])`);
21
+ return !pattern.test(base);
22
+ });
23
+
24
+ if (missingTags.length === 0) return base;
25
+ const suffix = missingTags.map(tag => `#${tag}`).join(' ');
26
+ return base ? `${base}\n${suffix}` : suffix;
27
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightcone-ai/daemon",
3
- "version": "0.9.70",
3
+ "version": "0.9.72",
4
4
  "type": "module",
5
5
  "main": "src/index.js",
6
6
  "bin": {