@fcg-labs/cx-agent-hook 0.3.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 +82 -0
- package/README.md +67 -0
- package/actions.js +168 -0
- package/agent.d.ts +128 -1
- package/agent.js +56 -3
- 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 +166 -0
- package/element.d.ts +12 -1
- package/element.js +124 -1
- package/locales.js +68 -0
- 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 +192 -0
- 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 +29 -2
- package/view.js +129 -0
- 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,85 @@
|
|
|
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
|
+
|
|
54
|
+
# 0.3.0 → 0.4.0 마이그레이션
|
|
55
|
+
|
|
56
|
+
## 요약 — 깨지는 것은 없다
|
|
57
|
+
|
|
58
|
+
0.4.0 은 **추가형**이다. 0.3.0 표면(`createCxAgent`·`session`·어댑터·클래스명)은
|
|
59
|
+
전부 그대로 동작한다 (기존 시험 107건 무수정 통과, 신규 10건).
|
|
60
|
+
|
|
61
|
+
## 새로 생긴 것 — 처리 액션 (선택)
|
|
62
|
+
|
|
63
|
+
- `createCxAgent({ actorAssertion })` — 고객사가 발행한 단명·목적 한정 행위자
|
|
64
|
+
토큰을 돌려주는 함수. **넘기지 않으면 아무것도 바뀌지 않는다** (표면 닫힘).
|
|
65
|
+
- `session.offers` / `session.attachActionUi(ui)` / `session.executeAction(offer)` /
|
|
66
|
+
`session.reconcileAction(requestId)` / `session.pendingActions()`
|
|
67
|
+
- `agent.actionsEnabled`
|
|
68
|
+
- 헤드리스: `actionOffersView` · `actionOffersTree` · `ACTION_CLS` (`view.js`)
|
|
69
|
+
- 로케일 키 `ui_action_*` 14종 (ko 정본, en/ja/zh-TW 미검수)
|
|
70
|
+
- 저장: `storage.actionsImpl` — 눌렀던 처리의 pending 기록(기본 localStorage,
|
|
71
|
+
종단 후 24h). 세션 영속(sessionStorage 30분)과 **수명이 다르다**.
|
|
72
|
+
|
|
73
|
+
## 행동 계약 (새 표면에만 해당)
|
|
74
|
+
|
|
75
|
+
- 실행 요청은 **재시도 0**. 네트워크 오류 → 상태 조회 1회 → 허브에 기록이
|
|
76
|
+
없을 때(404)만 같은 request_id 로 1회 재전송.
|
|
77
|
+
- `unknown`(실행 여부 미확인)은 재실행 버튼이 없다 — `reconcileAction`(조회)로만
|
|
78
|
+
수렴한다. 화면이 이 규칙을 어기면 이중 실행이 된다.
|
|
79
|
+
- 결과 `result` 는 서버 화이트리스트 키만 온다. GET 상태 조회에는 `result` 가 없다.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
1
83
|
# 0.2.x → 0.3.0 마이그레이션
|
|
2
84
|
|
|
3
85
|
## 요약 — 깨지는 것은 없다
|
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,6 +73,57 @@ CSS 는 사용처 `var(--fcx-*, 폴백)` 직참조라 소비처 `:root` 한 줄
|
|
|
57
73
|
바꾼다. 다크는 조상에 `data-fcx-theme="dark"`. headless 2단계(스타일 0 /
|
|
58
74
|
UI 0 — `aiSuggestTree`)도 공식 표면이다.
|
|
59
75
|
|
|
76
|
+
## 0.5.0 — 처리 액션 (브라우저 실행기: 실행기는 CMS 자신)
|
|
77
|
+
|
|
78
|
+
답변 제안 옆에 **처리 선택지**가 실릴 수 있다 (`payload.suggested_actions`). 이 SDK 는
|
|
79
|
+
그 선택지를 보관·표시하고, 상담사 확인 뒤 **CMS 의 기존 관리자 API 를 CMS 세션으로**
|
|
80
|
+
불러 처리하고, 결과를 허브 원장에 보고한다. 허브는 어떤 고객사 서버도 부르지 않는다.
|
|
81
|
+
**고객사 개발자가 새로 짜는 코드는 없다** — 이미 있는 요청 헬퍼와 엔드포인트 맵의
|
|
82
|
+
참조 두 개를 옵션으로 넘길 뿐이다.
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
import TempAdminApi, { EndPoint } from "@/constant/TempAdminApi"; // CMS 에 이미 있는 것
|
|
86
|
+
import TempAdminApiMap from "@/constant/TempAdminApiMap";
|
|
87
|
+
const agent = createCxAgent({
|
|
88
|
+
baseUrl, token, domain,
|
|
89
|
+
adminApi: { request: TempAdminApi.request, endpoints: EndPoint, map: TempAdminApiMap,
|
|
90
|
+
onAuthExpired: handleAuthExpiredResponse }, // 없으면 액션 표면은 닫힌다
|
|
91
|
+
});
|
|
92
|
+
const session = agent.session(inquiryId);
|
|
93
|
+
session.attachActionUi({ setOffers: (offers, pending) => render(offers, pending) });
|
|
94
|
+
// 사람 확인(확인 문구·고위험은 2단) 뒤에만:
|
|
95
|
+
const r = await session.executeAction(offer, { actorClaimed: agentDisplayName });
|
|
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()} />
|
|
117
|
+
```
|
|
118
|
+
|
|
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`) 는 시험·커스텀 경로용으로 공개.
|
|
126
|
+
|
|
60
127
|
## 지원 프레임워크
|
|
61
128
|
|
|
62
129
|
| 진입점 | 대상 | 필요한 peer |
|
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
|
@@ -39,9 +39,42 @@ export type CxAgentSetup = Partial<CxAgentConfig> & {
|
|
|
39
39
|
source?: string;
|
|
40
40
|
/** 초안 후처리 사슬 — 기본 [koreanGreetingDecorator]. 비한국어 테넌트는 [] */
|
|
41
41
|
draftDecorators?: DraftDecorator[];
|
|
42
|
-
storage?: SessionStorageConfig
|
|
42
|
+
storage?: SessionStorageConfig & {
|
|
43
|
+
/** 처리 액션 pending 저장소 구현 (기본 localStorage) — 세션 영속과 수명이 다르다 */
|
|
44
|
+
actionsImpl?: Storage;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
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) 시 호출
|
|
54
|
+
*/
|
|
55
|
+
adminApi?: AdminApi;
|
|
43
56
|
};
|
|
44
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
|
+
|
|
45
78
|
export interface ComposeHandle {
|
|
46
79
|
promise: Promise<AnswerResult>;
|
|
47
80
|
abort: () => void;
|
|
@@ -85,6 +118,80 @@ export declare class InquirySession {
|
|
|
85
118
|
scored(score: number, agent?: string): void;
|
|
86
119
|
edited(finalText: string, agent?: string): void;
|
|
87
120
|
discarded(agent?: string, note?: string): void;
|
|
121
|
+
|
|
122
|
+
// ── 처리 액션 (actions/1) ──
|
|
123
|
+
/** 마지막 답변의 처리 선택지 (만료 제외). 실행 시맨틱 없음 — 사람 확인 후 executeAction */
|
|
124
|
+
readonly offers: ActionOffer[];
|
|
125
|
+
/** 선택지 UI sink — setOffers(offers, pending) 를 받는다 */
|
|
126
|
+
attachActionUi(ui: { setOffers: (offers: ActionOffer[], pending: PendingAction[]) => void }): void;
|
|
127
|
+
detachActionUi(): void;
|
|
128
|
+
/** 이 문의에서 눌렀던 처리들 (정본은 허브 원장 — 브라우저 소실은 표시 손실뿐) */
|
|
129
|
+
pendingActions(): PendingAction[];
|
|
130
|
+
/**
|
|
131
|
+
* 실행 (3단) — 확인은 호출자가 끝낸 뒤. ① 허브 선점(request_id 발급·전송 전 영속·재시도 0)
|
|
132
|
+
* ② adminApi.request 로 CMS 자기 API 1회 ③ 결과 보고. unknown 은 재실행하지 않는다.
|
|
133
|
+
*/
|
|
134
|
+
executeAction(offer: ActionOffer, opts?: { actorClaimed?: string }): Promise<ActionResult & { requestId: string }>;
|
|
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>;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** 답변 payload 의 suggested_actions 항목 (허브 평탄화 뷰 — endpoint·트리거·승인 시각 없음) */
|
|
144
|
+
export interface ActionOffer {
|
|
145
|
+
offer_id: string;
|
|
146
|
+
event_id: string;
|
|
147
|
+
action_key: string;
|
|
148
|
+
params_bound: Record<string, string>;
|
|
149
|
+
params_hash: string;
|
|
150
|
+
params_display: string;
|
|
151
|
+
label: string;
|
|
152
|
+
description: string;
|
|
153
|
+
confirm: string;
|
|
154
|
+
risk_level: "low" | "medium" | "high";
|
|
155
|
+
expires_at: string;
|
|
156
|
+
catalog_version: number;
|
|
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;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface PendingAction {
|
|
168
|
+
requestId: string;
|
|
169
|
+
offerId: string;
|
|
170
|
+
actionKey: string;
|
|
171
|
+
targetKey?: string;
|
|
172
|
+
paramsHash?: string;
|
|
173
|
+
label: string;
|
|
174
|
+
state: "pending" | "received" | "succeeded" | "failed" | "unknown" | string;
|
|
175
|
+
reasonCode?: string;
|
|
176
|
+
message?: string;
|
|
177
|
+
/** 실행은 끝났으나 허브 보고가 안 됨 — resendResult 대상 */
|
|
178
|
+
unreported?: boolean;
|
|
179
|
+
at: number;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface ActionResult {
|
|
183
|
+
ok: boolean;
|
|
184
|
+
/** received | succeeded | failed | unknown | '' */
|
|
185
|
+
state: string;
|
|
186
|
+
reasonCode: string;
|
|
187
|
+
message: string;
|
|
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 | … */
|
|
193
|
+
error: string | null;
|
|
194
|
+
httpStatus?: number;
|
|
88
195
|
}
|
|
89
196
|
|
|
90
197
|
export interface CxAgent {
|
|
@@ -110,6 +217,12 @@ export interface CxAgent {
|
|
|
110
217
|
investigationStatus(
|
|
111
218
|
externalId: string | number,
|
|
112
219
|
): Promise<{ ok: boolean; status: string; jobId?: number; error: string | null }>;
|
|
220
|
+
/** 처리 액션 표면 열림 여부 — 전송 3요소 + adminApi(request·endpoints) */
|
|
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 }>;
|
|
113
226
|
inquirySent(payload: {
|
|
114
227
|
externalId: string | number;
|
|
115
228
|
inquiry: string;
|
|
@@ -124,3 +237,17 @@ export declare function createCxAgent(config?: CxAgentSetup): CxAgent;
|
|
|
124
237
|
export declare const koreanGreetingDecorator: DraftDecorator;
|
|
125
238
|
export declare function notConfiguredResult(): AnswerResult;
|
|
126
239
|
export declare function textOf(messages: Record<string, string>, reason: string): string;
|
|
240
|
+
|
|
241
|
+
// 헤드리스 액션 뷰 재수출 (view.d.ts 정본)
|
|
242
|
+
export { ACTION_CLS, actionOffersTree, actionOffersView } from "./view.js";
|
|
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 든 존재하는 것들: 설정
|
|
@@ -23,9 +24,16 @@
|
|
|
23
24
|
* session.answerSent(finalText, agentId); // gold 쌍 후킹 + 세션 소멸
|
|
24
25
|
* ```
|
|
25
26
|
*/
|
|
26
|
-
import { CxAgentClient } from "./client.js";
|
|
27
|
+
import { CxAgentClient, newRequestId } from "./client.js";
|
|
27
28
|
import { LOCALES, MESSAGES, normalizeLocale, resolveMessages } from "./locales.js";
|
|
28
|
-
import { InquirySession, SessionStore } from "./session.js";
|
|
29
|
+
import { ActionLedger, InquirySession, SessionStore } from "./session.js";
|
|
30
|
+
import { ACTION_CLS, actionOffersTree, actionOffersView } from "./view.js";
|
|
31
|
+
import { buildCandidateSnapshot, executeViaAdminApi, judgeResponse, resolveExecution } from "./actions.js";
|
|
32
|
+
|
|
33
|
+
// 헤드리스 액션 뷰 — 프레임워크 어댑터(react/vue) 슬롯은 0.4.x 후속. 그전엔
|
|
34
|
+
// 소비처가 이 순수 함수로 자기 렌더러에 그린다 (텍스트 노드만 — innerHTML 금지).
|
|
35
|
+
export { ACTION_CLS, actionOffersTree, actionOffersView };
|
|
36
|
+
export { buildCandidateSnapshot, executeViaAdminApi, judgeResponse, resolveExecution };
|
|
29
37
|
|
|
30
38
|
export { LOCALES, MESSAGES, normalizeLocale };
|
|
31
39
|
|
|
@@ -86,6 +94,11 @@ export function createCxAgent(config = {}) {
|
|
|
86
94
|
source = "cms",
|
|
87
95
|
draftDecorators = [koreanGreetingDecorator],
|
|
88
96
|
storage = {},
|
|
97
|
+
// 처리 액션 (actions/1 프로파일 B — 브라우저 실행기): CMS 의 기존 관리자 API
|
|
98
|
+
// 헬퍼와 엔드포인트 맵을 참조로 넘긴다 — { request, endpoints, map?, onAuthExpired? }.
|
|
99
|
+
// 없으면 액션 표면은 닫힌다 (offers 는 오되 실행 버튼은 없다). 고객사가 새로
|
|
100
|
+
// 짜는 코드는 없다: 이미 있는 것 두 개의 참조뿐 (0.4.x actorAssertion 폐기).
|
|
101
|
+
adminApi,
|
|
89
102
|
...rest
|
|
90
103
|
} = config;
|
|
91
104
|
|
|
@@ -174,12 +187,45 @@ export function createCxAgent(config = {}) {
|
|
|
174
187
|
},
|
|
175
188
|
});
|
|
176
189
|
|
|
190
|
+
const actionLedger = storage.enabled === false || !client
|
|
191
|
+
? null
|
|
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;
|
|
177
195
|
const sessionDeps = {
|
|
178
196
|
client, messages, decorate, store,
|
|
179
197
|
textOf: (reason) => textOf(messages, reason),
|
|
198
|
+
adminApi: admin, actionLedger, newRequestId,
|
|
180
199
|
};
|
|
181
200
|
const sessions = new Map();
|
|
182
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
|
+
|
|
183
229
|
return {
|
|
184
230
|
/** 전송 3요소가 다 있으면 true */
|
|
185
231
|
enabled: Boolean(client),
|
|
@@ -233,6 +279,13 @@ export function createCxAgent(config = {}) {
|
|
|
233
279
|
return client.investigationStatus(externalId);
|
|
234
280
|
},
|
|
235
281
|
|
|
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 || {}); },
|
|
288
|
+
|
|
236
289
|
/** 문의+최종답변 쌍 적재 (fire-and-forget, external_id 멱등) */
|
|
237
290
|
inquirySent({ externalId, inquiry, reply, agent, meta } = {}) {
|
|
238
291
|
if (!client || !externalId || !inquiry) return;
|
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