
    .i_                         d Z ddlZddlZddlmZ ddlmZ ddlmZ ddl	m
Z
 ddlmZ ddlmZmZmZ dd	lmZ  ee          Zd
ZdZdZdZdZdZdZ G d d          ZdS )uC  Slack Interactive Gateway — Block Kit control panel for zhihu-hunter.

Manages two human-in-the-loop approval gates:

    Gate 1  [✍️ 生成草稿] / [🚫 忽略]
            Triggered when ZhihuHunter surfaces candidate questions.

    Gate 2  [✅ 确认发布] / [🔄 重新生成] / [❌ 取消]
            Triggered after an answer draft has been generated.

Usage:
    gateway = SlackInteractiveGateway(slack_client, hunter, engine, channel)
    gateway.register(app)   # call once at bot startup
    gateway.post_question_cards(questions)  # call from dispatcher
    N)Path)Optional)App)	WebClient)setup_logger)AnswerDraftZhihuHunterZhihuQuestion)ZhihuPlaywrightEnginezhihu_generate_draftzhihu_ignorezhihu_publishzhihu_regeneratezhihu_cancelzhihu_regen_modali   c                      e Zd ZdZ	 d'dededededee	         ddfd	Z
d
eddfdZ	 d'dee         dee         ddfdZdedededdfdZd(dZd(dZd(dZd(dZd(dZd(dZdedee         fdZdededee         fdZdedee         fdZdedee         fdZdedee         fdZdedee	         fd Zd!ed"eddfd#Zdedefd$Z dedee         fd%Z!deddfd&Z"dS ))SlackInteractiveGatewaya  Block Kit gateway providing two approval gates for the zhihu-hunter workflow.

    Args:
        slack_client: Authenticated WebClient (same token as the Bolt App).
        hunter: ZhihuHunter instance for draft generation.
        engine: ZhihuPlaywrightEngine instance for publishing.
        notify_channel: Default Slack channel/DM for outbound cards.
        state_dir: Directory for storing draft JSON files between interactions.
            Defaults to <project_root>/data/zhihu_drafts.
    Nslack_clienthunterenginenotify_channel	state_dirreturnc                     || _         || _        || _        || _        |p6t	          t
                                                    j        d         dz  dz  | _        | j        	                    dd           d S )N   datazhihu_draftsTparentsexist_ok)
clientr   r   r   r   __file__resolver   r   mkdir)selfr   r   r   r   r   s         B/root/projects/butler/slack_bot/zhihu/slack_interactive_gateway.py__init__z SlackInteractiveGateway.__init__8   sz     *##,& 
NN""$$,Q/&8>I 	 	TD99999    appc                     |                     t                    | j                    |                     t                    | j                    |                     t
                    | j                    |                     t                    | j                    |                     t                    | j
                    |                    t                    | j                   t                              d           dS )zAttach all action handlers to a Slack Bolt App instance.

        Call this once during bot startup, after the App object is created.

        Args:
            app: The Slack Bolt App that receives Socket Mode events.
        z-ZhihuHunter: Slack action handlers registeredN)action_ACT_GENERATE_handle_generate_draft_ACT_IGNORE_handle_ignore_ACT_PUBLISH_handle_publish
_ACT_REGEN_handle_regenerate_ACT_CANCEL_handle_cancelview_VIEW_REGEN_handle_regenerate_submitloggerinfo)r%   r)   s     r&   registerz SlackInteractiveGateway.registerK   s     	"

=!!$"=>>>

; 3444 

<  !5666

:t6777

; 3444d<===CDDDDDr(   	questionschannelc                    |p| j         }|s| j                            |d           dS | j                            |dt          |           d           |D ]w}	 | j                            ||                     |          d|j                    <# t          $ r/}t                              d|j         d	|            Y d}~pd}~ww xY wdS )
u:  Post one Block Kit card per discovered question (Gate 1).

        Each card includes [✍️ 生成草稿] and [🚫 忽略] buttons.

        Args:
            questions: Candidate questions returned by ZhihuHunter.scan_and_hunt().
            channel: Override channel. Defaults to self.notify_channel.
        uA   🔍 本次未发现符合条件的知乎问题，下次再试。)r=   textNu   🔍 发现 *u,   * 个知乎相关问题，请逐一确认：u   🔍 知乎问题: )r=   blocksr?   z"Failed to post question card for 'z': )	r   r!   chat_postMessagelen_question_blockstitle	Exceptionr9   error)r%   r<   r=   chqes         r&   post_question_cardsz+SlackInteractiveGateway.post_question_cards]   s>    ++ 	K((X )    F$$]Y]]] 	% 	
 	
 	
  	S 	SAS,,00338qw88 -    
  S S SQ!'QQaQQRRRRRRRRS	S 	Ss   8B
