mirror of
https://github.com/NomaDamas/k-skill.git
synced 2026-06-24 02:04:11 +00:00
Narrow KakaoTalk deletion to visible text proof
Constrain destructive KakaoTalk deletion to the proof the helper can actually establish: an outbound DB candidate whose normalized text maps to exactly one visible targetable UI bubble, with explicit confirmation success required.\n\nConstraint: KakaoTalk Accessibility does not expose a reliable DB message-id identity in this non-interactive automation path.\nRejected: Preserve [type] fallback targets or raw-text duplicate checks | they can bridge DB selection to a different visible UI bubble.\nRejected: Report deletion success after best-effort confirmation clicks | UI label drift could create false success.\nConfidence: high\nScope-risk: narrow\nDirective: Do not widen delete automation beyond the visible-bubble contract unless UI identity proof is added and tested.\nTested: PYTHONPATH=.:scripts python3 -m unittest scripts.test_kakaotalk_mac; python3 scripts/kakaotalk_mac.py delete --help; python3 scripts/kakaotalk_mac.py delete-last --help; git diff --check; npm run ci\nNot-tested: Live KakaoTalk/macOS Accessibility deletion was not executed in this non-interactive session.
This commit is contained in:
parent
0358fa53d2
commit
daf98ee292
4 changed files with 72 additions and 16 deletions
|
|
@ -7,7 +7,7 @@
|
|||
- 키워드로 전체 대화 검색
|
||||
- 나와의 채팅으로 안전하게 테스트 전송
|
||||
- 사용자 확인 후 특정 채팅방으로 메시지 전송
|
||||
- 로컬 메시지 ID 또는 최근 보낸 메시지를 기준으로 UI 삭제 자동화
|
||||
- 로컬 메시지 ID로 후보를 고른 뒤 현재 보이는 정확히 하나의 UI bubble 삭제 자동화
|
||||
|
||||
## 먼저 필요한 것
|
||||
|
||||
|
|
@ -82,7 +82,7 @@ helper `scripts/kakaotalk_mac.py` 는 그 얇은 read-only 어댑터 역할을
|
|||
|
||||
## 메시지 삭제
|
||||
|
||||
`delete` / `delete-last` 는 카카오톡 Mac UI의 삭제 메뉴를 Accessibility 로 자동화한다. 메시지 조회는 로컬 DB에서 하고, 실제 삭제는 UI에서 수행한다.
|
||||
`delete` / `delete-last` 는 카카오톡 Mac UI의 삭제 메뉴를 Accessibility 로 자동화한다. 메시지 조회와 outbound 여부 검증은 로컬 DB에서 하고, 실제 삭제는 현재 보이는 UI bubble 에서 수행한다.
|
||||
|
||||
```bash
|
||||
python3 scripts/kakaotalk_mac.py messages --chat "팀 공지방" --limit 20 --json
|
||||
|
|
@ -92,7 +92,7 @@ python3 scripts/kakaotalk_mac.py delete-last "팀 공지방" --everyone
|
|||
```
|
||||
|
||||
- `--everyone` 은 내가 보낸 메시지에만 허용된다.
|
||||
- UI 삭제 단계는 활성 채팅방을 확인하고, 대화 transcript 영역에서 정규화된 텍스트가 정확히 하나의 visible message bubble 과 일치할 때만 진행한다. 메시지 텍스트가 보이지 않거나 같은 텍스트가 여러 개면 실패한다.
|
||||
- UI 삭제 단계는 활성 채팅방을 확인하고, 선택된 outbound DB 메시지의 정규화된 텍스트가 대화 transcript 영역에서 정확히 하나의 visible targetable message bubble 과 일치할 때만 진행한다. 로컬 DB message id가 UI bubble identity를 직접 증명하는 것은 아니므로, 메시지 텍스트가 비어 있거나 첨부/비텍스트이거나 보이지 않거나 정규화 후 같은 텍스트가 여러 개이거나 최종 확인 버튼을 클릭할 수 없으면 실패한다.
|
||||
- `chats`, `messages`, `search`, `schema` 는 read-only 이지만 `delete` / `delete-last` 는 side effect 이다.
|
||||
|
||||
## 주의할 점
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@ kakaocli send "채팅방 이름" "보낼 문장"
|
|||
|
||||
### 7. Delete a sent message only with explicit operator intent
|
||||
|
||||
삭제는 카카오톡 Mac UI(우클릭 → 삭제)를 접근성으로 자동화한다. 먼저 `messages --json` 으로 로컬 메시지 ID를 확인하고, 항상 `--dry-run` 으로 대상 채팅방/메시지를 확인한 뒤 실행한다. `--everyone` 은 내가 보낸 메시지에만 허용된다.
|
||||
삭제는 카카오톡 Mac UI(우클릭 → 삭제)를 접근성으로 자동화한다. 먼저 `messages --json` 으로 로컬 메시지 ID와 보낸 메시지 여부를 확인하고, 항상 `--dry-run` 으로 대상 채팅방/메시지를 확인한 뒤 실행한다. `--everyone` 은 내가 보낸 메시지에만 허용된다.
|
||||
|
||||
```bash
|
||||
python3 scripts/kakaotalk_mac.py messages --chat "채팅방 이름" --limit 20 --json
|
||||
|
|
@ -182,8 +182,8 @@ python3 scripts/kakaotalk_mac.py delete-last "채팅방 이름" --everyone
|
|||
주의:
|
||||
|
||||
- helper의 `chats`, `messages`, `search`, `schema` 는 read-only 경로다. `delete` / `delete-last` 는 UI side effect 이므로 Accessibility 권한과 명시적 실행 의도가 필요하다.
|
||||
- 메시지 ID는 로컬 DB의 `messages --json` 출력 기준이다. UI 삭제 단계는 활성 채팅방을 확인하고, 대화 transcript 영역에서 정규화된 텍스트가 정확히 하나의 visible message bubble 과 일치할 때만 진행한다.
|
||||
- 대상 메시지 텍스트가 보이지 않거나, 같은 텍스트가 여러 개 있거나, 활성 채팅방을 확인할 수 없으면 삭제 자동화는 실패한다.
|
||||
- 메시지 ID는 로컬 DB의 `messages --json` 출력 기준이며 UI에서 동일한 DB row를 직접 증명할 수 있다는 뜻은 아니다. 실행 계약은 선택된 outbound DB 메시지의 정규화된 텍스트가 현재 활성 채팅방 transcript 영역에서 정확히 하나의 visible targetable message bubble 과 일치할 때만 삭제하는 것이다.
|
||||
- 대상 메시지 텍스트가 비어 있거나 첨부/비텍스트 메시지이거나, 정규화 후 같은 텍스트가 여러 개 있거나, 대상 bubble 이 보이지 않거나, 활성 채팅방/삭제 범위/최종 확인 버튼을 확인할 수 없으면 삭제 자동화는 실패한다.
|
||||
|
||||
### 8. Use login storage only when the user explicitly wants auto-login
|
||||
|
||||
|
|
|
|||
|
|
@ -628,14 +628,23 @@ def select_delete_target(
|
|||
)
|
||||
|
||||
text = raw.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
text = f"[{raw.get('type', 'message')}]"
|
||||
matching_text_ids = [_message_id(message) for message in messages if message.get("text") == text]
|
||||
normalized_text = _normalize_delete_text(text)
|
||||
if normalized_text is None:
|
||||
raise AuthResolutionError(
|
||||
"Delete automation requires a selected outbound message with non-empty text; "
|
||||
"non-text, attachment, or empty-text messages are not safe UI delete targets."
|
||||
)
|
||||
matching_text_ids = [
|
||||
_message_id(message)
|
||||
for message in messages
|
||||
if _normalize_delete_text(message.get("text")) == normalized_text
|
||||
]
|
||||
if len([item for item in matching_text_ids if item is not None]) > 1:
|
||||
raise AuthResolutionError(
|
||||
"Refusing to automate deletion because multiple fetched messages have the same text. "
|
||||
"Refusing to automate deletion because multiple fetched messages have the same normalized visible text. "
|
||||
"Open the chat with only the target visible or use delete-last for the latest outbound message."
|
||||
)
|
||||
text = normalized_text
|
||||
timestamp = raw.get("timestamp")
|
||||
return DeleteTarget(
|
||||
message_id=selected_id,
|
||||
|
|
@ -645,6 +654,13 @@ def select_delete_target(
|
|||
)
|
||||
|
||||
|
||||
def _normalize_delete_text(value: Any) -> str | None:
|
||||
if not isinstance(value, str):
|
||||
return None
|
||||
normalized = " ".join(value.split())
|
||||
return normalized or None
|
||||
|
||||
|
||||
def _message_id(message: dict[str, Any]) -> int | None:
|
||||
value = message.get("id")
|
||||
if isinstance(value, bool):
|
||||
|
|
@ -701,7 +717,6 @@ end normalizeText
|
|||
|
||||
set chatName to {_applescript_string(chat)}
|
||||
set messageText to {_applescript_string(target.text)}
|
||||
set messageTimestamp to {_applescript_string(target.timestamp or "")}
|
||||
set normalizedChatName to normalizeText(chatName)
|
||||
set normalizedMessageText to normalizeText(messageText)
|
||||
set deleteLabels to {{{labels}}}
|
||||
|
|
@ -828,12 +843,18 @@ tell application "System Events"
|
|||
if didChooseDeleteScope is false then error "Could not choose the requested delete scope."
|
||||
delay 0.3
|
||||
|
||||
set didConfirmDelete to false
|
||||
try
|
||||
click button "삭제" of window 1
|
||||
set didConfirmDelete to true
|
||||
end try
|
||||
try
|
||||
click button "Delete" of window 1
|
||||
end try
|
||||
if didConfirmDelete is false then
|
||||
try
|
||||
click button "Delete" of window 1
|
||||
set didConfirmDelete to true
|
||||
end try
|
||||
end if
|
||||
if didConfirmDelete is false then error "Could not confirm the KakaoTalk delete dialog."
|
||||
end tell
|
||||
end tell
|
||||
""".strip()
|
||||
|
|
|
|||
|
|
@ -367,7 +367,7 @@ class KakaoTalkMacHelperTests(unittest.TestCase):
|
|||
self.assertIn("AXShowMenu", script)
|
||||
self.assertIn("Target message text matched multiple visible targetable message bubbles", script)
|
||||
self.assertIn("Could not verify the active KakaoTalk chat", script)
|
||||
self.assertIn("set messageTimestamp to", script)
|
||||
self.assertNotIn("set messageTimestamp to", script)
|
||||
|
||||
def test_run_delete_dry_run_validates_target_but_skips_ui_side_effect(self) -> None:
|
||||
stdout = io.StringIO()
|
||||
|
|
@ -412,7 +412,42 @@ class KakaoTalkMacHelperTests(unittest.TestCase):
|
|||
with self.assertRaises(kakaotalk_mac.AuthResolutionError) as context:
|
||||
kakaotalk_mac.select_delete_target(messages, message_id=42, delete_last=False, everyone=True)
|
||||
|
||||
self.assertIn("same text", str(context.exception))
|
||||
self.assertIn("same normalized visible text", str(context.exception))
|
||||
|
||||
|
||||
def test_select_delete_target_rejects_duplicate_normalized_visible_text(self) -> None:
|
||||
messages = [
|
||||
{"id": 42, "text": "same visible text", "is_from_me": True},
|
||||
{"id": 41, "text": "same visible text", "is_from_me": True},
|
||||
]
|
||||
|
||||
with self.assertRaises(kakaotalk_mac.AuthResolutionError) as context:
|
||||
kakaotalk_mac.select_delete_target(messages, message_id=42, delete_last=False, everyone=False)
|
||||
|
||||
self.assertIn("same normalized visible text", str(context.exception))
|
||||
|
||||
def test_select_delete_target_rejects_empty_or_non_text_delete_target(self) -> None:
|
||||
messages = [{"id": 42, "text": " ", "type": "photo", "is_from_me": True}]
|
||||
|
||||
with self.assertRaises(kakaotalk_mac.AuthResolutionError) as context:
|
||||
kakaotalk_mac.select_delete_target(messages, message_id=42, delete_last=False, everyone=False)
|
||||
|
||||
self.assertIn("non-empty text", str(context.exception))
|
||||
|
||||
def test_build_delete_osascript_fails_when_final_confirmation_is_missing(self) -> None:
|
||||
target = kakaotalk_mac.DeleteTarget(
|
||||
message_id=42,
|
||||
text="테스트 메시지",
|
||||
timestamp="2026-05-14T00:00:00Z",
|
||||
is_from_me=True,
|
||||
)
|
||||
|
||||
script = kakaotalk_mac.build_delete_osascript("팀 공지방", target, everyone=True)
|
||||
|
||||
self.assertIn("set didConfirmDelete to false", script)
|
||||
self.assertIn("set didConfirmDelete to true", script)
|
||||
self.assertIn("if didConfirmDelete is false then error", script)
|
||||
self.assertIn("Could not confirm the KakaoTalk delete dialog", script)
|
||||
|
||||
def test_build_parser_rejects_negative_max_user_id(self) -> None:
|
||||
parser = kakaotalk_mac.build_parser()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue