瀏覽代碼

fix(cfc-web): resolve merge conflict in vue.config.js, keep .vue resolution + node=false

Xiaogang Liao 3 月之前
父節點
當前提交
521448ebc0

+ 1 - 0
cfc-frontend/.npmrc

@@ -1 +1,2 @@
 legacy-peer-deps=true
+engine-strict=false

+ 1 - 1
cfc-frontend/components/ActionArticleRecommend.vue

@@ -18,7 +18,7 @@
       <view
         class="rec-card"
         v-for="(article, idx) in displayArticles"
-        :key="article.id || idx"
+        :key="idx"
         @click="$emit('articleClick', article)"
       >
         <!-- 封面图(可选) -->

+ 2 - 2
cfc-frontend/components/HealthDimensionsSection.vue

@@ -14,8 +14,8 @@
     <view v-else class="dimension-grid">
       <view
         class="dimension-item"
-        v-for="dim in displayDimensions"
-        :key="dim.dimension || dim.key"
+        v-for="(dim, idx) in displayDimensions"
+        :key="idx"
         @click="goDimensionDetail(dim)">
         <text class="dimension-icon">{{ getDimIcon(dim.dimension || dim.key) }}</text>
         <text class="dimension-name">{{ dim.label }}</text>

+ 2 - 2
cfc-frontend/components/RecommendedFeed.vue

@@ -19,8 +19,8 @@
       <template v-else>
         <view
           class="feed-card"
-          v-for="item in displayItems"
-          :key="item.id + '-' + item.type"
+          v-for="(item, idx) in displayItems"
+          :key="idx"
           @click="$emit('itemClick', item)"
         >
           <!-- 商品卡片 -->

+ 5 - 1
cfc-frontend/components/member-detail-sections/mind-section.vue

@@ -42,7 +42,11 @@
       <view class="section-header">
         <text class="section-title">🎯 即将到来的里程碑</text>
       </view>
-      <view class="milestone-item" v-for="ms in milestones" :key="ms.id || ms.title">
+      <view
+        class="milestone-item"
+        v-for="(ms, idx) in milestones"
+        :key="idx"
+      >
         <text class="milestone-title">{{ ms.title }}</text>
         <text class="milestone-date">{{ ms.date }}</text>
       </view>

+ 45 - 0
cfc-frontend/fix-hbuilderx-scoped.js

@@ -0,0 +1,45 @@
+#!/usr/bin/env node
+
+const fs = require('fs');
+const path = require('path');
+
+// HBuilderX 内部的 scoped.js 路径
+const SCOPED_FILE = '/Applications/HBuilderX.app/Contents/HBuilderX/plugins/uniapp-cli/node_modules/@dcloudio/vue-cli-plugin-uni/packages/@vue/component-compiler-utils/dist/stylePlugins/scoped.js';
+
+if (!fs.existsSync(SCOPED_FILE)) {
+  console.error('错误:找不到 HBuilderX 的 scoped.js 文件');
+  console.error(`路径:${SCOPED_FILE}`);
+  process.exit(1);
+}
+
+console.log(`正在修复 ${SCOPED_FILE}...`);
+
+let content = fs.readFileSync(SCOPED_FILE, 'utf8');
+
+// 修复 1: n.spaces.before = n.spaces.after = ''; (第 40 行)
+content = content.replace(
+  /n\.value = ' ';[\s\n]*n\.spaces\.before = n\.spaces\.after = '';/,
+  `n.value = ' ';\n            n.spaces = n.spaces || {}; n.spaces.before = ''; n.spaces.after = '';`
+);
+
+// 修复 2: n.value = n.spaces.before = n.spaces.after = ''; (::v-deep)
+content = content.replace(
+  /if \(n\.type === '::v-deep' \) \{[\s\n]*n\.value = n\.spaces\.before = n\.spaces\.after = '';[\s\n]*return false;[\s\n]*\}/,
+  `if (n.type === '::v-deep') {\n            n.value = '';\n            n.spaces = { before: '', after: '' };\n            return false;\n          }`
+);
+
+// 修复 3: node.spaces.after = '';
+content = content.replace(
+  /if \(node\) \{[\s\n]*node\.spaces\.after = '';[\s\n]\}/,
+  `if (node) {\n          node.spaces = node.spaces || {}; node.spaces.after = '';`
+);
+
+// 修复 4: selector.first.spaces.before = '';
+content = content.replace(
+  /selector\.first\.spaces\.before = '';/,
+  `if (selector.first && selector.first.spaces) { selector.first.spaces.before = ''; }`
+);
+
+fs.writeFileSync(SCOPED_FILE, content, 'utf8');
+
+console.log('✅ 修复完成!请重启 HBuilderX 并重新编译。');

+ 21 - 0
cfc-frontend/fix-postcss.sh