C%C		Cdraft	thread_tsc                    |                      |          }	 | j                            |||                     ||          d|j        j                    dS # t          $ r(}t                              d|            Y d}~dS d}~ww xY w)uW  Post an answer draft card as a thread reply (Gate 2).

        Includes [✅ 确认发布], [🔄 重新生成], and [❌ 取消] buttons.

        Args:
            draft: Generated AnswerDraft.
            channel: Channel containing the original question card.
            thread_ts: ts of the question card message (reply target).
        u+   📝 草稿已生成，等待确认发布: r=   rL   r@   r?   zFailed to post draft card: N)	_save_draftr!   rA   _draft_blocksquestionrD   rE   r9   rF   )r%   rK   r=   rL   draft_idrI   s         r&   post_draft_cardz'SlackInteractiveGateway.post_draft_card   s     ##E**	<K((#))%::Y5>CWYY	 )       	< 	< 	<LL:q::;;;;;;;;;	<s   ?A 
B
"BB
c                     |             |d         d         }|d         d         }|d         d         d         }|                     |||                     d          d	
           	 t          j        |          }| j                            |          }|                     |          }	|	r%|                    dt          |	          i          }| 	                    |          }
|
                    |||                     ||
          d|j                    |                     |||                     d|j         d|j         d          d
           dS # t          $ rY}t                               d| d           |                     |||                     d|           d
           Y d}~dS d}~ww xY w)uJ   Gate 1 → [✍️ 生成草稿]: generate draft and post as thread reply.r=   idmessagetsactionsr   valueu%   ✍️ 草稿生成中，请稍候...u   草稿生成中...r=   rW   r@   r?   
vault_fileupdateu   📝 草稿已生成: rN   uA   📩 草稿已生成，请在上方回复中查看并确认。
*<|>*u   草稿已生成zDraft generation failed: Texc_infou   草稿生成失败：u   草稿生成失败N)chat_update_loading_blockr
   model_validate_jsonr   draft_answer_save_to_vault
model_copystrrO   rA   rP   rD   _done_blockurlrE   r9   rF   _error_block)r%   ackbodyr!   r=   msg_tsraw_valrQ   rK   r[   rR   rI   s               r&   r-   z.SlackInteractiveGateway._handle_generate_draft   s:   	?4(	?4(	?1%g. 	&&'NOO%	 	 	
 	
 	
$	$8AAH{//99E ,,U33J Q((s:0O(PP''..H ## ))%::>hn>>	 $    ''zYaYezzhphvzzz  '        	 	 	LL8Q884LHHH(()D)D)DEE)	          	s   (C1E 
F>%AF99F>c                 \     |              |dd|                      d                     dS )u4   Gate 1 → [🚫 忽略]: dismiss the question card.Tu   🚫 已忽略u   🚫 已忽略此问题replace_originalr?   r@   Nri   r%   rl   responds      r&   r/   z&SlackInteractiveGateway._handle_ignore   sH    !!##$=>>	
 	
 	
 	
 	
 	
r(   c                     |             |d         d         }|d         d         }|d         d         d         }|                     |||                     d          d	
           	 |                     |          }|t          d| d          | j                                        st          d          | j                            |j        j        |j	                  }|                     ||| 
                    d|j        j         d|j        j         d| d          d| 
           |j        r|                     |j        |           |                     |           dS # t          $ rZ}	t                               d|	 d           |                     |||                     d|	 d          d
           Y d}	~	dS d}	~	ww xY w)uE   Gate 2 → [✅ 确认发布]: login to Zhihu and publish the answer.r=   rU   rV   rW   rX   r   rY   u.   ⏳ 正在登录知乎并发布，请稍候...u   发布中...rZ   Nu   草稿文件不存在（id=u   ），可能已过期u'   知乎登录失败，请扫码后重试)question_urlanswer_textu    ✅ *发布成功！*
