@@ -29,6 +29,61 @@ function logTutorialPromptFlow(step, details = {}) {
2929 console . log ( TUTORIAL_PROMPT_FLOW_PREFIX + ' ' + step , details ) ;
3030}
3131
32+ async function getTutorialMutationHeaders ( ) {
33+ const headers = { 'Content-Type' : 'application/json' } ;
34+ const helper = window . nekoLocalMutationSecurity ;
35+ if ( helper && typeof helper . getMutationHeaders === 'function' ) {
36+ try {
37+ return Object . assign ( headers , await helper . getMutationHeaders ( ) ) ;
38+ } catch ( error ) {
39+ console . warn ( '[Tutorial] 获取本地写入安全头失败,尝试直接读取页面配置:' , error ) ;
40+ }
41+ }
42+
43+ try {
44+ const response = await fetch ( '/api/config/page_config' , { cache : 'no-store' } ) ;
45+ if ( ! response . ok ) {
46+ return headers ;
47+ }
48+ const data = await response . json ( ) ;
49+ if ( data && typeof data . autostart_csrf_token === 'string' && data . autostart_csrf_token ) {
50+ headers [ 'X-CSRF-Token' ] = data . autostart_csrf_token ;
51+ }
52+ } catch ( error ) {
53+ console . warn ( '[Tutorial] 读取页面配置失败,继续使用基础请求头:' , error ) ;
54+ }
55+ return headers ;
56+ }
57+
58+ async function postTutorialPromptReset ( reason ) {
59+ const body = JSON . stringify ( { reason } ) ;
60+ const sendResetRequest = async ( ) => fetch ( '/api/tutorial-prompt/reset' , {
61+ method : 'POST' ,
62+ headers : await getTutorialMutationHeaders ( ) ,
63+ body,
64+ } ) ;
65+
66+ let response = await sendResetRequest ( ) ;
67+ if ( response . status === 403 && window . nekoLocalMutationSecurity &&
68+ typeof window . nekoLocalMutationSecurity . refreshToken === 'function' ) {
69+ let shouldRetry = false ;
70+ try {
71+ const payload = await response . clone ( ) . json ( ) ;
72+ shouldRetry = payload && payload . error_code === 'csrf_validation_failed' ;
73+ } catch ( _ ) {
74+ shouldRetry = false ;
75+ }
76+ if ( shouldRetry ) {
77+ await window . nekoLocalMutationSecurity . refreshToken ( ) ;
78+ response = await sendResetRequest ( ) ;
79+ }
80+ }
81+ if ( ! response . ok ) {
82+ throw new Error ( `tutorial prompt reset failed: ${ response . status } ` ) ;
83+ }
84+ return response . json ( ) ;
85+ }
86+
3287window . getTutorialStorageKeyForPage = getTutorialStorageKeyForPage ;
3388window . getTutorialManualIntentKeyForPage = getTutorialManualIntentKeyForPage ;
3489window . logTutorialPromptFlow = logTutorialPromptFlow ;
@@ -4735,6 +4790,24 @@ class UniversalTutorialManager {
47354790
47364791 this . _teardownTutorialUI ( ) ;
47374792
4793+ if ( endMeta . reason === 'destroy' ) {
4794+ window . dispatchEvent ( new CustomEvent ( 'neko:tutorial-ended-without-completion' , {
4795+ detail : {
4796+ page : this . currentPage ,
4797+ source : completedSource ,
4798+ reason : endMeta . rawReason
4799+ }
4800+ } ) ) ;
4801+ this . logPromptFlow ( 'tutorial-ended-without-completion' , {
4802+ page : this . currentPage ,
4803+ source : completedSource ,
4804+ reason : endMeta . reason ,
4805+ rawReason : endMeta . rawReason
4806+ } ) ;
4807+ console . log ( '[Tutorial] 引导未完成即结束,页面:' , this . currentPage , 'reason:' , endMeta . rawReason ) ;
4808+ return ;
4809+ }
4810+
47384811 // 标记用户已看过该页面的引导
47394812 const storageKey = this . getStorageKey ( ) ;
47404813 localStorage . setItem ( storageKey , 'true' ) ;
@@ -4744,6 +4817,24 @@ class UniversalTutorialManager {
47444817 console . log ( '[Tutorial] 已标记模型管理通用步骤为已看过' ) ;
47454818 }
47464819
4820+ if ( endMeta . reason === 'skip' ) {
4821+ window . dispatchEvent ( new CustomEvent ( 'neko:tutorial-skipped' , {
4822+ detail : {
4823+ page : this . currentPage ,
4824+ source : completedSource ,
4825+ reason : endMeta . rawReason
4826+ }
4827+ } ) ) ;
4828+ this . logPromptFlow ( 'tutorial-skipped' , {
4829+ page : this . currentPage ,
4830+ source : completedSource ,
4831+ reason : endMeta . reason ,
4832+ rawReason : endMeta . rawReason
4833+ } ) ;
4834+ console . log ( '[Tutorial] 引导已跳过并标记看过,页面:' , this . currentPage ) ;
4835+ return ;
4836+ }
4837+
47474838 window . dispatchEvent ( new CustomEvent ( 'neko:tutorial-completed' , {
47484839 detail : {
47494840 page : this . currentPage ,
@@ -5145,7 +5236,12 @@ class UniversalTutorialManager {
51455236 /**
51465237 * 重置所有页面的引导状态
51475238 */
5148- resetAllTutorials ( ) {
5239+ async resetHomeTutorialPromptState ( reason = 'manual_home_tutorial_reset' ) {
5240+ return postTutorialPromptReset ( reason ) ;
5241+ }
5242+
5243+ async resetAllTutorials ( ) {
5244+ await this . resetHomeTutorialPromptState ( 'manual_all_tutorial_reset' ) ;
51495245 TUTORIAL_PAGES . forEach ( page => {
51505246 this . getStorageKeysForPage ( page ) . forEach ( key => localStorage . removeItem ( key ) ) ;
51515247 } ) ;
@@ -5157,12 +5253,16 @@ class UniversalTutorialManager {
51575253 /**
51585254 * 重置指定页面的引导状态
51595255 */
5160- resetPageTutorial ( pageKey ) {
5256+ async resetPageTutorial ( pageKey ) {
51615257 if ( pageKey === 'all' ) {
5162- this . resetAllTutorials ( ) ;
5258+ await this . resetAllTutorials ( ) ;
51635259 return ;
51645260 }
51655261
5262+ if ( pageKey === 'home' ) {
5263+ await this . resetHomeTutorialPromptState ( 'manual_home_tutorial_reset' ) ;
5264+ }
5265+
51665266 this . getStorageKeysForPage ( pageKey ) . forEach ( ( storageKey ) => {
51675267 const oldVal = localStorage . getItem ( storageKey ) ;
51685268 localStorage . removeItem ( storageKey ) ;
@@ -5295,11 +5395,12 @@ async function initUniversalTutorialManager() {
52955395 * 全局函数:重置所有引导
52965396 * 供 HTML 按钮调用
52975397 */
5298- function resetAllTutorials ( ) {
5398+ async function resetAllTutorials ( ) {
52995399 if ( window . universalTutorialManager ) {
5300- window . universalTutorialManager . resetAllTutorials ( ) ;
5400+ await window . universalTutorialManager . resetAllTutorials ( ) ;
53015401 } else {
53025402 // 如果管理器未初始化,直接清除 localStorage
5403+ await postTutorialPromptReset ( 'manual_all_tutorial_reset' ) ;
53035404 TUTORIAL_PAGES . forEach ( page => { localStorage . removeItem ( getTutorialStorageKeyForPage ( page ) ) ; } ) ;
53045405 localStorage . setItem ( getTutorialManualIntentKeyForPage ( 'home' ) , 'true' ) ;
53055406 }
@@ -5310,12 +5411,12 @@ function resetAllTutorials() {
53105411 * 全局函数:重置指定页面的引导
53115412 * 供下拉菜单调用
53125413 */
5313- function resetTutorialForPage ( pageKey ) {
5414+ async function resetTutorialForPage ( pageKey ) {
53145415 if ( ! pageKey ) return ;
53155416 console . log ( '%c[Tutorial] resetTutorialForPage 被调用, pageKey:' , 'color: red; font-weight: bold' , pageKey ) ;
53165417
53175418 if ( pageKey === 'all' ) {
5318- resetAllTutorials ( ) ;
5419+ await resetAllTutorials ( ) ;
53195420 return ;
53205421 }
53215422
@@ -5351,8 +5452,11 @@ function resetTutorialForPage(pageKey) {
53515452 }
53525453
53535454 if ( window . universalTutorialManager ) {
5354- window . universalTutorialManager . resetPageTutorial ( pageKey ) ;
5455+ await window . universalTutorialManager . resetPageTutorial ( pageKey ) ;
53555456 } else {
5457+ if ( pageKey === 'home' ) {
5458+ await postTutorialPromptReset ( 'manual_home_tutorial_reset' ) ;
5459+ }
53565460 if ( pageKey === 'model_manager' ) {
53575461 localStorage . removeItem ( getTutorialStorageKeyForPage ( 'model_manager' ) ) ;
53585462 localStorage . removeItem ( getTutorialStorageKeyForPage ( 'model_manager_live2d' ) ) ;
0 commit comments