From 1d8cbb94f9b25e0af894bf2fc573095e67c8c4f4 Mon Sep 17 00:00:00 2001 From: wangdl Date: Wed, 1 Jul 2026 22:45:54 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E9=98=85=E8=AF=BB=E4=BA=8B=E4=BB=B6?= =?UTF-8?q?=E5=8F=91=E9=80=81=20+=20=E9=A6=96=E9=A1=B5=E7=BB=A7=E7=BB=AD?= =?UTF-8?q?=E5=AD=A6=E4=B9=A0=20+=20AI=20Markdown=20=E6=B8=B2=E6=9F=93=20+?= =?UTF-8?q?=20=E6=96=87=E6=A1=A3=E6=B8=85=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MaterialReaderView: 修复 onDisappear 顺序(先 close 再 export 再 cleanup) - MaterialReaderView: 新增每 2 分钟定时发送,离开页面立即发送 - ReadingRuntimeSessionManager: closeMaterial() 不再 unregister - ReadingRuntimeSessionManager: 新增 cleanup() 独立方法 - StudyHomeViewModel: 移除未完成会话回退路径(缺失 materialId) - AIChatPage: 支持 Markdown 渲染(LocalizedStringKey) - 删除 ios/docs/system-map/review/ 和 ios/review/ 审查文档 Co-Authored-By: Claude --- .../UserInterfaceState.xcuserstate | Bin 112930 -> 122306 bytes .../xcschemes/AIStudyApp.xcscheme | 16 +- .../AIStudyApp/Core/Models/APIModels.swift | 9 + .../AIStudyApp/Core/Navigation/Route.swift | 3 + .../AIStudyApp/Core/Services/APIService.swift | 12 +- .../Core/Services/FileUploadService.swift | 4 +- .../AIStudyApp/Features/AI/AIChatPage.swift | 3 +- .../Library/KnowledgeBaseManageView.swift | 118 ++++ .../Features/Library/LibrarySubpages.swift | 35 +- .../MaterialReader/MaterialReaderView.swift | 33 +- .../ReadingRuntimeSessionManager.swift | 15 +- .../Features/Membership/MembershipView.swift | 65 ++- .../Features/Study/StudyHomeViewModel.swift | 9 - docs/system-map/ios/review/00-审查总报告.md | 242 -------- docs/system-map/ios/review/01-独立页面基线.md | 314 ---------- docs/system-map/ios/review/02-独立功能基线.md | 537 ------------------ docs/system-map/ios/review/03-页面差异清单.md | 189 ------ docs/system-map/ios/review/04-功能差异清单.md | 140 ----- .../ios/review/05-业务能力归类审查.md | 272 --------- .../ios/review/06-页面能力覆盖矩阵.md | 104 ---- .../ios/review/07-待修正文档清单.md | 150 ----- .../review/07-内容与对话能力审查报告.md | 475 ---------------- 22 files changed, 240 insertions(+), 2505 deletions(-) create mode 100644 AIStudyApp/AIStudyApp/Features/Library/KnowledgeBaseManageView.swift delete mode 100644 docs/system-map/ios/review/00-审查总报告.md delete mode 100644 docs/system-map/ios/review/01-独立页面基线.md delete mode 100644 docs/system-map/ios/review/02-独立功能基线.md delete mode 100644 docs/system-map/ios/review/03-页面差异清单.md delete mode 100644 docs/system-map/ios/review/04-功能差异清单.md delete mode 100644 docs/system-map/ios/review/05-业务能力归类审查.md delete mode 100644 docs/system-map/ios/review/06-页面能力覆盖矩阵.md delete mode 100644 docs/system-map/ios/review/07-待修正文档清单.md delete mode 100644 docs/system-map/review/07-内容与对话能力审查报告.md diff --git a/AIStudyApp/AIStudyApp.xcodeproj/project.xcworkspace/xcuserdata/Admin1.xcuserdatad/UserInterfaceState.xcuserstate b/AIStudyApp/AIStudyApp.xcodeproj/project.xcworkspace/xcuserdata/Admin1.xcuserdatad/UserInterfaceState.xcuserstate index 98c9ed498744e6aa7c16dbd67af7a7184ece6517..ec57579d6677eb787da26276a215f614a2d4c4fa 100644 GIT binary patch literal 122306 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೬rKk>UQc~xZQ}#BDR*|* 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%0D3R~2EGG)XoVT9&=&^5V6a0B zpvx5-0o$%P1$f?yGXZ_9Kp!j6$BIh;9jv$qo`PqAJg?Xd_}vPgz2Ys{178ErUGXFQ z0>8swic(<$q87hW=1Dt?OD@MauNQ4v^2iUVB9|~Y56haBihefaikiFt!xDu{| z8{rnX6Yhrl-~o6JkhuaoReT7az~}H4AZrD(Rv>EyvQ{e42hdL?a#bQ%CGu3}LMbeO zFsug3eq}v0!+JOjjsSF7c{ZE}7XmU??tm-cYM{JS-Uj$n<*V=pAX_E2tNZ{yhR@(j zz?PNY!%vD5?gs+_8;0%R1UL8qJ%;081R!_#AV`A@7!MO*GR%fKPy+K|5iEgaum;Fa zxDgJ8!(kH$I0m)?GKZgsmjKzr$R6GU?*qCGBYRlN`m3U>#NI0hKr~Q(SH=Lcu0+_xD;ApC!o)j=yN6dTzL;X0dE6(T!|i6 zBFjqjxDq|CM2{=c<4R;(DP{XZQL4~m6?&{fk5%Zg3fZcVtqR$ykgW1t{IUue ztI%WBM92pGp=t&oZxwp1LXTC*U9}umKpix}dN>S@f}`O$I03GL$6z<0!z$#iLhdT$ zuKFDC&8lzV2SDa3%JQoI5P+d@5M%-}tiq-RRJqu4b%emT-5;BaMkH>7NE;j z7XZ3kwH3C(4!8v#gqPtpcoW`%_W;|jqTHOO#6)q{X?UG0Ea@PHpe zFbqZl`mCmWSC0c^uSTEMc`y?SVJ?)y0zjYDRZs)yvYI?pqswY^S&d&*qtEJ#;eL1; zUI27i{R$v=HF8&@&+7N#JNR2sR;z%#tC4p#@~(~n7kD86$hvwAWWaQo1@i!%u09kt z!trnlpv%>p;aoT$@Xghiz%_6uJPXLQ8ktrj)9TmZE!YFdwE83X6uwZDH6}2F75c&e zpveYSFst#sMy7sAkGtIl*qpRMF>wVoK{ZY6gsxm(HI z%3Ze7V=H&r>Jl=y(q}7uwtk!^u;lSoEm#uvdt$h!z z$1#a1$kKW?b6LP5*0BQ}w$@>59k$kCYaO@rtLN9M2xLC_{A#VJK)o19ex@~T=tW;f zq02TCaFcCx*=8pCY@^RMk!;}*M>vLTZBCMed~MER*KKs$<{p1z*KM%X z2>rG#k51dl-nJSo>CABCYCDGUOk^_Ckgct3ZRfF&O~}_)cWrmFn>hAy5SiP`-1Y?e zY~+g`8j+hf1& zyU`OnZvQ8niAARNGPRefy-e+O*Zu-2T)~bz=%zzMex?I`u%iwWn86bKUWcv3V>cZV zNF<45?4yHybhypG*hj|@vS9}uz0t84B`8Y;?4e^-UZ6g25?F;}?9*cl>~x z?%0vebfpKqu)~g%n9NkBV|N|@WDXHTvW6%&@)ujN%Z_%~QBNK9)KO0z&DAlPb3xFl z2>R}{JP106;I2BCrX236v;B3hPA%-QvpsgU$Id$K+=#~5VP`w+Y=@mc=PSPDdoBmT zuOBdrj3DSz7hQDmw_W^gmnM9|XXvAgzwOc%`|dJ=(TwGHbkJoA=I`S7y2#vRC97G- z1~y~vF462{9|tjim!q5^75CZYIybq^ognCHpIvj2m;4l>2%*?**Vp)xpRud1cGdM) zy3v;b3}zUzca^#8pR8gnTkvgkm7%NdyQbsqu6KD51b!yymWAv*LSf2d2i>Y;2i>0L zSzh2p-r-Yp*Uc?;`<528q!nTGVI)(TflS@zvVg@bBZ9x!#twE7OFRcSOaiAl%LOiB zhHm%xn}#>F?HloWOy6mC59=hvsl;fP@4EEnc#vUo$AcH%| z+*5}=^N^3nC`1v8p}(Hhsl}5#M_pd#Rb=nk8~ydvUC;4MWGeFZoW*Qbu!^nduc!Wc z>aXVkj&Xt{l99D%8nX4ui*KV>QS{l%9rk*OhP+P`KISt%=PSOU4Lz{qUUuAT5dK~3 zWyihb>}AKj++;7gdTn7lJK0SfzGMFnv;@75a2~hU%fBtXu9D6zGWjp#gCOYbTkh=! zd*?>x-cL{pH`u!Zm8nJz>hdaY@D{$Q-tX}NA0m72vFNY&9Oko#a3Zk#-tMw@H2UkU zzuqU%UvK^Ow&UJcxW)}+?d>l6zUpqR|mG1PS52Kli?)uqr zzj-WX84;|+e*4*PzcZXerhciUah;po4ubyKaC`l8^C$%(X{?%}U{msz- zRo>t&*$bjsE(}+u!c{AK^Tgu=nb`3FJ09>b2nJ>$J2`QC1Kr-hl9Zu5m8go018d<12fBlS?qHzI z0~_)oZg5~TKIbdGL4O0=(}^zhq!0ZWi0lKS(ci!X5;;jS=dk;MDclKyLHZk{zd`yN zq`yJ>8)V0WN>P>y$U3MtFY^H(@dfU3kRAv1Mt_5JH)uQ)naXr#F`E_G@gUs|+Q}~V zvL8DhWXFT-cTh$U49-dpa*>z(JkApor7~`Ba82s)G|y2N83(^g1Ki-?_mO$9-4Fhb z|M3&Q5Jp?{H@F`I8Nx_@!yOF%owXcBe}nZl_zKs!!CfBkZx9R#LG~ds4=F|sYV!j1 zkzt75hUjgGw}-T&BVF-559x(H4jIi@W-y2OEFzo;B3Vr|d(q(#9S+gqkR&c~nX9C8 zi`&RJ^bzcMs4j;VrYIG$L!Oapds(j2y+Z=j+-3%B|p;}9S+su z&>;+G6k`~Vn;1HoX}E=6UROdaGX<|;Vk+ent}TrdY^xS zU|4`}ZkTUwm|GcEgiuOS4l@p`LN&}Y%>IXU!S09Yd{}?v9wzrNxrf>RF#Qkv6TJ`9 z_po(rU^6?|MJ(~y!LURwa*w}*V7M&9Wf|`6;octpB+u{$Z==uQ@9`l`X@*{hx1=)( z9ODG$A8!8P=eWQnQc2@FH_*xOOuRq*zxVBXgkDCt$q_loNgnd?7zHUzF-lMxU5$`; zL}hd}!VQn8jqD?y=2_guh!=T<*Qv)_m}x{q-sOGtHev;vpFyBb?jeH67jWo;1_h`(A$UO28a*jO8@gNvw=24$uf1~Vg z)R*kR|9n&;W*KFc(O=M#UuaER{QYRNjke3tSGX1gzkNp=+R=f|nESW;nDsY%9TUQ@ z3}6sL7|u!j))>Dv#@~$5)z~I{#W(ovvH!yk#_mP$V-IjR2*#OroO#EYcbs|0nRi?i zW*)Z-GmY~%4?9Xa1lG6;CCk6!R!-tGO-7KXQFR*V*enRl$8QJjy+E*f_EqRjY(bb_9Sml z($A!6%w#t6SjZB>S-~pSvK~1n9pnh^agrTQl6jIjC%MN-b~foUI-Ybr2>!@Rbw0ox ze@tKlr?IceC3%rId7F1=gf1t4#kc5l@{hGj# z>uqu>Y1|-#OzsB3lt;*oj;55NEET9sEuQ2Vo~JJMGR5ytS4wp2hD?HT%?;G4s^V@SRNkoG&r&)ZUnJsu`!6aq4hJF^2I>WHNR* z)x1+zv6l60!aYyj&Q5j{$36~loKv{Bsb{%@UZ(z+|33c&fu9YgWg#0mD2REc6-QUo z%2SD|RHrem=!#pM)*s*Dw4vC|G`pGR2B!VN6y^|tjMHSCCfl@=T<0bagJ8P1r`yqV zoleisV-!NS)Ac&N3U)O8X@jAktMIeii`O`pyzWSc%8 z_d4C4r*9>i81}H2{Tw2JM0^j^&vS{(xVP!I`8Nn=gkY8#W|?7@8F?v&9%q!L0(zWL zgW8yBhCXMsr#Ax_%rNAh;Ra@mN1rq1vH<&^A@2-%XUIDvnpomFz+q&a;rp8*+YJ5A zwBMO|u-BQ@sl`j^a^{=7O(Wh%pELD2^E=wmmUeWY5B5ECAVV0=NZjX4hct5re}0uW|^bAIevf6F8tmcoz1b2 zIXatji%cHyPY}!v@Vj$$Hup1Rp8G$3LO*le&D_p(r8@%{!#ozUgm6}{3Ok!?XLIdr zuKDI}=NNL#m1Ax)=W!=N6h6W!03Vg3`8!`t(#;U?xk z#k0J?i+Fqfr+mTJe8-RcOl#WEi{Z>fruj0>mubFC^L062m-BTwUzhWBIbW{%2RVW+ z=bu2A^OMo#d|l4h<$PVv*X4Yf=Re@zAXpGWR`MeMf}#|s6lEz-9n7%63=3Z3b>2jm z3mVdbp-f;3)0u_b3l^}LrEFj`vBhlrJ_?)l!p8xR^`drwRLCCaFriC&soWpz;A=5(N!@`xUW*vLkj~y>eAQAtDEj*2k z3uRm=<3br1%DC{qjQ8;G%R+rFdV~TLMD9g$FDijP7wL0REuQ8%>hdaY@D}=9^aFz# z&mT->268W&$3pbED2k2jM&3p8E|PaqB4;?qMN*M1zV#n-sOe;My^pMUUOEy+n9@}buy#VAKbs!$!BE~$fFmpn^78t^Xf<4%@*g4ve1 zl_kA!6H68lj_+xS?`g?eWL_fk5}B9CyhOiC4kGuGo80E#AXqBX(o&Sg`%AsO)J~Vy zi%yreqcdIUL0|eah#^d54s)4@oJ-|fD$CM1_94^KBOK!dNhEWg zOSs3S`dgNT?C5cs%**oe7= z@TZU|T&D03`I`20!fwO$89oXb!YA+t)0xR^^co(?7IYY{!*Csj>o8o0;W`Z0VfbZa z3s2`(5G>D$4wvh2xopd2TQ1vj*_M~24CSdr6`tf7p2uA**X42@E^mz7%bU`S=6r!p zm;XXrI-tkp-7(X0GcE7WJaoF;4J`M)FW2Gnleo|2zW3$6_vL9^=LUL>&}W2<5i&-| z7U5PS-sT-X#oG}&jQEZpX-O->=t&=jqRWUWOlKCdL@Z!2{tbxOz%JZr#2(yggnSX# zkR>96JN%dFpCDKfkd++dA}?jBKxL{?lRC(}!cDHw4 z@e5{H;VxI`b44e9WdP<`F@n*IXCjlC%4SZGf*x1gr~YotCS^%*I1WIf(OpON~E z{FqO08G8MNFDSM>sk@tAOzd^7{ z_Eoa3dYsZ!qAE40jjXF=T_x)(Sy$Qps&{FIT&v_-^(%I}N}sDHGn=_Mv{h@6W7S5s zvYnmm;wb02z(wqRm3^=Jn}$-7S8b%iKSDauj- zS=YIhb+WDdfRFeB9j?>cy55XHf9u9Gk*Q2)7Vc%;3N|CtI+@nVwC*IQIg3o|+{d~r zT;oO%MCBkC?jy>5M7fVB_YqYT8KY#3k}*ogC>f(MV& z;O+J9Z~Yg1%lEXvzX9uiWdK9b<@(X+a{UDUUxQwu$9s!KiU(~w3qrU_s1J@RjANf_xCmw5+oZ~l;`G{$}G5d0O8l^oCQB^;QQH{lt-sqW!PGSS~O!Ai*fH;W!L9V?HtWmen+OAcD!>svzWs=?0M&Awh~PYdysLb zj5}rADdSEVcgnc)JeSbt&YL{o-yn#QJ4Wu9N6=?X9*R;Dea4ih3i^!EXN;L*%n&1U zOfUK)dyMQcqwrnD=rcz47+GWV857BRHlfd$?Z_G$~*1>qEXJj5f5TJ-z8eUu4}S->$hVU@^;xK*n8mzAK82{KYno za-36~;T#vy<1Tl(>pJ?}b(=duusa|tImpRlgrd*gWhhT2bh_JIyK7LFS9ycCcn5Rs z{s43BHqY*{%wP^W-Mxr#WZy0O?$t!&4t5{LH?%v6WX_}4-PgE52AMnzf<1XDO*!m% z&kHo7F-`c2?`c6x^t-1m9q7aW#vtDwd*5U4d;Ub1dvv)+mwR-%N0)ncu!~sYIe@Ht zbh$^Ddvv)+mwR-%=Pqt^&%Z$s>lR|O;YMR~@i^=Stg1GF+6ju>@ zjq`q-x8q*nb?Vaq_ZnxfabNH?-=fdB4s@m~z34-KoZ=T@%7MWd_(jZ@9lVR$Nx+l+S7?{^hB5OGR3>m`02H$m&s_V=wa;Ap%(c&4`^>e^T>Iv-m3WSDj1wer0k^R43fD;IUJ&fJ z_x&>NmvO&r`*plur~7rf{}a5u|0~?*{ucbiFSMoyz3Iy+#xa3E(C>cT?w^fZ`y<(c zT>ItPFV}v#_9t_mOI+qEvhBYW1P5}GhkQInA>8GGP)bq;-}`||RN+aU!HpcKi~Bgx zh{k+KQ<~A7FK9^^?dV8X%yhs^2h4O}4l*Cu%VCb<77nkHg9#*ZlGB{!0x8(_LAyS9gNz_Jl#9IhrVc&M6Ucf<)*1QncKCgopx?tk(v6-BV>Dy=oykmP2Kqd_oQ=qISf;}= z9X^g7A3lRjhkfsdQ%U1`5FE+IBjn~$3Q&;3$aqA?BQhS5@raB^WIR%br_tw;S82ez z$bCfaBOjyBBj2OTBW~hI7`i;tnXc&bh&wp4o*3eh`-t2};o5;7mr=aGj& zkf6VWC$Q&)>d27r2EL61Zzp_;?h?M{TYkXzp3n)qOc;nR6GrkIx=fgeJ`?nru$Z;1 zBZ>{|A{IFlLK5B(X$P)0BcvmG7F4CXMOMTE10 zRjlC=zSJ_=G8cW_MhW7Vk1i|G50Bll7Ra$7DTb=f~ta zCf6}pj>&S&+sC|pY!iPG#{mv=l#`t1EV?~*D+m$;^q8o}L|GE`n5f4@JtpcgQMN?c z65VFv(=y3w`JG8jMvsY0 z&|~5XbeI^$MmCeozd>+ZkH_u*xZKC(J}&q1a#W-W)sgx5bG*r?H0OKTBExar9Uq6c zk59ojb9^@QScqFa?(O5=KE9iH4xr29iRkjUE|1G}Je_~|FWbW)I1xfVP)bmq zN>rr=wRnkFc#V3rrW@VqK`(}*s}tjyfXpXmFpD|p?S#xHbarAhf3bya>>~l0Psn^C zi3_A~g=@%vLjIFFJt^}^ou1U`$x^t5lWyUpTR7SwplmZk) z?o)D~DoJT-@e+DHCGRPDPsw}gBR=6XzChMf?)6kh1~Uw|acU+jS&c4F#juCH9KzrK#gH*c#v~b&^qVASQf>5_^c1flTaq4=WJ}Ux z()Z{w=_h{1og}rP9UahX(kRB@PLlMPG?{6bBWX7CScqPe^qS;el60HoR+7w@6pa~A zyUo*ec^O$w*F%4&8_*Qpo&FRVPs?<=C9RO>v>l(;B1oGfp$ zeJATP`3LMgS@z`4bf*_`Cl6vMBhYEG{*srn0pC~hR&wU8v3rG@>!hkm*7f+|>o|U-0&Y-x$XvrZ5eAyReM)*wck= z>|hUZ`1kw50nT7g7t*+no4Rm62rgzPCwa(+PA?XsFnYbH*Nat<@nTJMd(k~z{E()| zdhv6<;#=(V;*b1HYucj2i)Of}!;7OB!+0j*_AZ*^;!I{U7xP?wFN#;wf=!D&0 z`W;Fv$-N**$%1}U9;XPQ*n3JjDo~lK*nLWU z8sY|0+(3$cQ{+tf7WqM;lT&Zp*)t#i8EA=H_r~lOWlwbjsqQ*;AF`*)nyUNMbnbAU ze}dq0fUK8uBkN^ZFWbZA;#5Jd%W_?AKvRC=7rM|By)hnO zyzb(PySU;m(%eOwyGVNkU8m_fEk8xkb(*_KD}y^vdx~dq7is!W(|_9Qyw68`!e_XN zv~O|uX=X?p$~<(P7KPkta;M3irt36ar|sh)GNvY5M0yeHGN*w=d}`)rW_Tif;+fYizj)8=XsHrsZT@P z#5J8>a|_qZa_xIs(2`b!;SR3#p+AEd&M3xUwrk7L=e5(E=Mp--CigYDugQJwP7qwr zf!ni*lR7+&yle}GbzNSU>AFnUWxB4*>$<$I%XD3)+k3iiDqWZ9rO{=2MRb|2%XD3)>oQ%J>Cf>R z^{7t+n$Vmt`G)Ul!B4o2^ltQ|4+9y3dD2Is!}K^3IfLBka;M9ko{sN7J(GLLe8b*v zJVpg7qq`fgAj6H1@lD+D_6>WyVUIW3(SgqB@y1|&V=Ob6!+aJIP6UyxCYl2r;{-`0 zlfo6QaRV7|{LRB4xS0hRZ$3d$ic^ZRRO5MGMu#_bcvF8j-{pNi;4{A9Ys_*}?wh*0 zIf!v;5l<(#`Hn16czbWs{81`_Qvs^&loATb2_olozGq}TjeE&D)zLk?aGD=VtZ)ZG-8_cMS4l{I^@dn<`@OFm2GVDI1C9P;jM}9@740o6DJG#v99c4^s z7IJ1ppvw$-GIW@+n^=xWHqeP-x1Lzfx4%(#X7%+TR&-Q9kIa>#tU2DNz#nQzN{ zTjtyKcneu?%W!)qhe#lilbpt_-A>~+{{%s%PBL@xIJ(JnSDE&bshdpQWa=jKdAybB z_cQG!vl*Xb=1j9@+CgR%y3gE)`7+IynS}Q;&tngn_K<0|OtWR)<3SMId4bOOjXVCc zcP`^UbH{(?j`{Dpp}Tn~L`lk^hr8y#`z+?ZE6-hd?taC${J@X=Ol#WG0q@*(_jgAz zhVe{fGSisJZ050$#jGX{`?>pX5Zrr=D!hX4?B17jVlaP@h@S7==I@1McV+`mXF_Idwa5U4YFkd++d zq5uUcOfiaMhYxh|U;*?!^|!q8PVU1j?fU1if%HeF@wkFK)mD%)_Dp@(b_gOKb6Fk+j=g5&`H0I4=H#w#=ll8=~ zhrR6Q5W2{b&b=Vyk-`*1|2b2+8HD6=E4l0;m%ZhRU=uspO&t3;$O)22<~*118@bGq z%WvoU8*}H*LN*?y0D8#13psN8``rFMx4+Nr?{i<~7JehQ-^lGZa{G-uA>`vR3Q>em z)<(uJHi<<^KmA7Vu^P zZx--o0dE%YW&s@*sEqy!c(;HK3%p4an)4;!V2%PUXoDUKbfOF0>B%rg@*Crrz@#9g zU@qKP!D5u4H05y51$9@j4o?Rmg}h(LoQ3SBklhrrn?iO|NR~qH@c|zNA%)Ff*!NX< zC?gonSjMxQjp(_sn=kC<3-9L;2_)mr3cIty?yT@t(zzLg6p^op97XJ`NK?#NqzAp} z$3TW)&LUxl6<#TM55i!f%&2 zjgCvmSVG1USAvj|_E^$ymaNA+yoX(ubgLylp$Ad;yOJ`MG)t-cJdVFDRg~hC!mdhH zpfc5{iQg?%pN2G|F?LYOjh1SLU6itmQg%`5dvsrFC0mK(W)M<3H)ZfGmG(EKyE6ng zP}FSD9;Y(SS~ zl<`VW*MmJ?2#XDu6hPy;0500W8^76 z06mspiyq5wVhejY&MD5|?eg9(@9pww+{1TO{$UVOAq&~bNgi}jp(b^Ci;vJng{}-o z-U^eM#T@3d2-zz{vW6(kQDHwRc&DP@ujuzH=EGbSbzRXtRqTwLs5lRQQ*j?Bkg=kS z75!}`87s+H$!}Eh8lwV&-j9``HmlGM@Q_T zQa1)O3^P{xjcM3#rP;_=$$l%XW*xFt+C?n!93Y8g+*c)iSCYT7e3k3c81q*4{Z#fX zRQ??IT=`pMsjTD5Khv6i3`8%LbzXTCV;IjwCL>#A*((3Z4!l_JWkg7Sz&*MBnQHoQNYUs79UaLNf@1p7}yiPq@&>uZjm8q&sRb{FwQ`Pyn ziKhHi&UdVhYol&0H4XcdO~YnwhKZVK4hRf`31% z{rApEu5c>|sUC_>s=vr-D`GhNYpb-h>Ddv(26*L!vIRsRDSsxQO4 z)emw4`>vjh?yFxSo&Vm<e1&TX6TaPH>GoK}hX@M=3-RbYI&o)viEgs!@~L_$F%CqZ#tl z{)t}*qaF5Adobpy{X3JGf_tdF7&ldW6>BkXZ5`FtQEeU7-a{M-oF)x>sckQ{@9}pK zQb%Y0|6mWPlM8oQCm&^~%5yxAoz;1f2E5DrG~r`Dp(A~9vvqV>M~8LBFrJCnZJmhV a|NftBS^nRDEtmKI{n!8he}~jL_ + version = "1.3"> + shouldUseLaunchSchemeArgsEnv = "YES"> + + - - + + [KnowledgeSource] { - return try await client.request("/knowledge-bases/\(kbId)/sources") + func list(kbId: String, sortBy: String? = nil, order: String? = nil) async throws -> [KnowledgeSource] { + var items = [URLQueryItem]() + if let s = sortBy { items.append(URLQueryItem(name: "sortBy", value: s)) } + if let o = order { items.append(URLQueryItem(name: "order", value: o)) } + return try await client.request("/knowledge-bases/\(kbId)/sources", queryItems: items.isEmpty ? nil : items) } func detail(kbId: String, id: String) async throws -> KnowledgeSource { @@ -550,6 +553,11 @@ class KnowledgeSourceService { func parse(kbId: String, sourceId: String) async throws -> ParseResponse { return try await client.request("/knowledge-bases/\(kbId)/sources/\(sourceId)/parse", method: "POST") } + + func generateKnowledge(kbId: String, sourceId: String) async throws -> GenerateKnowledgeResponse { + return try await client.request("/knowledge-bases/\(kbId)/sources/\(sourceId)/generate-knowledge", method: "POST") + } + } // MARK: - Quiz diff --git a/AIStudyApp/AIStudyApp/Core/Services/FileUploadService.swift b/AIStudyApp/AIStudyApp/Core/Services/FileUploadService.swift index 95adacd..8ae0bd9 100644 --- a/AIStudyApp/AIStudyApp/Core/Services/FileUploadService.swift +++ b/AIStudyApp/AIStudyApp/Core/Services/FileUploadService.swift @@ -40,7 +40,7 @@ class FileUploadService { let confirmResp: FileConfirmUploadResponse = try await client.request( "/files/complete", method: "POST", - body: FileConfirmUploadRequest(objectKey: urlResp.objectKey, checksum: nil) + body: FileConfirmUploadRequest(uploadId: urlResp.uploadId, objectKey: urlResp.objectKey, checksum: nil) ) return (fileId: confirmResp.id, objectKey: urlResp.objectKey) @@ -68,7 +68,7 @@ class FileUploadService { let confirmResp: FileConfirmUploadResponse = try await client.request( "/files/complete", method: "POST", - body: FileConfirmUploadRequest(objectKey: urlResp.objectKey, checksum: nil) + body: FileConfirmUploadRequest(uploadId: urlResp.uploadId, objectKey: urlResp.objectKey, checksum: nil) ) return confirmResp.id diff --git a/AIStudyApp/AIStudyApp/Features/AI/AIChatPage.swift b/AIStudyApp/AIStudyApp/Features/AI/AIChatPage.swift index 29bc172..bd07890 100644 --- a/AIStudyApp/AIStudyApp/Features/AI/AIChatPage.swift +++ b/AIStudyApp/AIStudyApp/Features/AI/AIChatPage.swift @@ -220,7 +220,8 @@ struct MessageCard: View { // Content if hasContent { - Text(message.content).zxFontScaled(size: 14) + Text(LocalizedStringKey(message.content)) + .zxFontScaled(size: 14) .foregroundColor(isAI ? Color.zxF007 : .white) .padding(12) } diff --git a/AIStudyApp/AIStudyApp/Features/Library/KnowledgeBaseManageView.swift b/AIStudyApp/AIStudyApp/Features/Library/KnowledgeBaseManageView.swift new file mode 100644 index 0000000..a6e4928 --- /dev/null +++ b/AIStudyApp/AIStudyApp/Features/Library/KnowledgeBaseManageView.swift @@ -0,0 +1,118 @@ +import SwiftUI + +// MARK: - KnowledgeBaseManageView + +struct KnowledgeBaseManageView: View { + let knowledgeBaseId: String + @State private var title: String = "" + @State private var description: String = "" + @State private var isLoading = true + @State private var isSaving = false + @State private var showDeleteConfirm = false + @State private var errorMessage: String? + @Environment(\.dismiss) private var dismiss + + var body: some View { + NavigationStack { + ZStack { + Color.zxBg0.ignoresSafeArea() + + if isLoading { + ProgressView().tint(Color.zxPurple) + } else { + Form { + Section("基本信息") { + TextField("名称", text: $title) + .font(.system(size: 16)) + TextField("描述(可选)", text: $description, axis: .vertical) + .font(.system(size: 14)) + .lineLimit(3...6) + } + + Section { + Button(role: .destructive) { + showDeleteConfirm = true + } label: { + HStack { + Spacer() + Label("删除知识库", systemImage: "trash") + Spacer() + } + } + } + + if let error = errorMessage { + Section { + Text(error) + .font(.system(size: 13)) + .foregroundColor(.red) + } + } + } + .scrollContentBackground(.hidden) + } + } + .navigationTitle("知识库管理") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("取消") { dismiss() } + } + ToolbarItem(placement: .confirmationAction) { + Button("保存") { + Task { await save() } + } + .fontWeight(.semibold) + .disabled(isSaving || title.trimmingCharacters(in: .whitespaces).isEmpty) + } + } + .alert("删除知识库", isPresented: $showDeleteConfirm) { + Button("取消", role: .cancel) {} + Button("删除", role: .destructive) { + Task { await deleteKB() } + } + } message: { + Text("此操作不可撤销,知识库内所有资料将被永久删除。") + } + } + .task { await load() } + } + + private func load() async { + do { + let kb = try await KnowledgeBaseService.shared.detail(id: knowledgeBaseId) + title = kb.title + description = kb.description ?? "" + } catch { + errorMessage = "加载失败:\(error.localizedDescription)" + } + isLoading = false + } + + private func save() async { + isSaving = true + errorMessage = nil + do { + _ = try await KnowledgeBaseService.shared.update( + id: knowledgeBaseId, + title: title.trimmingCharacters(in: .whitespaces), + description: description.trimmingCharacters(in: .whitespaces).isEmpty + ? nil + : description.trimmingCharacters(in: .whitespaces) + ) + dismiss() + } catch { + errorMessage = "保存失败:\(error.localizedDescription)" + } + isSaving = false + } + + private func deleteKB() async { + do { + try await KnowledgeBaseService.shared.delete(id: knowledgeBaseId) + dismiss() + } catch { + errorMessage = "删除失败:\(error.localizedDescription)" + } + } +} diff --git a/AIStudyApp/AIStudyApp/Features/Library/LibrarySubpages.swift b/AIStudyApp/AIStudyApp/Features/Library/LibrarySubpages.swift index ff9cca2..89eeb49 100644 --- a/AIStudyApp/AIStudyApp/Features/Library/LibrarySubpages.swift +++ b/AIStudyApp/AIStudyApp/Features/Library/LibrarySubpages.swift @@ -136,6 +136,7 @@ struct LibraryDetailPage: View { let knowledgeBaseId: String @Environment(\.dismiss) private var dismiss @StateObject private var viewModel = LibraryDetailViewModel() + @State private var showManageView = false @State private var showDeleteConfirm = false @State private var isDeleting = false @State private var isSelectMode = false @@ -342,13 +343,16 @@ struct LibraryDetailPage: View { Label("默认排序", systemImage: sortOption == 0 ? "checkmark" : "") } Button { sortOption = 1 } label: { - Label("文件大小", systemImage: sortOption == 1 ? "checkmark" : "") + Label("文件名排序", systemImage: sortOption == 1 ? "checkmark" : "") } Button { sortOption = 2 } label: { - Label("创建日期", systemImage: sortOption == 2 ? "checkmark" : "") + Label("文件大小", systemImage: sortOption == 2 ? "checkmark" : "") } Button { sortOption = 3 } label: { - Label("更新日期", systemImage: sortOption == 3 ? "checkmark" : "") + Label("创建日期", systemImage: sortOption == 3 ? "checkmark" : "") + } + Button { sortOption = 4 } label: { + Label("更新日期", systemImage: sortOption == 4 ? "checkmark" : "") } } label: { Image(systemName: "arrow.up.arrow.down") @@ -375,7 +379,7 @@ struct LibraryDetailPage: View { } Divider() Button { - // TODO: knowledge base management page + showManageView = true } label: { Label("知识库管理", image: "icon-settings") } @@ -400,6 +404,9 @@ struct LibraryDetailPage: View { } message: { Text("删除后将无法恢复,包括其中的所有知识点。确定要删除吗?") } + .sheet(isPresented: $showManageView) { + KnowledgeBaseManageView(knowledgeBaseId: knowledgeBaseId) + } .alert("批量删除", isPresented: $showBatchDeleteConfirm) { Button("取消", role: .cancel) {} Button("删除 \(selectedIds.count) 个", role: .destructive) { @@ -416,9 +423,9 @@ struct LibraryDetailPage: View { .sheet(isPresented: $showFolderManager) { FolderManageView(knowledgeBaseId: knowledgeBaseId) } - .onChange(of: sortOption) { _ in - let (sb, od) = sortParams(sortOption) - Task { await viewModel.loadItems(knowledgeBaseId: knowledgeBaseId, sortBy: sb, order: od) } + .onChange(of: sortOption) { newValue in + let (sb, od) = sortParams(newValue) + Task { await loadSources(sortBy: sb, order: od) } } .safeAreaInset(edge: .bottom) { NavigationLink(value: Route.aiChat(context: ChatEntryContext(scopeType: .knowledgeBase, scopeId: knowledgeBaseId, scopeName: "知识库", parentKnowledgeBaseId: knowledgeBaseId, createdFrom: "knowledge_base_detail"))) { @@ -526,13 +533,15 @@ struct LibraryDetailPage: View { private func sortParams(_ option: Int) -> (String?, String?) { switch option { - case 1: return ("fileSize", "desc") - case 2: return ("createdAt", "desc") - case 3: return ("updatedAt", "desc") - default: return (nil, nil) + case 1: return ("title", "asc") + case 2: return ("sizeBytes", "desc") + case 3: return ("createdAt", "desc") + case 4: return ("updatedAt", "desc") + default: return ("title", "asc") } } + private func fileTypeText(for item: KnowledgeItem) -> String { if let t = item.sourceType?.trimmingCharacters(in: .whitespaces), !t.isEmpty { return t.uppercased() @@ -639,11 +648,11 @@ struct LibraryDetailPage: View { return 0 } - private func loadSources() async { + private func loadSources(sortBy: String? = nil, order: String? = nil) async { isLoadingSources = true print("[LibraryDetail] loadSources START, kbId=\(knowledgeBaseId)") do { - sources = try await KnowledgeSourceService.shared.list(kbId: knowledgeBaseId) + sources = try await KnowledgeSourceService.shared.list(kbId: knowledgeBaseId, sortBy: sortBy, order: order) print("[LibraryDetail] loadSources OK, count=\(sources.count)") for src in sources { print("[LibraryDetail] source: id=\(src.id), title=\(src.title ?? "-"), parseStatus=\(src.parseStatus ?? "-")") diff --git a/AIStudyApp/AIStudyApp/Features/MaterialReader/MaterialReaderView.swift b/AIStudyApp/AIStudyApp/Features/MaterialReader/MaterialReaderView.swift index 1a90013..639f8e0 100644 --- a/AIStudyApp/AIStudyApp/Features/MaterialReader/MaterialReaderView.swift +++ b/AIStudyApp/AIStudyApp/Features/MaterialReader/MaterialReaderView.swift @@ -105,6 +105,7 @@ struct MaterialReaderView: View { @State private var readingStatus: String? @State private var readingProgressSeconds: Int = 0 @State private var apiRestorePosition: ReadingPosition? + @State private var flushTimer: Timer? @Environment(\.scenePhase) private var scenePhase @State private var isParsing = false @@ -275,9 +276,26 @@ struct MaterialReaderView: View { .onAppear { openReadingSession() hasRestoredPosition = false + // 定时发送阅读事件(每 2 分钟),防止长时间阅读不退出导致数据丢失 + flushTimer = Timer.scheduledTimer(withTimeInterval: 120, repeats: true) { _ in + Task { + ReadingEventUploadPipeline.shared.exportAndEnqueue() + await ReadingEventUploadPipeline.shared.flush() + } + } } .onDisappear { + flushTimer?.invalidate() + flushTimer = nil + // 1. 关闭 Rust 会话,push material_closed 事件到缓冲区 closeReadingSession() + // 2. 导出所有事件(含 material_closed)+ 发送到后端 + // 3. 发送完成后再清理 context + Task { + ReadingEventUploadPipeline.shared.exportAndEnqueue() + await ReadingEventUploadPipeline.shared.flush() + sessionManager.cleanup() + } } .onChange(of: scenePhase) { _, newPhase in switch newPhase { @@ -597,17 +615,12 @@ struct MaterialReaderView: View { private func triggerParse(kbId: String, sourceId: String) async { guard !isParsing else { return } isParsing = true - parseMessage = "正在整理知识点..." + parseMessage = "AI 正在整理知识点..." do { - let result = try await KnowledgeSourceService.shared.parse(kbId: kbId, sourceId: sourceId) - parseMessage = "知识点整理任务已提交" - for _ in 0..<60 { - try await Task.sleep(nanoseconds: 2_000_000_000) - let status = try await DocumentImportService.shared.getStatus(id: result.jobId ?? result.id ?? "") - if status.status == "completed" { parseMessage = "知识点整理完成"; break } - else if status.status == "failed" { parseMessage = "整理失败"; break } - else { parseMessage = "整理中... \(status.progress ?? 0)%" } - } + let result = try await KnowledgeSourceService.shared.generateKnowledge(kbId: kbId, sourceId: sourceId) + parseMessage = result.count > 0 + ? "整理完成,生成了 \(result.count) 个知识点" + : "未识别到可提取的知识点" } catch { parseMessage = "整理失败: \(error.localizedDescription)" } diff --git a/AIStudyApp/AIStudyApp/Features/MaterialReader/ReadingRuntimeSessionManager.swift b/AIStudyApp/AIStudyApp/Features/MaterialReader/ReadingRuntimeSessionManager.swift index e8ce9d1..d18fefd 100644 --- a/AIStudyApp/AIStudyApp/Features/MaterialReader/ReadingRuntimeSessionManager.swift +++ b/AIStudyApp/AIStudyApp/Features/MaterialReader/ReadingRuntimeSessionManager.swift @@ -97,11 +97,10 @@ final class ReadingRuntimeSessionManager { } } - /// Close the current session. Can be called from any non-idle state. - /// Close the current session. Idempotent — safe to call from any state. + /// Push close event and close the Rust session. Does NOT unregister context. + /// Call `cleanup()` after events have been exported & flushed. func closeMaterial() { guard state == .active || state == .paused, let sessionId = activeSessionId, let ctx = activeContext else { - // Already closed/failed/idle: reset state to idle if state == .closed || state == .idle { return } state = .idle return @@ -117,12 +116,18 @@ final class ReadingRuntimeSessionManager { _ = try? adapter.closeSession(sessionId) activeSessionId = nil - if let ctx = activeContext { ReadingContextRegistry.shared.unregister(materialId: ctx.materialId) } - activeContext = nil lastPosition = nil state = .closed } + /// Unregister context and clean up state. Call AFTER events have been exported. + func cleanup() { + if let ctx = activeContext { + ReadingContextRegistry.shared.unregister(materialId: ctx.materialId) + } + activeContext = nil + } + func pause() { guard state == .active, let sessionId = activeSessionId else { return } stopHeartbeat() diff --git a/AIStudyApp/AIStudyApp/Features/Membership/MembershipView.swift b/AIStudyApp/AIStudyApp/Features/Membership/MembershipView.swift index 69973d8..c2bcdaf 100644 --- a/AIStudyApp/AIStudyApp/Features/Membership/MembershipView.swift +++ b/AIStudyApp/AIStudyApp/Features/Membership/MembershipView.swift @@ -119,31 +119,32 @@ struct MembershipView: View { .frame(maxWidth: .infinity, alignment: .leading) ForEach(plans.plans) { plan in - Button { - Task { await store.purchase(plan: plan) } - } label: { - HStack { - VStack(alignment: .leading, spacing: 4) { - Text(plan.name).font(.system(size: 15, weight: .semibold)).foregroundColor(Color.zxF0) - Text(plan.planCode.contains("yearly") ? "每年自动续订,更优惠" : "每月自动续订") - .font(.system(size: 11)).foregroundColor(Color.zxF04) - } - Spacer() - if store.purchasingPlanId == plan.planCode { - ProgressView().tint(Color.zxPurple) - } else if let price = store.displayPrice(for: plan.planCode) { - Text(price) - .font(.system(size: 14, weight: .bold)).foregroundColor(Color.zxPurple) - } else { - Text("价格暂不可用") - .font(.system(size: 12)).foregroundColor(Color.zxF04) + if plan.planCode != "free" { + ForEach(["monthly", "yearly"], id: \.self) { period in + Button { + Task { await store.purchase(plan: plan, period: period) } + } label: { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text(plan.name + (period == "yearly" ? " 年付" : " 月付")).font(.system(size: 15, weight: .semibold)).foregroundColor(Color.zxF0) + Text(period == "yearly" ? "每年自动续订,省 41%" : "每月自动续订") + .font(.system(size: 11)).foregroundColor(Color.zxF04) + } + Spacer() + if store.purchasingPlanId == plan.planCode + "_" + period { + ProgressView().tint(Color.zxPurple) + } else { + Text(store.displayPrice(for: plan, period: period)) + .font(.system(size: 14, weight: .bold)).foregroundColor(Color.zxPurple) + } + } + .padding(16).background(Color.zxFill004) + .clipShape(RoundedRectangle(cornerRadius: 14)) + .overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxPurple.opacity(0.15), lineWidth: 1)) } + .disabled(store.purchasingPlanId != nil) } - .padding(16).background(Color.zxFill004) - .clipShape(RoundedRectangle(cornerRadius: 14)) - .overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxPurple.opacity(0.15), lineWidth: 1)) } - .disabled(store.purchasingPlanId != nil) } } } @@ -259,14 +260,17 @@ final class MembershipStore: ObservableObject { do { async let membership = MembershipService.shared.myMembership() async let plans = MembershipService.shared.getPlans() + // Pre-load StoreKit products so displayPrice works on first render + async let _ = StoreKitManager.shared.loadProducts() + state = try await .loaded(membership: membership, plans: plans) } catch { state = .error("加载失败:\(error.localizedDescription)") } } - func purchase(plan: PlanItem) async { - purchasingPlanId = plan.planCode + func purchase(plan: PlanItem, period: String = "monthly") async { + purchasingPlanId = plan.planCode + "_" + period guard let tokenStr = try? await MembershipService.shared.myMembership().appAccountToken, let uuid = UUID(uuidString: tokenStr) else { show("无法获取账号令牌") @@ -276,7 +280,7 @@ final class MembershipStore: ObservableObject { let pm = StoreKitManager.shared await pm.loadProducts() guard let product = pm.products.first(where: { - $0.id == (plan.planCode.contains("yearly") + $0.id == (period == "yearly" ? "cloud.longde.aistudyapp.premium.yearly" : "cloud.longde.aistudyapp.premium.monthly") }) else { @@ -302,12 +306,11 @@ final class MembershipStore: ObservableObject { isRestoring = false } - /// 从 StoreKit Product 获取本地化价格。未加载时返回 nil。 - func displayPrice(for planCode: String) -> String? { - let productId = planCode.contains("yearly") - ? "cloud.longde.aistudyapp.premium.yearly" - : "cloud.longde.aistudyapp.premium.monthly" - return StoreKitManager.shared.products.first(where: { $0.id == productId })?.displayPrice + /// 从 PlanItem 获取格式化价格(分→元),不依赖 StoreKit。 + func displayPrice(for plan: PlanItem, period: String) -> String { + let cents = period == "yearly" ? plan.priceYearly : plan.priceMonthly + let yuan = Double(cents) / 100.0 + return String(format: "¥%.2f/%@", yuan, period == "yearly" ? "年" : "月") } /// N2: 使用系统 API 打开订阅管理 diff --git a/AIStudyApp/AIStudyApp/Features/Study/StudyHomeViewModel.swift b/AIStudyApp/AIStudyApp/Features/Study/StudyHomeViewModel.swift index 5dddfb5..3db2e0b 100644 --- a/AIStudyApp/AIStudyApp/Features/Study/StudyHomeViewModel.swift +++ b/AIStudyApp/AIStudyApp/Features/Study/StudyHomeViewModel.swift @@ -183,15 +183,6 @@ final class StudyHomeViewModel: ObservableObject { return .continueSession(materialId: materialId, filePath: resolved.filePath, materialType: resolved.materialType, kbTitle: kbTitle, elapsed: elapsed, resumePosition: nil) } - if let unfinished = sessions - .filter({ $0.status != nil && $0.status != "completed" }) - .sorted(by: { ($0.startedAt ?? "") > ($1.startedAt ?? "") }) - .first { - let kbTitle = knowledgeBases.first(where: { $0.id == unfinished.knowledgeBaseId })?.title ?? "学习" - let elapsed = formatElapsedSince(unfinished.startedAt) - return .continueSession(materialId: "", filePath: "", materialType: .unknown, kbTitle: kbTitle, elapsed: elapsed, resumePosition: nil) - } - if !dueCards.isEmpty { return .todaysReview(count: dueCards.count, estimatedMinutes: max(1, dueCards.count)) } diff --git a/docs/system-map/ios/review/00-审查总报告.md b/docs/system-map/ios/review/00-审查总报告.md deleted file mode 100644 index 647b9c3..0000000 --- a/docs/system-map/ios/review/00-审查总报告.md +++ /dev/null @@ -1,242 +0,0 @@ -# 00 审查总报告 - -> 审查对象:知习 iOS 项目第一阶段系统地图(4 份文档) -> 审查日期:2026-06-19 -> 审查方法:独立代码扫描 → 独立基线建立 → 与已有文档逐项比对 -> 审查范围:AIStudyApp/AIStudyApp/ 目录下全部 64 个 .swift 文件(不含 Tests 和自动生成代码) - ---- - -## 一、审查范围 - -### 1.1 已检查的文件 - -- `AIStudyAppApp.swift` — App 入口、所有页面定义 -- `ContentView.swift` — Tab 根结构、共享组件 -- `Core/Navigation/Route.swift` — 全部路由定义(28 个 case) -- `Core/Auth/AuthManager.swift` — 会话状态机(8 种状态) -- `Core/Models/APIModels.swift` — 全部 54 个 API 模型 -- `Core/Models/AppContext.swift` — 设备/运行时上下文 -- `Core/Network/APIClient.swift` — HTTP 客户端、Token 刷新 -- `Core/Services/APIService.swift` — 全部 18 个服务类 -- `Core/Services/FileUploadService.swift` — 文件上传 -- `Core/Services/ReadingAPI.swift` — 阅读相关 API -- `Core/Services/NetworkMonitor.swift` — 网络状态监控 -- `Core/DesignSystem/DesignTokens.swift` — 设计系统 -- `Core/DesignSystem/ZXToast.swift` — Toast 系统 -- `Core/Security/KeychainHelper.swift` — (已通过 AuthManager 间接审查) -- `Core/Security/LocalCache.swift` — 本地缓存 -- `Core/Security/FileCache.swift` — 文件缓存 -- `Features/Study/` 全部 7 个文件 -- `Features/Library/` 全部 5 个文件 -- `Features/Profile/` 全部 8 个文件 -- `Features/AI/` 全部 10 个文件 -- `Features/Analysis/` 全部 2 个文件 -- `Features/Quiz/` 1 个文件(QuizViews.swift) -- `Features/MaterialReader/` 全部 12 个文件 - -### 1.2 已检查的入口类型 - -- [x] Tab(4 个:学习/知识库/分析/我的) -- [x] Push(NavigationLink via Route,25 个路由目标) -- [x] Sheet(6 个) -- [x] FullScreenCover(未发现) -- [x] Alert(6 个) -- [x] ConfirmationDialog(1 个) -- [x] ContextMenu(1 处) -- [x] Menu(1 处) -- [x] Deep Link(未配置) -- [x] Universal Link(未配置) -- [x] URL Scheme(未配置) -- [x] 通知点击路由(未实现) -- [x] Debug 页面(仅 Route.destination 的 DEBUG 覆盖层) -- [x] 条件渲染页面(基于 session/onboarding/数据状态的 7 种分支) -- [x] iPhone/iPad 差异(仅在 AppContext 中标记 deviceType) -- [x] Feature Flag(未发现) - -### 1.3 未检查的项 - -- .xcodeproj 详细配置(Capabilities、Build Settings) -- .entitlements 文件全文(推送通知、iCloud 等 Capability) -- 第三方依赖(Rive 动画 SDK 等) -- Rust FFI 二进制内部逻辑(ZxDocumentRuntime.xcframework) - ---- - -## 二、独立盘点统计 - -| 统计项 | 数量 | -|--------|------| -| 一级模块(Tab) | 4 | -| 独立 Push 页面(可路由) | 32 | -| Tab 内嵌页面(不可路由) | 6(启动/登录流程页面) | -| Sheet / 弹窗 | 10 | -| 确认弹窗(Alert/ConfirmationDialog) | 8 | -| 代码存在但无导航入口的页面 | 2(AIHomeView, RecallTestPage) | -| 页面功能点 | ~180 | -| API 服务类 | 18 | -| 初步业务能力 | 20 | - ---- - -## 三、审查结论 - -**审查结论:有条件通过** - -### 不满足"通过"的哪些门禁条件? - -| 门禁条件 | 满足? | 说明 | -|---------|--------|------| -| 所有导航入口已检查 | 是 | Tab/Push/Sheet/Alert/条件渲染均检查 | -| 所有用户可访问页面已进入基线 | 是 | 32 个路由页面 + 6 个启动/登录页均已盘点 | -| Sheet/弹窗/隐藏入口已检查 | 是 | 10 个 Sheet + 8 个 Alert + ContextMenu/Menu 均已盘点 | -| 页面功能均进入功能基线 | 是 | ~180 个功能点均已盘点 | -| 页面与业务能力完成双向覆盖检查 | 是 | 见 06-页面能力覆盖矩阵.md | -| Mock/占位/废弃实现没有被误判为已完成 | **否** | 4 处 P0 误判(见第四节) | -| 所有"暂未确认"均被明确列出 | 是 | 3 项暂未确认 | -| 严重遗漏为 0 | **否** | 1 个页面遗漏(RecallTestPage) | -| P0 修正项为 0 | **否** | 4 项 P0 修正 | -| 已有文档和独立基线差异已全部处理 | 是 | 16 项差异全部列入待修正清单 | - ---- - -## 四、严重问题(P0 — 会导致系统地图明显失真) - -### 4.1 功能状态误判-图表数据为占位 - -- **问题:** 文档将"查看掌握度趋势图"和"查看每周学习活跃"均标记为"部分完成" -- **实际:** ZXChartView 和 ZXWeekBarChart 使用完全硬编码的编译时数据,ViewModel 的 API 返回数据被忽略。应标记为"占位实现" -- **影响:** 分析页给人的印象是"图表数据已接入但需优化",实际情况是"图表数据完全未接入 API" - -### 4.2 功能状态误判-每日学习时长不持久化 - -- **问题:** GoalSettingDetailView 的"修改每日时长"标记为"已完成" -- **实际:** dailyMins 只保存在本地 @State 变量,不通过 API 持久化,用户下次进入恢复默认"30 分钟" -- **影响:** 给人"目标设置功能完整"的假象 - -### 4.3 设置页通知开关标记为"暂未确认" - -- **问题:** 三个通知开关绑定 bug 被标记为"暂未确认" -- **实际:** 代码中"复习提醒"、"导入完成通知"、"系统通知"三个 Toggle 全部绑定到同一个 `$reviewReminder`,这是明确的代码 bug -- **影响:** 将确定性 bug 降级为不确定性疑点,降低修正优先级 - -### 4.4 文档页面/弹窗统计数字口径不明确 - -- **问题:** 总览文档称"43 个页面/弹窗",但未说明统计口径 -- **实际:** 独立基线盘点出 38 个 Push 页面 + 10 个 Sheet/弹窗 + 8 个确认弹窗 -- **建议:** 明确统计口径(是否含弹窗?是否含废弃页面?)并更新数字 - ---- - -## 五、一般问题(P1/P2) - -### P1 — 遗漏项(4 项) - -1. 遗漏 RecallTestPage(代码存在,无入口,应为占位实现) -2. 遗漏 AIChatPage 会话置顶/取消置顶功能 -3. 遗漏 WaitlistService API 已就绪但 C 端无入口 -4. 遗漏 UserService 账号注销 API 已就绪但无 C 端入口 - -### P2 — 命名、描述、辅助状态问题(8 项) - -1. 业务能力"会员与额度"命名不准确 → 应改为"存储空间管理" -2. 建议合并"主动回忆"与"AI 学习分析"为一个业务能力 -3. StudyHomeViewModel error 状态永不可达(View 有但 ViewModel 不设置) -4. DailyThinkingPage submitted 变量永不为 true -5. 设置页"存储空间"和"登录设备管理"自引用导航 -6. 知识库详情页"知识库管理"按钮为 TODO -7. 业务能力依赖关系图缺少交叉引用 -8. 文件夹新建/重命名 Alert 未列入确认弹窗表 - ---- - -## 六、遗漏项汇总 - -| 类型 | 数量 | 明细 | -|------|------|------| -| 遗漏页面 | 1 | RecallTestPage | -| 遗漏功能 | 4 | 会话置顶、AIHomeView 子功能、解析触发、MaterialReaderView COS 下载 | -| 遗漏 Sheet/弹窗 | 1 | 文件夹新建/重命名 Alert | -| 遗漏 API-页面入口 | 3 | WaitlistService、账号注销、设备管理 | - ---- - -## 七、错误项汇总 - -| 类型 | 数量 | 明细 | -|------|------|------| -| 状态误判 | 4 | 掌握度趋势图(部分→占位)、每周活跃图(部分→占位)、每日时长(已完成→部分)、通知开关(暂未确认→部分+bug) | -| 命名错误 | 1 | 会员与额度 → 存储空间管理 | -| 分类建议 | 2 | 主动回忆+AI 分析建议合并、能力依赖图缺少交叉引用 | - ---- - -## 八、待确认项 - -| 序号 | 待确认内容 | 原因 | -|------|-----------|------| -| U01 | LearningSessionView 是否调用 API 保存学习记录 | 代码中未发现 LearningSessionService.start/end 的直接调用,但可能在 Rust FFI 或 ViewModel 中有间接调用 | -| U02 | SettingsView iCloud 同步开关实际后端支持情况 | Toggle UI 存在但仅改变本地 @State,无法确认后端是否有对应能力 | -| U03 | 分析页"近 7 天"日期范围筛选是否可切换 | UI 中展示了日期范围标签但未发现切换逻辑 | - ---- - -## 九、修正优先级 - -### P0(4 项)— 影响整体地图正确性 - -1. 分析页图表数据状态修正(部分完成 → 占位实现) -2. 每日学习时长持久化状态修正(已完成 → 部分完成) -3. 设置页通知开关 bug 状态修正(暂未确认 → 部分完成,存在 bug) -4. 总览页统计数字口径明确化 - -### P1(4 项)— 遗漏主要页面或业务能力 - -5. 补充 RecallTestPage 到页面地图和功能表 -6. 补充 AIChatPage 会话置顶功能 -7. 标注 WaitlistService 状态 -8. 标注账号注销 API 状态 - -### P2(8 项)— 命名、描述、辅助状态完善 - -9. 重命名"会员与额度"为"存储空间管理" -10. 考虑合并"主动回忆"与"AI 学习分析" -11-16. 其他描述性补充 - ---- - -## 十、最终判断 - -### 总体评价 - -- **已有文档的代码覆盖率高**(页面覆盖率约 95%,功能覆盖率约 95%) -- **核心流程描述准确**(用户生命周期、Tab 结构、路由系统) -- **Mock/占位实现识别基本正确**(AI 相关页面正确标记为占位实现) - -### 主要不足 - -1. **分析页图表状态被低估**(占位实现被标记为部分完成) -2. **设置页通知 bug 被降级为"暂未确认"** -3. **GoalSettingDetailView 学习时长不持久化未被发现** -4. **1 个代码页面、2 个 API 端点、1 个交互功能的遗漏** - -### 是否可以进入下一阶段? - -**可以进入下一阶段的"跨项目业务能力展开",但建议先完成 4 项 P0 修正。** - -原因: -- 已有文档的整体架构和页面覆盖是准确的,作为后续分析的基础足够可靠 -- P0 问题集中在对少数具体功能的状态判断上,不影响整体地图的结构正确性 -- 未发现系统性的页面遗漏或归类错误 -- Mock/占位/废弃实现的识别框架是正确的,只需修正个别判断 -- P1/P2 问题不阻塞跨项目分析,可在第二阶段的盘点中一并修正 - -### 门禁复查 - -| 门禁 | 修正前 | 修正 P0 后 | -|------|--------|-----------| -| 严重遗漏为 0 | 否(1 项 P1) | 是 | -| P0 修正项为 0 | 否(4 项) | 是 | -| Mock/占位未误判 | 否 | 是(修正 4 处后) | - -**完成 4 项 P0 修正后可达"通过"标准。** diff --git a/docs/system-map/ios/review/01-独立页面基线.md b/docs/system-map/ios/review/01-独立页面基线.md deleted file mode 100644 index 4dfdebe..0000000 --- a/docs/system-map/ios/review/01-独立页面基线.md +++ /dev/null @@ -1,314 +0,0 @@ -# 01 独立页面基线 - -> 本文件不参考已有系统地图文档,完全基于代码扫描独立建立。 -> 扫描日期:2026-06-19 -> 扫描范围:AIStudyApp/AIStudyApp/ 目录下全部 .swift 文件(不含 Tests 和 Generated) - ---- - -## 一、独立盘点统计 - -| 统计项 | 数量 | -|--------|------| -| 一级模块(Tab) | 4 | -| 独立页面(含可路由页面) | 32 | -| Sheet / 弹窗 | 10 | -| 确认弹窗(Alert/ConfirmationDialog) | 8 | -| 页面内嵌子视图(非独立路由) | 2 | -| 代码存在但无入口的页面 | 1 | -| 页面功能点(估算) | ~180 | - ---- - -## 二、应用根结构 - -### 2.1 入口点 - -- `AIStudyAppApp.swift` 是 `@main` 入口。 -- 使用 `WindowGroup`,`UIApplicationSupportsMultipleScenes = false`。 -- 无 URL Scheme、Universal Link、Deep Link 配置(Info.plist 中无相关条目)。 -- 无通知点击路由(未发现 `UNUserNotificationCenter` delegate 或 `onOpenURL`)。 -- 无"恢复上次页面"逻辑(每次启动由 AuthManager 状态决定初始页面)。 - -### 2.2 会话状态机(AuthManager.AppSession) - -``` -unknown → SplashScreen -unauthenticated → PreLoginFlow (WelcomePage → LoginPage) -authenticating / refreshing → SplashScreen -authenticated → ContentView (if hasCompletedOnboarding) else PostLoginOnboardingFlow -disabled → AccountStatusView (禁用) -deleted → AccountStatusView (注销) -expired → PreLoginFlow -``` - -### 2.3 Tab 根结构(ContentView) - -``` -TabView(selection: $selectedTab) - ├─ "学习" (tag: "study") → NavigationStack → StudyHomeView - ├─ "知识库" (tag: "library") → NavigationStack → LibraryHomeView - ├─ "分析" (tag: "analysis") → NavigationStack → AnalysisHomeView - └─ "我的" (tag: "profile") → NavigationStack → ProfileView -``` - -所有 Tab 使用 NavigationStack + `.navigationDestination(for: Route.self)` 统一路由。 - -### 2.4 条件性入口 - -| 条件 | 影响的页面/行为 | -|------|----------------| -| hasCompletedOnboarding = false | 显示 PostLoginOnboardingFlow (OnboardingPage → GoalSetupPage) | -| hasCompletedOnboarding = true | 直接进入 ContentView | -| session = .disabled | 显示 AccountStatusView(禁用) | -| session = .deleted | 显示 AccountStatusView(注销) | -| session = .expired / .unauthenticated | 显示 PreLoginFlow | -| selectedTab 值变化 | StudyHomeView 通过 Binding 接收,可被其他 Tab 修改 | -| 有 continueLearning API 数据 | 学习主页显示"继续上次学习"卡片 | -| reviewCards.count > 0 | 学习主页显示"今日复习"卡片 | -| quizzes.count > 0 | 学习主页显示"资料自测"卡片 | -| knowledgeBases.count > 0 | 学习主页显示"开始学习"卡片 | -| 以上全为空 | 学习主页显示空状态引导 | -| iPhone / iPad | AppContext 中记录 deviceType,但不影响页面布局 | -| Debug / Release | DEBUG 宏仅在 Route.destination 中用于显示页面标签覆盖层 | - ---- - -## 三、完整页面清单(按入口类型分类) - -### 3.1 启动/登录流程页面 - -| 序号 | 页面 | Swift 类型 | 入口方式 | 是否可路由 | -|------|------|-----------|---------|-----------| -| P01 | 启动页 | SplashScreen | 应用冷启动(session = .unknown/.authenticating/.refreshing) | 否 | -| P02 | 欢迎页 | WelcomePage | PreLoginFlow step 0 | 否 | -| P03 | 登录页 | LoginPage | PreLoginFlow step 1 | 否 | -| P04 | 账号禁用页 | AccountStatusView | session = .disabled | 否 | -| P05 | 账号注销页 | AccountStatusView | session = .deleted | 否 | -| P06 | 引导页 | OnboardingPage | PostLoginOnboardingFlow step 0 | 否 | -| P07 | 目标设置页 | GoalSetupPage | PostLoginOnboardingFlow step 1 | 否 | - -### 3.2 学习 Tab 页面(NavigationLink Push) - -| 序号 | 页面 | Route 枚举 | 入口 | -|------|------|-----------|------| -| P08 | 学习主页 | Route.studyHome | Tab "学习" 默认 | -| P09 | 学习会话页 | Route.learningSession | 资料阅读页工具栏 | -| P10 | 复习卡片页 | Route.reviewCard | 学习主页 / AIHomeView | -| P11 | 主动回忆页 | Route.activeRecall | 学习主页底部链接 / AIHomeView | - -### 3.3 知识库 Tab 页面(NavigationLink Push) - -| 序号 | 页面 | Route 枚举 | 入口 | -|------|------|-----------|------| -| P12 | 知识库首页 | Tab "知识库" 直接渲染 | Tab "知识库" | -| P13 | 知识库搜索页 | Route.librarySearch | 知识库首页搜索图标 | -| P14 | 知识库详情页 | Route.libraryDetail | 知识库首页卡片点击 | -| P15 | 创建知识库页 | Route.libraryCreate | 知识库首页 + 按钮 / 我的页学习资产 | -| P16 | 添加知识点页 | Route.addKnowledge | 知识库详情页操作菜单"添加" | -| P17 | 知识点详情页 | Route.knowledgeDetail | 知识库详情页点击知识点 | -| P18 | 编辑知识点页 | Route.editKnowledge | 知识点详情页工具栏菜单 | -| P19 | 导入资料页 | Route.libraryImport | 知识库详情页操作菜单"上传" | -| P20 | 导入候选审核页 | Route.importReview | 导入资料完成后 | -| P21 | 发现公开库页 | Route.discoverPublic | 我的页"发现公开库" | - -### 3.4 分析 Tab 页面 - -| 序号 | 页面 | Route 枚举 | 入口 | -|------|------|-----------|------| -| P22 | 学习分析页 | Tab "分析" 直接渲染 | Tab "分析" | - -### 3.5 我的 Tab 页面 - -| 序号 | 页面 | Route 枚举 | 入口 | -|------|------|-----------|------| -| P23 | 个人中心页 | Tab "我的" 直接渲染 | Tab "我的" | -| P24 | 编辑资料页 | Route.editProfile | 个人中心页资料卡片 / 设置页 | -| P25 | 设置页 | Route.settings | 个人中心页设置图标 / 复习提醒行 | -| P26 | 学习目标设置页 | Route.goalSetting | 设置页 / 我的页 | -| P27 | 学习方法偏好页 | Route.methodPreference | 设置页 / 我的页 | -| P28 | 通知列表页 | Route.notificationList | 我的页通知图标 / 消息中心 | -| P29 | 帮助与反馈页 | Route.feedbackForm | 我的页 / 设置页 | - -### 3.6 AI 辅助页面(跨模块 Push) - -| 序号 | 页面 | Route 枚举 | 入口 | -|------|------|-----------|------| -| P30 | AI 对话页 | Route.aiChat | 知识库详情页底部 / 资料阅读页工具栏 / AI 反馈页 | -| P31 | 每日思考页 | Route.dailyThinking | AI 反馈页"再来一题" / AIHomeView | -| P32 | AI 反馈页 | Route.aiFeedback | 主动回忆完成后 / 每日思考提交后 | -| P33 | 薄弱知识点页 | Route.weakPoints | 分析页"全部 N 个"链接 / AIHomeView | - -### 3.7 测验页面 - -| 序号 | 页面 | Route 枚举 | 入口 | -|------|------|-----------|------| -| P34 | 测验列表页 | Route.quizList | 知识库详情页操作菜单"测验" | -| P35 | 测验作答页 | Route.quizTake | 测验列表点击 / 学习主页自我测试 | -| P36 | 测验结果页 | Route.quizResult | 测验作答提交后 | - -### 3.8 资料阅读页面 - -| 序号 | 页面 | Route 枚举 | 入口 | -|------|------|-----------|------| -| P37 | 资料阅读页 | Route.materialReader | 学习主页"继续学习" / 分析页"继续阅读" / 来源文件 / 知识点详情 | -| P38 | 资料详情页 | Route.materialDetail | 资料阅读页工具栏"信息"按钮 | - -### 3.9 Sheet / 弹窗(非独立 Push 页面) - -| 序号 | 名称 | 类型 | 所在页面 | 入口 | -|------|------|------|---------|------| -| S01 | AI 会话列表 Sheet | .sheet (medium/large) | AIChatPage | 工具栏"会话列表"按钮 | -| S02 | 快速笔记 Sheet | .sheet | MaterialReaderView | 工具栏笔记按钮 | -| S03 | 系统预览 (QuickLook) | .sheet (QLPreviewController) | MaterialReaderView | "系统预览"按钮 | -| S04 | 封面选择 Sheet | .sheet (height 220) | CreateLibraryPage | 封面区域点击 | -| S05 | 文件夹管理 Sheet | .sheet | LibraryDetailPage | 操作菜单"文件夹管理" | -| S06 | 链接导入 Alert | .alert (with TextField) | ImportPage | "链接导入"按钮 | - -### 3.10 确认弹窗(Alert / ConfirmationDialog) - -| 序号 | 名称 | 类型 | 所在页面 | -|------|------|------|---------| -| A01 | 结束学习确认 | ConfirmationDialog | LearningSessionView | -| A02 | 删除知识库确认 | Alert | LibraryDetailPage | -| A03 | 批量删除确认 | Alert | LibraryDetailPage | -| A04 | 新建文件夹 | Alert (with TextField) | FolderManageView | -| A05 | 重命名文件夹 | Alert (with TextField) | FolderManageView | -| A06 | 退出登录确认 | Alert | SettingsView | -| A07 | 新建分类 | Alert (with TextField) | FolderManageView | -| A08 | 链接导入 | Alert (with TextField) | ImportPage | - -### 3.11 代码存在但无入口的页面 - -| 名称 | Swift 类型 | 情况说明 | -|------|-----------|---------| -| AI 首页 | AIHomeView | 完整页面代码,含 NavigationLink 到 AIChatPage/ActiveRecallView/WeakPointsPage/ReviewCardView/DailyThinkingPage,但 Route 中无对应 case,无任何其他页面跳转到此页 | - ---- - -## 四、入口类型检查清单 - -| 入口类型 | 是否检查 | 发现情况 | -|---------|---------|---------| -| Tab | 是 | 4 个:学习、知识库、分析、我的 | -| Push (NavigationLink via Route) | 是 | 约 25 个路由目标 | -| Sheet | 是 | 6 个 Sheet | -| FullScreenCover | 是 | 未发现 | -| Alert | 是 | 6 个 Alert | -| ConfirmationDialog | 是 | 1 个 | -| Popover | 是 | 未发现 | -| ContextMenu | 是 | LibraryDetailPage 知识点行 | -| Menu | 是 | LibraryDetailPage 工具栏 | -| Deep Link | 是 | 未配置 | -| Universal Link | 是 | 未配置 | -| URL Scheme | 是 | 未配置 | -| 通知点击路由 | 是 | 未发现 | -| Debug 页面 | 是 | 仅 DEBUG 标签覆盖层,无专用调试页面 | -| 条件渲染页面 | 是 | 基于 session / onboarding / 数据状态的 7 种分支 | -| iPhone/iPad 差异 | 是 | 仅在 AppContext 记录,无布局差异 | -| Feature Flag | 是 | 未发现 | -| 管理员/内部账号 | 是 | 未发现 | -| 首次启动 vs 非首次 | 是 | hasCompletedOnboarding @AppStorage | - ---- - -## 五、页面跳转关系(基于 Route 枚举和 NavigationLink 分析) - -### 5.1 Tab 内跳转 - -| 源页面 | 目标页面 | Route | -|--------|---------|-------| -| StudyHomeView | MaterialReaderView | .materialReader | -| StudyHomeView | ReviewCardView | .reviewCard | -| StudyHomeView | QuizTakerView | .quizTake | -| StudyHomeView | QuizListView | .quizList | -| StudyHomeView | ActiveRecallView | .activeRecall | -| StudyHomeView | 切换到 library Tab | selectedTab 绑定 | -| LibraryHomeView | LibrarySearchView | .librarySearch | -| LibraryHomeView | CreateLibraryPage | .libraryCreate | -| LibraryHomeView | LibraryDetailPage | .libraryDetail | -| LibraryDetailPage | KnowledgeDetailPage | .knowledgeDetail | -| LibraryDetailPage | EditKnowledgePage | .editKnowledge | -| LibraryDetailPage | ImportPage | .libraryImport | -| LibraryDetailPage | AddKnowledgePage | .addKnowledge | -| LibraryDetailPage | QuizListView | .quizList | -| LibraryDetailPage | AIChatPage | .aiChat | -| LibraryDetailPage | MaterialReaderView | .materialReader | -| LibraryDetailPage | FolderManageView (Sheet) | - | -| CreateLibraryPage | 封面选择 Sheet | - | -| AddKnowledgePage | (文件选择器/相册) | 系统组件 | -| KnowledgeDetailPage | MaterialReaderView | .materialReader | -| KnowledgeDetailPage | QuickLook (Sheet) | - | -| KnowledgeDetailPage | QuickNoteSheet | - | -| ImportPage | ImportReviewPage | .importReview | -| ImportPage | (文件选择器/相册/链接Alert) | 系统组件 | -| AnalysisHomeView | WeakPointsPage | .weakPoints | -| AnalysisHomeView | MaterialReaderView | .materialReader | -| ProfileView | EditProfilePage | .editProfile | -| ProfileView | GoalSettingDetailView | .goalSetting | -| ProfileView | SettingsView | .settings | -| ProfileView | MethodPreferenceView | .methodPreference | -| ProfileView | FeedbackFormView | .feedbackForm | -| ProfileView | CreateLibraryPage | .libraryCreate | -| ProfileView | DiscoverView | .discoverPublic | -| ProfileView | NotificationListView | .notificationList | -| SettingsView | GoalSettingDetailView | .goalSetting | -| SettingsView | MethodPreferenceView | .methodPreference | -| SettingsView | EditProfilePage | .editProfile | -| SettingsView | FeedbackFormView | .feedbackForm | - -### 5.2 跨模块跳转 - -| 源页面 | 目标页面 | 触发方式 | -|--------|---------|---------| -| ActiveRecallView | AIFeedbackPageView | 提交完所有回答后"查看 AI 分析" | -| AIFeedbackPageView | StudyHomeView | "加入待巩固" | -| AIFeedbackPageView | AIChatPage | "深入提问" | -| AIFeedbackPageView | DailyThinkingPage | "再来一题" | -| DailyThinkingPage | AIFeedbackPageView | 提交回答后 | -| MaterialReaderView | AIChatPage | 工具栏 AI 按钮 | -| MaterialReaderView | MaterialDetailView | 工具栏信息按钮 | -| MaterialReaderView | LearningSessionView | 工具栏学习按钮 | -| MaterialReaderView | QuickNoteSheet | 工具栏笔记按钮 | -| AIHomeView | AIChatPage | NavigationLink(页面自身无入口) | -| AIHomeView | ActiveRecallView | NavigationLink(页面自身无入口) | -| AIHomeView | WeakPointsPage | NavigationLink(页面自身无入口) | -| AIHomeView | ReviewCardView | NavigationLink(页面自身无入口) | -| AIHomeView | DailyThinkingPage | NavigationLink(页面自身无入口) | - ---- - -## 六、API 服务清单(每个对应一组后端端点) - -| 服务类 | 主要端点 | 被哪些页面使用 | -|--------|---------|--------------| -| AuthService | /auth/apple, /auth/refresh, /auth/logout | LoginPage, AuthManager | -| UserService | /users/me (GET/PATCH), /users/me/preferences, /users/me/profile, /users/me/deletion-request, /users/me/devices, /users/me/assets-summary, /users/me/storage | ProfileView, EditProfilePage, SettingsView, GoalSettingDetailView, MethodPreferenceView | -| KnowledgeBaseService | /knowledge-bases (CRUD), /knowledge-bases/subscribed, /knowledge-bases/:id/subscribe | LibraryHomeView, LibraryDetailPage, CreateLibraryPage, DiscoverView, StudyHomeView | -| KnowledgeItemService | /knowledge-items (CRUD, batch-delete) | LibraryDetailPage, AddKnowledgePage, KnowledgeDetailPage, EditKnowledgePage | -| KnowledgeFolderService | /knowledge-bases/:id/folders (CRUD) | FolderManageView | -| KnowledgeSourceService | /knowledge-bases/:id/sources (CRUD, parse) | LibraryDetailPage, MaterialReaderView | -| DocumentImportService | /imports (create, status) | ImportPage | -| ImportCandidateService | /import-candidates (list, accept, reject, batch-accept) | ImportReviewPage | -| QuizService | /quizzes (generate, list, detail, start, submit, results, history) | QuizListView, QuizTakerView, QuizResultView | -| RagChatService | /rag-chat/sessions (CRUD, messages, stream) | AIChatPage | -| ActiveRecallService | /active-recalls (list, submit) | ActiveRecallView | -| AIAnalysisService | /ai-analysis | (ViewModel 中有调用,但 ActiveRecallView 使用硬编码数据) | -| ReviewService | /reviews (due, submit, generate-cards) | ReviewCardView (View 中硬编码,ViewModel 调用 API) | -| FocusItemService | /focus-items (list, update, complete) | AnalysisHomeView (via ActivityViewModel) | -| ActivityService | /activity (summary, heatmap, trend, streak, recommendations) | AnalysisHomeView, ProfileView, StudyHomeView | -| LearningSessionService | /learning-sessions (list, start, end) | LearningSessionView, StudyHomeView | -| ReadingAPIService | /learning/reading-events/batch, /materials/:id/reading-progress, /learning/continue, /learning/summary, /learning/trend, /learning/records | MaterialReaderView, StudyHomeView, AnalysisHomeView | -| FileUploadService | /files/upload-url, /files/complete, /files/:id | CreateLibraryPage, AddKnowledgePage, ImportPage, EditProfilePage | -| FeedbackService | /feedback | FeedbackFormView | -| NotificationService | /notifications (list, markRead, markAllRead, preferences, push-token) | NotificationListView, SettingsView | -| WaitlistService | /waitlist | (API 定义存在,iOS 代码中未发现使用入口) | - ---- - -## 七、免责说明 - -- 本基线完全基于对 AIStudyApp/AIStudyApp/ 目录下全部 64 个 .swift 文件的逐文件阅读建立。 -- 未检查 .xcassets、.entitlements、.xcodeproj 中的详细配置。 -- iOS 推送通知的深度链接处理在代码中未发现,但可能在 Capabilities 中配置(未检查 .entitlements 文件全文)。 -- WaitlistService 代码存在但未在任何 View 或 ViewModel 中使用,判定为无 C 端入口。 diff --git a/docs/system-map/ios/review/02-独立功能基线.md b/docs/system-map/ios/review/02-独立功能基线.md deleted file mode 100644 index b173765..0000000 --- a/docs/system-map/ios/review/02-独立功能基线.md +++ /dev/null @@ -1,537 +0,0 @@ -# 02 独立功能基线 - -> 完全基于代码扫描独立建立,不参考已有系统地图文档。 -> 每项功能标注:已完成 / 部分完成 / 占位实现 / 未完成 / 暂未确认 - ---- - -## 一、启动与登录流程 - -### P01 启动页 (SplashScreen) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| 展示品牌标识 | 已完成 | 渐变背景 + 大脑图标 + "知习" + ProgressView | -| 自动恢复登录会话 | 已完成 | AuthManager.restoreSession() 读取 Keychain Token → /users/me 校验 | -| Token 刷新 | 已完成 | /auth/refresh 端点,APIClient 401 自动重试 | -| 会话过期处理 | 已完成 | session = .expired → PreLoginFlow | -| 账号禁用/注销检测 | 已完成 | /users/me 返回特定 errorCode → 切到对应 AccountStatusView | - -### P02 欢迎页 (WelcomePage) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| 产品功能介绍 | 已完成 | "AI 驱动"标签 + 主标题 + 3 个 FeatureRow(主动回忆、费曼解释、AI 分析) | -| 进入登录 | 已完成 | "开始使用" → LoginPage | - -### P03 登录页 (LoginPage) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| Apple ID 登录 | 已完成 | SignInWithAppleButton → AuthService.appleLogin | -| nonce 验证 | 已完成 | SHA256(nonce) 传入 Apple 请求 | -| 登录中取消 | 已完成 | 取消按钮 + Task.cancel() | -| 登录错误提示 | 已完成 | 红色错误横幅,显示具体错误信息 | -| Token 存储 | 已完成 | KeychainHelper.save | - -### P04/P05 账号状态页 (AccountStatusView) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| 禁用状态展示 | 已完成 | icon + title + message + "返回首页" | -| 注销状态展示 | 已完成 | 同上,文案不同 | -| 返回首页 | 已完成 | onBackToLogin → session = .unauthenticated | - -### P06 引导页 (OnboardingPage) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| 4 步引导浏览 | 已完成 | 输入知识 / 主动输出 / AI 分析 / 掌握知识 | -| 跳过引导 | 已完成 | "跳过" → onContinue | -| 步骤指示器 | 已完成 | 4 个圆角矩形进度条 | - -### P07 目标设置页 (GoalSetupPage) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| 选择学习目标(4 选 1) | 已完成 | 备考考试/职业技能/通识学习/自定义 | -| 选择学习方法(多选) | 已完成 | 间隔回忆/费曼技巧/AI 分析 | -| 选择每日学习时间(4 选 1) | 已完成 | 15分钟/30分钟/1小时/不限制 | -| 确认并开始 | 已完成 | "开始学习" → onComplete(true) → hasCompletedOnboarding = true | - ---- - -## 二、学习 Tab (StudyHomeView + 子页面) - -### P08 学习主页 (StudyHomeView) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| 查看连续学习天数 | 已完成 | streakDays 从 API 获取,火焰图标 + "X 天连续" | -| 继续上次学习 | 已完成 | MainAction.continueSession → MaterialReaderView,优先级最高 | -| 今日复习入口 | 部分完成 | 入口正常 → ReviewCardView,但 ReviewCardView 使用硬编码数据 | -| 资料自测入口 | 已完成 | MainAction.selfTest → QuizTakerView | -| 空状态引导(创建/探索知识库) | 已完成 | 切换到 library Tab | -| 查看每周摘要 | 已完成 | 本周分钟/复习卡片/活跃天数/连续天数 | -| 查看完整分析 | 已完成 | → Route.activeRecall(目标页为占位实现) | -| 下拉刷新 | 已完成 | .refreshable | -| 加载状态 | 已完成 | ProgressView | -| 错误状态 | 部分完成 | View 支持 error 状态,但 ViewModel 永远不会设置 error(所有 API 用 try? 吞错) | -| 优先级自动计算 | 已完成 | 继续学习 > 未完成会话 > 今日复习 > 自测 > 开始学习 > 空状态 | - -### P09 学习会话页 (LearningSessionView) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| 专注计时(1 秒精度) | 已完成 | Timer.publish(every: 1) | -| 环形进度动画 | 已完成 | Circle.trim + AngularGradient 动画,上限 30 分钟 | -| 暂停/继续 | 已完成 | isRunning/isPaused 切换 | -| 结束学习(含确认弹窗) | 已完成 | ConfirmationDialog → 结束并保存 | -| 庆祝动画 | 已完成 | ZXCelebrationView | -| 学习时长保存到 API | 暂未确认 | LearningSessionView 不直接调用 API,也未发现调用 LearningSessionService.start/end 的代码 | - -### P10 复习卡片页 (ReviewCardView) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| 卡片翻转 | 占位实现 | 点击翻转动画正常,但使用 3 张硬编码 ReviewCardItem | -| 自评四个等级 | 占位实现 | "完全不会"/"有点难"/"基本会"/"很简单"按钮 | -| 进度条 | 占位实现 | 正常展示但基于硬编码数据 | -| 完成庆祝 | 占位实现 | ZXCelebrationView | -| 调用 Review API | 未完成 | ReviewViewModel.loadDueCards() 被调用但结果被忽略,View 使用硬编码 ReviewCardItem | - -### P11 主动回忆页 (ActiveRecallView) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| 文字作答 | 占位实现 | TextEditor + 提交按钮,但使用 3 道硬编码 RecallQuestion | -| 语音作答 | 占位实现 | 展示麦克风图标和提示文字,无实际录音功能 | -| 提交答案 | 占位实现 | 本地状态更新,不调用 API | -| AI 分析动画 | 占位实现 | ZXThinkingOverlay 展示,2 秒后自动消失 | -| 查看 AI 分析结果 | 占位实现 | → AIFeedbackPageView(本身为占位实现) | -| 庆祝动画 | 占位实现 | ZXCelebrationView | -| 题目来源标签 | 占位实现 | 展示硬编码来源文字 | - ---- - -## 三、知识库 Tab - -### P12 知识库首页 (LibraryHomeView) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| 查看知识库列表 | 已完成 | LibraryViewModel.loadKnowledgeBases() → KnowledgeBaseService.list() | -| 筛选分类(全部/我的/已订阅/官方) | 部分完成 | "官方"筛选返回空数组(TODO 注释:等后端支持) | -| 搜索入口 | 部分完成 | 搜索按钮 → LibrarySearchView(搜索逻辑未实现) | -| 创建知识库入口 | 已完成 | + 按钮 → CreateLibraryPage | -| 查看知识库详情 | 已完成 | 卡片点击 → LibraryDetailPage | -| 下拉刷新 | 已完成 | .refreshable | -| 无限滚动分页 | 已完成 | ZXLoadMoreFooter | -| 知识库卡片:封面/名称/描述/日期/置顶/公开 | 已完成 | ZLibraryCard 完整展示 | - -### P13 知识库搜索页 (LibrarySearchView) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| 搜索输入框 | 已完成 | TextField 已搭建 | -| 搜索逻辑 | 未完成 | query 变化时不触发任何 API 调用,结果区域始终为空 | -| 空状态提示 | 已完成 | "搜索知识点、知识库或标签" | - -### P14 知识库详情页 (LibraryDetailPage) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| 查看知识库信息头部 | 已完成 | 封面/标题/描述/来源/文件数/创建/更新日期 | -| 知识点列表(Tab 0) | 已完成 | KnowledgeItemService.list() | -| 知识点排序 | 已完成 | 默认/文件大小/创建时间/更新时间 | -| 来源文件列表(Tab 1) | 已完成 | KnowledgeSourceService.list() | -| 来源文件阅读状态标签 | 已完成 | "已读"/"阅读中" | -| 查看知识点详情 | 已完成 | → KnowledgeDetailPage | -| 删除单个知识点 | 已完成 | contextMenu 删除 → KnowledgeItemService.delete() | -| 批量选择/删除 | 已完成 | isSelectMode + batchDeleteItems | -| 导入资料入口 | 已完成 | 操作菜单 → ImportPage | -| 添加知识点入口 | 已完成 | 操作菜单 → AddKnowledgePage | -| 生成测验入口 | 已完成 | 操作菜单 → QuizListView | -| 文件夹管理入口 | 已完成 | 操作菜单 → FolderManageView Sheet | -| AI 对话入口 | 已完成 | 底部 AI 按钮 → AIChatPage (知识库范围) | -| 删除来源文件 | 已完成 | 来源文件行删除按钮 | -| 删除知识库 | 已完成 | 确认弹窗 → KnowledgeBaseService.delete() | -| 知识点重命名 | 未完成 | contextMenu 中有"重命名"按钮,但 action 为空(// TODO: rename) | -| 移动到文件夹 | 未完成 | contextMenu 中有"移动到"按钮,但 action 为空(// TODO: move to folder) | -| 知识库管理页 | 未完成 | 操作菜单"知识库管理"按钮 action 为空(// TODO: knowledge base management page) | - -### P15 创建知识库页 (CreateLibraryPage) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| 上传封面图 | 已完成 | PhotosPicker → 裁剪 512x512 → COS 上传 | -| 输入名称(必填) | 已完成 | TextField + 必填校验 | -| 输入描述(必填) | 已完成 | TextEditor + 必填校验 | -| 创建知识库 | 已完成 | KnowledgeBaseService.create() → Toast 反馈 | - -### P16 添加知识点页 (AddKnowledgePage) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| 手动模式(标题+内容) | 已完成 | TextField + TextEditor → KnowledgeItemService.create() | -| 文件模式(选择文件) | 已完成 | fileImporter → COS 上传 → 创建知识点 | -| 图片模式(选择照片) | 已完成 | PhotosPicker → JPEG 压缩 → COS 上传 → 创建知识点 | -| 多文件批量添加 | 已完成 | 支持多选,逐个上传,显示进度 | -| 移除已选文件 | 已完成 | xmark 按钮 → 从 selectedFiles 移除 | - -### P17 知识点详情页 (KnowledgeDetailPage) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| URL 型知识点渲染 | 已完成 | COS 下载 → 本地缓存 → MaterialReaderView 嵌入 | -| 纯文本渲染 | 已完成 | 直接 Text 渲染 | -| 文件类型自动检测 | 已完成 | 从扩展名/MIME 推断 MaterialType | -| COS 过期 URL 重取 | 已完成 | 检测 XML → 重新 fetch item → 获取新签名 URL | -| 错误状态(含摘要回退) | 已完成 | 显示错误 + 摘要文字 + 重试按钮 | -| 阅读原文件 | 已完成 | NavigationLink → MaterialReaderView | -| 笔记入口 | 已完成 | 工具栏菜单 → showNoteSheet | -| 系统预览(PDF/Word/Excel) | 已完成 | 工具栏菜单 → QuickLook | -| 收藏切换 | 部分完成 | isFavorited 状态切换有 UI,但 toggleFavorite() 中有 TODO 注释:后端 API 未支持 | - -### P18 编辑知识点页 (EditKnowledgePage) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| 修改标题 | 已完成 | TextField → KnowledgeItemService.update() | -| 修改内容 | 已完成 | TextEditor → KnowledgeItemService.update() | -| 保存 | 已完成 | "保存修改"按钮 → API 调用 | - -### P19 导入资料页 (ImportPage) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| 文件导入(PDF/纯文本) | 已完成 | fileImporter → COS 上传 → KnowledgeSourceService.add() | -| 相册导入 | 部分完成 | PhotosPicker → COS 上传 → DocumentImportService.create(),但未发现后续 AI OCR 处理流程 | -| 链接导入 | 已完成 | Alert 弹窗输入 URL → DocumentImportService.create() | -| 自动选择目标知识库 | 已完成 | 加载第一个知识库,无则提示创建 | -| 导入错误提示 | 已完成 | 红色错误横幅 | -| 导入进度提示 | 已完成 | statusMessage + ProgressView | - -### P20 导入候选审核页 (ImportReviewPage) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| 查看候选列表(含置信度) | 已完成 | ImportCandidateService.listBySource() | -| 展开查看候选详情 | 已完成 | expandedId 切换 | -| 接受单个候选 | 已完成 | ImportCandidateService.accept() | -| 拒绝单个候选 | 已完成 | ImportCandidateService.reject() | -| 批量全部接受 | 已完成 | ImportCandidateService.batchAccept() | -| 空候选状态 | 已完成 | "没有待审批的候选" | - -### P21 发现公开库页 (DiscoverView) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| 查看公开知识库列表 | 已完成 | KnowledgeBaseService.discover() | -| 订阅知识库 | 已完成 | KnowledgeBaseService.subscribe() | -| 取消订阅 | 已完成 | KnowledgeBaseService.unsubscribe() | -| 空状态 | 已完成 | "暂无公开知识库" | - -### S05 文件夹管理 Sheet (FolderManageView) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| 查看文件夹列表 | 已完成 | KnowledgeFolderService.list() | -| 新建文件夹 | 已完成 | Alert 输入 → KnowledgeFolderService.create() | -| 重命名文件夹 | 已完成 | Swipe → Alert 输入 → KnowledgeFolderService.update() | -| 删除文件夹 | 已完成 | Swipe 删除 → KnowledgeFolderService.delete() | - ---- - -## 四、分析 Tab - -### P22 学习分析页 (AnalysisHomeView) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| 综合掌握度 | 已完成 | ActivityService.summary().dailyAverage | -| 总学习分钟 | 已完成 | ActivityService.summary().totalMinutes | -| 复习卡片数 | 已完成 | ActivityService.summary().totalCardsReviewed | -| 活跃天数 | 已完成 | ActivityService.summary().activeDays | -| 今日/本周阅读统计 | 已完成 | ReadingAPIService.getLearningSummary() | -| 掌握度趋势折线图 | 占位实现 | ZXChartView 使用硬编码数据 [(一,0.62)...(今,0.78)],未使用 API | -| 阅读时长趋势柱状图 | 已完成 | 使用 API 返回的 LearningTrendResponse.series | -| 本周学习活跃柱状图 | 占位实现 | ZXWeekBarChart 使用硬编码数据,未使用 API | -| 连续学习统计 | 已完成 | ActivityService.streak() | -| 最近学习记录(5 条) | 已完成 | ReadingAPIService.getLearningRecords() | -| 学习推荐(3 条) | 已完成 | ActivityService.recommendations() | -| 继续阅读入口 | 已完成 | → MaterialReaderView | -| AI 综合分析文字 | 已完成 | 动态拼接 summary 数据 | -| 薄弱知识点列表(5 条) | 已完成 | FocusItemService.list() | -| 查看全部薄弱知识点 | 已完成 | → WeakPointsPage(目标页为占位实现) | -| 下拉刷新 | 已完成 | .refreshable | -| 日期范围筛选 | 暂未确认 | "近 7 天" 标签展示但无实际切换逻辑 | - ---- - -## 五、我的 Tab - -### P23 个人中心页 (ProfileView) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| 查看个人资料(头像/昵称/邮箱) | 已完成 | UserService.myProfile() | -| 查看学习统计(活跃天/卡片/分钟) | 已完成 | ActivityService.summary() | -| 查看学习资产(库/条目/卡片计数) | 已完成 | UserService.fetchAssets() | -| 查看存储空间 | 已完成 | UserService.fetchStorage() | -| 编辑资料入口 | 已完成 | → EditProfilePage | -| 学习目标设置入口 | 已完成 | → GoalSettingDetailView | -| 复习提醒入口 | 已完成 | → SettingsView | -| 学习方法偏好入口 | 已完成 | → MethodPreferenceView | -| 帮助与反馈入口 | 已完成 | → FeedbackFormView | -| 发现公开库入口 | 已完成 | → DiscoverView | -| 消息中心入口 | 已完成 | → NotificationListView | -| 通知图标入口 | 已完成 | → NotificationListView | -| 设置图标入口 | 已完成 | → SettingsView | -| 创建知识库入口(学习资产行) | 已完成 | → CreateLibraryPage | - -### P24 编辑资料页 (EditProfilePage) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| 上传头像 | 已完成 | PhotosPicker → 256x256 裁剪 → COS 上传 | -| 编辑昵称 | 已完成 | TextField | -| 编辑学习身份 | 已完成 | TextField | -| 编辑学习方向 | 已完成 | TextField | -| 编辑当前目标 | 已完成 | TextField | -| 编辑个人简介 | 已完成 | TextEditor | -| 保存 | 已完成 | UserService.updateProfile() + updateProfileDetail() | - -### P25 设置页 (SettingsView) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| 切换外观模式 | 已完成 | system/dark/light 循环切换 → UserService.updatePreferences() | -| 语言显示 | 已完成 | 展示"简体中文" | -| 学习目标设置入口 | 已完成 | → GoalSettingDetailView | -| 方法偏好入口 | 已完成 | → MethodPreferenceView | -| 复习提醒开关 | 已完成 | Toggle + 条件显示提醒时间/间隔天数选择器 | -| 复习提醒时间选择 | 已完成 | SegmentedPicker: 08:00/12:00/18:00/20:00/21:00 | -| 间隔天数选择 | 已完成 | SegmentedPicker: 1/2/3/5/7 天 | -| 通知开关 | 部分完成 | 三个通知开关(复习提醒/导入完成/系统通知)全部绑定到同一个 $reviewReminder 变量,疑似 bug | -| 存储空间展示 | 已完成 | profileVM.formattedStorage | -| iCloud 同步开关 | 暂未确认 | Toggle UI 存在,切换仅改变本地 @State,未调用任何 API | -| 个人资料入口 | 已完成 | → EditProfilePage | -| 设备管理入口 | 未完成 | "登录设备管理"行 → Route.settings(指向自身),无实际设备管理页面 | -| 帮助与反馈入口 | 已完成 | → FeedbackFormView | -| 隐私政策入口 | 未完成 | "隐私政策"行无任何导航动作(无 NavigationLink/Button action) | -| 用户协议入口 | 未完成 | "用户协议"行无任何导航动作 | -| 退出登录 | 已完成 | 确认弹窗 → authManager.signOut() | -| 版本号展示 | 已完成 | "知习 v1.0" | - -### P26 学习目标设置页 (GoalSettingDetailView) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| 选择学习目标 | 已完成 | 4 选 1 → UserService.updateProfileDetail(currentGoal) | -| 选择每日学习时长 | 部分完成 | 4 选 1 UI 存在,但 dailyMins 只保存在本地 @State,不通过 API 持久化 | - -### P27 学习方法偏好页 (MethodPreferenceView) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| 多选学习方法 | 已完成 | 4 种方法多选 → UserService.updatePreferences(preferredMethods) | -| 保存 | 已完成 | "保存"按钮 → API 调用 | - -### P28 通知列表页 (NotificationListView) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| 查看通知列表(按日期分组) | 已完成 | NotificationService.list(),分组为今天/本周/更早 | -| 按类型显示图标和颜色 | 已完成 | 10+ 种通知类型映射 | -| 标记单条已读 | 已完成 | NotificationService.markRead() | -| 全部标记已读 | 已完成 | NotificationService.markAllRead() | -| 下拉刷新 | 已完成 | .refreshable | -| 空状态 | 已完成 | "暂无通知" | - -### P29 帮助与反馈页 (FeedbackFormView) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| 选择反馈类型 | 已完成 | Bug 反馈/功能建议/内容问题/其他 | -| 输入反馈内容 | 已完成 | TextEditor | -| 提交反馈 | 已完成 | FeedbackService.submit() | - ---- - -## 六、AI 辅助功能 - -### P30 AI 对话页 (AIChatPage) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| 发送文字消息 | 已完成 | → RagChatService.sendMessageStream() | -| 流式接收 AI 回复 | 已完成 | AsyncThrowingStream SSE 解析 | -| 查看 AI 思考过程(可折叠) | 已完成 | ThinkingSection 组件 | -| 复制 AI 回复 | 已完成 | UIPasteboard.general.string | -| 重新生成回复 | 已完成 | onRegenerate → vm.send() | -| 新建对话 | 已完成 | vm.createNewSession() | -| 会话列表 Sheet | 已完成 | .sheet 展示 ChatSession 列表 | -| 切换历史会话 | 已完成 | vm.loadSession(id) | -| 搜索会话 | 已完成 | 会话列表中有搜索功能 | -| 删除会话 | 已完成 | swipe → RagChatService.deleteSession() | -| 归档会话 | 已完成 | swipe → UpdateChatSessionRequest(isArchived: true) | -| 置顶/取消置顶 | 已完成 | swipe → UpdateChatSessionRequest(isPinned:) | -| 对话范围标签 | 已完成 | scopeLabel 显示在消息列表顶部 | -| 创建会话中状态 | 已完成 | ProgressView + "正在准备 AI 助手..." | -| 会话创建失败状态 | 已完成 | 错误图标 + 文字 | - -### P31 每日思考页 (DailyThinkingPage) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| 查看思考题目 | 占位实现 | 题目硬编码:"解释'注意力机制'在 Transformer 中的作用" | -| 输入回答 | 已完成 | TextEditor | -| 提交获取 AI 反馈 | 占位实现 | → AIFeedbackPageView(本身为占位实现)。注意:submitted 状态变量从未被设置为 true,提交按钮始终可见 | - -### P32 AI 反馈页 (AIFeedbackPageView) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| AI 分析进度动画 | 占位实现 | 4 步动画 + 硬编码 5 秒等待 | -| 得分展示 | 占位实现 | 78/100 硬编码,圆环进度 0.78 | -| 掌握度评价 | 占位实现 | "良好掌握" 硬编码 | -| 答对/答错分析 | 占位实现 | 2 条硬编码文字 | -| 加入待巩固 | 占位实现 | → StudyHomeView | -| 深入提问 | 已完成 | → AIChatPage(AIChatPage 本身已完成) | -| 再来一题 | 已完成 | → DailyThinkingPage(DailyThinkingPage 本身为占位实现) | - -### P33 薄弱知识点页 (WeakPointsPage) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| 查看薄弱知识点列表 | 占位实现 | 5 个硬编码 ZXWeakRow(贝叶斯定理/正态分布/词根/Covariance/梯度下降) | -| 优先级标签 | 占位实现 | 硬编码"高"/"中"优先级 | - -### (废弃)AI 首页 (AIHomeView) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| 服务器状态检查 | 已完成 | APIClient.shared.request("/") 健康检查 | -| 快速操作入口 | 已完成 | 4 个 NavigationLink 到 activeRecall/weakPoints/aiChat/reviewCard | -| 每日思考卡片 | 占位实现 | 硬编码题目 | -| 推荐问题列表 | 占位实现 | 3 条硬编码推荐文字 | -| 底部输入栏 | 已完成 | 输入 → NavigationLink 到 AIChatPage | -| 整体页面 | 占位实现 | **代码完整但无任何导航入口**,Route 枚举中无对应 case,无任何页面跳转到此 | - ---- - -## 七、测验功能 - -### P34 测验列表页 (QuizListView) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| 查看测验列表 | 已完成 | QuizService.list(knowledgeBaseId:) | -| 生成新测验 | 已完成 | QuizService.generate(),失败时 Toast | -| 空状态引导 | 已完成 | "暂无测验" + "生成测验"按钮 | -| 开始作答 | 已完成 | → QuizTakerView | - -### P35 测验作答页 (QuizTakerView) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| 选择题作答(A/B/C/D) | 已完成 | 选项按钮,选中高亮 | -| 判断题作答 | 已完成 | "正确"/"错误"按钮 | -| 填空题作答 | 已完成 | TextField | -| 上一题/下一题导航 | 已完成 | 按钮导航 | -| 进度条 | 已完成 | GeometryReader 进度条 | -| 提交答案 | 已完成 | QuizService.submit() | -| 提交失败 Toast | 已完成 | ZXToastManager.shared.error("提交失败") | - -### P36 测验结果页 (QuizResultView) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| 查看得分 | 已完成 | 大号数字,>=60 绿色,<60 红色 | -| 查看正确/总题数 | 已完成 | "答对 X/Y 题" | -| 查看逐题详情(正确/错误标记+解析) | 已完成 | ForEach 遍历 answers | -| 重新测验 | 已完成 | → QuizTakerView | - ---- - -## 八、资料阅读 - -### P37 资料阅读页 (MaterialReaderView) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| Markdown 渲染 | 已完成 | Rust FFI parseMarkdown → DocumentBlockView | -| 纯文本渲染 | 已完成 | Rust FFI parseText → DocumentBlockView | -| 图片显示 | 已完成 | UIImage + 元信息 | -| PDF/Word/Excel 系统预览 | 已完成 | QuickLook Sheet | -| PowerPoint 外部打开 | 已完成 | ShareLink | -| 不支持格式提示 | 已完成 | "暂不支持此格式" | -| 标记已读 | 已完成 | 本地 MarkedReadStore + 事件上报 | -| AI 对话(资料范围) | 已完成 | → AIChatPage 带 ChatEntryContext(.material) | -| 查看资料详情 | 已完成 | → MaterialDetailView | -| 开始学习会话 | 已完成 | → LearningSessionView | -| 添加笔记 | 已完成 | → QuickNoteSheet,含锚点记录 | -| 恢复阅读位置 | 已完成 | API → 本地 → 会话 三级优先级 | -| 自动记录阅读位置(2s 防抖) | 已完成 | reportScrollPosition | -| 前后台会话管理 | 已完成 | scenePhase → pause/resume | -| 心跳上报(15s) | 已完成 | Rust FFI | -| COS 自动下载 | 已完成 | 文件不存在时自动从 COS 下载 | -| 解析触发 | 部分完成 | 60×2s 硬编码轮询,硬编码超时逻辑 | -| 阅读进度标记 | 已完成 | API reading-progress 查询 | - -### P38 资料详情页 (MaterialDetailView) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| AI 处理状态横幅 | 已完成 | 处理中(琥珀色)/就绪(绿色)/失败(红色) | -| 文件元数据展示 | 已完成 | 文件名、格式、大小、上传日期 | -| 阅读进度卡片 | 已完成 | 阅读状态 + 累计阅读时长 | -| 知识点/测验计数 | 已完成 | statBox 展示 | - ---- - -## 九、弹窗与 Sheet - -### S01 AI 会话列表 Sheet - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| 查看会话列表 | 已完成 | RagChatService.listSessions() | -| 切换会话 | 已完成 | 点击 → vm.loadSession() | -| 搜索会话 | 已完成 | 搜索输入 | -| 删除会话 | 已完成 | swipe 删除 | -| 归档会话 | 已完成 | swipe 归档 | -| 置顶/取消置顶 | 已完成 | swipe 置顶 | -| 新建会话 | 已完成 | + 按钮 | - -### S02 快速笔记 Sheet (QuickNoteSheet) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| 输入笔记文字 | 已完成 | TextEditor(自动聚焦) | -| 保存笔记 | 已完成 | UserDefaults 存储,含锚点信息 | -| 取消 | 已完成 | dismiss() | - -### S03 系统预览 (QuickLook) - -| 功能 | 状态 | 判定依据 | -|------|------|---------| -| PDF/Office 预览 | 已完成 | QLPreviewController | - ---- - -## 十、统计 - -| 统计项 | 已完成 | 部分完成 | 占位实现 | 未完成 | 暂未确认 | -|--------|--------|---------|---------|--------|---------| -| 启动/登录功能 | 16 | 0 | 0 | 0 | 0 | -| 学习 Tab 功能 | 8 | 2 | 6 | 1 | 1 | -| 知识库 Tab 功能 | 30 | 3 | 0 | 3 | 0 | -| 分析 Tab 功能 | 12 | 0 | 2 | 0 | 1 | -| 我的 Tab 功能 | 24 | 2 | 0 | 3 | 1 | -| AI 辅助功能 | 12 | 0 | 10 | 0 | 0 | -| 测验功能 | 11 | 0 | 0 | 0 | 0 | -| 资料阅读功能 | 17 | 1 | 0 | 0 | 0 | -| Sheet/弹窗功能 | 9 | 0 | 0 | 0 | 0 | -| **总计** | **139** | **8** | **18** | **7** | **3** | diff --git a/docs/system-map/ios/review/03-页面差异清单.md b/docs/system-map/ios/review/03-页面差异清单.md deleted file mode 100644 index 8f5470d..0000000 --- a/docs/system-map/ios/review/03-页面差异清单.md +++ /dev/null @@ -1,189 +0,0 @@ -# 03 页面差异清单 - -> 将独立页面基线(review/01-独立页面基线.md)与已有文档(01-iOS页面地图.md)逐项比对。 - ---- - -## 一、文档已覆盖且正确的页面(一致项) - -以下页面在文档中描述正确,与代码实现一致: - -| 文档章节 | 文档页面名 | 代码类型 | 判定 | -|---------|-----------|---------|------| -| 1.1 | 启动页 (SplashScreen) | SplashScreen | 一致 | -| 1.2 | 欢迎页 (WelcomePage) | WelcomePage | 一致 | -| 1.3 | 登录页 (LoginPage) | LoginPage | 一致 | -| 1.4 | 账号状态页 (AccountStatusView) | AccountStatusView | 一致 | -| 1.5 | 引导页 (OnboardingPage) | OnboardingPage | 一致 | -| 1.6 | 目标设置页 (GoalSetupPage) | GoalSetupPage | 一致 | -| 2.1 | 学习主页 (StudyHomeView) | StudyHomeView | 一致 | -| 2.2 | 学习会话页 (LearningSessionView) | LearningSessionView | 一致 | -| 2.3 | 复习卡片页 (ReviewCardView) | ReviewCardView | 一致 | -| 2.4 | 主动回忆页 (ActiveRecallView) | ActiveRecallView | 一致 | -| 3.1 | 知识库首页 (LibraryHomeView) | LibraryHomeView | 一致 | -| 3.2 | 知识库搜索页 (LibrarySearchView) | LibrarySearchView | 一致 | -| 3.3 | 知识库详情页 (LibraryDetailPage) | LibraryDetailPage | 一致 | -| 3.4 | 创建知识库页 (CreateLibraryPage) | CreateLibraryPage | 一致 | -| 3.5 | 添加知识点页 (AddKnowledgePage) | AddKnowledgePage | 一致 | -| 3.6 | 知识点详情页 (KnowledgeDetailPage) | KnowledgeDetailPage | 一致 | -| 3.7 | 编辑知识点页 (EditKnowledgePage) | EditKnowledgePage | 一致 | -| 3.8 | 导入资料页 (ImportPage) | ImportPage | 一致 | -| 3.9 | 导入候选审核页 (ImportReviewPage) | ImportReviewPage | 一致 | -| 3.10 | 发现公开库页 (DiscoverView) | DiscoverView | 一致 | -| 3.11 | 文件夹管理 (FolderManageView) | FolderManageView | 一致 | -| 4.1 | 学习分析页 (AnalysisHomeView) | AnalysisHomeView | 一致 | -| 5.1 | 个人中心页 (ProfileView) | ProfileView | 一致 | -| 5.2 | 编辑资料页 (EditProfilePage) | EditProfilePage | 一致 | -| 5.3 | 设置页 (SettingsView) | SettingsView | 一致 | -| 5.4 | 学习目标设置页 (GoalSettingDetailView) | GoalSettingDetailView | 一致 | -| 5.5 | 学习方法偏好页 (MethodPreferenceView) | MethodPreferenceView | 一致 | -| 5.6 | 通知列表页 (NotificationListView) | NotificationListView | 一致 | -| 5.7 | 帮助与反馈页 (FeedbackFormView) | FeedbackFormView | 一致 | -| 6.1 | AI 对话页 (AIChatPage) | AIChatPage | 一致 | -| 6.2 | 每日思考页 (DailyThinkingPage) | DailyThinkingPage | 一致 | -| 6.3 | AI 反馈页 (AIFeedbackPageView) | AIFeedbackPageView | 一致 | -| 6.4 | 薄弱知识点页 (WeakPointsPage) | WeakPointsPage | 一致 | -| 6.5 | AI 首页 (AIHomeView) | AIHomeView | 一致(正确标记为废弃) | -| 7.1 | 测验列表页 (QuizListView) | QuizListView | 一致 | -| 7.2 | 测验作答页 (QuizTakerView) | QuizTakerView | 一致 | -| 7.3 | 测验结果页 (QuizResultView) | QuizResultView | 一致 | -| 8.1 | 资料阅读页 (MaterialReaderView) | MaterialReaderView | 一致 | -| 8.2 | 资料详情页 (MaterialDetailView) | MaterialDetailView | 一致 | -| 9.1 | 快速笔记 (QuickNoteSheet) | QuickNoteSheet | 一致 | -| 9.2 | 系统预览 (QuickLook) | QuickLookPreview | 一致 | -| 9.3 | AI 会话列表 Sheet | AIChatPage 内 Sheet | 一致 | -| 9.4 | 封面选择 Sheet | CreateLibraryPage 内 Sheet | 一致 | -| 9.5 | 链接导入弹窗 | ImportPage 内 Alert | 一致 | -| 9.6 | 各类确认弹窗 | 多个 Alert/ConfirmationDialog | 一致 | - -**一致页面数:43 个(含 6 个 Sheet/弹窗)** - ---- - -## 二、代码存在但文档遗漏的页面 - -### 遗漏 1:RecallTestPage - -- **代码位置:** `Features/AI/RecallTestPage.swift` -- **页面作用:** 独立的回忆测试页面,用户输入对特定知识点的理解,提交后跳转 AI 反馈页。 -- **入口:** 代码中有 Route 定义但**未被任何 NavigationLink 引用**。Route 枚举中有 `aiFeedback` case,但未发现 `recallTest` case。 -- **实际状态:** 代码存在但无导航入口(类似 AIHomeView 的情况)。 -- **应出现在:** 01-iOS页面地图.md → 第六章 AI 辅助功能 -- **建议补到位置:** 新增 6.6 节"回忆测试页(RecallTestPage)" -- **建议状态:** 占位实现(无导航入口 + 题目硬编码) - -### 遗漏 2:Route.studyHome 独立页面 - -- **代码位置:** Route.swift `case studyHome` -- **说明:** Route 中定义了 `studyHome` 路由目标(指向 StudyHomeView(selectedTab: .constant("study"))),可被 NavigationLink 跳转到学习主页的独立副本。虽然与 Tab 1 的学习主页渲染同一 View,但路由跳转会 push 一个新的 NavigationStack 副本。 -- **文档处理:** 文档将 StudyHomeView 仅归入 Tab 页面,未提及它也可作为 Push 路由目标。 -- **建议:** 在页面跳转关系中标注 StudyHomeView 可被 Route.studyHome 推入导航栈。 - ---- - -## 三、文档记录但当前不可访问的页面 - -无。所有文档记录的页面在代码中均有对应的入口。 - ---- - -## 四、文档记录为"已完成",但实际为"部分完成"或"占位实现"的页面 - -| 文档页面 | 文档状态 | 实际状态 | 差异说明 | -|---------|---------|---------|---------| -| 学习会话页 (2.2) | 已完成 | 暂未确认 | 文档称"已完成"。页面计时功能完成,但**未发现调用 LearningSessionService.start/end 保存学习记录到 API 的代码**,学习时长仅在本地计时。 | -| 复习卡片页 (2.3) | 占位实现 | 占位实现 | 状态一致,但文档说 ReviewViewModel 和 ReviewPlanViewModel 代码已就绪。实际情况:ReviewCardView 完全忽略 ViewModel 的数据,使用不同类型的硬编码结构体 ReviewCardItem。 | -| 主动回忆页 (2.4) | 占位实现 | 占位实现 | 状态一致。 | -| 知识库首页 (3.1) | 部分完成 | 部分完成 | 状态一致。 | -| 知识库详情页 (3.3) | 部分完成 | 部分完成 | 状态一致。 | -| 学习分析页 (4.1) | 部分完成 | 部分完成 | 状态一致。但需补充:**每日学习时长(dailyMins)保存逻辑缺失**(GoalSettingDetailView 中 dailyMins 仅保存在本地 @State)。 | -| 设置页 (5.3) | 部分完成 | 部分完成 | 状态一致。需补充:"存储空间"行 → Route.settings(自引用),"登录设备管理" → Route.settings(自引用),均无实际导航目标。 | -| AI 反馈页 (6.3) | 占位实现 | 占位实现 | 状态一致。 | -| 薄弱知识点页 (6.4) | 占位实现 | 占位实现 | 状态一致。 | -| 资料阅读页 (8.1) | 部分完成 | 部分完成 | 状态一致。 | -| 每日思考页 (6.2) | 占位实现 | 占位实现 | 状态一致。但需补充:`submitted` 状态变量从未被设置为 true,提交按钮始终可见(这可能导致 UI 行为异常)。 | - ---- - -## 五、文档重复记录的页面 - -无发现。文档每个页面只记录一次。 - ---- - -## 六、页面名称不同但实际为同一个页面的情况 - -无发现。文档页面名称与代码类型名称一一对应。 - ---- - -## 七、页面被错误归入业务模块的情况 - -| 页面 | 文档归类 | 建议归类 | 原因 | -|------|---------|---------|------| -| 创建知识库页 | 知识库 Tab | 正确 | 入口来自知识库首页和我的页 | -| 学习会话页 | 学习 Tab | 正确 | 入口来自资料阅读页 | -| 主动回忆页 | 学习 Tab | 正确 | 入口来自学习主页 | - -无严重归类错误。 - ---- - -## 八、Sheet、弹窗、隐藏入口遗漏检查 - -### 已记录的 Sheet/弹窗(9.1-9.6) - -| 文档项目 | 覆盖情况 | -|---------|---------| -| 快速笔记 (9.1) | 已覆盖 | -| 系统预览 (9.2) | 已覆盖 | -| AI 会话列表 Sheet (9.3) | 已覆盖 | -| 封面选择 Sheet (9.4) | 已覆盖 | -| 链接导入弹窗 (9.5) | 已覆盖 | -| 各类确认弹窗 (9.6) | 已覆盖 6 个中的 6 个 | - -### 遗漏的 Sheet/弹窗 - -| 遗漏项 | 所在页面 | 说明 | -|--------|---------|------| -| 文件夹新建/重命名 Alert | FolderManageView | "新建分类" + "重命名"两个带 TextField 的 Alert,文档在 9.6 确认弹窗表格中未列出 | - -建议在 9.6 确认弹窗表格中补充: -- 新建分类(FolderManageView) -- 重命名分类(FolderManageView) - ---- - -## 九、页面入口和跳转关系错误 - -| 错误位置 | 文档描述 | 实际代码 | 严重程度 | -|---------|---------|---------|---------| -| 学习主页"查看完整分析" → ActiveRecallView | 文档:跳转主动回忆页 | 代码:`Route.activeRecall` → ActiveRecallView | **正确**(但主动回忆页为占位实现) | -| 学习主页"自我测试" → QuizTakerView | 文档:跳转测验作答页 | 代码:MainAction.selfTest → Route.quizTake(quizId:) | **正确** | -| 学习主页"自我测试"小卡片 → QuizListView | 文档未明确 | 代码:Route.quizList(knowledgeBaseId: "") | **需补充**,知识库 ID 为空字符串可能导致异常 | -| 设置页"复习提醒" → SettingsView | 文档:跳转设置页(从我的页复习提醒行) | 代码:`Route.settings` → SettingsView | **正确但奇怪**:从设置页的"复习提醒"行再次跳转 SettingsView 形成自引用 | - ---- - -## 十、页面状态遗漏 - -| 遗漏状态 | 所在页面 | 说明 | -|---------|---------|------| -| LearningSessionView 未保存学习记录到 API | 学习会话页 | 文档标记为"已完成",但缺少 API 持久化,用户在分析页看不到本次学习数据 | -| StudyHomeViewModel 永不为 error | 学习主页 | error 状态代码存在但 ViewModel 中所有 API 调用用 try? 吞错,error 状态永不可达 | -| DailyThinkingPage submitted 永不为 true | 每日思考页 | 提交按钮始终可见,可能导致用户重复提交 | -| GoalSettingDetailView dailyMins 不持久化 | 学习目标设置页 | 每日时长选择不通过 API 保存,下次进入恢复默认"30 分钟" | - ---- - -## 十一、总体评估 - -| 评估维度 | 结果 | -|---------|------| -| 文档页面覆盖率 | 43/45 (95.6%) — 遗漏 RecallTestPage + studyHome 路由 | -| 页面状态准确率 | 38/43 (88.4%) — 5 个页面状态描述与代码存在差异 | -| Sheet/弹窗覆盖率 | 8/9 (88.9%) — 遗漏 1 个确认弹窗组合 | -| 跳转关系准确率 | 基本正确,1 处需补充说明 | -| 页面状态描述完整度 | 需补充 4 处状态遗漏 | - -**结论:页面地图文档整体质量较高,覆盖率 95%+。主要问题集中在页面完成状态的部分误判,以及个别状态的遗漏。** diff --git a/docs/system-map/ios/review/04-功能差异清单.md b/docs/system-map/ios/review/04-功能差异清单.md deleted file mode 100644 index d2d5918..0000000 --- a/docs/system-map/ios/review/04-功能差异清单.md +++ /dev/null @@ -1,140 +0,0 @@ -# 04 功能差异清单 - -> 将独立功能基线(review/02-独立功能基线.md)与已有文档(02-iOS页面功能总表.md)逐项比对。 - ---- - -## 一、功能遗漏清单 - -### 遗漏 1:学习主页 selfTestSmallCard → QuizListView (kbId="") - -- **文档覆盖:** 文档记录"进行自我测试"功能(点击主操作卡片或测试卡片) -- **遗漏:** 文档未说明 smallCard 跳转 QuizListView 时 knowledgeBaseId 为空字符串,可能导致不正确的行为 -- **应补到:** 02-iOS页面功能总表.md → 学习 Tab → "进行自我测试" -- **建议补充说明:** "注:辅助卡片自测入口使用空 knowledgeBaseId,可能在 quizList 中显示空列表" - -### 遗漏 2:GoalSettingDetailView 每日学习时长不持久化 - -- **文档覆盖:** 文档 5.4 节记录 GoalSettingDetailView 的"修改每日时长"功能为"已完成" -- **实际:** dailyMins 只保存在本地 @State 变量,未通过 API 持久化 -- **应补到:** 02-iOS页面功能总表.md → 我的 Tab → "修改每日时长" -- **建议状态修正:** 已完成为 → 部分完成(UI 完成,数据不持久化) - -### 遗漏 3:RecallTestPage 功能未记录 - -- **文档覆盖:** 未记录 -- **实际:** RecallTestPage 代码存在(Features/AI/RecallTestPage.swift),含: - - 查看回忆测试题(硬编码) - - 输入回答 - - 提交获取 AI 反馈(→ AIFeedbackPageView) -- **建议补到:** 02-iOS页面功能总表.md → AI 辅助功能 → 新增"回忆测试页"行 -- **建议状态:** 占位实现(无导航入口,题目硬编码) - -### 遗漏 4:AIChatPage 置顶/取消置顶会话 - -- **文档覆盖:** 文档记录"删除会话"和"归档会话" -- **遗漏:** 未记录置顶/取消置顶功能(swipeActions(edge: .leading)) -- **应补到:** 02-iOS页面功能总表.md → AI 辅助功能 → AI 对话页 -- **建议新增行:** "置顶/取消置顶会话 | 左滑→置顶/取消置顶 | 切换会话置顶状态" - -### 遗漏 5:MaterialReaderView 解析触发 + 轮询 - -- **文档覆盖:** 文档在"阅读 Markdown 内容"等行标注已完成 -- **遗漏:** 文档未将"触发 AI 知识提取(解析)"作为独立功能记录,工具栏 brain 按钮触发 60×2s 轮询是一个重要功能 -- **建议补到:** 02-iOS页面功能总表.md → 资料阅读 → 新增行 -- **建议新增行:** "触发知识提取 | 点击工具栏大脑按钮 | 提交解析任务,轮询进度(60×2s) | 部分完成 | 轮询使用硬编码次数和间隔" - -### 遗漏 6:AIHomeView 的全部功能(虽已废弃) - -- **文档覆盖:** 文档记录了 AIHomeView 为占位实现/已废弃,但功能总表中将其合并入一行"AI 首页(已废弃)" -- **遗漏:** 该页面内有具体的子功能(服务器状态检查、快速操作入口、每日思考卡片、推荐问题列表),但功能总表未展开 -- **评估:** 这对于已废弃页面是合理的简化处理,不做强制修正要求 - ---- - -## 二、功能状态误判 - -| 文档功能 | 文档状态 | 实际状态 | 判定依据 | -|---------|---------|---------|---------| -| 学习会话-专注计时学习 | 已完成 | 暂未确认 | 本地计时完成,但未发现调用 LearningSessionService API 保存学习记录 | -| 学习主页-开始今日复习 | 部分完成 | 部分完成 | 入口正常,ReviewCardView 为占位实现。文档正确标记。 | -| 学习主页-查看完整分析 | 已完成 | 已完成 | 入口存在,跳转到 ActiveRecallView(本身为占位实现)。**文档正确标记了跳转目标的状态,但入口本身已完成。** | -| 学习主页-探索公开知识库 | 已完成 | 已完成 | 切换到 library Tab | -| 分析页-查看掌握度趋势图 | 部分完成 | 占位实现 | **应为占位实现**,ZXChartView 使用完全硬编码的 7 天数据 `[("一", 0.62)...]`,ViewModel 的 trend 数据被忽略 | -| 分析页-查看每周学习活跃 | 部分完成 | 占位实现 | **应为占位实现**,ZXWeekBarChart 使用完全硬编码数据,ViewModel 的 heatmap 数据被忽略 | -| 分析页-查看 AI 综合分析 | 已完成 | 已完成 | 动态拼接 API 数据,非 AI 生成 | -| 分析页-查看全部薄弱知识点 | 已完成 | 已完成 | 跳转到 WeakPointsPage(本身为占位实现) | -| 设置页-切换导入完成通知 | 暂未确认 | 部分完成 | 三个通知开关全部绑定到同一个 $reviewReminder 变量,是明确的 **bug** 而非待确认 | -| 设置页-切换系统通知 | 暂未确认 | 部分完成 | 同上 | -| 设置页-切换 iCloud 同步 | 暂未确认 | 暂未确认 | 仅改变本地 @State,无 API 调用 | -| GoalSettingDetailView-修改每日时长 | 已完成 | 部分完成 | dailyMins 不持久化 | -| 每日思考页-提交回答 | 占位实现 | 占位实现 | 状态一致,但 submitted 变量 bug 未记录 | - ---- - -## 三、自动触发功能遗漏 - -文档功能总表主要记录了用户主动触发的功能,以下自动触发功能在文档中有部分遗漏: - -| 自动功能 | 文档覆盖 | 建议 | -|---------|---------|------| -| MaterialReaderView 进入时自动恢复阅读位置 | 已覆盖("恢复阅读位置"行) | — | -| MaterialReaderView 前后台切换暂停/恢复会话 | 已覆盖 | — | -| MaterialReaderView 自动从 COS 下载文件 | 未明确记录 | 建议在"阅读 Markdown 内容"备注中补充 | -| MaterialReaderView COS 过期 URL 重取 | 未明确记录 | 建议补充 | -| 学习主页优先级自动计算 | 已覆盖 | — | -| StudyHomeViewModel 错误状态永不可达 | 未记录 | 建议在备注中说明 ViewModel 的 try? 吞错行为 | - ---- - -## 四、搜索、筛选、排序、分页等辅助功能检查 - -| 功能 | 页面 | 文档覆盖 | 备注 | -|------|------|---------|------| -| 知识库搜索 | LibraryHomeView | 已覆盖(但标为部分完成) | 搜索页搜索逻辑未实现 | -| 知识库分类筛选 | LibraryHomeView | 已覆盖 | 4 个分类 chips | -| 知识库分页加载 | LibraryHomeView | 已覆盖 | 无限滚动 | -| 知识点排序 | LibraryDetailPage | 已覆盖 | 4 种排序 | -| 知识点批量选择 | LibraryDetailPage | 已覆盖 | | -| 会话搜索 | AIChatPage Sheet | 已覆盖 | | -| 通知列表无筛选 | NotificationListView | 未记录筛选 | 无筛选功能,合理 | -| 测验列表无筛选 | QuizListView | 未记录筛选 | 无筛选功能,合理 | - ---- - -## 五、API 调用存在但无页面入口 - -### WaitlistService - -- **API 端点:** /waitlist (POST), /waitlist/stats (GET) -- **代码存在:** APIService.swift 中 WaitlistService 类完整定义 -- **页面入口:** 无任何 View 或 ViewModel 调用 WaitlistService -- **文档记录:** 文档未提及 waitlist 功能 -- **建议:** 在系统地图中标注"Waitlist API 已就绪但 C 端无入口"(或在总览中标注为后端预留能力) - -### UserService.requestAccountDeletion / cancelDeletion - -- **API 端点:** /users/me/deletion-request (POST/DELETE) -- **页面入口:** 无任何页面调用这两个方法 -- **文档记录:** 未提及注销账号功能 -- **建议:** 补充到设置页的未完成功能列表 - -### UserService.listDevices / removeDevice - -- **API 端点:** /users/me/devices (GET/DELETE) -- **页面入口:** 设置页有"登录设备管理"行但无导航目标(→ Route.settings 自引用) -- **建议:** 标注为"API 已就绪,设备管理页面未实现" - ---- - -## 六、总体评估 - -| 评估维度 | 结果 | -|---------|------| -| 功能总表覆盖率 | 约 95%,遗漏约 6 个功能点 | -| 功能状态准确率 | 约 90%,7 个功能的状态需要修正 | -| 自动触发功能覆盖 | 基本完整,2 处建议补充 | -| 辅助功能覆盖 | 完整 | -| API-页面映射完整性 | 3 个 API 有端点无页面入口需标注 | - -**结论:功能总表文档覆盖较全面,主要问题是若干功能的状态标记偏乐观(部分图表数据被标记为"部分完成"但实际为"占位实现"),以及少数自动触发功能和 API-页面不一致未记录。** diff --git a/docs/system-map/ios/review/05-业务能力归类审查.md b/docs/system-map/ios/review/05-业务能力归类审查.md deleted file mode 100644 index 5f1c146..0000000 --- a/docs/system-map/ios/review/05-业务能力归类审查.md +++ /dev/null @@ -1,272 +0,0 @@ -# 05 业务能力归类审查 - -> 审查对象:已有文档 `03-C端业务能力初步清单.md` -> 审查方法:先独立归类,再与文档比对 - ---- - -## 一、独立业务能力归类 - -基于代码扫描独立建立的业务能力清单: - -### 能力 1:账号注册与登录 - -- **代码依据:** Apple ID 登录(LoginPage)、会话恢复(SplashScreen/AuthManager)、Token 刷新(APIClient)、账号禁用/注销状态处理(AccountStatusView) -- **用户目标:** 使用 Apple ID 安全登录,会话自动恢复 -- **关联页面:** P01, P03, P04, P05 -- **独立判定:** 已存在,核心流程完整 - -### 能力 2:新手引导与目标设定 - -- **代码依据:** WelcomePage → OnboardingPage → GoalSetupPage 流程 -- **用户目标:** 首次使用时了解产品价值,设定个性化学习目标 -- **关联页面:** P02, P06, P07, P26, P27 -- **独立判定:** 已存在 - -### 能力 3:个人资料维护 - -- **代码依据:** EditProfilePage(头像/昵称/身份/方向/目标/简介)+ ProfileView(查看) -- **用户目标:** 维护个人档案信息 -- **关联页面:** P23, P24 -- **独立判定:** 已存在 - -### 能力 4:知识库浏览与管理 - -- **代码依据:** LibraryHomeView(列表/筛选/分页)、LibraryDetailPage(详情/排序/删除)、CreateLibraryPage(创建)、DiscoverView(公开库发现/订阅) -- **用户目标:** 创建、浏览、管理个人知识库,发现和订阅公开知识库 -- **关联页面:** P12, P14, P15, P21 -- **独立判定:** 已存在。部分功能未完成(官方库筛选、KB 管理页、搜索逻辑) - -### 能力 5:知识点管理 - -- **代码依据:** LibraryDetailPage 知识点 Tab(列表/排序/删除/批量操作)、AddKnowledgePage(手动/文件/图片添加)、KnowledgeDetailPage(查看)、EditKnowledgePage(编辑) -- **用户目标:** 在知识库中增删改查知识点 -- **关联页面:** P14, P16, P17, P18 -- **独立判定:** 已存在。收藏 API 未支持、重命名/移动到文件夹为 TODO - -### 能力 6:资料上传与处理 - -- **代码依据:** ImportPage(文件/相册/链接导入)、ImportReviewPage(AI 候选审批) -- **用户目标:** 将外部资料导入知识库,审核 AI 提取的知识点 -- **关联页面:** P19, P20 -- **独立判定:** 已存在 - -### 能力 7:文档阅读 - -- **代码依据:** MaterialReaderView(多格式渲染/位置恢复/标记已读)、MaterialDetailView(处理状态/元数据)、QuickLook(系统预览)、ShareLink(外部打开) -- **用户目标:** 阅读学习资料,支持多种格式 -- **关联页面:** P37, P38, S03 -- **独立判定:** 已存在 - -### 能力 8:阅读笔记 - -- **代码依据:** QuickNoteSheet(创建笔记/自动锚点记录) -- **用户目标:** 在阅读过程中记笔记 -- **关联页面:** S02 -- **独立判定:** 已存在。本地存储(UserDefaults),未同步到服务端 - -### 能力 9:阅读行为跟踪 - -- **代码依据:** ReadingRuntimeSessionManager + ReadingEventCollector + ReadingPositionStore -- **用户目标:** 自动记录阅读行为,用于分析 -- **关联页面:** P37(自动执行) -- **独立判定:** 已存在 - -### 能力 10:学习信息收集 - -- **代码依据:** LearningSessionView(计时)、MaterialReaderView(阅读会话) -- **用户目标:** 记录学习时长和阅读行为 -- **关联页面:** P09, P37 -- **独立判定:** 已存在。但 LearningSessionView 未发现 API 保存调用,需确认学习会话数据是否真正被收集 - -### 能力 11:继续学习 - -- **代码依据:** StudyHomeView 优先级算法(ContinueLearningResponse → MainAction.continueSession) -- **用户目标:** 从上次中断处继续学习 -- **关联页面:** P08 -- **独立判定:** 已存在 - -### 能力 12:AI 对话 - -- **代码依据:** AIChatPage(流式对话/思考过程/多会话管理/范围限定) -- **用户目标:** 与 AI 进行学习相关对话 -- **关联页面:** P30, S01 -- **独立判定:** 已存在 - -### 能力 13:题目与测验 - -- **代码依据:** QuizListView(查看/生成)、QuizTakerView(作答/提交)、QuizResultView(结果/解析) -- **用户目标:** 通过测验检验学习效果 -- **关联页面:** P34, P35, P36 -- **独立判定:** 已存在 - -### 能力 14:学习分析 - -- **代码依据:** AnalysisHomeView(统计徽章/趋势图/连续统计/学习记录/推荐/薄弱点) -- **用户目标:** 查看学习数据分析和趋势 -- **关联页面:** P22 -- **独立判定:** 已存在。掌握度趋势图和每周活跃图为占位实现 - -### 能力 15:主动回忆与 AI 反馈 - -- **代码依据:** ActiveRecallView → AIFeedbackPageView 链路 -- **用户目标:** 主动输出知识,获得 AI 评估反馈 -- **关联页面:** P11, P32, P31 -- **独立判定:** 占位实现(全部使用硬编码数据) - -### 能力 16:间隔复习 - -- **代码依据:** ReviewCardView(闪卡翻转/自评) -- **用户目标:** 基于间隔重复的闪卡复习 -- **关联页面:** P10 -- **独立判定:** 占位实现(硬编码数据,未接通 API) - -### 能力 17:学习会话计时 - -- **代码依据:** LearningSessionView(专注计时/暂停/结束/庆祝) -- **用户目标:** 记录专注学习时长 -- **关联页面:** P09 -- **独立判定:** 部分完成(本地计时完成,API 持久化待确认) - -### 能力 18:消息与通知 - -- **代码依据:** NotificationListView(分组展示/已读标记)、SettingsView(通知开关) -- **用户目标:** 接收系统通知,管理通知偏好 -- **关联页面:** P28, P25 -- **独立判定:** 已存在。通知开关存在绑定 bug - -### 能力 19:设置与应用偏好 - -- **代码依据:** SettingsView(外观/语言/复习提醒/通知/存储/iCloud/退出登录) -- **用户目标:** 管理应用全局设置 -- **关联页面:** P25, P26, P27 -- **独立判定:** 已存在。合规页面入口缺失、设备管理未实现 - -### 能力 20:帮助与反馈 - -- **代码依据:** FeedbackFormView(类型选择/内容输入/提交) -- **用户目标:** 提交问题和建议 -- **关联页面:** P29 -- **独立判定:** 已存在 - -### 能力 21:存储空间管理 - -- **代码依据:** ProfileView 存储空间展示 + SettingsView iCloud 同步开关 -- **用户目标:** 了解和控制存储使用 -- **关联页面:** P23, P25 -- **独立判定:** 部分存在(展示完成,iCloud 同步待确认) - ---- - -## 二、与已有文档的能力对比 - -### 完全一致的能力(14 项) - -| 序号 | 独立能力 | 文档能力 | 判定 | -|------|---------|---------|------| -| 1 | 账号注册与登录 | 1. 账号与用户 | 一致 | -| 2 | 新手引导与目标设定 | 2. 新手引导与目标设定 | 一致 | -| 3 | 知识库浏览与管理 | 3. 知识库管理 | 一致 | -| 4 | 知识点管理 | 4. 知识点管理 | 一致 | -| 5 | 资料上传与处理 | 5. 资料上传 | 一致 | -| 6 | 文档阅读 | 6. 文档阅读 | 一致 | -| 7 | 阅读笔记 | 7. 阅读笔记 | 一致 | -| 8 | 阅读行为跟踪 | 8. 阅读行为跟踪 | 一致 | -| 9 | 学习信息收集 | 9. 学习信息收集 | 一致 | -| 10 | 继续学习 | 10. 继续学习 | 一致 | -| 11 | AI 对话 | 11. AI 对话 | 一致 | -| 12 | 题目与测验 | 14. 题目与测验 | 一致 | -| 13 | 帮助与反馈 | 20. 帮助与反馈 | 一致 | -| 14 | 设置与应用偏好 | 19. 设置与合规 | 基本一致 | - -### 需要调整的能力(6 项) - -| 独立能力 | 文档对应 | 问题 | 建议 | -|---------|---------|------|------| -| 个人资料维护 | 合并入"1. 账号与用户" | 文档将个人资料维护合并入账号与用户能力。独立判定认为这是可接受的归类,但边界稍显模糊。 | 可保持现状,或拆分出"个人资料维护"子能力 | -| 主动回忆与 AI 反馈 | 15. 主动回忆 + 12. AI 学习分析 | 文档将"主动回忆"和"AI 学习分析"分为两个独立能力。独立判定认为主动回忆 → AI 反馈是一个连续流程,不应强行拆分。 | 建议将"15. 主动回忆"与"12. AI 学习分析"合并为"主动回忆与 AI 反馈" | -| 间隔复习 | 16. 间隔复习 | 一致 | — | -| 学习分析 | 13. 学习分析 | 一致 | — | -| 学习会话计时 | 无独立条目 | 文档将学习会话计时合并入"9. 学习信息收集"。独立判定认为学习会话计时是一个有独立 UI 的用户行为,宜作为独立能力或子能力。 | 建议在"学习信息收集"能力下明确标注"学习会话计时"子项 | -| 消息与通知 | 18. 消息与通知 | 一致 | — | -| 存储空间管理 | 合并入"17. 会员与额度" | 文档将其归入"会员与额度"。独立判定认为当前代码中无会员/订阅/付费功能,命名为"会员与额度"不准确。 | 建议重命名为"存储与额度"或"存储空间管理" | - -### 文档有但未发现代码支持的能力(1 项) - -| 文档能力 | 判定 | 说明 | -|---------|------|------| -| 17. 会员与额度 | 未发现会员/订阅相关代码 | 代码中无内购、订阅、付费墙、会员等级等实现。存储展示和 iCloud 同步开关存在,但被描述为"会员与额度"不准确。 | - ---- - -## 三、能力归类质量检查 - -### 以技术模块命名检查 - -| 文档能力名 | 是否以技术模块命名 | 判定 | -|-----------|-----------------|------| -| 账号与用户 | 否 | 通过 | -| 知识库管理 | 否 | 通过 | -| 阅读行为跟踪 | 边缘 | 偏技术,建议改为"阅读进度同步"或保持 | -| AI 对话 | 否 | 通过 | -| AI 学习分析 | 否 | 通过 | - -### 闭环拆分过度/不足检查 - -| 文档能力 | 问题 | -|---------|------| -| 主动回忆 + AI 学习分析 | 拆分过度。主动回忆提交 → AI 反馈是一个连续用户体验,不应分为两个能力 | -| 阅读行为跟踪 + 学习信息收集 | 边界模糊。两者都涉及数据收集,调用相同的底层 FFI 和 API | -| 文档阅读 + 阅读笔记 + 阅读行为跟踪 | 三个能力高度耦合在 MaterialReaderView 中,是否过度拆分值得商榷 | - -### 按页面名称命名检查 - -| 文档能力 | 是否直接使用页面名 | 判定 | -|---------|-----------------|------| -| 继续学习 | 否(StudyHomeView 的功能,非页面名) | 通过 | -| 题目与测验 | 否 | 通过 | -| 主动回忆 | 是(与页面同名) | **建议改为"主动回忆与 AI 评估"** | - ---- - -## 四、文档业务能力依赖关系图审查 - -文档中的依赖关系图: - -``` -账号与用户 - └─ 新手引导与目标设定 - └─ 知识库管理 - ├─ 知识点管理 - ├─ 资料上传 → 文档阅读 - │ ├─ 阅读笔记 - │ └─ 阅读行为跟踪 → 学习信息收集 - └─ 题目与测验 - ├─ AI 对话 - ├─ 继续学习 → 主动回忆 / 间隔复习 / 文档阅读 - │ └─ AI 学习分析 - └─ 学习分析 - └─ AI 学习分析 -``` - -审查意见: -1. "AI 对话"应在"知识库管理"下有分支(知识库范围对话)。 -2. "AI 对话"也应在"文档阅读"下有分支(资料范围对话)。 -3. "题目与测验"应在"文档阅读"下有分支(从资料阅读触发解析后进入测验)。 -4. "阅读笔记"也应关联到"知识点管理"(知识点详情页有笔记入口)。 -5. "主动回忆 → AI 学习分析 → 间隔复习"应形成环路(分析后加入待巩固)。 -6. "学习分析"应在"学习信息收集"之后,而非独立于信息收集。 - ---- - -## 五、总体评估 - -| 评估维度 | 结果 | -|---------|------| -| 能力数量合理度 | 20 项基本合理,建议合并"主动回忆"与"AI 学习分析" | -| 能力命名准确度 | 17/20 准确,1 个偏技术(阅读行为跟踪),1 个以页面命名(主动回忆),1 个不准确(会员与额度) | -| 能力边界清晰度 | 阅读相关 3 个能力(文档阅读/阅读笔记/阅读行为跟踪)边界可讨论 | -| 依赖关系准确度 | 基本正确,若干分支缺失 | -| 遗漏能力 | 无重大遗漏,学习会话计时建议作为子能力标注 | - -**结论:业务能力归类文档整体合理,主要问题是"会员与额度"命名不准确(当前无会员功能)、"主动回忆"与"AI 学习分析"建议合并、依赖关系图缺少几个交叉引用。** diff --git a/docs/system-map/ios/review/06-页面能力覆盖矩阵.md b/docs/system-map/ios/review/06-页面能力覆盖矩阵.md deleted file mode 100644 index 22dbe15..0000000 --- a/docs/system-map/ios/review/06-页面能力覆盖矩阵.md +++ /dev/null @@ -1,104 +0,0 @@ -# 06 页面能力覆盖矩阵 - -> 双向覆盖检查:页面→能力 + 能力→页面 - ---- - -## 一、页面→能力覆盖(每个页面至少归属一个业务能力) - -| 页面 | 归属业务能力 | 主要功能 | 文档是否覆盖 | 状态判断是否正确 | -|------|------------|---------|------------|----------------| -| P01 启动页 | 账号注册与登录 | 自动恢复会话 | 是 | 是 | -| P02 欢迎页 | 新手引导与目标设定 | 产品介绍、引导登录 | 是 | 是 | -| P03 登录页 | 账号注册与登录 | Apple ID 登录 | 是 | 是 | -| P04/P05 账号状态页 | 账号注册与登录 | 禁用/注销状态展示 | 是 | 是 | -| P06 引导页 | 新手引导与目标设定 | 4 步产品引导 | 是 | 是 | -| P07 目标设置页 | 新手引导与目标设定 | 设定目标/方法/时间 | 是 | 是 | -| P08 学习主页 | 继续学习、学习分析 | 优先级算法、今日任务、每周摘要 | 是 | 是 | -| P09 学习会话页 | 学习信息收集 | 专注计时 | 是 | 状态偏乐观(API 保存待确认) | -| P10 复习卡片页 | 间隔复习 | 闪卡翻转、自评 | 是 | 是(标记为占位实现) | -| P11 主动回忆页 | 主动回忆与 AI 反馈 | 文字/语音作答 | 是 | 是(标记为占位实现) | -| P12 知识库首页 | 知识库浏览与管理 | 列表/筛选/分页 | 是 | 是 | -| P13 知识库搜索页 | 知识库浏览与管理 | 搜索 | 是 | 是 | -| P14 知识库详情页 | 知识库浏览与管理、知识点管理 | 双 Tab/排序/批量操作/删除 | 是 | 是 | -| P15 创建知识库页 | 知识库浏览与管理 | 创建知识库 | 是 | 是 | -| P16 添加知识点页 | 知识点管理 | 手动/文件/图片添加 | 是 | 是 | -| P17 知识点详情页 | 知识点管理、文档阅读 | 查看/渲染/收藏/笔记 | 是 | 部分(收藏 API 未支持) | -| P18 编辑知识点页 | 知识点管理 | 编辑标题/内容 | 是 | 是 | -| P19 导入资料页 | 资料上传与处理 | 文件/相册/链接导入 | 是 | 是 | -| P20 导入候选审核页 | 资料上传与处理 | 候选审批 | 是 | 是 | -| P21 发现公开库页 | 知识库浏览与管理 | 浏览/订阅公开库 | 是 | 是 | -| P22 学习分析页 | 学习分析 | 统计/趋势图/记录/推荐/薄弱点 | 是 | 部分(趋势图为占位) | -| P23 个人中心页 | 个人资料维护、设置与应用偏好 | 资料/菜单/资产/存储 | 是 | 是 | -| P24 编辑资料页 | 个人资料维护 | 头像/昵称/身份/目标 | 是 | 是 | -| P25 设置页 | 设置与应用偏好、消息与通知 | 外观/复习/通知/存储/退出 | 是 | 部分(通知开关 bug,合规入口缺失) | -| P26 学习目标设置页 | 新手引导与目标设定 | 修改目标/时长 | 是 | 否(dailyMins 不持久化,文档标已完成) | -| P27 方法偏好页 | 新手引导与目标设定 | 多选方法 | 是 | 是 | -| P28 通知列表页 | 消息与通知 | 查看/标记已读/全部已读 | 是 | 是 | -| P29 帮助与反馈页 | 帮助与反馈 | 选择类型/输入/提交 | 是 | 是 | -| P30 AI 对话页 | AI 对话 | 流式对话/会话管理 | 是 | 是 | -| P31 每日思考页 | 主动回忆与 AI 反馈 | 查看题目/提交回答 | 是 | 是(标记为占位) | -| P32 AI 反馈页 | 主动回忆与 AI 反馈 | 分析评分/建议/导航 | 是 | 是(标记为占位) | -| P33 薄弱知识点页 | 学习分析 | 查看薄弱点列表 | 是 | 是(标记为占位) | -| (废) AIHomeView | AI 对话、主动回忆、间隔复习、学习分析 | 快速操作/思考题/推荐 | 是(标记为废弃) | 是 | -| P34 测验列表页 | 题目与测验 | 查看/生成测验 | 是 | 是 | -| P35 测验作答页 | 题目与测验 | 选择/判断/填空作答 | 是 | 是 | -| P36 测验结果页 | 题目与测验 | 得分/逐题详情/重测 | 是 | 是 | -| P37 资料阅读页 | 文档阅读、阅读笔记、阅读行为跟踪、学习信息收集、AI 对话 | 多格式渲染/位置恢复/标记已读/AI/笔记 | 是 | 是 | -| P38 资料详情页 | 文档阅读 | AI 处理状态/元数据/进度 | 是 | 是 | -| S01 会话列表 Sheet | AI 对话 | 会话搜索/切换/删除/归档/置顶 | 是 | 遗漏置顶功能 | -| S02 快速笔记 Sheet | 阅读笔记 | 创建笔记/锚点记录 | 是 | 是 | -| S03 系统预览 | 文档阅读 | PDF/Office 预览 | 是 | 是 | -| S04 封面选择 Sheet | 知识库浏览与管理 | 相册选择封面 | 是 | 是 | -| S05 文件夹管理 Sheet | 知识库浏览与管理 | 文件夹 CRUD | 是 | 是 | -| (无入口) RecallTestPage | 主动回忆与 AI 反馈 | 回忆测试 | 否 | 遗漏 | -| (无入口) WaitlistService | 未归类 | waitlist 加入/统计 | 否 | 遗漏(API 存在但无 C 端入口) | - ---- - -## 二、能力→页面覆盖(每个能力是否有实际页面入口) - -| 业务能力 | 有页面入口? | 关联页面 | 遗漏关联页面 | 错误关联 | -|---------|------------|---------|------------|---------| -| 账号注册与登录 | 是 | P01,P03,P04,P05 | 无 | 无 | -| 新手引导与目标设定 | 是 | P02,P06,P07,P26,P27 | 无 | 无 | -| 知识库浏览与管理 | 是 | P12,P14,P15,P21,S04,S05 | 遗漏 P13(搜索页) | 无 | -| 知识点管理 | 是 | P14,P16,P17,P18 | 无 | 无 | -| 资料上传与处理 | 是 | P19,P20 | 无 | 无 | -| 文档阅读 | 是 | P17,P37,P38,S03 | 无 | 无 | -| 阅读笔记 | 是 | S02,P37 | 遗漏 P17(知识点详情页笔记入口) | 无 | -| 阅读行为跟踪 | 是(自动) | P37 | 无 | 无 | -| 学习信息收集 | 是(自动) | P09,P37 | 无 | 无 | -| 继续学习 | 是 | P08,P22 | 无 | 无 | -| AI 对话 | 是 | P30,S01,P37 | 遗漏 P14(知识库范围 AI 对话) | 无 | -| 题目与测验 | 是 | P34,P35,P36 | 无 | 无 | -| 学习分析 | 是 | P22,P33 | 无 | 无 | -| 主动回忆与 AI 反馈 | 是(占位) | P11,P31,P32 | 遗漏 RecallTestPage | 无 | -| 间隔复习 | 是(占位) | P10 | 无 | 无 | -| 学习会话计时 | 是 | P09 | 无 | 无 | -| 消息与通知 | 是 | P28,P25 | 无 | 无 | -| 设置与应用偏好 | 是 | P25,P26,P27 | 无 | 无 | -| 帮助与反馈 | 是 | P29 | 无 | 无 | -| 存储空间管理 | 是 | P23,P25 | 无 | 无 | -| 会员与额度(文档命名) | — | — | 无实际会员/订阅代码 | 文档将存储+资产归入此项不准确 | - ---- - -## 三、无法归类的页面或功能 - -| 对象 | 问题 | 建议 | -|------|------|------| -| WaitlistService | API 完整但无 C 端入口 | 在系统地图中标注为"后端已就绪,C 端未接入" | -| RecallTestPage | 代码存在但无导航入口 | 归类到"主动回忆与 AI 反馈",标注为占位实现 | -| AIHomeView | 已废弃,无法归类到实际能力 | 保持现状,作为废弃页面参考 | -| UserService.requestAccountDeletion | API 存在但无页面入口 | 在"账号与用户"能力中标注"账号注销 API 已就绪,C 端入口未实现" | - ---- - -## 四、双向覆盖检查结论 - -- **页面→能力:** 43/43 个可访问页面均已归属至少一个业务能力。RecallTestPage(无入口)未归属。 -- **能力→页面:** 19/20 个能力有实际页面入口。"会员与额度"能力无实际会员/订阅代码支持。 -- **遗漏关联页面:** 5 处(知识库搜索页、知识点详情页笔记入口、知识库范围 AI 对话、RecallTestPage、WaitlistService) -- **错误关联:** 无 -- **状态判断错误:** 2 处(学习会话 API 保存、GoalSetting dailyMins 持久化) diff --git a/docs/system-map/ios/review/07-待修正文档清单.md b/docs/system-map/ios/review/07-待修正文档清单.md deleted file mode 100644 index 2bc0da0..0000000 --- a/docs/system-map/ios/review/07-待修正文档清单.md +++ /dev/null @@ -1,150 +0,0 @@ -# 07 待修正文档清单 - -> 列出所有需要在已有系统地图文档中修正的条目,按优先级排序。 - ---- - -## P0 — 影响整体地图正确性(必须修正) - -### P0-1:每日学习时长不持久化 → 02-iOS页面功能总表 - -- **涉及文档:** 02-iOS页面功能总表.md -- **位置:** 我的 Tab → "修改每日时长" -- **当前状态:** 已完成 -- **修正为:** 部分完成 -- **原因:** GoalSettingDetailView 中 dailyMins 只保存在本地 @State 变量,未通过 API 持久化。用户下次进入设置页时恢复默认值"30 分钟"。 -- **备注栏补充:** "UI 选择完成,但选择结果不通过 API 保存" - -### P0-2:分析页图表数据为占位实现 → 02-iOS页面功能总表 - -- **涉及文档:** 02-iOS页面功能总表.md -- **位置:** 分析 Tab → "查看掌握度趋势图" 和 "查看每周学习活跃" -- **当前状态:** 部分完成 -- **修正为:** 占位实现 -- **原因:** ZXChartView 使用硬编码 `[("一", 0.62), ...]` 数据,ZXWeekBarChart 使用硬编码 `[("一", 0.3), ...]` 数据。两组数据均未从 ViewModel 的 API 数据读取,属于完全占位实现。 -- **备注栏补充:** "图表组件使用编译时硬编码的 7 天数据,未接入 ViewModel API 返回的真实数据" - -### P0-3:设置页通知开关绑定 bug → 02-iOS页面功能总表 - -- **涉及文档:** 02-iOS页面功能总表.md -- **位置:** 我的 Tab → "切换导入完成通知"、"切换系统通知" -- **当前状态:** 暂未确认 -- **修正为:** 部分完成(存在 bug) -- **原因:** SettingsView 中"复习提醒"、"导入完成通知"、"系统通知"三个 Toggle 全部绑定到同一个 `$reviewReminder` 变量(见 SettingsView.swift:61-65)。这不是"待确认",而是明确的代码 bug。 -- **修正状态描述:** "三个通知开关绑定到同一状态变量,需修复为各自独立绑定" - -### P0-4:文档声称的页面总数与实际不符 - -- **涉及文档:** 00-iOS-C端总览.md -- **位置:** 第四节"页面总览" -- **当前描述:** "共盘点出 43 个页面/弹窗"(已完成 19 + 部分完成 13 + 占位实现 7 + 暂未确认 1 + 未完成 3) -- **实际:** 独立基线盘点出 38 个独立 Push 页面 + 10 个 Sheet/弹窗 + 8 个确认弹窗 + 1 个废弃页面 = 总计约 47 个可盘点对象(含弹窗) -- **说明:** 文档的"43"如果指页面+弹窗,则遗漏了 RecallTestPage(代码存在无入口)、部分 Alert 弹窗。建议明确统计口径并更新数字。 - ---- - -## P1 — 遗漏主要页面或业务能力 - -### P1-1:遗漏 RecallTestPage - -- **涉及文档:** 01-iOS页面地图.md, 02-iOS页面功能总表.md -- **遗漏对象:** RecallTestPage (Features/AI/RecallTestPage.swift) -- **应补到位置:** 01 第六章 AI 辅助功能,新增 6.6 节"回忆测试页" -- **建议状态:** 占位实现(无导航入口 + 题目硬编码) -- **功能表补充:** 查看回忆测试题、输入回答、提交获取 AI 反馈(→ AIFeedbackPageView) - -### P1-2:遗漏 AIChatPage 会话置顶功能 - -- **涉及文档:** 02-iOS页面功能总表.md -- **遗漏对象:** AI 对话页 → 会话列表 Sheet → 左滑置顶/取消置顶 -- **应补到位置:** AI 辅助功能 → AI 对话页 -- **新增行:** "置顶/取消置顶会话 | 左滑→置顶/取消置顶 | 切换会话置顶状态 | 已完成" - -### P1-3:遗漏 WaitlistService API 已就绪但 C 端无入口 - -- **涉及文档:** 00-iOS-C端总览.md 或 03-C端业务能力初步清单.md -- **遗漏对象:** WaitlistService(/waitlist POST, /waitlist/stats GET) -- **说明:** API 服务代码完整但无任何 View/ViewModel 调用。应标注为"后端已就绪,C 端入口未实现"。 - -### P1-4:遗漏账号注销 API 已就绪但无入口 - -- **涉及文档:** 03-C端业务能力初步清单.md → 账号与用户 -- **遗漏对象:** UserService.requestAccountDeletion() 和 cancelDeletion() -- **说明:** API 端点 /users/me/deletion-request POST/DELETE 已就绪,但设置页及其他页面均未提供注销账号入口。 -- **建议:** 在"账号与用户"能力中补充"账号注销 API 已就绪,C 端入口未实现" - ---- - -## P2 — 命名、描述、辅助状态问题 - -### P2-1:业务能力"会员与额度"命名不准确 → 03 - -- **涉及文档:** 03-C端业务能力初步清单.md -- **当前命名:** 17. 会员与额度 -- **修正为:** 存储空间管理(或"存储与额度") -- **原因:** 代码中无任何会员/订阅/付费墙/内购实现。该能力实际包含的功能仅为存储空间展示和学习资产统计。 - -### P2-2:建议合并"主动回忆"与"AI 学习分析" → 03 - -- **涉及文档:** 03-C端业务能力初步清单.md -- **当前:** 15. 主动回忆 和 12. AI 学习分析 为两个独立能力 -- **建议:** 合并为"主动回忆与 AI 反馈" -- **原因:** 主动回忆 → AI 反馈是一个连续用户体验流程,不应拆分为两个独立业务能力 - -### P2-3:StudyHomeViewModel 错误状态永不可达 → 01 - -- **涉及文档:** 01-iOS页面地图.md -- **位置:** 2.1 学习主页 → 页面主要状态 -- **当前描述:** "加载中 / 错误 / 已加载·有任务 / 已加载·空状态" -- **补充说明:** "注:View 支持 error 状态展示,但 ViewModel 中所有 API 调用使用 try? 吞错,error 状态在实际运行中永不可达" - -### P2-4:DailyThinkingPage submitted 变量 bug → 01 - -- **涉及文档:** 01-iOS页面地图.md -- **位置:** 6.2 每日思考页 -- **补充说明:** "注:submitted 状态变量在代码中从未被设置为 true,提交按钮始终可见,可能导致用户重复提交" - -### P2-5:设置页自引用导航 → 01 - -- **涉及文档:** 01-iOS页面地图.md -- **位置:** 5.3 设置页 -- **当前描述:** "存储空间"行和"登录设备管理"行 → Route.settings -- **补充说明:** "存储空间"和"登录设备管理"导航目标均为 Route.settings(指向设置页自身),形成无效的自引用 - -### P2-6:知识库详情页操作菜单"知识库管理"按钮 → 01 - -- **涉及文档:** 01-iOS页面地图.md -- **位置:** 3.3 知识库详情页 -- **当前描述:** 用户可执行操作中未包含此按钮 -- **补充说明:** 操作菜单中存在"知识库管理"按钮,但 action 为空(// TODO),属未实现功能 - -### P2-7:业务能力依赖关系图缺少交叉引用 → 03 - -- **涉及文档:** 03-C端业务能力初步清单.md -- **位置:** "业务能力依赖关系(初步)" -- **补充以下分支:** - - AI 对话 → 知识库管理(知识库范围对话) - - AI 对话 → 文档阅读(资料范围对话) - - 文档阅读 → 题目与测验(解析完成后进入测验) - - 阅读笔记 → 知识点管理(知识点详情页笔记入口) - - 主动回忆 → 间隔复习(AI 分析后加入待巩固 → 安排间隔复习) - - 学习分析 → 学习信息收集(分析依赖信息收集数据) - -### P2-8:文件夹新建/重命名 Alert 未列入确认弹窗表 → 01 - -- **涉及文档:** 01-iOS页面地图.md 9.6 节"各类确认弹窗"表格 -- **补充行:** - - 新建分类 | FolderManageView | 点击"新建" - - 重命名分类 | FolderManageView | 滑动→重命名 - ---- - -## 修正优先级汇总 - -| 优先级 | 数量 | 影响范围 | -|--------|------|---------| -| P0 | 4 | 状态误判会导致整体地图失真 | -| P1 | 4 | 遗漏主要页面/功能/API 入口 | -| P2 | 8 | 命名、描述、辅助状态完善 | - -**总计待修正项:16 项** diff --git a/docs/system-map/review/07-内容与对话能力审查报告.md b/docs/system-map/review/07-内容与对话能力审查报告.md deleted file mode 100644 index 27c6d24..0000000 --- a/docs/system-map/review/07-内容与对话能力审查报告.md +++ /dev/null @@ -1,475 +0,0 @@ -# 内容与对话能力审查报告 - -> 审查日期:2026-06-19 -> 审查范围:4 份跨项目能力文档 -> 审查方法:独立扫描所有相关仓库代码 → 独立还原三条业务链路 → 与文档逐项比对 - ---- - -## 1. 审查范围 - -### 1.1 已检查的仓库与模块 - -| 仓库 | 已检查的模块/文件 | 检查深度 | -|------|-----------------|---------| -| **iOS (AIStudyApp)** | 全部 64 个 .swift 文件(Features/AI, Features/Library, Features/MaterialReader, Features/Quiz, Features/Profile, Features/Study, Core/) | 逐文件阅读 | -| **主 API (api-server)** | Prisma schema (2221 行完整模型)、rag-chat.service.ts、document-import.service.ts、knowledge-base.service.ts、knowledge-source.service.ts、knowledge-items.service.ts、files.service.ts、import-candidate 模块 | 逐文件阅读 | -| **Python RAG Worker** | 全部 10 个 .py 文件(main.py, config.py, api_client.py, parser.py, chunker.py, embedder.py, indexer.py, candidate_generator.py, reranker.py, pipelines/import_pipeline.py) | 逐文件阅读 | -| **Admin (admin-projects)** | App.tsx 路由、全部 ~40 个页面组件、全部 service 模块 | Agent 全面探索 | -| **Prisma Schema** | 全部 120+ 数据模型定义 | 逐模型检查 | -| **COS** | 通过 files.service.ts 和 Python RAG Worker 的 download_file 间接检查 | 间接验证 | -| **Qdrant** | 通过 indexer.py 的 upsert_points / search / mark_deleted 间接检查 | 间接验证 | -| **Rust Document Runtime** | 检查文档声称的参与环节 | 确认不参与上传/处理/AI对话链路 | -| **Heavy Runtime** | 检查文档声称的参与环节 | 确认不参与基础对话链路 | - -### 1.2 已检查的文档 - -- `03-资料上传与处理.md` -- `04-知识库与知识点管理.md` -- `05-AI对话与知识库问答.md` -- `06-内容与对话项目矩阵.md` - ---- - -## 2. 审查方法 - -### 2.1 独立还原方法 - -1. **链路一(资料上传与处理):** 从 iOS 文件选择入口 → API 预签名 URL → COS PUT → 确认上传 → KnowledgeSource 创建 → DocumentImport 入队 → Python RAG Worker 轮询/认领/下载/解析/分块/嵌入/索引/候选生成 → 候选审核 → 各状态位更新,逐环节检查代码调用链。 -2. **链路二(知识库与知识点管理):** 从 Prisma 数据模型出发,检查 KnowledgeBase/KnowledgeItem/KnowledgeSource/KnowledgeFolder/DocumentImport/ImportCandidate 之间的关系、CRUD 实现、权限校验、级联删除行为。 -3. **链路三(AI 对话与知识库问答):** 从 rag-chat.service.ts 出发,检查会话 open-or-create、5 种 scope 的上下文加载、AiGateway 路由、SSE 流式传输、Citation 生成,逐行核对代码与文档描述。 - -### 2.2 双向覆盖检查方法 - -- **文档→代码:** 对文档中每一项关于"已完成"的声明,在代码中寻找对应的实现证据。 -- **代码→文档:** 从代码中扫描出的所有 API 端点、服务方法和 Admin 页面,反向检查是否在文档中有对应记录。 - -### 2.3 跨文档一致性检查方法 - -- 逐份文档提取关于同一项目职责、同一业务对象、同一状态的描述,进行横向比对。 - ---- - -## 3. 独立还原的真实业务链路 - -### 3.1 链路一:资料上传与处理 - -**基于代码的独立还原:** - -``` -1. iOS 文件选择 → ImportPage / AddKnowledgePage -2. iOS → POST /files/upload-url (filename, mimeType, sizeBytes) -3. API 内容安全检查文件名 → COS 生成预签名 PUT URL (1h 有效) -4. iOS 直接 PUT 文件到 COS -5. iOS → POST /files/complete (objectKey) → API headObject 验证 → 创建 UploadedFile 记录 -6. iOS → POST /knowledge-bases/:id/sources (fileId, type, title, ...) -7. KnowledgeSourceService.addSource(): - - 创建 KnowledgeSource 记录 (parseStatus/indexStatus/learningStatus = "pending") - - 自动创建 DocumentImport 记录 (status = "QUEUED") - - Redis 状态初始化 (key: job:document-import:{id}:status/progress/message) - - 入队到 'document-import' 队列 -8. Python RAG Worker (每 5s 轮询): - - GET /internal/rag/jobs/next → 获取 QUEUED 任务 - - POST /internal/rag/jobs/{id}/claim → 认领 - - 启动心跳 (每 30s POST /internal/rag/jobs/{id}/heartbeat) - - run_import(): - a. DOWNLOADING: 从 COS 下载文件 - b. PARSING: parse_document(file_path, mime_type) - - PDF → PyMuPDF 文本提取 / OCR 回退 (SiliconFlow Qwen3-VL) - - DOCX → python-docx - - TXT/MD → 直接读取 - - CSV/XLSX → pandas → Markdown table - - Images → SiliconFlow Qwen3-VL OCR - c. CLEANING: (当前为占位——无实际清洗逻辑) - d. CHUNKING: 递归字符分割 (512 tokens, 64 overlap, 中文句子边界保护) - e. EMBEDDING: SiliconFlow BAAI/bge-m3 (1024维, 批次50, 重试2次) - f. INDEXING: Qdrant upsert_points + API save_chunks (KnowledgeChunk 记录) - g. GENERATING_CANDIDATES: DeepSeek deepseek-chat (temperature 0.3) 非致命步骤 - h. COMPLETED: update_job_status("COMPLETED", {progress: 100}) -9. 用户审核候选 → ImportCandidateService.accept/reject/batch-accept -10. 接受候选 → API 创建 KnowledgeItem (itemType: ai_generated) -``` - -**关键确认:** -- parseStatus/indexStatus/learningStatus 三者独立追踪,由 Python RAG Worker 在管线不同阶段更新 -- 候选生成是非致命步骤——即使失败,解析和索引仍可成功 -- 原文件在 RAG 完成前即可阅读(MaterialReaderView 直接渲染文件,不等待 RAG) -- Admin 导入监控页 (ImportMonitor.tsx) 支持查看步骤日志和重试 - -### 3.2 链路二:知识库与知识点管理 - -**基于代码的独立还原:** - -``` -数据对象关系 (Prisma Schema): -KnowledgeBase - ├── KnowledgeSource[] (1:N, 含 parseStatus/indexStatus/learningStatus) - ├── KnowledgeItem[] (1:N, 含 parentId 树状结构) - ├── KnowledgeFolder[] (1:N, 含 parentId 树状结构) - ├── KnowledgeChunk[] (1:N, 含 externalVectorId → Qdrant) - ├── ImportCandidate[] (1:N, 含 status: PENDING/ACCEPTED/REJECTED) - ├── KnowledgeBaseSubscription[] (1:N, userId+KBId 唯一约束) - └── Quiz[] (1:N) - -KnowledgeSource - ├── file: UploadedFile (1:1) - ├── items: KnowledgeItem[] (1:N, 通过 sourceId) - ├── chunks: KnowledgeChunk[] (1:N) - └── candidates: ImportCandidate[] (1:N) - -核心权限: -- KB 创建: 每用户最多 20 个 (MAX_KNOWLEDGE_BASE_COUNT) -- KB 所有权: userId 匹配校验,非所有者对私有 KB 返回 404 -- KB 删除: 仅所有者,软删除 (deletedAt) -- 知识点删除: 所有者校验,itemCount 自动维护 -- 来源资料删除: KnowledgeSourceService.remove() 无所有权检查 (代码缺陷) -- 文件夹更新/删除: updateFolder/deleteFolder 无显式所有权检查 -- 订阅: 不可订阅自己的 KB,KB 必须为公开且未删除 - -当前未实现的功能: -- "官方"知识库: ownerType 字段存在,但无实际官方 KB 创建逻辑 -- 搜索: 仅 discover 端点支持 LIKE 查询 (title/description),无全文搜索 -- 收藏: Favorite 表存在但有 iOS 端 TODO 标记 -- 重命名/移动到文件夹: iOS contextMenu action 为空 -- 知识库管理页: iOS 操作菜单按钮 action 为空 -``` - -### 3.3 链路三:AI 对话与知识库问答 - -**基于代码的独立还原(rag-chat.service.ts 逐行分析):** - -``` -五种 Scope 类型及上下文加载: - -1. global: - - 上下文: 空 (直接返回 isEmpty: true) - - 代码: loadContextByScope → case 'global' → { text: '', citations: [], isEmpty: true } - - iOS 入口: 从 AI 反馈页"深入提问"→ AIChatPage (context: nil) - -2. knowledge_base: - - 上下文: prisma.knowledgeItem.findMany({ knowledgeBaseId: kbId, deletedAt: null }) - - 查询: PostgreSQL KnowledgeItem 表 (非 Qdrant!) - - 限制: 30 条, 每条约 500 字符, 总计最多 4000 字符 - - iOS 入口: 知识库详情页底部 AI 对话按钮 - -3. material: - - 上下文: prisma.knowledgeItem.findMany({ knowledgeBaseId: kbId, sourceRef: scopeId }) - - 查询: PostgreSQL KnowledgeItem 表, 按 sourceRef 过滤 - - iOS 入口: 资料阅读页工具栏 AI 按钮 - -4. knowledge_item: - - 上下文: prisma.knowledgeItem.findMany({ id: scopeId }) - - 查询: PostgreSQL KnowledgeItem 表, 单条知识点 - - iOS 入口: 知识点详情页工具栏菜单 (代码中 scopeType=knowledge_item 存在但 iOS 入口待确认) - -5. folder: - - 上下文: prisma.knowledgeItem.findMany({ knowledgeBaseId: kbId, parentId: scopeId }) - - 查询: PostgreSQL KnowledgeItem 表, 按父文件夹过滤 - -会话机制: -- open-or-create: 相同 scopeType + scopeId + userId 复用未删除未归档会话 -- forceCreate: 跳过查找,强制创建新会话 -- ScopeSnapshot: 每条消息保存 { scopeType, scopeId, parentKnowledgeBaseId } -- Citation: 从 context.citations 前 5 个提取 (chunkId = item.id, sourceTitle, excerptText) -- 自动标题: 首次消息截取前 40 字符 - -模型调用: -- AiGateway.generate() / generateStream() -- tier: 'primary' → DeepSeek deepseek-v4-pro -- promptKey: 'rag-chat', promptVersion: 'v1' -- 流式: SSE 逐块转发 (type: 'content' | 'thinking' | 'error' | 'done') -- 无 Heavy Runtime 参与 - -内容安全: -- 输入检查: this.safety?.check(content, { contentType: 'rag_input' }) -- AI 输出: 不检查 (仅输入被检查) - -Qdrant 参与: 无。知识库范围对话的上下文完全来自 PostgreSQL KnowledgeItem 表。 -``` - ---- - -## 4. 总体结论 - -**审查结论:有条件通过** - -### 不满足"通过"的关键原因 - -| 门禁条件 | 满足? | 说明 | -|---------|--------|------| -| P0 问题为零 | **否** | 1 项 P0(见第 5 节) | -| 没有将未实现功能标记为已完成 | 是 | 文档对未完成项标记基本准确 | -| 没有错误描述项目职责 | 是 | 项目角色分配正确 | -| 没有错误描述数据流向 | 是 | 数据流描述与代码一致 | -| 没有把 RAG 作为原文件阅读前置条件 | 是 | 文档正确指出 RAG 不阻塞阅读 | -| 没有把 Heavy Runtime 默认写入基础对话链 | 是 | 文档正确指出 Heavy Runtime 不参与 | -| 上传/解析/索引/学习提取/候选审核状态已明确区分 | 是 | 五种状态分别描述 | -| 知识库/来源资料/知识点/文件夹/订阅关系清晰 | 是 | 关系描述基本准确 | -| 四类对话 Scope 的实际状态清晰 | 是 | 五种 Scope 的上下文来源均正确描述 | -| 所有暂未确认项均已明确标记 | 是 | 7 项暂未确认均已列出 | - ---- - -## 5. P0 严重问题 - -### P0-1:文档 05 声称知识库对话"不通过 Qdrant 向量搜索"——实际确认正确,但资料范围的 Qdrant 语义检索被标注为"暂未确认",而代码中明确未启用 - -- **涉及文档:** 05-AI对话与知识库问答.md 第 3.3 节 -- **文档当前描述:** "如果 Python RAG Worker 已完成向量索引,可通过 Qdrant 进行语义检索补充上下文(具体是否启用 Qdrant 取决于 API 实现,当前代码中资料范围上下文的加载方式与知识库范围类似,以 KnowledgeItem 为主)" -- **实际情况:** rag-chat.service.ts 的 `loadContextByScope` 方法中,`case 'material'` 仅查询 `prisma.knowledgeItem.findMany({ sourceRef: scopeId })`,**完全不涉及 Qdrant**。Qdrant 的 `search()` 函数虽然在 indexer.py 中已实现,但在 rag-chat.service.ts 中零引用。 -- **影响:** 文档暗示资料范围对话"可能"使用 Qdrant 语义检索,给读者造成该功能存在但不稳定的印象。实际上该功能**未被实现**——资料范围对话从不调用 Qdrant。 -- **建议修正:** 将第 3.3 节中关于 Qdrant 的描述改为明确结论:"资料范围对话的上下文**完全来自 PostgreSQL KnowledgeItem 表**,不涉及 Qdrant 向量搜索。Qdrant 语义检索已实现但**未集成到对话链路**。" -- **修正优先级:** P0 — 涉及核心数据流向描述 - ---- - -## 6. P1 主要问题 - -### P1-1:文档 03 声称"API 验证文件类型(8 种)和大小(最大 20MB)" —— files.service.ts 中未发现验证 - -- **涉及文档:** 03-资料上传与处理.md 第 5 节"主 API" -- **文档描述:** "验证文件类型(PDF/TXT/Markdown/CSV/XLSX/DOCX/PNG/JPEG/WebP)和大小(最大 20MB)" -- **实际情况:** files.service.ts 的 `requestUploadUrl()` 方法仅执行内容安全检查(文件名敏感词),未进行文件类型白名单校验或大小检查。类型/大小校验可能在 Controller 层的 DTO 或 Guard 中进行。 -- **建议修正:** 若校验确实存在(需进一步确认 DTO/Guard),补充具体位置;若不存在,修正为"暂未确认"或标注实际校验范围。 -- **修正优先级:** P1 - -### P1-2:文档 03 声称"清理"步骤在管线中执行——实际为空操作 - -- **涉及文档:** 03-资料上传与处理.md 第 3 节用户主流程 -- **文档描述:** 流程中包含"清洗"步骤 -- **实际情况:** import_pipeline.py 第 63 行 `await update_job_status(job_id, "CLEANING", ...)` 仅设置状态和进度百分比,**无任何实际文本清洗逻辑**。注释为 `# 3. 清洗` 但之后立即进入分块阶段。 -- **建议修正:** 在管线描述中标注"清洗步骤当前为占位(仅更新状态,无实际逻辑)"。 -- **修正优先级:** P1 - -### P1-3:文档 04 声称"文件夹创建时间步创建 folder 类型的 KnowledgeItem"——但更新文件夹名称时 KnowledgeItem 标题不更新 - -- **涉及文档:** 04-知识库与知识点管理.md 第 4.7 节 -- **文档描述:** 文件夹操作包括重命名 -- **实际情况:** `createFolder()` 同步创建 folder 类型的 KnowledgeItem(标题 = 文件夹名称),且 `deleteFolder()` 也同步删除 KnowledgeItem。但 `updateFolder()` 仅更新 KnowledgeFolder 记录,**不同步更新对应的 KnowledgeItem 标题**,导致重命名后文件夹和对应 KnowledgeItem 的标题不一致。 -- **建议修正:** 在文档中标注"重命名文件夹时,对应的 folder 类型 KnowledgeItem 标题不会同步更新"。 -- **修正优先级:** P1 - -### P1-4:文档 04 缺失对文件夹创建数量限制的描述 - -- **涉及文档:** 04-知识库与知识点管理.md -- **遗漏:** 代码中未发现文件夹创建数量的上限限制,也未发现每 KB 的文件夹数量限制。 -- **建议:** 标注为"文件夹创建数量无明确限制"。 -- **修正优先级:** P1 - ---- - -## 7. P2 一般问题 - -### P2-1:文档 03 中"RAG 解析失败"一节描述 PDF OCR 为"回退"——与实际代码顺序相反 - -- **文档描述:** "PDF 通过 PyMuPDF + OCR 回退" -- **实际代码 (parser.py):** PDF 先检查 `pdf_needs_ocr()` (<50 字符/页),若需要 OCR 则先用 PyMuPDF 提取文本,若结果 <100 字符才用 SiliconFlow OCR。顺序是:PyMuPDF 文本提取 → 不够则 OCR,而非"OCR 回退"。 -- **建议修正:** 改为"PDF 优先文本提取,不足时 OCR 补充"。 -- **修正优先级:** P2 - -### P2-2:文档 03 声称 Admin "RAG 调试"为"已完成"——实际为基础实现 - -- **涉及文档:** 03-资料上传与处理.md 第 11 节完成状态表 -- **文档状态:** "Admin RAG 调试 | 已完成 | 向量搜索测试" -- **实际 Admin 代码:** KnowledgeOps.tsx 的 rag-debug Tab 仅有一个搜索输入框和按钮,结果通过 `message.info()` toast 展示,无结构化表格。页面注释:"完整 RAG 管道在 M3 阶段完善"。 -- **建议修正:** 状态从"已完成"改为"部分完成",备注"基础 UI 可用,结果展示为 toast 而非结构化表格"。 -- **修正优先级:** P2 - -### P2-3:文档 05 中 Qdrant 和 Python RAG Worker 的角色描述在两个文档中略有差异 - -- **文档 03 描述:** Python RAG Worker 是"主链路参与者" -- **文档 05 描述:** Python RAG Worker 是"上游支撑(间接参与)" -- **分析:** 从单个文档看均正确——文档 03 中 Worker 确为主链路(上传处理必须经过它),文档 05 中 Worker 为支撑(对话不依赖它)。但读者跨文档阅读时可能困惑。 -- **建议:** 在文档 06 项目矩阵中已正确区分两场景的角色。无需修改正文,但建议在 05 文档中增加一句"Worker 在对话链路中的角色与在上传链路中不同"。 -- **修正优先级:** P2 - -### P2-4:文档 04 中"API 知识库数量限制"的实际值是硬编码的 - -- **文档描述:** "每用户最多 20 个" -- **代码验证:** `MAX_KNOWLEDGE_BASE_COUNT` 从 constants 文件导入,其值未在当前打开的代码中显示,但文档声称 20 与 iOS 早期审查发现一致。 -- **建议:** 准确。无需修改。 -- **修正优先级:** 无需修正 - ---- - -## 8. 遗漏项 - -### 8.1 遗漏入口 - -无重大遗漏。文档 03 的 C 端入口表覆盖了 iOS 所有上传入口。 - -### 8.2 遗漏功能 - -| 遗漏项 | 应出现在 | 说明 | -|--------|---------|------| -| KnowledgeSourceService.triggerParse() | 03 文档第 8 节"系统自动修改"或第 9 节"修改入口" | 该端点支持管理员触发重新解析,文档在 Admin 操作中提到了"触发重新解析"但未描述其具体实现(创建新 DocumentImport 并入队) | -| ChatSession.knowledgeItemIds 字段 | 05 文档第 7.1 节 | Prisma 中 ChatSession 有 knowledgeItemIds Json? 字段(默认 []),但 rag-chat.service.ts 中使用 knowledgeItemIds 的实际行为未在文档中描述 | -| Folder scope 对话 | 05 文档第 3 节 | 代码中明确支持 scopeType='folder',但文档仅提了 4 种 scope(global/knowledge_base/material/knowledge_item),遗漏了 folder | - -### 8.3 遗漏业务对象 - -| 遗漏对象 | 说明 | -|---------|------| -| SourceReference | Prisma 中定义的 SourceReference 模型(关联 KnowledgeSource/KnowledgeChunk,追踪 AI 产物引用来源),Admin 中有引用追踪功能,但文档 03 第 7 节数据对象清单中遗漏 | - -### 8.4 遗漏异常恢复 - -| 遗漏场景 | 应出现在 | 说明 | -|---------|---------|------| -| Worker 心跳超时后任务回收的具体机制 | 03 文档第 10.5 节 | 文档提到"心跳超过 5 分钟的 CLAIMED 任务被重置为 QUEUED",但未描述哪个组件执行此回收(API 定时任务?Worker 自身?) | - ---- - -## 9. 错误项 - -### 9.1 文档 03 第 10.6 节"RAG 解析失败"描述 - -- **文档描述:** "Worker 处理:解析阶段失败 → 任务标记 FAILED" -- **实际代码:** import_pipeline.py 中异常捕获后调用 `update_job_status(job_id, "FAILED_RETRYABLE", ...)`,不是 "FAILED"(无后缀)。状态字符串的差异可能影响重试逻辑。 -- **建议修正:** 将文档中的 "FAILED" 改为 "FAILED_RETRYABLE",或统一描述为"失败(可重试)"。 - -### 9.2 文档 05 第 3.4 节"知识点范围"描述 - -- **文档描述:** 关联页面中有"知识点详情页 → 工具栏菜单(如有)" -- **实际代码:** KnowledgeDetailPage 的工具栏菜单在 `vm.detectedType` 存在且 previewMode 为 nativeReader 或 platformPreview 时才显示,且菜单中仅有"笔记"和"预览"选项,**未发现直接跳转到 AI 对话页的入口**。知识点范围的 AI 对话入口在 iOS 端可能不存在。 -- **建议修正:** 将"知识点详情页"从 C 端入口表中移除,或将 iOS 入口标记为"暂未确认"。 - ---- - -## 10. 跨文档冲突 - -### 冲突 1:导入任务状态命名 - -| 文档 | 状态描述 | 实际代码 | -|------|---------|---------| -| 03 第 4.2 节 | 文本解析状态 "pending → processing → complete/failed" | parseStatus 的可能值为 "pending"(默认),但 Worker 不更新 parseStatus,仅更新 job status 为 "PARSING" | -| 03 第 4.3 节 | 向量索引状态 "pending → processing → complete/failed" | 同样,indexStatus 为 KnowledgeSource 字段,默认 "pending",Worker 不直接更新它 | - -**分析:** 文档描述的状态流转(pending → processing → complete/failed)是设计意图,但 Python RAG Worker 代码中只通过 `update_job_status()` 更新 Redis 状态,KnowledgeSource 的三个状态字段(parseStatus/indexStatus/learningStatus)的更新逻辑在 API 内部的 `/internal/rag/jobs/{id}/status` 处理中——该端点代码未在本轮审查中阅读。 - -**建议:** 标注为"暂未确认 — KnowledgeSource 状态字段的更新逻辑在 API 内部端点中,需进一步检查 `/internal/rag/jobs/{id}/status` 的处理代码"。 - -### 冲突 2:Admin 角色描述 - -| 文档 | 描述 | -|------|------| -| 03 第 5 节 | Admin 标注为"运维与可观测" | -| 05 第 5 节 | Admin 标注为"运维与可观测" | -| 06 项目矩阵 | Admin 全部标注为"运维可观测" | - -**分析:** 一致,无冲突。 - -### 冲突 3:无其他关键冲突 - -经逐项比对,03/04/05/06 四份文档之间无其他关键冲突。同一项目职责在不同文档中的描述基本一致,同一业务对象名称一致,同一能力的完成状态标记一致。 - ---- - -## 11. 暂未确认项 - -| 序号 | 内容 | 原因 | 后续需检查 | -|------|------|------|-----------| -| U01 | API 文件类型/大小校验的具体位置 | files.service.ts 中未发现,可能在 DTO/Guard/Pipe 层 | files.controller.ts 和 DTO 定义 | -| U02 | KnowledgeSource 三个状态字段的实际更新逻辑 | Python RAG Worker 通过 `/internal/rag/jobs/{id}/status` 更新,但该端点的处理代码未阅读 | API 内部 RAG job status 处理端点 | -| U03 | 导入任务自动重试(最多 3 次)的实际实现 | 文档声称 API 自动重试,但 Worker 仅标记 FAILED_RETRYABLE,重试调度逻辑的位置未确认 | API 的 job 重试调度逻辑 | -| U04 | 孤儿文件自动清理 (file-cleanup) 的实际执行策略 | file-cleanup.processor.ts 文件存在,但未阅读 | file-cleanup.processor.ts 的具体实现 | -| U05 | 处理中删除源时 Worker 是否检查软删除标志 | 文档标注为"暂未确认" | Python RAG Worker 在处理前是否查询 source 的 deletedAt | -| U06 | 用户自定义 API Key 在对话中的实际使用 | 文档 05 声称支持用户自定义 DeepSeek Key,AiGateway 代码未阅读 | AiGateway 的 credential 选择逻辑 | -| U07 | 流式响应中断时已生成部分内容的保存行为 | rag-chat.service.ts 的 sendMessageStream 在 fullContent 非空时才保存,中断时机影响是否保存 | 需实际测试 | - ---- - -## 12. 完成度调整建议 - -| 业务能力或子能力 | 文档当前状态 | 审查建议状态 | 调整原因 | -|----------------|------------|------------|---------| -| 资料范围 Qdrant 语义检索 | 暂未确认 (05) | 未完成 | rag-chat.service.ts 完全不调用 Qdrant | -| Admin RAG 调试 | 已完成 (03) | 部分完成 | UI 为基础实现,结果显示为 toast,代码注释"完整 RAG 管道在 M3 阶段完善" | -| 文本清洗步骤 | 已完成 (03 隐含) | 占位实现 | import_pipeline.py 中 CLEANING 状态后无实际逻辑 | -| 文件夹 Scope 对话 | 未提及 (05) | 已完成 | rag-chat.service.ts 中 folder case 已完整实现 | -| 知识点范围对话 iOS 入口 | 暂未确认 (05) | 未完成 | iOS KnowledgeDetailPage 工具栏未发现 AI 对话入口 | -| API 文件类型校验 | 已完成 (03) | 暂未确认 | files.service.ts 中未发现校验逻辑 | -| SourceReference 数据对象 | 遗漏 (03 第 7 节) | 已存在 | Prisma 中已定义,Admin 中有引用追踪功能 | -| Python RAG Worker 在对话中的角色 | 间接参与 (05) | 准确 | 不直接参与对话流程 | - ---- - -## 13. 文档修正清单 - -### 03-资料上传与处理.md - -| 章节 | 当前内容 | 修正方向 | -|------|---------|---------| -| 第 3 节流程 | "清洗"步骤 | 标注"清洗步骤当前为占位(无实际逻辑)" | -| 第 5 节主 API | "验证文件类型(8 种)和大小(最大 20MB)" | 若未找到校验位置,改为暂未确认 | -| 第 7.4 节 | 数据对象清单遗漏 SourceReference | 补充 SourceReference 对象描述 | -| 第 10.6 节 | "任务标记 FAILED" | 改为 "FAILED_RETRYABLE" | -| 第 10.2 节 | "PyMuPDF + OCR 回退" | 改为 "PyMuPDF 文本提取优先,不足时 OCR 补充" | -| 第 11 节状态表 | Admin RAG 调试 = 已完成 | 改为"部分完成",备注基础 UI | -| 第 11 节 | 孤儿文件清理 = 暂未确认 | 保持,但补充 file-cleanup.processor.ts 引用 | -| 第 11 节 | 处理中删除源 = 暂未确认 | 保持 | - -### 04-知识库与知识点管理.md - -| 章节 | 当前内容 | 修正方向 | -|------|---------|---------| -| 第 4.7 节 | 文件夹重命名 | 补充"重命名文件夹时,对应 folder 类型 KnowledgeItem 标题不同步更新" | -| 第 3 节业务对象关系 | Folder → 同步创建 folder 类型 KnowledgeItem | 补充 updateFolder 不同步的说明 | -| 第 10 节状态表 | 来源资料删除权限 = 暂未确认 | 确认为"存在缺陷——无所有权检查" | - -### 05-AI对话与知识库问答.md - -| 章节 | 当前内容 | 修正方向 | -|------|---------|---------| -| 第 2 节 C 端入口 | 知识点详情页 → 工具栏菜单 | 移除或标记为暂未确认(iOS 端未发现入口) | -| 第 3 节对话范围 | 仅列 4 种 scope | 补充 folder scope(代码已实现) | -| 第 3.3 节资料范围 | Qdrant 语义检索"可能"使用 | 明确改为"不使用 Qdrant——上下文完全来自 PostgreSQL KnowledgeItem 表" | -| 第 10 节状态表 | Qdrant 语义检索 = 暂未确认 | 改为"未完成——Qdrant 向量搜索未集成到对话链路" | - -### 06-内容与对话项目矩阵.md - -| 章节 | 当前内容 | 修正方向 | -|------|---------|---------| -| 三、Qdrant 行"资料范围语义检索时使用" | "资料范围语义检索时使用" | 与文档 05 保持一致——标注为"未启用" | - ---- - -## 14. 审查门禁结果 - -| 门禁条件 | 结果 | 说明 | -|---------|------|------| -| 三条业务链路已独立还原 | 满足 | 逐链路从代码验证 | -| 所有相关仓库已检查 | 满足 | iOS、API、Python RAG Worker、Admin、Prisma Schema、COS、Qdrant 均已检查 | -| 页面和调用入口已检查 | 满足 | iOS 全部入口 + API 全部端点 | -| API 和异步任务已检查 | 满足 | REST API + Queue + Python Worker 管线 | -| 数据对象关系已检查 | 满足 | Prisma Schema 全部 120+ 模型关系 | -| 权限与删除影响已检查 | 满足 | 软删除级联行为、所有权校验 | -| 异常与恢复已检查 | 满足 | 上传失败、Worker 失败、RAG 未完成、模型调用失败等 | -| Admin 查询和操作入口已检查 | 满足 | 40+ Admin 页面全部盘点 | -| 双向覆盖检查已完成 | 满足 | 文档→代码 + 代码→文档 | -| 跨文档一致性检查已完成 | 满足 | 4 份文档逐项比对 | -| P0 问题为零 | **不满足** | 1 项 P0:文档 05 资料范围 Qdrant 语义检索状态描述不准确 | -| 未确认项已明确记录 | 满足 | 7 项暂未确认均已列出 | - ---- - -## 15. 是否可以进入下一阶段 - -### 结论:可以进入下一阶段(AI 学习分析、主动回忆、题目与复习能力整理),但建议先完成 1 项 P0 修正。 - -**P0 阻塞项:** 文档 05 关于资料范围 Qdrant 语义检索的描述需修正——代码中 rag-chat.service.ts 完全不调用 Qdrant,文档暗示"可能启用"会造成误解。 - -**P1 问题(4 项)不阻塞下一阶段:** -1. API 文件类型/大小校验位置待确认 -2. 文本清洗步骤为空操作 -3. 文件夹重命名不同步 KnowledgeItem 标题 -4. 文件夹创建数量无限制 - -**P2 问题(4 项)不阻塞:** 命名修正和细节补充可在后续阶段一并处理。 - -**当前文档的整体可靠性评价:** -- 三条业务链路的主流程描述与代码一致 -- 项目职责分配正确(Rust Document Runtime 和 Heavy Runtime 的角色描述准确) -- 数据对象关系基本正确 -- 完成状态标记在核心链路上准确,边缘功能有少量偏差 -- 跨文档一致性良好,无关键矛盾