feat(archival): move client archiving from mock to API and SQLite
This commit is contained in:
parent
d711df312f
commit
cd985e38ea
@ -1530,23 +1530,34 @@ xray.module5: track1(数组,每项含node/action/strategy/purpose), track2(数
|
||||
if not user:
|
||||
return web.json_response({"error": "Unauthorized"}, status=401, headers=_CORS_HEADERS)
|
||||
|
||||
userDir = self._getUserStorageDir(user.get("sub", "unknown"))
|
||||
clientsDir = os.path.join(userDir, "clients")
|
||||
|
||||
clients = []
|
||||
if os.path.exists(clientsDir):
|
||||
for d in os.listdir(clientsDir):
|
||||
clientPath = os.path.join(clientsDir, d)
|
||||
if os.path.isdir(clientPath):
|
||||
# 尝试读取 profile.md 以获取首行做名字(可选优化)
|
||||
# 默认使用目录名作为 ID 和名字
|
||||
clients.append({
|
||||
"id": d,
|
||||
"name": d,
|
||||
"hasProfile": os.path.exists(os.path.join(clientPath, "profile.md"))
|
||||
})
|
||||
|
||||
return web.json_response({"clients": clients}, headers=_CORS_HEADERS)
|
||||
try:
|
||||
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 = []
|
||||
for row in rows:
|
||||
c_id, c_name, c_phone, c_created, c_updated = row
|
||||
|
||||
# Option 1: To get report count, we could do SQL count or just skip it for now.
|
||||
# Here we just map directly.
|
||||
clients.append({
|
||||
"id": c_id,
|
||||
"name": c_name,
|
||||
"phone": c_phone,
|
||||
"createdAt": c_created,
|
||||
"updatedAt": c_updated
|
||||
})
|
||||
|
||||
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":
|
||||
"""
|
||||
POST /deepview/clients/create
|
||||
@ -1575,10 +1586,23 @@ xray.module5: track1(数组,每项含node/action/strategy/purpose), track2(数
|
||||
clientDir = os.path.join(userDir, "clients", clientId)
|
||||
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:
|
||||
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({
|
||||
"id": clientId,
|
||||
"name": name,
|
||||
@ -1654,10 +1678,33 @@ xray.module5: track1(数组,每项含node/action/strategy/purpose), track2(数
|
||||
from hermes_state import SessionDB
|
||||
db = SessionDB()
|
||||
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(
|
||||
"UPDATE deepview_reports_v2 SET client_id=?, context_id=? WHERE report_id=? AND user_id=?",
|
||||
(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()
|
||||
logger.info(f"[DeepviewSSE] DB archived: {reportId} → client {clientId}")
|
||||
except Exception as e:
|
||||
@ -1677,6 +1724,27 @@ xray.module5: track1(数组,每项含node/action/strategy/purpose), track2(数
|
||||
|
||||
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/chat/history", self._handleHistory)
|
||||
|
||||
@ -62,6 +62,12 @@ export class ApiService {
|
||||
}, { 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> {
|
||||
return this.http.post(`${this.apiUrl}/clients/create`, {
|
||||
name, phone
|
||||
|
||||
@ -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: [] }
|
||||
];
|
||||
@ -20,27 +20,27 @@
|
||||
<div class="info">
|
||||
<div class="name-row">
|
||||
<span class="name">{{ client.name }}</span>
|
||||
<span class="tag" [ngClass]="getStageBadgeClass(client.ltcStatus.stage)">
|
||||
{{ getStageLabel(client.ltcStatus.stage) }}
|
||||
<span class="tag" *ngIf="client.ltcStatus?.stage" [ngClass]="getStageBadgeClass(client.ltcStatus?.stage)">
|
||||
{{ getStageLabel(client.ltcStatus?.stage) }}
|
||||
</span>
|
||||
</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 class="client-body">
|
||||
<div class="demand-row">
|
||||
<strong>核心诉求:</strong>{{ client.project }}
|
||||
<strong>核心诉求:</strong>{{ client.project || '待AI挖掘/人工补全' }}
|
||||
</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-text">{{ client.ltcStatus.coreBarrier }}</div>
|
||||
<div class="barrier-text">{{ client.ltcStatus?.coreBarrier }}</div>
|
||||
</div>
|
||||
|
||||
<div class="action-box">
|
||||
<div class="action-box" *ngIf="client.ltcStatus">
|
||||
<div class="action-title">💡 下一步建议</div>
|
||||
<div class="action-text">{{ client.ltcStatus.nextAction }}</div>
|
||||
<div class="action-text">{{ client.ltcStatus?.nextAction }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { MOCK_PATIENTS, Patient } from '../../data/mock-data';
|
||||
import { ApiService } from '../../core/api.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-client-ltc-dashboard',
|
||||
@ -11,7 +11,22 @@ import { MOCK_PATIENTS, Patient } from '../../data/mock-data';
|
||||
styleUrl: './client-ltc-dashboard.css',
|
||||
})
|
||||
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 {
|
||||
const map: Record<string, string> = {
|
||||
|
||||
@ -3,7 +3,6 @@ import { CommonModule } from '@angular/common';
|
||||
import { RouterLink, ActivatedRoute } from '@angular/router';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MOCK_REPORTS, Report } from '../../data/mock-data';
|
||||
import { MOCK_CLIENTS } from '../../core/client-mock';
|
||||
import { ApiService } from '../../core/api.service';
|
||||
import { AuthService } from '../../core/auth.service';
|
||||
import { ChatContextService } from '../../core/chat-context.service';
|
||||
@ -25,8 +24,8 @@ export class ReportDetail implements OnInit {
|
||||
archiveStatus: 'unarchived' | 'archived' = 'unarchived';
|
||||
showArchivePanel = false;
|
||||
searchQuery = '';
|
||||
clients = MOCK_CLIENTS;
|
||||
filteredClients = MOCK_CLIENTS;
|
||||
clients: any[] = [];
|
||||
filteredClients: any[] = [];
|
||||
isArchiving = false;
|
||||
isCreatingClient = false;
|
||||
|
||||
@ -40,6 +39,17 @@ export class ReportDetail implements OnInit {
|
||||
const realId = decodeURIComponent(id);
|
||||
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 缓存 */
|
||||
@ -86,7 +96,7 @@ export class ReportDetail implements OnInit {
|
||||
filterClients() {
|
||||
const q = this.searchQuery.toLowerCase();
|
||||
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 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);
|
||||
await this.archiveToClient(newClient);
|
||||
this.clients.unshift(res); // Add newly created to the top
|
||||
this.filteredClients = [...this.clients];
|
||||
await this.archiveToClient(res);
|
||||
} catch (e: any) {
|
||||
alert("建档失败:" + e.message);
|
||||
} finally {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user