@@ -0,0 +1,21 @@
+#!/bin/bash
+
+# 删除 node_modules 和 lock 文件
+echo "清理旧依赖..."
+rm -rf node_modules package-lock.json unpackage/dist
+
+# 创建 .npmrc 文件配置
+echo "配置 npm..."
+cat > .npmrc << 'NPMRC'
+legacy-peer-deps=true
+engine-strict=false
+NPMRC
+
+# 安装依赖
+echo "开始安装依赖..."
+npm install --legacy-peer-deps
+
+# 检查 postcss 版本
+echo ""
+echo "=== 检查结果 ==="
+npm list postcss @vue/compiler-sfc | grep -E "(postcss|compiler-sfc)" | head -10

文件差異過大導致無法顯示
+ 351 - 906
cfc-frontend/package-lock.json


+ 17 - 7
cfc-frontend/package.json

@@ -9,11 +9,12 @@
     "test:coverage": "jest --coverage",
     "dev:h5": "cross-env NODE_ENV=development UNI_PLATFORM=h5 UNI_INPUT_DIR=. UNI_CLI_CONTEXT=. vue-cli-service uni-serve",
     "build:h5": "cross-env NODE_ENV=production UNI_PLATFORM=h5 UNI_INPUT_DIR=. UNI_CLI_CONTEXT=. vue-cli-service uni-build",
+    "build:mp-weixin": "cross-env NODE_ENV=development UNI_PLATFORM=mp-weixin UNI_INPUT_DIR=. UNI_CLI_CONTEXT=. vue-cli-service uni-build",
     "postbuild": "node -e \"try{require('fs').unlinkSync('dist/dev/mp-weixin/custom-tab-bar/index.vue')}catch(e){};try{const c=require('./dist/dev/mp-weixin/project.config.json');c.setting=c.setting||{};c.setting.lazyCodeLoading='requiredComponents';require('fs').writeFileSync('./dist/dev/mp-weixin/project.config.json',JSON.stringify(c,null,2))}catch(e){}\""
   },
   "devDependencies": {
-    "@babel/core": "^7.29.7",
-    "@babel/preset-env": "^7.29.7",
+    "@babel/core": "^7.16.0",
+    "@babel/preset-env": "^7.16.0",
     "@dcloudio/uni-app": "^2.0.2-5010420260703001",
     "@dcloudio/uni-cli-i18n": "^2.0.2-5010420260703001",
     "@dcloudio/uni-cli-shared": "^2.0.2-5010420260703001",
@@ -23,19 +24,28 @@
     "@dcloudio/uni-mp-weixin": "^2.0.2-5010420260703001",
     "@dcloudio/uni-template-compiler": "^2.0.2-5010420260703001",
     "@dcloudio/webpack-uni-pages-loader": "^2.0.2-5010420260703001",
-    "@vue/cli-plugin-babel": "^5.0.9",
-    "@vue/cli-service": "^4.5.19",
+    "@vue/cli-plugin-babel": "~4.5.0",
+    "@vue/cli-service": "~4.5.0",
+    "@vue/compiler-sfc": "2.7.16",
     "babel-jest": "^26.6.3",
     "copy-webpack-plugin": "^5.1.2",
     "jest": "^26.6.3",
-    "vue": "^2.7.16",
-    "vuex": "^3.6.2"
+    "vue": "2.7.16",
+    "vuex": "^3.6.2",
+    "yalc": "^1.0.0-pre.53"
   },
   "dependencies": {
     "@dcloudio/vue-cli-plugin-uni": "^2.0.2-5000720260410001",
     "@dcloudio/webpack-uni-mp-loader": "^2.0.2-5010520260709001",
-    "postcss": "^8.4.49",
+    "postcss": "7.0.39",
     "regenerator-runtime": "^0.14.1",
     "yargs-parser": "^22.0.0"
+  },
+  "overrides": {
+    "postcss": "7.0.39",
+    "postcss-selector-parser": "5.0.0",
+    "@vue/compiler-sfc": "2.7.16",
+    "@vue/babel-plugin-jsx": "npm:@vue/babel-plugin-jsx@^1.1.1",
+    "@vue/babel-plugin-resolve-type": "npm:@vue/babel-plugin-resolve-type@1.0.0"
   }
 }

+ 1 - 1
cfc-frontend/pages/health/physical-exam-detail.vue

@@ -48,7 +48,7 @@
         </view>
 
         <view class="indicator-list">
-          <view class="indicator-item" v-for="ind in group" :key="ind.id || ind.indicatorName">
+          <view class="indicator-item" v-for="(ind, idx) in group" :key="idx">
             <view class="indicator-left">
               <text class="indicator-name">{{ ind.indicatorName }}</text>
               <text class="indicator-range" v-if="ind.refRange">参考值: {{ ind.refRange }}</text>

+ 19 - 12
cfc-frontend/pages/health/report-confirm.vue

@@ -64,7 +64,7 @@
           <view
             class="candidate-item"
             v-for="(cand, idx) in candidates"
-            :key="cand.id || idx"
+            :key="idx"
             :class="{ 'candidate-selected': selectedMemberId === (cand.id || cand.userId) }"
             @click="selectCandidate(cand)"
           >
@@ -89,8 +89,8 @@
         <view class="member-list" v-if="familyMembers.length > 0 && (candidates.length === 0 || !showCandidatesOnly)">
           <view
             class="member-item"
