@fcg-labs/cx-agent-hook 0.4.0 → 0.5.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/AGENTS.md +14 -0
- package/MIGRATION.md +53 -0
- package/README.md +53 -14
- package/actions.js +168 -0
- package/agent.d.ts +72 -16
- package/agent.js +45 -8
- package/angular.d.ts +2 -2
- package/angular.js +1 -1
- package/astro.d.ts +2 -2
- package/astro.js +1 -0
- package/bin/cx-agent-hook.js +78 -0
- package/client.js +78 -31
- package/element.d.ts +12 -1
- package/element.js +124 -1
- package/locales.js +16 -4
- package/next.d.ts +2 -2
- package/next.js +1 -1
- package/package.json +11 -4
- package/react.d.ts +14 -0
- package/react.js +80 -1
- package/session.js +109 -23
- package/skills/cx-agent-hook-setup/SKILL.md +195 -0
- package/skills/cx-agent-hook-setup/reference.md +92 -0
- package/styles.css +65 -1
- package/view.d.ts +13 -5
- package/view.js +51 -19
- package/vue.d.ts +10 -0
- package/vue.js +80 -2
package/AGENTS.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# AGENTS.md — @fcg-labs/cx-agent-hook
|
|
2
|
+
|
|
3
|
+
이 패키지를 고객사 관리자 화면(CMS)에 붙이거나 올리는 작업을 맡은 에이전트는
|
|
4
|
+
**`skills/cx-agent-hook-setup/SKILL.md`** 를 먼저 읽고 그 절차(실측 → 결정 규칙 →
|
|
5
|
+
6단계 → 완료 체크리스트)를 따른다. 인용해도 되는 API 이름은 같은 폴더의
|
|
6
|
+
`reference.md` 가 정본이며 `test/skills.test.js` 가 실제 export 와 대조한다.
|
|
7
|
+
|
|
8
|
+
- 셋업 = 답변 초안 패널 **+ 처리 카드(`adminApi`)** 까지. 관리자 API 가 있는 프로젝트에서
|
|
9
|
+
`adminApi` 를 빼먹으면 미완이다.
|
|
10
|
+
- 서버 코드·새 엔드포인트·서명·토큰 발급을 만들지 않는다. 비밀을 저장소에 넣지 않는다.
|
|
11
|
+
- 프로젝트 성격(프레임워크·번들러·경로 별칭·요청 헬퍼 시그니처)에 맞춰 코드를 조정하되,
|
|
12
|
+
스킬에 없는 API 를 지어내지 않는다.
|
|
13
|
+
|
|
14
|
+
설치: `npx cx-agent-hook skills install [--all]` (프로젝트의 `.claude/skills/`·`.cursor/rules/`·`AGENTS.md`).
|
package/MIGRATION.md
CHANGED
|
@@ -1,3 +1,56 @@
|
|
|
1
|
+
# 0.4.0 → 0.5.0 마이그레이션
|
|
2
|
+
|
|
3
|
+
## 요약 — 처리 액션 표면만 바뀐다 (0.3.0 표면은 그대로)
|
|
4
|
+
|
|
5
|
+
0.4.0 의 처리 액션은 "허브가 고객사 서버(실행기)를 부른다"였다 — 고객사에게 어댑터
|
|
6
|
+
서버·서명 검증·단명 토큰을 요구했다. 0.5.0 은 **실행기가 CMS 자신**이다: 브라우저가
|
|
7
|
+
CMS 의 기존 관리자 API 를 CMS 세션으로 부르고, 허브는 원장(선점·결과·잠금)만이다.
|
|
8
|
+
0.4.0 을 실배선한 소비처는 없었다(CMS 는 0.3.0). `createCxAgent`·`session`·어댑터·
|
|
9
|
+
클래스명은 전부 그대로 (기존 시험 107건 무수정 통과, 액션 시험 17건 재작성).
|
|
10
|
+
|
|
11
|
+
## 깨지는 것 (0.4.0 액션 표면만)
|
|
12
|
+
|
|
13
|
+
- `createCxAgent({ actorAssertion })` **삭제** → `createCxAgent({ adminApi })`.
|
|
14
|
+
`adminApi = { request, endpoints, map?, onAuthExpired? }` — CMS 에 **이미 있는**
|
|
15
|
+
요청 헬퍼(예: `TempAdminApi.request`)와 엔드포인트 맵(예: `EndPoint`)의 참조.
|
|
16
|
+
- `session.reconcileAction(requestId)` **삭제** → 조회 대상(고객사 서버 lookup)이 없다.
|
|
17
|
+
대신 `session.resendResult(requestId)`(미보고 결과 재보고, 재실행 아님) ·
|
|
18
|
+
`session.flushUnreportedActions()` · `session.refreshActionLocks()`.
|
|
19
|
+
- `ActionResult` 에서 `actorAttested`·`auditRef`·`result` 삭제(고객사 서버 응답 개념),
|
|
20
|
+
`reported`·`targetKey` 추가. `state` 에 `received`(선점 완료) 추가.
|
|
21
|
+
- 뷰: `ACTION_CLS.reconcile` → `locked`·`resend`; `ActionOfferItemView.canReconcile/
|
|
22
|
+
reconcileLabel` → `canResend/resendLabel` + `available/locked/lockedText/unavailableText`.
|
|
23
|
+
트리 action `"reconcile"` → `"resend"`. 로케일 키 `ui_action_reconcile` → `ui_action_resend`,
|
|
24
|
+
신설 `ui_action_locked`·`ui_action_unavailable`·`ui_action_unreported`.
|
|
25
|
+
|
|
26
|
+
## 새로 생긴 것
|
|
27
|
+
|
|
28
|
+
- `agent.candidatesReady` / `agent.publishActionCandidates({force})` — 초기화 시 엔드포인트
|
|
29
|
+
맵의 키·메서드·경로 템플릿(호스트 없음)을 허브에 1회 발행(로컬 지문 마커로 재발행 0).
|
|
30
|
+
- `offer.execution`(카탈로그 실행 사양 원문)·`offer.target_key`, `session.offers` 가
|
|
31
|
+
`available`(이 CMS 가 API 를 아는가)·`locked`(같은 대상·입력 최근 성공)를 병합.
|
|
32
|
+
- `actions.js` 순수 함수: `buildCandidateSnapshot`·`resolveExecution`·`judgeResponse`·
|
|
33
|
+
`executeViaAdminApi` (agent 에서 재수출).
|
|
34
|
+
- 어댑터 처리 카드: `/react` `ActionOffersPanel` · `/vue` `ActionOffersPanel` · `/element`
|
|
35
|
+
`<cx-action-offers>`(`defineCxActionOffers`, 이벤트 `cx-action-result`) + `styles.css` `.fcx-act-*`.
|
|
36
|
+
0.4.0 의 "소비처가 actionOffersTree 로 직접 그린다"는 더 이상 필요 없다(헤드리스는 그대로 가능).
|
|
37
|
+
|
|
38
|
+
## 에이전트 스킬 (신설)
|
|
39
|
+
|
|
40
|
+
패키지에 `skills/cx-agent-hook-setup/`(SKILL.md·reference.md)과 `npx cx-agent-hook skills install`
|
|
41
|
+
CLI 가 들어 있다. 개발자가 자기 코딩 에이전트에게 "cx-agent-hook 셋업 도와줘" 라고만 해도
|
|
42
|
+
실측→adminApi 포함 배선→처리 카드→확인까지 하게 만드는 절차다. `AGENTS.md` 도 같은 곳을 가리킨다.
|
|
43
|
+
|
|
44
|
+
## 행동 계약
|
|
45
|
+
|
|
46
|
+
- 선점(①) 없이는 CMS 를 부르지 않는다 (원장 없는 실행 금지). CMS 호출(②)·보고(③)는
|
|
47
|
+
**재시도 0**. 선점만 미도달 확정(404) 시 1회 재전송.
|
|
48
|
+
- `unknown`(전송 후 결과 불명·5xx)은 재실행 버튼이 없다 — 카드가 "확인 필요"로 남고,
|
|
49
|
+
공장 관측의 "확인 필요 24h+" 칩이 정본. 미보고는 `resendResult` 로만 수렴(재실행 0).
|
|
50
|
+
- 잠금은 편의다: 정본은 허브 원장과 CMS 자기 API 의 정책. 다른 입력이면 허용.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
1
54
|
# 0.3.0 → 0.4.0 마이그레이션
|
|
2
55
|
|
|
3
56
|
## 요약 — 깨지는 것은 없다
|
package/README.md
CHANGED
|
@@ -30,6 +30,22 @@ npm install @fcg-labs/cx-agent-hook
|
|
|
30
30
|
React·Vue 어댑터를 쓸 때만 그 프레임워크가 필요하다 (optional peer). 전송 계층만
|
|
31
31
|
쓰면 의존성은 0 이다.
|
|
32
32
|
|
|
33
|
+
### AI 코딩 에이전트에게 맡기기 (권장)
|
|
34
|
+
|
|
35
|
+
패키지 안에 **에이전트 스킬**이 들어 있다 — 설치→환경변수→훅(`adminApi` 포함)→답변
|
|
36
|
+
슬롯→처리 카드→확인까지, 프로젝트를 먼저 실측하고(프레임워크·번들러·기존 관리자 API
|
|
37
|
+
헬퍼·엔드포인트 맵·세션 만료 처리·문의 화면) 그 성격에 맞춰 배선하는 절차와, 인용해도
|
|
38
|
+
되는 API 이름 표(reference.md, 시험이 실제 export 와 대조)다.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npx cx-agent-hook skills install # Claude Code: .claude/skills/cx-agent-hook-setup/
|
|
42
|
+
npx cx-agent-hook skills install --all # + Cursor 규칙(.cursor/rules) + AGENTS.md 한 줄
|
|
43
|
+
npx cx-agent-hook skills path # 그냥 "이 파일 읽고 따라줘" 라고 줄 때
|
|
44
|
+
```
|
|
45
|
+
그 뒤 에이전트에게 **"cx-agent-hook 셋업 도와줘"** — 답변 패널만이 아니라 처리 카드
|
|
46
|
+
(adminApi)까지가 셋업이라고 스킬이 못박는다. 사람이 읽을 때는 아래 절과
|
|
47
|
+
`skills/cx-agent-hook-setup/SKILL.md` 가 같은 내용이다.
|
|
48
|
+
|
|
33
49
|
## 0.3.0 — 문의별 세션 (권장 표면)
|
|
34
50
|
|
|
35
51
|
`createCxHook` 은 그대로 동작한다(호환층). 문의별 초안 상태·영속이 필요한
|
|
@@ -57,33 +73,56 @@ CSS 는 사용처 `var(--fcx-*, 폴백)` 직참조라 소비처 `:root` 한 줄
|
|
|
57
73
|
바꾼다. 다크는 조상에 `data-fcx-theme="dark"`. headless 2단계(스타일 0 /
|
|
58
74
|
UI 0 — `aiSuggestTree`)도 공식 표면이다.
|
|
59
75
|
|
|
60
|
-
## 0.
|
|
76
|
+
## 0.5.0 — 처리 액션 (브라우저 실행기: 실행기는 CMS 자신)
|
|
61
77
|
|
|
62
78
|
답변 제안 옆에 **처리 선택지**가 실릴 수 있다 (`payload.suggested_actions`). 이 SDK 는
|
|
63
|
-
그 선택지를 보관·표시하고, 상담사 확인 뒤
|
|
64
|
-
|
|
65
|
-
|
|
79
|
+
그 선택지를 보관·표시하고, 상담사 확인 뒤 **CMS 의 기존 관리자 API 를 CMS 세션으로**
|
|
80
|
+
불러 처리하고, 결과를 허브 원장에 보고한다. 허브는 어떤 고객사 서버도 부르지 않는다.
|
|
81
|
+
**고객사 개발자가 새로 짜는 코드는 없다** — 이미 있는 요청 헬퍼와 엔드포인트 맵의
|
|
82
|
+
참조 두 개를 옵션으로 넘길 뿐이다.
|
|
66
83
|
|
|
67
84
|
```js
|
|
85
|
+
import TempAdminApi, { EndPoint } from "@/constant/TempAdminApi"; // CMS 에 이미 있는 것
|
|
86
|
+
import TempAdminApiMap from "@/constant/TempAdminApiMap";
|
|
68
87
|
const agent = createCxAgent({
|
|
69
88
|
baseUrl, token, domain,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
actorAssertion: () => fetchMyActionToken(),
|
|
89
|
+
adminApi: { request: TempAdminApi.request, endpoints: EndPoint, map: TempAdminApiMap,
|
|
90
|
+
onAuthExpired: handleAuthExpiredResponse }, // 없으면 액션 표면은 닫힌다
|
|
73
91
|
});
|
|
74
92
|
const session = agent.session(inquiryId);
|
|
75
93
|
session.attachActionUi({ setOffers: (offers, pending) => render(offers, pending) });
|
|
76
94
|
// 사람 확인(확인 문구·고위험은 2단) 뒤에만:
|
|
77
95
|
const r = await session.executeAction(offer, { actorClaimed: agentDisplayName });
|
|
78
|
-
// r.state: succeeded | failed | unknown — unknown 은 "
|
|
79
|
-
|
|
96
|
+
// r.state: succeeded | failed | unknown — unknown 은 재실행 없음("확인 필요"로 남는다)
|
|
97
|
+
// 보고가 안 됐으면(r.reported=false) session.resendResult(r.requestId) — 재실행 아님
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
3단: ① 허브 선점(request_id ULID, 전송 전 영속, 재시도 0 — 네트워크 오류면 상태를
|
|
101
|
+
묻고 기록이 없을 때만 같은 request_id 로 1회 재전송) → ② `adminApi.request` 1회 —
|
|
102
|
+
카탈로그의 실행 사양(`offer.execution`)이 `$name` 을 `params_bound` 로 치환하고
|
|
103
|
+
`response_rule` 이 성공/실패를 읽는다 → ③ 결과 보고(허브가 상태 단조·멱등 보장).
|
|
104
|
+
|
|
105
|
+
초기화 시 SDK 가 엔드포인트 맵의 **키·메서드·경로 템플릿(호스트 없음)** 을 허브에
|
|
106
|
+
1회 발행한다 — 공장 시스템 탭의 "이 CMS 가 이미 하는 처리" 목록이 여기서 온다.
|
|
107
|
+
같은 지문이면 네트워크 0(`agent.candidatesReady`, `publishActionCandidates({force})`).
|
|
108
|
+
|
|
109
|
+
처리 카드 슬롯은 어댑터 한 줄이다 — React `ActionOffersPanel`(`/react`), Vue `ActionOffersPanel`
|
|
110
|
+
(`/vue`), 그 밖은 `<cx-action-offers>`(`/element`, `defineCxActionOffers()`), 스타일은 같은
|
|
111
|
+
`styles.css`(`.fcx-act-*`). 확인 2단·고위험 체크·재실행 금지·미보고 재보고 규약을 컴포넌트가 갖는다:
|
|
112
|
+
|
|
113
|
+
```jsx
|
|
114
|
+
import { ActionOffersPanel } from "@fcg-labs/cx-agent-hook/react";
|
|
115
|
+
<ActionOffersPanel session={agent.session(csId)} agent={agent} actorClaimed={answerWriter}
|
|
116
|
+
onResult={(r) => r.state === "succeeded" && refresh()} />
|
|
80
117
|
```
|
|
81
118
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
(정본은 허브 원장). 헤드리스 뷰:
|
|
86
|
-
클래스 `fcx-act-*`, 문구는
|
|
119
|
+
카드 상태: `locked`(같은 대상·같은 입력의 최근 성공 — "이미 처리됨 · 시각 · 행위자")
|
|
120
|
+
· `unavailable`(이 CMS 가 그 API 를 모름 — fail-closed) · 실행 중 · 완료 · 실패(재시도
|
|
121
|
+
가능) · 확인 필요(unknown) · 미보고(결과 다시 보고). 눌렀던 처리는
|
|
122
|
+
`session.pendingActions()` 로 새로고침 뒤에도 안다(정본은 허브 원장). 헤드리스 뷰:
|
|
123
|
+
`actionOffersView` / `actionOffersTree`(`view.js`), 클래스 `fcx-act-*`, 문구는
|
|
124
|
+
`ui_action_*` 로케일 키. 순수 함수 `buildCandidateSnapshot` · `resolveExecution` ·
|
|
125
|
+
`judgeResponse` · `executeViaAdminApi` (`actions.js`) 는 시험·커스텀 경로용으로 공개.
|
|
87
126
|
|
|
88
127
|
## 지원 프레임워크
|
|
89
128
|
|
package/actions.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 처리 액션 — 브라우저 실행기 프로파일(actions/1 프로파일 B)의 순수 함수.
|
|
3
|
+
*
|
|
4
|
+
* 실행기는 CMS 자신이다: 이 SDK 는 CMS 가 주입한 `adminApi`(요청 헬퍼 + 엔드포인트
|
|
5
|
+
* 맵)로 **CMS 의 기존 관리자 API 를 CMS 세션으로** 부르고, 결과를 허브 원장에
|
|
6
|
+
* 보고한다. 허브는 어떤 고객사 서버도 부르지 않는다. 고객사 개발자가 새로 짜는
|
|
7
|
+
* 코드는 0 — 훅 옵션 한 줄(`adminApi: { request, endpoints, map, onAuthExpired }`)뿐.
|
|
8
|
+
*
|
|
9
|
+
* 여기 있는 것 전부가 결정적·의존성 0 이라 시험이 쉽고, 어느 CMS 든 같다:
|
|
10
|
+
* buildCandidateSnapshot — 엔드포인트 맵 → "이 CMS 가 이미 하는 처리" 스냅샷 (호스트 제거)
|
|
11
|
+
* resolveExecution — 카탈로그 실행 사양의 `$name` 을 offer.params_bound 로 치환
|
|
12
|
+
* judgeResponse — 응답 → succeeded | failed | unknown (카탈로그 response_rule)
|
|
13
|
+
* executeViaAdminApi — 위 둘을 이어 CMS API 1회 호출 (재시도 0)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const METHODS = ["get", "post", "put", "patch", "delete"];
|
|
17
|
+
|
|
18
|
+
/** 원점(스킴+호스트) 제거 — `http://h/x/:id` → `/x/:id`, 상대 경로면 그대로. */
|
|
19
|
+
export function stripOrigin(url) {
|
|
20
|
+
const s = String(url || "").trim();
|
|
21
|
+
const m = /^[a-z][a-z0-9+.-]*:\/\/[^/]*(\/.*)?$/i.exec(s);
|
|
22
|
+
if (m) return m[1] || "/";
|
|
23
|
+
return s.startsWith("/") ? s : (s ? "/" + s : "");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 결정적 64bit 지문(FNV-1a ×2) — 스냅샷 멱등 키. 암호학적 용도 아님. */
|
|
27
|
+
export function fingerprint(text) {
|
|
28
|
+
const s = String(text);
|
|
29
|
+
let h1 = 0x811c9dc5, h2 = 0x01000193 ^ 0x9747b28c;
|
|
30
|
+
for (let i = 0; i < s.length; i++) {
|
|
31
|
+
const c = s.charCodeAt(i);
|
|
32
|
+
h1 = Math.imul(h1 ^ c, 0x01000193) >>> 0;
|
|
33
|
+
h2 = Math.imul(h2 ^ c, 0x01000193) >>> 0;
|
|
34
|
+
}
|
|
35
|
+
return h1.toString(16).padStart(8, "0") + h2.toString(16).padStart(8, "0");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 엔드포인트 맵 → 후보 스냅샷. `endpoints` 는 {KEY: urlTemplate}, `map` 은
|
|
40
|
+
* {method: {urlTemplate: {}}}(화이트리스트; 없으면 메서드 []). 키 정렬·호스트 제거.
|
|
41
|
+
* @returns {{snapshotHash:string, items:Array<{endpoint_key:string, methods:string[], path_template:string}>}}
|
|
42
|
+
*/
|
|
43
|
+
export function buildCandidateSnapshot(adminApi) {
|
|
44
|
+
const endpoints = (adminApi && adminApi.endpoints && typeof adminApi.endpoints === "object") ? adminApi.endpoints : {};
|
|
45
|
+
const map = (adminApi && adminApi.map && typeof adminApi.map === "object") ? adminApi.map : {};
|
|
46
|
+
const items = [];
|
|
47
|
+
for (const key of Object.keys(endpoints).sort()) {
|
|
48
|
+
const url = endpoints[key];
|
|
49
|
+
if (typeof url !== "string" || !url) continue;
|
|
50
|
+
const methods = METHODS.filter((m) => map[m] && typeof map[m] === "object" && map[m][url] !== undefined);
|
|
51
|
+
items.push({ endpoint_key: key, methods, path_template: stripOrigin(url) });
|
|
52
|
+
}
|
|
53
|
+
return { snapshotHash: fingerprint(JSON.stringify(items)), items };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function subst(mapping, bound, missing) {
|
|
57
|
+
const out = {};
|
|
58
|
+
for (const [k, v] of Object.entries(mapping || {})) {
|
|
59
|
+
if (typeof v === "string" && v.startsWith("$")) {
|
|
60
|
+
const name = v.slice(1);
|
|
61
|
+
if (bound[name] === undefined || bound[name] === null || bound[name] === "") { missing.push(name); continue; }
|
|
62
|
+
out[k] = String(bound[name]);
|
|
63
|
+
} else {
|
|
64
|
+
out[k] = v;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 실행 사양 + params_bound → 호출 인자. endpoints 에 키가 없으면 ok:false(endpoint_missing) —
|
|
72
|
+
* fail-closed 는 여기(브라우저)다: 카탈로그가 승인돼 있어도 이 CMS 가 그 API 를
|
|
73
|
+
* 모르면 카드는 열리지 않는다.
|
|
74
|
+
* @returns {{ok:boolean, error?:string, missing?:string[], method?:string, url?:string,
|
|
75
|
+
* path?:object, query?:object, data?:object}}
|
|
76
|
+
*/
|
|
77
|
+
export function resolveExecution(execution, paramsBound, endpoints) {
|
|
78
|
+
const ex = execution && typeof execution === "object" ? execution : null;
|
|
79
|
+
if (!ex || !ex.endpoint_key) return { ok: false, error: "no_execution" };
|
|
80
|
+
const url = endpoints && typeof endpoints === "object" ? endpoints[ex.endpoint_key] : undefined;
|
|
81
|
+
if (typeof url !== "string" || !url) return { ok: false, error: "endpoint_missing", endpointKey: ex.endpoint_key };
|
|
82
|
+
const method = String(ex.method || "").toLowerCase();
|
|
83
|
+
if (!METHODS.includes(method)) return { ok: false, error: "invalid_method" };
|
|
84
|
+
const bound = paramsBound && typeof paramsBound === "object" ? paramsBound : {};
|
|
85
|
+
const missing = [];
|
|
86
|
+
const path = subst(ex.path, bound, missing);
|
|
87
|
+
const query = subst(ex.query, bound, missing);
|
|
88
|
+
const data = subst(ex.data, bound, missing);
|
|
89
|
+
if (missing.length) return { ok: false, error: "params_missing", missing };
|
|
90
|
+
return { ok: true, method, url, endpointKey: ex.endpoint_key, path, query, data };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const has = (list, code) => Array.isArray(list) && list.some((c) => String(c) === String(code));
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 응답 판정 — 계약 §B.5. 규칙은 카탈로그(response_rule)에서 온다: 성공 판정이 음성
|
|
97
|
+
* 정의(code≠4000, HTTP 항상 200)인 CMS 가 실측됐기 때문에 기본값이 없다.
|
|
98
|
+
* HTTP 4xx → failed/http_client_error · 5xx → unknown/http_server_error(적용됐을 수 있음)
|
|
99
|
+
* 2xx: auth_expired_codes → failed/auth_expired · failure_codes 또는 success_field===false → failed/business
|
|
100
|
+
* · 그 외 succeeded
|
|
101
|
+
* 전송 실패·타임아웃 → unknown/transport (호출자가 err 로 넘긴다)
|
|
102
|
+
* @returns {{state:"succeeded"|"failed"|"unknown", reasonCode:string, message:string, authExpired:boolean}}
|
|
103
|
+
*/
|
|
104
|
+
export function judgeResponse(rule, { httpStatus, body, error } = {}) {
|
|
105
|
+
const r = rule && typeof rule === "object" ? rule : {};
|
|
106
|
+
if (error) return { state: "unknown", reasonCode: "transport", message: String(error.message || error || "").slice(0, 200), authExpired: false };
|
|
107
|
+
const st = Number(httpStatus) || 0;
|
|
108
|
+
if (st >= 500) return { state: "unknown", reasonCode: "http_server_error", message: `HTTP ${st}`, authExpired: false };
|
|
109
|
+
if (st >= 400) return { state: "failed", reasonCode: "http_client_error", message: `HTTP ${st}`, authExpired: st === 401 || st === 403 };
|
|
110
|
+
if (st < 200 || st >= 300) return { state: "unknown", reasonCode: "http_other", message: `HTTP ${st}`, authExpired: false };
|
|
111
|
+
const b = body && typeof body === "object" ? body : {};
|
|
112
|
+
const codeField = String(r.code_field || "code");
|
|
113
|
+
const code = b[codeField];
|
|
114
|
+
const msg = String(b.message || b.msg || "").slice(0, 200);
|
|
115
|
+
if (code !== undefined && has(r.auth_expired_codes, code)) {
|
|
116
|
+
return { state: "failed", reasonCode: "auth_expired", message: msg, authExpired: true };
|
|
117
|
+
}
|
|
118
|
+
if (code !== undefined && has(r.failure_codes, code)) {
|
|
119
|
+
return { state: "failed", reasonCode: "business", message: msg, authExpired: false };
|
|
120
|
+
}
|
|
121
|
+
const sf = String(r.success_field || "");
|
|
122
|
+
if (sf && b[sf] === false) {
|
|
123
|
+
return { state: "failed", reasonCode: "business", message: msg, authExpired: false };
|
|
124
|
+
}
|
|
125
|
+
return { state: "succeeded", reasonCode: "", message: "", authExpired: false };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** axios 응답·평문 응답 양쪽 수용 — {status, data}. */
|
|
129
|
+
function normalizeResponse(res) {
|
|
130
|
+
if (res && typeof res === "object" && typeof res.status === "number" && "data" in res) {
|
|
131
|
+
return { httpStatus: res.status, body: res.data };
|
|
132
|
+
}
|
|
133
|
+
return { httpStatus: 200, body: res };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* CMS API 1회 호출 + 판정. **재시도 0.** adminApi.request({method,url,path,query,data}) 는
|
|
138
|
+
* CMS 의 기존 헬퍼(예: TempAdminApi.request) 참조 — 세션 인증·화이트리스트·진행바는
|
|
139
|
+
* 그 헬퍼의 몫이다. 반환은 허브 보고 형태.
|
|
140
|
+
* @returns {Promise<{state:string, reasonCode:string, message:string, httpStatus:number,
|
|
141
|
+
* latencyMs:number, authExpired:boolean, body?:any}>}
|
|
142
|
+
*/
|
|
143
|
+
export async function executeViaAdminApi(adminApi, offer) {
|
|
144
|
+
const resolved = resolveExecution(offer && offer.execution, offer && offer.params_bound, adminApi && adminApi.endpoints);
|
|
145
|
+
if (!resolved.ok) {
|
|
146
|
+
return { state: "failed", reasonCode: resolved.error === "endpoint_missing" ? "endpoint_missing" : "invalid_execution",
|
|
147
|
+
message: resolved.error, httpStatus: 0, latencyMs: 0, authExpired: false };
|
|
148
|
+
}
|
|
149
|
+
const rule = (offer.execution && offer.execution.response_rule) || {};
|
|
150
|
+
const started = Date.now();
|
|
151
|
+
let out;
|
|
152
|
+
try {
|
|
153
|
+
const res = await adminApi.request({ method: resolved.method, url: resolved.url,
|
|
154
|
+
path: resolved.path, query: resolved.query, data: resolved.data });
|
|
155
|
+
const { httpStatus, body } = normalizeResponse(res);
|
|
156
|
+
out = { ...judgeResponse(rule, { httpStatus, body }), httpStatus, body };
|
|
157
|
+
} catch (err) {
|
|
158
|
+
// axios 류: err.response 가 있으면 전송·수신은 됐다 → 상태코드로 판정
|
|
159
|
+
const resp = err && err.response;
|
|
160
|
+
if (resp && typeof resp.status === "number") {
|
|
161
|
+
out = { ...judgeResponse(rule, { httpStatus: resp.status, body: resp.data }), httpStatus: resp.status, body: resp.data };
|
|
162
|
+
} else {
|
|
163
|
+
out = { ...judgeResponse(rule, { error: err || new Error("transport") }), httpStatus: 0 };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
out.latencyMs = Date.now() - started;
|
|
167
|
+
return out;
|
|
168
|
+
}
|
package/agent.d.ts
CHANGED
|
@@ -44,13 +44,37 @@ export type CxAgentSetup = Partial<CxAgentConfig> & {
|
|
|
44
44
|
actionsImpl?: Storage;
|
|
45
45
|
};
|
|
46
46
|
/**
|
|
47
|
-
* 처리 액션 (actions/1
|
|
48
|
-
*
|
|
49
|
-
*
|
|
47
|
+
* 처리 액션 (actions/1 프로파일 B — 브라우저 실행기): CMS 의 **기존** 관리자 API
|
|
48
|
+
* 헬퍼와 엔드포인트 맵을 참조로 넘긴다. 없으면 액션 표면은 닫힌다 (선택지는 오되
|
|
49
|
+
* 실행 버튼이 없다). 고객사가 새로 짜는 코드는 없다 — 이미 있는 것 두 개의 참조.
|
|
50
|
+
* request: ({method,url,path,query,data}) → axios 응답 {status,data} 또는 본문
|
|
51
|
+
* endpoints: {KEY: urlTemplate} (`:param` 표기)
|
|
52
|
+
* map?: {method: {urlTemplate: {}}} 화이트리스트 — 후보 메서드 추출용
|
|
53
|
+
* onAuthExpired?: 세션 만료 판정(response_rule.auth_expired_codes) 시 호출
|
|
50
54
|
*/
|
|
51
|
-
|
|
55
|
+
adminApi?: AdminApi;
|
|
52
56
|
};
|
|
53
57
|
|
|
58
|
+
export interface AdminApi {
|
|
59
|
+
request: (args: { method: string; url: string; path?: Record<string, string>;
|
|
60
|
+
query?: Record<string, string>; data?: Record<string, unknown> }) => Promise<unknown>;
|
|
61
|
+
endpoints: Record<string, string>;
|
|
62
|
+
map?: Record<string, Record<string, unknown>>;
|
|
63
|
+
onAuthExpired?: (body: unknown) => void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 카탈로그 실행 사양 (offer 에 원문 그대로 실린다 — `$name` 은 params_bound 의 name) */
|
|
67
|
+
export interface ActionExecution {
|
|
68
|
+
endpoint_key: string;
|
|
69
|
+
method: "get" | "post" | "put" | "patch" | "delete";
|
|
70
|
+
path: Record<string, string>;
|
|
71
|
+
query: Record<string, string>;
|
|
72
|
+
data: Record<string, unknown>;
|
|
73
|
+
target_from: string[];
|
|
74
|
+
response_rule: { success_field?: string; code_field?: string; failure_codes?: Array<number | string>;
|
|
75
|
+
auth_expired_codes?: Array<number | string> };
|
|
76
|
+
}
|
|
77
|
+
|
|
54
78
|
export interface ComposeHandle {
|
|
55
79
|
promise: Promise<AnswerResult>;
|
|
56
80
|
abort: () => void;
|
|
@@ -103,10 +127,17 @@ export declare class InquirySession {
|
|
|
103
127
|
detachActionUi(): void;
|
|
104
128
|
/** 이 문의에서 눌렀던 처리들 (정본은 허브 원장 — 브라우저 소실은 표시 손실뿐) */
|
|
105
129
|
pendingActions(): PendingAction[];
|
|
106
|
-
/**
|
|
130
|
+
/**
|
|
131
|
+
* 실행 (3단) — 확인은 호출자가 끝낸 뒤. ① 허브 선점(request_id 발급·전송 전 영속·재시도 0)
|
|
132
|
+
* ② adminApi.request 로 CMS 자기 API 1회 ③ 결과 보고. unknown 은 재실행하지 않는다.
|
|
133
|
+
*/
|
|
107
134
|
executeAction(offer: ActionOffer, opts?: { actorClaimed?: string }): Promise<ActionResult & { requestId: string }>;
|
|
108
|
-
/**
|
|
109
|
-
|
|
135
|
+
/** 미보고 결과 재보고 (재실행 아님) — pending.unreported 인 것만 */
|
|
136
|
+
resendResult(requestId: string): Promise<{ ok: boolean; state?: string; error: string | null; httpStatus?: number }>;
|
|
137
|
+
/** 미보고 전부 재보고 — offer 갱신 시 자동 1회 */
|
|
138
|
+
flushUnreportedActions(): Promise<void>;
|
|
139
|
+
/** 잠금 조회 — target_key 있는 offer 마다 허브에 1회(세션 캐시). offers 의 locked 에 반영 */
|
|
140
|
+
refreshActionLocks(): Promise<void>;
|
|
110
141
|
}
|
|
111
142
|
|
|
112
143
|
/** 답변 payload 의 suggested_actions 항목 (허브 평탄화 뷰 — endpoint·트리거·승인 시각 없음) */
|
|
@@ -124,32 +155,43 @@ export interface ActionOffer {
|
|
|
124
155
|
expires_at: string;
|
|
125
156
|
catalog_version: number;
|
|
126
157
|
offer_reason: "issue_key" | "context" | "tokens";
|
|
158
|
+
/** 프로파일 B: 실행 사양 (원문) 과 잠금 축 */
|
|
159
|
+
execution: ActionExecution | Record<string, never>;
|
|
160
|
+
target_key: string;
|
|
161
|
+
/** session.offers 가 병합하는 브라우저 판정 — 이 CMS 가 API 를 아는가 / 같은 대상·입력 최근 성공 */
|
|
162
|
+
available?: boolean;
|
|
163
|
+
locked?: boolean;
|
|
164
|
+
lock?: { at: string; actorClaimed: string; requestId: string } | null;
|
|
127
165
|
}
|
|
128
166
|
|
|
129
167
|
export interface PendingAction {
|
|
130
168
|
requestId: string;
|
|
131
169
|
offerId: string;
|
|
132
170
|
actionKey: string;
|
|
171
|
+
targetKey?: string;
|
|
172
|
+
paramsHash?: string;
|
|
133
173
|
label: string;
|
|
134
|
-
state: "pending" | "succeeded" | "failed" | "unknown" | string;
|
|
174
|
+
state: "pending" | "received" | "succeeded" | "failed" | "unknown" | string;
|
|
135
175
|
reasonCode?: string;
|
|
136
176
|
message?: string;
|
|
177
|
+
/** 실행은 끝났으나 허브 보고가 안 됨 — resendResult 대상 */
|
|
178
|
+
unreported?: boolean;
|
|
137
179
|
at: number;
|
|
138
180
|
}
|
|
139
181
|
|
|
140
182
|
export interface ActionResult {
|
|
141
183
|
ok: boolean;
|
|
142
|
-
/** succeeded | failed | unknown |
|
|
184
|
+
/** received | succeeded | failed | unknown | '' */
|
|
143
185
|
state: string;
|
|
144
186
|
reasonCode: string;
|
|
145
187
|
message: string;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
/** offer_locked | request_conflict |
|
|
188
|
+
targetKey?: string;
|
|
189
|
+
deduplicated?: boolean;
|
|
190
|
+
/** 보고까지 끝났는가 (false 면 unreported — resendResult) */
|
|
191
|
+
reported?: boolean;
|
|
192
|
+
/** offer_locked | request_conflict | endpoint_missing | admin_api_missing | network_error | … */
|
|
151
193
|
error: string | null;
|
|
152
|
-
httpStatus
|
|
194
|
+
httpStatus?: number;
|
|
153
195
|
}
|
|
154
196
|
|
|
155
197
|
export interface CxAgent {
|
|
@@ -175,8 +217,12 @@ export interface CxAgent {
|
|
|
175
217
|
investigationStatus(
|
|
176
218
|
externalId: string | number,
|
|
177
219
|
): Promise<{ ok: boolean; status: string; jobId?: number; error: string | null }>;
|
|
178
|
-
/** 처리 액션 표면 열림 여부 — 전송 3요소 +
|
|
220
|
+
/** 처리 액션 표면 열림 여부 — 전송 3요소 + adminApi(request·endpoints) */
|
|
179
221
|
readonly actionsEnabled: boolean;
|
|
222
|
+
/** 후보 발행 완료 신호 (초기화 시 1회 자동) */
|
|
223
|
+
candidatesReady: Promise<{ ok: boolean; unchanged?: boolean; count?: number; snapshotHash?: string; error?: string | null }>;
|
|
224
|
+
/** 후보 재발행 — force 로 로컬 마커 무시 */
|
|
225
|
+
publishActionCandidates(opts?: { force?: boolean }): Promise<{ ok: boolean; unchanged?: boolean; count?: number; snapshotHash?: string; error?: string | null }>;
|
|
180
226
|
inquirySent(payload: {
|
|
181
227
|
externalId: string | number;
|
|
182
228
|
inquiry: string;
|
|
@@ -195,3 +241,13 @@ export declare function textOf(messages: Record<string, string>, reason: string)
|
|
|
195
241
|
// 헤드리스 액션 뷰 재수출 (view.d.ts 정본)
|
|
196
242
|
export { ACTION_CLS, actionOffersTree, actionOffersView } from "./view.js";
|
|
197
243
|
export type { ActionOfferItemView } from "./view.js";
|
|
244
|
+
// 브라우저 실행기 순수 함수 (actions.js) — 시험·커스텀 실행 경로용
|
|
245
|
+
export declare function buildCandidateSnapshot(adminApi: Pick<AdminApi, "endpoints" | "map">):
|
|
246
|
+
{ snapshotHash: string; items: Array<{ endpoint_key: string; methods: string[]; path_template: string }> };
|
|
247
|
+
export declare function resolveExecution(execution: unknown, paramsBound: Record<string, string>, endpoints: Record<string, string>):
|
|
248
|
+
{ ok: boolean; error?: string; missing?: string[]; method?: string; url?: string;
|
|
249
|
+
path?: Record<string, string>; query?: Record<string, string>; data?: Record<string, unknown> };
|
|
250
|
+
export declare function judgeResponse(rule: unknown, input: { httpStatus?: number; body?: unknown; error?: unknown }):
|
|
251
|
+
{ state: "succeeded" | "failed" | "unknown"; reasonCode: string; message: string; authExpired: boolean };
|
|
252
|
+
export declare function executeViaAdminApi(adminApi: AdminApi, offer: ActionOffer):
|
|
253
|
+
Promise<{ state: "succeeded" | "failed" | "unknown"; reasonCode: string; message: string; httpStatus: number; latencyMs: number; authExpired: boolean; body?: unknown }>;
|
package/agent.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CxAgent — 0.3.0 객체
|
|
2
|
+
* CxAgent — 0.3.0 객체 표면 (0.5.0: 처리 액션 브라우저 실행기). `createCxAgent(config)`
|
|
3
|
+
* 하나로 테넌트를 세운다.
|
|
3
4
|
*
|
|
4
5
|
* 구조 원칙 (객체화의 이유):
|
|
5
6
|
* - **보편 구조는 코어가 소유한다** — 어느 CMS 든 존재하는 것들: 설정
|
|
@@ -27,10 +28,12 @@ import { CxAgentClient, newRequestId } from "./client.js";
|
|
|
27
28
|
import { LOCALES, MESSAGES, normalizeLocale, resolveMessages } from "./locales.js";
|
|
28
29
|
import { ActionLedger, InquirySession, SessionStore } from "./session.js";
|
|
29
30
|
import { ACTION_CLS, actionOffersTree, actionOffersView } from "./view.js";
|
|
31
|
+
import { buildCandidateSnapshot, executeViaAdminApi, judgeResponse, resolveExecution } from "./actions.js";
|
|
30
32
|
|
|
31
33
|
// 헤드리스 액션 뷰 — 프레임워크 어댑터(react/vue) 슬롯은 0.4.x 후속. 그전엔
|
|
32
34
|
// 소비처가 이 순수 함수로 자기 렌더러에 그린다 (텍스트 노드만 — innerHTML 금지).
|
|
33
35
|
export { ACTION_CLS, actionOffersTree, actionOffersView };
|
|
36
|
+
export { buildCandidateSnapshot, executeViaAdminApi, judgeResponse, resolveExecution };
|
|
34
37
|
|
|
35
38
|
export { LOCALES, MESSAGES, normalizeLocale };
|
|
36
39
|
|
|
@@ -91,9 +94,11 @@ export function createCxAgent(config = {}) {
|
|
|
91
94
|
source = "cms",
|
|
92
95
|
draftDecorators = [koreanGreetingDecorator],
|
|
93
96
|
storage = {},
|
|
94
|
-
// 처리 액션 (actions/1):
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
+
// 처리 액션 (actions/1 프로파일 B — 브라우저 실행기): CMS 의 기존 관리자 API
|
|
98
|
+
// 헬퍼와 엔드포인트 맵을 참조로 넘긴다 — { request, endpoints, map?, onAuthExpired? }.
|
|
99
|
+
// 없으면 액션 표면은 닫힌다 (offers 는 오되 실행 버튼은 없다). 고객사가 새로
|
|
100
|
+
// 짜는 코드는 없다: 이미 있는 것 두 개의 참조뿐 (0.4.x actorAssertion 폐기).
|
|
101
|
+
adminApi,
|
|
97
102
|
...rest
|
|
98
103
|
} = config;
|
|
99
104
|
|
|
@@ -185,14 +190,42 @@ export function createCxAgent(config = {}) {
|
|
|
185
190
|
const actionLedger = storage.enabled === false || !client
|
|
186
191
|
? null
|
|
187
192
|
: new ActionLedger({ impl: storage.actionsImpl, prefix: `cx-agent-actions:${domain}` });
|
|
193
|
+
const admin = adminApi && typeof adminApi === "object" && typeof adminApi.request === "function"
|
|
194
|
+
&& adminApi.endpoints && typeof adminApi.endpoints === "object" ? adminApi : null;
|
|
188
195
|
const sessionDeps = {
|
|
189
196
|
client, messages, decorate, store,
|
|
190
197
|
textOf: (reason) => textOf(messages, reason),
|
|
191
|
-
|
|
192
|
-
actionLedger, newRequestId,
|
|
198
|
+
adminApi: admin, actionLedger, newRequestId,
|
|
193
199
|
};
|
|
194
200
|
const sessions = new Map();
|
|
195
201
|
|
|
202
|
+
// 후보 발행 — "이 CMS 가 이미 하는 처리" 스냅샷(키·메서드·경로 템플릿, 호스트 없음).
|
|
203
|
+
// 지문이 마지막 발행과 같으면 네트워크 0 (localStorage 마커, 실패 시 다음 로드에
|
|
204
|
+
// 재시도). 허브도 같은 해시면 쓰기 0. 백그라운드 — 화면을 막지 않는다.
|
|
205
|
+
const publishCandidates = async ({ force = false } = {}) => {
|
|
206
|
+
if (!client || !admin || api !== "hub") return { ok: false, error: "not_configured" };
|
|
207
|
+
const snap = buildCandidateSnapshot(admin);
|
|
208
|
+
const key = `cx-agent-catalog:${domain}`;
|
|
209
|
+
let ls = null;
|
|
210
|
+
try { ls = storage.actionsImpl || (typeof localStorage !== "undefined" ? localStorage : null); } catch { ls = null; }
|
|
211
|
+
if (!force && ls) {
|
|
212
|
+
try {
|
|
213
|
+
const prev = JSON.parse(ls.getItem(key) || "null");
|
|
214
|
+
if (prev && prev.hash === snap.snapshotHash && Date.now() - (prev.at || 0) < 24 * 60 * 60 * 1000) {
|
|
215
|
+
return { ok: true, unchanged: true, count: snap.items.length, snapshotHash: snap.snapshotHash };
|
|
216
|
+
}
|
|
217
|
+
} catch { /* 마커 손상 — 발행 진행 */ }
|
|
218
|
+
}
|
|
219
|
+
const r = await client.publishActionCandidates(snap);
|
|
220
|
+
if (r.ok && ls) {
|
|
221
|
+
try { ls.setItem(key, JSON.stringify({ hash: snap.snapshotHash, at: Date.now() })); } catch { /* quota */ }
|
|
222
|
+
}
|
|
223
|
+
return { ...r, snapshotHash: snap.snapshotHash, count: snap.items.length };
|
|
224
|
+
};
|
|
225
|
+
const candidatesReady = admin && client && api === "hub"
|
|
226
|
+
? publishCandidates().catch(() => ({ ok: false, error: "network_error" }))
|
|
227
|
+
: Promise.resolve({ ok: false, error: "not_configured" });
|
|
228
|
+
|
|
196
229
|
return {
|
|
197
230
|
/** 전송 3요소가 다 있으면 true */
|
|
198
231
|
enabled: Boolean(client),
|
|
@@ -246,8 +279,12 @@ export function createCxAgent(config = {}) {
|
|
|
246
279
|
return client.investigationStatus(externalId);
|
|
247
280
|
},
|
|
248
281
|
|
|
249
|
-
/** 처리 액션 표면이 열려 있는가 — 전송 3요소 +
|
|
250
|
-
get actionsEnabled() { return Boolean(client && sessionDeps.
|
|
282
|
+
/** 처리 액션 표면이 열려 있는가 — 전송 3요소 + adminApi(request·endpoints) */
|
|
283
|
+
get actionsEnabled() { return Boolean(client && sessionDeps.adminApi); },
|
|
284
|
+
/** 후보 발행 완료 신호 (초기화 시 1회 자동) — 결과 {ok, unchanged, count, snapshotHash} */
|
|
285
|
+
candidatesReady,
|
|
286
|
+
/** 후보 재발행 — force 로 마커 무시 (CMS 배포 직후 등) */
|
|
287
|
+
publishActionCandidates(opts) { return publishCandidates(opts || {}); },
|
|
251
288
|
|
|
252
289
|
/** 문의+최종답변 쌍 적재 (fire-and-forget, external_id 멱등) */
|
|
253
290
|
inquirySent({ externalId, inquiry, reply, agent, meta } = {}) {
|
package/angular.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Angular 진입점 — 표준 커스텀 엘리먼트를 쓴다 (CUSTOM_ELEMENTS_SCHEMA). */
|
|
2
|
-
export { defineCxAiSuggest, TAG } from "./element.js";
|
|
2
|
+
export { defineCxAiSuggest, TAG, defineCxActionOffers, ACTIONS_TAG } from "./element.js";
|
|
3
3
|
export { aiSuggestView } from "./view.js";
|
|
4
4
|
export type { AiSuggestView } from "./view.js";
|
|
5
|
-
export type { CxAiSuggestElement } from "./element.js";
|
|
5
|
+
export type { CxAiSuggestElement, CxActionOffersElement } from "./element.js";
|
package/angular.js
CHANGED
|
@@ -34,5 +34,5 @@
|
|
|
34
34
|
* `angular.json` 의 styles 배열)에 넣는다. 컴포넌트 스코프 스타일에 넣으면
|
|
35
35
|
* 뷰 캡슐화 때문에 안 닿는다 — 이 엘리먼트는 섀도 DOM 을 쓰지 않는다.
|
|
36
36
|
*/
|
|
37
|
-
export { defineCxAiSuggest, TAG } from "./element.js";
|
|
37
|
+
export { defineCxAiSuggest, TAG, defineCxActionOffers, ACTIONS_TAG } from "./element.js";
|
|
38
38
|
export { aiSuggestView } from "./view.js";
|
package/astro.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { CxHook } from "./index.js";
|
|
2
2
|
|
|
3
|
-
export { defineCxAiSuggest, TAG } from "./element.js";
|
|
3
|
+
export { defineCxAiSuggest, TAG, defineCxActionOffers, ACTIONS_TAG } from "./element.js";
|
|
4
4
|
export { aiSuggestView } from "./view.js";
|
|
5
5
|
export type { AiSuggestView } from "./view.js";
|
|
6
|
-
export type { CxAiSuggestElement } from "./element.js";
|
|
6
|
+
export type { CxAiSuggestElement, CxActionOffersElement } from "./element.js";
|
|
7
7
|
|
|
8
8
|
/** 엘리먼트 등록 + 값 주입 + 채택 구독을 한 번에. 반환값은 구독 해제. */
|
|
9
9
|
export declare function setupCxAiSuggest(
|
package/astro.js
CHANGED