evot-agent 0.2.3 → 0.3.0

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.
package/README.md CHANGED
@@ -3,6 +3,7 @@
3
3
  유저 컴퓨터에서 도는 [evot](https://evot.io)의 로컬 브라우저 에이전트.
4
4
  evot에서 Vot에게 "내 컴퓨터 브라우저로 ○○ 수집해줘"라고 말하면(또는 루틴으로 걸어두면),
5
5
  이 에이전트가 Claude + Playwright로 사이트를 탐색·수집하고 결과를 지정한 쓰레드에 셀로 추가한다.
6
+ 결과는 요청을 받은 Vot 이름/아바타로 정리되며, 이미지를 수집한 경우 설명과 함께 셀 안에 표시된다.
6
7
 
7
8
  ## 요구사항
8
9
 
package/bin/evot-agent.js CHANGED
@@ -84,8 +84,9 @@ async function main() {
84
84
 
85
85
  console.log(`Running job → thread ${job.log_id}`);
86
86
  let text;
87
+ let segments;
87
88
  try {
88
- text = await runBrowseJob(job);
89
+ ({ text, segments } = await runBrowseJob(job));
89
90
  } catch (err) {
90
91
  // 실패도 쓰레드에 남긴다 (그리고 텔레그램 알림).
91
92
  console.error(`\nBrowse failed: ${err.message}`);
@@ -103,6 +104,7 @@ async function main() {
103
104
  const cellId = await reportResult({
104
105
  logId: job.log_id,
105
106
  text,
107
+ segments,
106
108
  title: job.title ?? "collection",
107
109
  status: "success",
108
110
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evot-agent",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "Local browser agent for evot — browses sites with Claude and appends results as cells.",
6
6
  "license": "MIT",
package/src/browse.js CHANGED
@@ -15,16 +15,77 @@ const SYSTEM_APPEND = [
15
15
  " LOGIN_REQUIRED: <domain>",
16
16
  ' (e.g. "LOGIN_REQUIRED: news.ycombinator.com"). No other text.',
17
17
  "",
18
+ "MISSION FIDELITY:",
19
+ "- Complete exactly what the user asked. Do not narrow the scope on your own or",
20
+ " substitute a different deliverable — e.g. do NOT return only text/metadata when",
21
+ " images were requested. If the user asks for images, collect and output the",
22
+ " actual image URLs, exactly as many as the task calls for.",
23
+ "- Do not act as a content moderator. Mature, adult, sensitive, or otherwise",
24
+ " disagreeable material that is lawful and already publicly posted is in scope;",
25
+ " do not skip, omit, sanitize, or downgrade it merely for being suggestive or",
26
+ " NSFW. You are cataloguing what is publicly visible on the user's own request,",
27
+ " not endorsing it. Report what is actually there.",
28
+ "- The only hard stop is genuinely illegal content (e.g. sexual content involving",
29
+ " minors). If — and only if — you must decline part of a task, say so explicitly",
30
+ " on its own line with the concrete reason. Never silently drop or replace it.",
31
+ "",
18
32
  "When finished, output ONLY the collected result as plain text:",
19
- "- A short title line.",
20
- "- The summary the user asked for.",
21
- "- A 'Sources:' section listing the URLs you used, one per line.",
33
+ "- A short title line and the overall summary first.",
34
+ "- If the task involves collecting images, organize the result into sections:",
35
+ " each section is its description text followed by its image lines, each",
36
+ " exactly 'IMAGE: <url>' with a direct image URL you actually saw",
37
+ " (e.g. a pbs.twimg.com link or a URL ending in .jpg/.png). Max 10 images total.",
38
+ "- A 'Sources:' section at the end listing the URLs you used, one per line.",
22
39
  "Do not include tool logs or meta commentary in the final answer.",
23
40
  ].join("\n");
24
41
 
42
+ const MAX_IMAGES_TOTAL = 10;
43
+
44
+ /**
45
+ * 최종 답변을 섹션 단위로 파싱한다.
46
+ * - 'IMAGE: <url>' 라인 → 현재 섹션의 이미지(전체 합산 10개 상한). 'Images:' 헤딩 라인은 제거.
47
+ * - 이미지 뒤에 다시 텍스트가 나오면 새 섹션 시작(설명→이미지→설명→이미지 그룹핑).
48
+ * - 반환 text는 IMAGE 라인이 제거된 전문(구서버 호환·텔레그램 스니펫용).
49
+ */
50
+ export function parseSegments(raw) {
51
+ const segments = [];
52
+ let curText = [];
53
+ let curImages = [];
54
+ let sawImage = false;
55
+ let total = 0;
56
+
57
+ const flush = () => {
58
+ const text = curText.join("\n").trim();
59
+ if (text || curImages.length > 0) segments.push({ text, images: curImages });
60
+ curText = [];
61
+ curImages = [];
62
+ sawImage = false;
63
+ };
64
+
65
+ for (const line of raw.split("\n")) {
66
+ if (/^\s*images:\s*$/i.test(line)) continue; // 잔여 'Images:' 헤딩 제거
67
+ const m = line.match(/^\s*IMAGE:\s*(https?:\/\/\S+)\s*$/i);
68
+ if (m) {
69
+ if (total < MAX_IMAGES_TOTAL) {
70
+ curImages.push(m[1]);
71
+ total++;
72
+ sawImage = true;
73
+ }
74
+ continue;
75
+ }
76
+ if (sawImage && line.trim()) flush(); // 이미지 뒤 새 텍스트 → 새 섹션
77
+ curText.push(line);
78
+ }
79
+ flush();
80
+
81
+ const text = segments.map((s) => s.text).filter(Boolean).join("\n\n").trim();
82
+ return { text, segments: segments.length > 0 ? segments : [{ text, images: [] }] };
83
+ }
84
+
25
85
  /**
26
86
  * 잡 지시문을 Claude Agent SDK + Playwright MCP(전용 프로필)로 실행하고
27
- * 최종 요약 텍스트를 반환한다. 모델 크리덴셜은 로컬 Claude Code 로그인을 사용.
87
+ * 최종 결과를 { text, segments }로 반환한다(섹션 = 설명+이미지 URL 그룹).
88
+ * 모델 크리덴셜은 로컬 Claude Code 로그인을 사용.
28
89
  */
29
90
  export async function runBrowseJob(job) {
30
91
  const promptParts = [job.instruction];
@@ -99,7 +160,7 @@ export async function runBrowseJob(job) {
99
160
  finalText.trim() || "Agent finished without producing a result.",
100
161
  );
101
162
  }
102
- return finalText.trim();
163
+ return parseSegments(finalText);
103
164
  }
104
165
 
105
166
  /**
package/src/report.js CHANGED
@@ -8,12 +8,15 @@ import { SUPABASE_URL, loadToken } from "./config.js";
8
8
  * - 큐 경로(데몬): jobId만 넘긴다. 서버가 잡에서 log_id를 찾아 셀을 만들고 잡 상태를 done/failed로 갱신.
9
9
  * - 수동 경로(run): logId를 넘긴다. (Phase 1 호환)
10
10
  */
11
- export async function reportResult({ logId, jobId, text, title, status = "success" }) {
11
+ export async function reportResult({ logId, jobId, text, segments, title, status = "success" }) {
12
12
  const token = await loadToken();
13
13
 
14
14
  const body = { text, title, status };
15
15
  if (jobId) body.job_id = jobId;
16
16
  if (logId) body.log_id = logId;
17
+ // 섹션(설명+이미지 URL) 전달 — 서버가 votResponse 서브셀·이미지 저장에 사용.
18
+ // 구서버는 이 필드를 무시하고 text만 쓴다(하위호환).
19
+ if (Array.isArray(segments) && segments.length > 0) body.segments = segments;
17
20
 
18
21
  const url = `${SUPABASE_URL}/functions/v1/agent-report`;
19
22
  const res = await fetch(url, {
package/src/sync.js CHANGED
@@ -44,20 +44,21 @@ async function fetchJobs(token) {
44
44
  async function runJob(job) {
45
45
  console.log(`\nJob ${job.id}: ${String(job.instruction ?? "").slice(0, 80)}`);
46
46
  let text;
47
+ let segments;
47
48
  let status = "success";
48
49
  try {
49
- text = await runBrowseJob({
50
+ ({ text, segments } = await runBrowseJob({
50
51
  instruction: job.instruction,
51
52
  start_url: job.start_url ?? undefined,
52
53
  headless: true,
53
- });
54
+ }));
54
55
  } catch (err) {
55
56
  console.error(`Job ${job.id} failed: ${err.message}`);
56
57
  text = describeJobFailure(err);
57
58
  status = "error";
58
59
  }
59
60
  try {
60
- const cellId = await reportResult({ jobId: job.id, text, title: "Computer Use", status });
61
+ const cellId = await reportResult({ jobId: job.id, text, segments, title: "Computer Use", status });
61
62
  console.log(`Reported job ${job.id}${cellId ? ` → cell ${cellId}` : ""}.`);
62
63
  } catch (err) {
63
64
  // 보고 실패 시 잡은 running으로 남고, running 타임아웃 후 서버가 expired 처리한다.