问题: *<r^   u   >*
回答: <u   |查看已发布回答>u   ✅ 已发布: zPublish failed: Tr`   u   发布失败：u7   
你可以重新点击发布，或联系排查日志。u   发布失败)rb   rc   _load_draftRuntimeErrorr   ensure_logged_inpublish_answerrQ   rj   contentri   rD   r[   _update_vault_published_delete_draftrE   r9   rF   rk   )
r%   rl   rm   r!   r=   rn   rR   rK   
answer_urlrI   s
             r&   r1   z'SlackInteractiveGateway._handle_publish   sj   	?4(	?4(	?1%g.&&'WXX	 	 	
 	
 	
'	$$X..E}"#`#`#`#`aaa ;//11 N"#LMMM33"^/!M 4  J
 ''D!&!3D D6;n6JD D *D D D 
 4z33  	 	 	  K,,U-=zJJJx((((( 		 		 		LL/A//$L???((aaaaa  $          		s   (DE+ +
G5AG

Gc                     |             |d         d         }|d         d         }|d         d         d         }	 t          j        |          }|j        }n# t          $ r d}Y nw xY wt	          j        |||d	          }	|                    |d
         dt          ddddddddd|	ddd| dddddddddddddddddgd           d S )!uQ   Gate 2 → [🔄 重新生成]: open a modal for user to enter revision feedback.r=   rU   rV   rW   rX   r   rY   u   （问题信息丢失）)r=   rn   regen_value
trigger_idmodal
plain_textu   重新生成草稿typer?   u   生成u   取消sectionmrkdwn*u=   *

请输入修改意见，Bot 会据此重新生成草稿。inputfeedback_blockTu   修改意见plain_text_inputfeedback_inputu`   例：内容太技术了，偏向大众一些；字数控制在500字以内；结尾加一句诗)r   	action_id	multilineplaceholder)r   block_idoptionallabelelement)r   callback_idrD   submitcloseprivate_metadatar@   )r   r6   N)r
   rd   rD   rE   jsondumps
views_openr7   )
r%   rl   rm   r!   r=   rn   ro   rQ   question_titler   s
             r&   r3   z*SlackInteractiveGateway._handle_regenerate  s   	?4(	?4(	?1%g.	,$8AAH &^NN  	8 	8 	87NNN	8
  :! "'
 '
   	L)*".8LMM#/BB#/BB$4 !*$,$w$w$w$w! !  !($4$(*6!O!O$6)9)-(4 )K, ,	$ $   	 "	
 "	
 "	
 "	
 "	
s   A A'&A'c           	          |             t          j        |d         d                   }|d         }|d         }|d         }|d         d         d                             di                               d	i                               d
          pd}|                    |||                     d          d           	 t          j        |          }	| j                            |	|          }
| 	                    |
          }|r%|

                    dt          |          i          }
|                     |
          }|                    |||                     |
|          d|	j                    dS # t          $ rY}t                               d| d           |                    |||                     d|           d           Y d}~dS d}~ww xY w)u5   Modal submit → regenerate draft with user feedback.r6   r   r=   rn   r   statevaluesr   r   rY    u#   🔄 重新生成中，请稍候...u   重新生成中...rZ   )feedbackr[   r\   u   📝 草稿已重新生成: zRegenerate submit failed: Tr`   u   重新生成失败：u   重新生成失败N)r   loadsgetrb   rc   r
   rd   r   re   rf   rg   rh   rO   rP   rD   rE   r9   rF   rk   )r%   rl   rm   r!   metadatar=   rn   ro   r   rQ   rK   r[   rR   rI   s                 r&   r8   z1SlackInteractiveGateway._handle_regenerate_submitK  s5   JtF|,>?@@Y'X&]+L!(+S!2&&S!2&&S\\   	 	 	&&'LMM%	 	 	
 	
 	
	$8AAH{//8/LLE ,,U33J Q((s:0O(PP''..H))%::DHNDD	        	 	 	LL9a99DLIII(()D)D)DEE)	          	s   B6E9 9
GAGGc                 \     |              |dd|                      d                     dS )u0   Gate 2 → [❌ 取消]: dismiss the draft card.Tu   ❌ 已取消u   ❌ 已取消本次发布rq   Nrs   rt   s      r&   r5   z&SlackInteractiveGateway._handle_cancel{  sH    ! ##$?@@	
 	
 	
 	
 	
 	
r(   rQ   c                    |                     d|j        pddd         i          }|                                }d|j         d|j         dg}|j        r%|                    d	|j        dd
                     |j        r1|                    dd                    |j                   d           ddd                    |          dddddddddt          |ddddddt          ddgdgS )zBuild Gate 1 Block Kit blocks for a single question.

        Args:
            question: ZhihuQuestion to display.

        Returns:
            List of Slack Block Kit block dicts.
        snippetr   Nd   r\   z*<r^   r_   z>    u   _关键词：    · _r   r   