-            v-for="member in familyMembers"
-            :key="member.userId || member.id"
+            v-for="(member, mIdx) in familyMembers"
+            :key="mIdx"
             :class="{ 'member-selected': selectedMemberId === (member.userId || member.id) }"
             @click="selectMember(member)"
           >
@@ -219,7 +219,7 @@
 
 <script>
 import { confirmReportPreview, discardReportDraft, confirmTongue, discardTongue } from '../../utils/api.js'
-import { addFamilyMember } from '../../utils/api.js'
+import { addFamilyMember, getFamilyMembers } from '../../utils/api.js'
 
 export default {
   data() {
@@ -347,14 +347,21 @@ export default {
   },
   methods: {
     loadFamilyMembers() {
-      var store = uni.getStorageSync('familyChildren')
-      if (store) {
-        try {
-          this.familyMembers = typeof store === 'string' ? JSON.parse(store) : store
-        } catch (e) {
-          this.familyMembers = []
-        }
-      }
+      var self = this
+      // 从后端获取真实家庭成员(家长 + 孩子),storage 中并无该数据
+      getFamilyMembers().then(function(res) {
+        var data = (res && res.data) || {}
+        var parents = (data.parents || []).map(function(p) {
+          return { id: p.id, nickname: p.realName || p.nickname, gender: p.gender, role: 'parent' }
+        })
+        var children = (data.children || []).map(function(c) {
+          return { id: c.id, nickname: c.nickname, gender: c.gender, age: c.age, role: 'child' }
+        })
+        self.familyMembers = parents.concat(children)
+      }).catch(function(e) {
+        console.log('加载家庭成员失败', e)
+        self.familyMembers = []
+      })
     },
     selectMember(member) {
       this.selectedMemberId = member.userId || member.id

+ 5 - 5
cfc-frontend/pages/health/report-survey.vue

@@ -126,7 +126,7 @@
         <view class="q-options">
           <view class="q-option" :class="{ 'q-selected': survey.water === 'enough' }" @click="survey.water = 'enough'">
             <text>💧</text>
-            <text class="q-text">充足(>1500ml)</text>
+            <text class="q-text">充足(>1500ml)</text>
           </view>
           <view class="q-option" :class="{ 'q-selected': survey.water === 'moderate' }" @click="survey.water = 'moderate'">
             <text>🥤</text>
@@ -134,7 +134,7 @@
           </view>
           <view class="q-option" :class="{ 'q-selected': survey.water === 'little' }" @click="survey.water = 'little'">
             <text>😥</text>
-            <text class="q-text">偏少(<500ml)</text>
+            <text class="q-text">偏少(<500ml)</text>
           </view>
         </view>
       </view>
@@ -259,7 +259,7 @@
           <view class="family-col">
             <text class="family-role">父亲</text>
             <view class="family-options">
-              <view v-for="opt in familyDiseaseOptions" :key="'f_' + opt.value"
+              <view v-for="opt in familyDiseaseOptions" :key="opt.value"
                 class="family-tag" :class="{ 'tag-selected': hasFamilyDisease('father', opt.value) }"
                 @click="toggleFamilyDisease('father', opt.value)">
                 <text>{{ opt.label }}</text>
@@ -273,7 +273,7 @@
           <view class="family-col">
             <text class="family-role">母亲</text>
             <view class="family-options">
-              <view v-for="opt in familyDiseaseOptions" :key="'m_' + opt.value"
+              <view v-for="opt in familyDiseaseOptions" :key="opt.value"
                 class="family-tag" :class="{ 'tag-selected': hasFamilyDisease('mother', opt.value) }"
                 @click="toggleFamilyDisease('mother', opt.value)">
                 <text>{{ opt.label }}</text>
@@ -287,7 +287,7 @@
           <view class="family-col">
             <text class="family-role">兄弟姐妹</text>
             <view class="family-options">
-              <view v-for="opt in familyDiseaseOptions" :key="'s_' + opt.value"
+              <view v-for="opt in familyDiseaseOptions" :key="opt.value"
                 class="family-tag" :class="{ 'tag-selected': hasFamilyDisease('sibling', opt.value) }"
                 @click="toggleFamilyDisease('sibling', opt.value)">
                 <text>{{ opt.label }}</text>

+ 123 - 12
cfc-frontend/pages/health/report-upload.vue

@@ -47,13 +47,16 @@
           class="member-item"
           v-for="member in familyMembers"
           :key="member.id"
-          :class="{ 'member-selected': selectedMemberId === member.userId }"
+          :class="{ 'member-selected': selectedMemberId === (member.userId || member.id) }"
           @click="selectMember(member)"
         >
           <text class="member-avatar">{{ member.nickname ? member.nickname.substring(0, 1) : '?' }}</text>
           <text class="member-name">{{ member.nickname || '未知' }}</text>
         </view>
       </view>
+      <view class="add-member-btn" v-if="parsedInfo && parsedInfo.personName" @click="chooseRelationAndAdd">
+        <text class="add-member-text">+ 将「{{ parsedInfo.personName }}」添加为新成员</text>
+      </view>
     </view>
 
     <!-- 表单 -->
@@ -209,7 +212,7 @@
 </template>
 
 <script>
-import { createHealthReport, parseReportPreview, productList } from '../../utils/api.js'
+import { createHealthReport, parseReportPreview, productList, getFamilyMembers, addFamilyMember } from '../../utils/api.js'
 import config from '@/config.js'
 
 // 报告类型 → 维度映射(用于推荐商品)
@@ -424,6 +427,10 @@ export default {
         if (detail.needBind) {
           self.showMemberSelector = true
           self.loadFamilyMembers()
+          // 未匹配到成员时,主动询问是否把解析到的人添加为新成员
+          if (self.parsedInfo.personName) {
+            self.promptAddParsedMember()
+          }
         }
         // 从解析结果自动填充报告日期
         if (detail.extractedReportDate) {
@@ -438,22 +445,114 @@ export default {
     },
     loadFamilyMembers() {
       var self = this
-      // 从Vuex store获取家庭成员
-      var store = uni.getStorageSync('familyChildren')
-      if (store) {
-        try {
-          this.familyMembers = typeof store === 'string' ? JSON.parse(store) : store
-        } catch (e) {
-          this.familyMembers = []
-        }
-      }
+      // 从后端获取真实家庭成员(家长 + 孩子),storage 中并无该数据
+      getFamilyMembers().then(function(res) {
+        var data = (res && res.data) || {}
+        var parents = (data.parents || []).map(function(p) {
+          return { id: p.id, nickname: p.realName || p.nickname, gender: p.gender, role: 'parent' }
+        })
+        var children = (data.children || []).map(function(c) {
+          return { id: c.id, nickname: c.nickname, gender: c.gender, age: c.age, role: 'child' }
+        })
+        self.familyMembers = parents.concat(children)
+      }).catch(function(e) {
+        console.log('加载家庭成员失败', e)
+        self.familyMembers = []
+      })
     },
     selectMember(member) {
-      this.selectedMemberId = member.userId || member.id
+      this.selectedMemberId = member.id || member.userId
       this.parsedInfo = this.parsedInfo || {}
+      // 手动选择的成员通过 matchedMemberId 传递给确认页
+      this.parsedInfo.matchedMemberId = this.selectedMemberId
       this.parsedInfo.needManualSelect = false
       this.showMemberSelector = false
     },
+    genderText(gender) {
+      if (gender === 'male') return '男'
+      if (gender === 'female') return '女'
+      return ''
+    },
+    // 询问是否将解析到的人添加为家庭成员
+    promptAddParsedMember() {
+      var self = this
+      if (!this.parsedInfo || !this.parsedInfo.personName) return
+      var name = this.parsedInfo.personName
+      var desc = name
+      var extra = []
+      var g = this.genderText(this.parsedInfo.extractedGender)
+      if (g) extra.push(g)
+      if (this.parsedInfo.extractedAge) extra.push(this.parsedInfo.extractedAge + '岁')
+      if (extra.length > 0) desc = name + '(' + extra.join(',') + ')'
+      uni.showModal({
+        title: '未匹配到家庭成员',
+        content: '是否将解析到的「' + desc + '」添加为家庭成员?',
+        confirmText: '添加',
+        cancelText: '手动选择',
+        success: function(res) {
+          if (res.confirm) {
+            self.chooseRelationAndAdd()
+          }
+          // 取消则保留下方的手动选择列表
+        }
+      })
+    },
+    // 选择新成员与家庭的关系(后端要求辈分必填)后添加
+    chooseRelationAndAdd() {
+      var self = this
+      var relOptions = [
+        { label: '孩子', generationLevel: 'child' },
+        { label: '配偶', generationLevel: 'peer', peerType: 'spouse' },
+        { label: '父母', generationLevel: 'parent' },
+        { label: '兄弟姐妹', generationLevel: 'peer', peerType: 'sibling' },
+        { label: '祖父母', generationLevel: 'grandparent' }
+      ]
+      uni.showActionSheet({
+        itemList: relOptions.map(function(o) { return o.label }),
+        success: function(res) {
+          var rel = relOptions[res.tapIndex]
+          if (rel) {
+            self.doAddParsedMember(rel)
+          }
+        }
+      })
+    },
+    // 调用后端添加成员,并将其作为本次报告对象
+    doAddParsedMember(rel) {
+      var self = this
+      var name = this.parsedInfo.personName
+      var dto = {
+        nickname: name,
+        gender: this.parsedInfo.extractedGender || '',
+        generationLevel: rel.generationLevel
+      }
+      if (rel.peerType) dto.peerType = rel.peerType
+      // 由年龄推算大致出生年份(用于展示,用户后续可修改)
+      var age = parseInt(this.parsedInfo.extractedAge)
+      if (!isNaN(age) && age > 0 && age < 130) {
+        dto.birthday = (new Date().getFullYear() - age) + '-01-01'
+      }
+      uni.showLoading({ title: '添加成员...' })
+      addFamilyMember(dto).then(function(res) {
+        uni.hideLoading()
+        if (res && res.code === 200) {
+          var newId = res.data && (res.data.id || res.data.userId)
+          self.selectedMemberId = newId
+          self.parsedInfo.matchedMemberId = newId
+          self.parsedInfo.matchedMemberName = name
+          self.parsedInfo.needManualSelect = false
+          self.showMemberSelector = false
+          self.loadFamilyMembers()
+          uni.showToast({ title: '已添加并选中', icon: 'success' })
+        } else {
+          uni.showToast({ title: (res && res.message) || '添加失败', icon: 'none' })
+        }
+      }).catch(function(e) {
+        uni.hideLoading()
+        console.log('添加成员失败', e)
+        uni.showToast({ title: '添加失败,请重试', icon: 'none' })
+      })
+    },
     removeFile() {
       this.selectedFile = null
       this.parsedInfo = null
@@ -915,6 +1014,18 @@ uni.showToast({ title: '提交成功', icon: 'success' })
   font-size: 24rpx;
   color: #333;
 }
+.add-member-btn {
+  margin-top: 16rpx;
+  padding: 20rpx;
+  border: 1rpx dashed #4caf50;
+  border-radius: 12rpx;
+  text-align: center;
+  background: #f6fbf6;
+}
+.add-member-text {
+  font-size: 26rpx;
+  color: #4caf50;
+}
 
 /* ===== 舌诊拍照区 ===== */
 .photo-area {

+ 1 - 1
cfc-frontend/pages/index/child-index.vue

@@ -1122,7 +1122,7 @@ padding-top: 4rpx;
   flex-wrap: wrap;
   gap: 20rpx;
 }
-.quick-grid > * {
+.quick-grid .quick-clay-btn {
   width: calc(50% - 10rpx);
 }
 

+ 1 - 1
cfc-frontend/pages/index/index.vue

@@ -137,7 +137,7 @@
         </view>
         <!-- 无数据时显示占位卡片 -->
         <template v-else-if="indexArticles.length === 0">
-          <view class="article-card" v-for="i in 3" :key="'ph-' + i">
+          <view class="article-card" v-for="(item, i) in [1,2,3]" :key="i">
             <view class="article-category" :style="{ background: gradientColors[(i-1) % gradientColors.length] }">
               <text class="article-category-text">推荐文章</text>
             </view>

+ 2 - 2
cfc-frontend/pages/shop/address-edit/address-edit.vue

@@ -76,8 +76,8 @@
           </view>
           <scroll-view class="region-list" scroll-y>
             <view
-              v-for="item in currentRegionItems"
-              :key="item.id || item.code"
+              v-for="(item, idx) in currentRegionItems"
+              :key="idx"
               :class="['region-item', isRegionSelected(item) ? 'region-item-selected' : '']"
               @click="selectRegionItem(item)"
             >

+ 1 - 1
cfc-frontend/pages/shop/payment/payment.vue

@@ -316,7 +316,7 @@ export default {
 }
 </script>
 
-<style scoped>
+<style>
 .container {
   min-height: 100vh;
   background: #f5f5f5;

+ 2 - 2
cfc-frontend/pages/wisdom/wisdom_temp/training-hub.vue

@@ -29,8 +29,8 @@
               <view class="difficulty-row">
                 <text class="difficulty-label">难度:</text>
                 <view class="stars-row">
-                  <text class="star filled" v-for="i in game.difficulty" :key="'on' + i">&#x2605;</text>
-                  <text class="star empty" v-for="i in (5 - game.difficulty)" :key="'off' + i">&#x2605;</text>
+                  <text class="star filled" v-for="(i, idx) in game.difficulty" :key="idx">&#x2605;</text>
+                  <text class="star empty" v-for="(i, idx) in (5 - game.difficulty)" :key="idx">&#x2605;</text>
                 </view>
               </view>
               <text class="game-badge" v-if="game.badge">{{ game.badge }}</text>

+ 6 - 2
cfc-frontend/postcss.config.js

@@ -1,5 +1,9 @@
 // PostCSS config for uni-app mp-weixin build
+// Fixed: ensure compat with @vue/component-compiler-utils (PostCSS 7.x)
 module.exports = {
-  plugins: []
+  plugins: [
+    require('postcss-import'),
+    require('postcss-url'),
+    require('autoprefixer')
+  ]
 }
-

+ 4 - 5
cfc-frontend/vue.config.js

@@ -1,12 +1,11 @@
 // vue.config.js - disable thread-loader for compatibility
 module.exports = {
-  parallel: false,
-  // Ensure we use the correct source directory
+  parallel: false, // Ensure we use the correct source directory
   pages: undefined,
-  
   configureWebpack: {
     resolve: {
       extensions: ['.vue', '.js', '.json'], // Ensure .vue files resolve correctly
-    }
-  }
+    },
+    node: false,
+  },
 }

+ 10 - 1
cfc-langgraph/app/graphs/chat_graph.py

@@ -47,7 +47,6 @@ def create_chat_graph():
     """创建聊天 StateGraph"""
     agent = ChatAgent()
     classifier = IntentClassifier()
-    memory_mgr = MemoryManager()
 
     llm = ChatOpenAI(
         model=settings.llm_model,
@@ -55,6 +54,16 @@ def create_chat_graph():
         base_url=settings.llm_base_url,
         temperature=0.7,
     )
+    
+    # 初始化嵌入模型用于记忆存储
+    from langchain_openai import OpenAIEmbeddings
+    embeddings = OpenAIEmbeddings(
+        model=settings.embedding_model,
+        api_key=settings.effective_embedding_api_key,
+        base_url=settings.effective_embedding_base_url,
+    )
+    memory_mgr = MemoryManager(embeddings)
+
     llm_with_tools = llm.bind_tools([
         search_product_by_keyword,
         search_article_by_keyword,

+ 19 - 48
cfc-langgraph/app/memory/store.py

@@ -1,59 +1,30 @@
-from langchain.memory import ConversationSummaryBufferMemory, VectorStoreRetrieverMemory
-from langchain_openai import ChatOpenAI, OpenAIEmbeddings
-from langchain_chroma import Chroma
-from app.config import settings
-from typing import Optional
+import os
 import logging
+from datetime import datetime
 
 logger = logging.getLogger(__name__)
 
 
 class MemoryManager:
-    """三层记忆管理器
-
-    Layer 1 - 工作记忆: 最近 20 轮 + 超出自动摘要
-    Layer 2 - 长期事实: VectorStoreRetrieverMemory, 跨会话相似召回
-    Layer 3 - 语义记忆: 每次对话后向量化存储, 供后续会话召回
+    """轻量级记忆管理器 - 仅保存对话历史到向量数据库
+    
+    不再使用 langchain.memory(已废弃),直接实现核心功能:
+    - Layer 3 - 语义记忆:每次对话后向量化存储,供后续会话召回
     """
 
-    def __init__(self):
-        self.llm = ChatOpenAI(
-            model=settings.llm_model,
-            api_key=settings.llm_api_key,
-            base_url=settings.llm_base_url,
-        )
-        self.embeddings = OpenAIEmbeddings(
-            model=settings.embedding_model,
-            api_key=settings.effective_embedding_api_key,
-            base_url=settings.effective_embedding_base_url,
-        )
-        # Layer 3 向量库: 历史对话记忆
+    def __init__(self, embeddings):
+        """初始化记忆管理器
+        
+        Args:
+            embeddings: 嵌入模型实例 (OpenAIEmbeddings 或其他兼容接口)
+        """
+        self.embeddings = embeddings
+        # Layer 3 向量库:历史对话记忆
+        from langchain_chroma import Chroma
         self.memory_vectorstore = Chroma(
             collection_name="user_memory",
             embedding_function=self.embeddings,
-            persist_directory=settings.chroma_db_path + "_memory",
-        )
-
-    def get_working_memory(self) -> ConversationSummaryBufferMemory:
-        """Layer 1: 工作记忆 (当前会话)"""
-        return ConversationSummaryBufferMemory(
-            llm=self.llm,
-            max_token_limit=2000,
-            memory_key="history",
-            return_messages=True,
-        )
-
-    def get_longterm_memory(self, user_id: int) -> VectorStoreRetrieverMemory:
-        """Layer 2+3: 长期 + 语义记忆"""
-        return VectorStoreRetrieverMemory(
-            retriever=self.memory_vectorstore.as_retriever(
-                search_kwargs={
-                    "k": 3,
-                    "filter": {"user_id": user_id},
-                }
-            ),
-            memory_key="long_term_memory",
-            input_key="input",
+            persist_directory=os.path.expanduser("~/.cfc/langgraph/memory"),
         )
 
     async def save_conversation(self, user_id: int, conversation_id: str,
@@ -69,7 +40,7 @@ class MemoryManager:
         metadata = {
             "user_id": user_id,
             "conversation_id": conversation_id,
-            "timestamp": str(__import__("datetime").datetime.now()),
+            "timestamp": str(datetime.now()),
         }
 
         await self.memory_vectorstore.aadd_texts(
@@ -77,10 +48,10 @@ class MemoryManager:
             metadatas=[metadata],
         )
         self.memory_vectorstore.persist()
-        logger.info("已保存对话到向量记忆: conv=%s, user=%s", conversation_id, user_id)
+        logger.info("已保存对话到向量记忆:conv=%s, user=%s", conversation_id, user_id)
 
     async def recall(self, user_id: int, query: str, k: int = 3) -> list[str]:
-        """语义召回: 查询与 query 最相似的历史对话片段"""
+        """语义召回:查询与 query 最相似的历史对话片段"""
         results = self.memory_vectorstore.similarity_search(
             query,
             k=k,

+ 44 - 66
cfc-langgraph/app/rag/retriever.py

@@ -1,10 +1,5 @@
 from langchain_chroma import Chroma
-from langchain.retrievers import EnsembleRetriever
-from langchain_community.retrievers import BM25Retriever
-from langchain.retrievers.document_compressors import LLMChainExtractor
-from langchain.retrievers import ContextualCompressionRetriever
-from langchain_openai import ChatOpenAI
-from .embeddings import get_embeddings
+from langchain_openai import ChatOpenAI, OpenAIEmbeddings
 from app.config import settings
 from app.tools.java_client import JavaClient
 from typing import Optional
@@ -14,96 +9,79 @@ logger = logging.getLogger(__name__)
 
 
 class RagRetriever:
-    """升级版混合检索器: 向量 + BM25 + LLM 压缩重排序"""
+    """简单的向量检索器 - 基于 ChromaDB"""
 
     def __init__(self, collection_name: str = "cfc_knowledge"):
-        embeddings = get_embeddings()
+        embeddings = OpenAIEmbeddings(
+            model=settings.embedding_model,
+            api_key=settings.effective_embedding_api_key,
+            base_url=settings.effective_embedding_base_url,
+        )
         self.vectorstore = Chroma(
             collection_name=collection_name,
             embedding_function=embeddings,
             persist_directory=settings.chroma_db_path,
         )
         self.java_client = JavaClient()
-        self._bm25_retriever: Optional[BM25Retriever] = None
-        self._bm25_texts: list[str] = []
 
     async def initialize(self):
-        """从 Java 侧拉取知识库, 构建 BM25 索引"""
+        """从 Java 侧拉取知识库,更新到向量库"""
         try:
             articles = await self.java_client.get_published_articles()
-            self._bm25_texts = [
-                f"{a['title']} {a['summary']} {a.get('tags', '')}"
-                for a in articles
-            ]
-            if self._bm25_texts:
-                self._bm25_retriever = BM25Retriever.from_texts(
-                    self._bm25_texts,
-                    metadatas=articles,
-                )
-                logger.info("BM25 索引就绪: %d 条", len(self._bm25_texts))
+            if articles:
+                # 构建文本和内容元数据
+                texts = [f"{a['title']} {a.get('summary', '')} {' '.join(a.get('tags', []))}" 
+                        for a in articles]
+                metadatas = [
+                    {
+                        "id": a["id"],
+                        "title": a["title"],
+                        "summary": a.get("summary", ""),
+                        "tags": a.get("tags", []),
+                        "type": "article",
+                    }
+                    for a in articles
+                ]
+
+                # 添加或更新向量数据库
+                await self.vectorstore.aadd_texts(texts=texts, metadatas=metadatas)
+                logger.info("知识库向量化完成:%d 篇文章", len(articles))
         except Exception as e:
-            logger.warning("BM25 初始化失败: %s", e)
+            logger.warning("知识库初始化失败:%s", e)
 
     async def retrieve(
         self,
         query: str,
         filters: Optional[dict] = None,
         k: int = 5,
-        use_compression: bool = True,
     ) -> list[dict]:
-        """混合检索 + 可选 LLM 压缩重排序"""
-        retrievers = []
-
-        # 1. 向量检索 (多取一些方便后续 ensemble 排序)
-        vector_retriever = self.vectorstore.as_retriever(
-            search_kwargs={"k": k * 2, "filter": filters},
-        )
-        retrievers.append(vector_retriever)
-
-        # 2. BM25 关键词检索
-        if self._bm25_retriever:
-            bm25_k = self._bm25_retriever.k
-            self._bm25_retriever.k = k * 2
-            retrievers.append(self._bm25_retriever)
-            self._bm25_retriever.k = bm25_k
-
-        if len(retrievers) == 1:
-            docs = await retrievers[0].ainvoke(query)
-            ensemble = retrievers[0]
-        else:
-            ensemble = EnsembleRetriever(
-                retrievers=retrievers,
-                weights=[0.6, 0.4],
-            )
-            docs = await ensemble.ainvoke(query)
-
-        # 3. LLM 压缩 (剔除不相关内容)
-        if use_compression and docs:
-            llm = ChatOpenAI(
-                model=settings.llm_model,
-                api_key=settings.llm_api_key,
-                base_url=settings.llm_base_url,
-                temperature=0,
-            )
-            compressor = LLMChainExtractor.from_llm(llm)
-            compression_retriever = ContextualCompressionRetriever(
-                base_compressor=compressor,
-                base_retriever=ensemble if len(retrievers) > 1 else retrievers[0],
-            )
-            docs = await compression_retriever.ainvoke(query)
-
-        # 4. 格式化为统一输出 + 去重
+        """向量相似度检索"""
         results = []
+        
+        # 向量检索
+        filter_query = {"user_id": filters["user_id"]} if filters and "user_id" in filters else None
+        
+        docs = self.vectorstore.similarity_search(
+            query,
+            k=k * 2,  # 多取一些
+            filter=filter_query,
+        )
+        
+        # 格式化结果并去重
         seen = set()
         for doc in docs:
             content_hash = hash(doc.page_content[:100])
             if content_hash in seen:
                 continue
             seen.add(content_hash)
+            
             results.append({
                 "content": doc.page_content,
                 "metadata": doc.metadata,
                 "score": doc.metadata.get("score", 0) if hasattr(doc, "metadata") else 0,
             })
-
+            
+            if len(results) >= k:
+                break
+        
         return results[:k]

+ 3 - 3
cfc-langgraph/app/rag/splitter.py

@@ -1,7 +1,7 @@
-from langchain.text_splitter import RecursiveCharacterTextSplitter
+from langchain_text_splitters import RecursiveCharacterTextSplitter
 
 
-def get_knowledge_splitter() -> RecursiveCharacterTextSplitter:
+def get_knowledge_splitter():
     """知识库文档分块器"""
     return RecursiveCharacterTextSplitter(
         chunk_size=500,
@@ -11,7 +11,7 @@ def get_knowledge_splitter() -> RecursiveCharacterTextSplitter:
     )
 
 
-def get_summary_splitter() -> RecursiveCharacterTextSplitter:
+def get_summary_splitter():
     """摘要分块器 (用于对话记忆)"""
     return RecursiveCharacterTextSplitter(
         chunk_size=1000,

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-fb66b06cbb4045108ca96b2d670e1d7cefb05989
+fa292926579cb307cc2080a8e3c4b29a45ad4649

+ 2 - 2
cfc-web/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.451",
+  "version": "1.0.454",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.451",
+      "version": "1.0.454",
       "dependencies": {
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^1.0.2",

+ 1 - 1
cfc-web/package.json

@@ -1,6 +1,6 @@
 {
   "name": "cfc-web",
-  "version": "1.0.452",
+  "version": "1.0.455",
   "private": true,
   "scripts": {
     "dev": "vue-cli-service serve",

+ 45 - 0
cfc-web/public/CHANGELOG-v1.0.md

@@ -4,6 +4,51 @@
 
 ---
 
+## v1.0.455 (2026-07-21)
+
+### Bug 修复
+- 修复 uni-app 小程序编译兼容性问题及运行优化
+
+### 其他
+- - 降级 @vue/compiler-sfc 到 2.7.16 确保 HBuilderX 兼容
+- - 添加 postcss-import、postcss-url、autoprefixer 配置
+- - 添加 fix-hbuilderx-scoped.js 和 fix-postcss.sh 自动化修复脚本
+- - 回退 Babel 和 Vue CLI 版本到兼容的 4.x 系列
+- - 优化小程序样式 scoped 问题修复工具
+- - 修复组件编译警告和问题:
+-   * ActionArticleRecommend.vue
+-   * HealthDimensionsSection.vue
+-   * RecommendedFeed.vue
+-   * mind-section.vue
+-   * health 相关页面
+-   * payment.vue (移除 scoped)
+-   * child-index.vue
+-   * index.vue
+- - 优化 Python LangGraph 记忆和检索逻辑
+- 
+
+
+## v1.0.454 (2026-07-21)
+
+### 文档
+- sync phase1-4 plan checkpoints; add langgraph start.sh; ignore .venv
+
+
+## v1.0.453 (2026-07-21)
+
+### 文档
+- PROJECT-OVERVIEW.md v1.1 audit calibration
+
+### 其他
+- - Downgrade 2 features to ⚠️ (e-commerce shop_ tables missing, promotion frontend missing)
+- - Mark langgraph-requirements-summary.md as ⚠️ file missing
+- - Version bump v1.0 → v1.1
+- 
+
+### Bug 修复
+- align MemorySaver import path and pin dependency upper bounds
+
+
 ## v1.0.452 (2026-07-21)
 
 ### 文档

+ 46 - 1
cfc-web/public/CHANGELOG.md

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.452
+> 当前版本: v1.0.455
 
 ## 历史版本
 
@@ -8,6 +8,51 @@
 
 ---
 
+## v1.0.455 (2026-07-21)
+
+### Bug 修复
+- 修复 uni-app 小程序编译兼容性问题及运行优化
+
+### 其他
+- - 降级 @vue/compiler-sfc 到 2.7.16 确保 HBuilderX 兼容
+- - 添加 postcss-import、postcss-url、autoprefixer 配置
+- - 添加 fix-hbuilderx-scoped.js 和 fix-postcss.sh 自动化修复脚本
+- - 回退 Babel 和 Vue CLI 版本到兼容的 4.x 系列
+- - 优化小程序样式 scoped 问题修复工具
+- - 修复组件编译警告和问题:
+-   * ActionArticleRecommend.vue
+-   * HealthDimensionsSection.vue
+-   * RecommendedFeed.vue
+-   * mind-section.vue
+-   * health 相关页面
+-   * payment.vue (移除 scoped)
+-   * child-index.vue
+-   * index.vue
+- - 优化 Python LangGraph 记忆和检索逻辑
+- 
+
+
+## v1.0.454 (2026-07-21)
+
+### 文档
+- sync phase1-4 plan checkpoints; add langgraph start.sh; ignore .venv
+
+
+## v1.0.453 (2026-07-21)
+
+### 文档
+- PROJECT-OVERVIEW.md v1.1 audit calibration
+
+### 其他
+- - Downgrade 2 features to ⚠️ (e-commerce shop_ tables missing, promotion frontend missing)
+- - Mark langgraph-requirements-summary.md as ⚠️ file missing
+- - Version bump v1.0 → v1.1
+- 
+
+### Bug 修复
+- align MemorySaver import path and pin dependency upper bounds
+
+
 ## v1.0.452 (2026-07-21)
 
 ### 文档

部分文件因文件數量過多而無法顯示