adyou 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 +64 -0
- package/dist/adapters/google.d.ts +41 -0
- package/dist/adapters/google.js +301 -0
- package/dist/adapters/index.d.ts +6 -0
- package/dist/adapters/index.js +17 -0
- package/dist/adapters/meta.d.ts +33 -0
- package/dist/adapters/meta.js +275 -0
- package/dist/adapters/mock.d.ts +28 -0
- package/dist/adapters/mock.js +67 -0
- package/dist/adapters/types.d.ts +100 -0
- package/dist/adapters/types.js +20 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +315 -0
- package/dist/core/brief.d.ts +12 -0
- package/dist/core/brief.js +71 -0
- package/dist/core/check.d.ts +11 -0
- package/dist/core/check.js +55 -0
- package/dist/core/creatives/copy.d.ts +14 -0
- package/dist/core/creatives/copy.js +52 -0
- package/dist/core/creatives/images.d.ts +9 -0
- package/dist/core/creatives/images.js +89 -0
- package/dist/core/creatives/render.d.ts +25 -0
- package/dist/core/creatives/render.js +157 -0
- package/dist/core/creatives/specs.d.ts +40 -0
- package/dist/core/creatives/specs.js +51 -0
- package/dist/core/llm.d.ts +23 -0
- package/dist/core/llm.js +77 -0
- package/dist/core/money.d.ts +9 -0
- package/dist/core/money.js +34 -0
- package/dist/core/ops.d.ts +121 -0
- package/dist/core/ops.js +407 -0
- package/dist/core/optimize.d.ts +27 -0
- package/dist/core/optimize.js +56 -0
- package/dist/core/plan.d.ts +22 -0
- package/dist/core/plan.js +59 -0
- package/dist/core/report.d.ts +30 -0
- package/dist/core/report.js +34 -0
- package/dist/core/site.d.ts +30 -0
- package/dist/core/site.js +80 -0
- package/dist/core/state.d.ts +162 -0
- package/dist/core/state.js +110 -0
- package/dist/core/tracking.d.ts +19 -0
- package/dist/core/tracking.js +45 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +19 -0
- package/dist/mcp.d.ts +1 -0
- package/dist/mcp.js +130 -0
- package/package.json +63 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type SiteSnapshot = {
|
|
2
|
+
url: string;
|
|
3
|
+
finalUrl: string;
|
|
4
|
+
status: number;
|
|
5
|
+
title: string;
|
|
6
|
+
description: string;
|
|
7
|
+
lang: string;
|
|
8
|
+
ogImage: string | null;
|
|
9
|
+
logo: string | null;
|
|
10
|
+
themeColor: string | null;
|
|
11
|
+
colors: string[];
|
|
12
|
+
headings: string[];
|
|
13
|
+
prices: string[];
|
|
14
|
+
text: string;
|
|
15
|
+
hasFbq: boolean;
|
|
16
|
+
pixelIds: string[];
|
|
17
|
+
gtagIds: string[];
|
|
18
|
+
ga4Ids: string[];
|
|
19
|
+
csp: string | null;
|
|
20
|
+
utmPreserved: boolean | null;
|
|
21
|
+
};
|
|
22
|
+
export declare function fetchHtml(url: string, timeoutMs?: number): Promise<{
|
|
23
|
+
html: string;
|
|
24
|
+
status: number;
|
|
25
|
+
finalUrl: string;
|
|
26
|
+
headers: Headers;
|
|
27
|
+
}>;
|
|
28
|
+
export declare function decode(s: string): string;
|
|
29
|
+
export declare function stripHtml(html: string): string;
|
|
30
|
+
export declare function readSite(url: string): Promise<SiteSnapshot>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0 Safari/537.36 adpilot/0.1';
|
|
2
|
+
export async function fetchHtml(url, timeoutMs = 20_000) {
|
|
3
|
+
const res = await fetch(url, { headers: { 'user-agent': UA, accept: 'text/html,*/*' }, redirect: 'follow', signal: AbortSignal.timeout(timeoutMs) });
|
|
4
|
+
const html = await res.text();
|
|
5
|
+
return { html, status: res.status, finalUrl: res.url || url, headers: res.headers };
|
|
6
|
+
}
|
|
7
|
+
function attr(tag, name) { const m = tag.match(new RegExp(`\\s${name}\\s*=\\s*("([^"]*)"|'([^']*)'|([^\\s>]+))`, 'i')); return m ? (m[2] ?? m[3] ?? m[4] ?? '') : null; }
|
|
8
|
+
function meta(html, key) {
|
|
9
|
+
const tags = html.match(/<meta\b[^>]*>/gi) || [];
|
|
10
|
+
for (const t of tags) {
|
|
11
|
+
const n = (attr(t, 'property') || attr(t, 'name') || '').toLowerCase();
|
|
12
|
+
if (n === key.toLowerCase())
|
|
13
|
+
return decode(attr(t, 'content') || '');
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
export function decode(s) { return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'|'/g, "'").replace(/ /g, ' ').replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d))); }
|
|
18
|
+
function abs(base, href) { if (!href)
|
|
19
|
+
return null; try {
|
|
20
|
+
return new URL(href, base).toString();
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
} }
|
|
25
|
+
export function stripHtml(html) {
|
|
26
|
+
return decode(html.replace(/<script[\s\S]*?<\/script>/gi, ' ').replace(/<style[\s\S]*?<\/style>/gi, ' ').replace(/<noscript[\s\S]*?<\/noscript>/gi, ' ').replace(/<[^>]+>/g, ' ')).replace(/\s+/g, ' ').trim();
|
|
27
|
+
}
|
|
28
|
+
export async function readSite(url) {
|
|
29
|
+
const { html, status, finalUrl, headers } = await fetchHtml(url);
|
|
30
|
+
const title = decode((html.match(/<title[^>]*>([\s\S]*?)<\/title>/i)?.[1] || meta(html, 'og:title') || '').trim());
|
|
31
|
+
const description = meta(html, 'description') || meta(html, 'og:description') || '';
|
|
32
|
+
const lang = (html.match(/<html[^>]*\slang\s*=\s*["']?([a-zA-Z-]+)/i)?.[1] || meta(html, 'og:locale') || '').toLowerCase().slice(0, 2);
|
|
33
|
+
const ogImage = abs(finalUrl, meta(html, 'og:image'));
|
|
34
|
+
const links = html.match(/<link\b[^>]*>/gi) || [];
|
|
35
|
+
let logo = null;
|
|
36
|
+
for (const l of links) {
|
|
37
|
+
const rel = (attr(l, 'rel') || '').toLowerCase();
|
|
38
|
+
if (/apple-touch-icon/.test(rel)) {
|
|
39
|
+
logo = abs(finalUrl, attr(l, 'href'));
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (!logo)
|
|
44
|
+
for (const l of links) {
|
|
45
|
+
const rel = (attr(l, 'rel') || '').toLowerCase();
|
|
46
|
+
if (/\bicon\b/.test(rel) && !/mask/.test(rel)) {
|
|
47
|
+
const h = attr(l, 'href');
|
|
48
|
+
if (h && !/\.ico(\?|$)/i.test(h)) {
|
|
49
|
+
logo = abs(finalUrl, h);
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!logo) {
|
|
55
|
+
const img = (html.match(/<img\b[^>]*>/gi) || []).find((t) => /logo/i.test(t));
|
|
56
|
+
if (img)
|
|
57
|
+
logo = abs(finalUrl, attr(img, 'src'));
|
|
58
|
+
}
|
|
59
|
+
const themeColor = meta(html, 'theme-color');
|
|
60
|
+
const colors = [...new Set([...(html.match(/#[0-9a-fA-F]{6}\b/g) || [])].map((c) => c.toLowerCase()).filter((c) => !/^#(fff|000|ffffff|000000)/.test(c)))].slice(0, 12);
|
|
61
|
+
const headings = [...new Set((html.match(/<h[1-3][^>]*>([\s\S]*?)<\/h[1-3]>/gi) || []).map((h) => stripHtml(h)).filter((h) => h.length > 1 && h.length < 120))].slice(0, 20);
|
|
62
|
+
const text = stripHtml(html).slice(0, 6000);
|
|
63
|
+
const prices = [...new Set((text.match(/(?:₩|\$|€|£|¥)\s?\d[\d,]*(?:\.\d+)?|\d[\d,]*\s?(?:원|달러|만원)/g) || []))].slice(0, 12);
|
|
64
|
+
const pixelIds = [...new Set([...html.matchAll(/fbq\(\s*['"]init['"]\s*,\s*['"](\d{8,20})['"]/g)].map((m) => m[1]))];
|
|
65
|
+
const gtagIds = [...new Set([...html.matchAll(/\b(AW-\d{6,12})\b/g)].map((m) => m[1]))];
|
|
66
|
+
const ga4Ids = [...new Set([...html.matchAll(/\b(G-[A-Z0-9]{6,12})\b/g)].map((m) => m[1]))];
|
|
67
|
+
const csp = headers.get('content-security-policy') || (html.match(/<meta[^>]*http-equiv\s*=\s*["']content-security-policy["'][^>]*content\s*=\s*["']([^"']+)/i)?.[1] ?? null);
|
|
68
|
+
// UTM 보존: utm을 붙여 요청했을 때 최종 URL에 utm이 남는가(301이 쿼리를 지우는지)
|
|
69
|
+
let utmPreserved = null;
|
|
70
|
+
try {
|
|
71
|
+
const u = new URL(url);
|
|
72
|
+
u.searchParams.set('utm_source', 'adpilot_check');
|
|
73
|
+
const r = await fetch(u, { headers: { 'user-agent': UA }, redirect: 'follow', signal: AbortSignal.timeout(15_000) });
|
|
74
|
+
utmPreserved = /utm_source=adpilot_check/.test(r.url);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
utmPreserved = null;
|
|
78
|
+
}
|
|
79
|
+
return { url, finalUrl, status, title, description, lang: lang || 'ko', ogImage, logo, themeColor, colors, headings, prices, text, hasFbq: /fbq\(/.test(html), pixelIds, gtagIds, ga4Ids, csp, utmPreserved };
|
|
80
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
export declare const HOME: string;
|
|
2
|
+
export declare const PROJECTS: string;
|
|
3
|
+
export type MetaCreds = {
|
|
4
|
+
accessToken: string;
|
|
5
|
+
adAccountId: string;
|
|
6
|
+
pageId?: string;
|
|
7
|
+
instagramActorId?: string;
|
|
8
|
+
pixelId?: string;
|
|
9
|
+
appId?: string;
|
|
10
|
+
};
|
|
11
|
+
export type GoogleCreds = {
|
|
12
|
+
serviceAccountJson: string;
|
|
13
|
+
customerId: string;
|
|
14
|
+
loginCustomerId?: string;
|
|
15
|
+
};
|
|
16
|
+
export type Credentials = {
|
|
17
|
+
meta?: MetaCreds;
|
|
18
|
+
google?: GoogleCreds;
|
|
19
|
+
};
|
|
20
|
+
export type Config = {
|
|
21
|
+
llm?: {
|
|
22
|
+
baseUrl?: string;
|
|
23
|
+
apiKey?: string;
|
|
24
|
+
model?: string;
|
|
25
|
+
};
|
|
26
|
+
image?: {
|
|
27
|
+
provider?: 'openai';
|
|
28
|
+
apiKey?: string;
|
|
29
|
+
model?: string;
|
|
30
|
+
};
|
|
31
|
+
/** 원화 기준 환율 — 매체 계정 통화가 KRW가 아닐 때 예산 환산에 쓴다 */
|
|
32
|
+
fx?: Record<string, number>;
|
|
33
|
+
lastProject?: string;
|
|
34
|
+
};
|
|
35
|
+
export type Goal = 'traffic' | 'lead' | 'signup' | 'purchase';
|
|
36
|
+
export type Market = {
|
|
37
|
+
country: string;
|
|
38
|
+
language: string;
|
|
39
|
+
};
|
|
40
|
+
export type Brief = {
|
|
41
|
+
url: string;
|
|
42
|
+
company: string;
|
|
43
|
+
offer: string;
|
|
44
|
+
category: string;
|
|
45
|
+
audience: string[];
|
|
46
|
+
usp: string[];
|
|
47
|
+
tone: string;
|
|
48
|
+
language: string;
|
|
49
|
+
palette: {
|
|
50
|
+
primary: string;
|
|
51
|
+
dark: string;
|
|
52
|
+
light: string;
|
|
53
|
+
};
|
|
54
|
+
logoUrl?: string | null;
|
|
55
|
+
ogImage?: string | null;
|
|
56
|
+
specialCategory?: string | null;
|
|
57
|
+
confirmed?: boolean;
|
|
58
|
+
notes?: string;
|
|
59
|
+
};
|
|
60
|
+
export type Concept = {
|
|
61
|
+
key: string;
|
|
62
|
+
name: string;
|
|
63
|
+
angle: string;
|
|
64
|
+
headlines: string[];
|
|
65
|
+
bodies: string[];
|
|
66
|
+
descriptions: string[];
|
|
67
|
+
cta: string;
|
|
68
|
+
imagePrompt: string;
|
|
69
|
+
theme: 'dark' | 'light';
|
|
70
|
+
};
|
|
71
|
+
export type CreativeAsset = {
|
|
72
|
+
concept: string;
|
|
73
|
+
w: number;
|
|
74
|
+
h: number;
|
|
75
|
+
ratio: string;
|
|
76
|
+
file: string;
|
|
77
|
+
medium: 'meta' | 'google' | 'both';
|
|
78
|
+
};
|
|
79
|
+
export type PlanMedium = {
|
|
80
|
+
medium: 'meta' | 'google';
|
|
81
|
+
share: number;
|
|
82
|
+
dailyBudgetKrw: number;
|
|
83
|
+
currency: string;
|
|
84
|
+
dailyBudgetMinor: number;
|
|
85
|
+
note: string;
|
|
86
|
+
};
|
|
87
|
+
export type Plan = {
|
|
88
|
+
monthlyKrw: number;
|
|
89
|
+
dailyKrw: number;
|
|
90
|
+
media: PlanMedium[];
|
|
91
|
+
concepts: string[];
|
|
92
|
+
markets: Market[];
|
|
93
|
+
goal: Goal;
|
|
94
|
+
endDate?: string;
|
|
95
|
+
explain: string;
|
|
96
|
+
createdAt: string;
|
|
97
|
+
};
|
|
98
|
+
export type Placement = {
|
|
99
|
+
medium: 'meta' | 'google';
|
|
100
|
+
kind: string;
|
|
101
|
+
concept?: string;
|
|
102
|
+
externalId: string;
|
|
103
|
+
name: string;
|
|
104
|
+
status: string;
|
|
105
|
+
createdAt: string;
|
|
106
|
+
};
|
|
107
|
+
export type ProjectStatus = 'draft' | 'briefed' | 'creatives' | 'planned' | 'built' | 'live' | 'paused' | 'ended';
|
|
108
|
+
export type Project = {
|
|
109
|
+
slug: string;
|
|
110
|
+
url: string;
|
|
111
|
+
createdAt: string;
|
|
112
|
+
status: ProjectStatus;
|
|
113
|
+
budget: {
|
|
114
|
+
monthlyKrw: number;
|
|
115
|
+
goal: Goal;
|
|
116
|
+
markets: Market[];
|
|
117
|
+
endDate?: string;
|
|
118
|
+
autoApprove?: boolean;
|
|
119
|
+
};
|
|
120
|
+
brief?: Brief;
|
|
121
|
+
concepts?: Concept[];
|
|
122
|
+
assets?: CreativeAsset[];
|
|
123
|
+
plan?: Plan;
|
|
124
|
+
placements: Placement[];
|
|
125
|
+
tracking?: {
|
|
126
|
+
pixelId?: string;
|
|
127
|
+
gtagId?: string;
|
|
128
|
+
gtagLabel?: string;
|
|
129
|
+
ga4Id?: string;
|
|
130
|
+
conversionActionRn?: string;
|
|
131
|
+
};
|
|
132
|
+
approvedAt?: string;
|
|
133
|
+
launchedAt?: string;
|
|
134
|
+
};
|
|
135
|
+
export declare function loadConfig(): Config;
|
|
136
|
+
export declare function saveConfig(c: Config): void;
|
|
137
|
+
export declare function loadCredentials(): Credentials;
|
|
138
|
+
export declare function saveCredentials(c: Credentials): void;
|
|
139
|
+
/** 환경변수 > credentials.json. 오늘 키트의 ~/.config/adsctl/meta.env 변수명(ACCESS_TOKEN·AD_ACCOUNT_ID…)도 그대로 읽는다. */
|
|
140
|
+
export declare function metaCreds(): MetaCreds | null;
|
|
141
|
+
export declare function googleCreds(): GoogleCreds | null;
|
|
142
|
+
export declare function slugOf(url: string): string;
|
|
143
|
+
export declare function projectDir(slug: string): string;
|
|
144
|
+
export declare function creativesDir(slug: string): string;
|
|
145
|
+
export declare function listProjects(): string[];
|
|
146
|
+
export declare function loadProject(slug: string): Project | null;
|
|
147
|
+
export declare function saveProject(p: Project): void;
|
|
148
|
+
/** 현재 프로젝트: --project > 현재 폴더 .adpilot.json > 마지막 사용 > 유일한 하나 */
|
|
149
|
+
export declare function resolveProject(explicit?: string): Project;
|
|
150
|
+
export declare function newProject(url: string, budget: Project['budget']): Project;
|
|
151
|
+
export type Decision = {
|
|
152
|
+
at: string;
|
|
153
|
+
kind: string;
|
|
154
|
+
medium?: string;
|
|
155
|
+
target?: string;
|
|
156
|
+
action: string;
|
|
157
|
+
reason?: string;
|
|
158
|
+
by: 'user' | 'rule' | 'llm';
|
|
159
|
+
data?: unknown;
|
|
160
|
+
};
|
|
161
|
+
export declare function logDecision(slug: string, d: Omit<Decision, 'at'>): void;
|
|
162
|
+
export declare function readDecisions(slug: string): Decision[];
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// 로컬 상태 — 모든 것은 광고주 기기의 ~/.adpilot/ 아래에. 키는 credentials.json(0600), 프로젝트는 projects/<slug>/.
|
|
2
|
+
// ~/.adpilot/config.json LLM·이미지 생성 키·기본값(환경변수가 우선)
|
|
3
|
+
// ~/.adpilot/credentials.json 매체 자격(meta·google) — 0600
|
|
4
|
+
// ~/.adpilot/projects/<slug>/project.json 브리프·예산·계획·매체 id·상태
|
|
5
|
+
// ~/.adpilot/projects/<slug>/creatives/ 합성된 소재 PNG · copy.json
|
|
6
|
+
// ~/.adpilot/projects/<slug>/decisions.jsonl 모든 mutate·자동 조치 감사 로그
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
export const HOME = process.env.ADPILOT_HOME || path.join(os.homedir(), '.adpilot');
|
|
11
|
+
export const PROJECTS = path.join(HOME, 'projects');
|
|
12
|
+
function ensureDir(p) { fs.mkdirSync(p, { recursive: true, mode: 0o700 }); }
|
|
13
|
+
function readJson(file, fallback) {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return fallback;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function writeJson(file, data, mode = 0o644) {
|
|
22
|
+
ensureDir(path.dirname(file));
|
|
23
|
+
const tmp = file + '.tmp';
|
|
24
|
+
fs.writeFileSync(tmp, JSON.stringify(data, null, 2), { mode });
|
|
25
|
+
fs.renameSync(tmp, file);
|
|
26
|
+
}
|
|
27
|
+
export function loadConfig() { return readJson(path.join(HOME, 'config.json'), {}); }
|
|
28
|
+
export function saveConfig(c) { writeJson(path.join(HOME, 'config.json'), c, 0o600); }
|
|
29
|
+
export function loadCredentials() { return readJson(path.join(HOME, 'credentials.json'), {}); }
|
|
30
|
+
export function saveCredentials(c) { writeJson(path.join(HOME, 'credentials.json'), c, 0o600); }
|
|
31
|
+
/** 환경변수 > credentials.json. 오늘 키트의 ~/.config/adsctl/meta.env 변수명(ACCESS_TOKEN·AD_ACCOUNT_ID…)도 그대로 읽는다. */
|
|
32
|
+
export function metaCreds() {
|
|
33
|
+
const f = loadCredentials().meta;
|
|
34
|
+
const accessToken = process.env.META_ACCESS_TOKEN || process.env.ACCESS_TOKEN || f?.accessToken;
|
|
35
|
+
const adAccountId = process.env.META_AD_ACCOUNT_ID || process.env.AD_ACCOUNT_ID || f?.adAccountId;
|
|
36
|
+
if (!accessToken || !adAccountId)
|
|
37
|
+
return null;
|
|
38
|
+
return {
|
|
39
|
+
accessToken, adAccountId: adAccountId.startsWith('act_') ? adAccountId : `act_${adAccountId}`,
|
|
40
|
+
pageId: process.env.META_PAGE_ID || process.env.PAGE_ID || f?.pageId,
|
|
41
|
+
instagramActorId: process.env.META_INSTAGRAM_ACTOR_ID || process.env.INSTAGRAM_ACTOR_ID || f?.instagramActorId,
|
|
42
|
+
pixelId: process.env.META_PIXEL_ID || process.env.PIXEL_ID || f?.pixelId,
|
|
43
|
+
appId: process.env.META_APP_ID || f?.appId,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function googleCreds() {
|
|
47
|
+
const f = loadCredentials().google;
|
|
48
|
+
const serviceAccountJson = process.env.GOOGLE_ADS_SA_JSON || f?.serviceAccountJson;
|
|
49
|
+
const customerId = (process.env.GOOGLE_ADS_CUSTOMER_ID || f?.customerId || '').replace(/-/g, '');
|
|
50
|
+
if (!serviceAccountJson || !customerId)
|
|
51
|
+
return null;
|
|
52
|
+
return { serviceAccountJson, customerId, loginCustomerId: (process.env.GOOGLE_ADS_LOGIN_CUSTOMER_ID || f?.loginCustomerId || '').replace(/-/g, '') || undefined };
|
|
53
|
+
}
|
|
54
|
+
export function slugOf(url) {
|
|
55
|
+
try {
|
|
56
|
+
const h = new URL(url).hostname.replace(/^www\./, '');
|
|
57
|
+
return h.replace(/[^a-z0-9]+/gi, '-').replace(/^-|-$/g, '').toLowerCase() || 'site';
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return 'site';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function projectDir(slug) { return path.join(PROJECTS, slug); }
|
|
64
|
+
export function creativesDir(slug) { const d = path.join(projectDir(slug), 'creatives'); ensureDir(d); return d; }
|
|
65
|
+
export function listProjects() {
|
|
66
|
+
try {
|
|
67
|
+
return fs.readdirSync(PROJECTS).filter((s) => fs.existsSync(path.join(PROJECTS, s, 'project.json')));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export function loadProject(slug) { return readJson(path.join(projectDir(slug), 'project.json'), null); }
|
|
74
|
+
export function saveProject(p) {
|
|
75
|
+
writeJson(path.join(projectDir(p.slug), 'project.json'), p);
|
|
76
|
+
const c = loadConfig();
|
|
77
|
+
if (c.lastProject !== p.slug) {
|
|
78
|
+
c.lastProject = p.slug;
|
|
79
|
+
saveConfig(c);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** 현재 프로젝트: --project > 현재 폴더 .adpilot.json > 마지막 사용 > 유일한 하나 */
|
|
83
|
+
export function resolveProject(explicit) {
|
|
84
|
+
const local = readJson(path.join(process.cwd(), '.adpilot.json'), {});
|
|
85
|
+
const slug = explicit || process.env.ADPILOT_PROJECT || local.project || loadConfig().lastProject || (listProjects().length === 1 ? listProjects()[0] : undefined);
|
|
86
|
+
const p = slug ? loadProject(slug) : null;
|
|
87
|
+
if (!p)
|
|
88
|
+
throw new Error(slug ? `프로젝트 「${slug}」 가 없어요. \`adpilot brief <url>\` 로 먼저 만들어 주세요.` : '프로젝트가 없어요. `adpilot brief <url>` 로 먼저 만들어 주세요.');
|
|
89
|
+
return p;
|
|
90
|
+
}
|
|
91
|
+
export function newProject(url, budget) {
|
|
92
|
+
const slug = slugOf(url);
|
|
93
|
+
const existing = loadProject(slug);
|
|
94
|
+
const p = existing ? { ...existing, url, budget: { ...existing.budget, ...budget } } : { slug, url, createdAt: new Date().toISOString(), status: 'draft', budget, placements: [] };
|
|
95
|
+
saveProject(p);
|
|
96
|
+
return p;
|
|
97
|
+
}
|
|
98
|
+
export function logDecision(slug, d) {
|
|
99
|
+
const file = path.join(projectDir(slug), 'decisions.jsonl');
|
|
100
|
+
ensureDir(path.dirname(file));
|
|
101
|
+
fs.appendFileSync(file, JSON.stringify({ at: new Date().toISOString(), ...d }) + '\n');
|
|
102
|
+
}
|
|
103
|
+
export function readDecisions(slug) {
|
|
104
|
+
try {
|
|
105
|
+
return fs.readFileSync(path.join(projectDir(slug), 'decisions.jsonl'), 'utf8').split('\n').filter(Boolean).map((l) => JSON.parse(l));
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CheckItem } from '../adapters/types.js';
|
|
2
|
+
import type { Project } from './state.js';
|
|
3
|
+
export type TrackingIds = {
|
|
4
|
+
pixelId?: string;
|
|
5
|
+
gtagId?: string;
|
|
6
|
+
gtagLabel?: string;
|
|
7
|
+
ga4Id?: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function snippet(ids: TrackingIds, opts?: {
|
|
10
|
+
goal: string;
|
|
11
|
+
spa?: boolean;
|
|
12
|
+
}): {
|
|
13
|
+
head: string;
|
|
14
|
+
conversion: string;
|
|
15
|
+
csp: string[];
|
|
16
|
+
};
|
|
17
|
+
/** 랜딩 정적 검증 — 태그 존재·CSP·UTM 보존·SPA 여부 */
|
|
18
|
+
export declare function verifyLanding(url: string, ids: TrackingIds): Promise<CheckItem[]>;
|
|
19
|
+
export declare function idsOf(p: Project): TrackingIds;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { readSite } from './site.js';
|
|
2
|
+
export function snippet(ids, opts = { goal: 'traffic' }) {
|
|
3
|
+
const parts = ['<!-- adpilot tracking · 모든 페이지 <head> · 이 블록 하나면 픽셀·구글 태그·UTM 보존이 끝나요 -->', '<script>', `(function(){var q=location.search;if(/utm_/.test(q)){try{sessionStorage.setItem('adp_utm',q)}catch(e){}}window.__adpUtm=function(){try{return sessionStorage.getItem('adp_utm')||''}catch(e){return ''}};})();`, '</script>'];
|
|
4
|
+
if (ids.pixelId)
|
|
5
|
+
parts.push(`<script>!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,document,'script','https://connect.facebook.net/en_US/fbevents.js');fbq('init','${ids.pixelId}');fbq('track','PageView');</script>`);
|
|
6
|
+
const gid = ids.gtagId || ids.ga4Id;
|
|
7
|
+
if (gid) {
|
|
8
|
+
parts.push(`<script async src="https://www.googletagmanager.com/gtag/js?id=${gid}"></script>`);
|
|
9
|
+
parts.push(`<script>window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments)}gtag('js',new Date());${ids.gtagId ? `gtag('config','${ids.gtagId}');` : ''}${ids.ga4Id ? `gtag('config','${ids.ga4Id}');` : ''}</script>`);
|
|
10
|
+
}
|
|
11
|
+
if (opts.spa)
|
|
12
|
+
parts.push(`<script>(function(){var p=history.pushState;history.pushState=function(){p.apply(this,arguments);window.fbq&&fbq('track','PageView');window.gtag&>ag('event','page_view')};window.addEventListener('hashchange',function(){window.fbq&&fbq('track','PageView')})})();</script>`);
|
|
13
|
+
const ev = opts.goal === 'purchase' ? 'Purchase' : opts.goal === 'lead' ? 'Lead' : opts.goal === 'signup' ? 'CompleteRegistration' : 'ViewContent';
|
|
14
|
+
const conversion = [`<!-- 목표 달성 화면(${opts.goal === 'purchase' ? '결제 완료' : opts.goal === 'lead' ? '문의 접수 완료' : opts.goal === 'signup' ? '가입 완료' : '핵심 페이지'})에서 1회 · value·currency·orderId는 실제 값으로 -->`, `<script>(function(){var id=String(window.__adpOrderId||Date.now());try{if(localStorage.getItem('adp_conv_'+id))return;localStorage.setItem('adp_conv_'+id,'1')}catch(e){}`];
|
|
15
|
+
if (ids.pixelId)
|
|
16
|
+
conversion.push(`window.fbq&&fbq('track','${ev}',{value:window.__adpValue||0,currency:window.__adpCurrency||'KRW'},{eventID:id});`);
|
|
17
|
+
if (ids.gtagId && ids.gtagLabel)
|
|
18
|
+
conversion.push(`window.gtag&>ag('event','conversion',{send_to:'${ids.gtagId}/${ids.gtagLabel}',value:window.__adpValue||0,currency:window.__adpCurrency||'KRW',transaction_id:id});`);
|
|
19
|
+
if (ids.ga4Id)
|
|
20
|
+
conversion.push(`window.gtag&>ag('event','${opts.goal === 'purchase' ? 'purchase' : opts.goal === 'lead' ? 'generate_lead' : opts.goal === 'signup' ? 'sign_up' : 'view_item'}',{value:window.__adpValue||0,currency:window.__adpCurrency||'KRW',transaction_id:id});`);
|
|
21
|
+
conversion.push('})();</script>');
|
|
22
|
+
const csp = [...(ids.pixelId ? ['https://connect.facebook.net', 'https://www.facebook.com'] : []), ...(gid ? ['https://www.googletagmanager.com', 'https://www.googleadservices.com', 'https://googleads.g.doubleclick.net', 'https://www.google.com', 'https://www.google-analytics.com'] : [])];
|
|
23
|
+
return { head: parts.join('\n'), conversion: conversion.join('\n'), csp };
|
|
24
|
+
}
|
|
25
|
+
/** 랜딩 정적 검증 — 태그 존재·CSP·UTM 보존·SPA 여부 */
|
|
26
|
+
export async function verifyLanding(url, ids) {
|
|
27
|
+
const s = await readSite(url);
|
|
28
|
+
const items = [];
|
|
29
|
+
items.push({ key: 'landing.reach', ok: s.status < 400, title: '랜딩 응답', detail: `HTTP ${s.status} · ${s.finalUrl}` });
|
|
30
|
+
if (ids.pixelId)
|
|
31
|
+
items.push({ key: 'landing.pixel', ok: s.pixelIds.includes(ids.pixelId), title: `메타 픽셀 ${ids.pixelId} 설치`, detail: s.pixelIds.length ? `발견된 픽셀: ${s.pixelIds.join(', ')}` : s.hasFbq ? 'fbq는 있지만 init 픽셀 id를 못 읽었어요(동적 로드일 수 있음)' : '픽셀 코드가 없어요', fix: s.pixelIds.includes(ids.pixelId) ? undefined : '`adpilot track install` 스니펫을 <head> 에 넣어 주세요.' });
|
|
32
|
+
if (ids.gtagId)
|
|
33
|
+
items.push({ key: 'landing.gtag', ok: s.gtagIds.includes(ids.gtagId), title: `구글 태그 ${ids.gtagId} 설치`, detail: s.gtagIds.length ? `발견: ${s.gtagIds.join(', ')}` : '구글 태그가 없어요', fix: s.gtagIds.includes(ids.gtagId) ? undefined : '`adpilot track install` 스니펫을 <head> 에 넣어 주세요.' });
|
|
34
|
+
if (ids.ga4Id)
|
|
35
|
+
items.push({ key: 'landing.ga4', ok: s.ga4Ids.includes(ids.ga4Id), title: `GA4 ${ids.ga4Id} 설치`, detail: s.ga4Ids.join(', ') || '없음' });
|
|
36
|
+
items.push({ key: 'landing.utm', ok: s.utmPreserved, title: 'UTM 보존(리다이렉트가 쿼리를 지우지 않음)', detail: s.utmPreserved === null ? '확인 실패' : s.utmPreserved ? '보존됨' : '최종 URL에서 utm이 사라졌어요', fix: s.utmPreserved === false ? '301/302 규칙과 `?ref=` 같은 자체 쿼리 처리 코드가 utm을 지우는지 확인해 주세요.' : undefined });
|
|
37
|
+
if (s.csp) {
|
|
38
|
+
const need = snippet(ids).csp.filter((d) => !s.csp.includes(new URL(d).hostname));
|
|
39
|
+
items.push({ key: 'landing.csp', ok: !need.length, title: 'CSP script-src에 매체 도메인', detail: need.length ? `빠짐: ${need.join(', ')}` : '허용됨', fix: need.length ? 'Content-Security-Policy의 script-src·connect-src·img-src에 위 도메인을 추가해 주세요.' : undefined });
|
|
40
|
+
}
|
|
41
|
+
const spa = /<div id="(root|app|__next)"/.test(s.text) || /__NEXT_DATA__|data-reactroot|ng-version/.test(s.text);
|
|
42
|
+
items.push({ key: 'landing.spa', ok: null, title: 'SPA 여부', detail: spa ? 'SPA로 보여요 — 경로가 바뀔 때 PageView를 수동으로 보내야 해요(스니펫의 --spa 옵션)' : '일반 페이지' });
|
|
43
|
+
return items;
|
|
44
|
+
}
|
|
45
|
+
export function idsOf(p) { return { pixelId: p.tracking?.pixelId, gtagId: p.tracking?.gtagId, gtagLabel: p.tracking?.gtagLabel, ga4Id: p.tracking?.ga4Id }; }
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export * from './core/state.js';
|
|
2
|
+
export * from './core/money.js';
|
|
3
|
+
export * from './core/llm.js';
|
|
4
|
+
export * from './core/site.js';
|
|
5
|
+
export * from './core/brief.js';
|
|
6
|
+
export { SIZES, BG_RATIOS, wlen, fit, GOOGLE_LIMITS, GOOGLE_CTA, META_CTA, ctaFor, policyIssues, checkCopy, type Size, type CopyCheck } from './core/creatives/specs.js';
|
|
7
|
+
export * from './core/creatives/copy.js';
|
|
8
|
+
export * from './core/creatives/images.js';
|
|
9
|
+
export * from './core/creatives/render.js';
|
|
10
|
+
export { computePlan, type PlanInput } from './core/plan.js';
|
|
11
|
+
export * from './core/optimize.js';
|
|
12
|
+
export * from './core/report.js';
|
|
13
|
+
export * from './core/tracking.js';
|
|
14
|
+
export * from './core/check.js';
|
|
15
|
+
export * from './adapters/types.js';
|
|
16
|
+
export { MetaConnector } from './adapters/meta.js';
|
|
17
|
+
export { GoogleConnector, GEO, LANG } from './adapters/google.js';
|
|
18
|
+
export { MockConnector } from './adapters/mock.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// 라이브러리 진입점 — 웹 SaaS 등이 코어를 재사용한다.
|
|
2
|
+
export * from './core/state.js';
|
|
3
|
+
export * from './core/money.js';
|
|
4
|
+
export * from './core/llm.js';
|
|
5
|
+
export * from './core/site.js';
|
|
6
|
+
export * from './core/brief.js';
|
|
7
|
+
export { SIZES, BG_RATIOS, wlen, fit, GOOGLE_LIMITS, GOOGLE_CTA, META_CTA, ctaFor, policyIssues, checkCopy } from './core/creatives/specs.js';
|
|
8
|
+
export * from './core/creatives/copy.js';
|
|
9
|
+
export * from './core/creatives/images.js';
|
|
10
|
+
export * from './core/creatives/render.js';
|
|
11
|
+
export { computePlan } from './core/plan.js';
|
|
12
|
+
export * from './core/optimize.js';
|
|
13
|
+
export * from './core/report.js';
|
|
14
|
+
export * from './core/tracking.js';
|
|
15
|
+
export * from './core/check.js';
|
|
16
|
+
export * from './adapters/types.js';
|
|
17
|
+
export { MetaConnector } from './adapters/meta.js';
|
|
18
|
+
export { GoogleConnector, GEO, LANG } from './adapters/google.js';
|
|
19
|
+
export { MockConnector } from './adapters/mock.js';
|
package/dist/mcp.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function startMcp(): Promise<void>;
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// MCP 서버(stdio) — CLI와 같은 작업층을 툴로 노출. Claude Code: `claude mcp add adpilot -- npx adpilot-cli mcp`
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { connectedMedia } from './adapters/index.js';
|
|
6
|
+
import { formatCheck, runCheck } from './core/check.js';
|
|
7
|
+
import { fmtKrw } from './core/money.js';
|
|
8
|
+
import { opAudit, opBrief, opBuild, opChange, opCreatives, opLaunch, opOptimize, opPause, opPlan, opPreview, opReport, opStatus, opTrackSnippet } from './core/ops.js';
|
|
9
|
+
import { tableOf } from './core/report.js';
|
|
10
|
+
import { listProjects, loadProject, resolveProject } from './core/state.js';
|
|
11
|
+
import { idsOf, verifyLanding } from './core/tracking.js';
|
|
12
|
+
const text = (s) => ({ content: [{ type: 'text', text: s }] });
|
|
13
|
+
const err = (e) => ({ content: [{ type: 'text', text: `오류: ${e instanceof Error ? e.message : String(e)}${e?.hint ? `\n→ ${e.hint}` : ''}` }], isError: true });
|
|
14
|
+
const proj = (slug) => resolveProject(slug);
|
|
15
|
+
const mediaZ = z.array(z.enum(['meta', 'google'])).optional();
|
|
16
|
+
export async function startMcp() {
|
|
17
|
+
const server = new McpServer({ name: 'adyou', version: '0.3.0' });
|
|
18
|
+
server.registerTool('adyou_check', { description: '매체 자격·계정·픽셀·랜딩을 실측 검사하고 실패 항목마다 어느 화면에서 무엇을 누를지 알려 준다. 처음 연결할 때·문제 생겼을 때.', inputSchema: { project: z.string().optional(), media: mediaZ } }, async (a) => { try {
|
|
19
|
+
let p = null;
|
|
20
|
+
try {
|
|
21
|
+
p = proj(a.project);
|
|
22
|
+
}
|
|
23
|
+
catch { /* none */ }
|
|
24
|
+
const { items } = await runCheck(p, { media: a.media });
|
|
25
|
+
return text(`연결된 매체: ${connectedMedia().join(', ') || '없음'}\n${formatCheck(items)}`);
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
return err(e);
|
|
29
|
+
} });
|
|
30
|
+
server.registerTool('adyou_brief', { description: '사이트 주소와 월 예산(원)·목표로 광고 브리프를 만든다(프로젝트 생성). 결과 설명을 사용자에게 보여 주고 맞는지 확인받는다.', inputSchema: { url: z.string(), budgetKrw: z.number(), goal: z.enum(['traffic', 'lead', 'signup', 'purchase']).default('traffic'), markets: z.string().optional().describe('예 "KR" 또는 "US:en,JP:ja"'), hints: z.string().optional(), endDate: z.string().optional() } }, async (a) => { try {
|
|
31
|
+
const { project, explain } = await opBrief(a.url, { budget: a.budgetKrw, goal: a.goal, markets: a.markets, hints: a.hints, endDate: a.endDate });
|
|
32
|
+
return text(`${explain}\n\n프로젝트 slug: ${project.slug}`);
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
return err(e);
|
|
36
|
+
} });
|
|
37
|
+
server.registerTool('adyou_creatives', { description: '컨셉·카피 생성 + 배경 이미지 + 12규격 소재 합성 + 규격 검사. feedback을 주면 그 방향으로 전부 다시 만든다.', inputSchema: { project: z.string().optional(), concepts: z.number().optional(), feedback: z.string().optional(), imagesFolder: z.string().optional(), noImages: z.boolean().optional() } }, async (a) => { try {
|
|
38
|
+
const logs = [];
|
|
39
|
+
const { project, report } = await opCreatives(proj(a.project), { count: a.concepts, feedback: a.feedback, images: a.imagesFolder, noImages: a.noImages, log: (s) => logs.push(s) });
|
|
40
|
+
return text(`컨셉 ${project.concepts.length} · 소재 ${project.assets.length}장\n${project.concepts.map((c) => `· ${c.name}(${c.key}) — ${c.headlines[0]} / ${c.bodies[0]}`).join('\n')}\n${report.length ? `규격 경고:\n${report.join('\n')}\n` : ''}갤러리: ~/.adpilot/projects/${project.slug}/creatives/index.html`);
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
return err(e);
|
|
44
|
+
} });
|
|
45
|
+
server.registerTool('adyou_plan', { description: '월 예산을 매체·시장·컨셉으로 나누고 사람 말로 설명한다.', inputSchema: { project: z.string().optional(), media: mediaZ } }, async (a) => { try {
|
|
46
|
+
const p = await opPlan(proj(a.project), { media: a.media });
|
|
47
|
+
return text(p.plan.explain);
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
return err(e);
|
|
51
|
+
} });
|
|
52
|
+
server.registerTool('adyou_build', { description: '매체에 캠페인·광고를 전부 PAUSED(대기 상태)로 생성한다. 멱등 · 부분 실패 후 재실행 안전. validate=true 면 서버 검증만.', inputSchema: { project: z.string().optional(), media: mediaZ, validate: z.boolean().optional() } }, async (a) => { try {
|
|
53
|
+
const logs = [];
|
|
54
|
+
const { project, warnings, errors } = await opBuild(proj(a.project), { media: a.media, validate: a.validate, log: (s) => logs.push(s) });
|
|
55
|
+
return text(`${logs.join('\n')}\n${warnings.map((w) => `⚠ ${w}`).join('\n')}\n${errors.map((e) => `❌ ${e}`).join('\n')}\n생성물 ${project.placements.length}개(전부 PAUSED):\n${project.placements.map((x) => `${x.medium} ${x.kind} ${x.concept || ''} ${x.externalId}`).join('\n')}`);
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
return err(e);
|
|
59
|
+
} });
|
|
60
|
+
server.registerTool('adyou_preview', { description: '미리보기 HTML 파일을 만든다(메타 실제 렌더 · 구글 근사).', inputSchema: { project: z.string().optional() } }, async (a) => { try {
|
|
61
|
+
const r = await opPreview(proj(a.project));
|
|
62
|
+
return text(`미리보기 ${r.count}개 → ${r.file}`);
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
return err(e);
|
|
66
|
+
} });
|
|
67
|
+
server.registerTool('adyou_launch', { description: '광고를 시작한다(ACTIVE). 반드시 사용자의 명시적 승인을 받은 뒤에만 호출한다. 시작하는 순간부터 일 예산이 나간다.', inputSchema: { project: z.string().optional(), media: mediaZ, confirmed: z.literal(true).describe('사용자가 시작해도 된다고 명시적으로 말했을 때만 true') } }, async (a) => { try {
|
|
68
|
+
const { launched, errors } = await opLaunch(proj(a.project), { media: a.media, yes: true });
|
|
69
|
+
return text(`광고를 시작한 매체: ${launched.join(', ') || '없음'}${errors.length ? `\n${errors.join('\n')}` : ''}\n매체 검수(메타 24h · 구글 1영업일) 뒤 노출.`);
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
return err(e);
|
|
73
|
+
} });
|
|
74
|
+
server.registerTool('adyou_pause', { description: '광고를 즉시 전부 끈다.', inputSchema: { project: z.string().optional(), media: mediaZ } }, async (a) => { try {
|
|
75
|
+
const { paused, errors } = await opPause(proj(a.project), { media: a.media });
|
|
76
|
+
return text(`껐어요: ${paused.join(', ') || '없음'}${errors.length ? `\n${errors.join('\n')}` : ''}`);
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
return err(e);
|
|
80
|
+
} });
|
|
81
|
+
server.registerTool('adyou_status', { description: '최근 N일 지표(소재별 쓴 돈·노출·클릭·전환)와 검수 상태.', inputSchema: { project: z.string().optional(), days: z.number().optional() } }, async (a) => { try {
|
|
82
|
+
const p = proj(a.project);
|
|
83
|
+
const st = await opStatus(p, { days: a.days });
|
|
84
|
+
return text(`상태 ${p.status} · ${st.range.since}~${st.range.until}\n${Object.entries(st.metrics).map(([m, rows]) => `[${m}]\n${tableOf(rows).map((r) => ` ${r.concept}: ${fmtKrw(r.spendKrw)} · 노출 ${r.impressions} · 클릭 ${r.clicks} · CTR ${(r.ctr * 100).toFixed(2)}% · 전환 ${r.conversions}`).join('\n') || ' 지표 없음'}\n${(st.policy[m] || []).map((x) => ` 검수 ${x.name}: ${x.status} ${x.issues.join('; ')}`).join('\n')}`).join('\n')}`);
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
return err(e);
|
|
88
|
+
} });
|
|
89
|
+
server.registerTool('adyou_optimize', { description: '규칙 기반 최적화 제안(소재 끄기·예산 ±20% · 학습 단계 보호 · 월 상한 준수). apply=true 면 실행.', inputSchema: { project: z.string().optional(), apply: z.boolean().optional() } }, async (a) => { try {
|
|
90
|
+
const { proposals, applied } = await opOptimize(proj(a.project), { apply: a.apply });
|
|
91
|
+
return text(`${proposals.map((p) => `[${p.medium}] ${p.kind}: ${p.reason}`).join('\n')}${a.apply ? `\n적용: ${applied.join(' · ') || '없음'}` : ''}`);
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
return err(e);
|
|
95
|
+
} });
|
|
96
|
+
server.registerTool('adyou_report', { description: '사람 말 보고서(주간 기본). HTML/MD 파일도 저장.', inputSchema: { project: z.string().optional(), days: z.number().optional() } }, async (a) => { try {
|
|
97
|
+
const r = await opReport(proj(a.project), { days: a.days });
|
|
98
|
+
return text(`${r.text}\n\n${r.markdown}\n\n파일: ${r.file}`);
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
return err(e);
|
|
102
|
+
} });
|
|
103
|
+
server.registerTool('adyou_audit', { description: '연결된 광고 계정의 모든 캠페인·광고를 전수 조회하고 낭비·중복·정책 문제를 진단한다(기존 광고 인수).', inputSchema: { media: mediaZ } }, async (a) => { try {
|
|
104
|
+
const r = await opAudit({ media: a.media });
|
|
105
|
+
return text(Object.entries(r).map(([m, v]) => `[${m}] 캠페인 ${v.rows.filter((x) => x.level === 'campaign').length}\n${v.rows.filter((x) => x.level === 'campaign').map((c) => ` ${c.status} ${c.name} · 30일 ${c.spend30d} · 노출 ${c.impressions30d} · 클릭 ${c.clicks30d}`).join('\n')}\n진단: ${v.findings.join(' / ') || '특이사항 없음'}`).join('\n\n'));
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
return err(e);
|
|
109
|
+
} });
|
|
110
|
+
server.registerTool('adyou_change', { description: '자연어 수정 요청을 적용한다 — 예산·목표·시장·컨셉 제외·소재 방향·끄기/시작.', inputSchema: { project: z.string().optional(), request: z.string() } }, async (a) => { try {
|
|
111
|
+
const r = await opChange(proj(a.project), a.request);
|
|
112
|
+
return text(`${r.summary}\n${r.steps.join('\n')}`);
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
return err(e);
|
|
116
|
+
} });
|
|
117
|
+
server.registerTool('adyou_track', { description: '추적 스니펫 발급(install) 또는 랜딩 검증(verify).', inputSchema: { project: z.string().optional(), action: z.enum(['install', 'verify']), spa: z.boolean().optional() } }, async (a) => { try {
|
|
118
|
+
const p = proj(a.project);
|
|
119
|
+
if (a.action === 'install') {
|
|
120
|
+
const s = opTrackSnippet(p, { spa: a.spa });
|
|
121
|
+
return text(`■ <head>\n${s.head}\n\n■ 목표 달성 화면\n${s.conversion}\n\n■ CSP: ${s.csp.join(' ')}`);
|
|
122
|
+
}
|
|
123
|
+
return text(formatCheck(await verifyLanding(p.url, idsOf(p))));
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
return err(e);
|
|
127
|
+
} });
|
|
128
|
+
server.registerTool('adyou_projects', { description: '프로젝트 목록과 상태.', inputSchema: {} }, async () => text(listProjects().map((s) => { const p = loadProject(s); return `${s} · ${p.status} · ${fmtKrw(p.budget.monthlyKrw)}/월 · ${p.budget.goal} · 생성물 ${p.placements.length}`; }).join('\n') || '없음'));
|
|
129
|
+
await server.connect(new StdioServerTransport());
|
|
130
|
+
}
|