@underdogai/mesh-event-schemas 0.3.0 → 0.8.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/dist/apppacks.d.ts +6070 -693
- package/dist/apppacks.d.ts.map +1 -1
- package/dist/apppacks.js +1 -1
- package/dist/blocks.d.ts +1402 -84
- package/dist/blocks.d.ts.map +1 -1
- package/dist/blocks.js +246 -13
- package/dist/blocks.js.map +1 -1
- package/dist/capabilities.d.ts +13 -1
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +13 -1
- package/dist/capabilities.js.map +1 -1
- package/dist/events.d.ts +1875 -174
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +190 -2
- package/dist/events.js.map +1 -1
- package/dist/geo.d.ts +52 -0
- package/dist/geo.d.ts.map +1 -0
- package/dist/geo.js +65 -0
- package/dist/geo.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/namespace.d.ts +13 -0
- package/dist/namespace.d.ts.map +1 -1
- package/dist/namespace.js +13 -0
- package/dist/namespace.js.map +1 -1
- package/dist/registry.d.ts +3 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +36 -3
- package/dist/registry.js.map +1 -1
- package/dist/surfaces.d.ts +31135 -3417
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +8 -1
- package/dist/surfaces.js.map +1 -1
- package/package.json +1 -1
- package/src/apppacks.ts +1 -1
- package/src/blocks.ts +260 -14
- package/src/capabilities.ts +13 -1
- package/src/events.ts +207 -5
- package/src/geo.ts +75 -0
- package/src/index.ts +1 -0
- package/src/namespace.ts +13 -0
- package/src/registry.ts +38 -2
- package/src/surfaces.ts +8 -0
package/src/events.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* 모든 형태의 단일 진실 공급원이며, appservice·bridge·client가 공통으로 import 한다.
|
|
6
6
|
*/
|
|
7
7
|
import { z } from "zod";
|
|
8
|
-
import { BlockSchema
|
|
8
|
+
import { BlockSchema } from "./blocks.js";
|
|
9
9
|
import { EventType, UI_CONTENT_KEY } from "./namespace.js";
|
|
10
10
|
|
|
11
11
|
export const SCHEMA_VERSION = 1 as const;
|
|
@@ -75,16 +75,48 @@ export type UiCardContent = z.infer<typeof UiCardContentSchema>;
|
|
|
75
75
|
// org.corp.ui.action (사용자 → 에이전트, 메시지 이벤트)
|
|
76
76
|
// --------------------------------------------------------------------------
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* 작성 클라이언트 환경(SCM-2 G6) — 폼 제출 시 클라가 **자기 신고로** 동봉하는 진단 메타.
|
|
80
|
+
* 접수 폼처럼 "어떤 버전에서 쓰였나"가 중요한 액션의 디버깅 재료다. 위조 가능한 자기 신고이므로
|
|
81
|
+
* 표시·진단용이지 권한·라우팅의 근거가 아니다(권위는 언제나 sender/서버). 모든 필드가 optional 인
|
|
82
|
+
* 이유: 플랫폼마다 없는 축이 있고(웹엔 셸이 없다), 옛 클라는 아예 안 싣는다.
|
|
83
|
+
*/
|
|
84
|
+
export const UiActionClientSchema = z.object({
|
|
85
|
+
/** "ios" | "android" | "desktop" | "web" — 새 값이 올 수 있어 enum 으로 닫지 않는다. */
|
|
86
|
+
platform: z.string().optional(),
|
|
87
|
+
/** 실행 중인 웹 번들 id(`version+commit`) — 네이티브에선 지금 도는 OTA 번들이기도 하다. */
|
|
88
|
+
bundle: z.string().optional(),
|
|
89
|
+
/** 셸 버전 — 네이티브 "version (build)" / 데스크탑 Tauri version. 웹은 없음. */
|
|
90
|
+
shell: z.string().optional(),
|
|
91
|
+
/** navigator.userAgent — OS 버전·웹뷰 판별용. 길어서 표시는 별도 줄로. */
|
|
92
|
+
ua: z.string().optional(),
|
|
93
|
+
});
|
|
94
|
+
export type UiActionClient = z.infer<typeof UiActionClientSchema>;
|
|
95
|
+
|
|
78
96
|
export const UiActionContentSchema = z.object({
|
|
79
97
|
v: VersionSchema,
|
|
80
98
|
card_id: z.string().min(1),
|
|
81
99
|
/** 어떤 컨트롤이 트리거했는지 (버튼의 action_id 등) */
|
|
82
100
|
action_id: z.string().min(1),
|
|
83
|
-
|
|
101
|
+
/**
|
|
102
|
+
* 사람이 읽는 컨트롤 이름 — 누른 시점의 버튼 글자(`ButtonBlock.text`)·모달 확인 라벨·명령 라벨의 **스냅샷**.
|
|
103
|
+
* 로그·흔적은 이것을 먼저 보이고 없으면 `action_id` 로 폴백한다. 카드가 나중에 갱신·삭제돼도 "무엇을
|
|
104
|
+
* 눌렀는지"가 이벤트 안에 남게 하려는 것이라, 렌더러가 발행 시점에 싣는다. 선택 필드 — 옛 이벤트·
|
|
105
|
+
* 카드 없는 호출(SDK invoke)은 비어 있을 수 있다.
|
|
106
|
+
*/
|
|
107
|
+
label: z.string().optional(),
|
|
108
|
+
/**
|
|
109
|
+
* `"query"`(0.7.0) — **후보를 묻는** 액션이다(`select` 의 `source: { kind: "query" }`). 사람이
|
|
110
|
+
* 누른 게 아니라 타이핑이 만든 요청이므로 앱은 부작용 없이 `ctx.result({ kind: "options", … })` 로만
|
|
111
|
+
* 답해야 한다. enum 확장이라 **옛 서버는 zod 실패로 무시**한다 — 게시 순서가 서버 먼저인 이유다.
|
|
112
|
+
*/
|
|
113
|
+
action_type: z.enum(["button", "submit", "select", "cancel", "query"]),
|
|
84
114
|
/** 폼 입력 수집 결과 (input/select/checkbox name → value) */
|
|
85
115
|
values: z.record(z.string(), z.unknown()).optional(),
|
|
86
116
|
/** 어떤 버튼의 value였는지 (선택) */
|
|
87
117
|
value: z.string().optional(),
|
|
118
|
+
/** 작성 환경(선택, G6) — `ModalSpec.diagnostics` 를 켠 모달의 confirm 제출에만 클라가 싣는다. */
|
|
119
|
+
client: UiActionClientSchema.optional(),
|
|
88
120
|
"m.relates_to": z
|
|
89
121
|
.object({
|
|
90
122
|
rel_type: z.literal("m.reference"),
|
|
@@ -94,6 +126,69 @@ export const UiActionContentSchema = z.object({
|
|
|
94
126
|
});
|
|
95
127
|
export type UiActionContent = z.infer<typeof UiActionContentSchema>;
|
|
96
128
|
|
|
129
|
+
// --------------------------------------------------------------------------
|
|
130
|
+
// org.corp.ui.action.result (에이전트 → 누른 사람, message) — 0.7.0
|
|
131
|
+
// --------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 액션 한 건의 **판정 결과**. 지금까지 앱이 `send` 로 방에 쌓던 오류 카드·거절 카드가 이 채널로 온다.
|
|
135
|
+
*
|
|
136
|
+
* 네 종류가 한 모양을 쓴다(감사 문서 A): 값 검증 실패(`reject`) · 권한 거절(`denied`) ·
|
|
137
|
+
* 성공 확인(`ok`) · 동적 후보 응답(`options`). 생애주기가 같은 술어 — "이 액션이 (이 값, 이 사람)에게
|
|
138
|
+
* 지금 유효한가" — 라서 채널을 나눌 이유가 없다.
|
|
139
|
+
*
|
|
140
|
+
* 🔴 **v1 은 방 이벤트다.** to-device 가 아니라: (1) 페더레이션·다중 기기에서 방 이벤트가 단순하고,
|
|
141
|
+
* (2) 클라가 이미 방 타임라인을 읽고 있어 새 배관이 필요 없다. 대신 **방 멤버가 읽을 수는 있다** —
|
|
142
|
+
* 그래서 비밀(다른 사람의 데이터)을 여기 싣지 않는다. `to` 는 "누구에게 그릴지"이지 접근 통제가 아니다.
|
|
143
|
+
* to-device 전환은 후속 최적화다(그때도 이 스키마는 그대로 쓴다).
|
|
144
|
+
*/
|
|
145
|
+
export const UiActionResultContentSchema = z.object({
|
|
146
|
+
v: VersionSchema,
|
|
147
|
+
/** 어떤 카드의 결과인지 — 액션의 `card_id` 를 그대로 되돌려 준다. */
|
|
148
|
+
card_id: z.string().min(1),
|
|
149
|
+
/** 어떤 컨트롤의 결과인지 — 액션의 `action_id`. 클라가 그 버튼/필드 아래에 그린다. */
|
|
150
|
+
action_id: z.string().min(1),
|
|
151
|
+
/**
|
|
152
|
+
* 무엇을 말하는가.
|
|
153
|
+
* - `ok`: 처리됐다(`message` 를 토스트로).
|
|
154
|
+
* - `reject`: 값이 잘못됐다 — `errors` 가 필드 이름 → 문장.
|
|
155
|
+
* - `denied`: 이 사람은 못 한다 — `reason` 한 줄.
|
|
156
|
+
* - `options`: 물어본 후보를 준다 — `name`(어느 select 인지) + `options`.
|
|
157
|
+
*/
|
|
158
|
+
kind: z.enum(["ok", "reject", "denied", "options"]),
|
|
159
|
+
/** `ok`/`denied` 의 사람이 읽을 한 줄(작성자 소유 문장). */
|
|
160
|
+
message: z.string().optional(),
|
|
161
|
+
/** `reject` — 입력 블록의 `name` → 그 필드 아래에 그릴 문장. */
|
|
162
|
+
errors: z.record(z.string(), z.string()).optional(),
|
|
163
|
+
/** `denied` — 왜 못 하는지 한 줄. */
|
|
164
|
+
reason: z.string().optional(),
|
|
165
|
+
/** `options` — 어느 `select` 의 후보인가(그 블록의 `name`). */
|
|
166
|
+
name: z.string().optional(),
|
|
167
|
+
/** `options` — 후보 목록. 모양은 `SelectBlockSchema.options` 와 같다(hint 포함). */
|
|
168
|
+
options: z
|
|
169
|
+
.object({ value: z.string(), label: z.string(), hint: z.string().optional() })
|
|
170
|
+
.array()
|
|
171
|
+
.optional(),
|
|
172
|
+
/**
|
|
173
|
+
* 이 결과를 **볼 사람**(MXID). 액션을 보낸 사람이다. 🔴 다른 뷰어는 이 이벤트를 받아도 그리지 않는다 —
|
|
174
|
+
* 판정은 클라가 하고(`to !== selfId` 면 무시), 그게 접근 통제가 아니라는 점은 위 머리말 그대로다.
|
|
175
|
+
*/
|
|
176
|
+
to: z.string().min(1),
|
|
177
|
+
/**
|
|
178
|
+
* 원본 **카드** 이벤트 id(있으면). `m.relates_to` 는 **액션** 이벤트를 가리키므로, 카드까지 두 단계를
|
|
179
|
+
* 거슬러야 한다 — 이 키가 있으면 클라 인덱싱이 한 단계로 끝난다(없어도 동작한다, 느릴 뿐).
|
|
180
|
+
*/
|
|
181
|
+
card_event_id: z.string().optional(),
|
|
182
|
+
/** 이 결과가 답하는 **액션** 이벤트(`org.corp.ui.action`). */
|
|
183
|
+
"m.relates_to": z
|
|
184
|
+
.object({
|
|
185
|
+
rel_type: z.literal("m.reference"),
|
|
186
|
+
event_id: z.string(),
|
|
187
|
+
})
|
|
188
|
+
.optional(),
|
|
189
|
+
});
|
|
190
|
+
export type UiActionResultContent = z.infer<typeof UiActionResultContentSchema>;
|
|
191
|
+
|
|
97
192
|
// --------------------------------------------------------------------------
|
|
98
193
|
// org.corp.command (구조화된 명령, 선택적)
|
|
99
194
|
// --------------------------------------------------------------------------
|
|
@@ -602,6 +697,85 @@ export const A2aResultContentSchema = z.object({
|
|
|
602
697
|
});
|
|
603
698
|
export type A2aResultContent = z.infer<typeof A2aResultContentSchema>;
|
|
604
699
|
|
|
700
|
+
// --------------------------------------------------------------------------
|
|
701
|
+
// org.corp.file.preview (파생 미리보기, 메시지 이벤트 — 원본 파일을 m.reference)
|
|
702
|
+
// --------------------------------------------------------------------------
|
|
703
|
+
// 브라우저가 직접 못 여는 형식의 파일(설계 도면 등)을 변환기 봇이 이미지로 렌더해, **원본 파일 타일 자체가
|
|
704
|
+
// 미리보기로 대체되게** 하는 데이터 이벤트다. Matrix 에선 남이 보낸 이벤트를 고칠 수 없으므로
|
|
705
|
+
// 원본 m.file 을 수정하는 대신 원본을 m.reference 로 가리키는 별도 이벤트를 봇이 발행하고,
|
|
706
|
+
// 클라이언트가 m.replace/m.annotation 과 같은 방식으로 흡수해 렌더한다. 미지 타입은 아무것도
|
|
707
|
+
// 안 그리므로 봇이 먼저 배포돼도 화면은 무변화 — 계약/발행/흡수의 배포 순서가 자유롭다.
|
|
708
|
+
//
|
|
709
|
+
// 한 원본의 미리보기는 pending → ready(또는 failed)로 **여러 번** 발행된다. 변환이 수 초~수십 초
|
|
710
|
+
// 걸리는 게 정상이라, 그동안 아무 신호도 없으면 사용자에겐 고장과 구별되지 않는다 — 그래서
|
|
711
|
+
// "만드는 중"이 1급 상태다. 상태 전이는 이벤트 편집이 아니라 새 이벤트 발행으로 표현한다(§1).
|
|
712
|
+
//
|
|
713
|
+
// 수신 규칙 — 흡수하는 쪽(클라이언트)이 반드시 지켜야 하는 계약의 일부:
|
|
714
|
+
// 1. **최신 승**: 같은 원본을 가리키는 미리보기가 여럿이면 origin_server_ts 가 가장 큰 것 하나만
|
|
715
|
+
// 쓴다. pending → ready 전이도 재렌더도 전부 이 규칙 하나로 처리된다 — 같은 원본에 미리보기가
|
|
716
|
+
// 여러 개 쌓이는 건 정상 상태이지 오류가 아니다.
|
|
717
|
+
// 2. **failed 는 철회**: 최신이 failed 면 자리표시까지 걷고 원본 타일로 되돌린다. 실패를 무시하고
|
|
718
|
+
// pending 을 남기면 영원히 도는 스피너가 된다(변환기는 이미 포기했는데).
|
|
719
|
+
// 3. **원본 redact → 미리보기 숨김**: 원본 m.file 이 지워지면 파생물도 화면에서 사라진다.
|
|
720
|
+
// 미리보기 이벤트 자체가 redact 되기를 기다리면 삭제한 파일의 도면이 남는다.
|
|
721
|
+
// 4. **발신자 신뢰**: AS(Application Service) 네임스페이스의 봇 계정이 보낸 것만 흡수한다.
|
|
722
|
+
// 이 저장소에서 그 판정은 classifySender + canPublish 다(레지스트리 publishableBy =
|
|
723
|
+
// agent/bridge, 사람 발행 금지) — 아무나 남의 파일을 임의 이미지로 덮어쓰는 통로가 되면 안 된다.
|
|
724
|
+
// AS 로컬파트 관례는 배포마다 다르니(여기는 @agent_*/@erp_*, `_` 접두를 쓰는 배포도 있다)
|
|
725
|
+
// 접두사를 하드코딩하지 말고 분류 함수를 거칠 것.
|
|
726
|
+
|
|
727
|
+
export const FilePreviewContentSchema = z
|
|
728
|
+
.object({
|
|
729
|
+
v: VersionSchema,
|
|
730
|
+
/**
|
|
731
|
+
* 생성 상태. 생략하면 ready 로 본다.
|
|
732
|
+
* pending = 변환 중(수신측이 자리표시를 띄운다), failed = 생성 포기(철회 — 원본 타일로 복귀).
|
|
733
|
+
*/
|
|
734
|
+
status: z.enum(["pending", "ready", "failed"]).optional(),
|
|
735
|
+
/** 봇이 업로드한 **미리보기 이미지**의 MXC URI (원본 파일의 mxc 가 아니다). ready 면 필수. */
|
|
736
|
+
mxc: z.string().startsWith("mxc://").optional(),
|
|
737
|
+
/** 미리보기 이미지의 MIME (보통 image/png). 원본 파일의 MIME 이 아니다. ready 면 필수. */
|
|
738
|
+
mime: z.string().min(1).optional(),
|
|
739
|
+
/**
|
|
740
|
+
* 미리보기 파일명(선택). 클라 렌더러 판정은 mime 이 1차 키지만 확장자를 보조로 쓰는 경로가
|
|
741
|
+
* 있어(previewRegistry), mime 이 뭉뚱그려 올 때 이 값이 판정을 건진다.
|
|
742
|
+
*/
|
|
743
|
+
filename: z.string().min(1).optional(),
|
|
744
|
+
/** 미리보기 이미지의 픽셀 폭/높이 — 받기 전에 자리를 잡아 레이아웃 점프를 막는 힌트(선택). */
|
|
745
|
+
w: z.number().int().nonnegative().optional(),
|
|
746
|
+
h: z.number().int().nonnegative().optional(),
|
|
747
|
+
/** 미리보기 이미지 바이트 크기(선택). */
|
|
748
|
+
size: z.number().int().nonnegative().optional(),
|
|
749
|
+
/**
|
|
750
|
+
* 어느 변환기가 만들었나(예: "drawing-renderer") — 진단 + 변환기별 라우팅 키.
|
|
751
|
+
* 이름이 `source` 가 아닌 이유: 이 통로에서 "source" 는 원본 파일로 읽히는 게 자연스럽고,
|
|
752
|
+
* 원본은 m.relates_to 가 가리킨다. 두 뜻이 한 이름을 나눠 쓰면 조용히 어긋난다.
|
|
753
|
+
*/
|
|
754
|
+
generator: z.string().min(1),
|
|
755
|
+
/**
|
|
756
|
+
* 원본 파일 이벤트를 참조(m.reference). 참조 이벤트 관례를 따라 형태는 optional 이지만,
|
|
757
|
+
* 이게 없으면 어느 파일에 붙일지 알 수 없어 수신측이 그냥 무시한다 — 발행할 땐 항상 붙인다.
|
|
758
|
+
*/
|
|
759
|
+
"m.relates_to": z
|
|
760
|
+
.object({ rel_type: z.literal("m.reference"), event_id: z.string() })
|
|
761
|
+
.optional(),
|
|
762
|
+
})
|
|
763
|
+
.superRefine((c, ctx) => {
|
|
764
|
+
// pending/failed 는 아직/영영 이미지가 없다. 그 외(ready·생략)는 렌더할 바이트와 그 바이트를
|
|
765
|
+
// 무엇으로 그릴지가 둘 다 있어야 한다 — 하나만 온 ready 는 수신측에서 빈 타일이 된다.
|
|
766
|
+
if (c.status === "pending" || c.status === "failed") return;
|
|
767
|
+
for (const key of ["mxc", "mime"] as const) {
|
|
768
|
+
if (!c[key]) {
|
|
769
|
+
ctx.addIssue({
|
|
770
|
+
code: z.ZodIssueCode.custom,
|
|
771
|
+
path: [key],
|
|
772
|
+
message: "ready(또는 status 생략) 미리보기에는 mxc 와 mime 이 필수다",
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
export type FilePreviewContent = z.infer<typeof FilePreviewContentSchema>;
|
|
778
|
+
|
|
605
779
|
// --------------------------------------------------------------------------
|
|
606
780
|
// m.room.message 안에 UI 카드를 끼워 넣는 형태 (graceful degrade 기본 경로)
|
|
607
781
|
// --------------------------------------------------------------------------
|
|
@@ -671,8 +845,8 @@ export function buildUiMessageEdit(
|
|
|
671
845
|
|
|
672
846
|
/** 라이브 카드 부분 갱신 명세 — 바뀐 progress/steps/state/title 만 담는다. */
|
|
673
847
|
export interface CardPatch {
|
|
674
|
-
/** 첫 progress 블록 갱신 — 숫자면 value, 객체면 value/label
|
|
675
|
-
progress?: number | { value?: number; label?: string
|
|
848
|
+
/** 첫 progress 블록 갱신 — 숫자면 value, 객체면 value/label 부분 갱신. */
|
|
849
|
+
progress?: number | { value?: number; label?: string };
|
|
676
850
|
/** 첫 steps 블록의 항목을 인덱스로 부분 갱신(작업/단계 진행). */
|
|
677
851
|
steps?: { index: number; status?: "done" | "active" | "todo" | "error"; note?: string }[];
|
|
678
852
|
/** 카드 상태(open→submitted 등) — 종료 시 클라가 입력/버튼 비활성. */
|
|
@@ -698,7 +872,6 @@ export function applyCardPatch(card: UiCardContent, patch: CardPatch): UiCardCon
|
|
|
698
872
|
// 와이어 canonical JSON 은 소수 금지 → 분수 퍼센트(예 100/3)가 들어와도 반올림해 int 로 보낸다(400 방지).
|
|
699
873
|
value: pp.value !== undefined ? Math.round(pp.value) : b.value,
|
|
700
874
|
label: pp.label !== undefined ? pp.label : b.label,
|
|
701
|
-
tone: pp.tone !== undefined ? pp.tone : b.tone,
|
|
702
875
|
};
|
|
703
876
|
}
|
|
704
877
|
if (b.type === "steps" && patch.steps?.length && !stepsPatched) {
|
|
@@ -853,6 +1026,16 @@ export function buildTaskUpdate(
|
|
|
853
1026
|
return TaskUpdateContentSchema.parse({ ...update, v: SCHEMA_VERSION });
|
|
854
1027
|
}
|
|
855
1028
|
|
|
1029
|
+
/**
|
|
1030
|
+
* 파생 미리보기 content를 만든다 (org.corp.file.preview — 원본 파일 이벤트를 m.reference).
|
|
1031
|
+
* 한 원본에 대해 pending → ready(또는 failed) 순으로 여러 번 발행한다(전이 = 새 이벤트).
|
|
1032
|
+
*/
|
|
1033
|
+
export function buildFilePreview(
|
|
1034
|
+
preview: Omit<FilePreviewContent, "v"> & { v?: typeof SCHEMA_VERSION },
|
|
1035
|
+
): FilePreviewContent {
|
|
1036
|
+
return FilePreviewContentSchema.parse({ ...preview, v: SCHEMA_VERSION });
|
|
1037
|
+
}
|
|
1038
|
+
|
|
856
1039
|
/** UI 액션 회신 content를 만든다. */
|
|
857
1040
|
export function buildUiActionContent(
|
|
858
1041
|
action: Omit<UiActionContent, "v"> & { v?: typeof SCHEMA_VERSION },
|
|
@@ -860,6 +1043,25 @@ export function buildUiActionContent(
|
|
|
860
1043
|
return UiActionContentSchema.parse({ ...action, v: SCHEMA_VERSION });
|
|
861
1044
|
}
|
|
862
1045
|
|
|
1046
|
+
/**
|
|
1047
|
+
* 액션 결과 회신 content 를 만든다 (0.7.0). **strict** 다 — 보낼 때 잘못된 모양은 즉시 거부한다
|
|
1048
|
+
* (`buildUiCardContent` 와 같은 자세: 인바운드만 관대하고 아웃바운드는 엄격).
|
|
1049
|
+
*/
|
|
1050
|
+
export function buildUiActionResult(
|
|
1051
|
+
result: Omit<UiActionResultContent, "v"> & { v?: typeof SCHEMA_VERSION },
|
|
1052
|
+
): UiActionResultContent {
|
|
1053
|
+
return UiActionResultContentSchema.parse({ ...result, v: SCHEMA_VERSION });
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/**
|
|
1057
|
+
* 인바운드 액션 결과를 읽는다 — 모양이 안 맞으면 `null`(타임라인을 깨뜨리지 않는다).
|
|
1058
|
+
* 클라는 여기서 나온 뒤 `to === selfId` 인 것만 그린다(`to` 는 표시 대상이지 접근 통제가 아니다).
|
|
1059
|
+
*/
|
|
1060
|
+
export function extractUiActionResult(content: unknown): UiActionResultContent | null {
|
|
1061
|
+
const parsed = UiActionResultContentSchema.safeParse(content);
|
|
1062
|
+
return parsed.success ? parsed.data : null;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
863
1065
|
/** m.room.message content 또는 임의 객체에서 끼워진 UI 카드를 추출한다(있으면). */
|
|
864
1066
|
export function extractUiCard(content: unknown): UiCardContent | null {
|
|
865
1067
|
if (content && typeof content === "object" && UI_CONTENT_KEY in content) {
|
package/src/geo.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 위치 값 — `location` 블록이 폼에 수집하는 값의 모양과 그 문자열 표현(geo URI).
|
|
3
|
+
*
|
|
4
|
+
* 🔴 **좌표를 숫자로 싣지 않는다.** Matrix canonical JSON 은 부동소수를 허용하지 않아서 이 레포는
|
|
5
|
+
* 슬라이더·진행률의 값을 전부 정수로 강제해 왔다(blocks.ts). 그런데 위도·경도는 본질적으로
|
|
6
|
+
* 소수라 같은 처방(반올림)을 쓸 수가 없다 — 정수로 접으면 좌표가 100km 단위로 뭉개진다.
|
|
7
|
+
* 표준이 이미 답을 갖고 있다: `m.location`(MSC3488)은 좌표를 **문자열 geo URI**로 싣는다
|
|
8
|
+
* (RFC 5870 — `geo:37.5665,126.978;u=35`). 우리도 같은 문자열을 쓴다 → 와이어 안전 + 표준 호환.
|
|
9
|
+
*
|
|
10
|
+
* 정확도·시각은 정수라 그대로 실을 수 있고, uri 안의 `u=` 와 **중복해서** 둔다 — 받는 쪽이
|
|
11
|
+
* 파싱 없이 "얼마나 믿을 값인가"를 읽게 하려는 것이다(uri 는 사람이 지도 앱으로 여는 용도).
|
|
12
|
+
*/
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
|
|
15
|
+
/** 소수 자릿수 상한 — 6자리 ≈ 0.1m. 기기가 주는 15자리를 그대로 실으면 사람이 못 읽고 이득도 없다. */
|
|
16
|
+
const GEO_DECIMALS = 6;
|
|
17
|
+
|
|
18
|
+
/** `37.566500` → `37.5665` — 끝자리 0 을 지운다(정수면 소수점도 지운다). */
|
|
19
|
+
function trimNumber(n: number): string {
|
|
20
|
+
return n.toFixed(GEO_DECIMALS).replace(/\.?0+$/, "");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** geo URI 로 실린 좌표 — 파싱 결과이자 포맷 입력. */
|
|
24
|
+
export interface GeoCoordinates {
|
|
25
|
+
latitude: number;
|
|
26
|
+
longitude: number;
|
|
27
|
+
/** 반경 오차(미터). geo URI 의 `u=` 파라미터. */
|
|
28
|
+
accuracyM?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 폼에 수집되는 위치 값 — `values[name]` 에 이 모양으로 들어간다(빈 값은 `""`). */
|
|
32
|
+
export const LocationValueSchema = z.object({
|
|
33
|
+
/** RFC 5870 geo URI — `geo:<위도>,<경도>[;u=<정확도 m>]`. `m.location` 의 uri 와 같은 형식. */
|
|
34
|
+
uri: z.string().min(1),
|
|
35
|
+
/** 반경 오차(미터, 정수). uri 의 `u=` 와 같은 값. */
|
|
36
|
+
accuracy_m: z.number().int().nonnegative().optional(),
|
|
37
|
+
/** 좌표가 만들어진 시각(epoch ms). 캐시된 좌표일 수 있어 **제출 시각과 다르다**. */
|
|
38
|
+
captured_at: z.number().int().optional(),
|
|
39
|
+
});
|
|
40
|
+
export type LocationValue = z.infer<typeof LocationValueSchema>;
|
|
41
|
+
|
|
42
|
+
/** 좌표 → geo URI. 정확도는 정수로 반올림(미지정이면 `u=` 를 안 붙인다 — "모른다"와 "0m"는 다르다). */
|
|
43
|
+
export function formatGeoUri(coords: GeoCoordinates): string {
|
|
44
|
+
const base = `geo:${trimNumber(coords.latitude)},${trimNumber(coords.longitude)}`;
|
|
45
|
+
return coords.accuracyM == null ? base : `${base};u=${Math.round(coords.accuracyM)}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* geo URI → 좌표. 못 읽으면 null(던지지 않는다 — 값은 사용자 기기가 만들어 와이어를 타고 온 것이라
|
|
50
|
+
* 언젠가는 이상한 게 온다). `geo:` 는 대소문자 무시(RFC 5870 §3.1), 파라미터는 `u=` 만 본다.
|
|
51
|
+
*/
|
|
52
|
+
export function parseGeoUri(uri: string): GeoCoordinates | null {
|
|
53
|
+
const m = /^geo:(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)(?:,-?\d+(?:\.\d+)?)?(;.*)?$/i.exec(uri.trim());
|
|
54
|
+
if (!m) return null;
|
|
55
|
+
const latitude = Number(m[1]);
|
|
56
|
+
const longitude = Number(m[2]);
|
|
57
|
+
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) return null;
|
|
58
|
+
if (Math.abs(latitude) > 90 || Math.abs(longitude) > 180) return null;
|
|
59
|
+
const u = m[3] ? /(?:^|;)u=(\d+(?:\.\d+)?)/i.exec(m[3]) : null;
|
|
60
|
+
const accuracyM = u ? Number(u[1]) : undefined;
|
|
61
|
+
return accuracyM == null || !Number.isFinite(accuracyM) ? { latitude, longitude } : { latitude, longitude, accuracyM };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 값이 "채워진 위치 값"인가 — required 게이트·요약이 쓰는 판정(빈 값 `""`·수집 중 마커와 가른다). */
|
|
65
|
+
export function isLocationValue(v: unknown): v is LocationValue {
|
|
66
|
+
return LocationValueSchema.safeParse(v).success && parseGeoUri((v as LocationValue).uri) != null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 좌표 → 폼 값. 클라이언트가 플랫폼 어댑터 결과를 이 한 줄로 와이어 모양으로 바꾼다. */
|
|
70
|
+
export function locationValueOf(coords: GeoCoordinates & { timestamp?: number }): LocationValue {
|
|
71
|
+
const value: LocationValue = { uri: formatGeoUri(coords) };
|
|
72
|
+
if (coords.accuracyM != null) value.accuracy_m = Math.round(coords.accuracyM);
|
|
73
|
+
if (coords.timestamp != null) value.captured_at = Math.round(coords.timestamp);
|
|
74
|
+
return value;
|
|
75
|
+
}
|
package/src/index.ts
CHANGED
package/src/namespace.ts
CHANGED
|
@@ -18,6 +18,17 @@ export const EventType = {
|
|
|
18
18
|
UiCard: `${NS}.ui.card`,
|
|
19
19
|
/** 사용자 → 에이전트: UI 카드 상호작용 결과 */
|
|
20
20
|
UiAction: `${NS}.ui.action`,
|
|
21
|
+
/**
|
|
22
|
+
* 에이전트 → **누른 사람**: 액션 한 건의 판정 결과 (0.7.0, 메시지 이벤트).
|
|
23
|
+
*
|
|
24
|
+
* 검증 오류·권한 거절·성공 확인·동적 옵션 응답이 전부 이 한 채널을 탄다 — 그래서 앱이 `send` 로
|
|
25
|
+
* 오류 카드·거절 카드를 방에 쌓지 않아도 된다. `m.reference` 로 **액션 이벤트**를 가리키고,
|
|
26
|
+
* `to` 가 수신자를 지정한다(다른 뷰어는 받아도 그리지 않는다).
|
|
27
|
+
*
|
|
28
|
+
* 🔴 **옛 클라는 이 타입을 모른다 → 클라 registry 가 null 을 돌려 타임라인에서 조용히 빠진다.**
|
|
29
|
+
* 그 성질이 전환기의 안전망이다 — 앱은 `result` 와 옛 알림 카드를 한동안 같이 낼 수 있다.
|
|
30
|
+
*/
|
|
31
|
+
UiActionResult: `${NS}.ui.action.result`,
|
|
21
32
|
/** 상태 이벤트: 에이전트 역량 프로필 (state_key = 에이전트 MXID) */
|
|
22
33
|
AgentCapability: `${NS}.agent.capability`,
|
|
23
34
|
/** 상태 이벤트: 엔티티(방)의 현재 워크플로 상태 (last-writer-wins) */
|
|
@@ -64,6 +75,8 @@ export const EventType = {
|
|
|
64
75
|
* 강제 잠금이 아니라 **표시용**이고, 유효기간은 수신측이 `ts` 로 판정한다(스테일 자동 소멸).
|
|
65
76
|
*/
|
|
66
77
|
NoteEditing: `${NS}.note.editing`,
|
|
78
|
+
/** 파생 미리보기: 변환기 봇이 원본 파일(m.file)을 렌더한 이미지 — 원본을 m.reference (메시지 이벤트) */
|
|
79
|
+
FilePreview: `${NS}.file.preview`,
|
|
67
80
|
} as const;
|
|
68
81
|
|
|
69
82
|
/** ERP 인바운드 이벤트 타입 접두사. 실제 타입은 `org.corp.erp.<entity>.<verb>` 로 만든다. */
|
package/src/registry.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
CommandContentSchema,
|
|
18
18
|
EntitySnapshotContentSchema,
|
|
19
19
|
ErpEventContentSchema,
|
|
20
|
+
FilePreviewContentSchema,
|
|
20
21
|
NoteBlockContentSchema,
|
|
21
22
|
NoteEditingContentSchema,
|
|
22
23
|
NoteMetaContentSchema,
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
TaskCreateContentSchema,
|
|
26
27
|
TaskUpdateContentSchema,
|
|
27
28
|
UiActionContentSchema,
|
|
29
|
+
UiActionResultContentSchema,
|
|
28
30
|
UiCardContentSchema,
|
|
29
31
|
WorkflowStateContentSchema,
|
|
30
32
|
} from "./events.js";
|
|
@@ -74,6 +76,17 @@ export const EVENT_REGISTRY: RegistryEntry[] = [
|
|
|
74
76
|
publishableBy: ["human", "agent"],
|
|
75
77
|
description: "사용자 → 에이전트: UI 카드 상호작용 결과",
|
|
76
78
|
},
|
|
79
|
+
{
|
|
80
|
+
type: EventType.UiActionResult,
|
|
81
|
+
kind: "message",
|
|
82
|
+
schema: UiActionResultContentSchema,
|
|
83
|
+
/**
|
|
84
|
+
* 🔴 **에이전트만** 발행한다 — 판정의 권위가 서버라서다. 사람이 "당신은 거절됐습니다" 를 보낼 수
|
|
85
|
+
* 있으면 이 채널이 곧 남을 속이는 통로가 된다(canPublish 가 인바운드에서 강제한다).
|
|
86
|
+
*/
|
|
87
|
+
publishableBy: ["agent"],
|
|
88
|
+
description: "에이전트 → 누른 사람: 액션 한 건의 판정 결과 (검증 오류·권한 거절·성공 확인·동적 후보). to 로 수신자 지정, m.reference 로 액션 이벤트를 가리킨다",
|
|
89
|
+
},
|
|
77
90
|
{
|
|
78
91
|
prefix: ERP_EVENT_PREFIX,
|
|
79
92
|
kind: "message",
|
|
@@ -200,6 +213,14 @@ export const EVENT_REGISTRY: RegistryEntry[] = [
|
|
|
200
213
|
publishableBy: ["agent"],
|
|
201
214
|
description: "에이전트 → 에이전트 위임 결과 회신 (A2A)",
|
|
202
215
|
},
|
|
216
|
+
{
|
|
217
|
+
type: EventType.FilePreview,
|
|
218
|
+
kind: "message",
|
|
219
|
+
schema: FilePreviewContentSchema,
|
|
220
|
+
publishableBy: ["agent", "bridge"],
|
|
221
|
+
description:
|
|
222
|
+
"파생 미리보기 이미지 (org.corp.file.preview) — 변환기 봇이 발행하고 원본 파일 이벤트를 m.reference. pending→ready/failed 를 새 이벤트로 흘리고 최신이 이긴다. 사람 발행 금지: 클라가 원본 타일을 이 이미지로 대체하는 신뢰 통로다",
|
|
223
|
+
},
|
|
203
224
|
];
|
|
204
225
|
|
|
205
226
|
/** 플러그인이 런타임에 추가한 엔트리(org.corp.<domain>.*). 코어 뒤에 둔다 = 코어 우선. */
|
|
@@ -269,11 +290,25 @@ export function canPublish(type: string, publisher: Publisher): boolean {
|
|
|
269
290
|
}
|
|
270
291
|
|
|
271
292
|
/**
|
|
272
|
-
*
|
|
293
|
+
* AS 네임스페이스 **밖**에 사는 봇 계정 — 접두사 관례로는 못 잡아 정확 일치로 인정한다.
|
|
294
|
+
*
|
|
295
|
+
* 이 홈서버의 mesh-bots 가 그렇다(`@mesh-bot`): `@agent_*` 도 `@erp_*` 도 아니라 human 으로 떨어지고,
|
|
296
|
+
* 그러면 file.preview 처럼 봇만 발행할 수 있는 타입이 **발행은 성공하는데 수신측에서 통째로 버려진다**
|
|
297
|
+
* — 화면엔 아무것도 안 나오고 이유도 로그에 안 남는 실패다.
|
|
298
|
+
*
|
|
299
|
+
* ⚠️ 부분 일치("bot 이 들어가면")로 넓히지 말 것. 이 함수는 파일 미리보기 전용이 아니라 **전 이벤트의
|
|
300
|
+
* 권한 게이트**이고, `ui.action` 은 `["human","agent"]` 라 bridge 가 못 보낸다 — `@robot` 같은 사람
|
|
301
|
+
* 계정이 봇 매칭에 걸리면 그 사람의 카드 버튼이 조용히 막힌다.
|
|
302
|
+
*/
|
|
303
|
+
const BRIDGE_LOCALPARTS = new Set(["@mesh-bot"]);
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* 발신자 MXID 를 발행자 분류(human/agent/bridge)로 매핑한다 (규범: docs/event-model.md §발행자 권한).
|
|
273
307
|
*
|
|
274
308
|
* 서버가 인바운드 이벤트를 받을 때 `canPublish(type, classifySender(sender))` 로 "이 발신자가 이 타입을
|
|
275
309
|
* 보낼 자격이 있는가"를 강제하는 데 쓴다. 분류는 AS 독점 네임스페이스 관례를 따른다
|
|
276
|
-
* (infra/registration: `@agent_*` = 에이전트, `@erp_*` = 브리지, 그 외 = 사람)
|
|
310
|
+
* (infra/registration: `@agent_*` = 에이전트, `@erp_*` = 브리지, 그 외 = 사람) — 그 관례 밖의 봇
|
|
311
|
+
* 계정은 BRIDGE_LOCALPARTS 로 정확히 하나씩 인정한다.
|
|
277
312
|
*
|
|
278
313
|
* ⚠️ 반드시 홈서버가 서명·귀속한 event.sender 로 호출할 것 — 본문(content)의 from_* 필드는 위조 가능하다.
|
|
279
314
|
*/
|
|
@@ -281,5 +316,6 @@ export function classifySender(senderId: string): Publisher {
|
|
|
281
316
|
const localpart = senderId.split(":")[0] ?? "";
|
|
282
317
|
if (localpart.startsWith("@agent_")) return "agent";
|
|
283
318
|
if (localpart.startsWith("@erp_")) return "bridge";
|
|
319
|
+
if (BRIDGE_LOCALPARTS.has(localpart)) return "bridge";
|
|
284
320
|
return "human";
|
|
285
321
|
}
|
package/src/surfaces.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { z } from "zod";
|
|
|
12
12
|
import {
|
|
13
13
|
CheckboxBlockSchema,
|
|
14
14
|
InputBlockSchema,
|
|
15
|
+
LocationBlockSchema,
|
|
15
16
|
RadioBlockSchema,
|
|
16
17
|
SelectBlockSchema,
|
|
17
18
|
SliderBlockSchema,
|
|
@@ -428,6 +429,7 @@ export const ModalFormBlockSchema = z.discriminatedUnion("type", [
|
|
|
428
429
|
CheckboxBlockSchema,
|
|
429
430
|
ToggleBlockSchema,
|
|
430
431
|
SliderBlockSchema,
|
|
432
|
+
LocationBlockSchema,
|
|
431
433
|
]);
|
|
432
434
|
export type ModalFormBlock = z.infer<typeof ModalFormBlockSchema>;
|
|
433
435
|
|
|
@@ -461,6 +463,12 @@ export const ModalSpecSchema = z.object({
|
|
|
461
463
|
steps: ModalStepSchema.array().optional(),
|
|
462
464
|
/** 자동 확인(리뷰) 스텝(G3) — steps 가 있을 때 마지막에 수집값 요약(라벨→값) 스텝을 붙인다. 기본 true. */
|
|
463
465
|
review: z.boolean().optional(),
|
|
466
|
+
/**
|
|
467
|
+
* 작성 환경 자동 동봉(SCM-2 G6) — true 면 클라가 confirm 제출의 `UiActionContent.client` 에
|
|
468
|
+
* 플랫폼·버전 메타를 싣고, **무엇을 싣는지 폼 하단에 표시한다**(몰래 수집 금지 — 액션 이벤트는
|
|
469
|
+
* 방 멤버 전원이 읽는다). 이 필드를 모르는 옛 클라는 zod strip → 메타 없이 제출(수신측은 생략 처리).
|
|
470
|
+
*/
|
|
471
|
+
diagnostics: z.boolean().optional(),
|
|
464
472
|
/** 확인 버튼(있으면) — 누르면 confirm.action_id 를 발행하고 닫는다. */
|
|
465
473
|
confirm: ModalConfirmSchema.optional(),
|
|
466
474
|
/** 취소/닫기 라벨(기본 "Cancel"). */
|