r   rX   buttonr   u   ✍️ 生成草稿Tr   r?   emojiprimaryr   r?   styler   rY   u   🚫 忽略ignorer   r?   r   rY   r   elements)
rg   r   model_dump_jsonrj   rD   appendkeywordsjoinr,   r.   )r%   rQ   q_for_button	btn_value
body_liness        r&   rC   z(SlackInteractiveGateway._question_blocks  sf     ** 0 6B=> + 
 
 !0022	<8<<<(.<<<=
 	=;8#3DSD#9;;<<< 	QOfkk(:K.L.LOOOPPP "!)499Z3H3HII 
 " !))5?T_c d d!*%2!*  !))5}W[ \ \%0!)	  
 	
r(   rR   c                    |j         dt                   }t          |j                   t          k    r|dt          |j                    dz  }|j                            d|j        j        pddd         i          }|                                }dd	d
|j        j         d|j        j         dddddidd	|ddg}|j	        r?|
                    dd	dd                    |j	        dd                    dgd           |ddidddddddt          |ddddddt          |ddddddd t          d!dgdgz  }|S )"zBuild Gate 2 Block Kit blocks for a draft answer.

        Args:
            draft: AnswerDraft to display.
            draft_id: UUID key used as the publish button value.

        Returns:
            List of Slack Block Kit block dicts.
        Nu   
