feat(archival): move client archiving from mock to API and SQLite

This commit is contained in:
lidf 2026-04-12 17:07:37 +08:00
parent d711df312f
commit cd985e38ea
6 changed files with 134 additions and 48 deletions

View File

@ -1530,23 +1530,34 @@ xray.module5: track1(数组,每项含node/action/strategy/purpose), track2(数
if not user: if not user:
return web.json_response({"error": "Unauthorized"}, status=401, headers=_CORS_HEADERS) return web.json_response({"error": "Unauthorized"}, status=401, headers=_CORS_HEADERS)
userDir = self._getUserStorageDir(user.get("sub", "unknown")) try:
clientsDir = os.path.join(userDir, "clients") from hermes_state import SessionDB
db = SessionDB()
with db._lock:
cursor = db._conn.execute(
"SELECT client_id, name, phone, created_at, updated_at FROM deepview_clients WHERE user_id=? ORDER BY updated_at DESC",
(user["userId"],)
)
rows = cursor.fetchall()
clients = [] clients = []
if os.path.exists(clientsDir): for row in rows:
for d in os.listdir(clientsDir): c_id, c_name, c_phone, c_created, c_updated = row
clientPath = os.path.join(clientsDir, d)
if os.path.isdir(clientPath): # Option 1: To get report count, we could do SQL count or just skip it for now.
# 尝试读取 profile.md 以获取首行做名字(可选优化) # Here we just map directly.
# 默认使用目录名作为 ID 和名字
clients.append({ clients.append({
"id": d, "id": c_id,
"name": d, "name": c_name,
"hasProfile": os.path.exists(os.path.join(clientPath, "profile.md")) "phone": c_phone,
"createdAt": c_created,
"updatedAt": c_updated
}) })
return web.json_response({"clients": clients}, headers=_CORS_HEADERS) return web.json_response({"clients": clients}, headers=_CORS_HEADERS)
except Exception as e:
logger.error(f"[DeepviewSSE] DB Clients list failed: {e}")
return web.json_response({"error": str(e)}, status=500, headers=_CORS_HEADERS)
async def _handleClientCreate(self, request: "web.Request") -> "web.Response": async def _handleClientCreate(self, request: "web.Request") -> "web.Response":
""" """
POST /deepview/clients/create POST /deepview/clients/create
@ -1575,10 +1586,23 @@ xray.module5: track1(数组,每项含node/action/strategy/purpose), track2(数
clientDir = os.path.join(userDir, "clients", clientId) clientDir = os.path.join(userDir, "clients", clientId)
os.makedirs(clientDir, exist_ok=True) os.makedirs(clientDir, exist_ok=True)
profileData = f"# 客户基本档案\n姓名:{name}\n手机号/尾号:{phone}\n建档时间:{time.strftime('%Y-%m-%d')}\n" profileData = f"# 客户基本档案\n姓名:{name}\n手机号/尾号:{phone}\n建档时间:{time.strftime('%Y-%m-%d')}\n\n## 标签\n- 新客\n\n## AI 沉淀洞察\n- 暂无\n"
with open(os.path.join(clientDir, "profile.md"), "w", encoding="utf-8") as f: with open(os.path.join(clientDir, "profile.md"), "w", encoding="utf-8") as f:
f.write(profileData) f.write(profileData)
try:
from hermes_state import SessionDB
db = SessionDB()
with db._lock:
db._conn.execute(
"INSERT INTO deepview_clients (client_id, user_id, name, phone) VALUES (?, ?, ?, ?)",
(clientId, user["userId"], name, phone)
)
db._conn.commit()
logger.info(f"[DeepviewSSE] Created client {clientId} in DB")
except Exception as e:
logger.error(f"[DeepviewSSE] Failed to create client {clientId} in DB: {e}")
return web.json_response({ return web.json_response({
"id": clientId, "id": clientId,
"name": name, "name": name,
@ -1654,10 +1678,33 @@ xray.module5: track1(数组,每项含node/action/strategy/purpose), track2(数
from hermes_state import SessionDB from hermes_state import SessionDB
db = SessionDB() db = SessionDB()
with db._lock: with db._lock:
# Update report records to associate with client
cursor = db._conn.execute("SELECT report_json FROM deepview_reports_v2 WHERE report_id=? AND user_id=?", (reportId, userId))
row = cursor.fetchone()
# Update clientName directly into the json to avoid joins in reports/list and report/get
if row:
try:
rData = json.loads(row[0])
# Find client name
c_cursor = db._conn.execute("SELECT name FROM deepview_clients WHERE client_id=?", (clientId,))
c_row = c_cursor.fetchone()
c_name = c_row[0] if c_row else "未知访客"
rData["clientName"] = c_name
db._conn.execute("UPDATE deepview_reports_v2 SET report_json=? WHERE report_id=?", (json.dumps(rData, ensure_ascii=False), reportId))
except:
pass
db._conn.execute( db._conn.execute(
"UPDATE deepview_reports_v2 SET client_id=?, context_id=? WHERE report_id=? AND user_id=?", "UPDATE deepview_reports_v2 SET client_id=?, context_id=? WHERE report_id=? AND user_id=?",
(clientId, f"recording:{clientId}/{reportId}", reportId, userId) (clientId, f"recording:{clientId}/{reportId}", reportId, userId)
) )
# Update client's updated_at timestamp to float to top in the list
db._conn.execute("UPDATE deepview_clients SET updated_at=strftime('%s','now') WHERE client_id=?", (clientId,))
db._conn.commit() db._conn.commit()
logger.info(f"[DeepviewSSE] DB archived: {reportId} → client {clientId}") logger.info(f"[DeepviewSSE] DB archived: {reportId} → client {clientId}")
except Exception as e: except Exception as e:
@ -1677,6 +1724,27 @@ xray.module5: track1(数组,每项含node/action/strategy/purpose), track2(数
self._app = web.Application() self._app = web.Application()
# 初始化数据库 schema
try:
from hermes_state import SessionDB
db = SessionDB()
with db._lock:
db._conn.execute("""
CREATE TABLE IF NOT EXISTS deepview_clients (
client_id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
phone TEXT DEFAULT '',
created_at REAL DEFAULT (strftime('%s','now')),
updated_at REAL DEFAULT (strftime('%s','now'))
)
""")
db._conn.execute("CREATE INDEX IF NOT EXISTS idx_clients_user ON deepview_clients(user_id)")
db._conn.commit()
logger.info("[DeepviewSSE] DB schema for clients initialized")
except Exception as e:
logger.error(f"[DeepviewSSE] DB schema init failed: {e}")
# 注册路由 # 注册路由
self._app.router.add_get("/deepview/health", self._handleHealth) self._app.router.add_get("/deepview/health", self._handleHealth)
self._app.router.add_get("/deepview/chat/history", self._handleHistory) self._app.router.add_get("/deepview/chat/history", self._handleHistory)

View File

@ -62,6 +62,12 @@ export class ApiService {
}, { headers: this.getHeaders() }).toPromise(); }, { headers: this.getHeaders() }).toPromise();
} }
async getClients(): Promise<any> {
return this.http.get(`${this.apiUrl}/clients/list`, {
headers: this.getHeaders()
}).toPromise();
}
async createClient(name: string, phone: string = ''): Promise<any> { async createClient(name: string, phone: string = ''): Promise<any> {
return this.http.post(`${this.apiUrl}/clients/create`, { return this.http.post(`${this.apiUrl}/clients/create`, {
name, phone name, phone

View File

@ -1,6 +0,0 @@
export const MOCK_CLIENTS = [
{ id: 'p_li001', name: '李梅梅', phone: '8812', age: 38, lastVisit: '2026-03-20', tags: ['乔雅登', '极度怕疼'] },
{ id: 'p_wang002', name: '王晓菲', phone: '1024', age: 29, lastVisit: '2026-04-10', tags: ['光子嫩肤'] },
{ id: 'p_zhao003', name: '赵佳佳', phone: '5566', age: 45, lastVisit: '2025-12-05', tags: ['超声炮'] },
{ id: 'p_unknown', name: '散客/未建档', phone: '', age: 0, lastVisit: '首次面诊', tags: [] }
];

View File

@ -20,27 +20,27 @@
<div class="info"> <div class="info">
<div class="name-row"> <div class="name-row">
<span class="name">{{ client.name }}</span> <span class="name">{{ client.name }}</span>
<span class="tag" [ngClass]="getStageBadgeClass(client.ltcStatus.stage)"> <span class="tag" *ngIf="client.ltcStatus?.stage" [ngClass]="getStageBadgeClass(client.ltcStatus?.stage)">
{{ getStageLabel(client.ltcStatus.stage) }} {{ getStageLabel(client.ltcStatus?.stage) }}
</span> </span>
</div> </div>
<div class="meta">{{ client.gender }} · {{ client.age }}岁 · {{ client.lastVisit }} 入库</div> <div class="meta">{{ client.gender || '女' }} · {{ client.age || '-' }}岁 · 入库时间: {{ client.createdAt ? (client.createdAt * 1000 | date:'MM-dd HH:mm') : client.lastVisit }}</div>
</div> </div>
</div> </div>
<div class="client-body"> <div class="client-body">
<div class="demand-row"> <div class="demand-row">
<strong>核心诉求:</strong>{{ client.project }} <strong>核心诉求:</strong>{{ client.project || '待AI挖掘/人工补全' }}
</div> </div>
<div class="barrier-box" *ngIf="client.ltcStatus.stage !== 'won'"> <div class="barrier-box" *ngIf="client.ltcStatus && client.ltcStatus?.stage !== 'won'">
<div class="barrier-title">⚠️ AI 诊断阻力</div> <div class="barrier-title">⚠️ AI 诊断阻力</div>
<div class="barrier-text">{{ client.ltcStatus.coreBarrier }}</div> <div class="barrier-text">{{ client.ltcStatus?.coreBarrier }}</div>
</div> </div>
<div class="action-box"> <div class="action-box" *ngIf="client.ltcStatus">
<div class="action-title">💡 下一步建议</div> <div class="action-title">💡 下一步建议</div>
<div class="action-text">{{ client.ltcStatus.nextAction }}</div> <div class="action-text">{{ client.ltcStatus?.nextAction }}</div>
</div> </div>
</div> </div>

View File

@ -1,7 +1,7 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { MOCK_PATIENTS, Patient } from '../../data/mock-data'; import { ApiService } from '../../core/api.service';
@Component({ @Component({
selector: 'app-client-ltc-dashboard', selector: 'app-client-ltc-dashboard',
@ -11,7 +11,22 @@ import { MOCK_PATIENTS, Patient } from '../../data/mock-data';
styleUrl: './client-ltc-dashboard.css', styleUrl: './client-ltc-dashboard.css',
}) })
export class ClientLtcDashboard { export class ClientLtcDashboard {
clients = MOCK_PATIENTS; clients: any[] = [];
constructor(private api: ApiService) {}
ngOnInit() {
this.loadClients();
}
async loadClients() {
try {
const res = await this.api.getClients();
this.clients = res.clients || [];
} catch (e) {
console.error('Failed to load clients in LTC dashboard', e);
}
}
getStageBadgeClass(stage: string): string { getStageBadgeClass(stage: string): string {
const map: Record<string, string> = { const map: Record<string, string> = {

View File

@ -3,7 +3,6 @@ import { CommonModule } from '@angular/common';
import { RouterLink, ActivatedRoute } from '@angular/router'; import { RouterLink, ActivatedRoute } from '@angular/router';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { MOCK_REPORTS, Report } from '../../data/mock-data'; import { MOCK_REPORTS, Report } from '../../data/mock-data';
import { MOCK_CLIENTS } from '../../core/client-mock';
import { ApiService } from '../../core/api.service'; import { ApiService } from '../../core/api.service';
import { AuthService } from '../../core/auth.service'; import { AuthService } from '../../core/auth.service';
import { ChatContextService } from '../../core/chat-context.service'; import { ChatContextService } from '../../core/chat-context.service';
@ -25,8 +24,8 @@ export class ReportDetail implements OnInit {
archiveStatus: 'unarchived' | 'archived' = 'unarchived'; archiveStatus: 'unarchived' | 'archived' = 'unarchived';
showArchivePanel = false; showArchivePanel = false;
searchQuery = ''; searchQuery = '';
clients = MOCK_CLIENTS; clients: any[] = [];
filteredClients = MOCK_CLIENTS; filteredClients: any[] = [];
isArchiving = false; isArchiving = false;
isCreatingClient = false; isCreatingClient = false;
@ -40,6 +39,17 @@ export class ReportDetail implements OnInit {
const realId = decodeURIComponent(id); const realId = decodeURIComponent(id);
this.loadReportFromApi(realId); this.loadReportFromApi(realId);
}); });
this.loadClients();
}
async loadClients() {
try {
const res = await this.api.getClients();
this.clients = res.clients || [];
this.filteredClients = [...this.clients];
} catch (e) {
console.error('Failed to load clients', e);
}
} }
/** 从后端 DB 加载静态报告,失败时降级到 localStorage 缓存 */ /** 从后端 DB 加载静态报告,失败时降级到 localStorage 缓存 */
@ -86,7 +96,7 @@ export class ReportDetail implements OnInit {
filterClients() { filterClients() {
const q = this.searchQuery.toLowerCase(); const q = this.searchQuery.toLowerCase();
this.filteredClients = this.clients.filter(c => this.filteredClients = this.clients.filter(c =>
c.name.includes(q) || c.phone.includes(q) c.name?.toLowerCase().includes(q) || c.phone?.includes(q)
); );
} }
@ -116,17 +126,10 @@ export class ReportDetail implements OnInit {
const name = this.searchQuery.replace(/\d+/g, '').replace('尾号', '').replace(/\(\)/g, '').trim(); const name = this.searchQuery.replace(/\d+/g, '').replace('尾号', '').replace(/\(\)/g, '').trim();
const res = await this.api.createClient(name, phone); const res = await this.api.createClient(name, phone);
const newClient = {
id: res.id,
name: res.name,
phone: res.phone,
age: 30,
lastVisit: '首次建档',
tags: ['新客']
};
this.clients.push(newClient as any); this.clients.unshift(res); // Add newly created to the top
await this.archiveToClient(newClient); this.filteredClients = [...this.clients];
await this.archiveToClient(res);
} catch (e: any) { } catch (e: any) {
alert("建档失败:" + e.message); alert("建档失败:" + e.message);
} finally { } finally {