@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/view.js
CHANGED
|
@@ -105,11 +105,17 @@ export function aiSuggestTree(input) {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
|
|
108
|
-
// ── 처리 액션 (actions/1) —
|
|
108
|
+
// ── 처리 액션 (actions/1 프로파일 B) — 선택지·확인·상태·잠금의 표시 판단 ─────
|
|
109
109
|
//
|
|
110
|
-
// 실행 시맨틱은 여기 없다. offers 는
|
|
111
|
-
//
|
|
112
|
-
//
|
|
110
|
+
// 실행 시맨틱은 여기 없다. offers 는 session.offers(available·locked 병합), pending 은
|
|
111
|
+
// 세션 ActionLedger 그대로 — 이 함수는 "무엇을 어떤 상태로 보일지"만 결정한다.
|
|
112
|
+
// 텍스트 노드만 (innerHTML 금지 — 고객사 기입 문구가 실린다).
|
|
113
|
+
//
|
|
114
|
+
// 카드 상태 (하나만):
|
|
115
|
+
// locked 같은 대상·같은 입력의 최근 성공 기록 — "이미 처리됨 · 시각 · 행위자", 버튼 없음
|
|
116
|
+
// unavailable 이 CMS 가 그 API 를 모름(endpoint_key 부재) — 회색·사유, 버튼 없음
|
|
117
|
+
// pending/received 실행 중 · succeeded · failed(재시도 가능) · unknown(확인 필요, 재실행 없음)
|
|
118
|
+
// unreported 실행은 끝났으나 허브 보고가 안 됨 — "결과 다시 보고" (재실행 아님)
|
|
113
119
|
|
|
114
120
|
export const ACTION_CLS = {
|
|
115
121
|
root: "fcx-act",
|
|
@@ -122,14 +128,23 @@ export const ACTION_CLS = {
|
|
|
122
128
|
confirmText: "fcx-act-confirm-text",
|
|
123
129
|
ack: "fcx-act-ack",
|
|
124
130
|
status: "fcx-act-status",
|
|
125
|
-
|
|
131
|
+
locked: "fcx-act-locked",
|
|
132
|
+
resend: "fcx-act-resend",
|
|
126
133
|
};
|
|
127
134
|
|
|
128
135
|
const RISK_KEY = { low: "ui_action_risk_low", medium: "ui_action_risk_medium", high: "ui_action_risk_high" };
|
|
129
136
|
|
|
137
|
+
function fmtWhen(iso) {
|
|
138
|
+
if (!iso) return "";
|
|
139
|
+
const d = new Date(iso);
|
|
140
|
+
if (Number.isNaN(d.getTime())) return String(iso);
|
|
141
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
142
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
130
145
|
/**
|
|
131
|
-
* @param {Array} offers session.offers
|
|
132
|
-
* @param {Array} pending session.pendingActions() [{requestId, offerId, state, reasonCode, message, label}]
|
|
146
|
+
* @param {Array} offers session.offers (available·locked·lock 병합됨)
|
|
147
|
+
* @param {Array} pending session.pendingActions() [{requestId, offerId, state, reasonCode, message, label, unreported}]
|
|
133
148
|
* @param {boolean} enabled agent.actionsEnabled
|
|
134
149
|
* @param {string|null} confirming 확인 패널이 열린 offer_id
|
|
135
150
|
* @param {Record<string,string>} messages
|
|
@@ -141,17 +156,27 @@ export function actionOffersView({ offers = [], pending = [], enabled = true, co
|
|
|
141
156
|
const p = byOffer.get(o.offer_id) || null;
|
|
142
157
|
const state = p ? p.state : "";
|
|
143
158
|
const terminal = state === "succeeded" || state === "failed";
|
|
159
|
+
const available = o.available !== false;
|
|
160
|
+
const locked = Boolean(o.locked) && !p;
|
|
144
161
|
let statusText = "";
|
|
145
|
-
if (state === "pending") statusText = messages.ui_action_running;
|
|
162
|
+
if (state === "pending" || state === "received") statusText = messages.ui_action_running;
|
|
146
163
|
else if (state === "succeeded") statusText = messages.ui_action_succeeded;
|
|
147
164
|
else if (state === "failed") statusText = `${messages.ui_action_failed}${p && p.message ? " — " + p.message : ""}`;
|
|
148
165
|
else if (state === "unknown") statusText = messages.ui_action_unknown;
|
|
166
|
+
if (p && p.unreported) statusText = `${statusText} · ${messages.ui_action_unreported}`;
|
|
167
|
+
let lockedText = "";
|
|
168
|
+
if (locked) {
|
|
169
|
+
const who = o.lock && o.lock.actorClaimed ? ` · ${o.lock.actorClaimed}` : "";
|
|
170
|
+
lockedText = `${messages.ui_action_locked} · ${fmtWhen(o.lock && o.lock.at)}${who}`;
|
|
171
|
+
}
|
|
149
172
|
return {
|
|
150
173
|
offerId: o.offer_id, label: o.label || o.action_key, params: o.params_display || "",
|
|
151
174
|
riskLabel: messages[RISK_KEY[o.risk_level] || RISK_KEY.low] || "",
|
|
152
175
|
risk: o.risk_level || "low",
|
|
153
|
-
|
|
154
|
-
|
|
176
|
+
available, locked, lockedText,
|
|
177
|
+
unavailableText: available ? "" : messages.ui_action_unavailable,
|
|
178
|
+
// 실행 버튼: 표면 열림 ∧ 이 CMS 가 API 를 앎 ∧ 잠기지 않음 ∧ 미실행(또는 실패-재시도 가능)
|
|
179
|
+
canExecute: enabled && available && !locked && (!p || state === "failed"),
|
|
155
180
|
buttonLabel: messages.ui_action_execute,
|
|
156
181
|
confirming: confirming === o.offer_id,
|
|
157
182
|
confirmText: o.confirm || messages.ui_action_confirm_default,
|
|
@@ -160,16 +185,18 @@ export function actionOffersView({ offers = [], pending = [], enabled = true, co
|
|
|
160
185
|
okLabel: messages.ui_action_confirm_ok,
|
|
161
186
|
cancelLabel: messages.ui_action_confirm_cancel,
|
|
162
187
|
state, statusText, terminal,
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
188
|
+
// 미보고만 "결과 다시 보고" — 재실행 버튼은 없다 (unknown 은 확인 필요로 남는다)
|
|
189
|
+
canResend: Boolean(p && p.unreported),
|
|
190
|
+
resendLabel: messages.ui_action_resend,
|
|
166
191
|
requestId: p ? p.requestId : "",
|
|
167
192
|
};
|
|
168
193
|
});
|
|
169
194
|
return { visible: items.length > 0, items, disabledHint: enabled ? "" : messages.ui_action_disabled };
|
|
170
195
|
}
|
|
171
196
|
|
|
172
|
-
/** 프레임워크 중립 트리 — action: "confirm"(패널 열기) / "execute" / "cancel" / "
|
|
197
|
+
/** 프레임워크 중립 트리 — action: "confirm"(패널 열기) / "execute" / "cancel" / "resend" / "ack".
|
|
198
|
+
* 노드 필드: tag·cls·text·children·type·action·offerId·requestId·risk·state·live(상태 = role=status)·
|
|
199
|
+
* pressed(ack 토글 — 어댑터가 aria-pressed 를 offer 단위 ack 로 채운다). */
|
|
173
200
|
export function actionOffersTree(input) {
|
|
174
201
|
const v = actionOffersView(input);
|
|
175
202
|
if (!v.visible) return null;
|
|
@@ -179,11 +206,16 @@ export function actionOffersTree(input) {
|
|
|
179
206
|
...(it.params ? [{ tag: "div", cls: ACTION_CLS.params, text: it.params }] : []),
|
|
180
207
|
{ tag: "span", cls: ACTION_CLS.risk, text: it.riskLabel, risk: it.risk },
|
|
181
208
|
];
|
|
182
|
-
if (it.
|
|
209
|
+
if (it.locked) {
|
|
210
|
+
children.push({ tag: "div", cls: ACTION_CLS.locked, text: it.lockedText });
|
|
211
|
+
} else if (!it.available) {
|
|
212
|
+
children.push({ tag: "div", cls: ACTION_CLS.status, text: it.unavailableText, state: "unavailable" });
|
|
213
|
+
} else if (it.confirming) {
|
|
183
214
|
children.push({
|
|
184
215
|
tag: "div", cls: ACTION_CLS.confirm, children: [
|
|
185
216
|
{ tag: "div", cls: ACTION_CLS.confirmText, text: it.confirmText },
|
|
186
|
-
|
|
217
|
+
// 고위험 2단 확인 — 토글 **버튼**(aria-pressed): 포커스·Space/Enter 가 되어야 키보드 사용자도 실행할 수 있다
|
|
218
|
+
...(it.needsAck ? [{ tag: "button", type: "button", cls: ACTION_CLS.ack, text: it.ackLabel, action: "ack", offerId: it.offerId, pressed: true }] : []),
|
|
187
219
|
{ tag: "button", cls: ACTION_CLS.button, type: "button", text: it.okLabel, action: "execute", offerId: it.offerId },
|
|
188
220
|
{ tag: "button", cls: ACTION_CLS.button, type: "button", text: it.cancelLabel, action: "cancel", offerId: it.offerId },
|
|
189
221
|
],
|
|
@@ -191,9 +223,9 @@ export function actionOffersTree(input) {
|
|
|
191
223
|
} else if (it.canExecute) {
|
|
192
224
|
children.push({ tag: "button", cls: ACTION_CLS.button, type: "button", text: it.buttonLabel, action: "confirm", offerId: it.offerId });
|
|
193
225
|
}
|
|
194
|
-
if (it.statusText) children.push({ tag: "div", cls: ACTION_CLS.status, text: it.statusText, state: it.state });
|
|
195
|
-
if (it.
|
|
196
|
-
children.push({ tag: "button", cls: ACTION_CLS.
|
|
226
|
+
if (it.statusText) children.push({ tag: "div", cls: ACTION_CLS.status, text: it.statusText, state: it.state, live: true });
|
|
227
|
+
if (it.canResend) {
|
|
228
|
+
children.push({ tag: "button", cls: ACTION_CLS.resend, type: "button", text: it.resendLabel, action: "resend", requestId: it.requestId });
|
|
197
229
|
}
|
|
198
230
|
return { tag: "div", cls: ACTION_CLS.item, children };
|
|
199
231
|
});
|
package/vue.d.ts
CHANGED
|
@@ -15,4 +15,14 @@ export interface AiSuggestPanelProps {
|
|
|
15
15
|
|
|
16
16
|
/** `@adopt` 로 채택 문구를 emit 한다. hook 이 미설정이면 아무것도 그리지 않는다. */
|
|
17
17
|
export declare const AiSuggestPanel: DefineComponent<AiSuggestPanelProps>;
|
|
18
|
+
|
|
19
|
+
export interface ActionOffersPanelProps {
|
|
20
|
+
session: import("./agent.js").InquirySession;
|
|
21
|
+
agent: import("./agent.js").CxAgent;
|
|
22
|
+
actorClaimed?: string;
|
|
23
|
+
className?: string;
|
|
24
|
+
}
|
|
25
|
+
/** 처리 선택지 카드 (actions/1 프로파일 B) — emits "result" */
|
|
26
|
+
export declare const ActionOffersPanel: DefineComponent<ActionOffersPanelProps>;
|
|
27
|
+
|
|
18
28
|
export default AiSuggestPanel;
|
package/vue.js
CHANGED
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
* JSX·SFC 를 쓰지 않는 이유는 react.js 와 같다: 이 패키지에는 빌드 단계가 없다.
|
|
20
20
|
* Vue 는 optional peer 다 (>=3.0).
|
|
21
21
|
*/
|
|
22
|
-
import { computed, defineComponent, h, ref, watch } from "vue";
|
|
22
|
+
import { computed, defineComponent, h, onBeforeUnmount, ref, watch } from "vue";
|
|
23
23
|
|
|
24
|
-
import { aiSuggestTree, aiSuggestView, CLS } from "./view.js";
|
|
24
|
+
import { actionOffersTree, aiSuggestTree, aiSuggestView, CLS } from "./view.js";
|
|
25
25
|
|
|
26
26
|
export { aiSuggestView };
|
|
27
27
|
|
|
@@ -107,4 +107,82 @@ export const AiSuggestPanel = defineComponent({
|
|
|
107
107
|
},
|
|
108
108
|
});
|
|
109
109
|
|
|
110
|
+
// ── 처리 선택지 카드 (actions/1 프로파일 B) — react.js ActionOffersPanel 과 같은 규약 ──
|
|
111
|
+
//
|
|
112
|
+
// ```vue
|
|
113
|
+
// <ActionOffersPanel :session="getCsSession(csId)" :agent="agent" :actor-claimed="answerWriter" @result="onResult" />
|
|
114
|
+
// ```
|
|
115
|
+
export const ActionOffersPanel = defineComponent({
|
|
116
|
+
name: "ActionOffersPanel",
|
|
117
|
+
props: {
|
|
118
|
+
session: { type: Object, required: true },
|
|
119
|
+
agent: { type: Object, required: true },
|
|
120
|
+
actorClaimed: { type: String, default: "" },
|
|
121
|
+
className: { type: String, default: "" },
|
|
122
|
+
},
|
|
123
|
+
emits: ["result"],
|
|
124
|
+
setup(props, { emit }) {
|
|
125
|
+
const offers = ref(props.session ? props.session.offers : []);
|
|
126
|
+
const pending = ref(props.session ? props.session.pendingActions() : []);
|
|
127
|
+
const confirming = ref(null);
|
|
128
|
+
const ackedOffer = ref(null); // 고위험 확인은 offer 단위
|
|
129
|
+
let attached = null;
|
|
130
|
+
const attach = (s) => {
|
|
131
|
+
if (attached && typeof attached.detachActionUi === "function") attached.detachActionUi();
|
|
132
|
+
attached = s || null;
|
|
133
|
+
confirming.value = null; ackedOffer.value = null;
|
|
134
|
+
if (s && typeof s.attachActionUi === "function") {
|
|
135
|
+
s.attachActionUi({ setOffers: (o, p) => { offers.value = o; pending.value = p; } });
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
watch(() => props.session, attach, { immediate: true });
|
|
139
|
+
onBeforeUnmount(() => attach(null));
|
|
140
|
+
|
|
141
|
+
const execute = async (offerId) => {
|
|
142
|
+
const offer = offers.value.find((o) => o.offer_id === offerId);
|
|
143
|
+
if (!offer) return;
|
|
144
|
+
if (offer.risk_level === "high" && ackedOffer.value !== offerId) return;
|
|
145
|
+
confirming.value = null; ackedOffer.value = null;
|
|
146
|
+
const r = await props.session.executeAction(offer, { actorClaimed: props.actorClaimed });
|
|
147
|
+
emit("result", r);
|
|
148
|
+
};
|
|
149
|
+
const actions = {
|
|
150
|
+
confirm: (e) => { confirming.value = e.currentTarget.dataset.offer || null; ackedOffer.value = null; },
|
|
151
|
+
cancel: () => { confirming.value = null; ackedOffer.value = null; },
|
|
152
|
+
ack: (e) => { const id = e.currentTarget.dataset.offer; ackedOffer.value = ackedOffer.value === id ? null : id; },
|
|
153
|
+
execute: (e) => execute(e.currentTarget.dataset.offer),
|
|
154
|
+
resend: (e) => props.session.resendResult(e.currentTarget.dataset.request),
|
|
155
|
+
};
|
|
156
|
+
const tree = computed(() => actionOffersTree({
|
|
157
|
+
offers: offers.value, pending: pending.value, enabled: Boolean(props.agent && props.agent.actionsEnabled),
|
|
158
|
+
confirming: confirming.value, messages: props.agent ? props.agent.messages : {},
|
|
159
|
+
}));
|
|
160
|
+
const render = (node) => {
|
|
161
|
+
const p = { class: node.cls };
|
|
162
|
+
if (node.tag === "button") p.type = node.type || "button";
|
|
163
|
+
if (node.action && actions[node.action]) {
|
|
164
|
+
p.onClick = actions[node.action];
|
|
165
|
+
if (node.offerId) p["data-offer"] = node.offerId;
|
|
166
|
+
if (node.requestId) p["data-request"] = node.requestId;
|
|
167
|
+
if (node.action === "execute" && confirming.value) {
|
|
168
|
+
const o = offers.value.find((x) => x.offer_id === node.offerId);
|
|
169
|
+
if (o && o.risk_level === "high" && ackedOffer.value !== node.offerId) p.disabled = true;
|
|
170
|
+
}
|
|
171
|
+
if (node.pressed) p["aria-pressed"] = ackedOffer.value === node.offerId;
|
|
172
|
+
}
|
|
173
|
+
if (node.live) p.role = "status";
|
|
174
|
+
if (node.state) p["data-state"] = node.state;
|
|
175
|
+
if (node.risk) p["data-risk"] = node.risk;
|
|
176
|
+
return h(node.tag, p, node.children ? node.children.map(render) : node.text);
|
|
177
|
+
};
|
|
178
|
+
return () => {
|
|
179
|
+
if (!props.session || !props.agent) return null;
|
|
180
|
+
const t = tree.value;
|
|
181
|
+
if (!t) return null;
|
|
182
|
+
return h("div", { class: props.className ? `${t.cls} ${props.className}` : t.cls,
|
|
183
|
+
onClick: (event) => event.stopPropagation() }, t.children.map(render));
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
|
|
110
188
|
export default AiSuggestPanel;
|