_…（共 u     字，发布时使用全文）_r   r   r   r\   r   r   u   📝 *草稿已生成* | <r^   >r   r   dividercontextu   📚 RAG 来源：r      r   rX   r   r   u   ✅ 确认发布Tr   r   r   u   🔄 重新生成r   u
   ❌ 取消dangercancel)r}   _PREVIEW_CHARSrB   rQ   rg   r   r   rj   rD   rag_sourcesr   r   r0   r2   r4   )r%   rK   rR   preview	q_for_btnr   r@   s          r&   rP   z%SlackInteractiveGateway._draft_blocks  s    -0u}..[s5='9'9[[[[G N-- 6 <"dsdCD . 
 
	  //11 "$I!N.I I161EI I I 	 	 Y!!)7;; 
$  	MM!$UU=NrPQr=R1S1SUU       	Y! !))5?Q\` a a!*%1!)  !))5?R]a b b%/!,	  !))5|VZ [ [!)%0!)  
 	
8 r(   rV   c                     dd|ddgS )zSimple single-section block for loading state.

        Args:
            message: Text to display (supports mrkdwn).

        Returns:
            List with one section block.
        r   r   r    r%   rV   s     r&   rc   z&SlackInteractiveGateway._loading_block       #Xw,O,OPPQQr(   c                     dd|ddgS )zSingle-section block for a completed/dismissed state.

        Args:
            message: Text to display (supports mrkdwn).

        Returns:
            List with one section block (no action buttons).
        r   r   r   r   r   s     r&   ri   z#SlackInteractiveGateway._done_block  r   r(   c                     ddd| ddgS )u   Single-section block for error display.

        Args:
            message: Error description (supports mrkdwn).

        Returns:
            List with one section block prefixed with ⚠️.
        r   r   u   ⚠️ r   r   r   s     r&   rk   z$SlackInteractiveGateway._error_block  s/     %/B/B/BCC
 
  	r(   c                 |   ddl }ddlm} 	 | j        j        dz  }|                    dd           |                                                    d          }|                    dd	|j	        j
                  dd
                             d	          }|| d	| dz  }|                    d|j	        j
         d|j	        j         d| d|j	        j
         d|j         dd           t                              d|            |S # t"          $ r(}t                              d|            Y d}~dS d}~ww xY w)zPersist a draft as a Markdown file in <vault>/zhihu/.

        Args:
            draft: The generated AnswerDraft to save.

        Returns:
            Absolute path of the written file, or None on failure.
        r   N)datezhihuTr   z%Y-%m-%dz[^\w\u4e00-\u9fff]+-(   z.mdz---
title: "z"
url: z
date: z$
status: draft
tags: [zhihu]
---

# z

r   utf-8encodingzDraft saved to vault: zFailed to save draft to vault: )redatetimer   r   
vault_pathr$   todaystrftimesubrQ   rD   strip
write_textrj   r}   r9   r:   rE   warning)	r%   rK   r   _date	zhihu_dirdate_strslug	file_pathrI   s	            r&   rf   z&SlackInteractiveGateway._save_to_vault'  s    				******	.8IOOD4O888{{}}--j99H660#u~7KLLSbSQWWX[\\D!x$;$;$$;$;$;;I  %!N0% %*% % "% % ^)% % =% % % ! !    KK<<<=== 	 	 	NN@Q@@AAA44444	s   C<D	 	
D;D66D;r[   r   c                    	 t          |          }|                                sdS |                    d          }|                    dd| d          }|                    |d           t
                              d|            dS # t          $ r(}t
                              d|            Y d}~dS d}~ww xY w)	zUpdate the vault file's frontmatter status to published.

        Args:
            vault_file: Absolute path string stored in AnswerDraft.vault_file.
            answer_url: Published answer URL to record.
        Nr   r   zstatus: draftzstatus: published
answer_url:    zVault file marked published: zFailed to update vault file: )	r   exists	read_textreplacer   r9   r:   rE   r   )r%   r[   r   fpr}   rI   s         r&   r~   z/SlackInteractiveGateway._update_vault_publishedM  s    	@j!!B99;; llGl44Goo>*>> G
 MM'GM444KK<<<===== 	@ 	@ 	@NN>1>>?????????	@s   #B A$B 
B?B::B?c                     t          j                    j        dd         }| j        | dz  }|                    |                                d           t                              d|            |S )zPersist a draft to disk and return its UUID key.

        Args:
            draft: AnswerDraft to save.

        Returns:
            UUID string key (used as publish button value).
        N   .jsonr   r   zDraft saved: )uuiduuid4hexr   r   r   r9   debug)r%   rK   rR   r   s       r&   rO   z#SlackInteractiveGateway._save_draftc  su     JLL$SbS)N%7%7%77	U2244wGGG0Y00111r(   c                 
   | j         | dz  }|                                sdS 	 t          j        |                    d                    S # t
          $ r+}t                              d| d|            Y d}~dS d}~ww xY w)zLoad a draft by its UUID key.

        Args:
            draft_id: Key returned by _save_draft().

        Returns:
            AnswerDraft if found, None if the file does not exist.
        r   Nr   r   zFailed to load draft : )r   r   r   rd   r   rE   r9   rF   r%   rR   r   rI   s       r&   ry   z#SlackInteractiveGateway._load_draftr  s     N%7%7%77	!! 	4	293F3FPW3F3X3XYYY 	 	 	LL@@@Q@@AAA44444	s   'A 
B A==Bc                     | j         | dz  }	 |                    d           dS # t          $ r+}t                              d| d|            Y d}~dS d}~ww xY w)zwRemove a draft file after successful publish.

        Args:
            draft_id: Key of the draft to delete.
        r   T)
missing_okzCould not delete draft file r   N)r   unlinkrE   r9   r   r   s       r&   r   z%SlackInteractiveGateway._delete_draft  s     N%7%7%77		K----- 	K 	K 	KNNI(IIaIIJJJJJJJJJ	Ks   ' 
A AA)N)r   N)#__name__
__module____qualname____doc__r   r	   r   rh   r   r   r'   r   r;   listr
   rJ   r   rS   r-   r/   r1   r3   r8   r5   dictrC   rP   rc   ri   rk   rf   r~   rO   ry   r   r   r(   r&   r   r   ,   sH       	 	" %): :: : &	:
 : D>: 
: : : :&EC ED E E E E* "&!S !S&!S #!S 
	!S !S !S !SJ<< < 	<
 
< < < <83 3 3 3j
 
 
 
5 5 5 5n6
 6
 6
 6
p. . . .`
 
 
 
,
 ,
4: ,
 ,
 ,
 ,
\K; K# K$t* K K K KZ	Rc 	Rd4j 	R 	R 	R 	R	R3 	R4: 	R 	R 	R 	RC DJ     $K $HTN $ $ $ $L@# @3 @4 @ @ @ @,     C H[,A    $
Kc 
Kd 
K 
K 
K 
K 
K 
Kr(   r   )r   r   r   pathlibr   typingr   
slack_boltr   	slack_sdkr   health.utils.logging_configr   slack_bot.zhihu.zhihu_hunterr   r	   r
   'slack_bot.zhihu.zhihu_playwright_enginer   r   r9   r,   r.   r0   r2   r4   r7   r   r   r   r(   r&   <module>r     s)                               4 4 4 4 4 4 P P P P P P P P P P I I I I I I	h		 '"
# b	K b	K b	K b	K b	K b	K b	K b	K b	K b	Kr(   