@web-auto/webauto 0.1.8 → 0.1.11

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.
Files changed (43) hide show
  1. package/apps/desktop-console/dist/main/index.mjs +909 -105
  2. package/apps/desktop-console/dist/main/preload.mjs +3 -0
  3. package/apps/desktop-console/dist/renderer/index.html +9 -1
  4. package/apps/desktop-console/dist/renderer/index.js +796 -331
  5. package/apps/desktop-console/entry/ui-cli.mjs +59 -9
  6. package/apps/desktop-console/entry/ui-console.mjs +8 -3
  7. package/apps/webauto/entry/account.mjs +70 -9
  8. package/apps/webauto/entry/lib/account-detect.mjs +106 -25
  9. package/apps/webauto/entry/lib/account-store.mjs +122 -35
  10. package/apps/webauto/entry/lib/profilepool.mjs +45 -13
  11. package/apps/webauto/entry/lib/schedule-store.mjs +1 -25
  12. package/apps/webauto/entry/profilepool.mjs +45 -3
  13. package/apps/webauto/entry/schedule.mjs +44 -2
  14. package/apps/webauto/entry/weibo-unified.mjs +2 -2
  15. package/apps/webauto/entry/xhs-install.mjs +248 -52
  16. package/apps/webauto/entry/xhs-unified.mjs +33 -6
  17. package/bin/webauto.mjs +137 -5
  18. package/dist/modules/camo-runtime/src/utils/browser-service.mjs +4 -0
  19. package/dist/services/unified-api/server.js +5 -0
  20. package/dist/services/unified-api/task-state.js +2 -0
  21. package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +142 -14
  22. package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +16 -1
  23. package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +104 -0
  24. package/modules/camo-runtime/src/autoscript/runtime.mjs +14 -4
  25. package/modules/camo-runtime/src/autoscript/schema.mjs +9 -0
  26. package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +9 -2
  27. package/modules/camo-runtime/src/container/runtime-core/checkpoint.mjs +107 -1
  28. package/modules/camo-runtime/src/container/runtime-core/subscription.mjs +24 -2
  29. package/modules/camo-runtime/src/utils/browser-service.mjs +4 -0
  30. package/package.json +7 -3
  31. package/runtime/infra/utils/README.md +13 -0
  32. package/runtime/infra/utils/scripts/README.md +0 -0
  33. package/runtime/infra/utils/scripts/development/eval-in-session.mjs +40 -0
  34. package/runtime/infra/utils/scripts/development/highlight-search-containers.mjs +35 -0
  35. package/runtime/infra/utils/scripts/service/kill-port.mjs +24 -0
  36. package/runtime/infra/utils/scripts/service/start-api.mjs +103 -0
  37. package/runtime/infra/utils/scripts/service/start-browser-service.mjs +173 -0
  38. package/runtime/infra/utils/scripts/service/stop-api.mjs +30 -0
  39. package/runtime/infra/utils/scripts/service/stop-browser-service.mjs +104 -0
  40. package/runtime/infra/utils/scripts/test-services.mjs +94 -0
  41. package/scripts/bump-version.mjs +120 -0
  42. package/services/unified-api/server.ts +4 -0
  43. package/services/unified-api/task-state.ts +5 -0
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env node
2
+ import { execSync, spawn } from 'node:child_process';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { setTimeout as wait } from 'node:timers/promises';
7
+
8
+ function resolveWebautoRoot() {
9
+ const explicitHome = String(process.env.WEBAUTO_HOME || '').trim();
10
+ if (explicitHome) return path.resolve(explicitHome);
11
+
12
+ const legacyRoot = String(process.env.WEBAUTO_ROOT || process.env.WEBAUTO_PORTABLE_ROOT || '').trim();
13
+ if (legacyRoot) {
14
+ const normalized = path.resolve(legacyRoot);
15
+ const base = path.basename(normalized).toLowerCase();
16
+ if (base === '.webauto' || base === 'webauto') return normalized;
17
+ return path.join(normalized, '.webauto');
18
+ }
19
+
20
+ if (process.platform === 'win32') {
21
+ try {
22
+ if (fs.existsSync('D:\\')) return 'D:\\webauto';
23
+ } catch {
24
+ // ignore probing errors
25
+ }
26
+ }
27
+ return path.join(os.homedir(), '.webauto');
28
+ }
29
+
30
+ const RUN_DIR = path.join(resolveWebautoRoot(), 'run');
31
+ const PID_FILE = path.join(RUN_DIR, 'browser-service.pid');
32
+ const DEFAULT_HOST = process.env.WEBAUTO_BROWSER_HOST || '127.0.0.1';
33
+ const DEFAULT_PORT = Number(process.env.WEBAUTO_BROWSER_PORT || 7704);
34
+
35
+ function isAlive(pid) {
36
+ try {
37
+ process.kill(pid, 0);
38
+ return true;
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+
44
+ async function health(host = DEFAULT_HOST, port = DEFAULT_PORT) {
45
+ try {
46
+ const res = await fetch(`http://${host}:${port}/health`);
47
+ if (!res.ok) return false;
48
+ const body = await res.json().catch(() => ({}));
49
+ return Boolean(body?.ok ?? true);
50
+ } catch {
51
+ return false;
52
+ }
53
+ }
54
+
55
+ function parseArgs() {
56
+ const args = process.argv.slice(2);
57
+ const hostIdx = args.findIndex((item) => item === '--host');
58
+ const portIdx = args.findIndex((item) => item === '--port');
59
+ const host = hostIdx >= 0 && args[hostIdx + 1] ? String(args[hostIdx + 1]) : DEFAULT_HOST;
60
+ const port = portIdx >= 0 && args[portIdx + 1] ? Number(args[portIdx + 1]) : DEFAULT_PORT;
61
+ return {
62
+ host,
63
+ port: Number.isFinite(port) && port > 0 ? port : DEFAULT_PORT,
64
+ };
65
+ }
66
+
67
+ function latestMtimeMs(targetPath) {
68
+ if (!fs.existsSync(targetPath)) return 0;
69
+ const stat = fs.statSync(targetPath);
70
+ if (stat.isFile()) return Number(stat.mtimeMs || 0);
71
+ if (!stat.isDirectory()) return 0;
72
+ let latest = Number(stat.mtimeMs || 0);
73
+ const stack = [targetPath];
74
+ while (stack.length) {
75
+ const current = stack.pop();
76
+ if (!current) continue;
77
+ let entries = [];
78
+ try {
79
+ entries = fs.readdirSync(current, { withFileTypes: true });
80
+ } catch {
81
+ continue;
82
+ }
83
+ for (const entry of entries) {
84
+ const full = path.join(current, entry.name);
85
+ let entryStat;
86
+ try {
87
+ entryStat = fs.statSync(full);
88
+ } catch {
89
+ continue;
90
+ }
91
+ const mtime = Number(entryStat.mtimeMs || 0);
92
+ if (mtime > latest) latest = mtime;
93
+ if (entryStat.isDirectory()) stack.push(full);
94
+ }
95
+ }
96
+ return latest;
97
+ }
98
+
99
+ function shouldRebuild(distEntry) {
100
+ if (!fs.existsSync(distEntry)) return true;
101
+ if (String(process.env.WEBAUTO_SKIP_BUILD_CHECK || '') === '1') return false;
102
+ if (!fs.existsSync(path.resolve('tsconfig.services.json'))) return false;
103
+ const distMtime = Number(fs.statSync(distEntry).mtimeMs || 0);
104
+ const watchRoots = [
105
+ path.resolve('modules/camo-backend/src'),
106
+ path.resolve('modules/logging/src'),
107
+ ];
108
+ const latestSourceMtime = Math.max(...watchRoots.map((root) => latestMtimeMs(root)));
109
+ return latestSourceMtime > distMtime;
110
+ }
111
+
112
+ function ensureBuild() {
113
+ const distEntry = path.resolve('dist/modules/camo-backend/src/index.js');
114
+ if (!shouldRebuild(distEntry)) return distEntry;
115
+ console.log('[browser-service] backend entry missing/stale, running npm run -s build:services');
116
+ execSync('npm run -s build:services', { stdio: 'inherit' });
117
+ if (!fs.existsSync(distEntry)) {
118
+ throw new Error(`backend entry missing after build: ${distEntry}`);
119
+ }
120
+ return distEntry;
121
+ }
122
+
123
+ async function main() {
124
+ const { host, port } = parseArgs();
125
+ fs.mkdirSync(RUN_DIR, { recursive: true });
126
+
127
+ if (await health(host, port)) {
128
+ console.log(`Browser service already healthy on http://${host}:${port}`);
129
+ return;
130
+ }
131
+
132
+ if (fs.existsSync(PID_FILE)) {
133
+ try {
134
+ const oldPid = Number(fs.readFileSync(PID_FILE, 'utf8'));
135
+ if (oldPid && isAlive(oldPid)) {
136
+ console.log(`Browser service already running (pid=${oldPid}).`);
137
+ return;
138
+ }
139
+ } catch {
140
+ // stale pid file will be overwritten
141
+ }
142
+ }
143
+
144
+ const entry = ensureBuild();
145
+ const child = spawn(process.execPath, [entry], {
146
+ detached: true,
147
+ stdio: 'ignore',
148
+ env: {
149
+ ...process.env,
150
+ BROWSER_SERVICE_HOST: host,
151
+ BROWSER_SERVICE_PORT: String(port),
152
+ WEBAUTO_BROWSER_HOST: host,
153
+ WEBAUTO_BROWSER_PORT: String(port),
154
+ },
155
+ });
156
+ child.unref();
157
+ fs.writeFileSync(PID_FILE, String(child.pid));
158
+
159
+ for (let i = 0; i < 30; i += 1) {
160
+ if (await health(host, port)) {
161
+ console.log(`Browser service started (pid=${child.pid}) on http://${host}:${port}`);
162
+ return;
163
+ }
164
+ await wait(300);
165
+ }
166
+
167
+ throw new Error('browser service did not become healthy in time');
168
+ }
169
+
170
+ main().catch((err) => {
171
+ console.error(`[browser-service] start failed: ${err?.message || String(err)}`);
172
+ process.exit(1);
173
+ });
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, existsSync, unlinkSync } from 'node:fs';
3
+ import { spawnSync } from 'node:child_process';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+
7
+ const PID_FILE = process.env.WEBAUTO_API_PID_FILE
8
+ ? path.resolve(process.env.WEBAUTO_API_PID_FILE)
9
+ : path.join(os.tmpdir(), 'webauto-api.pid');
10
+
11
+ function killPid(pid){
12
+ if (process.platform === 'win32') {
13
+ try {
14
+ spawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true });
15
+ } catch {}
16
+ return;
17
+ }
18
+ try { process.kill(pid, 'SIGTERM'); } catch {}
19
+ setTimeout(() => { try { process.kill(pid, 'SIGKILL'); } catch {} }, 1000);
20
+ }
21
+
22
+ async function main(){
23
+ if (!existsSync(PID_FILE)) { console.log('No pid file.'); return; }
24
+ const pid = Number(readFileSync(PID_FILE,'utf8').trim());
25
+ if (pid>0) killPid(pid);
26
+ try { unlinkSync(PID_FILE); } catch {}
27
+ console.log('API stopped.');
28
+ }
29
+
30
+ main().catch(e=>{ console.error(e); process.exit(1); });
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+ import { execSync } from 'node:child_process';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { setTimeout as wait } from 'node:timers/promises';
7
+
8
+ const RUN_DIR = path.join(os.homedir(), '.webauto', 'run');
9
+ const PID_FILE = path.join(RUN_DIR, 'browser-service.pid');
10
+ const DEFAULT_PORT = Number(process.env.WEBAUTO_BROWSER_PORT || 7704);
11
+
12
+ function isAlive(pid) {
13
+ try {
14
+ process.kill(pid, 0);
15
+ return true;
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+
21
+ function killByPort(port) {
22
+ try {
23
+ if (process.platform === 'win32') {
24
+ const out = execSync(`netstat -ano | findstr :${port}`, { encoding: 'utf8' });
25
+ const pids = new Set();
26
+ out.split(/\r?\n/).forEach((line) => {
27
+ const match = line.trim().match(/\s(\d+)\s*$/);
28
+ if (match) pids.add(Number(match[1]));
29
+ });
30
+ for (const pid of pids) {
31
+ try {
32
+ execSync(`taskkill /F /PID ${pid}`, { stdio: 'ignore' });
33
+ } catch {
34
+ // ignore
35
+ }
36
+ }
37
+ return pids.size > 0;
38
+ }
39
+
40
+ const out = execSync(`lsof -ti :${port} || true`, { encoding: 'utf8' });
41
+ const pids = out.split(/\s+/).map((item) => Number(item.trim())).filter(Boolean);
42
+ for (const pid of pids) {
43
+ try {
44
+ process.kill(pid, 'SIGKILL');
45
+ } catch {
46
+ // ignore
47
+ }
48
+ }
49
+ return pids.length > 0;
50
+ } catch {
51
+ return false;
52
+ }
53
+ }
54
+
55
+ async function main() {
56
+ if (!fs.existsSync(PID_FILE)) {
57
+ const killed = killByPort(DEFAULT_PORT);
58
+ console.log(killed ? `Killed processes on :${DEFAULT_PORT}` : 'No PID file found. Service may not be running.');
59
+ return;
60
+ }
61
+
62
+ const pid = Number(fs.readFileSync(PID_FILE, 'utf8'));
63
+ if (!pid) {
64
+ fs.rmSync(PID_FILE, { force: true });
65
+ killByPort(DEFAULT_PORT);
66
+ console.log('Invalid PID file. Performed port cleanup.');
67
+ return;
68
+ }
69
+
70
+ if (!isAlive(pid)) {
71
+ fs.rmSync(PID_FILE, { force: true });
72
+ killByPort(DEFAULT_PORT);
73
+ console.log(`Process ${pid} is not running. Cleaned stale PID.`);
74
+ return;
75
+ }
76
+
77
+ try {
78
+ process.kill(pid, 'SIGTERM');
79
+ } catch {
80
+ // ignore
81
+ }
82
+
83
+ for (let i = 0; i < 15; i += 1) {
84
+ if (!isAlive(pid)) break;
85
+ await wait(200);
86
+ }
87
+
88
+ if (isAlive(pid)) {
89
+ try {
90
+ process.kill(pid, 'SIGKILL');
91
+ } catch {
92
+ // ignore
93
+ }
94
+ }
95
+
96
+ fs.rmSync(PID_FILE, { force: true });
97
+ killByPort(DEFAULT_PORT);
98
+ console.log(`Browser service stopped (pid=${pid}).`);
99
+ }
100
+
101
+ main().catch((err) => {
102
+ console.error(`[browser-service] stop failed: ${err?.message || String(err)}`);
103
+ process.exit(1);
104
+ });
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+ // 测试脚本:验证服务是否能正常启动
3
+ import { spawn, execSync } from 'node:child_process';
4
+ import { setTimeout as wait } from 'node:timers/promises';
5
+
6
+ function log(msg){ console.log(`[test-services] ${msg}`); }
7
+
8
+ async function testHealth(port, name, timeout = 10000) {
9
+ const startTime = Date.now();
10
+
11
+ while (Date.now() - startTime < timeout) {
12
+ try {
13
+ const response = await fetch(`http://127.0.0.1:${port}/health`);
14
+ if (response.ok) {
15
+ const data = await response.json();
16
+ log(`✅ ${name} (${port}): 健康检查通过 - ${JSON.stringify(data)}`);
17
+ return true;
18
+ }
19
+ } catch (e) {
20
+ // 继续等待
21
+ }
22
+ await wait(500);
23
+ }
24
+
25
+ log(`❌ ${name} (${port}): 健康检查失败`);
26
+ return false;
27
+ }
28
+
29
+ async function main() {
30
+ log('🧪 测试服务启动...');
31
+
32
+ // 1. 构建服务
33
+ try {
34
+ log('构建服务...');
35
+ execSync('npm run -s build:services', { stdio: 'inherit' });
36
+ log('✅ 构建完成');
37
+ } catch (e) {
38
+ log('❌ 构建失败');
39
+ process.exit(1);
40
+ }
41
+
42
+ // 2. 清理端口
43
+ const ports = [7704, 7705, 7706];
44
+ for (const port of ports) {
45
+ try {
46
+ execSync(`lsof -ti :${port} | xargs kill -9 || true`, { stdio: 'ignore' });
47
+ } catch {}
48
+ }
49
+
50
+ // 3. 测试 Unified API
51
+ log('测试 Unified API...');
52
+ const unifiedProc = spawn('node', ['dist/apps/webauto/server.js'], {
53
+ env: { ...process.env, WEBAUTO_RUNTIME_MODE: 'unified', WEBAUTO_UNIFIED_PORT: '7704' },
54
+ stdio: ['ignore', 'pipe', 'pipe']
55
+ });
56
+
57
+ unifiedProc.stdout.on('data', (data) => {
58
+ log(`[Unified API] ${data.toString().trim()}`);
59
+ });
60
+
61
+ unifiedProc.stderr.on('data', (data) => {
62
+ log(`[Unified API ERROR] ${data.toString().trim()}`);
63
+ });
64
+
65
+ unifiedProc.on('error', (err) => {
66
+ log(`❌ Unified API 启动错误: ${err.message}`);
67
+ });
68
+
69
+ const unifiedOk = await testHealth(7704, 'Unified API', 15000);
70
+
71
+ if (unifiedOk) {
72
+ // 4. 测试基本 API
73
+ try {
74
+ const response = await fetch('http://127.0.0.1:7704/v1/system/state');
75
+ const data = await response.json();
76
+ log(`📋 系统状态: ${JSON.stringify(data)}`);
77
+ } catch (e) {
78
+ log(`❌ 系统状态获取失败: ${e.message}`);
79
+ }
80
+ }
81
+
82
+ // 5. 清理
83
+ unifiedProc.kill('SIGTERM');
84
+ await wait(1000);
85
+ unifiedProc.kill('SIGKILL');
86
+
87
+ log('🧹 测试完成');
88
+ process.exit(unifiedOk ? 0 : 1);
89
+ }
90
+
91
+ main().catch(e => {
92
+ console.error('❌ 测试失败:', e.message);
93
+ process.exit(1);
94
+ });
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ const ROOT = path.resolve(__dirname, '..');
8
+ const ROOT_PACKAGE = path.join(ROOT, 'package.json');
9
+ const DESKTOP_PACKAGE = path.join(ROOT, 'apps', 'desktop-console', 'package.json');
10
+ const ROOT_LOCK = path.join(ROOT, 'package-lock.json');
11
+
12
+ function usage(exitCode = 0) {
13
+ console.log(`Usage:
14
+ node scripts/bump-version.mjs [patch|minor|major] [--json]
15
+
16
+ Examples:
17
+ node scripts/bump-version.mjs
18
+ node scripts/bump-version.mjs patch
19
+ node scripts/bump-version.mjs minor --json`);
20
+ process.exit(exitCode);
21
+ }
22
+
23
+ function readJson(filePath) {
24
+ return JSON.parse(readFileSync(filePath, 'utf8'));
25
+ }
26
+
27
+ function writeJson(filePath, json) {
28
+ writeFileSync(filePath, `${JSON.stringify(json, null, 2)}\n`, 'utf8');
29
+ }
30
+
31
+ function bumpSemver(version, kind) {
32
+ const match = String(version || '').trim().match(/^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
33
+ if (!match) {
34
+ throw new Error(`invalid semver: ${version}`);
35
+ }
36
+ let major = Number(match[1]);
37
+ let minor = Number(match[2]);
38
+ let patch = Number(match[3]);
39
+ if (kind === 'major') {
40
+ major += 1;
41
+ minor = 0;
42
+ patch = 0;
43
+ } else if (kind === 'minor') {
44
+ minor += 1;
45
+ patch = 0;
46
+ } else {
47
+ patch += 1;
48
+ }
49
+ return `${major}.${minor}.${patch}`;
50
+ }
51
+
52
+ function updatePackageVersion(filePath, nextVersion) {
53
+ const json = readJson(filePath);
54
+ const prev = String(json.version || '').trim();
55
+ if (!prev) throw new Error(`missing version in ${filePath}`);
56
+ json.version = nextVersion;
57
+ writeJson(filePath, json);
58
+ return prev;
59
+ }
60
+
61
+ function updateRootLockVersion(filePath, nextVersion) {
62
+ if (!existsSync(filePath)) return false;
63
+ const lock = readJson(filePath);
64
+ lock.version = nextVersion;
65
+ if (lock.packages && lock.packages['']) {
66
+ lock.packages[''].version = nextVersion;
67
+ }
68
+ writeJson(filePath, lock);
69
+ return true;
70
+ }
71
+
72
+ function main() {
73
+ const argv = process.argv.slice(2);
74
+ if (argv.includes('--help') || argv.includes('-h')) usage(0);
75
+
76
+ const jsonMode = argv.includes('--json');
77
+ const kindArg = argv.find((arg) => !arg.startsWith('-')) || 'patch';
78
+ const kind = String(kindArg || 'patch').trim().toLowerCase();
79
+ if (!['patch', 'minor', 'major'].includes(kind)) {
80
+ console.error(`[version] unsupported bump type: ${kind}`);
81
+ usage(2);
82
+ }
83
+
84
+ const rootPkg = readJson(ROOT_PACKAGE);
85
+ const prevRootVersion = String(rootPkg.version || '').trim();
86
+ if (!prevRootVersion) {
87
+ throw new Error(`missing version in ${ROOT_PACKAGE}`);
88
+ }
89
+ const nextVersion = bumpSemver(prevRootVersion, kind);
90
+
91
+ const prevDesktopVersion = updatePackageVersion(DESKTOP_PACKAGE, nextVersion);
92
+ const prevVersion = updatePackageVersion(ROOT_PACKAGE, nextVersion);
93
+ const lockUpdated = updateRootLockVersion(ROOT_LOCK, nextVersion);
94
+
95
+ const result = {
96
+ ok: true,
97
+ kind,
98
+ previous: prevVersion,
99
+ next: nextVersion,
100
+ desktopPrevious: prevDesktopVersion,
101
+ desktopNext: nextVersion,
102
+ lockUpdated,
103
+ files: [
104
+ ROOT_PACKAGE,
105
+ DESKTOP_PACKAGE,
106
+ ...(lockUpdated ? [ROOT_LOCK] : []),
107
+ ],
108
+ };
109
+
110
+ if (jsonMode) {
111
+ console.log(JSON.stringify(result, null, 2));
112
+ return;
113
+ }
114
+ console.log(`[version] bump ${kind}: ${prevVersion} -> ${nextVersion}`);
115
+ console.log(`[version] updated: ${ROOT_PACKAGE}`);
116
+ console.log(`[version] updated: ${DESKTOP_PACKAGE}`);
117
+ if (lockUpdated) console.log(`[version] updated: ${ROOT_LOCK}`);
118
+ }
119
+
120
+ main();
@@ -296,11 +296,13 @@ class UnifiedApiServer {
296
296
  if (existing) return existing;
297
297
  const profileId = String(seed?.profileId || 'unknown').trim() || 'unknown';
298
298
  const keyword = String(seed?.keyword || '').trim();
299
+ const uiTriggerId = String(seed?.uiTriggerId || seed?.triggerId || '').trim();
299
300
  const phase = normalizeTaskPhase(seed?.phase);
300
301
  return this.taskRegistry.createTask({
301
302
  runId: normalizedRunId,
302
303
  profileId,
303
304
  keyword,
305
+ uiTriggerId,
304
306
  phase,
305
307
  });
306
308
  };
@@ -311,11 +313,13 @@ class UnifiedApiServer {
311
313
  const phase = normalizeTaskPhase(payload?.phase);
312
314
  const profileId = String(payload?.profileId || '').trim();
313
315
  const keyword = String(payload?.keyword || '').trim();
316
+ const uiTriggerId = String(payload?.uiTriggerId || payload?.triggerId || '').trim();
314
317
  const details = payload?.details && typeof payload.details === 'object' ? payload.details : undefined;
315
318
  const patch: any = {};
316
319
  if (phase !== 'unknown') patch.phase = phase;
317
320
  if (profileId) patch.profileId = profileId;
318
321
  if (keyword) patch.keyword = keyword;
322
+ if (uiTriggerId) patch.uiTriggerId = uiTriggerId;
319
323
  if (details) patch.details = details;
320
324
  if (Object.keys(patch).length > 0) {
321
325
  this.taskRegistry.updateTask(normalizedRunId, patch);
@@ -37,10 +37,12 @@ export interface TaskState {
37
37
  runId: string;
38
38
  profileId: string;
39
39
  keyword: string;
40
+ uiTriggerId?: string;
40
41
  phase: TaskPhase;
41
42
  status: TaskStatus;
42
43
  progress: TaskProgress;
43
44
  stats: TaskStats;
45
+ createdAt: number;
44
46
  startedAt: number;
45
47
  updatedAt: number;
46
48
  completedAt?: number;
@@ -71,6 +73,7 @@ class TaskStateRegistry {
71
73
  runId: string;
72
74
  profileId: string;
73
75
  keyword: string;
76
+ uiTriggerId?: string;
74
77
  phase?: TaskPhase;
75
78
  }): TaskState {
76
79
  const now = Date.now();
@@ -78,6 +81,7 @@ class TaskStateRegistry {
78
81
  runId: partial.runId,
79
82
  profileId: partial.profileId,
80
83
  keyword: partial.keyword,
84
+ uiTriggerId: partial.uiTriggerId ? String(partial.uiTriggerId).trim() : undefined,
81
85
  phase: partial.phase || 'unknown',
82
86
  status: 'starting',
83
87
  progress: { total: 0, processed: 0, failed: 0 },
@@ -89,6 +93,7 @@ class TaskStateRegistry {
89
93
  imagesDownloaded: 0,
90
94
  ocrProcessed: 0,
91
95
  },
96
+ createdAt: now,
92
97
  startedAt: now,
93
98
  updatedAt: now,
94
99
  details: {